@burdenoff/microfe-atheletebridge 2026.629.1 → 2026.705.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/atheletebridge/AthleteBridgeRoot.js +45 -37
  2. package/dist/atheletebridge/AthleteBridgeRoot.js.map +1 -1
  3. package/dist/atheletebridge/AthleteBridgeRoutes.js +10 -6
  4. package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
  5. package/dist/atheletebridge/components/AthleteSelect.js +66 -0
  6. package/dist/atheletebridge/components/AthleteSelect.js.map +1 -0
  7. package/dist/atheletebridge/components/DrillVideoLink.js +28 -0
  8. package/dist/atheletebridge/components/DrillVideoLink.js.map +1 -0
  9. package/dist/atheletebridge/components/MetricsCard.js +20 -46
  10. package/dist/atheletebridge/components/MetricsCard.js.map +1 -1
  11. package/dist/atheletebridge/components/UserSearchSelect.js +129 -0
  12. package/dist/atheletebridge/components/UserSearchSelect.js.map +1 -0
  13. package/dist/atheletebridge/components/domain.js +10 -1
  14. package/dist/atheletebridge/components/domain.js.map +1 -1
  15. package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +313 -180
  16. package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
  17. package/dist/atheletebridge/pages/DashboardPage.js +77 -69
  18. package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
  19. package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js +4 -4
  20. package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js.map +1 -1
  21. package/dist/atheletebridge/pages/performance/PerformancePage.js +496 -0
  22. package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -0
  23. package/dist/atheletebridge/pages/programs/ProgramDetailPage.js +176 -142
  24. package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
  25. package/dist/atheletebridge/pages/sessions/SessionDetailPage.js +86 -85
  26. package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
  27. package/dist/atheletebridge/pages/sessions/SessionFormPage.js +92 -98
  28. package/dist/atheletebridge/pages/sessions/SessionFormPage.js.map +1 -1
  29. package/dist/atheletebridge/pages/teams/TeamDetailPage.js +132 -137
  30. package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
  31. package/dist/atheletebridge/utils/formatters.js +2 -25
  32. package/dist/atheletebridge/utils/formatters.js.map +1 -1
  33. package/dist/atheletebridge/utils/url.js +17 -0
  34. package/dist/atheletebridge/utils/url.js.map +1 -0
  35. package/dist/index.js +4 -4
  36. package/package.json +5 -5
@@ -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 { ArrowLeft, Edit, Archive, Plus, Trash2, Loader2, Send, Undo2, Copy } from 'lucide-react';\nimport {\n GlassCard,\n EmphasisPanel,\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 { 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} 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\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\n const [drillOpen, setDrillOpen] = useState(false);\n const [drillForm, setDrillForm] = useState<DrillForm>(EMPTY_DRILL);\n const [drillError, setDrillError] = useState<string | null>(null);\n\n const drillsByWeek = useMemo(() => {\n const drills = program.data?.drills ?? [];\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?.length ?? 0;\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 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?.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 });\n setDrillForm(EMPTY_DRILL);\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 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 flex-wrap items-center gap-3\">\n <StatusBadge status={p.status} />\n {p.isTemplate && (\n <span className=\"inline-flex items-center gap-1 text-sm text-text-secondary\">\n <Copy className=\"h-3.5 w-3.5\" /> Template\n </span>\n )}\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 <div className=\"space-y-6\">\n {drillsByWeek.map(({ week, drills }) => (\n <div key={week}>\n <h3 className=\"mb-3 text-sm font-semibold uppercase tracking-wide text-text-secondary\">\n Week {week}\n </h3>\n <EmphasisPanel>\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-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-text-secondary\">\n {d.sets !== null && <span>{d.sets} sets</span>}\n {d.reps !== null && <span>{d.reps} reps</span>}\n {d.durationSec !== null && <span>{d.durationSec}s work</span>}\n {d.restSec !== null && <span>{d.restSec}s rest</span>}\n </div>\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 </EmphasisPanel>\n </div>\n ))}\n </div>\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-3 gap-4\">\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 {drillError && <p className=\"text-sm text-status-error-text\">{drillError}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => setDrillOpen(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={addDrill.isPending}\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 && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n 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 EmphasisPanel,\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_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\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 ?? [];\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?.length ?? 0;\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?.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 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 flex-wrap items-center gap-3\">\n <StatusBadge status={p.status} />\n {p.isTemplate && (\n <span className=\"inline-flex items-center gap-1 text-sm text-text-secondary\">\n <Copy className=\"h-3.5 w-3.5\" /> Template\n </span>\n )}\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 <div className=\"space-y-6\">\n {drillsByWeek.map(({ week, drills }) => (\n <div key={week}>\n <h3 className=\"mb-3 text-sm font-semibold uppercase tracking-wide text-text-secondary\">\n Week {week}\n </h3>\n <EmphasisPanel>\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-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-text-secondary\">\n {d.sets !== null && <span>{d.sets} sets</span>}\n {d.reps !== null && <span>{d.reps} reps</span>}\n {d.durationSec !== null && <span>{d.durationSec}s work</span>}\n {d.restSec !== null && <span>{d.restSec}s rest</span>}\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 </EmphasisPanel>\n </div>\n ))}\n </div>\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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"}
@@ -6,109 +6,110 @@ import { formatDate as i, formatDuration as a } from "../../utils/formatters.js"
6
6
  import { ErrorState as o, LoadingState as s } from "../../components/LoadingState.js";
