@odla-ai/chapter 0.7.0 → 0.9.0

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/ui/index.js CHANGED
@@ -1,798 +1,23 @@
1
- // src/ui/admin.tsx
2
- import { useEffect as useEffect2, useMemo, useState as useState2 } from "react";
3
- import { ClerkGate, SignedIn, SignedOut, SignIn, useClerkAuth, clerkAppearanceFromTokens } from "@odla-ai/auth-clerk";
4
- import { CrmClient } from "@odla-ai/crm";
5
-
6
- // src/ui/chrome.tsx
7
- import { useCallback, useEffect, useState } from "react";
8
- import { TopBar, TopBarLink } from "@odla-ai/ui/components";
9
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
- var badgeText = (brand) => brand.badge ?? brand.name.slice(0, 3).toUpperCase();
11
- var brandDisplay = (brand) => brand.wordmark ?? brand.name;
12
- var S = {
13
- gate: {
14
- minHeight: "100vh",
15
- display: "grid",
16
- placeItems: "center",
17
- padding: 24,
18
- background: "radial-gradient(900px 480px at 50% -8%, var(--ui-accent-soft), transparent 70%), radial-gradient(700px 500px at 110% 10%, var(--ui-good-soft), transparent 60%)"
19
- },
20
- gateInner: { width: "100%", maxWidth: 400, display: "flex", flexDirection: "column", alignItems: "center" },
21
- brandBox: { textAlign: "center", marginBottom: 22 },
22
- badge: {
23
- width: 52,
24
- height: 52,
25
- margin: "0 auto 14px",
26
- display: "grid",
27
- placeItems: "center",
28
- fontSize: 15,
29
- fontWeight: 700,
30
- color: "var(--ui-on-accent)",
31
- background: "linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))",
32
- borderRadius: 14,
33
- boxShadow: "0 10px 28px var(--ui-accent-soft)"
34
- },
35
- badgeSm: {
36
- width: 26,
37
- height: 26,
38
- display: "grid",
39
- placeItems: "center",
40
- fontSize: 10,
41
- fontWeight: 700,
42
- color: "var(--ui-on-accent)",
43
- background: "linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))",
44
- borderRadius: 7,
45
- marginRight: 9
46
- },
47
- h1: { margin: 0, fontSize: 28, letterSpacing: "-0.02em", fontWeight: 700 },
48
- muted: { color: "var(--ui-text-muted)" },
49
- role: {
50
- fontFamily: "var(--ui-font-mono)",
51
- fontSize: 11,
52
- textTransform: "uppercase",
53
- letterSpacing: "0.04em",
54
- padding: "2px 8px",
55
- borderRadius: 999,
56
- background: "var(--ui-accent-soft)",
57
- border: "1px solid var(--ui-accent)",
58
- color: "var(--ui-accent-strong)"
59
- },
60
- whoami: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 },
61
- // Sections own their own width/padding (e.g. a .wrap container).
62
- main: { minHeight: "calc(100vh - 61px)" },
63
- card: { width: "100%", textAlign: "center" }
64
- };
65
- function Gate(props) {
66
- const { brand, tagline, children } = props;
67
- return /* @__PURE__ */ jsx("div", { style: S.gate, children: /* @__PURE__ */ jsxs("div", { style: S.gateInner, children: [
68
- /* @__PURE__ */ jsxs("div", { style: S.brandBox, children: [
69
- /* @__PURE__ */ jsx("div", { style: S.badge, children: badgeText(brand) }),
70
- /* @__PURE__ */ jsx("h1", { style: S.h1, children: brandDisplay(brand) }),
71
- tagline ? /* @__PURE__ */ jsx("p", { style: { ...S.muted, margin: "8px 0 0", fontSize: 14 }, children: tagline }) : null
72
- ] }),
73
- children
74
- ] }) });
75
- }
76
- function AdminShell(props) {
77
- const { sections, basePath, brand, client, getToken, signOut, email } = props;
78
- const sectionFromPath = useCallback(() => {
79
- const fallback = sections[0]?.id ?? "";
80
- if (typeof window === "undefined") return fallback;
81
- const rest = window.location.pathname.slice(basePath.length).replace(/^\//, "");
82
- const id = rest.split("/")[0] ?? "";
83
- return sections.some((s) => s.id === id) ? id : fallback;
84
- }, [sections, basePath]);
85
- const [section, setSection] = useState(sectionFromPath);
86
- const go = useCallback(
87
- (id) => {
88
- window.history.pushState(null, "", `${basePath}/${id}`);
89
- setSection(id);
90
- },
91
- [basePath]
92
- );
93
- useEffect(() => {
94
- const onPop = () => setSection(sectionFromPath());
95
- window.addEventListener("popstate", onPop);
96
- return () => window.removeEventListener("popstate", onPop);
97
- }, [sectionFromPath]);
98
- const active = sections.find((s) => s.id === section) ?? sections[0];
99
- return /* @__PURE__ */ jsxs(Fragment, { children: [
100
- /* @__PURE__ */ jsxs(TopBar, { children: [
101
- /* @__PURE__ */ jsxs("a", { className: "topbar-brand", href: "/", style: { display: "inline-flex", alignItems: "center" }, children: [
102
- /* @__PURE__ */ jsx("span", { style: S.badgeSm, children: badgeText(brand) }),
103
- brandDisplay(brand)
104
- ] }),
105
- /* @__PURE__ */ jsx("nav", { className: "topbar-nav", "aria-label": "Admin navigation", children: sections.map((s) => /* @__PURE__ */ jsx(TopBarLink, { active: section === s.id, onClick: () => go(s.id), children: s.label }, s.id)) }),
106
- /* @__PURE__ */ jsx("div", { className: "topbar-spacer" }),
107
- /* @__PURE__ */ jsxs("div", { className: "topbar-actions", children: [
108
- /* @__PURE__ */ jsxs("span", { style: S.whoami, children: [
109
- /* @__PURE__ */ jsx("span", { style: S.role, children: "admin" }),
110
- email ? /* @__PURE__ */ jsx("span", { style: S.muted, children: email }) : null
111
- ] }),
112
- /* @__PURE__ */ jsx("button", { className: "btn secondary mini", onClick: () => signOut(), children: "Sign out" })
113
- ] })
114
- ] }),
115
- /* @__PURE__ */ jsx("main", { className: "shell-main", style: S.main, children: active ? active.render({ client, getToken, navigate: go }) : null })
116
- ] });
117
- }
118
-
119
- // src/ui/admin.tsx
120
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
121
- function Authed(props) {
122
- const { sections, basePath, brand, crmBasePath, apiBase } = props;
123
- const { getToken, signOut } = useClerkAuth();
124
- const client = useMemo(
125
- () => new CrmClient({
126
- basePath: crmBasePath,
127
- headers: async () => {
128
- const t = await getToken();
129
- return t ? { authorization: `Bearer ${t}` } : {};
130
- }
131
- }),
132
- [getToken, crmBasePath]
133
- );
134
- const [state, setState] = useState2({
135
- status: "checking"
136
- });
137
- useEffect2(() => {
138
- let live = true;
139
- void (async () => {
140
- try {
141
- const t = await getToken();
142
- const res = await fetch(`${apiBase}/api/me`, { headers: t ? { authorization: `Bearer ${t}` } : {} });
143
- const body = await res.json().catch(() => ({}));
144
- if (live) setState({ status: body.authorized ? "ok" : "denied", email: body.email ?? null });
145
- } catch {
146
- if (live) setState({ status: "denied" });
147
- }
148
- })();
149
- return () => {
150
- live = false;
151
- };
152
- }, [getToken, apiBase]);
153
- if (state.status === "checking") {
154
- return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsx2("p", { style: S.muted, children: "Checking access\u2026" }) });
155
- }
156
- if (state.status === "denied") {
157
- return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsxs2("div", { className: "card", style: S.card, children: [
158
- /* @__PURE__ */ jsx2("h2", { style: { marginTop: 0 }, children: "Not authorized" }),
159
- /* @__PURE__ */ jsxs2("p", { style: S.muted, children: [
160
- state.email ? `${state.email} isn't` : "This account isn't",
161
- " on the admin list. Ask an existing admin to add you in odla Studio."
162
- ] }),
163
- /* @__PURE__ */ jsx2("button", { className: "btn secondary", onClick: () => signOut(), style: { marginTop: 12 }, children: "Sign out" })
164
- ] }) });
165
- }
166
- return /* @__PURE__ */ jsx2(
167
- AdminShell,
168
- {
169
- sections,
170
- basePath,
171
- brand,
172
- client,
173
- getToken,
174
- signOut,
175
- email: state.email ?? null
176
- }
177
- );
178
- }
179
- function ChapterAdmin(props) {
180
- const sections = props.sections;
181
- const basePath = props.basePath ?? "/admin";
182
- const brand = props.brand ?? { name: "Admin" };
183
- const crmBasePath = props.crmBasePath ?? "/api/crm";
184
- const apiBase = props.apiBase ?? "";
185
- const [pk, setPk] = useState2(void 0);
186
- useEffect2(() => {
187
- let live = true;
188
- void (async () => {
189
- try {
190
- const res = await fetch(`${apiBase}/api/config`);
191
- const body = await res.json();
192
- if (live) setPk(body.clerkPublishableKey ?? null);
193
- } catch {
194
- if (live) setPk(null);
195
- }
196
- })();
197
- return () => {
198
- live = false;
199
- };
200
- }, [apiBase]);
201
- if (pk === void 0) {
202
- return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsx2("p", { style: S.muted, children: "Loading\u2026" }) });
203
- }
204
- if (!pk) {
205
- return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsxs2("div", { className: "card", style: S.card, children: [
206
- /* @__PURE__ */ jsx2("h2", { style: { marginTop: 0 }, children: "Sign-in not configured" }),
207
- /* @__PURE__ */ jsx2("p", { style: S.muted, children: "No Clerk publishable key is set for this environment yet." })
208
- ] }) });
209
- }
210
- return /* @__PURE__ */ jsxs2(ClerkGate, { publishableKey: pk, appearance: clerkAppearanceFromTokens(), afterSignOutUrl: "/", children: [
211
- /* @__PURE__ */ jsx2(SignedOut, { children: /* @__PURE__ */ jsx2(Gate, { brand, tagline: "Admin sign-in \u2014 invite only", children: /* @__PURE__ */ jsx2(SignIn, { routing: "hash", forceRedirectUrl: basePath, signUpForceRedirectUrl: basePath }) }) }),
212
- /* @__PURE__ */ jsx2(SignedIn, { children: /* @__PURE__ */ jsx2(Authed, { sections, basePath, brand, crmBasePath, apiBase }) })
213
- ] });
214
- }
215
-
216
- // src/ui/admin-people.tsx
217
- import { useState as useState3 } from "react";
218
- import { CrmList, RecordPanel, useCrmQuery, useCrmRecord } from "@odla-ai/crm/ui";
219
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
220
- function PeopleBody(props) {
221
- const { crm, client, type } = props;
222
- const hookClient = client;
223
- const query = useCrmQuery(hookClient, type);
224
- const [openId, setOpenId] = useState3(null);
225
- const record = useCrmRecord(hookClient, openId);
226
- const [saving, setSaving] = useState3(false);
227
- const saveFields = async (input) => {
228
- if (!openId) return;
229
- setSaving(true);
230
- try {
231
- await client.updateRecord(openId, { input });
232
- record.refresh();
233
- query.refresh();
234
- } finally {
235
- setSaving(false);
236
- }
237
- };
238
- const moveStage = async (to) => {
239
- if (!openId) return;
240
- await client.setStage(openId, to);
241
- record.refresh();
242
- query.refresh();
243
- };
244
- const addTag = async (tag) => {
245
- if (openId) {
246
- await client.addTag(openId, tag);
247
- record.refresh();
248
- }
249
- };
250
- const removeTag = async (tag) => {
251
- if (openId) {
252
- await client.removeTag(openId, tag);
253
- record.refresh();
254
- }
255
- };
256
- return /* @__PURE__ */ jsxs3("div", { className: "wrap", children: [
257
- /* @__PURE__ */ jsx3(CrmList, { crm, type, query, onOpenRecord: (r) => setOpenId(r.id) }),
258
- record.detail ? /* @__PURE__ */ jsx3(
259
- RecordPanel,
260
- {
261
- crm,
262
- detail: record.detail,
263
- onSaveFields: saveFields,
264
- onMoveStage: moveStage,
265
- onAddTag: addTag,
266
- onRemoveTag: removeTag,
267
- saving
268
- }
269
- ) : null
270
- ] });
271
- }
272
- function peopleSection(options) {
273
- const { crm, id = "people", label = "People", type = "person" } = options;
274
- return { id, label, render: (ctx) => /* @__PURE__ */ jsx3(PeopleBody, { crm, client: ctx.client, type }) };
275
- }
276
-
277
- // src/ui/slot-picker.tsx
278
- import { useMemo as useMemo2, useState as useState4 } from "react";
279
-
280
- // src/ui/datetime.ts
281
- function tzShort(tz) {
282
- try {
283
- const parts = new Intl.DateTimeFormat(void 0, { timeZone: tz, timeZoneName: "short" }).formatToParts(
284
- /* @__PURE__ */ new Date()
285
- );
286
- return parts.find((p) => p.type === "timeZoneName")?.value ?? tz;
287
- } catch {
288
- return tz;
289
- }
290
- }
291
- function dayKey(ms, tz) {
292
- return new Date(ms).toLocaleDateString("en-CA", { timeZone: tz });
293
- }
294
- function dayLabel(ms, tz) {
295
- return new Date(ms).toLocaleDateString(void 0, { timeZone: tz, weekday: "short", month: "short", day: "numeric" });
296
- }
297
- function timeLabel(ms, tz) {
298
- return new Date(ms).toLocaleTimeString(void 0, { timeZone: tz, hour: "numeric", minute: "2-digit" });
299
- }
300
- function fullLabel(ms, tz) {
301
- return new Date(ms).toLocaleString(void 0, {
302
- timeZone: tz,
303
- weekday: "long",
304
- month: "long",
305
- day: "numeric",
306
- hour: "numeric",
307
- minute: "2-digit",
308
- timeZoneName: "short"
309
- });
310
- }
311
- function fmtMoney(cents) {
312
- return "$" + Math.round(cents / 100).toLocaleString();
313
- }
314
- function fmtDate(ms) {
315
- return new Date(ms).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
316
- }
317
- function groupSlotsByDay(slots, tz) {
318
- const byDay = /* @__PURE__ */ new Map();
319
- for (const s of slots) {
320
- const k = dayKey(s.startAt, tz);
321
- const bucket = byDay.get(k);
322
- if (bucket) bucket.push(s);
323
- else byDay.set(k, [s]);
324
- }
325
- return byDay;
326
- }
327
-
328
- // src/ui/slot-picker.tsx
329
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
330
- var DEFAULT_CLASSES = {
331
- days: "slot-days",
332
- day: "slot-day",
333
- times: "slot-grid",
334
- time: "slot-time"
335
- };
336
- function SlotPicker(props) {
337
- const { slots, timezone, selectedStartAt, onPick, onDayChange, classes = DEFAULT_CLASSES } = props;
338
- const byDay = useMemo2(() => groupSlotsByDay(slots, timezone), [slots, timezone]);
339
- const dayKeys = [...byDay.keys()];
340
- const [activeDay, setActiveDay] = useState4(dayKeys[0]);
341
- const day = activeDay !== void 0 && byDay.has(activeDay) ? activeDay : dayKeys[0];
342
- const times = (day !== void 0 ? byDay.get(day) : void 0) ?? [];
343
- return /* @__PURE__ */ jsxs4(Fragment2, { children: [
344
- /* @__PURE__ */ jsx4("div", { className: classes.days, children: dayKeys.map((key) => {
345
- const first = byDay.get(key)?.[0];
346
- return /* @__PURE__ */ jsx4(
347
- "button",
348
- {
349
- type: "button",
350
- className: classes.day,
351
- "aria-pressed": key === day,
352
- onClick: () => {
353
- setActiveDay(key);
354
- onDayChange?.();
355
- },
356
- children: first ? dayLabel(first.startAt, timezone) : key
357
- },
358
- key
359
- );
360
- }) }),
361
- /* @__PURE__ */ jsx4("div", { className: classes.times, children: times.map((s) => /* @__PURE__ */ jsx4(
362
- "button",
363
- {
364
- type: "button",
365
- className: classes.time,
366
- "aria-pressed": selectedStartAt === s.startAt,
367
- onClick: () => onPick(s),
368
- children: timeLabel(s.startAt, timezone)
369
- },
370
- s.startAt
371
- )) })
372
- ] });
373
- }
374
-
375
- // src/ui/members.tsx
376
- import { useEffect as useEffect3, useState as useState6 } from "react";
377
-
378
- // src/ui/reschedule.tsx
379
- import { useState as useState5 } from "react";
380
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
381
- function Rescheduler(props) {
382
- const { api, applicationId, timezone, onRescheduled, label = "Change your time" } = props;
383
- const [open, setOpen] = useState5(false);
384
- const [slots, setSlots] = useState5(null);
385
- const [tz, setTz] = useState5(timezone);
386
- const [busy, setBusy] = useState5(false);
387
- const [msg, setMsg] = useState5(null);
388
- const start = async () => {
389
- setOpen(true);
390
- setSlots(null);
391
- setMsg(null);
392
- try {
393
- const r = await api("/api/schedule/slots");
394
- if (r.schedulingReady === false) {
395
- setSlots([]);
396
- setMsg("Scheduling is briefly unavailable. We'll reach out by email to arrange your call.");
397
- return;
398
- }
399
- setSlots(r.slots ?? []);
400
- if (r.timezone) setTz(r.timezone);
401
- } catch {
402
- setSlots([]);
403
- setMsg("Times are briefly unavailable. Please try again.");
404
- }
405
- };
406
- const pick = async (slot) => {
407
- setBusy(true);
408
- setMsg(null);
409
- try {
410
- await api("/api/schedule/book", { method: "POST", body: JSON.stringify({ applicationId, startAt: slot.startAt }) });
411
- setOpen(false);
412
- await onRescheduled();
413
- } catch (e) {
414
- setMsg(e instanceof Error ? e.message : "That time is no longer available. Please pick another.");
415
- } finally {
416
- setBusy(false);
417
- }
418
- };
419
- if (!open) {
420
- return /* @__PURE__ */ jsx5("p", { className: "meeting-note", children: /* @__PURE__ */ jsx5(
421
- "a",
422
- {
423
- href: "#",
424
- onClick: (e) => {
425
- e.preventDefault();
426
- void start();
427
- },
428
- children: label
429
- }
430
- ) });
431
- }
432
- return /* @__PURE__ */ jsxs5("div", { className: "msched", children: [
433
- slots === null ? /* @__PURE__ */ jsx5("p", { className: "meeting-note", children: "Loading available times\u2026" }) : slots.length === 0 ? /* @__PURE__ */ jsx5("p", { className: "meeting-note", children: msg ?? "No open times right now. Please check back soon." }) : /* @__PURE__ */ jsx5(
434
- SlotPicker,
435
- {
436
- slots,
437
- timezone: tz,
438
- classes: { days: "msched-days", day: "msched-day", times: "msched-times", time: "msched-time" },
439
- onPick: (s) => void pick(s)
440
- }
441
- ),
442
- busy ? /* @__PURE__ */ jsx5("p", { className: "meeting-note", children: "Rescheduling\u2026" }) : null,
443
- msg && slots && slots.length > 0 ? /* @__PURE__ */ jsx5("p", { className: "meeting-note error", children: msg }) : null,
444
- /* @__PURE__ */ jsx5("p", { className: "meeting-note", children: /* @__PURE__ */ jsx5(
445
- "a",
446
- {
447
- href: "#",
448
- onClick: (e) => {
449
- e.preventDefault();
450
- setOpen(false);
451
- },
452
- children: "Keep my current time"
453
- }
454
- ) })
455
- ] });
456
- }
457
-
458
- // src/ui/members.tsx
459
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
460
- function card(kicker, body) {
461
- return /* @__PURE__ */ jsxs6("div", { className: "card", children: [
462
- /* @__PURE__ */ jsx6("div", { className: "card-label", children: "Your Application" }),
463
- /* @__PURE__ */ jsxs6("div", { className: "meeting-block", children: [
464
- /* @__PURE__ */ jsx6("div", { className: "meeting-kicker", children: kicker }),
465
- body
466
- ] })
467
- ] });
468
- }
469
- function ProvisionalCard(props) {
470
- const { api, application, applyHref, onReschedule } = props;
471
- if (!application) {
472
- return card(
473
- "One step remains",
474
- /* @__PURE__ */ jsxs6(Fragment3, { children: [
475
- /* @__PURE__ */ jsx6("div", { className: "meeting-note", children: "Your account is ready, and the application that completes it takes a few minutes." }),
476
- /* @__PURE__ */ jsx6("a", { className: "apply-link", href: applyHref, children: "Apply for membership" })
477
- ] })
478
- );
479
- }
480
- if (application.status === "refunded") {
481
- return card("Membership refunded", /* @__PURE__ */ jsx6("div", { className: "meeting-note", children: "Your fee has been refunded in full and your membership is canceled." }));
482
- }
483
- const membership = application.paid ? /* @__PURE__ */ jsxs6("div", { className: "meeting-note", children: [
484
- "Your membership is active",
485
- application.renewalAt ? ` and renews ${fmtDate(application.renewalAt)}` : "",
486
- "."
487
- ] }) : null;
488
- if (application.meetingAt) {
489
- return card(
490
- "Your introduction call",
491
- /* @__PURE__ */ jsxs6(Fragment3, { children: [
492
- /* @__PURE__ */ jsx6("div", { className: "meeting-date", children: fullLabel(application.meetingAt, application.timezone) }),
493
- /* @__PURE__ */ jsx6("div", { className: "meeting-note", children: "A calendar invitation with the video call link is in your email." }),
494
- application.meetUrl ? /* @__PURE__ */ jsx6("div", { className: "meeting-note", children: /* @__PURE__ */ jsx6("a", { href: application.meetUrl, target: "_blank", rel: "noopener", children: "Join the video call" }) }) : null,
495
- /* @__PURE__ */ jsx6(Rescheduler, { api, applicationId: application.id, timezone: application.timezone, onRescheduled: onReschedule }),
496
- membership
497
- ] })
498
- );
499
- }
500
- return card(
501
- "Book your introduction call",
502
- /* @__PURE__ */ jsxs6(Fragment3, { children: [
503
- /* @__PURE__ */ jsx6("div", { className: "meeting-note", children: "Your application is in. Choose a time below, and a calendar invitation will reach your email." }),
504
- /* @__PURE__ */ jsx6(Rescheduler, { api, applicationId: application.id, timezone: application.timezone, onRescheduled: onReschedule, label: "Choose a time" }),
505
- membership
506
- ] })
507
- );
508
- }
509
- function MembersArea(props) {
510
- const { api, signOut, adminHref = "/admin/", applyHref = "/join.html", memberContent } = props;
511
- const [me, setMe] = useState6(null);
512
- const [error, setError] = useState6(false);
513
- const reload = async () => {
514
- try {
515
- setMe(await api("/api/me"));
516
- } catch {
517
- setError(true);
518
- }
519
- };
520
- useEffect3(() => {
521
- void reload();
522
- }, []);
523
- if (error) return /* @__PURE__ */ jsx6("p", { className: "meeting-note", children: "Sign in is briefly unavailable. Please refresh." });
524
- if (!me) return /* @__PURE__ */ jsx6("p", { className: "meeting-note", children: "Loading\u2026" });
525
- const role = me.role || "provisional";
526
- return /* @__PURE__ */ jsxs6(Fragment3, { children: [
527
- /* @__PURE__ */ jsxs6("div", { className: "card", children: [
528
- /* @__PURE__ */ jsxs6("div", { className: "member-row", children: [
529
- /* @__PURE__ */ jsx6("span", { className: "member-email", children: me.email }),
530
- /* @__PURE__ */ jsx6("span", { className: "role-badge " + role, children: role })
531
- ] }),
532
- /* @__PURE__ */ jsxs6("div", { className: "account-actions", children: [
533
- /* @__PURE__ */ jsx6("button", { className: "btn secondary mini", onClick: signOut, children: "Sign out" }),
534
- role === "admin" ? /* @__PURE__ */ jsx6("a", { className: "admin-console-link", href: adminHref, children: "Admin console" }) : null
535
- ] })
536
- ] }),
537
- role === "provisional" ? /* @__PURE__ */ jsx6(ProvisionalCard, { api, application: me.application, applyHref, onReschedule: reload }) : memberContent ?? /* @__PURE__ */ jsx6("div", { className: "card", children: /* @__PURE__ */ jsx6("div", { className: "card-label", children: "Welcome back." }) })
538
- ] });
539
- }
540
-
541
- // src/ui/join.tsx
542
- import { useEffect as useEffect4, useState as useState8 } from "react";
543
-
544
- // src/ui/payment-step.tsx
545
- import { useRef, useState as useState7 } from "react";
546
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
547
- var loader = null;
548
- function loadStripe() {
549
- const existing = globalThis.Stripe;
550
- if (existing) return Promise.resolve(existing);
551
- if (!loader) {
552
- loader = new Promise((resolve, reject) => {
553
- const s = document.createElement("script");
554
- s.src = "https://js.stripe.com/v3/";
555
- s.onload = () => {
556
- const fn = globalThis.Stripe;
557
- if (fn) resolve(fn);
558
- else reject(new Error("stripe.js unavailable"));
559
- };
560
- s.onerror = () => reject(new Error("stripe.js failed to load"));
561
- document.head.appendChild(s);
562
- });
563
- }
564
- return loader;
565
- }
566
- function PaymentStep(props) {
567
- const { applicationId, refundPolicyText, onPaid } = props;
568
- const [status, setStatus] = useState7("idle");
569
- const [error, setError] = useState7(null);
570
- const mountRef = useRef(null);
571
- const stripeRef = useRef(null);
572
- const elementsRef = useRef(null);
573
- const begin = async () => {
574
- setStatus("loading");
575
- setError(null);
576
- try {
577
- const res = await fetch("/api/payments/subscription", {
578
- method: "POST",
579
- headers: { "content-type": "application/json" },
580
- body: JSON.stringify({ applicationId, refundPolicyAck: true })
581
- });
582
- const data = await res.json();
583
- if (!res.ok || !data.clientSecret || !data.publishableKey) throw new Error("Payment could not be set up. Please try again.");
584
- const stripe = (await loadStripe())(data.publishableKey);
585
- const elements = stripe.elements({ clientSecret: data.clientSecret });
586
- const element = elements.create("payment");
587
- if (mountRef.current) element.mount(mountRef.current);
588
- stripeRef.current = stripe;
589
- elementsRef.current = elements;
590
- setStatus("ready");
591
- } catch (e) {
592
- setStatus("idle");
593
- setError(e instanceof Error ? e.message : "Payment could not be set up. Please try again.");
594
- }
595
- };
596
- const pay = async () => {
597
- const stripe = stripeRef.current;
598
- const elements = elementsRef.current;
599
- if (!stripe || !elements) return;
600
- setStatus("confirming");
601
- setError(null);
602
- const returnUrl = typeof window !== "undefined" ? `${window.location.origin}${window.location.pathname}?redirect_status=succeeded` : void 0;
603
- const result = await stripe.confirmPayment({ elements, confirmParams: { return_url: returnUrl }, redirect: "if_required" });
604
- if (result.error) {
605
- setError(result.error.message ?? "The payment could not be completed.");
606
- setStatus("ready");
607
- return;
608
- }
609
- const paid = result.paymentIntent?.status;
610
- if (paid === "succeeded" || paid === "processing") onPaid();
611
- else {
612
- setError("The payment did not complete. Please try again.");
613
- setStatus("ready");
614
- }
615
- };
616
- return /* @__PURE__ */ jsxs7("div", { className: "join-pay", children: [
617
- /* @__PURE__ */ jsxs7("label", { className: "compliance-box", children: [
618
- /* @__PURE__ */ jsx7(
619
- "input",
620
- {
621
- type: "checkbox",
622
- disabled: status !== "idle",
623
- onChange: (e) => {
624
- if (e.currentTarget.checked) void begin();
625
- }
626
- }
627
- ),
628
- /* @__PURE__ */ jsx7("span", { children: refundPolicyText })
629
- ] }),
630
- status === "loading" ? /* @__PURE__ */ jsx7("p", { className: "pay-status", children: "Preparing secure payment\u2026" }) : null,
631
- /* @__PURE__ */ jsx7("div", { ref: mountRef, hidden: status === "idle" || status === "loading" }),
632
- status === "ready" || status === "confirming" ? /* @__PURE__ */ jsx7("button", { className: "submit-btn", disabled: status === "confirming", onClick: () => void pay(), children: status === "confirming" ? "Processing\u2026" : "Pay and continue" }) : null,
633
- error ? /* @__PURE__ */ jsx7("p", { className: "pay-error", children: error }) : null
634
- ] });
635
- }
636
-
637
- // src/ui/join.tsx
638
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
639
- function JoinBooking(props) {
640
- const { applicationId, onBooked } = props;
641
- const [state, setState] = useState8(null);
642
- const [msg, setMsg] = useState8(null);
643
- const [busy, setBusy] = useState8(false);
644
- const [selected, setSelected] = useState8(null);
645
- const load = async () => {
646
- setMsg(null);
647
- try {
648
- const res = await fetch("/api/schedule/slots");
649
- const data = await res.json();
650
- if (data.schedulingReady === false || !data.slots?.length) {
651
- setState(null);
652
- setMsg("Scheduling is briefly unavailable \u2014 we'll reach out by email to arrange your call.");
653
- return;
654
- }
655
- setState({ slots: data.slots, timezone: data.timezone ?? "UTC" });
656
- } catch {
657
- setMsg("Times are briefly unavailable. Please try again.");
658
- }
659
- };
660
- useEffect4(() => {
661
- void load();
662
- }, []);
663
- const book = async () => {
664
- if (!selected) return;
665
- setBusy(true);
666
- setMsg(null);
667
- try {
668
- const res = await fetch("/api/schedule/book", {
669
- method: "POST",
670
- headers: { "content-type": "application/json" },
671
- body: JSON.stringify({ applicationId, startAt: selected.startAt })
672
- });
673
- const data = await res.json();
674
- if (res.status === 409 && data.code === "calendar_slot_unavailable") {
675
- setSelected(null);
676
- setMsg("That time was just taken. Here are the current openings.");
677
- await load();
678
- return;
679
- }
680
- if (!res.ok) throw new Error(data.error ?? "The booking could not be completed.");
681
- onBooked({ startAt: data.startAt ?? selected.startAt, timezone: state?.timezone ?? "UTC" });
682
- } catch (e) {
683
- setMsg(e instanceof Error ? e.message : "The booking could not be completed.");
684
- } finally {
685
- setBusy(false);
686
- }
687
- };
688
- if (!state) return /* @__PURE__ */ jsx8("p", { className: "slots-status", children: msg ?? "Loading available times\u2026" });
689
- return /* @__PURE__ */ jsxs8("div", { className: "join-book", children: [
690
- /* @__PURE__ */ jsx8(
691
- SlotPicker,
692
- {
693
- slots: state.slots,
694
- timezone: state.timezone,
695
- selectedStartAt: selected?.startAt,
696
- onPick: setSelected,
697
- onDayChange: () => setSelected(null)
698
- }
699
- ),
700
- /* @__PURE__ */ jsxs8("div", { className: "slot-confirm", hidden: !selected, children: [
701
- /* @__PURE__ */ jsx8("button", { className: "submit-btn", disabled: busy, onClick: () => void book(), children: busy ? "Booking\u2026" : "Book this time" }),
702
- msg ? /* @__PURE__ */ jsx8("p", { className: "step2-note", children: msg }) : null
703
- ] })
704
- ] });
705
- }
706
- function JoinIsland(props) {
707
- const { config, children, membersHref = "/members/" } = props;
708
- const [step, setStep] = useState8("form");
709
- const [applicationId, setApplicationId] = useState8(null);
710
- const [error, setError] = useState8(null);
711
- const [submitting, setSubmitting] = useState8(false);
712
- const [booked, setBooked] = useState8(null);
713
- const submit = async (e) => {
714
- e.preventDefault();
715
- setSubmitting(true);
716
- setError(null);
717
- try {
718
- const fields = {};
719
- for (const [k, v] of new FormData(e.currentTarget).entries()) fields[k] = v;
720
- fields.submissionId = crypto.randomUUID();
721
- const res = await fetch("/api/applications", {
722
- method: "POST",
723
- headers: { "content-type": "application/json" },
724
- body: JSON.stringify(fields)
725
- });
726
- const data = await res.json();
727
- if (!res.ok || !data.id) throw new Error(data.error ?? "Your application could not be submitted.");
728
- setApplicationId(data.id);
729
- setStep(config.paymentsReady ? "pay" : "book");
730
- } catch (err) {
731
- setError(err instanceof Error ? err.message : "Something went wrong. Please try again.");
732
- } finally {
733
- setSubmitting(false);
734
- }
735
- };
736
- if (step === "done" && booked) {
737
- return /* @__PURE__ */ jsxs8("div", { className: "join-done card", children: [
738
- /* @__PURE__ */ jsx8("div", { className: "card-label", children: "You're booked" }),
739
- /* @__PURE__ */ jsx8("p", { className: "meeting-date", children: fullLabel(booked.startAt, booked.timezone) }),
740
- /* @__PURE__ */ jsx8("p", { className: "meeting-note", children: "A calendar invitation with the video call link is on its way to your email." }),
741
- /* @__PURE__ */ jsx8("a", { className: "apply-link", href: membersHref, children: "Go to your member area" })
742
- ] });
743
- }
744
- if (step === "book" && applicationId) {
745
- return /* @__PURE__ */ jsx8(
746
- JoinBooking,
747
- {
748
- applicationId,
749
- onBooked: (b) => {
750
- setBooked(b);
751
- setStep("done");
752
- }
753
- }
754
- );
755
- }
756
- if (step === "pay" && applicationId) {
757
- return /* @__PURE__ */ jsx8(PaymentStep, { applicationId, refundPolicyText: config.refundPolicyText ?? "", onPaid: () => setStep("book") });
758
- }
759
- return /* @__PURE__ */ jsxs8("form", { className: "join-form", onSubmit: (e) => void submit(e), children: [
760
- children,
761
- error ? /* @__PURE__ */ jsx8("p", { className: "join-error", children: error }) : null,
762
- /* @__PURE__ */ jsx8("button", { className: "submit-btn", type: "submit", disabled: submitting, children: submitting ? "Submitting\u2026" : "Submit application" })
763
- ] });
764
- }
765
-
766
- // src/brand.ts
767
- function paletteVar(key) {
768
- return key.startsWith("--") ? key : `--${key}`;
769
- }
770
- function cleanValue(value) {
771
- return value.replace(/[<>{};]/g, "").trim();
772
- }
773
- function brandTokens(brand) {
774
- if (!brand) return "";
775
- const decls = [];
776
- for (const [key, value] of Object.entries(brand.palette ?? {})) {
777
- if (typeof value === "string" && value.trim()) decls.push(`${paletteVar(key)}: ${cleanValue(value)};`);
778
- }
779
- const fonts = brand.fonts;
780
- if (fonts?.display) decls.push(`--ui-font-display: ${cleanValue(fonts.display)};`);
781
- if (fonts?.body) decls.push(`--ui-font-sans: ${cleanValue(fonts.body)};`);
782
- if (fonts?.numeral) decls.push(`--ui-font-numeral: ${cleanValue(fonts.numeral)};`);
783
- return decls.length ? `:root {
784
- ${decls.join("\n ")}
785
- }
786
- ` : "";
787
- }
788
-
789
- // src/ui/brand-style.tsx
790
- import { jsx as jsx9 } from "react/jsx-runtime";
791
- function BrandStyle(props) {
792
- const css = brandTokens(props.brand);
793
- if (!css) return null;
794
- return /* @__PURE__ */ jsx9("style", { children: css });
795
- }
1
+ import {
2
+ BrandStyle,
3
+ JoinIsland,
4
+ MembersArea,
5
+ PaymentStep,
6
+ Rescheduler,
7
+ SlotPicker,
8
+ dayKey,
9
+ dayLabel,
10
+ fmtDate,
11
+ fmtMoney,
12
+ fullLabel,
13
+ groupSlotsByDay,
14
+ timeLabel,
15
+ tzShort
16
+ } from "../chunk-ZCZK6QLC.js";
17
+ import {
18
+ ChapterAdmin,
19
+ peopleSection
20
+ } from "../chunk-YO6DY5DL.js";
796
21
  export {
797
22
  BrandStyle,
798
23
  ChapterAdmin,