@burdenoff/microfe-atheletebridge 2026.706.1 → 2026.710.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atheletebridge/AthleteBridgeRoot.d.ts +5 -0
- package/dist/atheletebridge/AthleteBridgeRoot.d.ts.map +1 -0
- package/dist/atheletebridge/AthleteBridgeRoutes.d.ts +4 -0
- package/dist/atheletebridge/AthleteBridgeRoutes.d.ts.map +1 -0
- package/dist/atheletebridge/AthleteBridgeRoutes.js +135 -57
- package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
- package/dist/atheletebridge/components/AthleteSelect.d.ts +26 -0
- package/dist/atheletebridge/components/AthleteSelect.d.ts.map +1 -0
- package/dist/atheletebridge/components/Crest.d.ts +17 -0
- package/dist/atheletebridge/components/Crest.d.ts.map +1 -0
- package/dist/atheletebridge/components/CursorPager.d.ts +12 -0
- package/dist/atheletebridge/components/CursorPager.d.ts.map +1 -0
- package/dist/atheletebridge/components/DrillVideoLink.d.ts +12 -0
- package/dist/atheletebridge/components/DrillVideoLink.d.ts.map +1 -0
- package/dist/atheletebridge/components/EmptyState.d.ts +14 -0
- package/dist/atheletebridge/components/EmptyState.d.ts.map +1 -0
- package/dist/atheletebridge/components/FilterSelect.d.ts +23 -0
- package/dist/atheletebridge/components/FilterSelect.d.ts.map +1 -0
- package/dist/atheletebridge/components/FixtureCard.d.ts +10 -0
- package/dist/atheletebridge/components/FixtureCard.d.ts.map +1 -0
- package/dist/atheletebridge/components/LoadingState.d.ts +16 -0
- package/dist/atheletebridge/components/LoadingState.d.ts.map +1 -0
- package/dist/atheletebridge/components/MatchupRow.d.ts +16 -0
- package/dist/atheletebridge/components/MatchupRow.d.ts.map +1 -0
- package/dist/atheletebridge/components/MatchupRow.js.map +1 -1
- package/dist/atheletebridge/components/MetricsCard.d.ts +26 -0
- package/dist/atheletebridge/components/MetricsCard.d.ts.map +1 -0
- package/dist/atheletebridge/components/MetricsCard.js.map +1 -1
- package/dist/atheletebridge/components/PageLayout.d.ts +22 -0
- package/dist/atheletebridge/components/PageLayout.d.ts.map +1 -0
- package/dist/atheletebridge/components/ProgramCard.d.ts +15 -0
- package/dist/atheletebridge/components/ProgramCard.d.ts.map +1 -0
- package/dist/atheletebridge/components/RouteAnalytics.d.ts +9 -0
- package/dist/atheletebridge/components/RouteAnalytics.d.ts.map +1 -0
- package/dist/atheletebridge/components/RouteAnalytics.js +30 -0
- package/dist/atheletebridge/components/RouteAnalytics.js.map +1 -0
- package/dist/atheletebridge/components/Scoreboard.d.ts +47 -0
- package/dist/atheletebridge/components/Scoreboard.d.ts.map +1 -0
- package/dist/atheletebridge/components/Scoreboard.js.map +1 -1
- package/dist/atheletebridge/components/SessionCard.d.ts +14 -0
- package/dist/atheletebridge/components/SessionCard.d.ts.map +1 -0
- package/dist/atheletebridge/components/SessionCard.js.map +1 -1
- package/dist/atheletebridge/components/SessionStatusTimeline.d.ts +14 -0
- package/dist/atheletebridge/components/SessionStatusTimeline.d.ts.map +1 -0
- package/dist/atheletebridge/components/SessionStatusTimeline.js.map +1 -1
- package/dist/atheletebridge/components/StatusBadge.d.ts +16 -0
- package/dist/atheletebridge/components/StatusBadge.d.ts.map +1 -0
- package/dist/atheletebridge/components/StatusBadge.js.map +1 -1
- package/dist/atheletebridge/components/TeamCard.d.ts +16 -0
- package/dist/atheletebridge/components/TeamCard.d.ts.map +1 -0
- package/dist/atheletebridge/components/UserAvatar.d.ts +27 -0
- package/dist/atheletebridge/components/UserAvatar.d.ts.map +1 -0
- package/dist/atheletebridge/components/UserSearchSelect.d.ts +24 -0
- package/dist/atheletebridge/components/UserSearchSelect.d.ts.map +1 -0
- package/dist/atheletebridge/components/domain.d.ts +18 -0
- package/dist/atheletebridge/components/domain.d.ts.map +1 -0
- package/dist/atheletebridge/components/form.d.ts +15 -0
- package/dist/atheletebridge/components/form.d.ts.map +1 -0
- package/dist/atheletebridge/components/index.d.ts +30 -0
- package/dist/atheletebridge/components/index.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/AvatarStack.d.ts +13 -0
- package/dist/atheletebridge/components/viz/AvatarStack.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/BarChart.d.ts +17 -0
- package/dist/atheletebridge/components/viz/BarChart.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/DonutChart.d.ts +20 -0
- package/dist/atheletebridge/components/viz/DonutChart.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/LineChart.d.ts +20 -0
- package/dist/atheletebridge/components/viz/LineChart.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/ProgressRing.d.ts +20 -0
- package/dist/atheletebridge/components/viz/ProgressRing.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/RatingStars.d.ts +28 -0
- package/dist/atheletebridge/components/viz/RatingStars.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/Sparkline.d.ts +16 -0
- package/dist/atheletebridge/components/viz/Sparkline.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/StatCard.d.ts +22 -0
- package/dist/atheletebridge/components/viz/StatCard.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/StepProgress.d.ts +17 -0
- package/dist/atheletebridge/components/viz/StepProgress.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/Timeline.d.ts +21 -0
- package/dist/atheletebridge/components/viz/Timeline.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/index.d.ts +25 -0
- package/dist/atheletebridge/components/viz/index.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/lib/Avatar.d.ts +15 -0
- package/dist/atheletebridge/components/viz/lib/Avatar.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/lib/CartesianFrame.d.ts +44 -0
- package/dist/atheletebridge/components/viz/lib/CartesianFrame.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/lib/ResponsiveSvg.d.ts +27 -0
- package/dist/atheletebridge/components/viz/lib/ResponsiveSvg.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/lib/format.d.ts +10 -0
- package/dist/atheletebridge/components/viz/lib/format.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/lib/geometry.d.ts +41 -0
- package/dist/atheletebridge/components/viz/lib/geometry.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/lib/hooks.d.ts +18 -0
- package/dist/atheletebridge/components/viz/lib/hooks.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/lib/palette.d.ts +31 -0
- package/dist/atheletebridge/components/viz/lib/palette.d.ts.map +1 -0
- package/dist/atheletebridge/components/viz/lib/types.d.ts +27 -0
- package/dist/atheletebridge/components/viz/lib/types.d.ts.map +1 -0
- package/dist/atheletebridge/context/AthleteBridgeContext.d.ts +16 -0
- package/dist/atheletebridge/context/AthleteBridgeContext.d.ts.map +1 -0
- package/dist/atheletebridge/context/index.d.ts +6 -0
- package/dist/atheletebridge/context/index.d.ts.map +1 -0
- package/dist/atheletebridge/hooks/index.d.ts +3 -0
- package/dist/atheletebridge/hooks/index.d.ts.map +1 -0
- package/dist/atheletebridge/hooks/useAthleteBridgeAnalytics.d.ts +26 -0
- package/dist/atheletebridge/hooks/useAthleteBridgeAnalytics.d.ts.map +1 -0
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts +215 -0
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts.map +1 -0
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
- package/dist/atheletebridge/hooks/useAthleteBridgePageView.d.ts +16 -0
- package/dist/atheletebridge/hooks/useAthleteBridgePageView.d.ts.map +1 -0
- package/dist/atheletebridge/index.d.ts +11 -0
- package/dist/atheletebridge/index.d.ts.map +1 -0
- package/dist/atheletebridge/pages/DashboardPage.d.ts +12 -0
- package/dist/atheletebridge/pages/DashboardPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
- package/dist/atheletebridge/pages/StandingsPage.d.ts +10 -0
- package/dist/atheletebridge/pages/StandingsPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.d.ts +18 -0
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/athletes/AthleteProfilePage.d.ts +16 -0
- package/dist/atheletebridge/pages/athletes/AthleteProfilePage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.d.ts +12 -0
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/fixtures/FixtureFormPage.d.ts +12 -0
- package/dist/atheletebridge/pages/fixtures/FixtureFormPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/fixtures/FixtureFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.d.ts +11 -0
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/performance/PerformancePage.d.ts +18 -0
- package/dist/atheletebridge/pages/performance/PerformancePage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.d.ts +10 -0
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/programs/ProgramFormPage.d.ts +6 -0
- package/dist/atheletebridge/pages/programs/ProgramFormPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/programs/ProgramsListPage.d.ts +6 -0
- package/dist/atheletebridge/pages/programs/ProgramsListPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.d.ts +16 -0
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/sessions/SessionFormPage.d.ts +11 -0
- package/dist/atheletebridge/pages/sessions/SessionFormPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/sessions/SessionsListPage.d.ts +6 -0
- package/dist/atheletebridge/pages/sessions/SessionsListPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/teams/TeamDetailPage.d.ts +13 -0
- package/dist/atheletebridge/pages/teams/TeamDetailPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/teams/TeamFormPage.d.ts +6 -0
- package/dist/atheletebridge/pages/teams/TeamFormPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/teams/TeamsListPage.d.ts +6 -0
- package/dist/atheletebridge/pages/teams/TeamsListPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/tournaments/TournamentBracketPage.d.ts +18 -0
- package/dist/atheletebridge/pages/tournaments/TournamentBracketPage.d.ts.map +1 -0
- package/dist/atheletebridge/store/atheletebridgeStore.d.ts +50 -0
- package/dist/atheletebridge/store/atheletebridgeStore.d.ts.map +1 -0
- package/dist/atheletebridge/store/index.d.ts +6 -0
- package/dist/atheletebridge/store/index.d.ts.map +1 -0
- package/dist/atheletebridge/types/assistant.d.ts +62 -0
- package/dist/atheletebridge/types/assistant.d.ts.map +1 -0
- package/dist/atheletebridge/types/index.d.ts +411 -0
- package/dist/atheletebridge/types/index.d.ts.map +1 -0
- package/dist/atheletebridge/utils/cn.d.ts +4 -0
- package/dist/atheletebridge/utils/cn.d.ts.map +1 -0
- package/dist/atheletebridge/utils/datetime.d.ts +16 -0
- package/dist/atheletebridge/utils/datetime.d.ts.map +1 -0
- package/dist/atheletebridge/utils/formatters.d.ts +30 -0
- package/dist/atheletebridge/utils/formatters.d.ts.map +1 -0
- package/dist/atheletebridge/utils/identity.d.ts +24 -0
- package/dist/atheletebridge/utils/identity.d.ts.map +1 -0
- package/dist/atheletebridge/utils/index.d.ts +12 -0
- package/dist/atheletebridge/utils/index.d.ts.map +1 -0
- package/dist/atheletebridge/utils/navigation.d.ts +20 -0
- package/dist/atheletebridge/utils/navigation.d.ts.map +1 -0
- package/dist/atheletebridge/utils/pageContext.d.ts +25 -0
- package/dist/atheletebridge/utils/pageContext.d.ts.map +1 -0
- package/dist/atheletebridge/utils/pageContext.js +48 -0
- package/dist/atheletebridge/utils/pageContext.js.map +1 -0
- package/dist/atheletebridge/utils/timeAgo.d.ts +16 -0
- package/dist/atheletebridge/utils/timeAgo.d.ts.map +1 -0
- package/dist/atheletebridge/utils/url.d.ts +22 -0
- package/dist/atheletebridge/utils/url.d.ts.map +1 -0
- package/dist/generated/global-operations.d.ts +2 -0
- package/dist/generated/global-operations.d.ts.map +1 -0
- package/dist/generated/global-types.d.ts +23527 -0
- package/dist/generated/global-types.d.ts.map +1 -0
- package/dist/generated/wspace-operations.d.ts +208 -0
- package/dist/generated/wspace-operations.d.ts.map +1 -0
- package/dist/generated/wspace-operations.js +59 -0
- package/dist/generated/wspace-operations.js.map +1 -0
- package/dist/generated/wspace-types.d.ts +57851 -0
- package/dist/generated/wspace-types.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -1
- package/dist/services/assistantApi.d.ts +70 -0
- package/dist/services/assistantApi.d.ts.map +1 -0
- package/dist/services/assistantApi.js +202 -0
- package/dist/services/assistantApi.js.map +1 -0
- package/dist/services/createSandboxAssistantTransport.d.ts +23 -0
- package/dist/services/createSandboxAssistantTransport.d.ts.map +1 -0
- package/dist/services/createSandboxAssistantTransport.js +120 -0
- package/dist/services/createSandboxAssistantTransport.js.map +1 -0
- package/package.json +5 -1
|
@@ -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 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';\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 { 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 label=\"Loading your sporting operation…\" />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={`Welcome back, ${firstName}`}\n description=\"Your teams, fixtures, training and coaching — one connected record.\"\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\" /> New team\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('/performance')}\n className={GRADIENT_CTA}\n >\n <TrendingUp className=\"h-4 w-4\" /> Track performance\n </button>\n </div>\n }\n >\n <PagePurpose className=\"mb-6\">\n AthleteBridge is the operating system for sports, coaching and training. From here you can\n assemble your roster, schedule fixtures, build training programs, run coaching sessions and\n track athlete performance — all reading and writing the same workspace-scoped source of\n truth.\n </PagePurpose>\n\n {/* KPIs — glass tiles with teal stat values (2-col mobile → 4-col lg) */}\n <div 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=\"Teams\"\n tone=\"accent\"\n featured\n />\n <MetricsCard\n icon={<CalendarDays className=\"h-5 w-5\" />}\n value={fixtureItems.length}\n label=\"Fixtures\"\n tone=\"accent\"\n />\n <MetricsCard\n icon={<Dumbbell className=\"h-5 w-5\" />}\n value={`${publishedPrograms}/${programItems.length}`}\n label=\"Programs published\"\n tone=\"accent\"\n />\n <MetricsCard\n icon={<ClipboardCheck className=\"h-5 w-5\" />}\n value={sessionItems.length}\n label=\"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 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\">Upcoming fixtures</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View all fixtures <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=\"No upcoming fixtures\"\n description=\"Schedule a match to start tracking results and standings.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> 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>\n <div className=\"mb-4 flex items-center min-h-[1.75rem]\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Recent activity</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=\"No recent activity\"\n description=\"Create teams, publish programs or record results — the latest moves land here.\"\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 className=\"mb-8\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Upcoming sessions</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n 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=\"No upcoming sessions\"\n description=\"Book a coaching session with an athlete to get started.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> 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>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Your teams</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n 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=\"No teams yet\"\n description=\"Create your first team to build a roster and schedule fixtures.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> Create team\n </button>\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 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';\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 { 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 label=\"Loading your sporting operation…\" />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={`Welcome back, ${firstName}`}\n description=\"Your teams, fixtures, training and coaching — one connected record.\"\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\" /> New team\n </button>\n <button type=\"button\" onClick={() => navigateTo('/performance')} className={GRADIENT_CTA}>\n <TrendingUp className=\"h-4 w-4\" /> Track performance\n </button>\n </div>\n }\n >\n <PagePurpose className=\"mb-6\">\n AthleteBridge is the operating system for sports, coaching and training. From here you can\n assemble your roster, schedule fixtures, build training programs, run coaching sessions and\n track athlete performance — all reading and writing the same workspace-scoped source of\n truth.\n </PagePurpose>\n\n {/* KPIs — glass tiles with teal stat values (2-col mobile → 4-col lg) */}\n <div 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=\"Teams\"\n tone=\"accent\"\n featured\n />\n <MetricsCard\n icon={<CalendarDays className=\"h-5 w-5\" />}\n value={fixtureItems.length}\n label=\"Fixtures\"\n tone=\"accent\"\n />\n <MetricsCard\n icon={<Dumbbell className=\"h-5 w-5\" />}\n value={`${publishedPrograms}/${programItems.length}`}\n label=\"Programs published\"\n tone=\"accent\"\n />\n <MetricsCard\n icon={<ClipboardCheck className=\"h-5 w-5\" />}\n value={sessionItems.length}\n label=\"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 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\">Upcoming fixtures</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View all fixtures <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=\"No upcoming fixtures\"\n description=\"Schedule a match to start tracking results and standings.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> 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>\n <div className=\"mb-4 flex items-center min-h-[1.75rem]\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Recent activity</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=\"No recent activity\"\n description=\"Create teams, publish programs or record results — the latest moves land here.\"\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 className=\"mb-8\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Upcoming sessions</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n 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=\"No upcoming sessions\"\n description=\"Book a coaching session with an athlete to get started.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> 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>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Your teams</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n 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=\"No teams yet\"\n description=\"Create your first team to build a roster and schedule fixtures.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> Create team\n </button>\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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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standings
|
|
3
|
+
* @module atheletebridge/pages
|
|
4
|
+
*
|
|
5
|
+
* Reads the live competition table (athStandings) for a chosen competition.
|
|
6
|
+
* The svc requires a competition name, so the user picks one (seeded from
|
|
7
|
+
* fixtures that carry a competition, or typed directly).
|
|
8
|
+
*/
|
|
9
|
+
export declare function StandingsPage(): import("react").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=StandingsPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StandingsPage.d.ts","sourceRoot":"","sources":["../../../src/atheletebridge/pages/StandingsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAcH,wBAAgB,aAAa,gCAmM5B"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coach Analytics
|
|
3
|
+
* @module atheletebridge/pages/analytics
|
|
4
|
+
*
|
|
5
|
+
* An honest coaching-analytics dashboard built ENTIRELY from real aggregates via
|
|
6
|
+
* `useCoachAnalytics()` — total sessions, average session rating, completion
|
|
7
|
+
* rate, session-status mix, average rating by coach, weekly training-load /
|
|
8
|
+
* session trends, and a league-standings snapshot.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately OMITS the mock's fabricated composite indices (Form Index,
|
|
11
|
+
* Fitness Score, Fatigue Index, Injury Status, Availability %) — there is no
|
|
12
|
+
* wellness / injury / availability data in the schema, so those are not faked.
|
|
13
|
+
* The one derived metric shown (ACWR training load) uses standard sports-science
|
|
14
|
+
* maths over real `workout.totalLoad` and carries an explanatory tooltip.
|
|
15
|
+
* Semantic tokens only; responsive.
|
|
16
|
+
*/
|
|
17
|
+
export declare function CoachAnalyticsPage(): import("react").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=CoachAnalyticsPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CoachAnalyticsPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/analytics/CoachAnalyticsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA6DH,wBAAgB,kBAAkB,gCAmUjC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Athlete Profile
|
|
3
|
+
* @module atheletebridge/pages/athletes
|
|
4
|
+
*
|
|
5
|
+
* A consolidated single-athlete view composed PURELY from real data via
|
|
6
|
+
* `useAthleteProfile(teamId, memberId)` — the resolved team member (identity +
|
|
7
|
+
* physical stats), their performance metrics (trend charts + improvement
|
|
8
|
+
* tiles), workout history and coaching sessions (with read-only ratings).
|
|
9
|
+
*
|
|
10
|
+
* Honesty rules: there is no photo, age/DOB, or "fitness score" field on the
|
|
11
|
+
* schema, so this screen renders an initials/jersey monogram (never a
|
|
12
|
+
* fabricated headshot) and OMITS any composite wellness score. Every number
|
|
13
|
+
* traces to a real row. Semantic tokens only; fully responsive.
|
|
14
|
+
*/
|
|
15
|
+
export declare function AthleteProfilePage(): import("react").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=AthleteProfilePage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AthleteProfilePage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/athletes/AthleteProfilePage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAmHH,wBAAgB,kBAAkB,gCAkbjC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture Detail
|
|
3
|
+
* @module atheletebridge/pages/fixtures
|
|
4
|
+
*
|
|
5
|
+
* Reads a single fixture (athFixture) and lets the user record a final result
|
|
6
|
+
* (athRecordFixtureResult) which marks it COMPLETED and recomputes standings.
|
|
7
|
+
*
|
|
8
|
+
* The header is a crest-vs-crest Scoreboard hero; the record-result panel shows
|
|
9
|
+
* each team's crest beside its score input. Semantic tokens only, responsive.
|
|
10
|
+
*/
|
|
11
|
+
export declare function FixtureDetailPage(): import("react").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=FixtureDetailPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FixtureDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/fixtures/FixtureDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAeH,wBAAgB,iBAAiB,gCA2KhC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture Schedule Form
|
|
3
|
+
* @module atheletebridge/pages/fixtures
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the concept mock: a GlassCard-framed two-column form with a segmented
|
|
6
|
+
* fixture-type control (all five AthFixtureType values incl. SCRIMMAGE), team
|
|
7
|
+
* crest previews, competition/venue comboboxes seeded from existing fixtures,
|
|
8
|
+
* and iconized date/time inputs. Every field maps to AthScheduleFixtureInput.
|
|
9
|
+
* Semantic tokens only, responsive.
|
|
10
|
+
*/
|
|
11
|
+
export declare function FixtureFormPage(): import("react").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=FixtureFormPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FixtureFormPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/fixtures/FixtureFormPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsCH,wBAAgB,eAAe,gCAkU9B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FixtureFormPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/fixtures/FixtureFormPage.tsx"],"sourcesContent":["/**\n * Fixture Schedule Form\n * @module atheletebridge/pages/fixtures\n *\n * Mirrors the concept mock: a GlassCard-framed two-column form with a segmented\n * fixture-type control (all five AthFixtureType values incl. SCRIMMAGE), team\n * crest previews, competition/venue comboboxes seeded from existing fixtures,\n * and iconized date/time inputs. Every field maps to AthScheduleFixtureInput.\n * Semantic tokens only, responsive.\n */\n\nimport { type KeyboardEvent, useMemo, useState } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Loader2,\n Save,\n Calendar,\n Clock,\n Handshake,\n Shield,\n Trophy,\n Network,\n Swords,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { LoadingState } from '../../components/LoadingState';\nimport { Crest } from '../../components/Crest';\nimport { FIXTURE_TYPE_LABELS } from '../../components/domain';\nimport { cn } from '../../utils/cn';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useScheduleFixture, useTeams, useFixtures } from '../../hooks/useAthleteBridgeApi';\nimport { toIsoOrUndefined } from '../../utils/datetime';\nimport type { AthFixtureType } from '../../types';\n\nconst TYPE_ICONS: Record<AthFixtureType, LucideIcon> = {\n FRIENDLY: Handshake,\n LEAGUE: Shield,\n CUP: Trophy,\n TOURNAMENT: Network,\n SCRIMMAGE: Swords,\n};\nconst TYPES = Object.keys(FIXTURE_TYPE_LABELS) as AthFixtureType[];\n\nexport function FixtureFormPage() {\n const [searchParams] = useSearchParams();\n const { navigateTo } = useAthleteBridge();\n\n const teams = useTeams({ pagination: { limit: 100 } });\n const existingFixtures = useFixtures({ pagination: { limit: 100 } });\n const scheduleFixture = useScheduleFixture();\n\n const [homeTeamId, setHomeTeamId] = useState(searchParams.get('homeTeamId') ?? '');\n const [awayTeamId, setAwayTeamId] = useState('');\n const [type, setType] = useState<AthFixtureType>('FRIENDLY');\n const [competition, setCompetition] = useState('');\n const [round, setRound] = useState('');\n const [venue, setVenue] = useState('');\n const [scheduledAt, setScheduledAt] = useState('');\n const [kickoffAt, setKickoffAt] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const teamOptions = teams.data?.items ?? [];\n const homeTeam = teamOptions.find((t) => t.id === homeTeamId);\n const awayTeam = teamOptions.find((t) => t.id === awayTeamId);\n\n // Seed comboboxes from distinct values on existing fixtures (honest, derived).\n const { competitions, venues } = useMemo(() => {\n const comp = new Set<string>();\n const ven = new Set<string>();\n for (const f of existingFixtures.data?.items ?? []) {\n if (f.competition) comp.add(f.competition);\n if (f.venue) ven.add(f.venue);\n }\n return {\n competitions: Array.from(comp).sort(),\n venues: Array.from(ven).sort(),\n };\n }, [existingFixtures.data]);\n\n const handleTypeKey = (e: KeyboardEvent<HTMLDivElement>) => {\n const idx = TYPES.indexOf(type);\n if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n e.preventDefault();\n setType(TYPES[(idx + 1) % TYPES.length]);\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n e.preventDefault();\n setType(TYPES[(idx - 1 + TYPES.length) % TYPES.length]);\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!homeTeamId) {\n setError('A home team is required.');\n return;\n }\n if (!scheduledAt) {\n setError('A scheduled date and time is required.');\n return;\n }\n if (awayTeamId && awayTeamId === homeTeamId) {\n setError('Home and away teams must be different.');\n return;\n }\n\n try {\n const created = await scheduleFixture.mutateAsync({\n homeTeamId,\n awayTeamId: awayTeamId || undefined,\n type,\n competition: competition.trim() || undefined,\n round: round.trim() || undefined,\n venue: venue.trim() || undefined,\n scheduledAt: new Date(scheduledAt).toISOString(),\n kickoffAt: toIsoOrUndefined(kickoffAt),\n notes: notes.trim() || undefined,\n });\n navigateTo(`/fixtures/${created.id}`);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to schedule fixture.');\n }\n };\n\n if (teams.isLoading) {\n return (\n <PageLayout title=\"Schedule fixture\">\n <LoadingState label=\"Loading teams…\" />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Schedule fixture\"\n description=\"Set up a match between a home team and an optional away team.\"\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\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-3xl\">\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-7\">\n {/* Home / away teams with crest preview */}\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Home team\" htmlFor=\"homeTeam\" required>\n <select\n id=\"homeTeam\"\n className={fieldInputClass}\n value={homeTeamId}\n onChange={(e) => setHomeTeamId(e.target.value)}\n >\n <option value=\"\">Select a team</option>\n {teamOptions.map((tm) => (\n <option key={tm.id} value={tm.id}>\n {tm.name}\n </option>\n ))}\n </select>\n {homeTeam ? (\n <div className=\"mt-2 flex items-center gap-2\">\n <Crest name={homeTeam.name} sport={homeTeam.sport} size={28} />\n <span className=\"truncate text-xs font-medium text-text-secondary\">\n {homeTeam.name}\n </span>\n </div>\n ) : null}\n </FormField>\n\n <FormField label=\"Away team\" htmlFor=\"awayTeam\" hint=\"Optional for trials / open events.\">\n <select\n id=\"awayTeam\"\n className={fieldInputClass}\n value={awayTeamId}\n onChange={(e) => setAwayTeamId(e.target.value)}\n >\n <option value=\"\">No away team</option>\n {teamOptions.map((tm) => (\n <option key={tm.id} value={tm.id}>\n {tm.name}\n </option>\n ))}\n </select>\n {awayTeam ? (\n <div className=\"mt-2 flex items-center gap-2\">\n <Crest name={awayTeam.name} sport={awayTeam.sport} size={28} />\n <span className=\"truncate text-xs font-medium text-text-secondary\">\n {awayTeam.name}\n </span>\n </div>\n ) : null}\n </FormField>\n </div>\n\n {/* Type — segmented control (all five values incl. SCRIMMAGE) */}\n <div className=\"mt-5\">\n <span className=\"mb-1.5 block text-sm font-medium text-text-primary\">Type</span>\n <div\n role=\"radiogroup\"\n aria-label=\"Fixture type\"\n onKeyDown={handleTypeKey}\n className=\"grid grid-cols-2 gap-2 sm:grid-cols-5\"\n >\n {TYPES.map((tp) => {\n const Icon = TYPE_ICONS[tp];\n const active = type === tp;\n return (\n <button\n key={tp}\n type=\"button\"\n role=\"radio\"\n aria-checked={active}\n tabIndex={active ? 0 : -1}\n onClick={() => setType(tp)}\n className={cn(\n 'flex items-center justify-center gap-1.5 rounded-lg border px-2.5 py-2.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n active\n ? 'border-action-primary-bg bg-[var(--color-accent-soft)] text-text-primary ring-1 ring-inset ring-action-primary-bg'\n : 'border-border-seam text-text-secondary hover:bg-bg-sunken'\n )}\n >\n <Icon className=\"h-4 w-4 shrink-0\" aria-hidden=\"true\" />\n {FIXTURE_TYPE_LABELS[tp]}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Competition / round */}\n <div className=\"mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Competition\" htmlFor=\"competition\" hint=\"Used to group standings.\">\n <input\n id=\"competition\"\n list=\"ath-fixture-competitions\"\n className={fieldInputClass}\n value={competition}\n onChange={(e) => setCompetition(e.target.value)}\n placeholder=\"e.g. Spring League 2026\"\n />\n <datalist id=\"ath-fixture-competitions\">\n {competitions.map((c) => (\n <option key={c} value={c} />\n ))}\n </datalist>\n </FormField>\n\n <FormField label=\"Round\" htmlFor=\"round\">\n <input\n id=\"round\"\n className={fieldInputClass}\n value={round}\n onChange={(e) => setRound(e.target.value)}\n placeholder=\"e.g. Round 4\"\n />\n </FormField>\n </div>\n\n {/* Venue */}\n <div className=\"mt-5\">\n <FormField label=\"Venue\" htmlFor=\"venue\">\n <input\n id=\"venue\"\n list=\"ath-fixture-venues\"\n className={fieldInputClass}\n value={venue}\n onChange={(e) => setVenue(e.target.value)}\n placeholder=\"e.g. Riverside Stadium\"\n />\n <datalist id=\"ath-fixture-venues\">\n {venues.map((v) => (\n <option key={v} value={v} />\n ))}\n </datalist>\n </FormField>\n </div>\n\n {/* Schedule / kickoff */}\n <div className=\"mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Scheduled at\" htmlFor=\"scheduledAt\" required>\n <div className=\"relative\">\n <Calendar\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"scheduledAt\"\n type=\"datetime-local\"\n className={cn(fieldInputClass, 'pl-9')}\n value={scheduledAt}\n onChange={(e) => setScheduledAt(e.target.value)}\n />\n </div>\n </FormField>\n\n <FormField label=\"Kick-off at\" htmlFor=\"kickoffAt\" hint=\"Optional refinement.\">\n <div className=\"relative\">\n <Clock\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"kickoffAt\"\n type=\"datetime-local\"\n className={cn(fieldInputClass, 'pl-9')}\n value={kickoffAt}\n onChange={(e) => setKickoffAt(e.target.value)}\n />\n </div>\n </FormField>\n </div>\n\n {/* Notes */}\n <div className=\"mt-5\">\n <FormField label=\"Notes\" htmlFor=\"notes\">\n <textarea\n id=\"notes\"\n className={fieldInputClass}\n rows={3}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"Any extra context for this fixture.\"\n />\n </FormField>\n </div>\n\n {error && <p className=\"mt-5 text-sm text-status-error-text\">{error}</p>}\n\n {/* Footer */}\n <div className=\"mt-6 flex items-center justify-end gap-3 border-t border-border-seam pt-5\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={scheduleFixture.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {scheduleFixture.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Schedule fixture\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FixtureFormPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/fixtures/FixtureFormPage.tsx"],"sourcesContent":["/**\n * Fixture Schedule Form\n * @module atheletebridge/pages/fixtures\n *\n * Mirrors the concept mock: a GlassCard-framed two-column form with a segmented\n * fixture-type control (all five AthFixtureType values incl. SCRIMMAGE), team\n * crest previews, competition/venue comboboxes seeded from existing fixtures,\n * and iconized date/time inputs. Every field maps to AthScheduleFixtureInput.\n * Semantic tokens only, responsive.\n */\n\nimport { type KeyboardEvent, useMemo, useState } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Loader2,\n Save,\n Calendar,\n Clock,\n Handshake,\n Shield,\n Trophy,\n Network,\n Swords,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { LoadingState } from '../../components/LoadingState';\nimport { Crest } from '../../components/Crest';\nimport { FIXTURE_TYPE_LABELS } from '../../components/domain';\nimport { cn } from '../../utils/cn';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useScheduleFixture, useTeams, useFixtures } from '../../hooks/useAthleteBridgeApi';\nimport { toIsoOrUndefined } from '../../utils/datetime';\nimport type { AthFixtureType } from '../../types';\n\nconst TYPE_ICONS: Record<AthFixtureType, LucideIcon> = {\n FRIENDLY: Handshake,\n LEAGUE: Shield,\n CUP: Trophy,\n TOURNAMENT: Network,\n SCRIMMAGE: Swords,\n};\nconst TYPES = Object.keys(FIXTURE_TYPE_LABELS) as AthFixtureType[];\n\nexport function FixtureFormPage() {\n const [searchParams] = useSearchParams();\n const { navigateTo } = useAthleteBridge();\n\n const teams = useTeams({ pagination: { limit: 100 } });\n const existingFixtures = useFixtures({ pagination: { limit: 100 } });\n const scheduleFixture = useScheduleFixture();\n\n const [homeTeamId, setHomeTeamId] = useState(searchParams.get('homeTeamId') ?? '');\n const [awayTeamId, setAwayTeamId] = useState('');\n const [type, setType] = useState<AthFixtureType>('FRIENDLY');\n const [competition, setCompetition] = useState('');\n const [round, setRound] = useState('');\n const [venue, setVenue] = useState('');\n const [scheduledAt, setScheduledAt] = useState('');\n const [kickoffAt, setKickoffAt] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const teamOptions = teams.data?.items ?? [];\n const homeTeam = teamOptions.find((t) => t.id === homeTeamId);\n const awayTeam = teamOptions.find((t) => t.id === awayTeamId);\n\n // Seed comboboxes from distinct values on existing fixtures (honest, derived).\n const { competitions, venues } = useMemo(() => {\n const comp = new Set<string>();\n const ven = new Set<string>();\n for (const f of existingFixtures.data?.items ?? []) {\n if (f.competition) comp.add(f.competition);\n if (f.venue) ven.add(f.venue);\n }\n return {\n competitions: Array.from(comp).sort(),\n venues: Array.from(ven).sort(),\n };\n }, [existingFixtures.data]);\n\n const handleTypeKey = (e: KeyboardEvent<HTMLDivElement>) => {\n const idx = TYPES.indexOf(type);\n if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n e.preventDefault();\n setType(TYPES[(idx + 1) % TYPES.length]);\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n e.preventDefault();\n setType(TYPES[(idx - 1 + TYPES.length) % TYPES.length]);\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!homeTeamId) {\n setError('A home team is required.');\n return;\n }\n if (!scheduledAt) {\n setError('A scheduled date and time is required.');\n return;\n }\n if (awayTeamId && awayTeamId === homeTeamId) {\n setError('Home and away teams must be different.');\n return;\n }\n\n try {\n const created = await scheduleFixture.mutateAsync({\n homeTeamId,\n awayTeamId: awayTeamId || undefined,\n type,\n competition: competition.trim() || undefined,\n round: round.trim() || undefined,\n venue: venue.trim() || undefined,\n scheduledAt: new Date(scheduledAt).toISOString(),\n kickoffAt: toIsoOrUndefined(kickoffAt),\n notes: notes.trim() || undefined,\n });\n navigateTo(`/fixtures/${created.id}`);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to schedule fixture.');\n }\n };\n\n if (teams.isLoading) {\n return (\n <PageLayout title=\"Schedule fixture\">\n <LoadingState label=\"Loading teams…\" />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Schedule fixture\"\n description=\"Set up a match between a home team and an optional away team.\"\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\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-3xl\">\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-7\">\n {/* Home / away teams with crest preview */}\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Home team\" htmlFor=\"homeTeam\" required>\n <select\n id=\"homeTeam\"\n className={fieldInputClass}\n value={homeTeamId}\n onChange={(e) => setHomeTeamId(e.target.value)}\n >\n <option value=\"\">Select a team</option>\n {teamOptions.map((tm) => (\n <option key={tm.id} value={tm.id}>\n {tm.name}\n </option>\n ))}\n </select>\n {homeTeam ? (\n <div className=\"mt-2 flex items-center gap-2\">\n <Crest name={homeTeam.name} sport={homeTeam.sport} size={28} />\n <span className=\"truncate text-xs font-medium text-text-secondary\">\n {homeTeam.name}\n </span>\n </div>\n ) : null}\n </FormField>\n\n <FormField\n label=\"Away team\"\n htmlFor=\"awayTeam\"\n hint=\"Optional for trials / open events.\"\n >\n <select\n id=\"awayTeam\"\n className={fieldInputClass}\n value={awayTeamId}\n onChange={(e) => setAwayTeamId(e.target.value)}\n >\n <option value=\"\">No away team</option>\n {teamOptions.map((tm) => (\n <option key={tm.id} value={tm.id}>\n {tm.name}\n </option>\n ))}\n </select>\n {awayTeam ? (\n <div className=\"mt-2 flex items-center gap-2\">\n <Crest name={awayTeam.name} sport={awayTeam.sport} size={28} />\n <span className=\"truncate text-xs font-medium text-text-secondary\">\n {awayTeam.name}\n </span>\n </div>\n ) : null}\n </FormField>\n </div>\n\n {/* Type — segmented control (all five values incl. SCRIMMAGE) */}\n <div className=\"mt-5\">\n <span className=\"mb-1.5 block text-sm font-medium text-text-primary\">Type</span>\n <div\n role=\"radiogroup\"\n aria-label=\"Fixture type\"\n onKeyDown={handleTypeKey}\n className=\"grid grid-cols-2 gap-2 sm:grid-cols-5\"\n >\n {TYPES.map((tp) => {\n const Icon = TYPE_ICONS[tp];\n const active = type === tp;\n return (\n <button\n key={tp}\n type=\"button\"\n role=\"radio\"\n aria-checked={active}\n tabIndex={active ? 0 : -1}\n onClick={() => setType(tp)}\n className={cn(\n 'flex items-center justify-center gap-1.5 rounded-lg border px-2.5 py-2.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n active\n ? 'border-action-primary-bg bg-[var(--color-accent-soft)] text-text-primary ring-1 ring-inset ring-action-primary-bg'\n : 'border-border-seam text-text-secondary hover:bg-bg-sunken'\n )}\n >\n <Icon className=\"h-4 w-4 shrink-0\" aria-hidden=\"true\" />\n {FIXTURE_TYPE_LABELS[tp]}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Competition / round */}\n <div className=\"mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Competition\" htmlFor=\"competition\" hint=\"Used to group standings.\">\n <input\n id=\"competition\"\n list=\"ath-fixture-competitions\"\n className={fieldInputClass}\n value={competition}\n onChange={(e) => setCompetition(e.target.value)}\n placeholder=\"e.g. Spring League 2026\"\n />\n <datalist id=\"ath-fixture-competitions\">\n {competitions.map((c) => (\n <option key={c} value={c} />\n ))}\n </datalist>\n </FormField>\n\n <FormField label=\"Round\" htmlFor=\"round\">\n <input\n id=\"round\"\n className={fieldInputClass}\n value={round}\n onChange={(e) => setRound(e.target.value)}\n placeholder=\"e.g. Round 4\"\n />\n </FormField>\n </div>\n\n {/* Venue */}\n <div className=\"mt-5\">\n <FormField label=\"Venue\" htmlFor=\"venue\">\n <input\n id=\"venue\"\n list=\"ath-fixture-venues\"\n className={fieldInputClass}\n value={venue}\n onChange={(e) => setVenue(e.target.value)}\n placeholder=\"e.g. Riverside Stadium\"\n />\n <datalist id=\"ath-fixture-venues\">\n {venues.map((v) => (\n <option key={v} value={v} />\n ))}\n </datalist>\n </FormField>\n </div>\n\n {/* Schedule / kickoff */}\n <div className=\"mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Scheduled at\" htmlFor=\"scheduledAt\" required>\n <div className=\"relative\">\n <Calendar\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"scheduledAt\"\n type=\"datetime-local\"\n className={cn(fieldInputClass, 'pl-9')}\n value={scheduledAt}\n onChange={(e) => setScheduledAt(e.target.value)}\n />\n </div>\n </FormField>\n\n <FormField label=\"Kick-off at\" htmlFor=\"kickoffAt\" hint=\"Optional refinement.\">\n <div className=\"relative\">\n <Clock\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"kickoffAt\"\n type=\"datetime-local\"\n className={cn(fieldInputClass, 'pl-9')}\n value={kickoffAt}\n onChange={(e) => setKickoffAt(e.target.value)}\n />\n </div>\n </FormField>\n </div>\n\n {/* Notes */}\n <div className=\"mt-5\">\n <FormField label=\"Notes\" htmlFor=\"notes\">\n <textarea\n id=\"notes\"\n className={fieldInputClass}\n rows={3}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"Any extra context for this fixture.\"\n />\n </FormField>\n </div>\n\n {error && <p className=\"mt-5 text-sm text-status-error-text\">{error}</p>}\n\n {/* Footer */}\n <div className=\"mt-6 flex items-center justify-end gap-3 border-t border-border-seam pt-5\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={scheduleFixture.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {scheduleFixture.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Schedule fixture\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixtures List
|
|
3
|
+
* @module atheletebridge/pages/fixtures
|
|
4
|
+
*
|
|
5
|
+
* Crest-vs-crest FixtureCard grid with status, team and type filters. The team
|
|
6
|
+
* filter is server-backed (athListFixtures accepts teamId); the type filter is
|
|
7
|
+
* applied over the loaded page. A "Tournament bracket" shortcut links to the
|
|
8
|
+
* tournaments view. Semantic tokens only, responsive.
|
|
9
|
+
*/
|
|
10
|
+
export declare function FixturesListPage(): import("react").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=FixturesListPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FixturesListPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/fixtures/FixturesListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA6BH,wBAAgB,gBAAgB,gCA+H/B"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance
|
|
3
|
+
* @module atheletebridge/pages/performance
|
|
4
|
+
*
|
|
5
|
+
* The coach's athlete-performance workbench: pick an athlete from a team roster,
|
|
6
|
+
* then log workouts (athLogWorkout) and record performance metrics
|
|
7
|
+
* (athRecordMetric), and review that athlete's workout history
|
|
8
|
+
* (athListWorkoutLogs, offset-paginated) and metrics.
|
|
9
|
+
*
|
|
10
|
+
* Elevated to the concept bar: each measured metric (type + label) becomes a
|
|
11
|
+
* hero stat tile with its big latest value, a real recordedAt-ordered trend
|
|
12
|
+
* sparkline, a min→max range scale and a signed delta-vs-previous — all derived
|
|
13
|
+
* from the real time-series (nothing fabricated). Workout history renders as a
|
|
14
|
+
* responsive table that reflows to cards on mobile, with a thumb-reachable
|
|
15
|
+
* sticky "Log workout" action.
|
|
16
|
+
*/
|
|
17
|
+
export declare function PerformancePage(): import("react").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=PerformancePage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PerformancePage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAuGH,wBAAgB,eAAe,gCA+B9B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PerformancePage.js","names":[],"sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"sourcesContent":["/**\n * Performance\n * @module atheletebridge/pages/performance\n *\n * The coach's athlete-performance workbench: pick an athlete from a team roster,\n * then log workouts (athLogWorkout) and record performance metrics\n * (athRecordMetric), and review that athlete's workout history\n * (athListWorkoutLogs, offset-paginated) and metrics.\n *\n * Elevated to the concept bar: each measured metric (type + label) becomes a\n * hero stat tile with its big latest value, a real recordedAt-ordered trend\n * sparkline, a min→max range scale and a signed delta-vs-previous — all derived\n * from the real time-series (nothing fabricated). Workout history renders as a\n * responsive table that reflows to cards on mobile, with a thumb-reachable\n * sticky \"Log workout\" action.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n ArrowDown,\n ArrowUp,\n Dumbbell,\n Gauge,\n HeartPulse,\n Loader2,\n Move,\n Plus,\n Save,\n Scale,\n Target,\n Users,\n Zap,\n type LucideIcon,\n} from 'lucide-react';\nimport {\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { AthleteSelect, type AthleteSelectValue } from '../../components/AthleteSelect';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { CursorPager } from '../../components/CursorPager';\nimport { Sparkline } from '../../components/viz';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { METRIC_TYPE_LABELS } from '../../components/domain';\nimport {\n useWorkoutLogs,\n usePerformanceMetrics,\n useLogWorkout,\n useRecordMetric,\n} from '../../hooks/useAthleteBridgeApi';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type {\n AthMetricType,\n AthPerformanceMetric,\n AthWorkoutLog,\n AthWorkoutStatus,\n} from '../../types';\n\nconst PAGE_SIZE = 10;\n\nconst WORKOUT_STATUSES: AthWorkoutStatus[] = ['COMPLETED', 'PLANNED', 'SKIPPED'];\nconst METRIC_TYPES = Object.keys(METRIC_TYPE_LABELS) as AthMetricType[];\n\n/** Brand gradient CTA — token-only (no raw colors). */\nconst GRADIENT_CTA =\n 'inline-flex items-center justify-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow] hover:shadow-elevation-2 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\n/** Per-metric-type icon for the stat tiles. */\nconst METRIC_ICON: Record<AthMetricType, LucideIcon> = {\n SPEED: Zap,\n ENDURANCE: Activity,\n STRENGTH: Dumbbell,\n AGILITY: Move,\n ACCURACY: Target,\n WEIGHT: Scale,\n HEART_RATE: HeartPulse,\n CUSTOM: Gauge,\n};\n\nconst numOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nconst floatOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseFloat(t);\n return Number.isNaN(n) ? undefined : n;\n};\n\n/** Current local time as a `datetime-local` value (`YYYY-MM-DDTHH:mm`). */\nconst nowLocal = (): string => {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60_000);\n return local.toISOString().slice(0, 16);\n};\n\n/** Trim trailing zeros from a fixed-precision number for compact display. */\nconst fmtNum = (n: number): string => {\n const abs = Math.abs(n);\n const s = abs >= 100 ? n.toFixed(0) : n.toFixed(2);\n return s.replace(/\\.?0+$/, '');\n};\n\nexport function PerformancePage() {\n const [athlete, setAthlete] = useState<AthleteSelectValue>({ athleteId: '', displayName: '' });\n\n return (\n <PageLayout\n title=\"Performance\"\n description=\"Log workouts, record metrics and track an athlete's progress over time.\"\n >\n {/* Athlete picker — sticks to the top of the scroll area as an insight panel */}\n <div className=\"sticky top-0 z-20 -mx-6 mb-8 bg-bg-canvas/80 px-6 pb-4 pt-1 backdrop-blur supports-[backdrop-filter]:bg-bg-canvas/60\">\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <AthleteSelect\n athleteId={athlete.athleteId}\n onSelect={setAthlete}\n idPrefix=\"perf-athlete\"\n />\n </GlassCard>\n </div>\n\n {athlete.athleteId ? (\n <AthletePerformance athleteId={athlete.athleteId} displayName={athlete.displayName} />\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Users className=\"h-10 w-10\" />}\n title=\"Pick an athlete to begin\"\n description=\"Choose a team and an athlete above to log workouts, record metrics and review their history.\"\n />\n )}\n </PageLayout>\n );\n}\n\ninterface AthletePerformanceProps {\n athleteId: string;\n displayName: string;\n}\n\n/** A single measured metric (type + label) collapsed into a real time-series. */\ninterface MetricSeries {\n key: string;\n type: AthMetricType;\n label: string;\n unit: string | null;\n latest: number;\n previous: number | null;\n min: number;\n max: number;\n /** Values oldest → newest (for the trend sparkline). */\n series: number[];\n recordedAt: string;\n count: number;\n}\n\nfunction AthletePerformance({ athleteId, displayName }: AthletePerformanceProps) {\n const [page, setPage] = useState(0);\n const [logOpen, setLogOpen] = useState(false);\n const [metricOpen, setMetricOpen] = useState(false);\n\n const logs = useWorkoutLogs({\n athleteId,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const metrics = usePerformanceMetrics(athleteId);\n\n const logItems = logs.data?.items ?? [];\n const hasMore = logs.data?.pageInfo.hasMore ?? false;\n\n // Group by (type + label) so every distinct measurement is its own trend.\n const metricSeries = useMemo<MetricSeries[]>(() => {\n const groups = new Map<string, AthPerformanceMetric[]>();\n for (const m of metrics.data ?? []) {\n const key = `${m.type}::${m.label}`;\n const list = groups.get(key) ?? [];\n list.push(m);\n groups.set(key, list);\n }\n return Array.from(groups.entries())\n .map(([key, list]) => {\n const asc = [...list].sort(\n (a, b) => new Date(a.recordedAt).getTime() - new Date(b.recordedAt).getTime()\n );\n const values = asc.map((m) => m.value);\n const newest = asc[asc.length - 1];\n const prior = asc.length > 1 ? asc[asc.length - 2] : null;\n return {\n key,\n type: newest.type,\n label: newest.label,\n unit: newest.unit,\n latest: newest.value,\n previous: prior ? prior.value : null,\n min: Math.min(...values),\n max: Math.max(...values),\n series: values,\n recordedAt: newest.recordedAt,\n count: asc.length,\n };\n })\n .sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime());\n }, [metrics.data]);\n\n const columns: ResponsiveTableColumn<AthWorkoutLog>[] = useMemo(\n () => [\n {\n key: 'workout',\n header: 'Workout',\n priority: 'primary',\n cell: (log) => {\n const Icon = workoutIcon(log.title);\n return (\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </span>\n <span className=\"min-w-0 truncate font-medium text-text-primary\">{log.title}</span>\n </div>\n );\n },\n },\n {\n key: 'duration',\n header: 'Duration',\n priority: 'secondary',\n mobileLabel: 'Duration',\n cell: (log) =>\n log.durationMin != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatDuration(log.durationMin)}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n {\n key: 'rpe',\n header: 'RPE',\n priority: 'secondary',\n mobileLabel: 'RPE',\n cell: (log) => (log.rpe != null ? <RpeChip rpe={log.rpe} /> : <span className=\"text-text-muted\">—</span>),\n },\n {\n key: 'date',\n header: 'Date',\n priority: 'secondary',\n mobileLabel: 'Date',\n cell: (log) => <span className=\"tabular-nums text-text-secondary\">{formatDate(log.performedAt)}</span>,\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'primary',\n mobileLabel: 'Status',\n cell: (log) => <StatusBadge status={log.status} />,\n },\n {\n key: 'notes',\n header: 'Notes',\n priority: 'hidden-on-mobile',\n cell: (log) =>\n log.notes ? (\n <span className=\"line-clamp-2 max-w-[22ch] text-text-secondary\">{log.notes}</span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n ],\n []\n );\n\n return (\n <div className=\"space-y-8 pb-24 sm:pb-0\">\n {/* Actions */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n Tracking <span className=\"font-medium text-text-primary\">{displayName || 'athlete'}</span>\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => setMetricOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Gauge className=\"h-4 w-4\" /> Record metric\n </button>\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'hidden sm:inline-flex')}\n >\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n </div>\n </div>\n\n {/* Metrics — hero stat tiles with real trend sparklines */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Performance metrics</h2>\n {metrics.isLoading ? (\n <LoadingState label=\"Loading metrics…\" />\n ) : metrics.isError ? (\n <ErrorState onRetry={() => void metrics.refetch()} />\n ) : metricSeries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Gauge className=\"h-10 w-10\" />}\n title=\"No metrics recorded\"\n description=\"Record a metric — speed, strength, endurance and more — to start charting progress.\"\n action={\n <button type=\"button\" onClick={() => setMetricOpen(true)} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" /> Record metric\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {metricSeries.map((m) => (\n <MetricTile key={m.key} metric={m} />\n ))}\n </div>\n )}\n </section>\n\n {/* Workout logs */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Workout history</h2>\n {logs.isLoading ? (\n <LoadingState label=\"Loading workouts…\" />\n ) : logs.isError ? (\n <ErrorState onRetry={() => void logs.refetch()} />\n ) : logItems.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title=\"No workouts logged\"\n description=\"Log a workout to build this athlete's training history.\"\n action={\n <button type=\"button\" onClick={() => setLogOpen(true)} className={GRADIENT_CTA}>\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"metric\" className=\"overflow-hidden p-1 sm:p-2\">\n <ResponsiveTable\n columns={columns}\n data={logItems}\n rowKey={(log) => log.id}\n caption={`Workout history for ${displayName || 'athlete'}`}\n />\n </GlassCard>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </section>\n\n {/* Mobile sticky Log workout — thumb-reachable primary action */}\n <div className=\"sticky bottom-0 z-30 -mx-6 border-t border-border-seam bg-bg-canvas/85 px-6 pb-[max(env(safe-area-inset-bottom),0.75rem)] pt-3 backdrop-blur sm:hidden\">\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'w-full py-3 text-base')}\n >\n <Plus className=\"h-5 w-5\" /> Log workout\n </button>\n </div>\n\n <LogWorkoutDialog\n open={logOpen}\n onOpenChange={setLogOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n <RecordMetricDialog\n open={metricOpen}\n onOpenChange={setMetricOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n </div>\n );\n}\n\n/** Pick a workout row icon from the (real) title text. Cosmetic only. */\nfunction workoutIcon(title: string): LucideIcon {\n const t = title.toLowerCase();\n if (/strength|lift|squat|deadlift|bench|1rm|weight/.test(t)) return Dumbbell;\n if (/speed|sprint|agility|accel|split/.test(t)) return Zap;\n if (/endurance|aerobic|cardio|run|beep|yo-?yo/.test(t)) return Activity;\n if (/recovery|mobility|stretch|rest/.test(t)) return HeartPulse;\n if (/accuracy|shoot|target/.test(t)) return Target;\n return Dumbbell;\n}\n\n/** Bordered RPE chip, color-scaled by session intensity (semantic tokens). */\nfunction RpeChip({ rpe }: { rpe: number }) {\n const tone =\n rpe >= 8\n ? 'border-status-error-border text-status-error-text'\n : rpe >= 5\n ? 'border-status-warning-border text-status-warning-text'\n : 'border-border-seam text-text-secondary';\n return (\n <span\n className={cn(\n 'inline-flex h-7 min-w-[1.75rem] items-center justify-center rounded-md border px-1.5 text-sm font-semibold tabular-nums',\n tone\n )}\n >\n {rpe}\n </span>\n );\n}\n\n/** Hero stat tile for one measured metric: big value, trend sparkline, range. */\nfunction MetricTile({ metric }: { metric: MetricSeries }) {\n const Icon = METRIC_ICON[metric.type];\n const delta = metric.previous == null ? null : metric.latest - metric.previous;\n const hasRange = metric.max > metric.min;\n const pct = hasRange\n ? Math.round(((metric.latest - metric.min) / (metric.max - metric.min)) * 100)\n : 100;\n const unitSuffix = metric.unit ? ` ${metric.unit}` : '';\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n {METRIC_TYPE_LABELS[metric.type]}\n </span>\n <span className=\"text-xs text-text-muted\">{formatDate(metric.recordedAt)}</span>\n </div>\n\n <div className=\"flex items-end justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-secondary\">{metric.label}</p>\n <p className=\"mt-0.5 text-3xl font-bold leading-none tracking-tight tabular-nums text-text-primary\">\n {fmtNum(metric.latest)}\n {metric.unit ? (\n <span className=\"ml-1 text-base font-medium text-text-secondary\">{metric.unit}</span>\n ) : null}\n </p>\n </div>\n {metric.series.length > 1 ? (\n <div className=\"w-24 shrink-0 sm:w-28\">\n <Sparkline\n data={metric.series}\n variant=\"area\"\n height={40}\n label={`${metric.label} trend`}\n />\n </div>\n ) : null}\n </div>\n\n {/* Range scale: min → latest position → max (from the athlete's own series) */}\n {hasRange ? (\n <div>\n <span\n className=\"block h-1.5 overflow-hidden rounded-full bg-bg-sunken\"\n aria-hidden=\"true\"\n >\n <span\n className=\"block h-1.5 rounded-full bg-[var(--color-accent-teal)]\"\n style={{ width: `${Math.max(4, Math.min(100, pct))}%` }}\n />\n </span>\n <div className=\"mt-1 flex justify-between text-[0.7rem] tabular-nums text-text-muted\">\n <span>{fmtNum(metric.min)}</span>\n <span>{fmtNum((metric.min + metric.max) / 2)}</span>\n <span>{fmtNum(metric.max)}</span>\n </div>\n </div>\n ) : null}\n\n {/* Delta vs previous reading — direction shown neutrally (no good/bad claim) */}\n <div className=\"mt-auto flex items-center gap-1.5 text-sm\">\n {delta == null || delta === 0 ? (\n <span className=\"text-text-muted\">\n {metric.count > 1 ? 'No change vs previous' : 'First reading'}\n </span>\n ) : (\n <>\n <span className=\"inline-flex items-center gap-0.5 font-medium tabular-nums text-text-secondary\">\n {delta > 0 ? (\n <ArrowUp className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <ArrowDown className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n {fmtNum(Math.abs(delta))}\n {unitSuffix}\n </span>\n <span className=\"text-text-muted\">vs previous</span>\n </>\n )}\n </div>\n </GlassCard>\n );\n}\n\ninterface DialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n athleteId: string;\n displayName: string;\n}\n\nfunction LogWorkoutDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const logWorkout = useLogWorkout();\n const [title, setTitle] = useState('');\n const [status, setStatus] = useState<AthWorkoutStatus>('COMPLETED');\n const [performedAt, setPerformedAt] = useState(nowLocal);\n const [durationMin, setDurationMin] = useState('');\n const [rpe, setRpe] = useState('');\n const [totalLoad, setTotalLoad] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setStatus('COMPLETED');\n setPerformedAt(nowLocal());\n setDurationMin('');\n setRpe('');\n setTotalLoad('');\n setNotes('');\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!title.trim()) {\n setError('A workout title is required.');\n return;\n }\n if (!performedAt) {\n setError('A performed date and time is required.');\n return;\n }\n try {\n await logWorkout.mutateAsync({\n athleteId,\n title: title.trim(),\n status,\n performedAt: new Date(performedAt).toISOString(),\n durationMin: numOrUndefined(durationMin),\n rpe: numOrUndefined(rpe),\n totalLoad: floatOrUndefined(totalLoad),\n notes: notes.trim() || undefined,\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to log workout.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Log workout{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <FormField label=\"Title\" htmlFor=\"workoutTitle\" required>\n <input\n id=\"workoutTitle\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Lower-body strength\"\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Status\" htmlFor=\"workoutStatus\">\n <select\n id=\"workoutStatus\"\n className={fieldInputClass}\n value={status}\n onChange={(e) => setStatus(e.target.value as AthWorkoutStatus)}\n >\n {WORKOUT_STATUSES.map((s) => (\n <option key={s} value={s}>\n {s.charAt(0) + s.slice(1).toLowerCase()}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Performed at\" htmlFor=\"workoutPerformedAt\" required>\n <input\n id=\"workoutPerformedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={performedAt}\n onChange={(e) => setPerformedAt(e.target.value)}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <FormField label=\"Duration (min)\" htmlFor=\"workoutDuration\">\n <input\n id=\"workoutDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </FormField>\n <FormField label=\"RPE (1–10)\" htmlFor=\"workoutRpe\">\n <input\n id=\"workoutRpe\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rpe}\n onChange={(e) => setRpe(e.target.value)}\n />\n </FormField>\n <FormField label=\"Total load\" htmlFor=\"workoutLoad\">\n <input\n id=\"workoutLoad\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={totalLoad}\n onChange={(e) => setTotalLoad(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Notes\" htmlFor=\"workoutNotes\">\n <textarea\n id=\"workoutNotes\"\n rows={2}\n className={fieldInputClass}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"How did the session go?\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={logWorkout.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {logWorkout.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Log workout\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction RecordMetricDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const recordMetric = useRecordMetric();\n const [type, setType] = useState<AthMetricType>('CUSTOM');\n const [label, setLabel] = useState('');\n const [value, setValue] = useState('');\n const [unit, setUnit] = useState('');\n const [source, setSource] = useState('');\n const [recordedAt, setRecordedAt] = useState(nowLocal);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setType('CUSTOM');\n setLabel('');\n setValue('');\n setUnit('');\n setSource('');\n setRecordedAt(nowLocal());\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!label.trim()) {\n setError('A metric label is required.');\n return;\n }\n const numericValue = floatOrUndefined(value);\n if (numericValue === undefined) {\n setError('A numeric value is required.');\n return;\n }\n if (!recordedAt) {\n setError('A recorded date and time is required.');\n return;\n }\n try {\n await recordMetric.mutateAsync({\n athleteId,\n type,\n label: label.trim(),\n value: numericValue,\n unit: unit.trim() || undefined,\n source: source.trim() || undefined,\n recordedAt: new Date(recordedAt).toISOString(),\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to record metric.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Record metric{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Type\" htmlFor=\"metricType\">\n <select\n id=\"metricType\"\n className={fieldInputClass}\n value={type}\n onChange={(e) => setType(e.target.value as AthMetricType)}\n >\n {METRIC_TYPES.map((t) => (\n <option key={t} value={t}>\n {METRIC_TYPE_LABELS[t]}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Recorded at\" htmlFor=\"metricRecordedAt\" required>\n <input\n id=\"metricRecordedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={recordedAt}\n onChange={(e) => setRecordedAt(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Label\" htmlFor=\"metricLabel\" required>\n <input\n id=\"metricLabel\"\n className={fieldInputClass}\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder=\"e.g. 40m sprint\"\n />\n </FormField>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField label=\"Value\" htmlFor=\"metricValue\" required>\n <input\n id=\"metricValue\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder=\"e.g. 4.8\"\n />\n </FormField>\n <FormField label=\"Unit\" htmlFor=\"metricUnit\">\n <input\n id=\"metricUnit\"\n className={fieldInputClass}\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n placeholder=\"e.g. s, kg, bpm\"\n />\n </FormField>\n </div>\n <FormField label=\"Source\" htmlFor=\"metricSource\" hint=\"Optional — device, test or coach.\">\n <input\n id=\"metricSource\"\n className={fieldInputClass}\n value={source}\n onChange={(e) => setSource(e.target.value)}\n placeholder=\"e.g. GPS vest\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={recordMetric.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {recordMetric.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n Record metric\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PerformancePage.js","names":[],"sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"sourcesContent":["/**\n * Performance\n * @module atheletebridge/pages/performance\n *\n * The coach's athlete-performance workbench: pick an athlete from a team roster,\n * then log workouts (athLogWorkout) and record performance metrics\n * (athRecordMetric), and review that athlete's workout history\n * (athListWorkoutLogs, offset-paginated) and metrics.\n *\n * Elevated to the concept bar: each measured metric (type + label) becomes a\n * hero stat tile with its big latest value, a real recordedAt-ordered trend\n * sparkline, a min→max range scale and a signed delta-vs-previous — all derived\n * from the real time-series (nothing fabricated). Workout history renders as a\n * responsive table that reflows to cards on mobile, with a thumb-reachable\n * sticky \"Log workout\" action.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n ArrowDown,\n ArrowUp,\n Dumbbell,\n Gauge,\n HeartPulse,\n Loader2,\n Move,\n Plus,\n Save,\n Scale,\n Target,\n Users,\n Zap,\n type LucideIcon,\n} from 'lucide-react';\nimport {\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { AthleteSelect, type AthleteSelectValue } from '../../components/AthleteSelect';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { CursorPager } from '../../components/CursorPager';\nimport { Sparkline } from '../../components/viz';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { METRIC_TYPE_LABELS } from '../../components/domain';\nimport {\n useWorkoutLogs,\n usePerformanceMetrics,\n useLogWorkout,\n useRecordMetric,\n} from '../../hooks/useAthleteBridgeApi';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type {\n AthMetricType,\n AthPerformanceMetric,\n AthWorkoutLog,\n AthWorkoutStatus,\n} from '../../types';\n\nconst PAGE_SIZE = 10;\n\nconst WORKOUT_STATUSES: AthWorkoutStatus[] = ['COMPLETED', 'PLANNED', 'SKIPPED'];\nconst METRIC_TYPES = Object.keys(METRIC_TYPE_LABELS) as AthMetricType[];\n\n/** Brand gradient CTA — token-only (no raw colors). */\nconst GRADIENT_CTA =\n 'inline-flex items-center justify-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow] hover:shadow-elevation-2 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\n/** Per-metric-type icon for the stat tiles. */\nconst METRIC_ICON: Record<AthMetricType, LucideIcon> = {\n SPEED: Zap,\n ENDURANCE: Activity,\n STRENGTH: Dumbbell,\n AGILITY: Move,\n ACCURACY: Target,\n WEIGHT: Scale,\n HEART_RATE: HeartPulse,\n CUSTOM: Gauge,\n};\n\nconst numOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nconst floatOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseFloat(t);\n return Number.isNaN(n) ? undefined : n;\n};\n\n/** Current local time as a `datetime-local` value (`YYYY-MM-DDTHH:mm`). */\nconst nowLocal = (): string => {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60_000);\n return local.toISOString().slice(0, 16);\n};\n\n/** Trim trailing zeros from a fixed-precision number for compact display. */\nconst fmtNum = (n: number): string => {\n const abs = Math.abs(n);\n const s = abs >= 100 ? n.toFixed(0) : n.toFixed(2);\n return s.replace(/\\.?0+$/, '');\n};\n\nexport function PerformancePage() {\n const [athlete, setAthlete] = useState<AthleteSelectValue>({ athleteId: '', displayName: '' });\n\n return (\n <PageLayout\n title=\"Performance\"\n description=\"Log workouts, record metrics and track an athlete's progress over time.\"\n >\n {/* Athlete picker — sticks to the top of the scroll area as an insight panel */}\n <div className=\"sticky top-0 z-20 -mx-6 mb-8 bg-bg-canvas/80 px-6 pb-4 pt-1 backdrop-blur supports-[backdrop-filter]:bg-bg-canvas/60\">\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <AthleteSelect\n athleteId={athlete.athleteId}\n onSelect={setAthlete}\n idPrefix=\"perf-athlete\"\n />\n </GlassCard>\n </div>\n\n {athlete.athleteId ? (\n <AthletePerformance athleteId={athlete.athleteId} displayName={athlete.displayName} />\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Users className=\"h-10 w-10\" />}\n title=\"Pick an athlete to begin\"\n description=\"Choose a team and an athlete above to log workouts, record metrics and review their history.\"\n />\n )}\n </PageLayout>\n );\n}\n\ninterface AthletePerformanceProps {\n athleteId: string;\n displayName: string;\n}\n\n/** A single measured metric (type + label) collapsed into a real time-series. */\ninterface MetricSeries {\n key: string;\n type: AthMetricType;\n label: string;\n unit: string | null;\n latest: number;\n previous: number | null;\n min: number;\n max: number;\n /** Values oldest → newest (for the trend sparkline). */\n series: number[];\n recordedAt: string;\n count: number;\n}\n\nfunction AthletePerformance({ athleteId, displayName }: AthletePerformanceProps) {\n const [page, setPage] = useState(0);\n const [logOpen, setLogOpen] = useState(false);\n const [metricOpen, setMetricOpen] = useState(false);\n\n const logs = useWorkoutLogs({\n athleteId,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const metrics = usePerformanceMetrics(athleteId);\n\n const logItems = logs.data?.items ?? [];\n const hasMore = logs.data?.pageInfo.hasMore ?? false;\n\n // Group by (type + label) so every distinct measurement is its own trend.\n const metricSeries = useMemo<MetricSeries[]>(() => {\n const groups = new Map<string, AthPerformanceMetric[]>();\n for (const m of metrics.data ?? []) {\n const key = `${m.type}::${m.label}`;\n const list = groups.get(key) ?? [];\n list.push(m);\n groups.set(key, list);\n }\n return Array.from(groups.entries())\n .map(([key, list]) => {\n const asc = [...list].sort(\n (a, b) => new Date(a.recordedAt).getTime() - new Date(b.recordedAt).getTime()\n );\n const values = asc.map((m) => m.value);\n const newest = asc[asc.length - 1];\n const prior = asc.length > 1 ? asc[asc.length - 2] : null;\n return {\n key,\n type: newest.type,\n label: newest.label,\n unit: newest.unit,\n latest: newest.value,\n previous: prior ? prior.value : null,\n min: Math.min(...values),\n max: Math.max(...values),\n series: values,\n recordedAt: newest.recordedAt,\n count: asc.length,\n };\n })\n .sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime());\n }, [metrics.data]);\n\n const columns: ResponsiveTableColumn<AthWorkoutLog>[] = useMemo(\n () => [\n {\n key: 'workout',\n header: 'Workout',\n priority: 'primary',\n cell: (log) => {\n const Icon = workoutIcon(log.title);\n return (\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </span>\n <span className=\"min-w-0 truncate font-medium text-text-primary\">{log.title}</span>\n </div>\n );\n },\n },\n {\n key: 'duration',\n header: 'Duration',\n priority: 'secondary',\n mobileLabel: 'Duration',\n cell: (log) =>\n log.durationMin != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatDuration(log.durationMin)}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n {\n key: 'rpe',\n header: 'RPE',\n priority: 'secondary',\n mobileLabel: 'RPE',\n cell: (log) =>\n log.rpe != null ? <RpeChip rpe={log.rpe} /> : <span className=\"text-text-muted\">—</span>,\n },\n {\n key: 'date',\n header: 'Date',\n priority: 'secondary',\n mobileLabel: 'Date',\n cell: (log) => (\n <span className=\"tabular-nums text-text-secondary\">{formatDate(log.performedAt)}</span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'primary',\n mobileLabel: 'Status',\n cell: (log) => <StatusBadge status={log.status} />,\n },\n {\n key: 'notes',\n header: 'Notes',\n priority: 'hidden-on-mobile',\n cell: (log) =>\n log.notes ? (\n <span className=\"line-clamp-2 max-w-[22ch] text-text-secondary\">{log.notes}</span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n ],\n []\n );\n\n return (\n <div className=\"space-y-8 pb-24 sm:pb-0\">\n {/* Actions */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n Tracking <span className=\"font-medium text-text-primary\">{displayName || 'athlete'}</span>\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => setMetricOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Gauge className=\"h-4 w-4\" /> Record metric\n </button>\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'hidden sm:inline-flex')}\n >\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n </div>\n </div>\n\n {/* Metrics — hero stat tiles with real trend sparklines */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Performance metrics</h2>\n {metrics.isLoading ? (\n <LoadingState label=\"Loading metrics…\" />\n ) : metrics.isError ? (\n <ErrorState onRetry={() => void metrics.refetch()} />\n ) : metricSeries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Gauge className=\"h-10 w-10\" />}\n title=\"No metrics recorded\"\n description=\"Record a metric — speed, strength, endurance and more — to start charting progress.\"\n action={\n <button type=\"button\" onClick={() => setMetricOpen(true)} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" /> Record metric\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {metricSeries.map((m) => (\n <MetricTile key={m.key} metric={m} />\n ))}\n </div>\n )}\n </section>\n\n {/* Workout logs */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Workout history</h2>\n {logs.isLoading ? (\n <LoadingState label=\"Loading workouts…\" />\n ) : logs.isError ? (\n <ErrorState onRetry={() => void logs.refetch()} />\n ) : logItems.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title=\"No workouts logged\"\n description=\"Log a workout to build this athlete's training history.\"\n action={\n <button type=\"button\" onClick={() => setLogOpen(true)} className={GRADIENT_CTA}>\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"metric\" className=\"overflow-hidden p-1 sm:p-2\">\n <ResponsiveTable\n columns={columns}\n data={logItems}\n rowKey={(log) => log.id}\n caption={`Workout history for ${displayName || 'athlete'}`}\n />\n </GlassCard>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </section>\n\n {/* Mobile sticky Log workout — thumb-reachable primary action */}\n <div className=\"sticky bottom-0 z-30 -mx-6 border-t border-border-seam bg-bg-canvas/85 px-6 pb-[max(env(safe-area-inset-bottom),0.75rem)] pt-3 backdrop-blur sm:hidden\">\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'w-full py-3 text-base')}\n >\n <Plus className=\"h-5 w-5\" /> Log workout\n </button>\n </div>\n\n <LogWorkoutDialog\n open={logOpen}\n onOpenChange={setLogOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n <RecordMetricDialog\n open={metricOpen}\n onOpenChange={setMetricOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n </div>\n );\n}\n\n/** Pick a workout row icon from the (real) title text. Cosmetic only. */\nfunction workoutIcon(title: string): LucideIcon {\n const t = title.toLowerCase();\n if (/strength|lift|squat|deadlift|bench|1rm|weight/.test(t)) return Dumbbell;\n if (/speed|sprint|agility|accel|split/.test(t)) return Zap;\n if (/endurance|aerobic|cardio|run|beep|yo-?yo/.test(t)) return Activity;\n if (/recovery|mobility|stretch|rest/.test(t)) return HeartPulse;\n if (/accuracy|shoot|target/.test(t)) return Target;\n return Dumbbell;\n}\n\n/** Bordered RPE chip, color-scaled by session intensity (semantic tokens). */\nfunction RpeChip({ rpe }: { rpe: number }) {\n const tone =\n rpe >= 8\n ? 'border-status-error-border text-status-error-text'\n : rpe >= 5\n ? 'border-status-warning-border text-status-warning-text'\n : 'border-border-seam text-text-secondary';\n return (\n <span\n className={cn(\n 'inline-flex h-7 min-w-[1.75rem] items-center justify-center rounded-md border px-1.5 text-sm font-semibold tabular-nums',\n tone\n )}\n >\n {rpe}\n </span>\n );\n}\n\n/** Hero stat tile for one measured metric: big value, trend sparkline, range. */\nfunction MetricTile({ metric }: { metric: MetricSeries }) {\n const Icon = METRIC_ICON[metric.type];\n const delta = metric.previous == null ? null : metric.latest - metric.previous;\n const hasRange = metric.max > metric.min;\n const pct = hasRange\n ? Math.round(((metric.latest - metric.min) / (metric.max - metric.min)) * 100)\n : 100;\n const unitSuffix = metric.unit ? ` ${metric.unit}` : '';\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n {METRIC_TYPE_LABELS[metric.type]}\n </span>\n <span className=\"text-xs text-text-muted\">{formatDate(metric.recordedAt)}</span>\n </div>\n\n <div className=\"flex items-end justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-secondary\">{metric.label}</p>\n <p className=\"mt-0.5 text-3xl font-bold leading-none tracking-tight tabular-nums text-text-primary\">\n {fmtNum(metric.latest)}\n {metric.unit ? (\n <span className=\"ml-1 text-base font-medium text-text-secondary\">{metric.unit}</span>\n ) : null}\n </p>\n </div>\n {metric.series.length > 1 ? (\n <div className=\"w-24 shrink-0 sm:w-28\">\n <Sparkline\n data={metric.series}\n variant=\"area\"\n height={40}\n label={`${metric.label} trend`}\n />\n </div>\n ) : null}\n </div>\n\n {/* Range scale: min → latest position → max (from the athlete's own series) */}\n {hasRange ? (\n <div>\n <span\n className=\"block h-1.5 overflow-hidden rounded-full bg-bg-sunken\"\n aria-hidden=\"true\"\n >\n <span\n className=\"block h-1.5 rounded-full bg-[var(--color-accent-teal)]\"\n style={{ width: `${Math.max(4, Math.min(100, pct))}%` }}\n />\n </span>\n <div className=\"mt-1 flex justify-between text-[0.7rem] tabular-nums text-text-muted\">\n <span>{fmtNum(metric.min)}</span>\n <span>{fmtNum((metric.min + metric.max) / 2)}</span>\n <span>{fmtNum(metric.max)}</span>\n </div>\n </div>\n ) : null}\n\n {/* Delta vs previous reading — direction shown neutrally (no good/bad claim) */}\n <div className=\"mt-auto flex items-center gap-1.5 text-sm\">\n {delta == null || delta === 0 ? (\n <span className=\"text-text-muted\">\n {metric.count > 1 ? 'No change vs previous' : 'First reading'}\n </span>\n ) : (\n <>\n <span className=\"inline-flex items-center gap-0.5 font-medium tabular-nums text-text-secondary\">\n {delta > 0 ? (\n <ArrowUp className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <ArrowDown className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n {fmtNum(Math.abs(delta))}\n {unitSuffix}\n </span>\n <span className=\"text-text-muted\">vs previous</span>\n </>\n )}\n </div>\n </GlassCard>\n );\n}\n\ninterface DialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n athleteId: string;\n displayName: string;\n}\n\nfunction LogWorkoutDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const logWorkout = useLogWorkout();\n const [title, setTitle] = useState('');\n const [status, setStatus] = useState<AthWorkoutStatus>('COMPLETED');\n const [performedAt, setPerformedAt] = useState(nowLocal);\n const [durationMin, setDurationMin] = useState('');\n const [rpe, setRpe] = useState('');\n const [totalLoad, setTotalLoad] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setStatus('COMPLETED');\n setPerformedAt(nowLocal());\n setDurationMin('');\n setRpe('');\n setTotalLoad('');\n setNotes('');\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!title.trim()) {\n setError('A workout title is required.');\n return;\n }\n if (!performedAt) {\n setError('A performed date and time is required.');\n return;\n }\n try {\n await logWorkout.mutateAsync({\n athleteId,\n title: title.trim(),\n status,\n performedAt: new Date(performedAt).toISOString(),\n durationMin: numOrUndefined(durationMin),\n rpe: numOrUndefined(rpe),\n totalLoad: floatOrUndefined(totalLoad),\n notes: notes.trim() || undefined,\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to log workout.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Log workout{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <FormField label=\"Title\" htmlFor=\"workoutTitle\" required>\n <input\n id=\"workoutTitle\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Lower-body strength\"\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Status\" htmlFor=\"workoutStatus\">\n <select\n id=\"workoutStatus\"\n className={fieldInputClass}\n value={status}\n onChange={(e) => setStatus(e.target.value as AthWorkoutStatus)}\n >\n {WORKOUT_STATUSES.map((s) => (\n <option key={s} value={s}>\n {s.charAt(0) + s.slice(1).toLowerCase()}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Performed at\" htmlFor=\"workoutPerformedAt\" required>\n <input\n id=\"workoutPerformedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={performedAt}\n onChange={(e) => setPerformedAt(e.target.value)}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <FormField label=\"Duration (min)\" htmlFor=\"workoutDuration\">\n <input\n id=\"workoutDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </FormField>\n <FormField label=\"RPE (1–10)\" htmlFor=\"workoutRpe\">\n <input\n id=\"workoutRpe\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rpe}\n onChange={(e) => setRpe(e.target.value)}\n />\n </FormField>\n <FormField label=\"Total load\" htmlFor=\"workoutLoad\">\n <input\n id=\"workoutLoad\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={totalLoad}\n onChange={(e) => setTotalLoad(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Notes\" htmlFor=\"workoutNotes\">\n <textarea\n id=\"workoutNotes\"\n rows={2}\n className={fieldInputClass}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"How did the session go?\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={logWorkout.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {logWorkout.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Log workout\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction RecordMetricDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const recordMetric = useRecordMetric();\n const [type, setType] = useState<AthMetricType>('CUSTOM');\n const [label, setLabel] = useState('');\n const [value, setValue] = useState('');\n const [unit, setUnit] = useState('');\n const [source, setSource] = useState('');\n const [recordedAt, setRecordedAt] = useState(nowLocal);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setType('CUSTOM');\n setLabel('');\n setValue('');\n setUnit('');\n setSource('');\n setRecordedAt(nowLocal());\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!label.trim()) {\n setError('A metric label is required.');\n return;\n }\n const numericValue = floatOrUndefined(value);\n if (numericValue === undefined) {\n setError('A numeric value is required.');\n return;\n }\n if (!recordedAt) {\n setError('A recorded date and time is required.');\n return;\n }\n try {\n await recordMetric.mutateAsync({\n athleteId,\n type,\n label: label.trim(),\n value: numericValue,\n unit: unit.trim() || undefined,\n source: source.trim() || undefined,\n recordedAt: new Date(recordedAt).toISOString(),\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to record metric.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Record metric{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Type\" htmlFor=\"metricType\">\n <select\n id=\"metricType\"\n className={fieldInputClass}\n value={type}\n onChange={(e) => setType(e.target.value as AthMetricType)}\n >\n {METRIC_TYPES.map((t) => (\n <option key={t} value={t}>\n {METRIC_TYPE_LABELS[t]}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Recorded at\" htmlFor=\"metricRecordedAt\" required>\n <input\n id=\"metricRecordedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={recordedAt}\n onChange={(e) => setRecordedAt(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Label\" htmlFor=\"metricLabel\" required>\n <input\n id=\"metricLabel\"\n className={fieldInputClass}\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder=\"e.g. 40m sprint\"\n />\n </FormField>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField label=\"Value\" htmlFor=\"metricValue\" required>\n <input\n id=\"metricValue\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder=\"e.g. 4.8\"\n />\n </FormField>\n <FormField label=\"Unit\" htmlFor=\"metricUnit\">\n <input\n id=\"metricUnit\"\n className={fieldInputClass}\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n placeholder=\"e.g. s, kg, bpm\"\n />\n </FormField>\n </div>\n <FormField label=\"Source\" htmlFor=\"metricSource\" hint=\"Optional — device, test or coach.\">\n <input\n id=\"metricSource\"\n className={fieldInputClass}\n value={source}\n onChange={(e) => setSource(e.target.value)}\n placeholder=\"e.g. GPS vest\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={recordMetric.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {recordMetric.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n Record metric\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Program Detail
|
|
3
|
+
* @module atheletebridge/pages/programs
|
|
4
|
+
*
|
|
5
|
+
* Reads a single program with its drills (athProgram), supports the publish
|
|
6
|
+
* lifecycle (athTransitionProgram), editing/archiving, and adding/removing
|
|
7
|
+
* drills (athAddDrill / athRemoveDrill).
|
|
8
|
+
*/
|
|
9
|
+
export declare function ProgramDetailPage(): import("react").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=ProgramDetailPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProgramDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/programs/ProgramDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAiFH,wBAAgB,iBAAiB,gCA+ahC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProgramFormPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/programs/ProgramFormPage.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAoCH,wBAAgB,eAAe,gCA2N9B"}
|