@burdenoff/microfe-atheletebridge 2026.720.1 → 2026.731.1

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 (91) hide show
  1. package/dist/atheletebridge/AthleteBridgeRoot.js.map +1 -1
  2. package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
  3. package/dist/atheletebridge/admin/AdminDashboardPage.js.map +1 -1
  4. package/dist/atheletebridge/admin/AdminRoot.js.map +1 -1
  5. package/dist/atheletebridge/components/AthleteSelect.js.map +1 -1
  6. package/dist/atheletebridge/components/Crest.js.map +1 -1
  7. package/dist/atheletebridge/components/CursorPager.js.map +1 -1
  8. package/dist/atheletebridge/components/DrillVideoLink.js.map +1 -1
  9. package/dist/atheletebridge/components/EmptyState.js.map +1 -1
  10. package/dist/atheletebridge/components/FilterSelect.js.map +1 -1
  11. package/dist/atheletebridge/components/FixtureCard.js.map +1 -1
  12. package/dist/atheletebridge/components/LoadingState.js.map +1 -1
  13. package/dist/atheletebridge/components/MatchupRow.js.map +1 -1
  14. package/dist/atheletebridge/components/MetricsCard.js.map +1 -1
  15. package/dist/atheletebridge/components/PageLayout.js.map +1 -1
  16. package/dist/atheletebridge/components/ProgramCard.js.map +1 -1
  17. package/dist/atheletebridge/components/RouteAnalytics.js.map +1 -1
  18. package/dist/atheletebridge/components/Scoreboard.js.map +1 -1
  19. package/dist/atheletebridge/components/SessionCard.js.map +1 -1
  20. package/dist/atheletebridge/components/SessionStatusTimeline.js.map +1 -1
  21. package/dist/atheletebridge/components/StatusBadge.js.map +1 -1
  22. package/dist/atheletebridge/components/TeamCard.js.map +1 -1
  23. package/dist/atheletebridge/components/UserAvatar.js.map +1 -1
  24. package/dist/atheletebridge/components/UserSearchSelect.js.map +1 -1
  25. package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.d.ts +9 -0
  26. package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.d.ts.map +1 -0
  27. package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.js +118 -0
  28. package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.js.map +1 -0
  29. package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.d.ts +9 -0
  30. package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.d.ts.map +1 -0
  31. package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.js +88 -0
  32. package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.js.map +1 -0
  33. package/dist/atheletebridge/components/domain.js.map +1 -1
  34. package/dist/atheletebridge/components/form.js.map +1 -1
  35. package/dist/atheletebridge/components/viz/BarChart.js.map +1 -1
  36. package/dist/atheletebridge/components/viz/DonutChart.js +1 -1
  37. package/dist/atheletebridge/components/viz/DonutChart.js.map +1 -1
  38. package/dist/atheletebridge/components/viz/LineChart.js.map +1 -1
  39. package/dist/atheletebridge/components/viz/RatingStars.js.map +1 -1
  40. package/dist/atheletebridge/components/viz/Sparkline.js.map +1 -1
  41. package/dist/atheletebridge/components/viz/StatCard.js.map +1 -1
  42. package/dist/atheletebridge/components/viz/Timeline.js.map +1 -1
  43. package/dist/atheletebridge/components/viz/lib/CartesianFrame.js.map +1 -1
  44. package/dist/atheletebridge/components/viz/lib/ResponsiveSvg.js.map +1 -1
  45. package/dist/atheletebridge/components/viz/lib/format.js.map +1 -1
  46. package/dist/atheletebridge/components/viz/lib/geometry.js.map +1 -1
  47. package/dist/atheletebridge/components/viz/lib/hooks.js.map +1 -1
  48. package/dist/atheletebridge/components/viz/lib/palette.js.map +1 -1
  49. package/dist/atheletebridge/context/AthleteBridgeContext.js.map +1 -1
  50. package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts +18 -1
  51. package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts.map +1 -1
  52. package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +71 -33
  53. package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
  54. package/dist/atheletebridge/i18n.js.map +1 -1
  55. package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
  56. package/dist/atheletebridge/pages/StandingsPage.js.map +1 -1
  57. package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js.map +1 -1
  58. package/dist/atheletebridge/pages/athletes/AthleteProfilePage.js.map +1 -1
  59. package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.d.ts.map +1 -1
  60. package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js +87 -75
  61. package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js.map +1 -1
  62. package/dist/atheletebridge/pages/fixtures/FixtureFormPage.js.map +1 -1
  63. package/dist/atheletebridge/pages/fixtures/FixturesListPage.js.map +1 -1
  64. package/dist/atheletebridge/pages/onboarding/OnboardingPage.js.map +1 -1
  65. package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
  66. package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
  67. package/dist/atheletebridge/pages/programs/ProgramFormPage.js.map +1 -1
  68. package/dist/atheletebridge/pages/programs/ProgramsListPage.js.map +1 -1
  69. package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
  70. package/dist/atheletebridge/pages/sessions/SessionFormPage.js.map +1 -1
  71. package/dist/atheletebridge/pages/sessions/SessionsListPage.js.map +1 -1
  72. package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
  73. package/dist/atheletebridge/pages/teams/TeamFormPage.js.map +1 -1
  74. package/dist/atheletebridge/pages/teams/TeamsListPage.js.map +1 -1
  75. package/dist/atheletebridge/pages/tournaments/TournamentBracketPage.js.map +1 -1
  76. package/dist/atheletebridge/store/atheletebridgeStore.js.map +1 -1
  77. package/dist/atheletebridge/types/index.d.ts +27 -0
  78. package/dist/atheletebridge/types/index.d.ts.map +1 -1
  79. package/dist/atheletebridge/utils/cn.js.map +1 -1
  80. package/dist/atheletebridge/utils/datetime.js.map +1 -1
  81. package/dist/atheletebridge/utils/formatters.js.map +1 -1
  82. package/dist/atheletebridge/utils/identity.js.map +1 -1
  83. package/dist/atheletebridge/utils/navigation.js.map +1 -1
  84. package/dist/atheletebridge/utils/pageContext.js.map +1 -1
  85. package/dist/atheletebridge/utils/timeAgo.js.map +1 -1
  86. package/dist/atheletebridge/utils/url.js.map +1 -1
  87. package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
  88. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1 -1
  89. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
  90. package/dist/services/createSandboxAssistantTransport.js.map +1 -1
  91. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"PerformancePage.js","names":[],"sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"sourcesContent":["/**\n * Performance\n * @module atheletebridge/pages/performance\n *\n * The coach's athlete-performance workbench: pick an athlete from a team roster,\n * then log workouts (athLogWorkout) and record performance metrics\n * (athRecordMetric), and review that athlete's workout history\n * (athListWorkoutLogs, offset-paginated) and metrics.\n *\n * Elevated to the concept bar: each measured metric (type + label) becomes a\n * hero stat tile with its big latest value, a real recordedAt-ordered trend\n * sparkline, a min→max range scale and a signed delta-vs-previous — all derived\n * from the real time-series (nothing fabricated). Workout history renders as a\n * responsive table that reflows to cards on mobile, with a thumb-reachable\n * sticky \"Log workout\" action.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n ArrowDown,\n ArrowUp,\n Dumbbell,\n Gauge,\n HeartPulse,\n Loader2,\n Move,\n Plus,\n Save,\n Scale,\n Target,\n Users,\n Zap,\n type LucideIcon,\n} from 'lucide-react';\nimport {\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { AthleteSelect, type AthleteSelectValue } from '../../components/AthleteSelect';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { CursorPager } from '../../components/CursorPager';\nimport { Sparkline } from '../../components/viz';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { METRIC_TYPE_LABELS } from '../../components/domain';\nimport {\n useWorkoutLogs,\n usePerformanceMetrics,\n useLogWorkout,\n useRecordMetric,\n} from '../../hooks/useAthleteBridgeApi';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type {\n AthMetricType,\n AthPerformanceMetric,\n AthWorkoutLog,\n AthWorkoutStatus,\n} from '../../types';\n\nconst PAGE_SIZE = 10;\n\nconst WORKOUT_STATUSES: AthWorkoutStatus[] = ['COMPLETED', 'PLANNED', 'SKIPPED'];\nconst METRIC_TYPES = Object.keys(METRIC_TYPE_LABELS) as AthMetricType[];\n\n/** Brand gradient CTA — token-only (no raw colors). */\nconst GRADIENT_CTA =\n 'inline-flex items-center justify-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow] hover:shadow-elevation-2 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\n/** Per-metric-type icon for the stat tiles. */\nconst METRIC_ICON: Record<AthMetricType, LucideIcon> = {\n SPEED: Zap,\n ENDURANCE: Activity,\n STRENGTH: Dumbbell,\n AGILITY: Move,\n ACCURACY: Target,\n WEIGHT: Scale,\n HEART_RATE: HeartPulse,\n CUSTOM: Gauge,\n};\n\nconst numOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nconst floatOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseFloat(t);\n return Number.isNaN(n) ? undefined : n;\n};\n\n/** Current local time as a `datetime-local` value (`YYYY-MM-DDTHH:mm`). */\nconst nowLocal = (): string => {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60_000);\n return local.toISOString().slice(0, 16);\n};\n\n/** Trim trailing zeros from a fixed-precision number for compact display. */\nconst fmtNum = (n: number): string => {\n const abs = Math.abs(n);\n const s = abs >= 100 ? n.toFixed(0) : n.toFixed(2);\n return s.replace(/\\.?0+$/, '');\n};\n\nexport function PerformancePage() {\n const [athlete, setAthlete] = useState<AthleteSelectValue>({ athleteId: '', displayName: '' });\n\n return (\n <PageLayout\n title=\"Performance\"\n description=\"Log workouts, record metrics and track an athlete's progress over time.\"\n >\n {/* Athlete picker — sticks to the top of the scroll area as an insight panel */}\n <div\n data-tour=\"performance-athlete-picker\"\n className=\"sticky top-0 z-20 -mx-6 mb-8 bg-bg-canvas/80 px-6 pb-4 pt-1 backdrop-blur supports-[backdrop-filter]:bg-bg-canvas/60\"\n >\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <AthleteSelect\n athleteId={athlete.athleteId}\n onSelect={setAthlete}\n idPrefix=\"perf-athlete\"\n />\n </GlassCard>\n </div>\n\n {athlete.athleteId ? (\n <AthletePerformance athleteId={athlete.athleteId} displayName={athlete.displayName} />\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Users className=\"h-10 w-10\" />}\n title=\"Pick an athlete to begin\"\n description=\"Choose a team and an athlete above to log workouts, record metrics and review their history.\"\n />\n )}\n </PageLayout>\n );\n}\n\ninterface AthletePerformanceProps {\n athleteId: string;\n displayName: string;\n}\n\n/** A single measured metric (type + label) collapsed into a real time-series. */\ninterface MetricSeries {\n key: string;\n type: AthMetricType;\n label: string;\n unit: string | null;\n latest: number;\n previous: number | null;\n min: number;\n max: number;\n /** Values oldest → newest (for the trend sparkline). */\n series: number[];\n recordedAt: string;\n count: number;\n}\n\nfunction AthletePerformance({ athleteId, displayName }: AthletePerformanceProps) {\n const [page, setPage] = useState(0);\n const [logOpen, setLogOpen] = useState(false);\n const [metricOpen, setMetricOpen] = useState(false);\n\n const logs = useWorkoutLogs({\n athleteId,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const metrics = usePerformanceMetrics(athleteId);\n\n const logItems = logs.data?.items ?? [];\n const hasMore = logs.data?.pageInfo.hasMore ?? false;\n\n // Group by (type + label) so every distinct measurement is its own trend.\n const metricSeries = useMemo<MetricSeries[]>(() => {\n const groups = new Map<string, AthPerformanceMetric[]>();\n for (const m of metrics.data ?? []) {\n const key = `${m.type}::${m.label}`;\n const list = groups.get(key) ?? [];\n list.push(m);\n groups.set(key, list);\n }\n return Array.from(groups.entries())\n .map(([key, list]) => {\n const asc = [...list].sort(\n (a, b) => new Date(a.recordedAt).getTime() - new Date(b.recordedAt).getTime()\n );\n const values = asc.map((m) => m.value);\n const newest = asc[asc.length - 1];\n const prior = asc.length > 1 ? asc[asc.length - 2] : null;\n return {\n key,\n type: newest.type,\n label: newest.label,\n unit: newest.unit,\n latest: newest.value,\n previous: prior ? prior.value : null,\n min: Math.min(...values),\n max: Math.max(...values),\n series: values,\n recordedAt: newest.recordedAt,\n count: asc.length,\n };\n })\n .sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime());\n }, [metrics.data]);\n\n const columns: ResponsiveTableColumn<AthWorkoutLog>[] = useMemo(\n () => [\n {\n key: 'workout',\n header: 'Workout',\n priority: 'primary',\n cell: (log) => {\n const Icon = workoutIcon(log.title);\n return (\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </span>\n <span className=\"min-w-0 truncate font-medium text-text-primary\">{log.title}</span>\n </div>\n );\n },\n },\n {\n key: 'duration',\n header: 'Duration',\n priority: 'secondary',\n mobileLabel: 'Duration',\n cell: (log) =>\n log.durationMin != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatDuration(log.durationMin)}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n {\n key: 'rpe',\n header: 'RPE',\n priority: 'secondary',\n mobileLabel: 'RPE',\n cell: (log) =>\n log.rpe != null ? <RpeChip rpe={log.rpe} /> : <span className=\"text-text-muted\">—</span>,\n },\n {\n key: 'date',\n header: 'Date',\n priority: 'secondary',\n mobileLabel: 'Date',\n cell: (log) => (\n <span className=\"tabular-nums text-text-secondary\">{formatDate(log.performedAt)}</span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'primary',\n mobileLabel: 'Status',\n cell: (log) => <StatusBadge status={log.status} />,\n },\n {\n key: 'notes',\n header: 'Notes',\n priority: 'hidden-on-mobile',\n cell: (log) =>\n log.notes ? (\n <span className=\"line-clamp-2 max-w-[22ch] text-text-secondary\">{log.notes}</span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n ],\n []\n );\n\n return (\n <div className=\"space-y-8 pb-24 sm:pb-0\">\n {/* Actions */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n Tracking <span className=\"font-medium text-text-primary\">{displayName || 'athlete'}</span>\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => setMetricOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Gauge className=\"h-4 w-4\" /> Record metric\n </button>\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'hidden sm:inline-flex')}\n >\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n </div>\n </div>\n\n {/* Metrics — hero stat tiles with real trend sparklines */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Performance metrics</h2>\n {metrics.isLoading ? (\n <LoadingState label=\"Loading metrics…\" />\n ) : metrics.isError ? (\n <ErrorState onRetry={() => void metrics.refetch()} />\n ) : metricSeries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Gauge className=\"h-10 w-10\" />}\n title=\"No metrics recorded\"\n description=\"Record a metric — speed, strength, endurance and more — to start charting progress.\"\n action={\n <button type=\"button\" onClick={() => setMetricOpen(true)} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" /> Record metric\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {metricSeries.map((m) => (\n <MetricTile key={m.key} metric={m} />\n ))}\n </div>\n )}\n </section>\n\n {/* Workout logs */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Workout history</h2>\n {logs.isLoading ? (\n <LoadingState label=\"Loading workouts…\" />\n ) : logs.isError ? (\n <ErrorState onRetry={() => void logs.refetch()} />\n ) : logItems.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title=\"No workouts logged\"\n description=\"Log a workout to build this athlete's training history.\"\n action={\n <button type=\"button\" onClick={() => setLogOpen(true)} className={GRADIENT_CTA}>\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"metric\" className=\"overflow-hidden p-1 sm:p-2\">\n <ResponsiveTable\n columns={columns}\n data={logItems}\n rowKey={(log) => log.id}\n caption={`Workout history for ${displayName || 'athlete'}`}\n />\n </GlassCard>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </section>\n\n {/* Mobile sticky Log workout — thumb-reachable primary action */}\n <div className=\"sticky bottom-0 z-30 -mx-6 border-t border-border-seam bg-bg-canvas/85 px-6 pb-[max(env(safe-area-inset-bottom),0.75rem)] pt-3 backdrop-blur sm:hidden\">\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'w-full py-3 text-base')}\n >\n <Plus className=\"h-5 w-5\" /> Log workout\n </button>\n </div>\n\n <LogWorkoutDialog\n open={logOpen}\n onOpenChange={setLogOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n <RecordMetricDialog\n open={metricOpen}\n onOpenChange={setMetricOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n </div>\n );\n}\n\n/** Pick a workout row icon from the (real) title text. Cosmetic only. */\nfunction workoutIcon(title: string): LucideIcon {\n const t = title.toLowerCase();\n if (/strength|lift|squat|deadlift|bench|1rm|weight/.test(t)) return Dumbbell;\n if (/speed|sprint|agility|accel|split/.test(t)) return Zap;\n if (/endurance|aerobic|cardio|run|beep|yo-?yo/.test(t)) return Activity;\n if (/recovery|mobility|stretch|rest/.test(t)) return HeartPulse;\n if (/accuracy|shoot|target/.test(t)) return Target;\n return Dumbbell;\n}\n\n/** Bordered RPE chip, color-scaled by session intensity (semantic tokens). */\nfunction RpeChip({ rpe }: { rpe: number }) {\n const tone =\n rpe >= 8\n ? 'border-status-error-border text-status-error-text'\n : rpe >= 5\n ? 'border-status-warning-border text-status-warning-text'\n : 'border-border-seam text-text-secondary';\n return (\n <span\n className={cn(\n 'inline-flex h-7 min-w-[1.75rem] items-center justify-center rounded-md border px-1.5 text-sm font-semibold tabular-nums',\n tone\n )}\n >\n {rpe}\n </span>\n );\n}\n\n/** Hero stat tile for one measured metric: big value, trend sparkline, range. */\nfunction MetricTile({ metric }: { metric: MetricSeries }) {\n const Icon = METRIC_ICON[metric.type];\n const delta = metric.previous == null ? null : metric.latest - metric.previous;\n const hasRange = metric.max > metric.min;\n const pct = hasRange\n ? Math.round(((metric.latest - metric.min) / (metric.max - metric.min)) * 100)\n : 100;\n const unitSuffix = metric.unit ? ` ${metric.unit}` : '';\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n {METRIC_TYPE_LABELS[metric.type]}\n </span>\n <span className=\"text-xs text-text-muted\">{formatDate(metric.recordedAt)}</span>\n </div>\n\n <div className=\"flex items-end justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-secondary\">{metric.label}</p>\n <p className=\"mt-0.5 text-3xl font-bold leading-none tracking-tight tabular-nums text-text-primary\">\n {fmtNum(metric.latest)}\n {metric.unit ? (\n <span className=\"ml-1 text-base font-medium text-text-secondary\">{metric.unit}</span>\n ) : null}\n </p>\n </div>\n {metric.series.length > 1 ? (\n <div className=\"w-24 shrink-0 sm:w-28\">\n <Sparkline\n data={metric.series}\n variant=\"area\"\n height={40}\n label={`${metric.label} trend`}\n />\n </div>\n ) : null}\n </div>\n\n {/* Range scale: min → latest position → max (from the athlete's own series) */}\n {hasRange ? (\n <div>\n <span\n className=\"block h-1.5 overflow-hidden rounded-full bg-bg-sunken\"\n aria-hidden=\"true\"\n >\n <span\n className=\"block h-1.5 rounded-full bg-[var(--color-accent-teal)]\"\n style={{ width: `${Math.max(4, Math.min(100, pct))}%` }}\n />\n </span>\n <div className=\"mt-1 flex justify-between text-[0.7rem] tabular-nums text-text-muted\">\n <span>{fmtNum(metric.min)}</span>\n <span>{fmtNum((metric.min + metric.max) / 2)}</span>\n <span>{fmtNum(metric.max)}</span>\n </div>\n </div>\n ) : null}\n\n {/* Delta vs previous reading — direction shown neutrally (no good/bad claim) */}\n <div className=\"mt-auto flex items-center gap-1.5 text-sm\">\n {delta == null || delta === 0 ? (\n <span className=\"text-text-muted\">\n {metric.count > 1 ? 'No change vs previous' : 'First reading'}\n </span>\n ) : (\n <>\n <span className=\"inline-flex items-center gap-0.5 font-medium tabular-nums text-text-secondary\">\n {delta > 0 ? (\n <ArrowUp className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <ArrowDown className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n {fmtNum(Math.abs(delta))}\n {unitSuffix}\n </span>\n <span className=\"text-text-muted\">vs previous</span>\n </>\n )}\n </div>\n </GlassCard>\n );\n}\n\ninterface DialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n athleteId: string;\n displayName: string;\n}\n\nfunction LogWorkoutDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const logWorkout = useLogWorkout();\n const [title, setTitle] = useState('');\n const [status, setStatus] = useState<AthWorkoutStatus>('COMPLETED');\n const [performedAt, setPerformedAt] = useState(nowLocal);\n const [durationMin, setDurationMin] = useState('');\n const [rpe, setRpe] = useState('');\n const [totalLoad, setTotalLoad] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setStatus('COMPLETED');\n setPerformedAt(nowLocal());\n setDurationMin('');\n setRpe('');\n setTotalLoad('');\n setNotes('');\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!title.trim()) {\n setError('A workout title is required.');\n return;\n }\n if (!performedAt) {\n setError('A performed date and time is required.');\n return;\n }\n try {\n await logWorkout.mutateAsync({\n athleteId,\n title: title.trim(),\n status,\n performedAt: new Date(performedAt).toISOString(),\n durationMin: numOrUndefined(durationMin),\n rpe: numOrUndefined(rpe),\n totalLoad: floatOrUndefined(totalLoad),\n notes: notes.trim() || undefined,\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to log workout.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Log workout{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <FormField label=\"Title\" htmlFor=\"workoutTitle\" required>\n <input\n id=\"workoutTitle\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Lower-body strength\"\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Status\" htmlFor=\"workoutStatus\">\n <select\n id=\"workoutStatus\"\n className={fieldInputClass}\n value={status}\n onChange={(e) => setStatus(e.target.value as AthWorkoutStatus)}\n >\n {WORKOUT_STATUSES.map((s) => (\n <option key={s} value={s}>\n {s.charAt(0) + s.slice(1).toLowerCase()}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Performed at\" htmlFor=\"workoutPerformedAt\" required>\n <input\n id=\"workoutPerformedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={performedAt}\n onChange={(e) => setPerformedAt(e.target.value)}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <FormField label=\"Duration (min)\" htmlFor=\"workoutDuration\">\n <input\n id=\"workoutDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </FormField>\n <FormField label=\"RPE (1–10)\" htmlFor=\"workoutRpe\">\n <input\n id=\"workoutRpe\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rpe}\n onChange={(e) => setRpe(e.target.value)}\n />\n </FormField>\n <FormField label=\"Total load\" htmlFor=\"workoutLoad\">\n <input\n id=\"workoutLoad\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={totalLoad}\n onChange={(e) => setTotalLoad(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Notes\" htmlFor=\"workoutNotes\">\n <textarea\n id=\"workoutNotes\"\n rows={2}\n className={fieldInputClass}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"How did the session go?\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={logWorkout.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {logWorkout.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Log workout\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction RecordMetricDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const recordMetric = useRecordMetric();\n const [type, setType] = useState<AthMetricType>('CUSTOM');\n const [label, setLabel] = useState('');\n const [value, setValue] = useState('');\n const [unit, setUnit] = useState('');\n const [source, setSource] = useState('');\n const [recordedAt, setRecordedAt] = useState(nowLocal);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setType('CUSTOM');\n setLabel('');\n setValue('');\n setUnit('');\n setSource('');\n setRecordedAt(nowLocal());\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!label.trim()) {\n setError('A metric label is required.');\n return;\n }\n const numericValue = floatOrUndefined(value);\n if (numericValue === undefined) {\n setError('A numeric value is required.');\n return;\n }\n if (!recordedAt) {\n setError('A recorded date and time is required.');\n return;\n }\n try {\n await recordMetric.mutateAsync({\n athleteId,\n type,\n label: label.trim(),\n value: numericValue,\n unit: unit.trim() || undefined,\n source: source.trim() || undefined,\n recordedAt: new Date(recordedAt).toISOString(),\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to record metric.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Record metric{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Type\" htmlFor=\"metricType\">\n <select\n id=\"metricType\"\n className={fieldInputClass}\n value={type}\n onChange={(e) => setType(e.target.value as AthMetricType)}\n >\n {METRIC_TYPES.map((t) => (\n <option key={t} value={t}>\n {METRIC_TYPE_LABELS[t]}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Recorded at\" htmlFor=\"metricRecordedAt\" required>\n <input\n id=\"metricRecordedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={recordedAt}\n onChange={(e) => setRecordedAt(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Label\" htmlFor=\"metricLabel\" required>\n <input\n id=\"metricLabel\"\n className={fieldInputClass}\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder=\"e.g. 40m sprint\"\n />\n </FormField>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField label=\"Value\" htmlFor=\"metricValue\" required>\n <input\n id=\"metricValue\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder=\"e.g. 4.8\"\n />\n </FormField>\n <FormField label=\"Unit\" htmlFor=\"metricUnit\">\n <input\n id=\"metricUnit\"\n className={fieldInputClass}\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n placeholder=\"e.g. s, kg, bpm\"\n />\n </FormField>\n </div>\n <FormField label=\"Source\" htmlFor=\"metricSource\" hint=\"Optional — device, test or coach.\">\n <input\n id=\"metricSource\"\n className={fieldInputClass}\n value={source}\n onChange={(e) => setSource(e.target.value)}\n placeholder=\"e.g. GPS vest\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={recordMetric.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {recordMetric.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n Record metric\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PerformancePage.js","names":[],"sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"sourcesContent":["/**\n * Performance\n * @module atheletebridge/pages/performance\n *\n * The coach's athlete-performance workbench: pick an athlete from a team roster,\n * then log workouts (athLogWorkout) and record performance metrics\n * (athRecordMetric), and review that athlete's workout history\n * (athListWorkoutLogs, offset-paginated) and metrics.\n *\n * Elevated to the concept bar: each measured metric (type + label) becomes a\n * hero stat tile with its big latest value, a real recordedAt-ordered trend\n * sparkline, a min→max range scale and a signed delta-vs-previous — all derived\n * from the real time-series (nothing fabricated). Workout history renders as a\n * responsive table that reflows to cards on mobile, with a thumb-reachable\n * sticky \"Log workout\" action.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n ArrowDown,\n ArrowUp,\n Dumbbell,\n Gauge,\n HeartPulse,\n Loader2,\n Move,\n Plus,\n Save,\n Scale,\n Target,\n Users,\n Zap,\n type LucideIcon,\n} from 'lucide-react';\nimport {\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { AthleteSelect, type AthleteSelectValue } from '../../components/AthleteSelect';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { CursorPager } from '../../components/CursorPager';\nimport { Sparkline } from '../../components/viz';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { METRIC_TYPE_LABELS } from '../../components/domain';\nimport {\n useWorkoutLogs,\n usePerformanceMetrics,\n useLogWorkout,\n useRecordMetric,\n} from '../../hooks/useAthleteBridgeApi';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type {\n AthMetricType,\n AthPerformanceMetric,\n AthWorkoutLog,\n AthWorkoutStatus,\n} from '../../types';\n\nconst PAGE_SIZE = 10;\n\nconst WORKOUT_STATUSES: AthWorkoutStatus[] = ['COMPLETED', 'PLANNED', 'SKIPPED'];\nconst METRIC_TYPES = Object.keys(METRIC_TYPE_LABELS) as AthMetricType[];\n\n/** Brand gradient CTA — token-only (no raw colors). */\nconst GRADIENT_CTA =\n 'inline-flex items-center justify-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow] hover:shadow-elevation-2 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\n/** Per-metric-type icon for the stat tiles. */\nconst METRIC_ICON: Record<AthMetricType, LucideIcon> = {\n SPEED: Zap,\n ENDURANCE: Activity,\n STRENGTH: Dumbbell,\n AGILITY: Move,\n ACCURACY: Target,\n WEIGHT: Scale,\n HEART_RATE: HeartPulse,\n CUSTOM: Gauge,\n};\n\nconst numOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nconst floatOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseFloat(t);\n return Number.isNaN(n) ? undefined : n;\n};\n\n/** Current local time as a `datetime-local` value (`YYYY-MM-DDTHH:mm`). */\nconst nowLocal = (): string => {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60_000);\n return local.toISOString().slice(0, 16);\n};\n\n/** Trim trailing zeros from a fixed-precision number for compact display. */\nconst fmtNum = (n: number): string => {\n const abs = Math.abs(n);\n const s = abs >= 100 ? n.toFixed(0) : n.toFixed(2);\n return s.replace(/\\.?0+$/, '');\n};\n\nexport function PerformancePage() {\n const [athlete, setAthlete] = useState<AthleteSelectValue>({ athleteId: '', displayName: '' });\n\n return (\n <PageLayout\n title=\"Performance\"\n description=\"Log workouts, record metrics and track an athlete's progress over time.\"\n >\n {/* Athlete picker — sticks to the top of the scroll area as an insight panel */}\n <div\n data-tour=\"performance-athlete-picker\"\n className=\"sticky top-0 z-20 -mx-6 mb-8 bg-bg-canvas/80 px-6 pb-4 pt-1 backdrop-blur supports-[backdrop-filter]:bg-bg-canvas/60\"\n >\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <AthleteSelect\n athleteId={athlete.athleteId}\n onSelect={setAthlete}\n idPrefix=\"perf-athlete\"\n />\n </GlassCard>\n </div>\n\n {athlete.athleteId ? (\n <AthletePerformance athleteId={athlete.athleteId} displayName={athlete.displayName} />\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Users className=\"h-10 w-10\" />}\n title=\"Pick an athlete to begin\"\n description=\"Choose a team and an athlete above to log workouts, record metrics and review their history.\"\n />\n )}\n </PageLayout>\n );\n}\n\ninterface AthletePerformanceProps {\n athleteId: string;\n displayName: string;\n}\n\n/** A single measured metric (type + label) collapsed into a real time-series. */\ninterface MetricSeries {\n key: string;\n type: AthMetricType;\n label: string;\n unit: string | null;\n latest: number;\n previous: number | null;\n min: number;\n max: number;\n /** Values oldest → newest (for the trend sparkline). */\n series: number[];\n recordedAt: string;\n count: number;\n}\n\nfunction AthletePerformance({ athleteId, displayName }: AthletePerformanceProps) {\n const [page, setPage] = useState(0);\n const [logOpen, setLogOpen] = useState(false);\n const [metricOpen, setMetricOpen] = useState(false);\n\n const logs = useWorkoutLogs({\n athleteId,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const metrics = usePerformanceMetrics(athleteId);\n\n const logItems = logs.data?.items ?? [];\n const hasMore = logs.data?.pageInfo.hasMore ?? false;\n\n // Group by (type + label) so every distinct measurement is its own trend.\n const metricSeries = useMemo<MetricSeries[]>(() => {\n const groups = new Map<string, AthPerformanceMetric[]>();\n for (const m of metrics.data ?? []) {\n const key = `${m.type}::${m.label}`;\n const list = groups.get(key) ?? [];\n list.push(m);\n groups.set(key, list);\n }\n return Array.from(groups.entries())\n .map(([key, list]) => {\n const asc = [...list].sort(\n (a, b) => new Date(a.recordedAt).getTime() - new Date(b.recordedAt).getTime()\n );\n const values = asc.map((m) => m.value);\n const newest = asc[asc.length - 1];\n const prior = asc.length > 1 ? asc[asc.length - 2] : null;\n return {\n key,\n type: newest.type,\n label: newest.label,\n unit: newest.unit,\n latest: newest.value,\n previous: prior ? prior.value : null,\n min: Math.min(...values),\n max: Math.max(...values),\n series: values,\n recordedAt: newest.recordedAt,\n count: asc.length,\n };\n })\n .sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime());\n }, [metrics.data]);\n\n const columns: ResponsiveTableColumn<AthWorkoutLog>[] = useMemo(\n () => [\n {\n key: 'workout',\n header: 'Workout',\n priority: 'primary',\n cell: (log) => {\n const Icon = workoutIcon(log.title);\n return (\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </span>\n <span className=\"min-w-0 truncate font-medium text-text-primary\">{log.title}</span>\n </div>\n );\n },\n },\n {\n key: 'duration',\n header: 'Duration',\n priority: 'secondary',\n mobileLabel: 'Duration',\n cell: (log) =>\n log.durationMin != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatDuration(log.durationMin)}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n {\n key: 'rpe',\n header: 'RPE',\n priority: 'secondary',\n mobileLabel: 'RPE',\n cell: (log) =>\n log.rpe != null ? <RpeChip rpe={log.rpe} /> : <span className=\"text-text-muted\">—</span>,\n },\n {\n key: 'date',\n header: 'Date',\n priority: 'secondary',\n mobileLabel: 'Date',\n cell: (log) => (\n <span className=\"tabular-nums text-text-secondary\">{formatDate(log.performedAt)}</span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'primary',\n mobileLabel: 'Status',\n cell: (log) => <StatusBadge status={log.status} />,\n },\n {\n key: 'notes',\n header: 'Notes',\n priority: 'hidden-on-mobile',\n cell: (log) =>\n log.notes ? (\n <span className=\"line-clamp-2 max-w-[22ch] text-text-secondary\">{log.notes}</span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n ],\n []\n );\n\n return (\n <div className=\"space-y-8 pb-24 sm:pb-0\">\n {/* Actions */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n Tracking <span className=\"font-medium text-text-primary\">{displayName || 'athlete'}</span>\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => setMetricOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Gauge className=\"h-4 w-4\" /> Record metric\n </button>\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'hidden sm:inline-flex')}\n >\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n </div>\n </div>\n\n {/* Metrics — hero stat tiles with real trend sparklines */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Performance metrics</h2>\n {metrics.isLoading ? (\n <LoadingState label=\"Loading metrics…\" />\n ) : metrics.isError ? (\n <ErrorState onRetry={() => void metrics.refetch()} />\n ) : metricSeries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Gauge className=\"h-10 w-10\" />}\n title=\"No metrics recorded\"\n description=\"Record a metric — speed, strength, endurance and more — to start charting progress.\"\n action={\n <button type=\"button\" onClick={() => setMetricOpen(true)} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" /> Record metric\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {metricSeries.map((m) => (\n <MetricTile key={m.key} metric={m} />\n ))}\n </div>\n )}\n </section>\n\n {/* Workout logs */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Workout history</h2>\n {logs.isLoading ? (\n <LoadingState label=\"Loading workouts…\" />\n ) : logs.isError ? (\n <ErrorState onRetry={() => void logs.refetch()} />\n ) : logItems.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title=\"No workouts logged\"\n description=\"Log a workout to build this athlete's training history.\"\n action={\n <button type=\"button\" onClick={() => setLogOpen(true)} className={GRADIENT_CTA}>\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"metric\" className=\"overflow-hidden p-1 sm:p-2\">\n <ResponsiveTable\n columns={columns}\n data={logItems}\n rowKey={(log) => log.id}\n caption={`Workout history for ${displayName || 'athlete'}`}\n />\n </GlassCard>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </section>\n\n {/* Mobile sticky Log workout — thumb-reachable primary action */}\n <div className=\"sticky bottom-0 z-30 -mx-6 border-t border-border-seam bg-bg-canvas/85 px-6 pb-[max(env(safe-area-inset-bottom),0.75rem)] pt-3 backdrop-blur sm:hidden\">\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'w-full py-3 text-base')}\n >\n <Plus className=\"h-5 w-5\" /> Log workout\n </button>\n </div>\n\n <LogWorkoutDialog\n open={logOpen}\n onOpenChange={setLogOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n <RecordMetricDialog\n open={metricOpen}\n onOpenChange={setMetricOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n </div>\n );\n}\n\n/** Pick a workout row icon from the (real) title text. Cosmetic only. */\nfunction workoutIcon(title: string): LucideIcon {\n const t = title.toLowerCase();\n if (/strength|lift|squat|deadlift|bench|1rm|weight/.test(t)) return Dumbbell;\n if (/speed|sprint|agility|accel|split/.test(t)) return Zap;\n if (/endurance|aerobic|cardio|run|beep|yo-?yo/.test(t)) return Activity;\n if (/recovery|mobility|stretch|rest/.test(t)) return HeartPulse;\n if (/accuracy|shoot|target/.test(t)) return Target;\n return Dumbbell;\n}\n\n/** Bordered RPE chip, color-scaled by session intensity (semantic tokens). */\nfunction RpeChip({ rpe }: { rpe: number }) {\n const tone =\n rpe >= 8\n ? 'border-status-error-border text-status-error-text'\n : rpe >= 5\n ? 'border-status-warning-border text-status-warning-text'\n : 'border-border-seam text-text-secondary';\n return (\n <span\n className={cn(\n 'inline-flex h-7 min-w-[1.75rem] items-center justify-center rounded-md border px-1.5 text-sm font-semibold tabular-nums',\n tone\n )}\n >\n {rpe}\n </span>\n );\n}\n\n/** Hero stat tile for one measured metric: big value, trend sparkline, range. */\nfunction MetricTile({ metric }: { metric: MetricSeries }) {\n const Icon = METRIC_ICON[metric.type];\n const delta = metric.previous == null ? null : metric.latest - metric.previous;\n const hasRange = metric.max > metric.min;\n const pct = hasRange\n ? Math.round(((metric.latest - metric.min) / (metric.max - metric.min)) * 100)\n : 100;\n const unitSuffix = metric.unit ? ` ${metric.unit}` : '';\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n {METRIC_TYPE_LABELS[metric.type]}\n </span>\n <span className=\"text-xs text-text-muted\">{formatDate(metric.recordedAt)}</span>\n </div>\n\n <div className=\"flex items-end justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-secondary\">{metric.label}</p>\n <p className=\"mt-0.5 text-3xl font-bold leading-none tracking-tight tabular-nums text-text-primary\">\n {fmtNum(metric.latest)}\n {metric.unit ? (\n <span className=\"ml-1 text-base font-medium text-text-secondary\">{metric.unit}</span>\n ) : null}\n </p>\n </div>\n {metric.series.length > 1 ? (\n <div className=\"w-24 shrink-0 sm:w-28\">\n <Sparkline\n data={metric.series}\n variant=\"area\"\n height={40}\n label={`${metric.label} trend`}\n />\n </div>\n ) : null}\n </div>\n\n {/* Range scale: min → latest position → max (from the athlete's own series) */}\n {hasRange ? (\n <div>\n <span\n className=\"block h-1.5 overflow-hidden rounded-full bg-bg-sunken\"\n aria-hidden=\"true\"\n >\n <span\n className=\"block h-1.5 rounded-full bg-[var(--color-accent-teal)]\"\n style={{ width: `${Math.max(4, Math.min(100, pct))}%` }}\n />\n </span>\n <div className=\"mt-1 flex justify-between text-[0.7rem] tabular-nums text-text-muted\">\n <span>{fmtNum(metric.min)}</span>\n <span>{fmtNum((metric.min + metric.max) / 2)}</span>\n <span>{fmtNum(metric.max)}</span>\n </div>\n </div>\n ) : null}\n\n {/* Delta vs previous reading — direction shown neutrally (no good/bad claim) */}\n <div className=\"mt-auto flex items-center gap-1.5 text-sm\">\n {delta == null || delta === 0 ? (\n <span className=\"text-text-muted\">\n {metric.count > 1 ? 'No change vs previous' : 'First reading'}\n </span>\n ) : (\n <>\n <span className=\"inline-flex items-center gap-0.5 font-medium tabular-nums text-text-secondary\">\n {delta > 0 ? (\n <ArrowUp className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <ArrowDown className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n {fmtNum(Math.abs(delta))}\n {unitSuffix}\n </span>\n <span className=\"text-text-muted\">vs previous</span>\n </>\n )}\n </div>\n </GlassCard>\n );\n}\n\ninterface DialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n athleteId: string;\n displayName: string;\n}\n\nfunction LogWorkoutDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const logWorkout = useLogWorkout();\n const [title, setTitle] = useState('');\n const [status, setStatus] = useState<AthWorkoutStatus>('COMPLETED');\n const [performedAt, setPerformedAt] = useState(nowLocal);\n const [durationMin, setDurationMin] = useState('');\n const [rpe, setRpe] = useState('');\n const [totalLoad, setTotalLoad] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setStatus('COMPLETED');\n setPerformedAt(nowLocal());\n setDurationMin('');\n setRpe('');\n setTotalLoad('');\n setNotes('');\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!title.trim()) {\n setError('A workout title is required.');\n return;\n }\n if (!performedAt) {\n setError('A performed date and time is required.');\n return;\n }\n try {\n await logWorkout.mutateAsync({\n athleteId,\n title: title.trim(),\n status,\n performedAt: new Date(performedAt).toISOString(),\n durationMin: numOrUndefined(durationMin),\n rpe: numOrUndefined(rpe),\n totalLoad: floatOrUndefined(totalLoad),\n notes: notes.trim() || undefined,\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to log workout.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Log workout{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <FormField label=\"Title\" htmlFor=\"workoutTitle\" required>\n <input\n id=\"workoutTitle\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Lower-body strength\"\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Status\" htmlFor=\"workoutStatus\">\n <select\n id=\"workoutStatus\"\n className={fieldInputClass}\n value={status}\n onChange={(e) => setStatus(e.target.value as AthWorkoutStatus)}\n >\n {WORKOUT_STATUSES.map((s) => (\n <option key={s} value={s}>\n {s.charAt(0) + s.slice(1).toLowerCase()}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Performed at\" htmlFor=\"workoutPerformedAt\" required>\n <input\n id=\"workoutPerformedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={performedAt}\n onChange={(e) => setPerformedAt(e.target.value)}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <FormField label=\"Duration (min)\" htmlFor=\"workoutDuration\">\n <input\n id=\"workoutDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </FormField>\n <FormField label=\"RPE (1–10)\" htmlFor=\"workoutRpe\">\n <input\n id=\"workoutRpe\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rpe}\n onChange={(e) => setRpe(e.target.value)}\n />\n </FormField>\n <FormField label=\"Total load\" htmlFor=\"workoutLoad\">\n <input\n id=\"workoutLoad\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={totalLoad}\n onChange={(e) => setTotalLoad(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Notes\" htmlFor=\"workoutNotes\">\n <textarea\n id=\"workoutNotes\"\n rows={2}\n className={fieldInputClass}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"How did the session go?\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={logWorkout.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {logWorkout.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Log workout\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction RecordMetricDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const recordMetric = useRecordMetric();\n const [type, setType] = useState<AthMetricType>('CUSTOM');\n const [label, setLabel] = useState('');\n const [value, setValue] = useState('');\n const [unit, setUnit] = useState('');\n const [source, setSource] = useState('');\n const [recordedAt, setRecordedAt] = useState(nowLocal);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setType('CUSTOM');\n setLabel('');\n setValue('');\n setUnit('');\n setSource('');\n setRecordedAt(nowLocal());\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!label.trim()) {\n setError('A metric label is required.');\n return;\n }\n const numericValue = floatOrUndefined(value);\n if (numericValue === undefined) {\n setError('A numeric value is required.');\n return;\n }\n if (!recordedAt) {\n setError('A recorded date and time is required.');\n return;\n }\n try {\n await recordMetric.mutateAsync({\n athleteId,\n type,\n label: label.trim(),\n value: numericValue,\n unit: unit.trim() || undefined,\n source: source.trim() || undefined,\n recordedAt: new Date(recordedAt).toISOString(),\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to record metric.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Record metric{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Type\" htmlFor=\"metricType\">\n <select\n id=\"metricType\"\n className={fieldInputClass}\n value={type}\n onChange={(e) => setType(e.target.value as AthMetricType)}\n >\n {METRIC_TYPES.map((t) => (\n <option key={t} value={t}>\n {METRIC_TYPE_LABELS[t]}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Recorded at\" htmlFor=\"metricRecordedAt\" required>\n <input\n id=\"metricRecordedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={recordedAt}\n onChange={(e) => setRecordedAt(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Label\" htmlFor=\"metricLabel\" required>\n <input\n id=\"metricLabel\"\n className={fieldInputClass}\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder=\"e.g. 40m sprint\"\n />\n </FormField>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField label=\"Value\" htmlFor=\"metricValue\" required>\n <input\n id=\"metricValue\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder=\"e.g. 4.8\"\n />\n </FormField>\n <FormField label=\"Unit\" htmlFor=\"metricUnit\">\n <input\n id=\"metricUnit\"\n className={fieldInputClass}\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n placeholder=\"e.g. s, kg, bpm\"\n />\n </FormField>\n </div>\n <FormField label=\"Source\" htmlFor=\"metricSource\" hint=\"Optional — device, test or coach.\">\n <input\n id=\"metricSource\"\n className={fieldInputClass}\n value={source}\n onChange={(e) => setSource(e.target.value)}\n placeholder=\"e.g. GPS vest\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={recordMetric.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {recordMetric.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n Record metric\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProgramDetailPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/programs/ProgramDetailPage.tsx"],"sourcesContent":["/**\n * Program Detail\n * @module atheletebridge/pages/programs\n *\n * Reads a single program with its drills (athProgram), supports the publish\n * lifecycle (athTransitionProgram), editing/archiving, and adding/removing\n * drills (athAddDrill / athRemoveDrill).\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Edit,\n Archive,\n Plus,\n Trash2,\n Loader2,\n Send,\n Undo2,\n Copy,\n FileVideo,\n} from 'lucide-react';\nimport {\n GlassCard,\n Badge,\n Accordion,\n AccordionItem,\n AccordionTrigger,\n AccordionContent,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { DrillVideoLink } from '../../components/DrillVideoLink';\nimport { SPORT_ICONS, SPORT_LABELS, PROGRAM_LEVEL_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport {\n useProgram,\n useTransitionProgram,\n useArchiveProgram,\n useAddDrill,\n useRemoveDrill,\n useUploadFile,\n} from '../../hooks/useAthleteBridgeApi';\nimport type { AthDrill, AthProgramStatus } from '../../types';\n\ninterface DrillForm {\n name: string;\n description: string;\n weekNumber: string;\n sets: string;\n reps: string;\n durationSec: string;\n restSec: string;\n}\n\nconst EMPTY_DRILL: DrillForm = {\n name: '',\n description: '',\n weekNumber: '1',\n sets: '',\n reps: '',\n durationSec: '',\n restSec: '',\n};\n\n/** Small tokenized metric pill for a drill's sets/reps/work/rest. */\nconst MetricChip = ({ children }: { children: React.ReactNode }) => (\n <span className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-sunken px-2 py-0.5 text-xs font-medium tabular-nums text-text-secondary\">\n {children}\n </span>\n);\n\nconst numOrUndefined = (v: string): number | undefined => {\n const t = v.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nexport function ProgramDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const program = useProgram(id);\n const transition = useTransitionProgram();\n const archive = useArchiveProgram();\n const addDrill = useAddDrill();\n const removeDrill = useRemoveDrill(id);\n const uploadFile = useUploadFile();\n\n const [drillOpen, setDrillOpen] = useState(false);\n const [drillForm, setDrillForm] = useState<DrillForm>(EMPTY_DRILL);\n const [videoFile, setVideoFile] = useState<File | null>(null);\n const [uploadingVideo, setUploadingVideo] = useState(false);\n const [drillError, setDrillError] = useState<string | null>(null);\n\n const drillsByWeek = useMemo(() => {\n const drills = program.data?.drills?.items ?? [];\n const groups = new Map<number, AthDrill[]>();\n for (const d of drills) {\n const list = groups.get(d.weekNumber) ?? [];\n list.push(d);\n groups.set(d.weekNumber, list);\n }\n return Array.from(groups.entries())\n .sort(([a], [b]) => a - b)\n .map(([week, list]) => ({\n week,\n drills: [...list].sort((a, b) => a.orderIndex - b.orderIndex),\n }));\n }, [program.data]);\n\n if (program.isLoading) {\n return (\n <PageLayout title=\"Program\">\n <LoadingState label=\"Loading program…\" />\n </PageLayout>\n );\n }\n\n if (program.isError || !program.data) {\n return (\n <PageLayout title=\"Program\">\n <ErrorState\n title=\"Program not found\"\n message=\"This program does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/programs')}\n />\n </PageLayout>\n );\n }\n\n const p = program.data;\n const drillCount = p.drills?.items.length ?? 0;\n const SportIcon = SPORT_ICONS[p.sport];\n const openWeeks = drillsByWeek.map(({ week }) => `week-${week}`);\n\n const handleTransition = (status: AthProgramStatus) => {\n if (!id) return;\n void transition.mutate({ id, status });\n };\n\n const handleArchive = async () => {\n if (!id) return;\n await archive.mutateAsync(id);\n navigateTo('/programs');\n };\n\n const handleAddDrill = async (e: React.FormEvent) => {\n e.preventDefault();\n setDrillError(null);\n if (!id) return;\n if (!drillForm.name.trim()) {\n setDrillError('Drill name is required.');\n return;\n }\n const week = numOrUndefined(drillForm.weekNumber) ?? 1;\n\n let videoFileId: string | undefined;\n if (videoFile) {\n setUploadingVideo(true);\n try {\n videoFileId = await uploadFile(videoFile);\n } catch (err) {\n setDrillError(err instanceof Error ? err.message : 'Failed to upload the demo video.');\n setUploadingVideo(false);\n return;\n }\n setUploadingVideo(false);\n }\n\n try {\n await addDrill.mutateAsync({\n programId: id,\n name: drillForm.name.trim(),\n description: drillForm.description.trim() || undefined,\n weekNumber: week,\n orderIndex: program.data?.drills?.items.filter((d) => d.weekNumber === week).length ?? 0,\n sets: numOrUndefined(drillForm.sets),\n reps: numOrUndefined(drillForm.reps),\n durationSec: numOrUndefined(drillForm.durationSec),\n restSec: numOrUndefined(drillForm.restSec),\n videoFileId,\n });\n setDrillForm(EMPTY_DRILL);\n setVideoFile(null);\n setDrillOpen(false);\n } catch (err) {\n setDrillError(err instanceof Error ? err.message : 'Failed to add drill.');\n }\n };\n\n return (\n <PageLayout\n title={p.name}\n gradientTitle\n description={`${SPORT_LABELS[p.sport]} · ${PROGRAM_LEVEL_LABELS[p.level]} · ${p.weeks} ${\n p.weeks === 1 ? 'week' : 'weeks'\n }`}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/programs')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/programs/${p.id}/edit`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Edit className=\"h-4 w-4\" /> Edit\n </button>\n {p.status !== 'ARCHIVED' && (\n <button\n type=\"button\"\n onClick={handleArchive}\n disabled={archive.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-60\"\n >\n {archive.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Archive className=\"h-4 w-4\" />\n )}{' '}\n Archive\n </button>\n )}\n </div>\n }\n >\n {/* Summary + lifecycle */}\n <GlassCard treatment=\"insight\" className=\"mb-8 p-6\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-[var(--color-accent-soft)] text-text-primary\">\n <SportIcon className=\"h-7 w-7\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <Badge variant=\"secondary\">{SPORT_LABELS[p.sport]}</Badge>\n <Badge variant=\"secondary\">{PROGRAM_LEVEL_LABELS[p.level]}</Badge>\n <StatusBadge status={p.status} />\n {p.isTemplate && (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-secondary\">\n <Copy className=\"h-3.5 w-3.5\" /> Template\n </span>\n )}\n </div>\n <p className=\"mt-2 text-sm text-text-secondary\">\n {p.weeks} {p.weeks === 1 ? 'week' : 'weeks'} · {drillCount}{' '}\n {drillCount === 1 ? 'drill' : 'drills'}\n </p>\n </div>\n </div>\n {p.summary && <p className=\"mt-4 text-sm text-text-secondary\">{p.summary}</p>}\n {p.tags.length > 0 && (\n <div className=\"mt-4 flex flex-wrap gap-1.5\">\n {p.tags.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {/* Lifecycle transitions */}\n <div className=\"mt-5 flex flex-wrap items-center gap-2 border-t border-border-seam pt-5\">\n <span className=\"text-sm font-medium text-text-primary\">Publish lifecycle:</span>\n {p.status === 'DRAFT' && (\n <button\n type=\"button\"\n onClick={() => handleTransition('PUBLISHED')}\n disabled={transition.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-1.5 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n <Send className=\"h-4 w-4\" /> Publish\n </button>\n )}\n {p.status === 'PUBLISHED' && (\n <>\n <button\n type=\"button\"\n onClick={() => handleTransition('DRAFT')}\n disabled={transition.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-60\"\n >\n <Undo2 className=\"h-4 w-4\" /> Pull back to draft\n </button>\n <button\n type=\"button\"\n onClick={() => handleTransition('ARCHIVED')}\n disabled={transition.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-60\"\n >\n <Archive className=\"h-4 w-4\" /> Archive\n </button>\n </>\n )}\n {p.status === 'ARCHIVED' && (\n <span className=\"text-sm text-text-secondary\">This program is archived.</span>\n )}\n {transition.isPending && <Loader2 className=\"h-4 w-4 animate-spin text-text-secondary\" />}\n </div>\n </GlassCard>\n\n {/* Drills */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Drills ({drillCount})</h2>\n <button\n type=\"button\"\n onClick={() => {\n setDrillError(null);\n setDrillOpen(true);\n }}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-1.5 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" /> Add drill\n </button>\n </div>\n\n {drillCount === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Plus className=\"h-10 w-10\" />}\n title=\"No drills yet\"\n description=\"Add drills with sets, reps and durations, organized by week.\"\n />\n ) : (\n <Accordion type=\"multiple\" defaultValue={openWeeks} className=\"flex flex-col gap-3\">\n {drillsByWeek.map(({ week, drills }) => (\n <GlassCard key={week} treatment=\"insight\" className=\"overflow-hidden px-4 py-0\">\n <AccordionItem value={`week-${week}`} className=\"border-b-0\">\n <AccordionTrigger className=\"py-4\">\n <span className=\"flex items-center gap-2.5\">\n <span className=\"text-sm font-semibold text-text-primary\">Week {week}</span>\n <Badge variant=\"secondary\">\n {drills.length} {drills.length === 1 ? 'drill' : 'drills'}\n </Badge>\n </span>\n </AccordionTrigger>\n <AccordionContent className=\"pb-2\">\n <div className=\"divide-y divide-border-seam\">\n {drills.map((d) => (\n <div key={d.id} className=\"flex items-start gap-4 py-3\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">{d.name}</p>\n {d.description && (\n <p className=\"mt-0.5 text-xs text-text-secondary\">{d.description}</p>\n )}\n <div className=\"mt-2 flex flex-wrap gap-1.5\">\n {d.sets !== null && <MetricChip>{d.sets} sets</MetricChip>}\n {d.reps !== null && <MetricChip>{d.reps} reps</MetricChip>}\n {d.durationSec !== null && (\n <MetricChip>{d.durationSec}s work</MetricChip>\n )}\n {d.restSec !== null && <MetricChip>{d.restSec}s rest</MetricChip>}\n </div>\n {d.video?.id && (\n <div>\n <DrillVideoLink fileId={d.video.id} />\n </div>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => void removeDrill.mutate(d.id)}\n disabled={removeDrill.isPending}\n aria-label={`Remove ${d.name}`}\n className=\"rounded-lg p-2 text-text-secondary transition-colors hover:bg-status-error-bg-subtle hover:text-status-error-text disabled:opacity-60\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n ))}\n </div>\n </AccordionContent>\n </AccordionItem>\n </GlassCard>\n ))}\n </Accordion>\n )}\n </section>\n\n {/* Add drill dialog */}\n <ResponsiveDialog open={drillOpen} onOpenChange={setDrillOpen}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add drill</ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleAddDrill} className=\"space-y-4 px-1\">\n <FormField label=\"Name\" htmlFor=\"drillName\" required>\n <input\n id=\"drillName\"\n className={fieldInputClass}\n value={drillForm.name}\n onChange={(e) => setDrillForm((p) => ({ ...p, name: e.target.value }))}\n placeholder=\"e.g. Box jumps\"\n />\n </FormField>\n <FormField label=\"Description\" htmlFor=\"drillDesc\">\n <textarea\n id=\"drillDesc\"\n className={fieldInputClass}\n rows={2}\n value={drillForm.description}\n onChange={(e) => setDrillForm((p) => ({ ...p, description: e.target.value }))}\n placeholder=\"How to perform the drill.\"\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <FormField label=\"Week\" htmlFor=\"drillWeek\">\n <input\n id=\"drillWeek\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.weekNumber}\n onChange={(e) => setDrillForm((p) => ({ ...p, weekNumber: e.target.value }))}\n />\n </FormField>\n <FormField label=\"Sets\" htmlFor=\"drillSets\">\n <input\n id=\"drillSets\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.sets}\n onChange={(e) => setDrillForm((p) => ({ ...p, sets: e.target.value }))}\n />\n </FormField>\n <FormField label=\"Reps\" htmlFor=\"drillReps\">\n <input\n id=\"drillReps\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.reps}\n onChange={(e) => setDrillForm((p) => ({ ...p, reps: e.target.value }))}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField label=\"Work (sec)\" htmlFor=\"drillDuration\">\n <input\n id=\"drillDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.durationSec}\n onChange={(e) => setDrillForm((p) => ({ ...p, durationSec: e.target.value }))}\n />\n </FormField>\n <FormField label=\"Rest (sec)\" htmlFor=\"drillRest\">\n <input\n id=\"drillRest\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.restSec}\n onChange={(e) => setDrillForm((p) => ({ ...p, restSec: e.target.value }))}\n />\n </FormField>\n </div>\n <FormField\n label=\"Demo video\"\n htmlFor=\"drillVideo\"\n hint=\"Optional — attach a short demo clip; it's stored in workspace files.\"\n >\n <input\n id=\"drillVideo\"\n type=\"file\"\n accept=\"video/*\"\n className={fieldInputClass}\n onChange={(e) => setVideoFile(e.target.files?.[0] ?? null)}\n />\n {videoFile && (\n <p className=\"mt-1 inline-flex items-center gap-1 text-xs text-text-secondary\">\n <FileVideo className=\"h-3.5 w-3.5\" /> {videoFile.name}\n </p>\n )}\n </FormField>\n {drillError && <p className=\"text-sm text-status-error-text\">{drillError}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => {\n setDrillOpen(false);\n setVideoFile(null);\n }}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={addDrill.isPending || uploadingVideo}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {(addDrill.isPending || uploadingVideo) && (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n )}\n {uploadingVideo ? 'Uploading video…' : 'Add drill'}\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProgramDetailPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/programs/ProgramDetailPage.tsx"],"sourcesContent":["/**\n * Program Detail\n * @module atheletebridge/pages/programs\n *\n * Reads a single program with its drills (athProgram), supports the publish\n * lifecycle (athTransitionProgram), editing/archiving, and adding/removing\n * drills (athAddDrill / athRemoveDrill).\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Edit,\n Archive,\n Plus,\n Trash2,\n Loader2,\n Send,\n Undo2,\n Copy,\n FileVideo,\n} from 'lucide-react';\nimport {\n GlassCard,\n Badge,\n Accordion,\n AccordionItem,\n AccordionTrigger,\n AccordionContent,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { DrillVideoLink } from '../../components/DrillVideoLink';\nimport { SPORT_ICONS, SPORT_LABELS, PROGRAM_LEVEL_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport {\n useProgram,\n useTransitionProgram,\n useArchiveProgram,\n useAddDrill,\n useRemoveDrill,\n useUploadFile,\n} from '../../hooks/useAthleteBridgeApi';\nimport type { AthDrill, AthProgramStatus } from '../../types';\n\ninterface DrillForm {\n name: string;\n description: string;\n weekNumber: string;\n sets: string;\n reps: string;\n durationSec: string;\n restSec: string;\n}\n\nconst EMPTY_DRILL: DrillForm = {\n name: '',\n description: '',\n weekNumber: '1',\n sets: '',\n reps: '',\n durationSec: '',\n restSec: '',\n};\n\n/** Small tokenized metric pill for a drill's sets/reps/work/rest. */\nconst MetricChip = ({ children }: { children: React.ReactNode }) => (\n <span className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-sunken px-2 py-0.5 text-xs font-medium tabular-nums text-text-secondary\">\n {children}\n </span>\n);\n\nconst numOrUndefined = (v: string): number | undefined => {\n const t = v.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nexport function ProgramDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const program = useProgram(id);\n const transition = useTransitionProgram();\n const archive = useArchiveProgram();\n const addDrill = useAddDrill();\n const removeDrill = useRemoveDrill(id);\n const uploadFile = useUploadFile();\n\n const [drillOpen, setDrillOpen] = useState(false);\n const [drillForm, setDrillForm] = useState<DrillForm>(EMPTY_DRILL);\n const [videoFile, setVideoFile] = useState<File | null>(null);\n const [uploadingVideo, setUploadingVideo] = useState(false);\n const [drillError, setDrillError] = useState<string | null>(null);\n\n const drillsByWeek = useMemo(() => {\n const drills = program.data?.drills?.items ?? [];\n const groups = new Map<number, AthDrill[]>();\n for (const d of drills) {\n const list = groups.get(d.weekNumber) ?? [];\n list.push(d);\n groups.set(d.weekNumber, list);\n }\n return Array.from(groups.entries())\n .sort(([a], [b]) => a - b)\n .map(([week, list]) => ({\n week,\n drills: [...list].sort((a, b) => a.orderIndex - b.orderIndex),\n }));\n }, [program.data]);\n\n if (program.isLoading) {\n return (\n <PageLayout title=\"Program\">\n <LoadingState label=\"Loading program…\" />\n </PageLayout>\n );\n }\n\n if (program.isError || !program.data) {\n return (\n <PageLayout title=\"Program\">\n <ErrorState\n title=\"Program not found\"\n message=\"This program does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/programs')}\n />\n </PageLayout>\n );\n }\n\n const p = program.data;\n const drillCount = p.drills?.items.length ?? 0;\n const SportIcon = SPORT_ICONS[p.sport];\n const openWeeks = drillsByWeek.map(({ week }) => `week-${week}`);\n\n const handleTransition = (status: AthProgramStatus) => {\n if (!id) return;\n void transition.mutate({ id, status });\n };\n\n const handleArchive = async () => {\n if (!id) return;\n await archive.mutateAsync(id);\n navigateTo('/programs');\n };\n\n const handleAddDrill = async (e: React.FormEvent) => {\n e.preventDefault();\n setDrillError(null);\n if (!id) return;\n if (!drillForm.name.trim()) {\n setDrillError('Drill name is required.');\n return;\n }\n const week = numOrUndefined(drillForm.weekNumber) ?? 1;\n\n let videoFileId: string | undefined;\n if (videoFile) {\n setUploadingVideo(true);\n try {\n videoFileId = await uploadFile(videoFile);\n } catch (err) {\n setDrillError(err instanceof Error ? err.message : 'Failed to upload the demo video.');\n setUploadingVideo(false);\n return;\n }\n setUploadingVideo(false);\n }\n\n try {\n await addDrill.mutateAsync({\n programId: id,\n name: drillForm.name.trim(),\n description: drillForm.description.trim() || undefined,\n weekNumber: week,\n orderIndex: program.data?.drills?.items.filter((d) => d.weekNumber === week).length ?? 0,\n sets: numOrUndefined(drillForm.sets),\n reps: numOrUndefined(drillForm.reps),\n durationSec: numOrUndefined(drillForm.durationSec),\n restSec: numOrUndefined(drillForm.restSec),\n videoFileId,\n });\n setDrillForm(EMPTY_DRILL);\n setVideoFile(null);\n setDrillOpen(false);\n } catch (err) {\n setDrillError(err instanceof Error ? err.message : 'Failed to add drill.');\n }\n };\n\n return (\n <PageLayout\n title={p.name}\n gradientTitle\n description={`${SPORT_LABELS[p.sport]} · ${PROGRAM_LEVEL_LABELS[p.level]} · ${p.weeks} ${\n p.weeks === 1 ? 'week' : 'weeks'\n }`}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/programs')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/programs/${p.id}/edit`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Edit className=\"h-4 w-4\" /> Edit\n </button>\n {p.status !== 'ARCHIVED' && (\n <button\n type=\"button\"\n onClick={handleArchive}\n disabled={archive.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-60\"\n >\n {archive.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Archive className=\"h-4 w-4\" />\n )}{' '}\n Archive\n </button>\n )}\n </div>\n }\n >\n {/* Summary + lifecycle */}\n <GlassCard treatment=\"insight\" className=\"mb-8 p-6\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-[var(--color-accent-soft)] text-text-primary\">\n <SportIcon className=\"h-7 w-7\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <Badge variant=\"secondary\">{SPORT_LABELS[p.sport]}</Badge>\n <Badge variant=\"secondary\">{PROGRAM_LEVEL_LABELS[p.level]}</Badge>\n <StatusBadge status={p.status} />\n {p.isTemplate && (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-secondary\">\n <Copy className=\"h-3.5 w-3.5\" /> Template\n </span>\n )}\n </div>\n <p className=\"mt-2 text-sm text-text-secondary\">\n {p.weeks} {p.weeks === 1 ? 'week' : 'weeks'} · {drillCount}{' '}\n {drillCount === 1 ? 'drill' : 'drills'}\n </p>\n </div>\n </div>\n {p.summary && <p className=\"mt-4 text-sm text-text-secondary\">{p.summary}</p>}\n {p.tags.length > 0 && (\n <div className=\"mt-4 flex flex-wrap gap-1.5\">\n {p.tags.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {/* Lifecycle transitions */}\n <div className=\"mt-5 flex flex-wrap items-center gap-2 border-t border-border-seam pt-5\">\n <span className=\"text-sm font-medium text-text-primary\">Publish lifecycle:</span>\n {p.status === 'DRAFT' && (\n <button\n type=\"button\"\n onClick={() => handleTransition('PUBLISHED')}\n disabled={transition.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-1.5 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n <Send className=\"h-4 w-4\" /> Publish\n </button>\n )}\n {p.status === 'PUBLISHED' && (\n <>\n <button\n type=\"button\"\n onClick={() => handleTransition('DRAFT')}\n disabled={transition.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-60\"\n >\n <Undo2 className=\"h-4 w-4\" /> Pull back to draft\n </button>\n <button\n type=\"button\"\n onClick={() => handleTransition('ARCHIVED')}\n disabled={transition.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-60\"\n >\n <Archive className=\"h-4 w-4\" /> Archive\n </button>\n </>\n )}\n {p.status === 'ARCHIVED' && (\n <span className=\"text-sm text-text-secondary\">This program is archived.</span>\n )}\n {transition.isPending && <Loader2 className=\"h-4 w-4 animate-spin text-text-secondary\" />}\n </div>\n </GlassCard>\n\n {/* Drills */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Drills ({drillCount})</h2>\n <button\n type=\"button\"\n onClick={() => {\n setDrillError(null);\n setDrillOpen(true);\n }}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-1.5 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" /> Add drill\n </button>\n </div>\n\n {drillCount === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Plus className=\"h-10 w-10\" />}\n title=\"No drills yet\"\n description=\"Add drills with sets, reps and durations, organized by week.\"\n />\n ) : (\n <Accordion type=\"multiple\" defaultValue={openWeeks} className=\"flex flex-col gap-3\">\n {drillsByWeek.map(({ week, drills }) => (\n <GlassCard key={week} treatment=\"insight\" className=\"overflow-hidden px-4 py-0\">\n <AccordionItem value={`week-${week}`} className=\"border-b-0\">\n <AccordionTrigger className=\"py-4\">\n <span className=\"flex items-center gap-2.5\">\n <span className=\"text-sm font-semibold text-text-primary\">Week {week}</span>\n <Badge variant=\"secondary\">\n {drills.length} {drills.length === 1 ? 'drill' : 'drills'}\n </Badge>\n </span>\n </AccordionTrigger>\n <AccordionContent className=\"pb-2\">\n <div className=\"divide-y divide-border-seam\">\n {drills.map((d) => (\n <div key={d.id} className=\"flex items-start gap-4 py-3\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">{d.name}</p>\n {d.description && (\n <p className=\"mt-0.5 text-xs text-text-secondary\">{d.description}</p>\n )}\n <div className=\"mt-2 flex flex-wrap gap-1.5\">\n {d.sets !== null && <MetricChip>{d.sets} sets</MetricChip>}\n {d.reps !== null && <MetricChip>{d.reps} reps</MetricChip>}\n {d.durationSec !== null && (\n <MetricChip>{d.durationSec}s work</MetricChip>\n )}\n {d.restSec !== null && <MetricChip>{d.restSec}s rest</MetricChip>}\n </div>\n {d.video?.id && (\n <div>\n <DrillVideoLink fileId={d.video.id} />\n </div>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => void removeDrill.mutate(d.id)}\n disabled={removeDrill.isPending}\n aria-label={`Remove ${d.name}`}\n className=\"rounded-lg p-2 text-text-secondary transition-colors hover:bg-status-error-bg-subtle hover:text-status-error-text disabled:opacity-60\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n ))}\n </div>\n </AccordionContent>\n </AccordionItem>\n </GlassCard>\n ))}\n </Accordion>\n )}\n </section>\n\n {/* Add drill dialog */}\n <ResponsiveDialog open={drillOpen} onOpenChange={setDrillOpen}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add drill</ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleAddDrill} className=\"space-y-4 px-1\">\n <FormField label=\"Name\" htmlFor=\"drillName\" required>\n <input\n id=\"drillName\"\n className={fieldInputClass}\n value={drillForm.name}\n onChange={(e) => setDrillForm((p) => ({ ...p, name: e.target.value }))}\n placeholder=\"e.g. Box jumps\"\n />\n </FormField>\n <FormField label=\"Description\" htmlFor=\"drillDesc\">\n <textarea\n id=\"drillDesc\"\n className={fieldInputClass}\n rows={2}\n value={drillForm.description}\n onChange={(e) => setDrillForm((p) => ({ ...p, description: e.target.value }))}\n placeholder=\"How to perform the drill.\"\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <FormField label=\"Week\" htmlFor=\"drillWeek\">\n <input\n id=\"drillWeek\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.weekNumber}\n onChange={(e) => setDrillForm((p) => ({ ...p, weekNumber: e.target.value }))}\n />\n </FormField>\n <FormField label=\"Sets\" htmlFor=\"drillSets\">\n <input\n id=\"drillSets\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.sets}\n onChange={(e) => setDrillForm((p) => ({ ...p, sets: e.target.value }))}\n />\n </FormField>\n <FormField label=\"Reps\" htmlFor=\"drillReps\">\n <input\n id=\"drillReps\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.reps}\n onChange={(e) => setDrillForm((p) => ({ ...p, reps: e.target.value }))}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField label=\"Work (sec)\" htmlFor=\"drillDuration\">\n <input\n id=\"drillDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.durationSec}\n onChange={(e) => setDrillForm((p) => ({ ...p, durationSec: e.target.value }))}\n />\n </FormField>\n <FormField label=\"Rest (sec)\" htmlFor=\"drillRest\">\n <input\n id=\"drillRest\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.restSec}\n onChange={(e) => setDrillForm((p) => ({ ...p, restSec: e.target.value }))}\n />\n </FormField>\n </div>\n <FormField\n label=\"Demo video\"\n htmlFor=\"drillVideo\"\n hint=\"Optional — attach a short demo clip; it's stored in workspace files.\"\n >\n <input\n id=\"drillVideo\"\n type=\"file\"\n accept=\"video/*\"\n className={fieldInputClass}\n onChange={(e) => setVideoFile(e.target.files?.[0] ?? null)}\n />\n {videoFile && (\n <p className=\"mt-1 inline-flex items-center gap-1 text-xs text-text-secondary\">\n <FileVideo className=\"h-3.5 w-3.5\" /> {videoFile.name}\n </p>\n )}\n </FormField>\n {drillError && <p className=\"text-sm text-status-error-text\">{drillError}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => {\n setDrillOpen(false);\n setVideoFile(null);\n }}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={addDrill.isPending || uploadingVideo}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {(addDrill.isPending || uploadingVideo) && (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n )}\n {uploadingVideo ? 'Uploading video…' : 'Add drill'}\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgEA,IAAM,IAAyB;CAC7B,MAAM;CACN,aAAa;CACb,YAAY;CACZ,MAAM;CACN,MAAM;CACN,aAAa;CACb,SAAS;AACX,GAGM,KAAc,EAAE,kBACpB,kBAAC,QAAD;CAAM,WAAU;CACb;AACG,CAAA,GAGF,KAAkB,MAAkC;CACxD,IAAM,IAAI,EAAE,KAAK;CACjB,IAAI,CAAC,GAAG;CACR,IAAM,IAAI,OAAO,SAAS,GAAG,EAAE;CAC/B,OAAO,OAAO,MAAM,CAAC,IAAI,KAAA,IAAY;AACvC;AAEA,SAAgB,IAAoB;CAClC,IAAM,EAAE,UAAO,GAA0B,GACnC,EAAE,kBAAe,EAAiB,GAElC,IAAU,GAAW,CAAE,GACvB,IAAa,GAAqB,GAClC,IAAU,GAAkB,GAC5B,IAAW,EAAY,GACvB,IAAc,GAAe,CAAE,GAC/B,KAAa,GAAc,GAE3B,CAAC,IAAW,KAAgB,EAAS,EAAK,GAC1C,CAAC,GAAW,KAAgB,EAAoB,CAAW,GAC3D,CAAC,GAAW,KAAgB,EAAsB,IAAI,GACtD,CAAC,GAAgB,KAAqB,EAAS,EAAK,GACpD,CAAC,GAAY,KAAiB,EAAwB,IAAI,GAE1D,IAAe,QAAc;EACjC,IAAM,IAAS,EAAQ,MAAM,QAAQ,SAAS,CAAC,GACzC,oBAAS,IAAI,IAAwB;EAC3C,KAAK,IAAM,KAAK,GAAQ;GACtB,IAAM,IAAO,EAAO,IAAI,EAAE,UAAU,KAAK,CAAC;GAE1C,AADA,EAAK,KAAK,CAAC,GACX,EAAO,IAAI,EAAE,YAAY,CAAI;EAC/B;EACA,OAAO,MAAM,KAAK,EAAO,QAAQ,CAAC,EAC/B,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,EACxB,KAAK,CAAC,GAAM,QAAW;GACtB;GACA,QAAQ,CAAC,GAAG,CAAI,EAAE,MAAM,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;EAC9D,EAAE;CACN,GAAG,CAAC,EAAQ,IAAI,CAAC;CAEjB,IAAI,EAAQ,WACV,OACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD,EAAc,OAAM,mBAAoB,CAAA;CAC9B,CAAA;CAIhB,IAAI,EAAQ,WAAW,CAAC,EAAQ,MAC9B,OACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD;GACE,OAAM;GACN,SAAQ;GACR,eAAe,EAAW,WAAW;EACtC,CAAA;CACS,CAAA;CAIhB,IAAM,IAAI,EAAQ,MACZ,IAAa,EAAE,QAAQ,MAAM,UAAU,GACvC,KAAY,EAAY,EAAE,QAC1B,KAAY,EAAa,KAAK,EAAE,cAAW,QAAQ,GAAM,GAEzD,KAAoB,MAA6B;EAChD,KACL,EAAgB,OAAO;GAAE;GAAI;EAAO,CAAC;CACvC;CAoDA,OACE,kBAAC,GAAD;EACE,OAAO,EAAE;EACT,eAAA;EACA,aAAa,GAAG,EAAa,EAAE,OAAO,KAAK,EAAqB,EAAE,OAAO,KAAK,EAAE,MAAM,GACpF,EAAE,UAAU,IAAI,SAAS;EAE3B,SACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,WAAW;KACrC,WAAU;eAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA,GAAC,OAC3B;;IACR,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,aAAa,EAAE,GAAG,MAAM;KAClD,WAAU;eAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,GAAC,OACtB;;IACP,EAAE,WAAW,cACZ,kBAAC,UAAD;KACE,MAAK;KACL,SAAS,YA5Ea;MAC3B,MACL,MAAM,EAAQ,YAAY,CAAE,GAC5B,EAAW,WAAW;KACxB;KAyEY,UAAU,EAAQ;KAClB,WAAU;eAJZ;MAMG,EAAQ,YACP,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,IAE3C,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;MAC7B;MAAI;KAED;;GAEP;;YArCT;GAyCE,kBAAC,GAAD;IAAW,WAAU;IAAU,WAAU;cAAzC;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,IAAD;QAAW,WAAU;QAAU,eAAY;OAAQ,CAAA;MAChD,CAAA,GACL,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,GAAD;UAAO,SAAQ;oBAAa,EAAa,EAAE;SAAc,CAAA;SACzD,kBAAC,GAAD;UAAO,SAAQ;oBAAa,EAAqB,EAAE;SAAc,CAAA;SACjE,kBAAC,GAAD,EAAa,QAAQ,EAAE,OAAS,CAAA;SAC/B,EAAE,cACD,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CACE,kBAAC,GAAD,EAAM,WAAU,cAAe,CAAA,GAAC,WAC5B;;QAEL;WACL,kBAAC,KAAD;QAAG,WAAU;kBAAb;SACG,EAAE;SAAM;SAAE,EAAE,UAAU,IAAI,SAAS;SAAQ;SAAI;SAAY;SAC3D,MAAe,IAAI,UAAU;QAC7B;SACA;QACF;;KACJ,EAAE,WAAW,kBAAC,KAAD;MAAG,WAAU;gBAAoC,EAAE;KAAW,CAAA;KAC3E,EAAE,KAAK,SAAS,KACf,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAE,KAAK,KAAK,MACX,kBAAC,QAAD;OAEE,WAAU;iBAET;MACG,GAJC,CAID,CACP;KACE,CAAA;KAIP,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,QAAD;QAAM,WAAU;kBAAwC;OAAwB,CAAA;OAC/E,EAAE,WAAW,WACZ,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAiB,WAAW;QAC3C,UAAU,EAAW;QACrB,WAAU;kBAJZ,CAME,kBAAC,IAAD,EAAM,WAAU,UAAW,CAAA,GAAC,UACtB;;OAET,EAAE,WAAW,eACZ,kBAAA,IAAA,EAAA,UAAA,CACE,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAiB,OAAO;QACvC,UAAU,EAAW;QACrB,WAAU;kBAJZ,CAME,kBAAC,IAAD,EAAO,WAAU,UAAW,CAAA,GAAC,qBACvB;WACR,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAiB,UAAU;QAC1C,UAAU,EAAW;QACrB,WAAU;kBAJZ,CAME,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA,GAAC,UACzB;SACR,EAAA,CAAA;OAEH,EAAE,WAAW,cACZ,kBAAC,QAAD;QAAM,WAAU;kBAA8B;OAA+B,CAAA;OAE9E,EAAW,aAAa,kBAAC,GAAD,EAAS,WAAU,2CAA4C,CAAA;MACrF;;IACI;;GAGX,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAd;MAAwD;MAAS;MAAW;KAAK;QACjF,kBAAC,UAAD;KACE,MAAK;KACL,eAAe;MAEb,AADA,EAAc,IAAI,GAClB,EAAa,EAAI;KACnB;KACA,WAAU;eANZ,CAQE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,GAAC,YACtB;MACL;OAEJ,MAAe,IACd,kBAAC,GAAD;IACE,cAAa;IACb,MAAM,kBAAC,GAAD,EAAM,WAAU,YAAa,CAAA;IACnC,OAAM;IACN,aAAY;GACb,CAAA,IAED,kBAAC,IAAD;IAAW,MAAK;IAAW,cAAc;IAAW,WAAU;cAC3D,EAAa,KAAK,EAAE,SAAM,gBACzB,kBAAC,GAAD;KAAsB,WAAU;KAAU,WAAU;eAClD,kBAAC,IAAD;MAAe,OAAO,QAAQ;MAAQ,WAAU;gBAAhD,CACE,kBAAC,IAAD;OAAkB,WAAU;iBAC1B,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CAA0D,SAAM,CAAW;YAC3E,kBAAC,GAAD;SAAO,SAAQ;mBAAf;UACG,EAAO;UAAO;UAAE,EAAO,WAAW,IAAI,UAAU;SAC5C;UACH;;MACU,CAAA,GAClB,kBAAC,IAAD;OAAkB,WAAU;iBAC1B,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAO,KAAK,MACX,kBAAC,OAAD;SAAgB,WAAU;mBAA1B,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf;WACE,kBAAC,KAAD;YAAG,WAAU;sBAAyC,EAAE;WAAQ,CAAA;WAC/D,EAAE,eACD,kBAAC,KAAD;YAAG,WAAU;sBAAsC,EAAE;WAAe,CAAA;WAEtE,kBAAC,OAAD;YAAK,WAAU;sBAAf;aACG,EAAE,SAAS,QAAQ,kBAAC,GAAD,EAAA,UAAA,CAAa,EAAE,MAAK,OAAiB,EAAA,CAAA;aACxD,EAAE,SAAS,QAAQ,kBAAC,GAAD,EAAA,UAAA,CAAa,EAAE,MAAK,OAAiB,EAAA,CAAA;aACxD,EAAE,gBAAgB,QACjB,kBAAC,GAAD,EAAA,UAAA,CAAa,EAAE,aAAY,QAAkB,EAAA,CAAA;aAE9C,EAAE,YAAY,QAAQ,kBAAC,GAAD,EAAA,UAAA,CAAa,EAAE,SAAQ,QAAkB,EAAA,CAAA;YAC7D;;WACJ,EAAE,OAAO,MACR,kBAAC,OAAD,EAAA,UACE,kBAAC,GAAD,EAAgB,QAAQ,EAAE,MAAM,GAAK,CAAA,EAClC,CAAA;UAEJ;aACL,kBAAC,UAAD;UACE,MAAK;UACL,eAAe,KAAK,EAAY,OAAO,EAAE,EAAE;UAC3C,UAAU,EAAY;UACtB,cAAY,UAAU,EAAE;UACxB,WAAU;oBAEV,kBAAC,IAAD,EAAQ,WAAU,UAAW,CAAA;SACvB,CAAA,CACL;WA7BK,EAAE,EA6BP,CACN;OACE,CAAA;MACW,CAAA,CACL;;IACN,GA/CK,CA+CL,CACZ;GACQ,CAAA,CAEN,EAAA,CAAA;GAGT,kBAAC,GAAD;IAAkB,MAAM;IAAW,cAAc;cAC/C,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAuB,YAAgC,CAAA,EACjC,CAAA,GACxB,kBAAC,QAAD;KAAM,UAAU,OAvPM,MAAuB;MAGnD,IAFA,EAAE,eAAe,GACjB,EAAc,IAAI,GACd,CAAC,GAAI;MACT,IAAI,CAAC,EAAU,KAAK,KAAK,GAAG;OAC1B,EAAc,yBAAyB;OACvC;MACF;MACA,IAAM,IAAO,EAAe,EAAU,UAAU,KAAK,GAEjD;MACJ,IAAI,GAAW;OACb,EAAkB,EAAI;OACtB,IAAI;QACF,IAAc,MAAM,GAAW,CAAS;OAC1C,SAAS,GAAK;QAEZ,AADA,EAAc,aAAe,QAAQ,EAAI,UAAU,kCAAkC,GACrF,EAAkB,EAAK;QACvB;OACF;OACA,EAAkB,EAAK;MACzB;MAEA,IAAI;OAeF,AAdA,MAAM,EAAS,YAAY;QACzB,WAAW;QACX,MAAM,EAAU,KAAK,KAAK;QAC1B,aAAa,EAAU,YAAY,KAAK,KAAK,KAAA;QAC7C,YAAY;QACZ,YAAY,EAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,EAAE,eAAe,CAAI,EAAE,UAAU;QACvF,MAAM,EAAe,EAAU,IAAI;QACnC,MAAM,EAAe,EAAU,IAAI;QACnC,aAAa,EAAe,EAAU,WAAW;QACjD,SAAS,EAAe,EAAU,OAAO;QACzC;OACF,CAAC,GACD,EAAa,CAAW,GACxB,EAAa,IAAI,GACjB,EAAa,EAAK;MACpB,SAAS,GAAK;OACZ,EAAc,aAAe,QAAQ,EAAI,UAAU,sBAAsB;MAC3E;KACF;KA6MwC,WAAU;eAA1C;MACE,kBAAC,GAAD;OAAW,OAAM;OAAO,SAAQ;OAAY,UAAA;iBAC1C,kBAAC,SAAD;QACE,IAAG;QACH,WAAW;QACX,OAAO,EAAU;QACjB,WAAW,MAAM,GAAc,OAAO;SAAE,GAAG;SAAG,MAAM,EAAE,OAAO;QAAM,EAAE;QACrE,aAAY;OACb,CAAA;MACQ,CAAA;MACX,kBAAC,GAAD;OAAW,OAAM;OAAc,SAAQ;iBACrC,kBAAC,YAAD;QACE,IAAG;QACH,WAAW;QACX,MAAM;QACN,OAAO,EAAU;QACjB,WAAW,MAAM,GAAc,OAAO;SAAE,GAAG;SAAG,aAAa,EAAE,OAAO;QAAM,EAAE;QAC5E,aAAY;OACb,CAAA;MACQ,CAAA;MACX,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD;SAAW,OAAM;SAAO,SAAQ;mBAC9B,kBAAC,SAAD;UACE,IAAG;UACH,WAAU;UACV,WAAW;UACX,OAAO,EAAU;UACjB,WAAW,MAAM,GAAc,OAAO;WAAE,GAAG;WAAG,YAAY,EAAE,OAAO;UAAM,EAAE;SAC5E,CAAA;QACQ,CAAA;QACX,kBAAC,GAAD;SAAW,OAAM;SAAO,SAAQ;mBAC9B,kBAAC,SAAD;UACE,IAAG;UACH,WAAU;UACV,WAAW;UACX,OAAO,EAAU;UACjB,WAAW,MAAM,GAAc,OAAO;WAAE,GAAG;WAAG,MAAM,EAAE,OAAO;UAAM,EAAE;SACtE,CAAA;QACQ,CAAA;QACX,kBAAC,GAAD;SAAW,OAAM;SAAO,SAAQ;mBAC9B,kBAAC,SAAD;UACE,IAAG;UACH,WAAU;UACV,WAAW;UACX,OAAO,EAAU;UACjB,WAAW,MAAM,GAAc,OAAO;WAAE,GAAG;WAAG,MAAM,EAAE,OAAO;UAAM,EAAE;SACtE,CAAA;QACQ,CAAA;OACR;;MACL,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAW,OAAM;QAAa,SAAQ;kBACpC,kBAAC,SAAD;SACE,IAAG;SACH,WAAU;SACV,WAAW;SACX,OAAO,EAAU;SACjB,WAAW,MAAM,GAAc,OAAO;UAAE,GAAG;UAAG,aAAa,EAAE,OAAO;SAAM,EAAE;QAC7E,CAAA;OACQ,CAAA,GACX,kBAAC,GAAD;QAAW,OAAM;QAAa,SAAQ;kBACpC,kBAAC,SAAD;SACE,IAAG;SACH,WAAU;SACV,WAAW;SACX,OAAO,EAAU;SACjB,WAAW,MAAM,GAAc,OAAO;UAAE,GAAG;UAAG,SAAS,EAAE,OAAO;SAAM,EAAE;QACzE,CAAA;OACQ,CAAA,CACR;;MACL,kBAAC,GAAD;OACE,OAAM;OACN,SAAQ;OACR,MAAK;iBAHP,CAKE,kBAAC,SAAD;QACE,IAAG;QACH,MAAK;QACL,QAAO;QACP,WAAW;QACX,WAAW,MAAM,EAAa,EAAE,OAAO,QAAQ,MAAM,IAAI;OAC1D,CAAA,GACA,KACC,kBAAC,KAAD;QAAG,WAAU;kBAAb;SACE,kBAAC,GAAD,EAAW,WAAU,cAAe,CAAA;SAAC;SAAE,EAAU;QAChD;SAEI;;MACV,KAAc,kBAAC,KAAD;OAAG,WAAU;iBAAkC;MAAc,CAAA;MAC5E,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe;QAEb,AADA,EAAa,EAAK,GAClB,EAAa,IAAI;OACnB;OACA,WAAU;iBACX;MAEO,CAAA,GACR,kBAAC,UAAD;OACE,MAAK;OACL,UAAU,EAAS,aAAa;OAChC,WAAU;iBAHZ,EAKI,EAAS,aAAa,MACtB,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,GAE5C,IAAiB,qBAAqB,WACjC;QACc,EAAA,CAAA;KACpB;MACiB,EAAA,CAAA;GACT,CAAA;EACR;;AAEhB"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProgramFormPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/programs/ProgramFormPage.tsx"],"sourcesContent":["/**\n * Program Create / Edit Form\n * @module atheletebridge/pages/programs\n */\n\nimport { useEffect, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, Loader2, Save } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { SPORT_LABELS, PROGRAM_LEVEL_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useProgram, useCreateProgram, useUpdateProgram } from '../../hooks/useAthleteBridgeApi';\nimport type { AthProgramLevel, AthSport } from '../../types';\n\nconst SPORTS = Object.keys(SPORT_LABELS) as AthSport[];\nconst LEVELS = Object.keys(PROGRAM_LEVEL_LABELS) as AthProgramLevel[];\n\ninterface ProgramFormState {\n name: string;\n sport: AthSport;\n level: AthProgramLevel;\n summary: string;\n weeks: string;\n isTemplate: boolean;\n tagsText: string;\n}\n\nconst EMPTY_FORM: ProgramFormState = {\n name: '',\n sport: 'OTHER',\n level: 'BEGINNER',\n summary: '',\n weeks: '4',\n isTemplate: false,\n tagsText: '',\n};\n\nexport function ProgramFormPage() {\n const { id } = useParams<{ id: string }>();\n const isEdit = Boolean(id);\n const { navigateTo } = useAthleteBridge();\n\n const existing = useProgram(id);\n const createProgram = useCreateProgram();\n const updateProgram = useUpdateProgram();\n\n const [form, setForm] = useState<ProgramFormState>(EMPTY_FORM);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const program = existing.data;\n if (isEdit && program) {\n setForm({\n name: program.name,\n sport: program.sport,\n level: program.level,\n summary: program.summary ?? '',\n weeks: String(program.weeks),\n isTemplate: program.isTemplate,\n tagsText: program.tags.join(', '),\n });\n }\n }, [isEdit, existing.data]);\n\n const set = <K extends keyof ProgramFormState>(key: K, value: ProgramFormState[K]) =>\n setForm((prev) => ({ ...prev, [key]: value }));\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!form.name.trim()) {\n setError('Program name is required.');\n return;\n }\n const weeks = Number.parseInt(form.weeks, 10);\n if (Number.isNaN(weeks) || weeks < 1) {\n setError('Weeks must be a positive number.');\n return;\n }\n\n const tags = form.tagsText\n .split(',')\n .map((t) => t.trim())\n .filter(Boolean);\n\n try {\n if (isEdit && id) {\n await updateProgram.mutateAsync({\n id,\n input: {\n name: form.name.trim(),\n sport: form.sport,\n level: form.level,\n summary: form.summary.trim() || undefined,\n weeks,\n isTemplate: form.isTemplate,\n tags,\n },\n });\n navigateTo(`/programs/${id}`);\n } else {\n const created = await createProgram.mutateAsync({\n name: form.name.trim(),\n sport: form.sport,\n level: form.level,\n summary: form.summary.trim() || undefined,\n weeks,\n isTemplate: form.isTemplate,\n tags,\n });\n navigateTo(`/programs/${created.id}`);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to save program.');\n }\n };\n\n const saving = createProgram.isPending || updateProgram.isPending;\n\n if (isEdit && existing.isLoading) {\n return (\n <PageLayout title=\"Edit program\">\n <LoadingState label=\"Loading program…\" />\n </PageLayout>\n );\n }\n\n if (isEdit && (existing.isError || (!existing.isLoading && !existing.data))) {\n return (\n <PageLayout title=\"Edit program\">\n <ErrorState\n title=\"Program not found\"\n message=\"This program does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/programs')}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={isEdit ? 'Edit program' : 'New program'}\n description={\n isEdit ? 'Update the training plan details.' : 'Create a new training plan in draft.'\n }\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && id ? `/programs/${id}` : '/programs')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"max-w-2xl space-y-5\">\n <FormField label=\"Program name\" htmlFor=\"name\" required>\n <input\n id=\"name\"\n className={fieldInputClass}\n value={form.name}\n onChange={(e) => set('name', e.target.value)}\n placeholder=\"e.g. Pre-season strength block\"\n />\n </FormField>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-3\">\n <FormField label=\"Sport\" htmlFor=\"sport\">\n <select\n id=\"sport\"\n className={fieldInputClass}\n value={form.sport}\n onChange={(e) => set('sport', e.target.value as AthSport)}\n >\n {SPORTS.map((s) => (\n <option key={s} value={s}>\n {SPORT_LABELS[s]}\n </option>\n ))}\n </select>\n </FormField>\n\n <FormField label=\"Level\" htmlFor=\"level\">\n <select\n id=\"level\"\n className={fieldInputClass}\n value={form.level}\n onChange={(e) => set('level', e.target.value as AthProgramLevel)}\n >\n {LEVELS.map((l) => (\n <option key={l} value={l}>\n {PROGRAM_LEVEL_LABELS[l]}\n </option>\n ))}\n </select>\n </FormField>\n\n <FormField label=\"Weeks\" htmlFor=\"weeks\">\n <input\n id=\"weeks\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={form.weeks}\n onChange={(e) => set('weeks', e.target.value)}\n placeholder=\"4\"\n />\n </FormField>\n </div>\n\n <FormField label=\"Summary\" htmlFor=\"summary\">\n <textarea\n id=\"summary\"\n className={fieldInputClass}\n rows={3}\n value={form.summary}\n onChange={(e) => set('summary', e.target.value)}\n placeholder=\"What this program is for and who it's aimed at.\"\n />\n </FormField>\n\n <FormField label=\"Tags\" htmlFor=\"tags\" hint=\"Comma-separated, e.g. strength, pre-season.\">\n <input\n id=\"tags\"\n className={fieldInputClass}\n value={form.tagsText}\n onChange={(e) => set('tagsText', e.target.value)}\n placeholder=\"strength, conditioning\"\n />\n </FormField>\n\n <label className=\"flex items-center gap-2.5 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={form.isTemplate}\n onChange={(e) => set('isTemplate', e.target.checked)}\n className=\"h-4 w-4 rounded border-border-seam text-action-primary-bg focus:ring-[var(--color-focus-ring)]\"\n />\n Mark as reusable template\n </label>\n\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n\n <div className=\"pt-2\">\n <button\n type=\"submit\"\n disabled={saving}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {saving ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Save className=\"h-4 w-4\" />}\n {isEdit ? 'Save changes' : 'Create program'}\n </button>\n </div>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProgramFormPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/programs/ProgramFormPage.tsx"],"sourcesContent":["/**\n * Program Create / Edit Form\n * @module atheletebridge/pages/programs\n */\n\nimport { useEffect, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, Loader2, Save } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { SPORT_LABELS, PROGRAM_LEVEL_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useProgram, useCreateProgram, useUpdateProgram } from '../../hooks/useAthleteBridgeApi';\nimport type { AthProgramLevel, AthSport } from '../../types';\n\nconst SPORTS = Object.keys(SPORT_LABELS) as AthSport[];\nconst LEVELS = Object.keys(PROGRAM_LEVEL_LABELS) as AthProgramLevel[];\n\ninterface ProgramFormState {\n name: string;\n sport: AthSport;\n level: AthProgramLevel;\n summary: string;\n weeks: string;\n isTemplate: boolean;\n tagsText: string;\n}\n\nconst EMPTY_FORM: ProgramFormState = {\n name: '',\n sport: 'OTHER',\n level: 'BEGINNER',\n summary: '',\n weeks: '4',\n isTemplate: false,\n tagsText: '',\n};\n\nexport function ProgramFormPage() {\n const { id } = useParams<{ id: string }>();\n const isEdit = Boolean(id);\n const { navigateTo } = useAthleteBridge();\n\n const existing = useProgram(id);\n const createProgram = useCreateProgram();\n const updateProgram = useUpdateProgram();\n\n const [form, setForm] = useState<ProgramFormState>(EMPTY_FORM);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const program = existing.data;\n if (isEdit && program) {\n setForm({\n name: program.name,\n sport: program.sport,\n level: program.level,\n summary: program.summary ?? '',\n weeks: String(program.weeks),\n isTemplate: program.isTemplate,\n tagsText: program.tags.join(', '),\n });\n }\n }, [isEdit, existing.data]);\n\n const set = <K extends keyof ProgramFormState>(key: K, value: ProgramFormState[K]) =>\n setForm((prev) => ({ ...prev, [key]: value }));\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!form.name.trim()) {\n setError('Program name is required.');\n return;\n }\n const weeks = Number.parseInt(form.weeks, 10);\n if (Number.isNaN(weeks) || weeks < 1) {\n setError('Weeks must be a positive number.');\n return;\n }\n\n const tags = form.tagsText\n .split(',')\n .map((t) => t.trim())\n .filter(Boolean);\n\n try {\n if (isEdit && id) {\n await updateProgram.mutateAsync({\n id,\n input: {\n name: form.name.trim(),\n sport: form.sport,\n level: form.level,\n summary: form.summary.trim() || undefined,\n weeks,\n isTemplate: form.isTemplate,\n tags,\n },\n });\n navigateTo(`/programs/${id}`);\n } else {\n const created = await createProgram.mutateAsync({\n name: form.name.trim(),\n sport: form.sport,\n level: form.level,\n summary: form.summary.trim() || undefined,\n weeks,\n isTemplate: form.isTemplate,\n tags,\n });\n navigateTo(`/programs/${created.id}`);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to save program.');\n }\n };\n\n const saving = createProgram.isPending || updateProgram.isPending;\n\n if (isEdit && existing.isLoading) {\n return (\n <PageLayout title=\"Edit program\">\n <LoadingState label=\"Loading program…\" />\n </PageLayout>\n );\n }\n\n if (isEdit && (existing.isError || (!existing.isLoading && !existing.data))) {\n return (\n <PageLayout title=\"Edit program\">\n <ErrorState\n title=\"Program not found\"\n message=\"This program does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/programs')}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={isEdit ? 'Edit program' : 'New program'}\n description={\n isEdit ? 'Update the training plan details.' : 'Create a new training plan in draft.'\n }\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && id ? `/programs/${id}` : '/programs')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"max-w-2xl space-y-5\">\n <FormField label=\"Program name\" htmlFor=\"name\" required>\n <input\n id=\"name\"\n className={fieldInputClass}\n value={form.name}\n onChange={(e) => set('name', e.target.value)}\n placeholder=\"e.g. Pre-season strength block\"\n />\n </FormField>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-3\">\n <FormField label=\"Sport\" htmlFor=\"sport\">\n <select\n id=\"sport\"\n className={fieldInputClass}\n value={form.sport}\n onChange={(e) => set('sport', e.target.value as AthSport)}\n >\n {SPORTS.map((s) => (\n <option key={s} value={s}>\n {SPORT_LABELS[s]}\n </option>\n ))}\n </select>\n </FormField>\n\n <FormField label=\"Level\" htmlFor=\"level\">\n <select\n id=\"level\"\n className={fieldInputClass}\n value={form.level}\n onChange={(e) => set('level', e.target.value as AthProgramLevel)}\n >\n {LEVELS.map((l) => (\n <option key={l} value={l}>\n {PROGRAM_LEVEL_LABELS[l]}\n </option>\n ))}\n </select>\n </FormField>\n\n <FormField label=\"Weeks\" htmlFor=\"weeks\">\n <input\n id=\"weeks\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={form.weeks}\n onChange={(e) => set('weeks', e.target.value)}\n placeholder=\"4\"\n />\n </FormField>\n </div>\n\n <FormField label=\"Summary\" htmlFor=\"summary\">\n <textarea\n id=\"summary\"\n className={fieldInputClass}\n rows={3}\n value={form.summary}\n onChange={(e) => set('summary', e.target.value)}\n placeholder=\"What this program is for and who it's aimed at.\"\n />\n </FormField>\n\n <FormField label=\"Tags\" htmlFor=\"tags\" hint=\"Comma-separated, e.g. strength, pre-season.\">\n <input\n id=\"tags\"\n className={fieldInputClass}\n value={form.tagsText}\n onChange={(e) => set('tagsText', e.target.value)}\n placeholder=\"strength, conditioning\"\n />\n </FormField>\n\n <label className=\"flex items-center gap-2.5 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={form.isTemplate}\n onChange={(e) => set('isTemplate', e.target.checked)}\n className=\"h-4 w-4 rounded border-border-seam text-action-primary-bg focus:ring-[var(--color-focus-ring)]\"\n />\n Mark as reusable template\n </label>\n\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n\n <div className=\"pt-2\">\n <button\n type=\"submit\"\n disabled={saving}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {saving ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Save className=\"h-4 w-4\" />}\n {isEdit ? 'Save changes' : 'Create program'}\n </button>\n </div>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProgramsListPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/programs/ProgramsListPage.tsx"],"sourcesContent":["/**\n * Programs List\n * @module atheletebridge/pages/programs\n */\n\nimport { useState } from 'react';\nimport { Dumbbell, Plus, Search } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { ProgramCard } from '../../components/ProgramCard';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FilterSelect } from '../../components/FilterSelect';\nimport { CursorPager } from '../../components/CursorPager';\nimport { SPORT_LABELS, PROGRAM_LEVEL_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { usePrograms } from '../../hooks/useAthleteBridgeApi';\nimport type { AthProgramLevel, AthProgramStatus, AthSport } from '../../types';\n\nconst PAGE_SIZE = 12;\n\nconst SPORT_OPTS = (Object.keys(SPORT_LABELS) as AthSport[]).map((value) => ({\n value,\n label: SPORT_LABELS[value],\n}));\n\nconst LEVEL_OPTS = (Object.keys(PROGRAM_LEVEL_LABELS) as AthProgramLevel[]).map((value) => ({\n value,\n label: PROGRAM_LEVEL_LABELS[value],\n}));\n\nconst STATUS_OPTS: ReadonlyArray<{ value: AthProgramStatus; label: string }> = [\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\nexport function ProgramsListPage() {\n const { navigateTo } = useAthleteBridge();\n const [sport, setSport] = useState<AthSport | ''>('');\n const [level, setLevel] = useState<AthProgramLevel | ''>('');\n const [status, setStatus] = useState<AthProgramStatus | ''>('');\n const [search, setSearch] = useState('');\n const [page, setPage] = useState(0);\n\n const programs = usePrograms({\n sport: sport || undefined,\n level: level || undefined,\n status: status || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n\n const allItems = programs.data?.items ?? [];\n // Name search filters the loaded page client-side (the query exposes no name\n // arg); it never fabricates rows, only narrows what's already fetched.\n const query = search.trim().toLowerCase();\n const items = query\n ? allItems.filter(\n (p) =>\n p.name.toLowerCase().includes(query) ||\n (p.summary ?? '').toLowerCase().includes(query) ||\n p.tags.some((t) => t.toLowerCase().includes(query))\n )\n : allItems;\n const hasMore = programs.data?.pageInfo.hasMore ?? false;\n\n const resetPage = () => setPage(0);\n\n return (\n <PageLayout\n title=\"Programs\"\n description=\"Coach-built training plans with a publish lifecycle and reusable templates.\"\n hero\n gradientTitle\n actions={\n <button\n type=\"button\"\n data-tour=\"programs-create\"\n onClick={() => navigateTo('/programs/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" /> New program\n </button>\n }\n >\n <div data-tour=\"programs-filters\" className=\"mb-6 flex flex-wrap items-center gap-3\">\n <div className=\"relative min-w-[14rem] flex-1\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-muted\"\n aria-hidden=\"true\"\n />\n <input\n type=\"search\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search programs…\"\n aria-label=\"Search programs\"\n className=\"w-full rounded-lg border border-border-seam bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n />\n </div>\n <FilterSelect\n ariaLabel=\"Filter by sport\"\n allLabel=\"All sports\"\n value={sport}\n onChange={(v) => {\n setSport(v);\n resetPage();\n }}\n options={SPORT_OPTS}\n />\n <FilterSelect\n ariaLabel=\"Filter by level\"\n allLabel=\"All levels\"\n value={level}\n onChange={(v) => {\n setLevel(v);\n resetPage();\n }}\n options={LEVEL_OPTS}\n />\n <FilterSelect\n ariaLabel=\"Filter by status\"\n allLabel=\"All statuses\"\n value={status}\n onChange={(v) => {\n setStatus(v);\n resetPage();\n }}\n options={STATUS_OPTS}\n />\n </div>\n\n {programs.isLoading ? (\n <LoadingState label=\"Loading programs…\" />\n ) : programs.isError ? (\n <ErrorState onRetry={() => void programs.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Dumbbell className=\"h-10 w-10\" />}\n title=\"No programs found\"\n description={\n sport || level || status || query\n ? 'Try clearing your search or filters to see all programs.'\n : 'Create your first training program to build drills and publish to athletes.'\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/programs/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n Create program\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {items.map((program) => (\n <ProgramCard\n key={program.id}\n program={program}\n drillCount={program.drills?.items.length}\n onClick={() => navigateTo(`/programs/${program.id}`)}\n />\n ))}\n </div>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProgramsListPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/programs/ProgramsListPage.tsx"],"sourcesContent":["/**\n * Programs List\n * @module atheletebridge/pages/programs\n */\n\nimport { useState } from 'react';\nimport { Dumbbell, Plus, Search } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { ProgramCard } from '../../components/ProgramCard';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FilterSelect } from '../../components/FilterSelect';\nimport { CursorPager } from '../../components/CursorPager';\nimport { SPORT_LABELS, PROGRAM_LEVEL_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { usePrograms } from '../../hooks/useAthleteBridgeApi';\nimport type { AthProgramLevel, AthProgramStatus, AthSport } from '../../types';\n\nconst PAGE_SIZE = 12;\n\nconst SPORT_OPTS = (Object.keys(SPORT_LABELS) as AthSport[]).map((value) => ({\n value,\n label: SPORT_LABELS[value],\n}));\n\nconst LEVEL_OPTS = (Object.keys(PROGRAM_LEVEL_LABELS) as AthProgramLevel[]).map((value) => ({\n value,\n label: PROGRAM_LEVEL_LABELS[value],\n}));\n\nconst STATUS_OPTS: ReadonlyArray<{ value: AthProgramStatus; label: string }> = [\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\nexport function ProgramsListPage() {\n const { navigateTo } = useAthleteBridge();\n const [sport, setSport] = useState<AthSport | ''>('');\n const [level, setLevel] = useState<AthProgramLevel | ''>('');\n const [status, setStatus] = useState<AthProgramStatus | ''>('');\n const [search, setSearch] = useState('');\n const [page, setPage] = useState(0);\n\n const programs = usePrograms({\n sport: sport || undefined,\n level: level || undefined,\n status: status || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n\n const allItems = programs.data?.items ?? [];\n // Name search filters the loaded page client-side (the query exposes no name\n // arg); it never fabricates rows, only narrows what's already fetched.\n const query = search.trim().toLowerCase();\n const items = query\n ? allItems.filter(\n (p) =>\n p.name.toLowerCase().includes(query) ||\n (p.summary ?? '').toLowerCase().includes(query) ||\n p.tags.some((t) => t.toLowerCase().includes(query))\n )\n : allItems;\n const hasMore = programs.data?.pageInfo.hasMore ?? false;\n\n const resetPage = () => setPage(0);\n\n return (\n <PageLayout\n title=\"Programs\"\n description=\"Coach-built training plans with a publish lifecycle and reusable templates.\"\n hero\n gradientTitle\n actions={\n <button\n type=\"button\"\n data-tour=\"programs-create\"\n onClick={() => navigateTo('/programs/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" /> New program\n </button>\n }\n >\n <div data-tour=\"programs-filters\" className=\"mb-6 flex flex-wrap items-center gap-3\">\n <div className=\"relative min-w-[14rem] flex-1\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-muted\"\n aria-hidden=\"true\"\n />\n <input\n type=\"search\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search programs…\"\n aria-label=\"Search programs\"\n className=\"w-full rounded-lg border border-border-seam bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n />\n </div>\n <FilterSelect\n ariaLabel=\"Filter by sport\"\n allLabel=\"All sports\"\n value={sport}\n onChange={(v) => {\n setSport(v);\n resetPage();\n }}\n options={SPORT_OPTS}\n />\n <FilterSelect\n ariaLabel=\"Filter by level\"\n allLabel=\"All levels\"\n value={level}\n onChange={(v) => {\n setLevel(v);\n resetPage();\n }}\n options={LEVEL_OPTS}\n />\n <FilterSelect\n ariaLabel=\"Filter by status\"\n allLabel=\"All statuses\"\n value={status}\n onChange={(v) => {\n setStatus(v);\n resetPage();\n }}\n options={STATUS_OPTS}\n />\n </div>\n\n {programs.isLoading ? (\n <LoadingState label=\"Loading programs…\" />\n ) : programs.isError ? (\n <ErrorState onRetry={() => void programs.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Dumbbell className=\"h-10 w-10\" />}\n title=\"No programs found\"\n description={\n sport || level || status || query\n ? 'Try clearing your search or filters to see all programs.'\n : 'Create your first training program to build drills and publish to athletes.'\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/programs/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n Create program\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {items.map((program) => (\n <ProgramCard\n key={program.id}\n program={program}\n drillCount={program.drills?.items.length}\n onClick={() => navigateTo(`/programs/${program.id}`)}\n />\n ))}\n </div>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAkBA,IAAM,IAAY,IAEZ,IAAc,OAAO,KAAK,CAAY,EAAiB,KAAK,OAAW;CAC3E;CACA,OAAO,EAAa;AACtB,EAAE,GAEI,IAAc,OAAO,KAAK,CAAoB,EAAwB,KAAK,OAAW;CAC1F;CACA,OAAO,EAAqB;AAC9B,EAAE,GAEI,IAAyE;CAC7E;EAAE,OAAO;EAAS,OAAO;CAAQ;CACjC;EAAE,OAAO;EAAa,OAAO;CAAY;CACzC;EAAE,OAAO;EAAY,OAAO;CAAW;AACzC;AAEA,SAAgB,IAAmB;CACjC,IAAM,EAAE,kBAAe,EAAiB,GAClC,CAAC,GAAO,KAAY,EAAwB,EAAE,GAC9C,CAAC,GAAO,KAAY,EAA+B,EAAE,GACrD,CAAC,GAAQ,KAAa,EAAgC,EAAE,GACxD,CAAC,GAAQ,KAAa,EAAS,EAAE,GACjC,CAAC,GAAM,KAAW,EAAS,CAAC,GAE5B,IAAW,EAAY;EAC3B,OAAO,KAAS,KAAA;EAChB,OAAO,KAAS,KAAA;EAChB,QAAQ,KAAU,KAAA;EAClB,YAAY;GAAE,OAAO;GAAW,QAAQ,IAAO;EAAU;CAC3D,CAAC,GAEK,IAAW,EAAS,MAAM,SAAS,CAAC,GAGpC,IAAQ,EAAO,KAAK,EAAE,YAAY,GAClC,IAAQ,IACV,EAAS,QACN,MACC,EAAE,KAAK,YAAY,EAAE,SAAS,CAAK,MAClC,EAAE,WAAW,IAAI,YAAY,EAAE,SAAS,CAAK,KAC9C,EAAE,KAAK,MAAM,MAAM,EAAE,YAAY,EAAE,SAAS,CAAK,CAAC,CACtD,IACA,GACE,IAAU,EAAS,MAAM,SAAS,WAAW,IAE7C,UAAkB,EAAQ,CAAC;CAEjC,OACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,MAAA;EACA,eAAA;EACA,SACE,kBAAC,UAAD;GACE,MAAK;GACL,aAAU;GACV,eAAe,EAAW,eAAe;GACzC,WAAU;aAJZ,CAME,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,GAAC,cACtB;;YAbZ,CAgBE,kBAAC,OAAD;GAAK,aAAU;GAAmB,WAAU;aAA5C;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,WAAU;MACV,eAAY;KACb,CAAA,GACD,kBAAC,SAAD;MACE,MAAK;MACL,OAAO;MACP,WAAW,MAAM,EAAU,EAAE,OAAO,KAAK;MACzC,aAAY;MACZ,cAAW;MACX,WAAU;KACX,CAAA,CACE;;IACL,kBAAC,GAAD;KACE,WAAU;KACV,UAAS;KACT,OAAO;KACP,WAAW,MAAM;MAEf,AADA,EAAS,CAAC,GACV,EAAU;KACZ;KACA,SAAS;IACV,CAAA;IACD,kBAAC,GAAD;KACE,WAAU;KACV,UAAS;KACT,OAAO;KACP,WAAW,MAAM;MAEf,AADA,EAAS,CAAC,GACV,EAAU;KACZ;KACA,SAAS;IACV,CAAA;IACD,kBAAC,GAAD;KACE,WAAU;KACV,UAAS;KACT,OAAO;KACP,WAAW,MAAM;MAEf,AADA,EAAU,CAAC,GACX,EAAU;KACZ;KACA,SAAS;IACV,CAAA;GACE;MAEJ,EAAS,YACR,kBAAC,GAAD,EAAc,OAAM,oBAAqB,CAAA,IACvC,EAAS,UACX,kBAAC,GAAD,EAAY,eAAe,KAAK,EAAS,QAAQ,EAAI,CAAA,IACnD,EAAM,WAAW,IACnB,kBAAC,GAAD;GACE,cAAa;GACb,MAAM,kBAAC,GAAD,EAAU,WAAU,YAAa,CAAA;GACvC,OAAM;GACN,aACE,KAAS,KAAS,KAAU,IACxB,6DACA;GAEN,QACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,eAAe;IACzC,WAAU;cACX;GAEO,CAAA;EAEX,CAAA,IAED,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAM,KAAK,MACV,kBAAC,GAAD;IAEW;IACT,YAAY,EAAQ,QAAQ,MAAM;IAClC,eAAe,EAAW,aAAa,EAAQ,IAAI;GACpD,GAJM,EAAQ,EAId,CACF;EACE,CAAA,GACL,kBAAC,GAAD;GACE,WAAU;GACJ;GACG;GACT,cAAc,GAAS,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC,CAAC;GAC/C,cAAc,GAAS,MAAM,IAAI,CAAC;EACnC,CAAA,CACD,EAAA,CAAA,CAEM;;AAEhB"}