@burdenoff/microfe-atheletebridge 2026.720.1 → 2026.731.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atheletebridge/AthleteBridgeRoot.js.map +1 -1
- package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
- package/dist/atheletebridge/admin/AdminDashboardPage.js.map +1 -1
- package/dist/atheletebridge/admin/AdminRoot.js.map +1 -1
- package/dist/atheletebridge/components/AthleteSelect.js.map +1 -1
- package/dist/atheletebridge/components/Crest.js.map +1 -1
- package/dist/atheletebridge/components/CursorPager.js.map +1 -1
- package/dist/atheletebridge/components/DrillVideoLink.js.map +1 -1
- package/dist/atheletebridge/components/EmptyState.js.map +1 -1
- package/dist/atheletebridge/components/FilterSelect.js.map +1 -1
- package/dist/atheletebridge/components/FixtureCard.js.map +1 -1
- package/dist/atheletebridge/components/LoadingState.js.map +1 -1
- package/dist/atheletebridge/components/MatchupRow.js.map +1 -1
- package/dist/atheletebridge/components/MetricsCard.js.map +1 -1
- package/dist/atheletebridge/components/PageLayout.js.map +1 -1
- package/dist/atheletebridge/components/ProgramCard.js.map +1 -1
- package/dist/atheletebridge/components/RouteAnalytics.js.map +1 -1
- package/dist/atheletebridge/components/Scoreboard.js.map +1 -1
- package/dist/atheletebridge/components/SessionCard.js.map +1 -1
- package/dist/atheletebridge/components/SessionStatusTimeline.js.map +1 -1
- package/dist/atheletebridge/components/StatusBadge.js.map +1 -1
- package/dist/atheletebridge/components/TeamCard.js.map +1 -1
- package/dist/atheletebridge/components/UserAvatar.js.map +1 -1
- package/dist/atheletebridge/components/UserSearchSelect.js.map +1 -1
- package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.d.ts +9 -0
- package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.d.ts.map +1 -0
- package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.js +118 -0
- package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.js.map +1 -0
- package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.d.ts +9 -0
- package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.d.ts.map +1 -0
- package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.js +88 -0
- package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.js.map +1 -0
- package/dist/atheletebridge/components/domain.js.map +1 -1
- package/dist/atheletebridge/components/form.js.map +1 -1
- package/dist/atheletebridge/components/viz/BarChart.js.map +1 -1
- package/dist/atheletebridge/components/viz/DonutChart.js +1 -1
- package/dist/atheletebridge/components/viz/DonutChart.js.map +1 -1
- package/dist/atheletebridge/components/viz/LineChart.js.map +1 -1
- package/dist/atheletebridge/components/viz/RatingStars.js.map +1 -1
- package/dist/atheletebridge/components/viz/Sparkline.js.map +1 -1
- package/dist/atheletebridge/components/viz/StatCard.js.map +1 -1
- package/dist/atheletebridge/components/viz/Timeline.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/CartesianFrame.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/ResponsiveSvg.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/format.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/geometry.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/hooks.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/palette.js.map +1 -1
- package/dist/atheletebridge/context/AthleteBridgeContext.js.map +1 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts +18 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts.map +1 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +71 -33
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
- package/dist/atheletebridge/i18n.js.map +1 -1
- package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
- package/dist/atheletebridge/pages/StandingsPage.js.map +1 -1
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js.map +1 -1
- package/dist/atheletebridge/pages/athletes/AthleteProfilePage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js +87 -75
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.js.map +1 -1
- package/dist/atheletebridge/pages/onboarding/OnboardingPage.js.map +1 -1
- package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/tournaments/TournamentBracketPage.js.map +1 -1
- package/dist/atheletebridge/store/atheletebridgeStore.js.map +1 -1
- package/dist/atheletebridge/types/index.d.ts +27 -0
- package/dist/atheletebridge/types/index.d.ts.map +1 -1
- package/dist/atheletebridge/utils/cn.js.map +1 -1
- package/dist/atheletebridge/utils/datetime.js.map +1 -1
- package/dist/atheletebridge/utils/formatters.js.map +1 -1
- package/dist/atheletebridge/utils/identity.js.map +1 -1
- package/dist/atheletebridge/utils/navigation.js.map +1 -1
- package/dist/atheletebridge/utils/pageContext.js.map +1 -1
- package/dist/atheletebridge/utils/timeAgo.js.map +1 -1
- package/dist/atheletebridge/utils/url.js.map +1 -1
- package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
- package/dist/services/createSandboxAssistantTransport.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/atheletebridge/components/PageLayout.tsx"],"sourcesContent":["/**\n * Page Layout Component\n * @module atheletebridge/components\n *\n * Boff UI 2.0: glassmorphic seam-bordered page header with an optional\n * living aurora backdrop (one per MFE — opt in via `hero`) and a gradient\n * hero title (`gradientTitle`). The backdrop is decorative, sits behind\n * content (-z-10) and never moves layout. Reduced-motion safe.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AuroraBackground, GradientText } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n /**\n * Render the living aurora backdrop behind the header. Use on exactly ONE\n * primary page per MFE (the root dashboard).\n */\n hero?: boolean;\n /**\n * When `title` is a string, wrap it in a subtle brand gradient (the ONE\n * hero heading per page). Ignored when `title` is a node.\n */\n gradientTitle?: boolean;\n}\n\nexport const PageLayout: FC<PageLayoutProps> = ({\n title,\n description,\n actions,\n children,\n className,\n hero = false,\n gradientTitle = false,\n}) => {\n const renderedTitle =\n gradientTitle && typeof title === 'string' ? <GradientText>{title}</GradientText> : title;\n\n return (\n <div className={cn('flex flex-col h-full', className)}>\n {/* Page Header */}\n <div\n className={cn(\n 'relative overflow-hidden flex items-center justify-between gap-4 px-6 py-4',\n 'border-b border-border-seam bg-bg-surface'\n )}\n >\n {hero && <AuroraBackground intensity=\"subtle\" />}\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-semibold tracking-tight text-text-primary truncate\">\n {renderedTitle}\n </h1>\n {description && <p className=\"mt-1 text-sm text-text-secondary\">{description}</p>}\n </div>\n {actions && <div className=\"flex items-center gap-2 shrink-0\">{actions}</div>}\n </div>\n\n {/* Page Content */}\n <div className=\"flex-1 overflow-auto p-6\">{children}</div>\n </div>\n );\n};\n"],"mappings":";;;;AAgCA,IAAa,KAAmC,EAC9C,UACA,gBACA,YACA,aACA,cACA,UAAO,IACP,mBAAgB,SACZ;CACJ,IAAM,IACJ,KAAiB,OAAO,KAAU,WAAW,kBAAC,GAAD,EAAA,UAAe,
|
|
1
|
+
{"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/atheletebridge/components/PageLayout.tsx"],"sourcesContent":["/**\n * Page Layout Component\n * @module atheletebridge/components\n *\n * Boff UI 2.0: glassmorphic seam-bordered page header with an optional\n * living aurora backdrop (one per MFE — opt in via `hero`) and a gradient\n * hero title (`gradientTitle`). The backdrop is decorative, sits behind\n * content (-z-10) and never moves layout. Reduced-motion safe.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AuroraBackground, GradientText } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n /**\n * Render the living aurora backdrop behind the header. Use on exactly ONE\n * primary page per MFE (the root dashboard).\n */\n hero?: boolean;\n /**\n * When `title` is a string, wrap it in a subtle brand gradient (the ONE\n * hero heading per page). Ignored when `title` is a node.\n */\n gradientTitle?: boolean;\n}\n\nexport const PageLayout: FC<PageLayoutProps> = ({\n title,\n description,\n actions,\n children,\n className,\n hero = false,\n gradientTitle = false,\n}) => {\n const renderedTitle =\n gradientTitle && typeof title === 'string' ? <GradientText>{title}</GradientText> : title;\n\n return (\n <div className={cn('flex flex-col h-full', className)}>\n {/* Page Header */}\n <div\n className={cn(\n 'relative overflow-hidden flex items-center justify-between gap-4 px-6 py-4',\n 'border-b border-border-seam bg-bg-surface'\n )}\n >\n {hero && <AuroraBackground intensity=\"subtle\" />}\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-semibold tracking-tight text-text-primary truncate\">\n {renderedTitle}\n </h1>\n {description && <p className=\"mt-1 text-sm text-text-secondary\">{description}</p>}\n </div>\n {actions && <div className=\"flex items-center gap-2 shrink-0\">{actions}</div>}\n </div>\n\n {/* Page Content */}\n <div className=\"flex-1 overflow-auto p-6\">{children}</div>\n </div>\n );\n};\n"],"mappings":";;;;AAgCA,IAAa,KAAmC,EAC9C,UACA,gBACA,YACA,aACA,cACA,UAAO,IACP,mBAAgB,SACZ;CACJ,IAAM,IACJ,KAAiB,OAAO,KAAU,WAAW,kBAAC,GAAD,EAAA,UAAe,EAAoB,CAAA,IAAI;CAEtF,OACE,kBAAC,OAAD;EAAK,WAAW,EAAG,wBAAwB,CAAS;YAApD,CAEE,kBAAC,OAAD;GACE,WAAW,EACT,8EACA,2CACF;aAJF;IAMG,KAAQ,kBAAC,GAAD,EAAkB,WAAU,SAAU,CAAA;IAC/C,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,WAAU;gBACX;KACC,CAAA,GACH,KAAe,kBAAC,KAAD;MAAG,WAAU;gBAAoC;KAAe,CAAA,CAC7E;;IACJ,KAAW,kBAAC,OAAD;KAAK,WAAU;eAAoC;IAAa,CAAA;GACzE;MAGL,kBAAC,OAAD;GAAK,WAAU;GAA4B;EAAc,CAAA,CACtD;;AAET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProgramCard.js","names":[],"sources":["../../../src/atheletebridge/components/ProgramCard.tsx"],"sourcesContent":["/**\n * Program Card\n * @module atheletebridge/components\n *\n * A training-program tile with a sport-icon tile, a level Badge tinted per tier\n * (green Beginner / blue Intermediate / amber Advanced / brand Elite), a status\n * pill, and a footer stat row of weeks + drill counts. Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { CalendarRange, Copy, Dumbbell } from 'lucide-react';\nimport { GlassCard, Badge, type BadgeProps } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { SPORT_ICONS, SPORT_LABELS, PROGRAM_LEVEL_LABELS } from './domain';\nimport { cn } from '../utils/cn';\nimport type { AthProgram, AthProgramLevel } from '../types';\n\ninterface ProgramCardProps {\n program: AthProgram;\n onClick?: () => void;\n /**\n * Drill count for the footer chip. Falls back to `program.drills?.length`\n * when the query selected drills; omit the chip if neither is available.\n */\n drillCount?: number;\n}\n\nconst LEVEL_VARIANT: Record<AthProgramLevel, BadgeProps['variant']> = {\n BEGINNER: 'success',\n INTERMEDIATE: 'info',\n ADVANCED: 'warning',\n ELITE: 'default',\n};\n\nexport const ProgramCard: FC<ProgramCardProps> = ({ program, onClick, drillCount }) => {\n const SportIcon = SPORT_ICONS[program.sport];\n const drills = drillCount ?? program.drills?.items.length;\n\n return (\n <GlassCard\n treatment=\"metric\"\n role=\"button\"\n tabIndex={0}\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }}\n className={cn(\n 'cursor-pointer p-5 transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\">\n <SportIcon className=\"h-5 w-5\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">{program.name}</h3>\n <div className=\"mt-1 flex flex-wrap items-center gap-1.5\">\n <Badge variant={LEVEL_VARIANT[program.level]}>\n {PROGRAM_LEVEL_LABELS[program.level]}\n </Badge>\n <span className=\"text-xs text-text-secondary\">{SPORT_LABELS[program.sport]}</span>\n </div>\n </div>\n </div>\n <StatusBadge status={program.status} />\n </div>\n\n {program.summary && (\n <p className=\"mt-3 line-clamp-2 text-sm text-text-secondary\">{program.summary}</p>\n )}\n\n <div className=\"mt-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <CalendarRange className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {program.weeks}{' '}\n {program.weeks === 1 ? 'week' : 'weeks'}\n </span>\n {drills !== undefined && (\n <span className=\"flex items-center gap-1\">\n <Dumbbell className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {drills}{' '}\n {drills === 1 ? 'drill' : 'drills'}\n </span>\n )}\n {program.isTemplate && (\n <span className=\"flex items-center gap-1\">\n <Copy className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> Template\n </span>\n )}\n </div>\n </GlassCard>\n );\n};\n"],"mappings":";;;;;;;AA2BA,IAAM,IAAgE;CACpE,UAAU;CACV,cAAc;CACd,UAAU;CACV,OAAO;
|
|
1
|
+
{"version":3,"file":"ProgramCard.js","names":[],"sources":["../../../src/atheletebridge/components/ProgramCard.tsx"],"sourcesContent":["/**\n * Program Card\n * @module atheletebridge/components\n *\n * A training-program tile with a sport-icon tile, a level Badge tinted per tier\n * (green Beginner / blue Intermediate / amber Advanced / brand Elite), a status\n * pill, and a footer stat row of weeks + drill counts. Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { CalendarRange, Copy, Dumbbell } from 'lucide-react';\nimport { GlassCard, Badge, type BadgeProps } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { SPORT_ICONS, SPORT_LABELS, PROGRAM_LEVEL_LABELS } from './domain';\nimport { cn } from '../utils/cn';\nimport type { AthProgram, AthProgramLevel } from '../types';\n\ninterface ProgramCardProps {\n program: AthProgram;\n onClick?: () => void;\n /**\n * Drill count for the footer chip. Falls back to `program.drills?.length`\n * when the query selected drills; omit the chip if neither is available.\n */\n drillCount?: number;\n}\n\nconst LEVEL_VARIANT: Record<AthProgramLevel, BadgeProps['variant']> = {\n BEGINNER: 'success',\n INTERMEDIATE: 'info',\n ADVANCED: 'warning',\n ELITE: 'default',\n};\n\nexport const ProgramCard: FC<ProgramCardProps> = ({ program, onClick, drillCount }) => {\n const SportIcon = SPORT_ICONS[program.sport];\n const drills = drillCount ?? program.drills?.items.length;\n\n return (\n <GlassCard\n treatment=\"metric\"\n role=\"button\"\n tabIndex={0}\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }}\n className={cn(\n 'cursor-pointer p-5 transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\">\n <SportIcon className=\"h-5 w-5\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">{program.name}</h3>\n <div className=\"mt-1 flex flex-wrap items-center gap-1.5\">\n <Badge variant={LEVEL_VARIANT[program.level]}>\n {PROGRAM_LEVEL_LABELS[program.level]}\n </Badge>\n <span className=\"text-xs text-text-secondary\">{SPORT_LABELS[program.sport]}</span>\n </div>\n </div>\n </div>\n <StatusBadge status={program.status} />\n </div>\n\n {program.summary && (\n <p className=\"mt-3 line-clamp-2 text-sm text-text-secondary\">{program.summary}</p>\n )}\n\n <div className=\"mt-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <CalendarRange className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {program.weeks}{' '}\n {program.weeks === 1 ? 'week' : 'weeks'}\n </span>\n {drills !== undefined && (\n <span className=\"flex items-center gap-1\">\n <Dumbbell className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {drills}{' '}\n {drills === 1 ? 'drill' : 'drills'}\n </span>\n )}\n {program.isTemplate && (\n <span className=\"flex items-center gap-1\">\n <Copy className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> Template\n </span>\n )}\n </div>\n </GlassCard>\n );\n};\n"],"mappings":";;;;;;;AA2BA,IAAM,IAAgE;CACpE,UAAU;CACV,cAAc;CACd,UAAU;CACV,OAAO;AACT,GAEa,KAAqC,EAAE,YAAS,YAAS,oBAAiB;CACrF,IAAM,IAAY,EAAY,EAAQ,QAChC,IAAS,KAAc,EAAQ,QAAQ,MAAM;CAEnD,OACE,kBAAC,GAAD;EACE,WAAU;EACV,MAAK;EACL,UAAU;EACD;EACT,YAAY,MAAM;GAChB,CAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,eAAe,GACjB,IAAU;EAEd;EACA,WAAW,EACT,0KACF;YAbF;GAeE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAW,WAAU;OAAU,eAAY;MAAQ,CAAA;KAChD,CAAA,GACL,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAsD,EAAQ;MAAS,CAAA,GACrF,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAS,EAAc,EAAQ;kBACnC,EAAqB,EAAQ;OACzB,CAAA,GACP,kBAAC,QAAD;QAAM,WAAU;kBAA+B,EAAa,EAAQ;OAAa,CAAA,CAC9E;QACF;OACF;QACL,kBAAC,GAAD,EAAa,QAAQ,EAAQ,OAAS,CAAA,CACnC;;GAEJ,EAAQ,WACP,kBAAC,KAAD;IAAG,WAAU;cAAiD,EAAQ;GAAW,CAAA;GAGnF,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD;QAAe,WAAU;QAAc,eAAY;OAAQ,CAAA;OAAC;OAAE,EAAQ;OAAO;OAC5E,EAAQ,UAAU,IAAI,SAAS;MAC5B;;KACL,MAAW,KAAA,KACV,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD;QAAU,WAAU;QAAc,eAAY;OAAQ,CAAA;OAAC;OAAE;OAAQ;OAChE,MAAW,IAAI,UAAU;MACtB;;KAEP,EAAQ,cACP,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,kBAAC,GAAD;OAAM,WAAU;OAAc,eAAY;MAAQ,CAAA,GAAC,WAC/C;;IAEL;;EACI;;AAEf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouteAnalytics.js","names":[],"sources":["../../../src/atheletebridge/components/RouteAnalytics.tsx"],"sourcesContent":["/**\n * RouteAnalytics\n *\n * Listens to route changes inside the AthleteBridge microfrontend and emits\n * `atheletebridge.page.viewed` events to the shared event bus. The host shell\n * forwards these to OtelBrowserSink / BackendAuditSink / RybbitSink.\n */\n\nimport { useEffect, useRef } from 'react';\nimport { useLocation } from 'react-router-dom';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\n\nfunction resolvePageName(pathname: string): string {\n const segments = pathname.split('/').filter(Boolean);\n // When mounted under /atheletebridge, the first segment is the product slug.\n const productIndex = segments.findIndex((s) => s === 'atheletebridge');\n const relevant = productIndex >= 0 ? segments.slice(productIndex + 1) : segments;\n if (relevant.length === 0) return 'dashboard';\n return relevant.join('/');\n}\n\nexport function RouteAnalytics() {\n const location = useLocation();\n const bus = useEventBus();\n const lastEmittedRef = useRef<string | null>(null);\n\n useEffect(() => {\n const route = `${location.pathname}${location.search}`;\n if (lastEmittedRef.current === route) return;\n lastEmittedRef.current = route;\n\n const page = resolvePageName(location.pathname);\n bus.emitCustom('atheletebridge.page.viewed', {\n page,\n route: location.pathname,\n product: 'atheletebridge',\n });\n }, [bus, location.pathname, location.search]);\n\n return null;\n}\n"],"mappings":";;;;AAYA,SAAS,EAAgB,GAA0B;CACjD,IAAM,IAAW,EAAS,MAAM,
|
|
1
|
+
{"version":3,"file":"RouteAnalytics.js","names":[],"sources":["../../../src/atheletebridge/components/RouteAnalytics.tsx"],"sourcesContent":["/**\n * RouteAnalytics\n *\n * Listens to route changes inside the AthleteBridge microfrontend and emits\n * `atheletebridge.page.viewed` events to the shared event bus. The host shell\n * forwards these to OtelBrowserSink / BackendAuditSink / RybbitSink.\n */\n\nimport { useEffect, useRef } from 'react';\nimport { useLocation } from 'react-router-dom';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\n\nfunction resolvePageName(pathname: string): string {\n const segments = pathname.split('/').filter(Boolean);\n // When mounted under /atheletebridge, the first segment is the product slug.\n const productIndex = segments.findIndex((s) => s === 'atheletebridge');\n const relevant = productIndex >= 0 ? segments.slice(productIndex + 1) : segments;\n if (relevant.length === 0) return 'dashboard';\n return relevant.join('/');\n}\n\nexport function RouteAnalytics() {\n const location = useLocation();\n const bus = useEventBus();\n const lastEmittedRef = useRef<string | null>(null);\n\n useEffect(() => {\n const route = `${location.pathname}${location.search}`;\n if (lastEmittedRef.current === route) return;\n lastEmittedRef.current = route;\n\n const page = resolvePageName(location.pathname);\n bus.emitCustom('atheletebridge.page.viewed', {\n page,\n route: location.pathname,\n product: 'atheletebridge',\n });\n }, [bus, location.pathname, location.search]);\n\n return null;\n}\n"],"mappings":";;;;AAYA,SAAS,EAAgB,GAA0B;CACjD,IAAM,IAAW,EAAS,MAAM,GAAG,EAAE,OAAO,OAAO,GAE7C,IAAe,EAAS,WAAW,MAAM,MAAM,gBAAgB,GAC/D,IAAW,KAAgB,IAAI,EAAS,MAAM,IAAe,CAAC,IAAI;CAExE,OADI,EAAS,WAAW,IAAU,cAC3B,EAAS,KAAK,GAAG;AAC1B;AAEA,SAAgB,IAAiB;CAC/B,IAAM,IAAW,EAAY,GACvB,IAAM,EAAY,GAClB,IAAiB,EAAsB,IAAI;CAejD,OAbA,QAAgB;EACd,IAAM,IAAQ,GAAG,EAAS,WAAW,EAAS;EAC9C,IAAI,EAAe,YAAY,GAAO;EACtC,EAAe,UAAU;EAEzB,IAAM,IAAO,EAAgB,EAAS,QAAQ;EAC9C,EAAI,WAAW,8BAA8B;GAC3C;GACA,OAAO,EAAS;GAChB,SAAS;EACX,CAAC;CACH,GAAG;EAAC;EAAK,EAAS;EAAU,EAAS;CAAM,CAAC,GAErC;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Scoreboard.js","names":[],"sources":["../../../src/atheletebridge/components/Scoreboard.tsx"],"sourcesContent":["/**\n * Scoreboard\n * @module atheletebridge/components\n *\n * Crest-vs-crest fixture presentation. `Scoreboard` renders the full match card\n * (competition strip → crest / score / crest → venue) used by the fixtures list\n * and fixture detail; the compact single-line `MatchupRow` (separate file)\n * reuses the same building blocks for the dashboard + team-detail feeds.\n *\n * All colors are semantic tokens. Winner side is tinted with\n * status-success-text, loser muted — driven purely by the real score.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { CalendarClock, MapPin, Trophy } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { Crest } from './Crest';\nimport { SPORT_ICONS, FIXTURE_TYPE_LABELS } from './domain';\nimport { formatDate } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthFixture, AthSport } from '../types';\n\n// ── Shared matchup model ─────────────────────────────────────────────────────\n\nexport interface Matchup {\n homeName: string;\n awayName: string;\n homeSport?: AthSport;\n awaySport?: AthSport;\n homeScore: number | null;\n awayScore: number | null;\n hasScore: boolean;\n homeWon: boolean;\n awayWon: boolean;\n}\n\n/** Derive the display matchup (names, scores, winner) from a fixture. */\nexport function resolveMatchup(fixture: AthFixture): Matchup {\n const homeName = fixture.homeTeam?.name ?? 'Home';\n const awayName = fixture.awayTeam?.name ?? (fixture.awayTeamId ? 'Away' : 'TBD');\n const hasScore = fixture.homeScore !== null && fixture.awayScore !== null;\n const homeScore = fixture.homeScore;\n const awayScore = fixture.awayScore;\n const homeWon = hasScore && (homeScore ?? 0) > (awayScore ?? 0);\n const awayWon = hasScore && (awayScore ?? 0) > (homeScore ?? 0);\n return {\n homeName,\n awayName,\n homeSport: fixture.homeTeam?.sport,\n awaySport: fixture.awayTeam?.sport,\n homeScore,\n awayScore,\n hasScore,\n homeWon,\n awayWon,\n };\n}\n\n// ── One team flank (crest + name) ────────────────────────────────────────────\n\nexport interface MatchupSideProps {\n name: string;\n sport?: AthSport;\n align?: 'start' | 'end';\n /** Winner emphasis — brightens the name. */\n won?: boolean;\n /** Loser de-emphasis when the fixture has a score. */\n dimmed?: boolean;\n crestSize?: number;\n /** Stack crest above name (hero/card) instead of inline (row). */\n stacked?: boolean;\n className?: string;\n}\n\nexport const MatchupSide: FC<MatchupSideProps> = ({\n name,\n sport,\n align = 'start',\n won = false,\n dimmed = false,\n crestSize = 40,\n stacked = false,\n className,\n}) => (\n <div\n className={cn(\n 'flex min-w-0 items-center gap-3',\n stacked\n ? 'flex-col text-center'\n : align === 'end'\n ? 'flex-row-reverse text-right'\n : 'text-left',\n className\n )}\n >\n <Crest name={name} sport={sport} size={crestSize} />\n <span\n className={cn(\n 'min-w-0 truncate font-semibold',\n stacked ? 'max-w-[10rem] text-sm' : 'text-sm',\n won ? 'text-status-success-text' : dimmed ? 'text-text-muted' : 'text-text-primary'\n )}\n >\n {name}\n </span>\n </div>\n);\n\n// ── Centerpiece (score or vs + kickoff) ──────────────────────────────────────\n\nexport interface ScoreCenterProps {\n matchup: Matchup;\n time?: ReactNode;\n size?: 'sm' | 'md' | 'lg';\n className?: string;\n}\n\nconst SCORE_SIZE = {\n sm: 'text-lg',\n md: 'text-2xl',\n lg: 'text-4xl sm:text-5xl',\n} as const;\n\nexport const ScoreCenter: FC<ScoreCenterProps> = ({ matchup, time, size = 'md', className }) => {\n if (!matchup.hasScore) {\n return (\n <div className={cn('flex flex-col items-center gap-0.5', className)}>\n <span className=\"rounded-lg bg-bg-sunken px-3 py-1 text-xs font-bold uppercase tracking-wide text-text-secondary\">\n vs\n </span>\n {time ? <span className=\"text-xs text-text-secondary\">{time}</span> : null}\n </div>\n );\n }\n return (\n <div className={cn('flex flex-col items-center gap-0.5', className)}>\n <span\n className={cn(\n 'flex items-center gap-2 font-bold leading-none tabular-nums',\n SCORE_SIZE[size]\n )}\n >\n <span className={matchup.homeWon ? 'text-status-success-text' : 'text-text-primary'}>\n {matchup.homeScore}\n </span>\n <span className=\"text-text-muted\">–</span>\n <span className={matchup.awayWon ? 'text-status-success-text' : 'text-text-primary'}>\n {matchup.awayScore}\n </span>\n </span>\n {time ? <span className=\"text-xs text-text-secondary\">{time}</span> : null}\n </div>\n );\n};\n\n// ── Competition eyebrow ──────────────────────────────────────────────────────\n\nconst CompetitionStrip: FC<{ fixture: AthFixture; center?: boolean }> = ({\n fixture,\n center = false,\n}) => {\n const SportIcon = fixture.homeTeam?.sport ? SPORT_ICONS[fixture.homeTeam.sport] : Trophy;\n const parts = [fixture.competition, fixture.round].filter(Boolean).join(' · ');\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 text-xs font-medium text-text-secondary',\n center && 'justify-center'\n )}\n >\n <SportIcon className=\"h-3.5 w-3.5 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n <span className=\"truncate\">{parts || FIXTURE_TYPE_LABELS[fixture.type]}</span>\n </span>\n );\n};\n\n// ── Full match card / hero ───────────────────────────────────────────────────\n\nexport interface ScoreboardProps {\n fixture: AthFixture;\n onClick?: () => void;\n /** `card` = fixtures-list tile; `hero` = large fixture-detail header. */\n variant?: 'card' | 'hero';\n className?: string;\n}\n\n/** Full crest-vs-crest match card (list tile or detail hero). */\nexport const Scoreboard: FC<ScoreboardProps> = ({\n fixture,\n onClick,\n variant = 'card',\n className,\n}) => {\n const matchup = resolveMatchup(fixture);\n const isHero = variant === 'hero';\n const timeLabel = matchup.hasScore ? null : formatDate(fixture.scheduledAt);\n\n const clickable = Boolean(onClick);\n\n return (\n <GlassCard\n treatment={isHero ? 'glass' : 'metric'}\n glow={isHero}\n role={clickable ? 'button' : undefined}\n tabIndex={clickable ? 0 : undefined}\n onClick={onClick}\n onKeyDown={\n clickable\n ? (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }\n : undefined\n }\n className={cn(\n isHero ? 'p-6 sm:p-8' : 'p-5',\n clickable &&\n 'cursor-pointer transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n className\n )}\n >\n <div className={cn('flex items-center justify-between gap-3', isHero && 'justify-center')}>\n <CompetitionStrip fixture={fixture} center={isHero} />\n {!isHero ? <StatusBadge status={fixture.status} /> : null}\n </div>\n\n <div\n className={cn(\n 'mt-4 grid items-center gap-3',\n 'grid-cols-[1fr_auto_1fr]',\n isHero && 'mt-6 gap-4 sm:gap-8'\n )}\n >\n <MatchupSide\n name={matchup.homeName}\n sport={matchup.homeSport}\n align={isHero ? undefined : 'end'}\n stacked={isHero}\n won={matchup.homeWon}\n dimmed={matchup.awayWon}\n crestSize={isHero ? 72 : 40}\n />\n <ScoreCenter matchup={matchup} time={timeLabel} size={isHero ? 'lg' : 'md'} />\n <MatchupSide\n name={matchup.awayName}\n sport={matchup.awaySport}\n align={isHero ? undefined : 'start'}\n stacked={isHero}\n won={matchup.awayWon}\n dimmed={matchup.homeWon}\n crestSize={isHero ? 72 : 40}\n />\n </div>\n\n {isHero ? (\n <div className=\"mt-5 flex justify-center\">\n <StatusBadge status={fixture.status} />\n </div>\n ) : null}\n\n <div\n className={cn(\n 'mt-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-secondary',\n isHero && 'mt-6 justify-center'\n )}\n >\n <span className=\"flex items-center gap-1\">\n <CalendarClock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />{' '}\n {formatDate(fixture.scheduledAt)}\n </span>\n {fixture.venue ? (\n <span className=\"flex items-center gap-1 truncate\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" /> {fixture.venue}\n </span>\n ) : null}\n </div>\n </GlassCard>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Scoreboard.js","names":[],"sources":["../../../src/atheletebridge/components/Scoreboard.tsx"],"sourcesContent":["/**\n * Scoreboard\n * @module atheletebridge/components\n *\n * Crest-vs-crest fixture presentation. `Scoreboard` renders the full match card\n * (competition strip → crest / score / crest → venue) used by the fixtures list\n * and fixture detail; the compact single-line `MatchupRow` (separate file)\n * reuses the same building blocks for the dashboard + team-detail feeds.\n *\n * All colors are semantic tokens. Winner side is tinted with\n * status-success-text, loser muted — driven purely by the real score.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { CalendarClock, MapPin, Trophy } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { Crest } from './Crest';\nimport { SPORT_ICONS, FIXTURE_TYPE_LABELS } from './domain';\nimport { formatDate } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthFixture, AthSport } from '../types';\n\n// ── Shared matchup model ─────────────────────────────────────────────────────\n\nexport interface Matchup {\n homeName: string;\n awayName: string;\n homeSport?: AthSport;\n awaySport?: AthSport;\n homeScore: number | null;\n awayScore: number | null;\n hasScore: boolean;\n homeWon: boolean;\n awayWon: boolean;\n}\n\n/** Derive the display matchup (names, scores, winner) from a fixture. */\nexport function resolveMatchup(fixture: AthFixture): Matchup {\n const homeName = fixture.homeTeam?.name ?? 'Home';\n const awayName = fixture.awayTeam?.name ?? (fixture.awayTeamId ? 'Away' : 'TBD');\n const hasScore = fixture.homeScore !== null && fixture.awayScore !== null;\n const homeScore = fixture.homeScore;\n const awayScore = fixture.awayScore;\n const homeWon = hasScore && (homeScore ?? 0) > (awayScore ?? 0);\n const awayWon = hasScore && (awayScore ?? 0) > (homeScore ?? 0);\n return {\n homeName,\n awayName,\n homeSport: fixture.homeTeam?.sport,\n awaySport: fixture.awayTeam?.sport,\n homeScore,\n awayScore,\n hasScore,\n homeWon,\n awayWon,\n };\n}\n\n// ── One team flank (crest + name) ────────────────────────────────────────────\n\nexport interface MatchupSideProps {\n name: string;\n sport?: AthSport;\n align?: 'start' | 'end';\n /** Winner emphasis — brightens the name. */\n won?: boolean;\n /** Loser de-emphasis when the fixture has a score. */\n dimmed?: boolean;\n crestSize?: number;\n /** Stack crest above name (hero/card) instead of inline (row). */\n stacked?: boolean;\n className?: string;\n}\n\nexport const MatchupSide: FC<MatchupSideProps> = ({\n name,\n sport,\n align = 'start',\n won = false,\n dimmed = false,\n crestSize = 40,\n stacked = false,\n className,\n}) => (\n <div\n className={cn(\n 'flex min-w-0 items-center gap-3',\n stacked\n ? 'flex-col text-center'\n : align === 'end'\n ? 'flex-row-reverse text-right'\n : 'text-left',\n className\n )}\n >\n <Crest name={name} sport={sport} size={crestSize} />\n <span\n className={cn(\n 'min-w-0 truncate font-semibold',\n stacked ? 'max-w-[10rem] text-sm' : 'text-sm',\n won ? 'text-status-success-text' : dimmed ? 'text-text-muted' : 'text-text-primary'\n )}\n >\n {name}\n </span>\n </div>\n);\n\n// ── Centerpiece (score or vs + kickoff) ──────────────────────────────────────\n\nexport interface ScoreCenterProps {\n matchup: Matchup;\n time?: ReactNode;\n size?: 'sm' | 'md' | 'lg';\n className?: string;\n}\n\nconst SCORE_SIZE = {\n sm: 'text-lg',\n md: 'text-2xl',\n lg: 'text-4xl sm:text-5xl',\n} as const;\n\nexport const ScoreCenter: FC<ScoreCenterProps> = ({ matchup, time, size = 'md', className }) => {\n if (!matchup.hasScore) {\n return (\n <div className={cn('flex flex-col items-center gap-0.5', className)}>\n <span className=\"rounded-lg bg-bg-sunken px-3 py-1 text-xs font-bold uppercase tracking-wide text-text-secondary\">\n vs\n </span>\n {time ? <span className=\"text-xs text-text-secondary\">{time}</span> : null}\n </div>\n );\n }\n return (\n <div className={cn('flex flex-col items-center gap-0.5', className)}>\n <span\n className={cn(\n 'flex items-center gap-2 font-bold leading-none tabular-nums',\n SCORE_SIZE[size]\n )}\n >\n <span className={matchup.homeWon ? 'text-status-success-text' : 'text-text-primary'}>\n {matchup.homeScore}\n </span>\n <span className=\"text-text-muted\">–</span>\n <span className={matchup.awayWon ? 'text-status-success-text' : 'text-text-primary'}>\n {matchup.awayScore}\n </span>\n </span>\n {time ? <span className=\"text-xs text-text-secondary\">{time}</span> : null}\n </div>\n );\n};\n\n// ── Competition eyebrow ──────────────────────────────────────────────────────\n\nconst CompetitionStrip: FC<{ fixture: AthFixture; center?: boolean }> = ({\n fixture,\n center = false,\n}) => {\n const SportIcon = fixture.homeTeam?.sport ? SPORT_ICONS[fixture.homeTeam.sport] : Trophy;\n const parts = [fixture.competition, fixture.round].filter(Boolean).join(' · ');\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 text-xs font-medium text-text-secondary',\n center && 'justify-center'\n )}\n >\n <SportIcon className=\"h-3.5 w-3.5 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n <span className=\"truncate\">{parts || FIXTURE_TYPE_LABELS[fixture.type]}</span>\n </span>\n );\n};\n\n// ── Full match card / hero ───────────────────────────────────────────────────\n\nexport interface ScoreboardProps {\n fixture: AthFixture;\n onClick?: () => void;\n /** `card` = fixtures-list tile; `hero` = large fixture-detail header. */\n variant?: 'card' | 'hero';\n className?: string;\n}\n\n/** Full crest-vs-crest match card (list tile or detail hero). */\nexport const Scoreboard: FC<ScoreboardProps> = ({\n fixture,\n onClick,\n variant = 'card',\n className,\n}) => {\n const matchup = resolveMatchup(fixture);\n const isHero = variant === 'hero';\n const timeLabel = matchup.hasScore ? null : formatDate(fixture.scheduledAt);\n\n const clickable = Boolean(onClick);\n\n return (\n <GlassCard\n treatment={isHero ? 'glass' : 'metric'}\n glow={isHero}\n role={clickable ? 'button' : undefined}\n tabIndex={clickable ? 0 : undefined}\n onClick={onClick}\n onKeyDown={\n clickable\n ? (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }\n : undefined\n }\n className={cn(\n isHero ? 'p-6 sm:p-8' : 'p-5',\n clickable &&\n 'cursor-pointer transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n className\n )}\n >\n <div className={cn('flex items-center justify-between gap-3', isHero && 'justify-center')}>\n <CompetitionStrip fixture={fixture} center={isHero} />\n {!isHero ? <StatusBadge status={fixture.status} /> : null}\n </div>\n\n <div\n className={cn(\n 'mt-4 grid items-center gap-3',\n 'grid-cols-[1fr_auto_1fr]',\n isHero && 'mt-6 gap-4 sm:gap-8'\n )}\n >\n <MatchupSide\n name={matchup.homeName}\n sport={matchup.homeSport}\n align={isHero ? undefined : 'end'}\n stacked={isHero}\n won={matchup.homeWon}\n dimmed={matchup.awayWon}\n crestSize={isHero ? 72 : 40}\n />\n <ScoreCenter matchup={matchup} time={timeLabel} size={isHero ? 'lg' : 'md'} />\n <MatchupSide\n name={matchup.awayName}\n sport={matchup.awaySport}\n align={isHero ? undefined : 'start'}\n stacked={isHero}\n won={matchup.awayWon}\n dimmed={matchup.homeWon}\n crestSize={isHero ? 72 : 40}\n />\n </div>\n\n {isHero ? (\n <div className=\"mt-5 flex justify-center\">\n <StatusBadge status={fixture.status} />\n </div>\n ) : null}\n\n <div\n className={cn(\n 'mt-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-secondary',\n isHero && 'mt-6 justify-center'\n )}\n >\n <span className=\"flex items-center gap-1\">\n <CalendarClock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />{' '}\n {formatDate(fixture.scheduledAt)}\n </span>\n {fixture.venue ? (\n <span className=\"flex items-center gap-1 truncate\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" /> {fixture.venue}\n </span>\n ) : null}\n </div>\n </GlassCard>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionCard.js","names":[],"sources":["../../../src/atheletebridge/components/SessionCard.tsx"],"sourcesContent":["/**\n * Session Card\n * @module atheletebridge/components\n *\n * A coaching-session tile showing the athlete identity (monogram avatar +\n * resolved name when available), session type, schedule, and a colored status\n * badge (Requested amber / Confirmed blue / Completed green / Cancelled grey).\n * A stored rating renders as read-only stars. Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { CalendarClock, Clock, Video, MapPin } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { UserAvatar } from './UserAvatar';\nimport { RatingStars } from './viz';\nimport { SESSION_TYPE_LABELS } from './domain';\nimport { formatDate, formatDuration } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthSession } from '../types';\n\ninterface SessionCardProps {\n session: AthSession;\n onClick?: () => void;\n /** Resolved athlete display name (else a monogram from the id is shown). */\n athleteName?: string | null;\n /** Resolved coach display name (optional sub-label). */\n coachName?: string | null;\n}\n\nexport const SessionCard: FC<SessionCardProps> = ({ session, onClick, athleteName, coachName }) => (\n <GlassCard\n treatment=\"metric\"\n role=\"button\"\n tabIndex={0}\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }}\n className={cn(\n 'cursor-pointer p-5 transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <UserAvatar name={athleteName} id={session.athleteId} size=\"md\" />\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">{session.title}</h3>\n <p className=\"truncate text-sm text-text-secondary\">\n {athleteName?.trim() || 'Athlete'} · {SESSION_TYPE_LABELS[session.type]}\n {coachName?.trim() ? ` · ${coachName.trim()}` : ''}\n </p>\n </div>\n </div>\n <StatusBadge status={session.status} />\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <CalendarClock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />{' '}\n {formatDate(session.scheduledAt)}\n </span>\n <span className=\"flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {formatDuration(session.durationMin)}\n </span>\n {session.type === 'IN_PERSON' && session.location && (\n <span className=\"flex items-center gap-1 truncate\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" /> {session.location}\n </span>\n )}\n {session.type !== 'IN_PERSON' && session.meetingUrl && (\n <span className=\"flex items-center gap-1\">\n <Video className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> Online\n </span>\n )}\n {session.rating !== null && <RatingStars value={session.rating} size={14} showValue />}\n </div>\n </GlassCard>\n);\n"],"mappings":";;;;;;;;;;;AA8BA,IAAa,KAAqC,EAAE,YAAS,YAAS,gBAAa,mBACjF,kBAAC,GAAD;CACE,WAAU;CACV,MAAK;CACL,UAAU;CACD;CACT,YAAY,MAAM;
|
|
1
|
+
{"version":3,"file":"SessionCard.js","names":[],"sources":["../../../src/atheletebridge/components/SessionCard.tsx"],"sourcesContent":["/**\n * Session Card\n * @module atheletebridge/components\n *\n * A coaching-session tile showing the athlete identity (monogram avatar +\n * resolved name when available), session type, schedule, and a colored status\n * badge (Requested amber / Confirmed blue / Completed green / Cancelled grey).\n * A stored rating renders as read-only stars. Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { CalendarClock, Clock, Video, MapPin } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { UserAvatar } from './UserAvatar';\nimport { RatingStars } from './viz';\nimport { SESSION_TYPE_LABELS } from './domain';\nimport { formatDate, formatDuration } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthSession } from '../types';\n\ninterface SessionCardProps {\n session: AthSession;\n onClick?: () => void;\n /** Resolved athlete display name (else a monogram from the id is shown). */\n athleteName?: string | null;\n /** Resolved coach display name (optional sub-label). */\n coachName?: string | null;\n}\n\nexport const SessionCard: FC<SessionCardProps> = ({ session, onClick, athleteName, coachName }) => (\n <GlassCard\n treatment=\"metric\"\n role=\"button\"\n tabIndex={0}\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }}\n className={cn(\n 'cursor-pointer p-5 transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <UserAvatar name={athleteName} id={session.athleteId} size=\"md\" />\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">{session.title}</h3>\n <p className=\"truncate text-sm text-text-secondary\">\n {athleteName?.trim() || 'Athlete'} · {SESSION_TYPE_LABELS[session.type]}\n {coachName?.trim() ? ` · ${coachName.trim()}` : ''}\n </p>\n </div>\n </div>\n <StatusBadge status={session.status} />\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <CalendarClock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />{' '}\n {formatDate(session.scheduledAt)}\n </span>\n <span className=\"flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {formatDuration(session.durationMin)}\n </span>\n {session.type === 'IN_PERSON' && session.location && (\n <span className=\"flex items-center gap-1 truncate\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" /> {session.location}\n </span>\n )}\n {session.type !== 'IN_PERSON' && session.meetingUrl && (\n <span className=\"flex items-center gap-1\">\n <Video className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> Online\n </span>\n )}\n {session.rating !== null && <RatingStars value={session.rating} size={14} showValue />}\n </div>\n </GlassCard>\n);\n"],"mappings":";;;;;;;;;;;AA8BA,IAAa,KAAqC,EAAE,YAAS,YAAS,gBAAa,mBACjF,kBAAC,GAAD;CACE,WAAU;CACV,MAAK;CACL,UAAU;CACD;CACT,YAAY,MAAM;EAChB,CAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,eAAe,GACjB,IAAU;CAEd;CACA,WAAW,EACT,0KACF;WAbF,CAeE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAY,MAAM;IAAa,IAAI,EAAQ;IAAW,MAAK;GAAM,CAAA,GACjE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAsD,EAAQ;IAAU,CAAA,GACtF,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,GAAa,KAAK,KAAK;MAAU;MAAI,EAAoB,EAAQ;MACjE,GAAW,KAAK,IAAI,MAAM,EAAU,KAAK,MAAM;KAC/C;MACA;KACF;MACL,kBAAC,GAAD,EAAa,QAAQ,EAAQ,OAAS,CAAA,CACnC;KAEL,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD;MAAe,WAAU;MAAc,eAAY;KAAQ,CAAA;KAAE;KAC5D,EAAW,EAAQ,WAAW;IAC3B;;GACN,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD;MAAO,WAAU;MAAc,eAAY;KAAQ,CAAA;KAAC;KAAE,EAAe,EAAQ,WAAW;IACpF;;GACL,EAAQ,SAAS,eAAe,EAAQ,YACvC,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD;MAAQ,WAAU;MAAuB,eAAY;KAAQ,CAAA;KAAC;KAAE,EAAQ;IACpE;;GAEP,EAAQ,SAAS,eAAe,EAAQ,cACvC,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACE,kBAAC,GAAD;KAAO,WAAU;KAAc,eAAY;IAAQ,CAAA,GAAC,SAChD;;GAEP,EAAQ,WAAW,QAAQ,kBAAC,GAAD;IAAa,OAAO,EAAQ;IAAQ,MAAM;IAAI,WAAA;GAAW,CAAA;EAClF;GACI"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionStatusTimeline.js","names":[],"sources":["../../../src/atheletebridge/components/SessionStatusTimeline.tsx"],"sourcesContent":["/**\n * SessionStatusTimeline\n * @module atheletebridge/components\n *\n * A horizontal lifecycle stepper for a coaching session:\n * Requested → Confirmed → Completed\n * with a distinct terminal variant when the session was Cancelled / No-show.\n * The happy path reuses the fe-libs Stepper; timestamps are shown ONLY where the\n * schema actually has them (createdAt for Requested, completedAt for Completed)\n * — Confirmed has no dedicated timestamp field, so none is fabricated.\n * Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { Check, X } from 'lucide-react';\nimport { Stepper, type StepperStep } from '@burdenoff/fe-libs/ui';\nimport { formatDate } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthSessionStatus } from '../types';\n\nexport interface SessionStatusTimelineProps {\n status: AthSessionStatus;\n /** Requested-at timestamp (session.createdAt). */\n createdAt?: string | null;\n /** Completed-at timestamp (session.completedAt). */\n completedAt?: string | null;\n className?: string;\n}\n\nconst shortWhen = (iso: string | null | undefined): string | undefined => {\n if (!iso) return undefined;\n return formatDate(iso, { weekday: 'short', year: undefined });\n};\n\n/** Lifecycle stepper for a coaching session (with cancelled/no-show terminal). */\nexport const SessionStatusTimeline: FC<SessionStatusTimelineProps> = ({\n status,\n createdAt,\n completedAt,\n className,\n}) => {\n // ── Terminal (cancelled / no-show) ─────────────────────────────────────────\n if (status === 'CANCELLED' || status === 'NO_SHOW') {\n const terminalLabel = status === 'CANCELLED' ? 'Cancelled' : 'No-show';\n return (\n <ol className={cn('flex items-start gap-2', className)} aria-label=\"Session lifecycle\">\n <li className=\"flex flex-1 items-center gap-3\">\n <span\n aria-hidden=\"true\"\n className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-full border border-action-primary-bg bg-action-primary-bg text-action-primary-text\"\n >\n <Check className=\"size-4\" />\n </span>\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium text-text-primary\">Requested</span>\n {shortWhen(createdAt) ? (\n <span className=\"block text-xs text-text-muted\">{shortWhen(createdAt)}</span>\n ) : null}\n </span>\n <span aria-hidden=\"true\" className=\"h-px flex-1 bg-status-error-border\" />\n </li>\n <li className=\"flex items-center gap-3\" aria-current=\"step\">\n <span\n aria-hidden=\"true\"\n className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-full border border-status-error-border bg-status-error-bg-subtle text-status-error-text\"\n >\n <X className=\"size-4\" />\n </span>\n <span className=\"text-sm font-medium text-status-error-text\">{terminalLabel}</span>\n </li>\n </ol>\n );\n }\n\n // ── Happy path ─────────────────────────────────────────────────────────────\n const steps: StepperStep[] = [\n { key: 'requested', label: 'Requested', description: shortWhen(createdAt) },\n { key: 'confirmed', label: 'Confirmed' },\n { key: 'completed', label: 'Completed', description: shortWhen(completedAt) },\n ];\n\n // active = index of the in-progress step; past `steps.length` marks all done.\n const active = status === 'REQUESTED' ? 0 : status === 'CONFIRMED' ? 1 : steps.length;\n\n return <Stepper steps={steps} active={active} className={className} />;\n};\n"],"mappings":";;;;;;AA6BA,IAAM,KAAa,MAAuD;
|
|
1
|
+
{"version":3,"file":"SessionStatusTimeline.js","names":[],"sources":["../../../src/atheletebridge/components/SessionStatusTimeline.tsx"],"sourcesContent":["/**\n * SessionStatusTimeline\n * @module atheletebridge/components\n *\n * A horizontal lifecycle stepper for a coaching session:\n * Requested → Confirmed → Completed\n * with a distinct terminal variant when the session was Cancelled / No-show.\n * The happy path reuses the fe-libs Stepper; timestamps are shown ONLY where the\n * schema actually has them (createdAt for Requested, completedAt for Completed)\n * — Confirmed has no dedicated timestamp field, so none is fabricated.\n * Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { Check, X } from 'lucide-react';\nimport { Stepper, type StepperStep } from '@burdenoff/fe-libs/ui';\nimport { formatDate } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthSessionStatus } from '../types';\n\nexport interface SessionStatusTimelineProps {\n status: AthSessionStatus;\n /** Requested-at timestamp (session.createdAt). */\n createdAt?: string | null;\n /** Completed-at timestamp (session.completedAt). */\n completedAt?: string | null;\n className?: string;\n}\n\nconst shortWhen = (iso: string | null | undefined): string | undefined => {\n if (!iso) return undefined;\n return formatDate(iso, { weekday: 'short', year: undefined });\n};\n\n/** Lifecycle stepper for a coaching session (with cancelled/no-show terminal). */\nexport const SessionStatusTimeline: FC<SessionStatusTimelineProps> = ({\n status,\n createdAt,\n completedAt,\n className,\n}) => {\n // ── Terminal (cancelled / no-show) ─────────────────────────────────────────\n if (status === 'CANCELLED' || status === 'NO_SHOW') {\n const terminalLabel = status === 'CANCELLED' ? 'Cancelled' : 'No-show';\n return (\n <ol className={cn('flex items-start gap-2', className)} aria-label=\"Session lifecycle\">\n <li className=\"flex flex-1 items-center gap-3\">\n <span\n aria-hidden=\"true\"\n className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-full border border-action-primary-bg bg-action-primary-bg text-action-primary-text\"\n >\n <Check className=\"size-4\" />\n </span>\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium text-text-primary\">Requested</span>\n {shortWhen(createdAt) ? (\n <span className=\"block text-xs text-text-muted\">{shortWhen(createdAt)}</span>\n ) : null}\n </span>\n <span aria-hidden=\"true\" className=\"h-px flex-1 bg-status-error-border\" />\n </li>\n <li className=\"flex items-center gap-3\" aria-current=\"step\">\n <span\n aria-hidden=\"true\"\n className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-full border border-status-error-border bg-status-error-bg-subtle text-status-error-text\"\n >\n <X className=\"size-4\" />\n </span>\n <span className=\"text-sm font-medium text-status-error-text\">{terminalLabel}</span>\n </li>\n </ol>\n );\n }\n\n // ── Happy path ─────────────────────────────────────────────────────────────\n const steps: StepperStep[] = [\n { key: 'requested', label: 'Requested', description: shortWhen(createdAt) },\n { key: 'confirmed', label: 'Confirmed' },\n { key: 'completed', label: 'Completed', description: shortWhen(completedAt) },\n ];\n\n // active = index of the in-progress step; past `steps.length` marks all done.\n const active = status === 'REQUESTED' ? 0 : status === 'CONFIRMED' ? 1 : steps.length;\n\n return <Stepper steps={steps} active={active} className={className} />;\n};\n"],"mappings":";;;;;;AA6BA,IAAM,KAAa,MAAuD;CACnE,OACL,OAAO,EAAW,GAAK;EAAE,SAAS;EAAS,MAAM,KAAA;CAAU,CAAC;AAC9D,GAGa,KAAyD,EACpE,WACA,cACA,gBACA,mBACI;CAEJ,IAAI,MAAW,eAAe,MAAW,WAAW;EAClD,IAAM,IAAgB,MAAW,cAAc,cAAc;EAC7D,OACE,kBAAC,MAAD;GAAI,WAAW,EAAG,0BAA0B,CAAS;GAAG,cAAW;aAAnE,CACE,kBAAC,MAAD;IAAI,WAAU;cAAd;KACE,kBAAC,QAAD;MACE,eAAY;MACZ,WAAU;gBAEV,kBAAC,GAAD,EAAO,WAAU,SAAU,CAAA;KACvB,CAAA;KACN,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,kBAAC,QAAD;OAAM,WAAU;iBAA8C;MAAe,CAAA,GAC5E,EAAU,CAAS,IAClB,kBAAC,QAAD;OAAM,WAAU;iBAAiC,EAAU,CAAS;MAAQ,CAAA,IAC1E,IACA;;KACN,kBAAC,QAAD;MAAM,eAAY;MAAO,WAAU;KAAsC,CAAA;IACvE;OACJ,kBAAC,MAAD;IAAI,WAAU;IAA0B,gBAAa;cAArD,CACE,kBAAC,QAAD;KACE,eAAY;KACZ,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,SAAU,CAAA;IACnB,CAAA,GACN,kBAAC,QAAD;KAAM,WAAU;eAA8C;IAAoB,CAAA,CAChF;KACF;;CAER;CAGA,IAAM,IAAuB;EAC3B;GAAE,KAAK;GAAa,OAAO;GAAa,aAAa,EAAU,CAAS;EAAE;EAC1E;GAAE,KAAK;GAAa,OAAO;EAAY;EACvC;GAAE,KAAK;GAAa,OAAO;GAAa,aAAa,EAAU,CAAW;EAAE;CAC9E;CAKA,OAAO,kBAAC,GAAD;EAAgB;EAAe,QAFvB,MAAW,cAAc,IAAI,MAAW,cAAc,IAAI,EAAM;EAEtB;CAAY,CAAA;AACvE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/atheletebridge/components/StatusBadge.tsx"],"sourcesContent":["/**\n * Status Badge Component\n * @module atheletebridge/components\n *\n * A colored badge based on a domain status value, with semantic-token color\n * mapping for the AthleteBridge lifecycle states (teams, members, fixtures,\n * programs, sessions, workouts). Uses semantic tokens only — never raw colors.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\n\ntype BadgeVariant = 'success' | 'warning' | 'destructive' | 'secondary' | 'info' | 'outline';\n\ninterface StatusBadgeProps {\n status: string;\n /**\n * Force a specific variant, overriding the auto status→color mapping. Useful\n * where the same status string reads differently by context (e.g. a cancelled\n * coaching session shown grey rather than the default destructive red).\n */\n variant?: BadgeVariant;\n className?: string;\n}\n\nconst variantStyles: Record<BadgeVariant, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n destructive: 'bg-status-error-bg-subtle text-status-error-text',\n secondary: 'bg-bg-sunken text-text-secondary',\n info: 'bg-[var(--color-accent-soft)] text-text-primary',\n outline: 'border border-border-seam text-text-primary',\n};\n\n// Statuses that represent a \"live\" / in-progress condition get a subtle,\n// opacity-only glow pulse (reduced-motion safe via motion-safe:).\nconst liveStatuses = new Set(['live', 'active', 'confirmed', 'in_progress']);\n\nconst statusVariantMap: Record<string, BadgeVariant> = {\n // Team status\n forming: 'warning',\n active: 'success',\n inactive: 'secondary',\n archived: 'secondary',\n\n // Member status\n trial: 'info',\n injured: 'warning',\n suspended: 'destructive',\n released: 'secondary',\n\n // Fixture status\n scheduled: 'info',\n live: 'success',\n completed: 'success',\n postponed: 'warning',\n cancelled: 'destructive',\n\n // Program status\n draft: 'secondary',\n published: 'success',\n\n // Session status\n requested: 'warning',\n confirmed: 'info',\n no_show: 'destructive',\n\n // Workout status\n planned: 'info',\n skipped: 'secondary',\n};\n\nfunction formatStatusLabel(status: string): string {\n return status\n .replace(/_/g, ' ')\n .toLowerCase()\n .replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n\nexport const StatusBadge: FC<StatusBadgeProps> = ({\n status,\n variant: variantOverride,\n className,\n}) => {\n const key = status.toLowerCase();\n const variant = variantOverride ?? statusVariantMap[key] ?? 'secondary';\n const isLive = liveStatuses.has(key);\n\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',\n variantStyles[variant],\n className\n )}\n >\n {isLive && (\n <span\n aria-hidden=\"true\"\n className=\"size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse\"\n />\n )}\n {formatStatusLabel(status)}\n </span>\n );\n};\n"],"mappings":";;;AAyBA,IAAM,IAA8C;CAClD,SAAS;CACT,SAAS;CACT,aAAa;CACb,WAAW;CACX,MAAM;CACN,SAAS;
|
|
1
|
+
{"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/atheletebridge/components/StatusBadge.tsx"],"sourcesContent":["/**\n * Status Badge Component\n * @module atheletebridge/components\n *\n * A colored badge based on a domain status value, with semantic-token color\n * mapping for the AthleteBridge lifecycle states (teams, members, fixtures,\n * programs, sessions, workouts). Uses semantic tokens only — never raw colors.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\n\ntype BadgeVariant = 'success' | 'warning' | 'destructive' | 'secondary' | 'info' | 'outline';\n\ninterface StatusBadgeProps {\n status: string;\n /**\n * Force a specific variant, overriding the auto status→color mapping. Useful\n * where the same status string reads differently by context (e.g. a cancelled\n * coaching session shown grey rather than the default destructive red).\n */\n variant?: BadgeVariant;\n className?: string;\n}\n\nconst variantStyles: Record<BadgeVariant, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n destructive: 'bg-status-error-bg-subtle text-status-error-text',\n secondary: 'bg-bg-sunken text-text-secondary',\n info: 'bg-[var(--color-accent-soft)] text-text-primary',\n outline: 'border border-border-seam text-text-primary',\n};\n\n// Statuses that represent a \"live\" / in-progress condition get a subtle,\n// opacity-only glow pulse (reduced-motion safe via motion-safe:).\nconst liveStatuses = new Set(['live', 'active', 'confirmed', 'in_progress']);\n\nconst statusVariantMap: Record<string, BadgeVariant> = {\n // Team status\n forming: 'warning',\n active: 'success',\n inactive: 'secondary',\n archived: 'secondary',\n\n // Member status\n trial: 'info',\n injured: 'warning',\n suspended: 'destructive',\n released: 'secondary',\n\n // Fixture status\n scheduled: 'info',\n live: 'success',\n completed: 'success',\n postponed: 'warning',\n cancelled: 'destructive',\n\n // Program status\n draft: 'secondary',\n published: 'success',\n\n // Session status\n requested: 'warning',\n confirmed: 'info',\n no_show: 'destructive',\n\n // Workout status\n planned: 'info',\n skipped: 'secondary',\n};\n\nfunction formatStatusLabel(status: string): string {\n return status\n .replace(/_/g, ' ')\n .toLowerCase()\n .replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n\nexport const StatusBadge: FC<StatusBadgeProps> = ({\n status,\n variant: variantOverride,\n className,\n}) => {\n const key = status.toLowerCase();\n const variant = variantOverride ?? statusVariantMap[key] ?? 'secondary';\n const isLive = liveStatuses.has(key);\n\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',\n variantStyles[variant],\n className\n )}\n >\n {isLive && (\n <span\n aria-hidden=\"true\"\n className=\"size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse\"\n />\n )}\n {formatStatusLabel(status)}\n </span>\n );\n};\n"],"mappings":";;;AAyBA,IAAM,IAA8C;CAClD,SAAS;CACT,SAAS;CACT,aAAa;CACb,WAAW;CACX,MAAM;CACN,SAAS;AACX,GAIM,IAAe,IAAI,IAAI;CAAC;CAAQ;CAAU;CAAa;AAAa,CAAC,GAErE,IAAiD;CAErD,SAAS;CACT,QAAQ;CACR,UAAU;CACV,UAAU;CAGV,OAAO;CACP,SAAS;CACT,WAAW;CACX,UAAU;CAGV,WAAW;CACX,MAAM;CACN,WAAW;CACX,WAAW;CACX,WAAW;CAGX,OAAO;CACP,WAAW;CAGX,WAAW;CACX,WAAW;CACX,SAAS;CAGT,SAAS;CACT,SAAS;AACX;AAEA,SAAS,EAAkB,GAAwB;CACjD,OAAO,EACJ,QAAQ,MAAM,GAAG,EACjB,YAAY,EACZ,QAAQ,UAAU,MAAS,EAAK,YAAY,CAAC;AAClD;AAEA,IAAa,KAAqC,EAChD,WACA,SAAS,GACT,mBACI;CACJ,IAAM,IAAM,EAAO,YAAY,GACzB,IAAU,KAAmB,EAAiB,MAAQ,aACtD,IAAS,EAAa,IAAI,CAAG;CAEnC,OACE,kBAAC,QAAD;EACE,WAAW,EACT,mFACA,EAAc,IACd,CACF;YALF,CAOG,KACC,kBAAC,QAAD;GACE,eAAY;GACZ,WAAU;EACX,CAAA,GAEF,EAAkB,CAAM,CACrB;;AAEV"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamCard.js","names":[],"sources":["../../../src/atheletebridge/components/TeamCard.tsx"],"sourcesContent":["/**\n * Team Card\n * @module atheletebridge/components\n *\n * A team tile fronted by a deterministic gradient monogram crest, with a sport\n * Badge, a StatusBadge (green Active / amber Forming / grey Inactive·Archived),\n * a member-count chip and a hover lift. Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { Users, MapPin, CalendarDays } from 'lucide-react';\nimport { GlassCard, Badge } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { Crest } from './Crest';\nimport { SPORT_ICONS, SPORT_LABELS } from './domain';\nimport { cn } from '../utils/cn';\nimport type { AthTeam } from '../types';\n\ninterface TeamCardProps {\n team: AthTeam;\n onClick?: () => void;\n /**\n * Member count for the footer chip. Falls back to `team.members?.length`\n * when the list query selected members; omit the chip entirely if neither is\n * available (never guess).\n */\n memberCount?: number;\n}\n\nexport const TeamCard: FC<TeamCardProps> = ({ team, onClick, memberCount }) => {\n const SportIcon = SPORT_ICONS[team.sport];\n const count = memberCount ?? team.members?.items.length;\n\n return (\n <GlassCard\n treatment=\"metric\"\n role=\"button\"\n tabIndex={0}\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }}\n className={cn(\n 'cursor-pointer p-5 transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <Crest name={team.name} sport={team.sport} size={44} />\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">{team.name}</h3>\n <div className=\"mt-1\">\n <Badge variant=\"secondary\" className=\"gap-1\">\n <SportIcon className=\"h-3 w-3\" aria-hidden=\"true\" />\n {SPORT_LABELS[team.sport]}\n </Badge>\n </div>\n </div>\n </div>\n <StatusBadge status={team.status} />\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-secondary\">\n {team.ageGroup && (\n <span className=\"flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {team.ageGroup}\n </span>\n )}\n {team.season && (\n <span className=\"flex items-center gap-1\">\n <CalendarDays className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {team.season}\n </span>\n )}\n {team.homeVenue && (\n <span className=\"flex items-center gap-1 truncate\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" /> {team.homeVenue}\n </span>\n )}\n </div>\n\n {team.tags.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {team.tags.slice(0, 4).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {count !== undefined && (\n <div className=\"mt-4 flex items-center gap-1.5 border-t border-border-seam pt-3 text-xs font-medium text-text-secondary\">\n <Users className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {count} {count === 1 ? 'member' : 'members'}\n </div>\n )}\n </GlassCard>\n );\n};\n"],"mappings":";;;;;;;;AA6BA,IAAa,KAA+B,EAAE,SAAM,YAAS,qBAAkB;CAC7E,IAAM,IAAY,EAAY,EAAK,QAC7B,IAAQ,KAAe,EAAK,SAAS,MAAM;
|
|
1
|
+
{"version":3,"file":"TeamCard.js","names":[],"sources":["../../../src/atheletebridge/components/TeamCard.tsx"],"sourcesContent":["/**\n * Team Card\n * @module atheletebridge/components\n *\n * A team tile fronted by a deterministic gradient monogram crest, with a sport\n * Badge, a StatusBadge (green Active / amber Forming / grey Inactive·Archived),\n * a member-count chip and a hover lift. Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { Users, MapPin, CalendarDays } from 'lucide-react';\nimport { GlassCard, Badge } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { Crest } from './Crest';\nimport { SPORT_ICONS, SPORT_LABELS } from './domain';\nimport { cn } from '../utils/cn';\nimport type { AthTeam } from '../types';\n\ninterface TeamCardProps {\n team: AthTeam;\n onClick?: () => void;\n /**\n * Member count for the footer chip. Falls back to `team.members?.length`\n * when the list query selected members; omit the chip entirely if neither is\n * available (never guess).\n */\n memberCount?: number;\n}\n\nexport const TeamCard: FC<TeamCardProps> = ({ team, onClick, memberCount }) => {\n const SportIcon = SPORT_ICONS[team.sport];\n const count = memberCount ?? team.members?.items.length;\n\n return (\n <GlassCard\n treatment=\"metric\"\n role=\"button\"\n tabIndex={0}\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }}\n className={cn(\n 'cursor-pointer p-5 transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <Crest name={team.name} sport={team.sport} size={44} />\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">{team.name}</h3>\n <div className=\"mt-1\">\n <Badge variant=\"secondary\" className=\"gap-1\">\n <SportIcon className=\"h-3 w-3\" aria-hidden=\"true\" />\n {SPORT_LABELS[team.sport]}\n </Badge>\n </div>\n </div>\n </div>\n <StatusBadge status={team.status} />\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-secondary\">\n {team.ageGroup && (\n <span className=\"flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {team.ageGroup}\n </span>\n )}\n {team.season && (\n <span className=\"flex items-center gap-1\">\n <CalendarDays className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {team.season}\n </span>\n )}\n {team.homeVenue && (\n <span className=\"flex items-center gap-1 truncate\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" /> {team.homeVenue}\n </span>\n )}\n </div>\n\n {team.tags.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {team.tags.slice(0, 4).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {count !== undefined && (\n <div className=\"mt-4 flex items-center gap-1.5 border-t border-border-seam pt-3 text-xs font-medium text-text-secondary\">\n <Users className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {count} {count === 1 ? 'member' : 'members'}\n </div>\n )}\n </GlassCard>\n );\n};\n"],"mappings":";;;;;;;;AA6BA,IAAa,KAA+B,EAAE,SAAM,YAAS,qBAAkB;CAC7E,IAAM,IAAY,EAAY,EAAK,QAC7B,IAAQ,KAAe,EAAK,SAAS,MAAM;CAEjD,OACE,kBAAC,GAAD;EACE,WAAU;EACV,MAAK;EACL,UAAU;EACD;EACT,YAAY,MAAM;GAChB,CAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,eAAe,GACjB,IAAU;EAEd;EACA,WAAW,EACT,0KACF;YAbF;GAeE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAO,MAAM,EAAK;MAAM,OAAO,EAAK;MAAO,MAAM;KAAK,CAAA,GACtD,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAsD,EAAK;MAAS,CAAA,GAClF,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD;QAAO,SAAQ;QAAY,WAAU;kBAArC,CACE,kBAAC,GAAD;SAAW,WAAU;SAAU,eAAY;QAAQ,CAAA,GAClD,EAAa,EAAK,MACd;;MACJ,CAAA,CACF;OACF;QACL,kBAAC,GAAD,EAAa,QAAQ,EAAK,OAAS,CAAA,CAChC;;GAEL,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,EAAK,YACJ,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD;QAAO,WAAU;QAAc,eAAY;OAAQ,CAAA;OAAC;OAAE,EAAK;MACvD;;KAEP,EAAK,UACJ,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD;QAAc,WAAU;QAAc,eAAY;OAAQ,CAAA;OAAC;OAAE,EAAK;MAC9D;;KAEP,EAAK,aACJ,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD;QAAQ,WAAU;QAAuB,eAAY;OAAQ,CAAA;OAAC;OAAE,EAAK;MACjE;;IAEL;;GAEJ,EAAK,KAAK,SAAS,KAClB,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAK,KAAK,MAAM,GAAG,CAAC,EAAE,KAAK,MAC1B,kBAAC,QAAD;KAEE,WAAU;eAET;IACG,GAJC,CAID,CACP;GACE,CAAA;GAGN,MAAU,KAAA,KACT,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAO,WAAU;MAAc,eAAY;KAAQ,CAAA;KAClD;KAAM;KAAE,MAAU,IAAI,WAAW;IAC/B;;EAEE;;AAEf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserAvatar.js","names":[],"sources":["../../../src/atheletebridge/components/UserAvatar.tsx"],"sourcesContent":["/**\n * User avatar (monogram)\n * @module atheletebridge/components\n *\n * A circular avatar for a user / athlete / coach. There is no photo field on the\n * schema, so this is an honest initials monogram over a deterministic gradient\n * derived from the resolved name (or the id as a stable fallback). Semantic\n * tokens only.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport { gradientCss, initials } from '../utils/identity';\n\nconst SIZE_MAP = { xs: 24, sm: 32, md: 40, lg: 56, xl: 72 } as const;\n\nexport type UserAvatarSize = keyof typeof SIZE_MAP | number;\n\nexport interface UserAvatarProps {\n /** Resolved display name (drives initials + gradient seed). */\n name?: string | null;\n /** Stable id — used as the gradient seed and initials fallback when no name. */\n id?: string | null;\n /** Optional jersey number rendered instead of initials (roster context). */\n jersey?: number | null;\n /** Named size token or explicit pixel diameter. */\n size?: UserAvatarSize;\n /** Extra ring (used inside overlapping stacks / on colored surfaces). */\n ring?: boolean;\n className?: string;\n}\n\n/** Circular monogram avatar for a user/athlete/coach (initials or jersey #). */\nexport const UserAvatar: FC<UserAvatarProps> = ({\n name,\n id,\n jersey,\n size = 'md',\n ring = false,\n className,\n}) => {\n const px = typeof size === 'number' ? size : SIZE_MAP[size];\n const label = name?.trim() || (id ? `User ${id.slice(0, 6)}` : 'Unknown user');\n const seed = name?.trim() || id || label;\n const content =\n typeof jersey === 'number' && Number.isFinite(jersey) ? String(jersey) : initials(label);\n\n return (\n <span\n role=\"img\"\n aria-label={label}\n title={label}\n className={cn(\n 'inline-flex shrink-0 items-center justify-center rounded-full font-semibold leading-none text-[var(--color-action-primary-text)]',\n ring && 'ring-2 ring-[var(--color-bg-surface)]',\n className\n )}\n style={{\n width: px,\n height: px,\n fontSize: Math.max(9, Math.round(px * 0.4)),\n background: gradientCss(seed),\n }}\n >\n <span aria-hidden=\"true\">{content}</span>\n </span>\n );\n};\n"],"mappings":";;;;AAcA,IAAM,IAAW;CAAE,IAAI;CAAI,IAAI;CAAI,IAAI;CAAI,IAAI;CAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"UserAvatar.js","names":[],"sources":["../../../src/atheletebridge/components/UserAvatar.tsx"],"sourcesContent":["/**\n * User avatar (monogram)\n * @module atheletebridge/components\n *\n * A circular avatar for a user / athlete / coach. There is no photo field on the\n * schema, so this is an honest initials monogram over a deterministic gradient\n * derived from the resolved name (or the id as a stable fallback). Semantic\n * tokens only.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport { gradientCss, initials } from '../utils/identity';\n\nconst SIZE_MAP = { xs: 24, sm: 32, md: 40, lg: 56, xl: 72 } as const;\n\nexport type UserAvatarSize = keyof typeof SIZE_MAP | number;\n\nexport interface UserAvatarProps {\n /** Resolved display name (drives initials + gradient seed). */\n name?: string | null;\n /** Stable id — used as the gradient seed and initials fallback when no name. */\n id?: string | null;\n /** Optional jersey number rendered instead of initials (roster context). */\n jersey?: number | null;\n /** Named size token or explicit pixel diameter. */\n size?: UserAvatarSize;\n /** Extra ring (used inside overlapping stacks / on colored surfaces). */\n ring?: boolean;\n className?: string;\n}\n\n/** Circular monogram avatar for a user/athlete/coach (initials or jersey #). */\nexport const UserAvatar: FC<UserAvatarProps> = ({\n name,\n id,\n jersey,\n size = 'md',\n ring = false,\n className,\n}) => {\n const px = typeof size === 'number' ? size : SIZE_MAP[size];\n const label = name?.trim() || (id ? `User ${id.slice(0, 6)}` : 'Unknown user');\n const seed = name?.trim() || id || label;\n const content =\n typeof jersey === 'number' && Number.isFinite(jersey) ? String(jersey) : initials(label);\n\n return (\n <span\n role=\"img\"\n aria-label={label}\n title={label}\n className={cn(\n 'inline-flex shrink-0 items-center justify-center rounded-full font-semibold leading-none text-[var(--color-action-primary-text)]',\n ring && 'ring-2 ring-[var(--color-bg-surface)]',\n className\n )}\n style={{\n width: px,\n height: px,\n fontSize: Math.max(9, Math.round(px * 0.4)),\n background: gradientCss(seed),\n }}\n >\n <span aria-hidden=\"true\">{content}</span>\n </span>\n );\n};\n"],"mappings":";;;;AAcA,IAAM,IAAW;CAAE,IAAI;CAAI,IAAI;CAAI,IAAI;CAAI,IAAI;CAAI,IAAI;AAAG,GAmB7C,KAAmC,EAC9C,SACA,OACA,WACA,UAAO,MACP,UAAO,IACP,mBACI;CACJ,IAAM,IAAK,OAAO,KAAS,WAAW,IAAO,EAAS,IAChD,IAAQ,GAAM,KAAK,MAAM,IAAK,QAAQ,EAAG,MAAM,GAAG,CAAC,MAAM,iBACzD,IAAO,GAAM,KAAK,KAAK,KAAM,GAC7B,IACJ,OAAO,KAAW,YAAY,OAAO,SAAS,CAAM,IAAI,OAAO,CAAM,IAAI,EAAS,CAAK;CAEzF,OACE,kBAAC,QAAD;EACE,MAAK;EACL,cAAY;EACZ,OAAO;EACP,WAAW,EACT,oIACA,KAAQ,yCACR,CACF;EACA,OAAO;GACL,OAAO;GACP,QAAQ;GACR,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,IAAK,EAAG,CAAC;GAC1C,YAAY,EAAY,CAAI;EAC9B;YAEA,kBAAC,QAAD;GAAM,eAAY;aAAQ;EAAc,CAAA;CACpC,CAAA;AAEV"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserSearchSelect.js","names":[],"sources":["../../../src/atheletebridge/components/UserSearchSelect.tsx"],"sourcesContent":["/**\n * User picker\n * @module atheletebridge/components\n *\n * Resolves a real workspace/tenant user by name or email (global user\n * directory) so a coach can add a team member without knowing a raw UUID.\n * Degrades gracefully: if the directory is unavailable the coach can still paste\n * a user id manually. Semantic tokens only.\n */\n\nimport { useState } from 'react';\nimport { Check, Loader2, Search, User as UserIcon, X } from 'lucide-react';\nimport { FormField, fieldInputClass } from './form';\nimport { cn } from '../utils/cn';\nimport { directoryUserName, useUserSearch, type DirectoryUser } from '../hooks/useAthleteBridgeApi';\n\nexport interface UserSelectValue {\n userId: string;\n displayName: string;\n}\n\ninterface UserSearchSelectProps {\n /** Currently selected user id. */\n userId: string;\n /** Display name currently chosen (echoed back so the parent can prefill). */\n displayName: string;\n onSelect: (value: UserSelectValue) => void;\n required?: boolean;\n}\n\nexport function UserSearchSelect({\n userId,\n displayName,\n onSelect,\n required = false,\n}: UserSearchSelectProps) {\n const [term, setTerm] = useState('');\n const [manual, setManual] = useState(false);\n\n const search = useUserSearch(term);\n const results = search.data ?? [];\n const active = term.trim().length >= 2;\n\n const pick = (user: DirectoryUser) => {\n onSelect({ userId: user.id, displayName: directoryUserName(user) });\n setTerm('');\n };\n\n // If the directory query fails (e.g. RBAC), offer manual entry automatically.\n const searchUnavailable = search.isError;\n\n if (manual || searchUnavailable) {\n return (\n <FormField\n label=\"User ID\"\n htmlFor=\"member-user-id\"\n required={required}\n hint={\n searchUnavailable\n ? 'User search is unavailable here — paste the auth-svc user id.'\n : 'Paste the auth-svc user id.'\n }\n >\n <input\n id=\"member-user-id\"\n className={fieldInputClass}\n value={userId}\n onChange={(e) => onSelect({ userId: e.target.value, displayName })}\n placeholder=\"user-uuid\"\n />\n {!searchUnavailable && (\n <button\n type=\"button\"\n onClick={() => setManual(false)}\n className=\"mt-1 self-start text-xs font-medium text-text-link hover:underline\"\n >\n Search the directory instead\n </button>\n )}\n </FormField>\n );\n }\n\n return (\n <FormField label=\"Athlete / user\" required={required}>\n {userId ? (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2\">\n <span className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-text-secondary\">\n <UserIcon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {displayName || 'Selected user'}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">{userId}</p>\n </div>\n <button\n type=\"button\"\n aria-label=\"Clear selected user\"\n onClick={() => onSelect({ userId: '', displayName: '' })}\n className=\"rounded-md p-1 text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n ) : (\n <div className=\"space-y-2\">\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n className={cn(fieldInputClass, 'pl-9')}\n value={term}\n onChange={(e) => setTerm(e.target.value)}\n placeholder=\"Search by name or email…\"\n aria-label=\"Search users by name or email\"\n />\n {search.isFetching && (\n <Loader2 className=\"absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 animate-spin text-text-secondary\" />\n )}\n </div>\n\n {active && !search.isFetching && results.length === 0 && (\n <p className=\"text-xs text-text-secondary\">No users match “{term.trim()}”.</p>\n )}\n\n {results.length > 0 && (\n <ul className=\"max-h-56 divide-y divide-border-seam overflow-auto rounded-lg border border-border-seam bg-bg-surface\">\n {results.map((user) => (\n <li key={user.id}>\n <button\n type=\"button\"\n onClick={() => pick(user)}\n className=\"flex w-full items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-bg-sunken\"\n >\n <span className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-text-secondary\">\n <UserIcon className=\"h-4 w-4\" />\n </span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">\n {directoryUserName(user)}\n </span>\n <span className=\"block truncate text-xs text-text-secondary\">\n {user.email}\n </span>\n </span>\n <Check className=\"h-4 w-4 shrink-0 text-text-secondary opacity-0\" />\n </button>\n </li>\n ))}\n </ul>\n )}\n\n <button\n type=\"button\"\n onClick={() => setManual(true)}\n className=\"text-xs font-medium text-text-link hover:underline\"\n >\n Enter a user ID manually\n </button>\n </div>\n )}\n </FormField>\n );\n}\n"],"mappings":";;;;;;;AA8BA,SAAgB,EAAiB,EAC/B,WACA,gBACA,aACA,cAAW,MACa;CACxB,IAAM,CAAC,GAAM,KAAW,EAAS,
|
|
1
|
+
{"version":3,"file":"UserSearchSelect.js","names":[],"sources":["../../../src/atheletebridge/components/UserSearchSelect.tsx"],"sourcesContent":["/**\n * User picker\n * @module atheletebridge/components\n *\n * Resolves a real workspace/tenant user by name or email (global user\n * directory) so a coach can add a team member without knowing a raw UUID.\n * Degrades gracefully: if the directory is unavailable the coach can still paste\n * a user id manually. Semantic tokens only.\n */\n\nimport { useState } from 'react';\nimport { Check, Loader2, Search, User as UserIcon, X } from 'lucide-react';\nimport { FormField, fieldInputClass } from './form';\nimport { cn } from '../utils/cn';\nimport { directoryUserName, useUserSearch, type DirectoryUser } from '../hooks/useAthleteBridgeApi';\n\nexport interface UserSelectValue {\n userId: string;\n displayName: string;\n}\n\ninterface UserSearchSelectProps {\n /** Currently selected user id. */\n userId: string;\n /** Display name currently chosen (echoed back so the parent can prefill). */\n displayName: string;\n onSelect: (value: UserSelectValue) => void;\n required?: boolean;\n}\n\nexport function UserSearchSelect({\n userId,\n displayName,\n onSelect,\n required = false,\n}: UserSearchSelectProps) {\n const [term, setTerm] = useState('');\n const [manual, setManual] = useState(false);\n\n const search = useUserSearch(term);\n const results = search.data ?? [];\n const active = term.trim().length >= 2;\n\n const pick = (user: DirectoryUser) => {\n onSelect({ userId: user.id, displayName: directoryUserName(user) });\n setTerm('');\n };\n\n // If the directory query fails (e.g. RBAC), offer manual entry automatically.\n const searchUnavailable = search.isError;\n\n if (manual || searchUnavailable) {\n return (\n <FormField\n label=\"User ID\"\n htmlFor=\"member-user-id\"\n required={required}\n hint={\n searchUnavailable\n ? 'User search is unavailable here — paste the auth-svc user id.'\n : 'Paste the auth-svc user id.'\n }\n >\n <input\n id=\"member-user-id\"\n className={fieldInputClass}\n value={userId}\n onChange={(e) => onSelect({ userId: e.target.value, displayName })}\n placeholder=\"user-uuid\"\n />\n {!searchUnavailable && (\n <button\n type=\"button\"\n onClick={() => setManual(false)}\n className=\"mt-1 self-start text-xs font-medium text-text-link hover:underline\"\n >\n Search the directory instead\n </button>\n )}\n </FormField>\n );\n }\n\n return (\n <FormField label=\"Athlete / user\" required={required}>\n {userId ? (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2\">\n <span className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-text-secondary\">\n <UserIcon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {displayName || 'Selected user'}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">{userId}</p>\n </div>\n <button\n type=\"button\"\n aria-label=\"Clear selected user\"\n onClick={() => onSelect({ userId: '', displayName: '' })}\n className=\"rounded-md p-1 text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n ) : (\n <div className=\"space-y-2\">\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n className={cn(fieldInputClass, 'pl-9')}\n value={term}\n onChange={(e) => setTerm(e.target.value)}\n placeholder=\"Search by name or email…\"\n aria-label=\"Search users by name or email\"\n />\n {search.isFetching && (\n <Loader2 className=\"absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 animate-spin text-text-secondary\" />\n )}\n </div>\n\n {active && !search.isFetching && results.length === 0 && (\n <p className=\"text-xs text-text-secondary\">No users match “{term.trim()}”.</p>\n )}\n\n {results.length > 0 && (\n <ul className=\"max-h-56 divide-y divide-border-seam overflow-auto rounded-lg border border-border-seam bg-bg-surface\">\n {results.map((user) => (\n <li key={user.id}>\n <button\n type=\"button\"\n onClick={() => pick(user)}\n className=\"flex w-full items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-bg-sunken\"\n >\n <span className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-text-secondary\">\n <UserIcon className=\"h-4 w-4\" />\n </span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">\n {directoryUserName(user)}\n </span>\n <span className=\"block truncate text-xs text-text-secondary\">\n {user.email}\n </span>\n </span>\n <Check className=\"h-4 w-4 shrink-0 text-text-secondary opacity-0\" />\n </button>\n </li>\n ))}\n </ul>\n )}\n\n <button\n type=\"button\"\n onClick={() => setManual(true)}\n className=\"text-xs font-medium text-text-link hover:underline\"\n >\n Enter a user ID manually\n </button>\n </div>\n )}\n </FormField>\n );\n}\n"],"mappings":";;;;;;;AA8BA,SAAgB,EAAiB,EAC/B,WACA,gBACA,aACA,cAAW,MACa;CACxB,IAAM,CAAC,GAAM,KAAW,EAAS,EAAE,GAC7B,CAAC,GAAQ,KAAa,EAAS,EAAK,GAEpC,IAAS,EAAc,CAAI,GAC3B,IAAU,EAAO,QAAQ,CAAC,GAC1B,IAAS,EAAK,KAAK,EAAE,UAAU,GAE/B,KAAQ,MAAwB;EAEpC,AADA,EAAS;GAAE,QAAQ,EAAK;GAAI,aAAa,EAAkB,CAAI;EAAE,CAAC,GAClE,EAAQ,EAAE;CACZ,GAGM,IAAoB,EAAO;CAkCjC,OAhCI,KAAU,IAEV,kBAAC,GAAD;EACE,OAAM;EACN,SAAQ;EACE;EACV,MACE,IACI,kEACA;YAPR,CAUE,kBAAC,SAAD;GACE,IAAG;GACH,WAAW;GACX,OAAO;GACP,WAAW,MAAM,EAAS;IAAE,QAAQ,EAAE,OAAO;IAAO;GAAY,CAAC;GACjE,aAAY;EACb,CAAA,GACA,CAAC,KACA,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAU,EAAK;GAC9B,WAAU;aACX;EAEO,CAAA,CAED;MAKb,kBAAC,GAAD;EAAW,OAAM;EAA2B;YACzC,IACC,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,QAAD;KAAM,WAAU;eACd,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA;IAC3B,CAAA;IACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBACV,KAAe;KACf,CAAA,GACH,kBAAC,KAAD;MAAG,WAAU;gBAAwC;KAAU,CAAA,CAC5D;;IACL,kBAAC,UAAD;KACE,MAAK;KACL,cAAW;KACX,eAAe,EAAS;MAAE,QAAQ;MAAI,aAAa;KAAG,CAAC;KACvD,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;IAClB,CAAA;GACL;OAEL,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD,EAAQ,WAAU,2FAA4F,CAAA;MAC9G,kBAAC,SAAD;OACE,WAAW,EAAG,GAAiB,MAAM;OACrC,OAAO;OACP,WAAW,MAAM,EAAQ,EAAE,OAAO,KAAK;OACvC,aAAY;OACZ,cAAW;MACZ,CAAA;MACA,EAAO,cACN,kBAAC,GAAD,EAAS,WAAU,qFAAsF,CAAA;KAExG;;IAEJ,KAAU,CAAC,EAAO,cAAc,EAAQ,WAAW,KAClD,kBAAC,KAAD;KAAG,WAAU;eAAb;MAA2C;MAAiB,EAAK,KAAK;MAAE;KAAK;;IAG9E,EAAQ,SAAS,KAChB,kBAAC,MAAD;KAAI,WAAU;eACX,EAAQ,KAAK,MACZ,kBAAC,MAAD,EAAA,UACE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAK,CAAI;MACxB,WAAU;gBAHZ;OAKE,kBAAC,QAAD;QAAM,WAAU;kBACd,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA;OAC3B,CAAA;OACN,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAkB,CAAI;QACnB,CAAA,GACN,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAK;QACF,CAAA,CACF;;OACN,kBAAC,GAAD,EAAO,WAAU,iDAAkD,CAAA;MAC7D;QACN,GAnBK,EAAK,EAmBV,CACL;IACC,CAAA;IAGN,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAU,EAAI;KAC7B,WAAU;eACX;IAEO,CAAA;GACL;;CAEE,CAAA;AAEf"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface AvailabilityRosterPanelProps {
|
|
2
|
+
teamId: string;
|
|
3
|
+
fixtureId?: string;
|
|
4
|
+
sessionId?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function AvailabilityRosterPanel({ teamId, fixtureId, sessionId, className, }: AvailabilityRosterPanelProps): import("react").JSX.Element | null;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=AvailabilityRosterPanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvailabilityRosterPanel.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/components/availability/AvailabilityRosterPanel.tsx"],"names":[],"mappings":"AAwBA,UAAU,4BAA4B;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,SAAS,EACT,SAAS,EACT,SAAS,GACV,EAAE,4BAA4B,sCAyG9B"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useAthleteBridge as e } from "../../context/AthleteBridgeContext.js";
|
|
2
|
+
import { cn as t } from "../../utils/cn.js";
|
|
3
|
+
import { MEMBER_ROLE_LABELS as n } from "../domain.js";
|
|
4
|
+
import { useAvailability as r, useRemindAvailability as i, useTeamMembers as a } from "../../hooks/useAthleteBridgeApi.js";
|
|
5
|
+
import { useMemo as o, useState as s } from "react";
|
|
6
|
+
import { Bell as c, Loader2 as l, Users as u } from "lucide-react";
|
|
7
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
8
|
+
import { GlassCard as p } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
//#region src/atheletebridge/components/availability/AvailabilityRosterPanel.tsx
|
|
10
|
+
var m = new Set([
|
|
11
|
+
"COACH",
|
|
12
|
+
"ASSISTANT_COACH",
|
|
13
|
+
"MANAGER"
|
|
14
|
+
]), h = {
|
|
15
|
+
YES: "bg-status-success-bg-subtle text-status-success-text",
|
|
16
|
+
NO: "bg-status-error-bg-subtle text-status-error-text",
|
|
17
|
+
MAYBE: "bg-status-warning-bg-subtle text-status-warning-text",
|
|
18
|
+
NO_RESPONSE: "bg-bg-sunken text-text-secondary"
|
|
19
|
+
}, g = {
|
|
20
|
+
YES: "Yes",
|
|
21
|
+
NO: "No",
|
|
22
|
+
MAYBE: "Maybe",
|
|
23
|
+
NO_RESPONSE: "Pending"
|
|
24
|
+
};
|
|
25
|
+
function _({ teamId: _, fixtureId: v, sessionId: y, className: b }) {
|
|
26
|
+
let { currentUser: x } = e(), S = a(_, "ACTIVE"), C = r({
|
|
27
|
+
fixtureId: v,
|
|
28
|
+
sessionId: y
|
|
29
|
+
}), w = i(), [T, E] = s(null), [D, O] = s(null), k = o(() => S.data?.find((e) => e.userId === x?.id) ?? null, [S.data, x?.id]), A = o(() => {
|
|
30
|
+
let e = new Map(C.data?.map((e) => [e.memberId, e]) ?? []);
|
|
31
|
+
return (S.data ?? []).map((t) => ({
|
|
32
|
+
member: t,
|
|
33
|
+
entry: e.get(t.id) ?? null
|
|
34
|
+
})).sort((e, t) => e.member.displayName.localeCompare(t.member.displayName));
|
|
35
|
+
}, [S.data, C.data]), j = A.filter((e) => !e.entry || e.entry.response === "NO_RESPONSE").length;
|
|
36
|
+
if (!k || !m.has(k.role)) return null;
|
|
37
|
+
let M = async () => {
|
|
38
|
+
O(null), E(null);
|
|
39
|
+
try {
|
|
40
|
+
let e = await w.mutateAsync({
|
|
41
|
+
fixtureId: v,
|
|
42
|
+
sessionId: y
|
|
43
|
+
});
|
|
44
|
+
E(e > 0 ? `Reminder queued for ${e} pending ${e === 1 ? "member" : "members"}.` : "Everyone has already responded.");
|
|
45
|
+
} catch (e) {
|
|
46
|
+
O(e instanceof Error ? e.message : "Failed to queue reminder.");
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
return /* @__PURE__ */ f(p, {
|
|
50
|
+
treatment: "glass",
|
|
51
|
+
className: t("p-5 sm:p-6", b),
|
|
52
|
+
children: [
|
|
53
|
+
/* @__PURE__ */ f("div", {
|
|
54
|
+
className: "mb-4 flex flex-wrap items-center justify-between gap-3",
|
|
55
|
+
children: [/* @__PURE__ */ f("h2", {
|
|
56
|
+
className: "flex items-center gap-2 text-sm font-semibold text-text-primary",
|
|
57
|
+
children: [/* @__PURE__ */ d(u, {
|
|
58
|
+
className: "h-4 w-4",
|
|
59
|
+
"aria-hidden": "true"
|
|
60
|
+
}), " Squad availability"]
|
|
61
|
+
}), /* @__PURE__ */ f("button", {
|
|
62
|
+
type: "button",
|
|
63
|
+
onClick: () => void M(),
|
|
64
|
+
disabled: w.isPending || j === 0,
|
|
65
|
+
className: "inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-xs font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50",
|
|
66
|
+
children: [
|
|
67
|
+
w.isPending ? /* @__PURE__ */ d(l, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ d(c, {
|
|
68
|
+
className: "h-3.5 w-3.5",
|
|
69
|
+
"aria-hidden": "true"
|
|
70
|
+
}),
|
|
71
|
+
"Remind ",
|
|
72
|
+
j > 0 ? `(${j})` : ""
|
|
73
|
+
]
|
|
74
|
+
})]
|
|
75
|
+
}),
|
|
76
|
+
S.isLoading || C.isLoading ? /* @__PURE__ */ f("div", {
|
|
77
|
+
className: "flex items-center gap-2 text-sm text-text-secondary",
|
|
78
|
+
children: [/* @__PURE__ */ d(l, { className: "h-4 w-4 animate-spin" }), " Loading roster…"]
|
|
79
|
+
}) : A.length === 0 ? /* @__PURE__ */ d("p", {
|
|
80
|
+
className: "text-sm text-text-secondary",
|
|
81
|
+
children: "No active roster members yet."
|
|
82
|
+
}) : /* @__PURE__ */ d("ul", {
|
|
83
|
+
className: "divide-y divide-border-seam",
|
|
84
|
+
children: A.map(({ member: e, entry: r }) => {
|
|
85
|
+
let i = r?.response ?? "NO_RESPONSE";
|
|
86
|
+
return /* @__PURE__ */ f("li", {
|
|
87
|
+
className: "flex items-center justify-between gap-3 py-2.5",
|
|
88
|
+
children: [/* @__PURE__ */ f("div", {
|
|
89
|
+
className: "min-w-0",
|
|
90
|
+
children: [/* @__PURE__ */ d("p", {
|
|
91
|
+
className: "truncate text-sm font-medium text-text-primary",
|
|
92
|
+
children: e.displayName
|
|
93
|
+
}), /* @__PURE__ */ d("p", {
|
|
94
|
+
className: "text-xs text-text-muted",
|
|
95
|
+
children: n[e.role]
|
|
96
|
+
})]
|
|
97
|
+
}), /* @__PURE__ */ d("span", {
|
|
98
|
+
className: t("shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium", h[i]),
|
|
99
|
+
children: g[i]
|
|
100
|
+
})]
|
|
101
|
+
}, e.id);
|
|
102
|
+
})
|
|
103
|
+
}),
|
|
104
|
+
T && /* @__PURE__ */ d("p", {
|
|
105
|
+
className: "mt-3 text-sm text-status-success-text",
|
|
106
|
+
children: T
|
|
107
|
+
}),
|
|
108
|
+
D && /* @__PURE__ */ d("p", {
|
|
109
|
+
className: "mt-3 text-sm text-status-error-text",
|
|
110
|
+
children: D
|
|
111
|
+
})
|
|
112
|
+
]
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
export { _ as AvailabilityRosterPanel };
|
|
117
|
+
|
|
118
|
+
//# sourceMappingURL=AvailabilityRosterPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvailabilityRosterPanel.js","names":[],"sources":["../../../../src/atheletebridge/components/availability/AvailabilityRosterPanel.tsx"],"sourcesContent":["/**\n * Availability Roster Panel\n * @module atheletebridge/components/availability\n *\n * Coach/manager-facing view of who has responded to a fixture (or session)\n * invitation, with a reminder trigger for anyone who hasn't (BOFF-4134).\n *\n * Only rendered for the signed-in user's coaching roles on this team (COACH,\n * ASSISTANT_COACH, MANAGER) — players see their own RSVP via\n * `AvailabilityRsvpControl` instead, not the whole squad's responses.\n */\nimport { useMemo, useState } from 'react';\nimport { Bell, Loader2, Users } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport {\n useAvailability,\n useRemindAvailability,\n useTeamMembers,\n} from '../../hooks/useAthleteBridgeApi';\nimport { MEMBER_ROLE_LABELS } from '../domain';\nimport type { AthAvailabilityResponse, AthMemberRole } from '../../types';\nimport { cn } from '../../utils/cn';\n\ninterface AvailabilityRosterPanelProps {\n teamId: string;\n fixtureId?: string;\n sessionId?: string;\n className?: string;\n}\n\nconst COACHING_ROLES: ReadonlySet<AthMemberRole> = new Set(['COACH', 'ASSISTANT_COACH', 'MANAGER']);\n\nconst RESPONSE_BADGE_CLASS: Record<AthAvailabilityResponse, string> = {\n YES: 'bg-status-success-bg-subtle text-status-success-text',\n NO: 'bg-status-error-bg-subtle text-status-error-text',\n MAYBE: 'bg-status-warning-bg-subtle text-status-warning-text',\n NO_RESPONSE: 'bg-bg-sunken text-text-secondary',\n};\n\nconst RESPONSE_LABEL: Record<AthAvailabilityResponse, string> = {\n YES: 'Yes',\n NO: 'No',\n MAYBE: 'Maybe',\n NO_RESPONSE: 'Pending',\n};\n\nexport function AvailabilityRosterPanel({\n teamId,\n fixtureId,\n sessionId,\n className,\n}: AvailabilityRosterPanelProps) {\n const { currentUser } = useAthleteBridge();\n const members = useTeamMembers(teamId, 'ACTIVE');\n const availability = useAvailability({ fixtureId, sessionId });\n const remind = useRemindAvailability();\n const [reminderMessage, setReminderMessage] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const myMember = useMemo(\n () => members.data?.find((m) => m.userId === currentUser?.id) ?? null,\n [members.data, currentUser?.id]\n );\n\n const rows = useMemo(() => {\n const responseByMemberId = new Map(availability.data?.map((a) => [a.memberId, a]) ?? []);\n return (members.data ?? [])\n .map((member) => ({\n member,\n entry: responseByMemberId.get(member.id) ?? null,\n }))\n .sort((a, b) => a.member.displayName.localeCompare(b.member.displayName));\n }, [members.data, availability.data]);\n\n const pendingCount = rows.filter((r) => !r.entry || r.entry.response === 'NO_RESPONSE').length;\n\n // Coach/manager gate — render nothing for players/captains/physios, and\n // nothing until we know the caller's role on this team.\n if (!myMember || !COACHING_ROLES.has(myMember.role)) {\n return null;\n }\n\n const handleRemind = async () => {\n setError(null);\n setReminderMessage(null);\n try {\n const count = await remind.mutateAsync({ fixtureId, sessionId });\n setReminderMessage(\n count > 0\n ? `Reminder queued for ${count} pending ${count === 1 ? 'member' : 'members'}.`\n : 'Everyone has already responded.'\n );\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to queue reminder.');\n }\n };\n\n return (\n <GlassCard treatment=\"glass\" className={cn('p-5 sm:p-6', className)}>\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <h2 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Users className=\"h-4 w-4\" aria-hidden=\"true\" /> Squad availability\n </h2>\n <button\n type=\"button\"\n onClick={() => void handleRemind()}\n disabled={remind.isPending || pendingCount === 0}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-xs font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50\"\n >\n {remind.isPending ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n ) : (\n <Bell className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n )}\n Remind {pendingCount > 0 ? `(${pendingCount})` : ''}\n </button>\n </div>\n\n {members.isLoading || availability.isLoading ? (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" /> Loading roster…\n </div>\n ) : rows.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">No active roster members yet.</p>\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {rows.map(({ member, entry }) => {\n const response = entry?.response ?? 'NO_RESPONSE';\n return (\n <li key={member.id} className=\"flex items-center justify-between gap-3 py-2.5\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {member.displayName}\n </p>\n <p className=\"text-xs text-text-muted\">{MEMBER_ROLE_LABELS[member.role]}</p>\n </div>\n <span\n className={cn(\n 'shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium',\n RESPONSE_BADGE_CLASS[response]\n )}\n >\n {RESPONSE_LABEL[response]}\n </span>\n </li>\n );\n })}\n </ul>\n )}\n\n {reminderMessage && (\n <p className=\"mt-3 text-sm text-status-success-text\">{reminderMessage}</p>\n )}\n {error && <p className=\"mt-3 text-sm text-status-error-text\">{error}</p>}\n </GlassCard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface AvailabilityRsvpControlProps {
|
|
2
|
+
teamId: string;
|
|
3
|
+
fixtureId?: string;
|
|
4
|
+
sessionId?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function AvailabilityRsvpControl({ teamId, fixtureId, sessionId, className, }: AvailabilityRsvpControlProps): import("react").JSX.Element | null;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=AvailabilityRsvpControl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvailabilityRsvpControl.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/components/availability/AvailabilityRsvpControl.tsx"],"names":[],"mappings":"AAwBA,UAAU,4BAA4B;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA4BD,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,SAAS,EACT,SAAS,EACT,SAAS,GACV,EAAE,4BAA4B,sCAiF9B"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useAthleteBridge as e } from "../../context/AthleteBridgeContext.js";
|
|
2
|
+
import { cn as t } from "../../utils/cn.js";
|
|
3
|
+
import { useAvailability as n, useSetAvailability as r, useTeamMembers as i } from "../../hooks/useAthleteBridgeApi.js";
|
|
4
|
+
import { useMemo as a, useState as o } from "react";
|
|
5
|
+
import { CheckCircle2 as s, HelpCircle as c, Loader2 as l, XCircle as u } from "lucide-react";
|
|
6
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
7
|
+
import { GlassCard as p } from "@burdenoff/fe-libs/ui";
|
|
8
|
+
//#region src/atheletebridge/components/availability/AvailabilityRsvpControl.tsx
|
|
9
|
+
var m = [
|
|
10
|
+
{
|
|
11
|
+
value: "YES",
|
|
12
|
+
label: "I'm in",
|
|
13
|
+
icon: s,
|
|
14
|
+
activeClass: "bg-status-success-bg-subtle text-status-success-text ring-status-success-border"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
value: "MAYBE",
|
|
18
|
+
label: "Maybe",
|
|
19
|
+
icon: c,
|
|
20
|
+
activeClass: "bg-status-warning-bg-subtle text-status-warning-text ring-status-warning-border"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
value: "NO",
|
|
24
|
+
label: "Can't make it",
|
|
25
|
+
icon: u,
|
|
26
|
+
activeClass: "bg-status-error-bg-subtle text-status-error-text ring-status-error-border"
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
function h({ teamId: s, fixtureId: c, sessionId: u, className: h }) {
|
|
30
|
+
let { currentUser: g } = e(), _ = i(s), v = n({
|
|
31
|
+
fixtureId: c,
|
|
32
|
+
sessionId: u
|
|
33
|
+
}), y = r(), [b, x] = o(null), S = a(() => _.data?.find((e) => e.userId === g?.id) ?? null, [_.data, g?.id]), C = a(() => v.data?.find((e) => e.memberId === S?.id)?.response ?? "NO_RESPONSE", [v.data, S?.id]);
|
|
34
|
+
if (!g?.id || _.isSuccess && !S) return null;
|
|
35
|
+
let w = _.isLoading || v.isLoading, T = y.isPending, E = async (e) => {
|
|
36
|
+
if (S) {
|
|
37
|
+
x(null);
|
|
38
|
+
try {
|
|
39
|
+
await y.mutateAsync({
|
|
40
|
+
memberId: S.id,
|
|
41
|
+
fixtureId: c,
|
|
42
|
+
sessionId: u,
|
|
43
|
+
response: e
|
|
44
|
+
});
|
|
45
|
+
} catch (e) {
|
|
46
|
+
x(e instanceof Error ? e.message : "Failed to save your response.");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return /* @__PURE__ */ f(p, {
|
|
51
|
+
treatment: "metric",
|
|
52
|
+
className: t("p-4", h),
|
|
53
|
+
children: [
|
|
54
|
+
/* @__PURE__ */ d("p", {
|
|
55
|
+
className: "text-xs font-medium uppercase tracking-wide text-text-muted",
|
|
56
|
+
children: "Your availability"
|
|
57
|
+
}),
|
|
58
|
+
w ? /* @__PURE__ */ f("div", {
|
|
59
|
+
className: "mt-3 flex items-center gap-2 text-sm text-text-secondary",
|
|
60
|
+
children: [/* @__PURE__ */ d(l, { className: "h-4 w-4 animate-spin" }), " Loading…"]
|
|
61
|
+
}) : /* @__PURE__ */ d("div", {
|
|
62
|
+
className: "mt-3 flex flex-wrap gap-2",
|
|
63
|
+
children: m.map(({ value: e, label: n, icon: r, activeClass: i }) => {
|
|
64
|
+
let a = C === e;
|
|
65
|
+
return /* @__PURE__ */ f("button", {
|
|
66
|
+
type: "button",
|
|
67
|
+
disabled: T,
|
|
68
|
+
onClick: () => void E(e),
|
|
69
|
+
"aria-pressed": a,
|
|
70
|
+
className: t("inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium ring-1 ring-inset transition-colors disabled:opacity-60", a ? i : "bg-bg-sunken text-text-secondary ring-border-seam hover:bg-bg-surface"),
|
|
71
|
+
children: [T && y.variables?.response === e ? /* @__PURE__ */ d(l, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ d(r, {
|
|
72
|
+
className: "h-4 w-4",
|
|
73
|
+
"aria-hidden": "true"
|
|
74
|
+
}), n]
|
|
75
|
+
}, e);
|
|
76
|
+
})
|
|
77
|
+
}),
|
|
78
|
+
b && /* @__PURE__ */ d("p", {
|
|
79
|
+
className: "mt-2 text-sm text-status-error-text",
|
|
80
|
+
children: b
|
|
81
|
+
})
|
|
82
|
+
]
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { h as AvailabilityRsvpControl };
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=AvailabilityRsvpControl.js.map
|