@burdenoff/microfe-atheletebridge 2026.829.1 → 2026.830.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atheletebridge/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":"LoadingState.d.ts","sourceRoot":"","sources":["../../../src/atheletebridge/components/LoadingState.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAKhC,UAAU,iBAAiB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CAY9C,CAAC;AAEF,UAAU,eAAe;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,
|
|
1
|
+
{"version":3,"file":"LoadingState.d.ts","sourceRoot":"","sources":["../../../src/atheletebridge/components/LoadingState.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAKhC,UAAU,iBAAiB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CAY9C,CAAC;AAEF,UAAU,eAAe;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,CA2B1C,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LoadingState.js","names":[],"sources":["../../../src/atheletebridge/components/LoadingState.tsx"],"sourcesContent":["/**\n * Loading / Error State Components\n * @module atheletebridge/components\n *\n * Small calm, centered states shared across list and detail pages.\n */\n\nimport type { FC } from 'react';\nimport { AlertCircle, Loader2 } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { useAbTranslate } from '../i18n';\n\ninterface LoadingStateProps {\n label?: string;\n className?: string;\n}\n\nexport const LoadingState: FC<LoadingStateProps> = ({ label, className }) => {\n const tr = useAbTranslate();\n return (\n <div\n className={cn('flex min-h-[40vh] flex-col items-center justify-center gap-3', className)}\n role=\"status\"\n aria-live=\"polite\"\n >\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n <p className=\"text-sm text-text-secondary\">{label ?? tr('common.loading', 'Loading…')}</p>\n </div>\n );\n};\n\ninterface ErrorStateProps {\n title?: string;\n message?: string;\n onRetry?: () => void;\n className?: string;\n}\n\nexport const ErrorState: FC<ErrorStateProps> = ({
|
|
1
|
+
{"version":3,"file":"LoadingState.js","names":[],"sources":["../../../src/atheletebridge/components/LoadingState.tsx"],"sourcesContent":["/**\n * Loading / Error State Components\n * @module atheletebridge/components\n *\n * Small calm, centered states shared across list and detail pages.\n */\n\nimport type { FC } from 'react';\nimport { AlertCircle, Loader2 } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { useAbTranslate } from '../i18n';\n\ninterface LoadingStateProps {\n label?: string;\n className?: string;\n}\n\nexport const LoadingState: FC<LoadingStateProps> = ({ label, className }) => {\n const tr = useAbTranslate();\n return (\n <div\n className={cn('flex min-h-[40vh] flex-col items-center justify-center gap-3', className)}\n role=\"status\"\n aria-live=\"polite\"\n >\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n <p className=\"text-sm text-text-secondary\">{label ?? tr('common.loading', 'Loading…')}</p>\n </div>\n );\n};\n\ninterface ErrorStateProps {\n title?: string;\n message?: string;\n onRetry?: () => void;\n className?: string;\n}\n\nexport const ErrorState: FC<ErrorStateProps> = ({ title, message, onRetry, className }) => {\n const tr = useAbTranslate();\n return (\n <div\n className={cn(\n 'flex min-h-[40vh] flex-col items-center justify-center gap-3 text-center',\n className\n )}\n >\n <AlertCircle className=\"h-10 w-10 text-status-error-text\" />\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {title ?? tr('common.errorTitle', 'Something went wrong')}\n </h3>\n <p className=\"max-w-sm text-sm text-text-secondary\">\n {message ?? tr('common.errorMessage', 'We could not load this data. Please try again.')}\n </p>\n {onRetry && (\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"mt-2 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.tryAgain', 'Try again')}\n </button>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;AAiBA,IAAa,KAAuC,EAAE,UAAO,mBAAgB;CAC3E,IAAM,IAAK,EAAe;CAC1B,OACE,kBAAC,OAAD;EACE,WAAW,EAAG,gEAAgE,CAAS;EACvF,MAAK;EACL,aAAU;YAHZ,CAKE,kBAAC,GAAD,EAAS,WAAU,2CAA4C,CAAA,GAC/D,kBAAC,KAAD;GAAG,WAAU;aAA+B,KAAS,EAAG,kBAAkB,UAAU;EAAK,CAAA,CACtF;;AAET,GASa,KAAmC,EAAE,UAAO,YAAS,YAAS,mBAAgB;CACzF,IAAM,IAAK,EAAe;CAC1B,OACE,kBAAC,OAAD;EACE,WAAW,EACT,4EACA,CACF;YAJF;GAME,kBAAC,GAAD,EAAa,WAAU,mCAAoC,CAAA;GAC3D,kBAAC,MAAD;IAAI,WAAU;cACX,KAAS,EAAG,qBAAqB,sBAAsB;GACtD,CAAA;GACJ,kBAAC,KAAD;IAAG,WAAU;cACV,KAAW,EAAG,uBAAuB,gDAAgD;GACrF,CAAA;GACF,KACC,kBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACT,WAAU;cAET,EAAG,mBAAmB,WAAW;GAC5B,CAAA;EAEP;;AAET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamCard.d.ts","sourceRoot":"","sources":["../../../src/atheletebridge/components/TeamCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAQhC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,UAAU,aAAa;IACrB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,
|
|
1
|
+
{"version":3,"file":"TeamCard.d.ts","sourceRoot":"","sources":["../../../src/atheletebridge/components/TeamCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAQhC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,UAAU,aAAa;IACrB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CAuFtC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamCard.js","names":[],"sources":["../../../src/atheletebridge/components/TeamCard.tsx"],"sourcesContent":["/**\n * Team Card\n * @module atheletebridge/components\n *\n * A team tile fronted by a deterministic gradient monogram crest, with a sport\n * Badge, a StatusBadge (green Active / amber Forming / grey Inactive·Archived),\n * a member-count chip and a hover lift. Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { Users, MapPin, CalendarDays } from 'lucide-react';\nimport { GlassCard, Badge } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { Crest } from './Crest';\nimport { SPORT_ICONS, SPORT_LABELS } from './domain';\nimport { cn } from '../utils/cn';\nimport { useAbTranslate } from '../i18n';\nimport type { AthTeam } from '../types';\n\ninterface TeamCardProps {\n team: AthTeam;\n onClick?: () => void;\n /**\n * Member count for the footer chip. Falls back to `team.members?.length`\n * when the list query selected members; omit the chip entirely if neither is\n * available (never guess).\n */\n memberCount?: number;\n}\n\nexport const TeamCard: FC<TeamCardProps> = ({ team, onClick, memberCount }) => {\n const tr = useAbTranslate();\n const SportIcon = SPORT_ICONS[team.sport];\n const count = memberCount ?? team.members?.items.length;\n\n const memberLabel =\n count === 1\n ? tr('atheletebridge.team.memberCount.one', '{{count}} member').replace('{{count}}'
|
|
1
|
+
{"version":3,"file":"TeamCard.js","names":[],"sources":["../../../src/atheletebridge/components/TeamCard.tsx"],"sourcesContent":["/**\n * Team Card\n * @module atheletebridge/components\n *\n * A team tile fronted by a deterministic gradient monogram crest, with a sport\n * Badge, a StatusBadge (green Active / amber Forming / grey Inactive·Archived),\n * a member-count chip and a hover lift. Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { Users, MapPin, CalendarDays } from 'lucide-react';\nimport { GlassCard, Badge } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { Crest } from './Crest';\nimport { SPORT_ICONS, SPORT_LABELS } from './domain';\nimport { cn } from '../utils/cn';\nimport { useAbTranslate } from '../i18n';\nimport type { AthTeam } from '../types';\n\ninterface TeamCardProps {\n team: AthTeam;\n onClick?: () => void;\n /**\n * Member count for the footer chip. Falls back to `team.members?.length`\n * when the list query selected members; omit the chip entirely if neither is\n * available (never guess).\n */\n memberCount?: number;\n}\n\nexport const TeamCard: FC<TeamCardProps> = ({ team, onClick, memberCount }) => {\n const tr = useAbTranslate();\n const SportIcon = SPORT_ICONS[team.sport];\n const count = memberCount ?? team.members?.items.length;\n\n const memberLabel =\n count === 1\n ? tr('atheletebridge.team.memberCount.one', '{{count}} member').replace(\n '{{count}}',\n String(count)\n )\n : tr('atheletebridge.team.memberCount.other', '{{count}} members').replace(\n '{{count}}',\n String(count ?? 0)\n );\n\n return (\n <GlassCard\n treatment=\"metric\"\n role=\"button\"\n tabIndex={0}\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }}\n className={cn(\n 'cursor-pointer p-5 transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <Crest name={team.name} sport={team.sport} size={44} />\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">{team.name}</h3>\n <div className=\"mt-1\">\n <Badge variant=\"secondary\" className=\"gap-1\">\n <SportIcon className=\"h-3 w-3\" aria-hidden=\"true\" />\n {SPORT_LABELS[team.sport]}\n </Badge>\n </div>\n </div>\n </div>\n <StatusBadge status={team.status} />\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-secondary\">\n {team.ageGroup && (\n <span className=\"flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {team.ageGroup}\n </span>\n )}\n {team.season && (\n <span className=\"flex items-center gap-1\">\n <CalendarDays className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {team.season}\n </span>\n )}\n {team.homeVenue && (\n <span className=\"flex items-center gap-1 truncate\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" /> {team.homeVenue}\n </span>\n )}\n </div>\n\n {team.tags.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {team.tags.slice(0, 4).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {count !== undefined && (\n <div className=\"mt-4 flex items-center gap-1.5 border-t border-border-seam pt-3 text-xs font-medium text-text-secondary\">\n <Users className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {memberLabel}\n </div>\n )}\n </GlassCard>\n );\n};\n"],"mappings":";;;;;;;;;AA8BA,IAAa,KAA+B,EAAE,SAAM,YAAS,qBAAkB;CAC7E,IAAM,IAAK,EAAe,GACpB,IAAY,EAAY,EAAK,QAC7B,IAAQ,KAAe,EAAK,SAAS,MAAM,QAE3C,IACJ,MAAU,IACN,EAAG,uCAAuC,kBAAkB,EAAE,QAC5D,aACA,OAAO,CAAK,CACd,IACA,EAAG,yCAAyC,mBAAmB,EAAE,QAC/D,aACA,OAAO,KAAS,CAAC,CACnB;CAEN,OACE,kBAAC,GAAD;EACE,WAAU;EACV,MAAK;EACL,UAAU;EACD;EACT,YAAY,MAAM;GAChB,CAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,eAAe,GACjB,IAAU;EAEd;EACA,WAAW,EACT,0KACF;YAbF;GAeE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAO,MAAM,EAAK;MAAM,OAAO,EAAK;MAAO,MAAM;KAAK,CAAA,GACtD,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAsD,EAAK;MAAS,CAAA,GAClF,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD;QAAO,SAAQ;QAAY,WAAU;kBAArC,CACE,kBAAC,GAAD;SAAW,WAAU;SAAU,eAAY;QAAQ,CAAA,GAClD,EAAa,EAAK,MACd;;MACJ,CAAA,CACF;OACF;QACL,kBAAC,GAAD,EAAa,QAAQ,EAAK,OAAS,CAAA,CAChC;;GAEL,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,EAAK,YACJ,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD;QAAO,WAAU;QAAc,eAAY;OAAQ,CAAA;OAAC;OAAE,EAAK;MACvD;;KAEP,EAAK,UACJ,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD;QAAc,WAAU;QAAc,eAAY;OAAQ,CAAA;OAAC;OAAE,EAAK;MAC9D;;KAEP,EAAK,aACJ,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD;QAAQ,WAAU;QAAuB,eAAY;OAAQ,CAAA;OAAC;OAAE,EAAK;MACjE;;IAEL;;GAEJ,EAAK,KAAK,SAAS,KAClB,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAK,KAAK,MAAM,GAAG,CAAC,EAAE,KAAK,MAC1B,kBAAC,QAAD;KAEE,WAAU;eAET;IACG,GAJC,CAID,CACP;GACE,CAAA;GAGN,MAAU,KAAA,KACT,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAO,WAAU;KAAc,eAAY;IAAQ,CAAA,GAClD,CACE;;EAEE;;AAEf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/atheletebridge/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsDH,wBAAgB,aAAa,
|
|
1
|
+
{"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/atheletebridge/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsDH,wBAAgB,aAAa,gCAyT5B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/atheletebridge/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * Dashboard / Overview\n * @module atheletebridge/pages\n *\n * The AthleteBridge landing page: a coach command-center that reads live counts\n * and recent items across teams, fixtures, programs and sessions from the real\n * `ath*` API. Elevated to the concept bar: glass teal-stat KPI tiles, a\n * crest-vs-crest upcoming-fixtures rail, and a real (derived) recent-activity\n * timeline — every item mapped to a real record, nothing fabricated.\n */\n\nimport { useMemo } from 'react';\nimport {\n Users,\n CalendarDays,\n Dumbbell,\n ClipboardCheck,\n ArrowRight,\n Plus,\n Rocket,\n Trophy,\n TrendingUp,\n UserPlus,\n CheckCircle2,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { TeamCard } from '../components/TeamCard';\nimport { MatchupRow } from '../components/MatchupRow';\nimport { SessionCard } from '../components/SessionCard';\nimport { EmptyState } from '../components/EmptyState';\nimport { LoadingState } from '../components/LoadingState';\nimport { Timeline, type TimelineItem, type TimelineTone } from '../components/viz';\nimport { useAthleteBridge } from '../context/AthleteBridgeContext';\nimport {\n useTeams,\n useFixtures,\n usePrograms,\n useSessions,\n useRecentActivity,\n type ActivityType,\n} from '../hooks/useAthleteBridgeApi';\nimport { timeAgo } from '../utils/timeAgo';\nimport { useAbTranslate } from '../i18n';\n\n/**\n * Brand gradient CTA — orange action-primary blended toward accent-purple, the\n * same brand gradient the hero title uses. Token-only (no raw colors).\n */\nconst GRADIENT_CTA =\n 'inline-flex items-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-activity-type node styling for the recent-activity timeline. */\nconst ACTIVITY_VISUAL: Record<ActivityType, { tone: TimelineTone; Icon: LucideIcon }> = {\n 'program-published': { tone: 'success', Icon: ClipboardCheck },\n 'member-added': { tone: 'accent', Icon: UserPlus },\n 'fixture-updated': { tone: 'warning', Icon: CalendarDays },\n 'session-completed': { tone: 'info', Icon: CheckCircle2 },\n 'team-created': { tone: 'accent', Icon: Users },\n};\n\nexport function DashboardPage() {\n const tr = useAbTranslate();\n const { navigateTo, currentUser } = useAthleteBridge();\n\n const teams = useTeams({ pagination: { limit: 50 } });\n const fixtures = useFixtures({ pagination: { limit: 50 } });\n const programs = usePrograms({ pagination: { limit: 50 } });\n const sessions = useSessions({ pagination: { limit: 50 } });\n\n const firstName = currentUser?.firstName ?? currentUser?.name ?? 'coach';\n\n const teamItems = useMemo(() => teams.data?.items ?? [], [teams.data]);\n const fixtureItems = useMemo(() => fixtures.data?.items ?? [], [fixtures.data]);\n const programItems = useMemo(() => programs.data?.items ?? [], [programs.data]);\n const sessionItems = useMemo(() => sessions.data?.items ?? [], [sessions.data]);\n\n const upcomingFixtures = useMemo(\n () =>\n [...fixtureItems]\n .filter((f) => f.status === 'SCHEDULED' || f.status === 'LIVE')\n .sort((a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime())\n .slice(0, 5),\n [fixtureItems]\n );\n\n const upcomingSessions = useMemo(\n () =>\n [...sessionItems]\n .filter((s) => s.status === 'REQUESTED' || s.status === 'CONFIRMED')\n .sort((a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime())\n .slice(0, 3),\n [sessionItems]\n );\n\n const recentTeams = useMemo(() => teamItems.slice(0, 6), [teamItems]);\n\n const publishedPrograms = programItems.filter((p) => p.status === 'PUBLISHED').length;\n\n const activity = useRecentActivity({\n teams: teamItems,\n fixtures: fixtureItems,\n sessions: sessionItems,\n programs: programItems,\n limit: 6,\n });\n\n const activityTimeline = useMemo<TimelineItem[]>(\n () =>\n activity.map((item) => {\n const visual = ACTIVITY_VISUAL[item.type];\n const Icon = visual.Icon;\n return {\n id: item.id,\n tone: visual.tone,\n icon: <Icon className=\"h-3 w-3\" aria-hidden=\"true\" />,\n title: item.title,\n meta: item.subtitle,\n time: timeAgo(item.at),\n };\n }),\n [activity]\n );\n\n const isLoading =\n teams.isLoading || fixtures.isLoading || programs.isLoading || sessions.isLoading;\n\n if (isLoading) {\n return (\n <PageLayout title=\"AthleteBridge\" hero gradientTitle>\n <LoadingState\n label={tr('atheletebridge.pages.dashboard.loading', 'Loading your sporting operation…')}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={`${tr('atheletebridge.pages.dashboard.welcomeBack', 'Welcome back')}, ${firstName}`}\n description={tr(\n 'atheletebridge.pages.dashboard.description',\n 'Your teams, fixtures, training and coaching — one connected record.'\n )}\n hero\n gradientTitle\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button type=\"button\" onClick={() => navigateTo('/teams/new')} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" /> {tr('atheletebridge.pages.dashboard.newTeam', 'New team')}\n </button>\n <button type=\"button\" onClick={() => navigateTo('/performance')} className={GRADIENT_CTA}>\n <TrendingUp className=\"h-4 w-4\" /> {tr('atheletebridge.pages.dashboard.trackPerformance', 'Track performance')}\n </button>\n </div>\n }\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'atheletebridge.pages.dashboard.purpose',\n 'AthleteBridge is the operating system for sports, coaching and training. From here you can assemble your roster, schedule fixtures, build training programs, run coaching sessions and track athlete performance — all reading and writing the same workspace-scoped source of truth.'\n )}\n </PagePurpose>\n\n {/* KPIs — glass tiles with teal stat values (2-col mobile → 4-col lg) */}\n <div data-tour=\"dashboard-kpis\" className=\"mb-8 grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n icon={<Users className=\"h-5 w-5\" />}\n value={teamItems.length}\n label={tr('atheletebridge.pages.dashboard.kpi.teams', 'Teams')}\n tone=\"accent\"\n featured\n />\n <MetricsCard\n icon={<CalendarDays className=\"h-5 w-5\" />}\n value={fixtureItems.length}\n label={tr('atheletebridge.pages.dashboard.kpi.fixtures', 'Fixtures')}\n tone=\"accent\"\n />\n <MetricsCard\n icon={<Dumbbell className=\"h-5 w-5\" />}\n value={`${publishedPrograms}/${programItems.length}`}\n label={tr('atheletebridge.pages.dashboard.kpi.programsPublished', 'Programs published')}\n tone=\"accent\"\n />\n <MetricsCard\n icon={<ClipboardCheck className=\"h-5 w-5\" />}\n value={sessionItems.length}\n label={tr('atheletebridge.pages.dashboard.kpi.sessions', 'Sessions')}\n tone=\"accent\"\n />\n </div>\n\n {/* Upcoming fixtures (matchup rail) + Recent activity (timeline) */}\n <div className=\"mb-8 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Fixtures — spans two columns on large screens */}\n <section data-tour=\"dashboard-fixtures\" className=\"lg:col-span-2\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.dashboard.upcomingFixtures', 'Upcoming fixtures')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n className=\"flex items-center gap-1 text-sm font-medium text-text-link hover:underline\"\n >\n {tr('atheletebridge.pages.dashboard.viewAllFixtures', 'View all fixtures')}{' '}\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {upcomingFixtures.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<CalendarDays className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.dashboard.noFixtures', 'No upcoming fixtures')}\n description={tr(\n 'atheletebridge.pages.dashboard.noFixturesHint',\n 'Schedule a match to start tracking results and standings.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> {tr('atheletebridge.pages.dashboard.scheduleFixture', 'Schedule fixture')}\n </button>\n }\n />\n ) : (\n <GlassCard treatment=\"glass\" className=\"divide-y divide-border-seam px-4 py-1 sm:px-5\">\n {upcomingFixtures.map((fixture) => (\n <MatchupRow\n key={fixture.id}\n fixture={fixture}\n onClick={() => navigateTo(`/fixtures/${fixture.id}`)}\n />\n ))}\n </GlassCard>\n )}\n </section>\n\n {/* Recent activity */}\n <section data-tour=\"dashboard-activity\">\n <div className=\"mb-4 flex items-center min-h-[1.75rem]\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.dashboard.recentActivity', 'Recent activity')}\n </h2>\n </div>\n {activityTimeline.length === 0 ? (\n <GlassCard treatment=\"metric\" className=\"p-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<TrendingUp className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.dashboard.noRecentActivity', 'No recent activity')}\n description={tr(\n 'atheletebridge.pages.dashboard.noRecentActivityHint',\n 'Create teams, publish programs or record results — the latest moves land here.'\n )}\n />\n </GlassCard>\n ) : (\n <GlassCard treatment=\"metric\" className=\"p-5\">\n <Timeline items={activityTimeline} />\n </GlassCard>\n )}\n </section>\n </div>\n\n {/* Upcoming sessions */}\n <section data-tour=\"dashboard-sessions\" className=\"mb-8\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.dashboard.upcomingSessions', 'Upcoming sessions')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions')}\n className=\"flex items-center gap-1 text-sm font-medium text-text-link hover:underline\"\n >\n {tr('atheletebridge.pages.dashboard.viewAll', 'View all')} <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {upcomingSessions.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<ClipboardCheck className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.dashboard.noSessions', 'No upcoming sessions')}\n description={tr(\n 'atheletebridge.pages.dashboard.noSessionsHint',\n 'Book a coaching session with an athlete to get started.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> {tr('atheletebridge.pages.dashboard.bookSession', 'Book session')}\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {upcomingSessions.map((session) => (\n <SessionCard\n key={session.id}\n session={session}\n onClick={() => navigateTo(`/sessions/${session.id}`)}\n />\n ))}\n </div>\n )}\n </section>\n\n {/* Teams */}\n <section data-tour=\"dashboard-teams\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.dashboard.yourTeams', 'Your teams')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams')}\n className=\"flex items-center gap-1 text-sm font-medium text-text-link hover:underline\"\n >\n {tr('atheletebridge.pages.dashboard.viewAll', 'View all')} <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {recentTeams.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Trophy className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.dashboard.noTeams', 'No teams yet')}\n description={tr(\n 'atheletebridge.pages.dashboard.noTeamsHint',\n 'Set up AthleteBridge in about a minute — pick your sport, create your first team and add yourself to the roster.'\n )}\n action={\n <div className=\"flex flex-wrap items-center justify-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/onboarding')}\n className={GRADIENT_CTA}\n >\n <Rocket className=\"h-4 w-4\" /> {tr('atheletebridge.pages.dashboard.getStarted', 'Get started')}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams/new')}\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 <Plus className=\"h-4 w-4\" /> {tr('atheletebridge.pages.dashboard.createTeam', 'Create team')}\n </button>\n </div>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {recentTeams.map((team) => (\n <TeamCard key={team.id} team={team} onClick={() => navigateTo(`/teams/${team.id}`)} />\n ))}\n </div>\n )}\n </section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/atheletebridge/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * Dashboard / Overview\n * @module atheletebridge/pages\n *\n * The AthleteBridge landing page: a coach command-center that reads live counts\n * and recent items across teams, fixtures, programs and sessions from the real\n * `ath*` API. Elevated to the concept bar: glass teal-stat KPI tiles, a\n * crest-vs-crest upcoming-fixtures rail, and a real (derived) recent-activity\n * timeline — every item mapped to a real record, nothing fabricated.\n */\n\nimport { useMemo } from 'react';\nimport {\n Users,\n CalendarDays,\n Dumbbell,\n ClipboardCheck,\n ArrowRight,\n Plus,\n Rocket,\n Trophy,\n TrendingUp,\n UserPlus,\n CheckCircle2,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { TeamCard } from '../components/TeamCard';\nimport { MatchupRow } from '../components/MatchupRow';\nimport { SessionCard } from '../components/SessionCard';\nimport { EmptyState } from '../components/EmptyState';\nimport { LoadingState } from '../components/LoadingState';\nimport { Timeline, type TimelineItem, type TimelineTone } from '../components/viz';\nimport { useAthleteBridge } from '../context/AthleteBridgeContext';\nimport {\n useTeams,\n useFixtures,\n usePrograms,\n useSessions,\n useRecentActivity,\n type ActivityType,\n} from '../hooks/useAthleteBridgeApi';\nimport { timeAgo } from '../utils/timeAgo';\nimport { useAbTranslate } from '../i18n';\n\n/**\n * Brand gradient CTA — orange action-primary blended toward accent-purple, the\n * same brand gradient the hero title uses. Token-only (no raw colors).\n */\nconst GRADIENT_CTA =\n 'inline-flex items-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-activity-type node styling for the recent-activity timeline. */\nconst ACTIVITY_VISUAL: Record<ActivityType, { tone: TimelineTone; Icon: LucideIcon }> = {\n 'program-published': { tone: 'success', Icon: ClipboardCheck },\n 'member-added': { tone: 'accent', Icon: UserPlus },\n 'fixture-updated': { tone: 'warning', Icon: CalendarDays },\n 'session-completed': { tone: 'info', Icon: CheckCircle2 },\n 'team-created': { tone: 'accent', Icon: Users },\n};\n\nexport function DashboardPage() {\n const tr = useAbTranslate();\n const { navigateTo, currentUser } = useAthleteBridge();\n\n const teams = useTeams({ pagination: { limit: 50 } });\n const fixtures = useFixtures({ pagination: { limit: 50 } });\n const programs = usePrograms({ pagination: { limit: 50 } });\n const sessions = useSessions({ pagination: { limit: 50 } });\n\n const firstName = currentUser?.firstName ?? currentUser?.name ?? 'coach';\n\n const teamItems = useMemo(() => teams.data?.items ?? [], [teams.data]);\n const fixtureItems = useMemo(() => fixtures.data?.items ?? [], [fixtures.data]);\n const programItems = useMemo(() => programs.data?.items ?? [], [programs.data]);\n const sessionItems = useMemo(() => sessions.data?.items ?? [], [sessions.data]);\n\n const upcomingFixtures = useMemo(\n () =>\n [...fixtureItems]\n .filter((f) => f.status === 'SCHEDULED' || f.status === 'LIVE')\n .sort((a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime())\n .slice(0, 5),\n [fixtureItems]\n );\n\n const upcomingSessions = useMemo(\n () =>\n [...sessionItems]\n .filter((s) => s.status === 'REQUESTED' || s.status === 'CONFIRMED')\n .sort((a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime())\n .slice(0, 3),\n [sessionItems]\n );\n\n const recentTeams = useMemo(() => teamItems.slice(0, 6), [teamItems]);\n\n const publishedPrograms = programItems.filter((p) => p.status === 'PUBLISHED').length;\n\n const activity = useRecentActivity({\n teams: teamItems,\n fixtures: fixtureItems,\n sessions: sessionItems,\n programs: programItems,\n limit: 6,\n });\n\n const activityTimeline = useMemo<TimelineItem[]>(\n () =>\n activity.map((item) => {\n const visual = ACTIVITY_VISUAL[item.type];\n const Icon = visual.Icon;\n return {\n id: item.id,\n tone: visual.tone,\n icon: <Icon className=\"h-3 w-3\" aria-hidden=\"true\" />,\n title: item.title,\n meta: item.subtitle,\n time: timeAgo(item.at),\n };\n }),\n [activity]\n );\n\n const isLoading =\n teams.isLoading || fixtures.isLoading || programs.isLoading || sessions.isLoading;\n\n if (isLoading) {\n return (\n <PageLayout title=\"AthleteBridge\" hero gradientTitle>\n <LoadingState\n label={tr('atheletebridge.pages.dashboard.loading', 'Loading your sporting operation…')}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={`${tr('atheletebridge.pages.dashboard.welcomeBack', 'Welcome back')}, ${firstName}`}\n description={tr(\n 'atheletebridge.pages.dashboard.description',\n 'Your teams, fixtures, training and coaching — one connected record.'\n )}\n hero\n gradientTitle\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button type=\"button\" onClick={() => navigateTo('/teams/new')} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" /> {tr('atheletebridge.pages.dashboard.newTeam', 'New team')}\n </button>\n <button type=\"button\" onClick={() => navigateTo('/performance')} className={GRADIENT_CTA}>\n <TrendingUp className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.dashboard.trackPerformance', 'Track performance')}\n </button>\n </div>\n }\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'atheletebridge.pages.dashboard.purpose',\n 'AthleteBridge is the operating system for sports, coaching and training. From here you can assemble your roster, schedule fixtures, build training programs, run coaching sessions and track athlete performance — all reading and writing the same workspace-scoped source of truth.'\n )}\n </PagePurpose>\n\n {/* KPIs — glass tiles with teal stat values (2-col mobile → 4-col lg) */}\n <div data-tour=\"dashboard-kpis\" className=\"mb-8 grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n icon={<Users className=\"h-5 w-5\" />}\n value={teamItems.length}\n label={tr('atheletebridge.pages.dashboard.kpi.teams', 'Teams')}\n tone=\"accent\"\n featured\n />\n <MetricsCard\n icon={<CalendarDays className=\"h-5 w-5\" />}\n value={fixtureItems.length}\n label={tr('atheletebridge.pages.dashboard.kpi.fixtures', 'Fixtures')}\n tone=\"accent\"\n />\n <MetricsCard\n icon={<Dumbbell className=\"h-5 w-5\" />}\n value={`${publishedPrograms}/${programItems.length}`}\n label={tr('atheletebridge.pages.dashboard.kpi.programsPublished', 'Programs published')}\n tone=\"accent\"\n />\n <MetricsCard\n icon={<ClipboardCheck className=\"h-5 w-5\" />}\n value={sessionItems.length}\n label={tr('atheletebridge.pages.dashboard.kpi.sessions', 'Sessions')}\n tone=\"accent\"\n />\n </div>\n\n {/* Upcoming fixtures (matchup rail) + Recent activity (timeline) */}\n <div className=\"mb-8 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Fixtures — spans two columns on large screens */}\n <section data-tour=\"dashboard-fixtures\" className=\"lg:col-span-2\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.dashboard.upcomingFixtures', 'Upcoming fixtures')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n className=\"flex items-center gap-1 text-sm font-medium text-text-link hover:underline\"\n >\n {tr('atheletebridge.pages.dashboard.viewAllFixtures', 'View all fixtures')}{' '}\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {upcomingFixtures.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<CalendarDays className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.dashboard.noFixtures', 'No upcoming fixtures')}\n description={tr(\n 'atheletebridge.pages.dashboard.noFixturesHint',\n 'Schedule a match to start tracking results and standings.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.dashboard.scheduleFixture', 'Schedule fixture')}\n </button>\n }\n />\n ) : (\n <GlassCard treatment=\"glass\" className=\"divide-y divide-border-seam px-4 py-1 sm:px-5\">\n {upcomingFixtures.map((fixture) => (\n <MatchupRow\n key={fixture.id}\n fixture={fixture}\n onClick={() => navigateTo(`/fixtures/${fixture.id}`)}\n />\n ))}\n </GlassCard>\n )}\n </section>\n\n {/* Recent activity */}\n <section data-tour=\"dashboard-activity\">\n <div className=\"mb-4 flex items-center min-h-[1.75rem]\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.dashboard.recentActivity', 'Recent activity')}\n </h2>\n </div>\n {activityTimeline.length === 0 ? (\n <GlassCard treatment=\"metric\" className=\"p-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<TrendingUp className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.dashboard.noRecentActivity', 'No recent activity')}\n description={tr(\n 'atheletebridge.pages.dashboard.noRecentActivityHint',\n 'Create teams, publish programs or record results — the latest moves land here.'\n )}\n />\n </GlassCard>\n ) : (\n <GlassCard treatment=\"metric\" className=\"p-5\">\n <Timeline items={activityTimeline} />\n </GlassCard>\n )}\n </section>\n </div>\n\n {/* Upcoming sessions */}\n <section data-tour=\"dashboard-sessions\" className=\"mb-8\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.dashboard.upcomingSessions', 'Upcoming sessions')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions')}\n className=\"flex items-center gap-1 text-sm font-medium text-text-link hover:underline\"\n >\n {tr('atheletebridge.pages.dashboard.viewAll', 'View all')}{' '}\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {upcomingSessions.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<ClipboardCheck className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.dashboard.noSessions', 'No upcoming sessions')}\n description={tr(\n 'atheletebridge.pages.dashboard.noSessionsHint',\n 'Book a coaching session with an athlete to get started.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.dashboard.bookSession', 'Book session')}\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {upcomingSessions.map((session) => (\n <SessionCard\n key={session.id}\n session={session}\n onClick={() => navigateTo(`/sessions/${session.id}`)}\n />\n ))}\n </div>\n )}\n </section>\n\n {/* Teams */}\n <section data-tour=\"dashboard-teams\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.dashboard.yourTeams', 'Your teams')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams')}\n className=\"flex items-center gap-1 text-sm font-medium text-text-link hover:underline\"\n >\n {tr('atheletebridge.pages.dashboard.viewAll', 'View all')}{' '}\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {recentTeams.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Trophy className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.dashboard.noTeams', 'No teams yet')}\n description={tr(\n 'atheletebridge.pages.dashboard.noTeamsHint',\n 'Set up AthleteBridge in about a minute — pick your sport, create your first team and add yourself to the roster.'\n )}\n action={\n <div className=\"flex flex-wrap items-center justify-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/onboarding')}\n className={GRADIENT_CTA}\n >\n <Rocket className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.dashboard.getStarted', 'Get started')}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams/new')}\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 <Plus className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.dashboard.createTeam', 'Create team')}\n </button>\n </div>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {recentTeams.map((team) => (\n <TeamCard key={team.id} team={team} onClick={() => navigateTo(`/teams/${team.id}`)} />\n ))}\n </div>\n )}\n </section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoachAnalyticsPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/analytics/CoachAnalyticsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;
|
|
1
|
+
{"version":3,"file":"CoachAnalyticsPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/analytics/CoachAnalyticsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA0GH,wBAAgB,kBAAkB,gCAkZjC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoachAnalyticsPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/analytics/CoachAnalyticsPage.tsx"],"sourcesContent":["/**\n * Coach Analytics\n * @module atheletebridge/pages/analytics\n *\n * An honest coaching-analytics dashboard built ENTIRELY from real aggregates via\n * `useCoachAnalytics()` — total sessions, average session rating, completion\n * rate, session-status mix, average rating by coach, weekly training-load /\n * session trends, a per-athlete acute:chronic training-load table, and a\n * league-standings snapshot.\n *\n * Deliberately OMITS the mock's fabricated composite indices (Form Index,\n * Fitness Score, Fatigue Index, Injury Status, Availability %) — there is no\n * wellness / injury / availability data in the schema, so those are not faked.\n * The one derived metric shown (ACWR training load) uses standard sports-science\n * maths over real `workout.totalLoad` and carries an explanatory tooltip — both\n * workspace-wide and, in the per-athlete table, grouped by each log's real\n * `athleteId`. Where an athlete's workouts carry no recorded load the table\n * says so rather than showing a fabricated zero.\n * Semantic tokens only; responsive.\n */\n\nimport { useMemo } from 'react';\nimport { Activity, BarChart3, CheckCircle2, Info, ListChecks, Star, Users } from 'lucide-react';\nimport {\n Badge,\n GlassCard,\n GradientText,\n ResponsiveTable,\n type ResponsiveTableColumn,\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { Crest } from '../../components/Crest';\nimport { UserAvatar } from '../../components/UserAvatar';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { LineChart, BarChart, DonutChart, RatingStars, STATUS } from '../../components/viz';\nimport { humanizeEnum } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useAbTranslate, type TranslateFn } from '../../i18n';\nimport {\n useCoachAnalytics,\n useUserDirectory,\n type AthleteTrainingLoad,\n type UserDirectory,\n} from '../../hooks/useAthleteBridgeApi';\nimport type { AthSessionStatus, AthStanding } from '../../types';\n\n/** Tokenized color per session status for the status donut. */\nconst STATUS_COLOR: Record<AthSessionStatus, string> = {\n COMPLETED: STATUS.success,\n CONFIRMED: STATUS.info,\n REQUESTED: STATUS.warning,\n CANCELLED: STATUS.danger,\n NO_SHOW: 'var(--color-text-muted)',\n};\n\n/** ACWR banding (acute:chronic workload ratio) — standard sweet-spot 0.8–1.3. */\nfunction acwrBand(ratio: number | null, tr: TranslateFn): {\n label: string;\n variant: 'default' | 'secondary' | 'destructive';\n} {\n if (ratio == null) {\n return { label: tr('atheletebridge.pages.analytics.acwr.noData', 'No data'), variant: 'secondary' };\n }\n if (ratio > 1.5) {\n return { label: tr('atheletebridge.pages.analytics.acwr.atRisk', 'At risk'), variant: 'destructive' };\n }\n if (ratio < 0.8 || ratio > 1.3) {\n return { label: tr('atheletebridge.pages.analytics.acwr.watch', 'Watch'), variant: 'secondary' };\n }\n return { label: tr('atheletebridge.pages.analytics.acwr.optimal', 'Optimal'), variant: 'default' };\n}\n\n/**\n * Short person display: the resolved directory name, else a monogram-friendly\n * `<role> <id fragment>` so an unresolvable id still reads as a person rather\n * than as a raw uuid.\n */\nfunction shortName(name: string | undefined, id: string, rolePrefix: string): string {\n if (name && name.trim()) {\n return name;\n }\n return `${rolePrefix} ${id.slice(0, 4)}`;\n}\n\n/** Whole-number load display; loads are arbitrary units, so no decimals. */\nconst fmtLoad = (value: number): string => Math.round(value).toLocaleString();\n\n/**\n * A load bar scaled against the heaviest load in the table, so acute and\n * chronic columns stay visually comparable to each other and across rows.\n * Purely decorative — the number beside it is the fact.\n */\nfunction LoadBar({ value, max }: { value: number; max: number }) {\n const pct = max > 0 ? Math.round((value / max) * 100) : 0;\n return (\n <span className=\"block h-1.5 w-16 overflow-hidden rounded-full bg-bg-sunken\" aria-hidden=\"true\">\n <span\n className=\"block h-1.5 rounded-full bg-[var(--color-accent-teal)]\"\n style={{ width: `${Math.max(value > 0 ? 4 : 0, Math.min(100, pct))}%` }}\n />\n </span>\n );\n}\n\nexport function CoachAnalyticsPage() {\n const tr = useAbTranslate();\n const { navigateTo } = useAthleteBridge();\n const analytics = useCoachAnalytics();\n // One directory round-trip for both surfaces — `useUsersByIds` dedupes/sorts.\n const directoryIds = useMemo(\n () => [...analytics.coachIds, ...analytics.athleteIds],\n [analytics.coachIds, analytics.athleteIds]\n );\n const directory = useUserDirectory(directoryIds);\n\n const statusSegments = useMemo(\n () =>\n analytics.sessionsByStatus.map((entry) => ({\n label: humanizeEnum(entry.status),\n value: entry.count,\n color: STATUS_COLOR[entry.status],\n })),\n [analytics.sessionsByStatus]\n );\n\n const coachBars = useMemo(\n () =>\n analytics.ratingByCoach.map((entry) => ({\n label: shortName(directory.nameOf(entry.coachId), entry.coachId, tr('atheletebridge.rolePrefix.coach', 'Coach')),\n value: Number(entry.avgRating.toFixed(2)),\n })),\n [analytics.ratingByCoach, directory, tr]\n );\n\n const standingsColumns: ResponsiveTableColumn<AthStanding>[] = [\n {\n key: 'rank',\n header: tr('atheletebridge.pages.analytics.standings.rank', '#'),\n priority: 'secondary',\n cell: (row, i) => (\n <span className=\"tabular-nums text-text-secondary\">{row.rank ?? i + 1}</span>\n ),\n },\n {\n key: 'team',\n header: tr('atheletebridge.pages.analytics.standings.team', 'Team'),\n priority: 'primary',\n cell: (row) => (\n <span className=\"flex items-center gap-2\">\n <Crest\n name={row.team?.name ?? tr('atheletebridge.pages.analytics.standings.teamFallback', 'Team')}\n sport={row.team?.sport}\n size={20}\n showSportGlyph={false}\n />\n <span className=\"truncate font-medium text-text-primary\">{row.team?.name ?? tr('atheletebridge.pages.analytics.standings.teamFallback', 'Team')}</span>\n </span>\n ),\n },\n {\n key: 'played',\n header: tr('atheletebridge.pages.analytics.standings.played', 'P'),\n priority: 'hidden-on-mobile',\n cell: (row) => <span className=\"tabular-nums text-text-secondary\">{row.played}</span>,\n },\n {\n key: 'gd',\n header: tr('atheletebridge.pages.analytics.standings.gd', 'GD'),\n priority: 'hidden-on-mobile',\n cell: (row) => {\n const gd = row.goalsFor - row.goalsAgainst;\n return (\n <span className=\"tabular-nums text-text-secondary\">\n {gd > 0 ? '+' : ''}\n {gd}\n </span>\n );\n },\n },\n {\n key: 'points',\n header: tr('atheletebridge.pages.analytics.standings.points', 'Pts'),\n priority: 'secondary',\n cell: (row) => (\n <span className=\"font-semibold tabular-nums text-text-primary\">{row.points}</span>\n ),\n },\n ];\n\n if (analytics.isLoading) {\n return (\n <PageLayout title={tr('atheletebridge.pages.analytics.title', 'Coach analytics')}>\n <LoadingState label={tr('atheletebridge.pages.analytics.loading', 'Crunching coaching data…')} />\n </PageLayout>\n );\n }\n\n if (analytics.isError) {\n return (\n <PageLayout title={tr('atheletebridge.pages.analytics.title', 'Coach analytics')}>\n <ErrorState\n title={tr('atheletebridge.pages.analytics.errorTitle', 'Analytics unavailable')}\n message={tr('atheletebridge.pages.analytics.errorMessage', 'We could not load coaching analytics. Please try again.')}\n />\n </PageLayout>\n );\n }\n\n const hasData =\n analytics.totalSessions > 0 ||\n analytics.standings.length > 0 ||\n analytics.loadByAthlete.length > 0;\n const band = acwrBand(analytics.trainingLoad.ratio, tr);\n\n return (\n <PageLayout\n title={\n <span className=\"flex items-center gap-2\">\n <GradientText>{tr('atheletebridge.pages.analytics.title', 'Coach analytics')}</GradientText>\n <Badge variant=\"outline\">{tr('common.beta', 'Beta')}</Badge>\n </span>\n }\n description={tr('atheletebridge.pages.analytics.description', 'Honest aggregates over your real sessions, workouts and results.')}\n >\n {!hasData ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<BarChart3 className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.analytics.emptyTitle', 'No coaching data yet')}\n description={tr('atheletebridge.pages.analytics.emptyDescription', 'Book and complete sessions, log workouts and record fixtures to unlock analytics.')}\n />\n ) : (\n <div className=\"space-y-8\">\n {/* KPI row */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <MetricsCard\n icon={<ListChecks className=\"h-5 w-5\" />}\n value={analytics.totalSessions}\n label={tr('atheletebridge.pages.analytics.totalSessions', 'Total sessions')}\n tone=\"accent\"\n />\n <GlassCard treatment=\"metric\" className=\"flex flex-col justify-between gap-3 p-5\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-secondary\">{tr('atheletebridge.pages.analytics.avgRating', 'Avg session rating')}</span>\n <Star className=\"h-5 w-5 text-text-muted\" />\n </div>\n {analytics.avgRating != null ? (\n <div className=\"flex items-end gap-3\">\n <span className=\"text-3xl font-semibold leading-none tabular-nums text-text-primary\">\n {analytics.avgRating.toFixed(1)}\n <span className=\"ml-1 text-base text-text-secondary\">{tr('atheletebridge.pages.analytics.ratingOutOf', '/5')}</span>\n </span>\n <div className=\"pb-1\">\n <RatingStars value={analytics.avgRating} max={5} size={16} />\n </div>\n </div>\n ) : (\n <span className=\"text-sm text-text-muted\">{tr('atheletebridge.pages.analytics.noRatedSessions', 'No rated sessions yet')}</span>\n )}\n <span className=\"text-xs text-text-muted tabular-nums\">\n {analytics.ratedCount}{' '}\n {tr('atheletebridge.pages.analytics.ratedOf', 'rated of')}{' '}\n {analytics.totalSessions}\n </span>\n </GlassCard>\n <MetricsCard\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n value={`${Math.round(analytics.completedPct)}%`}\n label={tr('atheletebridge.pages.analytics.sessionsCompleted', 'Sessions completed')}\n tone=\"success\"\n />\n </div>\n\n {/* Charts row: status donut + rating-by-coach bars */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.analytics.statusOverviewTitle', 'Session status overview')}\n </h2>\n {statusSegments.length > 0 ? (\n <DonutChart\n segments={statusSegments}\n centerLabel={analytics.totalSessions}\n centerCaption={tr('atheletebridge.pages.analytics.sessionsCaption', 'sessions')}\n valueFormatter={(v) => v.toLocaleString()}\n />\n ) : (\n <p className=\"text-sm text-text-muted\">{tr('atheletebridge.pages.analytics.statusOverviewEmpty', 'No sessions to break down yet.')}</p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.analytics.ratingByCoachTitle', 'Average rating by coach')}\n </h2>\n {coachBars.length > 0 ? (\n <>\n <BarChart\n series={coachBars}\n height={220}\n colorful\n valueFormatter={(v) => v.toFixed(1)}\n />\n <div className=\"mt-3 flex flex-wrap gap-x-4 gap-y-2\">\n {analytics.ratingByCoach.map((entry) => (\n <span\n key={entry.coachId}\n className=\"flex items-center gap-2 text-xs text-text-secondary\"\n >\n <UserAvatar\n name={directory.nameOf(entry.coachId)}\n id={entry.coachId}\n size=\"xs\"\n />\n <span className=\"truncate\">\n {shortName(directory.nameOf(entry.coachId), entry.coachId, tr('atheletebridge.rolePrefix.coach', 'Coach'))}\n <span className=\"ml-1 text-text-muted tabular-nums\">({entry.count})</span>\n </span>\n </span>\n ))}\n </div>\n </>\n ) : (\n <p className=\"text-sm text-text-muted\">\n {tr('atheletebridge.pages.analytics.ratingByCoachEmpty', 'Rate completed sessions to compare coaches.')}\n </p>\n )}\n </GlassCard>\n </div>\n\n {/* Trends row: training load + sessions per week */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <div className=\"mb-4 flex items-center justify-between gap-2\">\n <h2 className=\"text-lg font-semibold text-text-primary\">{tr('atheletebridge.pages.analytics.weeklyLoadTitle', 'Weekly training load')}</h2>\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n aria-label={tr('atheletebridge.pages.analytics.acwrAboutLabel', 'About ACWR training load')}\n className=\"rounded-full p-1 text-text-muted transition-colors hover:text-text-primary\"\n >\n <Info className=\"h-4 w-4\" />\n </button>\n </TooltipTrigger>\n <TooltipContent className=\"max-w-xs\">\n {tr(\n 'atheletebridge.pages.analytics.acwrAbout',\n 'Total workout load per week (sum of real totalLoad). The badge is the acute:chronic workload ratio (last 7 days vs 28-day average) — a standard sports-science load indicator; 0.8–1.3 is the typical sweet spot.'\n )}\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n </div>\n {analytics.loadTrend.some((p) => p.value > 0) ? (\n <>\n <LineChart\n series={analytics.loadTrend}\n seriesLabel={tr('atheletebridge.pages.analytics.trainingLoadLabel', 'Training load')}\n height={200}\n valueFormatter={(v) => v.toLocaleString()}\n />\n <div className=\"mt-4 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm\">\n <span className=\"text-text-secondary\">\n {tr('atheletebridge.pages.analytics.acuteLoad', 'Acute (7d)')}{' '}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {Math.round(analytics.trainingLoad.acute).toLocaleString()}\n </span>\n </span>\n <span className=\"text-text-secondary\">\n {tr('atheletebridge.pages.analytics.chronicLoad', 'Chronic (28d avg)')}{' '}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {Math.round(analytics.trainingLoad.chronic).toLocaleString()}\n </span>\n </span>\n <span className=\"flex items-center gap-2 text-text-secondary\">\n {tr('atheletebridge.pages.analytics.acwrAbbreviation', 'ACWR')}\n <Badge variant={band.variant} className=\"tabular-nums\">\n {analytics.trainingLoad.ratio != null\n ? `${analytics.trainingLoad.ratio.toFixed(2)} · ${band.label}`\n : band.label}\n </Badge>\n </span>\n </div>\n </>\n ) : (\n <p className=\"text-sm text-text-muted\">\n {tr('atheletebridge.pages.analytics.weeklyLoadEmpty', 'Log workouts with a load to see training trend.')}\n </p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">{tr('atheletebridge.pages.analytics.sessionsPerWeekTitle', 'Sessions per week')}</h2>\n {analytics.sessionTrend.some((p) => p.value > 0) ? (\n <BarChart\n series={analytics.sessionTrend}\n height={200}\n valueFormatter={(v) => v.toLocaleString()}\n />\n ) : (\n <p className=\"text-sm text-text-muted\">{tr('atheletebridge.pages.analytics.sessionsPerWeekEmpty', 'Schedule sessions to see weekly volume.')}</p>\n )}\n </GlassCard>\n </div>\n\n {/* Training load by athlete — the workspace ACWR, broken out per athlete */}\n <TrainingLoadByAthlete\n rows={analytics.loadByAthlete}\n directory={directory}\n truncated={analytics.loadSampleTruncated}\n tr={tr}\n />\n\n {/* League standings snapshot */}\n {analytics.standings.length > 0 ? (\n <section>\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <h2 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Activity className=\"h-5 w-5 text-text-muted\" />\n {tr('atheletebridge.pages.analytics.standingsTitle', 'League standings')}\n {analytics.competition ? (\n <Badge variant=\"secondary\">{analytics.competition}</Badge>\n ) : null}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/standings')}\n className=\"text-sm font-medium text-[var(--color-accent-teal)] transition-colors hover:underline\"\n >\n {tr('atheletebridge.pages.analytics.viewFullStandings', 'View full standings')}\n </button>\n </div>\n <GlassCard treatment=\"insight\" className=\"p-2 sm:p-4\">\n <ResponsiveTable\n columns={standingsColumns}\n data={analytics.standings}\n rowKey={(row) => row.id}\n />\n </GlassCard>\n </section>\n ) : null}\n </div>\n )}\n </PageLayout>\n );\n}\n\ninterface TrainingLoadByAthleteProps {\n rows: AthleteTrainingLoad[];\n directory: UserDirectory;\n /** The sampled workout page did not reach back 28 days — loads under-count. */\n truncated: boolean;\n tr: TranslateFn;\n}\n\n/**\n * \"Training load by athlete\" — the same acute:chronic maths the workspace-wide\n * ACWR badge already uses, broken out per athlete from each workout log's own\n * `athleteId` + `totalLoad` + `performedAt`.\n *\n * Nothing here is synthesised: an athlete whose 28-day workouts carry no\n * `totalLoad` renders as em-dashes and the shared `acwrBand(null)` \"No data\"\n * badge rather than as a real zero, and when the sampled page stops short of\n * 28 days the section says so instead of presenting a floor as a total.\n */\nfunction TrainingLoadByAthlete({ rows, directory, truncated, tr }: TrainingLoadByAthleteProps) {\n const maxLoad = useMemo(\n () => rows.reduce((max, row) => Math.max(max, row.acute, row.chronic), 0),\n [rows]\n );\n\n const columns: ResponsiveTableColumn<AthleteTrainingLoad>[] = useMemo(\n () => [\n {\n key: 'athlete',\n header: tr('atheletebridge.pages.analytics.loadByAthlete.athlete', 'Athlete'),\n priority: 'primary',\n cell: (row) => (\n <span className=\"flex items-center gap-2\">\n <UserAvatar name={directory.nameOf(row.athleteId)} id={row.athleteId} size=\"xs\" />\n <span className=\"min-w-0\">\n <span className=\"block truncate font-medium text-text-primary\">\n {shortName(directory.nameOf(row.athleteId), row.athleteId, tr('atheletebridge.rolePrefix.athlete', 'Athlete'))}\n </span>\n <span className=\"block text-xs tabular-nums text-text-muted\">\n {row.workoutCount}{' '}\n {tr('atheletebridge.pages.analytics.loadByAthlete.workouts28d', 'workouts · 28d')}\n </span>\n </span>\n </span>\n ),\n },\n {\n key: 'acute',\n header: tr('atheletebridge.pages.analytics.loadByAthlete.acute', 'Acute load (7d)'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.analytics.loadByAthlete.acute', 'Acute load (7d)'),\n cell: (row) =>\n row.loadedCount === 0 ? (\n <span className=\"text-text-muted\">—</span>\n ) : (\n <span className=\"flex items-center gap-2\">\n <span className=\"w-14 text-right font-medium tabular-nums text-text-primary\">\n {fmtLoad(row.acute)}\n </span>\n <LoadBar value={row.acute} max={maxLoad} />\n </span>\n ),\n },\n {\n key: 'chronic',\n header: tr(\n 'atheletebridge.pages.analytics.loadByAthlete.chronic',\n 'Chronic load (28d avg)'\n ),\n priority: 'secondary',\n mobileLabel: tr(\n 'atheletebridge.pages.analytics.loadByAthlete.chronic',\n 'Chronic load (28d avg)'\n ),\n cell: (row) =>\n row.loadedCount === 0 ? (\n <span className=\"text-text-muted\">—</span>\n ) : (\n <span className=\"flex items-center gap-2\">\n <span className=\"w-14 text-right font-medium tabular-nums text-text-primary\">\n {fmtLoad(row.chronic)}\n </span>\n <LoadBar value={row.chronic} max={maxLoad} />\n </span>\n ),\n },\n {\n key: 'ratio',\n header: tr('atheletebridge.pages.analytics.loadByAthlete.ratio', 'Load ratio'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.analytics.loadByAthlete.ratio', 'Load ratio'),\n cell: (row) =>\n row.ratio != null ? (\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {row.ratio.toFixed(2)}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n {\n key: 'band',\n header: tr('atheletebridge.pages.analytics.loadByAthlete.status', 'Status'),\n priority: 'primary',\n mobileLabel: tr('atheletebridge.pages.analytics.loadByAthlete.status', 'Status'),\n cell: (row) => {\n const rowBand = acwrBand(row.ratio, tr);\n return <Badge variant={rowBand.variant}>{rowBand.label}</Badge>;\n },\n },\n ],\n [directory, maxLoad, tr]\n );\n\n return (\n <section>\n <div className=\"mb-4 flex flex-wrap items-center gap-2\">\n <h2 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Users className=\"h-5 w-5 text-text-muted\" />\n {tr('atheletebridge.pages.analytics.loadByAthlete.title', 'Training load by athlete')}\n </h2>\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n aria-label={tr(\n 'atheletebridge.pages.analytics.loadByAthlete.aboutLabel',\n 'About per-athlete training load'\n )}\n className=\"rounded-full p-1 text-text-muted transition-colors hover:text-text-primary\"\n >\n <Info className=\"h-4 w-4\" />\n </button>\n </TooltipTrigger>\n <TooltipContent className=\"max-w-xs\">\n {tr(\n 'atheletebridge.pages.analytics.loadByAthlete.about',\n 'Each athlete’s acute:chronic workload ratio — their last 7 days of logged workout load against their 28-day weekly average. 0.8–1.3 is the typical sweet spot; above 1.5 is a spike worth reviewing.'\n )}\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n </div>\n\n {rows.length === 0 ? (\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <p className=\"text-sm text-text-muted\">\n {tr(\n 'atheletebridge.pages.analytics.loadByAthlete.empty',\n 'No workouts logged in the last 28 days. Log workouts with a total load to compare athletes.'\n )}\n </p>\n </GlassCard>\n ) : (\n <>\n <GlassCard treatment=\"insight\" className=\"p-2 sm:p-4\">\n <ResponsiveTable\n columns={columns}\n data={rows}\n rowKey={(row) => row.athleteId}\n caption={tr(\n 'atheletebridge.pages.analytics.loadByAthlete.caption',\n 'Acute and chronic training load per athlete over the last 28 days'\n )}\n />\n </GlassCard>\n {truncated ? (\n <p className=\"mt-3 text-xs text-text-muted\">\n {tr(\n 'atheletebridge.pages.analytics.loadByAthlete.truncated',\n 'Computed from the most recent page of workout logs, which does not reach back a full 28 days — these loads are a floor, not a total.'\n )}\n </p>\n ) : null}\n </>\n )}\n </section>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqDA,IAAM,IAAiD;CACrD,WAAW,EAAO;CAClB,WAAW,EAAO;CAClB,WAAW,EAAO;CAClB,WAAW,EAAO;CAClB,SAAS;AACX;AAGA,SAAS,EAAS,GAAsB,GAGtC;CAUA,OATI,KAAS,OACJ;EAAE,OAAO,EAAG,8CAA8C,SAAS;EAAG,SAAS;CAAY,IAEhG,IAAQ,MACH;EAAE,OAAO,EAAG,8CAA8C,SAAS;EAAG,SAAS;CAAc,IAElG,IAAQ,MAAO,IAAQ,MAClB;EAAE,OAAO,EAAG,6CAA6C,OAAO;EAAG,SAAS;CAAY,IAE1F;EAAE,OAAO,EAAG,+CAA+C,SAAS;EAAG,SAAS;CAAU;AACnG;AAOA,SAAS,EAAU,GAA0B,GAAY,GAA4B;CAInF,OAHI,KAAQ,EAAK,KAAK,IACb,IAEF,GAAG,EAAW,GAAG,EAAG,MAAM,GAAG,CAAC;AACvC;AAGA,IAAM,KAAW,MAA0B,KAAK,MAAM,CAAK,EAAE,eAAe;AAO5E,SAAS,EAAQ,EAAE,UAAO,UAAuC;CAC/D,IAAM,IAAM,IAAM,IAAI,KAAK,MAAO,IAAQ,IAAO,GAAG,IAAI;CACxD,OACE,kBAAC,QAAD;EAAM,WAAU;EAA6D,eAAY;YACvF,kBAAC,QAAD;GACE,WAAU;GACV,OAAO,EAAE,OAAO,GAAG,KAAK,IAAI,IAAQ,IAAI,IAAI,GAAG,KAAK,IAAI,KAAK,CAAG,CAAC,EAAE,GAAG;EACvE,CAAA;CACG,CAAA;AAEV;AAEA,SAAgB,IAAqB;CACnC,IAAM,IAAK,EAAe,GACpB,EAAE,kBAAe,EAAiB,GAClC,IAAY,EAAkB,GAM9B,IAAY,EAJG,QACb,CAAC,GAAG,EAAU,UAAU,GAAG,EAAU,UAAU,GACrD,CAAC,EAAU,UAAU,EAAU,UAAU,CAER,CAAY,GAEzC,IAAiB,QAEnB,EAAU,iBAAiB,KAAK,OAAW;EACzC,OAAO,EAAa,EAAM,MAAM;EAChC,OAAO,EAAM;EACb,OAAO,EAAa,EAAM;CAC5B,EAAE,GACJ,CAAC,EAAU,gBAAgB,CAC7B,GAEM,IAAY,QAEd,EAAU,cAAc,KAAK,OAAW;EACtC,OAAO,EAAU,EAAU,OAAO,EAAM,OAAO,GAAG,EAAM,SAAS,EAAG,mCAAmC,OAAO,CAAC;EAC/G,OAAO,OAAO,EAAM,UAAU,QAAQ,CAAC,CAAC;CAC1C,EAAE,GACJ;EAAC,EAAU;EAAe;EAAW;CAAE,CACzC,GAEM,IAAyD;EAC7D;GACE,KAAK;GACL,QAAQ,EAAG,iDAAiD,GAAG;GAC/D,UAAU;GACV,OAAO,GAAK,MACV,kBAAC,QAAD;IAAM,WAAU;cAAoC,EAAI,QAAQ,IAAI;GAAQ,CAAA;EAEhF;EACA;GACE,KAAK;GACL,QAAQ,EAAG,iDAAiD,MAAM;GAClE,UAAU;GACV,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACE,kBAAC,GAAD;KACE,MAAM,EAAI,MAAM,QAAQ,EAAG,yDAAyD,MAAM;KAC1F,OAAO,EAAI,MAAM;KACjB,MAAM;KACN,gBAAgB;IACjB,CAAA,GACD,kBAAC,QAAD;KAAM,WAAU;eAA0C,EAAI,MAAM,QAAQ,EAAG,yDAAyD,MAAM;IAAQ,CAAA,CAClJ;;EAEV;EACA;GACE,KAAK;GACL,QAAQ,EAAG,mDAAmD,GAAG;GACjE,UAAU;GACV,OAAO,MAAQ,kBAAC,QAAD;IAAM,WAAU;cAAoC,EAAI;GAAa,CAAA;EACtF;EACA;GACE,KAAK;GACL,QAAQ,EAAG,+CAA+C,IAAI;GAC9D,UAAU;GACV,OAAO,MAAQ;IACb,IAAM,IAAK,EAAI,WAAW,EAAI;IAC9B,OACE,kBAAC,QAAD;KAAM,WAAU;eAAhB,CACG,IAAK,IAAI,MAAM,IACf,CACG;;GAEV;EACF;EACA;GACE,KAAK;GACL,QAAQ,EAAG,mDAAmD,KAAK;GACnE,UAAU;GACV,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAAgD,EAAI;GAAa,CAAA;EAErF;CACF;CAEA,IAAI,EAAU,WACZ,OACE,kBAAC,GAAD;EAAY,OAAO,EAAG,wCAAwC,iBAAiB;YAC7E,kBAAC,GAAD,EAAc,OAAO,EAAG,0CAA0C,0BAA0B,EAAI,CAAA;CACtF,CAAA;CAIhB,IAAI,EAAU,SACZ,OACE,kBAAC,GAAD;EAAY,OAAO,EAAG,wCAAwC,iBAAiB;YAC7E,kBAAC,GAAD;GACE,OAAO,EAAG,6CAA6C,uBAAuB;GAC9E,SAAS,EAAG,+CAA+C,yDAAyD;EACrH,CAAA;CACS,CAAA;CAIhB,IAAM,IACJ,EAAU,gBAAgB,KAC1B,EAAU,UAAU,SAAS,KAC7B,EAAU,cAAc,SAAS,GAC7B,IAAO,EAAS,EAAU,aAAa,OAAO,CAAE;CAEtD,OACE,kBAAC,GAAD;EACE,OACE,kBAAC,QAAD;GAAM,WAAU;aAAhB,CACE,kBAAC,GAAD,EAAA,UAAe,EAAG,wCAAwC,iBAAiB,EAAgB,CAAA,GAC3F,kBAAC,GAAD;IAAO,SAAQ;cAAW,EAAG,eAAe,MAAM;GAAS,CAAA,CACvD;;EAER,aAAa,EAAG,8CAA8C,kEAAkE;YAE9H,IAQA,kBAAC,OAAD;GAAK,WAAU;aAAf;IAEE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD;OACE,MAAM,kBAAC,GAAD,EAAY,WAAU,UAAW,CAAA;OACvC,OAAO,EAAU;OACjB,OAAO,EAAG,gDAAgD,gBAAgB;OAC1E,MAAK;MACN,CAAA;MACD,kBAAC,GAAD;OAAW,WAAU;OAAS,WAAU;iBAAxC;QACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAA2C,EAAG,4CAA4C,oBAAoB;SAAQ,CAAA,GACtI,kBAAC,GAAD,EAAM,WAAU,0BAA2B,CAAA,CACxC;;QACJ,EAAU,aAAa,OAWtB,kBAAC,QAAD;SAAM,WAAU;mBAA2B,EAAG,kDAAkD,uBAAuB;QAAQ,CAAA,IAV/H,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CACG,EAAU,UAAU,QAAQ,CAAC,GAC9B,kBAAC,QAAD;WAAM,WAAU;qBAAsC,EAAG,8CAA8C,IAAI;UAAQ,CAAA,CAC/G;aACN,kBAAC,OAAD;UAAK,WAAU;oBACb,kBAAC,GAAD;WAAa,OAAO,EAAU;WAAW,KAAK;WAAG,MAAM;UAAK,CAAA;SACzD,CAAA,CACF;;QAIP,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UACG,EAAU;UAAY;UACtB,EAAG,0CAA0C,UAAU;UAAG;UAC1D,EAAU;SACP;;OACG;;MACX,kBAAC,GAAD;OACE,MAAM,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA;OACzC,OAAO,GAAG,KAAK,MAAM,EAAU,YAAY,EAAE;OAC7C,OAAO,EAAG,oDAAoD,oBAAoB;OAClF,MAAK;MACN,CAAA;KACE;;IAGL,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAW,WAAU;MAAU,WAAU;gBAAzC,CACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,sDAAsD,yBAAyB;MACjF,CAAA,GACH,EAAe,SAAS,IACvB,kBAAC,GAAD;OACE,UAAU;OACV,aAAa,EAAU;OACvB,eAAe,EAAG,kDAAkD,UAAU;OAC9E,iBAAiB,MAAM,EAAE,eAAe;MACzC,CAAA,IAED,kBAAC,KAAD;OAAG,WAAU;iBAA2B,EAAG,sDAAsD,gCAAgC;MAAK,CAAA,CAE/H;SAEX,kBAAC,GAAD;MAAW,WAAU;MAAU,WAAU;gBAAzC,CACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,qDAAqD,yBAAyB;MAChF,CAAA,GACH,EAAU,SAAS,IAClB,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;OACE,QAAQ;OACR,QAAQ;OACR,UAAA;OACA,iBAAiB,MAAM,EAAE,QAAQ,CAAC;MACnC,CAAA,GACD,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAU,cAAc,KAAK,MAC5B,kBAAC,QAAD;QAEE,WAAU;kBAFZ,CAIE,kBAAC,GAAD;SACE,MAAM,EAAU,OAAO,EAAM,OAAO;SACpC,IAAI,EAAM;SACV,MAAK;QACN,CAAA,GACD,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CACG,EAAU,EAAU,OAAO,EAAM,OAAO,GAAG,EAAM,SAAS,EAAG,mCAAmC,OAAO,CAAC,GACzG,kBAAC,QAAD;UAAM,WAAU;oBAAhB;WAAoD;WAAE,EAAM;WAAM;UAAO;WACrE;UACF;UAZC,EAAM,OAYP,CACP;MACE,CAAA,CACL,EAAA,CAAA,IAEF,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAG,qDAAqD,6CAA6C;MACrG,CAAA,CAEI;OACR;;IAGL,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAW,WAAU;MAAU,WAAU;gBAAzC,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,MAAD;QAAI,WAAU;kBAA2C,EAAG,kDAAkD,sBAAsB;OAAM,CAAA,GAC1I,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;QAAgB,SAAA;kBACd,kBAAC,UAAD;SACE,MAAK;SACL,cAAY,EAAG,iDAAiD,0BAA0B;SAC1F,WAAU;mBAEV,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA;QACrB,CAAA;OACM,CAAA,GAChB,kBAAC,GAAD;QAAgB,WAAU;kBACvB,EACC,4CACA,mNACF;OACc,CAAA,CACT,EAAA,CAAA,EACM,CAAA,CACd;UACJ,EAAU,UAAU,MAAM,MAAM,EAAE,QAAQ,CAAC,IAC1C,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;OACE,QAAQ,EAAU;OAClB,aAAa,EAAG,oDAAoD,eAAe;OACnF,QAAQ;OACR,iBAAiB,MAAM,EAAE,eAAe;MACzC,CAAA,GACD,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UACG,EAAG,4CAA4C,YAAY;UAAG;UAC/D,kBAAC,QAAD;WAAM,WAAU;qBACb,KAAK,MAAM,EAAU,aAAa,KAAK,EAAE,eAAe;UACrD,CAAA;SACF;;QACN,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UACG,EAAG,8CAA8C,mBAAmB;UAAG;UACxE,kBAAC,QAAD;WAAM,WAAU;qBACb,KAAK,MAAM,EAAU,aAAa,OAAO,EAAE,eAAe;UACvD,CAAA;SACF;;QACN,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CACG,EAAG,mDAAmD,MAAM,GAC7D,kBAAC,GAAD;UAAO,SAAS,EAAK;UAAS,WAAU;oBACrC,EAAU,aAAa,SAAS,OAE7B,EAAK,QADL,GAAG,EAAU,aAAa,MAAM,QAAQ,CAAC,EAAE,KAAK,EAAK;SAEpD,CAAA,CACH;;OACH;QACL,EAAA,CAAA,IAEF,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAG,kDAAkD,iDAAiD;MACtG,CAAA,CAEI;SAEX,kBAAC,GAAD;MAAW,WAAU;MAAU,WAAU;gBAAzC,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAgD,EAAG,uDAAuD,mBAAmB;MAAM,CAAA,GAChJ,EAAU,aAAa,MAAM,MAAM,EAAE,QAAQ,CAAC,IAC7C,kBAAC,GAAD;OACE,QAAQ,EAAU;OAClB,QAAQ;OACR,iBAAiB,MAAM,EAAE,eAAe;MACzC,CAAA,IAED,kBAAC,KAAD;OAAG,WAAU;iBAA2B,EAAG,uDAAuD,yCAAyC;MAAK,CAAA,CAEzI;OACR;;IAGL,kBAAC,GAAD;KACE,MAAM,EAAU;KACL;KACX,WAAW,EAAU;KACjB;IACL,CAAA;IAGA,EAAU,UAAU,SAAS,IAC5B,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACE,kBAAC,GAAD,EAAU,WAAU,0BAA2B,CAAA;OAC9C,EAAG,iDAAiD,kBAAkB;OACtE,EAAU,cACT,kBAAC,GAAD;QAAO,SAAQ;kBAAa,EAAU;OAAmB,CAAA,IACvD;MACF;SACJ,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAW,YAAY;MACtC,WAAU;gBAET,EAAG,oDAAoD,qBAAqB;KACvE,CAAA,CACL;QACL,kBAAC,GAAD;KAAW,WAAU;KAAU,WAAU;eACvC,kBAAC,GAAD;MACE,SAAS;MACT,MAAM,EAAU;MAChB,SAAS,MAAQ,EAAI;KACtB,CAAA;IACQ,CAAA,CACJ,EAAA,CAAA,IACP;GACD;OA1NL,kBAAC,GAAD;GACE,cAAa;GACb,MAAM,kBAAC,GAAD,EAAW,WAAU,YAAa,CAAA;GACxC,OAAO,EAAG,6CAA6C,sBAAsB;GAC7E,aAAa,EAAG,mDAAmD,mFAAmF;EACvJ,CAAA;CAuNO,CAAA;AAEhB;AAoBA,SAAS,EAAsB,EAAE,SAAM,cAAW,cAAW,SAAkC;CAC7F,IAAM,IAAU,QACR,EAAK,QAAQ,GAAK,MAAQ,KAAK,IAAI,GAAK,EAAI,OAAO,EAAI,OAAO,GAAG,CAAC,GACxE,CAAC,CAAI,CACP,GAEM,IAAwD,QACtD;EACJ;GACE,KAAK;GACL,QAAQ,EAAG,wDAAwD,SAAS;GAC5E,UAAU;GACV,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACE,kBAAC,GAAD;KAAY,MAAM,EAAU,OAAO,EAAI,SAAS;KAAG,IAAI,EAAI;KAAW,MAAK;IAAM,CAAA,GACjF,kBAAC,QAAD;KAAM,WAAU;eAAhB,CACE,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAU,EAAU,OAAO,EAAI,SAAS,GAAG,EAAI,WAAW,EAAG,qCAAqC,SAAS,CAAC;KACzG,CAAA,GACN,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACG,EAAI;OAAc;OAClB,EAAG,4DAA4D,gBAAgB;MAC5E;OACF;MACF;;EAEV;EACA;GACE,KAAK;GACL,QAAQ,EAAG,sDAAsD,iBAAiB;GAClF,UAAU;GACV,aAAa,EAAG,sDAAsD,iBAAiB;GACvF,OAAO,MACL,EAAI,gBAAgB,IAClB,kBAAC,QAAD;IAAM,WAAU;cAAkB;GAAO,CAAA,IAEzC,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACE,kBAAC,QAAD;KAAM,WAAU;eACb,EAAQ,EAAI,KAAK;IACd,CAAA,GACN,kBAAC,GAAD;KAAS,OAAO,EAAI;KAAO,KAAK;IAAU,CAAA,CACtC;;EAEZ;EACA;GACE,KAAK;GACL,QAAQ,EACN,wDACA,wBACF;GACA,UAAU;GACV,aAAa,EACX,wDACA,wBACF;GACA,OAAO,MACL,EAAI,gBAAgB,IAClB,kBAAC,QAAD;IAAM,WAAU;cAAkB;GAAO,CAAA,IAEzC,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACE,kBAAC,QAAD;KAAM,WAAU;eACb,EAAQ,EAAI,OAAO;IAChB,CAAA,GACN,kBAAC,GAAD;KAAS,OAAO,EAAI;KAAS,KAAK;IAAU,CAAA,CACxC;;EAEZ;EACA;GACE,KAAK;GACL,QAAQ,EAAG,sDAAsD,YAAY;GAC7E,UAAU;GACV,aAAa,EAAG,sDAAsD,YAAY;GAClF,OAAO,MACL,EAAI,SAAS,OAKX,kBAAC,QAAD;IAAM,WAAU;cAAkB;GAAO,CAAA,IAJzC,kBAAC,QAAD;IAAM,WAAU;cACb,EAAI,MAAM,QAAQ,CAAC;GAChB,CAAA;EAIZ;EACA;GACE,KAAK;GACL,QAAQ,EAAG,uDAAuD,QAAQ;GAC1E,UAAU;GACV,aAAa,EAAG,uDAAuD,QAAQ;GAC/E,OAAO,MAAQ;IACb,IAAM,IAAU,EAAS,EAAI,OAAO,CAAE;IACtC,OAAO,kBAAC,GAAD;KAAO,SAAS,EAAQ;eAAU,EAAQ;IAAa,CAAA;GAChE;EACF;CACF,GACA;EAAC;EAAW;EAAS;CAAE,CACzB;CAEA,OACE,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,MAAD;GAAI,WAAU;aAAd,CACE,kBAAC,GAAD,EAAO,WAAU,0BAA2B,CAAA,GAC3C,EAAG,sDAAsD,0BAA0B,CAClF;MACJ,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;GAAgB,SAAA;aACd,kBAAC,UAAD;IACE,MAAK;IACL,cAAY,EACV,2DACA,iCACF;IACA,WAAU;cAEV,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA;GACrB,CAAA;EACM,CAAA,GAChB,kBAAC,GAAD;GAAgB,WAAU;aACvB,EACC,sDACA,sMACF;EACc,CAAA,CACT,EAAA,CAAA,EACM,CAAA,CACd;KAEJ,EAAK,WAAW,IACf,kBAAC,GAAD;EAAW,WAAU;EAAU,WAAU;YACvC,kBAAC,KAAD;GAAG,WAAU;aACV,EACC,sDACA,6FACF;EACC,CAAA;CACM,CAAA,IAEX,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;EAAW,WAAU;EAAU,WAAU;YACvC,kBAAC,GAAD;GACW;GACT,MAAM;GACN,SAAS,MAAQ,EAAI;GACrB,SAAS,EACP,wDACA,mEACF;EACD,CAAA;CACQ,CAAA,GACV,IACC,kBAAC,KAAD;EAAG,WAAU;YACV,EACC,0DACA,sIACF;CACC,CAAA,IACD,IACJ,EAAA,CAAA,CAEG,EAAA,CAAA;AAEb"}
|
|
1
|
+
{"version":3,"file":"CoachAnalyticsPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/analytics/CoachAnalyticsPage.tsx"],"sourcesContent":["/**\n * Coach Analytics\n * @module atheletebridge/pages/analytics\n *\n * An honest coaching-analytics dashboard built ENTIRELY from real aggregates via\n * `useCoachAnalytics()` — total sessions, average session rating, completion\n * rate, session-status mix, average rating by coach, weekly training-load /\n * session trends, a per-athlete acute:chronic training-load table, and a\n * league-standings snapshot.\n *\n * Deliberately OMITS the mock's fabricated composite indices (Form Index,\n * Fitness Score, Fatigue Index, Injury Status, Availability %) — there is no\n * wellness / injury / availability data in the schema, so those are not faked.\n * The one derived metric shown (ACWR training load) uses standard sports-science\n * maths over real `workout.totalLoad` and carries an explanatory tooltip — both\n * workspace-wide and, in the per-athlete table, grouped by each log's real\n * `athleteId`. Where an athlete's workouts carry no recorded load the table\n * says so rather than showing a fabricated zero.\n * Semantic tokens only; responsive.\n */\n\nimport { useMemo } from 'react';\nimport { Activity, BarChart3, CheckCircle2, Info, ListChecks, Star, Users } from 'lucide-react';\nimport {\n Badge,\n GlassCard,\n GradientText,\n ResponsiveTable,\n type ResponsiveTableColumn,\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { Crest } from '../../components/Crest';\nimport { UserAvatar } from '../../components/UserAvatar';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { LineChart, BarChart, DonutChart, RatingStars, STATUS } from '../../components/viz';\nimport { humanizeEnum } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useAbTranslate, type TranslateFn } from '../../i18n';\nimport {\n useCoachAnalytics,\n useUserDirectory,\n type AthleteTrainingLoad,\n type UserDirectory,\n} from '../../hooks/useAthleteBridgeApi';\nimport type { AthSessionStatus, AthStanding } from '../../types';\n\n/** Tokenized color per session status for the status donut. */\nconst STATUS_COLOR: Record<AthSessionStatus, string> = {\n COMPLETED: STATUS.success,\n CONFIRMED: STATUS.info,\n REQUESTED: STATUS.warning,\n CANCELLED: STATUS.danger,\n NO_SHOW: 'var(--color-text-muted)',\n};\n\n/** ACWR banding (acute:chronic workload ratio) — standard sweet-spot 0.8–1.3. */\nfunction acwrBand(\n ratio: number | null,\n tr: TranslateFn\n): {\n label: string;\n variant: 'default' | 'secondary' | 'destructive';\n} {\n if (ratio == null) {\n return {\n label: tr('atheletebridge.pages.analytics.acwr.noData', 'No data'),\n variant: 'secondary',\n };\n }\n if (ratio > 1.5) {\n return {\n label: tr('atheletebridge.pages.analytics.acwr.atRisk', 'At risk'),\n variant: 'destructive',\n };\n }\n if (ratio < 0.8 || ratio > 1.3) {\n return {\n label: tr('atheletebridge.pages.analytics.acwr.watch', 'Watch'),\n variant: 'secondary',\n };\n }\n return {\n label: tr('atheletebridge.pages.analytics.acwr.optimal', 'Optimal'),\n variant: 'default',\n };\n}\n\n/**\n * Short person display: the resolved directory name, else a monogram-friendly\n * `<role> <id fragment>` so an unresolvable id still reads as a person rather\n * than as a raw uuid.\n */\nfunction shortName(name: string | undefined, id: string, rolePrefix: string): string {\n if (name && name.trim()) {\n return name;\n }\n return `${rolePrefix} ${id.slice(0, 4)}`;\n}\n\n/** Whole-number load display; loads are arbitrary units, so no decimals. */\nconst fmtLoad = (value: number): string => Math.round(value).toLocaleString();\n\n/**\n * A load bar scaled against the heaviest load in the table, so acute and\n * chronic columns stay visually comparable to each other and across rows.\n * Purely decorative — the number beside it is the fact.\n */\nfunction LoadBar({ value, max }: { value: number; max: number }) {\n const pct = max > 0 ? Math.round((value / max) * 100) : 0;\n return (\n <span className=\"block h-1.5 w-16 overflow-hidden rounded-full bg-bg-sunken\" aria-hidden=\"true\">\n <span\n className=\"block h-1.5 rounded-full bg-[var(--color-accent-teal)]\"\n style={{ width: `${Math.max(value > 0 ? 4 : 0, Math.min(100, pct))}%` }}\n />\n </span>\n );\n}\n\nexport function CoachAnalyticsPage() {\n const tr = useAbTranslate();\n const { navigateTo } = useAthleteBridge();\n const analytics = useCoachAnalytics();\n // One directory round-trip for both surfaces — `useUsersByIds` dedupes/sorts.\n const directoryIds = useMemo(\n () => [...analytics.coachIds, ...analytics.athleteIds],\n [analytics.coachIds, analytics.athleteIds]\n );\n const directory = useUserDirectory(directoryIds);\n\n const statusSegments = useMemo(\n () =>\n analytics.sessionsByStatus.map((entry) => ({\n label: humanizeEnum(entry.status),\n value: entry.count,\n color: STATUS_COLOR[entry.status],\n })),\n [analytics.sessionsByStatus]\n );\n\n const coachBars = useMemo(\n () =>\n analytics.ratingByCoach.map((entry) => ({\n label: shortName(\n directory.nameOf(entry.coachId),\n entry.coachId,\n tr('atheletebridge.rolePrefix.coach', 'Coach')\n ),\n value: Number(entry.avgRating.toFixed(2)),\n })),\n [analytics.ratingByCoach, directory, tr]\n );\n\n const standingsColumns: ResponsiveTableColumn<AthStanding>[] = [\n {\n key: 'rank',\n header: tr('atheletebridge.pages.analytics.standings.rank', '#'),\n priority: 'secondary',\n cell: (row, i) => (\n <span className=\"tabular-nums text-text-secondary\">{row.rank ?? i + 1}</span>\n ),\n },\n {\n key: 'team',\n header: tr('atheletebridge.pages.analytics.standings.team', 'Team'),\n priority: 'primary',\n cell: (row) => (\n <span className=\"flex items-center gap-2\">\n <Crest\n name={\n row.team?.name ?? tr('atheletebridge.pages.analytics.standings.teamFallback', 'Team')\n }\n sport={row.team?.sport}\n size={20}\n showSportGlyph={false}\n />\n <span className=\"truncate font-medium text-text-primary\">\n {row.team?.name ?? tr('atheletebridge.pages.analytics.standings.teamFallback', 'Team')}\n </span>\n </span>\n ),\n },\n {\n key: 'played',\n header: tr('atheletebridge.pages.analytics.standings.played', 'P'),\n priority: 'hidden-on-mobile',\n cell: (row) => <span className=\"tabular-nums text-text-secondary\">{row.played}</span>,\n },\n {\n key: 'gd',\n header: tr('atheletebridge.pages.analytics.standings.gd', 'GD'),\n priority: 'hidden-on-mobile',\n cell: (row) => {\n const gd = row.goalsFor - row.goalsAgainst;\n return (\n <span className=\"tabular-nums text-text-secondary\">\n {gd > 0 ? '+' : ''}\n {gd}\n </span>\n );\n },\n },\n {\n key: 'points',\n header: tr('atheletebridge.pages.analytics.standings.points', 'Pts'),\n priority: 'secondary',\n cell: (row) => (\n <span className=\"font-semibold tabular-nums text-text-primary\">{row.points}</span>\n ),\n },\n ];\n\n if (analytics.isLoading) {\n return (\n <PageLayout title={tr('atheletebridge.pages.analytics.title', 'Coach analytics')}>\n <LoadingState\n label={tr('atheletebridge.pages.analytics.loading', 'Crunching coaching data…')}\n />\n </PageLayout>\n );\n }\n\n if (analytics.isError) {\n return (\n <PageLayout title={tr('atheletebridge.pages.analytics.title', 'Coach analytics')}>\n <ErrorState\n title={tr('atheletebridge.pages.analytics.errorTitle', 'Analytics unavailable')}\n message={tr(\n 'atheletebridge.pages.analytics.errorMessage',\n 'We could not load coaching analytics. Please try again.'\n )}\n />\n </PageLayout>\n );\n }\n\n const hasData =\n analytics.totalSessions > 0 ||\n analytics.standings.length > 0 ||\n analytics.loadByAthlete.length > 0;\n const band = acwrBand(analytics.trainingLoad.ratio, tr);\n\n return (\n <PageLayout\n title={\n <span className=\"flex items-center gap-2\">\n <GradientText>\n {tr('atheletebridge.pages.analytics.title', 'Coach analytics')}\n </GradientText>\n <Badge variant=\"outline\">{tr('common.beta', 'Beta')}</Badge>\n </span>\n }\n description={tr(\n 'atheletebridge.pages.analytics.description',\n 'Honest aggregates over your real sessions, workouts and results.'\n )}\n >\n {!hasData ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<BarChart3 className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.analytics.emptyTitle', 'No coaching data yet')}\n description={tr(\n 'atheletebridge.pages.analytics.emptyDescription',\n 'Book and complete sessions, log workouts and record fixtures to unlock analytics.'\n )}\n />\n ) : (\n <div className=\"space-y-8\">\n {/* KPI row */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <MetricsCard\n icon={<ListChecks className=\"h-5 w-5\" />}\n value={analytics.totalSessions}\n label={tr('atheletebridge.pages.analytics.totalSessions', 'Total sessions')}\n tone=\"accent\"\n />\n <GlassCard treatment=\"metric\" className=\"flex flex-col justify-between gap-3 p-5\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-secondary\">\n {tr('atheletebridge.pages.analytics.avgRating', 'Avg session rating')}\n </span>\n <Star className=\"h-5 w-5 text-text-muted\" />\n </div>\n {analytics.avgRating != null ? (\n <div className=\"flex items-end gap-3\">\n <span className=\"text-3xl font-semibold leading-none tabular-nums text-text-primary\">\n {analytics.avgRating.toFixed(1)}\n <span className=\"ml-1 text-base text-text-secondary\">\n {tr('atheletebridge.pages.analytics.ratingOutOf', '/5')}\n </span>\n </span>\n <div className=\"pb-1\">\n <RatingStars value={analytics.avgRating} max={5} size={16} />\n </div>\n </div>\n ) : (\n <span className=\"text-sm text-text-muted\">\n {tr('atheletebridge.pages.analytics.noRatedSessions', 'No rated sessions yet')}\n </span>\n )}\n <span className=\"text-xs text-text-muted tabular-nums\">\n {analytics.ratedCount} {tr('atheletebridge.pages.analytics.ratedOf', 'rated of')}{' '}\n {analytics.totalSessions}\n </span>\n </GlassCard>\n <MetricsCard\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n value={`${Math.round(analytics.completedPct)}%`}\n label={tr('atheletebridge.pages.analytics.sessionsCompleted', 'Sessions completed')}\n tone=\"success\"\n />\n </div>\n\n {/* Charts row: status donut + rating-by-coach bars */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr(\n 'atheletebridge.pages.analytics.statusOverviewTitle',\n 'Session status overview'\n )}\n </h2>\n {statusSegments.length > 0 ? (\n <DonutChart\n segments={statusSegments}\n centerLabel={analytics.totalSessions}\n centerCaption={tr('atheletebridge.pages.analytics.sessionsCaption', 'sessions')}\n valueFormatter={(v) => v.toLocaleString()}\n />\n ) : (\n <p className=\"text-sm text-text-muted\">\n {tr(\n 'atheletebridge.pages.analytics.statusOverviewEmpty',\n 'No sessions to break down yet.'\n )}\n </p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.analytics.ratingByCoachTitle', 'Average rating by coach')}\n </h2>\n {coachBars.length > 0 ? (\n <>\n <BarChart\n series={coachBars}\n height={220}\n colorful\n valueFormatter={(v) => v.toFixed(1)}\n />\n <div className=\"mt-3 flex flex-wrap gap-x-4 gap-y-2\">\n {analytics.ratingByCoach.map((entry) => (\n <span\n key={entry.coachId}\n className=\"flex items-center gap-2 text-xs text-text-secondary\"\n >\n <UserAvatar\n name={directory.nameOf(entry.coachId)}\n id={entry.coachId}\n size=\"xs\"\n />\n <span className=\"truncate\">\n {shortName(\n directory.nameOf(entry.coachId),\n entry.coachId,\n tr('atheletebridge.rolePrefix.coach', 'Coach')\n )}\n <span className=\"ml-1 text-text-muted tabular-nums\">({entry.count})</span>\n </span>\n </span>\n ))}\n </div>\n </>\n ) : (\n <p className=\"text-sm text-text-muted\">\n {tr(\n 'atheletebridge.pages.analytics.ratingByCoachEmpty',\n 'Rate completed sessions to compare coaches.'\n )}\n </p>\n )}\n </GlassCard>\n </div>\n\n {/* Trends row: training load + sessions per week */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <div className=\"mb-4 flex items-center justify-between gap-2\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.analytics.weeklyLoadTitle', 'Weekly training load')}\n </h2>\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n aria-label={tr(\n 'atheletebridge.pages.analytics.acwrAboutLabel',\n 'About ACWR training load'\n )}\n className=\"rounded-full p-1 text-text-muted transition-colors hover:text-text-primary\"\n >\n <Info className=\"h-4 w-4\" />\n </button>\n </TooltipTrigger>\n <TooltipContent className=\"max-w-xs\">\n {tr(\n 'atheletebridge.pages.analytics.acwrAbout',\n 'Total workout load per week (sum of real totalLoad). The badge is the acute:chronic workload ratio (last 7 days vs 28-day average) — a standard sports-science load indicator; 0.8–1.3 is the typical sweet spot.'\n )}\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n </div>\n {analytics.loadTrend.some((p) => p.value > 0) ? (\n <>\n <LineChart\n series={analytics.loadTrend}\n seriesLabel={tr(\n 'atheletebridge.pages.analytics.trainingLoadLabel',\n 'Training load'\n )}\n height={200}\n valueFormatter={(v) => v.toLocaleString()}\n />\n <div className=\"mt-4 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm\">\n <span className=\"text-text-secondary\">\n {tr('atheletebridge.pages.analytics.acuteLoad', 'Acute (7d)')}{' '}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {Math.round(analytics.trainingLoad.acute).toLocaleString()}\n </span>\n </span>\n <span className=\"text-text-secondary\">\n {tr('atheletebridge.pages.analytics.chronicLoad', 'Chronic (28d avg)')}{' '}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {Math.round(analytics.trainingLoad.chronic).toLocaleString()}\n </span>\n </span>\n <span className=\"flex items-center gap-2 text-text-secondary\">\n {tr('atheletebridge.pages.analytics.acwrAbbreviation', 'ACWR')}\n <Badge variant={band.variant} className=\"tabular-nums\">\n {analytics.trainingLoad.ratio != null\n ? `${analytics.trainingLoad.ratio.toFixed(2)} · ${band.label}`\n : band.label}\n </Badge>\n </span>\n </div>\n </>\n ) : (\n <p className=\"text-sm text-text-muted\">\n {tr(\n 'atheletebridge.pages.analytics.weeklyLoadEmpty',\n 'Log workouts with a load to see training trend.'\n )}\n </p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.analytics.sessionsPerWeekTitle', 'Sessions per week')}\n </h2>\n {analytics.sessionTrend.some((p) => p.value > 0) ? (\n <BarChart\n series={analytics.sessionTrend}\n height={200}\n valueFormatter={(v) => v.toLocaleString()}\n />\n ) : (\n <p className=\"text-sm text-text-muted\">\n {tr(\n 'atheletebridge.pages.analytics.sessionsPerWeekEmpty',\n 'Schedule sessions to see weekly volume.'\n )}\n </p>\n )}\n </GlassCard>\n </div>\n\n {/* Training load by athlete — the workspace ACWR, broken out per athlete */}\n <TrainingLoadByAthlete\n rows={analytics.loadByAthlete}\n directory={directory}\n truncated={analytics.loadSampleTruncated}\n tr={tr}\n />\n\n {/* League standings snapshot */}\n {analytics.standings.length > 0 ? (\n <section>\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <h2 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Activity className=\"h-5 w-5 text-text-muted\" />\n {tr('atheletebridge.pages.analytics.standingsTitle', 'League standings')}\n {analytics.competition ? (\n <Badge variant=\"secondary\">{analytics.competition}</Badge>\n ) : null}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/standings')}\n className=\"text-sm font-medium text-[var(--color-accent-teal)] transition-colors hover:underline\"\n >\n {tr('atheletebridge.pages.analytics.viewFullStandings', 'View full standings')}\n </button>\n </div>\n <GlassCard treatment=\"insight\" className=\"p-2 sm:p-4\">\n <ResponsiveTable\n columns={standingsColumns}\n data={analytics.standings}\n rowKey={(row) => row.id}\n />\n </GlassCard>\n </section>\n ) : null}\n </div>\n )}\n </PageLayout>\n );\n}\n\ninterface TrainingLoadByAthleteProps {\n rows: AthleteTrainingLoad[];\n directory: UserDirectory;\n /** The sampled workout page did not reach back 28 days — loads under-count. */\n truncated: boolean;\n tr: TranslateFn;\n}\n\n/**\n * \"Training load by athlete\" — the same acute:chronic maths the workspace-wide\n * ACWR badge already uses, broken out per athlete from each workout log's own\n * `athleteId` + `totalLoad` + `performedAt`.\n *\n * Nothing here is synthesised: an athlete whose 28-day workouts carry no\n * `totalLoad` renders as em-dashes and the shared `acwrBand(null)` \"No data\"\n * badge rather than as a real zero, and when the sampled page stops short of\n * 28 days the section says so instead of presenting a floor as a total.\n */\nfunction TrainingLoadByAthlete({ rows, directory, truncated, tr }: TrainingLoadByAthleteProps) {\n const maxLoad = useMemo(\n () => rows.reduce((max, row) => Math.max(max, row.acute, row.chronic), 0),\n [rows]\n );\n\n const columns: ResponsiveTableColumn<AthleteTrainingLoad>[] = useMemo(\n () => [\n {\n key: 'athlete',\n header: tr('atheletebridge.pages.analytics.loadByAthlete.athlete', 'Athlete'),\n priority: 'primary',\n cell: (row) => (\n <span className=\"flex items-center gap-2\">\n <UserAvatar name={directory.nameOf(row.athleteId)} id={row.athleteId} size=\"xs\" />\n <span className=\"min-w-0\">\n <span className=\"block truncate font-medium text-text-primary\">\n {shortName(\n directory.nameOf(row.athleteId),\n row.athleteId,\n tr('atheletebridge.rolePrefix.athlete', 'Athlete')\n )}\n </span>\n <span className=\"block text-xs tabular-nums text-text-muted\">\n {row.workoutCount}{' '}\n {tr('atheletebridge.pages.analytics.loadByAthlete.workouts28d', 'workouts · 28d')}\n </span>\n </span>\n </span>\n ),\n },\n {\n key: 'acute',\n header: tr('atheletebridge.pages.analytics.loadByAthlete.acute', 'Acute load (7d)'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.analytics.loadByAthlete.acute', 'Acute load (7d)'),\n cell: (row) =>\n row.loadedCount === 0 ? (\n <span className=\"text-text-muted\">—</span>\n ) : (\n <span className=\"flex items-center gap-2\">\n <span className=\"w-14 text-right font-medium tabular-nums text-text-primary\">\n {fmtLoad(row.acute)}\n </span>\n <LoadBar value={row.acute} max={maxLoad} />\n </span>\n ),\n },\n {\n key: 'chronic',\n header: tr(\n 'atheletebridge.pages.analytics.loadByAthlete.chronic',\n 'Chronic load (28d avg)'\n ),\n priority: 'secondary',\n mobileLabel: tr(\n 'atheletebridge.pages.analytics.loadByAthlete.chronic',\n 'Chronic load (28d avg)'\n ),\n cell: (row) =>\n row.loadedCount === 0 ? (\n <span className=\"text-text-muted\">—</span>\n ) : (\n <span className=\"flex items-center gap-2\">\n <span className=\"w-14 text-right font-medium tabular-nums text-text-primary\">\n {fmtLoad(row.chronic)}\n </span>\n <LoadBar value={row.chronic} max={maxLoad} />\n </span>\n ),\n },\n {\n key: 'ratio',\n header: tr('atheletebridge.pages.analytics.loadByAthlete.ratio', 'Load ratio'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.analytics.loadByAthlete.ratio', 'Load ratio'),\n cell: (row) =>\n row.ratio != null ? (\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {row.ratio.toFixed(2)}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n {\n key: 'band',\n header: tr('atheletebridge.pages.analytics.loadByAthlete.status', 'Status'),\n priority: 'primary',\n mobileLabel: tr('atheletebridge.pages.analytics.loadByAthlete.status', 'Status'),\n cell: (row) => {\n const rowBand = acwrBand(row.ratio, tr);\n return <Badge variant={rowBand.variant}>{rowBand.label}</Badge>;\n },\n },\n ],\n [directory, maxLoad, tr]\n );\n\n return (\n <section>\n <div className=\"mb-4 flex flex-wrap items-center gap-2\">\n <h2 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Users className=\"h-5 w-5 text-text-muted\" />\n {tr('atheletebridge.pages.analytics.loadByAthlete.title', 'Training load by athlete')}\n </h2>\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n aria-label={tr(\n 'atheletebridge.pages.analytics.loadByAthlete.aboutLabel',\n 'About per-athlete training load'\n )}\n className=\"rounded-full p-1 text-text-muted transition-colors hover:text-text-primary\"\n >\n <Info className=\"h-4 w-4\" />\n </button>\n </TooltipTrigger>\n <TooltipContent className=\"max-w-xs\">\n {tr(\n 'atheletebridge.pages.analytics.loadByAthlete.about',\n 'Each athlete’s acute:chronic workload ratio — their last 7 days of logged workout load against their 28-day weekly average. 0.8–1.3 is the typical sweet spot; above 1.5 is a spike worth reviewing.'\n )}\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n </div>\n\n {rows.length === 0 ? (\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <p className=\"text-sm text-text-muted\">\n {tr(\n 'atheletebridge.pages.analytics.loadByAthlete.empty',\n 'No workouts logged in the last 28 days. Log workouts with a total load to compare athletes.'\n )}\n </p>\n </GlassCard>\n ) : (\n <>\n <GlassCard treatment=\"insight\" className=\"p-2 sm:p-4\">\n <ResponsiveTable\n columns={columns}\n data={rows}\n rowKey={(row) => row.athleteId}\n caption={tr(\n 'atheletebridge.pages.analytics.loadByAthlete.caption',\n 'Acute and chronic training load per athlete over the last 28 days'\n )}\n />\n </GlassCard>\n {truncated ? (\n <p className=\"mt-3 text-xs text-text-muted\">\n {tr(\n 'atheletebridge.pages.analytics.loadByAthlete.truncated',\n 'Computed from the most recent page of workout logs, which does not reach back a full 28 days — these loads are a floor, not a total.'\n )}\n </p>\n ) : null}\n </>\n )}\n </section>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FixtureDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/fixtures/FixtureDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAkBH,wBAAgB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"FixtureDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/fixtures/FixtureDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAkBH,wBAAgB,iBAAiB,gCA0MhC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FixtureDetailPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/fixtures/FixtureDetailPage.tsx"],"sourcesContent":["/**\n * Fixture Detail\n * @module atheletebridge/pages/fixtures\n *\n * Reads a single fixture (athFixture) and lets the user record a final result\n * (athRecordFixtureResult) which marks it COMPLETED and recomputes standings.\n *\n * The header is a crest-vs-crest Scoreboard hero; the record-result panel shows\n * each team's crest beside its score input. Semantic tokens only, responsive.\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router';\nimport { ArrowLeft, Trophy, Loader2, Flag, Minus } from 'lucide-react';\nimport { GlassCard, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Scoreboard } from '../../components/Scoreboard';\nimport { Crest } from '../../components/Crest';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { FIXTURE_TYPE_LABELS } from '../../components/domain';\nimport { AvailabilityRsvpControl } from '../../components/availability/AvailabilityRsvpControl';\nimport { AvailabilityRosterPanel } from '../../components/availability/AvailabilityRosterPanel';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useFixture, useRecordFixtureResult } from '../../hooks/useAthleteBridgeApi';\nimport { useAbTranslate } from '../../i18n';\n\nexport function FixtureDetailPage() {\n const tr = useAbTranslate();\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const fixture = useFixture(id);\n const recordResult = useRecordFixtureResult();\n\n const [homeScore, setHomeScore] = useState('');\n const [awayScore, setAwayScore] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n if (fixture.isLoading) {\n return (\n <PageLayout title={tr('atheletebridge.pages.fixtureDetail.title', 'Fixture')}>\n <LoadingState label={tr('atheletebridge.pages.fixtureDetail.loading', 'Loading fixture…')} />\n </PageLayout>\n );\n }\n\n if (fixture.isError || !fixture.data) {\n return (\n <PageLayout title={tr('atheletebridge.pages.fixtureDetail.title', 'Fixture')}>\n <ErrorState\n title={tr('atheletebridge.pages.fixtureDetail.notFoundTitle', 'Fixture not found')}\n message={tr(\n 'atheletebridge.pages.fixtureDetail.notFoundMessage',\n 'This fixture does not exist or you do not have access to it.'\n )}\n onRetry={() => navigateTo('/fixtures')}\n />\n </PageLayout>\n );\n }\n\n const f = fixture.data;\n const homeName = f.homeTeam?.name ?? tr('atheletebridge.pages.fixtureDetail.home', 'Home');\n const awayName = f.awayTeam?.name ?? (f.awayTeamId ? tr('atheletebridge.pages.fixtureDetail.away', 'Away') : 'TBD');\n const canRecordResult =\n Boolean(f.awayTeamId) && (f.status === 'SCHEDULED' || f.status === 'LIVE');\n\n const handleRecord = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!id) return;\n const home = Number.parseInt(homeScore, 10);\n const away = Number.parseInt(awayScore, 10);\n if (Number.isNaN(home) || Number.isNaN(away)) {\n setError('Both scores are required and must be numbers.');\n return;\n }\n if (home < 0 || away < 0) {\n setError('Scores cannot be negative.');\n return;\n }\n try {\n await recordResult.mutateAsync({ id, input: { homeScore: home, awayScore: away } });\n setHomeScore('');\n setAwayScore('');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to record result.');\n }\n };\n\n return (\n <PageLayout\n title={`${homeName} vs ${awayName}`}\n description={`${FIXTURE_TYPE_LABELS[f.type]}${f.competition ? ` · ${f.competition}` : ''}`}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> {tr('atheletebridge.pages.fixtureDetail.back', 'Back')}\n </button>\n }\n >\n {/* Crest-vs-crest hero scoreboard */}\n <Scoreboard fixture={f} variant=\"hero\" className=\"mb-8\" />\n\n {/* Availability / RSVP (BOFF-4134) — athlete's own response + coach roster view */}\n <div className=\"mb-8 grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_2fr]\">\n <AvailabilityRsvpControl teamId={f.homeTeamId} fixtureId={f.id} />\n <AvailabilityRosterPanel teamId={f.homeTeamId} fixtureId={f.id} />\n </div>\n\n {f.notes && (\n <EmphasisPanel className=\"mb-8\">\n <h2 className=\"mb-2 text-sm font-semibold text-text-primary\">{tr('atheletebridge.pages.fixtureDetail.notes', 'Notes')}</h2>\n <p className=\"text-sm text-text-secondary\">{f.notes}</p>\n </EmphasisPanel>\n )}\n\n {/* Record result */}\n {canRecordResult && (\n <section className=\"mb-8\">\n <h2 className=\"mb-4 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Flag className=\"h-5 w-5\" /> {tr('atheletebridge.pages.fixtureDetail.recordResult', 'Record result')}\n </h2>\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <p className=\"mb-5 text-sm text-text-secondary\">\n {tr(\n 'atheletebridge.pages.fixtureDetail.recordResultDescription',\n 'Recording a final score marks this fixture Completed and recomputes the competition standings for both teams.'\n )}\n </p>\n <form onSubmit={handleRecord}>\n <div className=\"flex flex-wrap items-end justify-center gap-4 sm:gap-6\">\n <div className=\"flex items-end gap-3\">\n <div className=\"flex flex-col items-center gap-2\">\n <Crest name={homeName} sport={f.homeTeam?.sport} size={40} />\n <span className=\"max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary\">\n {homeName}\n </span>\n </div>\n <FormField label={tr('atheletebridge.pages.fixtureDetail.home', 'Home')} htmlFor=\"homeScore\" className=\"w-24\">\n <input\n id=\"homeScore\"\n inputMode=\"numeric\"\n className={`${fieldInputClass} text-center text-lg font-bold tabular-nums`}\n value={homeScore}\n onChange={(e) => setHomeScore(e.target.value)}\n placeholder=\"0\"\n />\n </FormField>\n </div>\n\n <Minus className=\"mb-3 h-5 w-5 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n\n <div className=\"flex items-end gap-3\">\n <FormField label={tr('atheletebridge.pages.fixtureDetail.away', 'Away')} htmlFor=\"awayScore\" className=\"w-24\">\n <input\n id=\"awayScore\"\n inputMode=\"numeric\"\n className={`${fieldInputClass} text-center text-lg font-bold tabular-nums`}\n value={awayScore}\n onChange={(e) => setAwayScore(e.target.value)}\n placeholder=\"0\"\n />\n </FormField>\n <div className=\"flex flex-col items-center gap-2\">\n <Crest name={awayName} sport={f.awayTeam?.sport} size={40} />\n <span className=\"max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary\">\n {awayName}\n </span>\n </div>\n </div>\n </div>\n\n <div className=\"mt-6 flex justify-center\">\n <button\n type=\"submit\"\n disabled={recordResult.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-6 py-2.5 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {recordResult.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"h-4 w-4\" />\n )}\n {tr('atheletebridge.pages.fixtureDetail.recordResultAction', 'Record result')}\n </button>\n </div>\n </form>\n {error && <p className=\"mt-3 text-center text-sm text-status-error-text\">{error}</p>}\n </GlassCard>\n </section>\n )}\n\n {f.competition && (\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(`/standings?competition=${encodeURIComponent(f.competition ?? '')}`)\n }\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 <Trophy className=\"h-4 w-4\" /> {tr('atheletebridge.pages.fixtureDetail.viewStandings', `View ${f.competition} standings`)}\n </button>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FixtureDetailPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/fixtures/FixtureDetailPage.tsx"],"sourcesContent":["/**\n * Fixture Detail\n * @module atheletebridge/pages/fixtures\n *\n * Reads a single fixture (athFixture) and lets the user record a final result\n * (athRecordFixtureResult) which marks it COMPLETED and recomputes standings.\n *\n * The header is a crest-vs-crest Scoreboard hero; the record-result panel shows\n * each team's crest beside its score input. Semantic tokens only, responsive.\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router';\nimport { ArrowLeft, Trophy, Loader2, Flag, Minus } from 'lucide-react';\nimport { GlassCard, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Scoreboard } from '../../components/Scoreboard';\nimport { Crest } from '../../components/Crest';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { FIXTURE_TYPE_LABELS } from '../../components/domain';\nimport { AvailabilityRsvpControl } from '../../components/availability/AvailabilityRsvpControl';\nimport { AvailabilityRosterPanel } from '../../components/availability/AvailabilityRosterPanel';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useFixture, useRecordFixtureResult } from '../../hooks/useAthleteBridgeApi';\nimport { useAbTranslate } from '../../i18n';\n\nexport function FixtureDetailPage() {\n const tr = useAbTranslate();\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const fixture = useFixture(id);\n const recordResult = useRecordFixtureResult();\n\n const [homeScore, setHomeScore] = useState('');\n const [awayScore, setAwayScore] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n if (fixture.isLoading) {\n return (\n <PageLayout title={tr('atheletebridge.pages.fixtureDetail.title', 'Fixture')}>\n <LoadingState\n label={tr('atheletebridge.pages.fixtureDetail.loading', 'Loading fixture…')}\n />\n </PageLayout>\n );\n }\n\n if (fixture.isError || !fixture.data) {\n return (\n <PageLayout title={tr('atheletebridge.pages.fixtureDetail.title', 'Fixture')}>\n <ErrorState\n title={tr('atheletebridge.pages.fixtureDetail.notFoundTitle', 'Fixture not found')}\n message={tr(\n 'atheletebridge.pages.fixtureDetail.notFoundMessage',\n 'This fixture does not exist or you do not have access to it.'\n )}\n onRetry={() => navigateTo('/fixtures')}\n />\n </PageLayout>\n );\n }\n\n const f = fixture.data;\n const homeName = f.homeTeam?.name ?? tr('atheletebridge.pages.fixtureDetail.home', 'Home');\n const awayName =\n f.awayTeam?.name ??\n (f.awayTeamId ? tr('atheletebridge.pages.fixtureDetail.away', 'Away') : 'TBD');\n const canRecordResult =\n Boolean(f.awayTeamId) && (f.status === 'SCHEDULED' || f.status === 'LIVE');\n\n const handleRecord = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!id) return;\n const home = Number.parseInt(homeScore, 10);\n const away = Number.parseInt(awayScore, 10);\n if (Number.isNaN(home) || Number.isNaN(away)) {\n setError('Both scores are required and must be numbers.');\n return;\n }\n if (home < 0 || away < 0) {\n setError('Scores cannot be negative.');\n return;\n }\n try {\n await recordResult.mutateAsync({ id, input: { homeScore: home, awayScore: away } });\n setHomeScore('');\n setAwayScore('');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to record result.');\n }\n };\n\n return (\n <PageLayout\n title={`${homeName} vs ${awayName}`}\n description={`${FIXTURE_TYPE_LABELS[f.type]}${f.competition ? ` · ${f.competition}` : ''}`}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> {tr('atheletebridge.pages.fixtureDetail.back', 'Back')}\n </button>\n }\n >\n {/* Crest-vs-crest hero scoreboard */}\n <Scoreboard fixture={f} variant=\"hero\" className=\"mb-8\" />\n\n {/* Availability / RSVP (BOFF-4134) — athlete's own response + coach roster view */}\n <div className=\"mb-8 grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_2fr]\">\n <AvailabilityRsvpControl teamId={f.homeTeamId} fixtureId={f.id} />\n <AvailabilityRosterPanel teamId={f.homeTeamId} fixtureId={f.id} />\n </div>\n\n {f.notes && (\n <EmphasisPanel className=\"mb-8\">\n <h2 className=\"mb-2 text-sm font-semibold text-text-primary\">\n {tr('atheletebridge.pages.fixtureDetail.notes', 'Notes')}\n </h2>\n <p className=\"text-sm text-text-secondary\">{f.notes}</p>\n </EmphasisPanel>\n )}\n\n {/* Record result */}\n {canRecordResult && (\n <section className=\"mb-8\">\n <h2 className=\"mb-4 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Flag className=\"h-5 w-5\" />{' '}\n {tr('atheletebridge.pages.fixtureDetail.recordResult', 'Record result')}\n </h2>\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <p className=\"mb-5 text-sm text-text-secondary\">\n {tr(\n 'atheletebridge.pages.fixtureDetail.recordResultDescription',\n 'Recording a final score marks this fixture Completed and recomputes the competition standings for both teams.'\n )}\n </p>\n <form onSubmit={handleRecord}>\n <div className=\"flex flex-wrap items-end justify-center gap-4 sm:gap-6\">\n <div className=\"flex items-end gap-3\">\n <div className=\"flex flex-col items-center gap-2\">\n <Crest name={homeName} sport={f.homeTeam?.sport} size={40} />\n <span className=\"max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary\">\n {homeName}\n </span>\n </div>\n <FormField\n label={tr('atheletebridge.pages.fixtureDetail.home', 'Home')}\n htmlFor=\"homeScore\"\n className=\"w-24\"\n >\n <input\n id=\"homeScore\"\n inputMode=\"numeric\"\n className={`${fieldInputClass} text-center text-lg font-bold tabular-nums`}\n value={homeScore}\n onChange={(e) => setHomeScore(e.target.value)}\n placeholder=\"0\"\n />\n </FormField>\n </div>\n\n <Minus className=\"mb-3 h-5 w-5 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n\n <div className=\"flex items-end gap-3\">\n <FormField\n label={tr('atheletebridge.pages.fixtureDetail.away', 'Away')}\n htmlFor=\"awayScore\"\n className=\"w-24\"\n >\n <input\n id=\"awayScore\"\n inputMode=\"numeric\"\n className={`${fieldInputClass} text-center text-lg font-bold tabular-nums`}\n value={awayScore}\n onChange={(e) => setAwayScore(e.target.value)}\n placeholder=\"0\"\n />\n </FormField>\n <div className=\"flex flex-col items-center gap-2\">\n <Crest name={awayName} sport={f.awayTeam?.sport} size={40} />\n <span className=\"max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary\">\n {awayName}\n </span>\n </div>\n </div>\n </div>\n\n <div className=\"mt-6 flex justify-center\">\n <button\n type=\"submit\"\n disabled={recordResult.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-6 py-2.5 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {recordResult.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"h-4 w-4\" />\n )}\n {tr('atheletebridge.pages.fixtureDetail.recordResultAction', 'Record result')}\n </button>\n </div>\n </form>\n {error && <p className=\"mt-3 text-center text-sm text-status-error-text\">{error}</p>}\n </GlassCard>\n </section>\n )}\n\n {f.competition && (\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(`/standings?competition=${encodeURIComponent(f.competition ?? '')}`)\n }\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 <Trophy className=\"h-4 w-4\" />{' '}\n {tr(\n 'atheletebridge.pages.fixtureDetail.viewStandings',\n `View ${f.competition} standings`\n )}\n </button>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FixturesListPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/fixtures/FixturesListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA8BH,wBAAgB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"FixturesListPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/fixtures/FixturesListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA8BH,wBAAgB,gBAAgB,gCA4I/B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FixturesListPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/fixtures/FixturesListPage.tsx"],"sourcesContent":["/**\n * Fixtures List\n * @module atheletebridge/pages/fixtures\n *\n * Crest-vs-crest FixtureCard grid with status, team and type filters. The team\n * filter is server-backed (athListFixtures accepts teamId); the type filter is\n * applied over the loaded page. A \"Tournament bracket\" shortcut links to the\n * tournaments view. Semantic tokens only, responsive.\n */\n\nimport { useMemo, useState } from 'react';\nimport { CalendarDays, Plus, Trophy } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { FixtureCard } from '../../components/FixtureCard';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FilterSelect } from '../../components/FilterSelect';\nimport { CursorPager } from '../../components/CursorPager';\nimport { FIXTURE_TYPE_LABELS, FIXTURE_TYPE_OPTIONS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useFixtures, useTeams } from '../../hooks/useAthleteBridgeApi';\nimport { useAbTranslate } from '../../i18n';\nimport type { AthFixtureStatus, AthFixtureType } from '../../types';\n\nconst PAGE_SIZE = 12;\n\nconst STATUS_OPTS: ReadonlyArray<{ value: AthFixtureStatus; label: string }> = [\n { value: 'SCHEDULED', label: 'Scheduled' },\n { value: 'LIVE', label: 'Live' },\n { value: 'COMPLETED', label: 'Completed' },\n { value: 'POSTPONED', label: 'Postponed' },\n { value: 'CANCELLED', label: 'Cancelled' },\n];\n\nconst TYPE_OPTS: ReadonlyArray<{ value: AthFixtureType; label: string }> = FIXTURE_TYPE_OPTIONS.map(\n (t) => ({ value: t, label: FIXTURE_TYPE_LABELS[t] })\n);\n\nexport function FixturesListPage() {\n const tr = useAbTranslate();\n const { navigateTo } = useAthleteBridge();\n const [status, setStatus] = useState<AthFixtureStatus | ''>('');\n const [teamId, setTeamId] = useState<string>('');\n const [type, setType] = useState<AthFixtureType | ''>('');\n const [page, setPage] = useState(0);\n\n const teams = useTeams({ pagination: { limit: 100 } });\n const teamOpts = useMemo(\n () => (teams.data?.items ?? []).map((t) => ({ value: t.id, label: t.name })),\n [teams.data]\n );\n\n const fixtures = useFixtures({\n status: status || undefined,\n teamId: teamId || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n\n const items = useMemo(() => {\n const raw = fixtures.data?.items ?? [];\n return type ? raw.filter((f) => f.type === type) : raw;\n }, [fixtures.data, type]);\n const hasMore = fixtures.data?.pageInfo.hasMore ?? false;\n const hasFilters = Boolean(status || teamId || type);\n\n const resetPage = () => setPage(0);\n\n return (\n <PageLayout\n title={tr('atheletebridge.pages.fixtures.title', 'Fixtures')}\n description={tr(\n 'atheletebridge.pages.fixtures.description',\n 'Matches across friendlies, league games, cups, tournaments and scrimmages.'\n )}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/tournaments')}\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 <Trophy className=\"h-4 w-4\" /> {tr('atheletebridge.pages.fixtures.tournamentBracket', 'Tournament bracket')}\n </button>\n <button\n type=\"button\"\n data-tour=\"fixtures-create\"\n onClick={() => navigateTo('/fixtures/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('atheletebridge.pages.fixtures.scheduleFixture', 'Schedule fixture')}\n </button>\n </div>\n }\n >\n <div data-tour=\"fixtures-filters\" className=\"mb-6 flex flex-wrap items-center gap-3\">\n <FilterSelect\n ariaLabel={tr('atheletebridge.pages.fixtures.filterStatus', 'Filter by status')}\n allLabel={tr('atheletebridge.pages.fixtures.allStatuses', 'All statuses')}\n value={status}\n onChange={(v) => {\n setStatus(v);\n resetPage();\n }}\n options={STATUS_OPTS}\n />\n <FilterSelect\n ariaLabel={tr('atheletebridge.pages.fixtures.filterTeam', 'Filter by team')}\n allLabel={tr('atheletebridge.pages.fixtures.allTeams', 'All teams')}\n value={teamId}\n onChange={(v) => {\n setTeamId(v);\n resetPage();\n }}\n options={teamOpts}\n />\n <FilterSelect\n ariaLabel={tr('atheletebridge.pages.fixtures.filterType', 'Filter by type')}\n allLabel={tr('atheletebridge.pages.fixtures.allTypes', 'All types')}\n value={type}\n onChange={(v) => setType(v)}\n options={TYPE_OPTS}\n />\n </div>\n\n {fixtures.isLoading ? (\n <LoadingState label={tr('atheletebridge.pages.fixtures.loading', 'Loading fixtures…')} />\n ) : fixtures.isError ? (\n <ErrorState onRetry={() => void fixtures.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<CalendarDays className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.fixtures.noFixtures', 'No fixtures found')}\n description={\n hasFilters\n ? tr(\n 'atheletebridge.pages.fixtures.noFixturesFilterHint',\n 'Try clearing your filters to see all fixtures.'\n )\n : tr(\n 'atheletebridge.pages.fixtures.noFixturesHint',\n 'Schedule a match to start tracking results and standings.'\n )\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n {tr('atheletebridge.pages.fixtures.scheduleFixtureAction', 'Schedule fixture')}\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {items.map((fixture) => (\n <FixtureCard\n key={fixture.id}\n fixture={fixture}\n onClick={() => navigateTo(`/fixtures/${fixture.id}`)}\n />\n ))}\n </div>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,IAAM,IAAY,IAEZ,IAAyE;CAC7E;EAAE,OAAO;EAAa,OAAO;CAAY;CACzC;EAAE,OAAO;EAAQ,OAAO;CAAO;CAC/B;EAAE,OAAO;EAAa,OAAO;CAAY;CACzC;EAAE,OAAO;EAAa,OAAO;CAAY;CACzC;EAAE,OAAO;EAAa,OAAO;CAAY;AAC3C,GAEM,IAAqE,EAAqB,KAC7F,OAAO;CAAE,OAAO;CAAG,OAAO,EAAoB;AAAG,EACpD;AAEA,SAAgB,IAAmB;CACjC,IAAM,IAAK,EAAe,GACpB,EAAE,kBAAe,EAAiB,GAClC,CAAC,GAAQ,KAAa,EAAgC,EAAE,GACxD,CAAC,GAAQ,KAAa,EAAiB,EAAE,GACzC,CAAC,GAAM,KAAW,EAA8B,EAAE,GAClD,CAAC,GAAM,KAAW,EAAS,CAAC,GAE5B,IAAQ,EAAS,EAAE,YAAY,EAAE,OAAO,IAAI,EAAE,CAAC,GAC/C,IAAW,SACR,EAAM,MAAM,SAAS,CAAC,GAAG,KAAK,OAAO;EAAE,OAAO,EAAE;EAAI,OAAO,EAAE;CAAK,EAAE,GAC3E,CAAC,EAAM,IAAI,CACb,GAEM,IAAW,EAAY;EAC3B,QAAQ,KAAU,KAAA;EAClB,QAAQ,KAAU,KAAA;EAClB,YAAY;GAAE,OAAO;GAAW,QAAQ,IAAO;EAAU;CAC3D,CAAC,GAEK,IAAQ,QAAc;EAC1B,IAAM,IAAM,EAAS,MAAM,SAAS,CAAC;EACrC,OAAO,IAAO,EAAI,QAAQ,MAAM,EAAE,SAAS,CAAI,IAAI;CACrD,GAAG,CAAC,EAAS,MAAM,CAAI,CAAC,GAClB,IAAU,EAAS,MAAM,SAAS,WAAW,IAC7C,IAAa,GAAQ,KAAU,KAAU,IAEzC,UAAkB,EAAQ,CAAC;CAEjC,OACE,kBAAC,GAAD;EACE,OAAO,EAAG,uCAAuC,UAAU;EAC3D,aAAa,EACX,6CACA,4EACF;EACA,SACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,cAAc;IACxC,WAAU;cAHZ;KAKE,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"FixturesListPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/fixtures/FixturesListPage.tsx"],"sourcesContent":["/**\n * Fixtures List\n * @module atheletebridge/pages/fixtures\n *\n * Crest-vs-crest FixtureCard grid with status, team and type filters. The team\n * filter is server-backed (athListFixtures accepts teamId); the type filter is\n * applied over the loaded page. A \"Tournament bracket\" shortcut links to the\n * tournaments view. Semantic tokens only, responsive.\n */\n\nimport { useMemo, useState } from 'react';\nimport { CalendarDays, Plus, Trophy } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { FixtureCard } from '../../components/FixtureCard';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FilterSelect } from '../../components/FilterSelect';\nimport { CursorPager } from '../../components/CursorPager';\nimport { FIXTURE_TYPE_LABELS, FIXTURE_TYPE_OPTIONS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useFixtures, useTeams } from '../../hooks/useAthleteBridgeApi';\nimport { useAbTranslate } from '../../i18n';\nimport type { AthFixtureStatus, AthFixtureType } from '../../types';\n\nconst PAGE_SIZE = 12;\n\nconst STATUS_OPTS: ReadonlyArray<{ value: AthFixtureStatus; label: string }> = [\n { value: 'SCHEDULED', label: 'Scheduled' },\n { value: 'LIVE', label: 'Live' },\n { value: 'COMPLETED', label: 'Completed' },\n { value: 'POSTPONED', label: 'Postponed' },\n { value: 'CANCELLED', label: 'Cancelled' },\n];\n\nconst TYPE_OPTS: ReadonlyArray<{ value: AthFixtureType; label: string }> = FIXTURE_TYPE_OPTIONS.map(\n (t) => ({ value: t, label: FIXTURE_TYPE_LABELS[t] })\n);\n\nexport function FixturesListPage() {\n const tr = useAbTranslate();\n const { navigateTo } = useAthleteBridge();\n const [status, setStatus] = useState<AthFixtureStatus | ''>('');\n const [teamId, setTeamId] = useState<string>('');\n const [type, setType] = useState<AthFixtureType | ''>('');\n const [page, setPage] = useState(0);\n\n const teams = useTeams({ pagination: { limit: 100 } });\n const teamOpts = useMemo(\n () => (teams.data?.items ?? []).map((t) => ({ value: t.id, label: t.name })),\n [teams.data]\n );\n\n const fixtures = useFixtures({\n status: status || undefined,\n teamId: teamId || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n\n const items = useMemo(() => {\n const raw = fixtures.data?.items ?? [];\n return type ? raw.filter((f) => f.type === type) : raw;\n }, [fixtures.data, type]);\n const hasMore = fixtures.data?.pageInfo.hasMore ?? false;\n const hasFilters = Boolean(status || teamId || type);\n\n const resetPage = () => setPage(0);\n\n return (\n <PageLayout\n title={tr('atheletebridge.pages.fixtures.title', 'Fixtures')}\n description={tr(\n 'atheletebridge.pages.fixtures.description',\n 'Matches across friendlies, league games, cups, tournaments and scrimmages.'\n )}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/tournaments')}\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 <Trophy className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.fixtures.tournamentBracket', 'Tournament bracket')}\n </button>\n <button\n type=\"button\"\n data-tour=\"fixtures-create\"\n onClick={() => navigateTo('/fixtures/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.fixtures.scheduleFixture', 'Schedule fixture')}\n </button>\n </div>\n }\n >\n <div data-tour=\"fixtures-filters\" className=\"mb-6 flex flex-wrap items-center gap-3\">\n <FilterSelect\n ariaLabel={tr('atheletebridge.pages.fixtures.filterStatus', 'Filter by status')}\n allLabel={tr('atheletebridge.pages.fixtures.allStatuses', 'All statuses')}\n value={status}\n onChange={(v) => {\n setStatus(v);\n resetPage();\n }}\n options={STATUS_OPTS}\n />\n <FilterSelect\n ariaLabel={tr('atheletebridge.pages.fixtures.filterTeam', 'Filter by team')}\n allLabel={tr('atheletebridge.pages.fixtures.allTeams', 'All teams')}\n value={teamId}\n onChange={(v) => {\n setTeamId(v);\n resetPage();\n }}\n options={teamOpts}\n />\n <FilterSelect\n ariaLabel={tr('atheletebridge.pages.fixtures.filterType', 'Filter by type')}\n allLabel={tr('atheletebridge.pages.fixtures.allTypes', 'All types')}\n value={type}\n onChange={(v) => setType(v)}\n options={TYPE_OPTS}\n />\n </div>\n\n {fixtures.isLoading ? (\n <LoadingState label={tr('atheletebridge.pages.fixtures.loading', 'Loading fixtures…')} />\n ) : fixtures.isError ? (\n <ErrorState onRetry={() => void fixtures.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<CalendarDays className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.fixtures.noFixtures', 'No fixtures found')}\n description={\n hasFilters\n ? tr(\n 'atheletebridge.pages.fixtures.noFixturesFilterHint',\n 'Try clearing your filters to see all fixtures.'\n )\n : tr(\n 'atheletebridge.pages.fixtures.noFixturesHint',\n 'Schedule a match to start tracking results and standings.'\n )\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n {tr('atheletebridge.pages.fixtures.scheduleFixtureAction', 'Schedule fixture')}\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {items.map((fixture) => (\n <FixtureCard\n key={fixture.id}\n fixture={fixture}\n onClick={() => navigateTo(`/fixtures/${fixture.id}`)}\n />\n ))}\n </div>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,IAAM,IAAY,IAEZ,IAAyE;CAC7E;EAAE,OAAO;EAAa,OAAO;CAAY;CACzC;EAAE,OAAO;EAAQ,OAAO;CAAO;CAC/B;EAAE,OAAO;EAAa,OAAO;CAAY;CACzC;EAAE,OAAO;EAAa,OAAO;CAAY;CACzC;EAAE,OAAO;EAAa,OAAO;CAAY;AAC3C,GAEM,IAAqE,EAAqB,KAC7F,OAAO;CAAE,OAAO;CAAG,OAAO,EAAoB;AAAG,EACpD;AAEA,SAAgB,IAAmB;CACjC,IAAM,IAAK,EAAe,GACpB,EAAE,kBAAe,EAAiB,GAClC,CAAC,GAAQ,KAAa,EAAgC,EAAE,GACxD,CAAC,GAAQ,KAAa,EAAiB,EAAE,GACzC,CAAC,GAAM,KAAW,EAA8B,EAAE,GAClD,CAAC,GAAM,KAAW,EAAS,CAAC,GAE5B,IAAQ,EAAS,EAAE,YAAY,EAAE,OAAO,IAAI,EAAE,CAAC,GAC/C,IAAW,SACR,EAAM,MAAM,SAAS,CAAC,GAAG,KAAK,OAAO;EAAE,OAAO,EAAE;EAAI,OAAO,EAAE;CAAK,EAAE,GAC3E,CAAC,EAAM,IAAI,CACb,GAEM,IAAW,EAAY;EAC3B,QAAQ,KAAU,KAAA;EAClB,QAAQ,KAAU,KAAA;EAClB,YAAY;GAAE,OAAO;GAAW,QAAQ,IAAO;EAAU;CAC3D,CAAC,GAEK,IAAQ,QAAc;EAC1B,IAAM,IAAM,EAAS,MAAM,SAAS,CAAC;EACrC,OAAO,IAAO,EAAI,QAAQ,MAAM,EAAE,SAAS,CAAI,IAAI;CACrD,GAAG,CAAC,EAAS,MAAM,CAAI,CAAC,GAClB,IAAU,EAAS,MAAM,SAAS,WAAW,IAC7C,IAAa,GAAQ,KAAU,KAAU,IAEzC,UAAkB,EAAQ,CAAC;CAEjC,OACE,kBAAC,GAAD;EACE,OAAO,EAAG,uCAAuC,UAAU;EAC3D,aAAa,EACX,6CACA,4EACF;EACA,SACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,cAAc;IACxC,WAAU;cAHZ;KAKE,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;KAAE;KAC9B,EAAG,mDAAmD,oBAAoB;IACrE;OACR,kBAAC,UAAD;IACE,MAAK;IACL,aAAU;IACV,eAAe,EAAW,eAAe;IACzC,WAAU;cAJZ;KAME,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA;KAAE;KAC5B,EAAG,iDAAiD,kBAAkB;IACjE;KACL;;YAzBT,CA4BE,kBAAC,OAAD;GAAK,aAAU;GAAmB,WAAU;aAA5C;IACE,kBAAC,GAAD;KACE,WAAW,EAAG,8CAA8C,kBAAkB;KAC9E,UAAU,EAAG,6CAA6C,cAAc;KACxE,OAAO;KACP,WAAW,MAAM;MAEf,AADA,EAAU,CAAC,GACX,EAAU;KACZ;KACA,SAAS;IACV,CAAA;IACD,kBAAC,GAAD;KACE,WAAW,EAAG,4CAA4C,gBAAgB;KAC1E,UAAU,EAAG,0CAA0C,WAAW;KAClE,OAAO;KACP,WAAW,MAAM;MAEf,AADA,EAAU,CAAC,GACX,EAAU;KACZ;KACA,SAAS;IACV,CAAA;IACD,kBAAC,GAAD;KACE,WAAW,EAAG,4CAA4C,gBAAgB;KAC1E,UAAU,EAAG,0CAA0C,WAAW;KAClE,OAAO;KACP,WAAW,MAAM,EAAQ,CAAC;KAC1B,SAAS;IACV,CAAA;GACE;MAEJ,EAAS,YACR,kBAAC,GAAD,EAAc,OAAO,EAAG,yCAAyC,mBAAmB,EAAI,CAAA,IACtF,EAAS,UACX,kBAAC,GAAD,EAAY,eAAe,KAAK,EAAS,QAAQ,EAAI,CAAA,IACnD,EAAM,WAAW,IACnB,kBAAC,GAAD;GACE,cAAa;GACb,MAAM,kBAAC,GAAD,EAAc,WAAU,YAAa,CAAA;GAC3C,OAAO,EAAG,4CAA4C,mBAAmB;GACzE,aACE,IACI,EACE,sDACA,gDACF,IACA,EACE,gDACA,2DACF;GAEN,QACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,eAAe;IACzC,WAAU;cAET,EAAG,uDAAuD,kBAAkB;GACvE,CAAA;EAEX,CAAA,IAED,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAM,KAAK,MACV,kBAAC,GAAD;IAEW;IACT,eAAe,EAAW,aAAa,EAAQ,IAAI;GACpD,GAHM,EAAQ,EAGd,CACF;EACE,CAAA,GACL,kBAAC,GAAD;GACE,WAAU;GACJ;GACG;GACT,cAAc,GAAS,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC,CAAC;GAC/C,cAAc,GAAS,MAAM,IAAI,CAAC;EACnC,CAAA,CACD,EAAA,CAAA,CAEM;;AAEhB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OnboardingPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/onboarding/OnboardingPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAkC,KAAK,EAAE,EAAkB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"OnboardingPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/onboarding/OnboardingPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAkC,KAAK,EAAE,EAAkB,MAAM,OAAO,CAAC;AAgOhF,eAAO,MAAM,cAAc,EAAE,EA0X5B,CAAC"}
|