@burdenoff/microfe-atheletebridge 2026.628.1 → 2026.705.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.
- package/dist/atheletebridge/AthleteBridgeRoot.js +45 -37
- package/dist/atheletebridge/AthleteBridgeRoot.js.map +1 -1
- package/dist/atheletebridge/AthleteBridgeRoutes.js +10 -6
- package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
- package/dist/atheletebridge/components/AthleteSelect.js +66 -0
- package/dist/atheletebridge/components/AthleteSelect.js.map +1 -0
- package/dist/atheletebridge/components/DrillVideoLink.js +28 -0
- package/dist/atheletebridge/components/DrillVideoLink.js.map +1 -0
- package/dist/atheletebridge/components/FixtureCard.js.map +1 -1
- package/dist/atheletebridge/components/LoadingState.js.map +1 -1
- package/dist/atheletebridge/components/MetricsCard.js +20 -46
- package/dist/atheletebridge/components/MetricsCard.js.map +1 -1
- package/dist/atheletebridge/components/UserSearchSelect.js +129 -0
- package/dist/atheletebridge/components/UserSearchSelect.js.map +1 -0
- package/dist/atheletebridge/components/domain.js +10 -1
- package/dist/atheletebridge/components/domain.js.map +1 -1
- package/dist/atheletebridge/context/AthleteBridgeContext.js.map +1 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +313 -180
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
- package/dist/atheletebridge/pages/DashboardPage.js +77 -69
- package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
- package/dist/atheletebridge/pages/StandingsPage.js.map +1 -1
- package/dist/atheletebridge/pages/performance/PerformancePage.js +496 -0
- package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -0
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js +175 -141
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionFormPage.js +91 -98
- package/dist/atheletebridge/pages/sessions/SessionFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js +132 -137
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamsListPage.js.map +1 -1
- package/dist/atheletebridge/utils/formatters.js +2 -25
- package/dist/atheletebridge/utils/formatters.js.map +1 -1
- package/dist/index.js +4 -4
- 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 {\n ArrowLeft,\n Edit,\n Archive,\n Plus,\n Trash2,\n Loader2,\n Send,\n Undo2,\n Copy,\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 { 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 && (\n <Loader2 className=\"h-4 w-4 animate-spin text-text-secondary\" />\n )}\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) =>\n setDrillForm((p) => ({ ...p, description: e.target.value }))\n }\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) =>\n setDrillForm((p) => ({ ...p, weekNumber: e.target.value }))\n }\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) =>\n setDrillForm((p) => ({ ...p, durationSec: e.target.value }))\n }\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-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 <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 +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 {\n useProgram,\n useCreateProgram,\n useUpdateProgram,\n} 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\n label=\"Tags\"\n htmlFor=\"tags\"\n hint=\"Comma-separated, e.g. strength, pre-season.\"\n >\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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"}
|
|
@@ -3,197 +3,190 @@ import { PageLayout as t } from "../../components/PageLayout.js";
|
|
|
3
3
|
import { SESSION_TYPE_LABELS as n } from "../../components/domain.js";
|
|
4
4
|
import { useBookSession as r, usePrograms as i } from "../../hooks/useAthleteBridgeApi.js";
|
|
5
5
|
import { FormField as a, fieldInputClass as o } from "../../components/form.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { AthleteSelect as s } from "../../components/AthleteSelect.js";
|
|
7
|
+
import { useState as c } from "react";
|
|
8
|
+
import { ArrowLeft as l, Loader2 as u, Save as d } from "lucide-react";
|
|
9
|
+
import { jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
9
10
|
//#region src/atheletebridge/pages/sessions/SessionFormPage.tsx
|
|
10
|
-
var
|
|
11
|
-
function
|
|
12
|
-
let { navigateTo:
|
|
11
|
+
var m = Object.keys(n);
|
|
12
|
+
function h() {
|
|
13
|
+
let { navigateTo: h, currentUser: g } = e(), _ = i({
|
|
13
14
|
status: "PUBLISHED",
|
|
14
15
|
pagination: { limit: 100 }
|
|
15
|
-
}),
|
|
16
|
-
return /* @__PURE__ */
|
|
16
|
+
}), v = r(), [y, b] = c(g?.id ?? ""), [x, S] = c(""), [C, w] = c(""), [T, E] = c(""), [D, O] = c("IN_PERSON"), [k, A] = c(""), [j, M] = c(""), [N, P] = c(""), [F, I] = c("60"), [L, R] = c(null), z = _.data?.items ?? [];
|
|
17
|
+
return /* @__PURE__ */ f(t, {
|
|
17
18
|
title: "Book session",
|
|
18
19
|
description: "Schedule a coaching session between a coach and an athlete.",
|
|
19
|
-
actions: /* @__PURE__ */
|
|
20
|
+
actions: /* @__PURE__ */ p("button", {
|
|
20
21
|
type: "button",
|
|
21
|
-
onClick: () =>
|
|
22
|
+
onClick: () => h("/sessions"),
|
|
22
23
|
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",
|
|
23
|
-
children: [/* @__PURE__ */
|
|
24
|
+
children: [/* @__PURE__ */ f(l, { className: "h-4 w-4" }), " Cancel"]
|
|
24
25
|
}),
|
|
25
|
-
children: /* @__PURE__ */
|
|
26
|
+
children: /* @__PURE__ */ p("form", {
|
|
26
27
|
onSubmit: async (e) => {
|
|
27
|
-
if (e.preventDefault(),
|
|
28
|
-
|
|
28
|
+
if (e.preventDefault(), R(null), !y.trim()) {
|
|
29
|
+
R("Coach ID is required.");
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
|
-
if (!
|
|
32
|
-
|
|
32
|
+
if (!x.trim()) {
|
|
33
|
+
R("Athlete ID is required.");
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
|
-
if (!
|
|
36
|
-
|
|
36
|
+
if (!T.trim()) {
|
|
37
|
+
R("A session title is required.");
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
39
|
-
if (!
|
|
40
|
-
|
|
40
|
+
if (!N) {
|
|
41
|
+
R("A scheduled date and time is required.");
|
|
41
42
|
return;
|
|
42
43
|
}
|
|
43
|
-
let t = Number.parseInt(
|
|
44
|
+
let t = Number.parseInt(F, 10);
|
|
44
45
|
if (Number.isNaN(t) || t <= 0) {
|
|
45
|
-
|
|
46
|
+
R("Duration must be a positive number of minutes.");
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
48
49
|
try {
|
|
49
|
-
|
|
50
|
-
coachId:
|
|
51
|
-
athleteId:
|
|
52
|
-
programId:
|
|
53
|
-
title:
|
|
54
|
-
type:
|
|
55
|
-
location:
|
|
56
|
-
meetingUrl:
|
|
57
|
-
scheduledAt: new Date(
|
|
50
|
+
h(`/sessions/${(await v.mutateAsync({
|
|
51
|
+
coachId: y.trim(),
|
|
52
|
+
athleteId: x.trim(),
|
|
53
|
+
programId: C || void 0,
|
|
54
|
+
title: T.trim(),
|
|
55
|
+
type: D,
|
|
56
|
+
location: D === "ONLINE" ? void 0 : k.trim() || void 0,
|
|
57
|
+
meetingUrl: D === "IN_PERSON" ? void 0 : j.trim() || void 0,
|
|
58
|
+
scheduledAt: new Date(N).toISOString(),
|
|
58
59
|
durationMin: t
|
|
59
60
|
})).id}`);
|
|
60
61
|
} catch (e) {
|
|
61
|
-
|
|
62
|
+
R(e instanceof Error ? e.message : "Failed to book session.");
|
|
62
63
|
}
|
|
63
64
|
},
|
|
64
65
|
className: "max-w-2xl space-y-5",
|
|
65
66
|
children: [
|
|
66
|
-
/* @__PURE__ */ f(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
placeholder: "coach-uuid"
|
|
79
|
-
})
|
|
80
|
-
}), /* @__PURE__ */ d(a, {
|
|
81
|
-
label: "Athlete ID",
|
|
82
|
-
htmlFor: "athleteId",
|
|
83
|
-
required: !0,
|
|
84
|
-
children: /* @__PURE__ */ d("input", {
|
|
85
|
-
id: "athleteId",
|
|
86
|
-
className: o,
|
|
87
|
-
value: b,
|
|
88
|
-
onChange: (e) => x(e.target.value),
|
|
89
|
-
placeholder: "athlete-uuid"
|
|
90
|
-
})
|
|
91
|
-
})]
|
|
67
|
+
/* @__PURE__ */ f(a, {
|
|
68
|
+
label: "Coach ID",
|
|
69
|
+
htmlFor: "coachId",
|
|
70
|
+
required: !0,
|
|
71
|
+
hint: "Defaults to you.",
|
|
72
|
+
children: /* @__PURE__ */ f("input", {
|
|
73
|
+
id: "coachId",
|
|
74
|
+
className: o,
|
|
75
|
+
value: y,
|
|
76
|
+
onChange: (e) => b(e.target.value),
|
|
77
|
+
placeholder: "coach-uuid"
|
|
78
|
+
})
|
|
92
79
|
}),
|
|
93
|
-
/* @__PURE__ */
|
|
80
|
+
/* @__PURE__ */ f(s, {
|
|
81
|
+
athleteId: x,
|
|
82
|
+
onSelect: ({ athleteId: e }) => S(e),
|
|
83
|
+
required: !0,
|
|
84
|
+
idPrefix: "session-athlete"
|
|
85
|
+
}),
|
|
86
|
+
/* @__PURE__ */ f(a, {
|
|
94
87
|
label: "Title",
|
|
95
88
|
htmlFor: "title",
|
|
96
89
|
required: !0,
|
|
97
|
-
children: /* @__PURE__ */
|
|
90
|
+
children: /* @__PURE__ */ f("input", {
|
|
98
91
|
id: "title",
|
|
99
92
|
className: o,
|
|
100
|
-
value:
|
|
101
|
-
onChange: (e) =>
|
|
93
|
+
value: T,
|
|
94
|
+
onChange: (e) => E(e.target.value),
|
|
102
95
|
placeholder: "e.g. Sprint technique review"
|
|
103
96
|
})
|
|
104
97
|
}),
|
|
105
|
-
/* @__PURE__ */
|
|
98
|
+
/* @__PURE__ */ p("div", {
|
|
106
99
|
className: "grid grid-cols-1 gap-5 sm:grid-cols-2",
|
|
107
|
-
children: [/* @__PURE__ */
|
|
100
|
+
children: [/* @__PURE__ */ f(a, {
|
|
108
101
|
label: "Type",
|
|
109
102
|
htmlFor: "type",
|
|
110
|
-
children: /* @__PURE__ */
|
|
103
|
+
children: /* @__PURE__ */ f("select", {
|
|
111
104
|
id: "type",
|
|
112
105
|
className: o,
|
|
113
|
-
value:
|
|
114
|
-
onChange: (e) =>
|
|
115
|
-
children:
|
|
106
|
+
value: D,
|
|
107
|
+
onChange: (e) => O(e.target.value),
|
|
108
|
+
children: m.map((e) => /* @__PURE__ */ f("option", {
|
|
116
109
|
value: e,
|
|
117
110
|
children: n[e]
|
|
118
111
|
}, e))
|
|
119
112
|
})
|
|
120
|
-
}), /* @__PURE__ */
|
|
113
|
+
}), /* @__PURE__ */ f(a, {
|
|
121
114
|
label: "Program",
|
|
122
115
|
htmlFor: "program",
|
|
123
116
|
hint: "Optional — tie to a published plan.",
|
|
124
|
-
children: /* @__PURE__ */
|
|
117
|
+
children: /* @__PURE__ */ p("select", {
|
|
125
118
|
id: "program",
|
|
126
119
|
className: o,
|
|
127
|
-
value:
|
|
128
|
-
onChange: (e) =>
|
|
129
|
-
children: [/* @__PURE__ */
|
|
120
|
+
value: C,
|
|
121
|
+
onChange: (e) => w(e.target.value),
|
|
122
|
+
children: [/* @__PURE__ */ f("option", {
|
|
130
123
|
value: "",
|
|
131
124
|
children: "No program"
|
|
132
|
-
}),
|
|
125
|
+
}), z.map((e) => /* @__PURE__ */ f("option", {
|
|
133
126
|
value: e.id,
|
|
134
127
|
children: e.name
|
|
135
128
|
}, e.id))]
|
|
136
129
|
})
|
|
137
130
|
})]
|
|
138
131
|
}),
|
|
139
|
-
|
|
132
|
+
D !== "ONLINE" && /* @__PURE__ */ f(a, {
|
|
140
133
|
label: "Location",
|
|
141
134
|
htmlFor: "location",
|
|
142
|
-
children: /* @__PURE__ */
|
|
135
|
+
children: /* @__PURE__ */ f("input", {
|
|
143
136
|
id: "location",
|
|
144
137
|
className: o,
|
|
145
|
-
value:
|
|
146
|
-
onChange: (e) =>
|
|
138
|
+
value: k,
|
|
139
|
+
onChange: (e) => A(e.target.value),
|
|
147
140
|
placeholder: "e.g. Main gym, Court 2"
|
|
148
141
|
})
|
|
149
142
|
}),
|
|
150
|
-
|
|
143
|
+
D !== "IN_PERSON" && /* @__PURE__ */ f(a, {
|
|
151
144
|
label: "Meeting URL",
|
|
152
145
|
htmlFor: "meetingUrl",
|
|
153
|
-
children: /* @__PURE__ */
|
|
146
|
+
children: /* @__PURE__ */ f("input", {
|
|
154
147
|
id: "meetingUrl",
|
|
155
148
|
className: o,
|
|
156
|
-
value:
|
|
157
|
-
onChange: (e) =>
|
|
149
|
+
value: j,
|
|
150
|
+
onChange: (e) => M(e.target.value),
|
|
158
151
|
placeholder: "https://meet.example.com/…"
|
|
159
152
|
})
|
|
160
153
|
}),
|
|
161
|
-
/* @__PURE__ */
|
|
154
|
+
/* @__PURE__ */ p("div", {
|
|
162
155
|
className: "grid grid-cols-1 gap-5 sm:grid-cols-2",
|
|
163
|
-
children: [/* @__PURE__ */
|
|
156
|
+
children: [/* @__PURE__ */ f(a, {
|
|
164
157
|
label: "Scheduled at",
|
|
165
158
|
htmlFor: "scheduledAt",
|
|
166
159
|
required: !0,
|
|
167
|
-
children: /* @__PURE__ */
|
|
160
|
+
children: /* @__PURE__ */ f("input", {
|
|
168
161
|
id: "scheduledAt",
|
|
169
162
|
type: "datetime-local",
|
|
170
163
|
className: o,
|
|
171
|
-
value:
|
|
172
|
-
onChange: (e) =>
|
|
164
|
+
value: N,
|
|
165
|
+
onChange: (e) => P(e.target.value)
|
|
173
166
|
})
|
|
174
|
-
}), /* @__PURE__ */
|
|
167
|
+
}), /* @__PURE__ */ f(a, {
|
|
175
168
|
label: "Duration (minutes)",
|
|
176
169
|
htmlFor: "duration",
|
|
177
|
-
children: /* @__PURE__ */
|
|
170
|
+
children: /* @__PURE__ */ f("input", {
|
|
178
171
|
id: "duration",
|
|
179
172
|
inputMode: "numeric",
|
|
180
173
|
className: o,
|
|
181
|
-
value:
|
|
182
|
-
onChange: (e) =>
|
|
174
|
+
value: F,
|
|
175
|
+
onChange: (e) => I(e.target.value)
|
|
183
176
|
})
|
|
184
177
|
})]
|
|
185
178
|
}),
|
|
186
|
-
|
|
179
|
+
L && /* @__PURE__ */ f("p", {
|
|
187
180
|
className: "text-sm text-status-error-text",
|
|
188
|
-
children:
|
|
181
|
+
children: L
|
|
189
182
|
}),
|
|
190
|
-
/* @__PURE__ */
|
|
183
|
+
/* @__PURE__ */ f("div", {
|
|
191
184
|
className: "pt-2",
|
|
192
|
-
children: /* @__PURE__ */
|
|
185
|
+
children: /* @__PURE__ */ p("button", {
|
|
193
186
|
type: "submit",
|
|
194
|
-
disabled:
|
|
187
|
+
disabled: v.isPending,
|
|
195
188
|
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",
|
|
196
|
-
children: [
|
|
189
|
+
children: [v.isPending ? /* @__PURE__ */ f(u, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ f(d, { className: "h-4 w-4" }), "Book session"]
|
|
197
190
|
})
|
|
198
191
|
})
|
|
199
192
|
]
|
|
@@ -201,6 +194,6 @@ function m() {
|
|
|
201
194
|
});
|
|
202
195
|
}
|
|
203
196
|
//#endregion
|
|
204
|
-
export {
|
|
197
|
+
export { h as SessionFormPage };
|
|
205
198
|
|
|
206
199
|
//# sourceMappingURL=SessionFormPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionFormPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/sessions/SessionFormPage.tsx"],"sourcesContent":["/**\n * Book Session Form\n * @module atheletebridge/pages/sessions\n */\n\nimport { useState } from 'react';\nimport { ArrowLeft, Loader2, Save } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { SESSION_TYPE_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useBookSession, usePrograms } from '../../hooks/useAthleteBridgeApi';\nimport type { AthSessionType } from '../../types';\n\nconst TYPES = Object.keys(SESSION_TYPE_LABELS) as AthSessionType[];\n\nexport function SessionFormPage() {\n const { navigateTo, currentUser } = useAthleteBridge();\n\n const programs = usePrograms({ status: 'PUBLISHED', pagination: { limit: 100 } });\n const bookSession = useBookSession();\n\n const [coachId, setCoachId] = useState(currentUser?.id ?? '');\n const [athleteId, setAthleteId] = useState('');\n const [programId, setProgramId] = useState('');\n const [title, setTitle] = useState('');\n const [type, setType] = useState<AthSessionType>('IN_PERSON');\n const [location, setLocation] = useState('');\n const [meetingUrl, setMeetingUrl] = useState('');\n const [scheduledAt, setScheduledAt] = useState('');\n const [durationMin, setDurationMin] = useState('60');\n const [error, setError] = useState<string | null>(null);\n\n const programOptions = programs.data?.items ?? [];\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!coachId.trim()) {\n setError('Coach ID is required.');\n return;\n }\n if (!athleteId.trim()) {\n setError('Athlete ID is required.');\n return;\n }\n if (!title.trim()) {\n setError('A session title is required.');\n return;\n }\n if (!scheduledAt) {\n setError('A scheduled date and time is required.');\n return;\n }\n const duration = Number.parseInt(durationMin, 10);\n if (Number.isNaN(duration) || duration <= 0) {\n setError('Duration must be a positive number of minutes.');\n return;\n }\n\n try {\n const created = await bookSession.mutateAsync({\n coachId: coachId.trim(),\n athleteId: athleteId.trim(),\n programId: programId || undefined,\n title: title.trim(),\n type,\n location: type !== 'ONLINE' ? location.trim() || undefined : undefined,\n meetingUrl: type !== 'IN_PERSON' ? meetingUrl.trim() || undefined : undefined,\n scheduledAt: new Date(scheduledAt).toISOString(),\n durationMin: duration,\n });\n navigateTo(`/sessions/${created.id}`);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to book session.');\n }\n };\n\n return (\n <PageLayout\n title=\"Book session\"\n description=\"Schedule a coaching session between a coach and an athlete.\"\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\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"max-w-2xl space-y-5\">\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Coach ID\" htmlFor=\"coachId\" required hint=\"Defaults to you.\">\n <input\n id=\"coachId\"\n className={fieldInputClass}\n value={coachId}\n onChange={(e) => setCoachId(e.target.value)}\n placeholder=\"coach-uuid\"\n />\n </FormField>\n <FormField label=\"Athlete ID\" htmlFor=\"athleteId\" required>\n <input\n id=\"athleteId\"\n className={fieldInputClass}\n value={athleteId}\n onChange={(e) => setAthleteId(e.target.value)}\n placeholder=\"athlete-uuid\"\n />\n </FormField>\n </div>\n\n <FormField label=\"Title\" htmlFor=\"title\" required>\n <input\n id=\"title\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Sprint technique review\"\n />\n </FormField>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Type\" htmlFor=\"type\">\n <select\n id=\"type\"\n className={fieldInputClass}\n value={type}\n onChange={(e) => setType(e.target.value as AthSessionType)}\n >\n {TYPES.map((tp) => (\n <option key={tp} value={tp}>\n {SESSION_TYPE_LABELS[tp]}\n </option>\n ))}\n </select>\n </FormField>\n\n <FormField label=\"Program\" htmlFor=\"program\" hint=\"Optional — tie to a published plan.\">\n <select\n id=\"program\"\n className={fieldInputClass}\n value={programId}\n onChange={(e) => setProgramId(e.target.value)}\n >\n <option value=\"\">No program</option>\n {programOptions.map((pr) => (\n <option key={pr.id} value={pr.id}>\n {pr.name}\n </option>\n ))}\n </select>\n </FormField>\n </div>\n\n {type !== 'ONLINE' && (\n <FormField label=\"Location\" htmlFor=\"location\">\n <input\n id=\"location\"\n className={fieldInputClass}\n value={location}\n onChange={(e) => setLocation(e.target.value)}\n placeholder=\"e.g. Main gym, Court 2\"\n />\n </FormField>\n )}\n\n {type !== 'IN_PERSON' && (\n <FormField label=\"Meeting URL\" htmlFor=\"meetingUrl\">\n <input\n id=\"meetingUrl\"\n className={fieldInputClass}\n value={meetingUrl}\n onChange={(e) => setMeetingUrl(e.target.value)}\n placeholder=\"https://meet.example.com/…\"\n />\n </FormField>\n )}\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Scheduled at\" htmlFor=\"scheduledAt\" required>\n <input\n id=\"scheduledAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={scheduledAt}\n onChange={(e) => setScheduledAt(e.target.value)}\n />\n </FormField>\n <FormField label=\"Duration (minutes)\" htmlFor=\"duration\">\n <input\n id=\"duration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </FormField>\n </div>\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={bookSession.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 {bookSession.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Book session\n </button>\n </div>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SessionFormPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/sessions/SessionFormPage.tsx"],"sourcesContent":["/**\n * Book Session Form\n * @module atheletebridge/pages/sessions\n */\n\nimport { useState } from 'react';\nimport { ArrowLeft, Loader2, Save } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { AthleteSelect } from '../../components/AthleteSelect';\nimport { SESSION_TYPE_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useBookSession, usePrograms } from '../../hooks/useAthleteBridgeApi';\nimport type { AthSessionType } from '../../types';\n\nconst TYPES = Object.keys(SESSION_TYPE_LABELS) as AthSessionType[];\n\nexport function SessionFormPage() {\n const { navigateTo, currentUser } = useAthleteBridge();\n\n const programs = usePrograms({ status: 'PUBLISHED', pagination: { limit: 100 } });\n const bookSession = useBookSession();\n\n const [coachId, setCoachId] = useState(currentUser?.id ?? '');\n const [athleteId, setAthleteId] = useState('');\n const [programId, setProgramId] = useState('');\n const [title, setTitle] = useState('');\n const [type, setType] = useState<AthSessionType>('IN_PERSON');\n const [location, setLocation] = useState('');\n const [meetingUrl, setMeetingUrl] = useState('');\n const [scheduledAt, setScheduledAt] = useState('');\n const [durationMin, setDurationMin] = useState('60');\n const [error, setError] = useState<string | null>(null);\n\n const programOptions = programs.data?.items ?? [];\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!coachId.trim()) {\n setError('Coach ID is required.');\n return;\n }\n if (!athleteId.trim()) {\n setError('Athlete ID is required.');\n return;\n }\n if (!title.trim()) {\n setError('A session title is required.');\n return;\n }\n if (!scheduledAt) {\n setError('A scheduled date and time is required.');\n return;\n }\n const duration = Number.parseInt(durationMin, 10);\n if (Number.isNaN(duration) || duration <= 0) {\n setError('Duration must be a positive number of minutes.');\n return;\n }\n\n try {\n const created = await bookSession.mutateAsync({\n coachId: coachId.trim(),\n athleteId: athleteId.trim(),\n programId: programId || undefined,\n title: title.trim(),\n type,\n location: type !== 'ONLINE' ? location.trim() || undefined : undefined,\n meetingUrl: type !== 'IN_PERSON' ? meetingUrl.trim() || undefined : undefined,\n scheduledAt: new Date(scheduledAt).toISOString(),\n durationMin: duration,\n });\n navigateTo(`/sessions/${created.id}`);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to book session.');\n }\n };\n\n return (\n <PageLayout\n title=\"Book session\"\n description=\"Schedule a coaching session between a coach and an athlete.\"\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\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"max-w-2xl space-y-5\">\n <FormField label=\"Coach ID\" htmlFor=\"coachId\" required hint=\"Defaults to you.\">\n <input\n id=\"coachId\"\n className={fieldInputClass}\n value={coachId}\n onChange={(e) => setCoachId(e.target.value)}\n placeholder=\"coach-uuid\"\n />\n </FormField>\n\n <AthleteSelect\n athleteId={athleteId}\n onSelect={({ athleteId: id }) => setAthleteId(id)}\n required\n idPrefix=\"session-athlete\"\n />\n\n <FormField label=\"Title\" htmlFor=\"title\" required>\n <input\n id=\"title\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Sprint technique review\"\n />\n </FormField>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Type\" htmlFor=\"type\">\n <select\n id=\"type\"\n className={fieldInputClass}\n value={type}\n onChange={(e) => setType(e.target.value as AthSessionType)}\n >\n {TYPES.map((tp) => (\n <option key={tp} value={tp}>\n {SESSION_TYPE_LABELS[tp]}\n </option>\n ))}\n </select>\n </FormField>\n\n <FormField label=\"Program\" htmlFor=\"program\" hint=\"Optional — tie to a published plan.\">\n <select\n id=\"program\"\n className={fieldInputClass}\n value={programId}\n onChange={(e) => setProgramId(e.target.value)}\n >\n <option value=\"\">No program</option>\n {programOptions.map((pr) => (\n <option key={pr.id} value={pr.id}>\n {pr.name}\n </option>\n ))}\n </select>\n </FormField>\n </div>\n\n {type !== 'ONLINE' && (\n <FormField label=\"Location\" htmlFor=\"location\">\n <input\n id=\"location\"\n className={fieldInputClass}\n value={location}\n onChange={(e) => setLocation(e.target.value)}\n placeholder=\"e.g. Main gym, Court 2\"\n />\n </FormField>\n )}\n\n {type !== 'IN_PERSON' && (\n <FormField label=\"Meeting URL\" htmlFor=\"meetingUrl\">\n <input\n id=\"meetingUrl\"\n className={fieldInputClass}\n value={meetingUrl}\n onChange={(e) => setMeetingUrl(e.target.value)}\n placeholder=\"https://meet.example.com/…\"\n />\n </FormField>\n )}\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Scheduled at\" htmlFor=\"scheduledAt\" required>\n <input\n id=\"scheduledAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={scheduledAt}\n onChange={(e) => setScheduledAt(e.target.value)}\n />\n </FormField>\n <FormField label=\"Duration (minutes)\" htmlFor=\"duration\">\n <input\n id=\"duration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </FormField>\n </div>\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={bookSession.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 {bookSession.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Book session\n </button>\n </div>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|