@utopia-studio-design/design-system-cli 0.7.1 → 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.
Files changed (247) hide show
  1. package/bin/utopia-ds-mcp.mjs +18 -5
  2. package/bin/utopia-ds.mjs +16 -3
  3. package/data/blocks/semantic-pattern-parts/AuthParts.tsx +48 -0
  4. package/data/blocks/semantic-pattern-parts/ChoiceCards.tsx +7 -0
  5. package/data/blocks/semantic-pattern-parts/ListFilters.tsx +9 -0
  6. package/data/blocks/semantic-pattern-parts/OutcomeNotice.tsx +7 -0
  7. package/data/blocks/semantic-pattern-parts/PatternParts.tsx +7 -0
  8. package/data/blocks/semantic-pattern-parts/ProductParts.tsx +38 -0
  9. package/data/blocks/semantic-pattern-parts/ProgressSteps.tsx +6 -0
  10. package/data/blocks/semantic-pattern-parts/ReviewList.tsx +7 -0
  11. package/data/blocks/semantic-pattern-parts/ValidatedField.tsx +8 -0
  12. package/data/blocks/semantic-pattern-parts/WorkflowParts.tsx +339 -0
  13. package/data/blocks/semantic-pattern-parts/auth-parts.css +27 -0
  14. package/data/blocks/semantic-pattern-parts/pattern-parts.css +25 -0
  15. package/data/blocks/semantic-pattern-parts/product-parts.css +25 -0
  16. package/data/blocks/semantic-pattern-parts/types.ts +3 -0
  17. package/data/blocks/semantic-pattern-parts/workflow-parts.css +118 -0
  18. package/data/blocks/ux-pattern-examples/AccessExamples.tsx +46 -0
  19. package/data/blocks/ux-pattern-examples/AccountExamples.tsx +133 -0
  20. package/data/blocks/ux-pattern-examples/OnboardingExample.tsx +15 -0
  21. package/data/blocks/ux-pattern-examples/ProductExamples.tsx +79 -0
  22. package/data/blocks/ux-pattern-examples/ReviewExample.tsx +14 -0
  23. package/data/blocks/ux-pattern-examples/SignupExample.tsx +22 -0
  24. package/data/blocks/ux-pattern-examples/WorkflowExample.tsx +1761 -0
  25. package/data/blocks/ux-pattern-examples/WorkflowExamples.tsx +190 -0
  26. package/data/blocks/ux-pattern-examples/copy.ts +6 -0
  27. package/data/blocks/ux-pattern-examples/ux-examples.css +18 -0
  28. package/data/blocks/ux-pattern-examples/workflow-copy.ts +76 -0
  29. package/data/blocks/ux-pattern-examples/workflow-definitions.ts +1291 -0
  30. package/data/docs/ceramic-release-2026-09-06.md +2 -0
  31. package/data/docs/create-a-brandbook.md +79 -0
  32. package/data/docs/dextrum-document-family.md +47 -0
  33. package/data/docs/guide.md +2 -0
  34. package/data/docs/presentation-asset-repository.md +5 -0
  35. package/data/docs/ui-parts-qa.md +172 -0
  36. package/data/docs/ui-parts.md +105 -0
  37. package/data/docs/ux-pattern-adaptation-qa.md +38 -0
  38. package/data/docs/ux-pattern-adaptation.md +41 -0
  39. package/data/docs/ux-pattern-migration.md +111 -0
  40. package/data/docs/ux-pattern-release-2026-09-13.md +26 -0
  41. package/data/docs/ux-source-evidence/account-auth.json +41 -0
  42. package/data/docs/ux-source-evidence/account-forgot-password.json +36 -0
  43. package/data/docs/ux-source-evidence/account-invite.json +63 -0
  44. package/data/docs/ux-source-evidence/account-locked.json +63 -0
  45. package/data/docs/ux-source-evidence/account-login.json +28 -0
  46. package/data/docs/ux-source-evidence/account-mfa.json +62 -0
  47. package/data/docs/ux-source-evidence/account-onboarding-credentials.json +52 -0
  48. package/data/docs/ux-source-evidence/account-onboarding-legal.json +56 -0
  49. package/data/docs/ux-source-evidence/account-onboarding-orientation.json +66 -0
  50. package/data/docs/ux-source-evidence/account-onboarding-profile.json +50 -0
  51. package/data/docs/ux-source-evidence/account-onboarding.json +69 -0
  52. package/data/docs/ux-source-evidence/account-reset-password.json +27 -0
  53. package/data/docs/ux-source-evidence/account-session-expired.json +62 -0
  54. package/data/docs/ux-source-evidence/account-signup.json +33 -0
  55. package/data/docs/ux-source-evidence/account-verify-email.json +34 -0
  56. package/data/docs/ux-source-evidence/account-welcome.json +41 -0
  57. package/data/docs/ux-source-evidence/admin-analytics.json +54 -0
  58. package/data/docs/ux-source-evidence/admin-audit.json +31 -0
  59. package/data/docs/ux-source-evidence/admin-broadcasts-new.json +36 -0
  60. package/data/docs/ux-source-evidence/admin-broadcasts.json +41 -0
  61. package/data/docs/ux-source-evidence/admin-closures.json +52 -0
  62. package/data/docs/ux-source-evidence/admin-devices.json +51 -0
  63. package/data/docs/ux-source-evidence/admin-escalations.json +35 -0
  64. package/data/docs/ux-source-evidence/admin-incidents.json +54 -0
  65. package/data/docs/ux-source-evidence/admin-loading.json +17 -0
  66. package/data/docs/ux-source-evidence/admin-onboarding-detail.json +80 -0
  67. package/data/docs/ux-source-evidence/admin-onboarding.json +62 -0
  68. package/data/docs/ux-source-evidence/admin-payments.json +45 -0
  69. package/data/docs/ux-source-evidence/admin-providers.json +38 -0
  70. package/data/docs/ux-source-evidence/admin-settings.json +18 -0
  71. package/data/docs/ux-source-evidence/admin-subscriptions-detail-allocate.json +42 -0
  72. package/data/docs/ux-source-evidence/admin-subscriptions-detail.json +48 -0
  73. package/data/docs/ux-source-evidence/admin-subscriptions-new.json +43 -0
  74. package/data/docs/ux-source-evidence/admin-subscriptions.json +41 -0
  75. package/data/docs/ux-source-evidence/admin-transactions.json +41 -0
  76. package/data/docs/ux-source-evidence/error.json +30 -0
  77. package/data/docs/ux-source-evidence/loading.json +19 -0
  78. package/data/docs/ux-source-evidence/marketing-blog.json +48 -0
  79. package/data/docs/ux-source-evidence/marketing-contact.json +36 -0
  80. package/data/docs/ux-source-evidence/marketing.json +64 -0
  81. package/data/docs/ux-source-evidence/not-found.json +28 -0
  82. package/data/docs/ux-source-evidence/portal-loading.json +17 -0
  83. package/data/docs/ux-source-evidence/portal-mentor-messages.json +31 -0
  84. package/data/docs/ux-source-evidence/portal-mentor-profile.json +23 -0
  85. package/data/docs/ux-source-evidence/portal-mentor-requests-detail.json +54 -0
  86. package/data/docs/ux-source-evidence/portal-mentor-sessions-detail-assess.json +42 -0
  87. package/data/docs/ux-source-evidence/portal-mentor-settings.json +18 -0
  88. package/data/docs/ux-source-evidence/portal-mentor.json +38 -0
  89. package/data/docs/ux-source-evidence/portal-sessions-detail-live.json +48 -0
  90. package/data/docs/ux-source-evidence/portal-sponsor-billing.json +28 -0
  91. package/data/docs/ux-source-evidence/portal-sponsor-profile.json +22 -0
  92. package/data/docs/ux-source-evidence/portal-sponsor-sessions.json +28 -0
  93. package/data/docs/ux-source-evidence/portal-sponsor-settings.json +18 -0
  94. package/data/docs/ux-source-evidence/portal-sponsor-trainees.json +36 -0
  95. package/data/docs/ux-source-evidence/portal-sponsor.json +52 -0
  96. package/data/docs/ux-source-evidence/portal-trainee-competency.json +42 -0
  97. package/data/docs/ux-source-evidence/portal-trainee-credits.json +40 -0
  98. package/data/docs/ux-source-evidence/portal-trainee-mentors.json +48 -0
  99. package/data/docs/ux-source-evidence/portal-trainee-messages.json +32 -0
  100. package/data/docs/ux-source-evidence/portal-trainee-notifications.json +35 -0
  101. package/data/docs/ux-source-evidence/portal-trainee-profile.json +26 -0
  102. package/data/docs/ux-source-evidence/portal-trainee-request.json +54 -0
  103. package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +36 -0
  104. package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +42 -0
  105. package/data/docs/ux-source-evidence/portal-trainee-sessions-detail.json +55 -0
  106. package/data/docs/ux-source-evidence/portal-trainee-sessions.json +36 -0
  107. package/data/docs/ux-source-evidence/portal-trainee-settings-notifications.json +38 -0
  108. package/data/docs/ux-source-evidence/portal-trainee-settings.json +18 -0
  109. package/data/docs/ux-source-evidence/portal-trainee.json +44 -0
  110. package/data/docs/ux-source-evidence/portal-tv-detail.json +39 -0
  111. package/data/docs/ux-source-evidence/portal-tv.json +44 -0
  112. package/data/docs/ux-source-evidence/shared-admin-admin-shell.json +35 -0
  113. package/data/docs/ux-source-evidence/shared-auth-auth-primitives.json +105 -0
  114. package/data/docs/ux-source-evidence/shared-auth-auth-shell.json +25 -0
  115. package/data/docs/ux-source-evidence/shared-auth-onboarding-primitives.json +52 -0
  116. package/data/docs/ux-source-evidence/shared-auth-onboarding-shell.json +28 -0
  117. package/data/docs/ux-source-evidence/shared-auth-status-indicator.json +41 -0
  118. package/data/docs/ux-source-evidence/shared-portal-empty-state.json +24 -0
  119. package/data/docs/ux-source-evidence/shared-portal-loading-skeleton.json +27 -0
  120. package/data/docs/ux-source-evidence/shared-portal-notifications-dropdown.json +39 -0
  121. package/data/docs/ux-source-evidence/shared-portal-portal-shell.json +36 -0
  122. package/data/docs/ux-source-evidence/shared-portal-profile-form.json +32 -0
  123. package/data/docs/ux-source-evidence/shared-portal-settings-view.json +81 -0
  124. package/data/docs/ux-source-evidence/shared-portal-usage-sparkline.json +18 -0
  125. package/data/docs/ux-source-evidence/shared-portal-user-menu.json +34 -0
  126. package/data/manifests/blocks.json +230 -21
  127. package/data/manifests/brandbook-skill.json +33 -0
  128. package/data/manifests/catalog.json +1 -0
  129. package/data/manifests/patterns.json +358 -0
  130. package/data/manifests/theme-dextrum.json +132 -21
  131. package/data/manifests/themes.json +253 -39
  132. package/data/manifests/ui-patterns.json +1029 -0
  133. package/data/manifests/ux-examples.json +2177 -0
  134. package/data/manifests/ux-patterns.json +1135 -0
  135. package/data/manifests/ux-source-inventory.json +4669 -0
  136. package/data/ui-parts/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
  137. package/data/ui-parts/blocks/analytics-dashboard/analytics-dashboard.css +92 -0
  138. package/data/ui-parts/blocks/semantic-pattern-parts/AuthParts.tsx +48 -0
  139. package/data/ui-parts/blocks/semantic-pattern-parts/ChoiceCards.tsx +7 -0
  140. package/data/ui-parts/blocks/semantic-pattern-parts/ListFilters.tsx +9 -0
  141. package/data/ui-parts/blocks/semantic-pattern-parts/OutcomeNotice.tsx +7 -0
  142. package/data/ui-parts/blocks/semantic-pattern-parts/ProductParts.tsx +38 -0
  143. package/data/ui-parts/blocks/semantic-pattern-parts/ProgressSteps.tsx +6 -0
  144. package/data/ui-parts/blocks/semantic-pattern-parts/ReviewList.tsx +7 -0
  145. package/data/ui-parts/blocks/semantic-pattern-parts/ValidatedField.tsx +8 -0
  146. package/data/ui-parts/blocks/semantic-pattern-parts/WorkflowParts.tsx +339 -0
  147. package/data/ui-parts/blocks/semantic-pattern-parts/auth-parts.css +27 -0
  148. package/data/ui-parts/blocks/semantic-pattern-parts/pattern-parts.css +25 -0
  149. package/data/ui-parts/blocks/semantic-pattern-parts/product-parts.css +25 -0
  150. package/data/ui-parts/blocks/semantic-pattern-parts/types.ts +3 -0
  151. package/data/ui-parts/blocks/semantic-pattern-parts/workflow-parts.css +118 -0
  152. package/data/ui-parts/blocks/ux-pattern-examples/AccessExamples.tsx +46 -0
  153. package/data/ui-parts/blocks/ux-pattern-examples/AccountExamples.tsx +133 -0
  154. package/data/ui-parts/blocks/ux-pattern-examples/ProductExamples.tsx +79 -0
  155. package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExample.tsx +1761 -0
  156. package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExamples.tsx +190 -0
  157. package/data/ui-parts/blocks/ux-pattern-examples/copy.ts +6 -0
  158. package/data/ui-parts/blocks/ux-pattern-examples/ux-examples.css +18 -0
  159. package/data/ui-parts/blocks/ux-pattern-examples/workflow-copy.ts +76 -0
  160. package/data/ui-parts/blocks/ux-pattern-examples/workflow-definitions.ts +1291 -0
  161. package/data/ux-source-evidence/account-auth.json +41 -0
  162. package/data/ux-source-evidence/account-forgot-password.json +36 -0
  163. package/data/ux-source-evidence/account-invite.json +63 -0
  164. package/data/ux-source-evidence/account-locked.json +63 -0
  165. package/data/ux-source-evidence/account-login.json +28 -0
  166. package/data/ux-source-evidence/account-mfa.json +62 -0
  167. package/data/ux-source-evidence/account-onboarding-credentials.json +52 -0
  168. package/data/ux-source-evidence/account-onboarding-legal.json +56 -0
  169. package/data/ux-source-evidence/account-onboarding-orientation.json +66 -0
  170. package/data/ux-source-evidence/account-onboarding-profile.json +50 -0
  171. package/data/ux-source-evidence/account-onboarding.json +69 -0
  172. package/data/ux-source-evidence/account-reset-password.json +27 -0
  173. package/data/ux-source-evidence/account-session-expired.json +62 -0
  174. package/data/ux-source-evidence/account-signup.json +33 -0
  175. package/data/ux-source-evidence/account-verify-email.json +34 -0
  176. package/data/ux-source-evidence/account-welcome.json +41 -0
  177. package/data/ux-source-evidence/admin-analytics.json +54 -0
  178. package/data/ux-source-evidence/admin-audit.json +31 -0
  179. package/data/ux-source-evidence/admin-broadcasts-new.json +36 -0
  180. package/data/ux-source-evidence/admin-broadcasts.json +41 -0
  181. package/data/ux-source-evidence/admin-closures.json +52 -0
  182. package/data/ux-source-evidence/admin-devices.json +51 -0
  183. package/data/ux-source-evidence/admin-escalations.json +35 -0
  184. package/data/ux-source-evidence/admin-incidents.json +54 -0
  185. package/data/ux-source-evidence/admin-loading.json +17 -0
  186. package/data/ux-source-evidence/admin-onboarding-detail.json +80 -0
  187. package/data/ux-source-evidence/admin-onboarding.json +62 -0
  188. package/data/ux-source-evidence/admin-payments.json +45 -0
  189. package/data/ux-source-evidence/admin-providers.json +38 -0
  190. package/data/ux-source-evidence/admin-settings.json +18 -0
  191. package/data/ux-source-evidence/admin-subscriptions-detail-allocate.json +42 -0
  192. package/data/ux-source-evidence/admin-subscriptions-detail.json +48 -0
  193. package/data/ux-source-evidence/admin-subscriptions-new.json +43 -0
  194. package/data/ux-source-evidence/admin-subscriptions.json +41 -0
  195. package/data/ux-source-evidence/admin-transactions.json +41 -0
  196. package/data/ux-source-evidence/error.json +30 -0
  197. package/data/ux-source-evidence/loading.json +19 -0
  198. package/data/ux-source-evidence/marketing-blog.json +48 -0
  199. package/data/ux-source-evidence/marketing-contact.json +36 -0
  200. package/data/ux-source-evidence/marketing.json +64 -0
  201. package/data/ux-source-evidence/not-found.json +28 -0
  202. package/data/ux-source-evidence/portal-loading.json +17 -0
  203. package/data/ux-source-evidence/portal-mentor-messages.json +31 -0
  204. package/data/ux-source-evidence/portal-mentor-profile.json +23 -0
  205. package/data/ux-source-evidence/portal-mentor-requests-detail.json +54 -0
  206. package/data/ux-source-evidence/portal-mentor-sessions-detail-assess.json +42 -0
  207. package/data/ux-source-evidence/portal-mentor-settings.json +18 -0
  208. package/data/ux-source-evidence/portal-mentor.json +38 -0
  209. package/data/ux-source-evidence/portal-sessions-detail-live.json +48 -0
  210. package/data/ux-source-evidence/portal-sponsor-billing.json +28 -0
  211. package/data/ux-source-evidence/portal-sponsor-profile.json +22 -0
  212. package/data/ux-source-evidence/portal-sponsor-sessions.json +28 -0
  213. package/data/ux-source-evidence/portal-sponsor-settings.json +18 -0
  214. package/data/ux-source-evidence/portal-sponsor-trainees.json +36 -0
  215. package/data/ux-source-evidence/portal-sponsor.json +52 -0
  216. package/data/ux-source-evidence/portal-trainee-competency.json +42 -0
  217. package/data/ux-source-evidence/portal-trainee-credits.json +40 -0
  218. package/data/ux-source-evidence/portal-trainee-mentors.json +48 -0
  219. package/data/ux-source-evidence/portal-trainee-messages.json +32 -0
  220. package/data/ux-source-evidence/portal-trainee-notifications.json +35 -0
  221. package/data/ux-source-evidence/portal-trainee-profile.json +26 -0
  222. package/data/ux-source-evidence/portal-trainee-request.json +54 -0
  223. package/data/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +36 -0
  224. package/data/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +42 -0
  225. package/data/ux-source-evidence/portal-trainee-sessions-detail.json +55 -0
  226. package/data/ux-source-evidence/portal-trainee-sessions.json +36 -0
  227. package/data/ux-source-evidence/portal-trainee-settings-notifications.json +38 -0
  228. package/data/ux-source-evidence/portal-trainee-settings.json +18 -0
  229. package/data/ux-source-evidence/portal-trainee.json +44 -0
  230. package/data/ux-source-evidence/portal-tv-detail.json +39 -0
  231. package/data/ux-source-evidence/portal-tv.json +44 -0
  232. package/data/ux-source-evidence/shared-admin-admin-shell.json +35 -0
  233. package/data/ux-source-evidence/shared-auth-auth-primitives.json +105 -0
  234. package/data/ux-source-evidence/shared-auth-auth-shell.json +25 -0
  235. package/data/ux-source-evidence/shared-auth-onboarding-primitives.json +52 -0
  236. package/data/ux-source-evidence/shared-auth-onboarding-shell.json +28 -0
  237. package/data/ux-source-evidence/shared-auth-status-indicator.json +41 -0
  238. package/data/ux-source-evidence/shared-portal-empty-state.json +24 -0
  239. package/data/ux-source-evidence/shared-portal-loading-skeleton.json +27 -0
  240. package/data/ux-source-evidence/shared-portal-notifications-dropdown.json +39 -0
  241. package/data/ux-source-evidence/shared-portal-portal-shell.json +36 -0
  242. package/data/ux-source-evidence/shared-portal-profile-form.json +32 -0
  243. package/data/ux-source-evidence/shared-portal-settings-view.json +81 -0
  244. package/data/ux-source-evidence/shared-portal-usage-sparkline.json +18 -0
  245. package/data/ux-source-evidence/shared-portal-user-menu.json +34 -0
  246. package/lib/api.mjs +228 -4
  247. package/package.json +2 -2
