@utopia-studio-design/design-system-cli 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/utopia-ds-mcp.mjs +15 -4
- package/bin/utopia-ds.mjs +16 -3
- package/data/blocks/semantic-pattern-parts/AuthParts.tsx +48 -0
- package/data/blocks/semantic-pattern-parts/ChoiceCards.tsx +7 -0
- package/data/blocks/semantic-pattern-parts/ListFilters.tsx +9 -0
- package/data/blocks/semantic-pattern-parts/OutcomeNotice.tsx +7 -0
- package/data/blocks/semantic-pattern-parts/PatternParts.tsx +7 -0
- package/data/blocks/semantic-pattern-parts/ProductParts.tsx +38 -0
- package/data/blocks/semantic-pattern-parts/ProgressSteps.tsx +6 -0
- package/data/blocks/semantic-pattern-parts/ReviewList.tsx +7 -0
- package/data/blocks/semantic-pattern-parts/ValidatedField.tsx +8 -0
- package/data/blocks/semantic-pattern-parts/WorkflowParts.tsx +339 -0
- package/data/blocks/semantic-pattern-parts/auth-parts.css +27 -0
- package/data/blocks/semantic-pattern-parts/pattern-parts.css +25 -0
- package/data/blocks/semantic-pattern-parts/product-parts.css +25 -0
- package/data/blocks/semantic-pattern-parts/types.ts +3 -0
- package/data/blocks/semantic-pattern-parts/workflow-parts.css +118 -0
- package/data/blocks/ux-pattern-examples/AccessExamples.tsx +46 -0
- package/data/blocks/ux-pattern-examples/AccountExamples.tsx +133 -0
- package/data/blocks/ux-pattern-examples/OnboardingExample.tsx +15 -0
- package/data/blocks/ux-pattern-examples/ProductExamples.tsx +79 -0
- package/data/blocks/ux-pattern-examples/ReviewExample.tsx +14 -0
- package/data/blocks/ux-pattern-examples/SignupExample.tsx +22 -0
- package/data/blocks/ux-pattern-examples/WorkflowExample.tsx +1761 -0
- package/data/blocks/ux-pattern-examples/WorkflowExamples.tsx +190 -0
- package/data/blocks/ux-pattern-examples/copy.ts +6 -0
- package/data/blocks/ux-pattern-examples/ux-examples.css +18 -0
- package/data/blocks/ux-pattern-examples/workflow-copy.ts +76 -0
- package/data/blocks/ux-pattern-examples/workflow-definitions.ts +1291 -0
- package/data/docs/ui-parts-qa.md +172 -0
- package/data/docs/ui-parts.md +105 -0
- package/data/docs/ux-pattern-adaptation-qa.md +38 -0
- package/data/docs/ux-pattern-adaptation.md +41 -0
- package/data/docs/ux-pattern-migration.md +111 -0
- package/data/docs/ux-pattern-publish-2026-09-13.md +22 -0
- package/data/docs/ux-pattern-release-2026-09-13.md +26 -0
- package/data/docs/ux-source-evidence/account-auth.json +41 -0
- package/data/docs/ux-source-evidence/account-forgot-password.json +36 -0
- package/data/docs/ux-source-evidence/account-invite.json +63 -0
- package/data/docs/ux-source-evidence/account-locked.json +63 -0
- package/data/docs/ux-source-evidence/account-login.json +28 -0
- package/data/docs/ux-source-evidence/account-mfa.json +62 -0
- package/data/docs/ux-source-evidence/account-onboarding-credentials.json +52 -0
- package/data/docs/ux-source-evidence/account-onboarding-legal.json +56 -0
- package/data/docs/ux-source-evidence/account-onboarding-orientation.json +66 -0
- package/data/docs/ux-source-evidence/account-onboarding-profile.json +50 -0
- package/data/docs/ux-source-evidence/account-onboarding.json +69 -0
- package/data/docs/ux-source-evidence/account-reset-password.json +27 -0
- package/data/docs/ux-source-evidence/account-session-expired.json +62 -0
- package/data/docs/ux-source-evidence/account-signup.json +33 -0
- package/data/docs/ux-source-evidence/account-verify-email.json +34 -0
- package/data/docs/ux-source-evidence/account-welcome.json +41 -0
- package/data/docs/ux-source-evidence/admin-analytics.json +54 -0
- package/data/docs/ux-source-evidence/admin-audit.json +31 -0
- package/data/docs/ux-source-evidence/admin-broadcasts-new.json +36 -0
- package/data/docs/ux-source-evidence/admin-broadcasts.json +41 -0
- package/data/docs/ux-source-evidence/admin-closures.json +52 -0
- package/data/docs/ux-source-evidence/admin-devices.json +51 -0
- package/data/docs/ux-source-evidence/admin-escalations.json +35 -0
- package/data/docs/ux-source-evidence/admin-incidents.json +54 -0
- package/data/docs/ux-source-evidence/admin-loading.json +17 -0
- package/data/docs/ux-source-evidence/admin-onboarding-detail.json +80 -0
- package/data/docs/ux-source-evidence/admin-onboarding.json +62 -0
- package/data/docs/ux-source-evidence/admin-payments.json +45 -0
- package/data/docs/ux-source-evidence/admin-providers.json +38 -0
- package/data/docs/ux-source-evidence/admin-settings.json +18 -0
- package/data/docs/ux-source-evidence/admin-subscriptions-detail-allocate.json +42 -0
- package/data/docs/ux-source-evidence/admin-subscriptions-detail.json +48 -0
- package/data/docs/ux-source-evidence/admin-subscriptions-new.json +43 -0
- package/data/docs/ux-source-evidence/admin-subscriptions.json +41 -0
- package/data/docs/ux-source-evidence/admin-transactions.json +41 -0
- package/data/docs/ux-source-evidence/error.json +30 -0
- package/data/docs/ux-source-evidence/loading.json +19 -0
- package/data/docs/ux-source-evidence/marketing-blog.json +48 -0
- package/data/docs/ux-source-evidence/marketing-contact.json +36 -0
- package/data/docs/ux-source-evidence/marketing.json +64 -0
- package/data/docs/ux-source-evidence/not-found.json +28 -0
- package/data/docs/ux-source-evidence/portal-loading.json +17 -0
- package/data/docs/ux-source-evidence/portal-mentor-messages.json +31 -0
- package/data/docs/ux-source-evidence/portal-mentor-profile.json +23 -0
- package/data/docs/ux-source-evidence/portal-mentor-requests-detail.json +54 -0
- package/data/docs/ux-source-evidence/portal-mentor-sessions-detail-assess.json +42 -0
- package/data/docs/ux-source-evidence/portal-mentor-settings.json +18 -0
- package/data/docs/ux-source-evidence/portal-mentor.json +38 -0
- package/data/docs/ux-source-evidence/portal-sessions-detail-live.json +48 -0
- package/data/docs/ux-source-evidence/portal-sponsor-billing.json +28 -0
- package/data/docs/ux-source-evidence/portal-sponsor-profile.json +22 -0
- package/data/docs/ux-source-evidence/portal-sponsor-sessions.json +28 -0
- package/data/docs/ux-source-evidence/portal-sponsor-settings.json +18 -0
- package/data/docs/ux-source-evidence/portal-sponsor-trainees.json +36 -0
- package/data/docs/ux-source-evidence/portal-sponsor.json +52 -0
- package/data/docs/ux-source-evidence/portal-trainee-competency.json +42 -0
- package/data/docs/ux-source-evidence/portal-trainee-credits.json +40 -0
- package/data/docs/ux-source-evidence/portal-trainee-mentors.json +48 -0
- package/data/docs/ux-source-evidence/portal-trainee-messages.json +32 -0
- package/data/docs/ux-source-evidence/portal-trainee-notifications.json +35 -0
- package/data/docs/ux-source-evidence/portal-trainee-profile.json +26 -0
- package/data/docs/ux-source-evidence/portal-trainee-request.json +54 -0
- package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +36 -0
- package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +42 -0
- package/data/docs/ux-source-evidence/portal-trainee-sessions-detail.json +55 -0
- package/data/docs/ux-source-evidence/portal-trainee-sessions.json +36 -0
- package/data/docs/ux-source-evidence/portal-trainee-settings-notifications.json +38 -0
- package/data/docs/ux-source-evidence/portal-trainee-settings.json +18 -0
- package/data/docs/ux-source-evidence/portal-trainee.json +44 -0
- package/data/docs/ux-source-evidence/portal-tv-detail.json +39 -0
- package/data/docs/ux-source-evidence/portal-tv.json +44 -0
- package/data/docs/ux-source-evidence/shared-admin-admin-shell.json +35 -0
- package/data/docs/ux-source-evidence/shared-auth-auth-primitives.json +105 -0
- package/data/docs/ux-source-evidence/shared-auth-auth-shell.json +25 -0
- package/data/docs/ux-source-evidence/shared-auth-onboarding-primitives.json +52 -0
- package/data/docs/ux-source-evidence/shared-auth-onboarding-shell.json +28 -0
- package/data/docs/ux-source-evidence/shared-auth-status-indicator.json +41 -0
- package/data/docs/ux-source-evidence/shared-portal-empty-state.json +24 -0
- package/data/docs/ux-source-evidence/shared-portal-loading-skeleton.json +27 -0
- package/data/docs/ux-source-evidence/shared-portal-notifications-dropdown.json +39 -0
- package/data/docs/ux-source-evidence/shared-portal-portal-shell.json +36 -0
- package/data/docs/ux-source-evidence/shared-portal-profile-form.json +32 -0
- package/data/docs/ux-source-evidence/shared-portal-settings-view.json +81 -0
- package/data/docs/ux-source-evidence/shared-portal-usage-sparkline.json +18 -0
- package/data/docs/ux-source-evidence/shared-portal-user-menu.json +34 -0
- package/data/manifests/blocks.json +230 -21
- package/data/manifests/patterns.json +330 -0
- package/data/manifests/ui-patterns.json +1029 -0
- package/data/manifests/ux-examples.json +2177 -0
- package/data/manifests/ux-patterns.json +1135 -0
- package/data/manifests/ux-source-inventory.json +4669 -0
- package/data/ui-parts/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
- package/data/ui-parts/blocks/analytics-dashboard/analytics-dashboard.css +92 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/AuthParts.tsx +48 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/ChoiceCards.tsx +7 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/ListFilters.tsx +9 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/OutcomeNotice.tsx +7 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/ProductParts.tsx +38 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/ProgressSteps.tsx +6 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/ReviewList.tsx +7 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/ValidatedField.tsx +8 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/WorkflowParts.tsx +339 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/auth-parts.css +27 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/pattern-parts.css +25 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/product-parts.css +25 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/types.ts +3 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/workflow-parts.css +118 -0
- package/data/ui-parts/blocks/ux-pattern-examples/AccessExamples.tsx +46 -0
- package/data/ui-parts/blocks/ux-pattern-examples/AccountExamples.tsx +133 -0
- package/data/ui-parts/blocks/ux-pattern-examples/ProductExamples.tsx +79 -0
- package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExample.tsx +1761 -0
- package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExamples.tsx +190 -0
- package/data/ui-parts/blocks/ux-pattern-examples/copy.ts +6 -0
- package/data/ui-parts/blocks/ux-pattern-examples/ux-examples.css +18 -0
- package/data/ui-parts/blocks/ux-pattern-examples/workflow-copy.ts +76 -0
- package/data/ui-parts/blocks/ux-pattern-examples/workflow-definitions.ts +1291 -0
- package/data/ux-source-evidence/account-auth.json +41 -0
- package/data/ux-source-evidence/account-forgot-password.json +36 -0
- package/data/ux-source-evidence/account-invite.json +63 -0
- package/data/ux-source-evidence/account-locked.json +63 -0
- package/data/ux-source-evidence/account-login.json +28 -0
- package/data/ux-source-evidence/account-mfa.json +62 -0
- package/data/ux-source-evidence/account-onboarding-credentials.json +52 -0
- package/data/ux-source-evidence/account-onboarding-legal.json +56 -0
- package/data/ux-source-evidence/account-onboarding-orientation.json +66 -0
- package/data/ux-source-evidence/account-onboarding-profile.json +50 -0
- package/data/ux-source-evidence/account-onboarding.json +69 -0
- package/data/ux-source-evidence/account-reset-password.json +27 -0
- package/data/ux-source-evidence/account-session-expired.json +62 -0
- package/data/ux-source-evidence/account-signup.json +33 -0
- package/data/ux-source-evidence/account-verify-email.json +34 -0
- package/data/ux-source-evidence/account-welcome.json +41 -0
- package/data/ux-source-evidence/admin-analytics.json +54 -0
- package/data/ux-source-evidence/admin-audit.json +31 -0
- package/data/ux-source-evidence/admin-broadcasts-new.json +36 -0
- package/data/ux-source-evidence/admin-broadcasts.json +41 -0
- package/data/ux-source-evidence/admin-closures.json +52 -0
- package/data/ux-source-evidence/admin-devices.json +51 -0
- package/data/ux-source-evidence/admin-escalations.json +35 -0
- package/data/ux-source-evidence/admin-incidents.json +54 -0
- package/data/ux-source-evidence/admin-loading.json +17 -0
- package/data/ux-source-evidence/admin-onboarding-detail.json +80 -0
- package/data/ux-source-evidence/admin-onboarding.json +62 -0
- package/data/ux-source-evidence/admin-payments.json +45 -0
- package/data/ux-source-evidence/admin-providers.json +38 -0
- package/data/ux-source-evidence/admin-settings.json +18 -0
- package/data/ux-source-evidence/admin-subscriptions-detail-allocate.json +42 -0
- package/data/ux-source-evidence/admin-subscriptions-detail.json +48 -0
- package/data/ux-source-evidence/admin-subscriptions-new.json +43 -0
- package/data/ux-source-evidence/admin-subscriptions.json +41 -0
- package/data/ux-source-evidence/admin-transactions.json +41 -0
- package/data/ux-source-evidence/error.json +30 -0
- package/data/ux-source-evidence/loading.json +19 -0
- package/data/ux-source-evidence/marketing-blog.json +48 -0
- package/data/ux-source-evidence/marketing-contact.json +36 -0
- package/data/ux-source-evidence/marketing.json +64 -0
- package/data/ux-source-evidence/not-found.json +28 -0
- package/data/ux-source-evidence/portal-loading.json +17 -0
- package/data/ux-source-evidence/portal-mentor-messages.json +31 -0
- package/data/ux-source-evidence/portal-mentor-profile.json +23 -0
- package/data/ux-source-evidence/portal-mentor-requests-detail.json +54 -0
- package/data/ux-source-evidence/portal-mentor-sessions-detail-assess.json +42 -0
- package/data/ux-source-evidence/portal-mentor-settings.json +18 -0
- package/data/ux-source-evidence/portal-mentor.json +38 -0
- package/data/ux-source-evidence/portal-sessions-detail-live.json +48 -0
- package/data/ux-source-evidence/portal-sponsor-billing.json +28 -0
- package/data/ux-source-evidence/portal-sponsor-profile.json +22 -0
- package/data/ux-source-evidence/portal-sponsor-sessions.json +28 -0
- package/data/ux-source-evidence/portal-sponsor-settings.json +18 -0
- package/data/ux-source-evidence/portal-sponsor-trainees.json +36 -0
- package/data/ux-source-evidence/portal-sponsor.json +52 -0
- package/data/ux-source-evidence/portal-trainee-competency.json +42 -0
- package/data/ux-source-evidence/portal-trainee-credits.json +40 -0
- package/data/ux-source-evidence/portal-trainee-mentors.json +48 -0
- package/data/ux-source-evidence/portal-trainee-messages.json +32 -0
- package/data/ux-source-evidence/portal-trainee-notifications.json +35 -0
- package/data/ux-source-evidence/portal-trainee-profile.json +26 -0
- package/data/ux-source-evidence/portal-trainee-request.json +54 -0
- package/data/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +36 -0
- package/data/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +42 -0
- package/data/ux-source-evidence/portal-trainee-sessions-detail.json +55 -0
- package/data/ux-source-evidence/portal-trainee-sessions.json +36 -0
- package/data/ux-source-evidence/portal-trainee-settings-notifications.json +38 -0
- package/data/ux-source-evidence/portal-trainee-settings.json +18 -0
- package/data/ux-source-evidence/portal-trainee.json +44 -0
- package/data/ux-source-evidence/portal-tv-detail.json +39 -0
- package/data/ux-source-evidence/portal-tv.json +44 -0
- package/data/ux-source-evidence/shared-admin-admin-shell.json +35 -0
- package/data/ux-source-evidence/shared-auth-auth-primitives.json +105 -0
- package/data/ux-source-evidence/shared-auth-auth-shell.json +25 -0
- package/data/ux-source-evidence/shared-auth-onboarding-primitives.json +52 -0
- package/data/ux-source-evidence/shared-auth-onboarding-shell.json +28 -0
- package/data/ux-source-evidence/shared-auth-status-indicator.json +41 -0
- package/data/ux-source-evidence/shared-portal-empty-state.json +24 -0
- package/data/ux-source-evidence/shared-portal-loading-skeleton.json +27 -0
- package/data/ux-source-evidence/shared-portal-notifications-dropdown.json +39 -0
- package/data/ux-source-evidence/shared-portal-portal-shell.json +36 -0
- package/data/ux-source-evidence/shared-portal-profile-form.json +32 -0
- package/data/ux-source-evidence/shared-portal-settings-view.json +81 -0
- package/data/ux-source-evidence/shared-portal-usage-sparkline.json +18 -0
- package/data/ux-source-evidence/shared-portal-user-menu.json +34 -0
- package/lib/api.mjs +193 -4
- package/package.json +2 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
+
"file": "app/(auth)/mfa/page.tsx",
|
|
4
|
+
"sha256": "9b8200151e4c65bf37b7de3a17f967595d1303a2b28d4e9afa8a6bbed6c95108",
|
|
5
|
+
"imports": [
|
|
6
|
+
"next/link",
|
|
7
|
+
"react",
|
|
8
|
+
"next/navigation",
|
|
9
|
+
"lucide-react",
|
|
10
|
+
"@/components/ui/button",
|
|
11
|
+
"@/components/ui/input",
|
|
12
|
+
"@/components/auth/auth-shell",
|
|
13
|
+
"@/components/auth/auth-primitives"
|
|
14
|
+
],
|
|
15
|
+
"tokens": [
|
|
16
|
+
"--color-bg-subtle",
|
|
17
|
+
"--color-border-default",
|
|
18
|
+
"--color-border-muted",
|
|
19
|
+
"--color-text-brand",
|
|
20
|
+
"--color-text-muted",
|
|
21
|
+
"--color-text-primary",
|
|
22
|
+
"--color-text-secondary"
|
|
23
|
+
],
|
|
24
|
+
"fragments": [
|
|
25
|
+
{
|
|
26
|
+
"id": "MfaPage",
|
|
27
|
+
"line": 14,
|
|
28
|
+
"code": "export default function MfaPage() {\n const search = useSearchParams();\n const forced = search.get(\"state\") as State | null;\n const modeParam = (search.get(\"mode\") as Mode | null) ?? \"verify\";\n\n if (modeParam === \"setup\") return <SetupView forced={forced} />;\n if (modeParam === \"backup\") return <BackupCodesView />;\n if (modeParam === \"recovery\") return <RecoveryView />;\n return <VerifyView forced={forced} />;\n}"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "VerifyView",
|
|
32
|
+
"line": 27,
|
|
33
|
+
"code": "function VerifyView({ forced }: { forced: State | null }) {\n const [code, setCode] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n const [errored, setErrored] = useState(false);\n const [attempts, setAttempts] = useState(0);\n const [trustDevice, setTrustDevice] = useState(false);\n\n const state: State = forced ?? (submitting ? \"loading\" : errored ? \"error\" : \"idle\");\n\n useEffect(() => {\n if (code.length === 6) {\n setSubmitting(true);\n setErrored(false);\n setTimeout(() => {\n setSubmitting(false);\n if (code === \"123456\") window.location.href = \"/welcome\";\n else { setErrored(true); setAttempts(a => a + 1); }\n }, 900);\n }\n }, [code]);\n\n return (\n <AuthShell\n kicker=\"Two-factor authentication\"\n title=\"Enter your verification code\"\n subtitle={<>Open your authenticator app and enter the 6-digit code for <strong style={{ color: \"var(--color-text-primary)\" }}>Mentix</strong>.</>}\n >\n <div className=\"space-y-5\">\n {state === \"error\" && (\n <InlineAlert variant=\"error\" title=\"Incorrect code\">\n {attempts >= 3 ? <>That's {attempts} incorrect attempts. <Link href=\"/locked\" className=\"font-medium hover:underline underline-offset-4\">Account will be locked.</Link></> : \"Double-check your authenticator app and try again.\"}\n </InlineAlert>\n )}\n\n <div className=\"flex justify-center py-2\">\n <OtpInput value={code} onChange={(v) => { setCode(v); if (errored) setErrored(false); }} error={state === \"error\"} disabled={state === \"loading\"} />\n </div>\n\n {state === \"loading\" && (\n <div className=\"flex items-center justify-center gap-2 text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Spinner className=\"w-4 h-4\" /> Verifying…\n </div>\n )}\n\n <div className=\"flex items-center justify-center\">\n <Checkbox id=\"trust-device\" checked={trustDevice} onChange={setTrustDevice}>\n Trust this device for 30 days\n </Checkbox>\n </div>\n\n <div className=\"text-center space-y-1.5 text-[12px] pt-2\" style={{ color: \"var(--color-text-muted)\" }}>\n <p>\n Lost access to your authenticator?{\" \"}\n <Link href=\"?mode=recovery\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>\n Use a backup code\n </Link>\n </p>\n <p>\n <Link href=\"/login\" className=\"hover:underline underline-offset-4\">← Sign in as a different user</Link>\n </p>\n </div>\n </div>\n </AuthShell>\n );\n}"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "SetupView",
|
|
37
|
+
"line": 95,
|
|
38
|
+
"code": "function SetupView({ forced }: { forced: State | null }) {\n const [step, setStep] = useState<1 | 2 | 3>(1);\n const [code, setCode] = useState(\"\");\n const [confirming, setConfirming] = useState(false);\n const [secretCopied, setSecretCopied] = useState(false);\n const SECRET = \"MNTX-K47L-9P3Q-2WR8\";\n\n const copySecret = () => {\n navigator.clipboard?.writeText(SECRET);\n setSecretCopied(true);\n setTimeout(() => setSecretCopied(false), 1500);\n };\n\n if (step === 3) {\n return (\n <AuthShell kicker=\"MFA enabled\" title=\"Two-factor authentication is on\" subtitle=\"Save these backup codes somewhere safe. Each can be used once if you lose access to your authenticator.\">\n <BackupCodesPanel />\n <div className=\"mt-6\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\" onClick={() => window.location.href = \"/welcome\"}>\n I've saved my codes <ArrowRight size={14} />\n </Button>\n </div>\n </AuthShell>\n );\n }\n\n return (\n <AuthShell\n kicker={`Step ${step} of 2 — Authenticator setup`}\n title={step === 1 ? \"Scan the QR code\" : \"Verify the code\"}\n subtitle={step === 1 ? \"Use Google Authenticator, 1Password, Authy, or any TOTP-compatible app.\" : \"Enter the 6-digit code from your authenticator app to confirm.\"}\n >\n {step === 1 ? (\n <div className=\"space-y-5\">\n {/* QR Code placeholder */}\n <div className=\"flex justify-center\">\n <div className=\"w-44 h-44 rounded-xl flex items-center justify-center\" style={{ background: \"var(--color-bg-subtle)\", border: \"1px solid var(--color-border-default)\" }}>\n <QrPlaceholder />\n </div>\n </div>\n\n {/* Can't scan? show secret */}\n <details className=\"rounded-lg border p-3\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <summary className=\"text-[13px] font-medium cursor-pointer select-none\">Can't scan? Enter the code manually</summary>\n <div className=\"mt-3 flex items-center gap-2\">\n <code className=\"flex-1 px-3 py-2 rounded-md text-[12px] font-mono\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-primary)\" }}>{SECRET}</code>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={copySecret}>\n {secretCopied ? <><CheckCircle2 size={12} /> Copied</> : <><Copy size={12} /> Copy</>}\n </Button>\n </div>\n </details>\n\n <div className=\"flex items-center gap-2\">\n <Link href=\"/login\"><Button variant=\"outline\" size=\"lg\"><ArrowLeft size={14} /> Cancel</Button></Link>\n <Button variant=\"brand\" size=\"lg\" className=\"flex-1\" onClick={() => setStep(2)}>Continue <ArrowRight size={14} /></Button>\n </div>\n </div>\n ) : (\n <div className=\"space-y-5\">\n <div className=\"flex justify-center\">\n <OtpInput value={code} onChange={setCode} />\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" size=\"lg\" onClick={() => setStep(1)}><ArrowLeft size={14} /> Back</Button>\n <Button variant=\"brand\" size=\"lg\" className=\"flex-1\" disabled={code.length !== 6 || confirming} onClick={() => { setConfirming(true); setTimeout(() => setStep(3), 900); }}>\n {confirming ? <><Spinner className=\"w-4 h-4 mr-1\" /> Verifying…</> : <>Enable MFA <ShieldCheck size={14} /></>}\n </Button>\n </div>\n </div>\n )}\n </AuthShell>\n );\n}"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "BackupCodesView",
|
|
42
|
+
"line": 171,
|
|
43
|
+
"code": "function BackupCodesView() {\n return (\n <AuthShell kicker=\"Backup codes\" title=\"Your backup codes\" subtitle=\"Each backup code can be used once to sign in if you lose your authenticator. Store them in a safe place — like a password manager.\">\n <BackupCodesPanel />\n <div className=\"mt-6 grid grid-cols-2 gap-2\">\n <Link href=\"/login\"><Button variant=\"outline\" size=\"lg\" className=\"w-full\">Back to sign in</Button></Link>\n <Link href=\"?mode=verify\"><Button variant=\"brand\" size=\"lg\" className=\"w-full\">I've saved them</Button></Link>\n </div>\n </AuthShell>\n );\n}"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "BackupCodesPanel",
|
|
47
|
+
"line": 183,
|
|
48
|
+
"code": "function BackupCodesPanel() {\n const codes = [\"4f7a-2c91\", \"9b3e-8d45\", \"1k7m-6q23\", \"x82p-9r4w\", \"7vn3-pq85\", \"3hj5-2sb9\", \"k4w7-mn22\", \"f9q8-3xy1\"];\n const [copied, setCopied] = useState(false);\n const copyAll = () => { navigator.clipboard?.writeText(codes.join(\"\\n\")); setCopied(true); setTimeout(() => setCopied(false), 1500); };\n return (\n <div className=\"rounded-xl border p-5\" style={{ borderColor: \"var(--color-border-default)\", background: \"var(--color-bg-subtle)\" }}>\n <div className=\"grid grid-cols-2 gap-x-6 gap-y-2 font-mono text-[13px] mb-4\">\n {codes.map((c, i) => (\n <div key={c} className=\"flex items-center gap-2\" style={{ color: \"var(--color-text-primary)\" }}>\n <span className=\"text-[11px] w-4\" style={{ color: \"var(--color-text-muted)\" }}>{i + 1}.</span>\n {c}\n </div>\n ))}\n </div>\n <div className=\"flex items-center gap-2 pt-4 border-t\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={copyAll}>\n {copied ? <><CheckCircle2 size={12} /> Copied</> : <><Copy size={12} /> Copy all</>}\n </Button>\n <Button type=\"button\" variant=\"outline\" size=\"sm\">\n <Download size={12} /> Download .txt\n </Button>\n </div>\n </div>\n );\n}"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "RecoveryView",
|
|
52
|
+
"line": 211,
|
|
53
|
+
"code": "function RecoveryView() {\n const [code, setCode] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n return (\n <AuthShell kicker=\"Account recovery\" title=\"Use a backup code\" subtitle=\"Enter one of the 8-character backup codes you saved when enabling MFA.\">\n <form className=\"space-y-4\" onSubmit={(e) => { e.preventDefault(); setSubmitting(true); setTimeout(() => window.location.href = \"/welcome\", 1000); }}>\n <Field label=\"Backup code\" htmlFor=\"bc\" hint=\"Format: xxxx-xxxx\">\n <Input id=\"bc\" value={code} onChange={(e) => setCode(e.target.value)} placeholder=\"e.g. 4f7a-2c91\" className=\"h-10 font-mono\" autoComplete=\"one-time-code\" required />\n </Field>\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" className=\"w-full\" disabled={code.length < 8 || submitting}>\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Verifying…</> : <>Sign in <ArrowRight size={14} /></>}\n </Button>\n <Link href=\"?mode=verify\" className=\"block text-center text-[12px] hover:underline underline-offset-4\" style={{ color: \"var(--color-text-muted)\" }}>\n ← Use authenticator code instead\n </Link>\n </form>\n </AuthShell>\n );\n}"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "QrPlaceholder",
|
|
57
|
+
"line": 233,
|
|
58
|
+
"code": "function QrPlaceholder() {\n // Pseudo-QR grid for visual realism\n return (\n <div className=\"grid grid-cols-12 gap-[2px] p-3\">\n {Array.from({ length: 144 }).map((_, i) => {\n const filled = (i * 13 + Math.floor(i / 12) * 7) % 5 < 3 || i < 12 || i > 131 || i % 12 < 1 || i % 12 > 10;\n return <div key={i} className=\"w-1.5 h-1.5 rounded-[1px]\" style={{ background: filled ? \"var(--color-text-primary)\" : \"transparent\" }} />;\n })}\n </div>\n );\n}"
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"content": "\"use client\";\nimport Link from \"next/link\";\nimport { useState, useMemo, useEffect } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { ArrowRight, ArrowLeft, Smartphone, Key, ShieldCheck, Copy, CheckCircle2, Download } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { AuthShell } from \"@/components/auth/auth-shell\";\nimport { Field, InlineAlert, Spinner, OtpInput, Checkbox } from \"@/components/auth/auth-primitives\";\n\ntype Mode = \"verify\" | \"setup\" | \"backup\" | \"recovery\";\ntype State = \"idle\" | \"loading\" | \"error\" | \"success\" | \"setup-success\";\n\nexport default function MfaPage() {\n const search = useSearchParams();\n const forced = search.get(\"state\") as State | null;\n const modeParam = (search.get(\"mode\") as Mode | null) ?? \"verify\";\n\n if (modeParam === \"setup\") return <SetupView forced={forced} />;\n if (modeParam === \"backup\") return <BackupCodesView />;\n if (modeParam === \"recovery\") return <RecoveryView />;\n return <VerifyView forced={forced} />;\n}\n\n/* ─── VERIFY (default) ──────────────────────────────────────────────────── */\n\nfunction VerifyView({ forced }: { forced: State | null }) {\n const [code, setCode] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n const [errored, setErrored] = useState(false);\n const [attempts, setAttempts] = useState(0);\n const [trustDevice, setTrustDevice] = useState(false);\n\n const state: State = forced ?? (submitting ? \"loading\" : errored ? \"error\" : \"idle\");\n\n useEffect(() => {\n if (code.length === 6) {\n setSubmitting(true);\n setErrored(false);\n setTimeout(() => {\n setSubmitting(false);\n if (code === \"123456\") window.location.href = \"/welcome\";\n else { setErrored(true); setAttempts(a => a + 1); }\n }, 900);\n }\n }, [code]);\n\n return (\n <AuthShell\n kicker=\"Two-factor authentication\"\n title=\"Enter your verification code\"\n subtitle={<>Open your authenticator app and enter the 6-digit code for <strong style={{ color: \"var(--color-text-primary)\" }}>Mentix</strong>.</>}\n >\n <div className=\"space-y-5\">\n {state === \"error\" && (\n <InlineAlert variant=\"error\" title=\"Incorrect code\">\n {attempts >= 3 ? <>That's {attempts} incorrect attempts. <Link href=\"/locked\" className=\"font-medium hover:underline underline-offset-4\">Account will be locked.</Link></> : \"Double-check your authenticator app and try again.\"}\n </InlineAlert>\n )}\n\n <div className=\"flex justify-center py-2\">\n <OtpInput value={code} onChange={(v) => { setCode(v); if (errored) setErrored(false); }} error={state === \"error\"} disabled={state === \"loading\"} />\n </div>\n\n {state === \"loading\" && (\n <div className=\"flex items-center justify-center gap-2 text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Spinner className=\"w-4 h-4\" /> Verifying…\n </div>\n )}\n\n <div className=\"flex items-center justify-center\">\n <Checkbox id=\"trust-device\" checked={trustDevice} onChange={setTrustDevice}>\n Trust this device for 30 days\n </Checkbox>\n </div>\n\n <div className=\"text-center space-y-1.5 text-[12px] pt-2\" style={{ color: \"var(--color-text-muted)\" }}>\n <p>\n Lost access to your authenticator?{\" \"}\n <Link href=\"?mode=recovery\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>\n Use a backup code\n </Link>\n </p>\n <p>\n <Link href=\"/login\" className=\"hover:underline underline-offset-4\">← Sign in as a different user</Link>\n </p>\n </div>\n </div>\n </AuthShell>\n );\n}\n\n/* ─── SETUP (Authenticator app) ─────────────────────────────────────────── */\n\nfunction SetupView({ forced }: { forced: State | null }) {\n const [step, setStep] = useState<1 | 2 | 3>(1);\n const [code, setCode] = useState(\"\");\n const [confirming, setConfirming] = useState(false);\n const [secretCopied, setSecretCopied] = useState(false);\n const SECRET = \"MNTX-K47L-9P3Q-2WR8\";\n\n const copySecret = () => {\n navigator.clipboard?.writeText(SECRET);\n setSecretCopied(true);\n setTimeout(() => setSecretCopied(false), 1500);\n };\n\n if (step === 3) {\n return (\n <AuthShell kicker=\"MFA enabled\" title=\"Two-factor authentication is on\" subtitle=\"Save these backup codes somewhere safe. Each can be used once if you lose access to your authenticator.\">\n <BackupCodesPanel />\n <div className=\"mt-6\">\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\" onClick={() => window.location.href = \"/welcome\"}>\n I've saved my codes <ArrowRight size={14} />\n </Button>\n </div>\n </AuthShell>\n );\n }\n\n return (\n <AuthShell\n kicker={`Step ${step} of 2 — Authenticator setup`}\n title={step === 1 ? \"Scan the QR code\" : \"Verify the code\"}\n subtitle={step === 1 ? \"Use Google Authenticator, 1Password, Authy, or any TOTP-compatible app.\" : \"Enter the 6-digit code from your authenticator app to confirm.\"}\n >\n {step === 1 ? (\n <div className=\"space-y-5\">\n {/* QR Code placeholder */}\n <div className=\"flex justify-center\">\n <div className=\"w-44 h-44 rounded-xl flex items-center justify-center\" style={{ background: \"var(--color-bg-subtle)\", border: \"1px solid var(--color-border-default)\" }}>\n <QrPlaceholder />\n </div>\n </div>\n\n {/* Can't scan? show secret */}\n <details className=\"rounded-lg border p-3\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <summary className=\"text-[13px] font-medium cursor-pointer select-none\">Can't scan? Enter the code manually</summary>\n <div className=\"mt-3 flex items-center gap-2\">\n <code className=\"flex-1 px-3 py-2 rounded-md text-[12px] font-mono\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-primary)\" }}>{SECRET}</code>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={copySecret}>\n {secretCopied ? <><CheckCircle2 size={12} /> Copied</> : <><Copy size={12} /> Copy</>}\n </Button>\n </div>\n </details>\n\n <div className=\"flex items-center gap-2\">\n <Link href=\"/login\"><Button variant=\"outline\" size=\"lg\"><ArrowLeft size={14} /> Cancel</Button></Link>\n <Button variant=\"brand\" size=\"lg\" className=\"flex-1\" onClick={() => setStep(2)}>Continue <ArrowRight size={14} /></Button>\n </div>\n </div>\n ) : (\n <div className=\"space-y-5\">\n <div className=\"flex justify-center\">\n <OtpInput value={code} onChange={setCode} />\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" size=\"lg\" onClick={() => setStep(1)}><ArrowLeft size={14} /> Back</Button>\n <Button variant=\"brand\" size=\"lg\" className=\"flex-1\" disabled={code.length !== 6 || confirming} onClick={() => { setConfirming(true); setTimeout(() => setStep(3), 900); }}>\n {confirming ? <><Spinner className=\"w-4 h-4 mr-1\" /> Verifying…</> : <>Enable MFA <ShieldCheck size={14} /></>}\n </Button>\n </div>\n </div>\n )}\n </AuthShell>\n );\n}\n\n/* ─── BACKUP CODES ──────────────────────────────────────────────────────── */\n\nfunction BackupCodesView() {\n return (\n <AuthShell kicker=\"Backup codes\" title=\"Your backup codes\" subtitle=\"Each backup code can be used once to sign in if you lose your authenticator. Store them in a safe place — like a password manager.\">\n <BackupCodesPanel />\n <div className=\"mt-6 grid grid-cols-2 gap-2\">\n <Link href=\"/login\"><Button variant=\"outline\" size=\"lg\" className=\"w-full\">Back to sign in</Button></Link>\n <Link href=\"?mode=verify\"><Button variant=\"brand\" size=\"lg\" className=\"w-full\">I've saved them</Button></Link>\n </div>\n </AuthShell>\n );\n}\n\nfunction BackupCodesPanel() {\n const codes = [\"4f7a-2c91\", \"9b3e-8d45\", \"1k7m-6q23\", \"x82p-9r4w\", \"7vn3-pq85\", \"3hj5-2sb9\", \"k4w7-mn22\", \"f9q8-3xy1\"];\n const [copied, setCopied] = useState(false);\n const copyAll = () => { navigator.clipboard?.writeText(codes.join(\"\\n\")); setCopied(true); setTimeout(() => setCopied(false), 1500); };\n return (\n <div className=\"rounded-xl border p-5\" style={{ borderColor: \"var(--color-border-default)\", background: \"var(--color-bg-subtle)\" }}>\n <div className=\"grid grid-cols-2 gap-x-6 gap-y-2 font-mono text-[13px] mb-4\">\n {codes.map((c, i) => (\n <div key={c} className=\"flex items-center gap-2\" style={{ color: \"var(--color-text-primary)\" }}>\n <span className=\"text-[11px] w-4\" style={{ color: \"var(--color-text-muted)\" }}>{i + 1}.</span>\n {c}\n </div>\n ))}\n </div>\n <div className=\"flex items-center gap-2 pt-4 border-t\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={copyAll}>\n {copied ? <><CheckCircle2 size={12} /> Copied</> : <><Copy size={12} /> Copy all</>}\n </Button>\n <Button type=\"button\" variant=\"outline\" size=\"sm\">\n <Download size={12} /> Download .txt\n </Button>\n </div>\n </div>\n );\n}\n\n/* ─── RECOVERY (Use backup code) ────────────────────────────────────────── */\n\nfunction RecoveryView() {\n const [code, setCode] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n return (\n <AuthShell kicker=\"Account recovery\" title=\"Use a backup code\" subtitle=\"Enter one of the 8-character backup codes you saved when enabling MFA.\">\n <form className=\"space-y-4\" onSubmit={(e) => { e.preventDefault(); setSubmitting(true); setTimeout(() => window.location.href = \"/welcome\", 1000); }}>\n <Field label=\"Backup code\" htmlFor=\"bc\" hint=\"Format: xxxx-xxxx\">\n <Input id=\"bc\" value={code} onChange={(e) => setCode(e.target.value)} placeholder=\"e.g. 4f7a-2c91\" className=\"h-10 font-mono\" autoComplete=\"one-time-code\" required />\n </Field>\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" className=\"w-full\" disabled={code.length < 8 || submitting}>\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Verifying…</> : <>Sign in <ArrowRight size={14} /></>}\n </Button>\n <Link href=\"?mode=verify\" className=\"block text-center text-[12px] hover:underline underline-offset-4\" style={{ color: \"var(--color-text-muted)\" }}>\n ← Use authenticator code instead\n </Link>\n </form>\n </AuthShell>\n );\n}\n\n/* ─── QR placeholder (visual only) ──────────────────────────────────────── */\n\nfunction QrPlaceholder() {\n // Pseudo-QR grid for visual realism\n return (\n <div className=\"grid grid-cols-12 gap-[2px] p-3\">\n {Array.from({ length: 144 }).map((_, i) => {\n const filled = (i * 13 + Math.floor(i / 12) * 7) % 5 < 3 || i < 12 || i > 131 || i % 12 < 1 || i % 12 > 10;\n return <div key={i} className=\"w-1.5 h-1.5 rounded-[1px]\" style={{ background: filled ? \"var(--color-text-primary)\" : \"transparent\" }} />;\n })}\n </div>\n );\n}\n"
|
|
62
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
+
"file": "app/(auth)/onboarding/credentials/page.tsx",
|
|
4
|
+
"sha256": "9a1231a58e1a425ea7c214ceb63375b95a1a2ba5599a8d17c254bc3d4d40651b",
|
|
5
|
+
"imports": [
|
|
6
|
+
"react",
|
|
7
|
+
"next/navigation",
|
|
8
|
+
"@/components/ui/input",
|
|
9
|
+
"@/components/ui/textarea",
|
|
10
|
+
"@/components/auth/onboarding-shell",
|
|
11
|
+
"@/components/auth/auth-primitives",
|
|
12
|
+
"@/components/auth/onboarding-primitives",
|
|
13
|
+
"@/lib/verification"
|
|
14
|
+
],
|
|
15
|
+
"tokens": [
|
|
16
|
+
"--color-bg-brand-muted",
|
|
17
|
+
"--color-border-brand",
|
|
18
|
+
"--color-border-default",
|
|
19
|
+
"--color-text-brand",
|
|
20
|
+
"--color-text-muted",
|
|
21
|
+
"--color-text-primary",
|
|
22
|
+
"--color-text-secondary"
|
|
23
|
+
],
|
|
24
|
+
"fragments": [
|
|
25
|
+
{
|
|
26
|
+
"id": "CredentialsPage",
|
|
27
|
+
"line": 44,
|
|
28
|
+
"code": "export default function CredentialsPage() {\n const router = useRouter();\n const role = (useSearchParams().get(\"role\") as Role | null) ?? \"trainee\";\n\n if (role === \"sponsor\") return <SponsorQuestionnaire router={router} />;\n if (role === \"mentor\") return <MentorCredentials router={router} />;\n return <TraineeCredentials router={router} />;\n}"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "TraineeCredentials",
|
|
32
|
+
"line": 55,
|
|
33
|
+
"code": "function TraineeCredentials({ router }: { router: ReturnType<typeof useRouter> }) {\n const [gmc, setGmc] = useState(\"\");\n const [hospital, setHospital] = useState(\"\");\n const [country, setCountry] = useState(\"\");\n const [specialty, setSpecialty] = useState(\"\");\n const [trainingProgramme, setTrainingProgramme] = useState(\"\");\n const [supervisingInstitution, setSupervisingInstitution] = useState(\"\");\n const [supervisorName, setSupervisorName] = useState(\"\");\n const [trainingGrade, setTrainingGrade] = useState(\"\");\n const [interests, setInterests] = useState<string[]>([]);\n const [experiencePerProcedure, setExperiencePerProcedure] = useState<Record<string, ExperienceLevel>>({});\n const [saving, setSaving] = useState(false);\n\n const grades = [\"ST1–ST3\", \"ST4–ST6\", \"Fellow\", \"Attending\"];\n\n const allProceduresLevelled = interests.every((p) => !!experiencePerProcedure[p]);\n const canContinue = !!(\n gmc.length >= 4 && hospital.length >= 2 && country && specialty &&\n trainingProgramme && supervisingInstitution && supervisorName && trainingGrade &&\n interests.length >= 1 && allProceduresLevelled\n );\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/legal?role=trainee\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"trainee\" stepKey=\"credentials\"\n title=\"Credentials\"\n subtitle=\"So we can verify you and match the right mentors.\"\n previousHref=\"/onboarding/profile?role=trainee\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n saveLabel=\"Submit for verification\"\n >\n <InlineAlert variant=\"info\">\n Verification typically takes < 48 hours. You'll get an email once approved.\n </InlineAlert>\n\n <SectionHeader title=\"Identity & practice\" />\n <Field label=\"Medical registration number\" htmlFor=\"gmc\" hint=\"GMC, NMC, or equivalent national body.\">\n <Input id=\"gmc\" value={gmc} onChange={(e) => setGmc(e.target.value)} className=\"h-10\" placeholder=\"e.g. 7654321\" />\n </Field>\n <Field label=\"Hospital / Institution\" htmlFor=\"hosp\">\n <Input id=\"hosp\" value={hospital} onChange={(e) => setHospital(e.target.value)} className=\"h-10\" placeholder=\"e.g. King's College Hospital\" />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Country of practice\" htmlFor=\"ctry\"><Select id=\"ctry\" value={country} onChange={setCountry} options={COUNTRIES} placeholder=\"Select country\" /></Field>\n <Field label=\"Specialty\" htmlFor=\"sp\"><Select id=\"sp\" value={specialty} onChange={setSpecialty} options={SPECIALTIES} placeholder=\"Select specialty\" /></Field>\n </div>\n\n <SectionHeader title=\"Training programme\" />\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Training programme\" htmlFor=\"tp\">\n <Input id=\"tp\" value={trainingProgramme} onChange={(e) => setTrainingProgramme(e.target.value)} className=\"h-10\" placeholder=\"e.g. JCST Higher Surgical Training\" />\n </Field>\n <Field label=\"Supervising institution\" htmlFor=\"si\">\n <Input id=\"si\" value={supervisingInstitution} onChange={(e) => setSupervisingInstitution(e.target.value)} className=\"h-10\" placeholder=\"e.g. KSS Deanery\" />\n </Field>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Clinical supervisor / TPD\" htmlFor=\"sup\">\n <Input id=\"sup\" value={supervisorName} onChange={(e) => setSupervisorName(e.target.value)} className=\"h-10\" placeholder=\"Prof. Jane Smith\" />\n </Field>\n <Field label=\"Training grade\" htmlFor=\"grade\">\n <div className=\"grid grid-cols-4 gap-1\">\n {grades.map((g) => (\n <button\n key={g}\n type=\"button\"\n onClick={() => setTrainingGrade(g)}\n className=\"h-10 rounded-lg border text-[11px] font-medium transition-all\"\n style={{\n borderColor: trainingGrade === g ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: trainingGrade === g ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n color: trainingGrade === g ? \"var(--color-text-brand)\" : \"var(--color-text-primary)\",\n }}\n >{g}</button>\n ))}\n </div>\n </Field>\n </div>\n\n <SectionHeader title=\"Mentorship needs\" />\n <Field label=\"Procedures requiring mentorship\" htmlFor=\"proc\" hint=\"We'll match you with mentors who cover these.\">\n <MultiSelect value={interests} onChange={setInterests} options={PROCEDURES} />\n </Field>\n {interests.length > 0 && (\n <Field label=\"Experience level per procedure\" htmlFor=\"explvl\" hint=\"Self-reported. Helps mentors pitch their guidance.\">\n <div className=\"space-y-2\">\n {interests.map((proc) => (\n <div key={proc} className=\"rounded-lg border p-3\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"text-[12px] font-medium mb-2\" style={{ color: \"var(--color-text-primary)\" }}>{proc}</div>\n <div className=\"grid grid-cols-4 gap-1\">\n {EXPERIENCE_LEVELS.map((lvl) => {\n const active = experiencePerProcedure[proc] === lvl;\n return (\n <button\n key={lvl}\n type=\"button\"\n onClick={() => setExperiencePerProcedure({ ...experiencePerProcedure, [proc]: lvl })}\n className=\"h-8 rounded-md border text-[11px] font-medium transition-all\"\n style={{\n borderColor: active ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: active ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n color: active ? \"var(--color-text-brand)\" : \"var(--color-text-secondary)\",\n }}\n >{lvl}</button>\n );\n })}\n </div>\n </div>\n ))}\n </div>\n </Field>\n )}\n </OnboardingShell>\n );\n}"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "MentorCredentials",
|
|
37
|
+
"line": 178,
|
|
38
|
+
"code": "function MentorCredentials({ router }: { router: ReturnType<typeof useRouter> }) {\n const [title, setTitle] = useState(\"\");\n const [institution, setInstitution] = useState(\"\");\n const [gmc, setGmc] = useState(\"\");\n const [country, setCountry] = useState(\"\");\n const [specialty, setSpecialty] = useState(\"\");\n const [expertise, setExpertise] = useState<string[]>([]);\n // Volumes per procedure\n const [volumes, setVolumes] = useState<Record<string, string>>({});\n // Indemnity\n const [indemnityProvider, setIndemnityProvider] = useState(\"\");\n const [indemnityPolicy, setIndemnityPolicy] = useState(\"\");\n const [indemnityExpiry, setIndemnityExpiry] = useState(\"\");\n // Uploads\n const [affiliationLetter, setAffiliationLetter] = useState<{ name: string; size: number } | null>(null);\n const [cv, setCv] = useState<{ name: string; size: number } | null>(null);\n const [saving, setSaving] = useState(false);\n\n const canContinue = !!(\n title && institution && gmc.length >= 4 && country && specialty && expertise.length >= 1 &&\n indemnityProvider && indemnityPolicy && indemnityExpiry && affiliationLetter\n );\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/legal?role=mentor\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"mentor\" stepKey=\"credentials\"\n title=\"Professional credentials\"\n subtitle=\"Required for verification. We'll send MCA & NDA after admin review.\"\n previousHref=\"/onboarding/profile?role=mentor\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n saveLabel=\"Submit for verification\"\n >\n <InlineAlert variant=\"info\">\n Verification typically takes < 7 days. Once approved, we'll email the MCA & NDA to sign in Step 3.\n </InlineAlert>\n\n {/* ── Professional details ── */}\n <SectionHeader title=\"Professional details\" />\n <Field label=\"Professional title\" htmlFor=\"t\" hint=\"e.g. Consultant Interventional Endoscopist\">\n <Input id=\"t\" value={title} onChange={(e) => setTitle(e.target.value)} className=\"h-10\" />\n </Field>\n <Field label=\"Institution / Affiliation\" htmlFor=\"i\">\n <Input id=\"i\" value={institution} onChange={(e) => setInstitution(e.target.value)} className=\"h-10\" placeholder=\"King's College Hospital, London\" />\n </Field>\n <Field label=\"Medical registration number\" htmlFor=\"gmc\" hint=\"GMC, NMC, or equivalent national body.\">\n <Input id=\"gmc\" value={gmc} onChange={(e) => setGmc(e.target.value)} className=\"h-10\" placeholder=\"e.g. 7654321\" />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Country of practice\" htmlFor=\"ctry\"><Select id=\"ctry\" value={country} onChange={setCountry} options={COUNTRIES} placeholder=\"Select country\" /></Field>\n <Field label=\"Specialty / Subspecialty\" htmlFor=\"sp\"><Select id=\"sp\" value={specialty} onChange={setSpecialty} options={SPECIALTIES} placeholder=\"Select\" /></Field>\n </div>\n\n {/* ── Procedures & volume ── */}\n <SectionHeader title=\"Procedures & volume\" />\n <Field label=\"Expertise & procedures\" htmlFor=\"exp\" hint=\"Procedures you're qualified to mentor.\">\n <MultiSelect value={expertise} onChange={setExpertise} options={PROCEDURES} />\n </Field>\n {expertise.length > 0 && (\n <Field label=\"Approximate volume per year\" htmlFor=\"vol\" hint=\"Roughly how many of each you perform annually.\">\n <div className=\"space-y-2\">\n {expertise.map((proc) => (\n <div key={proc} className=\"flex items-center gap-3\">\n <span className=\"text-[12px] flex-1 min-w-0 truncate\" style={{ color: \"var(--color-text-secondary)\" }}>{proc}</span>\n <Input\n type=\"number\"\n className=\"h-9 w-24\"\n value={volumes[proc] ?? \"\"}\n onChange={(e) => setVolumes({ ...volumes, [proc]: e.target.value })}\n placeholder=\"0\"\n />\n <span className=\"text-[11px] w-12\" style={{ color: \"var(--color-text-muted)\" }}>/ year</span>\n </div>\n ))}\n </div>\n </Field>\n )}\n\n {/* ── Indemnity ── */}\n <SectionHeader title=\"Professional indemnity insurance\" />\n <Field label=\"Indemnity provider / defence organisation\" htmlFor=\"ip\">\n <Input id=\"ip\" value={indemnityProvider} onChange={(e) => setIndemnityProvider(e.target.value)} className=\"h-10\" placeholder=\"e.g. Medical Defence Union (MDU)\" />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Policy / Membership number\" htmlFor=\"ipnum\">\n <Input id=\"ipnum\" value={indemnityPolicy} onChange={(e) => setIndemnityPolicy(e.target.value)} className=\"h-10\" placeholder=\"e.g. MDU-1234567\" />\n </Field>\n <Field label=\"Policy expiry date\" htmlFor=\"ipexp\" hint=\"We'll remind you 60 days before.\">\n <Input id=\"ipexp\" type=\"date\" value={indemnityExpiry} onChange={(e) => setIndemnityExpiry(e.target.value)} className=\"h-10\" />\n </Field>\n </div>\n\n {/* ── Supporting documents ── */}\n <SectionHeader title=\"Supporting documents\" />\n <FileDropZone\n label=\"Institutional affiliation letter\"\n accept=\"application/pdf,image/*\"\n file={affiliationLetter}\n onFile={setAffiliationLetter}\n hint=\"PDF or image · A letter from your employer confirming your role.\"\n />\n <FileDropZone\n label=\"CV (optional)\"\n accept=\"application/pdf\"\n file={cv}\n onFile={setCv}\n hint=\"PDF · helpful for admin review but not required.\"\n />\n </OnboardingShell>\n );\n}"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "SectionHeader",
|
|
42
|
+
"line": 296,
|
|
43
|
+
"code": "function SectionHeader({ title }: { title: string }) {\n return (\n <p className=\"text-[10px] font-semibold uppercase tracking-[0.12em] pt-3 -mb-1\" style={{ color: \"var(--color-text-muted)\" }}>\n {title}\n </p>\n );\n}"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "SponsorQuestionnaire",
|
|
47
|
+
"line": 306,
|
|
48
|
+
"code": "function SponsorQuestionnaire({ router }: { router: ReturnType<typeof useRouter> }) {\n const [industry, setIndustry] = useState(\"\");\n const [objectives, setObjectives] = useState(\"\");\n const [traineeCount, setTraineeCount] = useState(\"\");\n const [specialties, setSpecialties] = useState<string[]>([]);\n const [geographies, setGeographies] = useState<string[]>([]);\n const [saving, setSaving] = useState(false);\n\n const canContinue = !!(industry && objectives.length >= 20 && traineeCount && specialties.length >= 1);\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/legal?role=sponsor\"), 600);\n };\n\n const INDUSTRIES = [\n { value: \"medtech\", label: \"Medical Device / MedTech\" },\n { value: \"pharma\", label: \"Pharmaceutical\" },\n { value: \"hospital\", label: \"Hospital / Trust\" },\n { value: \"society\", label: \"Society / Association\" },\n { value: \"education\", label: \"Academic / Education\" },\n ];\n\n return (\n <OnboardingShell\n role=\"sponsor\" stepKey=\"credentials\"\n title=\"Programme details\"\n subtitle=\"Tell us what you'd like to achieve so we can scope a programme.\"\n previousHref=\"/onboarding/profile?role=sponsor\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n >\n <Field label=\"Industry / Sector\" htmlFor=\"ind\"><Select id=\"ind\" value={industry} onChange={setIndustry} options={INDUSTRIES} placeholder=\"Select industry\" /></Field>\n <Field label=\"Training objectives / Goals\" htmlFor=\"obj\" hint=\"What outcomes do you want to drive?\">\n <Textarea id=\"obj\" value={objectives} onChange={(e) => setObjectives(e.target.value)} rows={4} placeholder=\"e.g. Accelerate ESD adoption across 30 trainees in the UK & Germany; build a measurable competency record.\" />\n </Field>\n <Field label=\"Number of trainees to sponsor\" htmlFor=\"tc\">\n <Input id=\"tc\" type=\"number\" value={traineeCount} onChange={(e) => setTraineeCount(e.target.value)} className=\"h-10\" placeholder=\"e.g. 25\" />\n </Field>\n <Field label=\"Preferred specialties / procedures\" htmlFor=\"sp\">\n <MultiSelect value={specialties} onChange={setSpecialties} options={PROCEDURES} />\n </Field>\n <Field label=\"Target geographies\" htmlFor=\"geo\" optional>\n <MultiSelect value={geographies} onChange={setGeographies} options={[\"UK\", \"EU\", \"US\", \"APAC\", \"ME\", \"Latin America\"]} />\n </Field>\n </OnboardingShell>\n );\n}"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useSearchParams, useRouter } from \"next/navigation\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { OnboardingShell, Role } from \"@/components/auth/onboarding-shell\";\nimport { Field, InlineAlert } from \"@/components/auth/auth-primitives\";\nimport { Select, MultiSelect, FileDropZone } from \"@/components/auth/onboarding-primitives\";\nimport { EXPERIENCE_LEVELS, type ExperienceLevel } from \"@/lib/verification\";\n\nconst COUNTRIES = [\n { value: \"GB\", label: \"United Kingdom\" },\n { value: \"DE\", label: \"Germany\" },\n { value: \"FR\", label: \"France\" },\n { value: \"ES\", label: \"Spain\" },\n { value: \"IT\", label: \"Italy\" },\n { value: \"NL\", label: \"Netherlands\" },\n { value: \"US\", label: \"United States\" },\n { value: \"KR\", label: \"South Korea\" },\n { value: \"JP\", label: \"Japan\" },\n { value: \"SG\", label: \"Singapore\" },\n { value: \"AU\", label: \"Australia\" },\n];\nconst SPECIALTIES = [\n { value: \"gi\", label: \"Gastroenterology\" },\n { value: \"colorectal\", label: \"Colorectal Surgery\" },\n { value: \"upper-gi\", label: \"Upper GI Surgery\" },\n { value: \"hepatology\", label: \"Hepatology\" },\n { value: \"general\", label: \"General Surgery\" },\n { value: \"hepatobiliary\", label: \"Hepatobiliary Surgery\" },\n];\nconst PROCEDURES = [\n \"Endoscopic Submucosal Dissection (ESD)\",\n \"Endoscopic Mucosal Resection (EMR)\",\n \"POEM\",\n \"Colonoscopy & polypectomy\",\n \"ERCP\",\n \"Capsule endoscopy\",\n \"Bariatric endoscopy\",\n \"Transanal Endoscopic Microsurgery (TEM)\",\n];\n\nexport default function CredentialsPage() {\n const router = useRouter();\n const role = (useSearchParams().get(\"role\") as Role | null) ?? \"trainee\";\n\n if (role === \"sponsor\") return <SponsorQuestionnaire router={router} />;\n if (role === \"mentor\") return <MentorCredentials router={router} />;\n return <TraineeCredentials router={router} />;\n}\n\n/* ─── TRAINEE Credentials ───────────────────────────────────────────────── */\n\nfunction TraineeCredentials({ router }: { router: ReturnType<typeof useRouter> }) {\n const [gmc, setGmc] = useState(\"\");\n const [hospital, setHospital] = useState(\"\");\n const [country, setCountry] = useState(\"\");\n const [specialty, setSpecialty] = useState(\"\");\n const [trainingProgramme, setTrainingProgramme] = useState(\"\");\n const [supervisingInstitution, setSupervisingInstitution] = useState(\"\");\n const [supervisorName, setSupervisorName] = useState(\"\");\n const [trainingGrade, setTrainingGrade] = useState(\"\");\n const [interests, setInterests] = useState<string[]>([]);\n const [experiencePerProcedure, setExperiencePerProcedure] = useState<Record<string, ExperienceLevel>>({});\n const [saving, setSaving] = useState(false);\n\n const grades = [\"ST1–ST3\", \"ST4–ST6\", \"Fellow\", \"Attending\"];\n\n const allProceduresLevelled = interests.every((p) => !!experiencePerProcedure[p]);\n const canContinue = !!(\n gmc.length >= 4 && hospital.length >= 2 && country && specialty &&\n trainingProgramme && supervisingInstitution && supervisorName && trainingGrade &&\n interests.length >= 1 && allProceduresLevelled\n );\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/legal?role=trainee\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"trainee\" stepKey=\"credentials\"\n title=\"Credentials\"\n subtitle=\"So we can verify you and match the right mentors.\"\n previousHref=\"/onboarding/profile?role=trainee\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n saveLabel=\"Submit for verification\"\n >\n <InlineAlert variant=\"info\">\n Verification typically takes < 48 hours. You'll get an email once approved.\n </InlineAlert>\n\n <SectionHeader title=\"Identity & practice\" />\n <Field label=\"Medical registration number\" htmlFor=\"gmc\" hint=\"GMC, NMC, or equivalent national body.\">\n <Input id=\"gmc\" value={gmc} onChange={(e) => setGmc(e.target.value)} className=\"h-10\" placeholder=\"e.g. 7654321\" />\n </Field>\n <Field label=\"Hospital / Institution\" htmlFor=\"hosp\">\n <Input id=\"hosp\" value={hospital} onChange={(e) => setHospital(e.target.value)} className=\"h-10\" placeholder=\"e.g. King's College Hospital\" />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Country of practice\" htmlFor=\"ctry\"><Select id=\"ctry\" value={country} onChange={setCountry} options={COUNTRIES} placeholder=\"Select country\" /></Field>\n <Field label=\"Specialty\" htmlFor=\"sp\"><Select id=\"sp\" value={specialty} onChange={setSpecialty} options={SPECIALTIES} placeholder=\"Select specialty\" /></Field>\n </div>\n\n <SectionHeader title=\"Training programme\" />\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Training programme\" htmlFor=\"tp\">\n <Input id=\"tp\" value={trainingProgramme} onChange={(e) => setTrainingProgramme(e.target.value)} className=\"h-10\" placeholder=\"e.g. JCST Higher Surgical Training\" />\n </Field>\n <Field label=\"Supervising institution\" htmlFor=\"si\">\n <Input id=\"si\" value={supervisingInstitution} onChange={(e) => setSupervisingInstitution(e.target.value)} className=\"h-10\" placeholder=\"e.g. KSS Deanery\" />\n </Field>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Clinical supervisor / TPD\" htmlFor=\"sup\">\n <Input id=\"sup\" value={supervisorName} onChange={(e) => setSupervisorName(e.target.value)} className=\"h-10\" placeholder=\"Prof. Jane Smith\" />\n </Field>\n <Field label=\"Training grade\" htmlFor=\"grade\">\n <div className=\"grid grid-cols-4 gap-1\">\n {grades.map((g) => (\n <button\n key={g}\n type=\"button\"\n onClick={() => setTrainingGrade(g)}\n className=\"h-10 rounded-lg border text-[11px] font-medium transition-all\"\n style={{\n borderColor: trainingGrade === g ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: trainingGrade === g ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n color: trainingGrade === g ? \"var(--color-text-brand)\" : \"var(--color-text-primary)\",\n }}\n >{g}</button>\n ))}\n </div>\n </Field>\n </div>\n\n <SectionHeader title=\"Mentorship needs\" />\n <Field label=\"Procedures requiring mentorship\" htmlFor=\"proc\" hint=\"We'll match you with mentors who cover these.\">\n <MultiSelect value={interests} onChange={setInterests} options={PROCEDURES} />\n </Field>\n {interests.length > 0 && (\n <Field label=\"Experience level per procedure\" htmlFor=\"explvl\" hint=\"Self-reported. Helps mentors pitch their guidance.\">\n <div className=\"space-y-2\">\n {interests.map((proc) => (\n <div key={proc} className=\"rounded-lg border p-3\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"text-[12px] font-medium mb-2\" style={{ color: \"var(--color-text-primary)\" }}>{proc}</div>\n <div className=\"grid grid-cols-4 gap-1\">\n {EXPERIENCE_LEVELS.map((lvl) => {\n const active = experiencePerProcedure[proc] === lvl;\n return (\n <button\n key={lvl}\n type=\"button\"\n onClick={() => setExperiencePerProcedure({ ...experiencePerProcedure, [proc]: lvl })}\n className=\"h-8 rounded-md border text-[11px] font-medium transition-all\"\n style={{\n borderColor: active ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: active ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n color: active ? \"var(--color-text-brand)\" : \"var(--color-text-secondary)\",\n }}\n >{lvl}</button>\n );\n })}\n </div>\n </div>\n ))}\n </div>\n </Field>\n )}\n </OnboardingShell>\n );\n}\n\n/* ─── MENTOR Credentials ───────────────────────────────────────────────── */\n\nfunction MentorCredentials({ router }: { router: ReturnType<typeof useRouter> }) {\n const [title, setTitle] = useState(\"\");\n const [institution, setInstitution] = useState(\"\");\n const [gmc, setGmc] = useState(\"\");\n const [country, setCountry] = useState(\"\");\n const [specialty, setSpecialty] = useState(\"\");\n const [expertise, setExpertise] = useState<string[]>([]);\n // Volumes per procedure\n const [volumes, setVolumes] = useState<Record<string, string>>({});\n // Indemnity\n const [indemnityProvider, setIndemnityProvider] = useState(\"\");\n const [indemnityPolicy, setIndemnityPolicy] = useState(\"\");\n const [indemnityExpiry, setIndemnityExpiry] = useState(\"\");\n // Uploads\n const [affiliationLetter, setAffiliationLetter] = useState<{ name: string; size: number } | null>(null);\n const [cv, setCv] = useState<{ name: string; size: number } | null>(null);\n const [saving, setSaving] = useState(false);\n\n const canContinue = !!(\n title && institution && gmc.length >= 4 && country && specialty && expertise.length >= 1 &&\n indemnityProvider && indemnityPolicy && indemnityExpiry && affiliationLetter\n );\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/legal?role=mentor\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"mentor\" stepKey=\"credentials\"\n title=\"Professional credentials\"\n subtitle=\"Required for verification. We'll send MCA & NDA after admin review.\"\n previousHref=\"/onboarding/profile?role=mentor\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n saveLabel=\"Submit for verification\"\n >\n <InlineAlert variant=\"info\">\n Verification typically takes < 7 days. Once approved, we'll email the MCA & NDA to sign in Step 3.\n </InlineAlert>\n\n {/* ── Professional details ── */}\n <SectionHeader title=\"Professional details\" />\n <Field label=\"Professional title\" htmlFor=\"t\" hint=\"e.g. Consultant Interventional Endoscopist\">\n <Input id=\"t\" value={title} onChange={(e) => setTitle(e.target.value)} className=\"h-10\" />\n </Field>\n <Field label=\"Institution / Affiliation\" htmlFor=\"i\">\n <Input id=\"i\" value={institution} onChange={(e) => setInstitution(e.target.value)} className=\"h-10\" placeholder=\"King's College Hospital, London\" />\n </Field>\n <Field label=\"Medical registration number\" htmlFor=\"gmc\" hint=\"GMC, NMC, or equivalent national body.\">\n <Input id=\"gmc\" value={gmc} onChange={(e) => setGmc(e.target.value)} className=\"h-10\" placeholder=\"e.g. 7654321\" />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Country of practice\" htmlFor=\"ctry\"><Select id=\"ctry\" value={country} onChange={setCountry} options={COUNTRIES} placeholder=\"Select country\" /></Field>\n <Field label=\"Specialty / Subspecialty\" htmlFor=\"sp\"><Select id=\"sp\" value={specialty} onChange={setSpecialty} options={SPECIALTIES} placeholder=\"Select\" /></Field>\n </div>\n\n {/* ── Procedures & volume ── */}\n <SectionHeader title=\"Procedures & volume\" />\n <Field label=\"Expertise & procedures\" htmlFor=\"exp\" hint=\"Procedures you're qualified to mentor.\">\n <MultiSelect value={expertise} onChange={setExpertise} options={PROCEDURES} />\n </Field>\n {expertise.length > 0 && (\n <Field label=\"Approximate volume per year\" htmlFor=\"vol\" hint=\"Roughly how many of each you perform annually.\">\n <div className=\"space-y-2\">\n {expertise.map((proc) => (\n <div key={proc} className=\"flex items-center gap-3\">\n <span className=\"text-[12px] flex-1 min-w-0 truncate\" style={{ color: \"var(--color-text-secondary)\" }}>{proc}</span>\n <Input\n type=\"number\"\n className=\"h-9 w-24\"\n value={volumes[proc] ?? \"\"}\n onChange={(e) => setVolumes({ ...volumes, [proc]: e.target.value })}\n placeholder=\"0\"\n />\n <span className=\"text-[11px] w-12\" style={{ color: \"var(--color-text-muted)\" }}>/ year</span>\n </div>\n ))}\n </div>\n </Field>\n )}\n\n {/* ── Indemnity ── */}\n <SectionHeader title=\"Professional indemnity insurance\" />\n <Field label=\"Indemnity provider / defence organisation\" htmlFor=\"ip\">\n <Input id=\"ip\" value={indemnityProvider} onChange={(e) => setIndemnityProvider(e.target.value)} className=\"h-10\" placeholder=\"e.g. Medical Defence Union (MDU)\" />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Policy / Membership number\" htmlFor=\"ipnum\">\n <Input id=\"ipnum\" value={indemnityPolicy} onChange={(e) => setIndemnityPolicy(e.target.value)} className=\"h-10\" placeholder=\"e.g. MDU-1234567\" />\n </Field>\n <Field label=\"Policy expiry date\" htmlFor=\"ipexp\" hint=\"We'll remind you 60 days before.\">\n <Input id=\"ipexp\" type=\"date\" value={indemnityExpiry} onChange={(e) => setIndemnityExpiry(e.target.value)} className=\"h-10\" />\n </Field>\n </div>\n\n {/* ── Supporting documents ── */}\n <SectionHeader title=\"Supporting documents\" />\n <FileDropZone\n label=\"Institutional affiliation letter\"\n accept=\"application/pdf,image/*\"\n file={affiliationLetter}\n onFile={setAffiliationLetter}\n hint=\"PDF or image · A letter from your employer confirming your role.\"\n />\n <FileDropZone\n label=\"CV (optional)\"\n accept=\"application/pdf\"\n file={cv}\n onFile={setCv}\n hint=\"PDF · helpful for admin review but not required.\"\n />\n </OnboardingShell>\n );\n}\n\n/* ─── Reusable section header ─────────────────────────────────────────── */\n\nfunction SectionHeader({ title }: { title: string }) {\n return (\n <p className=\"text-[10px] font-semibold uppercase tracking-[0.12em] pt-3 -mb-1\" style={{ color: \"var(--color-text-muted)\" }}>\n {title}\n </p>\n );\n}\n\n/* ─── SPONSOR Onboarding Questionnaire ──────────────────────────────────── */\n\nfunction SponsorQuestionnaire({ router }: { router: ReturnType<typeof useRouter> }) {\n const [industry, setIndustry] = useState(\"\");\n const [objectives, setObjectives] = useState(\"\");\n const [traineeCount, setTraineeCount] = useState(\"\");\n const [specialties, setSpecialties] = useState<string[]>([]);\n const [geographies, setGeographies] = useState<string[]>([]);\n const [saving, setSaving] = useState(false);\n\n const canContinue = !!(industry && objectives.length >= 20 && traineeCount && specialties.length >= 1);\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/legal?role=sponsor\"), 600);\n };\n\n const INDUSTRIES = [\n { value: \"medtech\", label: \"Medical Device / MedTech\" },\n { value: \"pharma\", label: \"Pharmaceutical\" },\n { value: \"hospital\", label: \"Hospital / Trust\" },\n { value: \"society\", label: \"Society / Association\" },\n { value: \"education\", label: \"Academic / Education\" },\n ];\n\n return (\n <OnboardingShell\n role=\"sponsor\" stepKey=\"credentials\"\n title=\"Programme details\"\n subtitle=\"Tell us what you'd like to achieve so we can scope a programme.\"\n previousHref=\"/onboarding/profile?role=sponsor\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n >\n <Field label=\"Industry / Sector\" htmlFor=\"ind\"><Select id=\"ind\" value={industry} onChange={setIndustry} options={INDUSTRIES} placeholder=\"Select industry\" /></Field>\n <Field label=\"Training objectives / Goals\" htmlFor=\"obj\" hint=\"What outcomes do you want to drive?\">\n <Textarea id=\"obj\" value={objectives} onChange={(e) => setObjectives(e.target.value)} rows={4} placeholder=\"e.g. Accelerate ESD adoption across 30 trainees in the UK & Germany; build a measurable competency record.\" />\n </Field>\n <Field label=\"Number of trainees to sponsor\" htmlFor=\"tc\">\n <Input id=\"tc\" type=\"number\" value={traineeCount} onChange={(e) => setTraineeCount(e.target.value)} className=\"h-10\" placeholder=\"e.g. 25\" />\n </Field>\n <Field label=\"Preferred specialties / procedures\" htmlFor=\"sp\">\n <MultiSelect value={specialties} onChange={setSpecialties} options={PROCEDURES} />\n </Field>\n <Field label=\"Target geographies\" htmlFor=\"geo\" optional>\n <MultiSelect value={geographies} onChange={setGeographies} options={[\"UK\", \"EU\", \"US\", \"APAC\", \"ME\", \"Latin America\"]} />\n </Field>\n </OnboardingShell>\n );\n}\n"
|
|
52
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
+
"file": "app/(auth)/onboarding/legal/page.tsx",
|
|
4
|
+
"sha256": "8490371af183a11eeb42ca2d6cfe8cf4308e50b03f9509f0c6551d608c860b24",
|
|
5
|
+
"imports": [
|
|
6
|
+
"react",
|
|
7
|
+
"next/navigation",
|
|
8
|
+
"lucide-react",
|
|
9
|
+
"@/components/ui/button",
|
|
10
|
+
"@/components/auth/onboarding-shell",
|
|
11
|
+
"@/components/auth/auth-primitives",
|
|
12
|
+
"@/components/auth/onboarding-primitives"
|
|
13
|
+
],
|
|
14
|
+
"tokens": [
|
|
15
|
+
"--color-bg-brand-muted",
|
|
16
|
+
"--color-bg-subtle",
|
|
17
|
+
"--color-border-brand",
|
|
18
|
+
"--color-border-default",
|
|
19
|
+
"--color-text-brand",
|
|
20
|
+
"--color-text-muted",
|
|
21
|
+
"--color-text-secondary"
|
|
22
|
+
],
|
|
23
|
+
"fragments": [
|
|
24
|
+
{
|
|
25
|
+
"id": "LegalPage",
|
|
26
|
+
"line": 13,
|
|
27
|
+
"code": "export default function LegalPage() {\n const router = useRouter();\n const role = (useSearchParams().get(\"role\") as Role | null) ?? \"trainee\";\n\n if (role === \"sponsor\") return <SponsorLegal router={router} />;\n if (role === \"mentor\") return <MentorLegal router={router} />;\n return <TraineeLegal router={router} />;\n}"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "ContractRow",
|
|
31
|
+
"line": 24,
|
|
32
|
+
"code": "function ContractRow({ name, hint }: { name: string; hint: string }) {\n return (\n <div className=\"flex items-center gap-3 p-3 rounded-lg border\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"w-9 h-9 rounded-md flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n <FileText size={16} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">{name}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>{hint}</div>\n </div>\n <Button variant=\"outline\" size=\"sm\">\n <Download size={12} /> Download\n </Button>\n </div>\n );\n}"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "TraineeLegal",
|
|
36
|
+
"line": 43,
|
|
37
|
+
"code": "function TraineeLegal({ router }: { router: ReturnType<typeof useRouter> }) {\n const [file, setFile] = useState<FileMeta>(null);\n const [saving, setSaving] = useState(false);\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/orientation?role=trainee\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"trainee\" stepKey=\"legal\"\n title=\"Sign the Mentee Agreement\"\n subtitle=\"Download, sign, then upload the signed PDF below.\"\n previousHref=\"/onboarding/credentials?role=trainee\"\n onSave={handleSave} saving={saving} canContinue={!!file}\n >\n <InlineAlert variant=\"info\" title=\"Check your email\">\n We sent your personalised Mentee Agreement to your inbox. Sign and re-upload here.\n </InlineAlert>\n <ContractRow name=\"Mentee Agreement.pdf\" hint=\"Personalised for your account · Sign electronically with any standard signer.\" />\n <FileDropZone\n label=\"Drop your signed Mentee Agreement here\"\n accept=\"application/pdf\"\n file={file}\n onFile={setFile}\n hint=\"PDF only · max 10 MB\"\n />\n </OnboardingShell>\n );\n}"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "MentorLegal",
|
|
41
|
+
"line": 77,
|
|
42
|
+
"code": "function MentorLegal({ router }: { router: ReturnType<typeof useRouter> }) {\n const [mca, setMca] = useState<FileMeta>(null);\n const [nda, setNda] = useState<FileMeta>(null);\n const [saving, setSaving] = useState(false);\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/orientation?role=mentor\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"mentor\" stepKey=\"legal\"\n title=\"Sign your mentor contracts\"\n subtitle=\"Two short documents. Bank & indemnity details live inside the MCA.\"\n previousHref=\"/onboarding/credentials?role=mentor\"\n onSave={handleSave} saving={saving} canContinue={!!mca && !!nda}\n >\n <InlineAlert variant=\"info\" title=\"Sent to your email\" action={<a href=\"#\" className=\"text-[12px] font-medium hover:underline underline-offset-4 inline-flex items-center gap-1\"><Mail size={12} /> Resend</a>}>\n The MCA and NDA were emailed to you. They contain bank, indemnity, and counter-signature fields.\n </InlineAlert>\n\n <div className=\"space-y-2\">\n <ContractRow name=\"Mentor Consultancy Agreement (MCA).pdf\" hint=\"Includes bank details, indemnity insurance, and rate schedule.\" />\n <ContractRow name=\"Non-Disclosure Agreement (NDA).pdf\" hint=\"Protects patient and institutional confidentiality.\" />\n </div>\n\n <FileDropZone label=\"Signed MCA\" accept=\"application/pdf\" file={mca} onFile={setMca} hint=\"PDF only · max 10 MB\" />\n <FileDropZone label=\"Signed NDA\" accept=\"application/pdf\" file={nda} onFile={setNda} hint=\"PDF only · max 10 MB\" />\n </OnboardingShell>\n );\n}"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "SponsorLegal",
|
|
46
|
+
"line": 112,
|
|
47
|
+
"code": "function SponsorLegal({ router }: { router: ReturnType<typeof useRouter> }) {\n const [terms, setTerms] = useState(false);\n const [privacy, setPrivacy] = useState(false);\n const [dpa, setDpa] = useState(false);\n const [saving, setSaving] = useState(false);\n\n const canContinue = terms && privacy && dpa;\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/orientation?role=sponsor\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"sponsor\" stepKey=\"legal\"\n title=\"Agreements\"\n subtitle=\"Three standard agreements to activate your sponsor workspace.\"\n previousHref=\"/onboarding/credentials?role=sponsor\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n >\n <AgreementCard\n title=\"Terms & Conditions\"\n hint=\"The general terms of use for Mentix sponsors.\"\n checked={terms}\n onChange={setTerms}\n />\n <AgreementCard\n title=\"Privacy Policy\"\n hint=\"How we collect and process data for sponsored programmes.\"\n checked={privacy}\n onChange={setPrivacy}\n />\n <AgreementCard\n title=\"Data Processing Agreement (DPA)\"\n hint=\"GDPR-compliant DPA covering data shared with sponsored trainees.\"\n checked={dpa}\n onChange={setDpa}\n />\n </OnboardingShell>\n );\n}"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "AgreementCard",
|
|
51
|
+
"line": 155,
|
|
52
|
+
"code": "function AgreementCard({ title, hint, checked, onChange }: { title: string; hint: string; checked: boolean; onChange: (v: boolean) => void }) {\n return (\n <div\n className=\"rounded-xl border p-4 transition-colors\"\n style={{\n borderColor: checked ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: checked ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n }}\n >\n <div className=\"flex items-start gap-3\">\n <FileText size={16} strokeWidth={1.8} className=\"mt-0.5 shrink-0\" style={{ color: checked ? \"var(--color-text-brand)\" : \"var(--color-text-secondary)\" }} />\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-semibold mb-0.5\">{title}</div>\n <p className=\"text-[12px] mb-3\" style={{ color: \"var(--color-text-secondary)\" }}>{hint}</p>\n <div className=\"flex items-center justify-between\">\n <Checkbox id={`agree-${title}`} checked={checked} onChange={onChange}>\n I agree\n </Checkbox>\n <a href=\"#\" className=\"text-[12px] inline-flex items-center gap-1 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-brand)\" }}>\n <Download size={11} /> Read\n </a>\n </div>\n </div>\n </div>\n </div>\n );\n}"
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useSearchParams, useRouter } from \"next/navigation\";\nimport { Download, FileText, Mail } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { OnboardingShell, Role } from \"@/components/auth/onboarding-shell\";\nimport { Checkbox, InlineAlert } from \"@/components/auth/auth-primitives\";\nimport { FileDropZone } from \"@/components/auth/onboarding-primitives\";\n\ntype FileMeta = { name: string; size: number } | null;\n\nexport default function LegalPage() {\n const router = useRouter();\n const role = (useSearchParams().get(\"role\") as Role | null) ?? \"trainee\";\n\n if (role === \"sponsor\") return <SponsorLegal router={router} />;\n if (role === \"mentor\") return <MentorLegal router={router} />;\n return <TraineeLegal router={router} />;\n}\n\n/* ─── Reusable contract download row ─────────────────────────────────────── */\n\nfunction ContractRow({ name, hint }: { name: string; hint: string }) {\n return (\n <div className=\"flex items-center gap-3 p-3 rounded-lg border\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"w-9 h-9 rounded-md flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n <FileText size={16} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">{name}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>{hint}</div>\n </div>\n <Button variant=\"outline\" size=\"sm\">\n <Download size={12} /> Download\n </Button>\n </div>\n );\n}\n\n/* ─── TRAINEE Legal ─────────────────────────────────────────────────────── */\n\nfunction TraineeLegal({ router }: { router: ReturnType<typeof useRouter> }) {\n const [file, setFile] = useState<FileMeta>(null);\n const [saving, setSaving] = useState(false);\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/orientation?role=trainee\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"trainee\" stepKey=\"legal\"\n title=\"Sign the Mentee Agreement\"\n subtitle=\"Download, sign, then upload the signed PDF below.\"\n previousHref=\"/onboarding/credentials?role=trainee\"\n onSave={handleSave} saving={saving} canContinue={!!file}\n >\n <InlineAlert variant=\"info\" title=\"Check your email\">\n We sent your personalised Mentee Agreement to your inbox. Sign and re-upload here.\n </InlineAlert>\n <ContractRow name=\"Mentee Agreement.pdf\" hint=\"Personalised for your account · Sign electronically with any standard signer.\" />\n <FileDropZone\n label=\"Drop your signed Mentee Agreement here\"\n accept=\"application/pdf\"\n file={file}\n onFile={setFile}\n hint=\"PDF only · max 10 MB\"\n />\n </OnboardingShell>\n );\n}\n\n/* ─── MENTOR Legal ───────────────────────────────────────────────────────── */\n\nfunction MentorLegal({ router }: { router: ReturnType<typeof useRouter> }) {\n const [mca, setMca] = useState<FileMeta>(null);\n const [nda, setNda] = useState<FileMeta>(null);\n const [saving, setSaving] = useState(false);\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/orientation?role=mentor\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"mentor\" stepKey=\"legal\"\n title=\"Sign your mentor contracts\"\n subtitle=\"Two short documents. Bank & indemnity details live inside the MCA.\"\n previousHref=\"/onboarding/credentials?role=mentor\"\n onSave={handleSave} saving={saving} canContinue={!!mca && !!nda}\n >\n <InlineAlert variant=\"info\" title=\"Sent to your email\" action={<a href=\"#\" className=\"text-[12px] font-medium hover:underline underline-offset-4 inline-flex items-center gap-1\"><Mail size={12} /> Resend</a>}>\n The MCA and NDA were emailed to you. They contain bank, indemnity, and counter-signature fields.\n </InlineAlert>\n\n <div className=\"space-y-2\">\n <ContractRow name=\"Mentor Consultancy Agreement (MCA).pdf\" hint=\"Includes bank details, indemnity insurance, and rate schedule.\" />\n <ContractRow name=\"Non-Disclosure Agreement (NDA).pdf\" hint=\"Protects patient and institutional confidentiality.\" />\n </div>\n\n <FileDropZone label=\"Signed MCA\" accept=\"application/pdf\" file={mca} onFile={setMca} hint=\"PDF only · max 10 MB\" />\n <FileDropZone label=\"Signed NDA\" accept=\"application/pdf\" file={nda} onFile={setNda} hint=\"PDF only · max 10 MB\" />\n </OnboardingShell>\n );\n}\n\n/* ─── SPONSOR Legal ──────────────────────────────────────────────────────── */\n\nfunction SponsorLegal({ router }: { router: ReturnType<typeof useRouter> }) {\n const [terms, setTerms] = useState(false);\n const [privacy, setPrivacy] = useState(false);\n const [dpa, setDpa] = useState(false);\n const [saving, setSaving] = useState(false);\n\n const canContinue = terms && privacy && dpa;\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/orientation?role=sponsor\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"sponsor\" stepKey=\"legal\"\n title=\"Agreements\"\n subtitle=\"Three standard agreements to activate your sponsor workspace.\"\n previousHref=\"/onboarding/credentials?role=sponsor\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n >\n <AgreementCard\n title=\"Terms & Conditions\"\n hint=\"The general terms of use for Mentix sponsors.\"\n checked={terms}\n onChange={setTerms}\n />\n <AgreementCard\n title=\"Privacy Policy\"\n hint=\"How we collect and process data for sponsored programmes.\"\n checked={privacy}\n onChange={setPrivacy}\n />\n <AgreementCard\n title=\"Data Processing Agreement (DPA)\"\n hint=\"GDPR-compliant DPA covering data shared with sponsored trainees.\"\n checked={dpa}\n onChange={setDpa}\n />\n </OnboardingShell>\n );\n}\n\nfunction AgreementCard({ title, hint, checked, onChange }: { title: string; hint: string; checked: boolean; onChange: (v: boolean) => void }) {\n return (\n <div\n className=\"rounded-xl border p-4 transition-colors\"\n style={{\n borderColor: checked ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: checked ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n }}\n >\n <div className=\"flex items-start gap-3\">\n <FileText size={16} strokeWidth={1.8} className=\"mt-0.5 shrink-0\" style={{ color: checked ? \"var(--color-text-brand)\" : \"var(--color-text-secondary)\" }} />\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-semibold mb-0.5\">{title}</div>\n <p className=\"text-[12px] mb-3\" style={{ color: \"var(--color-text-secondary)\" }}>{hint}</p>\n <div className=\"flex items-center justify-between\">\n <Checkbox id={`agree-${title}`} checked={checked} onChange={onChange}>\n I agree\n </Checkbox>\n <a href=\"#\" className=\"text-[12px] inline-flex items-center gap-1 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-brand)\" }}>\n <Download size={11} /> Read\n </a>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"
|
|
56
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
+
"file": "app/(auth)/onboarding/orientation/page.tsx",
|
|
4
|
+
"sha256": "9f5d06650b9281d3adf8cdbc14c45ec87a37ae457b5df82d27e598301ae38b27",
|
|
5
|
+
"imports": [
|
|
6
|
+
"react",
|
|
7
|
+
"next/navigation",
|
|
8
|
+
"lucide-react",
|
|
9
|
+
"@/components/ui/button",
|
|
10
|
+
"@/components/auth/onboarding-shell",
|
|
11
|
+
"@/components/auth/auth-primitives"
|
|
12
|
+
],
|
|
13
|
+
"tokens": [
|
|
14
|
+
"--color-bg-brand-muted",
|
|
15
|
+
"--color-bg-subtle",
|
|
16
|
+
"--color-border-brand",
|
|
17
|
+
"--color-border-default",
|
|
18
|
+
"--color-border-muted",
|
|
19
|
+
"--color-status-success-text",
|
|
20
|
+
"--color-text-brand",
|
|
21
|
+
"--color-text-muted",
|
|
22
|
+
"--color-text-primary",
|
|
23
|
+
"--color-text-secondary",
|
|
24
|
+
"--gray-950",
|
|
25
|
+
"--shadow-glow",
|
|
26
|
+
"--teal-500"
|
|
27
|
+
],
|
|
28
|
+
"fragments": [
|
|
29
|
+
{
|
|
30
|
+
"id": "OrientationPage",
|
|
31
|
+
"line": 10,
|
|
32
|
+
"code": "export default function OrientationPage() {\n const router = useRouter();\n const role = (useSearchParams().get(\"role\") as Role | null) ?? \"trainee\";\n\n if (role === \"sponsor\") return <SponsorCreditSetup router={router} />;\n if (role === \"mentor\") return <MentorOrientation router={router} />;\n return <TraineeOrientation router={router} />;\n}"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "finish",
|
|
36
|
+
"line": 19,
|
|
37
|
+
"code": "const finish = (router: ReturnType<typeof useRouter>) => router.push(\"/welcome?state=redirect\");"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "TraineeOrientation",
|
|
41
|
+
"line": 23,
|
|
42
|
+
"code": "function TraineeOrientation({ router }: { router: ReturnType<typeof useRouter> }) {\n const [watched, setWatched] = useState(false);\n const [acknowledged, setAcknowledged] = useState(false);\n const [saving, setSaving] = useState(false);\n\n return (\n <OnboardingShell\n role=\"trainee\" stepKey=\"orientation\"\n title=\"Quick orientation\"\n subtitle=\"One short video, then you're in.\"\n previousHref=\"/onboarding/legal?role=trainee\"\n onSave={() => { setSaving(true); setTimeout(() => finish(router), 600); }}\n saving={saving} canContinue={watched && acknowledged}\n saveLabel=\"Finish onboarding\"\n >\n <VideoCard\n title=\"Getting the most out of Mentix\"\n duration=\"3 min\"\n watched={watched}\n onWatched={setWatched}\n />\n <div className=\"rounded-xl border p-4\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <p className=\"text-[13px] font-medium mb-3\">Acknowledgement</p>\n <Checkbox id=\"ack\" checked={acknowledged} onChange={setAcknowledged}>\n I understand how to request a session, share context, and review feedback.\n </Checkbox>\n </div>\n </OnboardingShell>\n );\n}"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "MentorOrientation",
|
|
46
|
+
"line": 56,
|
|
47
|
+
"code": "function MentorOrientation({ router }: { router: ReturnType<typeof useRouter> }) {\n const [governance, setGovernance] = useState(false);\n const [deid, setDeid] = useState(false);\n const [casePrep, setCasePrep] = useState(false);\n const [feedback, setFeedback] = useState(false);\n const [saving, setSaving] = useState(false);\n\n const canContinue = governance && deid && casePrep && feedback;\n\n return (\n <OnboardingShell\n role=\"mentor\" stepKey=\"orientation\"\n title=\"Mentor guidelines\"\n subtitle=\"Four short acknowledgements before you go live.\"\n previousHref=\"/onboarding/legal?role=mentor\"\n onSave={() => { setSaving(true); setTimeout(() => finish(router), 600); }}\n saving={saving} canContinue={canContinue}\n saveLabel=\"Finish onboarding\"\n >\n <AckCard\n icon={ShieldCheck}\n title=\"Platform guidelines & clinical governance\"\n body=\"Read our mentor playbook covering professionalism, session structure, and incident reporting.\"\n checked={governance}\n onChange={setGovernance}\n readLink\n />\n <AckCard\n icon={FileText}\n title=\"De-identified patient information\"\n body=\"All shared media must be de-identified per our protocol. No identifiable data is permitted in sessions.\"\n checked={deid}\n onChange={setDeid}\n />\n <AckCard\n icon={BookOpen}\n title=\"Case preparation expectations\"\n body=\"Review the case context before each session. Trainees expect structured, focused mentorship.\"\n checked={casePrep}\n onChange={setCasePrep}\n />\n <AckCard\n icon={CheckCircle2}\n title=\"Structured competency feedback\"\n body=\"At the end of each session, provide structured feedback using our 4-point competency rubric.\"\n checked={feedback}\n onChange={setFeedback}\n />\n </OnboardingShell>\n );\n}"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "SponsorCreditSetup",
|
|
51
|
+
"line": 110,
|
|
52
|
+
"code": "function SponsorCreditSetup({ router }: { router: ReturnType<typeof useRouter> }) {\n const [tier, setTier] = useState<\"starter\" | \"growth\" | \"scale\" | \"enterprise\" | null>(null);\n const [credits, setCredits] = useState<number>(0);\n const [saving, setSaving] = useState(false);\n\n const tiers = [\n { id: \"starter\" as const, name: \"Starter\", accessFee: \"£15k\", sessionFee: \"£14k\", total: \"£29k\", credits: 100, features: [\"Up to 25 trainees\", \"Standard reporting\", \"Email support\"] },\n { id: \"growth\" as const, name: \"Growth\", accessFee: \"£15k\", sessionFee: \"£24k\", total: \"£39k\", credits: 250, features: [\"Up to 75 trainees\", \"Custom KPI dashboard\", \"Priority support\"], featured: true },\n { id: \"scale\" as const, name: \"Scale\", accessFee: \"£15k\", sessionFee: \"£50k\", total: \"£65k\", credits: 600, features: [\"Unlimited trainees\", \"Dedicated success manager\", \"SLA + integrations\"] },\n { id: \"enterprise\" as const, name: \"Enterprise\", accessFee: \"Custom\", sessionFee: \"Custom\", total: \"Custom\", credits: 0, features: [\"Multi-region rollout\", \"Custom contracts & DPA\", \"White-glove onboarding\"] },\n ];\n\n return (\n <OnboardingShell\n role=\"sponsor\" stepKey=\"orientation\"\n title=\"Choose your subscription\"\n subtitle=\"Annual platform access fee + session credit package. Top up anytime.\"\n previousHref=\"/onboarding/legal?role=sponsor\"\n onSave={() => { setSaving(true); setTimeout(() => finish(router), 600); }}\n saving={saving} canContinue={!!tier}\n saveLabel=\"Finalise & activate\"\n >\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2\">\n {tiers.map((t) => {\n const active = tier === t.id;\n return (\n <button\n key={t.id}\n type=\"button\"\n onClick={() => { setTier(t.id); setCredits(t.credits); }}\n aria-pressed={active}\n className=\"text-left p-4 rounded-2xl border transition-all focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30 relative\"\n style={{\n borderColor: active ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: active ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n boxShadow: active ? \"var(--shadow-glow)\" : \"none\",\n }}\n >\n {t.featured && !active && (\n <span className=\"absolute -top-2 left-3 text-[9px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded-md\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>\n Most popular\n </span>\n )}\n <div className=\"text-[15px] font-semibold mb-1\" style={{ color: active ? \"var(--color-text-brand)\" : \"var(--color-text-primary)\" }}>{t.name}</div>\n <div className=\"text-[18px] font-bold mb-1\">{t.total} <span className=\"text-[11px] font-normal\" style={{ color: \"var(--color-text-muted)\" }}>/ yr</span></div>\n <div className=\"text-[10px] mb-2 pb-2 border-b\" style={{ color: \"var(--color-text-muted)\", borderColor: \"var(--color-border-muted)\" }}>\n <div>Platform access: {t.accessFee}</div>\n <div>Session credits: {t.sessionFee}</div>\n </div>\n <div className=\"text-[11px] mb-2\" style={{ color: \"var(--color-text-secondary)\" }}>\n {t.credits > 0 ? `${t.credits} credits / year` : \"Custom credit pool\"}\n </div>\n <ul className=\"space-y-1 text-[11px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n {t.features.map((f) => (\n <li key={f} className=\"flex items-start gap-1.5\">\n <CheckCircle2 size={10} strokeWidth={2.5} className=\"mt-0.5 shrink-0\" style={{ color: \"var(--teal-500)\" }} />\n {f}\n </li>\n ))}\n </ul>\n </button>\n );\n })}\n </div>\n\n {tier && (\n <div className=\"rounded-xl border p-4 flex items-start gap-3\" style={{ borderColor: \"var(--color-border-default)\", background: \"var(--color-bg-subtle)\" }}>\n <Coins size={16} strokeWidth={1.8} className=\"shrink-0 mt-0.5\" style={{ color: \"var(--color-text-brand)\" }} />\n <div className=\"flex-1 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <p className=\"font-medium mb-1\" style={{ color: \"var(--color-text-primary)\" }}>How credits work</p>\n <p>1 credit = 1 session ≤ 3h · 2 credits = 3–6h · 3 credits = > 6h. You allocate credits to trainees or a shared pool after onboarding.</p>\n </div>\n </div>\n )}\n\n <InlineAlert variant=\"info\">\n Our team will call within 24 hours to finalise contracts and walk you through the sponsor dashboard.\n </InlineAlert>\n </OnboardingShell>\n );\n}"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "VideoCard",
|
|
56
|
+
"line": 194,
|
|
57
|
+
"code": "function VideoCard({ title, duration, watched, onWatched }: { title: string; duration: string; watched: boolean; onWatched: (v: boolean) => void }) {\n return (\n <div className=\"rounded-2xl overflow-hidden border\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <button\n type=\"button\"\n onClick={() => onWatched(true)}\n className=\"w-full aspect-video flex flex-col items-center justify-center gap-2 transition-colors hover:bg-[var(--color-bg-subtle)]\"\n style={{ background: \"var(--color-bg-subtle)\" }}\n >\n {watched ? (\n <>\n <CheckCircle2 size={36} strokeWidth={1.5} style={{ color: \"var(--color-status-success-text)\" }} />\n <span className=\"text-[12px] font-medium\" style={{ color: \"var(--color-status-success-text)\" }}>Watched</span>\n </>\n ) : (\n <>\n <PlayCircle size={48} strokeWidth={1.3} style={{ color: \"var(--teal-500)\" }} />\n <span className=\"text-[12px] font-medium\" style={{ color: \"var(--color-text-secondary)\" }}>Click to watch</span>\n </>\n )}\n </button>\n <div className=\"p-4 flex items-center justify-between\">\n <div>\n <div className=\"text-[13px] font-semibold\">{title}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>{duration} · auto-marks when finished</div>\n </div>\n </div>\n </div>\n );\n}"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "AckCard",
|
|
61
|
+
"line": 225,
|
|
62
|
+
"code": "function AckCard({ icon: Icon, title, body, checked, onChange, readLink }: { icon: typeof ShieldCheck; title: string; body: string; checked: boolean; onChange: (v: boolean) => void; readLink?: boolean }) {\n return (\n <div className=\"rounded-xl border p-4\" style={{ borderColor: checked ? \"var(--color-border-brand)\" : \"var(--color-border-default)\", background: checked ? \"var(--color-bg-brand-muted)\" : \"transparent\" }}>\n <div className=\"flex items-start gap-3\">\n <Icon size={18} strokeWidth={1.7} className=\"shrink-0 mt-0.5\" style={{ color: checked ? \"var(--color-text-brand)\" : \"var(--color-text-secondary)\" }} />\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-semibold mb-1\">{title}</div>\n <p className=\"text-[12px] mb-3\" style={{ color: \"var(--color-text-secondary)\" }}>{body}</p>\n <div className=\"flex items-center justify-between\">\n <Checkbox id={`ack-${title}`} checked={checked} onChange={onChange}>\n I acknowledge\n </Checkbox>\n {readLink && (\n <a href=\"#\" className=\"text-[12px] font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>\n Read the playbook →\n </a>\n )}\n </div>\n </div>\n </div>\n </div>\n );\n}"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useSearchParams, useRouter } from \"next/navigation\";\nimport { PlayCircle, ShieldCheck, FileText, BookOpen, Coins, CheckCircle2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { OnboardingShell, Role } from \"@/components/auth/onboarding-shell\";\nimport { Checkbox, InlineAlert } from \"@/components/auth/auth-primitives\";\n\nexport default function OrientationPage() {\n const router = useRouter();\n const role = (useSearchParams().get(\"role\") as Role | null) ?? \"trainee\";\n\n if (role === \"sponsor\") return <SponsorCreditSetup router={router} />;\n if (role === \"mentor\") return <MentorOrientation router={router} />;\n return <TraineeOrientation router={router} />;\n}\n\nconst finish = (router: ReturnType<typeof useRouter>) => router.push(\"/welcome?state=redirect\");\n\n/* ─── TRAINEE Orientation ───────────────────────────────────────────────── */\n\nfunction TraineeOrientation({ router }: { router: ReturnType<typeof useRouter> }) {\n const [watched, setWatched] = useState(false);\n const [acknowledged, setAcknowledged] = useState(false);\n const [saving, setSaving] = useState(false);\n\n return (\n <OnboardingShell\n role=\"trainee\" stepKey=\"orientation\"\n title=\"Quick orientation\"\n subtitle=\"One short video, then you're in.\"\n previousHref=\"/onboarding/legal?role=trainee\"\n onSave={() => { setSaving(true); setTimeout(() => finish(router), 600); }}\n saving={saving} canContinue={watched && acknowledged}\n saveLabel=\"Finish onboarding\"\n >\n <VideoCard\n title=\"Getting the most out of Mentix\"\n duration=\"3 min\"\n watched={watched}\n onWatched={setWatched}\n />\n <div className=\"rounded-xl border p-4\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <p className=\"text-[13px] font-medium mb-3\">Acknowledgement</p>\n <Checkbox id=\"ack\" checked={acknowledged} onChange={setAcknowledged}>\n I understand how to request a session, share context, and review feedback.\n </Checkbox>\n </div>\n </OnboardingShell>\n );\n}\n\n/* ─── MENTOR Orientation ────────────────────────────────────────────────── */\n\nfunction MentorOrientation({ router }: { router: ReturnType<typeof useRouter> }) {\n const [governance, setGovernance] = useState(false);\n const [deid, setDeid] = useState(false);\n const [casePrep, setCasePrep] = useState(false);\n const [feedback, setFeedback] = useState(false);\n const [saving, setSaving] = useState(false);\n\n const canContinue = governance && deid && casePrep && feedback;\n\n return (\n <OnboardingShell\n role=\"mentor\" stepKey=\"orientation\"\n title=\"Mentor guidelines\"\n subtitle=\"Four short acknowledgements before you go live.\"\n previousHref=\"/onboarding/legal?role=mentor\"\n onSave={() => { setSaving(true); setTimeout(() => finish(router), 600); }}\n saving={saving} canContinue={canContinue}\n saveLabel=\"Finish onboarding\"\n >\n <AckCard\n icon={ShieldCheck}\n title=\"Platform guidelines & clinical governance\"\n body=\"Read our mentor playbook covering professionalism, session structure, and incident reporting.\"\n checked={governance}\n onChange={setGovernance}\n readLink\n />\n <AckCard\n icon={FileText}\n title=\"De-identified patient information\"\n body=\"All shared media must be de-identified per our protocol. No identifiable data is permitted in sessions.\"\n checked={deid}\n onChange={setDeid}\n />\n <AckCard\n icon={BookOpen}\n title=\"Case preparation expectations\"\n body=\"Review the case context before each session. Trainees expect structured, focused mentorship.\"\n checked={casePrep}\n onChange={setCasePrep}\n />\n <AckCard\n icon={CheckCircle2}\n title=\"Structured competency feedback\"\n body=\"At the end of each session, provide structured feedback using our 4-point competency rubric.\"\n checked={feedback}\n onChange={setFeedback}\n />\n </OnboardingShell>\n );\n}\n\n/* ─── SPONSOR Credit Setup ──────────────────────────────────────────────── */\n\nfunction SponsorCreditSetup({ router }: { router: ReturnType<typeof useRouter> }) {\n const [tier, setTier] = useState<\"starter\" | \"growth\" | \"scale\" | \"enterprise\" | null>(null);\n const [credits, setCredits] = useState<number>(0);\n const [saving, setSaving] = useState(false);\n\n const tiers = [\n { id: \"starter\" as const, name: \"Starter\", accessFee: \"£15k\", sessionFee: \"£14k\", total: \"£29k\", credits: 100, features: [\"Up to 25 trainees\", \"Standard reporting\", \"Email support\"] },\n { id: \"growth\" as const, name: \"Growth\", accessFee: \"£15k\", sessionFee: \"£24k\", total: \"£39k\", credits: 250, features: [\"Up to 75 trainees\", \"Custom KPI dashboard\", \"Priority support\"], featured: true },\n { id: \"scale\" as const, name: \"Scale\", accessFee: \"£15k\", sessionFee: \"£50k\", total: \"£65k\", credits: 600, features: [\"Unlimited trainees\", \"Dedicated success manager\", \"SLA + integrations\"] },\n { id: \"enterprise\" as const, name: \"Enterprise\", accessFee: \"Custom\", sessionFee: \"Custom\", total: \"Custom\", credits: 0, features: [\"Multi-region rollout\", \"Custom contracts & DPA\", \"White-glove onboarding\"] },\n ];\n\n return (\n <OnboardingShell\n role=\"sponsor\" stepKey=\"orientation\"\n title=\"Choose your subscription\"\n subtitle=\"Annual platform access fee + session credit package. Top up anytime.\"\n previousHref=\"/onboarding/legal?role=sponsor\"\n onSave={() => { setSaving(true); setTimeout(() => finish(router), 600); }}\n saving={saving} canContinue={!!tier}\n saveLabel=\"Finalise & activate\"\n >\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2\">\n {tiers.map((t) => {\n const active = tier === t.id;\n return (\n <button\n key={t.id}\n type=\"button\"\n onClick={() => { setTier(t.id); setCredits(t.credits); }}\n aria-pressed={active}\n className=\"text-left p-4 rounded-2xl border transition-all focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30 relative\"\n style={{\n borderColor: active ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: active ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n boxShadow: active ? \"var(--shadow-glow)\" : \"none\",\n }}\n >\n {t.featured && !active && (\n <span className=\"absolute -top-2 left-3 text-[9px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded-md\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>\n Most popular\n </span>\n )}\n <div className=\"text-[15px] font-semibold mb-1\" style={{ color: active ? \"var(--color-text-brand)\" : \"var(--color-text-primary)\" }}>{t.name}</div>\n <div className=\"text-[18px] font-bold mb-1\">{t.total} <span className=\"text-[11px] font-normal\" style={{ color: \"var(--color-text-muted)\" }}>/ yr</span></div>\n <div className=\"text-[10px] mb-2 pb-2 border-b\" style={{ color: \"var(--color-text-muted)\", borderColor: \"var(--color-border-muted)\" }}>\n <div>Platform access: {t.accessFee}</div>\n <div>Session credits: {t.sessionFee}</div>\n </div>\n <div className=\"text-[11px] mb-2\" style={{ color: \"var(--color-text-secondary)\" }}>\n {t.credits > 0 ? `${t.credits} credits / year` : \"Custom credit pool\"}\n </div>\n <ul className=\"space-y-1 text-[11px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n {t.features.map((f) => (\n <li key={f} className=\"flex items-start gap-1.5\">\n <CheckCircle2 size={10} strokeWidth={2.5} className=\"mt-0.5 shrink-0\" style={{ color: \"var(--teal-500)\" }} />\n {f}\n </li>\n ))}\n </ul>\n </button>\n );\n })}\n </div>\n\n {tier && (\n <div className=\"rounded-xl border p-4 flex items-start gap-3\" style={{ borderColor: \"var(--color-border-default)\", background: \"var(--color-bg-subtle)\" }}>\n <Coins size={16} strokeWidth={1.8} className=\"shrink-0 mt-0.5\" style={{ color: \"var(--color-text-brand)\" }} />\n <div className=\"flex-1 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <p className=\"font-medium mb-1\" style={{ color: \"var(--color-text-primary)\" }}>How credits work</p>\n <p>1 credit = 1 session ≤ 3h · 2 credits = 3–6h · 3 credits = > 6h. You allocate credits to trainees or a shared pool after onboarding.</p>\n </div>\n </div>\n )}\n\n <InlineAlert variant=\"info\">\n Our team will call within 24 hours to finalise contracts and walk you through the sponsor dashboard.\n </InlineAlert>\n </OnboardingShell>\n );\n}\n\n/* ─── Reusable cards ─────────────────────────────────────────────────────── */\n\nfunction VideoCard({ title, duration, watched, onWatched }: { title: string; duration: string; watched: boolean; onWatched: (v: boolean) => void }) {\n return (\n <div className=\"rounded-2xl overflow-hidden border\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <button\n type=\"button\"\n onClick={() => onWatched(true)}\n className=\"w-full aspect-video flex flex-col items-center justify-center gap-2 transition-colors hover:bg-[var(--color-bg-subtle)]\"\n style={{ background: \"var(--color-bg-subtle)\" }}\n >\n {watched ? (\n <>\n <CheckCircle2 size={36} strokeWidth={1.5} style={{ color: \"var(--color-status-success-text)\" }} />\n <span className=\"text-[12px] font-medium\" style={{ color: \"var(--color-status-success-text)\" }}>Watched</span>\n </>\n ) : (\n <>\n <PlayCircle size={48} strokeWidth={1.3} style={{ color: \"var(--teal-500)\" }} />\n <span className=\"text-[12px] font-medium\" style={{ color: \"var(--color-text-secondary)\" }}>Click to watch</span>\n </>\n )}\n </button>\n <div className=\"p-4 flex items-center justify-between\">\n <div>\n <div className=\"text-[13px] font-semibold\">{title}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>{duration} · auto-marks when finished</div>\n </div>\n </div>\n </div>\n );\n}\n\nfunction AckCard({ icon: Icon, title, body, checked, onChange, readLink }: { icon: typeof ShieldCheck; title: string; body: string; checked: boolean; onChange: (v: boolean) => void; readLink?: boolean }) {\n return (\n <div className=\"rounded-xl border p-4\" style={{ borderColor: checked ? \"var(--color-border-brand)\" : \"var(--color-border-default)\", background: checked ? \"var(--color-bg-brand-muted)\" : \"transparent\" }}>\n <div className=\"flex items-start gap-3\">\n <Icon size={18} strokeWidth={1.7} className=\"shrink-0 mt-0.5\" style={{ color: checked ? \"var(--color-text-brand)\" : \"var(--color-text-secondary)\" }} />\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-semibold mb-1\">{title}</div>\n <p className=\"text-[12px] mb-3\" style={{ color: \"var(--color-text-secondary)\" }}>{body}</p>\n <div className=\"flex items-center justify-between\">\n <Checkbox id={`ack-${title}`} checked={checked} onChange={onChange}>\n I acknowledge\n </Checkbox>\n {readLink && (\n <a href=\"#\" className=\"text-[12px] font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>\n Read the playbook →\n </a>\n )}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"
|
|
66
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
+
"file": "app/(auth)/onboarding/profile/page.tsx",
|
|
4
|
+
"sha256": "5c78d6fd41776263444d3eca9a0c576d0a011edb213adbdc46a0b7cb147b2cc1",
|
|
5
|
+
"imports": [
|
|
6
|
+
"react",
|
|
7
|
+
"next/navigation",
|
|
8
|
+
"lucide-react",
|
|
9
|
+
"@/components/ui/input",
|
|
10
|
+
"@/components/ui/textarea",
|
|
11
|
+
"@/components/auth/onboarding-shell",
|
|
12
|
+
"@/components/auth/auth-primitives",
|
|
13
|
+
"@/components/auth/onboarding-primitives"
|
|
14
|
+
],
|
|
15
|
+
"tokens": [
|
|
16
|
+
"--color-bg-subtle",
|
|
17
|
+
"--color-border-strong",
|
|
18
|
+
"--color-status-error-text",
|
|
19
|
+
"--color-text-muted",
|
|
20
|
+
"--teal-500"
|
|
21
|
+
],
|
|
22
|
+
"fragments": [
|
|
23
|
+
{
|
|
24
|
+
"id": "OnboardingProfilePage",
|
|
25
|
+
"line": 32,
|
|
26
|
+
"code": "export default function OnboardingProfilePage() {\n const search = useSearchParams();\n const router = useRouter();\n const role = (search.get(\"role\") as Role | null) ?? \"trainee\";\n\n if (role === \"sponsor\") return <SponsorProfile router={router} />;\n if (role === \"mentor\") return <MentorProfile router={router} />;\n return <TraineeProfile router={router} />;\n}"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "PhotoUpload",
|
|
30
|
+
"line": 44,
|
|
31
|
+
"code": "function PhotoUpload({ photo, onPhoto, required }: { photo: string | null; onPhoto: (v: string | null) => void; required?: boolean }) {\n const inputRef = useRef<HTMLInputElement>(null);\n return (\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => inputRef.current?.click()}\n className=\"w-16 h-16 rounded-full flex items-center justify-center border-2 border-dashed shrink-0 transition-colors hover:bg-[var(--color-bg-subtle)] focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30\"\n style={{\n borderColor: photo ? \"transparent\" : \"var(--color-border-strong)\",\n background: photo ? `url(${photo}) center/cover` : \"transparent\",\n color: \"var(--color-text-muted)\",\n }}\n aria-label=\"Upload profile photo\"\n >\n {!photo && <Camera size={18} strokeWidth={1.6} />}\n </button>\n <div className=\"flex-1\">\n <div className=\"text-[13px] font-medium mb-0.5\">Profile photo {required && <span style={{ color: \"var(--color-status-error-text)\" }}>*</span>}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>\n {required ? \"Required — visible on the mentor directory.\" : \"Optional — JPG or PNG, max 2MB.\"}\n </div>\n </div>\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/png,image/jpeg\"\n className=\"sr-only\"\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) onPhoto(URL.createObjectURL(f));\n }}\n />\n </div>\n );\n}"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "TraineeProfile",
|
|
35
|
+
"line": 83,
|
|
36
|
+
"code": "function TraineeProfile({ router }: { router: ReturnType<typeof useRouter> }) {\n const [firstName, setFirstName] = useState(\"\");\n const [lastName, setLastName] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [photo, setPhoto] = useState<string | null>(null);\n const [languages, setLanguages] = useState<string[]>([]);\n const [region, setRegion] = useState(\"\");\n const [tz, setTz] = useState(\"\");\n const [saving, setSaving] = useState(false);\n\n const canContinue = !!(firstName.length >= 1 && lastName.length >= 1 && /.+@.+\\..+/.test(email) && languages.length >= 1 && region && tz);\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/credentials?role=trainee\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"trainee\" stepKey=\"profile\"\n title=\"Your profile\"\n subtitle=\"The basics. Takes under a minute.\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n >\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"First name\" htmlFor=\"fn\"><Input id=\"fn\" value={firstName} onChange={(e) => setFirstName(e.target.value)} className=\"h-10\" /></Field>\n <Field label=\"Last name\" htmlFor=\"ln\"><Input id=\"ln\" value={lastName} onChange={(e) => setLastName(e.target.value)} className=\"h-10\" /></Field>\n </div>\n <Field label=\"Email address\" htmlFor=\"em\" hint=\"We'll send a verification link to confirm.\">\n <Input id=\"em\" type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} placeholder=\"you@hospital.com\" className=\"h-10\" />\n </Field>\n <PhotoUpload photo={photo} onPhoto={setPhoto} />\n <Field label=\"Languages\" htmlFor=\"lang\" hint=\"Select all languages you can train in.\">\n <MultiSelect value={languages} onChange={setLanguages} options={LANGUAGES} />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Region\" htmlFor=\"region\"><Select id=\"region\" value={region} onChange={setRegion} options={REGIONS} placeholder=\"Select region\" /></Field>\n <Field label=\"Time zone\" htmlFor=\"tz\"><Select id=\"tz\" value={tz} onChange={setTz} options={TIMEZONES} placeholder=\"Select time zone\" /></Field>\n </div>\n </OnboardingShell>\n );\n}"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "MentorProfile",
|
|
40
|
+
"line": 128,
|
|
41
|
+
"code": "function MentorProfile({ router }: { router: ReturnType<typeof useRouter> }) {\n const [firstName, setFirstName] = useState(\"\");\n const [lastName, setLastName] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [photo, setPhoto] = useState<string | null>(null);\n const [bio, setBio] = useState(\"\");\n const [languages, setLanguages] = useState<string[]>([]);\n const [region, setRegion] = useState(\"\");\n const [tz, setTz] = useState(\"\");\n const [availability, setAvailability] = useState<string[]>([]);\n const [saving, setSaving] = useState(false);\n\n const canContinue = !!(\n firstName.length >= 1 && lastName.length >= 1 && /.+@.+\\..+/.test(email) &&\n !!photo && bio.length >= 30 && languages.length >= 1 && region && tz && availability.length >= 1\n );\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/credentials?role=mentor\"), 600);\n };\n\n const slots = [\"Mon AM\", \"Mon PM\", \"Tue AM\", \"Tue PM\", \"Wed AM\", \"Wed PM\", \"Thu AM\", \"Thu PM\", \"Fri AM\", \"Fri PM\"];\n\n return (\n <OnboardingShell\n role=\"mentor\" stepKey=\"profile\"\n title=\"Your mentor profile\"\n subtitle=\"This is what trainees see in the mentor directory.\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n >\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"First name\" htmlFor=\"fn\"><Input id=\"fn\" value={firstName} onChange={(e) => setFirstName(e.target.value)} className=\"h-10\" /></Field>\n <Field label=\"Last name\" htmlFor=\"ln\"><Input id=\"ln\" value={lastName} onChange={(e) => setLastName(e.target.value)} className=\"h-10\" /></Field>\n </div>\n <Field label=\"Email address\" htmlFor=\"em\">\n <Input id=\"em\" type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} placeholder=\"you@hospital.com\" className=\"h-10\" />\n </Field>\n <PhotoUpload photo={photo} onPhoto={setPhoto} required />\n <Field label=\"Short bio\" htmlFor=\"bio\" hint=\"2–3 sentences. Shown on your directory card.\">\n <Textarea id=\"bio\" value={bio} onChange={(e) => setBio(e.target.value)} rows={4} placeholder=\"Consultant interventional endoscopist with 15+ years of experience in ESD and POEM…\" />\n </Field>\n <Field label=\"Languages\" htmlFor=\"lang\">\n <MultiSelect value={languages} onChange={setLanguages} options={LANGUAGES} />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Region\" htmlFor=\"region\"><Select id=\"region\" value={region} onChange={setRegion} options={REGIONS} placeholder=\"Select region\" /></Field>\n <Field label=\"Time zone\" htmlFor=\"tz\"><Select id=\"tz\" value={tz} onChange={setTz} options={TIMEZONES} placeholder=\"Select time zone\" /></Field>\n </div>\n <Field label=\"Availability\" htmlFor=\"avail\" hint=\"Tap blocks when you can mentor — refine later in your calendar.\">\n <MultiSelect value={availability} onChange={setAvailability} options={slots} />\n </Field>\n </OnboardingShell>\n );\n}"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "SponsorProfile",
|
|
45
|
+
"line": 186,
|
|
46
|
+
"code": "function SponsorProfile({ router }: { router: ReturnType<typeof useRouter> }) {\n const [companyName, setCompanyName] = useState(\"\");\n const [contactName, setContactName] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [website, setWebsite] = useState(\"\");\n const [region, setRegion] = useState(\"\");\n const [languages, setLanguages] = useState<string[]>([]);\n const [tz, setTz] = useState(\"\");\n const [saving, setSaving] = useState(false);\n\n const canContinue = !!(companyName && contactName && /.+@.+\\..+/.test(email) && region && languages.length >= 1 && tz);\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/credentials?role=sponsor\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"sponsor\" stepKey=\"profile\"\n title=\"Your organisation\"\n subtitle=\"Quick info about the company sponsoring training.\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n >\n <Field label=\"Company name\" htmlFor=\"cn\"><Input id=\"cn\" value={companyName} onChange={(e) => setCompanyName(e.target.value)} className=\"h-10\" placeholder=\"Olympus Medical UK\" /></Field>\n <Field label=\"Contact name\" htmlFor=\"ctn\"><Input id=\"ctn\" value={contactName} onChange={(e) => setContactName(e.target.value)} className=\"h-10\" placeholder=\"Sarah Whitfield\" /></Field>\n <Field label=\"Email address\" htmlFor=\"em\"><Input id=\"em\" type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} className=\"h-10\" placeholder=\"you@company.com\" /></Field>\n <Field label=\"Company website\" htmlFor=\"ws\" optional><Input id=\"ws\" type=\"url\" value={website} onChange={(e) => setWebsite(e.target.value)} className=\"h-10\" placeholder=\"https://company.com\" /></Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Region\" htmlFor=\"region\"><Select id=\"region\" value={region} onChange={setRegion} options={REGIONS} placeholder=\"Select region\" /></Field>\n <Field label=\"Time zone\" htmlFor=\"tz\"><Select id=\"tz\" value={tz} onChange={setTz} options={TIMEZONES} placeholder=\"Select time zone\" /></Field>\n </div>\n <Field label=\"Languages\" htmlFor=\"lang\">\n <MultiSelect value={languages} onChange={setLanguages} options={LANGUAGES} />\n </Field>\n </OnboardingShell>\n );\n}"
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"content": "\"use client\";\n\nimport { useState, useRef } from \"react\";\nimport { useSearchParams, useRouter } from \"next/navigation\";\nimport { Camera } from \"lucide-react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { OnboardingShell, Role } from \"@/components/auth/onboarding-shell\";\nimport { Field } from \"@/components/auth/auth-primitives\";\nimport { Select, MultiSelect } from \"@/components/auth/onboarding-primitives\";\n\nconst LANGUAGES = [\"English\", \"Français\", \"Español\", \"Deutsch\", \"Italiano\", \"中文\", \"日本語\", \"العربية\"];\nconst TIMEZONES = [\n { value: \"Europe/London\", label: \"London (GMT/BST)\" },\n { value: \"Europe/Dublin\", label: \"Dublin (GMT/IST)\" },\n { value: \"Europe/Berlin\", label: \"Berlin (CET)\" },\n { value: \"America/New_York\", label: \"New York (ET)\" },\n { value: \"America/Los_Angeles\", label: \"Los Angeles (PT)\" },\n { value: \"Asia/Tokyo\", label: \"Tokyo (JST)\" },\n { value: \"Asia/Singapore\", label: \"Singapore (SGT)\" },\n { value: \"Australia/Sydney\", label: \"Sydney (AEDT)\" },\n];\nconst REGIONS = [\n { value: \"UK\", label: \"United Kingdom\" },\n { value: \"EU\", label: \"European Union\" },\n { value: \"US\", label: \"United States\" },\n { value: \"APAC\", label: \"Asia-Pacific\" },\n { value: \"ME\", label: \"Middle East\" },\n { value: \"OTHER\", label: \"Other\" },\n];\n\nexport default function OnboardingProfilePage() {\n const search = useSearchParams();\n const router = useRouter();\n const role = (search.get(\"role\") as Role | null) ?? \"trainee\";\n\n if (role === \"sponsor\") return <SponsorProfile router={router} />;\n if (role === \"mentor\") return <MentorProfile router={router} />;\n return <TraineeProfile router={router} />;\n}\n\n/* ─── Common photo uploader ──────────────────────────────────────────────── */\n\nfunction PhotoUpload({ photo, onPhoto, required }: { photo: string | null; onPhoto: (v: string | null) => void; required?: boolean }) {\n const inputRef = useRef<HTMLInputElement>(null);\n return (\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => inputRef.current?.click()}\n className=\"w-16 h-16 rounded-full flex items-center justify-center border-2 border-dashed shrink-0 transition-colors hover:bg-[var(--color-bg-subtle)] focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30\"\n style={{\n borderColor: photo ? \"transparent\" : \"var(--color-border-strong)\",\n background: photo ? `url(${photo}) center/cover` : \"transparent\",\n color: \"var(--color-text-muted)\",\n }}\n aria-label=\"Upload profile photo\"\n >\n {!photo && <Camera size={18} strokeWidth={1.6} />}\n </button>\n <div className=\"flex-1\">\n <div className=\"text-[13px] font-medium mb-0.5\">Profile photo {required && <span style={{ color: \"var(--color-status-error-text)\" }}>*</span>}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>\n {required ? \"Required — visible on the mentor directory.\" : \"Optional — JPG or PNG, max 2MB.\"}\n </div>\n </div>\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/png,image/jpeg\"\n className=\"sr-only\"\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) onPhoto(URL.createObjectURL(f));\n }}\n />\n </div>\n );\n}\n\n/* ─── TRAINEE Profile ───────────────────────────────────────────────────── */\n\nfunction TraineeProfile({ router }: { router: ReturnType<typeof useRouter> }) {\n const [firstName, setFirstName] = useState(\"\");\n const [lastName, setLastName] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [photo, setPhoto] = useState<string | null>(null);\n const [languages, setLanguages] = useState<string[]>([]);\n const [region, setRegion] = useState(\"\");\n const [tz, setTz] = useState(\"\");\n const [saving, setSaving] = useState(false);\n\n const canContinue = !!(firstName.length >= 1 && lastName.length >= 1 && /.+@.+\\..+/.test(email) && languages.length >= 1 && region && tz);\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/credentials?role=trainee\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"trainee\" stepKey=\"profile\"\n title=\"Your profile\"\n subtitle=\"The basics. Takes under a minute.\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n >\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"First name\" htmlFor=\"fn\"><Input id=\"fn\" value={firstName} onChange={(e) => setFirstName(e.target.value)} className=\"h-10\" /></Field>\n <Field label=\"Last name\" htmlFor=\"ln\"><Input id=\"ln\" value={lastName} onChange={(e) => setLastName(e.target.value)} className=\"h-10\" /></Field>\n </div>\n <Field label=\"Email address\" htmlFor=\"em\" hint=\"We'll send a verification link to confirm.\">\n <Input id=\"em\" type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} placeholder=\"you@hospital.com\" className=\"h-10\" />\n </Field>\n <PhotoUpload photo={photo} onPhoto={setPhoto} />\n <Field label=\"Languages\" htmlFor=\"lang\" hint=\"Select all languages you can train in.\">\n <MultiSelect value={languages} onChange={setLanguages} options={LANGUAGES} />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Region\" htmlFor=\"region\"><Select id=\"region\" value={region} onChange={setRegion} options={REGIONS} placeholder=\"Select region\" /></Field>\n <Field label=\"Time zone\" htmlFor=\"tz\"><Select id=\"tz\" value={tz} onChange={setTz} options={TIMEZONES} placeholder=\"Select time zone\" /></Field>\n </div>\n </OnboardingShell>\n );\n}\n\n/* ─── MENTOR Profile ───────────────────────────────────────────────────── */\n\nfunction MentorProfile({ router }: { router: ReturnType<typeof useRouter> }) {\n const [firstName, setFirstName] = useState(\"\");\n const [lastName, setLastName] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [photo, setPhoto] = useState<string | null>(null);\n const [bio, setBio] = useState(\"\");\n const [languages, setLanguages] = useState<string[]>([]);\n const [region, setRegion] = useState(\"\");\n const [tz, setTz] = useState(\"\");\n const [availability, setAvailability] = useState<string[]>([]);\n const [saving, setSaving] = useState(false);\n\n const canContinue = !!(\n firstName.length >= 1 && lastName.length >= 1 && /.+@.+\\..+/.test(email) &&\n !!photo && bio.length >= 30 && languages.length >= 1 && region && tz && availability.length >= 1\n );\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/credentials?role=mentor\"), 600);\n };\n\n const slots = [\"Mon AM\", \"Mon PM\", \"Tue AM\", \"Tue PM\", \"Wed AM\", \"Wed PM\", \"Thu AM\", \"Thu PM\", \"Fri AM\", \"Fri PM\"];\n\n return (\n <OnboardingShell\n role=\"mentor\" stepKey=\"profile\"\n title=\"Your mentor profile\"\n subtitle=\"This is what trainees see in the mentor directory.\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n >\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"First name\" htmlFor=\"fn\"><Input id=\"fn\" value={firstName} onChange={(e) => setFirstName(e.target.value)} className=\"h-10\" /></Field>\n <Field label=\"Last name\" htmlFor=\"ln\"><Input id=\"ln\" value={lastName} onChange={(e) => setLastName(e.target.value)} className=\"h-10\" /></Field>\n </div>\n <Field label=\"Email address\" htmlFor=\"em\">\n <Input id=\"em\" type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} placeholder=\"you@hospital.com\" className=\"h-10\" />\n </Field>\n <PhotoUpload photo={photo} onPhoto={setPhoto} required />\n <Field label=\"Short bio\" htmlFor=\"bio\" hint=\"2–3 sentences. Shown on your directory card.\">\n <Textarea id=\"bio\" value={bio} onChange={(e) => setBio(e.target.value)} rows={4} placeholder=\"Consultant interventional endoscopist with 15+ years of experience in ESD and POEM…\" />\n </Field>\n <Field label=\"Languages\" htmlFor=\"lang\">\n <MultiSelect value={languages} onChange={setLanguages} options={LANGUAGES} />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Region\" htmlFor=\"region\"><Select id=\"region\" value={region} onChange={setRegion} options={REGIONS} placeholder=\"Select region\" /></Field>\n <Field label=\"Time zone\" htmlFor=\"tz\"><Select id=\"tz\" value={tz} onChange={setTz} options={TIMEZONES} placeholder=\"Select time zone\" /></Field>\n </div>\n <Field label=\"Availability\" htmlFor=\"avail\" hint=\"Tap blocks when you can mentor — refine later in your calendar.\">\n <MultiSelect value={availability} onChange={setAvailability} options={slots} />\n </Field>\n </OnboardingShell>\n );\n}\n\n/* ─── SPONSOR Profile ──────────────────────────────────────────────────── */\n\nfunction SponsorProfile({ router }: { router: ReturnType<typeof useRouter> }) {\n const [companyName, setCompanyName] = useState(\"\");\n const [contactName, setContactName] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [website, setWebsite] = useState(\"\");\n const [region, setRegion] = useState(\"\");\n const [languages, setLanguages] = useState<string[]>([]);\n const [tz, setTz] = useState(\"\");\n const [saving, setSaving] = useState(false);\n\n const canContinue = !!(companyName && contactName && /.+@.+\\..+/.test(email) && region && languages.length >= 1 && tz);\n\n const handleSave = () => {\n setSaving(true);\n setTimeout(() => router.push(\"/onboarding/credentials?role=sponsor\"), 600);\n };\n\n return (\n <OnboardingShell\n role=\"sponsor\" stepKey=\"profile\"\n title=\"Your organisation\"\n subtitle=\"Quick info about the company sponsoring training.\"\n onSave={handleSave} saving={saving} canContinue={canContinue}\n >\n <Field label=\"Company name\" htmlFor=\"cn\"><Input id=\"cn\" value={companyName} onChange={(e) => setCompanyName(e.target.value)} className=\"h-10\" placeholder=\"Olympus Medical UK\" /></Field>\n <Field label=\"Contact name\" htmlFor=\"ctn\"><Input id=\"ctn\" value={contactName} onChange={(e) => setContactName(e.target.value)} className=\"h-10\" placeholder=\"Sarah Whitfield\" /></Field>\n <Field label=\"Email address\" htmlFor=\"em\"><Input id=\"em\" type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} className=\"h-10\" placeholder=\"you@company.com\" /></Field>\n <Field label=\"Company website\" htmlFor=\"ws\" optional><Input id=\"ws\" type=\"url\" value={website} onChange={(e) => setWebsite(e.target.value)} className=\"h-10\" placeholder=\"https://company.com\" /></Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Region\" htmlFor=\"region\"><Select id=\"region\" value={region} onChange={setRegion} options={REGIONS} placeholder=\"Select region\" /></Field>\n <Field label=\"Time zone\" htmlFor=\"tz\"><Select id=\"tz\" value={tz} onChange={setTz} options={TIMEZONES} placeholder=\"Select time zone\" /></Field>\n </div>\n <Field label=\"Languages\" htmlFor=\"lang\">\n <MultiSelect value={languages} onChange={setLanguages} options={LANGUAGES} />\n </Field>\n </OnboardingShell>\n );\n}\n"
|
|
50
|
+
}
|