@burdenoff/microfe-atheletebridge 2026.705.1 → 2026.706.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.
Files changed (103) hide show
  1. package/dist/atheletebridge/AthleteBridgeRoot.js +55 -39
  2. package/dist/atheletebridge/AthleteBridgeRoot.js.map +1 -1
  3. package/dist/atheletebridge/AthleteBridgeRoutes.js +18 -6
  4. package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
  5. package/dist/atheletebridge/components/Crest.js +70 -0
  6. package/dist/atheletebridge/components/Crest.js.map +1 -0
  7. package/dist/atheletebridge/components/FixtureCard.js +8 -72
  8. package/dist/atheletebridge/components/FixtureCard.js.map +1 -1
  9. package/dist/atheletebridge/components/MatchupRow.js +74 -0
  10. package/dist/atheletebridge/components/MatchupRow.js.map +1 -0
  11. package/dist/atheletebridge/components/MetricsCard.js +61 -21
  12. package/dist/atheletebridge/components/MetricsCard.js.map +1 -1
  13. package/dist/atheletebridge/components/ProgramCard.js +75 -43
  14. package/dist/atheletebridge/components/ProgramCard.js.map +1 -1
  15. package/dist/atheletebridge/components/Scoreboard.js +163 -0
  16. package/dist/atheletebridge/components/Scoreboard.js.map +1 -0
  17. package/dist/atheletebridge/components/SessionCard.js +64 -41
  18. package/dist/atheletebridge/components/SessionCard.js.map +1 -1
  19. package/dist/atheletebridge/components/SessionStatusTimeline.js +80 -0
  20. package/dist/atheletebridge/components/SessionStatusTimeline.js.map +1 -0
  21. package/dist/atheletebridge/components/StatusBadge.js +5 -5
  22. package/dist/atheletebridge/components/StatusBadge.js.map +1 -1
  23. package/dist/atheletebridge/components/TeamCard.js +65 -35
  24. package/dist/atheletebridge/components/TeamCard.js.map +1 -1
  25. package/dist/atheletebridge/components/UserAvatar.js +33 -0
  26. package/dist/atheletebridge/components/UserAvatar.js.map +1 -0
  27. package/dist/atheletebridge/components/domain.js +5 -2
  28. package/dist/atheletebridge/components/domain.js.map +1 -1
  29. package/dist/atheletebridge/components/viz/AvatarStack.js +2 -0
  30. package/dist/atheletebridge/components/viz/BarChart.js +83 -0
  31. package/dist/atheletebridge/components/viz/BarChart.js.map +1 -0
  32. package/dist/atheletebridge/components/viz/DonutChart.js +109 -0
  33. package/dist/atheletebridge/components/viz/DonutChart.js.map +1 -0
  34. package/dist/atheletebridge/components/viz/LineChart.js +123 -0
  35. package/dist/atheletebridge/components/viz/LineChart.js.map +1 -0
  36. package/dist/atheletebridge/components/viz/ProgressRing.js +6 -0
  37. package/dist/atheletebridge/components/viz/ProgressRing.js.map +1 -0
  38. package/dist/atheletebridge/components/viz/RatingStars.js +92 -0
  39. package/dist/atheletebridge/components/viz/RatingStars.js.map +1 -0
  40. package/dist/atheletebridge/components/viz/Sparkline.js +74 -0
  41. package/dist/atheletebridge/components/viz/Sparkline.js.map +1 -0
  42. package/dist/atheletebridge/components/viz/StatCard.js +60 -0
  43. package/dist/atheletebridge/components/viz/StatCard.js.map +1 -0
  44. package/dist/atheletebridge/components/viz/StepProgress.js +3 -0
  45. package/dist/atheletebridge/components/viz/Timeline.js +66 -0
  46. package/dist/atheletebridge/components/viz/Timeline.js.map +1 -0
  47. package/dist/atheletebridge/components/viz/index.js +13 -0
  48. package/dist/atheletebridge/components/viz/lib/Avatar.js +1 -0
  49. package/dist/atheletebridge/components/viz/lib/CartesianFrame.js +119 -0
  50. package/dist/atheletebridge/components/viz/lib/CartesianFrame.js.map +1 -0
  51. package/dist/atheletebridge/components/viz/lib/ResponsiveSvg.js +35 -0
  52. package/dist/atheletebridge/components/viz/lib/ResponsiveSvg.js.map +1 -0
  53. package/dist/atheletebridge/components/viz/lib/format.js +17 -0
  54. package/dist/atheletebridge/components/viz/lib/format.js.map +1 -0
  55. package/dist/atheletebridge/components/viz/lib/geometry.js +61 -0
  56. package/dist/atheletebridge/components/viz/lib/geometry.js.map +1 -0
  57. package/dist/atheletebridge/components/viz/lib/hooks.js +39 -0
  58. package/dist/atheletebridge/components/viz/lib/hooks.js.map +1 -0
  59. package/dist/atheletebridge/components/viz/lib/palette.js +27 -0
  60. package/dist/atheletebridge/components/viz/lib/palette.js.map +1 -0
  61. package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +369 -163
  62. package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
  63. package/dist/atheletebridge/pages/DashboardPage.js +182 -117
  64. package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
  65. package/dist/atheletebridge/pages/StandingsPage.js +119 -95
  66. package/dist/atheletebridge/pages/StandingsPage.js.map +1 -1
  67. package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js +372 -0
  68. package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js.map +1 -0
  69. package/dist/atheletebridge/pages/athletes/AthleteProfilePage.js +511 -0
  70. package/dist/atheletebridge/pages/athletes/AthleteProfilePage.js.map +1 -0
  71. package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js +120 -131
  72. package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js.map +1 -1
  73. package/dist/atheletebridge/pages/fixtures/FixtureFormPage.js +264 -176
  74. package/dist/atheletebridge/pages/fixtures/FixtureFormPage.js.map +1 -1
  75. package/dist/atheletebridge/pages/fixtures/FixturesListPage.js +84 -47
  76. package/dist/atheletebridge/pages/fixtures/FixturesListPage.js.map +1 -1
  77. package/dist/atheletebridge/pages/performance/PerformancePage.js +435 -284
  78. package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
  79. package/dist/atheletebridge/pages/programs/ProgramDetailPage.js +188 -124
  80. package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
  81. package/dist/atheletebridge/pages/programs/ProgramsListPage.js +56 -39
  82. package/dist/atheletebridge/pages/programs/ProgramsListPage.js.map +1 -1
  83. package/dist/atheletebridge/pages/sessions/SessionDetailPage.js +268 -139
  84. package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
  85. package/dist/atheletebridge/pages/sessions/SessionFormPage.js +254 -162
  86. package/dist/atheletebridge/pages/sessions/SessionFormPage.js.map +1 -1
  87. package/dist/atheletebridge/pages/sessions/SessionsListPage.js +38 -36
  88. package/dist/atheletebridge/pages/sessions/SessionsListPage.js.map +1 -1
  89. package/dist/atheletebridge/pages/teams/TeamDetailPage.js +437 -189
  90. package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
  91. package/dist/atheletebridge/pages/teams/TeamFormPage.js +368 -177
  92. package/dist/atheletebridge/pages/teams/TeamFormPage.js.map +1 -1
  93. package/dist/atheletebridge/pages/teams/TeamsListPage.js +3 -0
  94. package/dist/atheletebridge/pages/teams/TeamsListPage.js.map +1 -1
  95. package/dist/atheletebridge/pages/tournaments/TournamentBracketPage.js +298 -0
  96. package/dist/atheletebridge/pages/tournaments/TournamentBracketPage.js.map +1 -0
  97. package/dist/atheletebridge/utils/identity.js +31 -0
  98. package/dist/atheletebridge/utils/identity.js.map +1 -0
  99. package/dist/atheletebridge/utils/timeAgo.js +20 -0
  100. package/dist/atheletebridge/utils/timeAgo.js.map +1 -0
  101. package/dist/atheletebridge/utils/url.js +17 -0
  102. package/dist/atheletebridge/utils/url.js.map +1 -0
  103. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"TeamFormPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/teams/TeamFormPage.tsx"],"sourcesContent":["/**\n * Team Create / Edit Form\n * @module atheletebridge/pages/teams\n */\n\nimport { useEffect, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, Loader2, Save } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { SPORT_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useTeam, useCreateTeam, useUpdateTeam } from '../../hooks/useAthleteBridgeApi';\nimport type { AthSport, AthTeamStatus } from '../../types';\n\nconst SPORTS = Object.keys(SPORT_LABELS) as AthSport[];\nconst STATUSES: AthTeamStatus[] = ['FORMING', 'ACTIVE', 'INACTIVE', 'ARCHIVED'];\n\ninterface TeamFormState {\n name: string;\n sport: AthSport;\n status: AthTeamStatus;\n description: string;\n ageGroup: string;\n season: string;\n homeVenue: string;\n tagsText: string;\n}\n\nconst EMPTY_FORM: TeamFormState = {\n name: '',\n sport: 'OTHER',\n status: 'FORMING',\n description: '',\n ageGroup: '',\n season: '',\n homeVenue: '',\n tagsText: '',\n};\n\nexport function TeamFormPage() {\n const { id } = useParams<{ id: string }>();\n const isEdit = Boolean(id);\n const { navigateTo } = useAthleteBridge();\n\n const existing = useTeam(id);\n const createTeam = useCreateTeam();\n const updateTeam = useUpdateTeam();\n\n const [form, setForm] = useState<TeamFormState>(EMPTY_FORM);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const team = existing.data;\n if (isEdit && team) {\n setForm({\n name: team.name,\n sport: team.sport,\n status: team.status,\n description: team.description ?? '',\n ageGroup: team.ageGroup ?? '',\n season: team.season ?? '',\n homeVenue: team.homeVenue ?? '',\n tagsText: team.tags.join(', '),\n });\n }\n }, [isEdit, existing.data]);\n\n const set = <K extends keyof TeamFormState>(key: K, value: TeamFormState[K]) =>\n setForm((prev) => ({ ...prev, [key]: value }));\n\n const parseTags = (text: string): string[] =>\n text\n .split(',')\n .map((t) => t.trim())\n .filter(Boolean);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!form.name.trim()) {\n setError('Team name is required.');\n return;\n }\n\n const tags = parseTags(form.tagsText);\n\n try {\n if (isEdit && id) {\n await updateTeam.mutateAsync({\n id,\n input: {\n name: form.name.trim(),\n sport: form.sport,\n status: form.status,\n description: form.description.trim() || undefined,\n ageGroup: form.ageGroup.trim() || undefined,\n season: form.season.trim() || undefined,\n homeVenue: form.homeVenue.trim() || undefined,\n tags,\n },\n });\n navigateTo(`/teams/${id}`);\n } else {\n const created = await createTeam.mutateAsync({\n name: form.name.trim(),\n sport: form.sport,\n status: form.status,\n description: form.description.trim() || undefined,\n ageGroup: form.ageGroup.trim() || undefined,\n season: form.season.trim() || undefined,\n homeVenue: form.homeVenue.trim() || undefined,\n tags,\n });\n navigateTo(`/teams/${created.id}`);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to save team.');\n }\n };\n\n const saving = createTeam.isPending || updateTeam.isPending;\n\n if (isEdit && existing.isLoading) {\n return (\n <PageLayout title=\"Edit team\">\n <LoadingState label=\"Loading team…\" />\n </PageLayout>\n );\n }\n\n if (isEdit && (existing.isError || (!existing.isLoading && !existing.data))) {\n return (\n <PageLayout title=\"Edit team\">\n <ErrorState\n title=\"Team not found\"\n message=\"This team does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/teams')}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={isEdit ? 'Edit team' : 'New team'}\n description={isEdit ? 'Update the squad details.' : 'Create a new squad to build a roster.'}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && id ? `/teams/${id}` : '/teams')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"max-w-2xl space-y-5\">\n <FormField label=\"Team name\" htmlFor=\"name\" required>\n <input\n id=\"name\"\n className={fieldInputClass}\n value={form.name}\n onChange={(e) => set('name', e.target.value)}\n placeholder=\"e.g. Riverside United U18\"\n />\n </FormField>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Sport\" htmlFor=\"sport\">\n <select\n id=\"sport\"\n className={fieldInputClass}\n value={form.sport}\n onChange={(e) => set('sport', e.target.value as AthSport)}\n >\n {SPORTS.map((s) => (\n <option key={s} value={s}>\n {SPORT_LABELS[s]}\n </option>\n ))}\n </select>\n </FormField>\n\n <FormField label=\"Status\" htmlFor=\"status\">\n <select\n id=\"status\"\n className={fieldInputClass}\n value={form.status}\n onChange={(e) => set('status', e.target.value as AthTeamStatus)}\n >\n {STATUSES.map((s) => (\n <option key={s} value={s}>\n {s.charAt(0) + s.slice(1).toLowerCase()}\n </option>\n ))}\n </select>\n </FormField>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Age group\" htmlFor=\"ageGroup\">\n <input\n id=\"ageGroup\"\n className={fieldInputClass}\n value={form.ageGroup}\n onChange={(e) => set('ageGroup', e.target.value)}\n placeholder=\"e.g. U18\"\n />\n </FormField>\n\n <FormField label=\"Season\" htmlFor=\"season\">\n <input\n id=\"season\"\n className={fieldInputClass}\n value={form.season}\n onChange={(e) => set('season', e.target.value)}\n placeholder=\"e.g. 2025/26\"\n />\n </FormField>\n </div>\n\n <FormField label=\"Home venue\" htmlFor=\"homeVenue\">\n <input\n id=\"homeVenue\"\n className={fieldInputClass}\n value={form.homeVenue}\n onChange={(e) => set('homeVenue', e.target.value)}\n placeholder=\"e.g. Riverside Stadium\"\n />\n </FormField>\n\n <FormField label=\"Description\" htmlFor=\"description\">\n <textarea\n id=\"description\"\n className={fieldInputClass}\n rows={3}\n value={form.description}\n onChange={(e) => set('description', e.target.value)}\n placeholder=\"A short description of the team.\"\n />\n </FormField>\n\n <FormField\n label=\"Tags\"\n htmlFor=\"tags\"\n hint=\"Comma-separated, e.g. youth, development, first-team.\"\n >\n <input\n id=\"tags\"\n className={fieldInputClass}\n value={form.tagsText}\n onChange={(e) => set('tagsText', e.target.value)}\n placeholder=\"youth, development\"\n />\n </FormField>\n\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n\n <div className=\"flex items-center gap-3 pt-2\">\n <button\n type=\"submit\"\n disabled={saving}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {saving ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Save className=\"h-4 w-4\" />}\n {isEdit ? 'Save changes' : 'Create team'}\n </button>\n </div>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;AAgBA,IAAM,IAAS,OAAO,KAAK,EAAa,EAClC,IAA4B;CAAC;CAAW;CAAU;CAAY;CAAW,EAazE,IAA4B;CAChC,MAAM;CACN,OAAO;CACP,QAAQ;CACR,aAAa;CACb,UAAU;CACV,QAAQ;CACR,WAAW;CACX,UAAU;CACX;AAED,SAAgB,IAAe;CAC7B,IAAM,EAAE,UAAO,GAA2B,EACpC,IAAS,EAAQ,GACjB,EAAE,kBAAe,GAAkB,EAEnC,IAAW,EAAQ,EAAG,EACtB,IAAa,GAAe,EAC5B,IAAa,GAAe,EAE5B,CAAC,GAAM,KAAW,EAAwB,EAAW,EACrD,CAAC,GAAO,KAAY,EAAwB,KAAK;AAEvD,SAAgB;EACd,IAAM,IAAO,EAAS;AACtB,EAAI,KAAU,KACZ,EAAQ;GACN,MAAM,EAAK;GACX,OAAO,EAAK;GACZ,QAAQ,EAAK;GACb,aAAa,EAAK,eAAe;GACjC,UAAU,EAAK,YAAY;GAC3B,QAAQ,EAAK,UAAU;GACvB,WAAW,EAAK,aAAa;GAC7B,UAAU,EAAK,KAAK,KAAK,KAAK;GAC/B,CAAC;IAEH,CAAC,GAAQ,EAAS,KAAK,CAAC;CAE3B,IAAM,KAAsC,GAAQ,MAClD,GAAS,OAAU;EAAE,GAAG;GAAO,IAAM;EAAO,EAAE,EAE1C,KAAa,MACjB,EACG,MAAM,IAAI,CACV,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,OAAO,QAAQ,EAEd,IAAe,OAAO,MAAuB;AAIjD,MAHA,EAAE,gBAAgB,EAClB,EAAS,KAAK,EAEV,CAAC,EAAK,KAAK,MAAM,EAAE;AACrB,KAAS,yBAAyB;AAClC;;EAGF,IAAM,IAAO,EAAU,EAAK,SAAS;AAErC,MAAI;AACF,GAAI,KAAU,KACZ,MAAM,EAAW,YAAY;IAC3B;IACA,OAAO;KACL,MAAM,EAAK,KAAK,MAAM;KACtB,OAAO,EAAK;KACZ,QAAQ,EAAK;KACb,aAAa,EAAK,YAAY,MAAM,IAAI,KAAA;KACxC,UAAU,EAAK,SAAS,MAAM,IAAI,KAAA;KAClC,QAAQ,EAAK,OAAO,MAAM,IAAI,KAAA;KAC9B,WAAW,EAAK,UAAU,MAAM,IAAI,KAAA;KACpC;KACD;IACF,CAAC,EACF,EAAW,UAAU,IAAK,IAY1B,EAAW,WAVK,MAAM,EAAW,YAAY;IAC3C,MAAM,EAAK,KAAK,MAAM;IACtB,OAAO,EAAK;IACZ,QAAQ,EAAK;IACb,aAAa,EAAK,YAAY,MAAM,IAAI,KAAA;IACxC,UAAU,EAAK,SAAS,MAAM,IAAI,KAAA;IAClC,QAAQ,EAAK,OAAO,MAAM,IAAI,KAAA;IAC9B,WAAW,EAAK,UAAU,MAAM,IAAI,KAAA;IACpC;IACD,CAAC,EAC2B,KAAK;WAE7B,GAAK;AACZ,KAAS,aAAe,QAAQ,EAAI,UAAU,uBAAuB;;IAInE,IAAS,EAAW,aAAa,EAAW;AAsBlD,QApBI,KAAU,EAAS,YAEnB,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD,EAAc,OAAM,iBAAkB,CAAA;EAC3B,CAAA,GAIb,MAAW,EAAS,WAAY,CAAC,EAAS,aAAa,CAAC,EAAS,QAEjE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD;GACE,OAAM;GACN,SAAQ;GACR,eAAe,EAAW,SAAS;GACnC,CAAA;EACS,CAAA,GAKf,kBAAC,GAAD;EACE,OAAO,IAAS,cAAc;EAC9B,aAAa,IAAS,8BAA8B;EACpD,SACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAW,KAAU,IAAK,UAAU,MAAO,SAAS;GACnE,WAAU;aAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAAA,UAC1B;;YAGX,kBAAC,QAAD;GAAM,UAAU;GAAc,WAAU;aAAxC;IACE,kBAAC,GAAD;KAAW,OAAM;KAAY,SAAQ;KAAO,UAAA;eAC1C,kBAAC,SAAD;MACE,IAAG;MACH,WAAW;MACX,OAAO,EAAK;MACZ,WAAW,MAAM,EAAI,QAAQ,EAAE,OAAO,MAAM;MAC5C,aAAY;MACZ,CAAA;KACQ,CAAA;IAEZ,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAW,OAAM;MAAQ,SAAQ;gBAC/B,kBAAC,UAAD;OACE,IAAG;OACH,WAAW;OACX,OAAO,EAAK;OACZ,WAAW,MAAM,EAAI,SAAS,EAAE,OAAO,MAAkB;iBAExD,EAAO,KAAK,MACX,kBAAC,UAAD;QAAgB,OAAO;kBACpB,EAAa;QACP,EAFI,EAEJ,CACT;OACK,CAAA;MACC,CAAA,EAEZ,kBAAC,GAAD;MAAW,OAAM;MAAS,SAAQ;gBAChC,kBAAC,UAAD;OACE,IAAG;OACH,WAAW;OACX,OAAO,EAAK;OACZ,WAAW,MAAM,EAAI,UAAU,EAAE,OAAO,MAAuB;iBAE9D,EAAS,KAAK,MACb,kBAAC,UAAD;QAAgB,OAAO;kBACpB,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,aAAa;QAChC,EAFI,EAEJ,CACT;OACK,CAAA;MACC,CAAA,CACR;;IAEN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAW,OAAM;MAAY,SAAQ;gBACnC,kBAAC,SAAD;OACE,IAAG;OACH,WAAW;OACX,OAAO,EAAK;OACZ,WAAW,MAAM,EAAI,YAAY,EAAE,OAAO,MAAM;OAChD,aAAY;OACZ,CAAA;MACQ,CAAA,EAEZ,kBAAC,GAAD;MAAW,OAAM;MAAS,SAAQ;gBAChC,kBAAC,SAAD;OACE,IAAG;OACH,WAAW;OACX,OAAO,EAAK;OACZ,WAAW,MAAM,EAAI,UAAU,EAAE,OAAO,MAAM;OAC9C,aAAY;OACZ,CAAA;MACQ,CAAA,CACR;;IAEN,kBAAC,GAAD;KAAW,OAAM;KAAa,SAAQ;eACpC,kBAAC,SAAD;MACE,IAAG;MACH,WAAW;MACX,OAAO,EAAK;MACZ,WAAW,MAAM,EAAI,aAAa,EAAE,OAAO,MAAM;MACjD,aAAY;MACZ,CAAA;KACQ,CAAA;IAEZ,kBAAC,GAAD;KAAW,OAAM;KAAc,SAAQ;eACrC,kBAAC,YAAD;MACE,IAAG;MACH,WAAW;MACX,MAAM;MACN,OAAO,EAAK;MACZ,WAAW,MAAM,EAAI,eAAe,EAAE,OAAO,MAAM;MACnD,aAAY;MACZ,CAAA;KACQ,CAAA;IAEZ,kBAAC,GAAD;KACE,OAAM;KACN,SAAQ;KACR,MAAK;eAEL,kBAAC,SAAD;MACE,IAAG;MACH,WAAW;MACX,OAAO,EAAK;MACZ,WAAW,MAAM,EAAI,YAAY,EAAE,OAAO,MAAM;MAChD,aAAY;MACZ,CAAA;KACQ,CAAA;IAEX,KAAS,kBAAC,KAAD;KAAG,WAAU;eAAkC;KAAU,CAAA;IAEnE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,UAAD;MACE,MAAK;MACL,UAAU;MACV,WAAU;gBAHZ,CAKG,IAAS,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAAG,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EACpF,IAAS,iBAAiB,cACpB;;KACL,CAAA;IACD;;EACI,CAAA"}