@@ -0,0 +1,42 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/trainee/sessions/[id]/precheck/page.tsx",
4
+ "sha256": "f2f6afdc9b28525e803edfc4eac36394f76916e7c513f2ce1887c9752e03eacb",
5
+ "imports": [
6
+ "next/link",
7
+ "react",
8
+ "lucide-react",
9
+ "@/components/ui/button",
10
+ "@/components/ui/card",
11
+ "@/components/ui/badge",
12
+ "@/components/ui/separator",
13
+ "@/components/portal/portal-shell",
14
+ "@/components/auth/auth-primitives"
15
+ ],
16
+ "tokens": [
17
+ "--color-bg-subtle",
18
+ "--color-border-muted",
19
+ "--color-status-error-bg",
20
+ "--color-status-error-text",
21
+ "--color-status-success-bg",
22
+ "--color-status-success-text",
23
+ "--color-status-warning-bg",
24
+ "--color-status-warning-text",
25
+ "--color-text-muted",
26
+ "--color-text-primary",
27
+ "--color-text-secondary"
28
+ ],
29
+ "fragments": [
30
+ {
31
+ "id": "PreCheckPage",
32
+ "line": 26,
33
+ "code": "export default function PreCheckPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = use(params);\n const [results, setResults] = useState<Record<string, CheckResult>>({});\n const [activeIdx, setActiveIdx] = useState(0);\n\n const passCount = Object.values(results).filter((r) => r === \"pass\").length;\n const failCount = Object.values(results).filter((r) => r === \"fail\").length;\n const allDone = passCount + failCount === CHECKS.length;\n const allPass = passCount === CHECKS.length;\n\n const setResult = (id: string, r: CheckResult) => {\n setResults((s) => ({ ...s, [id]: r }));\n if (r === \"pass\" && activeIdx < CHECKS.length - 1) setActiveIdx(activeIdx + 1);\n };\n\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Yuki Tanaka\", subtitle: \"ST4 · GI\", initials: \"YT\" }}>\n <Link href={`/portal/trainee/sessions/${id}`} 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 session\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 Connectivity Pre-Check\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Verify your teleproctoring setup is ready. Complete at least 30 minutes before your session.\n </p>\n </div>\n\n {/* Status banner */}\n {allDone && allPass && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"success\" title=\"Pre-check complete\">\n Your site is ready for the session. Your mentor has been notified.\n </InlineAlert>\n </div>\n )}\n {allDone && !allPass && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"error\" title={`${failCount} check${failCount === 1 ? \"\" : \"s\"} failed`} action={<Button variant=\"outline\" size=\"sm\"><Phone size={11} /> Tech support</Button>}>\n We've alerted the Teleproctoring Provider technical team. Follow troubleshooting steps for each failed item.\n </InlineAlert>\n </div>\n )}\n\n <div className=\"grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6\">\n {/* Checklist */}\n <Card className=\"p-0\">\n <CardHeader className=\"px-5 py-4\">\n <CardTitle className=\"text-base\">Pre-flight checklist</CardTitle>\n <CardDescription>{passCount} of {CHECKS.length} complete</CardDescription>\n </CardHeader>\n\n <div className=\"divide-y\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n {CHECKS.map((check, i) => {\n const result = results[check.id] ?? \"pending\";\n const Icon = check.icon;\n const isOpen = i === activeIdx;\n return (\n <div key={check.id} className=\"px-5 py-4\">\n <button\n type=\"button\"\n onClick={() => setActiveIdx(i)}\n className=\"w-full flex items-center gap-3 text-left\"\n >\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{\n background: result === \"pass\" ? \"var(--color-status-success-bg)\" : result === \"fail\" ? \"var(--color-status-error-bg)\" : \"var(--color-bg-subtle)\",\n color: result === \"pass\" ? \"var(--color-status-success-text)\" : result === \"fail\" ? \"var(--color-status-error-text)\" : \"var(--color-text-secondary)\",\n }}>\n {result === \"pass\" ? <CheckCircle2 size={16} strokeWidth={2} /> : result === \"fail\" ? <XCircle size={16} strokeWidth={1.8} /> : <Icon size={16} strokeWidth={1.7} />}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">\n <span className=\"text-[10px] font-semibold tabular-nums mr-2\" style={{ color: \"var(--color-text-muted)\" }}>{i + 1}</span>\n {check.title}\n </div>\n {!isOpen && <div className=\"text-[11px] mt-0.5\" style={{ color: \"var(--color-text-muted)\" }}>{check.hint}</div>}\n </div>\n <Badge variant=\"outline\" className=\"text-[10px] capitalize\">\n {result === \"pass\" ? \"Pass\" : result === \"fail\" ? \"Fail\" : \"Pending\"}\n </Badge>\n </button>\n\n {isOpen && (\n <div className=\"mt-4 pl-12 space-y-3\">\n <p className=\"text-[12px]\" style={{ color: \"var(--color-text-secondary)\", lineHeight: 1.5 }}>{check.hint}</p>\n\n {check.id === \"video\" && (\n <div className=\"aspect-video rounded-lg flex items-center justify-center text-[11px]\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-muted)\" }}>\n Live camera feed mock — verify image is clear and unobstructed\n </div>\n )}\n {check.id === \"audio\" && (\n <div className=\"rounded-lg p-3 flex items-center gap-3\" style={{ background: \"var(--color-bg-subtle)\" }}>\n <Volume2 size={16} strokeWidth={1.7} style={{ color: \"var(--color-text-secondary)\" }} />\n <div className=\"flex-1 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>Test tone playing… can you hear it?</div>\n <Button variant=\"outline\" size=\"sm\"><RotateCw size={11} /> Replay</Button>\n </div>\n )}\n {check.id === \"bandwidth\" && (\n <div className=\"rounded-lg p-3 grid grid-cols-3 gap-3 text-center\" style={{ background: \"var(--color-bg-subtle)\" }}>\n <BwStat label=\"Download\" value=\"124 Mbps\" ok />\n <BwStat label=\"Upload\" value=\"48 Mbps\" ok />\n <BwStat label=\"Latency\" value=\"22 ms\" ok />\n </div>\n )}\n {check.id === \"failsafe\" && (\n <div className=\"rounded-lg p-3\" style={{ background: \"var(--color-status-warning-bg)\", color: \"var(--color-status-warning-text)\" }}>\n <p className=\"text-[11px] font-semibold uppercase tracking-wide mb-1.5\">Fail-safe protocol</p>\n <p className=\"text-[12px]\">If connectivity is lost mid-procedure, immediately revert to local supervision. Do not proceed without a senior colleague present.</p>\n </div>\n )}\n\n <div className=\"flex items-center gap-2 pt-1\">\n <Button variant=\"outline\" size=\"sm\" onClick={() => setResult(check.id, \"fail\")}>\n <XCircle size={11} /> Fail\n </Button>\n <Button variant=\"brand\" size=\"sm\" onClick={() => setResult(check.id, \"pass\")}>\n <CheckCircle2 size={11} /> Pass\n </Button>\n </div>\n </div>\n )}\n </div>\n );\n })}\n </div>\n\n {allDone && (\n <div className=\"px-5 py-4 border-t flex justify-end gap-2\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n <Link href={`/portal/trainee/sessions/${id}`}><Button variant=\"outline\">Back to session</Button></Link>\n {allPass && (\n <Link href={`/portal/sessions/${id}/live`}>\n <Button variant=\"brand\">Continue <ArrowRight size={13} /></Button>\n </Link>\n )}\n </div>\n )}\n </Card>\n\n {/* Sidebar */}\n <Card className=\"h-fit\">\n <CardHeader>\n <CardTitle className=\"text-[13px]\">If something fails</CardTitle>\n </CardHeader>\n <CardContent>\n <ul className=\"space-y-2.5 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <li className=\"flex items-start gap-2\"><AlertTriangle size={11} className=\"mt-0.5 shrink-0\" style={{ color: \"var(--color-status-warning-text)\" }} /> Tech support is automatically alerted</li>\n <li className=\"flex items-start gap-2\"><AlertTriangle size={11} className=\"mt-0.5 shrink-0\" style={{ color: \"var(--color-status-warning-text)\" }} /> You can choose to reschedule without losing your credit</li>\n <li className=\"flex items-start gap-2\"><AlertTriangle size={11} className=\"mt-0.5 shrink-0\" style={{ color: \"var(--color-status-warning-text)\" }} /> Site IT contact is on file for emergency assistance</li>\n </ul>\n <Separator className=\"my-4\" />\n <Button variant=\"outline\" size=\"sm\" className=\"w-full\">\n <Phone size={11} /> Call tech support\n </Button>\n </CardContent>\n </Card>\n </div>\n </PortalShell>\n );\n}"
34
+ },
35
+ {
36
+ "id": "BwStat",
37
+ "line": 189,
38
+ "code": "function BwStat({ label, value, ok }: { label: string; value: string; ok?: boolean }) {\n return (\n <div>\n <div className=\"text-[10px] uppercase tracking-wide\" style={{ color: \"var(--color-text-muted)\" }}>{label}</div>\n <div className=\"text-[13px] font-semibold tabular-nums mt-0.5\" style={{ color: ok ? \"var(--color-status-success-text)\" : \"var(--color-text-primary)\" }}>{value}</div>\n </div>\n );\n}"
39
+ }
40
+ ],
41
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { use, useState } from \"react\";\nimport {\n ArrowLeft, ArrowRight, CheckCircle2, XCircle, Camera, Volume2, Wifi, Shield,\n AlertTriangle, RotateCw, Phone,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\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 { InlineAlert } from \"@/components/auth/auth-primitives\";\n\ntype CheckResult = \"pending\" | \"pass\" | \"fail\";\n\nconst CHECKS = [\n { id: \"device\", icon: Camera, title: \"Device powered & camera positioned\", hint: \"Confirm the teleproctoring device is on and the camera is correctly positioned.\" },\n { id: \"video\", icon: Camera, title: \"Video quality check\", hint: \"Live camera feed should be clear and unobstructed.\" },\n { id: \"audio\", icon: Volume2, title: \"Audio quality check\", hint: \"Play test tone — you should hear it clearly.\" },\n { id: \"bandwidth\", icon: Wifi, title: \"Internet bandwidth\", hint: \"Upload/download speed test.\" },\n { id: \"failsafe\", icon: Shield, title: \"Fail-safe protocol acknowledged\", hint: \"Confirm you understand: if connectivity is lost, revert to local supervision.\" },\n] as const;\n\nexport default function PreCheckPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = use(params);\n const [results, setResults] = useState<Record<string, CheckResult>>({});\n const [activeIdx, setActiveIdx] = useState(0);\n\n const passCount = Object.values(results).filter((r) => r === \"pass\").length;\n const failCount = Object.values(results).filter((r) => r === \"fail\").length;\n const allDone = passCount + failCount === CHECKS.length;\n const allPass = passCount === CHECKS.length;\n\n const setResult = (id: string, r: CheckResult) => {\n setResults((s) => ({ ...s, [id]: r }));\n if (r === \"pass\" && activeIdx < CHECKS.length - 1) setActiveIdx(activeIdx + 1);\n };\n\n return (\n <PortalShell kind=\"trainee\" account={{ name: \"Dr. Yuki Tanaka\", subtitle: \"ST4 · GI\", initials: \"YT\" }}>\n <Link href={`/portal/trainee/sessions/${id}`} 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 session\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 Connectivity Pre-Check\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Verify your teleproctoring setup is ready. Complete at least 30 minutes before your session.\n </p>\n </div>\n\n {/* Status banner */}\n {allDone && allPass && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"success\" title=\"Pre-check complete\">\n Your site is ready for the session. Your mentor has been notified.\n </InlineAlert>\n </div>\n )}\n {allDone && !allPass && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"error\" title={`${failCount} check${failCount === 1 ? \"\" : \"s\"} failed`} action={<Button variant=\"outline\" size=\"sm\"><Phone size={11} /> Tech support</Button>}>\n We've alerted the Teleproctoring Provider technical team. Follow troubleshooting steps for each failed item.\n </InlineAlert>\n </div>\n )}\n\n <div className=\"grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6\">\n {/* Checklist */}\n <Card className=\"p-0\">\n <CardHeader className=\"px-5 py-4\">\n <CardTitle className=\"text-base\">Pre-flight checklist</CardTitle>\n <CardDescription>{passCount} of {CHECKS.length} complete</CardDescription>\n </CardHeader>\n\n <div className=\"divide-y\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n {CHECKS.map((check, i) => {\n const result = results[check.id] ?? \"pending\";\n const Icon = check.icon;\n const isOpen = i === activeIdx;\n return (\n <div key={check.id} className=\"px-5 py-4\">\n <button\n type=\"button\"\n onClick={() => setActiveIdx(i)}\n className=\"w-full flex items-center gap-3 text-left\"\n >\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{\n background: result === \"pass\" ? \"var(--color-status-success-bg)\" : result === \"fail\" ? \"var(--color-status-error-bg)\" : \"var(--color-bg-subtle)\",\n color: result === \"pass\" ? \"var(--color-status-success-text)\" : result === \"fail\" ? \"var(--color-status-error-text)\" : \"var(--color-text-secondary)\",\n }}>\n {result === \"pass\" ? <CheckCircle2 size={16} strokeWidth={2} /> : result === \"fail\" ? <XCircle size={16} strokeWidth={1.8} /> : <Icon size={16} strokeWidth={1.7} />}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">\n <span className=\"text-[10px] font-semibold tabular-nums mr-2\" style={{ color: \"var(--color-text-muted)\" }}>{i + 1}</span>\n {check.title}\n </div>\n {!isOpen && <div className=\"text-[11px] mt-0.5\" style={{ color: \"var(--color-text-muted)\" }}>{check.hint}</div>}\n </div>\n <Badge variant=\"outline\" className=\"text-[10px] capitalize\">\n {result === \"pass\" ? \"Pass\" : result === \"fail\" ? \"Fail\" : \"Pending\"}\n </Badge>\n </button>\n\n {isOpen && (\n <div className=\"mt-4 pl-12 space-y-3\">\n <p className=\"text-[12px]\" style={{ color: \"var(--color-text-secondary)\", lineHeight: 1.5 }}>{check.hint}</p>\n\n {check.id === \"video\" && (\n <div className=\"aspect-video rounded-lg flex items-center justify-center text-[11px]\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-muted)\" }}>\n Live camera feed mock — verify image is clear and unobstructed\n </div>\n )}\n {check.id === \"audio\" && (\n <div className=\"rounded-lg p-3 flex items-center gap-3\" style={{ background: \"var(--color-bg-subtle)\" }}>\n <Volume2 size={16} strokeWidth={1.7} style={{ color: \"var(--color-text-secondary)\" }} />\n <div className=\"flex-1 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>Test tone playing… can you hear it?</div>\n <Button variant=\"outline\" size=\"sm\"><RotateCw size={11} /> Replay</Button>\n </div>\n )}\n {check.id === \"bandwidth\" && (\n <div className=\"rounded-lg p-3 grid grid-cols-3 gap-3 text-center\" style={{ background: \"var(--color-bg-subtle)\" }}>\n <BwStat label=\"Download\" value=\"124 Mbps\" ok />\n <BwStat label=\"Upload\" value=\"48 Mbps\" ok />\n <BwStat label=\"Latency\" value=\"22 ms\" ok />\n </div>\n )}\n {check.id === \"failsafe\" && (\n <div className=\"rounded-lg p-3\" style={{ background: \"var(--color-status-warning-bg)\", color: \"var(--color-status-warning-text)\" }}>\n <p className=\"text-[11px] font-semibold uppercase tracking-wide mb-1.5\">Fail-safe protocol</p>\n <p className=\"text-[12px]\">If connectivity is lost mid-procedure, immediately revert to local supervision. Do not proceed without a senior colleague present.</p>\n </div>\n )}\n\n <div className=\"flex items-center gap-2 pt-1\">\n <Button variant=\"outline\" size=\"sm\" onClick={() => setResult(check.id, \"fail\")}>\n <XCircle size={11} /> Fail\n </Button>\n <Button variant=\"brand\" size=\"sm\" onClick={() => setResult(check.id, \"pass\")}>\n <CheckCircle2 size={11} /> Pass\n </Button>\n </div>\n </div>\n )}\n </div>\n );\n })}\n </div>\n\n {allDone && (\n <div className=\"px-5 py-4 border-t flex justify-end gap-2\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n <Link href={`/portal/trainee/sessions/${id}`}><Button variant=\"outline\">Back to session</Button></Link>\n {allPass && (\n <Link href={`/portal/sessions/${id}/live`}>\n <Button variant=\"brand\">Continue <ArrowRight size={13} /></Button>\n </Link>\n )}\n </div>\n )}\n </Card>\n\n {/* Sidebar */}\n <Card className=\"h-fit\">\n <CardHeader>\n <CardTitle className=\"text-[13px]\">If something fails</CardTitle>\n </CardHeader>\n <CardContent>\n <ul className=\"space-y-2.5 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <li className=\"flex items-start gap-2\"><AlertTriangle size={11} className=\"mt-0.5 shrink-0\" style={{ color: \"var(--color-status-warning-text)\" }} /> Tech support is automatically alerted</li>\n <li className=\"flex items-start gap-2\"><AlertTriangle size={11} className=\"mt-0.5 shrink-0\" style={{ color: \"var(--color-status-warning-text)\" }} /> You can choose to reschedule without losing your credit</li>\n <li className=\"flex items-start gap-2\"><AlertTriangle size={11} className=\"mt-0.5 shrink-0\" style={{ color: \"var(--color-status-warning-text)\" }} /> Site IT contact is on file for emergency assistance</li>\n </ul>\n <Separator className=\"my-4\" />\n <Button variant=\"outline\" size=\"sm\" className=\"w-full\">\n <Phone size={11} /> Call tech support\n </Button>\n </CardContent>\n </Card>\n </div>\n </PortalShell>\n );\n}\n\nfunction BwStat({ label, value, ok }: { label: string; value: string; ok?: boolean }) {\n return (\n <div>\n <div className=\"text-[10px] uppercase tracking-wide\" style={{ color: \"var(--color-text-muted)\" }}>{label}</div>\n <div className=\"text-[13px] font-semibold tabular-nums mt-0.5\" style={{ color: ok ? \"var(--color-status-success-text)\" : \"var(--color-text-primary)\" }}>{value}</div>\n </div>\n );\n}\n"
42
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/trainee/sessions/[id]/page.tsx",
4
+ "sha256": "f7c3b7146c75781e4c6ac5016ccb7d32258b66e6c293842876c45c6f8a04184a",
5
+ "imports": [
6
+ "next/link",
7
+ "react",
8
+ "lucide-react",
9
+ "@/components/ui/button",
10
+ "@/components/ui/input",
11
+ "@/components/ui/textarea",
12
+ "@/components/ui/badge",
13
+ "@/components/ui/card",
14
+ "@/components/ui/tabs",
15
+ "@/components/ui/separator",
16
+ "@/components/portal/portal-shell",
17
+ "@/components/auth/auth-primitives",
18
+ "@/lib/sessions-mock",
19
+ "@/lib/sessions"
20
+ ],
21
+ "tokens": [
22
+ "--color-bg-brand-muted",
23
+ "--color-bg-subtle",
24
+ "--color-border-default",
25
+ "--color-border-muted",
26
+ "--color-status-success-bg",
27
+ "--color-status-success-text",
28
+ "--color-status-warning-bg",
29
+ "--color-status-warning-text",
30
+ "--color-text-brand",
31
+ "--color-text-muted",
32
+ "--color-text-primary",
33
+ "--color-text-secondary",
34
+ "--gray-950",
35
+ "--teal-500"
36
+ ],
37
+ "fragments": [
38
+ {
39
+ "id": "SessionDetailPage",
40
+ "line": 21,
41
+ "code": "export default function SessionDetailPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = use(params);\n const req = REQUESTS.find((r) => r.id === id) ?? REQUESTS[3];\n const thread = CHAT_THREADS[req.id] ?? [];\n const docs = SESSION_DOCS[req.id] ?? [];\n const record = ALIGNMENT_RECORDS[req.id];\n\n const [msg, setMsg] = useState(\"\");\n const [messages, setMessages] = useState(thread);\n\n const send = () => {\n if (!msg.trim()) return;\n setMessages([...messages, { id: `m${Date.now()}`, from: \"trainee\", author: \"Dr. Yuki Tanaka\", text: msg, at: new Date().toISOString() }]);\n setMsg(\"\");\n };\n\n const h = hoursUntil(req.scheduledFor);\n const within24h = h <= 24;\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\n </Link>\n\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 {req.procedure}\n </h1>\n <div className=\"flex items-center gap-2\">\n <Badge variant=\"outline\" className=\"capitalize\">{req.status.replace(\"_\", \" \")}</Badge>\n <span className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n with {req.mentorName} · {fmtDateTime(req.scheduledFor)}\n </span>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {within24h && (\n <Link href={`/portal/trainee/sessions/${req.id}/precheck`}>\n <Button variant=\"brand\">Start connectivity pre-check <ArrowRight size={13} /></Button>\n </Link>\n )}\n </div>\n </div>\n\n {within24h && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"warning\" title=\"Session is in less than 24 hours\">\n Please complete the Connectivity Pre-Check at least 30 minutes before your procedure.\n </InlineAlert>\n </div>\n )}\n\n <div className=\"grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6\">\n {/* Main column with tabs */}\n <div className=\"space-y-5\">\n <Tabs defaultValue=\"alignment\">\n <TabsList>\n <TabsTrigger value=\"alignment\">Alignment record</TabsTrigger>\n <TabsTrigger value=\"docs\">Documents ({docs.length})</TabsTrigger>\n <TabsTrigger value=\"chat\">Chat</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"alignment\">\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div>\n <CardTitle className=\"text-base flex items-center gap-2\">\n <ClipboardCheck size={15} /> Pre-Session Alignment Record\n </CardTitle>\n <CardDescription>{record ? `Created ${new Date(record.createdAt).toLocaleString()}` : \"Not yet created by mentor\"}</CardDescription>\n </div>\n {record && <Badge className=\"text-[10px]\" style={{ background: \"var(--color-status-success-bg)\", color: \"var(--color-status-success-text)\" }}>Appropriate for trainee level</Badge>}\n </div>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n {!record ? (\n <div className=\"rounded-lg border border-dashed p-6 text-center\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <p className=\"text-[13px] mb-1\" style={{ color: \"var(--color-text-secondary)\" }}>Your mentor will create this record after your pre-session consultation.</p>\n <p className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>It captures learning goals, planned strategy, anticipated challenges, and escalation protocol.</p>\n </div>\n ) : (\n <>\n <Field label=\"Agreed learning goals\" value={record.learningGoals} />\n <Field label=\"Planned procedural strategy\" value={record.procedural} />\n <div className=\"grid grid-cols-2 gap-4\">\n <Field label=\"Anticipated challenges\" value={record.challenges} />\n <Field label=\"Contingency plans\" value={record.contingency} />\n </div>\n <div className=\"rounded-lg p-3 flex items-start gap-3\" style={{ background: \"var(--color-status-warning-bg)\" }}>\n <Shield size={14} className=\"mt-0.5 shrink-0\" style={{ color: \"var(--color-status-warning-text)\" }} />\n <div>\n <p className=\"text-[11px] font-semibold uppercase tracking-wide\" style={{ color: \"var(--color-status-warning-text)\" }}>Fail-safe protocol</p>\n <p className=\"text-[12px] mt-0.5\" style={{ color: \"var(--color-status-warning-text)\" }}>{record.escalation}</p>\n </div>\n </div>\n </>\n )}\n </CardContent>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"docs\">\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div>\n <CardTitle className=\"text-base\">Shared document workspace</CardTitle>\n <CardDescription>Case files visible to both you and {req.mentorName}.</CardDescription>\n </div>\n <Button variant=\"outline\" size=\"sm\"><Upload size={12} /> Upload</Button>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-2\">\n {docs.map((doc) => (\n <div key={doc.id} className=\"rounded-lg border p-3 flex items-center gap-3\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"w-10 h-10 rounded-md flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n {doc.kind === \"image\" ? <ImageIcon size={16} strokeWidth={1.8} /> : <FileText size={16} strokeWidth={1.8} />}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[12px] font-medium truncate\">{doc.name}</div>\n <div className=\"text-[10px]\" style={{ color: \"var(--color-text-muted)\" }}>{doc.size} · {doc.uploader}</div>\n </div>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"chat\">\n <Card className=\"overflow-hidden\">\n <CardHeader>\n <CardTitle className=\"text-base\">Private chat with {req.mentorName}</CardTitle>\n </CardHeader>\n <CardContent className=\"p-0\">\n <div className=\"px-5 py-4 space-y-3 max-h-[480px] overflow-y-auto\" style={{ borderTop: \"1px solid var(--color-border-muted)\" }}>\n {messages.map((m) => (\n <div key={m.id} className={`flex gap-3 ${m.from === \"trainee\" ? \"flex-row-reverse\" : \"\"}`}>\n <div className=\"w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0\" style={{ background: m.from === \"trainee\" ? \"var(--color-bg-brand-muted)\" : \"var(--teal-500)\", color: m.from === \"trainee\" ? \"var(--color-text-brand)\" : \"var(--gray-950)\" }}>\n {m.author.split(\" \").slice(-1)[0][0]}\n </div>\n <div className={`flex-1 max-w-[80%] ${m.from === \"trainee\" ? \"text-right\" : \"\"}`}>\n <div className=\"text-[10px] mb-1\" style={{ color: \"var(--color-text-muted)\" }}>\n {m.author} · {new Date(m.at).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" })}\n </div>\n <div\n className=\"inline-block px-3 py-2 rounded-2xl text-[13px] text-left\"\n style={{\n background: m.from === \"trainee\" ? \"var(--teal-500)\" : \"var(--color-bg-subtle)\",\n color: m.from === \"trainee\" ? \"var(--gray-950)\" : \"var(--color-text-primary)\",\n borderTopLeftRadius: m.from === \"mentor\" ? 4 : undefined,\n borderTopRightRadius: m.from === \"trainee\" ? 4 : undefined,\n }}\n >\n {m.text}\n </div>\n </div>\n </div>\n ))}\n </div>\n <div className=\"p-3 border-t flex items-center gap-2\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n <Button variant=\"ghost\" size=\"icon-sm\"><Paperclip size={14} /></Button>\n <Input\n value={msg}\n onChange={(e) => setMsg(e.target.value)}\n onKeyDown={(e) => { if (e.key === \"Enter\") { e.preventDefault(); send(); } }}\n placeholder=\"Type a message…\"\n className=\"h-9\"\n />\n <Button variant=\"brand\" size=\"icon-sm\" disabled={!msg.trim()} onClick={send} aria-label=\"Send\"><Send size={13} /></Button>\n </div>\n </CardContent>\n </Card>\n </TabsContent>\n </Tabs>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-4\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-[13px]\">Session quick actions</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <Button variant=\"outline\" size=\"sm\" className=\"w-full justify-start\"><Video size={12} /> Schedule pre-session video call</Button>\n <Button variant=\"outline\" size=\"sm\" className=\"w-full justify-start\"><Upload size={12} /> Add case documents</Button>\n <Link href={`/portal/sessions/${req.id}/live`}>\n <Button variant=\"brand\" size=\"sm\" className=\"w-full justify-start\"><Activity size={12} /> Join live session</Button>\n </Link>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-[13px] flex items-center gap-2\"><Calendar size={13} /> Schedule</CardTitle>\n </CardHeader>\n <CardContent>\n <dl className=\"space-y-2.5 text-[12px]\">\n <DRowSmall label=\"Procedure\" value={fmtDateTime(req.scheduledFor)} />\n <DRowSmall label=\"Lead time\" value={`in ${h}h`} />\n <DRowSmall label=\"Pre-check\" value=\"30 min before\" />\n <DRowSmall label=\"Complexity\" value={req.complexity} />\n </dl>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-[13px]\">Mentor</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-center gap-3\">\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center text-[11px] font-semibold shrink-0\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>\n {req.mentorName?.split(\" \").slice(-1)[0][0] ?? \"M\"}\n </div>\n <div className=\"min-w-0\">\n <div className=\"text-[13px] font-medium truncate\">{req.mentorName}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>Senior Endoscopist</div>\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </PortalShell>\n );\n}"
42
+ },
43
+ {
44
+ "id": "Field",
45
+ "line": 253,
46
+ "code": "function Field({ label, value }: { label: string; value: string }) {\n return (\n <div>\n <p className=\"text-[10px] font-semibold uppercase tracking-wide mb-1\" style={{ color: \"var(--color-text-muted)\" }}>{label}</p>\n <p className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\", lineHeight: 1.55 }}>{value}</p>\n </div>\n );\n}"
47
+ },
48
+ {
49
+ "id": "DRowSmall",
50
+ "line": 262,
51
+ "code": "function DRowSmall({ label, value }: { label: string; value: string }) {\n return (\n <div className=\"flex items-center justify-between gap-2\">\n <dt style={{ color: \"var(--color-text-muted)\" }}>{label}</dt>\n <dd className=\"text-right\" style={{ color: \"var(--color-text-primary)\" }}>{value}</dd>\n </div>\n );\n}"
52
+ }
53
+ ],
54
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { use, useState } from \"react\";\nimport {\n ArrowLeft, ArrowRight, Send, Video, FileText, Image as ImageIcon, Upload,\n Calendar, ClipboardCheck, Activity, Shield, Plus, Paperclip,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card, CardHeader, CardTitle, CardDescription, CardContent } from \"@/components/ui/card\";\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { InlineAlert } from \"@/components/auth/auth-primitives\";\nimport { REQUESTS, CHAT_THREADS, SESSION_DOCS, ALIGNMENT_RECORDS } from \"@/lib/sessions-mock\";\nimport { fmtDateTime, hoursUntil } from \"@/lib/sessions\";\n\nexport default function SessionDetailPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = use(params);\n const req = REQUESTS.find((r) => r.id === id) ?? REQUESTS[3];\n const thread = CHAT_THREADS[req.id] ?? [];\n const docs = SESSION_DOCS[req.id] ?? [];\n const record = ALIGNMENT_RECORDS[req.id];\n\n const [msg, setMsg] = useState(\"\");\n const [messages, setMessages] = useState(thread);\n\n const send = () => {\n if (!msg.trim()) return;\n setMessages([...messages, { id: `m${Date.now()}`, from: \"trainee\", author: \"Dr. Yuki Tanaka\", text: msg, at: new Date().toISOString() }]);\n setMsg(\"\");\n };\n\n const h = hoursUntil(req.scheduledFor);\n const within24h = h <= 24;\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\n </Link>\n\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 {req.procedure}\n </h1>\n <div className=\"flex items-center gap-2\">\n <Badge variant=\"outline\" className=\"capitalize\">{req.status.replace(\"_\", \" \")}</Badge>\n <span className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n with {req.mentorName} · {fmtDateTime(req.scheduledFor)}\n </span>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {within24h && (\n <Link href={`/portal/trainee/sessions/${req.id}/precheck`}>\n <Button variant=\"brand\">Start connectivity pre-check <ArrowRight size={13} /></Button>\n </Link>\n )}\n </div>\n </div>\n\n {within24h && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"warning\" title=\"Session is in less than 24 hours\">\n Please complete the Connectivity Pre-Check at least 30 minutes before your procedure.\n </InlineAlert>\n </div>\n )}\n\n <div className=\"grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6\">\n {/* Main column with tabs */}\n <div className=\"space-y-5\">\n <Tabs defaultValue=\"alignment\">\n <TabsList>\n <TabsTrigger value=\"alignment\">Alignment record</TabsTrigger>\n <TabsTrigger value=\"docs\">Documents ({docs.length})</TabsTrigger>\n <TabsTrigger value=\"chat\">Chat</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"alignment\">\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div>\n <CardTitle className=\"text-base flex items-center gap-2\">\n <ClipboardCheck size={15} /> Pre-Session Alignment Record\n </CardTitle>\n <CardDescription>{record ? `Created ${new Date(record.createdAt).toLocaleString()}` : \"Not yet created by mentor\"}</CardDescription>\n </div>\n {record && <Badge className=\"text-[10px]\" style={{ background: \"var(--color-status-success-bg)\", color: \"var(--color-status-success-text)\" }}>Appropriate for trainee level</Badge>}\n </div>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n {!record ? (\n <div className=\"rounded-lg border border-dashed p-6 text-center\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <p className=\"text-[13px] mb-1\" style={{ color: \"var(--color-text-secondary)\" }}>Your mentor will create this record after your pre-session consultation.</p>\n <p className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>It captures learning goals, planned strategy, anticipated challenges, and escalation protocol.</p>\n </div>\n ) : (\n <>\n <Field label=\"Agreed learning goals\" value={record.learningGoals} />\n <Field label=\"Planned procedural strategy\" value={record.procedural} />\n <div className=\"grid grid-cols-2 gap-4\">\n <Field label=\"Anticipated challenges\" value={record.challenges} />\n <Field label=\"Contingency plans\" value={record.contingency} />\n </div>\n <div className=\"rounded-lg p-3 flex items-start gap-3\" style={{ background: \"var(--color-status-warning-bg)\" }}>\n <Shield size={14} className=\"mt-0.5 shrink-0\" style={{ color: \"var(--color-status-warning-text)\" }} />\n <div>\n <p className=\"text-[11px] font-semibold uppercase tracking-wide\" style={{ color: \"var(--color-status-warning-text)\" }}>Fail-safe protocol</p>\n <p className=\"text-[12px] mt-0.5\" style={{ color: \"var(--color-status-warning-text)\" }}>{record.escalation}</p>\n </div>\n </div>\n </>\n )}\n </CardContent>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"docs\">\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div>\n <CardTitle className=\"text-base\">Shared document workspace</CardTitle>\n <CardDescription>Case files visible to both you and {req.mentorName}.</CardDescription>\n </div>\n <Button variant=\"outline\" size=\"sm\"><Upload size={12} /> Upload</Button>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-2\">\n {docs.map((doc) => (\n <div key={doc.id} className=\"rounded-lg border p-3 flex items-center gap-3\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"w-10 h-10 rounded-md flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n {doc.kind === \"image\" ? <ImageIcon size={16} strokeWidth={1.8} /> : <FileText size={16} strokeWidth={1.8} />}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[12px] font-medium truncate\">{doc.name}</div>\n <div className=\"text-[10px]\" style={{ color: \"var(--color-text-muted)\" }}>{doc.size} · {doc.uploader}</div>\n </div>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"chat\">\n <Card className=\"overflow-hidden\">\n <CardHeader>\n <CardTitle className=\"text-base\">Private chat with {req.mentorName}</CardTitle>\n </CardHeader>\n <CardContent className=\"p-0\">\n <div className=\"px-5 py-4 space-y-3 max-h-[480px] overflow-y-auto\" style={{ borderTop: \"1px solid var(--color-border-muted)\" }}>\n {messages.map((m) => (\n <div key={m.id} className={`flex gap-3 ${m.from === \"trainee\" ? \"flex-row-reverse\" : \"\"}`}>\n <div className=\"w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0\" style={{ background: m.from === \"trainee\" ? \"var(--color-bg-brand-muted)\" : \"var(--teal-500)\", color: m.from === \"trainee\" ? \"var(--color-text-brand)\" : \"var(--gray-950)\" }}>\n {m.author.split(\" \").slice(-1)[0][0]}\n </div>\n <div className={`flex-1 max-w-[80%] ${m.from === \"trainee\" ? \"text-right\" : \"\"}`}>\n <div className=\"text-[10px] mb-1\" style={{ color: \"var(--color-text-muted)\" }}>\n {m.author} · {new Date(m.at).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" })}\n </div>\n <div\n className=\"inline-block px-3 py-2 rounded-2xl text-[13px] text-left\"\n style={{\n background: m.from === \"trainee\" ? \"var(--teal-500)\" : \"var(--color-bg-subtle)\",\n color: m.from === \"trainee\" ? \"var(--gray-950)\" : \"var(--color-text-primary)\",\n borderTopLeftRadius: m.from === \"mentor\" ? 4 : undefined,\n borderTopRightRadius: m.from === \"trainee\" ? 4 : undefined,\n }}\n >\n {m.text}\n </div>\n </div>\n </div>\n ))}\n </div>\n <div className=\"p-3 border-t flex items-center gap-2\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n <Button variant=\"ghost\" size=\"icon-sm\"><Paperclip size={14} /></Button>\n <Input\n value={msg}\n onChange={(e) => setMsg(e.target.value)}\n onKeyDown={(e) => { if (e.key === \"Enter\") { e.preventDefault(); send(); } }}\n placeholder=\"Type a message…\"\n className=\"h-9\"\n />\n <Button variant=\"brand\" size=\"icon-sm\" disabled={!msg.trim()} onClick={send} aria-label=\"Send\"><Send size={13} /></Button>\n </div>\n </CardContent>\n </Card>\n </TabsContent>\n </Tabs>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-4\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-[13px]\">Session quick actions</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <Button variant=\"outline\" size=\"sm\" className=\"w-full justify-start\"><Video size={12} /> Schedule pre-session video call</Button>\n <Button variant=\"outline\" size=\"sm\" className=\"w-full justify-start\"><Upload size={12} /> Add case documents</Button>\n <Link href={`/portal/sessions/${req.id}/live`}>\n <Button variant=\"brand\" size=\"sm\" className=\"w-full justify-start\"><Activity size={12} /> Join live session</Button>\n </Link>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-[13px] flex items-center gap-2\"><Calendar size={13} /> Schedule</CardTitle>\n </CardHeader>\n <CardContent>\n <dl className=\"space-y-2.5 text-[12px]\">\n <DRowSmall label=\"Procedure\" value={fmtDateTime(req.scheduledFor)} />\n <DRowSmall label=\"Lead time\" value={`in ${h}h`} />\n <DRowSmall label=\"Pre-check\" value=\"30 min before\" />\n <DRowSmall label=\"Complexity\" value={req.complexity} />\n </dl>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-[13px]\">Mentor</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-center gap-3\">\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center text-[11px] font-semibold shrink-0\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>\n {req.mentorName?.split(\" \").slice(-1)[0][0] ?? \"M\"}\n </div>\n <div className=\"min-w-0\">\n <div className=\"text-[13px] font-medium truncate\">{req.mentorName}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>Senior Endoscopist</div>\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </PortalShell>\n );\n}\n\nfunction Field({ label, value }: { label: string; value: string }) {\n return (\n <div>\n <p className=\"text-[10px] font-semibold uppercase tracking-wide mb-1\" style={{ color: \"var(--color-text-muted)\" }}>{label}</p>\n <p className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\", lineHeight: 1.55 }}>{value}</p>\n </div>\n );\n}\n\nfunction DRowSmall({ label, value }: { label: string; value: string }) {\n return (\n <div className=\"flex items-center justify-between gap-2\">\n <dt style={{ color: \"var(--color-text-muted)\" }}>{label}</dt>\n <dd className=\"text-right\" style={{ color: \"var(--color-text-primary)\" }}>{value}</dd>\n </div>\n );\n}\n"
55
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/trainee/sessions/page.tsx",
4
+ "sha256": "f3ea3086ebdd93ca28b2f58f0ccf743737b42b784a8cf774b7b4e43c3607a0ab",
5
+ "imports": [
6
+ "next/link",
7
+ "lucide-react",
8
+ "@/components/ui/button",
9
+ "@/components/ui/badge",
10
+ "@/components/ui/card",
11
+ "@/components/ui/tabs",
12
+ "@/components/ui/table",
13
+ "@/components/portal/portal-shell",
14
+ "@/components/portal/empty-state",
15
+ "@/lib/sessions-mock",
16
+ "@/lib/sessions"
17
+ ],
18
+ "tokens": [
19
+ "--color-bg-subtle",
20
+ "--color-text-muted",
21
+ "--color-text-secondary"
22
+ ],
23
+ "fragments": [
24
+ {
25
+ "id": "TraineeSessionsPage",
26
+ "line": 15,
27
+ "code": "export default function TraineeSessionsPage() {\n const upcoming = REQUESTS.filter((r) => r.status === \"confirmed\" && r.traineeName === \"Dr. Yuki Tanaka\");\n const pending = REQUESTS.filter((r) => r.status === \"pending\");\n const past: typeof REQUESTS = []; // demo empty\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 My sessions\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Upcoming, pending, and past mentorship sessions.\n </p>\n </div>\n <Link href=\"/portal/trainee/request\">\n <Button variant=\"brand\">Request session <ArrowRight size={13} /></Button>\n </Link>\n </div>\n\n <Tabs defaultValue=\"upcoming\">\n <TabsList>\n <TabsTrigger value=\"upcoming\">Upcoming ({upcoming.length})</TabsTrigger>\n <TabsTrigger value=\"pending\">Pending ({pending.length})</TabsTrigger>\n <TabsTrigger value=\"past\">Past ({past.length})</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"upcoming\">\n {upcoming.length === 0 ? (\n <EmptyState\n icon={Calendar}\n title=\"No upcoming sessions\"\n description=\"Once a mentor accepts your request, your confirmed sessions will appear here.\"\n action={<Link href=\"/portal/trainee/request\"><Button variant=\"brand\">Request a session <ArrowRight size={13} /></Button></Link>}\n />\n ) : (\n <SessionsTable rows={upcoming} />\n )}\n </TabsContent>\n\n <TabsContent value=\"pending\">\n {pending.length === 0 ? (\n <EmptyState icon={Calendar} title=\"No pending requests\" description=\"You don't have any session requests awaiting acceptance.\" />\n ) : (\n <SessionsTable rows={pending} />\n )}\n </TabsContent>\n\n <TabsContent value=\"past\">\n <EmptyState\n icon={Calendar}\n title=\"No past sessions yet\"\n description=\"After you complete your first mentorship session, it will appear here with feedback and recordings.\"\n />\n </TabsContent>\n </Tabs>\n </PortalShell>\n );\n}"
28
+ },
29
+ {
30
+ "id": "SessionsTable",
31
+ "line": 76,
32
+ "code": "function SessionsTable({ rows }: { rows: typeof REQUESTS }) {\n return (\n <Card className=\"p-0\">\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)\" }}>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\" style={{ color: \"var(--color-text-muted)\" }}>When</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Status</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Action</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {rows.map((r) => (\n <TableRow key={r.id}>\n <TableCell className=\"px-5 py-3 text-[12px] font-medium\">{r.procedure}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{r.mentorName ?? \"—\"}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\">{fmtDateTime(r.scheduledFor)}</TableCell>\n <TableCell className=\"px-5 py-3\"><Badge variant=\"outline\" className=\"text-[10px] capitalize\">{r.status}</Badge></TableCell>\n <TableCell className=\"px-5 py-3 text-right\">\n <Link href={`/portal/trainee/sessions/${r.id}`}><Button variant=\"outline\" size=\"sm\">Open <ArrowRight size={11} /></Button></Link>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </Card>\n );\n}"
33
+ }
34
+ ],
35
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { Calendar, ArrowRight } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card } from \"@/components/ui/card\";\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from \"@/components/ui/table\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { EmptyState } from \"@/components/portal/empty-state\";\nimport { REQUESTS } from \"@/lib/sessions-mock\";\nimport { fmtDateTime } from \"@/lib/sessions\";\n\nexport default function TraineeSessionsPage() {\n const upcoming = REQUESTS.filter((r) => r.status === \"confirmed\" && r.traineeName === \"Dr. Yuki Tanaka\");\n const pending = REQUESTS.filter((r) => r.status === \"pending\");\n const past: typeof REQUESTS = []; // demo empty\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 My sessions\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Upcoming, pending, and past mentorship sessions.\n </p>\n </div>\n <Link href=\"/portal/trainee/request\">\n <Button variant=\"brand\">Request session <ArrowRight size={13} /></Button>\n </Link>\n </div>\n\n <Tabs defaultValue=\"upcoming\">\n <TabsList>\n <TabsTrigger value=\"upcoming\">Upcoming ({upcoming.length})</TabsTrigger>\n <TabsTrigger value=\"pending\">Pending ({pending.length})</TabsTrigger>\n <TabsTrigger value=\"past\">Past ({past.length})</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"upcoming\">\n {upcoming.length === 0 ? (\n <EmptyState\n icon={Calendar}\n title=\"No upcoming sessions\"\n description=\"Once a mentor accepts your request, your confirmed sessions will appear here.\"\n action={<Link href=\"/portal/trainee/request\"><Button variant=\"brand\">Request a session <ArrowRight size={13} /></Button></Link>}\n />\n ) : (\n <SessionsTable rows={upcoming} />\n )}\n </TabsContent>\n\n <TabsContent value=\"pending\">\n {pending.length === 0 ? (\n <EmptyState icon={Calendar} title=\"No pending requests\" description=\"You don't have any session requests awaiting acceptance.\" />\n ) : (\n <SessionsTable rows={pending} />\n )}\n </TabsContent>\n\n <TabsContent value=\"past\">\n <EmptyState\n icon={Calendar}\n title=\"No past sessions yet\"\n description=\"After you complete your first mentorship session, it will appear here with feedback and recordings.\"\n />\n </TabsContent>\n </Tabs>\n </PortalShell>\n );\n}\n\nfunction SessionsTable({ rows }: { rows: typeof REQUESTS }) {\n return (\n <Card className=\"p-0\">\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)\" }}>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\" style={{ color: \"var(--color-text-muted)\" }}>When</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5\" style={{ color: \"var(--color-text-muted)\" }}>Status</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider px-5 text-right\" style={{ color: \"var(--color-text-muted)\" }}>Action</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {rows.map((r) => (\n <TableRow key={r.id}>\n <TableCell className=\"px-5 py-3 text-[12px] font-medium\">{r.procedure}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{r.mentorName ?? \"—\"}</TableCell>\n <TableCell className=\"px-5 py-3 text-[12px]\">{fmtDateTime(r.scheduledFor)}</TableCell>\n <TableCell className=\"px-5 py-3\"><Badge variant=\"outline\" className=\"text-[10px] capitalize\">{r.status}</Badge></TableCell>\n <TableCell className=\"px-5 py-3 text-right\">\n <Link href={`/portal/trainee/sessions/${r.id}`}><Button variant=\"outline\" size=\"sm\">Open <ArrowRight size={11} /></Button></Link>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </Card>\n );\n}\n"
36
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/trainee/settings/notifications/page.tsx",
4
+ "sha256": "c91c8d010e74e519364aa1451226d6c4c1a542b2bba19784405b48a18caebea9",
5
+ "imports": [
6
+ "next/link",
7
+ "react",
8
+ "lucide-react",
9
+ "@/components/ui/button",
10
+ "@/components/ui/card",
11
+ "@/components/ui/badge",
12
+ "@/components/ui/separator",
13
+ "@/components/portal/portal-shell",
14
+ "@/components/auth/auth-primitives"
15
+ ],
16
+ "tokens": [
17
+ "--color-bg-brand-muted",
18
+ "--color-bg-subtle",
19
+ "--color-border-brand",
20
+ "--color-border-default",
21
+ "--color-border-muted",
22
+ "--color-status-success-border",
23
+ "--color-status-success-text",
24
+ "--color-status-warning-border",
25
+ "--color-status-warning-text",
26
+ "--color-text-brand",
27
+ "--color-text-muted",
28
+ "--color-text-secondary"
29
+ ],
30
+ "fragments": [
31
+ {
32
+ "id": "NotificationPreferencesPage",
33
+ "line": 37,
34
+ "code": "export default function NotificationPreferencesPage() {\n const [settings, setSettings] = useState<NotifSetting[]>(DEFAULT_SETTINGS);\n const [saving, setSaving] = useState(false);\n const [saved, setSaved] = useState(false);\n\n const set = (id: string, channel: Channel) => {\n setSettings(settings.map((s) => s.id === id ? { ...s, channel } : s));\n setSaved(false);\n };\n\n return (\n <PortalShell kind=\"trainee\">\n <Link href=\"/portal/trainee/profile\" 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 profile\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 Notification preferences\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Choose how you receive each kind of notification. Critical notifications cannot be fully disabled.\n </p>\n </div>\n\n <div className=\"max-w-3xl space-y-5\">\n {saved && <InlineAlert variant=\"success\">Preferences saved.</InlineAlert>}\n\n <Card className=\"p-0\">\n {settings.map((s, i) => {\n const Icon = s.icon;\n return (\n <div key={s.id} className=\"px-5 py-4 flex items-start gap-4\" style={{ borderTop: i === 0 ? undefined : \"1px solid var(--color-border-muted)\" }}>\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n <Icon size={15} strokeWidth={1.7} />\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\">{s.title}</span>\n {s.critical && (\n <Badge variant=\"outline\" className=\"text-[10px]\" style={{ color: \"var(--color-status-warning-text)\", borderColor: \"var(--color-status-warning-border)\" }}>\n <Lock size={9} className=\"mr-1\" /> Always on\n </Badge>\n )}\n </div>\n <p className=\"text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{s.desc}</p>\n </div>\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-1\">\n {([\"push\", \"email\", \"both\", \"none\"] as Channel[]).map((c) => {\n const active = s.channel === c;\n const disabled = s.critical && c === \"none\";\n return (\n <button\n key={c}\n type=\"button\"\n onClick={() => !disabled && set(s.id, c)}\n disabled={disabled}\n aria-pressed={active}\n className=\"h-8 px-2 rounded-md text-[10px] font-medium transition-all border disabled:opacity-40 disabled:cursor-not-allowed capitalize\"\n style={{\n borderColor: active ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: active ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n color: active ? \"var(--color-text-brand)\" : \"var(--color-text-secondary)\",\n }}\n >\n {c}\n </button>\n );\n })}\n </div>\n </div>\n );\n })}\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Delivery channels</CardTitle>\n <CardDescription>Confirm where notifications can reach you.</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex items-center gap-3 py-2\">\n <BellRing size={15} strokeWidth={1.7} style={{ color: \"var(--color-text-secondary)\" }} />\n <div className=\"flex-1\">\n <div className=\"text-[13px] font-medium\">Push notifications</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>Browser & mobile · enabled in device settings</div>\n </div>\n <Badge variant=\"outline\" className=\"text-[10px]\" style={{ color: \"var(--color-status-success-text)\", borderColor: \"var(--color-status-success-border)\" }}>Enabled</Badge>\n </div>\n <Separator />\n <div className=\"flex items-center gap-3 py-2\">\n <Mail size={15} strokeWidth={1.7} style={{ color: \"var(--color-text-secondary)\" }} />\n <div className=\"flex-1\">\n <div className=\"text-[13px] font-medium\">Email</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>h.liu@ucl.ac.uk · verified</div>\n </div>\n <Badge variant=\"outline\" className=\"text-[10px]\" style={{ color: \"var(--color-status-success-text)\", borderColor: \"var(--color-status-success-border)\" }}>Verified</Badge>\n </div>\n </CardContent>\n </Card>\n\n <div className=\"flex justify-end\">\n <Button variant=\"brand\" onClick={() => { setSaving(true); setTimeout(() => { setSaving(false); setSaved(true); }, 600); }} disabled={saving}>\n {saving ? <><Spinner className=\"w-3.5 h-3.5 mr-1\" /> Saving…</> : \"Save preferences\"}\n </Button>\n </div>\n </div>\n </PortalShell>\n );\n}"
35
+ }
36
+ ],
37
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useState } from \"react\";\nimport {\n ArrowLeft, Calendar, MessageSquare, ClipboardCheck, AlertTriangle,\n Bell, Lock, Mail, BellRing,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\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 { Spinner, InlineAlert } from \"@/components/auth/auth-primitives\";\n\ntype Channel = \"push\" | \"email\" | \"both\" | \"none\";\n\ninterface NotifSetting {\n id: string;\n icon: typeof Bell;\n title: string;\n desc: string;\n channel: Channel;\n critical?: boolean;\n}\n\nconst DEFAULT_SETTINGS: NotifSetting[] = [\n { id: \"session_confirm\", icon: Calendar, title: \"Session confirmations\", desc: \"When a mentor accepts your request, or your session is scheduled.\", channel: \"both\" },\n { id: \"session_reminder\", icon: BellRing, title: \"Session reminders\", desc: \"Before your sessions start so you don't miss them.\", channel: \"both\", critical: true },\n { id: \"new_message\", icon: MessageSquare, title: \"New messages\", desc: \"Private chat messages from mentors.\", channel: \"both\" },\n { id: \"assessment_ready\", icon: ClipboardCheck, title: \"Assessments & feedback\", desc: \"When your mentor posts feedback or a competency assessment.\", channel: \"email\" },\n { id: \"connectivity\", icon: AlertTriangle, title: \"Connectivity issues\", desc: \"Pre-check failures, fail-safe activations.\", channel: \"both\", critical: true },\n { id: \"credits\", icon: Bell, title: \"Credit allocations\", desc: \"When your sponsor adds or removes credits.\", channel: \"email\" },\n { id: \"platform_updates\", icon: Bell, title: \"Platform updates\", desc: \"Product news, feature announcements.\", channel: \"email\" },\n];\n\nexport default function NotificationPreferencesPage() {\n const [settings, setSettings] = useState<NotifSetting[]>(DEFAULT_SETTINGS);\n const [saving, setSaving] = useState(false);\n const [saved, setSaved] = useState(false);\n\n const set = (id: string, channel: Channel) => {\n setSettings(settings.map((s) => s.id === id ? { ...s, channel } : s));\n setSaved(false);\n };\n\n return (\n <PortalShell kind=\"trainee\">\n <Link href=\"/portal/trainee/profile\" 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 profile\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 Notification preferences\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Choose how you receive each kind of notification. Critical notifications cannot be fully disabled.\n </p>\n </div>\n\n <div className=\"max-w-3xl space-y-5\">\n {saved && <InlineAlert variant=\"success\">Preferences saved.</InlineAlert>}\n\n <Card className=\"p-0\">\n {settings.map((s, i) => {\n const Icon = s.icon;\n return (\n <div key={s.id} className=\"px-5 py-4 flex items-start gap-4\" style={{ borderTop: i === 0 ? undefined : \"1px solid var(--color-border-muted)\" }}>\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n <Icon size={15} strokeWidth={1.7} />\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\">{s.title}</span>\n {s.critical && (\n <Badge variant=\"outline\" className=\"text-[10px]\" style={{ color: \"var(--color-status-warning-text)\", borderColor: \"var(--color-status-warning-border)\" }}>\n <Lock size={9} className=\"mr-1\" /> Always on\n </Badge>\n )}\n </div>\n <p className=\"text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{s.desc}</p>\n </div>\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-1\">\n {([\"push\", \"email\", \"both\", \"none\"] as Channel[]).map((c) => {\n const active = s.channel === c;\n const disabled = s.critical && c === \"none\";\n return (\n <button\n key={c}\n type=\"button\"\n onClick={() => !disabled && set(s.id, c)}\n disabled={disabled}\n aria-pressed={active}\n className=\"h-8 px-2 rounded-md text-[10px] font-medium transition-all border disabled:opacity-40 disabled:cursor-not-allowed capitalize\"\n style={{\n borderColor: active ? \"var(--color-border-brand)\" : \"var(--color-border-default)\",\n background: active ? \"var(--color-bg-brand-muted)\" : \"transparent\",\n color: active ? \"var(--color-text-brand)\" : \"var(--color-text-secondary)\",\n }}\n >\n {c}\n </button>\n );\n })}\n </div>\n </div>\n );\n })}\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Delivery channels</CardTitle>\n <CardDescription>Confirm where notifications can reach you.</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex items-center gap-3 py-2\">\n <BellRing size={15} strokeWidth={1.7} style={{ color: \"var(--color-text-secondary)\" }} />\n <div className=\"flex-1\">\n <div className=\"text-[13px] font-medium\">Push notifications</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>Browser & mobile · enabled in device settings</div>\n </div>\n <Badge variant=\"outline\" className=\"text-[10px]\" style={{ color: \"var(--color-status-success-text)\", borderColor: \"var(--color-status-success-border)\" }}>Enabled</Badge>\n </div>\n <Separator />\n <div className=\"flex items-center gap-3 py-2\">\n <Mail size={15} strokeWidth={1.7} style={{ color: \"var(--color-text-secondary)\" }} />\n <div className=\"flex-1\">\n <div className=\"text-[13px] font-medium\">Email</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>h.liu@ucl.ac.uk · verified</div>\n </div>\n <Badge variant=\"outline\" className=\"text-[10px]\" style={{ color: \"var(--color-status-success-text)\", borderColor: \"var(--color-status-success-border)\" }}>Verified</Badge>\n </div>\n </CardContent>\n </Card>\n\n <div className=\"flex justify-end\">\n <Button variant=\"brand\" onClick={() => { setSaving(true); setTimeout(() => { setSaving(false); setSaved(true); }, 600); }} disabled={saving}>\n {saving ? <><Spinner className=\"w-3.5 h-3.5 mr-1\" /> Saving…</> : \"Save preferences\"}\n </Button>\n </div>\n </div>\n </PortalShell>\n );\n}\n"
38
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/trainee/settings/page.tsx",
4
+ "sha256": "978036458aa158662a6d9288cef0745c98544f03c33efce31e63787563bb9d4d",
5
+ "imports": [
6
+ "@/components/portal/portal-shell",
7
+ "@/components/portal/settings-view"
8
+ ],
9
+ "tokens": [],
10
+ "fragments": [
11
+ {
12
+ "id": "TraineeSettingsPage",
13
+ "line": 4,
14
+ "code": "export default function TraineeSettingsPage() {\n return (\n <PortalShell kind=\"trainee\">\n <SettingsView\n kind=\"trainee\"\n name=\"Dr. Hannah Liu\"\n email=\"h.liu@ucl.ac.uk\"\n id=\"MX-87A37E\"\n notificationsHref=\"/portal/trainee/settings/notifications\"\n />\n </PortalShell>\n );\n}"
15
+ }
16
+ ],
17
+ "content": "import { PortalShell } from \"@/components/portal/portal-shell\";\nimport { SettingsView } from \"@/components/portal/settings-view\";\n\nexport default function TraineeSettingsPage() {\n return (\n <PortalShell kind=\"trainee\">\n <SettingsView\n kind=\"trainee\"\n name=\"Dr. Hannah Liu\"\n email=\"h.liu@ucl.ac.uk\"\n id=\"MX-87A37E\"\n notificationsHref=\"/portal/trainee/settings/notifications\"\n />\n </PortalShell>\n );\n}\n"
18
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/trainee/page.tsx",
4
+ "sha256": "e73ff66b2994dc3146bdd4e2af69bcc00301ff57cd02ae3ce5339c2b5c6d4a4c",
5
+ "imports": [
6
+ "next/link",
7
+ "lucide-react",
8
+ "@/components/ui/button",
9
+ "@/components/ui/badge",
10
+ "@/components/ui/card",
11
+ "@/components/ui/separator",
12
+ "@/components/ui/table",
13
+ "@/components/portal/portal-shell",
14
+ "@/components/auth/auth-primitives",
15
+ "@/lib/credits-mock",
16
+ "@/lib/credits"
17
+ ],
18
+ "tokens": [
19
+ "--color-bg-brand-muted",
20
+ "--color-bg-subtle",
21
+ "--color-border-default",
22
+ "--color-status-error-text",
23
+ "--color-status-success-text",
24
+ "--color-text-brand",
25
+ "--color-text-muted",
26
+ "--color-text-primary",
27
+ "--color-text-secondary",
28
+ "--teal-300",
29
+ "--teal-500"
30
+ ],
31
+ "fragments": [
32
+ {
33
+ "id": "TraineeDashboardPage",
34
+ "line": 23,
35
+ "code": "export default function TraineeDashboardPage() {\n const ledger = TRAINEE_LEDGERS[TRAINEE_ID];\n const daysToExpiry = daysFrom(ledger.expiresAt);\n const expiringSoon = daysToExpiry < 60;\n const lowBalance = ledger.available <= 2;\n\n return (\n <PortalShell\n kind=\"trainee\"\n account={{ name: \"Dr. Hannah Liu\", subtitle: \"ST5 · Gastroenterology\", initials: \"HL\" }}\n >\n {/* Heading */}\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 Welcome back, Hannah\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Request mentorship, review feedback, and track your progress.\n </p>\n </div>\n <Button variant=\"brand\" size=\"default\" disabled={ledger.available === 0}>\n Request session <ArrowRight size={13} />\n </Button>\n </div>\n\n {/* Low balance / expiry warnings */}\n {ledger.available === 0 && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"error\" title=\"No credits available\">\n You've used all allocated credits. Contact your sponsor (Olympus Medical UK) or Mentix support to request more.\n </InlineAlert>\n </div>\n )}\n {ledger.available > 0 && lowBalance && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"warning\" title=\"Running low on credits\">\n You have {ledger.available} credit{ledger.available === 1 ? \"\" : \"s\"} remaining. Plan your remaining sessions carefully.\n </InlineAlert>\n </div>\n )}\n {expiringSoon && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"info\">\n Your credits expire on <strong style={{ color: \"var(--color-text-primary)\" }}>{ledger.expiresAt}</strong> ({daysToExpiry} days). Use them or ask your sponsor for an extension.\n </InlineAlert>\n </div>\n )}\n\n {/* Credit balance hero card */}\n <Card className=\"mb-6\">\n <CardHeader>\n <div className=\"flex items-start gap-4\">\n <div className=\"w-12 h-12 rounded-xl flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Wallet size={22} strokeWidth={1.6} />\n </div>\n <div className=\"flex-1\">\n <CardTitle className=\"text-base mb-1\">Your credits</CardTitle>\n <CardDescription>\n Sponsored by <strong style={{ color: \"var(--color-text-primary)\" }}>{ledger.sponsorName}</strong> · expires {ledger.expiresAt}\n </CardDescription>\n </div>\n <Badge variant=\"outline\">{ledger.totalAllocated} total allocated</Badge>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-4\">\n <CreditMetric\n label=\"Available\"\n value={ledger.available}\n hint=\"Ready to request\"\n accent={ledger.available === 0 ? \"error\" : \"brand\"}\n />\n <CreditMetric label=\"In escrow\" value={ledger.escrowed} hint=\"Pending sessions\" />\n <CreditMetric label=\"Consumed\" value={ledger.consumed} hint=\"Sessions closed\" />\n </div>\n\n <Separator className=\"my-5\" />\n\n {/* Allocation breakdown bar */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-[11px] font-medium uppercase tracking-wide\" style={{ color: \"var(--color-text-muted)\" }}>\n Allocation breakdown\n </span>\n </div>\n <div className=\"h-2 rounded-full overflow-hidden flex\" style={{ background: \"var(--color-border-default)\" }}>\n <div className=\"h-full\" style={{ width: `${(ledger.consumed / ledger.totalAllocated) * 100}%`, background: \"var(--teal-500)\" }} />\n <div className=\"h-full\" style={{ width: `${(ledger.escrowed / ledger.totalAllocated) * 100}%`, background: \"var(--teal-300)\" }} />\n </div>\n <div className=\"flex items-center gap-4 mt-3 text-[11px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"w-2 h-2 rounded-full\" style={{ background: \"var(--teal-500)\" }} /> Consumed\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"w-2 h-2 rounded-full\" style={{ background: \"var(--teal-300)\" }} /> In escrow\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"w-2 h-2 rounded-full\" style={{ background: \"var(--color-border-default)\" }} /> Available\n </span>\n </div>\n </div>\n </CardContent>\n </Card>\n\n {/* Sessions history */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div>\n <CardTitle className=\"text-base\">Recent sessions</CardTitle>\n <CardDescription>Your latest mentorship sessions and pending requests.</CardDescription>\n </div>\n <Button variant=\"outline\" size=\"sm\" render={<Link href=\"/portal/trainee/sessions\" />}>\n View all <ArrowRight size={11} />\n </Button>\n </div>\n </CardHeader>\n <CardContent className=\"p-0\">\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider\" style={{ color: \"var(--color-text-muted)\" }}>Date</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider\" style={{ color: \"var(--color-text-muted)\" }}>Procedure</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider\" style={{ color: \"var(--color-text-muted)\" }}>Mentor</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider\" style={{ color: \"var(--color-text-muted)\" }}>Duration</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider text-right\" style={{ color: \"var(--color-text-muted)\" }}>Credits</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider\" style={{ color: \"var(--color-text-muted)\" }}>Status</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {SESSION_HISTORY.map((s, i) => (\n <TableRow key={i}>\n <TableCell className=\"text-[12px]\">{s.date}</TableCell>\n <TableCell><Badge variant=\"outline\">{s.procedure}</Badge></TableCell>\n <TableCell className=\"text-[12px]\">{s.mentor}</TableCell>\n <TableCell className=\"text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{s.duration}</TableCell>\n <TableCell className=\"text-right text-[12px] tabular-nums font-medium\">{s.credits}</TableCell>\n <TableCell>\n {s.status === \"Closed\" ? (\n <span className=\"inline-flex items-center gap-1.5 text-[11px] font-medium\" style={{ color: \"var(--color-status-success-text)\" }}>\n <CheckCircle2 size={11} strokeWidth={2} /> Closed\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1.5 text-[11px] font-medium\" style={{ color: \"var(--color-text-brand)\" }}>\n <Clock size={11} strokeWidth={1.8} /> Escrowed\n </span>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </CardContent>\n </Card>\n </PortalShell>\n );\n}"
36
+ },
37
+ {
38
+ "id": "CreditMetric",
39
+ "line": 184,
40
+ "code": "function CreditMetric({ label, value, hint, accent }: { label: string; value: number; hint: string; accent?: \"brand\" | \"error\" }) {\n const color = accent === \"error\"\n ? \"var(--color-status-error-text)\"\n : accent === \"brand\"\n ? \"var(--color-text-brand)\"\n : \"var(--color-text-primary)\";\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 }}>{value}</div>\n <div className=\"text-[11px] mt-0.5\" style={{ color: \"var(--color-text-muted)\" }}>{hint}</div>\n </div>\n );\n}"
41
+ }
42
+ ],
43
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { ArrowRight, Wallet, Calendar, Clock, CheckCircle2, AlertCircle } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card, CardHeader, CardTitle, CardDescription, CardContent } from \"@/components/ui/card\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from \"@/components/ui/table\";\nimport { PortalShell } from \"@/components/portal/portal-shell\";\nimport { InlineAlert } from \"@/components/auth/auth-primitives\";\nimport { TRAINEE_LEDGERS } from \"@/lib/credits-mock\";\nimport { daysFrom } from \"@/lib/credits\";\n\nconst TRAINEE_ID = \"app-105\"; // Dr. Hannah Liu — has credits to demo full state\n\nconst SESSION_HISTORY = [\n { id: \"ses-2401\", date: \"2026-05-22\", procedure: \"ESD\", mentor: \"Prof. Amyn Haji\", credits: 1, status: \"Closed\", duration: \"2h 45m\" },\n { id: \"ses-2389\", date: \"2026-05-15\", procedure: \"EMR\", mentor: \"Dr. Marie Dubois\", credits: 1, status: \"Closed\", duration: \"1h 30m\" },\n { id: \"ses-2401\", date: \"2026-05-24\", procedure: \"ESD\", mentor: \"Prof. Amyn Haji\", credits: 1, status: \"Escrowed\", duration: \"Scheduled 26 May, 10:00\" },\n] as const;\n\nexport default function TraineeDashboardPage() {\n const ledger = TRAINEE_LEDGERS[TRAINEE_ID];\n const daysToExpiry = daysFrom(ledger.expiresAt);\n const expiringSoon = daysToExpiry < 60;\n const lowBalance = ledger.available <= 2;\n\n return (\n <PortalShell\n kind=\"trainee\"\n account={{ name: \"Dr. Hannah Liu\", subtitle: \"ST5 · Gastroenterology\", initials: \"HL\" }}\n >\n {/* Heading */}\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 Welcome back, Hannah\n </h1>\n <p className=\"text-sm\" style={{ color: \"var(--color-text-secondary)\" }}>\n Request mentorship, review feedback, and track your progress.\n </p>\n </div>\n <Button variant=\"brand\" size=\"default\" disabled={ledger.available === 0}>\n Request session <ArrowRight size={13} />\n </Button>\n </div>\n\n {/* Low balance / expiry warnings */}\n {ledger.available === 0 && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"error\" title=\"No credits available\">\n You've used all allocated credits. Contact your sponsor (Olympus Medical UK) or Mentix support to request more.\n </InlineAlert>\n </div>\n )}\n {ledger.available > 0 && lowBalance && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"warning\" title=\"Running low on credits\">\n You have {ledger.available} credit{ledger.available === 1 ? \"\" : \"s\"} remaining. Plan your remaining sessions carefully.\n </InlineAlert>\n </div>\n )}\n {expiringSoon && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"info\">\n Your credits expire on <strong style={{ color: \"var(--color-text-primary)\" }}>{ledger.expiresAt}</strong> ({daysToExpiry} days). Use them or ask your sponsor for an extension.\n </InlineAlert>\n </div>\n )}\n\n {/* Credit balance hero card */}\n <Card className=\"mb-6\">\n <CardHeader>\n <div className=\"flex items-start gap-4\">\n <div className=\"w-12 h-12 rounded-xl flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Wallet size={22} strokeWidth={1.6} />\n </div>\n <div className=\"flex-1\">\n <CardTitle className=\"text-base mb-1\">Your credits</CardTitle>\n <CardDescription>\n Sponsored by <strong style={{ color: \"var(--color-text-primary)\" }}>{ledger.sponsorName}</strong> · expires {ledger.expiresAt}\n </CardDescription>\n </div>\n <Badge variant=\"outline\">{ledger.totalAllocated} total allocated</Badge>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-4\">\n <CreditMetric\n label=\"Available\"\n value={ledger.available}\n hint=\"Ready to request\"\n accent={ledger.available === 0 ? \"error\" : \"brand\"}\n />\n <CreditMetric label=\"In escrow\" value={ledger.escrowed} hint=\"Pending sessions\" />\n <CreditMetric label=\"Consumed\" value={ledger.consumed} hint=\"Sessions closed\" />\n </div>\n\n <Separator className=\"my-5\" />\n\n {/* Allocation breakdown bar */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-[11px] font-medium uppercase tracking-wide\" style={{ color: \"var(--color-text-muted)\" }}>\n Allocation breakdown\n </span>\n </div>\n <div className=\"h-2 rounded-full overflow-hidden flex\" style={{ background: \"var(--color-border-default)\" }}>\n <div className=\"h-full\" style={{ width: `${(ledger.consumed / ledger.totalAllocated) * 100}%`, background: \"var(--teal-500)\" }} />\n <div className=\"h-full\" style={{ width: `${(ledger.escrowed / ledger.totalAllocated) * 100}%`, background: \"var(--teal-300)\" }} />\n </div>\n <div className=\"flex items-center gap-4 mt-3 text-[11px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"w-2 h-2 rounded-full\" style={{ background: \"var(--teal-500)\" }} /> Consumed\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"w-2 h-2 rounded-full\" style={{ background: \"var(--teal-300)\" }} /> In escrow\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"w-2 h-2 rounded-full\" style={{ background: \"var(--color-border-default)\" }} /> Available\n </span>\n </div>\n </div>\n </CardContent>\n </Card>\n\n {/* Sessions history */}\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div>\n <CardTitle className=\"text-base\">Recent sessions</CardTitle>\n <CardDescription>Your latest mentorship sessions and pending requests.</CardDescription>\n </div>\n <Button variant=\"outline\" size=\"sm\" render={<Link href=\"/portal/trainee/sessions\" />}>\n View all <ArrowRight size={11} />\n </Button>\n </div>\n </CardHeader>\n <CardContent className=\"p-0\">\n <Table>\n <TableHeader style={{ background: \"var(--color-bg-subtle)\" }}>\n <TableRow>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider\" style={{ color: \"var(--color-text-muted)\" }}>Date</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider\" style={{ color: \"var(--color-text-muted)\" }}>Procedure</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider\" style={{ color: \"var(--color-text-muted)\" }}>Mentor</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider\" style={{ color: \"var(--color-text-muted)\" }}>Duration</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider text-right\" style={{ color: \"var(--color-text-muted)\" }}>Credits</TableHead>\n <TableHead className=\"text-[10px] font-semibold uppercase tracking-wider\" style={{ color: \"var(--color-text-muted)\" }}>Status</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {SESSION_HISTORY.map((s, i) => (\n <TableRow key={i}>\n <TableCell className=\"text-[12px]\">{s.date}</TableCell>\n <TableCell><Badge variant=\"outline\">{s.procedure}</Badge></TableCell>\n <TableCell className=\"text-[12px]\">{s.mentor}</TableCell>\n <TableCell className=\"text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{s.duration}</TableCell>\n <TableCell className=\"text-right text-[12px] tabular-nums font-medium\">{s.credits}</TableCell>\n <TableCell>\n {s.status === \"Closed\" ? (\n <span className=\"inline-flex items-center gap-1.5 text-[11px] font-medium\" style={{ color: \"var(--color-status-success-text)\" }}>\n <CheckCircle2 size={11} strokeWidth={2} /> Closed\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1.5 text-[11px] font-medium\" style={{ color: \"var(--color-text-brand)\" }}>\n <Clock size={11} strokeWidth={1.8} /> Escrowed\n </span>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </CardContent>\n </Card>\n </PortalShell>\n );\n}\n\n/* ─── Sub-components ─────────────────────────────────────────────────────── */\n\nfunction CreditMetric({ label, value, hint, accent }: { label: string; value: number; hint: string; accent?: \"brand\" | \"error\" }) {\n const color = accent === \"error\"\n ? \"var(--color-status-error-text)\"\n : accent === \"brand\"\n ? \"var(--color-text-brand)\"\n : \"var(--color-text-primary)\";\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 }}>{value}</div>\n <div className=\"text-[11px] mt-0.5\" style={{ color: \"var(--color-text-muted)\" }}>{hint}</div>\n </div>\n );\n}\n"
44
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
3
+ "file": "app/portal/tv/[id]/page.tsx",
4
+ "sha256": "8dc50c41d7a3771a8f80e8fdbb3fc4390781c0d89fc62492dea7a622e82e98d2",
5
+ "imports": [
6
+ "next/link",
7
+ "react",
8
+ "lucide-react",
9
+ "@/components/ui/button",
10
+ "@/components/ui/badge",
11
+ "@/components/ui/card",
12
+ "@/components/portal/portal-shell",
13
+ "@/lib/broadcasts-mock"
14
+ ],
15
+ "tokens": [
16
+ "--color-border-muted",
17
+ "--color-text-muted",
18
+ "--color-text-primary",
19
+ "--color-text-secondary",
20
+ "--gray-0",
21
+ "--gray-50",
22
+ "--gray-950",
23
+ "--red-500",
24
+ "--teal-500"
25
+ ],
26
+ "fragments": [
27
+ {
28
+ "id": "BroadcastViewerPage",
29
+ "line": 12,
30
+ "code": "export default function BroadcastViewerPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = use(params);\n const b = BROADCASTS.find((x) => x.id === id);\n\n if (!b) {\n return (\n <PortalShell kind=\"trainee\">\n <div className=\"text-center py-20\">\n <p className=\"text-[14px] mb-4\" style={{ color: \"var(--color-text-secondary)\" }}>Broadcast not found.</p>\n <Link href=\"/portal/tv\"><Button variant=\"outline\">← Back to Mentix TV</Button></Link>\n </div>\n </PortalShell>\n );\n }\n\n return (\n <PortalShell kind=\"trainee\">\n <Link href=\"/portal/tv\" 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 Mentix TV\n </Link>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6\">\n {/* Player */}\n <div>\n <Card className=\"overflow-hidden mb-4\">\n <div className=\"aspect-video relative\" style={{ background: \"var(--gray-950)\" }}>\n {b.ytId && b.state === \"live\" ? (\n <iframe\n className=\"absolute inset-0 w-full h-full\"\n src={`https://www.youtube.com/embed/${b.ytId}?start=${b.ytStart ?? 0}&autoplay=0&modestbranding=1&rel=0`}\n title={b.title}\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n />\n ) : b.state === \"scheduled\" ? (\n <div className=\"absolute inset-0 flex flex-col items-center justify-center text-center px-6\" style={{ color: \"var(--gray-50)\" }}>\n <Calendar size={36} strokeWidth={1.4} className=\"mb-3\" style={{ opacity: 0.7 }} />\n <h2 className=\"text-lg font-semibold mb-1\">{b.title}</h2>\n <p className=\"text-[13px] mb-4\" style={{ opacity: 0.7 }}>Starts {fmtBroadcastTime(b.scheduledFor)}</p>\n <Button variant=\"outline\" style={{ background: \"transparent\", borderColor: \"color-mix(in srgb, var(--gray-0) 20%, transparent)\", color: \"var(--gray-50)\" }}>\n <Calendar size={11} /> Notify me\n </Button>\n </div>\n ) : (\n <div className=\"absolute inset-0 flex items-center justify-center\" style={{ color: \"var(--gray-50)\" }}>\n <Tv size={36} strokeWidth={1.4} style={{ opacity: 0.6 }} />\n </div>\n )}\n </div>\n </Card>\n\n {/* Title & meta */}\n <div className=\"flex items-start justify-between gap-3 mb-3\">\n <div>\n <div className=\"flex items-center gap-2 mb-2\">\n {b.state === \"live\" && (\n <Badge className=\"text-[10px] inline-flex items-center gap-1\" style={{ background: \"var(--red-500)\", color: \"var(--gray-0)\" }}>\n <span className=\"w-1.5 h-1.5 rounded-full animate-pulse\" style={{ background: \"var(--gray-0)\" }} /> LIVE\n </Badge>\n )}\n {b.state === \"scheduled\" && <Badge variant=\"outline\" className=\"text-[10px]\"><Calendar size={9} className=\"mr-1\" /> Scheduled</Badge>}\n {b.state === \"ended\" && <Badge variant=\"outline\" className=\"text-[10px]\"><Tv size={9} className=\"mr-1\" /> Ended</Badge>}\n {b.access === \"cohort\" && <Badge variant=\"outline\" className=\"text-[10px]\"><Lock size={9} className=\"mr-1\" /> Cohort</Badge>}\n {b.state === \"live\" && b.viewers && (\n <span className=\"text-[12px] inline-flex items-center gap-1\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Users size={11} /> {b.viewers} watching\n </span>\n )}\n </div>\n <h1 className=\"text-[1.5rem] font-semibold tracking-tight\" style={{ letterSpacing: \"-0.025em\" }}>{b.title}</h1>\n </div>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">About this broadcast</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\", lineHeight: 1.6 }}>{b.description}</p>\n </CardContent>\n </Card>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-4\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-[13px]\">Host</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-center gap-3\">\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center text-[12px] font-semibold shrink-0\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>{b.hostInitials}</div>\n <div>\n <div className=\"text-[13px] font-medium\">{b.host}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>Broadcasting live</div>\n </div>\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-[13px]\">Details</CardTitle>\n </CardHeader>\n <CardContent>\n <dl className=\"space-y-2.5 text-[12px]\">\n <DRow label=\"When\" value={fmtBroadcastTime(b.scheduledFor)} />\n <DRow label=\"Duration\" value={`~${b.durationMin} min`} />\n <DRow label=\"Access\" value={b.access === \"all\" ? \"All platform users\" : b.access === \"cohort\" ? \"Cohort-restricted\" : \"Invite-only\"} />\n <DRow label=\"Cost\" value=\"Free\" />\n </dl>\n <p className=\"text-[11px] mt-4 pt-3 border-t\" style={{ borderColor: \"var(--color-border-muted)\", color: \"var(--color-text-muted)\" }}>\n Viewing is receive-only. No microphone, camera, or screen-share controls.\n </p>\n </CardContent>\n </Card>\n </div>\n </div>\n </PortalShell>\n );\n}"
31
+ },
32
+ {
33
+ "id": "DRow",
34
+ "line": 134,
35
+ "code": "function DRow({ label, value }: { label: string; value: string }) {\n return (\n <div className=\"flex items-start justify-between gap-2\">\n <dt className=\"shrink-0\" style={{ color: \"var(--color-text-muted)\" }}>{label}</dt>\n <dd className=\"text-right\" style={{ color: \"var(--color-text-primary)\" }}>{value}</dd>\n </div>\n );\n}"
36
+ }
37
+ ],
38
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { use } from \"react\";\nimport { ArrowLeft, Users, Radio, Calendar, Tv, Lock } from \"lucide-react\";\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 { BROADCASTS, fmtBroadcastTime } from \"@/lib/broadcasts-mock\";\n\nexport default function BroadcastViewerPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = use(params);\n const b = BROADCASTS.find((x) => x.id === id);\n\n if (!b) {\n return (\n <PortalShell kind=\"trainee\">\n <div className=\"text-center py-20\">\n <p className=\"text-[14px] mb-4\" style={{ color: \"var(--color-text-secondary)\" }}>Broadcast not found.</p>\n <Link href=\"/portal/tv\"><Button variant=\"outline\">← Back to Mentix TV</Button></Link>\n </div>\n </PortalShell>\n );\n }\n\n return (\n <PortalShell kind=\"trainee\">\n <Link href=\"/portal/tv\" 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 Mentix TV\n </Link>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6\">\n {/* Player */}\n <div>\n <Card className=\"overflow-hidden mb-4\">\n <div className=\"aspect-video relative\" style={{ background: \"var(--gray-950)\" }}>\n {b.ytId && b.state === \"live\" ? (\n <iframe\n className=\"absolute inset-0 w-full h-full\"\n src={`https://www.youtube.com/embed/${b.ytId}?start=${b.ytStart ?? 0}&autoplay=0&modestbranding=1&rel=0`}\n title={b.title}\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n />\n ) : b.state === \"scheduled\" ? (\n <div className=\"absolute inset-0 flex flex-col items-center justify-center text-center px-6\" style={{ color: \"var(--gray-50)\" }}>\n <Calendar size={36} strokeWidth={1.4} className=\"mb-3\" style={{ opacity: 0.7 }} />\n <h2 className=\"text-lg font-semibold mb-1\">{b.title}</h2>\n <p className=\"text-[13px] mb-4\" style={{ opacity: 0.7 }}>Starts {fmtBroadcastTime(b.scheduledFor)}</p>\n <Button variant=\"outline\" style={{ background: \"transparent\", borderColor: \"color-mix(in srgb, var(--gray-0) 20%, transparent)\", color: \"var(--gray-50)\" }}>\n <Calendar size={11} /> Notify me\n </Button>\n </div>\n ) : (\n <div className=\"absolute inset-0 flex items-center justify-center\" style={{ color: \"var(--gray-50)\" }}>\n <Tv size={36} strokeWidth={1.4} style={{ opacity: 0.6 }} />\n </div>\n )}\n </div>\n </Card>\n\n {/* Title & meta */}\n <div className=\"flex items-start justify-between gap-3 mb-3\">\n <div>\n <div className=\"flex items-center gap-2 mb-2\">\n {b.state === \"live\" && (\n <Badge className=\"text-[10px] inline-flex items-center gap-1\" style={{ background: \"var(--red-500)\", color: \"var(--gray-0)\" }}>\n <span className=\"w-1.5 h-1.5 rounded-full animate-pulse\" style={{ background: \"var(--gray-0)\" }} /> LIVE\n </Badge>\n )}\n {b.state === \"scheduled\" && <Badge variant=\"outline\" className=\"text-[10px]\"><Calendar size={9} className=\"mr-1\" /> Scheduled</Badge>}\n {b.state === \"ended\" && <Badge variant=\"outline\" className=\"text-[10px]\"><Tv size={9} className=\"mr-1\" /> Ended</Badge>}\n {b.access === \"cohort\" && <Badge variant=\"outline\" className=\"text-[10px]\"><Lock size={9} className=\"mr-1\" /> Cohort</Badge>}\n {b.state === \"live\" && b.viewers && (\n <span className=\"text-[12px] inline-flex items-center gap-1\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Users size={11} /> {b.viewers} watching\n </span>\n )}\n </div>\n <h1 className=\"text-[1.5rem] font-semibold tracking-tight\" style={{ letterSpacing: \"-0.025em\" }}>{b.title}</h1>\n </div>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">About this broadcast</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\", lineHeight: 1.6 }}>{b.description}</p>\n </CardContent>\n </Card>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-4\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-[13px]\">Host</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-center gap-3\">\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center text-[12px] font-semibold shrink-0\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>{b.hostInitials}</div>\n <div>\n <div className=\"text-[13px] font-medium\">{b.host}</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>Broadcasting live</div>\n </div>\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-[13px]\">Details</CardTitle>\n </CardHeader>\n <CardContent>\n <dl className=\"space-y-2.5 text-[12px]\">\n <DRow label=\"When\" value={fmtBroadcastTime(b.scheduledFor)} />\n <DRow label=\"Duration\" value={`~${b.durationMin} min`} />\n <DRow label=\"Access\" value={b.access === \"all\" ? \"All platform users\" : b.access === \"cohort\" ? \"Cohort-restricted\" : \"Invite-only\"} />\n <DRow label=\"Cost\" value=\"Free\" />\n </dl>\n <p className=\"text-[11px] mt-4 pt-3 border-t\" style={{ borderColor: \"var(--color-border-muted)\", color: \"var(--color-text-muted)\" }}>\n Viewing is receive-only. No microphone, camera, or screen-share controls.\n </p>\n </CardContent>\n </Card>\n </div>\n </div>\n </PortalShell>\n );\n}\n\nfunction DRow({ label, value }: { label: string; value: string }) {\n return (\n <div className=\"flex items-start justify-between gap-2\">\n <dt className=\"shrink-0\" style={{ color: \"var(--color-text-muted)\" }}>{label}</dt>\n <dd className=\"text-right\" style={{ color: \"var(--color-text-primary)\" }}>{value}</dd>\n </div>\n );\n}\n"
39
+ }