@burdenoff/microfe-atheletebridge 2026.829.1 → 2026.829.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.
- package/dist/atheletebridge/components/LoadingState.d.ts.map +1 -1
- package/dist/atheletebridge/components/LoadingState.js.map +1 -1
- package/dist/atheletebridge/components/TeamCard.d.ts.map +1 -1
- package/dist/atheletebridge/components/TeamCard.js.map +1 -1
- package/dist/atheletebridge/pages/DashboardPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.js.map +1 -1
- package/dist/atheletebridge/pages/onboarding/OnboardingPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/onboarding/OnboardingPage.js.map +1 -1
- package/dist/atheletebridge/pages/performance/PerformancePage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionsListPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamFormPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamFormPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OnboardingPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/onboarding/OnboardingPage.tsx"],"sourcesContent":["/**\n * First-run onboarding wizard\n * @module atheletebridge/pages/onboarding\n *\n * The AthleteBridge first-run flow: three steps — your sport → create your team\n * → your role — landing the user on a usable team page. Design reference is\n * mockup 26 (`atheletebridge-specs/product/mockups`), realised entirely with\n * fe-libs semantic tokens (the mockup's orange→red brand ramp maps to\n * `--color-action-primary-bg` blended toward `--color-accent-purple`, exactly as\n * the dashboard hero CTA does; the mockup's teal-green \"complete\" maps to\n * `--color-accent-teal`).\n *\n * Scope is deliberately narrow — sport, team, role. Workspace/organization\n * creation is NOT part of this flow: `global-tenant-svc` owns it and it already\n * precedes product entry, so duplicating it here would fork a platform module\n * (see `workspaces-specs/business/strategy/module-ownership`).\n *\n * Data layer (all real, nothing mocked):\n * - `useCreateTeam` → `athCreateTeam` — creates the squad from step 2.\n * - `useAddMember` → `athAddMember` — puts the calling user on that squad's\n * roster with the role picked in step 3. Best-effort: the team is the primary\n * outcome and is already persisted, so a roster hiccup must not strand the\n * user mid-flow.\n *\n * Unlike the entity pages this is a calm, centered flow rather than the admin\n * `PageLayout` header, and it renders no product wordmark — the app shell chrome\n * already carries AthleteBridge branding above this surface.\n */\n\nimport { useCallback, useMemo, useState, type FC, type FormEvent } from 'react';\nimport { ArrowLeft, ArrowRight, Check, Loader2, Users } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { FormField, fieldInputClass } from '../../components/form';\nimport {\n MEMBER_ROLE_OPTIONS,\n SPORT_ICONS,\n SPORT_OPTIONS,\n} from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useAddMember, useCreateTeam } from '../../hooks/useAthleteBridgeApi';\nimport type { AthMemberRole, AthSport } from '../../types';\nimport { cn } from '../../utils/cn';\nimport { useAbTranslate, type TranslateFn } from '../../i18n';\n\n// ============================================================================\n// Static option catalogues\n// ============================================================================\n\n/**\n * `AthTeam.ageGroup` is a free-text column on the subgraph; these are the\n * conventional squad bands offered as a shortcut. \"Senior\" and \"Masters\" cover\n * non-age-banded squads, and the empty value leaves the column unset.\n */\nfunction useAgeGroups(tr: TranslateFn): ReadonlyArray<{ value: string; label: string }> {\n return [\n { value: '', label: tr('atheletebridge.onboarding.ageGroup.none', 'Not age-banded') },\n { value: 'U8', label: 'U8' },\n { value: 'U10', label: 'U10' },\n { value: 'U12', label: 'U12' },\n { value: 'U14', label: 'U14' },\n { value: 'U16', label: 'U16' },\n { value: 'U18', label: 'U18' },\n { value: 'U21', label: 'U21' },\n { value: 'SENIOR', label: tr('atheletebridge.onboarding.ageGroup.senior', 'Senior') },\n { value: 'MASTERS', label: tr('atheletebridge.onboarding.ageGroup.masters', 'Masters') },\n ];\n}\n\n/**\n * `AthTeam.season` is also free text. Options are derived from the current date\n * rather than hard-coded, so the list never goes stale: the calendar year, the\n * split-year season either side of it, and the previous calendar year.\n */\nfunction buildSeasonOptions(now: Date, tr: TranslateFn): ReadonlyArray<{ value: string; label: string }> {\n const year = now.getFullYear();\n const short = (y: number): string => String(y % 100).padStart(2, '0');\n return [\n { value: '', label: tr('atheletebridge.onboarding.season.none', 'No season') },\n { value: String(year), label: String(year) },\n { value: `${year}/${short(year + 1)}`, label: `${year}/${short(year + 1)}` },\n { value: `${year - 1}/${short(year)}`, label: `${year - 1}/${short(year)}` },\n { value: String(year - 1), label: String(year - 1) },\n ];\n}\n\nconst TOTAL_STEPS = 3;\nconst TEAM_NAME_LIMIT = 120;\n\ntype StepNumber = 1 | 2 | 3;\n\nfunction stepLabel(step: StepNumber, tr: TranslateFn): string {\n switch (step) {\n case 1:\n return tr('atheletebridge.onboarding.stepLabel.sport', 'Your sport');\n case 2:\n return tr('atheletebridge.onboarding.stepLabel.team', 'Create your team');\n case 3:\n return tr('atheletebridge.onboarding.stepLabel.role', 'Your role');\n }\n}\n\n// ============================================================================\n// Shared token-only styling\n// ============================================================================\n\n/**\n * Brand gradient — the same orange→purple ramp the dashboard hero CTA uses, so\n * the wizard's active step and primary action read as one brand with the rest of\n * the product. Token-only (no raw colors).\n */\nconst BRAND_GRADIENT =\n 'bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\nconst PRIMARY_BUTTON = cn(\n 'inline-flex items-center justify-center gap-1.5 rounded-lg px-5 py-2.5 text-sm font-semibold',\n 'text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow]',\n 'hover:shadow-elevation-2 hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-60',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n BRAND_GRADIENT\n);\n\nconst SECONDARY_BUTTON = cn(\n 'inline-flex items-center justify-center gap-1.5 rounded-lg border border-border-seam px-5 py-2.5',\n 'text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n);\n\nconst GHOST_BUTTON = cn(\n 'rounded-lg px-4 py-2.5 text-sm font-medium text-text-secondary transition-colors',\n 'hover:text-text-primary focus-visible:outline-none focus-visible:ring-2',\n 'focus-visible:ring-[var(--color-focus-ring)]'\n);\n\nconst CHIP_BASE = cn(\n 'inline-flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-medium transition-all',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n 'motion-safe:hover:-translate-y-0.5'\n);\n\nconst CHIP_IDLE =\n 'border-border-seam bg-bg-surface text-text-secondary hover:border-border-strong hover:text-text-primary';\n\nconst CHIP_ACTIVE = cn(\n 'border-transparent text-action-primary-text shadow-elevation-1',\n BRAND_GRADIENT\n);\n\n// ============================================================================\n// Stepper\n// ============================================================================\n\ninterface StepperProps {\n current: StepNumber;\n}\n\n/**\n * The mockup's three-node stepper: teal-green + tick for a completed step, brand\n * gradient for the active step, hollow outline for an upcoming one. The nodes are\n * decorative — the accessible state is carried by the `progressbar` wrapper and\n * the visible step caption below the card.\n */\nconst Stepper: FC<StepperProps & { stepLabel: (step: StepNumber) => string; progressLabel: string }> = ({\n current,\n stepLabel,\n progressLabel,\n}) => (\n <div\n className=\"flex items-start justify-center\"\n role=\"progressbar\"\n aria-valuemin={1}\n aria-valuemax={TOTAL_STEPS}\n aria-valuenow={current}\n aria-valuetext={`${progressLabel} ${current} / ${TOTAL_STEPS}: ${stepLabel(current)}`}\n aria-label={progressLabel}\n >\n {([1, 2, 3] as StepNumber[]).map((step, index) => {\n const done = step < current;\n const active = step === current;\n return (\n <div key={step} className=\"flex items-start\" aria-hidden=\"true\">\n {index > 0 && (\n <span\n className={cn(\n 'mt-6 h-px w-10 shrink-0 transition-colors sm:w-20',\n step <= current ? 'bg-[var(--color-accent-teal)]' : 'bg-border-seam'\n )}\n />\n )}\n <div className=\"flex w-24 flex-col items-center gap-2 sm:w-32\">\n <span\n className={cn(\n 'flex h-12 w-12 items-center justify-center rounded-full text-base font-semibold transition-all',\n done && 'bg-[var(--color-accent-teal)] text-[var(--color-bg-canvas)]',\n active && cn('text-action-primary-text shadow-elevation-2', BRAND_GRADIENT),\n !done && !active && 'border border-border-strong text-text-muted'\n )}\n >\n {done ? <Check className=\"h-5 w-5\" /> : step}\n </span>\n <span\n className={cn(\n 'text-center text-sm',\n active ? 'font-semibold text-text-primary' : 'text-text-secondary'\n )}\n >\n {stepLabel(step)}\n </span>\n </div>\n </div>\n );\n })}\n </div>\n);\n\n// ============================================================================\n// Sport chips\n// ============================================================================\n\ninterface SportChipsProps {\n value: AthSport | null;\n onSelect: (sport: AthSport) => void;\n size: 'lg' | 'sm';\n getLabel: (sport: AthSport) => string;\n groupLabel: string;\n}\n\nconst SportChips: FC<SportChipsProps> = ({ value, onSelect, size, getLabel, groupLabel }) => (\n <div className=\"flex flex-wrap gap-2\" role=\"group\" aria-label={groupLabel}>\n {SPORT_OPTIONS.map((sport) => {\n const Icon = SPORT_ICONS[sport];\n const active = value === sport;\n return (\n <button\n key={sport}\n type=\"button\"\n onClick={() => onSelect(sport)}\n aria-pressed={active}\n className={cn(\n CHIP_BASE,\n size === 'lg' && 'px-5 py-2.5 text-base',\n active ? CHIP_ACTIVE : CHIP_IDLE\n )}\n >\n <Icon className={size === 'lg' ? 'h-5 w-5' : 'h-4 w-4'} aria-hidden=\"true\" />\n {getLabel(sport)}\n </button>\n );\n })}\n </div>\n);\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport const OnboardingPage: FC = () => {\n const tr = useAbTranslate();\n const { navigateTo, currentUser } = useAthleteBridge();\n\n const createTeam = useCreateTeam();\n const addMember = useAddMember();\n\n const [step, setStep] = useState<StepNumber>(1);\n const [sport, setSport] = useState<AthSport | null>(null);\n const [teamName, setTeamName] = useState('');\n const [ageGroup, setAgeGroup] = useState('');\n const [season, setSeason] = useState('');\n const [role, setRole] = useState<AthMemberRole | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const ageGroups = useAgeGroups(tr);\n const seasonOptions = useMemo(() => buildSeasonOptions(new Date(), tr), [tr]);\n\n const sportLabel = useCallback(\n (s: AthSport): string => {\n switch (s) {\n case 'ATHLETICS':\n return tr('atheletebridge.sport.athletics', 'Athletics');\n case 'BASKETBALL':\n return tr('atheletebridge.sport.basketball', 'Basketball');\n case 'CRICKET':\n return tr('atheletebridge.sport.cricket', 'Cricket');\n case 'ESPORTS':\n return tr('atheletebridge.sport.esports', 'Esports');\n case 'FOOTBALL':\n return tr('atheletebridge.sport.football', 'Football');\n case 'OTHER':\n return tr('atheletebridge.sport.other', 'Other');\n case 'SWIMMING':\n return tr('atheletebridge.sport.swimming', 'Swimming');\n case 'TENNIS':\n return tr('atheletebridge.sport.tennis', 'Tennis');\n }\n },\n [tr]\n );\n\n const roleLabel = useCallback(\n (r: AthMemberRole): string => {\n switch (r) {\n case 'PLAYER':\n return tr('atheletebridge.memberRole.player', 'Player');\n case 'CAPTAIN':\n return tr('atheletebridge.memberRole.captain', 'Captain');\n case 'COACH':\n return tr('atheletebridge.memberRole.coach', 'Coach');\n case 'ASSISTANT_COACH':\n return tr('atheletebridge.memberRole.assistantCoach', 'Assistant Coach');\n case 'PHYSIO':\n return tr('atheletebridge.memberRole.physio', 'Physio');\n case 'MANAGER':\n return tr('atheletebridge.memberRole.manager', 'Manager');\n }\n },\n [tr]\n );\n\n const busy = createTeam.isPending || addMember.isPending;\n\n const goDashboard = useCallback(() => navigateTo('/'), [navigateTo]);\n\n const handleBack = useCallback(() => {\n setError(null);\n setStep((prev) => (prev === 1 ? 1 : ((prev - 1) as StepNumber)));\n }, []);\n\n const handleSelectSport = useCallback((next: AthSport) => {\n setSport(next);\n setError(null);\n }, []);\n\n /**\n * Step 1/2 advance. Step 3 submits, so this only guards the two picker steps.\n */\n const handleContinue = useCallback(() => {\n if (step === 1) {\n if (!sport) {\n setError(tr('atheletebridge.pages.onboarding.errorSport', 'Pick a sport to continue.'));\n return;\n }\n setError(null);\n setStep(2);\n return;\n }\n if (!teamName.trim()) {\n setError(\n tr('atheletebridge.pages.onboarding.errorTeamName', 'Give your team a name to continue.')\n );\n return;\n }\n setError(null);\n setStep(3);\n }, [step, sport, teamName, tr]);\n\n /**\n * Final submit — creates the team, then (best-effort) puts the calling user on\n * its roster with the chosen role, and lands them on the new team page.\n */\n const handleFinish = useCallback(\n async (event: FormEvent) => {\n event.preventDefault();\n if (!sport || !role || !teamName.trim()) {\n setError(tr('atheletebridge.pages.onboarding.errorRole', 'Pick your role to finish.'));\n return;\n }\n setError(null);\n\n try {\n const team = await createTeam.mutateAsync({\n name: teamName.trim(),\n sport,\n status: 'FORMING',\n ageGroup: ageGroup || undefined,\n season: season || undefined,\n });\n\n if (currentUser?.id) {\n const fullName = [currentUser.firstName, currentUser.lastName]\n .filter((part): part is string => Boolean(part))\n .join(' ')\n .trim();\n const displayName = currentUser.name ?? (fullName || currentUser.email);\n try {\n await addMember.mutateAsync({\n teamId: team.id,\n userId: currentUser.id,\n displayName,\n role,\n status: 'ACTIVE',\n });\n } catch {\n // The team is created and is the primary outcome — a roster failure\n // must not strand the user in the wizard. They can add themselves\n // from the team page.\n }\n }\n\n navigateTo(`/teams/${team.id}`);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr(\n 'atheletebridge.pages.onboarding.errorCreate',\n \"We couldn't create your team. Please try again.\"\n )\n );\n }\n },\n [sport, role, teamName, ageGroup, season, currentUser, createTeam, addMember, navigateTo, tr]\n );\n\n const heading =\n step === 1\n ? tr('atheletebridge.pages.onboarding.step1Title', 'What do you coach?')\n : step === 2\n ? tr('atheletebridge.pages.onboarding.step2Title', 'Create your first team')\n : tr('atheletebridge.pages.onboarding.step3Title', \"What's your role?\");\n\n const subheading =\n step === 1\n ? tr(\n 'atheletebridge.pages.onboarding.step1Subtitle',\n 'Your sport shapes the squads, fixtures and training you set up next.'\n )\n : step === 2\n ? tr(\n 'atheletebridge.pages.onboarding.step2Subtitle',\n 'You can add players and fixtures once your team exists.'\n )\n : tr(\n 'atheletebridge.pages.onboarding.step3Subtitle',\n 'We add you to this squad so the roster starts with you.'\n );\n\n return (\n <div className=\"min-h-full bg-bg-canvas px-4 py-10 sm:px-6 sm:py-14\">\n <div className=\"mx-auto flex w-full max-w-2xl flex-col gap-8\">\n <Stepper\n current={step}\n stepLabel={(s) => stepLabel(s, tr)}\n progressLabel={tr('atheletebridge.pages.onboarding.progressLabel', 'Onboarding progress')}\n />\n\n <header className=\"text-center\">\n <h1 className=\"text-3xl font-bold tracking-tight text-text-primary sm:text-4xl\">\n {heading}\n </h1>\n <p className=\"mx-auto mt-2 max-w-lg text-sm text-text-secondary sm:text-base\">\n {subheading}\n </p>\n </header>\n\n <form onSubmit={handleFinish} noValidate>\n <GlassCard treatment=\"insight\" className=\"p-6 sm:p-8\">\n <div className=\"space-y-6\">\n {step === 1 && (\n <SportChips\n value={sport}\n onSelect={handleSelectSport}\n size=\"lg\"\n getLabel={sportLabel}\n groupLabel={tr('atheletebridge.pages.onboarding.sport', 'Sport')}\n />\n )}\n\n {step === 2 && (\n <>\n <FormField\n label={tr('atheletebridge.pages.onboarding.teamName', 'Team name')}\n htmlFor=\"onboarding-team-name\"\n required\n >\n <input\n id=\"onboarding-team-name\"\n className={fieldInputClass}\n value={teamName}\n maxLength={TEAM_NAME_LIMIT}\n onChange={(e) => {\n setTeamName(e.target.value);\n setError(null);\n }}\n placeholder={tr('atheletebridge.pages.onboarding.teamNamePlaceholder', 'e.g. Riverside United U16')}\n />\n </FormField>\n\n <FormField label={tr('atheletebridge.pages.onboarding.sport', 'Sport')}>\n <SportChips\n value={sport}\n onSelect={handleSelectSport}\n size=\"sm\"\n getLabel={sportLabel}\n groupLabel={tr('atheletebridge.pages.onboarding.sport', 'Sport')}\n />\n </FormField>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField\n label={tr('atheletebridge.pages.onboarding.ageGroup', 'Age group')}\n htmlFor=\"onboarding-age-group\"\n >\n <select\n id=\"onboarding-age-group\"\n className={fieldInputClass}\n value={ageGroup}\n onChange={(e) => setAgeGroup(e.target.value)}\n >\n {ageGroups.map((option) => (\n <option key={option.value || 'none'} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </FormField>\n\n <FormField\n label={tr('atheletebridge.pages.onboarding.season', 'Season')}\n htmlFor=\"onboarding-season\"\n >\n <select\n id=\"onboarding-season\"\n className={fieldInputClass}\n value={season}\n onChange={(e) => setSeason(e.target.value)}\n >\n {seasonOptions.map((option) => (\n <option key={option.value || 'none'} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </FormField>\n </div>\n </>\n )}\n\n {step === 3 && (\n <div\n className=\"flex flex-wrap gap-2\"\n role=\"group\"\n aria-label={tr('atheletebridge.pages.onboarding.yourRole', 'Your role')}\n >\n {MEMBER_ROLE_OPTIONS.map((option) => {\n const active = role === option;\n return (\n <button\n key={option}\n type=\"button\"\n onClick={() => {\n setRole(option);\n setError(null);\n }}\n aria-pressed={active}\n className={cn(\n CHIP_BASE,\n 'px-5 py-2.5 text-base',\n active ? CHIP_ACTIVE : CHIP_IDLE\n )}\n >\n <Users className=\"h-5 w-5\" aria-hidden=\"true\" />\n {roleLabel(option)}\n </button>\n );\n })}\n </div>\n )}\n\n {error && (\n <p role=\"alert\" className=\"text-sm text-status-error-text\">\n {error}\n </p>\n )}\n </div>\n\n <div className=\"mt-8 flex flex-wrap items-center justify-between gap-3 border-t border-border-seam pt-6\">\n <button\n type=\"button\"\n onClick={step === 1 ? goDashboard : handleBack}\n className={SECONDARY_BUTTON}\n disabled={busy}\n >\n <ArrowLeft className=\"h-4 w-4\" aria-hidden=\"true\" />\n {step === 1\n ? tr('atheletebridge.pages.onboarding.exit', 'Exit')\n : tr('atheletebridge.pages.onboarding.back', 'Back')}\n </button>\n\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={goDashboard}\n className={GHOST_BUTTON}\n disabled={busy}\n >\n {tr('atheletebridge.pages.onboarding.skip', 'Skip for now')}\n </button>\n\n {step === 3 ? (\n <button type=\"submit\" className={PRIMARY_BUTTON} disabled={busy || !role}>\n {busy ? (\n <>\n <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n {tr('atheletebridge.pages.onboarding.creating', 'Creating…')}\n </>\n ) : (\n <>\n {tr('atheletebridge.pages.onboarding.finish', 'Create team')}\n <ArrowRight className=\"h-4 w-4\" aria-hidden=\"true\" />\n </>\n )}\n </button>\n ) : (\n <button type=\"button\" onClick={handleContinue} className={PRIMARY_BUTTON}>\n {tr('atheletebridge.pages.onboarding.continue', 'Continue')}\n <ArrowRight className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n )}\n </div>\n </div>\n </GlassCard>\n </form>\n\n <p className=\"text-center text-sm text-text-secondary\">\n {tr('atheletebridge.pages.onboarding.footer', 'Step')} {step}{' '}\n {tr('atheletebridge.pages.onboarding.footerOf', 'of')} {TOTAL_STEPS} —{' '}\n {tr('atheletebridge.pages.onboarding.footerHint', 'takes about a minute.')}\n </p>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OnboardingPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/onboarding/OnboardingPage.tsx"],"sourcesContent":["/**\n * First-run onboarding wizard\n * @module atheletebridge/pages/onboarding\n *\n * The AthleteBridge first-run flow: three steps — your sport → create your team\n * → your role — landing the user on a usable team page. Design reference is\n * mockup 26 (`atheletebridge-specs/product/mockups`), realised entirely with\n * fe-libs semantic tokens (the mockup's orange→red brand ramp maps to\n * `--color-action-primary-bg` blended toward `--color-accent-purple`, exactly as\n * the dashboard hero CTA does; the mockup's teal-green \"complete\" maps to\n * `--color-accent-teal`).\n *\n * Scope is deliberately narrow — sport, team, role. Workspace/organization\n * creation is NOT part of this flow: `global-tenant-svc` owns it and it already\n * precedes product entry, so duplicating it here would fork a platform module\n * (see `workspaces-specs/business/strategy/module-ownership`).\n *\n * Data layer (all real, nothing mocked):\n * - `useCreateTeam` → `athCreateTeam` — creates the squad from step 2.\n * - `useAddMember` → `athAddMember` — puts the calling user on that squad's\n * roster with the role picked in step 3. Best-effort: the team is the primary\n * outcome and is already persisted, so a roster hiccup must not strand the\n * user mid-flow.\n *\n * Unlike the entity pages this is a calm, centered flow rather than the admin\n * `PageLayout` header, and it renders no product wordmark — the app shell chrome\n * already carries AthleteBridge branding above this surface.\n */\n\nimport { useCallback, useMemo, useState, type FC, type FormEvent } from 'react';\nimport { ArrowLeft, ArrowRight, Check, Loader2, Users } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { MEMBER_ROLE_OPTIONS, SPORT_ICONS, SPORT_OPTIONS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useAddMember, useCreateTeam } from '../../hooks/useAthleteBridgeApi';\nimport type { AthMemberRole, AthSport } from '../../types';\nimport { cn } from '../../utils/cn';\nimport { useAbTranslate, type TranslateFn } from '../../i18n';\n\n// ============================================================================\n// Static option catalogues\n// ============================================================================\n\n/**\n * `AthTeam.ageGroup` is a free-text column on the subgraph; these are the\n * conventional squad bands offered as a shortcut. \"Senior\" and \"Masters\" cover\n * non-age-banded squads, and the empty value leaves the column unset.\n */\nfunction useAgeGroups(tr: TranslateFn): ReadonlyArray<{ value: string; label: string }> {\n return [\n { value: '', label: tr('atheletebridge.onboarding.ageGroup.none', 'Not age-banded') },\n { value: 'U8', label: 'U8' },\n { value: 'U10', label: 'U10' },\n { value: 'U12', label: 'U12' },\n { value: 'U14', label: 'U14' },\n { value: 'U16', label: 'U16' },\n { value: 'U18', label: 'U18' },\n { value: 'U21', label: 'U21' },\n { value: 'SENIOR', label: tr('atheletebridge.onboarding.ageGroup.senior', 'Senior') },\n { value: 'MASTERS', label: tr('atheletebridge.onboarding.ageGroup.masters', 'Masters') },\n ];\n}\n\n/**\n * `AthTeam.season` is also free text. Options are derived from the current date\n * rather than hard-coded, so the list never goes stale: the calendar year, the\n * split-year season either side of it, and the previous calendar year.\n */\nfunction buildSeasonOptions(\n now: Date,\n tr: TranslateFn\n): ReadonlyArray<{ value: string; label: string }> {\n const year = now.getFullYear();\n const short = (y: number): string => String(y % 100).padStart(2, '0');\n return [\n { value: '', label: tr('atheletebridge.onboarding.season.none', 'No season') },\n { value: String(year), label: String(year) },\n { value: `${year}/${short(year + 1)}`, label: `${year}/${short(year + 1)}` },\n { value: `${year - 1}/${short(year)}`, label: `${year - 1}/${short(year)}` },\n { value: String(year - 1), label: String(year - 1) },\n ];\n}\n\nconst TOTAL_STEPS = 3;\nconst TEAM_NAME_LIMIT = 120;\n\ntype StepNumber = 1 | 2 | 3;\n\nfunction stepLabel(step: StepNumber, tr: TranslateFn): string {\n switch (step) {\n case 1:\n return tr('atheletebridge.onboarding.stepLabel.sport', 'Your sport');\n case 2:\n return tr('atheletebridge.onboarding.stepLabel.team', 'Create your team');\n case 3:\n return tr('atheletebridge.onboarding.stepLabel.role', 'Your role');\n }\n}\n\n// ============================================================================\n// Shared token-only styling\n// ============================================================================\n\n/**\n * Brand gradient — the same orange→purple ramp the dashboard hero CTA uses, so\n * the wizard's active step and primary action read as one brand with the rest of\n * the product. Token-only (no raw colors).\n */\nconst BRAND_GRADIENT =\n 'bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\nconst PRIMARY_BUTTON = cn(\n 'inline-flex items-center justify-center gap-1.5 rounded-lg px-5 py-2.5 text-sm font-semibold',\n 'text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow]',\n 'hover:shadow-elevation-2 hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-60',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n BRAND_GRADIENT\n);\n\nconst SECONDARY_BUTTON = cn(\n 'inline-flex items-center justify-center gap-1.5 rounded-lg border border-border-seam px-5 py-2.5',\n 'text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n);\n\nconst GHOST_BUTTON = cn(\n 'rounded-lg px-4 py-2.5 text-sm font-medium text-text-secondary transition-colors',\n 'hover:text-text-primary focus-visible:outline-none focus-visible:ring-2',\n 'focus-visible:ring-[var(--color-focus-ring)]'\n);\n\nconst CHIP_BASE = cn(\n 'inline-flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-medium transition-all',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n 'motion-safe:hover:-translate-y-0.5'\n);\n\nconst CHIP_IDLE =\n 'border-border-seam bg-bg-surface text-text-secondary hover:border-border-strong hover:text-text-primary';\n\nconst CHIP_ACTIVE = cn(\n 'border-transparent text-action-primary-text shadow-elevation-1',\n BRAND_GRADIENT\n);\n\n// ============================================================================\n// Stepper\n// ============================================================================\n\ninterface StepperProps {\n current: StepNumber;\n}\n\n/**\n * The mockup's three-node stepper: teal-green + tick for a completed step, brand\n * gradient for the active step, hollow outline for an upcoming one. The nodes are\n * decorative — the accessible state is carried by the `progressbar` wrapper and\n * the visible step caption below the card.\n */\nconst Stepper: FC<\n StepperProps & { stepLabel: (step: StepNumber) => string; progressLabel: string }\n> = ({ current, stepLabel, progressLabel }) => (\n <div\n className=\"flex items-start justify-center\"\n role=\"progressbar\"\n aria-valuemin={1}\n aria-valuemax={TOTAL_STEPS}\n aria-valuenow={current}\n aria-valuetext={`${progressLabel} ${current} / ${TOTAL_STEPS}: ${stepLabel(current)}`}\n aria-label={progressLabel}\n >\n {([1, 2, 3] as StepNumber[]).map((step, index) => {\n const done = step < current;\n const active = step === current;\n return (\n <div key={step} className=\"flex items-start\" aria-hidden=\"true\">\n {index > 0 && (\n <span\n className={cn(\n 'mt-6 h-px w-10 shrink-0 transition-colors sm:w-20',\n step <= current ? 'bg-[var(--color-accent-teal)]' : 'bg-border-seam'\n )}\n />\n )}\n <div className=\"flex w-24 flex-col items-center gap-2 sm:w-32\">\n <span\n className={cn(\n 'flex h-12 w-12 items-center justify-center rounded-full text-base font-semibold transition-all',\n done && 'bg-[var(--color-accent-teal)] text-[var(--color-bg-canvas)]',\n active && cn('text-action-primary-text shadow-elevation-2', BRAND_GRADIENT),\n !done && !active && 'border border-border-strong text-text-muted'\n )}\n >\n {done ? <Check className=\"h-5 w-5\" /> : step}\n </span>\n <span\n className={cn(\n 'text-center text-sm',\n active ? 'font-semibold text-text-primary' : 'text-text-secondary'\n )}\n >\n {stepLabel(step)}\n </span>\n </div>\n </div>\n );\n })}\n </div>\n);\n\n// ============================================================================\n// Sport chips\n// ============================================================================\n\ninterface SportChipsProps {\n value: AthSport | null;\n onSelect: (sport: AthSport) => void;\n size: 'lg' | 'sm';\n getLabel: (sport: AthSport) => string;\n groupLabel: string;\n}\n\nconst SportChips: FC<SportChipsProps> = ({ value, onSelect, size, getLabel, groupLabel }) => (\n <div className=\"flex flex-wrap gap-2\" role=\"group\" aria-label={groupLabel}>\n {SPORT_OPTIONS.map((sport) => {\n const Icon = SPORT_ICONS[sport];\n const active = value === sport;\n return (\n <button\n key={sport}\n type=\"button\"\n onClick={() => onSelect(sport)}\n aria-pressed={active}\n className={cn(\n CHIP_BASE,\n size === 'lg' && 'px-5 py-2.5 text-base',\n active ? CHIP_ACTIVE : CHIP_IDLE\n )}\n >\n <Icon className={size === 'lg' ? 'h-5 w-5' : 'h-4 w-4'} aria-hidden=\"true\" />\n {getLabel(sport)}\n </button>\n );\n })}\n </div>\n);\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport const OnboardingPage: FC = () => {\n const tr = useAbTranslate();\n const { navigateTo, currentUser } = useAthleteBridge();\n\n const createTeam = useCreateTeam();\n const addMember = useAddMember();\n\n const [step, setStep] = useState<StepNumber>(1);\n const [sport, setSport] = useState<AthSport | null>(null);\n const [teamName, setTeamName] = useState('');\n const [ageGroup, setAgeGroup] = useState('');\n const [season, setSeason] = useState('');\n const [role, setRole] = useState<AthMemberRole | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const ageGroups = useAgeGroups(tr);\n const seasonOptions = useMemo(() => buildSeasonOptions(new Date(), tr), [tr]);\n\n const sportLabel = useCallback(\n (s: AthSport): string => {\n switch (s) {\n case 'ATHLETICS':\n return tr('atheletebridge.sport.athletics', 'Athletics');\n case 'BASKETBALL':\n return tr('atheletebridge.sport.basketball', 'Basketball');\n case 'CRICKET':\n return tr('atheletebridge.sport.cricket', 'Cricket');\n case 'ESPORTS':\n return tr('atheletebridge.sport.esports', 'Esports');\n case 'FOOTBALL':\n return tr('atheletebridge.sport.football', 'Football');\n case 'OTHER':\n return tr('atheletebridge.sport.other', 'Other');\n case 'SWIMMING':\n return tr('atheletebridge.sport.swimming', 'Swimming');\n case 'TENNIS':\n return tr('atheletebridge.sport.tennis', 'Tennis');\n }\n },\n [tr]\n );\n\n const roleLabel = useCallback(\n (r: AthMemberRole): string => {\n switch (r) {\n case 'PLAYER':\n return tr('atheletebridge.memberRole.player', 'Player');\n case 'CAPTAIN':\n return tr('atheletebridge.memberRole.captain', 'Captain');\n case 'COACH':\n return tr('atheletebridge.memberRole.coach', 'Coach');\n case 'ASSISTANT_COACH':\n return tr('atheletebridge.memberRole.assistantCoach', 'Assistant Coach');\n case 'PHYSIO':\n return tr('atheletebridge.memberRole.physio', 'Physio');\n case 'MANAGER':\n return tr('atheletebridge.memberRole.manager', 'Manager');\n }\n },\n [tr]\n );\n\n const busy = createTeam.isPending || addMember.isPending;\n\n const goDashboard = useCallback(() => navigateTo('/'), [navigateTo]);\n\n const handleBack = useCallback(() => {\n setError(null);\n setStep((prev) => (prev === 1 ? 1 : ((prev - 1) as StepNumber)));\n }, []);\n\n const handleSelectSport = useCallback((next: AthSport) => {\n setSport(next);\n setError(null);\n }, []);\n\n /**\n * Step 1/2 advance. Step 3 submits, so this only guards the two picker steps.\n */\n const handleContinue = useCallback(() => {\n if (step === 1) {\n if (!sport) {\n setError(tr('atheletebridge.pages.onboarding.errorSport', 'Pick a sport to continue.'));\n return;\n }\n setError(null);\n setStep(2);\n return;\n }\n if (!teamName.trim()) {\n setError(\n tr('atheletebridge.pages.onboarding.errorTeamName', 'Give your team a name to continue.')\n );\n return;\n }\n setError(null);\n setStep(3);\n }, [step, sport, teamName, tr]);\n\n /**\n * Final submit — creates the team, then (best-effort) puts the calling user on\n * its roster with the chosen role, and lands them on the new team page.\n */\n const handleFinish = useCallback(\n async (event: FormEvent) => {\n event.preventDefault();\n if (!sport || !role || !teamName.trim()) {\n setError(tr('atheletebridge.pages.onboarding.errorRole', 'Pick your role to finish.'));\n return;\n }\n setError(null);\n\n try {\n const team = await createTeam.mutateAsync({\n name: teamName.trim(),\n sport,\n status: 'FORMING',\n ageGroup: ageGroup || undefined,\n season: season || undefined,\n });\n\n if (currentUser?.id) {\n const fullName = [currentUser.firstName, currentUser.lastName]\n .filter((part): part is string => Boolean(part))\n .join(' ')\n .trim();\n const displayName = currentUser.name ?? (fullName || currentUser.email);\n try {\n await addMember.mutateAsync({\n teamId: team.id,\n userId: currentUser.id,\n displayName,\n role,\n status: 'ACTIVE',\n });\n } catch {\n // The team is created and is the primary outcome — a roster failure\n // must not strand the user in the wizard. They can add themselves\n // from the team page.\n }\n }\n\n navigateTo(`/teams/${team.id}`);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr(\n 'atheletebridge.pages.onboarding.errorCreate',\n \"We couldn't create your team. Please try again.\"\n )\n );\n }\n },\n [sport, role, teamName, ageGroup, season, currentUser, createTeam, addMember, navigateTo, tr]\n );\n\n const heading =\n step === 1\n ? tr('atheletebridge.pages.onboarding.step1Title', 'What do you coach?')\n : step === 2\n ? tr('atheletebridge.pages.onboarding.step2Title', 'Create your first team')\n : tr('atheletebridge.pages.onboarding.step3Title', \"What's your role?\");\n\n const subheading =\n step === 1\n ? tr(\n 'atheletebridge.pages.onboarding.step1Subtitle',\n 'Your sport shapes the squads, fixtures and training you set up next.'\n )\n : step === 2\n ? tr(\n 'atheletebridge.pages.onboarding.step2Subtitle',\n 'You can add players and fixtures once your team exists.'\n )\n : tr(\n 'atheletebridge.pages.onboarding.step3Subtitle',\n 'We add you to this squad so the roster starts with you.'\n );\n\n return (\n <div className=\"min-h-full bg-bg-canvas px-4 py-10 sm:px-6 sm:py-14\">\n <div className=\"mx-auto flex w-full max-w-2xl flex-col gap-8\">\n <Stepper\n current={step}\n stepLabel={(s) => stepLabel(s, tr)}\n progressLabel={tr('atheletebridge.pages.onboarding.progressLabel', 'Onboarding progress')}\n />\n\n <header className=\"text-center\">\n <h1 className=\"text-3xl font-bold tracking-tight text-text-primary sm:text-4xl\">\n {heading}\n </h1>\n <p className=\"mx-auto mt-2 max-w-lg text-sm text-text-secondary sm:text-base\">\n {subheading}\n </p>\n </header>\n\n <form onSubmit={handleFinish} noValidate>\n <GlassCard treatment=\"insight\" className=\"p-6 sm:p-8\">\n <div className=\"space-y-6\">\n {step === 1 && (\n <SportChips\n value={sport}\n onSelect={handleSelectSport}\n size=\"lg\"\n getLabel={sportLabel}\n groupLabel={tr('atheletebridge.pages.onboarding.sport', 'Sport')}\n />\n )}\n\n {step === 2 && (\n <>\n <FormField\n label={tr('atheletebridge.pages.onboarding.teamName', 'Team name')}\n htmlFor=\"onboarding-team-name\"\n required\n >\n <input\n id=\"onboarding-team-name\"\n className={fieldInputClass}\n value={teamName}\n maxLength={TEAM_NAME_LIMIT}\n onChange={(e) => {\n setTeamName(e.target.value);\n setError(null);\n }}\n placeholder={tr(\n 'atheletebridge.pages.onboarding.teamNamePlaceholder',\n 'e.g. Riverside United U16'\n )}\n />\n </FormField>\n\n <FormField label={tr('atheletebridge.pages.onboarding.sport', 'Sport')}>\n <SportChips\n value={sport}\n onSelect={handleSelectSport}\n size=\"sm\"\n getLabel={sportLabel}\n groupLabel={tr('atheletebridge.pages.onboarding.sport', 'Sport')}\n />\n </FormField>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField\n label={tr('atheletebridge.pages.onboarding.ageGroup', 'Age group')}\n htmlFor=\"onboarding-age-group\"\n >\n <select\n id=\"onboarding-age-group\"\n className={fieldInputClass}\n value={ageGroup}\n onChange={(e) => setAgeGroup(e.target.value)}\n >\n {ageGroups.map((option) => (\n <option key={option.value || 'none'} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </FormField>\n\n <FormField\n label={tr('atheletebridge.pages.onboarding.season', 'Season')}\n htmlFor=\"onboarding-season\"\n >\n <select\n id=\"onboarding-season\"\n className={fieldInputClass}\n value={season}\n onChange={(e) => setSeason(e.target.value)}\n >\n {seasonOptions.map((option) => (\n <option key={option.value || 'none'} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </FormField>\n </div>\n </>\n )}\n\n {step === 3 && (\n <div\n className=\"flex flex-wrap gap-2\"\n role=\"group\"\n aria-label={tr('atheletebridge.pages.onboarding.yourRole', 'Your role')}\n >\n {MEMBER_ROLE_OPTIONS.map((option) => {\n const active = role === option;\n return (\n <button\n key={option}\n type=\"button\"\n onClick={() => {\n setRole(option);\n setError(null);\n }}\n aria-pressed={active}\n className={cn(\n CHIP_BASE,\n 'px-5 py-2.5 text-base',\n active ? CHIP_ACTIVE : CHIP_IDLE\n )}\n >\n <Users className=\"h-5 w-5\" aria-hidden=\"true\" />\n {roleLabel(option)}\n </button>\n );\n })}\n </div>\n )}\n\n {error && (\n <p role=\"alert\" className=\"text-sm text-status-error-text\">\n {error}\n </p>\n )}\n </div>\n\n <div className=\"mt-8 flex flex-wrap items-center justify-between gap-3 border-t border-border-seam pt-6\">\n <button\n type=\"button\"\n onClick={step === 1 ? goDashboard : handleBack}\n className={SECONDARY_BUTTON}\n disabled={busy}\n >\n <ArrowLeft className=\"h-4 w-4\" aria-hidden=\"true\" />\n {step === 1\n ? tr('atheletebridge.pages.onboarding.exit', 'Exit')\n : tr('atheletebridge.pages.onboarding.back', 'Back')}\n </button>\n\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={goDashboard}\n className={GHOST_BUTTON}\n disabled={busy}\n >\n {tr('atheletebridge.pages.onboarding.skip', 'Skip for now')}\n </button>\n\n {step === 3 ? (\n <button type=\"submit\" className={PRIMARY_BUTTON} disabled={busy || !role}>\n {busy ? (\n <>\n <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n {tr('atheletebridge.pages.onboarding.creating', 'Creating…')}\n </>\n ) : (\n <>\n {tr('atheletebridge.pages.onboarding.finish', 'Create team')}\n <ArrowRight className=\"h-4 w-4\" aria-hidden=\"true\" />\n </>\n )}\n </button>\n ) : (\n <button type=\"button\" onClick={handleContinue} className={PRIMARY_BUTTON}>\n {tr('atheletebridge.pages.onboarding.continue', 'Continue')}\n <ArrowRight className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n )}\n </div>\n </div>\n </GlassCard>\n </form>\n\n <p className=\"text-center text-sm text-text-secondary\">\n {tr('atheletebridge.pages.onboarding.footer', 'Step')} {step}{' '}\n {tr('atheletebridge.pages.onboarding.footerOf', 'of')} {TOTAL_STEPS} —{' '}\n {tr('atheletebridge.pages.onboarding.footerHint', 'takes about a minute.')}\n </p>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PerformancePage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAuNH,wBAAgB,eAAe,
|
|
1
|
+
{"version":3,"file":"PerformancePage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAuNH,wBAAgB,eAAe,gCA4C9B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PerformancePage.js","names":[],"sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"sourcesContent":["/**\n * Performance\n * @module atheletebridge/pages/performance\n *\n * The coach's athlete-performance workbench: pick an athlete from a team roster,\n * then log workouts (athLogWorkout) and record performance metrics\n * (athRecordMetric), and review that athlete's workout history\n * (athListWorkoutLogs, offset-paginated) and metrics.\n *\n * Elevated to the concept bar: each measured metric (type + label) becomes a\n * hero stat tile with its big latest value, a real recordedAt-ordered trend\n * sparkline, a min→max range scale and a signed delta-vs-previous — all derived\n * from the real time-series (nothing fabricated). Workout history renders as a\n * responsive table that reflows to cards on mobile, with a thumb-reachable\n * sticky \"Log workout\" action.\n *\n * A rolling date-range window (\"Last 7 / 28 / 90 days\", or All time) scopes\n * everything below it: which readings a tile is built from, what its delta is\n * measured against (\"vs previous 7 days\" really means the preceding 7 days,\n * not simply the preceding reading), and which workouts the history lists.\n * The window is applied client-side — see WINDOW_FETCH_LIMIT for why, and for\n * the coverage check that stops a truncated page reading as a complete one.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n ArrowDown,\n ArrowUp,\n CalendarRange,\n Dumbbell,\n Gauge,\n HeartPulse,\n Loader2,\n Move,\n Plus,\n Save,\n Scale,\n Target,\n Users,\n Zap,\n type LucideIcon,\n} from 'lucide-react';\nimport {\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { AthleteSelect, type AthleteSelectValue } from '../../components/AthleteSelect';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { CursorPager } from '../../components/CursorPager';\nimport { FilterSelect, type FilterOption } from '../../components/FilterSelect';\nimport { Sparkline } from '../../components/viz';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { METRIC_TYPE_LABELS } from '../../components/domain';\nimport { useAbTranslate, type TranslateFn } from '../../i18n';\nimport {\n useWorkoutLogs,\n usePerformanceMetrics,\n useLogWorkout,\n useRecordMetric,\n} from '../../hooks/useAthleteBridgeApi';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type {\n AthMetricType,\n AthPerformanceMetric,\n AthWorkoutLog,\n AthWorkoutStatus,\n} from '../../types';\n\nconst PAGE_SIZE = 10;\n\nconst WORKOUT_STATUSES: AthWorkoutStatus[] = ['COMPLETED', 'PLANNED', 'SKIPPED'];\nconst METRIC_TYPES = Object.keys(METRIC_TYPE_LABELS) as AthMetricType[];\n\n/**\n * How many workout rows to pull when a bounded window is active.\n *\n * Neither `athListWorkoutLogs` nor `athPerformanceMetrics` accepts a date-range\n * argument — the only filters on the composed schema are `athleteId` /\n * `programId` / `type` — so the window is applied client-side over a bounded\n * page. Both list fields are ordered newest-first by the svc (`performedAt` /\n * `recordedAt` desc) and every window here ends today, so the rows a window\n * needs are always the newest rows. The svc clamps `limit` to its own ceiling,\n * so this is a request rather than a guarantee: `windowCovered` below checks\n * whether the page actually reached back past the window's start and the UI\n * says so when it did not.\n */\nconst WINDOW_FETCH_LIMIT = 100;\n\n/**\n * Metric history depth, so a window has prior readings to compare against.\n * Hoisted so the same object identity is reused across renders.\n */\nconst METRIC_PAGINATION = { limit: 100 } as const;\n\n/** Rolling window presets. `''` (the FilterSelect \"all\" option) means no window. */\ntype RangeKey = '7d' | '28d' | '90d';\n\nconst RANGE_KEYS: RangeKey[] = ['7d', '28d', '90d'];\nconst RANGE_DAYS: Record<RangeKey, number> = { '7d': 7, '28d': 28, '90d': 90 };\n\n/** A resolved window of real time, inclusive at both ends. */\ninterface DateWindow {\n start: number;\n end: number;\n}\n\n/**\n * Resolve a preset into whole local calendar days ending with today — \"last 7\n * days\" is today plus the six days before it, which is what the range caption\n * claims. Anchoring on the day rather than on the instant of selection means a\n * reading taken moments after the window was chosen still falls inside it,\n * while a workout scheduled for tomorrow still falls outside. `setDate` is used\n * for the subtraction so a DST boundary cannot shift the start off midnight.\n */\nconst resolveWindow = (key: RangeKey): DateWindow => {\n const endOfToday = new Date();\n endOfToday.setHours(23, 59, 59, 999);\n const startOfWindow = new Date(endOfToday);\n startOfWindow.setHours(0, 0, 0, 0);\n startOfWindow.setDate(startOfWindow.getDate() - (RANGE_DAYS[key] - 1));\n return { start: startOfWindow.getTime(), end: endOfToday.getTime() };\n};\n\n/** Milliseconds for an ISO timestamp; NaN for an unparseable one. */\nconst msOf = (iso: string): number => new Date(iso).getTime();\n\n/** Date-only variant of the shared `formatDate` (drops the time part). */\nconst formatDay = (ms: number): string =>\n formatDate(new Date(ms), { hour: undefined, minute: undefined, hour12: undefined });\n\n/** e.g. \"5 Jul 2026 – 11 Jul 2026\". */\nconst windowLabel = (range: DateWindow): string =>\n `${formatDay(range.start)} – ${formatDay(range.end)}`;\n\n/**\n * Preset name. Written as literal `tr(key, fallback)` calls because the app's\n * i18n extractor reads the call site's AST and cannot resolve variables.\n */\nconst rangeName = (key: RangeKey, tr: TranslateFn): string => {\n switch (key) {\n case '7d':\n return tr('atheletebridge.pages.performance.window.last7', 'Last 7 days');\n case '28d':\n return tr('atheletebridge.pages.performance.window.last28', 'Last 28 days');\n case '90d':\n return tr('atheletebridge.pages.performance.window.last90', 'Last 90 days');\n }\n};\n\n/** What a windowed metric delta is measured against. */\nconst comparisonName = (key: RangeKey, tr: TranslateFn): string => {\n switch (key) {\n case '7d':\n return tr('atheletebridge.pages.performance.window.vsLast7', 'vs previous 7 days');\n case '28d':\n return tr('atheletebridge.pages.performance.window.vsLast28', 'vs previous 28 days');\n case '90d':\n return tr('atheletebridge.pages.performance.window.vsLast90', 'vs previous 90 days');\n }\n};\n\n/** Why a metric tile has no delta to show. */\nconst noComparisonName = (key: RangeKey, tr: TranslateFn): string => {\n switch (key) {\n case '7d':\n return tr(\n 'atheletebridge.pages.performance.window.noPrior7',\n 'No reading in the previous 7 days'\n );\n case '28d':\n return tr(\n 'atheletebridge.pages.performance.window.noPrior28',\n 'No reading in the previous 28 days'\n );\n case '90d':\n return tr(\n 'atheletebridge.pages.performance.window.noPrior90',\n 'No reading in the previous 90 days'\n );\n }\n};\n\n/** Brand gradient CTA — token-only (no raw colors). */\nconst GRADIENT_CTA =\n 'inline-flex items-center justify-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow] hover:shadow-elevation-2 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\n/** Per-metric-type icon for the stat tiles. */\nconst METRIC_ICON: Record<AthMetricType, LucideIcon> = {\n SPEED: Zap,\n ENDURANCE: Activity,\n STRENGTH: Dumbbell,\n AGILITY: Move,\n ACCURACY: Target,\n WEIGHT: Scale,\n HEART_RATE: HeartPulse,\n CUSTOM: Gauge,\n};\n\nconst numOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nconst floatOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseFloat(t);\n return Number.isNaN(n) ? undefined : n;\n};\n\n/** Current local time as a `datetime-local` value (`YYYY-MM-DDTHH:mm`). */\nconst nowLocal = (): string => {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60_000);\n return local.toISOString().slice(0, 16);\n};\n\n/** Trim trailing zeros from a fixed-precision number for compact display. */\nconst fmtNum = (n: number): string => {\n const abs = Math.abs(n);\n const s = abs >= 100 ? n.toFixed(0) : n.toFixed(2);\n return s.replace(/\\.?0+$/, '');\n};\n\nexport function PerformancePage() {\n const tr = useAbTranslate();\n const [athlete, setAthlete] = useState<AthleteSelectValue>({ athleteId: '', displayName: '' });\n\n return (\n <PageLayout\n title={tr('atheletebridge.pages.performance.title', 'Performance')}\n description={tr('atheletebridge.pages.performance.description', \"Log workouts, record metrics and track an athlete's progress over time.\")}\n >\n {/* Athlete picker — sticks to the top of the scroll area as an insight panel */}\n <div\n data-tour=\"performance-athlete-picker\"\n className=\"sticky top-0 z-20 -mx-6 mb-8 bg-bg-canvas/80 px-6 pb-4 pt-1 backdrop-blur supports-[backdrop-filter]:bg-bg-canvas/60\"\n >\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <AthleteSelect\n athleteId={athlete.athleteId}\n onSelect={setAthlete}\n idPrefix=\"perf-athlete\"\n />\n </GlassCard>\n </div>\n\n {athlete.athleteId ? (\n <AthletePerformance athleteId={athlete.athleteId} displayName={athlete.displayName} />\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Users className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.performance.pickAthleteTitle', 'Pick an athlete to begin')}\n description={tr('atheletebridge.pages.performance.pickAthleteHint', 'Choose a team and an athlete above to log workouts, record metrics and review their history.')}\n />\n )}\n </PageLayout>\n );\n}\n\ninterface AthletePerformanceProps {\n athleteId: string;\n displayName: string;\n}\n\n/** A single measured metric (type + label) collapsed into a real time-series. */\ninterface MetricSeries {\n key: string;\n type: AthMetricType;\n label: string;\n unit: string | null;\n latest: number;\n previous: number | null;\n min: number;\n max: number;\n /** Values oldest → newest (for the trend sparkline). */\n series: number[];\n recordedAt: string;\n}\n\n/** Copy for a tile's delta line, which depends on the active window. */\ninterface DeltaLabels {\n /** What the delta is measured against, e.g. \"vs previous 7 days\". */\n comparison: string;\n /** Shown when there is nothing to compare against. */\n noComparison: string;\n}\n\nfunction AthletePerformance({ athleteId, displayName }: AthletePerformanceProps) {\n const tr = useAbTranslate();\n const [page, setPage] = useState(0);\n const [rangeKey, setRangeKey] = useState<RangeKey | ''>('');\n const [logOpen, setLogOpen] = useState(false);\n const [metricOpen, setMetricOpen] = useState(false);\n\n // Resolved once per selection, so the window is stable across re-renders\n // (a bare Date.now() in render would invalidate every memo below).\n const range = useMemo<DateWindow | null>(\n () => (rangeKey === '' ? null : resolveWindow(rangeKey)),\n [rangeKey]\n );\n\n // With a window the whole (bounded) newest-first page is pulled and both the\n // filtering and the paging happen here; without one the svc pages as before.\n const logs = useWorkoutLogs({\n athleteId,\n pagination: range\n ? { limit: WINDOW_FETCH_LIMIT }\n : { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const metrics = usePerformanceMetrics(athleteId, METRIC_PAGINATION);\n\n const fetchedLogs = useMemo(() => logs.data?.items ?? [], [logs.data]);\n const fetchedHasMore = logs.data?.pageInfo.hasMore ?? false;\n\n const windowLogs = useMemo(() => {\n if (!range) {\n return fetchedLogs;\n }\n return fetchedLogs.filter((log) => {\n const at = msOf(log.performedAt);\n return at >= range.start && at <= range.end;\n });\n }, [fetchedLogs, range]);\n\n // Clamp the page against the windowed set so switching athlete (or a refetch\n // that shrinks the window) cannot strand the pager on an empty page.\n const lastPage = range ? Math.max(0, Math.ceil(windowLogs.length / PAGE_SIZE) - 1) : page;\n const pageIndex = Math.min(page, lastPage);\n const logItems = range\n ? windowLogs.slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE)\n : windowLogs;\n const hasMore = range ? windowLogs.length > (pageIndex + 1) * PAGE_SIZE : fetchedHasMore;\n\n /**\n * Whether the fetched page actually reaches back past the window's start.\n * Rows are `performedAt` desc, so the last fetched row is the oldest one we\n * hold: if there are more rows on the server AND that oldest row is still\n * newer than the window start, in-window workouts are missing and the list\n * must not be presented as complete.\n */\n const windowCovered = useMemo(() => {\n if (!range || !fetchedHasMore) {\n return true;\n }\n const oldest = fetchedLogs[fetchedLogs.length - 1];\n return oldest != null && msOf(oldest.performedAt) <= range.start;\n }, [range, fetchedHasMore, fetchedLogs]);\n\n const rangeOptions = useMemo<FilterOption<RangeKey>[]>(\n () =>\n RANGE_KEYS.map((key) => ({\n value: key,\n label: `${rangeName(key, tr)} · ${windowLabel(resolveWindow(key))}`,\n })),\n [tr]\n );\n\n const deltaLabels = useMemo<DeltaLabels>(\n () =>\n rangeKey === ''\n ? {\n comparison: tr('atheletebridge.pages.performance.window.vsPrevious', 'vs previous'),\n noComparison: tr(\n 'atheletebridge.pages.performance.window.firstReading',\n 'First reading'\n ),\n }\n : {\n comparison: comparisonName(rangeKey, tr),\n noComparison: noComparisonName(rangeKey, tr),\n },\n [rangeKey, tr]\n );\n\n // Group by (type + label) so every distinct measurement is its own trend.\n const metricSeries = useMemo<MetricSeries[]>(() => {\n const groups = new Map<string, AthPerformanceMetric[]>();\n for (const m of metrics.data ?? []) {\n const key = `${m.type}::${m.label}`;\n const list = groups.get(key) ?? [];\n list.push(m);\n groups.set(key, list);\n }\n return Array.from(groups.entries())\n .map(([key, list]): MetricSeries | null => {\n const asc = [...list].sort((a, b) => msOf(a.recordedAt) - msOf(b.recordedAt));\n // Readings inside the window drive the headline, sparkline and range.\n const inWindow = range\n ? asc.filter((m) => {\n const at = msOf(m.recordedAt);\n return at >= range.start && at <= range.end;\n })\n : asc;\n if (inWindow.length === 0) {\n return null;\n }\n const values = inWindow.map((m) => m.value);\n const newest = inWindow[inWindow.length - 1];\n\n // The comparison reading is the latest one in the window immediately\n // BEFORE this one (equal length), so \"vs previous 7 days\" really is the\n // previous 7 days. Without a window it stays the preceding reading.\n let previous: number | null = null;\n if (range) {\n const priorStart = range.start - (range.end - range.start + 1);\n const prior = asc.filter((m) => {\n const at = msOf(m.recordedAt);\n return at >= priorStart && at < range.start;\n });\n previous = prior.length > 0 ? prior[prior.length - 1].value : null;\n } else {\n previous = asc.length > 1 ? asc[asc.length - 2].value : null;\n }\n\n return {\n key,\n type: newest.type,\n label: newest.label,\n unit: newest.unit,\n latest: newest.value,\n previous,\n min: Math.min(...values),\n max: Math.max(...values),\n series: values,\n recordedAt: newest.recordedAt,\n };\n })\n .filter((entry): entry is MetricSeries => entry !== null)\n .sort((a, b) => msOf(b.recordedAt) - msOf(a.recordedAt));\n }, [metrics.data, range]);\n\n const columns: ResponsiveTableColumn<AthWorkoutLog>[] = useMemo(\n () => [\n {\n key: 'workout',\n header: tr('atheletebridge.pages.performance.column.workout', 'Workout'),\n priority: 'primary',\n cell: (log) => {\n const Icon = workoutIcon(log.title);\n return (\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </span>\n <span className=\"min-w-0 truncate font-medium text-text-primary\">{log.title}</span>\n </div>\n );\n },\n },\n {\n key: 'duration',\n header: tr('atheletebridge.pages.performance.column.duration', 'Duration'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.performance.column.duration', 'Duration'),\n cell: (log) =>\n log.durationMin != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatDuration(log.durationMin)}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n {\n key: 'rpe',\n header: tr('atheletebridge.pages.performance.column.rpe', 'RPE'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.performance.column.rpe', 'RPE'),\n cell: (log) =>\n log.rpe != null ? <RpeChip rpe={log.rpe} /> : <span className=\"text-text-muted\">—</span>,\n },\n {\n key: 'date',\n header: tr('atheletebridge.pages.performance.column.date', 'Date'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.performance.column.date', 'Date'),\n cell: (log) => (\n <span className=\"tabular-nums text-text-secondary\">{formatDate(log.performedAt)}</span>\n ),\n },\n {\n key: 'status',\n header: tr('atheletebridge.pages.performance.column.status', 'Status'),\n priority: 'primary',\n mobileLabel: tr('atheletebridge.pages.performance.column.status', 'Status'),\n cell: (log) => <StatusBadge status={log.status} />,\n },\n {\n key: 'notes',\n header: tr('atheletebridge.pages.performance.column.notes', 'Notes'),\n priority: 'hidden-on-mobile',\n cell: (log) =>\n log.notes ? (\n <span className=\"line-clamp-2 max-w-[22ch] text-text-secondary\">{log.notes}</span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n ],\n [tr]\n );\n\n return (\n <div className=\"space-y-8 pb-24 sm:pb-0\">\n {/* Actions */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n {tr('atheletebridge.pages.performance.tracking', 'Tracking')} <span className=\"font-medium text-text-primary\">{displayName || tr('atheletebridge.pages.performance.athleteFallback', 'athlete')}</span>\n {range ? (\n <>\n {' · '}\n <span className=\"tabular-nums text-text-muted\">{windowLabel(range)}</span>\n </>\n ) : null}\n </p>\n <div className=\"flex flex-wrap items-center gap-2\">\n {/* Date-range window — scopes the metric tiles, their deltas and the\n workout history below to a rolling period. */}\n <span className=\"flex items-center gap-2\" data-tour=\"performance-date-range\">\n <CalendarRange className=\"h-4 w-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n <FilterSelect<RangeKey>\n value={rangeKey}\n onChange={(next) => {\n setRangeKey(next);\n setPage(0);\n }}\n options={rangeOptions}\n allLabel={tr('atheletebridge.pages.performance.window.allTime', 'All time')}\n ariaLabel={tr('atheletebridge.pages.performance.window.ariaLabel', 'Date range')}\n />\n </span>\n <button\n type=\"button\"\n onClick={() => setMetricOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Gauge className=\"h-4 w-4\" /> {tr('atheletebridge.pages.performance.recordMetric', 'Record metric')}\n </button>\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'hidden sm:inline-flex')}\n >\n <Dumbbell className=\"h-4 w-4\" /> {tr('atheletebridge.pages.performance.logWorkout', 'Log workout')}\n </button>\n </div>\n </div>\n\n {/* Metrics — hero stat tiles with real trend sparklines */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">{tr('atheletebridge.pages.performance.metricsTitle', 'Performance metrics')}</h2>\n {metrics.isLoading ? (\n <LoadingState label={tr('atheletebridge.pages.performance.loadingMetrics', 'Loading metrics…')} />\n ) : metrics.isError ? (\n <ErrorState onRetry={() => void metrics.refetch()} />\n ) : metricSeries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Gauge className=\"h-10 w-10\" />}\n title={\n range\n ? tr(\n 'atheletebridge.pages.performance.metrics.emptyWindowTitle',\n 'No metrics in this window'\n )\n : tr('atheletebridge.pages.performance.metrics.emptyTitle', 'No metrics recorded')\n }\n description={\n range\n ? tr(\n 'atheletebridge.pages.performance.metrics.emptyWindowDescription',\n 'Nothing was recorded for this athlete in the selected period. Widen the date range, or record a metric.'\n )\n : tr('atheletebridge.pages.performance.noMetricsDescription', 'Record a metric — speed, strength, endurance and more — to start charting progress.')\n }\n action={\n <button type=\"button\" onClick={() => setMetricOpen(true)} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" /> {tr('atheletebridge.pages.performance.recordMetricAction', 'Record metric')}\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {metricSeries.map((m) => (\n <MetricTile key={m.key} metric={m} labels={deltaLabels} tr={tr} />\n ))}\n </div>\n )}\n </section>\n\n {/* Workout logs */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">{tr('atheletebridge.pages.performance.workoutsTitle', 'Workout history')}</h2>\n {logs.isLoading ? (\n <LoadingState label={tr('atheletebridge.pages.performance.loadingWorkouts', 'Loading workouts…')} />\n ) : logs.isError ? (\n <ErrorState onRetry={() => void logs.refetch()} />\n ) : logItems.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title={\n range\n ? tr(\n 'atheletebridge.pages.performance.workouts.emptyWindowTitle',\n 'No workouts in this window'\n )\n : tr('atheletebridge.pages.performance.workouts.emptyTitle', 'No workouts logged')\n }\n description={\n range\n ? tr(\n 'atheletebridge.pages.performance.workouts.emptyWindowDescription',\n 'Nothing was logged for this athlete in the selected period. Widen the date range, or log a workout.'\n )\n : tr('atheletebridge.pages.performance.noWorkoutsDescription', \"Log a workout to build this athlete's training history.\")\n }\n action={\n <button type=\"button\" onClick={() => setLogOpen(true)} className={GRADIENT_CTA}>\n <Dumbbell className=\"h-4 w-4\" /> {tr('atheletebridge.pages.performance.logWorkout', 'Log workout')}\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"metric\" className=\"overflow-hidden p-1 sm:p-2\">\n <ResponsiveTable\n columns={columns}\n data={logItems}\n rowKey={(log) => log.id}\n caption={tr('atheletebridge.pages.performance.workouts.caption', 'Workout history for {{name}}').replace('{{name}}', displayName || tr('atheletebridge.pages.performance.athleteFallback', 'athlete'))}\n />\n </GlassCard>\n <CursorPager\n className=\"mt-6\"\n page={pageIndex}\n hasMore={hasMore}\n onPrev={() => setPage(Math.max(0, pageIndex - 1))}\n onNext={() => setPage(pageIndex + 1)}\n />\n </>\n )}\n {!windowCovered ? (\n <p className=\"mt-3 text-xs text-text-muted\">\n {tr(\n 'atheletebridge.pages.performance.workouts.windowTruncated',\n 'Only the most recent page of workouts was searched, and it does not reach back to the start of this window — older sessions in the range are not listed.'\n )}\n </p>\n ) : null}\n </section>\n\n {/* Mobile sticky Log workout — thumb-reachable primary action */}\n <div className=\"sticky bottom-0 z-30 -mx-6 border-t border-border-seam bg-bg-canvas/85 px-6 pb-[max(env(safe-area-inset-bottom),0.75rem)] pt-3 backdrop-blur sm:hidden\">\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'w-full py-3 text-base')}\n >\n <Plus className=\"h-5 w-5\" /> {tr('atheletebridge.pages.performance.logWorkout', 'Log workout')}\n </button>\n </div>\n\n <LogWorkoutDialog\n open={logOpen}\n onOpenChange={setLogOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n <RecordMetricDialog\n open={metricOpen}\n onOpenChange={setMetricOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n </div>\n );\n}\n\n/** Pick a workout row icon from the (real) title text. Cosmetic only. */\nfunction workoutIcon(title: string): LucideIcon {\n const t = title.toLowerCase();\n if (/strength|lift|squat|deadlift|bench|1rm|weight/.test(t)) return Dumbbell;\n if (/speed|sprint|agility|accel|split/.test(t)) return Zap;\n if (/endurance|aerobic|cardio|run|beep|yo-?yo/.test(t)) return Activity;\n if (/recovery|mobility|stretch|rest/.test(t)) return HeartPulse;\n if (/accuracy|shoot|target/.test(t)) return Target;\n return Dumbbell;\n}\n\n/** Bordered RPE chip, color-scaled by session intensity (semantic tokens). */\nfunction RpeChip({ rpe }: { rpe: number }) {\n const tone =\n rpe >= 8\n ? 'border-status-error-border text-status-error-text'\n : rpe >= 5\n ? 'border-status-warning-border text-status-warning-text'\n : 'border-border-seam text-text-secondary';\n return (\n <span\n className={cn(\n 'inline-flex h-7 min-w-[1.75rem] items-center justify-center rounded-md border px-1.5 text-sm font-semibold tabular-nums',\n tone\n )}\n >\n {rpe}\n </span>\n );\n}\n\n/** Hero stat tile for one measured metric: big value, trend sparkline, range. */\nfunction MetricTile({\n metric,\n labels,\n tr,\n}: {\n metric: MetricSeries;\n labels: DeltaLabels;\n tr: TranslateFn;\n}) {\n const Icon = METRIC_ICON[metric.type];\n const delta = metric.previous == null ? null : metric.latest - metric.previous;\n const hasRange = metric.max > metric.min;\n const pct = hasRange\n ? Math.round(((metric.latest - metric.min) / (metric.max - metric.min)) * 100)\n : 100;\n const unitSuffix = metric.unit ? ` ${metric.unit}` : '';\n\n const metricTypeLabel = (t: AthMetricType): string => {\n switch (t) {\n case 'SPEED':\n return tr('atheletebridge.metricType.speed', 'Speed');\n case 'ENDURANCE':\n return tr('atheletebridge.metricType.endurance', 'Endurance');\n case 'STRENGTH':\n return tr('atheletebridge.metricType.strength', 'Strength');\n case 'AGILITY':\n return tr('atheletebridge.metricType.agility', 'Agility');\n case 'ACCURACY':\n return tr('atheletebridge.metricType.accuracy', 'Accuracy');\n case 'WEIGHT':\n return tr('atheletebridge.metricType.weight', 'Weight');\n case 'HEART_RATE':\n return tr('atheletebridge.metricType.heartRate', 'Heart Rate');\n case 'CUSTOM':\n return tr('atheletebridge.metricType.custom', 'Custom');\n }\n };\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n {metricTypeLabel(metric.type)}\n </span>\n <span className=\"text-xs text-text-muted\">{formatDate(metric.recordedAt)}</span>\n </div>\n\n <div className=\"flex items-end justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-secondary\">{metric.label}</p>\n <p className=\"mt-0.5 text-3xl font-bold leading-none tracking-tight tabular-nums text-text-primary\">\n {fmtNum(metric.latest)}\n {metric.unit ? (\n <span className=\"ml-1 text-base font-medium text-text-secondary\">{metric.unit}</span>\n ) : null}\n </p>\n </div>\n {metric.series.length > 1 ? (\n <div className=\"w-24 shrink-0 sm:w-28\">\n <Sparkline\n data={metric.series}\n variant=\"area\"\n height={40}\n label={`${metric.label} trend`}\n />\n </div>\n ) : null}\n </div>\n\n {/* Range scale: min → latest position → max (from the athlete's own series) */}\n {hasRange ? (\n <div>\n <span\n className=\"block h-1.5 overflow-hidden rounded-full bg-bg-sunken\"\n aria-hidden=\"true\"\n >\n <span\n className=\"block h-1.5 rounded-full bg-[var(--color-accent-teal)]\"\n style={{ width: `${Math.max(4, Math.min(100, pct))}%` }}\n />\n </span>\n <div className=\"mt-1 flex justify-between text-[0.7rem] tabular-nums text-text-muted\">\n <span>{fmtNum(metric.min)}</span>\n <span>{fmtNum((metric.min + metric.max) / 2)}</span>\n <span>{fmtNum(metric.max)}</span>\n </div>\n </div>\n ) : null}\n\n {/* Delta against the active comparison period — direction shown neutrally\n (no good/bad claim), and absent rather than guessed when there is no\n reading to compare against. */}\n <div className=\"mt-auto flex items-center gap-1.5 text-sm\">\n {delta == null ? (\n <span className=\"text-text-muted\">{labels.noComparison}</span>\n ) : delta === 0 ? (\n <span className=\"text-text-muted\">\n {tr('atheletebridge.pages.performance.window.noChange', 'No change')}{' '}\n {labels.comparison}\n </span>\n ) : (\n <>\n <span className=\"inline-flex items-center gap-0.5 font-medium tabular-nums text-text-secondary\">\n {delta > 0 ? (\n <ArrowUp className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <ArrowDown className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n {fmtNum(Math.abs(delta))}\n {unitSuffix}\n </span>\n <span className=\"text-text-muted\">{labels.comparison}</span>\n </>\n )}\n </div>\n </GlassCard>\n );\n}\n\ninterface DialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n athleteId: string;\n displayName: string;\n}\n\nfunction LogWorkoutDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const tr = useAbTranslate();\n const logWorkout = useLogWorkout();\n const [title, setTitle] = useState('');\n const [status, setStatus] = useState<AthWorkoutStatus>('COMPLETED');\n const [performedAt, setPerformedAt] = useState(nowLocal);\n const [durationMin, setDurationMin] = useState('');\n const [rpe, setRpe] = useState('');\n const [totalLoad, setTotalLoad] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setStatus('COMPLETED');\n setPerformedAt(nowLocal());\n setDurationMin('');\n setRpe('');\n setTotalLoad('');\n setNotes('');\n setError(null);\n };\n\n const statusLabel = (s: AthWorkoutStatus): string => {\n switch (s) {\n case 'COMPLETED':\n return tr('atheletebridge.workoutStatus.completed', 'Completed');\n case 'PLANNED':\n return tr('atheletebridge.workoutStatus.planned', 'Planned');\n case 'SKIPPED':\n return tr('atheletebridge.workoutStatus.skipped', 'Skipped');\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!title.trim()) {\n setError(tr('atheletebridge.pages.performance.error.titleRequired', 'A workout title is required.'));\n return;\n }\n if (!performedAt) {\n setError(tr('atheletebridge.pages.performance.error.performedAtRequired', 'A performed date and time is required.'));\n return;\n }\n try {\n await logWorkout.mutateAsync({\n athleteId,\n title: title.trim(),\n status,\n performedAt: new Date(performedAt).toISOString(),\n durationMin: numOrUndefined(durationMin),\n rpe: numOrUndefined(rpe),\n totalLoad: floatOrUndefined(totalLoad),\n notes: notes.trim() || undefined,\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : tr('atheletebridge.pages.performance.error.logFailed', 'Failed to log workout.'));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {tr('atheletebridge.pages.performance.logWorkoutDialogTitle', 'Log workout')}{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <FormField label={tr('atheletebridge.pages.performance.label.title', 'Title')} htmlFor=\"workoutTitle\" required>\n <input\n id=\"workoutTitle\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder={tr('atheletebridge.pages.performance.placeholder.title', 'e.g. Lower-body strength')}\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label={tr('atheletebridge.pages.performance.label.status', 'Status')} htmlFor=\"workoutStatus\">\n <select\n id=\"workoutStatus\"\n className={fieldInputClass}\n value={status}\n onChange={(e) => setStatus(e.target.value as AthWorkoutStatus)}\n >\n {WORKOUT_STATUSES.map((s) => (\n <option key={s} value={s}>\n {statusLabel(s)}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label={tr('atheletebridge.pages.performance.label.performedAt', 'Performed at')} htmlFor=\"workoutPerformedAt\" required>\n <input\n id=\"workoutPerformedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={performedAt}\n onChange={(e) => setPerformedAt(e.target.value)}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <FormField label={tr('atheletebridge.pages.performance.label.durationMin', 'Duration (min)')} htmlFor=\"workoutDuration\">\n <input\n id=\"workoutDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </FormField>\n <FormField label={tr('atheletebridge.pages.performance.label.rpe', 'RPE (1–10)')} htmlFor=\"workoutRpe\">\n <input\n id=\"workoutRpe\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rpe}\n onChange={(e) => setRpe(e.target.value)}\n />\n </FormField>\n <FormField label={tr('atheletebridge.pages.performance.label.totalLoad', 'Total load')} htmlFor=\"workoutLoad\">\n <input\n id=\"workoutLoad\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={totalLoad}\n onChange={(e) => setTotalLoad(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label={tr('atheletebridge.pages.performance.label.notes', 'Notes')} htmlFor=\"workoutNotes\">\n <textarea\n id=\"workoutNotes\"\n rows={2}\n className={fieldInputClass}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder={tr('atheletebridge.pages.performance.notesPlaceholder', 'How did the session go?')}\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={logWorkout.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {logWorkout.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {tr('atheletebridge.pages.performance.logWorkout', 'Log workout')}\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction RecordMetricDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const tr = useAbTranslate();\n const recordMetric = useRecordMetric();\n const [type, setType] = useState<AthMetricType>('CUSTOM');\n const [label, setLabel] = useState('');\n const [value, setValue] = useState('');\n const [unit, setUnit] = useState('');\n const [source, setSource] = useState('');\n const [recordedAt, setRecordedAt] = useState(nowLocal);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setType('CUSTOM');\n setLabel('');\n setValue('');\n setUnit('');\n setSource('');\n setRecordedAt(nowLocal());\n setError(null);\n };\n\n const metricTypeLabel = (t: AthMetricType): string => {\n switch (t) {\n case 'SPEED':\n return tr('atheletebridge.metricType.speed', 'Speed');\n case 'ENDURANCE':\n return tr('atheletebridge.metricType.endurance', 'Endurance');\n case 'STRENGTH':\n return tr('atheletebridge.metricType.strength', 'Strength');\n case 'AGILITY':\n return tr('atheletebridge.metricType.agility', 'Agility');\n case 'ACCURACY':\n return tr('atheletebridge.metricType.accuracy', 'Accuracy');\n case 'WEIGHT':\n return tr('atheletebridge.metricType.weight', 'Weight');\n case 'HEART_RATE':\n return tr('atheletebridge.metricType.heartRate', 'Heart Rate');\n case 'CUSTOM':\n return tr('atheletebridge.metricType.custom', 'Custom');\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!label.trim()) {\n setError(tr('atheletebridge.pages.performance.error.metricLabelRequired', 'A metric label is required.'));\n return;\n }\n const numericValue = floatOrUndefined(value);\n if (numericValue === undefined) {\n setError(tr('atheletebridge.pages.performance.error.metricValueRequired', 'A numeric value is required.'));\n return;\n }\n if (!recordedAt) {\n setError(tr('atheletebridge.pages.performance.error.recordedAtRequired', 'A recorded date and time is required.'));\n return;\n }\n try {\n await recordMetric.mutateAsync({\n athleteId,\n type,\n label: label.trim(),\n value: numericValue,\n unit: unit.trim() || undefined,\n source: source.trim() || undefined,\n recordedAt: new Date(recordedAt).toISOString(),\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : tr('atheletebridge.pages.performance.error.recordFailed', 'Failed to record metric.'));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {tr('atheletebridge.pages.performance.recordMetricDialogTitle', 'Record metric')}{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label={tr('atheletebridge.pages.performance.label.metricType', 'Metric type')} htmlFor=\"metricType\">\n <select\n id=\"metricType\"\n className={fieldInputClass}\n value={type}\n onChange={(e) => setType(e.target.value as AthMetricType)}\n >\n {METRIC_TYPES.map((t) => (\n <option key={t} value={t}>\n {metricTypeLabel(t)}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label={tr('atheletebridge.pages.performance.label.recordedAt', 'Recorded at')} htmlFor=\"metricRecordedAt\" required>\n <input\n id=\"metricRecordedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={recordedAt}\n onChange={(e) => setRecordedAt(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label={tr('atheletebridge.pages.performance.label.metricLabel', 'Label')} htmlFor=\"metricLabel\" required>\n <input\n id=\"metricLabel\"\n className={fieldInputClass}\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder={tr('atheletebridge.pages.performance.placeholder.metricLabel', 'e.g. 40m sprint')}\n />\n </FormField>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField label={tr('atheletebridge.pages.performance.label.value', 'Value')} htmlFor=\"metricValue\" required>\n <input\n id=\"metricValue\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder={tr('atheletebridge.pages.performance.placeholder.value', 'e.g. 4.8')}\n />\n </FormField>\n <FormField label={tr('atheletebridge.pages.performance.label.unit', 'Unit')} htmlFor=\"metricUnit\">\n <input\n id=\"metricUnit\"\n className={fieldInputClass}\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n placeholder={tr('atheletebridge.pages.performance.placeholder.unit', 'e.g. s, kg, bpm')}\n />\n </FormField>\n </div>\n <FormField label={tr('atheletebridge.pages.performance.label.source', 'Source')} htmlFor=\"metricSource\" hint={tr('atheletebridge.pages.performance.hint.source', 'Optional — device, test or coach.')}>\n <input\n id=\"metricSource\"\n className={fieldInputClass}\n value={source}\n onChange={(e) => setSource(e.target.value)}\n placeholder={tr('atheletebridge.pages.performance.placeholder.source', 'e.g. GPS vest')}\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={recordMetric.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {recordMetric.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n {tr('atheletebridge.pages.performance.recordMetric', 'Record metric')}\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PerformancePage.js","names":[],"sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"sourcesContent":["/**\n * Performance\n * @module atheletebridge/pages/performance\n *\n * The coach's athlete-performance workbench: pick an athlete from a team roster,\n * then log workouts (athLogWorkout) and record performance metrics\n * (athRecordMetric), and review that athlete's workout history\n * (athListWorkoutLogs, offset-paginated) and metrics.\n *\n * Elevated to the concept bar: each measured metric (type + label) becomes a\n * hero stat tile with its big latest value, a real recordedAt-ordered trend\n * sparkline, a min→max range scale and a signed delta-vs-previous — all derived\n * from the real time-series (nothing fabricated). Workout history renders as a\n * responsive table that reflows to cards on mobile, with a thumb-reachable\n * sticky \"Log workout\" action.\n *\n * A rolling date-range window (\"Last 7 / 28 / 90 days\", or All time) scopes\n * everything below it: which readings a tile is built from, what its delta is\n * measured against (\"vs previous 7 days\" really means the preceding 7 days,\n * not simply the preceding reading), and which workouts the history lists.\n * The window is applied client-side — see WINDOW_FETCH_LIMIT for why, and for\n * the coverage check that stops a truncated page reading as a complete one.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n ArrowDown,\n ArrowUp,\n CalendarRange,\n Dumbbell,\n Gauge,\n HeartPulse,\n Loader2,\n Move,\n Plus,\n Save,\n Scale,\n Target,\n Users,\n Zap,\n type LucideIcon,\n} from 'lucide-react';\nimport {\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { AthleteSelect, type AthleteSelectValue } from '../../components/AthleteSelect';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { CursorPager } from '../../components/CursorPager';\nimport { FilterSelect, type FilterOption } from '../../components/FilterSelect';\nimport { Sparkline } from '../../components/viz';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { METRIC_TYPE_LABELS } from '../../components/domain';\nimport { useAbTranslate, type TranslateFn } from '../../i18n';\nimport {\n useWorkoutLogs,\n usePerformanceMetrics,\n useLogWorkout,\n useRecordMetric,\n} from '../../hooks/useAthleteBridgeApi';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type {\n AthMetricType,\n AthPerformanceMetric,\n AthWorkoutLog,\n AthWorkoutStatus,\n} from '../../types';\n\nconst PAGE_SIZE = 10;\n\nconst WORKOUT_STATUSES: AthWorkoutStatus[] = ['COMPLETED', 'PLANNED', 'SKIPPED'];\nconst METRIC_TYPES = Object.keys(METRIC_TYPE_LABELS) as AthMetricType[];\n\n/**\n * How many workout rows to pull when a bounded window is active.\n *\n * Neither `athListWorkoutLogs` nor `athPerformanceMetrics` accepts a date-range\n * argument — the only filters on the composed schema are `athleteId` /\n * `programId` / `type` — so the window is applied client-side over a bounded\n * page. Both list fields are ordered newest-first by the svc (`performedAt` /\n * `recordedAt` desc) and every window here ends today, so the rows a window\n * needs are always the newest rows. The svc clamps `limit` to its own ceiling,\n * so this is a request rather than a guarantee: `windowCovered` below checks\n * whether the page actually reached back past the window's start and the UI\n * says so when it did not.\n */\nconst WINDOW_FETCH_LIMIT = 100;\n\n/**\n * Metric history depth, so a window has prior readings to compare against.\n * Hoisted so the same object identity is reused across renders.\n */\nconst METRIC_PAGINATION = { limit: 100 } as const;\n\n/** Rolling window presets. `''` (the FilterSelect \"all\" option) means no window. */\ntype RangeKey = '7d' | '28d' | '90d';\n\nconst RANGE_KEYS: RangeKey[] = ['7d', '28d', '90d'];\nconst RANGE_DAYS: Record<RangeKey, number> = { '7d': 7, '28d': 28, '90d': 90 };\n\n/** A resolved window of real time, inclusive at both ends. */\ninterface DateWindow {\n start: number;\n end: number;\n}\n\n/**\n * Resolve a preset into whole local calendar days ending with today — \"last 7\n * days\" is today plus the six days before it, which is what the range caption\n * claims. Anchoring on the day rather than on the instant of selection means a\n * reading taken moments after the window was chosen still falls inside it,\n * while a workout scheduled for tomorrow still falls outside. `setDate` is used\n * for the subtraction so a DST boundary cannot shift the start off midnight.\n */\nconst resolveWindow = (key: RangeKey): DateWindow => {\n const endOfToday = new Date();\n endOfToday.setHours(23, 59, 59, 999);\n const startOfWindow = new Date(endOfToday);\n startOfWindow.setHours(0, 0, 0, 0);\n startOfWindow.setDate(startOfWindow.getDate() - (RANGE_DAYS[key] - 1));\n return { start: startOfWindow.getTime(), end: endOfToday.getTime() };\n};\n\n/** Milliseconds for an ISO timestamp; NaN for an unparseable one. */\nconst msOf = (iso: string): number => new Date(iso).getTime();\n\n/** Date-only variant of the shared `formatDate` (drops the time part). */\nconst formatDay = (ms: number): string =>\n formatDate(new Date(ms), { hour: undefined, minute: undefined, hour12: undefined });\n\n/** e.g. \"5 Jul 2026 – 11 Jul 2026\". */\nconst windowLabel = (range: DateWindow): string =>\n `${formatDay(range.start)} – ${formatDay(range.end)}`;\n\n/**\n * Preset name. Written as literal `tr(key, fallback)` calls because the app's\n * i18n extractor reads the call site's AST and cannot resolve variables.\n */\nconst rangeName = (key: RangeKey, tr: TranslateFn): string => {\n switch (key) {\n case '7d':\n return tr('atheletebridge.pages.performance.window.last7', 'Last 7 days');\n case '28d':\n return tr('atheletebridge.pages.performance.window.last28', 'Last 28 days');\n case '90d':\n return tr('atheletebridge.pages.performance.window.last90', 'Last 90 days');\n }\n};\n\n/** What a windowed metric delta is measured against. */\nconst comparisonName = (key: RangeKey, tr: TranslateFn): string => {\n switch (key) {\n case '7d':\n return tr('atheletebridge.pages.performance.window.vsLast7', 'vs previous 7 days');\n case '28d':\n return tr('atheletebridge.pages.performance.window.vsLast28', 'vs previous 28 days');\n case '90d':\n return tr('atheletebridge.pages.performance.window.vsLast90', 'vs previous 90 days');\n }\n};\n\n/** Why a metric tile has no delta to show. */\nconst noComparisonName = (key: RangeKey, tr: TranslateFn): string => {\n switch (key) {\n case '7d':\n return tr(\n 'atheletebridge.pages.performance.window.noPrior7',\n 'No reading in the previous 7 days'\n );\n case '28d':\n return tr(\n 'atheletebridge.pages.performance.window.noPrior28',\n 'No reading in the previous 28 days'\n );\n case '90d':\n return tr(\n 'atheletebridge.pages.performance.window.noPrior90',\n 'No reading in the previous 90 days'\n );\n }\n};\n\n/** Brand gradient CTA — token-only (no raw colors). */\nconst GRADIENT_CTA =\n 'inline-flex items-center justify-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow] hover:shadow-elevation-2 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\n/** Per-metric-type icon for the stat tiles. */\nconst METRIC_ICON: Record<AthMetricType, LucideIcon> = {\n SPEED: Zap,\n ENDURANCE: Activity,\n STRENGTH: Dumbbell,\n AGILITY: Move,\n ACCURACY: Target,\n WEIGHT: Scale,\n HEART_RATE: HeartPulse,\n CUSTOM: Gauge,\n};\n\nconst numOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nconst floatOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseFloat(t);\n return Number.isNaN(n) ? undefined : n;\n};\n\n/** Current local time as a `datetime-local` value (`YYYY-MM-DDTHH:mm`). */\nconst nowLocal = (): string => {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60_000);\n return local.toISOString().slice(0, 16);\n};\n\n/** Trim trailing zeros from a fixed-precision number for compact display. */\nconst fmtNum = (n: number): string => {\n const abs = Math.abs(n);\n const s = abs >= 100 ? n.toFixed(0) : n.toFixed(2);\n return s.replace(/\\.?0+$/, '');\n};\n\nexport function PerformancePage() {\n const tr = useAbTranslate();\n const [athlete, setAthlete] = useState<AthleteSelectValue>({ athleteId: '', displayName: '' });\n\n return (\n <PageLayout\n title={tr('atheletebridge.pages.performance.title', 'Performance')}\n description={tr(\n 'atheletebridge.pages.performance.description',\n \"Log workouts, record metrics and track an athlete's progress over time.\"\n )}\n >\n {/* Athlete picker — sticks to the top of the scroll area as an insight panel */}\n <div\n data-tour=\"performance-athlete-picker\"\n className=\"sticky top-0 z-20 -mx-6 mb-8 bg-bg-canvas/80 px-6 pb-4 pt-1 backdrop-blur supports-[backdrop-filter]:bg-bg-canvas/60\"\n >\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <AthleteSelect\n athleteId={athlete.athleteId}\n onSelect={setAthlete}\n idPrefix=\"perf-athlete\"\n />\n </GlassCard>\n </div>\n\n {athlete.athleteId ? (\n <AthletePerformance athleteId={athlete.athleteId} displayName={athlete.displayName} />\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Users className=\"h-10 w-10\" />}\n title={tr(\n 'atheletebridge.pages.performance.pickAthleteTitle',\n 'Pick an athlete to begin'\n )}\n description={tr(\n 'atheletebridge.pages.performance.pickAthleteHint',\n 'Choose a team and an athlete above to log workouts, record metrics and review their history.'\n )}\n />\n )}\n </PageLayout>\n );\n}\n\ninterface AthletePerformanceProps {\n athleteId: string;\n displayName: string;\n}\n\n/** A single measured metric (type + label) collapsed into a real time-series. */\ninterface MetricSeries {\n key: string;\n type: AthMetricType;\n label: string;\n unit: string | null;\n latest: number;\n previous: number | null;\n min: number;\n max: number;\n /** Values oldest → newest (for the trend sparkline). */\n series: number[];\n recordedAt: string;\n}\n\n/** Copy for a tile's delta line, which depends on the active window. */\ninterface DeltaLabels {\n /** What the delta is measured against, e.g. \"vs previous 7 days\". */\n comparison: string;\n /** Shown when there is nothing to compare against. */\n noComparison: string;\n}\n\nfunction AthletePerformance({ athleteId, displayName }: AthletePerformanceProps) {\n const tr = useAbTranslate();\n const [page, setPage] = useState(0);\n const [rangeKey, setRangeKey] = useState<RangeKey | ''>('');\n const [logOpen, setLogOpen] = useState(false);\n const [metricOpen, setMetricOpen] = useState(false);\n\n // Resolved once per selection, so the window is stable across re-renders\n // (a bare Date.now() in render would invalidate every memo below).\n const range = useMemo<DateWindow | null>(\n () => (rangeKey === '' ? null : resolveWindow(rangeKey)),\n [rangeKey]\n );\n\n // With a window the whole (bounded) newest-first page is pulled and both the\n // filtering and the paging happen here; without one the svc pages as before.\n const logs = useWorkoutLogs({\n athleteId,\n pagination: range\n ? { limit: WINDOW_FETCH_LIMIT }\n : { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const metrics = usePerformanceMetrics(athleteId, METRIC_PAGINATION);\n\n const fetchedLogs = useMemo(() => logs.data?.items ?? [], [logs.data]);\n const fetchedHasMore = logs.data?.pageInfo.hasMore ?? false;\n\n const windowLogs = useMemo(() => {\n if (!range) {\n return fetchedLogs;\n }\n return fetchedLogs.filter((log) => {\n const at = msOf(log.performedAt);\n return at >= range.start && at <= range.end;\n });\n }, [fetchedLogs, range]);\n\n // Clamp the page against the windowed set so switching athlete (or a refetch\n // that shrinks the window) cannot strand the pager on an empty page.\n const lastPage = range ? Math.max(0, Math.ceil(windowLogs.length / PAGE_SIZE) - 1) : page;\n const pageIndex = Math.min(page, lastPage);\n const logItems = range\n ? windowLogs.slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE)\n : windowLogs;\n const hasMore = range ? windowLogs.length > (pageIndex + 1) * PAGE_SIZE : fetchedHasMore;\n\n /**\n * Whether the fetched page actually reaches back past the window's start.\n * Rows are `performedAt` desc, so the last fetched row is the oldest one we\n * hold: if there are more rows on the server AND that oldest row is still\n * newer than the window start, in-window workouts are missing and the list\n * must not be presented as complete.\n */\n const windowCovered = useMemo(() => {\n if (!range || !fetchedHasMore) {\n return true;\n }\n const oldest = fetchedLogs[fetchedLogs.length - 1];\n return oldest != null && msOf(oldest.performedAt) <= range.start;\n }, [range, fetchedHasMore, fetchedLogs]);\n\n const rangeOptions = useMemo<FilterOption<RangeKey>[]>(\n () =>\n RANGE_KEYS.map((key) => ({\n value: key,\n label: `${rangeName(key, tr)} · ${windowLabel(resolveWindow(key))}`,\n })),\n [tr]\n );\n\n const deltaLabels = useMemo<DeltaLabels>(\n () =>\n rangeKey === ''\n ? {\n comparison: tr('atheletebridge.pages.performance.window.vsPrevious', 'vs previous'),\n noComparison: tr(\n 'atheletebridge.pages.performance.window.firstReading',\n 'First reading'\n ),\n }\n : {\n comparison: comparisonName(rangeKey, tr),\n noComparison: noComparisonName(rangeKey, tr),\n },\n [rangeKey, tr]\n );\n\n // Group by (type + label) so every distinct measurement is its own trend.\n const metricSeries = useMemo<MetricSeries[]>(() => {\n const groups = new Map<string, AthPerformanceMetric[]>();\n for (const m of metrics.data ?? []) {\n const key = `${m.type}::${m.label}`;\n const list = groups.get(key) ?? [];\n list.push(m);\n groups.set(key, list);\n }\n return Array.from(groups.entries())\n .map(([key, list]): MetricSeries | null => {\n const asc = [...list].sort((a, b) => msOf(a.recordedAt) - msOf(b.recordedAt));\n // Readings inside the window drive the headline, sparkline and range.\n const inWindow = range\n ? asc.filter((m) => {\n const at = msOf(m.recordedAt);\n return at >= range.start && at <= range.end;\n })\n : asc;\n if (inWindow.length === 0) {\n return null;\n }\n const values = inWindow.map((m) => m.value);\n const newest = inWindow[inWindow.length - 1];\n\n // The comparison reading is the latest one in the window immediately\n // BEFORE this one (equal length), so \"vs previous 7 days\" really is the\n // previous 7 days. Without a window it stays the preceding reading.\n let previous: number | null = null;\n if (range) {\n const priorStart = range.start - (range.end - range.start + 1);\n const prior = asc.filter((m) => {\n const at = msOf(m.recordedAt);\n return at >= priorStart && at < range.start;\n });\n previous = prior.length > 0 ? prior[prior.length - 1].value : null;\n } else {\n previous = asc.length > 1 ? asc[asc.length - 2].value : null;\n }\n\n return {\n key,\n type: newest.type,\n label: newest.label,\n unit: newest.unit,\n latest: newest.value,\n previous,\n min: Math.min(...values),\n max: Math.max(...values),\n series: values,\n recordedAt: newest.recordedAt,\n };\n })\n .filter((entry): entry is MetricSeries => entry !== null)\n .sort((a, b) => msOf(b.recordedAt) - msOf(a.recordedAt));\n }, [metrics.data, range]);\n\n const columns: ResponsiveTableColumn<AthWorkoutLog>[] = useMemo(\n () => [\n {\n key: 'workout',\n header: tr('atheletebridge.pages.performance.column.workout', 'Workout'),\n priority: 'primary',\n cell: (log) => {\n const Icon = workoutIcon(log.title);\n return (\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </span>\n <span className=\"min-w-0 truncate font-medium text-text-primary\">{log.title}</span>\n </div>\n );\n },\n },\n {\n key: 'duration',\n header: tr('atheletebridge.pages.performance.column.duration', 'Duration'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.performance.column.duration', 'Duration'),\n cell: (log) =>\n log.durationMin != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatDuration(log.durationMin)}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n {\n key: 'rpe',\n header: tr('atheletebridge.pages.performance.column.rpe', 'RPE'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.performance.column.rpe', 'RPE'),\n cell: (log) =>\n log.rpe != null ? <RpeChip rpe={log.rpe} /> : <span className=\"text-text-muted\">—</span>,\n },\n {\n key: 'date',\n header: tr('atheletebridge.pages.performance.column.date', 'Date'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.performance.column.date', 'Date'),\n cell: (log) => (\n <span className=\"tabular-nums text-text-secondary\">{formatDate(log.performedAt)}</span>\n ),\n },\n {\n key: 'status',\n header: tr('atheletebridge.pages.performance.column.status', 'Status'),\n priority: 'primary',\n mobileLabel: tr('atheletebridge.pages.performance.column.status', 'Status'),\n cell: (log) => <StatusBadge status={log.status} />,\n },\n {\n key: 'notes',\n header: tr('atheletebridge.pages.performance.column.notes', 'Notes'),\n priority: 'hidden-on-mobile',\n cell: (log) =>\n log.notes ? (\n <span className=\"line-clamp-2 max-w-[22ch] text-text-secondary\">{log.notes}</span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n ],\n [tr]\n );\n\n return (\n <div className=\"space-y-8 pb-24 sm:pb-0\">\n {/* Actions */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n {tr('atheletebridge.pages.performance.tracking', 'Tracking')}{' '}\n <span className=\"font-medium text-text-primary\">\n {displayName || tr('atheletebridge.pages.performance.athleteFallback', 'athlete')}\n </span>\n {range ? (\n <>\n {' · '}\n <span className=\"tabular-nums text-text-muted\">{windowLabel(range)}</span>\n </>\n ) : null}\n </p>\n <div className=\"flex flex-wrap items-center gap-2\">\n {/* Date-range window — scopes the metric tiles, their deltas and the\n workout history below to a rolling period. */}\n <span className=\"flex items-center gap-2\" data-tour=\"performance-date-range\">\n <CalendarRange className=\"h-4 w-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n <FilterSelect<RangeKey>\n value={rangeKey}\n onChange={(next) => {\n setRangeKey(next);\n setPage(0);\n }}\n options={rangeOptions}\n allLabel={tr('atheletebridge.pages.performance.window.allTime', 'All time')}\n ariaLabel={tr('atheletebridge.pages.performance.window.ariaLabel', 'Date range')}\n />\n </span>\n <button\n type=\"button\"\n onClick={() => setMetricOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Gauge className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.performance.recordMetric', 'Record metric')}\n </button>\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'hidden sm:inline-flex')}\n >\n <Dumbbell className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.performance.logWorkout', 'Log workout')}\n </button>\n </div>\n </div>\n\n {/* Metrics — hero stat tiles with real trend sparklines */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.performance.metricsTitle', 'Performance metrics')}\n </h2>\n {metrics.isLoading ? (\n <LoadingState\n label={tr('atheletebridge.pages.performance.loadingMetrics', 'Loading metrics…')}\n />\n ) : metrics.isError ? (\n <ErrorState onRetry={() => void metrics.refetch()} />\n ) : metricSeries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Gauge className=\"h-10 w-10\" />}\n title={\n range\n ? tr(\n 'atheletebridge.pages.performance.metrics.emptyWindowTitle',\n 'No metrics in this window'\n )\n : tr('atheletebridge.pages.performance.metrics.emptyTitle', 'No metrics recorded')\n }\n description={\n range\n ? tr(\n 'atheletebridge.pages.performance.metrics.emptyWindowDescription',\n 'Nothing was recorded for this athlete in the selected period. Widen the date range, or record a metric.'\n )\n : tr(\n 'atheletebridge.pages.performance.noMetricsDescription',\n 'Record a metric — speed, strength, endurance and more — to start charting progress.'\n )\n }\n action={\n <button type=\"button\" onClick={() => setMetricOpen(true)} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.performance.recordMetricAction', 'Record metric')}\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {metricSeries.map((m) => (\n <MetricTile key={m.key} metric={m} labels={deltaLabels} tr={tr} />\n ))}\n </div>\n )}\n </section>\n\n {/* Workout logs */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.performance.workoutsTitle', 'Workout history')}\n </h2>\n {logs.isLoading ? (\n <LoadingState\n label={tr('atheletebridge.pages.performance.loadingWorkouts', 'Loading workouts…')}\n />\n ) : logs.isError ? (\n <ErrorState onRetry={() => void logs.refetch()} />\n ) : logItems.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title={\n range\n ? tr(\n 'atheletebridge.pages.performance.workouts.emptyWindowTitle',\n 'No workouts in this window'\n )\n : tr('atheletebridge.pages.performance.workouts.emptyTitle', 'No workouts logged')\n }\n description={\n range\n ? tr(\n 'atheletebridge.pages.performance.workouts.emptyWindowDescription',\n 'Nothing was logged for this athlete in the selected period. Widen the date range, or log a workout.'\n )\n : tr(\n 'atheletebridge.pages.performance.noWorkoutsDescription',\n \"Log a workout to build this athlete's training history.\"\n )\n }\n action={\n <button type=\"button\" onClick={() => setLogOpen(true)} className={GRADIENT_CTA}>\n <Dumbbell className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.performance.logWorkout', 'Log workout')}\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"metric\" className=\"overflow-hidden p-1 sm:p-2\">\n <ResponsiveTable\n columns={columns}\n data={logItems}\n rowKey={(log) => log.id}\n caption={tr(\n 'atheletebridge.pages.performance.workouts.caption',\n 'Workout history for {{name}}'\n ).replace(\n '{{name}}',\n displayName || tr('atheletebridge.pages.performance.athleteFallback', 'athlete')\n )}\n />\n </GlassCard>\n <CursorPager\n className=\"mt-6\"\n page={pageIndex}\n hasMore={hasMore}\n onPrev={() => setPage(Math.max(0, pageIndex - 1))}\n onNext={() => setPage(pageIndex + 1)}\n />\n </>\n )}\n {!windowCovered ? (\n <p className=\"mt-3 text-xs text-text-muted\">\n {tr(\n 'atheletebridge.pages.performance.workouts.windowTruncated',\n 'Only the most recent page of workouts was searched, and it does not reach back to the start of this window — older sessions in the range are not listed.'\n )}\n </p>\n ) : null}\n </section>\n\n {/* Mobile sticky Log workout — thumb-reachable primary action */}\n <div className=\"sticky bottom-0 z-30 -mx-6 border-t border-border-seam bg-bg-canvas/85 px-6 pb-[max(env(safe-area-inset-bottom),0.75rem)] pt-3 backdrop-blur sm:hidden\">\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'w-full py-3 text-base')}\n >\n <Plus className=\"h-5 w-5\" />{' '}\n {tr('atheletebridge.pages.performance.logWorkout', 'Log workout')}\n </button>\n </div>\n\n <LogWorkoutDialog\n open={logOpen}\n onOpenChange={setLogOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n <RecordMetricDialog\n open={metricOpen}\n onOpenChange={setMetricOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n </div>\n );\n}\n\n/** Pick a workout row icon from the (real) title text. Cosmetic only. */\nfunction workoutIcon(title: string): LucideIcon {\n const t = title.toLowerCase();\n if (/strength|lift|squat|deadlift|bench|1rm|weight/.test(t)) return Dumbbell;\n if (/speed|sprint|agility|accel|split/.test(t)) return Zap;\n if (/endurance|aerobic|cardio|run|beep|yo-?yo/.test(t)) return Activity;\n if (/recovery|mobility|stretch|rest/.test(t)) return HeartPulse;\n if (/accuracy|shoot|target/.test(t)) return Target;\n return Dumbbell;\n}\n\n/** Bordered RPE chip, color-scaled by session intensity (semantic tokens). */\nfunction RpeChip({ rpe }: { rpe: number }) {\n const tone =\n rpe >= 8\n ? 'border-status-error-border text-status-error-text'\n : rpe >= 5\n ? 'border-status-warning-border text-status-warning-text'\n : 'border-border-seam text-text-secondary';\n return (\n <span\n className={cn(\n 'inline-flex h-7 min-w-[1.75rem] items-center justify-center rounded-md border px-1.5 text-sm font-semibold tabular-nums',\n tone\n )}\n >\n {rpe}\n </span>\n );\n}\n\n/** Hero stat tile for one measured metric: big value, trend sparkline, range. */\nfunction MetricTile({\n metric,\n labels,\n tr,\n}: {\n metric: MetricSeries;\n labels: DeltaLabels;\n tr: TranslateFn;\n}) {\n const Icon = METRIC_ICON[metric.type];\n const delta = metric.previous == null ? null : metric.latest - metric.previous;\n const hasRange = metric.max > metric.min;\n const pct = hasRange\n ? Math.round(((metric.latest - metric.min) / (metric.max - metric.min)) * 100)\n : 100;\n const unitSuffix = metric.unit ? ` ${metric.unit}` : '';\n\n const metricTypeLabel = (t: AthMetricType): string => {\n switch (t) {\n case 'SPEED':\n return tr('atheletebridge.metricType.speed', 'Speed');\n case 'ENDURANCE':\n return tr('atheletebridge.metricType.endurance', 'Endurance');\n case 'STRENGTH':\n return tr('atheletebridge.metricType.strength', 'Strength');\n case 'AGILITY':\n return tr('atheletebridge.metricType.agility', 'Agility');\n case 'ACCURACY':\n return tr('atheletebridge.metricType.accuracy', 'Accuracy');\n case 'WEIGHT':\n return tr('atheletebridge.metricType.weight', 'Weight');\n case 'HEART_RATE':\n return tr('atheletebridge.metricType.heartRate', 'Heart Rate');\n case 'CUSTOM':\n return tr('atheletebridge.metricType.custom', 'Custom');\n }\n };\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n {metricTypeLabel(metric.type)}\n </span>\n <span className=\"text-xs text-text-muted\">{formatDate(metric.recordedAt)}</span>\n </div>\n\n <div className=\"flex items-end justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-secondary\">{metric.label}</p>\n <p className=\"mt-0.5 text-3xl font-bold leading-none tracking-tight tabular-nums text-text-primary\">\n {fmtNum(metric.latest)}\n {metric.unit ? (\n <span className=\"ml-1 text-base font-medium text-text-secondary\">{metric.unit}</span>\n ) : null}\n </p>\n </div>\n {metric.series.length > 1 ? (\n <div className=\"w-24 shrink-0 sm:w-28\">\n <Sparkline\n data={metric.series}\n variant=\"area\"\n height={40}\n label={`${metric.label} trend`}\n />\n </div>\n ) : null}\n </div>\n\n {/* Range scale: min → latest position → max (from the athlete's own series) */}\n {hasRange ? (\n <div>\n <span\n className=\"block h-1.5 overflow-hidden rounded-full bg-bg-sunken\"\n aria-hidden=\"true\"\n >\n <span\n className=\"block h-1.5 rounded-full bg-[var(--color-accent-teal)]\"\n style={{ width: `${Math.max(4, Math.min(100, pct))}%` }}\n />\n </span>\n <div className=\"mt-1 flex justify-between text-[0.7rem] tabular-nums text-text-muted\">\n <span>{fmtNum(metric.min)}</span>\n <span>{fmtNum((metric.min + metric.max) / 2)}</span>\n <span>{fmtNum(metric.max)}</span>\n </div>\n </div>\n ) : null}\n\n {/* Delta against the active comparison period — direction shown neutrally\n (no good/bad claim), and absent rather than guessed when there is no\n reading to compare against. */}\n <div className=\"mt-auto flex items-center gap-1.5 text-sm\">\n {delta == null ? (\n <span className=\"text-text-muted\">{labels.noComparison}</span>\n ) : delta === 0 ? (\n <span className=\"text-text-muted\">\n {tr('atheletebridge.pages.performance.window.noChange', 'No change')}{' '}\n {labels.comparison}\n </span>\n ) : (\n <>\n <span className=\"inline-flex items-center gap-0.5 font-medium tabular-nums text-text-secondary\">\n {delta > 0 ? (\n <ArrowUp className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <ArrowDown className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n {fmtNum(Math.abs(delta))}\n {unitSuffix}\n </span>\n <span className=\"text-text-muted\">{labels.comparison}</span>\n </>\n )}\n </div>\n </GlassCard>\n );\n}\n\ninterface DialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n athleteId: string;\n displayName: string;\n}\n\nfunction LogWorkoutDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const tr = useAbTranslate();\n const logWorkout = useLogWorkout();\n const [title, setTitle] = useState('');\n const [status, setStatus] = useState<AthWorkoutStatus>('COMPLETED');\n const [performedAt, setPerformedAt] = useState(nowLocal);\n const [durationMin, setDurationMin] = useState('');\n const [rpe, setRpe] = useState('');\n const [totalLoad, setTotalLoad] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setStatus('COMPLETED');\n setPerformedAt(nowLocal());\n setDurationMin('');\n setRpe('');\n setTotalLoad('');\n setNotes('');\n setError(null);\n };\n\n const statusLabel = (s: AthWorkoutStatus): string => {\n switch (s) {\n case 'COMPLETED':\n return tr('atheletebridge.workoutStatus.completed', 'Completed');\n case 'PLANNED':\n return tr('atheletebridge.workoutStatus.planned', 'Planned');\n case 'SKIPPED':\n return tr('atheletebridge.workoutStatus.skipped', 'Skipped');\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!title.trim()) {\n setError(\n tr('atheletebridge.pages.performance.error.titleRequired', 'A workout title is required.')\n );\n return;\n }\n if (!performedAt) {\n setError(\n tr(\n 'atheletebridge.pages.performance.error.performedAtRequired',\n 'A performed date and time is required.'\n )\n );\n return;\n }\n try {\n await logWorkout.mutateAsync({\n athleteId,\n title: title.trim(),\n status,\n performedAt: new Date(performedAt).toISOString(),\n durationMin: numOrUndefined(durationMin),\n rpe: numOrUndefined(rpe),\n totalLoad: floatOrUndefined(totalLoad),\n notes: notes.trim() || undefined,\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr('atheletebridge.pages.performance.error.logFailed', 'Failed to log workout.')\n );\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {tr('atheletebridge.pages.performance.logWorkoutDialogTitle', 'Log workout')}\n {displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <FormField\n label={tr('atheletebridge.pages.performance.label.title', 'Title')}\n htmlFor=\"workoutTitle\"\n required\n >\n <input\n id=\"workoutTitle\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder={tr(\n 'atheletebridge.pages.performance.placeholder.title',\n 'e.g. Lower-body strength'\n )}\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField\n label={tr('atheletebridge.pages.performance.label.status', 'Status')}\n htmlFor=\"workoutStatus\"\n >\n <select\n id=\"workoutStatus\"\n className={fieldInputClass}\n value={status}\n onChange={(e) => setStatus(e.target.value as AthWorkoutStatus)}\n >\n {WORKOUT_STATUSES.map((s) => (\n <option key={s} value={s}>\n {statusLabel(s)}\n </option>\n ))}\n </select>\n </FormField>\n <FormField\n label={tr('atheletebridge.pages.performance.label.performedAt', 'Performed at')}\n htmlFor=\"workoutPerformedAt\"\n required\n >\n <input\n id=\"workoutPerformedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={performedAt}\n onChange={(e) => setPerformedAt(e.target.value)}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <FormField\n label={tr('atheletebridge.pages.performance.label.durationMin', 'Duration (min)')}\n htmlFor=\"workoutDuration\"\n >\n <input\n id=\"workoutDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </FormField>\n <FormField\n label={tr('atheletebridge.pages.performance.label.rpe', 'RPE (1–10)')}\n htmlFor=\"workoutRpe\"\n >\n <input\n id=\"workoutRpe\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rpe}\n onChange={(e) => setRpe(e.target.value)}\n />\n </FormField>\n <FormField\n label={tr('atheletebridge.pages.performance.label.totalLoad', 'Total load')}\n htmlFor=\"workoutLoad\"\n >\n <input\n id=\"workoutLoad\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={totalLoad}\n onChange={(e) => setTotalLoad(e.target.value)}\n />\n </FormField>\n </div>\n <FormField\n label={tr('atheletebridge.pages.performance.label.notes', 'Notes')}\n htmlFor=\"workoutNotes\"\n >\n <textarea\n id=\"workoutNotes\"\n rows={2}\n className={fieldInputClass}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder={tr(\n 'atheletebridge.pages.performance.notesPlaceholder',\n 'How did the session go?'\n )}\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={logWorkout.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {logWorkout.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {tr('atheletebridge.pages.performance.logWorkout', 'Log workout')}\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction RecordMetricDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const tr = useAbTranslate();\n const recordMetric = useRecordMetric();\n const [type, setType] = useState<AthMetricType>('CUSTOM');\n const [label, setLabel] = useState('');\n const [value, setValue] = useState('');\n const [unit, setUnit] = useState('');\n const [source, setSource] = useState('');\n const [recordedAt, setRecordedAt] = useState(nowLocal);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setType('CUSTOM');\n setLabel('');\n setValue('');\n setUnit('');\n setSource('');\n setRecordedAt(nowLocal());\n setError(null);\n };\n\n const metricTypeLabel = (t: AthMetricType): string => {\n switch (t) {\n case 'SPEED':\n return tr('atheletebridge.metricType.speed', 'Speed');\n case 'ENDURANCE':\n return tr('atheletebridge.metricType.endurance', 'Endurance');\n case 'STRENGTH':\n return tr('atheletebridge.metricType.strength', 'Strength');\n case 'AGILITY':\n return tr('atheletebridge.metricType.agility', 'Agility');\n case 'ACCURACY':\n return tr('atheletebridge.metricType.accuracy', 'Accuracy');\n case 'WEIGHT':\n return tr('atheletebridge.metricType.weight', 'Weight');\n case 'HEART_RATE':\n return tr('atheletebridge.metricType.heartRate', 'Heart Rate');\n case 'CUSTOM':\n return tr('atheletebridge.metricType.custom', 'Custom');\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!label.trim()) {\n setError(\n tr(\n 'atheletebridge.pages.performance.error.metricLabelRequired',\n 'A metric label is required.'\n )\n );\n return;\n }\n const numericValue = floatOrUndefined(value);\n if (numericValue === undefined) {\n setError(\n tr(\n 'atheletebridge.pages.performance.error.metricValueRequired',\n 'A numeric value is required.'\n )\n );\n return;\n }\n if (!recordedAt) {\n setError(\n tr(\n 'atheletebridge.pages.performance.error.recordedAtRequired',\n 'A recorded date and time is required.'\n )\n );\n return;\n }\n try {\n await recordMetric.mutateAsync({\n athleteId,\n type,\n label: label.trim(),\n value: numericValue,\n unit: unit.trim() || undefined,\n source: source.trim() || undefined,\n recordedAt: new Date(recordedAt).toISOString(),\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr('atheletebridge.pages.performance.error.recordFailed', 'Failed to record metric.')\n );\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {tr('atheletebridge.pages.performance.recordMetricDialogTitle', 'Record metric')}\n {displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField\n label={tr('atheletebridge.pages.performance.label.metricType', 'Metric type')}\n htmlFor=\"metricType\"\n >\n <select\n id=\"metricType\"\n className={fieldInputClass}\n value={type}\n onChange={(e) => setType(e.target.value as AthMetricType)}\n >\n {METRIC_TYPES.map((t) => (\n <option key={t} value={t}>\n {metricTypeLabel(t)}\n </option>\n ))}\n </select>\n </FormField>\n <FormField\n label={tr('atheletebridge.pages.performance.label.recordedAt', 'Recorded at')}\n htmlFor=\"metricRecordedAt\"\n required\n >\n <input\n id=\"metricRecordedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={recordedAt}\n onChange={(e) => setRecordedAt(e.target.value)}\n />\n </FormField>\n </div>\n <FormField\n label={tr('atheletebridge.pages.performance.label.metricLabel', 'Label')}\n htmlFor=\"metricLabel\"\n required\n >\n <input\n id=\"metricLabel\"\n className={fieldInputClass}\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder={tr(\n 'atheletebridge.pages.performance.placeholder.metricLabel',\n 'e.g. 40m sprint'\n )}\n />\n </FormField>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField\n label={tr('atheletebridge.pages.performance.label.value', 'Value')}\n htmlFor=\"metricValue\"\n required\n >\n <input\n id=\"metricValue\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder={tr('atheletebridge.pages.performance.placeholder.value', 'e.g. 4.8')}\n />\n </FormField>\n <FormField\n label={tr('atheletebridge.pages.performance.label.unit', 'Unit')}\n htmlFor=\"metricUnit\"\n >\n <input\n id=\"metricUnit\"\n className={fieldInputClass}\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n placeholder={tr(\n 'atheletebridge.pages.performance.placeholder.unit',\n 'e.g. s, kg, bpm'\n )}\n />\n </FormField>\n </div>\n <FormField\n label={tr('atheletebridge.pages.performance.label.source', 'Source')}\n htmlFor=\"metricSource\"\n hint={tr(\n 'atheletebridge.pages.performance.hint.source',\n 'Optional — device, test or coach.'\n )}\n >\n <input\n id=\"metricSource\"\n className={fieldInputClass}\n value={source}\n onChange={(e) => setSource(e.target.value)}\n placeholder={tr(\n 'atheletebridge.pages.performance.placeholder.source',\n 'e.g. GPS vest'\n )}\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={recordMetric.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {recordMetric.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n {tr('atheletebridge.pages.performance.recordMetric', 'Record metric')}\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProgramDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/programs/ProgramDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAkFH,wBAAgB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"ProgramDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/programs/ProgramDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAkFH,wBAAgB,iBAAiB,gCAuchC"}
|