7
7
  import { useCancelSession as c, useCompleteSession as l, useConfirmSession as u, useSession as d } from "../../hooks/useAthleteBridgeApi.js";
8
8
  import { FormField as f, fieldInputClass as p } from "../../components/form.js";
9
- import { useState as m } from "react";
10
- import { ArrowLeft as h, CalendarClock as g, CheckCircle2 as _, Clock as v, Loader2 as y, MapPin as b, Star as x, Video as S, XCircle as C } from "lucide-react";
11
- import { useParams as w } from "react-router-dom";
12
- import { jsx as T, jsxs as E } from "react/jsx-runtime";
13
- import { EmphasisPanel as D, GlassCard as O, ResponsiveDialog as k, ResponsiveDialogContent as A, ResponsiveDialogFooter as j, ResponsiveDialogHeader as M, ResponsiveDialogTitle as N } from "@burdenoff/fe-libs/ui";
9
+ import { safeExternalHref as m } from "../../utils/url.js";
10
+ import { useState as h } from "react";
11
+ import { ArrowLeft as g, CalendarClock as _, CheckCircle2 as v, Clock as y, Loader2 as b, MapPin as x, Star as S, Video as C, XCircle as w } from "lucide-react";
12
+ import { useParams as T } from "react-router-dom";
13
+ import { jsx as E, jsxs as D } from "react/jsx-runtime";
14
+ import { EmphasisPanel as O, GlassCard as k, ResponsiveDialog as A, ResponsiveDialogContent as j, ResponsiveDialogFooter as M, ResponsiveDialogHeader as N, ResponsiveDialogTitle as P } from "@burdenoff/fe-libs/ui";
14
15
  //#region src/atheletebridge/pages/sessions/SessionDetailPage.tsx
