diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 695f241..2b0497e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,7 +3,7 @@ import { AuthProvider, useAuth } from './contexts/AuthContext'; import { I18nContext } from './contexts/I18nContext'; import { dictionaries } from './i18n/dictionaries'; import { AppLayout, type View } from './components/Layout'; -import { getSetupStatus, LoginScreen, SetupWizard } from './pages/AuthScreens'; +import { AcceptInviteScreen, getSetupStatus, LoginScreen, SetupWizard } from './pages/AuthScreens'; import { HomePage } from './pages/HomePage'; import { InventoryPage } from './pages/InventoryPage'; import { ShoppingPage } from './pages/ShoppingPage'; @@ -20,7 +20,9 @@ function languageFromBrowser(): Language { function Shell() { const { user, loading } = useAuth(); const [view, setView] = useState('home'); + const isAcceptingInvite = window.location.pathname === '/accept-invite'; if (loading) return
NexaPantry
; + if (isAcceptingInvite) return ; if (!user) return ; if (!user.onboarding_completed) return ; return ( diff --git a/frontend/src/i18n/dictionaries.ts b/frontend/src/i18n/dictionaries.ts index bc444b3..9176f20 100644 --- a/frontend/src/i18n/dictionaries.ts +++ b/frontend/src/i18n/dictionaries.ts @@ -13,6 +13,12 @@ export const dictionaries: Record> = { instanceName: 'Instanzname', timezone: 'Zeitzone', completeSetup: 'Setup abschließen', + acceptInvite: 'Einladung annehmen', + acceptInviteSubtitle: 'Lege dein Passwort fest, um NexaPantry zu nutzen.', + inviteMissingToken: 'Der Einladungslink ist unvollständig.', + inviteError: 'Die Einladung ist ungültig oder abgelaufen.', + setPassword: 'Passwort festlegen', + saving: 'Speichern...', login: 'Anmelden', logout: 'Abmelden', home: 'Home', @@ -84,6 +90,12 @@ export const dictionaries: Record> = { instanceName: 'Instance name', timezone: 'Timezone', completeSetup: 'Complete setup', + acceptInvite: 'Accept invitation', + acceptInviteSubtitle: 'Set your password to start using NexaPantry.', + inviteMissingToken: 'The invitation link is incomplete.', + inviteError: 'The invitation is invalid or expired.', + setPassword: 'Set password', + saving: 'Saving...', login: 'Sign in', logout: 'Sign out', home: 'Home', @@ -144,4 +156,3 @@ export const dictionaries: Record> = { systemTheme: 'System' } }; - diff --git a/frontend/src/pages/AuthScreens.tsx b/frontend/src/pages/AuthScreens.tsx index da42ccf..63e8d60 100644 --- a/frontend/src/pages/AuthScreens.tsx +++ b/frontend/src/pages/AuthScreens.tsx @@ -61,6 +61,58 @@ export function LoginScreen() { ); } +export function AcceptInviteScreen() { + const { refresh } = useAuth(); + const { language, t } = useI18n(); + const token = new URLSearchParams(window.location.search).get('token') ?? ''; + const [form, setForm] = useState({ + name: '', + password: '', + language, + theme: 'system' as Theme + }); + const [error, setError] = useState(''); + const [submitting, setSubmitting] = useState(false); + const set = (key: string, value: string) => setForm((current) => ({ ...current, [key]: value })); + + return ( +
+ +
{ + event.preventDefault(); + setError(''); + setSubmitting(true); + try { + await api('/auth/accept-invite', { method: 'POST', body: JSON.stringify({ ...form, token }) }); + window.history.replaceState(null, '', '/'); + await refresh(); + } catch { + setError(t('inviteError')); + } finally { + setSubmitting(false); + } + }}> +
+

{t('acceptInvite')}

+

{t('acceptInviteSubtitle')}

+
+ {!token ?

{t('inviteMissingToken')}

: null} + {error ?

{error}

: null} + set('name', e.target.value)} /> + set('password', e.target.value)} /> + set('language', e.target.value)}> + + + set('theme', e.target.value)}> + + + + +
+
+ ); +} + export async function getSetupStatus(): Promise { return api('/setup/status'); }