@utopia-studio-design/design-system-cli 0.7.2 → 0.8.0
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-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 +192 -4
- package/package.json +2 -2
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
+
"file": "app/portal/trainee/credits/page.tsx",
|
|
4
|
+
"sha256": "f97559e0361ae4b352458d23d11436bf54dcabc8810fac2fa4c02ddc167cc42f",
|
|
5
|
+
"imports": [
|
|
6
|
+
"next/link",
|
|
7
|
+
"lucide-react",
|
|
8
|
+
"@/components/ui/button",
|
|
9
|
+
"@/components/ui/card",
|
|
10
|
+
"@/components/ui/separator",
|
|
11
|
+
"@/components/ui/table",
|
|
12
|
+
"@/components/ui/badge",
|
|
13
|
+
"@/components/portal/portal-shell",
|
|
14
|
+
"@/components/portal/empty-state",
|
|
15
|
+
"@/lib/credits-mock"
|
|
16
|
+
],
|
|
17
|
+
"tokens": [
|
|
18
|
+
"--color-bg-brand-muted",
|
|
19
|
+
"--color-bg-subtle",
|
|
20
|
+
"--color-status-error-text",
|
|
21
|
+
"--color-status-success-text",
|
|
22
|
+
"--color-text-brand",
|
|
23
|
+
"--color-text-muted",
|
|
24
|
+
"--color-text-primary",
|
|
25
|
+
"--color-text-secondary"
|
|
26
|
+
],
|
|
27
|
+
"fragments": [
|
|
28
|
+
{
|
|
29
|
+
"id": "TraineeCreditsPage",
|
|
30
|
+
"line": 24,
|
|
31
|
+
"code": "export default function TraineeCreditsPage() {\n const ledger = TRAINEE_LEDGERS[\"app-105\"];\n\n return (\n <PortalShell kind=\"trainee\">\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Credits\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Sponsored by <strong style={{ color: \"var(--color-text-primary)\" }}>{ledger.sponsorName}</strong> · expires {ledger.expiresAt}\n </p>\n </div>\n <Button variant=\"outline\">Request more credits</Button>\n </div>\n\n {/* Balance */}\n <Card className=\"mb-5\">\n <CardHeader>\n <div className=\"flex items-center gap-3\">\n <div className=\"w-11 h-11 rounded-xl flex items-center justify-center\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Wallet size={20} strokeWidth={1.6} />\n </div>\n <div>\n <CardTitle className=\"text-base mb-0.5\">Credit balance</CardTitle>\n <CardDescription>{ledger.totalAllocated} total allocated</CardDescription>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-4\">\n <Block label=\"Available\" value={ledger.available} accent=\"brand\" />\n <Block label=\"In escrow\" value={ledger.escrowed} />\n <Block label=\"Consumed\" value={ledger.consumed} />\n </div>\n </CardContent>\n </Card>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-5\">\n <Card className=\"p-0\">\n <CardHeader className=\"px-5 py-4\">\n <CardTitle className=\"text-base\">Allocation history</CardTitle>\n <CardDescription>Credits granted to you by your sponsor.</CardDescription>\n </CardHeader>\n {ALLOCATION_HISTORY.length === 0 ? (\n <div className=\"p-5\">\n <EmptyState icon={Plus} title=\"No allocations yet\" description=\"Your sponsor will allocate credits as they become available.\" />\n </div>\n ) : (\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Date</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Sponsor</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Credits</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {ALLOCATION_HISTORY.map((a) => (\n <TableRow key={a.id}>\n <TableCell className=\"px-5 py-2.5 text-[12px]\">{a.at}</TableCell>\n <TableCell className=\"px-5 py-2.5 text-[12px]\">{a.sponsor}</TableCell>\n <TableCell className=\"px-5 py-2.5 text-right text-[12px] tabular-nums font-medium\" style={{ color: \"var(--color-status-success-text)\" }}>+{a.credits}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n )}\n </Card>\n\n <Card className=\"p-0\">\n <CardHeader className=\"px-5 py-4\">\n <CardTitle className=\"text-base\">Consumption history</CardTitle>\n <CardDescription>Closed sessions where credits were consumed.</CardDescription>\n </CardHeader>\n {CONSUMPTION_HISTORY.length === 0 ? (\n <div className=\"p-5\">\n <EmptyState icon={Wallet} title=\"No consumed credits\" description=\"When you complete sessions, their credit cost will appear here.\" />\n </div>\n ) : (\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Date</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Procedure</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Mentor</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Credits</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {CONSUMPTION_HISTORY.map((c) => (\n <TableRow key={c.id}>\n <TableCell className=\"px-5 py-2.5 text-[12px]\">{c.date}</TableCell>\n <TableCell className=\"px-5 py-2.5\"><Badge variant=\"outline\" className=\"text-[10px]\">{c.procedure}</Badge></TableCell>\n <TableCell className=\"px-5 py-2.5 text-[12px]\">{c.mentor}</TableCell>\n <TableCell className=\"px-5 py-2.5 text-right text-[12px] tabular-nums font-medium\" style={{ color: \"var(--color-status-error-text)\" }}>−{c.credits}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n )}\n </Card>\n </div>\n </PortalShell>\n );\n}"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "Block",
|
|
35
|
+
"line": 132,
|
|
36
|
+
"code": "function Block({ label, value, accent }: { label: string; value: number; accent?: \"brand\" }) {\n return (\n <div>\n <div className=\"text-[10px] font-semibold uppercase tracking-wide mb-1\" style={{ color: \"var(--color-text-muted)\" }}>{label}</div>\n <div className=\"text-3xl font-semibold tabular-nums\" style={{ letterSpacing: \"-0.02em\", color: accent === \"brand\" ? \"var(--color-text-brand)\" : \"var(--color-text-primary)\" }}>{value}</div>\n </div>\n );\n}"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { Wallet, ArrowRight, Plus } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardHeader, CardTitle, CardContent, CardDescription } from \"@/components/ui/card\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from \"@/components/ui/table\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { EmptyState } from \"@/components/portal/empty-state\";\nimport { TRAINEE_LEDGERS } from \"@/lib/credits-mock\";\n\nconst ALLOCATION_HISTORY = [\n { id: \"alloc-1\", at: \"2026-04-15\", sponsor: \"Olympus Medical UK\", credits: 5, by: \"Sarah K.\" },\n { id: \"alloc-2\", at: \"2026-02-10\", sponsor: \"Olympus Medical UK\", credits: 3, by: \"Sarah K.\" },\n];\n\nconst CONSUMPTION_HISTORY = [\n { id: \"ses-2401\", date: \"2026-05-22\", procedure: \"ESD\", mentor: \"Prof. Amyn Haji\", credits: 1 },\n { id: \"ses-2389\", date: \"2026-05-08\", procedure: \"ESD\", mentor: \"Prof. Amyn Haji\", credits: 1 },\n];\n\nexport default function TraineeCreditsPage() {\n const ledger = TRAINEE_LEDGERS[\"app-105\"];\n\n return (\n <PortalShell kind=\"trainee\">\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Credits\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Sponsored by <strong style={{ color: \"var(--color-text-primary)\" }}>{ledger.sponsorName}</strong> · expires {ledger.expiresAt}\n </p>\n </div>\n <Button variant=\"outline\">Request more credits</Button>\n </div>\n\n {/* Balance */}\n <Card className=\"mb-5\">\n <CardHeader>\n <div className=\"flex items-center gap-3\">\n <div className=\"w-11 h-11 rounded-xl flex items-center justify-center\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Wallet size={20} strokeWidth={1.6} />\n </div>\n <div>\n <CardTitle className=\"text-base mb-0.5\">Credit balance</CardTitle>\n <CardDescription>{ledger.totalAllocated} total allocated</CardDescription>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-4\">\n <Block label=\"Available\" value={ledger.available} accent=\"brand\" />\n <Block label=\"In escrow\" value={ledger.escrowed} />\n <Block label=\"Consumed\" value={ledger.consumed} />\n </div>\n </CardContent>\n </Card>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-5\">\n <Card className=\"p-0\">\n <CardHeader className=\"px-5 py-4\">\n <CardTitle className=\"text-base\">Allocation history</CardTitle>\n <CardDescription>Credits granted to you by your sponsor.</CardDescription>\n </CardHeader>\n {ALLOCATION_HISTORY.length === 0 ? (\n <div className=\"p-5\">\n <EmptyState icon={Plus} title=\"No allocations yet\" description=\"Your sponsor will allocate credits as they become available.\" />\n </div>\n ) : (\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Date</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Sponsor</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Credits</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {ALLOCATION_HISTORY.map((a) => (\n <TableRow key={a.id}>\n <TableCell className=\"px-5 py-2.5 text-[12px]\">{a.at}</TableCell>\n <TableCell className=\"px-5 py-2.5 text-[12px]\">{a.sponsor}</TableCell>\n <TableCell className=\"px-5 py-2.5 text-right text-[12px] tabular-nums font-medium\" style={{ color: \"var(--color-status-success-text)\" }}>+{a.credits}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n )}\n </Card>\n\n <Card className=\"p-0\">\n <CardHeader className=\"px-5 py-4\">\n <CardTitle className=\"text-base\">Consumption history</CardTitle>\n <CardDescription>Closed sessions where credits were consumed.</CardDescription>\n </CardHeader>\n {CONSUMPTION_HISTORY.length === 0 ? (\n <div className=\"p-5\">\n <EmptyState icon={Wallet} title=\"No consumed credits\" description=\"When you complete sessions, their credit cost will appear here.\" />\n </div>\n ) : (\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Date</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Procedure</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Mentor</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Credits</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {CONSUMPTION_HISTORY.map((c) => (\n <TableRow key={c.id}>\n <TableCell className=\"px-5 py-2.5 text-[12px]\">{c.date}</TableCell>\n <TableCell className=\"px-5 py-2.5\"><Badge variant=\"outline\" className=\"text-[10px]\">{c.procedure}</Badge></TableCell>\n <TableCell className=\"px-5 py-2.5 text-[12px]\">{c.mentor}</TableCell>\n <TableCell className=\"px-5 py-2.5 text-right text-[12px] tabular-nums font-medium\" style={{ color: \"var(--color-status-error-text)\" }}>−{c.credits}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n )}\n </Card>\n </div>\n </PortalShell>\n );\n}\n\nfunction Block({ label, value, accent }: { label: string; value: number; accent?: \"brand\" }) {\n return (\n <div>\n <div className=\"text-[10px] font-semibold uppercase tracking-wide mb-1\" style={{ color: \"var(--color-text-muted)\" }}>{label}</div>\n <div className=\"text-3xl font-semibold tabular-nums\" style={{ letterSpacing: \"-0.02em\", color: accent === \"brand\" ? \"var(--color-text-brand)\" : \"var(--color-text-primary)\" }}>{value}</div>\n </div>\n );\n}\n"
|
|
40
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
+
"file": "app/portal/trainee/mentors/page.tsx",
|
|
4
|
+
"sha256": "6b12fd2ceab7e8af4980b59e8639e682f6287c5f70f5f8358c2ae058f6c00b96",
|
|
5
|
+
"imports": [
|
|
6
|
+
"next/link",
|
|
7
|
+
"react",
|
|
8
|
+
"lucide-react",
|
|
9
|
+
"@/components/ui/input",
|
|
10
|
+
"@/components/ui/button",
|
|
11
|
+
"@/components/ui/badge",
|
|
12
|
+
"@/components/ui/card",
|
|
13
|
+
"@/components/portal/portal-shell",
|
|
14
|
+
"@/lib/sessions-mock"
|
|
15
|
+
],
|
|
16
|
+
"tokens": [
|
|
17
|
+
"--amber-500",
|
|
18
|
+
"--color-bg-subtle",
|
|
19
|
+
"--color-border-default",
|
|
20
|
+
"--color-status-success-bg",
|
|
21
|
+
"--color-status-success-text",
|
|
22
|
+
"--color-status-warning-bg",
|
|
23
|
+
"--color-status-warning-text",
|
|
24
|
+
"--color-text-muted",
|
|
25
|
+
"--color-text-primary",
|
|
26
|
+
"--color-text-secondary",
|
|
27
|
+
"--gray-950",
|
|
28
|
+
"--teal-500"
|
|
29
|
+
],
|
|
30
|
+
"fragments": [
|
|
31
|
+
{
|
|
32
|
+
"id": "MentorDirectoryPage",
|
|
33
|
+
"line": 19,
|
|
34
|
+
"code": "export default function MentorDirectoryPage() {\n const [q, setQ] = useState(\"\");\n const [procFilter, setProcFilter] = useState<string>(\"all\");\n const [availFilter, setAvailFilter] = useState<string>(\"all\");\n const [sort, setSort] = useState<\"rating\" | \"sessions\" | \"name\">(\"rating\");\n\n const filtered = useMemo(() => {\n let items = [...MENTORS];\n if (procFilter !== \"all\") items = items.filter((m) => m.procedures.includes(procFilter));\n if (availFilter !== \"all\") items = items.filter((m) => m.availability === availFilter);\n if (q) {\n const n = q.toLowerCase();\n items = items.filter((m) =>\n m.name.toLowerCase().includes(n) ||\n m.institution.toLowerCase().includes(n) ||\n m.expertise.join(\" \").toLowerCase().includes(n)\n );\n }\n if (sort === \"rating\") items.sort((a, b) => b.averageRating - a.averageRating);\n if (sort === \"sessions\") items.sort((a, b) => b.sessionsDelivered - a.sessionsDelivered);\n if (sort === \"name\") items.sort((a, b) => a.name.localeCompare(b.name));\n return items;\n }, [q, procFilter, availFilter, sort]);\n\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Hannah Liu\", subtitle: \"ST5 · Gastroenterology\", initials: \"HL\" }}>\n <div className=\"mb-6\">\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Browse mentors\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n {MENTORS.length} active mentors · sorted by rating\n </p>\n </div>\n\n {/* Filters */}\n <div className=\"flex items-center gap-2 mb-5 flex-wrap\">\n <div className=\"relative flex-1 max-w-md min-w-[200px]\">\n <Search size={14} className=\"absolute left-3 top-1/2 -translate-y-1/2\" style={{ color: \"var(--color-text-muted)\" }} />\n <Input value={q} onChange={(e) => setQ(e.target.value)} placeholder=\"Search mentors, institutions, procedures…\" className=\"h-9 pl-9\" />\n {q && (\n <button onClick={() => setQ(\"\")} className=\"absolute right-3 top-1/2 -translate-y-1/2 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-muted)\" }}>\n <X size={13} />\n </button>\n )}\n </div>\n <FilterSelect label=\"Procedure\" value={procFilter} onChange={setProcFilter} options={[{ value: \"all\", label: \"All procedures\" }, ...PROCEDURES.map((p) => ({ value: p, label: p }))]} />\n <FilterSelect label=\"Availability\" value={availFilter} onChange={setAvailFilter} options={[\n { value: \"all\", label: \"All\" }, { value: \"available\", label: \"Available\" }, { value: \"limited\", label: \"Limited\" },\n ]} />\n <FilterSelect label=\"Sort\" value={sort} onChange={(v) => setSort(v as typeof sort)} options={[\n { value: \"rating\", label: \"Highest rated\" }, { value: \"sessions\", label: \"Most sessions\" }, { value: \"name\", label: \"A–Z\" },\n ]} />\n </div>\n\n {/* Grid */}\n {filtered.length === 0 ? (\n <Card className=\"p-12 text-center\">\n <p className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n No mentors match these filters.\n </p>\n </Card>\n ) : (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3\">\n {filtered.map((m) => (\n <MentorCard key={m.id} mentor={m} />\n ))}\n </div>\n )}\n </PortalShell>\n );\n}"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "MentorCard",
|
|
38
|
+
"line": 92,
|
|
39
|
+
"code": "function MentorCard({ mentor }: { mentor: typeof MENTORS[number] }) {\n const avail = AVAIL_LABEL[mentor.availability];\n return (\n <Card>\n <CardHeader className=\"pb-3\">\n <div className=\"flex items-start justify-between gap-3 mb-2\">\n <div className=\"flex items-start gap-3 min-w-0\">\n <div className=\"w-11 h-11 rounded-full flex items-center justify-center text-[12px] font-semibold shrink-0\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>\n {mentor.initials}\n </div>\n <div className=\"min-w-0\">\n <CardTitle className=\"text-[14px] mb-0.5 truncate\">{mentor.name}</CardTitle>\n <p className=\"text-[11px] truncate\" style={{ color: \"var(--color-text-muted)\" }}>{mentor.title}</p>\n </div>\n </div>\n <span className=\"text-[10px] font-medium px-1.5 py-0.5 rounded-md shrink-0\" style={{ background: avail.bg, color: avail.color }}>\n {avail.label}\n </span>\n </div>\n <div className=\"flex items-center gap-3 text-[11px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <span className=\"inline-flex items-center gap-1\">\n <Star size={11} fill=\"currentColor\" strokeWidth={0} style={{ color: \"var(--amber-500)\" }} />\n <strong className=\"tabular-nums\" style={{ color: \"var(--color-text-primary)\" }}>{mentor.averageRating.toFixed(1)}</strong>\n <span style={{ color: \"var(--color-text-muted)\" }}>({mentor.totalRatings})</span>\n </span>\n <span style={{ color: \"var(--color-text-muted)\" }}>·</span>\n <span>{mentor.sessionsDelivered} sessions</span>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-center gap-1.5 text-[11px] mb-2\" style={{ color: \"var(--color-text-secondary)\" }}>\n <MapPin size={11} /> {mentor.institution}\n </div>\n <div className=\"flex items-center gap-1.5 text-[11px] mb-3\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Languages size={11} /> {mentor.languages.join(\", \")}\n </div>\n <div className=\"flex flex-wrap gap-1 mb-4\">\n {mentor.expertise.map((p) => <Badge key={p} variant=\"outline\" className=\"text-[10px]\">{p}</Badge>)}\n </div>\n <div className=\"flex items-center gap-2\">\n <Link href={`/portal/trainee/request?mentor=${mentor.id}`} className=\"flex-1\">\n <Button variant=\"brand\" size=\"sm\" className=\"w-full\">\n Request session <ArrowRight size={11} />\n </Button>\n </Link>\n <Button variant=\"outline\" size=\"sm\">View profile</Button>\n </div>\n </CardContent>\n </Card>\n );\n}"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "FilterSelect",
|
|
43
|
+
"line": 144,
|
|
44
|
+
"code": "function FilterSelect({ label, value, onChange, options }: { label: string; value: string; onChange: (v: string) => void; options: { value: string; label: string }[] }) {\n return (\n <div className=\"relative\">\n <select\n value={value}\n onChange={(e) => onChange(e.target.value)}\n className=\"h-9 pl-3 pr-8 rounded-lg border text-[12px] bg-transparent outline-none appearance-none cursor-pointer focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30\"\n style={{ borderColor: \"var(--color-border-default)\", color: \"var(--color-text-primary)\" }}\n aria-label={label}\n >\n {options.map((o) => <option key={o.value} value={o.value}>{label}: {o.label}</option>)}\n </select>\n <ChevronDown size={13} strokeWidth={1.8} className=\"absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none\" style={{ color: \"var(--color-text-muted)\" }} />\n </div>\n );\n}"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useState, useMemo } from \"react\";\nimport { Search, X, Star, ArrowRight, Filter, ChevronDown, MapPin, Languages } from \"lucide-react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card, CardHeader, CardTitle, CardContent } from \"@/components/ui/card\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { MENTORS, PROCEDURES } from \"@/lib/sessions-mock\";\n\nconst AVAIL_LABEL = {\n available: { label: \"Available\", color: \"var(--color-status-success-text)\", bg: \"var(--color-status-success-bg)\" },\n limited: { label: \"Limited\", color: \"var(--color-status-warning-text)\", bg: \"var(--color-status-warning-bg)\" },\n unavailable: { label: \"Unavailable\", color: \"var(--color-text-muted)\", bg: \"var(--color-bg-subtle)\" },\n} as const;\n\nexport default function MentorDirectoryPage() {\n const [q, setQ] = useState(\"\");\n const [procFilter, setProcFilter] = useState<string>(\"all\");\n const [availFilter, setAvailFilter] = useState<string>(\"all\");\n const [sort, setSort] = useState<\"rating\" | \"sessions\" | \"name\">(\"rating\");\n\n const filtered = useMemo(() => {\n let items = [...MENTORS];\n if (procFilter !== \"all\") items = items.filter((m) => m.procedures.includes(procFilter));\n if (availFilter !== \"all\") items = items.filter((m) => m.availability === availFilter);\n if (q) {\n const n = q.toLowerCase();\n items = items.filter((m) =>\n m.name.toLowerCase().includes(n) ||\n m.institution.toLowerCase().includes(n) ||\n m.expertise.join(\" \").toLowerCase().includes(n)\n );\n }\n if (sort === \"rating\") items.sort((a, b) => b.averageRating - a.averageRating);\n if (sort === \"sessions\") items.sort((a, b) => b.sessionsDelivered - a.sessionsDelivered);\n if (sort === \"name\") items.sort((a, b) => a.name.localeCompare(b.name));\n return items;\n }, [q, procFilter, availFilter, sort]);\n\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Hannah Liu\", subtitle: \"ST5 · Gastroenterology\", initials: \"HL\" }}>\n <div className=\"mb-6\">\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Browse mentors\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n {MENTORS.length} active mentors · sorted by rating\n </p>\n </div>\n\n {/* Filters */}\n <div className=\"flex items-center gap-2 mb-5 flex-wrap\">\n <div className=\"relative flex-1 max-w-md min-w-[200px]\">\n <Search size={14} className=\"absolute left-3 top-1/2 -translate-y-1/2\" style={{ color: \"var(--color-text-muted)\" }} />\n <Input value={q} onChange={(e) => setQ(e.target.value)} placeholder=\"Search mentors, institutions, procedures…\" className=\"h-9 pl-9\" />\n {q && (\n <button onClick={() => setQ(\"\")} className=\"absolute right-3 top-1/2 -translate-y-1/2 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-muted)\" }}>\n <X size={13} />\n </button>\n )}\n </div>\n <FilterSelect label=\"Procedure\" value={procFilter} onChange={setProcFilter} options={[{ value: \"all\", label: \"All procedures\" }, ...PROCEDURES.map((p) => ({ value: p, label: p }))]} />\n <FilterSelect label=\"Availability\" value={availFilter} onChange={setAvailFilter} options={[\n { value: \"all\", label: \"All\" }, { value: \"available\", label: \"Available\" }, { value: \"limited\", label: \"Limited\" },\n ]} />\n <FilterSelect label=\"Sort\" value={sort} onChange={(v) => setSort(v as typeof sort)} options={[\n { value: \"rating\", label: \"Highest rated\" }, { value: \"sessions\", label: \"Most sessions\" }, { value: \"name\", label: \"A–Z\" },\n ]} />\n </div>\n\n {/* Grid */}\n {filtered.length === 0 ? (\n <Card className=\"p-12 text-center\">\n <p className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n No mentors match these filters.\n </p>\n </Card>\n ) : (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3\">\n {filtered.map((m) => (\n <MentorCard key={m.id} mentor={m} />\n ))}\n </div>\n )}\n </PortalShell>\n );\n}\n\nfunction MentorCard({ mentor }: { mentor: typeof MENTORS[number] }) {\n const avail = AVAIL_LABEL[mentor.availability];\n return (\n <Card>\n <CardHeader className=\"pb-3\">\n <div className=\"flex items-start justify-between gap-3 mb-2\">\n <div className=\"flex items-start gap-3 min-w-0\">\n <div className=\"w-11 h-11 rounded-full flex items-center justify-center text-[12px] font-semibold shrink-0\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>\n {mentor.initials}\n </div>\n <div className=\"min-w-0\">\n <CardTitle className=\"text-[14px] mb-0.5 truncate\">{mentor.name}</CardTitle>\n <p className=\"text-[11px] truncate\" style={{ color: \"var(--color-text-muted)\" }}>{mentor.title}</p>\n </div>\n </div>\n <span className=\"text-[10px] font-medium px-1.5 py-0.5 rounded-md shrink-0\" style={{ background: avail.bg, color: avail.color }}>\n {avail.label}\n </span>\n </div>\n <div className=\"flex items-center gap-3 text-[11px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <span className=\"inline-flex items-center gap-1\">\n <Star size={11} fill=\"currentColor\" strokeWidth={0} style={{ color: \"var(--amber-500)\" }} />\n <strong className=\"tabular-nums\" style={{ color: \"var(--color-text-primary)\" }}>{mentor.averageRating.toFixed(1)}</strong>\n <span style={{ color: \"var(--color-text-muted)\" }}>({mentor.totalRatings})</span>\n </span>\n <span style={{ color: \"var(--color-text-muted)\" }}>·</span>\n <span>{mentor.sessionsDelivered} sessions</span>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-center gap-1.5 text-[11px] mb-2\" style={{ color: \"var(--color-text-secondary)\" }}>\n <MapPin size={11} /> {mentor.institution}\n </div>\n <div className=\"flex items-center gap-1.5 text-[11px] mb-3\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Languages size={11} /> {mentor.languages.join(\", \")}\n </div>\n <div className=\"flex flex-wrap gap-1 mb-4\">\n {mentor.expertise.map((p) => <Badge key={p} variant=\"outline\" className=\"text-[10px]\">{p}</Badge>)}\n </div>\n <div className=\"flex items-center gap-2\">\n <Link href={`/portal/trainee/request?mentor=${mentor.id}`} className=\"flex-1\">\n <Button variant=\"brand\" size=\"sm\" className=\"w-full\">\n Request session <ArrowRight size={11} />\n </Button>\n </Link>\n <Button variant=\"outline\" size=\"sm\">View profile</Button>\n </div>\n </CardContent>\n </Card>\n );\n}\n\nfunction FilterSelect({ label, value, onChange, options }: { label: string; value: string; onChange: (v: string) => void; options: { value: string; label: string }[] }) {\n return (\n <div className=\"relative\">\n <select\n value={value}\n onChange={(e) => onChange(e.target.value)}\n className=\"h-9 pl-3 pr-8 rounded-lg border text-[12px] bg-transparent outline-none appearance-none cursor-pointer focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30\"\n style={{ borderColor: \"var(--color-border-default)\", color: \"var(--color-text-primary)\" }}\n aria-label={label}\n >\n {options.map((o) => <option key={o.value} value={o.value}>{label}: {o.label}</option>)}\n </select>\n <ChevronDown size={13} strokeWidth={1.8} className=\"absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none\" style={{ color: \"var(--color-text-muted)\" }} />\n </div>\n );\n}\n"
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
+
"file": "app/portal/trainee/messages/page.tsx",
|
|
4
|
+
"sha256": "af07cb6491725fcd4f43c865b0f6ac32886219e8e45c849141dd75af03ee6172",
|
|
5
|
+
"imports": [
|
|
6
|
+
"next/link",
|
|
7
|
+
"lucide-react",
|
|
8
|
+
"@/components/ui/button",
|
|
9
|
+
"@/components/ui/card",
|
|
10
|
+
"@/components/ui/badge",
|
|
11
|
+
"@/components/portal/portal-shell",
|
|
12
|
+
"@/components/portal/empty-state"
|
|
13
|
+
],
|
|
14
|
+
"tokens": [
|
|
15
|
+
"--color-bg-brand-muted",
|
|
16
|
+
"--color-bg-subtle",
|
|
17
|
+
"--color-border-muted",
|
|
18
|
+
"--color-text-brand",
|
|
19
|
+
"--color-text-muted",
|
|
20
|
+
"--color-text-secondary",
|
|
21
|
+
"--gray-950",
|
|
22
|
+
"--teal-500"
|
|
23
|
+
],
|
|
24
|
+
"fragments": [
|
|
25
|
+
{
|
|
26
|
+
"id": "TraineeMessagesPage",
|
|
27
|
+
"line": 16,
|
|
28
|
+
"code": "export default function TraineeMessagesPage() {\n return (\n <PortalShell kind=\"trainee\">\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Messages\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Private chats with your mentors for active sessions.\n </p>\n </div>\n </div>\n\n {THREADS.length === 0 ? (\n <EmptyState\n icon={MessageSquare}\n title=\"No active conversations\"\n description=\"When a mentor accepts your session request, a private chat opens here for pre- and post-session coordination.\"\n action={<Link href=\"/portal/trainee/request\"><Button variant=\"brand\">Request a session <ArrowRight size={13} /></Button></Link>}\n />\n ) : (\n <Card className=\"p-0\">\n <div className=\"divide-y\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n {THREADS.map((t) => (\n <Link\n key={t.id}\n href={`/portal/trainee/sessions/${t.id}`}\n className=\"flex items-center gap-4 px-5 py-4 transition-colors hover:bg-[var(--color-bg-subtle)]\"\n >\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center text-[11px] font-semibold shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n {t.with.split(\" \").slice(-1)[0][0]}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 mb-0.5\">\n <span className=\"text-[13px] font-semibold truncate\">{t.with}</span>\n <Badge variant=\"outline\" className=\"text-[10px]\">{t.role}</Badge>\n </div>\n <div className=\"text-[12px] truncate\" style={{ color: \"var(--color-text-secondary)\" }}>{t.lastMsg}</div>\n </div>\n <div className=\"text-right shrink-0\">\n <div className=\"text-[10px] mb-1\" style={{ color: \"var(--color-text-muted)\" }}>{t.lastAt}</div>\n {t.unread > 0 && (\n <span className=\"inline-block min-w-[18px] h-[18px] px-1.5 rounded-full text-[10px] font-semibold flex items-center justify-center\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>{t.unread}</span>\n )}\n </div>\n </Link>\n ))}\n </div>\n </Card>\n )}\n </PortalShell>\n );\n}"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { MessageSquare, ArrowRight, Send } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardHeader, CardTitle, CardContent, CardDescription } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { EmptyState } from \"@/components/portal/empty-state\";\n\nconst THREADS = [\n { id: \"req-004\", with: \"Dr. Hiroshi Yamamoto\", role: \"Mentor\", lastMsg: \"Yes, 30 min before is fine. Make sure CO2 insufflator is connected…\", lastAt: \"5 min ago\", unread: 0 },\n { id: \"req-001\", with: \"Prof. Amyn Haji\", role: \"Mentor\", lastMsg: \"I'll review your case docs tonight and reply with notes.\", lastAt: \"Yesterday\", unread: 1 },\n];\n\nexport default function TraineeMessagesPage() {\n return (\n <PortalShell kind=\"trainee\">\n <div className=\"flex items-start justify-between gap-4 mb-6\">\n <div>\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Messages\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Private chats with your mentors for active sessions.\n </p>\n </div>\n </div>\n\n {THREADS.length === 0 ? (\n <EmptyState\n icon={MessageSquare}\n title=\"No active conversations\"\n description=\"When a mentor accepts your session request, a private chat opens here for pre- and post-session coordination.\"\n action={<Link href=\"/portal/trainee/request\"><Button variant=\"brand\">Request a session <ArrowRight size={13} /></Button></Link>}\n />\n ) : (\n <Card className=\"p-0\">\n <div className=\"divide-y\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n {THREADS.map((t) => (\n <Link\n key={t.id}\n href={`/portal/trainee/sessions/${t.id}`}\n className=\"flex items-center gap-4 px-5 py-4 transition-colors hover:bg-[var(--color-bg-subtle)]\"\n >\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center text-[11px] font-semibold shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n {t.with.split(\" \").slice(-1)[0][0]}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 mb-0.5\">\n <span className=\"text-[13px] font-semibold truncate\">{t.with}</span>\n <Badge variant=\"outline\" className=\"text-[10px]\">{t.role}</Badge>\n </div>\n <div className=\"text-[12px] truncate\" style={{ color: \"var(--color-text-secondary)\" }}>{t.lastMsg}</div>\n </div>\n <div className=\"text-right shrink-0\">\n <div className=\"text-[10px] mb-1\" style={{ color: \"var(--color-text-muted)\" }}>{t.lastAt}</div>\n {t.unread > 0 && (\n <span className=\"inline-block min-w-[18px] h-[18px] px-1.5 rounded-full text-[10px] font-semibold flex items-center justify-center\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>{t.unread}</span>\n )}\n </div>\n </Link>\n ))}\n </div>\n </Card>\n )}\n </PortalShell>\n );\n}\n"
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
+
"file": "app/portal/trainee/notifications/page.tsx",
|
|
4
|
+
"sha256": "cb58528019474524e624f74aad940c8d2250da73ec49137a4cd610a4cc255ab9",
|
|
5
|
+
"imports": [
|
|
6
|
+
"next/link",
|
|
7
|
+
"lucide-react",
|
|
8
|
+
"@/components/ui/card",
|
|
9
|
+
"@/components/ui/tabs",
|
|
10
|
+
"@/components/portal/portal-shell",
|
|
11
|
+
"@/components/portal/empty-state"
|
|
12
|
+
],
|
|
13
|
+
"tokens": [
|
|
14
|
+
"--color-bg-brand-muted",
|
|
15
|
+
"--color-bg-subtle",
|
|
16
|
+
"--color-border-muted",
|
|
17
|
+
"--color-text-brand",
|
|
18
|
+
"--color-text-muted",
|
|
19
|
+
"--color-text-secondary",
|
|
20
|
+
"--teal-500"
|
|
21
|
+
],
|
|
22
|
+
"fragments": [
|
|
23
|
+
{
|
|
24
|
+
"id": "TraineeNotificationsPage",
|
|
25
|
+
"line": 17,
|
|
26
|
+
"code": "export default function TraineeNotificationsPage() {\n return (\n <PortalShell kind=\"trainee\">\n <div className=\"mb-6\">\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Notifications\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n All updates across your sessions, credits, and messages.\n </p>\n </div>\n\n <Tabs defaultValue=\"all\">\n <TabsList>\n <TabsTrigger value=\"all\">All ({NOTIFS.length})</TabsTrigger>\n <TabsTrigger value=\"unread\">Unread ({NOTIFS.filter((n) => n.unread).length})</TabsTrigger>\n <TabsTrigger value=\"archived\">Archived</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"all\">\n <NotifList items={NOTIFS} />\n </TabsContent>\n <TabsContent value=\"unread\">\n <NotifList items={NOTIFS.filter((n) => n.unread)} />\n </TabsContent>\n <TabsContent value=\"archived\">\n <EmptyState icon={Bell} title=\"No archived notifications\" description=\"Notifications older than 30 days are archived here.\" />\n </TabsContent>\n </Tabs>\n </PortalShell>\n );\n}"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "NotifList",
|
|
30
|
+
"line": 50,
|
|
31
|
+
"code": "function NotifList({ items }: { items: typeof NOTIFS }) {\n if (items.length === 0) {\n return <EmptyState icon={Bell} title=\"You're all caught up\" description=\"When you have new activity, it will appear here.\" />;\n }\n return (\n <Card className=\"p-0\">\n <div className=\"divide-y\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n {items.map((n) => {\n const Icon = n.icon;\n return (\n <Link\n key={n.id}\n href={n.href}\n className=\"flex items-start gap-3 px-5 py-4 transition-colors hover:bg-[var(--color-bg-subtle)]\"\n style={{ background: n.unread ? \"var(--color-bg-subtle)\" : undefined }}\n >\n <div className=\"w-9 h-9 rounded-md flex items-center justify-center shrink-0 mt-0.5\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Icon size={15} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">{n.title}</div>\n <div className=\"text-[12px] mt-0.5\" style={{ color: \"var(--color-text-secondary)\" }}>{n.body}</div>\n <div className=\"text-[11px] mt-1\" style={{ color: \"var(--color-text-muted)\" }}>{n.time}</div>\n </div>\n {n.unread && <span className=\"w-2 h-2 rounded-full mt-2 shrink-0\" style={{ background: \"var(--teal-500)\" }} />}\n </Link>\n );\n })}\n </div>\n </Card>\n );\n}"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { Bell, Calendar, CheckCircle2, AlertTriangle, MessageSquare } from \"lucide-react\";\nimport { Card } from \"@/components/ui/card\";\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { EmptyState } from \"@/components/portal/empty-state\";\n\nconst NOTIFS = [\n { id: \"n1\", icon: Calendar, title: \"Prof. Amyn Haji accepted your session\", body: \"ESD on 26 May at 10:00\", time: \"12 min ago\", unread: true, href: \"/portal/trainee/sessions/req-001\" },\n { id: \"n2\", icon: MessageSquare, title: \"New message from Dr. Hiroshi Yamamoto\", body: \"Could you upload the manometry tracing?\", time: \"1 h ago\", unread: true, href: \"/portal/trainee/sessions/req-004\" },\n { id: \"n3\", icon: CheckCircle2, title: \"Credits allocated\", body: \"Olympus Medical UK added 3 credits to your account.\", time: \"Yesterday\", unread: false, href: \"/portal/trainee/credits\" },\n { id: \"n4\", icon: AlertTriangle, title: \"Connectivity pre-check due in 24h\", body: \"ESD on 27 May at 14:30\", time: \"Yesterday\", unread: false, href: \"/portal/trainee/sessions/req-002/precheck\" },\n];\n\nexport default function TraineeNotificationsPage() {\n return (\n <PortalShell kind=\"trainee\">\n <div className=\"mb-6\">\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Notifications\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n All updates across your sessions, credits, and messages.\n </p>\n </div>\n\n <Tabs defaultValue=\"all\">\n <TabsList>\n <TabsTrigger value=\"all\">All ({NOTIFS.length})</TabsTrigger>\n <TabsTrigger value=\"unread\">Unread ({NOTIFS.filter((n) => n.unread).length})</TabsTrigger>\n <TabsTrigger value=\"archived\">Archived</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"all\">\n <NotifList items={NOTIFS} />\n </TabsContent>\n <TabsContent value=\"unread\">\n <NotifList items={NOTIFS.filter((n) => n.unread)} />\n </TabsContent>\n <TabsContent value=\"archived\">\n <EmptyState icon={Bell} title=\"No archived notifications\" description=\"Notifications older than 30 days are archived here.\" />\n </TabsContent>\n </Tabs>\n </PortalShell>\n );\n}\n\nfunction NotifList({ items }: { items: typeof NOTIFS }) {\n if (items.length === 0) {\n return <EmptyState icon={Bell} title=\"You're all caught up\" description=\"When you have new activity, it will appear here.\" />;\n }\n return (\n <Card className=\"p-0\">\n <div className=\"divide-y\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n {items.map((n) => {\n const Icon = n.icon;\n return (\n <Link\n key={n.id}\n href={n.href}\n className=\"flex items-start gap-3 px-5 py-4 transition-colors hover:bg-[var(--color-bg-subtle)]\"\n style={{ background: n.unread ? \"var(--color-bg-subtle)\" : undefined }}\n >\n <div className=\"w-9 h-9 rounded-md flex items-center justify-center shrink-0 mt-0.5\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Icon size={15} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">{n.title}</div>\n <div className=\"text-[12px] mt-0.5\" style={{ color: \"var(--color-text-secondary)\" }}>{n.body}</div>\n <div className=\"text-[11px] mt-1\" style={{ color: \"var(--color-text-muted)\" }}>{n.time}</div>\n </div>\n {n.unread && <span className=\"w-2 h-2 rounded-full mt-2 shrink-0\" style={{ background: \"var(--teal-500)\" }} />}\n </Link>\n );\n })}\n </div>\n </Card>\n );\n}\n"
|
|
35
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
+
"file": "app/portal/trainee/profile/page.tsx",
|
|
4
|
+
"sha256": "e77ccc55be147e3e99c4e973e350263325866b37aea9807cede91bf7a021468b",
|
|
5
|
+
"imports": [
|
|
6
|
+
"react",
|
|
7
|
+
"@/components/ui/textarea",
|
|
8
|
+
"@/components/portal/portal-shell",
|
|
9
|
+
"@/components/portal/profile-form",
|
|
10
|
+
"@/components/auth/auth-primitives",
|
|
11
|
+
"@/components/auth/onboarding-primitives",
|
|
12
|
+
"@/components/ui/input"
|
|
13
|
+
],
|
|
14
|
+
"tokens": [
|
|
15
|
+
"--color-bg-subtle",
|
|
16
|
+
"--color-text-muted"
|
|
17
|
+
],
|
|
18
|
+
"fragments": [
|
|
19
|
+
{
|
|
20
|
+
"id": "TraineeProfilePage",
|
|
21
|
+
"line": 28,
|
|
22
|
+
"code": "export default function TraineeProfilePage() {\n const [bio, setBio] = useState(\"test\");\n const [langs, setLangs] = useState<string[]>([\"English\"]);\n const [region, setRegion] = useState(\"Europe/London\");\n const [procedures, setProcedures] = useState<string[]>([\"Variceal Band Ligation (VBL)\"]);\n const [experiences, setExperiences] = useState<Record<string, string>>({ \"Variceal Band Ligation (VBL)\": \"Proficient\" });\n const [timing, setTiming] = useState(\"\");\n const [siteName, setSiteName] = useState(\"\");\n const [siteAddress, setSiteAddress] = useState(\"\");\n\n return (\n <PortalShell kind=\"trainee\">\n <ProfileForm\n title=\"Profile\"\n subtitle=\"Update your photo, name, mentor-matching details, and sign-in email.\"\n initials=\"HL\"\n matchingTitle=\"Mentor matching profile\"\n matchingDescription=\"This information helps Mentix match you with mentors and sessions. First and second name are required to save; languages and region help mentors find you in the directory.\"\n initialFirstName=\"Hannah\"\n initialLastName=\"Liu\"\n initialEmail=\"h.liu@ucl.ac.uk\"\n fields={\n <>\n <Field label=\"Bio\" htmlFor=\"bio\">\n <Textarea id=\"bio\" value={bio} onChange={(e) => setBio(e.target.value)} rows={4} placeholder=\"A short bio visible to mentors.\" />\n </Field>\n <Field label=\"Languages\" htmlFor=\"lang\">\n <MultiSelect value={langs} onChange={setLangs} options={LANGUAGES} />\n </Field>\n <Field label=\"Region / time zone\" htmlFor=\"region\">\n <Select id=\"region\" value={region} onChange={setRegion} options={REGIONS} />\n </Field>\n <Field label=\"Procedures requiring mentorship\" htmlFor=\"proc\">\n <MultiSelect value={procedures} onChange={setProcedures} options={PROCEDURES} />\n </Field>\n {procedures.length > 0 && (\n <Field label=\"Experience level per procedure\" htmlFor=\"exp\" hint=\"Pick a level for each selected procedure.\">\n <div className=\"space-y-2\">\n {procedures.map((p) => (\n <div key={p} className=\"flex items-center gap-3\">\n <span className=\"text-[13px] flex-1 min-w-0 truncate\">{p}</span>\n <Select\n id={`exp-${p}`}\n value={experiences[p] ?? \"\"}\n onChange={(v) => setExperiences({ ...experiences, [p]: v })}\n placeholder=\"Select\"\n options={[\n { value: \"Novice\", label: \"Novice\" },\n { value: \"Early Learner\", label: \"Early Learner\" },\n { value: \"Developing\", label: \"Developing\" },\n { value: \"Proficient\", label: \"Proficient\" },\n ]}\n />\n <button\n type=\"button\"\n aria-label=\"Remove procedure\"\n onClick={() => setProcedures(procedures.filter((x) => x !== p))}\n className=\"text-[14px] w-7 h-7 rounded-md hover:bg-[var(--color-bg-subtle)]\"\n style={{ color: \"var(--color-text-muted)\" }}\n >\n ×\n </button>\n </div>\n ))}\n </div>\n </Field>\n )}\n <Field label=\"Preferred session timing\" htmlFor=\"ts\" optional>\n <Input id=\"ts\" value={timing} onChange={(e) => setTiming(e.target.value)} placeholder=\"e.g. Weekday mornings, GMT\" className=\"h-10\" />\n </Field>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3\">\n <Field label=\"Hospital site name\" htmlFor=\"sn\">\n <Input id=\"sn\" value={siteName} onChange={(e) => setSiteName(e.target.value)} className=\"h-10\" />\n </Field>\n <Field label=\"Hospital site address\" htmlFor=\"sa\">\n <Input id=\"sa\" value={siteAddress} onChange={(e) => setSiteAddress(e.target.value)} className=\"h-10\" />\n </Field>\n </div>\n </>\n }\n />\n </PortalShell>\n );\n}"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { ProfileForm } from \"@/components/portal/profile-form\";\nimport { Field } from \"@/components/auth/auth-primitives\";\nimport { Select, MultiSelect } from \"@/components/auth/onboarding-primitives\";\nimport { Input } from \"@/components/ui/input\";\n\nconst LANGUAGES = [\"English\", \"Français\", \"Español\", \"Deutsch\", \"Italiano\", \"中文\", \"日本語\", \"العربية\"];\nconst REGIONS = [\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: \"Asia/Tokyo\", label: \"Tokyo (JST)\" },\n];\nconst PROCEDURES = [\n \"Variceal Band Ligation (VBL)\",\n \"Endoscopic Submucosal Dissection (ESD)\",\n \"Endoscopic Mucosal Resection (EMR)\",\n \"POEM\",\n \"Colonoscopy & polypectomy\",\n \"ERCP\",\n];\n\nexport default function TraineeProfilePage() {\n const [bio, setBio] = useState(\"test\");\n const [langs, setLangs] = useState<string[]>([\"English\"]);\n const [region, setRegion] = useState(\"Europe/London\");\n const [procedures, setProcedures] = useState<string[]>([\"Variceal Band Ligation (VBL)\"]);\n const [experiences, setExperiences] = useState<Record<string, string>>({ \"Variceal Band Ligation (VBL)\": \"Proficient\" });\n const [timing, setTiming] = useState(\"\");\n const [siteName, setSiteName] = useState(\"\");\n const [siteAddress, setSiteAddress] = useState(\"\");\n\n return (\n <PortalShell kind=\"trainee\">\n <ProfileForm\n title=\"Profile\"\n subtitle=\"Update your photo, name, mentor-matching details, and sign-in email.\"\n initials=\"HL\"\n matchingTitle=\"Mentor matching profile\"\n matchingDescription=\"This information helps Mentix match you with mentors and sessions. First and second name are required to save; languages and region help mentors find you in the directory.\"\n initialFirstName=\"Hannah\"\n initialLastName=\"Liu\"\n initialEmail=\"h.liu@ucl.ac.uk\"\n fields={\n <>\n <Field label=\"Bio\" htmlFor=\"bio\">\n <Textarea id=\"bio\" value={bio} onChange={(e) => setBio(e.target.value)} rows={4} placeholder=\"A short bio visible to mentors.\" />\n </Field>\n <Field label=\"Languages\" htmlFor=\"lang\">\n <MultiSelect value={langs} onChange={setLangs} options={LANGUAGES} />\n </Field>\n <Field label=\"Region / time zone\" htmlFor=\"region\">\n <Select id=\"region\" value={region} onChange={setRegion} options={REGIONS} />\n </Field>\n <Field label=\"Procedures requiring mentorship\" htmlFor=\"proc\">\n <MultiSelect value={procedures} onChange={setProcedures} options={PROCEDURES} />\n </Field>\n {procedures.length > 0 && (\n <Field label=\"Experience level per procedure\" htmlFor=\"exp\" hint=\"Pick a level for each selected procedure.\">\n <div className=\"space-y-2\">\n {procedures.map((p) => (\n <div key={p} className=\"flex items-center gap-3\">\n <span className=\"text-[13px] flex-1 min-w-0 truncate\">{p}</span>\n <Select\n id={`exp-${p}`}\n value={experiences[p] ?? \"\"}\n onChange={(v) => setExperiences({ ...experiences, [p]: v })}\n placeholder=\"Select\"\n options={[\n { value: \"Novice\", label: \"Novice\" },\n { value: \"Early Learner\", label: \"Early Learner\" },\n { value: \"Developing\", label: \"Developing\" },\n { value: \"Proficient\", label: \"Proficient\" },\n ]}\n />\n <button\n type=\"button\"\n aria-label=\"Remove procedure\"\n onClick={() => setProcedures(procedures.filter((x) => x !== p))}\n className=\"text-[14px] w-7 h-7 rounded-md hover:bg-[var(--color-bg-subtle)]\"\n style={{ color: \"var(--color-text-muted)\" }}\n >\n ×\n </button>\n </div>\n ))}\n </div>\n </Field>\n )}\n <Field label=\"Preferred session timing\" htmlFor=\"ts\" optional>\n <Input id=\"ts\" value={timing} onChange={(e) => setTiming(e.target.value)} placeholder=\"e.g. Weekday mornings, GMT\" className=\"h-10\" />\n </Field>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3\">\n <Field label=\"Hospital site name\" htmlFor=\"sn\">\n <Input id=\"sn\" value={siteName} onChange={(e) => setSiteName(e.target.value)} className=\"h-10\" />\n </Field>\n <Field label=\"Hospital site address\" htmlFor=\"sa\">\n <Input id=\"sa\" value={siteAddress} onChange={(e) => setSiteAddress(e.target.value)} className=\"h-10\" />\n </Field>\n </div>\n </>\n }\n />\n </PortalShell>\n );\n}\n"
|
|
26
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
+
"file": "app/portal/trainee/request/page.tsx",
|
|
4
|
+
"sha256": "dd6d4497f23cdc5b386efe4f533b4e16cd08aaa5fde5e2b93a57312294949193",
|
|
5
|
+
"imports": [
|
|
6
|
+
"next/link",
|
|
7
|
+
"react",
|
|
8
|
+
"next/navigation",
|
|
9
|
+
"lucide-react",
|
|
10
|
+
"@/components/ui/button",
|
|
11
|
+
"@/components/ui/input",
|
|
12
|
+
"@/components/ui/textarea",
|
|
13
|
+
"@/components/ui/card",
|
|
14
|
+
"@/components/ui/badge",
|
|
15
|
+
"@/components/ui/separator",
|
|
16
|
+
"@/components/portal/portal-shell",
|
|
17
|
+
"@/components/auth/auth-primitives",
|
|
18
|
+
"@/components/auth/onboarding-primitives",
|
|
19
|
+
"@/lib/sessions-mock",
|
|
20
|
+
"@/lib/credits-mock"
|
|
21
|
+
],
|
|
22
|
+
"tokens": [
|
|
23
|
+
"--color-bg-brand-muted",
|
|
24
|
+
"--color-bg-subtle",
|
|
25
|
+
"--color-border-brand",
|
|
26
|
+
"--color-border-default",
|
|
27
|
+
"--color-status-success-bg",
|
|
28
|
+
"--color-status-success-text",
|
|
29
|
+
"--color-text-brand",
|
|
30
|
+
"--color-text-muted",
|
|
31
|
+
"--color-text-primary",
|
|
32
|
+
"--color-text-secondary",
|
|
33
|
+
"--gray-950",
|
|
34
|
+
"--teal-500"
|
|
35
|
+
],
|
|
36
|
+
"fragments": [
|
|
37
|
+
{
|
|
38
|
+
"id": "RequestSessionPage",
|
|
39
|
+
"line": 21,
|
|
40
|
+
"code": "export default function RequestSessionPage() {\n const search = useSearchParams();\n const preselectedMentor = search.get(\"mentor\");\n\n const ledger = TRAINEE_LEDGERS[TRAINEE_ID];\n const [mode, setMode] = useState<\"generic_broadcast\" | \"specific_mentor\">(preselectedMentor ? \"specific_mentor\" : \"generic_broadcast\");\n const [mentorId, setMentorId] = useState<string>(preselectedMentor ?? \"\");\n const [procedure, setProcedure] = useState(\"\");\n const [date, setDate] = useState(\"\");\n const [time, setTime] = useState(\"\");\n const [complexity, setComplexity] = useState(\"\");\n const [goals, setGoals] = useState(\"\");\n const [file, setFile] = useState<{ name: string; size: number } | null>(null);\n const [submitting, setSubmitting] = useState(false);\n const [done, setDone] = useState(false);\n\n const noCredits = ledger.available === 0;\n const selectedMentor = MENTORS.find((m) => m.id === mentorId);\n\n const canSubmit = !noCredits && procedure && date && time && complexity && goals.length >= 20 &&\n (mode === \"generic_broadcast\" || !!mentorId);\n\n if (done) {\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Hannah Liu\", subtitle: \"ST5 · Gastroenterology\", initials: \"HL\" }}>\n <div className=\"max-w-md mx-auto text-center py-16\">\n <div className=\"w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-5\" style={{ background: \"var(--color-status-success-bg)\", color: \"var(--color-status-success-text)\" }}>\n <CheckCircle2 size={32} strokeWidth={1.6} />\n </div>\n <h1 className=\"text-[1.5rem] font-semibold mb-2\" style={{ letterSpacing: \"-0.025em\" }}>\n Session request submitted\n </h1>\n <p className=\"text-sm mb-2\" style={{ color: \"var(--color-text-secondary)\" }}>\n {mode === \"specific_mentor\"\n ? <>Your request has been sent to <strong style={{ color: \"var(--color-text-primary)\" }}>{selectedMentor?.name}</strong>.</>\n : \"We've broadcast your request to all qualified mentors. First to accept wins.\"\n }\n </p>\n <p className=\"text-[12px] mb-6\" style={{ color: \"var(--color-text-muted)\" }}>\n 1 credit moved to escrow · You'll be notified when a mentor accepts.\n </p>\n <div className=\"flex items-center gap-2 justify-center\">\n <Link href=\"/portal/trainee\"><Button variant=\"outline\">Back to dashboard</Button></Link>\n <Link href=\"/portal/trainee/sessions/req-004\"><Button variant=\"brand\">View request <ArrowRight size={13} /></Button></Link>\n </div>\n </div>\n </PortalShell>\n );\n }\n\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Hannah Liu\", subtitle: \"ST5 · Gastroenterology\", initials: \"HL\" }}>\n <Link href=\"/portal/trainee\" className=\"inline-flex items-center gap-1.5 text-[12px] mb-4 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-secondary)\" }}>\n <ArrowLeft size={13} /> Back to dashboard\n </Link>\n\n <div className=\"mb-6\">\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Request a session\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Submit a mentorship request for your upcoming procedure.\n </p>\n </div>\n\n {noCredits && (\n <div className=\"mb-5\">\n <InlineAlert\n variant=\"error\"\n title=\"No available credits\"\n action={<Link href=\"#\" className=\"text-[12px] font-medium hover:underline underline-offset-4\">Contact sponsor</Link>}\n >\n You need at least 1 credit to submit a request. Contact your sponsor (Olympus Medical UK).\n </InlineAlert>\n </div>\n )}\n\n <div className=\"grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6\">\n <form onSubmit={(e) => { e.preventDefault(); setSubmitting(true); setTimeout(() => setDone(true), 900); }} className=\"space-y-5\">\n {/* Mode selector */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Request mode</CardTitle>\n <CardDescription>How would you like to find a mentor?</CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-2\">\n <ModeCard\n icon={Radio}\n active={mode === \"generic_broadcast\"}\n onClick={() => setMode(\"generic_broadcast\")}\n title=\"Generic Radio\"\n desc=\"Send to all qualified mentors. First to accept wins. Typical pickup < 4h.\"\n />\n <ModeCard\n icon={User}\n active={mode === \"specific_mentor\"}\n onClick={() => setMode(\"specific_mentor\")}\n title=\"Specific Mentor\"\n desc=\"Send to one named mentor. They have 24h to accept or decline.\"\n />\n </div>\n\n {mode === \"specific_mentor\" && (\n <div className=\"mt-4\">\n <Field label=\"Choose mentor\" htmlFor=\"mentor\">\n <Select\n id=\"mentor\"\n value={mentorId}\n onChange={setMentorId}\n placeholder=\"Select a mentor…\"\n options={MENTORS.map((m) => ({ value: m.id, label: `${m.name} — ${m.institution} (★${m.averageRating})` }))}\n />\n </Field>\n {selectedMentor && (\n <div className=\"mt-3 rounded-lg border p-3 flex items-center gap-3\" style={{ borderColor: \"var(--color-border-default)\", background: \"var(--color-bg-subtle)\" }}>\n <div className=\"w-9 h-9 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>{selectedMentor.initials}</div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">{selectedMentor.name}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>{selectedMentor.title} · {selectedMentor.institution}</div>\n </div>\n <Link href=\"/portal/trainee/mentors\" className=\"text-[11px] font-medium hover:underline underline-offset-4 shrink-0\" style={{ color: \"var(--color-text-brand)\" }}>Browse all</Link>\n </div>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n\n {/* Procedure details */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Case details</CardTitle>\n <CardDescription>The procedure and learning goals for this case.</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <Field label=\"Procedure type\" htmlFor=\"proc\">\n <Select id=\"proc\" value={procedure} onChange={setProcedure} placeholder=\"Select procedure…\" options={PROCEDURES.map((p) => ({ value: p, label: p }))} />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Scheduled date\" htmlFor=\"d\">\n <Input id=\"d\" type=\"date\" value={date} onChange={(e) => setDate(e.target.value)} className=\"h-10\" />\n </Field>\n <Field label=\"Scheduled time\" htmlFor=\"t\">\n <Input id=\"t\" type=\"time\" value={time} onChange={(e) => setTime(e.target.value)} className=\"h-10\" />\n </Field>\n </div>\n <Field label=\"Case complexity\" htmlFor=\"comp\">\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-2\">\n {([\"Routine\", \"Moderate\", \"Complex\", \"Highly Complex\"] as const).map((c) => (\n <button\n key={c}\n type=\"button\"\n onClick={() => setComplexity(c)}\n className=\"h-10 rounded-lg border text-[12px] font-medium transition-all\"\n style={{\n borderColor: complexity === c ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: complexity === c ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n color: complexity === c ? \"var(--color-text-brand)\" : \"var(--color-text-primary)\",\n }}\n >{c}</button>\n ))}\n </div>\n </Field>\n <Field label=\"Learning goals\" htmlFor=\"lg\" hint={`${goals.length} / 500 characters`}>\n <Textarea id=\"lg\" value={goals} onChange={(e) => setGoals(e.target.value.slice(0, 500))} rows={4} placeholder=\"What do you want to learn from this case? What specific challenges do you anticipate?\" />\n </Field>\n <Field label=\"Case documentation\" htmlFor=\"doc\" hint=\"JPEG, PNG, PDF, or DICOM · max 50MB · optional, recommended for prep\">\n <FileDropZone label=\"Drop case files\" file={file} onFile={setFile} hint=\"Endoscopy images, reports, clinical notes\" />\n </Field>\n </CardContent>\n </Card>\n\n <div className=\"flex items-center justify-end gap-2\">\n <Link href=\"/portal/trainee\"><Button type=\"button\" variant=\"outline\">Cancel</Button></Link>\n <Button type=\"submit\" variant=\"brand\" disabled={!canSubmit || submitting}>\n {submitting ? <><Spinner className=\"w-3.5 h-3.5 mr-1\" /> Submitting…</> : <>Submit request <ArrowRight size={13} /></>}\n </Button>\n </div>\n </form>\n\n {/* Sidebar — credit balance + summary */}\n <Card className=\"h-fit sticky top-20\">\n <CardHeader>\n <CardTitle className=\"text-[13px] flex items-center gap-2\">\n <Wallet size={13} /> Cost preview\n </CardTitle>\n </CardHeader>\n <CardContent>\n <dl className=\"space-y-2.5 text-[12px]\">\n <DRow label=\"This request\" value=\"1 credit\" />\n <DRow label=\"Currently available\" value={`${ledger.available} credit${ledger.available === 1 ? \"\" : \"s\"}`} />\n <DRow label=\"After submission\" value={`${Math.max(0, ledger.available - 1)} credit${ledger.available - 1 === 1 ? \"\" : \"s\"}`} />\n </dl>\n <Separator className=\"my-3\" />\n <p className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>\n The credit is held in escrow until the session is closed. It's returned if the mentor declines or you cancel before confirmation.\n </p>\n </CardContent>\n </Card>\n </div>\n </PortalShell>\n );\n}"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "ModeCard",
|
|
44
|
+
"line": 226,
|
|
45
|
+
"code": "function ModeCard({ icon: Icon, active, onClick, title, desc }: { icon: typeof User; active: boolean; onClick: () => void; title: string; desc: string }) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className=\"text-left p-4 rounded-xl border transition-all focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30\"\n style={{\n borderColor: active ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: active ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n }}\n >\n <div className=\"flex items-start gap-3\">\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{ background: active ? \"var(--teal-500)\" : \"var(--color-bg-subtle)\", color: active ? \"var(--gray-950)\" : \"var(--color-text-secondary)\" }}>\n <Icon size={16} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-semibold mb-1\" style={{ color: active ? \"var(--color-text-brand)\" : \"var(--color-text-primary)\" }}>{title}</div>\n <p className=\"text-[11px]\" style={{ color: \"var(--color-text-secondary)\" }}>{desc}</p>\n </div>\n </div>\n </button>\n );\n}"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "DRow",
|
|
49
|
+
"line": 251,
|
|
50
|
+
"code": "function DRow({ label, value }: { label: string; value: string }) {\n return (\n <div className=\"flex items-center justify-between\">\n <dt style={{ color: \"var(--color-text-muted)\" }}>{label}</dt>\n <dd className=\"font-medium\" style={{ color: \"var(--color-text-primary)\" }}>{value}</dd>\n </div>\n );\n}"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useState } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { ArrowLeft, ArrowRight, Radio, User, Calendar, Wallet, CheckCircle2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Card, CardHeader, CardTitle, CardDescription, CardContent } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { Field, Spinner, InlineAlert } from \"@/components/auth/auth-primitives\";\nimport { Select, FileDropZone } from \"@/components/auth/onboarding-primitives\";\nimport { MENTORS, PROCEDURES } from \"@/lib/sessions-mock\";\nimport { TRAINEE_LEDGERS } from \"@/lib/credits-mock\";\n\nconst TRAINEE_ID = \"app-105\";\n\nexport default function RequestSessionPage() {\n const search = useSearchParams();\n const preselectedMentor = search.get(\"mentor\");\n\n const ledger = TRAINEE_LEDGERS[TRAINEE_ID];\n const [mode, setMode] = useState<\"generic_broadcast\" | \"specific_mentor\">(preselectedMentor ? \"specific_mentor\" : \"generic_broadcast\");\n const [mentorId, setMentorId] = useState<string>(preselectedMentor ?? \"\");\n const [procedure, setProcedure] = useState(\"\");\n const [date, setDate] = useState(\"\");\n const [time, setTime] = useState(\"\");\n const [complexity, setComplexity] = useState(\"\");\n const [goals, setGoals] = useState(\"\");\n const [file, setFile] = useState<{ name: string; size: number } | null>(null);\n const [submitting, setSubmitting] = useState(false);\n const [done, setDone] = useState(false);\n\n const noCredits = ledger.available === 0;\n const selectedMentor = MENTORS.find((m) => m.id === mentorId);\n\n const canSubmit = !noCredits && procedure && date && time && complexity && goals.length >= 20 &&\n (mode === \"generic_broadcast\" || !!mentorId);\n\n if (done) {\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Hannah Liu\", subtitle: \"ST5 · Gastroenterology\", initials: \"HL\" }}>\n <div className=\"max-w-md mx-auto text-center py-16\">\n <div className=\"w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-5\" style={{ background: \"var(--color-status-success-bg)\", color: \"var(--color-status-success-text)\" }}>\n <CheckCircle2 size={32} strokeWidth={1.6} />\n </div>\n <h1 className=\"text-[1.5rem] font-semibold mb-2\" style={{ letterSpacing: \"-0.025em\" }}>\n Session request submitted\n </h1>\n <p className=\"text-sm mb-2\" style={{ color: \"var(--color-text-secondary)\" }}>\n {mode === \"specific_mentor\"\n ? <>Your request has been sent to <strong style={{ color: \"var(--color-text-primary)\" }}>{selectedMentor?.name}</strong>.</>\n : \"We've broadcast your request to all qualified mentors. First to accept wins.\"\n }\n </p>\n <p className=\"text-[12px] mb-6\" style={{ color: \"var(--color-text-muted)\" }}>\n 1 credit moved to escrow · You'll be notified when a mentor accepts.\n </p>\n <div className=\"flex items-center gap-2 justify-center\">\n <Link href=\"/portal/trainee\"><Button variant=\"outline\">Back to dashboard</Button></Link>\n <Link href=\"/portal/trainee/sessions/req-004\"><Button variant=\"brand\">View request <ArrowRight size={13} /></Button></Link>\n </div>\n </div>\n </PortalShell>\n );\n }\n\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Hannah Liu\", subtitle: \"ST5 · Gastroenterology\", initials: \"HL\" }}>\n <Link href=\"/portal/trainee\" className=\"inline-flex items-center gap-1.5 text-[12px] mb-4 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-secondary)\" }}>\n <ArrowLeft size={13} /> Back to dashboard\n </Link>\n\n <div className=\"mb-6\">\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Request a session\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Submit a mentorship request for your upcoming procedure.\n </p>\n </div>\n\n {noCredits && (\n <div className=\"mb-5\">\n <InlineAlert\n variant=\"error\"\n title=\"No available credits\"\n action={<Link href=\"#\" className=\"text-[12px] font-medium hover:underline underline-offset-4\">Contact sponsor</Link>}\n >\n You need at least 1 credit to submit a request. Contact your sponsor (Olympus Medical UK).\n </InlineAlert>\n </div>\n )}\n\n <div className=\"grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6\">\n <form onSubmit={(e) => { e.preventDefault(); setSubmitting(true); setTimeout(() => setDone(true), 900); }} className=\"space-y-5\">\n {/* Mode selector */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Request mode</CardTitle>\n <CardDescription>How would you like to find a mentor?</CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-2\">\n <ModeCard\n icon={Radio}\n active={mode === \"generic_broadcast\"}\n onClick={() => setMode(\"generic_broadcast\")}\n title=\"Generic Radio\"\n desc=\"Send to all qualified mentors. First to accept wins. Typical pickup < 4h.\"\n />\n <ModeCard\n icon={User}\n active={mode === \"specific_mentor\"}\n onClick={() => setMode(\"specific_mentor\")}\n title=\"Specific Mentor\"\n desc=\"Send to one named mentor. They have 24h to accept or decline.\"\n />\n </div>\n\n {mode === \"specific_mentor\" && (\n <div className=\"mt-4\">\n <Field label=\"Choose mentor\" htmlFor=\"mentor\">\n <Select\n id=\"mentor\"\n value={mentorId}\n onChange={setMentorId}\n placeholder=\"Select a mentor…\"\n options={MENTORS.map((m) => ({ value: m.id, label: `${m.name} — ${m.institution} (★${m.averageRating})` }))}\n />\n </Field>\n {selectedMentor && (\n <div className=\"mt-3 rounded-lg border p-3 flex items-center gap-3\" style={{ borderColor: \"var(--color-border-default)\", background: \"var(--color-bg-subtle)\" }}>\n <div className=\"w-9 h-9 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>{selectedMentor.initials}</div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">{selectedMentor.name}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>{selectedMentor.title} · {selectedMentor.institution}</div>\n </div>\n <Link href=\"/portal/trainee/mentors\" className=\"text-[11px] font-medium hover:underline underline-offset-4 shrink-0\" style={{ color: \"var(--color-text-brand)\" }}>Browse all</Link>\n </div>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n\n {/* Procedure details */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Case details</CardTitle>\n <CardDescription>The procedure and learning goals for this case.</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <Field label=\"Procedure type\" htmlFor=\"proc\">\n <Select id=\"proc\" value={procedure} onChange={setProcedure} placeholder=\"Select procedure…\" options={PROCEDURES.map((p) => ({ value: p, label: p }))} />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Scheduled date\" htmlFor=\"d\">\n <Input id=\"d\" type=\"date\" value={date} onChange={(e) => setDate(e.target.value)} className=\"h-10\" />\n </Field>\n <Field label=\"Scheduled time\" htmlFor=\"t\">\n <Input id=\"t\" type=\"time\" value={time} onChange={(e) => setTime(e.target.value)} className=\"h-10\" />\n </Field>\n </div>\n <Field label=\"Case complexity\" htmlFor=\"comp\">\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-2\">\n {([\"Routine\", \"Moderate\", \"Complex\", \"Highly Complex\"] as const).map((c) => (\n <button\n key={c}\n type=\"button\"\n onClick={() => setComplexity(c)}\n className=\"h-10 rounded-lg border text-[12px] font-medium transition-all\"\n style={{\n borderColor: complexity === c ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: complexity === c ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n color: complexity === c ? \"var(--color-text-brand)\" : \"var(--color-text-primary)\",\n }}\n >{c}</button>\n ))}\n </div>\n </Field>\n <Field label=\"Learning goals\" htmlFor=\"lg\" hint={`${goals.length} / 500 characters`}>\n <Textarea id=\"lg\" value={goals} onChange={(e) => setGoals(e.target.value.slice(0, 500))} rows={4} placeholder=\"What do you want to learn from this case? What specific challenges do you anticipate?\" />\n </Field>\n <Field label=\"Case documentation\" htmlFor=\"doc\" hint=\"JPEG, PNG, PDF, or DICOM · max 50MB · optional, recommended for prep\">\n <FileDropZone label=\"Drop case files\" file={file} onFile={setFile} hint=\"Endoscopy images, reports, clinical notes\" />\n </Field>\n </CardContent>\n </Card>\n\n <div className=\"flex items-center justify-end gap-2\">\n <Link href=\"/portal/trainee\"><Button type=\"button\" variant=\"outline\">Cancel</Button></Link>\n <Button type=\"submit\" variant=\"brand\" disabled={!canSubmit || submitting}>\n {submitting ? <><Spinner className=\"w-3.5 h-3.5 mr-1\" /> Submitting…</> : <>Submit request <ArrowRight size={13} /></>}\n </Button>\n </div>\n </form>\n\n {/* Sidebar — credit balance + summary */}\n <Card className=\"h-fit sticky top-20\">\n <CardHeader>\n <CardTitle className=\"text-[13px] flex items-center gap-2\">\n <Wallet size={13} /> Cost preview\n </CardTitle>\n </CardHeader>\n <CardContent>\n <dl className=\"space-y-2.5 text-[12px]\">\n <DRow label=\"This request\" value=\"1 credit\" />\n <DRow label=\"Currently available\" value={`${ledger.available} credit${ledger.available === 1 ? \"\" : \"s\"}`} />\n <DRow label=\"After submission\" value={`${Math.max(0, ledger.available - 1)} credit${ledger.available - 1 === 1 ? \"\" : \"s\"}`} />\n </dl>\n <Separator className=\"my-3\" />\n <p className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>\n The credit is held in escrow until the session is closed. It's returned if the mentor declines or you cancel before confirmation.\n </p>\n </CardContent>\n </Card>\n </div>\n </PortalShell>\n );\n}\n\nfunction ModeCard({ icon: Icon, active, onClick, title, desc }: { icon: typeof User; active: boolean; onClick: () => void; title: string; desc: string }) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className=\"text-left p-4 rounded-xl border transition-all focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30\"\n style={{\n borderColor: active ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: active ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n }}\n >\n <div className=\"flex items-start gap-3\">\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{ background: active ? \"var(--teal-500)\" : \"var(--color-bg-subtle)\", color: active ? \"var(--gray-950)\" : \"var(--color-text-secondary)\" }}>\n <Icon size={16} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-semibold mb-1\" style={{ color: active ? \"var(--color-text-brand)\" : \"var(--color-text-primary)\" }}>{title}</div>\n <p className=\"text-[11px]\" style={{ color: \"var(--color-text-secondary)\" }}>{desc}</p>\n </div>\n </div>\n </button>\n );\n}\n\nfunction DRow({ label, value }: { label: string; value: string }) {\n return (\n <div className=\"flex items-center justify-between\">\n <dt style={{ color: \"var(--color-text-muted)\" }}>{label}</dt>\n <dd className=\"font-medium\" style={{ color: \"var(--color-text-primary)\" }}>{value}</dd>\n </div>\n );\n}\n"
|
|
54
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
+
"file": "app/portal/trainee/sessions/[id]/feedback/page.tsx",
|
|
4
|
+
"sha256": "c38954e6a1b4ac992c403c6836b9b4b83dcaeff9dbc75a55770744e2675425f7",
|
|
5
|
+
"imports": [
|
|
6
|
+
"next/link",
|
|
7
|
+
"react",
|
|
8
|
+
"lucide-react",
|
|
9
|
+
"@/components/ui/button",
|
|
10
|
+
"@/components/ui/textarea",
|
|
11
|
+
"@/components/ui/card",
|
|
12
|
+
"@/components/ui/badge",
|
|
13
|
+
"@/components/portal/portal-shell",
|
|
14
|
+
"@/components/auth/auth-primitives",
|
|
15
|
+
"@/lib/sessions-mock"
|
|
16
|
+
],
|
|
17
|
+
"tokens": [
|
|
18
|
+
"--amber-500",
|
|
19
|
+
"--color-bg-subtle",
|
|
20
|
+
"--color-border-strong",
|
|
21
|
+
"--color-status-success-bg",
|
|
22
|
+
"--color-status-success-text",
|
|
23
|
+
"--color-text-muted",
|
|
24
|
+
"--color-text-primary",
|
|
25
|
+
"--color-text-secondary",
|
|
26
|
+
"--teal-500"
|
|
27
|
+
],
|
|
28
|
+
"fragments": [
|
|
29
|
+
{
|
|
30
|
+
"id": "FeedbackPage",
|
|
31
|
+
"line": 23,
|
|
32
|
+
"code": "export default function FeedbackPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = use(params);\n const req = REQUESTS.find((r) => r.id === id) ?? REQUESTS[3];\n\n const [ratings, setRatings] = useState<Record<DimId, number>>({} as Record<DimId, number>);\n const [comment, setComment] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n const [done, setDone] = useState(false);\n\n const allRated = DIMS.every((d) => ratings[d.id]);\n const avg = allRated ? DIMS.reduce((a, d) => a + ratings[d.id], 0) / DIMS.length : 0;\n\n if (done) {\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Yuki Tanaka\", subtitle: \"ST4 · GI\", initials: \"YT\" }}>\n <div className=\"max-w-md mx-auto text-center py-16\">\n <div className=\"w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-5\" style={{ background: \"var(--color-status-success-bg)\", color: \"var(--color-status-success-text)\" }}>\n <CheckCircle2 size={32} strokeWidth={1.6} />\n </div>\n <h1 className=\"text-[1.5rem] font-semibold mb-2\" style={{ letterSpacing: \"-0.025em\" }}>\n Thanks for your feedback\n </h1>\n <p className=\"text-sm mb-6\" style={{ color: \"var(--color-text-secondary)\" }}>\n Your ratings are anonymous and aggregated. They help other trainees pick the right mentor and keep platform quality high.\n </p>\n <Link href=\"/portal/trainee\"><Button variant=\"brand\">Back to dashboard <ArrowRight size={13} /></Button></Link>\n </div>\n </PortalShell>\n );\n }\n\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Yuki Tanaka\", subtitle: \"ST4 · GI\", initials: \"YT\" }}>\n <Link href=\"/portal/trainee\" className=\"inline-flex items-center gap-1.5 text-[12px] mb-4 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-secondary)\" }}>\n <ArrowLeft size={13} /> Back to dashboard\n </Link>\n\n <div className=\"mb-6 max-w-2xl\">\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Rate your mentor\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n How was your session with <strong style={{ color: \"var(--color-text-primary)\" }}>{req.mentorName}</strong>? Feedback is anonymous and helps maintain quality.\n </p>\n </div>\n\n <form onSubmit={(e) => { e.preventDefault(); setSubmitting(true); setTimeout(() => setDone(true), 800); }} className=\"max-w-2xl\">\n <Card className=\"mb-5\">\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"text-base\">Rate four dimensions</CardTitle>\n {allRated && <Badge variant=\"outline\">{avg.toFixed(1)} overall</Badge>}\n </div>\n <CardDescription>1 = Poor, 5 = Excellent.</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n {DIMS.map((d) => (\n <div key={d.id} className=\"flex items-center justify-between gap-4 flex-wrap\">\n <div>\n <div className=\"text-[13px] font-medium\">{d.label}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>{d.desc}</div>\n </div>\n <div className=\"flex items-center gap-1\">\n {[1, 2, 3, 4, 5].map((n) => {\n const active = (ratings[d.id] ?? 0) >= n;\n return (\n <button\n key={n}\n type=\"button\"\n onClick={() => setRatings({ ...ratings, [d.id]: n })}\n aria-label={`${n} star${n > 1 ? \"s\" : \"\"}`}\n className=\"w-9 h-9 rounded-md transition-all hover:bg-[var(--color-bg-subtle)] flex items-center justify-center focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30\"\n >\n <Star\n size={18}\n strokeWidth={1.8}\n fill={active ? \"var(--amber-500)\" : \"transparent\"}\n style={{ color: active ? \"var(--amber-500)\" : \"var(--color-border-strong)\" }}\n />\n </button>\n );\n })}\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card className=\"mb-5\">\n <CardHeader>\n <CardTitle className=\"text-base\">Anything else? <span className=\"text-[11px] font-normal\" style={{ color: \"var(--color-text-muted)\" }}>(Optional)</span></CardTitle>\n <CardDescription>What did your mentor do well? What could be improved?</CardDescription>\n </CardHeader>\n <CardContent>\n <Textarea\n value={comment}\n onChange={(e) => setComment(e.target.value.slice(0, 500))}\n rows={4}\n placeholder=\"e.g. Excellent guidance on injection technique. Would love more discussion on case selection.\"\n />\n <p className=\"text-[11px] mt-1\" style={{ color: \"var(--color-text-muted)\" }}>{comment.length} / 500</p>\n </CardContent>\n </Card>\n\n <div className=\"flex items-center justify-end gap-2\">\n <Link href=\"/portal/trainee\"><Button type=\"button\" variant=\"outline\">Maybe later</Button></Link>\n <Button type=\"submit\" variant=\"brand\" disabled={!allRated || submitting}>\n {submitting ? <><Spinner className=\"w-3.5 h-3.5 mr-1\" /> Submitting…</> : <>Submit feedback <ArrowRight size={13} /></>}\n </Button>\n </div>\n </form>\n </PortalShell>\n );\n}"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { use, useState } from \"react\";\nimport { ArrowLeft, ArrowRight, Star, CheckCircle2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Card, CardHeader, CardTitle, CardDescription, CardContent } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { Spinner } from \"@/components/auth/auth-primitives\";\nimport { REQUESTS } from \"@/lib/sessions-mock\";\n\nconst DIMS = [\n { id: \"expertise\", label: \"Expertise\", desc: \"Clinical knowledge and skill\" },\n { id: \"communication\", label: \"Communication\", desc: \"Clarity, responsiveness\" },\n { id: \"guidance\", label: \"Guidance Quality\", desc: \"Helpfulness, actionable feedback\" },\n { id: \"professionalism\", label: \"Professionalism\", desc: \"Respectful, supportive\" },\n] as const;\n\ntype DimId = (typeof DIMS)[number][\"id\"];\n\nexport default function FeedbackPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = use(params);\n const req = REQUESTS.find((r) => r.id === id) ?? REQUESTS[3];\n\n const [ratings, setRatings] = useState<Record<DimId, number>>({} as Record<DimId, number>);\n const [comment, setComment] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n const [done, setDone] = useState(false);\n\n const allRated = DIMS.every((d) => ratings[d.id]);\n const avg = allRated ? DIMS.reduce((a, d) => a + ratings[d.id], 0) / DIMS.length : 0;\n\n if (done) {\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Yuki Tanaka\", subtitle: \"ST4 · GI\", initials: \"YT\" }}>\n <div className=\"max-w-md mx-auto text-center py-16\">\n <div className=\"w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-5\" style={{ background: \"var(--color-status-success-bg)\", color: \"var(--color-status-success-text)\" }}>\n <CheckCircle2 size={32} strokeWidth={1.6} />\n </div>\n <h1 className=\"text-[1.5rem] font-semibold mb-2\" style={{ letterSpacing: \"-0.025em\" }}>\n Thanks for your feedback\n </h1>\n <p className=\"text-sm mb-6\" style={{ color: \"var(--color-text-secondary)\" }}>\n Your ratings are anonymous and aggregated. They help other trainees pick the right mentor and keep platform quality high.\n </p>\n <Link href=\"/portal/trainee\"><Button variant=\"brand\">Back to dashboard <ArrowRight size={13} /></Button></Link>\n </div>\n </PortalShell>\n );\n }\n\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Yuki Tanaka\", subtitle: \"ST4 · GI\", initials: \"YT\" }}>\n <Link href=\"/portal/trainee\" className=\"inline-flex items-center gap-1.5 text-[12px] mb-4 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-secondary)\" }}>\n <ArrowLeft size={13} /> Back to dashboard\n </Link>\n\n <div className=\"mb-6 max-w-2xl\">\n <h1 className=\"text-[1.75rem] font-semibold tracking-tight mb-1\" style={{ letterSpacing: \"-0.025em\" }}>\n Rate your mentor\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n How was your session with <strong style={{ color: \"var(--color-text-primary)\" }}>{req.mentorName}</strong>? Feedback is anonymous and helps maintain quality.\n </p>\n </div>\n\n <form onSubmit={(e) => { e.preventDefault(); setSubmitting(true); setTimeout(() => setDone(true), 800); }} className=\"max-w-2xl\">\n <Card className=\"mb-5\">\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"text-base\">Rate four dimensions</CardTitle>\n {allRated && <Badge variant=\"outline\">{avg.toFixed(1)} overall</Badge>}\n </div>\n <CardDescription>1 = Poor, 5 = Excellent.</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n {DIMS.map((d) => (\n <div key={d.id} className=\"flex items-center justify-between gap-4 flex-wrap\">\n <div>\n <div className=\"text-[13px] font-medium\">{d.label}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>{d.desc}</div>\n </div>\n <div className=\"flex items-center gap-1\">\n {[1, 2, 3, 4, 5].map((n) => {\n const active = (ratings[d.id] ?? 0) >= n;\n return (\n <button\n key={n}\n type=\"button\"\n onClick={() => setRatings({ ...ratings, [d.id]: n })}\n aria-label={`${n} star${n > 1 ? \"s\" : \"\"}`}\n className=\"w-9 h-9 rounded-md transition-all hover:bg-[var(--color-bg-subtle)] flex items-center justify-center focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-[var(--teal-500)]/30\"\n >\n <Star\n size={18}\n strokeWidth={1.8}\n fill={active ? \"var(--amber-500)\" : \"transparent\"}\n style={{ color: active ? \"var(--amber-500)\" : \"var(--color-border-strong)\" }}\n />\n </button>\n );\n })}\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card className=\"mb-5\">\n <CardHeader>\n <CardTitle className=\"text-base\">Anything else? <span className=\"text-[11px] font-normal\" style={{ color: \"var(--color-text-muted)\" }}>(Optional)</span></CardTitle>\n <CardDescription>What did your mentor do well? What could be improved?</CardDescription>\n </CardHeader>\n <CardContent>\n <Textarea\n value={comment}\n onChange={(e) => setComment(e.target.value.slice(0, 500))}\n rows={4}\n placeholder=\"e.g. Excellent guidance on injection technique. Would love more discussion on case selection.\"\n />\n <p className=\"text-[11px] mt-1\" style={{ color: \"var(--color-text-muted)\" }}>{comment.length} / 500</p>\n </CardContent>\n </Card>\n\n <div className=\"flex items-center justify-end gap-2\">\n <Link href=\"/portal/trainee\"><Button type=\"button\" variant=\"outline\">Maybe later</Button></Link>\n <Button type=\"submit\" variant=\"brand\" disabled={!allRated || submitting}>\n {submitting ? <><Spinner className=\"w-3.5 h-3.5 mr-1\" /> Submitting…</> : <>Submit feedback <ArrowRight size={13} /></>}\n </Button>\n </div>\n </form>\n </PortalShell>\n );\n}\n"
|
|
36
|
+
}
|