1
+ {"version":3,"file":"TeamFormPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/teams/TeamFormPage.tsx"],"sourcesContent":["/**\n * Team Create / Edit Form\n * @module atheletebridge/pages/teams\n */\n\nimport { useEffect, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, CalendarDays, Loader2, MapPin, Save, Search, X } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { UserAvatar } from '../../components/UserAvatar';\nimport { SPORT_ICONS, SPORT_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport {\n useTeam,\n useCreateTeam,\n useUpdateTeam,\n useUserDirectory,\n useUserSearch,\n directoryUserName,\n} from '../../hooks/useAthleteBridgeApi';\nimport type { AthSport, AthTeamStatus } from '../../types';\n\nconst SPORTS = Object.keys(SPORT_LABELS) as AthSport[];\nconst STATUSES: AthTeamStatus[] = ['FORMING', 'ACTIVE', 'INACTIVE', 'ARCHIVED'];\nconst DESCRIPTION_LIMIT = 300;\n\n/** Faint sport glyph rendered inside the Sport select trigger. */\nfunction SportGlyph({ sport }: { sport: AthSport }) {\n const Icon = SPORT_ICONS[sport];\n return (\n <Icon\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-muted\"\n aria-hidden=\"true\"\n />\n );\n}\n\ninterface HeadCoachPickerProps {\n userId: string;\n displayName: string;\n onSelect: (value: { userId: string; displayName: string }) => void;\n}\n\n/**\n * Optional head-coach picker — a lightweight directory search wired to the real\n * `useUserSearch` hook, degrading to a paste-a-user-id field when the directory\n * is unavailable. Labelled correctly for the head-coach role.\n */\nfunction HeadCoachPicker({ userId, displayName, onSelect }: HeadCoachPickerProps) {\n const [term, setTerm] = useState('');\n const search = useUserSearch(term);\n const results = search.data ?? [];\n const active = term.trim().length >= 2;\n const unavailable = search.isError;\n\n if (userId) {\n return (\n <FormField label=\"Head coach\" hint=\"Optional — the coach who leads this squad.\">\n <div className=\"flex items-center gap-2.5 rounded-lg border border-border-seam bg-bg-surface px-3 py-2\">\n <UserAvatar name={displayName} id={userId} size=\"sm\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {displayName || 'Selected coach'}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">{userId}</p>\n </div>\n <button\n type=\"button\"\n aria-label=\"Clear head coach\"\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 </FormField>\n );\n }\n\n if (unavailable) {\n return (\n <FormField\n label=\"Head coach\"\n htmlFor=\"headCoachId\"\n hint=\"Optional — directory unavailable here; paste the auth-svc user id.\"\n >\n <input\n id=\"headCoachId\"\n className={fieldInputClass}\n value={userId}\n onChange={(e) => onSelect({ userId: e.target.value, displayName })}\n placeholder=\"user-uuid\"\n />\n </FormField>\n );\n }\n\n return (\n <FormField label=\"Head coach\" hint=\"Optional — the coach who leads this squad.\">\n <div className=\"space-y-2\">\n <div className=\"relative\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-muted\"\n aria-hidden=\"true\"\n />\n <input\n className={`${fieldInputClass} pl-9`}\n value={term}\n onChange={(e) => setTerm(e.target.value)}\n placeholder=\"Search coaches by name or email…\"\n aria-label=\"Search for a head coach\"\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-muted\" />\n )}\n </div>\n {active && !search.isFetching && results.length === 0 && (\n <p className=\"text-xs text-text-secondary\">No users match “{term.trim()}”.</p>\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={() => {\n onSelect({ userId: user.id, displayName: directoryUserName(user) });\n setTerm('');\n }}\n className=\"flex w-full items-center gap-2.5 px-3 py-2 text-left transition-colors hover:bg-bg-sunken\"\n >\n <UserAvatar name={directoryUserName(user)} id={user.id} size=\"sm\" />\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\">{user.email}</span>\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n </FormField>\n );\n}\n\ninterface TeamFormState {\n name: string;\n sport: AthSport;\n status: AthTeamStatus;\n description: string;\n ageGroup: string;\n season: string;\n homeVenue: string;\n headCoachId: string;\n headCoachName: string;\n tags: string[];\n}\n\nconst EMPTY_FORM: TeamFormState = {\n name: '',\n sport: 'OTHER',\n status: 'FORMING',\n description: '',\n ageGroup: '',\n season: '',\n homeVenue: '',\n headCoachId: '',\n headCoachName: '',\n tags: [],\n};\n\nexport function TeamFormPage() {\n const { id } = useParams<{ id: string }>();\n const isEdit = Boolean(id);\n const { navigateTo } = useAthleteBridge();\n\n const existing = useTeam(id);\n const createTeam = useCreateTeam();\n const updateTeam = useUpdateTeam();\n\n const [form, setForm] = useState<TeamFormState>(EMPTY_FORM);\n const [tagInput, setTagInput] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n // Resolve the existing head coach's display name so the picker shows it on edit.\n const coachDir = useUserDirectory(isEdit ? [existing.data?.headCoachId] : []);\n const resolvedCoachName = coachDir.nameOf(existing.data?.headCoachId ?? undefined);\n\n useEffect(() => {\n const team = existing.data;\n if (isEdit && team) {\n setForm((prev) => ({\n name: team.name,\n sport: team.sport,\n status: team.status,\n description: team.description ?? '',\n ageGroup: team.ageGroup ?? '',\n season: team.season ?? '',\n homeVenue: team.homeVenue ?? '',\n headCoachId: team.headCoachId ?? '',\n headCoachName: resolvedCoachName ?? prev.headCoachName,\n tags: team.tags,\n }));\n }\n }, [isEdit, existing.data, resolvedCoachName]);\n\n const set = <K extends keyof TeamFormState>(key: K, value: TeamFormState[K]) =>\n setForm((prev) => ({ ...prev, [key]: value }));\n\n const commitTag = (raw: string) => {\n const tag = raw.trim();\n if (!tag) return;\n setForm((prev) =>\n prev.tags.some((t) => t.toLowerCase() === tag.toLowerCase())\n ? prev\n : { ...prev, tags: [...prev.tags, tag] }\n );\n setTagInput('');\n };\n\n const removeTag = (tag: string) =>\n setForm((prev) => ({ ...prev, tags: prev.tags.filter((t) => t !== tag) }));\n\n const handleTagKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter' || e.key === ',') {\n e.preventDefault();\n commitTag(tagInput);\n } else if (e.key === 'Backspace' && !tagInput && form.tags.length) {\n removeTag(form.tags[form.tags.length - 1]);\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!form.name.trim()) {\n setError('Team name is required.');\n return;\n }\n\n // Fold any half-typed tag still in the input into the submission.\n const pending = tagInput.trim();\n const tags =\n pending && !form.tags.some((t) => t.toLowerCase() === pending.toLowerCase())\n ? [...form.tags, pending]\n : form.tags;\n\n const shared = {\n name: form.name.trim(),\n sport: form.sport,\n status: form.status,\n description: form.description.trim() || undefined,\n ageGroup: form.ageGroup.trim() || undefined,\n season: form.season.trim() || undefined,\n homeVenue: form.homeVenue.trim() || undefined,\n headCoachId: form.headCoachId.trim() || undefined,\n tags,\n };\n\n try {\n if (isEdit && id) {\n await updateTeam.mutateAsync({ id, input: shared });\n navigateTo(`/teams/${id}`);\n } else {\n const created = await createTeam.mutateAsync(shared);\n navigateTo(`/teams/${created.id}`);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to save team.');\n }\n };\n\n const saving = createTeam.isPending || updateTeam.isPending;\n\n if (isEdit && existing.isLoading) {\n return (\n <PageLayout title=\"Edit team\">\n <LoadingState label=\"Loading team…\" />\n </PageLayout>\n );\n }\n\n if (isEdit && (existing.isError || (!existing.isLoading && !existing.data))) {\n return (\n <PageLayout title=\"Edit team\">\n <ErrorState\n title=\"Team not found\"\n message=\"This team does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/teams')}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={isEdit ? 'Edit team' : 'New team'}\n description={isEdit ? 'Update the squad details.' : 'Create a new squad to build a roster.'}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && id ? `/teams/${id}` : '/teams')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-3xl\">\n <GlassCard treatment=\"insight\" className=\"p-6 sm:p-8\">\n <div className=\"space-y-5\">\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Team name\" htmlFor=\"name\" required>\n <input\n id=\"name\"\n className={fieldInputClass}\n value={form.name}\n onChange={(e) => set('name', e.target.value)}\n placeholder=\"e.g. Riverside United U18\"\n />\n </FormField>\n\n <FormField label=\"Sport\" htmlFor=\"sport\">\n <div className=\"relative\">\n <SportGlyph sport={form.sport} />\n <select\n id=\"sport\"\n className={`${fieldInputClass} pl-9`}\n value={form.sport}\n onChange={(e) => set('sport', e.target.value as AthSport)}\n >\n {SPORTS.map((s) => (\n <option key={s} value={s}>\n {SPORT_LABELS[s]}\n </option>\n ))}\n </select>\n </div>\n </FormField>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Status\" htmlFor=\"status\">\n <select\n id=\"status\"\n className={fieldInputClass}\n value={form.status}\n onChange={(e) => set('status', e.target.value as AthTeamStatus)}\n >\n {STATUSES.map((s) => (\n <option key={s} value={s}>\n {s.charAt(0) + s.slice(1).toLowerCase()}\n </option>\n ))}\n </select>\n </FormField>\n\n <FormField label=\"Age group\" htmlFor=\"ageGroup\">\n <input\n id=\"ageGroup\"\n className={fieldInputClass}\n value={form.ageGroup}\n onChange={(e) => set('ageGroup', e.target.value)}\n placeholder=\"e.g. U18\"\n />\n </FormField>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Season\" htmlFor=\"season\">\n <div className=\"relative\">\n <CalendarDays\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-muted\"\n aria-hidden=\"true\"\n />\n <input\n id=\"season\"\n className={`${fieldInputClass} pl-9`}\n value={form.season}\n onChange={(e) => set('season', e.target.value)}\n placeholder=\"e.g. 2025/26\"\n />\n </div>\n </FormField>\n\n <FormField label=\"Home venue\" htmlFor=\"homeVenue\">\n <div className=\"relative\">\n <MapPin\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-muted\"\n aria-hidden=\"true\"\n />\n <input\n id=\"homeVenue\"\n className={`${fieldInputClass} pl-9`}\n value={form.homeVenue}\n onChange={(e) => set('homeVenue', e.target.value)}\n placeholder=\"e.g. Riverside Stadium\"\n />\n </div>\n </FormField>\n </div>\n\n <HeadCoachPicker\n userId={form.headCoachId}\n displayName={form.headCoachName}\n onSelect={({ userId, displayName }) =>\n setForm((prev) => ({\n ...prev,\n headCoachId: userId,\n headCoachName: displayName,\n }))\n }\n />\n\n <FormField label=\"Description\" htmlFor=\"description\">\n <textarea\n id=\"description\"\n className={fieldInputClass}\n rows={3}\n maxLength={DESCRIPTION_LIMIT}\n value={form.description}\n onChange={(e) => set('description', e.target.value)}\n placeholder=\"A short description of the team.\"\n />\n <div className=\"mt-1 text-right text-xs tabular-nums text-text-muted\">\n {form.description.length}/{DESCRIPTION_LIMIT}\n </div>\n </FormField>\n\n <FormField\n label=\"Tags\"\n htmlFor=\"tags\"\n hint=\"Type a tag and press Enter to add it; click × to remove.\"\n >\n <div className=\"flex flex-wrap items-center gap-1.5 rounded-lg border border-border-seam bg-bg-surface px-2 py-1.5 focus-within:ring-2 focus-within:ring-[var(--color-focus-ring)]\">\n {form.tags.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n <button\n type=\"button\"\n onClick={() => removeTag(tag)}\n aria-label={`Remove tag ${tag}`}\n className=\"rounded-full text-text-muted transition-colors hover:text-status-error-text\"\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))}\n <input\n id=\"tags\"\n className=\"min-w-[8rem] flex-1 bg-transparent px-1 py-0.5 text-sm text-text-primary placeholder:text-text-muted focus:outline-none\"\n value={tagInput}\n onChange={(e) => setTagInput(e.target.value)}\n onKeyDown={handleTagKeyDown}\n onBlur={() => commitTag(tagInput)}\n placeholder={form.tags.length ? 'Add tag and press Enter…' : 'youth, development'}\n />\n </div>\n </FormField>\n\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <div className=\"mt-6 flex items-center justify-end gap-3 border-t border-border-seam pt-5\">\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && id ? `/teams/${id}` : '/teams')}\n className=\"rounded-lg px-4 py-2 text-sm font-medium text-text-secondary transition-colors hover:text-text-primary\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={saving}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-semibold text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {saving ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Save className=\"h-4 w-4\" />}\n {isEdit ? 'Save changes' : 'Create team'}\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -44,6 +44,8 @@ function y() {
44
44
  return /* @__PURE__ */ h(t, {
45
45
  title: "Teams",
46
46
  description: "Squads, club sides, academy groups and esports rosters.",
47
+ hero: !0,
48
+ gradientTitle: !0,
47
49
  actions: /* @__PURE__ */ h("button", {
48
50
  type: "button",
49
51
  onClick: () => n("/teams/new"),
@@ -84,6 +86,7 @@ function y() {
84
86
  className: "grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3",
85
87
  children: E.map((e) => /* @__PURE__ */ m(r, {
86
88
  team: e,
89
+ memberCount: e.members?.length,
87
90
  onClick: () => n(`/teams/${e.id}`)
88
91
  }, e.id))
89
92
  }), /* @__PURE__ */ m(l, {
@@ -1 +1 @@
1
- {"version":3,"file":"TeamsListPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/teams/TeamsListPage.tsx"],"sourcesContent":["/**\n * Teams List\n * @module atheletebridge/pages/teams\n */\n\nimport { useState } from 'react';\nimport { Plus, Users } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { TeamCard } from '../../components/TeamCard';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FilterSelect } from '../../components/FilterSelect';\nimport { CursorPager } from '../../components/CursorPager';\nimport { SPORT_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useTeams } from '../../hooks/useAthleteBridgeApi';\nimport type { AthSport, AthTeamStatus } from '../../types';\n\nconst PAGE_SIZE = 12;\n\nconst SPORT_OPTS = (Object.keys(SPORT_LABELS) as AthSport[]).map((value) => ({\n value,\n label: SPORT_LABELS[value],\n}));\n\nconst STATUS_OPTS: ReadonlyArray<{ value: AthTeamStatus; label: string }> = [\n { value: 'FORMING', label: 'Forming' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'INACTIVE', label: 'Inactive' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\nexport function TeamsListPage() {\n const { navigateTo } = useAthleteBridge();\n const [sport, setSport] = useState<AthSport | ''>('');\n const [status, setStatus] = useState<AthTeamStatus | ''>('');\n const [page, setPage] = useState(0);\n\n const teams = useTeams({\n sport: sport || undefined,\n status: status || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n\n const items = teams.data?.items ?? [];\n const hasMore = teams.data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title=\"Teams\"\n description=\"Squads, club sides, academy groups and esports rosters.\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" /> New team\n </button>\n }\n >\n <div className=\"mb-6 flex flex-wrap items-center gap-3\">\n <FilterSelect\n ariaLabel=\"Filter by sport\"\n allLabel=\"All sports\"\n value={sport}\n onChange={(v) => {\n setSport(v);\n setPage(0);\n }}\n options={SPORT_OPTS}\n />\n <FilterSelect\n ariaLabel=\"Filter by status\"\n allLabel=\"All statuses\"\n value={status}\n onChange={(v) => {\n setStatus(v);\n setPage(0);\n }}\n options={STATUS_OPTS}\n />\n </div>\n\n {teams.isLoading ? (\n <LoadingState label=\"Loading teams…\" />\n ) : teams.isError ? (\n <ErrorState onRetry={() => void teams.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Users className=\"h-10 w-10\" />}\n title=\"No teams found\"\n description={\n sport || status\n ? 'Try clearing your filters to see all teams.'\n : 'Create your first team to build a roster and start scheduling fixtures.'\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n Create team\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {items.map((team) => (\n <TeamCard key={team.id} team={team} onClick={() => navigateTo(`/teams/${team.id}`)} />\n ))}\n </div>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAkBA,IAAM,IAAY,IAEZ,IAAc,OAAO,KAAK,EAAa,CAAgB,KAAK,OAAW;CAC3E;CACA,OAAO,EAAa;CACrB,EAAE,EAEG,IAAsE;CAC1E;EAAE,OAAO;EAAW,OAAO;EAAW;CACtC;EAAE,OAAO;EAAU,OAAO;EAAU;CACpC;EAAE,OAAO;EAAY,OAAO;EAAY;CACxC;EAAE,OAAO;EAAY,OAAO;EAAY;CACzC;AAED,SAAgB,IAAgB;CAC9B,IAAM,EAAE,kBAAe,GAAkB,EACnC,CAAC,GAAO,KAAY,EAAwB,GAAG,EAC/C,CAAC,GAAQ,KAAa,EAA6B,GAAG,EACtD,CAAC,GAAM,KAAW,EAAS,EAAE,EAE7B,IAAQ,EAAS;EACrB,OAAO,KAAS,KAAA;EAChB,QAAQ,KAAU,KAAA;EAClB,YAAY;GAAE,OAAO;GAAW,QAAQ,IAAO;GAAW;EAC3D,CAAC,EAEI,IAAQ,EAAM,MAAM,SAAS,EAAE,EAC/B,IAAU,EAAM,MAAM,SAAS,WAAW;AAEhD,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAW,aAAa;GACvC,WAAU;aAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,YACrB;;YAVb,CAaE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IACE,WAAU;IACV,UAAS;IACT,OAAO;IACP,WAAW,MAAM;AAEf,KADA,EAAS,EAAE,EACX,EAAQ,EAAE;;IAEZ,SAAS;IACT,CAAA,EACF,kBAAC,GAAD;IACE,WAAU;IACV,UAAS;IACT,OAAO;IACP,WAAW,MAAM;AAEf,KADA,EAAU,EAAE,EACZ,EAAQ,EAAE;;IAEZ,SAAS;IACT,CAAA,CACE;MAEL,EAAM,YACL,kBAAC,GAAD,EAAc,OAAM,kBAAmB,CAAA,GACrC,EAAM,UACR,kBAAC,GAAD,EAAY,eAAe,KAAK,EAAM,SAAS,EAAI,CAAA,GACjD,EAAM,WAAW,IACnB,kBAAC,GAAD;GACE,cAAa;GACb,MAAM,kBAAC,GAAD,EAAO,WAAU,aAAc,CAAA;GACrC,OAAM;GACN,aACE,KAAS,IACL,gDACA;GAEN,QACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,aAAa;IACvC,WAAU;cACX;IAEQ,CAAA;GAEX,CAAA,GAEF,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAM,KAAK,MACV,kBAAC,GAAD;IAA8B;IAAM,eAAe,EAAW,UAAU,EAAK,KAAK;IAAI,EAAvE,EAAK,GAAkE,CACtF;GACE,CAAA,EACN,kBAAC,GAAD;GACE,WAAU;GACJ;GACG;GACT,cAAc,GAAS,MAAM,KAAK,IAAI,GAAG,IAAI,EAAE,CAAC;GAChD,cAAc,GAAS,MAAM,IAAI,EAAE;GACnC,CAAA,CACD,EAAA,CAAA,CAEM"}
1
+ {"version":3,"file":"TeamsListPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/teams/TeamsListPage.tsx"],"sourcesContent":["/**\n * Teams List\n * @module atheletebridge/pages/teams\n */\n\nimport { useState } from 'react';\nimport { Plus, Users } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { TeamCard } from '../../components/TeamCard';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FilterSelect } from '../../components/FilterSelect';\nimport { CursorPager } from '../../components/CursorPager';\nimport { SPORT_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useTeams } from '../../hooks/useAthleteBridgeApi';\nimport type { AthSport, AthTeamStatus } from '../../types';\n\nconst PAGE_SIZE = 12;\n\nconst SPORT_OPTS = (Object.keys(SPORT_LABELS) as AthSport[]).map((value) => ({\n value,\n label: SPORT_LABELS[value],\n}));\n\nconst STATUS_OPTS: ReadonlyArray<{ value: AthTeamStatus; label: string }> = [\n { value: 'FORMING', label: 'Forming' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'INACTIVE', label: 'Inactive' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\nexport function TeamsListPage() {\n const { navigateTo } = useAthleteBridge();\n const [sport, setSport] = useState<AthSport | ''>('');\n const [status, setStatus] = useState<AthTeamStatus | ''>('');\n const [page, setPage] = useState(0);\n\n const teams = useTeams({\n sport: sport || undefined,\n status: status || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n\n const items = teams.data?.items ?? [];\n const hasMore = teams.data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title=\"Teams\"\n description=\"Squads, club sides, academy groups and esports rosters.\"\n hero\n gradientTitle\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" /> New team\n </button>\n }\n >\n <div className=\"mb-6 flex flex-wrap items-center gap-3\">\n <FilterSelect\n ariaLabel=\"Filter by sport\"\n allLabel=\"All sports\"\n value={sport}\n onChange={(v) => {\n setSport(v);\n setPage(0);\n }}\n options={SPORT_OPTS}\n />\n <FilterSelect\n ariaLabel=\"Filter by status\"\n allLabel=\"All statuses\"\n value={status}\n onChange={(v) => {\n setStatus(v);\n setPage(0);\n }}\n options={STATUS_OPTS}\n />\n </div>\n\n {teams.isLoading ? (\n <LoadingState label=\"Loading teams…\" />\n ) : teams.isError ? (\n <ErrorState onRetry={() => void teams.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Users className=\"h-10 w-10\" />}\n title=\"No teams found\"\n description={\n sport || status\n ? 'Try clearing your filters to see all teams.'\n : 'Create your first team to build a roster and start scheduling fixtures.'\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n Create team\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {items.map((team) => (\n <TeamCard\n key={team.id}\n team={team}\n memberCount={team.members?.length}\n onClick={() => navigateTo(`/teams/${team.id}`)}\n />\n ))}\n </div>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAkBA,IAAM,IAAY,IAEZ,IAAc,OAAO,KAAK,EAAa,CAAgB,KAAK,OAAW;CAC3E;CACA,OAAO,EAAa;CACrB,EAAE,EAEG,IAAsE;CAC1E;EAAE,OAAO;EAAW,OAAO;EAAW;CACtC;EAAE,OAAO;EAAU,OAAO;EAAU;CACpC;EAAE,OAAO;EAAY,OAAO;EAAY;CACxC;EAAE,OAAO;EAAY,OAAO;EAAY;CACzC;AAED,SAAgB,IAAgB;CAC9B,IAAM,EAAE,kBAAe,GAAkB,EACnC,CAAC,GAAO,KAAY,EAAwB,GAAG,EAC/C,CAAC,GAAQ,KAAa,EAA6B,GAAG,EACtD,CAAC,GAAM,KAAW,EAAS,EAAE,EAE7B,IAAQ,EAAS;EACrB,OAAO,KAAS,KAAA;EAChB,QAAQ,KAAU,KAAA;EAClB,YAAY;GAAE,OAAO;GAAW,QAAQ,IAAO;GAAW;EAC3D,CAAC,EAEI,IAAQ,EAAM,MAAM,SAAS,EAAE,EAC/B,IAAU,EAAM,MAAM,SAAS,WAAW;AAEhD,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,MAAA;EACA,eAAA;EACA,SACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAW,aAAa;GACvC,WAAU;aAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,YACrB;;YAZb,CAeE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IACE,WAAU;IACV,UAAS;IACT,OAAO;IACP,WAAW,MAAM;AAEf,KADA,EAAS,EAAE,EACX,EAAQ,EAAE;;IAEZ,SAAS;IACT,CAAA,EACF,kBAAC,GAAD;IACE,WAAU;IACV,UAAS;IACT,OAAO;IACP,WAAW,MAAM;AAEf,KADA,EAAU,EAAE,EACZ,EAAQ,EAAE;;IAEZ,SAAS;IACT,CAAA,CACE;MAEL,EAAM,YACL,kBAAC,GAAD,EAAc,OAAM,kBAAmB,CAAA,GACrC,EAAM,UACR,kBAAC,GAAD,EAAY,eAAe,KAAK,EAAM,SAAS,EAAI,CAAA,GACjD,EAAM,WAAW,IACnB,kBAAC,GAAD;GACE,cAAa;GACb,MAAM,kBAAC,GAAD,EAAO,WAAU,aAAc,CAAA;GACrC,OAAM;GACN,aACE,KAAS,IACL,gDACA;GAEN,QACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,aAAa;IACvC,WAAU;cACX;IAEQ,CAAA;GAEX,CAAA,GAEF,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAM,KAAK,MACV,kBAAC,GAAD;IAEQ;IACN,aAAa,EAAK,SAAS;IAC3B,eAAe,EAAW,UAAU,EAAK,KAAK;IAC9C,EAJK,EAAK,GAIV,CACF;GACE,CAAA,EACN,kBAAC,GAAD;GACE,WAAU;GACJ;GACG;GACT,cAAc,GAAS,MAAM,KAAK,IAAI,GAAG,IAAI,EAAE,CAAC;GAChD,cAAc,GAAS,MAAM,IAAI,EAAE;GACnC,CAAA,CACD,EAAA,CAAA,CAEM"}
@@ -0,0 +1,298 @@
1
+ import { useAthleteBridge as e } from "../../context/AthleteBridgeContext.js";
2
+ import { PageLayout as t } from "../../components/PageLayout.js";
3
+ import { STATUS as n } from "../../components/viz/lib/palette.js";
4
+ import "../../components/viz/index.js";
5
+ import { Crest as r } from "../../components/Crest.js";
6
+ import { formatDate as i } from "../../utils/formatters.js";
7
+ import { resolveMatchup as a } from "../../components/Scoreboard.js";
8
+ import { EmptyState as o } from "../../components/EmptyState.js";
9
+ import { ErrorState as s, LoadingState as c } from "../../components/LoadingState.js";
10
+ import { useFixtures as l } from "../../hooks/useAthleteBridgeApi.js";
11
+ import { FilterSelect as u } from "../../components/FilterSelect.js";
12
+ import { useMemo as d, useState as f } from "react";
13
+ import { Gamepad2 as p, Trophy as m } from "lucide-react";
14
+ import { jsx as h, jsxs as g } from "react/jsx-runtime";
15
+ import { Badge as _, GlassCard as v } from "@burdenoff/fe-libs/ui";
16
+ //#region src/atheletebridge/pages/tournaments/TournamentBracketPage.tsx
17
+ var y = {
18
+ COMPLETED: n.success,
19
+ LIVE: "var(--color-accent-primary)",
20
+ SCHEDULED: n.warning,
21
+ POSTPONED: "var(--color-text-muted)",
22
+ CANCELLED: "var(--color-text-muted)"
23
+ };
24
+ function b(e) {
25
+ if (!e) return 9999;
26
+ let t = e.toLowerCase(), n = t.match(/round of\s*(\d+)/);
27
+ return n ? 1e3 - Number(n[1]) : /third|3rd/.test(t) ? 1095 : /(grand\s*)?final/.test(t) && !/semi|quarter/.test(t) ? 1100 : /semi/.test(t) ? 1050 : /quarter/.test(t) ? 1020 : 9999;
28
+ }
29
+ function x(e) {
30
+ let t = /* @__PURE__ */ new Map();
31
+ for (let n of e) {
32
+ let e = n.round?.trim() || "Unscheduled round", r = t.get(e) ?? [];
33
+ r.push(n), t.set(e, r);
34
+ }
35
+ return Array.from(t.entries()).map(([e, t]) => {
36
+ let n = [...t].sort((e, t) => new Date(e.scheduledAt).getTime() - new Date(t.scheduledAt).getTime()), r = n.reduce((e, t) => Math.min(e, new Date(t.scheduledAt).getTime() || e), Infinity);
37
+ return {
38
+ key: e,
39
+ label: e,
40
+ rank: b(t[0]?.round ?? null),
41
+ earliest: Number.isFinite(r) ? r : 0,
42
+ fixtures: n
43
+ };
44
+ }).sort((e, t) => e.rank === t.rank ? e.earliest - t.earliest : e.rank - t.rank);
45
+ }
46
+ function S() {
47
+ let { navigateTo: y } = e(), b = l({ pagination: { limit: 200 } }), S = d(() => (b.data?.items ?? []).filter((e) => e.type === "TOURNAMENT"), [b.data]), w = d(() => {
48
+ let e = /* @__PURE__ */ new Set();
49
+ for (let t of S) e.add(t.competition?.trim() || "Tournament");
50
+ return Array.from(e).sort();
51
+ }, [S]), [E, D] = f(""), O = E || w[0] || "", k = d(() => x(S.filter((e) => (e.competition?.trim() || "Tournament") === O)), [S, O]), A = k.reduce((e, t) => e + t.fixtures.length, 0), j = d(() => {
52
+ let e = k[k.length - 1];
53
+ if (!e) return null;
54
+ for (let t of e.fixtures) {
55
+ if (t.status !== "COMPLETED") continue;
56
+ let e = a(t);
57
+ if (e.homeWon) return {
58
+ name: e.homeName,
59
+ sport: e.homeSport,
60
+ score: `${t.homeScore}–${t.awayScore}`
61
+ };
62
+ if (e.awayWon) return {
63
+ name: e.awayName,
64
+ sport: e.awaySport,
65
+ score: `${t.awayScore}–${t.homeScore}`
66
+ };
67
+ }
68
+ return null;
69
+ }, [k]);
70
+ return b.isLoading ? /* @__PURE__ */ h(t, {
71
+ title: "Tournaments",
72
+ children: /* @__PURE__ */ h(c, { label: "Loading tournament fixtures…" })
73
+ }) : b.isError ? /* @__PURE__ */ h(t, {
74
+ title: "Tournaments",
75
+ children: /* @__PURE__ */ h(s, { onRetry: () => void b.refetch() })
76
+ }) : /* @__PURE__ */ h(t, {
77
+ title: "Tournaments",
78
+ description: "Round-grouped brackets for your knockout competitions — every match is a real fixture.",
79
+ actions: w.length > 1 ? /* @__PURE__ */ h(u, {
80
+ value: O,
81
+ onChange: (e) => D(e),
82
+ options: w.map((e) => ({
83
+ value: e,
84
+ label: e
85
+ })),
86
+ allLabel: "All competitions",
87
+ ariaLabel: "Select competition"
88
+ }) : void 0,
89
+ children: S.length === 0 ? /* @__PURE__ */ h(o, {
90
+ illustration: "empty-calendar",
91
+ icon: /* @__PURE__ */ h(m, { className: "h-10 w-10" }),
92
+ title: "No tournament fixtures yet",
93
+ description: "Schedule fixtures with the Tournament type and a round label (e.g. “Quarter Finals”) to build a bracket.",
94
+ action: /* @__PURE__ */ h("button", {
95
+ type: "button",
96
+ onClick: () => y("/fixtures/new"),
97
+ className: "rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90",
98
+ children: "Schedule a fixture"
99
+ })
100
+ }) : /* @__PURE__ */ g("div", {
101
+ className: "space-y-6",
102
+ children: [
103
+ /* @__PURE__ */ g(v, {
104
+ treatment: "insight",
105
+ className: "flex flex-wrap items-center gap-x-6 gap-y-3 p-5",
106
+ children: [
107
+ /* @__PURE__ */ g("span", {
108
+ className: "flex items-center gap-2 text-lg font-semibold text-text-primary",
109
+ children: [/* @__PURE__ */ h(m, { className: "h-5 w-5 text-[var(--color-accent-teal)]" }), O]
110
+ }),
111
+ /* @__PURE__ */ g("span", {
112
+ className: "text-sm text-text-secondary",
113
+ children: [
114
+ /* @__PURE__ */ h("span", {
115
+ className: "font-semibold tabular-nums text-text-primary",
116
+ children: k.length
117
+ }),
118
+ " ",
119
+ k.length === 1 ? "round" : "rounds"
120
+ ]
121
+ }),
122
+ /* @__PURE__ */ g("span", {
123
+ className: "text-sm text-text-secondary",
124
+ children: [
125
+ /* @__PURE__ */ h("span", {
126
+ className: "font-semibold tabular-nums text-text-primary",
127
+ children: A
128
+ }),
129
+ " ",
130
+ A === 1 ? "match" : "matches"
131
+ ]
132
+ }),
133
+ /* @__PURE__ */ g("div", {
134
+ className: "ml-auto flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary",
135
+ children: [
136
+ /* @__PURE__ */ h(T, {
137
+ color: n.success,
138
+ label: "Completed"
139
+ }),
140
+ /* @__PURE__ */ h(T, {
141
+ color: "var(--color-accent-primary)",
142
+ label: "Live"
143
+ }),
144
+ /* @__PURE__ */ h(T, {
145
+ color: n.warning,
146
+ label: "Scheduled"
147
+ })
148
+ ]
149
+ })
150
+ ]
151
+ }),
152
+ /* @__PURE__ */ h("div", {
153
+ className: "overflow-x-auto pb-2",
154
+ children: /* @__PURE__ */ g("div", {
155
+ className: "flex min-w-max items-stretch gap-4",
156
+ children: [k.map((e) => /* @__PURE__ */ g("div", {
157
+ className: "flex w-72 shrink-0 flex-col gap-3",
158
+ children: [/* @__PURE__ */ g("div", {
159
+ className: "px-1",
160
+ children: [/* @__PURE__ */ h("h2", {
161
+ className: "text-xs font-semibold uppercase tracking-wide text-text-secondary",
162
+ children: e.label
163
+ }), e.earliest > 0 ? /* @__PURE__ */ h("p", {
164
+ className: "text-xs text-text-muted",
165
+ children: i(new Date(e.earliest).toISOString())
166
+ }) : null]
167
+ }), /* @__PURE__ */ h("div", {
168
+ className: "flex flex-1 flex-col justify-around gap-3",
169
+ children: e.fixtures.map((e) => /* @__PURE__ */ h(C, {
170
+ fixture: e,
171
+ onClick: () => y(`/fixtures/${e.id}`)
172
+ }, e.id))
173
+ })]
174
+ }, e.key)), j ? /* @__PURE__ */ g("div", {
175
+ className: "flex w-72 shrink-0 flex-col justify-center gap-3",
176
+ children: [/* @__PURE__ */ h("div", {
177
+ className: "px-1",
178
+ children: /* @__PURE__ */ h("h2", {
179
+ className: "text-xs font-semibold uppercase tracking-wide text-text-secondary",
180
+ children: "Champion"
181
+ })
182
+ }), /* @__PURE__ */ g(v, {
183
+ treatment: "glass",
184
+ className: "flex flex-col items-center gap-3 p-6 text-center",
185
+ children: [
186
+ /* @__PURE__ */ h(m, { className: "h-8 w-8 text-[var(--color-status-warning-text)]" }),
187
+ /* @__PURE__ */ h(r, {
188
+ name: j.name,
189
+ sport: j.sport,
190
+ size: 56
191
+ }),
192
+ /* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("p", {
193
+ className: "text-lg font-semibold text-text-primary",
194
+ children: j.name
195
+ }), /* @__PURE__ */ g("p", {
196
+ className: "mt-0.5 text-sm tabular-nums text-text-secondary",
197
+ children: ["Won ", j.score]
198
+ })] }),
199
+ /* @__PURE__ */ h(_, {
200
+ variant: "default",
201
+ children: "Champions"
202
+ })
203
+ ]
204
+ })]
205
+ }) : null]
206
+ })
207
+ }),
208
+ k.length === 0 ? /* @__PURE__ */ h(o, {
209
+ illustration: "empty-calendar",
210
+ icon: /* @__PURE__ */ h(p, { className: "h-10 w-10" }),
211
+ title: "No matches in this competition",
212
+ description: "Add round labels to this competition's tournament fixtures to populate the bracket."
213
+ }) : null
214
+ ]
215
+ })
216
+ });
217
+ }
218
+ function C({ fixture: e, onClick: t }) {
219
+ let n = a(e), r = y[e.status];
220
+ return /* @__PURE__ */ g("button", {
221
+ type: "button",
222
+ onClick: t,
223
+ className: "group w-full rounded-[var(--radius-card)] border border-border-seam bg-bg-surface p-3 text-left transition-all duration-200 hover:-translate-y-0.5 hover:border-border-strong hover:shadow-elevation-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
224
+ children: [
225
+ /* @__PURE__ */ h(w, {
226
+ name: n.homeName,
227
+ sport: n.homeSport,
228
+ score: e.homeScore,
229
+ won: n.homeWon,
230
+ dimmed: n.awayWon
231
+ }),
232
+ /* @__PURE__ */ h("div", {
233
+ className: "my-1 h-px bg-border-seam",
234
+ "aria-hidden": "true"
235
+ }),
236
+ /* @__PURE__ */ h(w, {
237
+ name: n.awayName,
238
+ sport: n.awaySport,
239
+ score: e.awayScore,
240
+ won: n.awayWon,
241
+ dimmed: n.homeWon
242
+ }),
243
+ /* @__PURE__ */ g("div", {
244
+ className: "mt-2 flex items-center gap-1.5 text-xs text-text-muted",
245
+ children: [
246
+ /* @__PURE__ */ h("span", {
247
+ className: "h-1.5 w-1.5 rounded-full",
248
+ style: { backgroundColor: r },
249
+ "aria-hidden": "true"
250
+ }),
251
+ /* @__PURE__ */ h("span", {
252
+ className: "capitalize",
253
+ children: e.status.toLowerCase().replace(/_/g, " ")
254
+ }),
255
+ e.venue ? /* @__PURE__ */ g("span", {
256
+ className: "truncate",
257
+ children: ["· ", e.venue]
258
+ }) : null
259
+ ]
260
+ })
261
+ ]
262
+ });
263
+ }
264
+ function w({ name: e, sport: t, score: n, won: i, dimmed: a }) {
265
+ return /* @__PURE__ */ g("div", {
266
+ className: "flex items-center gap-2",
267
+ children: [
268
+ /* @__PURE__ */ h(r, {
269
+ name: e,
270
+ sport: t,
271
+ size: 22,
272
+ showSportGlyph: !1
273
+ }),
274
+ /* @__PURE__ */ h("span", {
275
+ className: i ? "min-w-0 flex-1 truncate text-sm font-semibold text-status-success-text" : a ? "min-w-0 flex-1 truncate text-sm text-text-muted" : "min-w-0 flex-1 truncate text-sm font-medium text-text-primary",
276
+ children: e
277
+ }),
278
+ /* @__PURE__ */ h("span", {
279
+ className: i ? "shrink-0 text-sm font-bold tabular-nums text-status-success-text" : "shrink-0 text-sm font-semibold tabular-nums text-text-secondary",
280
+ children: n ?? "–"
281
+ })
282
+ ]
283
+ });
284
+ }
285
+ function T({ color: e, label: t }) {
286
+ return /* @__PURE__ */ g("span", {
287
+ className: "flex items-center gap-1.5",
288
+ children: [/* @__PURE__ */ h("span", {
289
+ className: "h-2 w-2 rounded-full",
290
+ style: { backgroundColor: e },
291
+ "aria-hidden": "true"
292
+ }), t]
293
+ });
294
+ }
295
+ //#endregion
296
+ export { S as TournamentBracketPage };
297
+
298
+ //# sourceMappingURL=TournamentBracketPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TournamentBracketPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/tournaments/TournamentBracketPage.tsx"],"sourcesContent":["/**\n * Tournament Bracket\n * @module atheletebridge/pages/tournaments\n *\n * A round-grouped bracket view for real TOURNAMENT-type fixtures. It lists every\n * competition that has TOURNAMENT fixtures, and for the selected competition\n * groups those fixtures by their `round` string into ordered columns\n * (Round of N → Quarter Finals → Semi Finals → Finals), each match showing crest\n * + score + status straight from the fixture row.\n *\n * Honesty: the backend stores fixtures FLAT (a round label, no parent/child\n * linkage), so columns are laid out as visual grouping only — there are NO\n * fabricated connector edges asserting stored progression, and no \"global rank\".\n * The champion card is driven solely by a real COMPLETED final with a decided\n * winner. Horizontally scrollable so it never breaks layout on mobile.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Gamepad2, Trophy } from 'lucide-react';\nimport { Badge, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Crest } from '../../components/Crest';\nimport { FilterSelect } from '../../components/FilterSelect';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { resolveMatchup } from '../../components/Scoreboard';\nimport { STATUS } from '../../components/viz';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useFixtures } from '../../hooks/useAthleteBridgeApi';\nimport { formatDate } from '../../utils/formatters';\nimport type { AthFixture, AthFixtureStatus, AthSport } from '../../types';\n\n/** Tokenized status dot color for a fixture. */\nconst STATUS_DOT: Record<AthFixtureStatus, string> = {\n COMPLETED: STATUS.success,\n LIVE: 'var(--color-accent-primary)',\n SCHEDULED: STATUS.warning,\n POSTPONED: 'var(--color-text-muted)',\n CANCELLED: 'var(--color-text-muted)',\n};\n\n/**\n * Order a round label within a single-elimination progression. Recognized\n * labels sort in bracket order; unrecognized labels fall to the end (tie-broken\n * by earliest kickoff) rather than being invented into the sequence.\n */\nfunction roundRank(round: string | null): number {\n if (!round) {\n return 9999;\n }\n const r = round.toLowerCase();\n const roundOf = r.match(/round of\\s*(\\d+)/);\n if (roundOf) {\n return 1000 - Number(roundOf[1]); // larger field ⇒ earlier column\n }\n if (/third|3rd/.test(r)) {\n return 1095;\n }\n if (/(grand\\s*)?final/.test(r) && !/semi|quarter/.test(r)) {\n return 1100;\n }\n if (/semi/.test(r)) {\n return 1050;\n }\n if (/quarter/.test(r)) {\n return 1020;\n }\n return 9999;\n}\n\ninterface RoundColumn {\n key: string;\n label: string;\n rank: number;\n earliest: number;\n fixtures: AthFixture[];\n}\n\n/** Group a competition's tournament fixtures into ordered round columns. */\nfunction buildRounds(fixtures: AthFixture[]): RoundColumn[] {\n const byRound = new Map<string, AthFixture[]>();\n for (const f of fixtures) {\n const key = f.round?.trim() || 'Unscheduled round';\n const list = byRound.get(key) ?? [];\n list.push(f);\n byRound.set(key, list);\n }\n return Array.from(byRound.entries())\n .map(([label, list]) => {\n const sorted = [...list].sort(\n (a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime()\n );\n const earliest = sorted.reduce(\n (acc, f) => Math.min(acc, new Date(f.scheduledAt).getTime() || acc),\n Number.POSITIVE_INFINITY\n );\n return {\n key: label,\n label,\n rank: roundRank(list[0]?.round ?? null),\n earliest: Number.isFinite(earliest) ? earliest : 0,\n fixtures: sorted,\n };\n })\n .sort((a, b) => (a.rank !== b.rank ? a.rank - b.rank : a.earliest - b.earliest));\n}\n\nexport function TournamentBracketPage() {\n const { navigateTo } = useAthleteBridge();\n const fixturesQuery = useFixtures({ pagination: { limit: 200 } });\n\n const tournamentFixtures = useMemo(\n () => (fixturesQuery.data?.items ?? []).filter((f) => f.type === 'TOURNAMENT'),\n [fixturesQuery.data]\n );\n\n const competitions = useMemo(() => {\n const set = new Set<string>();\n for (const f of tournamentFixtures) {\n set.add(f.competition?.trim() || 'Tournament');\n }\n return Array.from(set).sort();\n }, [tournamentFixtures]);\n\n const [selected, setSelected] = useState<string>('');\n const activeCompetition = selected || competitions[0] || '';\n\n const rounds = useMemo(() => {\n const forComp = tournamentFixtures.filter(\n (f) => (f.competition?.trim() || 'Tournament') === activeCompetition\n );\n return buildRounds(forComp);\n }, [tournamentFixtures, activeCompetition]);\n\n const matchCount = rounds.reduce((acc, r) => acc + r.fixtures.length, 0);\n\n // Champion: a COMPLETED final (last round) with a decided winner. No fabrication.\n const champion = useMemo(() => {\n const finalColumn = rounds[rounds.length - 1];\n if (!finalColumn) {\n return null;\n }\n for (const f of finalColumn.fixtures) {\n if (f.status !== 'COMPLETED') {\n continue;\n }\n const m = resolveMatchup(f);\n if (m.homeWon) {\n return { name: m.homeName, sport: m.homeSport, score: `${f.homeScore}–${f.awayScore}` };\n }\n if (m.awayWon) {\n return { name: m.awayName, sport: m.awaySport, score: `${f.awayScore}–${f.homeScore}` };\n }\n }\n return null;\n }, [rounds]);\n\n if (fixturesQuery.isLoading) {\n return (\n <PageLayout title=\"Tournaments\">\n <LoadingState label=\"Loading tournament fixtures…\" />\n </PageLayout>\n );\n }\n\n if (fixturesQuery.isError) {\n return (\n <PageLayout title=\"Tournaments\">\n <ErrorState onRetry={() => void fixturesQuery.refetch()} />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Tournaments\"\n description=\"Round-grouped brackets for your knockout competitions — every match is a real fixture.\"\n actions={\n competitions.length > 1 ? (\n <FilterSelect\n value={activeCompetition}\n onChange={(value) => setSelected(value)}\n options={competitions.map((c) => ({ value: c, label: c }))}\n allLabel=\"All competitions\"\n ariaLabel=\"Select competition\"\n />\n ) : undefined\n }\n >\n {tournamentFixtures.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<Trophy className=\"h-10 w-10\" />}\n title=\"No tournament fixtures yet\"\n description=\"Schedule fixtures with the Tournament type and a round label (e.g. “Quarter Finals”) to build a bracket.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n Schedule a fixture\n </button>\n }\n />\n ) : (\n <div className=\"space-y-6\">\n {/* Meta strip */}\n <GlassCard\n treatment=\"insight\"\n className=\"flex flex-wrap items-center gap-x-6 gap-y-3 p-5\"\n >\n <span className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Trophy className=\"h-5 w-5 text-[var(--color-accent-teal)]\" />\n {activeCompetition}\n </span>\n <span className=\"text-sm text-text-secondary\">\n <span className=\"font-semibold tabular-nums text-text-primary\">{rounds.length}</span>{' '}\n {rounds.length === 1 ? 'round' : 'rounds'}\n </span>\n <span className=\"text-sm text-text-secondary\">\n <span className=\"font-semibold tabular-nums text-text-primary\">{matchCount}</span>{' '}\n {matchCount === 1 ? 'match' : 'matches'}\n </span>\n <div className=\"ml-auto flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <StatusDot color={STATUS.success} label=\"Completed\" />\n <StatusDot color=\"var(--color-accent-primary)\" label=\"Live\" />\n <StatusDot color={STATUS.warning} label=\"Scheduled\" />\n </div>\n </GlassCard>\n\n {/* Bracket — horizontally scrollable columns */}\n <div className=\"overflow-x-auto pb-2\">\n <div className=\"flex min-w-max items-stretch gap-4\">\n {rounds.map((round) => (\n <div key={round.key} className=\"flex w-72 shrink-0 flex-col gap-3\">\n <div className=\"px-1\">\n <h2 className=\"text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n {round.label}\n </h2>\n {round.earliest > 0 ? (\n <p className=\"text-xs text-text-muted\">\n {formatDate(new Date(round.earliest).toISOString())}\n </p>\n ) : null}\n </div>\n <div className=\"flex flex-1 flex-col justify-around gap-3\">\n {round.fixtures.map((fixture) => (\n <MatchCard\n key={fixture.id}\n fixture={fixture}\n onClick={() => navigateTo(`/fixtures/${fixture.id}`)}\n />\n ))}\n </div>\n </div>\n ))}\n\n {/* Champion column (only when a real completed final exists) */}\n {champion ? (\n <div className=\"flex w-72 shrink-0 flex-col justify-center gap-3\">\n <div className=\"px-1\">\n <h2 className=\"text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n Champion\n </h2>\n </div>\n <GlassCard\n treatment=\"glass\"\n className=\"flex flex-col items-center gap-3 p-6 text-center\"\n >\n <Trophy className=\"h-8 w-8 text-[var(--color-status-warning-text)]\" />\n <Crest name={champion.name} sport={champion.sport} size={56} />\n <div>\n <p className=\"text-lg font-semibold text-text-primary\">{champion.name}</p>\n <p className=\"mt-0.5 text-sm tabular-nums text-text-secondary\">\n Won {champion.score}\n </p>\n </div>\n <Badge variant=\"default\">Champions</Badge>\n </GlassCard>\n </div>\n ) : null}\n </div>\n </div>\n\n {rounds.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<Gamepad2 className=\"h-10 w-10\" />}\n title=\"No matches in this competition\"\n description=\"Add round labels to this competition's tournament fixtures to populate the bracket.\"\n />\n ) : null}\n </div>\n )}\n </PageLayout>\n );\n}\n\n// ── Match card ───────────────────────────────────────────────────────────────\n\ninterface MatchCardProps {\n fixture: AthFixture;\n onClick: () => void;\n}\n\nfunction MatchCard({ fixture, onClick }: MatchCardProps) {\n const m = resolveMatchup(fixture);\n const dot = STATUS_DOT[fixture.status];\n\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className=\"group w-full rounded-[var(--radius-card)] border border-border-seam bg-bg-surface p-3 text-left transition-all duration-200 hover:-translate-y-0.5 hover:border-border-strong hover:shadow-elevation-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <MatchSide\n name={m.homeName}\n sport={m.homeSport}\n score={fixture.homeScore}\n won={m.homeWon}\n dimmed={m.awayWon}\n />\n <div className=\"my-1 h-px bg-border-seam\" aria-hidden=\"true\" />\n <MatchSide\n name={m.awayName}\n sport={m.awaySport}\n score={fixture.awayScore}\n won={m.awayWon}\n dimmed={m.homeWon}\n />\n <div className=\"mt-2 flex items-center gap-1.5 text-xs text-text-muted\">\n <span\n className=\"h-1.5 w-1.5 rounded-full\"\n style={{ backgroundColor: dot }}\n aria-hidden=\"true\"\n />\n <span className=\"capitalize\">{fixture.status.toLowerCase().replace(/_/g, ' ')}</span>\n {fixture.venue ? <span className=\"truncate\">· {fixture.venue}</span> : null}\n </div>\n </button>\n );\n}\n\ninterface MatchSideProps {\n name: string;\n sport?: AthSport;\n score: number | null;\n won: boolean;\n dimmed: boolean;\n}\n\nfunction MatchSide({ name, sport, score, won, dimmed }: MatchSideProps) {\n return (\n <div className=\"flex items-center gap-2\">\n <Crest name={name} sport={sport} size={22} showSportGlyph={false} />\n <span\n className={\n won\n ? 'min-w-0 flex-1 truncate text-sm font-semibold text-status-success-text'\n : dimmed\n ? 'min-w-0 flex-1 truncate text-sm text-text-muted'\n : 'min-w-0 flex-1 truncate text-sm font-medium text-text-primary'\n }\n >\n {name}\n </span>\n <span\n className={\n won\n ? 'shrink-0 text-sm font-bold tabular-nums text-status-success-text'\n : 'shrink-0 text-sm font-semibold tabular-nums text-text-secondary'\n }\n >\n {score ?? '–'}\n </span>\n </div>\n );\n}\n\n// ── Legend dot ───────────────────────────────────────────────────────────────\n\nfunction StatusDot({ color, label }: { color: string; label: string }) {\n return (\n <span className=\"flex items-center gap-1.5\">\n <span\n className=\"h-2 w-2 rounded-full\"\n style={{ backgroundColor: color }}\n aria-hidden=\"true\"\n />\n {label}\n </span>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ //#region src/atheletebridge/utils/identity.ts
2
+ function e(e) {
3
+ let t = 5381;
4
+ for (let n = 0; n < e.length; n += 1) t = t * 33 ^ e.charCodeAt(n);
5
+ return t >>> 0;
6
+ }
7
+ function t(e, t = 2) {
8
+ let n = e.trim().split(/\s+/).filter(Boolean);
9
+ return n.length === 0 ? "?" : n.length === 1 ? n[0].slice(0, t).toUpperCase() : n.slice(0, t).map((e) => e[0]).join("").toUpperCase();
10
+ }
11
+ var n = [
12
+ ["var(--color-accent-primary)", "var(--color-chart-2)"],
13
+ ["var(--color-accent-blue)", "var(--color-accent-indigo)"],
14
+ ["var(--color-accent-teal)", "var(--color-chart-3)"],
15
+ ["var(--color-accent-purple)", "var(--color-chart-11)"],
16
+ ["var(--color-chart-4)", "var(--color-chart-5)"],
17
+ ["var(--color-chart-6)", "var(--color-chart-7)"],
18
+ ["var(--color-chart-8)", "var(--color-chart-9)"],
19
+ ["var(--color-chart-10)", "var(--color-chart-12)"]
20
+ ];
21
+ function r(t) {
22
+ return n[e(t) % n.length];
23
+ }
24
+ function i(e, t = 135) {
25
+ let [n, i] = r(e);
26
+ return `linear-gradient(${t}deg, ${n}, ${i})`;
27
+ }
28
+ //#endregion
29
+ export { i as gradientCss, r as gradientForSeed, t as initials };
30
+
31
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","names":[],"sources":["../../../src/atheletebridge/utils/identity.ts"],"sourcesContent":["/**\n * Deterministic identity helpers\n * @module atheletebridge/utils/identity\n *\n * Stable, seed-derived visuals for teams / athletes / coaches. The backend has\n * NO logo or photo field, so crests and avatars are honest monogram stand-ins:\n * initials over a gradient chosen deterministically from the entity's name/id.\n * Every color is a fe-libs semantic token — never a raw hex.\n */\n\n/** Small stable, order-independent-per-char string hash (djb2 xor variant). */\nexport function stableHash(input: string): number {\n let hash = 5381;\n for (let i = 0; i < input.length; i += 1) {\n hash = (hash * 33) ^ input.charCodeAt(i);\n }\n // Force to a positive 32-bit int.\n return hash >>> 0;\n}\n\n/** Up-to-`max`-letter initials from a display name (e.g. \"Riverside FC\" → \"RF\"). */\nexport function initials(name: string, max = 2): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) {\n return '?';\n }\n if (parts.length === 1) {\n return parts[0].slice(0, max).toUpperCase();\n }\n return parts\n .slice(0, max)\n .map((p) => p[0])\n .join('')\n .toUpperCase();\n}\n\n/**\n * Curated tokenized gradient stop pairs. Drawn from the categorical chart tokens\n * and product accents so crests read as distinct \"club colors\" while staying\n * theme-aware. Never emit a raw color.\n */\nexport const CREST_GRADIENTS: readonly [string, string][] = [\n ['var(--color-accent-primary)', 'var(--color-chart-2)'],\n ['var(--color-accent-blue)', 'var(--color-accent-indigo)'],\n ['var(--color-accent-teal)', 'var(--color-chart-3)'],\n ['var(--color-accent-purple)', 'var(--color-chart-11)'],\n ['var(--color-chart-4)', 'var(--color-chart-5)'],\n ['var(--color-chart-6)', 'var(--color-chart-7)'],\n ['var(--color-chart-8)', 'var(--color-chart-9)'],\n ['var(--color-chart-10)', 'var(--color-chart-12)'],\n];\n\n/** Deterministic gradient stop pair for a seed (name or id). */\nexport function gradientForSeed(seed: string): [string, string] {\n return CREST_GRADIENTS[stableHash(seed) % CREST_GRADIENTS.length];\n}\n\n/** A CSS `linear-gradient(...)` string for a seed, ready for a `background` style. */\nexport function gradientCss(seed: string, angle = 135): string {\n const [from, to] = gradientForSeed(seed);\n return `linear-gradient(${angle}deg, ${from}, ${to})`;\n}\n"],"mappings":";AAWA,SAAgB,EAAW,GAAuB;CAChD,IAAI,IAAO;AACX,MAAK,IAAI,IAAI,GAAG,IAAI,EAAM,QAAQ,KAAK,EACrC,KAAQ,IAAO,KAAM,EAAM,WAAW,EAAE;AAG1C,QAAO,MAAS;;AAIlB,SAAgB,EAAS,GAAc,IAAM,GAAW;CACtD,IAAM,IAAQ,EAAK,MAAM,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ;AAOtD,QANI,EAAM,WAAW,IACZ,MAEL,EAAM,WAAW,IACZ,EAAM,GAAG,MAAM,GAAG,EAAI,CAAC,aAAa,GAEtC,EACJ,MAAM,GAAG,EAAI,CACb,KAAK,MAAM,EAAE,GAAG,CAChB,KAAK,GAAG,CACR,aAAa;;AAQlB,IAAa,IAA+C;CAC1D,CAAC,+BAA+B,uBAAuB;CACvD,CAAC,4BAA4B,6BAA6B;CAC1D,CAAC,4BAA4B,uBAAuB;CACpD,CAAC,8BAA8B,wBAAwB;CACvD,CAAC,wBAAwB,uBAAuB;CAChD,CAAC,wBAAwB,uBAAuB;CAChD,CAAC,wBAAwB,uBAAuB;CAChD,CAAC,yBAAyB,wBAAwB;CACnD;AAGD,SAAgB,EAAgB,GAAgC;AAC9D,QAAO,EAAgB,EAAW,EAAK,GAAG,EAAgB;;AAI5D,SAAgB,EAAY,GAAc,IAAQ,KAAa;CAC7D,IAAM,CAAC,GAAM,KAAM,EAAgB,EAAK;AACxC,QAAO,mBAAmB,EAAM,OAAO,EAAK,IAAI,EAAG"}
@@ -0,0 +1,20 @@
1
+ //#region src/atheletebridge/utils/timeAgo.ts
2
+ var e = 60, t = e * 60, n = t * 24, r = n * 7;
3
+ function i(e, t = /* @__PURE__ */ new Date()) {
4
+ if (!e) return "";
5
+ let n = typeof e == "string" ? new Date(e) : e;
6
+ if (Number.isNaN(n.getTime())) return "";
7
+ let i = Math.round((t.getTime() - n.getTime()) / 1e3);
8
+ return i < 0 ? `in ${a(-i)}` : i < 45 ? "just now" : i >= r * 4 ? new Intl.DateTimeFormat("en-IN", {
9
+ day: "numeric",
10
+ month: "short",
11
+ year: n.getFullYear() === t.getFullYear() ? void 0 : "numeric"
12
+ }).format(n) : `${a(i)} ago`;
13
+ }
14
+ function a(i) {
15
+ return i < e ? `${Math.max(1, Math.round(i))}s` : i < t ? `${Math.round(i / e)}m` : i < n ? `${Math.round(i / t)}h` : i < r ? `${Math.round(i / n)}d` : `${Math.round(i / r)}w`;
16
+ }
17
+ //#endregion
18
+ export { i as timeAgo };
19
+
20
+ //# sourceMappingURL=timeAgo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeAgo.js","names":[],"sources":["../../../src/atheletebridge/utils/timeAgo.ts"],"sourcesContent":["/**\n * Relative-time formatter\n * @module atheletebridge/utils/timeAgo\n *\n * Compact \"2h ago\" / \"3d ago\" strings for the activity feed and record\n * timestamps. Falls back to an absolute short date beyond ~4 weeks so old items\n * stay legible.\n */\n\nconst MINUTE = 60;\nconst HOUR = MINUTE * 60;\nconst DAY = HOUR * 24;\nconst WEEK = DAY * 7;\n\n/**\n * Format an ISO timestamp (or Date) as a short relative string from `now`.\n *\n * @example\n * timeAgo('2026-07-06T10:00:00Z') // => \"2h ago\" (2h earlier)\n */\nexport function timeAgo(input: string | Date | null | undefined, now: Date = new Date()): string {\n if (!input) {\n return '';\n }\n const then = typeof input === 'string' ? new Date(input) : input;\n if (Number.isNaN(then.getTime())) {\n return '';\n }\n const diffSec = Math.round((now.getTime() - then.getTime()) / 1000);\n\n if (diffSec < 0) {\n // Future timestamp — mirror the past phrasing (\"in 2h\").\n return `in ${relativeUnit(-diffSec)}`;\n }\n if (diffSec < 45) {\n return 'just now';\n }\n if (diffSec >= WEEK * 4) {\n return new Intl.DateTimeFormat('en-IN', {\n day: 'numeric',\n month: 'short',\n year: then.getFullYear() === now.getFullYear() ? undefined : 'numeric',\n }).format(then);\n }\n return `${relativeUnit(diffSec)} ago`;\n}\n\nfunction relativeUnit(seconds: number): string {\n if (seconds < MINUTE) {\n return `${Math.max(1, Math.round(seconds))}s`;\n }\n if (seconds < HOUR) {\n return `${Math.round(seconds / MINUTE)}m`;\n }\n if (seconds < DAY) {\n return `${Math.round(seconds / HOUR)}h`;\n }\n if (seconds < WEEK) {\n return `${Math.round(seconds / DAY)}d`;\n }\n return `${Math.round(seconds / WEEK)}w`;\n}\n"],"mappings":";AASA,IAAM,IAAS,IACT,IAAO,IAAS,IAChB,IAAM,IAAO,IACb,IAAO,IAAM;AAQnB,SAAgB,EAAQ,GAAyC,oBAAY,IAAI,MAAM,EAAU;AAC/F,KAAI,CAAC,EACH,QAAO;CAET,IAAM,IAAO,OAAO,KAAU,WAAW,IAAI,KAAK,EAAM,GAAG;AAC3D,KAAI,OAAO,MAAM,EAAK,SAAS,CAAC,CAC9B,QAAO;CAET,IAAM,IAAU,KAAK,OAAO,EAAI,SAAS,GAAG,EAAK,SAAS,IAAI,IAAK;AAgBnE,QAdI,IAAU,IAEL,MAAM,EAAa,CAAC,EAAQ,KAEjC,IAAU,KACL,aAEL,KAAW,IAAO,IACb,IAAI,KAAK,eAAe,SAAS;EACtC,KAAK;EACL,OAAO;EACP,MAAM,EAAK,aAAa,KAAK,EAAI,aAAa,GAAG,KAAA,IAAY;EAC9D,CAAC,CAAC,OAAO,EAAK,GAEV,GAAG,EAAa,EAAQ,CAAC;;AAGlC,SAAS,EAAa,GAAyB;AAa7C,QAZI,IAAU,IACL,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,EAAQ,CAAC,CAAC,KAEzC,IAAU,IACL,GAAG,KAAK,MAAM,IAAU,EAAO,CAAC,KAErC,IAAU,IACL,GAAG,KAAK,MAAM,IAAU,EAAK,CAAC,KAEnC,IAAU,IACL,GAAG,KAAK,MAAM,IAAU,EAAI,CAAC,KAE/B,GAAG,KAAK,MAAM,IAAU,EAAK,CAAC"}
@@ -0,0 +1,17 @@
1
+ //#region src/atheletebridge/utils/url.ts
2
+ function e(e) {
3
+ if (!e) return null;
4
+ let t = e.trim();
5
+ if (!t) return null;
6
+ let n;
7
+ try {
8
+ n = new URL(t);
9
+ } catch {
10
+ return null;
11
+ }
12
+ return n.protocol === "http:" || n.protocol === "https:" ? n.href : null;
13
+ }
14
+ //#endregion
15
+ export { e as safeExternalHref };
16
+
17
+ //# sourceMappingURL=url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url.js","names":[],"sources":["../../../src/atheletebridge/utils/url.ts"],"sourcesContent":["/**\n * URL safety helpers\n * @module atheletebridge/utils/url\n *\n * `safeExternalHref` is defense-in-depth against DOM-XSS from user-controlled\n * URLs (e.g. a session `meetingUrl`) that get rendered as clickable anchors.\n * A stored `javascript:` / `data:` / `vbscript:` URL in an `<a href>` executes\n * on click, so we only ever hand back a URL that parses as an absolute URL and\n * uses an `http:` or `https:` scheme. Anything else (bad scheme, relative,\n * unparseable) returns `null` and the caller must decline to render a link.\n */\n\n/**\n * Returns the URL only if it is a safe, absolute http(s) link; otherwise `null`.\n *\n * @example\n * safeExternalHref('https://meet.example.com/x') // => 'https://meet.example.com/x'\n * safeExternalHref('javascript:alert(1)') // => null\n * safeExternalHref('data:text/html,<script>') // => null\n * safeExternalHref('meet.example.com') // => null (not absolute)\n */\nexport function safeExternalHref(url: string | null | undefined): string | null {\n if (!url) return null;\n const trimmed = url.trim();\n if (!trimmed) return null;\n\n let parsed: URL;\n try {\n parsed = new URL(trimmed);\n } catch {\n return null;\n }\n\n if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {\n return parsed.href;\n }\n return null;\n}\n"],"mappings":";AAqBA,SAAgB,EAAiB,GAA+C;AAC9E,KAAI,CAAC,EAAK,QAAO;CACjB,IAAM,IAAU,EAAI,MAAM;AAC1B,KAAI,CAAC,EAAS,QAAO;CAErB,IAAI;AACJ,KAAI;AACF,MAAS,IAAI,IAAI,EAAQ;SACnB;AACN,SAAO;;AAMT,QAHI,EAAO,aAAa,WAAW,EAAO,aAAa,WAC9C,EAAO,OAET"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-atheletebridge",
3
- "version": "2026.705.1",
3
+ "version": "2026.706.1",
4
4
  "description": "Burdenoff AthleteBridge Microfrontend - Sports, coaching & training platform for teams, fixtures, programs, sessions and athlete performance",
5
5
  "type": "module",
6
6
  "files": [