@burdenoff/microfe-atheletebridge 2026.705.2 → 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.
- package/dist/atheletebridge/AthleteBridgeRoot.js +55 -39
- package/dist/atheletebridge/AthleteBridgeRoot.js.map +1 -1
- package/dist/atheletebridge/AthleteBridgeRoutes.js +18 -6
- package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
- package/dist/atheletebridge/components/Crest.js +70 -0
- package/dist/atheletebridge/components/Crest.js.map +1 -0
- package/dist/atheletebridge/components/FixtureCard.js +8 -72
- package/dist/atheletebridge/components/FixtureCard.js.map +1 -1
- package/dist/atheletebridge/components/MatchupRow.js +74 -0
- package/dist/atheletebridge/components/MatchupRow.js.map +1 -0
- package/dist/atheletebridge/components/MetricsCard.js +61 -21
- package/dist/atheletebridge/components/MetricsCard.js.map +1 -1
- package/dist/atheletebridge/components/ProgramCard.js +75 -43
- package/dist/atheletebridge/components/ProgramCard.js.map +1 -1
- package/dist/atheletebridge/components/Scoreboard.js +163 -0
- package/dist/atheletebridge/components/Scoreboard.js.map +1 -0
- package/dist/atheletebridge/components/SessionCard.js +64 -41
- package/dist/atheletebridge/components/SessionCard.js.map +1 -1
- package/dist/atheletebridge/components/SessionStatusTimeline.js +80 -0
- package/dist/atheletebridge/components/SessionStatusTimeline.js.map +1 -0
- package/dist/atheletebridge/components/StatusBadge.js +5 -5
- package/dist/atheletebridge/components/StatusBadge.js.map +1 -1
- package/dist/atheletebridge/components/TeamCard.js +65 -35
- package/dist/atheletebridge/components/TeamCard.js.map +1 -1
- package/dist/atheletebridge/components/UserAvatar.js +33 -0
- package/dist/atheletebridge/components/UserAvatar.js.map +1 -0
- package/dist/atheletebridge/components/domain.js +5 -2
- package/dist/atheletebridge/components/domain.js.map +1 -1
- package/dist/atheletebridge/components/viz/AvatarStack.js +2 -0
- package/dist/atheletebridge/components/viz/BarChart.js +83 -0
- package/dist/atheletebridge/components/viz/BarChart.js.map +1 -0
- package/dist/atheletebridge/components/viz/DonutChart.js +109 -0
- package/dist/atheletebridge/components/viz/DonutChart.js.map +1 -0
- package/dist/atheletebridge/components/viz/LineChart.js +123 -0
- package/dist/atheletebridge/components/viz/LineChart.js.map +1 -0
- package/dist/atheletebridge/components/viz/ProgressRing.js +6 -0
- package/dist/atheletebridge/components/viz/ProgressRing.js.map +1 -0
- package/dist/atheletebridge/components/viz/RatingStars.js +92 -0
- package/dist/atheletebridge/components/viz/RatingStars.js.map +1 -0
- package/dist/atheletebridge/components/viz/Sparkline.js +74 -0
- package/dist/atheletebridge/components/viz/Sparkline.js.map +1 -0
- package/dist/atheletebridge/components/viz/StatCard.js +60 -0
- package/dist/atheletebridge/components/viz/StatCard.js.map +1 -0
- package/dist/atheletebridge/components/viz/StepProgress.js +3 -0
- package/dist/atheletebridge/components/viz/Timeline.js +66 -0
- package/dist/atheletebridge/components/viz/Timeline.js.map +1 -0
- package/dist/atheletebridge/components/viz/index.js +13 -0
- package/dist/atheletebridge/components/viz/lib/Avatar.js +1 -0
- package/dist/atheletebridge/components/viz/lib/CartesianFrame.js +119 -0
- package/dist/atheletebridge/components/viz/lib/CartesianFrame.js.map +1 -0
- package/dist/atheletebridge/components/viz/lib/ResponsiveSvg.js +35 -0
- package/dist/atheletebridge/components/viz/lib/ResponsiveSvg.js.map +1 -0
- package/dist/atheletebridge/components/viz/lib/format.js +17 -0
- package/dist/atheletebridge/components/viz/lib/format.js.map +1 -0
- package/dist/atheletebridge/components/viz/lib/geometry.js +61 -0
- package/dist/atheletebridge/components/viz/lib/geometry.js.map +1 -0
- package/dist/atheletebridge/components/viz/lib/hooks.js +39 -0
- package/dist/atheletebridge/components/viz/lib/hooks.js.map +1 -0
- package/dist/atheletebridge/components/viz/lib/palette.js +27 -0
- package/dist/atheletebridge/components/viz/lib/palette.js.map +1 -0
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +369 -163
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
- package/dist/atheletebridge/pages/DashboardPage.js +182 -117
- package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
- package/dist/atheletebridge/pages/StandingsPage.js +119 -95
- package/dist/atheletebridge/pages/StandingsPage.js.map +1 -1
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js +372 -0
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js.map +1 -0
- package/dist/atheletebridge/pages/athletes/AthleteProfilePage.js +511 -0
- package/dist/atheletebridge/pages/athletes/AthleteProfilePage.js.map +1 -0
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js +120 -131
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureFormPage.js +264 -176
- package/dist/atheletebridge/pages/fixtures/FixtureFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.js +84 -47
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.js.map +1 -1
- package/dist/atheletebridge/pages/performance/PerformancePage.js +432 -281
- package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js +187 -123
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramsListPage.js +56 -39
- package/dist/atheletebridge/pages/programs/ProgramsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js +267 -139
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionFormPage.js +253 -162
- package/dist/atheletebridge/pages/sessions/SessionFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionsListPage.js +38 -36
- package/dist/atheletebridge/pages/sessions/SessionsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js +437 -189
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamFormPage.js +368 -177
- package/dist/atheletebridge/pages/teams/TeamFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamsListPage.js +3 -0
- package/dist/atheletebridge/pages/teams/TeamsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/tournaments/TournamentBracketPage.js +298 -0
- package/dist/atheletebridge/pages/tournaments/TournamentBracketPage.js.map +1 -0
- package/dist/atheletebridge/utils/identity.js +31 -0
- package/dist/atheletebridge/utils/identity.js.map +1 -0
- package/dist/atheletebridge/utils/timeAgo.js +20 -0
- package/dist/atheletebridge/utils/timeAgo.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,200 +1,291 @@
|
|
|
1
1
|
import { useAthleteBridge as e } from "../../context/AthleteBridgeContext.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
2
|
+
import { cn as t } from "../../utils/cn.js";
|
|
3
|
+
import { PageLayout as n } from "../../components/PageLayout.js";
|
|
4
|
+
import { SESSION_TYPE_LABELS as r, SPORT_ICONS as i } from "../../components/domain.js";
|
|
5
|
+
import { UserAvatar as a } from "../../components/UserAvatar.js";
|
|
6
|
+
import { useBookSession as o, usePrograms as s } from "../../hooks/useAthleteBridgeApi.js";
|
|
7
|
+
import { FormField as c, fieldInputClass as l } from "../../components/form.js";
|
|
8
|
+
import { AthleteSelect as u } from "../../components/AthleteSelect.js";
|
|
9
|
+
import { useState as d } from "react";
|
|
10
|
+
import { ArrowLeft as f, Blend as p, Calendar as m, Clock as h, Loader2 as g, Lock as _, Save as v, Users as y, Video as b } from "lucide-react";
|
|
11
|
+
import { jsx as x, jsxs as S } from "react/jsx-runtime";
|
|
12
|
+
import { GlassCard as C } from "@burdenoff/fe-libs/ui";
|
|
10
13
|
//#region src/atheletebridge/pages/sessions/SessionFormPage.tsx
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
var w = {
|
|
15
|
+
IN_PERSON: {
|
|
16
|
+
label: r.IN_PERSON,
|
|
17
|
+
icon: y
|
|
18
|
+
},
|
|
19
|
+
ONLINE: {
|
|
20
|
+
label: r.ONLINE,
|
|
21
|
+
icon: b
|
|
22
|
+
},
|
|
23
|
+
HYBRID: {
|
|
24
|
+
label: r.HYBRID,
|
|
25
|
+
icon: p
|
|
26
|
+
}
|
|
27
|
+
}, T = Object.keys(w);
|
|
28
|
+
function E() {
|
|
29
|
+
let { navigateTo: r, currentUser: p } = e(), y = s({
|
|
14
30
|
status: "PUBLISHED",
|
|
15
31
|
pagination: { limit: 100 }
|
|
16
|
-
}),
|
|
17
|
-
return /* @__PURE__ */
|
|
32
|
+
}), b = o(), E = p?.id ?? "", D = p?.name?.trim() || [p?.firstName, p?.lastName].filter(Boolean).join(" ").trim() || p?.email || "You", [O, k] = d(""), [A, j] = d(""), [M, N] = d(""), [P, F] = d(""), [I, L] = d("IN_PERSON"), [R, z] = d(""), [B, V] = d(""), [H, U] = d(""), [W, G] = d("60"), [K, q] = d(null), J = y.data?.items ?? [], Y = J.find((e) => e.id === M), X = Y ? i[Y.sport] : void 0;
|
|
33
|
+
return /* @__PURE__ */ x(n, {
|
|
18
34
|
title: "Book session",
|
|
19
35
|
description: "Schedule a coaching session between a coach and an athlete.",
|
|
20
|
-
actions: /* @__PURE__ */
|
|
36
|
+
actions: /* @__PURE__ */ S("button", {
|
|
21
37
|
type: "button",
|
|
22
|
-
onClick: () =>
|
|
38
|
+
onClick: () => r("/sessions"),
|
|
23
39
|
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",
|
|
24
|
-
children: [/* @__PURE__ */
|
|
40
|
+
children: [/* @__PURE__ */ x(f, { className: "h-4 w-4" }), " Cancel"]
|
|
25
41
|
}),
|
|
26
|
-
children: /* @__PURE__ */
|
|
42
|
+
children: /* @__PURE__ */ x("form", {
|
|
27
43
|
onSubmit: async (e) => {
|
|
28
|
-
if (e.preventDefault(),
|
|
29
|
-
|
|
44
|
+
if (e.preventDefault(), q(null), !E.trim()) {
|
|
45
|
+
q("Unable to identify you as the coach — please refresh and try again.");
|
|
30
46
|
return;
|
|
31
47
|
}
|
|
32
|
-
if (!
|
|
33
|
-
|
|
48
|
+
if (!O.trim()) {
|
|
49
|
+
q("Athlete ID is required.");
|
|
34
50
|
return;
|
|
35
51
|
}
|
|
36
|
-
if (!
|
|
37
|
-
|
|
52
|
+
if (!P.trim()) {
|
|
53
|
+
q("A session title is required.");
|
|
38
54
|
return;
|
|
39
55
|
}
|
|
40
|
-
if (!
|
|
41
|
-
|
|
56
|
+
if (!H) {
|
|
57
|
+
q("A scheduled date and time is required.");
|
|
42
58
|
return;
|
|
43
59
|
}
|
|
44
|
-
let t = Number.parseInt(
|
|
60
|
+
let t = Number.parseInt(W, 10);
|
|
45
61
|
if (Number.isNaN(t) || t <= 0) {
|
|
46
|
-
|
|
62
|
+
q("Duration must be a positive number of minutes.");
|
|
47
63
|
return;
|
|
48
64
|
}
|
|
49
65
|
try {
|
|
50
|
-
|
|
51
|
-
coachId:
|
|
52
|
-
athleteId:
|
|
53
|
-
programId:
|
|
54
|
-
title:
|
|
55
|
-
type:
|
|
56
|
-
location:
|
|
57
|
-
meetingUrl:
|
|
58
|
-
scheduledAt: new Date(
|
|
66
|
+
r(`/sessions/${(await b.mutateAsync({
|
|
67
|
+
coachId: E.trim(),
|
|
68
|
+
athleteId: O.trim(),
|
|
69
|
+
programId: M || void 0,
|
|
70
|
+
title: P.trim(),
|
|
71
|
+
type: I,
|
|
72
|
+
location: I === "ONLINE" ? void 0 : R.trim() || void 0,
|
|
73
|
+
meetingUrl: I === "IN_PERSON" ? void 0 : B.trim() || void 0,
|
|
74
|
+
scheduledAt: new Date(H).toISOString(),
|
|
59
75
|
durationMin: t
|
|
60
76
|
})).id}`);
|
|
61
77
|
} catch (e) {
|
|
62
|
-
|
|
78
|
+
q(e instanceof Error ? e.message : "Failed to book session.");
|
|
63
79
|
}
|
|
64
80
|
},
|
|
65
|
-
className: "max-w-
|
|
66
|
-
children:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
className: "
|
|
72
|
-
children: [/* @__PURE__ */
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
className: "mx-auto max-w-3xl",
|
|
82
|
+
children: /* @__PURE__ */ S(C, {
|
|
83
|
+
treatment: "insight",
|
|
84
|
+
className: "p-5 sm:p-7",
|
|
85
|
+
children: [
|
|
86
|
+
/* @__PURE__ */ S("div", {
|
|
87
|
+
className: "grid grid-cols-1 gap-5 sm:grid-cols-2",
|
|
88
|
+
children: [/* @__PURE__ */ x(c, {
|
|
89
|
+
label: "Coach",
|
|
90
|
+
hint: "You're booking this session as the coach.",
|
|
91
|
+
children: /* @__PURE__ */ S("div", {
|
|
92
|
+
className: "flex items-center gap-3 rounded-lg border border-border-seam bg-bg-sunken px-3 py-2 text-sm text-text-primary",
|
|
93
|
+
children: [
|
|
94
|
+
/* @__PURE__ */ x(a, {
|
|
95
|
+
name: D,
|
|
96
|
+
id: E,
|
|
97
|
+
size: "sm"
|
|
98
|
+
}),
|
|
99
|
+
/* @__PURE__ */ x("span", {
|
|
100
|
+
className: "min-w-0 flex-1 truncate font-medium",
|
|
101
|
+
children: D
|
|
102
|
+
}),
|
|
103
|
+
/* @__PURE__ */ x(_, {
|
|
104
|
+
className: "h-3.5 w-3.5 shrink-0 text-text-muted",
|
|
105
|
+
"aria-hidden": "true"
|
|
106
|
+
})
|
|
107
|
+
]
|
|
108
|
+
})
|
|
109
|
+
}), /* @__PURE__ */ x(c, {
|
|
110
|
+
label: "Title",
|
|
111
|
+
htmlFor: "title",
|
|
112
|
+
required: !0,
|
|
113
|
+
children: /* @__PURE__ */ x("input", {
|
|
114
|
+
id: "title",
|
|
115
|
+
className: l,
|
|
116
|
+
value: P,
|
|
117
|
+
onChange: (e) => F(e.target.value),
|
|
118
|
+
placeholder: "e.g. Sprint technique review"
|
|
119
|
+
})
|
|
78
120
|
})]
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
121
|
+
}),
|
|
122
|
+
/* @__PURE__ */ S("div", {
|
|
123
|
+
className: "mt-5",
|
|
124
|
+
children: [/* @__PURE__ */ x(u, {
|
|
125
|
+
athleteId: O,
|
|
126
|
+
onSelect: ({ athleteId: e, displayName: t }) => {
|
|
127
|
+
k(e), j(t);
|
|
128
|
+
},
|
|
129
|
+
required: !0,
|
|
130
|
+
idPrefix: "session-athlete"
|
|
131
|
+
}), O && A ? /* @__PURE__ */ S("div", {
|
|
132
|
+
className: "mt-3 inline-flex items-center gap-2 rounded-full border border-border-seam bg-bg-sunken py-1 pl-1 pr-3",
|
|
133
|
+
children: [/* @__PURE__ */ x(a, {
|
|
134
|
+
name: A,
|
|
135
|
+
id: O,
|
|
136
|
+
size: "xs"
|
|
137
|
+
}), /* @__PURE__ */ x("span", {
|
|
138
|
+
className: "text-xs font-medium text-text-primary",
|
|
139
|
+
children: A
|
|
140
|
+
})]
|
|
141
|
+
}) : null]
|
|
142
|
+
}),
|
|
143
|
+
/* @__PURE__ */ S("div", {
|
|
144
|
+
className: "mt-5",
|
|
145
|
+
children: [/* @__PURE__ */ x("span", {
|
|
146
|
+
className: "mb-1.5 block text-sm font-medium text-text-primary",
|
|
147
|
+
children: "Type"
|
|
148
|
+
}), /* @__PURE__ */ x("div", {
|
|
149
|
+
role: "radiogroup",
|
|
150
|
+
"aria-label": "Session type",
|
|
151
|
+
onKeyDown: (e) => {
|
|
152
|
+
let t = T.indexOf(I);
|
|
153
|
+
e.key === "ArrowRight" || e.key === "ArrowDown" ? (e.preventDefault(), L(T[(t + 1) % T.length])) : (e.key === "ArrowLeft" || e.key === "ArrowUp") && (e.preventDefault(), L(T[(t - 1 + T.length) % T.length]));
|
|
154
|
+
},
|
|
155
|
+
className: "grid grid-cols-3 gap-2",
|
|
156
|
+
children: T.map((e) => {
|
|
157
|
+
let { label: n, icon: r } = w[e], i = I === e;
|
|
158
|
+
return /* @__PURE__ */ S("button", {
|
|
159
|
+
type: "button",
|
|
160
|
+
role: "radio",
|
|
161
|
+
"aria-checked": i,
|
|
162
|
+
tabIndex: i ? 0 : -1,
|
|
163
|
+
onClick: () => L(e),
|
|
164
|
+
className: t("flex items-center justify-center gap-2 rounded-lg border px-3 py-2.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]", i ? "border-action-primary-bg bg-[var(--color-accent-soft)] text-text-primary ring-1 ring-inset ring-action-primary-bg" : "border-border-seam text-text-secondary hover:bg-bg-sunken"),
|
|
165
|
+
children: [/* @__PURE__ */ x(r, {
|
|
166
|
+
className: "h-4 w-4 shrink-0",
|
|
167
|
+
"aria-hidden": "true"
|
|
168
|
+
}), n]
|
|
169
|
+
}, e);
|
|
170
|
+
})
|
|
171
|
+
})]
|
|
172
|
+
}),
|
|
173
|
+
/* @__PURE__ */ x("div", {
|
|
174
|
+
className: "mt-5",
|
|
175
|
+
children: /* @__PURE__ */ x(c, {
|
|
176
|
+
label: "Program",
|
|
177
|
+
htmlFor: "program",
|
|
178
|
+
hint: "Optional — tie to a published plan.",
|
|
179
|
+
children: /* @__PURE__ */ S("div", {
|
|
180
|
+
className: "relative",
|
|
181
|
+
children: [X ? /* @__PURE__ */ x(X, {
|
|
182
|
+
className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary",
|
|
183
|
+
"aria-hidden": "true"
|
|
184
|
+
}) : null, /* @__PURE__ */ S("select", {
|
|
185
|
+
id: "program",
|
|
186
|
+
className: t(l, X && "pl-9"),
|
|
187
|
+
value: M,
|
|
188
|
+
onChange: (e) => N(e.target.value),
|
|
189
|
+
children: [/* @__PURE__ */ x("option", {
|
|
190
|
+
value: "",
|
|
191
|
+
children: "No program"
|
|
192
|
+
}), J.map((e) => /* @__PURE__ */ x("option", {
|
|
193
|
+
value: e.id,
|
|
194
|
+
children: e.name
|
|
195
|
+
}, e.id))]
|
|
196
|
+
})]
|
|
197
|
+
})
|
|
130
198
|
})
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
O !== "IN_PERSON" && /* @__PURE__ */ p(a, {
|
|
145
|
-
label: "Meeting URL",
|
|
146
|
-
htmlFor: "meetingUrl",
|
|
147
|
-
children: /* @__PURE__ */ p("input", {
|
|
148
|
-
id: "meetingUrl",
|
|
149
|
-
className: o,
|
|
150
|
-
value: M,
|
|
151
|
-
onChange: (e) => N(e.target.value),
|
|
152
|
-
placeholder: "https://meet.example.com/…"
|
|
153
|
-
})
|
|
154
|
-
}),
|
|
155
|
-
/* @__PURE__ */ m("div", {
|
|
156
|
-
className: "grid grid-cols-1 gap-5 sm:grid-cols-2",
|
|
157
|
-
children: [/* @__PURE__ */ p(a, {
|
|
158
|
-
label: "Scheduled at",
|
|
159
|
-
htmlFor: "scheduledAt",
|
|
160
|
-
required: !0,
|
|
161
|
-
children: /* @__PURE__ */ p("input", {
|
|
162
|
-
id: "scheduledAt",
|
|
163
|
-
type: "datetime-local",
|
|
164
|
-
className: o,
|
|
165
|
-
value: P,
|
|
166
|
-
onChange: (e) => F(e.target.value)
|
|
199
|
+
}),
|
|
200
|
+
I !== "ONLINE" && /* @__PURE__ */ x("div", {
|
|
201
|
+
className: "mt-5",
|
|
202
|
+
children: /* @__PURE__ */ x(c, {
|
|
203
|
+
label: "Location",
|
|
204
|
+
htmlFor: "location",
|
|
205
|
+
children: /* @__PURE__ */ x("input", {
|
|
206
|
+
id: "location",
|
|
207
|
+
className: l,
|
|
208
|
+
value: R,
|
|
209
|
+
onChange: (e) => z(e.target.value),
|
|
210
|
+
placeholder: "e.g. Main gym, Court 2"
|
|
211
|
+
})
|
|
167
212
|
})
|
|
168
|
-
}),
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
children: /* @__PURE__ */
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
213
|
+
}),
|
|
214
|
+
I !== "IN_PERSON" && /* @__PURE__ */ x("div", {
|
|
215
|
+
className: "mt-5",
|
|
216
|
+
children: /* @__PURE__ */ x(c, {
|
|
217
|
+
label: "Meeting URL",
|
|
218
|
+
htmlFor: "meetingUrl",
|
|
219
|
+
children: /* @__PURE__ */ x("input", {
|
|
220
|
+
id: "meetingUrl",
|
|
221
|
+
className: l,
|
|
222
|
+
value: B,
|
|
223
|
+
onChange: (e) => V(e.target.value),
|
|
224
|
+
placeholder: "https://meet.example.com/…"
|
|
225
|
+
})
|
|
177
226
|
})
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
227
|
+
}),
|
|
228
|
+
/* @__PURE__ */ S("div", {
|
|
229
|
+
className: "mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2",
|
|
230
|
+
children: [/* @__PURE__ */ x(c, {
|
|
231
|
+
label: "Scheduled at",
|
|
232
|
+
htmlFor: "scheduledAt",
|
|
233
|
+
required: !0,
|
|
234
|
+
children: /* @__PURE__ */ S("div", {
|
|
235
|
+
className: "relative",
|
|
236
|
+
children: [/* @__PURE__ */ x(m, {
|
|
237
|
+
className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary",
|
|
238
|
+
"aria-hidden": "true"
|
|
239
|
+
}), /* @__PURE__ */ x("input", {
|
|
240
|
+
id: "scheduledAt",
|
|
241
|
+
type: "datetime-local",
|
|
242
|
+
className: t(l, "pl-9"),
|
|
243
|
+
value: H,
|
|
244
|
+
onChange: (e) => U(e.target.value)
|
|
245
|
+
})]
|
|
246
|
+
})
|
|
247
|
+
}), /* @__PURE__ */ x(c, {
|
|
248
|
+
label: "Duration (minutes)",
|
|
249
|
+
htmlFor: "duration",
|
|
250
|
+
children: /* @__PURE__ */ S("div", {
|
|
251
|
+
className: "relative",
|
|
252
|
+
children: [/* @__PURE__ */ x(h, {
|
|
253
|
+
className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary",
|
|
254
|
+
"aria-hidden": "true"
|
|
255
|
+
}), /* @__PURE__ */ x("input", {
|
|
256
|
+
id: "duration",
|
|
257
|
+
inputMode: "numeric",
|
|
258
|
+
className: t(l, "pl-9"),
|
|
259
|
+
value: W,
|
|
260
|
+
onChange: (e) => G(e.target.value)
|
|
261
|
+
})]
|
|
262
|
+
})
|
|
263
|
+
})]
|
|
264
|
+
}),
|
|
265
|
+
K && /* @__PURE__ */ x("p", {
|
|
266
|
+
className: "mt-5 text-sm text-status-error-text",
|
|
267
|
+
children: K
|
|
268
|
+
}),
|
|
269
|
+
/* @__PURE__ */ S("div", {
|
|
270
|
+
className: "mt-6 flex items-center justify-end gap-3 border-t border-border-seam pt-5",
|
|
271
|
+
children: [/* @__PURE__ */ x("button", {
|
|
272
|
+
type: "button",
|
|
273
|
+
onClick: () => r("/sessions"),
|
|
274
|
+
className: "rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken",
|
|
275
|
+
children: "Cancel"
|
|
276
|
+
}), /* @__PURE__ */ S("button", {
|
|
277
|
+
type: "submit",
|
|
278
|
+
disabled: b.isPending,
|
|
279
|
+
className: "inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60",
|
|
280
|
+
children: [b.isPending ? /* @__PURE__ */ x(g, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ x(v, { className: "h-4 w-4" }), "Book session"]
|
|
281
|
+
})]
|
|
191
282
|
})
|
|
192
|
-
|
|
193
|
-
|
|
283
|
+
]
|
|
284
|
+
})
|
|
194
285
|
})
|
|
195
286
|
});
|
|
196
287
|
}
|
|
197
288
|
//#endregion
|
|
198
|
-
export {
|
|
289
|
+
export { E as SessionFormPage };
|
|
199
290
|
|
|
200
291
|
//# sourceMappingURL=SessionFormPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionFormPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/sessions/SessionFormPage.tsx"],"sourcesContent":["/**\n * Book Session Form\n * @module atheletebridge/pages/sessions\n */\n\nimport { useState } from 'react';\nimport { ArrowLeft, Loader2, Save, User as UserIcon } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { AthleteSelect } from '../../components/AthleteSelect';\nimport { SESSION_TYPE_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useBookSession, usePrograms } from '../../hooks/useAthleteBridgeApi';\nimport type { AthSessionType } from '../../types';\n\nconst TYPES = Object.keys(SESSION_TYPE_LABELS) as AthSessionType[];\n\nexport function SessionFormPage() {\n const { navigateTo, currentUser } = useAthleteBridge();\n\n const programs = usePrograms({ status: 'PUBLISHED', pagination: { limit: 100 } });\n const bookSession = useBookSession();\n\n // The coach is always the current user — sent under the hood, shown read-only.\n const coachId = currentUser?.id ?? '';\n const coachName =\n currentUser?.name?.trim() ||\n [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ').trim() ||\n currentUser?.email ||\n 'You';\n\n const [athleteId, setAthleteId] = useState('');\n const [programId, setProgramId] = useState('');\n const [title, setTitle] = useState('');\n const [type, setType] = useState<AthSessionType>('IN_PERSON');\n const [location, setLocation] = useState('');\n const [meetingUrl, setMeetingUrl] = useState('');\n const [scheduledAt, setScheduledAt] = useState('');\n const [durationMin, setDurationMin] = useState('60');\n const [error, setError] = useState<string | null>(null);\n\n const programOptions = programs.data?.items ?? [];\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!coachId.trim()) {\n setError('Unable to identify you as the coach — please refresh and try again.');\n return;\n }\n if (!athleteId.trim()) {\n setError('Athlete ID is required.');\n return;\n }\n if (!title.trim()) {\n setError('A session title is required.');\n return;\n }\n if (!scheduledAt) {\n setError('A scheduled date and time is required.');\n return;\n }\n const duration = Number.parseInt(durationMin, 10);\n if (Number.isNaN(duration) || duration <= 0) {\n setError('Duration must be a positive number of minutes.');\n return;\n }\n\n try {\n const created = await bookSession.mutateAsync({\n coachId: coachId.trim(),\n athleteId: athleteId.trim(),\n programId: programId || undefined,\n title: title.trim(),\n type,\n location: type !== 'ONLINE' ? location.trim() || undefined : undefined,\n meetingUrl: type !== 'IN_PERSON' ? meetingUrl.trim() || undefined : undefined,\n scheduledAt: new Date(scheduledAt).toISOString(),\n durationMin: duration,\n });\n navigateTo(`/sessions/${created.id}`);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to book session.');\n }\n };\n\n return (\n <PageLayout\n title=\"Book session\"\n description=\"Schedule a coaching session between a coach and an athlete.\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions')}\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=\"Coach\" hint=\"You're booking this session as the coach.\">\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-sunken px-3 py-2 text-sm text-text-primary\">\n <span className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-bg-surface text-text-secondary\">\n <UserIcon className=\"h-4 w-4\" />\n </span>\n <span className=\"min-w-0 truncate font-medium\">{coachName}</span>\n </div>\n </FormField>\n\n <AthleteSelect\n athleteId={athleteId}\n onSelect={({ athleteId: id }) => setAthleteId(id)}\n required\n idPrefix=\"session-athlete\"\n />\n\n <FormField label=\"Title\" htmlFor=\"title\" required>\n <input\n id=\"title\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Sprint technique review\"\n />\n </FormField>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Type\" htmlFor=\"type\">\n <select\n id=\"type\"\n className={fieldInputClass}\n value={type}\n onChange={(e) => setType(e.target.value as AthSessionType)}\n >\n {TYPES.map((tp) => (\n <option key={tp} value={tp}>\n {SESSION_TYPE_LABELS[tp]}\n </option>\n ))}\n </select>\n </FormField>\n\n <FormField label=\"Program\" htmlFor=\"program\" hint=\"Optional — tie to a published plan.\">\n <select\n id=\"program\"\n className={fieldInputClass}\n value={programId}\n onChange={(e) => setProgramId(e.target.value)}\n >\n <option value=\"\">No program</option>\n {programOptions.map((pr) => (\n <option key={pr.id} value={pr.id}>\n {pr.name}\n </option>\n ))}\n </select>\n </FormField>\n </div>\n\n {type !== 'ONLINE' && (\n <FormField label=\"Location\" htmlFor=\"location\">\n <input\n id=\"location\"\n className={fieldInputClass}\n value={location}\n onChange={(e) => setLocation(e.target.value)}\n placeholder=\"e.g. Main gym, Court 2\"\n />\n </FormField>\n )}\n\n {type !== 'IN_PERSON' && (\n <FormField label=\"Meeting URL\" htmlFor=\"meetingUrl\">\n <input\n id=\"meetingUrl\"\n className={fieldInputClass}\n value={meetingUrl}\n onChange={(e) => setMeetingUrl(e.target.value)}\n placeholder=\"https://meet.example.com/…\"\n />\n </FormField>\n )}\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Scheduled at\" htmlFor=\"scheduledAt\" required>\n <input\n id=\"scheduledAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={scheduledAt}\n onChange={(e) => setScheduledAt(e.target.value)}\n />\n </FormField>\n <FormField label=\"Duration (minutes)\" htmlFor=\"duration\">\n <input\n id=\"duration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </FormField>\n </div>\n\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n\n <div className=\"pt-2\">\n <button\n type=\"submit\"\n disabled={bookSession.isPending}\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 {bookSession.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Book session\n </button>\n </div>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SessionFormPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/sessions/SessionFormPage.tsx"],"sourcesContent":["/**\n * Book Session Form\n * @module atheletebridge/pages/sessions\n *\n * Mirrors the concept mock: a GlassCard-framed two-column form with a read-only\n * coach identity (the current user), a team→athlete picker, a segmented session\n * type control, and iconized schedule inputs. Every field maps to a real\n * AthBookSessionInput property — nothing is fabricated. Semantic tokens only.\n */\n\nimport { type KeyboardEvent, useState } from 'react';\nimport {\n ArrowLeft,\n Loader2,\n Save,\n Lock,\n Users,\n Video,\n Blend,\n Calendar,\n Clock,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { AthleteSelect } from '../../components/AthleteSelect';\nimport { UserAvatar } from '../../components/UserAvatar';\nimport { SPORT_ICONS, SESSION_TYPE_LABELS } from '../../components/domain';\nimport { cn } from '../../utils/cn';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useBookSession, usePrograms } from '../../hooks/useAthleteBridgeApi';\nimport type { AthSessionType } from '../../types';\n\nconst TYPE_META: Record<AthSessionType, { label: string; icon: LucideIcon }> = {\n IN_PERSON: { label: SESSION_TYPE_LABELS.IN_PERSON, icon: Users },\n ONLINE: { label: SESSION_TYPE_LABELS.ONLINE, icon: Video },\n HYBRID: { label: SESSION_TYPE_LABELS.HYBRID, icon: Blend },\n};\nconst TYPES = Object.keys(TYPE_META) as AthSessionType[];\n\nexport function SessionFormPage() {\n const { navigateTo, currentUser } = useAthleteBridge();\n\n const programs = usePrograms({ status: 'PUBLISHED', pagination: { limit: 100 } });\n const bookSession = useBookSession();\n\n // The coach is always the current user — sent under the hood, shown read-only.\n const coachId = currentUser?.id ?? '';\n const coachName =\n currentUser?.name?.trim() ||\n [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ').trim() ||\n currentUser?.email ||\n 'You';\n\n const [athleteId, setAthleteId] = useState('');\n const [athleteName, setAthleteName] = useState('');\n const [programId, setProgramId] = useState('');\n const [title, setTitle] = useState('');\n const [type, setType] = useState<AthSessionType>('IN_PERSON');\n const [location, setLocation] = useState('');\n const [meetingUrl, setMeetingUrl] = useState('');\n const [scheduledAt, setScheduledAt] = useState('');\n const [durationMin, setDurationMin] = useState('60');\n const [error, setError] = useState<string | null>(null);\n\n const programOptions = programs.data?.items ?? [];\n const selectedProgram = programOptions.find((pr) => pr.id === programId);\n const ProgramSportIcon = selectedProgram ? SPORT_ICONS[selectedProgram.sport] : undefined;\n\n const handleTypeKey = (e: KeyboardEvent<HTMLDivElement>) => {\n const idx = TYPES.indexOf(type);\n if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n e.preventDefault();\n setType(TYPES[(idx + 1) % TYPES.length]);\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n e.preventDefault();\n setType(TYPES[(idx - 1 + TYPES.length) % TYPES.length]);\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!coachId.trim()) {\n setError('Unable to identify you as the coach — please refresh and try again.');\n return;\n }\n if (!athleteId.trim()) {\n setError('Athlete ID is required.');\n return;\n }\n if (!title.trim()) {\n setError('A session title is required.');\n return;\n }\n if (!scheduledAt) {\n setError('A scheduled date and time is required.');\n return;\n }\n const duration = Number.parseInt(durationMin, 10);\n if (Number.isNaN(duration) || duration <= 0) {\n setError('Duration must be a positive number of minutes.');\n return;\n }\n\n try {\n const created = await bookSession.mutateAsync({\n coachId: coachId.trim(),\n athleteId: athleteId.trim(),\n programId: programId || undefined,\n title: title.trim(),\n type,\n location: type !== 'ONLINE' ? location.trim() || undefined : undefined,\n meetingUrl: type !== 'IN_PERSON' ? meetingUrl.trim() || undefined : undefined,\n scheduledAt: new Date(scheduledAt).toISOString(),\n durationMin: duration,\n });\n navigateTo(`/sessions/${created.id}`);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to book session.');\n }\n };\n\n return (\n <PageLayout\n title=\"Book session\"\n description=\"Schedule a coaching session between a coach and an athlete.\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-3xl\">\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-7\">\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n {/* Coach (read-only self) */}\n <FormField label=\"Coach\" hint=\"You're booking this session as the coach.\">\n <div className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-sunken px-3 py-2 text-sm text-text-primary\">\n <UserAvatar name={coachName} id={coachId} size=\"sm\" />\n <span className=\"min-w-0 flex-1 truncate font-medium\">{coachName}</span>\n <Lock className=\"h-3.5 w-3.5 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n </div>\n </FormField>\n\n {/* Title */}\n <FormField label=\"Title\" htmlFor=\"title\" required>\n <input\n id=\"title\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Sprint technique review\"\n />\n </FormField>\n </div>\n\n {/* Team → athlete picker (spans full width) */}\n <div className=\"mt-5\">\n <AthleteSelect\n athleteId={athleteId}\n onSelect={({ athleteId: id, displayName }) => {\n setAthleteId(id);\n setAthleteName(displayName);\n }}\n required\n idPrefix=\"session-athlete\"\n />\n {athleteId && athleteName ? (\n <div className=\"mt-3 inline-flex items-center gap-2 rounded-full border border-border-seam bg-bg-sunken py-1 pl-1 pr-3\">\n <UserAvatar name={athleteName} id={athleteId} size=\"xs\" />\n <span className=\"text-xs font-medium text-text-primary\">{athleteName}</span>\n </div>\n ) : null}\n </div>\n\n {/* Type — segmented control */}\n <div className=\"mt-5\">\n <span className=\"mb-1.5 block text-sm font-medium text-text-primary\">Type</span>\n <div\n role=\"radiogroup\"\n aria-label=\"Session type\"\n onKeyDown={handleTypeKey}\n className=\"grid grid-cols-3 gap-2\"\n >\n {TYPES.map((tp) => {\n const { label, icon: Icon } = TYPE_META[tp];\n const active = type === tp;\n return (\n <button\n key={tp}\n type=\"button\"\n role=\"radio\"\n aria-checked={active}\n tabIndex={active ? 0 : -1}\n onClick={() => setType(tp)}\n className={cn(\n 'flex items-center justify-center gap-2 rounded-lg border px-3 py-2.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n active\n ? 'border-action-primary-bg bg-[var(--color-accent-soft)] text-text-primary ring-1 ring-inset ring-action-primary-bg'\n : 'border-border-seam text-text-secondary hover:bg-bg-sunken'\n )}\n >\n <Icon className=\"h-4 w-4 shrink-0\" aria-hidden=\"true\" />\n {label}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Program */}\n <div className=\"mt-5\">\n <FormField label=\"Program\" htmlFor=\"program\" hint=\"Optional — tie to a published plan.\">\n <div className=\"relative\">\n {ProgramSportIcon ? (\n <ProgramSportIcon\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n ) : null}\n <select\n id=\"program\"\n className={cn(fieldInputClass, ProgramSportIcon && 'pl-9')}\n value={programId}\n onChange={(e) => setProgramId(e.target.value)}\n >\n <option value=\"\">No program</option>\n {programOptions.map((pr) => (\n <option key={pr.id} value={pr.id}>\n {pr.name}\n </option>\n ))}\n </select>\n </div>\n </FormField>\n </div>\n\n {type !== 'ONLINE' && (\n <div className=\"mt-5\">\n <FormField label=\"Location\" htmlFor=\"location\">\n <input\n id=\"location\"\n className={fieldInputClass}\n value={location}\n onChange={(e) => setLocation(e.target.value)}\n placeholder=\"e.g. Main gym, Court 2\"\n />\n </FormField>\n </div>\n )}\n\n {type !== 'IN_PERSON' && (\n <div className=\"mt-5\">\n <FormField label=\"Meeting URL\" htmlFor=\"meetingUrl\">\n <input\n id=\"meetingUrl\"\n className={fieldInputClass}\n value={meetingUrl}\n onChange={(e) => setMeetingUrl(e.target.value)}\n placeholder=\"https://meet.example.com/…\"\n />\n </FormField>\n </div>\n )}\n\n <div className=\"mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Scheduled at\" htmlFor=\"scheduledAt\" required>\n <div className=\"relative\">\n <Calendar\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"scheduledAt\"\n type=\"datetime-local\"\n className={cn(fieldInputClass, 'pl-9')}\n value={scheduledAt}\n onChange={(e) => setScheduledAt(e.target.value)}\n />\n </div>\n </FormField>\n <FormField label=\"Duration (minutes)\" htmlFor=\"duration\">\n <div className=\"relative\">\n <Clock\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"duration\"\n inputMode=\"numeric\"\n className={cn(fieldInputClass, 'pl-9')}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </div>\n </FormField>\n </div>\n\n {error && <p className=\"mt-5 text-sm text-status-error-text\">{error}</p>}\n\n {/* Footer */}\n <div className=\"mt-6 flex items-center justify-end gap-3 border-t border-border-seam pt-5\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions')}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={bookSession.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {bookSession.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Book session\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|