From 17a3a89865a9e9e3afeb422f252e45f0345a5b15 Mon Sep 17 00:00:00 2001 From: lashman Date: Mon, 20 Apr 2026 18:07:56 +0300 Subject: [PATCH] onboarding modal --- frontend/src/components/OnboardingModal.tsx | 161 ++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 frontend/src/components/OnboardingModal.tsx diff --git a/frontend/src/components/OnboardingModal.tsx b/frontend/src/components/OnboardingModal.tsx new file mode 100644 index 0000000..3d26194 --- /dev/null +++ b/frontend/src/components/OnboardingModal.tsx @@ -0,0 +1,161 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { motion } from 'motion/react' +import { IconCookieFilled, IconKeyFilled, IconDeviceDesktopFilled, IconArrowRight, IconUpload, IconUserFilled } from '@tabler/icons-react' +import * as api from '../api' +import { useStore } from '../store' +import { useFocusTrap } from '../hooks/useFocusTrap' + +const LIFETIME_KEYS = [ + { value: 0, key: 'settings.session.thisSession' }, + { value: 86400, key: 'settings.session.oneDay' }, + { value: 604800, key: 'settings.session.oneWeek' }, + { value: 2592000, key: 'settings.session.thirtyDays' }, + { value: 7776000, key: 'settings.session.ninetyDays' }, + { value: 31536000, key: 'settings.session.oneYear' }, +] + +export default function OnboardingModal() { + const trapRef = useFocusTrap(true) + const { t } = useTranslation() + const setShowOnboarding = useStore(s => s.setShowOnboarding) + const [lifetime, setLifetime] = useState(2592000) + const [username, setUsername] = useState('') + const [registering, setRegistering] = useState(false) + const [passkeyDone, setPasskeyDone] = useState(false) + const [error, setError] = useState('') + const [importResult, setImportResult] = useState('') + const [importing, setImporting] = useState(false) + + const supportsPasskey = !!window.PublicKeyCredential + + const dismiss = async () => { + const settings = await api.fetchSettings() + await api.saveSettings({ ...settings, cookieLifetime: lifetime }) + if (username.trim()) { + await api.updateMe({ display_name: username.trim() }) + } + setShowOnboarding(false) + } + + const setupPasskey = async () => { + setRegistering(true) + setError('') + try { + await api.registerPasskey() + setPasskeyDone(true) + } catch (e) { + setError(e instanceof Error ? e.message : t('onboarding.passkeyFailed')) + } + setRegistering(false) + } + + return ( + + +

{t('onboarding.welcomeTo')}

+ +
+
+
+

{t('onboarding.chooseName')}

+

{t('onboarding.chooseNameDesc')}

+
+
+ setUsername(e.target.value)} + maxLength={30} + autoComplete="nickname" + /> + +
+ +
+
+
+

{t('onboarding.progressSaved')}

+

{t('onboarding.progressSavedDesc')}

+
+
+ +
+ +
+ {LIFETIME_KEYS.map(opt => ( + + ))} +
+
+ +
+ +
+
+
+

{t('onboarding.playAcrossDevices')}

+

{t('onboarding.playAcrossDevicesDesc')}

+
+
+ + {supportsPasskey && !passkeyDone && ( + + )} + {passkeyDone && {t('onboarding.passkeySaved')}} + + {error &&
{error}
} + +
+ +
+ + + +
+ {importResult &&

{importResult}

} + + + ) +}