15
- function P() {
16
- let { id: P } = w(), { navigateTo: F } = e(), I = d(P), L = u(), R = l(), z = c(), [B, V] = m(!1), [H, U] = m(""), [W, G] = m(""), [K, q] = m(null);
17
- if (I.isLoading) return /* @__PURE__ */ T(t, {
16
+ function F() {
17
+ let { id: F } = T(), { navigateTo: I } = e(), L = d(F), R = u(), z = l(), B = c(), [V, H] = h(!1), [U, W] = h(""), [G, K] = h(""), [q, J] = h(null);
18
+ if (L.isLoading) return /* @__PURE__ */ E(t, {
18
19
  title: "Session",
19
- children: /* @__PURE__ */ T(s, { label: "Loading session…" })
20
+ children: /* @__PURE__ */ E(s, { label: "Loading session…" })
20
21
  });
21
- if (I.isError || !I.data) return /* @__PURE__ */ T(t, {
22
+ if (L.isError || !L.data) return /* @__PURE__ */ E(t, {
22
23
  title: "Session",
23
- children: /* @__PURE__ */ T(o, {
24
+ children: /* @__PURE__ */ E(o, {
24
25
  title: "Session not found",
25
26
  message: "This session does not exist or you do not have access to it.",
26
- onRetry: () => F("/sessions")
27
+ onRetry: () => I("/sessions")
27
28
  })
28
29
  });
29
- let J = I.data, Y = J.status === "REQUESTED", X = J.status === "CONFIRMED", Z = J.status === "REQUESTED" || J.status === "CONFIRMED";
30
- return /* @__PURE__ */ E(t, {
31
- title: J.title,
32
- description: r[J.type],
33
- actions: /* @__PURE__ */ E("button", {
30
+ let Y = L.data, X = Y.status === "REQUESTED", Z = Y.status === "CONFIRMED", Q = Y.status === "REQUESTED" || Y.status === "CONFIRMED", $ = m(Y.meetingUrl);
31
+ return /* @__PURE__ */ D(t, {
32
+ title: Y.title,
33
+ description: r[Y.type],
34
+ actions: /* @__PURE__ */ D("button", {
34
35
  type: "button",
35
- onClick: () => F("/sessions"),
36
+ onClick: () => I("/sessions"),
36
37
  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",
37
- children: [/* @__PURE__ */ T(h, { className: "h-4 w-4" }), " Back"]
38
+ children: [/* @__PURE__ */ E(g, { className: "h-4 w-4" }), " Back"]
38
39
  }),
39
40
  children: [
40
- /* @__PURE__ */ E(O, {
41
+ /* @__PURE__ */ D(k, {
41
42
  treatment: "insight",
42
43
  className: "mb-8 p-6",
43
44
  children: [
44
- /* @__PURE__ */ T("div", {
45
+ /* @__PURE__ */ E("div", {
45
46
  className: "flex flex-wrap items-center gap-3",
46
- children: /* @__PURE__ */ T(n, { status: J.status })
47
+ children: /* @__PURE__ */ E(n, { status: Y.status })
47
48
  }),
48
- /* @__PURE__ */ E("div", {
49
+ /* @__PURE__ */ D("div", {
49
50
  className: "mt-4 grid grid-cols-1 gap-3 text-sm text-text-secondary sm:grid-cols-2",
50
51
  children: [
51
- /* @__PURE__ */ E("span", {
52
+ /* @__PURE__ */ D("span", {
52
53
  className: "flex items-center gap-2",
53
54
  children: [
54
- /* @__PURE__ */ T(g, { className: "h-4 w-4" }),
55
+ /* @__PURE__ */ E(_, { className: "h-4 w-4" }),
55
56
  " ",
56
- i(J.scheduledAt)
57
+ i(Y.scheduledAt)
57
58
  ]
58
59
  }),
59
- /* @__PURE__ */ E("span", {
60
+ /* @__PURE__ */ D("span", {
60
61
  className: "flex items-center gap-2",
61
62
  children: [
62
- /* @__PURE__ */ T(v, { className: "h-4 w-4" }),
63
+ /* @__PURE__ */ E(y, { className: "h-4 w-4" }),
63
64
  " ",
64
- a(J.durationMin)
65
+ a(Y.durationMin)
65
66
  ]
66
67
  }),
67
- J.location && /* @__PURE__ */ E("span", {
68
+ Y.location && /* @__PURE__ */ D("span", {
68
69
  className: "flex items-center gap-2",
69
70
  children: [
70
- /* @__PURE__ */ T(b, { className: "h-4 w-4" }),
71
+ /* @__PURE__ */ E(x, { className: "h-4 w-4" }),
71
72
  " ",
72
- J.location
73
+ Y.location
73
74
  ]
74
75
  }),
75
- J.meetingUrl && /* @__PURE__ */ E("a", {
76
- href: J.meetingUrl,
76
+ $ && /* @__PURE__ */ D("a", {
77
+ href: $,
77
78
  target: "_blank",
78
79
  rel: "noopener noreferrer",
79
80
  className: "flex items-center gap-2 text-primary hover:underline",
80
- children: [/* @__PURE__ */ T(S, { className: "h-4 w-4" }), " Join meeting"]
81
+ children: [/* @__PURE__ */ E(C, { className: "h-4 w-4" }), " Join meeting"]
81
82
  })
82
83
  ]
83
84
  }),
84
- J.completedAt && /* @__PURE__ */ E("div", {
85
+ Y.completedAt && /* @__PURE__ */ D("div", {
85
86
  className: "mt-5 border-t border-border-seam pt-5",
86
87
  children: [
87
- /* @__PURE__ */ T("h2", {
88
+ /* @__PURE__ */ E("h2", {
88
89
  className: "mb-2 text-sm font-semibold text-text-primary",
89
90
  children: "Outcome"
90
91
  }),
91
- /* @__PURE__ */ E("p", {
92
+ /* @__PURE__ */ D("p", {
92
93
  className: "text-sm text-text-secondary",
93
94
  children: [
94
95
  "Completed ",
95
- i(J.completedAt),
96
- J.rating !== null && /* @__PURE__ */ E("span", {
96
+ i(Y.completedAt),
97
+ Y.rating !== null && /* @__PURE__ */ D("span", {
97
98
  className: "ml-2 inline-flex items-center gap-1",
98
99
  children: [
99
- /* @__PURE__ */ T(x, { className: "h-3.5 w-3.5" }),
100
+ /* @__PURE__ */ E(S, { className: "h-3.5 w-3.5" }),
100
101
  " ",
101
- J.rating,
102
+ Y.rating,
102
103
  "/5"
103
104
  ]
104
105
  })
105
106
  ]
106
107
  }),
107
- J.feedback && /* @__PURE__ */ E("p", {
108
+ Y.feedback && /* @__PURE__ */ D("p", {
108
109
  className: "mt-2 text-sm text-text-secondary",
109
110
  children: [
110
111
  "“",
111
- J.feedback,
112
+ Y.feedback,
112
113
  "”"
113
114
  ]
114
115
  })
@@ -116,101 +117,101 @@ function P() {
116
117
  })
117
118
  ]
118
119
  }),
119
- (Y || X || Z) && /* @__PURE__ */ E(D, { children: [/* @__PURE__ */ T("h2", {
120
+ (X || Z || Q) && /* @__PURE__ */ D(O, { children: [/* @__PURE__ */ E("h2", {
120
121
  className: "mb-4 text-sm font-semibold text-text-primary",
121
122
  children: "Actions"
122
- }), /* @__PURE__ */ E("div", {
123
+ }), /* @__PURE__ */ D("div", {
123
124
  className: "flex flex-wrap items-center gap-2",
124
125
  children: [
125
- Y && /* @__PURE__ */ E("button", {
126
+ X && /* @__PURE__ */ D("button", {
126
127
  type: "button",
127
- onClick: () => P && L.mutate(P),
128
- disabled: L.isPending,
128
+ onClick: () => F && R.mutate(F),
129
+ disabled: R.isPending,
129
130
  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",
130
- children: [L.isPending ? /* @__PURE__ */ T(y, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ T(_, { className: "h-4 w-4" }), "Confirm"]
131
+ children: [R.isPending ? /* @__PURE__ */ E(b, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ E(v, { className: "h-4 w-4" }), "Confirm"]
131
132
  }),
132
- X && /* @__PURE__ */ E("button", {
133
+ Z && /* @__PURE__ */ D("button", {
133
134
  type: "button",
134
135
  onClick: () => {
135
- q(null), V(!0);
136
+ J(null), H(!0);
136
137
  },
137
138
  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",
138
- children: [/* @__PURE__ */ T(_, { className: "h-4 w-4" }), " Complete"]
139
+ children: [/* @__PURE__ */ E(v, { className: "h-4 w-4" }), " Complete"]
139
140
  }),
140
- Z && /* @__PURE__ */ E("button", {
141
+ Q && /* @__PURE__ */ D("button", {
141
142
  type: "button",
142
- onClick: () => P && z.mutate(P),
143
- disabled: z.isPending,
143
+ onClick: () => F && B.mutate(F),
144
+ disabled: B.isPending,
144
145
  className: "inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-60",
145
- children: [z.isPending ? /* @__PURE__ */ T(y, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ T(C, { className: "h-4 w-4" }), "Cancel"]
146
+ children: [B.isPending ? /* @__PURE__ */ E(b, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ E(w, { className: "h-4 w-4" }), "Cancel"]
146
147
  })
147
148
  ]
148
149
  })] }),
149
- /* @__PURE__ */ T(k, {
150
- open: B,
151
- onOpenChange: V,
152
- children: /* @__PURE__ */ E(A, { children: [/* @__PURE__ */ T(M, { children: /* @__PURE__ */ T(N, { children: "Complete session" }) }), /* @__PURE__ */ E("form", {
150
+ /* @__PURE__ */ E(A, {
151
+ open: V,
152
+ onOpenChange: H,
153
+ children: /* @__PURE__ */ D(j, { children: [/* @__PURE__ */ E(N, { children: /* @__PURE__ */ E(P, { children: "Complete session" }) }), /* @__PURE__ */ D("form", {
153
154
  onSubmit: async (e) => {
154
- if (e.preventDefault(), q(null), !P) return;
155
+ if (e.preventDefault(), J(null), !F) return;
155
156
  let t;
156
- if (W.trim() && (t = Number.parseInt(W, 10), Number.isNaN(t) || t < 1 || t > 5)) {
157
- q("Rating must be between 1 and 5.");
157
+ if (G.trim() && (t = Number.parseInt(G, 10), Number.isNaN(t) || t < 1 || t > 5)) {
158
+ J("Rating must be between 1 and 5.");
158
159
  return;
159
160
  }
160
161
  try {
161
- await R.mutateAsync({
162
- id: P,
162
+ await z.mutateAsync({
163
+ id: F,
163
164
  input: {
164
- feedback: H.trim() || void 0,
165
+ feedback: U.trim() || void 0,
165
166
  rating: t
166
167
  }
167
- }), V(!1);
168
+ }), H(!1);
168
169
  } catch (e) {
169
- q(e instanceof Error ? e.message : "Failed to complete session.");
170
+ J(e instanceof Error ? e.message : "Failed to complete session.");
170
171
  }
171
172
  },
172
173
  className: "space-y-4 px-1",
173
174
  children: [
174
- /* @__PURE__ */ T(f, {
175
+ /* @__PURE__ */ E(f, {
175
176
  label: "Feedback",
176
177
  htmlFor: "feedback",
177
178
  hint: "Optional post-session notes.",
178
- children: /* @__PURE__ */ T("textarea", {
179
+ children: /* @__PURE__ */ E("textarea", {
179
180
  id: "feedback",
180
181
  className: p,
181
182
  rows: 3,
182
- value: H,
183
- onChange: (e) => U(e.target.value),
183
+ value: U,
184
+ onChange: (e) => W(e.target.value),
184
185
  placeholder: "How did the session go?"
185
186
  })
186
187
  }),
187
- /* @__PURE__ */ T(f, {
188
+ /* @__PURE__ */ E(f, {
188
189
  label: "Rating (1–5)",
189
190
  htmlFor: "rating",
190
191
  hint: "Optional.",
191
- children: /* @__PURE__ */ T("input", {
192
+ children: /* @__PURE__ */ E("input", {
192
193
  id: "rating",
193
194
  inputMode: "numeric",
194
195
  className: p,
195
- value: W,
196
- onChange: (e) => G(e.target.value),
196
+ value: G,
197
+ onChange: (e) => K(e.target.value),
197
198
  placeholder: "5"
198
199
  })
199
200
  }),
200
- K && /* @__PURE__ */ T("p", {
201
+ q && /* @__PURE__ */ E("p", {
201
202
  className: "text-sm text-status-error-text",
202
- children: K
203
+ children: q
203
204
  }),
204
- /* @__PURE__ */ E(j, { children: [/* @__PURE__ */ T("button", {
205
+ /* @__PURE__ */ D(M, { children: [/* @__PURE__ */ E("button", {
205
206
  type: "button",
206
- onClick: () => V(!1),
207
+ onClick: () => H(!1),
207
208
  className: "rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken",
208
209
  children: "Cancel"
209
- }), /* @__PURE__ */ E("button", {
210
+ }), /* @__PURE__ */ D("button", {
210
211
  type: "submit",
211
- disabled: R.isPending,
212
+ disabled: z.isPending,
212
213
  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",
213
- children: [R.isPending && /* @__PURE__ */ T(y, { className: "h-4 w-4 animate-spin" }), "Complete session"]
214
+ children: [z.isPending && /* @__PURE__ */ E(b, { className: "h-4 w-4 animate-spin" }), "Complete session"]
214
215
  })] })
215
216
  ]
216
217
  })] })
@@ -219,6 +220,6 @@ function P() {
219
220
  });
220
221
  }
221
222
  //#endregion
222
- export { P as SessionDetailPage };
223
+ export { F as SessionDetailPage };
223
224
 
224
225
  //# sourceMappingURL=SessionDetailPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SessionDetailPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/sessions/SessionDetailPage.tsx"],"sourcesContent":["/**\n * Session Detail\n * @module atheletebridge/pages/sessions\n *\n * Reads a single session (athSession) and drives its lifecycle:\n * confirm (athConfirmSession), complete with feedback + rating\n * (athCompleteSession) and cancel (athCancelSession).\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n CalendarClock,\n Clock,\n MapPin,\n Video,\n Star,\n CheckCircle2,\n XCircle,\n Loader2,\n} from 'lucide-react';\nimport {\n GlassCard,\n EmphasisPanel,\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 { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { SESSION_TYPE_LABELS } from '../../components/domain';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport {\n useSession,\n useConfirmSession,\n useCompleteSession,\n useCancelSession,\n} from '../../hooks/useAthleteBridgeApi';\n\nexport function SessionDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const session = useSession(id);\n const confirmSession = useConfirmSession();\n const completeSession = useCompleteSession();\n const cancelSession = useCancelSession();\n\n const [completeOpen, setCompleteOpen] = useState(false);\n const [feedback, setFeedback] = useState('');\n const [rating, setRating] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n if (session.isLoading) {\n return (\n <PageLayout title=\"Session\">\n <LoadingState label=\"Loading session…\" />\n </PageLayout>\n );\n }\n\n if (session.isError || !session.data) {\n return (\n <PageLayout title=\"Session\">\n <ErrorState\n title=\"Session not found\"\n message=\"This session does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/sessions')}\n />\n </PageLayout>\n );\n }\n\n const s = session.data;\n const canConfirm = s.status === 'REQUESTED';\n const canComplete = s.status === 'CONFIRMED';\n const canCancel = s.status === 'REQUESTED' || s.status === 'CONFIRMED';\n\n const handleComplete = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!id) return;\n let ratingNum: number | undefined;\n if (rating.trim()) {\n ratingNum = Number.parseInt(rating, 10);\n if (Number.isNaN(ratingNum) || ratingNum < 1 || ratingNum > 5) {\n setError('Rating must be between 1 and 5.');\n return;\n }\n }\n try {\n await completeSession.mutateAsync({\n id,\n input: {\n feedback: feedback.trim() || undefined,\n rating: ratingNum,\n },\n });\n setCompleteOpen(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to complete session.');\n }\n };\n\n return (\n <PageLayout\n title={s.title}\n description={SESSION_TYPE_LABELS[s.type]}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions')}\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\" /> Back\n </button>\n }\n >\n <GlassCard treatment=\"insight\" className=\"mb-8 p-6\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={s.status} />\n </div>\n <div className=\"mt-4 grid grid-cols-1 gap-3 text-sm text-text-secondary sm:grid-cols-2\">\n <span className=\"flex items-center gap-2\">\n <CalendarClock className=\"h-4 w-4\" /> {formatDate(s.scheduledAt)}\n </span>\n <span className=\"flex items-center gap-2\">\n <Clock className=\"h-4 w-4\" /> {formatDuration(s.durationMin)}\n </span>\n {s.location && (\n <span className=\"flex items-center gap-2\">\n <MapPin className=\"h-4 w-4\" /> {s.location}\n </span>\n )}\n {s.meetingUrl && (\n <a\n href={s.meetingUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 text-primary hover:underline\"\n >\n <Video className=\"h-4 w-4\" /> Join meeting\n </a>\n )}\n </div>\n\n {s.completedAt && (\n <div className=\"mt-5 border-t border-border-seam pt-5\">\n <h2 className=\"mb-2 text-sm font-semibold text-text-primary\">Outcome</h2>\n <p className=\"text-sm text-text-secondary\">\n Completed {formatDate(s.completedAt)}\n {s.rating !== null && (\n <span className=\"ml-2 inline-flex items-center gap-1\">\n <Star className=\"h-3.5 w-3.5\" /> {s.rating}/5\n </span>\n )}\n </p>\n {s.feedback && <p className=\"mt-2 text-sm text-text-secondary\">“{s.feedback}”</p>}\n </div>\n )}\n </GlassCard>\n\n {/* Lifecycle actions */}\n {(canConfirm || canComplete || canCancel) && (\n <EmphasisPanel>\n <h2 className=\"mb-4 text-sm font-semibold text-text-primary\">Actions</h2>\n <div className=\"flex flex-wrap items-center gap-2\">\n {canConfirm && (\n <button\n type=\"button\"\n onClick={() => id && confirmSession.mutate(id)}\n disabled={confirmSession.isPending}\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 {confirmSession.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"h-4 w-4\" />\n )}\n Confirm\n </button>\n )}\n {canComplete && (\n <button\n type=\"button\"\n onClick={() => {\n setError(null);\n setCompleteOpen(true);\n }}\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 <CheckCircle2 className=\"h-4 w-4\" /> Complete\n </button>\n )}\n {canCancel && (\n <button\n type=\"button\"\n onClick={() => id && cancelSession.mutate(id)}\n disabled={cancelSession.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-60\"\n >\n {cancelSession.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <XCircle className=\"h-4 w-4\" />\n )}\n Cancel\n </button>\n )}\n </div>\n </EmphasisPanel>\n )}\n\n {/* Complete dialog */}\n <ResponsiveDialog open={completeOpen} onOpenChange={setCompleteOpen}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Complete session</ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleComplete} className=\"space-y-4 px-1\">\n <FormField label=\"Feedback\" htmlFor=\"feedback\" hint=\"Optional post-session notes.\">\n <textarea\n id=\"feedback\"\n className={fieldInputClass}\n rows={3}\n value={feedback}\n onChange={(e) => setFeedback(e.target.value)}\n placeholder=\"How did the session go?\"\n />\n </FormField>\n <FormField label=\"Rating (1–5)\" htmlFor=\"rating\" hint=\"Optional.\">\n <input\n id=\"rating\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rating}\n onChange={(e) => setRating(e.target.value)}\n placeholder=\"5\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => setCompleteOpen(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={completeSession.isPending}\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 {completeSession.isPending && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n Complete session\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SessionDetailPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/sessions/SessionDetailPage.tsx"],"sourcesContent":["/**\n * Session Detail\n * @module atheletebridge/pages/sessions\n *\n * Reads a single session (athSession) and drives its lifecycle:\n * confirm (athConfirmSession), complete with feedback + rating\n * (athCompleteSession) and cancel (athCancelSession).\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n CalendarClock,\n Clock,\n MapPin,\n Video,\n Star,\n CheckCircle2,\n XCircle,\n Loader2,\n} from 'lucide-react';\nimport {\n GlassCard,\n EmphasisPanel,\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 { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { SESSION_TYPE_LABELS } from '../../components/domain';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { safeExternalHref } from '../../utils/url';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport {\n useSession,\n useConfirmSession,\n useCompleteSession,\n useCancelSession,\n} from '../../hooks/useAthleteBridgeApi';\n\nexport function SessionDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const session = useSession(id);\n const confirmSession = useConfirmSession();\n const completeSession = useCompleteSession();\n const cancelSession = useCancelSession();\n\n const [completeOpen, setCompleteOpen] = useState(false);\n const [feedback, setFeedback] = useState('');\n const [rating, setRating] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n if (session.isLoading) {\n return (\n <PageLayout title=\"Session\">\n <LoadingState label=\"Loading session…\" />\n </PageLayout>\n );\n }\n\n if (session.isError || !session.data) {\n return (\n <PageLayout title=\"Session\">\n <ErrorState\n title=\"Session not found\"\n message=\"This session does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/sessions')}\n />\n </PageLayout>\n );\n }\n\n const s = session.data;\n const canConfirm = s.status === 'REQUESTED';\n const canComplete = s.status === 'CONFIRMED';\n const canCancel = s.status === 'REQUESTED' || s.status === 'CONFIRMED';\n // Sanitize the user-controlled meeting URL before it becomes a clickable href.\n const meetingHref = safeExternalHref(s.meetingUrl);\n\n const handleComplete = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!id) return;\n let ratingNum: number | undefined;\n if (rating.trim()) {\n ratingNum = Number.parseInt(rating, 10);\n if (Number.isNaN(ratingNum) || ratingNum < 1 || ratingNum > 5) {\n setError('Rating must be between 1 and 5.');\n return;\n }\n }\n try {\n await completeSession.mutateAsync({\n id,\n input: {\n feedback: feedback.trim() || undefined,\n rating: ratingNum,\n },\n });\n setCompleteOpen(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to complete session.');\n }\n };\n\n return (\n <PageLayout\n title={s.title}\n description={SESSION_TYPE_LABELS[s.type]}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions')}\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\" /> Back\n </button>\n }\n >\n <GlassCard treatment=\"insight\" className=\"mb-8 p-6\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={s.status} />\n </div>\n <div className=\"mt-4 grid grid-cols-1 gap-3 text-sm text-text-secondary sm:grid-cols-2\">\n <span className=\"flex items-center gap-2\">\n <CalendarClock className=\"h-4 w-4\" /> {formatDate(s.scheduledAt)}\n </span>\n <span className=\"flex items-center gap-2\">\n <Clock className=\"h-4 w-4\" /> {formatDuration(s.durationMin)}\n </span>\n {s.location && (\n <span className=\"flex items-center gap-2\">\n <MapPin className=\"h-4 w-4\" /> {s.location}\n </span>\n )}\n {meetingHref && (\n <a\n href={meetingHref}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 text-primary hover:underline\"\n >\n <Video className=\"h-4 w-4\" /> Join meeting\n </a>\n )}\n </div>\n\n {s.completedAt && (\n <div className=\"mt-5 border-t border-border-seam pt-5\">\n <h2 className=\"mb-2 text-sm font-semibold text-text-primary\">Outcome</h2>\n <p className=\"text-sm text-text-secondary\">\n Completed {formatDate(s.completedAt)}\n {s.rating !== null && (\n <span className=\"ml-2 inline-flex items-center gap-1\">\n <Star className=\"h-3.5 w-3.5\" /> {s.rating}/5\n </span>\n )}\n </p>\n {s.feedback && <p className=\"mt-2 text-sm text-text-secondary\">“{s.feedback}”</p>}\n </div>\n )}\n </GlassCard>\n\n {/* Lifecycle actions */}\n {(canConfirm || canComplete || canCancel) && (\n <EmphasisPanel>\n <h2 className=\"mb-4 text-sm font-semibold text-text-primary\">Actions</h2>\n <div className=\"flex flex-wrap items-center gap-2\">\n {canConfirm && (\n <button\n type=\"button\"\n onClick={() => id && confirmSession.mutate(id)}\n disabled={confirmSession.isPending}\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 {confirmSession.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"h-4 w-4\" />\n )}\n Confirm\n </button>\n )}\n {canComplete && (\n <button\n type=\"button\"\n onClick={() => {\n setError(null);\n setCompleteOpen(true);\n }}\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 <CheckCircle2 className=\"h-4 w-4\" /> Complete\n </button>\n )}\n {canCancel && (\n <button\n type=\"button\"\n onClick={() => id && cancelSession.mutate(id)}\n disabled={cancelSession.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-60\"\n >\n {cancelSession.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <XCircle className=\"h-4 w-4\" />\n )}\n Cancel\n </button>\n )}\n </div>\n </EmphasisPanel>\n )}\n\n {/* Complete dialog */}\n <ResponsiveDialog open={completeOpen} onOpenChange={setCompleteOpen}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Complete session</ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleComplete} className=\"space-y-4 px-1\">\n <FormField label=\"Feedback\" htmlFor=\"feedback\" hint=\"Optional post-session notes.\">\n <textarea\n id=\"feedback\"\n className={fieldInputClass}\n rows={3}\n value={feedback}\n onChange={(e) => setFeedback(e.target.value)}\n placeholder=\"How did the session go?\"\n />\n </FormField>\n <FormField label=\"Rating (1–5)\" htmlFor=\"rating\" hint=\"Optional.\">\n <input\n id=\"rating\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rating}\n onChange={(e) => setRating(e.target.value)}\n placeholder=\"5\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => setCompleteOpen(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={completeSession.isPending}\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 {completeSession.isPending && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n Complete session\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}