@odla-ai/chapter 0.3.0 → 0.5.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
@@ -211,7 +211,603 @@ function ChapterAdmin(props) {
211
211
  /* @__PURE__ */ jsx2(SignedIn, { children: /* @__PURE__ */ jsx2(Authed, { sections, basePath, brand, crmBasePath, apiBase }) })
212
212
  ] });
213
213
  }
214
+
215
+ // src/ui/admin-people.tsx
216
+ import { useState as useState3 } from "react";
217
+ import { CrmList, RecordPanel, useCrmQuery, useCrmRecord } from "@odla-ai/crm/ui";
218
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
219
+ function PeopleBody(props) {
220
+ const { crm, client, type } = props;
221
+ const hookClient = client;
222
+ const query = useCrmQuery(hookClient, type);
223
+ const [openId, setOpenId] = useState3(null);
224
+ const record = useCrmRecord(hookClient, openId);
225
+ const [saving, setSaving] = useState3(false);
226
+ const saveFields = async (input) => {
227
+ if (!openId) return;
228
+ setSaving(true);
229
+ try {
230
+ await client.updateRecord(openId, { input });
231
+ record.refresh();
232
+ query.refresh();
233
+ } finally {
234
+ setSaving(false);
235
+ }
236
+ };
237
+ const moveStage = async (to) => {
238
+ if (!openId) return;
239
+ await client.setStage(openId, to);
240
+ record.refresh();
241
+ query.refresh();
242
+ };
243
+ const addTag = async (tag) => {
244
+ if (openId) {
245
+ await client.addTag(openId, tag);
246
+ record.refresh();
247
+ }
248
+ };
249
+ const removeTag = async (tag) => {
250
+ if (openId) {
251
+ await client.removeTag(openId, tag);
252
+ record.refresh();
253
+ }
254
+ };
255
+ return /* @__PURE__ */ jsxs3("div", { className: "wrap", children: [
256
+ /* @__PURE__ */ jsx3(CrmList, { crm, type, query, onOpenRecord: (r) => setOpenId(r.id) }),
257
+ record.detail ? /* @__PURE__ */ jsx3(
258
+ RecordPanel,
259
+ {
260
+ crm,
261
+ detail: record.detail,
262
+ onSaveFields: saveFields,
263
+ onMoveStage: moveStage,
264
+ onAddTag: addTag,
265
+ onRemoveTag: removeTag,
266
+ saving
267
+ }
268
+ ) : null
269
+ ] });
270
+ }
271
+ function peopleSection(options) {
272
+ const { crm, id = "people", label = "People", type = "person" } = options;
273
+ return { id, label, render: (ctx) => /* @__PURE__ */ jsx3(PeopleBody, { crm, client: ctx.client, type }) };
274
+ }
275
+
276
+ // src/ui/slot-picker.tsx
277
+ import { useMemo as useMemo2, useState as useState4 } from "react";
278
+
279
+ // src/ui/datetime.ts
280
+ function tzShort(tz) {
281
+ try {
282
+ const parts = new Intl.DateTimeFormat(void 0, { timeZone: tz, timeZoneName: "short" }).formatToParts(
283
+ /* @__PURE__ */ new Date()
284
+ );
285
+ return parts.find((p) => p.type === "timeZoneName")?.value ?? tz;
286
+ } catch {
287
+ return tz;
288
+ }
289
+ }
290
+ function dayKey(ms, tz) {
291
+ return new Date(ms).toLocaleDateString("en-CA", { timeZone: tz });
292
+ }
293
+ function dayLabel(ms, tz) {
294
+ return new Date(ms).toLocaleDateString(void 0, { timeZone: tz, weekday: "short", month: "short", day: "numeric" });
295
+ }
296
+ function timeLabel(ms, tz) {
297
+ return new Date(ms).toLocaleTimeString(void 0, { timeZone: tz, hour: "numeric", minute: "2-digit" });
298
+ }
299
+ function fullLabel(ms, tz) {
300
+ return new Date(ms).toLocaleString(void 0, {
301
+ timeZone: tz,
302
+ weekday: "long",
303
+ month: "long",
304
+ day: "numeric",
305
+ hour: "numeric",
306
+ minute: "2-digit",
307
+ timeZoneName: "short"
308
+ });
309
+ }
310
+ function fmtMoney(cents) {
311
+ return "$" + Math.round(cents / 100).toLocaleString();
312
+ }
313
+ function fmtDate(ms) {
314
+ return new Date(ms).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
315
+ }
316
+ function groupSlotsByDay(slots, tz) {
317
+ const byDay = /* @__PURE__ */ new Map();
318
+ for (const s of slots) {
319
+ const k = dayKey(s.startAt, tz);
320
+ const bucket = byDay.get(k);
321
+ if (bucket) bucket.push(s);
322
+ else byDay.set(k, [s]);
323
+ }
324
+ return byDay;
325
+ }
326
+
327
+ // src/ui/slot-picker.tsx
328
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
329
+ var DEFAULT_CLASSES = {
330
+ days: "slot-days",
331
+ day: "slot-day",
332
+ times: "slot-grid",
333
+ time: "slot-time"
334
+ };
335
+ function SlotPicker(props) {
336
+ const { slots, timezone, selectedStartAt, onPick, onDayChange, classes = DEFAULT_CLASSES } = props;
337
+ const byDay = useMemo2(() => groupSlotsByDay(slots, timezone), [slots, timezone]);
338
+ const dayKeys = [...byDay.keys()];
339
+ const [activeDay, setActiveDay] = useState4(dayKeys[0]);
340
+ const day = activeDay !== void 0 && byDay.has(activeDay) ? activeDay : dayKeys[0];
341
+ const times = (day !== void 0 ? byDay.get(day) : void 0) ?? [];
342
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
343
+ /* @__PURE__ */ jsx4("div", { className: classes.days, children: dayKeys.map((key) => {
344
+ const first = byDay.get(key)?.[0];
345
+ return /* @__PURE__ */ jsx4(
346
+ "button",
347
+ {
348
+ type: "button",
349
+ className: classes.day,
350
+ "aria-pressed": key === day,
351
+ onClick: () => {
352
+ setActiveDay(key);
353
+ onDayChange?.();
354
+ },
355
+ children: first ? dayLabel(first.startAt, timezone) : key
356
+ },
357
+ key
358
+ );
359
+ }) }),
360
+ /* @__PURE__ */ jsx4("div", { className: classes.times, children: times.map((s) => /* @__PURE__ */ jsx4(
361
+ "button",
362
+ {
363
+ type: "button",
364
+ className: classes.time,
365
+ "aria-pressed": selectedStartAt === s.startAt,
366
+ onClick: () => onPick(s),
367
+ children: timeLabel(s.startAt, timezone)
368
+ },
369
+ s.startAt
370
+ )) })
371
+ ] });
372
+ }
373
+
374
+ // src/ui/members.tsx
375
+ import { useEffect as useEffect3, useState as useState6 } from "react";
376
+
377
+ // src/ui/reschedule.tsx
378
+ import { useState as useState5 } from "react";
379
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
380
+ function Rescheduler(props) {
381
+ const { api, applicationId, timezone, onRescheduled, label = "Change your time" } = props;
382
+ const [open, setOpen] = useState5(false);
383
+ const [slots, setSlots] = useState5(null);
384
+ const [tz, setTz] = useState5(timezone);
385
+ const [busy, setBusy] = useState5(false);
386
+ const [msg, setMsg] = useState5(null);
387
+ const start = async () => {
388
+ setOpen(true);
389
+ setSlots(null);
390
+ setMsg(null);
391
+ try {
392
+ const r = await api("/api/schedule/slots");
393
+ if (r.schedulingReady === false) {
394
+ setSlots([]);
395
+ setMsg("Scheduling is briefly unavailable. We'll reach out by email to arrange your call.");
396
+ return;
397
+ }
398
+ setSlots(r.slots ?? []);
399
+ if (r.timezone) setTz(r.timezone);
400
+ } catch {
401
+ setSlots([]);
402
+ setMsg("Times are briefly unavailable. Please try again.");
403
+ }
404
+ };
405
+ const pick = async (slot) => {
406
+ setBusy(true);
407
+ setMsg(null);
408
+ try {
409
+ await api("/api/schedule/book", { method: "POST", body: JSON.stringify({ applicationId, startAt: slot.startAt }) });
410
+ setOpen(false);
411
+ await onRescheduled();
412
+ } catch (e) {
413
+ setMsg(e instanceof Error ? e.message : "That time is no longer available. Please pick another.");
414
+ } finally {
415
+ setBusy(false);
416
+ }
417
+ };
418
+ if (!open) {
419
+ return /* @__PURE__ */ jsx5("p", { className: "meeting-note", children: /* @__PURE__ */ jsx5(
420
+ "a",
421
+ {
422
+ href: "#",
423
+ onClick: (e) => {
424
+ e.preventDefault();
425
+ void start();
426
+ },
427
+ children: label
428
+ }
429
+ ) });
430
+ }
431
+ return /* @__PURE__ */ jsxs5("div", { className: "msched", children: [
432
+ 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(
433
+ SlotPicker,
434
+ {
435
+ slots,
436
+ timezone: tz,
437
+ classes: { days: "msched-days", day: "msched-day", times: "msched-times", time: "msched-time" },
438
+ onPick: (s) => void pick(s)
439
+ }
440
+ ),
441
+ busy ? /* @__PURE__ */ jsx5("p", { className: "meeting-note", children: "Rescheduling\u2026" }) : null,
442
+ msg && slots && slots.length > 0 ? /* @__PURE__ */ jsx5("p", { className: "meeting-note error", children: msg }) : null,
443
+ /* @__PURE__ */ jsx5("p", { className: "meeting-note", children: /* @__PURE__ */ jsx5(
444
+ "a",
445
+ {
446
+ href: "#",
447
+ onClick: (e) => {
448
+ e.preventDefault();
449
+ setOpen(false);
450
+ },
451
+ children: "Keep my current time"
452
+ }
453
+ ) })
454
+ ] });
455
+ }
456
+
457
+ // src/ui/members.tsx
458
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
459
+ function card(kicker, body) {
460
+ return /* @__PURE__ */ jsxs6("div", { className: "card", children: [
461
+ /* @__PURE__ */ jsx6("div", { className: "card-label", children: "Your Application" }),
462
+ /* @__PURE__ */ jsxs6("div", { className: "meeting-block", children: [
463
+ /* @__PURE__ */ jsx6("div", { className: "meeting-kicker", children: kicker }),
464
+ body
465
+ ] })
466
+ ] });
467
+ }
468
+ function ProvisionalCard(props) {
469
+ const { api, application, applyHref, onReschedule } = props;
470
+ if (!application) {
471
+ return card(
472
+ "One step remains",
473
+ /* @__PURE__ */ jsxs6(Fragment3, { children: [
474
+ /* @__PURE__ */ jsx6("div", { className: "meeting-note", children: "Your account is ready, and the application that completes it takes a few minutes." }),
475
+ /* @__PURE__ */ jsx6("a", { className: "apply-link", href: applyHref, children: "Apply for membership" })
476
+ ] })
477
+ );
478
+ }
479
+ if (application.status === "refunded") {
480
+ return card("Membership refunded", /* @__PURE__ */ jsx6("div", { className: "meeting-note", children: "Your fee has been refunded in full and your membership is canceled." }));
481
+ }
482
+ const membership = application.paid ? /* @__PURE__ */ jsxs6("div", { className: "meeting-note", children: [
483
+ "Your membership is active",
484
+ application.renewalAt ? ` and renews ${fmtDate(application.renewalAt)}` : "",
485
+ "."
486
+ ] }) : null;
487
+ if (application.meetingAt) {
488
+ return card(
489
+ "Your introduction call",
490
+ /* @__PURE__ */ jsxs6(Fragment3, { children: [
491
+ /* @__PURE__ */ jsx6("div", { className: "meeting-date", children: fullLabel(application.meetingAt, application.timezone) }),
492
+ /* @__PURE__ */ jsx6("div", { className: "meeting-note", children: "A calendar invitation with the video call link is in your email." }),
493
+ 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,
494
+ /* @__PURE__ */ jsx6(Rescheduler, { api, applicationId: application.id, timezone: application.timezone, onRescheduled: onReschedule }),
495
+ membership
496
+ ] })
497
+ );
498
+ }
499
+ return card(
500
+ "Book your introduction call",
501
+ /* @__PURE__ */ jsxs6(Fragment3, { children: [
502
+ /* @__PURE__ */ jsx6("div", { className: "meeting-note", children: "Your application is in. Choose a time below, and a calendar invitation will reach your email." }),
503
+ /* @__PURE__ */ jsx6(Rescheduler, { api, applicationId: application.id, timezone: application.timezone, onRescheduled: onReschedule, label: "Choose a time" }),
504
+ membership
505
+ ] })
506
+ );
507
+ }
508
+ function MembersArea(props) {
509
+ const { api, signOut, adminHref = "/admin/", applyHref = "/join.html", memberContent } = props;
510
+ const [me, setMe] = useState6(null);
511
+ const [error, setError] = useState6(false);
512
+ const reload = async () => {
513
+ try {
514
+ setMe(await api("/api/me"));
515
+ } catch {
516
+ setError(true);
517
+ }
518
+ };
519
+ useEffect3(() => {
520
+ void reload();
521
+ }, []);
522
+ if (error) return /* @__PURE__ */ jsx6("p", { className: "meeting-note", children: "Sign in is briefly unavailable. Please refresh." });
523
+ if (!me) return /* @__PURE__ */ jsx6("p", { className: "meeting-note", children: "Loading\u2026" });
524
+ const role = me.role || "provisional";
525
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
526
+ /* @__PURE__ */ jsxs6("div", { className: "card", children: [
527
+ /* @__PURE__ */ jsxs6("div", { className: "member-row", children: [
528
+ /* @__PURE__ */ jsx6("span", { className: "member-email", children: me.email }),
529
+ /* @__PURE__ */ jsx6("span", { className: "role-badge " + role, children: role })
530
+ ] }),
531
+ /* @__PURE__ */ jsxs6("div", { className: "account-actions", children: [
532
+ /* @__PURE__ */ jsx6("button", { className: "btn secondary mini", onClick: signOut, children: "Sign out" }),
533
+ role === "admin" ? /* @__PURE__ */ jsx6("a", { className: "admin-console-link", href: adminHref, children: "Admin console" }) : null
534
+ ] })
535
+ ] }),
536
+ 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." }) })
537
+ ] });
538
+ }
539
+
540
+ // src/ui/join.tsx
541
+ import { useEffect as useEffect4, useState as useState8 } from "react";
542
+
543
+ // src/ui/payment-step.tsx
544
+ import { useRef, useState as useState7 } from "react";
545
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
546
+ var loader = null;
547
+ function loadStripe() {
548
+ const existing = globalThis.Stripe;
549
+ if (existing) return Promise.resolve(existing);
550
+ if (!loader) {
551
+ loader = new Promise((resolve, reject) => {
552
+ const s = document.createElement("script");
553
+ s.src = "https://js.stripe.com/v3/";
554
+ s.onload = () => {
555
+ const fn = globalThis.Stripe;
556
+ if (fn) resolve(fn);
557
+ else reject(new Error("stripe.js unavailable"));
558
+ };
559
+ s.onerror = () => reject(new Error("stripe.js failed to load"));
560
+ document.head.appendChild(s);
561
+ });
562
+ }
563
+ return loader;
564
+ }
565
+ function PaymentStep(props) {
566
+ const { applicationId, refundPolicyText, onPaid } = props;
567
+ const [status, setStatus] = useState7("idle");
568
+ const [error, setError] = useState7(null);
569
+ const mountRef = useRef(null);
570
+ const stripeRef = useRef(null);
571
+ const elementsRef = useRef(null);
572
+ const begin = async () => {
573
+ setStatus("loading");
574
+ setError(null);
575
+ try {
576
+ const res = await fetch("/api/payments/subscription", {
577
+ method: "POST",
578
+ headers: { "content-type": "application/json" },
579
+ body: JSON.stringify({ applicationId, refundPolicyAck: true })
580
+ });
581
+ const data = await res.json();
582
+ if (!res.ok || !data.clientSecret || !data.publishableKey) throw new Error("Payment could not be set up. Please try again.");
583
+ const stripe = (await loadStripe())(data.publishableKey);
584
+ const elements = stripe.elements({ clientSecret: data.clientSecret });
585
+ const element = elements.create("payment");
586
+ if (mountRef.current) element.mount(mountRef.current);
587
+ stripeRef.current = stripe;
588
+ elementsRef.current = elements;
589
+ setStatus("ready");
590
+ } catch (e) {
591
+ setStatus("idle");
592
+ setError(e instanceof Error ? e.message : "Payment could not be set up. Please try again.");
593
+ }
594
+ };
595
+ const pay = async () => {
596
+ const stripe = stripeRef.current;
597
+ const elements = elementsRef.current;
598
+ if (!stripe || !elements) return;
599
+ setStatus("confirming");
600
+ setError(null);
601
+ const returnUrl = typeof window !== "undefined" ? `${window.location.origin}${window.location.pathname}?redirect_status=succeeded` : void 0;
602
+ const result = await stripe.confirmPayment({ elements, confirmParams: { return_url: returnUrl }, redirect: "if_required" });
603
+ if (result.error) {
604
+ setError(result.error.message ?? "The payment could not be completed.");
605
+ setStatus("ready");
606
+ return;
607
+ }
608
+ const paid = result.paymentIntent?.status;
609
+ if (paid === "succeeded" || paid === "processing") onPaid();
610
+ else {
611
+ setError("The payment did not complete. Please try again.");
612
+ setStatus("ready");
613
+ }
614
+ };
615
+ return /* @__PURE__ */ jsxs7("div", { className: "join-pay", children: [
616
+ /* @__PURE__ */ jsxs7("label", { className: "compliance-box", children: [
617
+ /* @__PURE__ */ jsx7(
618
+ "input",
619
+ {
620
+ type: "checkbox",
621
+ disabled: status !== "idle",
622
+ onChange: (e) => {
623
+ if (e.currentTarget.checked) void begin();
624
+ }
625
+ }
626
+ ),
627
+ /* @__PURE__ */ jsx7("span", { children: refundPolicyText })
628
+ ] }),
629
+ status === "loading" ? /* @__PURE__ */ jsx7("p", { className: "pay-status", children: "Preparing secure payment\u2026" }) : null,
630
+ /* @__PURE__ */ jsx7("div", { ref: mountRef, hidden: status === "idle" || status === "loading" }),
631
+ 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,
632
+ error ? /* @__PURE__ */ jsx7("p", { className: "pay-error", children: error }) : null
633
+ ] });
634
+ }
635
+
636
+ // src/ui/join.tsx
637
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
638
+ function JoinBooking(props) {
639
+ const { applicationId, onBooked } = props;
640
+ const [state, setState] = useState8(null);
641
+ const [msg, setMsg] = useState8(null);
642
+ const [busy, setBusy] = useState8(false);
643
+ const [selected, setSelected] = useState8(null);
644
+ const load = async () => {
645
+ setMsg(null);
646
+ try {
647
+ const res = await fetch("/api/schedule/slots");
648
+ const data = await res.json();
649
+ if (data.schedulingReady === false || !data.slots?.length) {
650
+ setState(null);
651
+ setMsg("Scheduling is briefly unavailable \u2014 we'll reach out by email to arrange your call.");
652
+ return;
653
+ }
654
+ setState({ slots: data.slots, timezone: data.timezone ?? "UTC" });
655
+ } catch {
656
+ setMsg("Times are briefly unavailable. Please try again.");
657
+ }
658
+ };
659
+ useEffect4(() => {
660
+ void load();
661
+ }, []);
662
+ const book = async () => {
663
+ if (!selected) return;
664
+ setBusy(true);
665
+ setMsg(null);
666
+ try {
667
+ const res = await fetch("/api/schedule/book", {
668
+ method: "POST",
669
+ headers: { "content-type": "application/json" },
670
+ body: JSON.stringify({ applicationId, startAt: selected.startAt })
671
+ });
672
+ const data = await res.json();
673
+ if (res.status === 409 && data.code === "calendar_slot_unavailable") {
674
+ setSelected(null);
675
+ setMsg("That time was just taken. Here are the current openings.");
676
+ await load();
677
+ return;
678
+ }
679
+ if (!res.ok) throw new Error(data.error ?? "The booking could not be completed.");
680
+ onBooked({ startAt: data.startAt ?? selected.startAt, timezone: state?.timezone ?? "UTC" });
681
+ } catch (e) {
682
+ setMsg(e instanceof Error ? e.message : "The booking could not be completed.");
683
+ } finally {
684
+ setBusy(false);
685
+ }
686
+ };
687
+ if (!state) return /* @__PURE__ */ jsx8("p", { className: "slots-status", children: msg ?? "Loading available times\u2026" });
688
+ return /* @__PURE__ */ jsxs8("div", { className: "join-book", children: [
689
+ /* @__PURE__ */ jsx8(
690
+ SlotPicker,
691
+ {
692
+ slots: state.slots,
693
+ timezone: state.timezone,
694
+ selectedStartAt: selected?.startAt,
695
+ onPick: setSelected,
696
+ onDayChange: () => setSelected(null)
697
+ }
698
+ ),
699
+ /* @__PURE__ */ jsxs8("div", { className: "slot-confirm", hidden: !selected, children: [
700
+ /* @__PURE__ */ jsx8("button", { className: "submit-btn", disabled: busy, onClick: () => void book(), children: busy ? "Booking\u2026" : "Book this time" }),
701
+ msg ? /* @__PURE__ */ jsx8("p", { className: "step2-note", children: msg }) : null
702
+ ] })
703
+ ] });
704
+ }
705
+ function JoinIsland(props) {
706
+ const { config, children, membersHref = "/members/" } = props;
707
+ const [step, setStep] = useState8("form");
708
+ const [applicationId, setApplicationId] = useState8(null);
709
+ const [error, setError] = useState8(null);
710
+ const [submitting, setSubmitting] = useState8(false);
711
+ const [booked, setBooked] = useState8(null);
712
+ const submit = async (e) => {
713
+ e.preventDefault();
714
+ setSubmitting(true);
715
+ setError(null);
716
+ try {
717
+ const fields = {};
718
+ for (const [k, v] of new FormData(e.currentTarget).entries()) fields[k] = v;
719
+ fields.submissionId = crypto.randomUUID();
720
+ const res = await fetch("/api/applications", {
721
+ method: "POST",
722
+ headers: { "content-type": "application/json" },
723
+ body: JSON.stringify(fields)
724
+ });
725
+ const data = await res.json();
726
+ if (!res.ok || !data.id) throw new Error(data.error ?? "Your application could not be submitted.");
727
+ setApplicationId(data.id);
728
+ setStep(config.paymentsReady ? "pay" : "book");
729
+ } catch (err) {
730
+ setError(err instanceof Error ? err.message : "Something went wrong. Please try again.");
731
+ } finally {
732
+ setSubmitting(false);
733
+ }
734
+ };
735
+ if (step === "done" && booked) {
736
+ return /* @__PURE__ */ jsxs8("div", { className: "join-done card", children: [
737
+ /* @__PURE__ */ jsx8("div", { className: "card-label", children: "You're booked" }),
738
+ /* @__PURE__ */ jsx8("p", { className: "meeting-date", children: fullLabel(booked.startAt, booked.timezone) }),
739
+ /* @__PURE__ */ jsx8("p", { className: "meeting-note", children: "A calendar invitation with the video call link is on its way to your email." }),
740
+ /* @__PURE__ */ jsx8("a", { className: "apply-link", href: membersHref, children: "Go to your member area" })
741
+ ] });
742
+ }
743
+ if (step === "book" && applicationId) {
744
+ return /* @__PURE__ */ jsx8(
745
+ JoinBooking,
746
+ {
747
+ applicationId,
748
+ onBooked: (b) => {
749
+ setBooked(b);
750
+ setStep("done");
751
+ }
752
+ }
753
+ );
754
+ }
755
+ if (step === "pay" && applicationId) {
756
+ return /* @__PURE__ */ jsx8(PaymentStep, { applicationId, refundPolicyText: config.refundPolicyText ?? "", onPaid: () => setStep("book") });
757
+ }
758
+ return /* @__PURE__ */ jsxs8("form", { className: "join-form", onSubmit: (e) => void submit(e), children: [
759
+ children,
760
+ error ? /* @__PURE__ */ jsx8("p", { className: "join-error", children: error }) : null,
761
+ /* @__PURE__ */ jsx8("button", { className: "submit-btn", type: "submit", disabled: submitting, children: submitting ? "Submitting\u2026" : "Submit application" })
762
+ ] });
763
+ }
764
+
765
+ // src/brand.ts
766
+ function paletteVar(key) {
767
+ return key.startsWith("--") ? key : `--${key}`;
768
+ }
769
+ function cleanValue(value) {
770
+ return value.replace(/[<>{};]/g, "").trim();
771
+ }
772
+ function brandTokens(brand) {
773
+ if (!brand) return "";
774
+ const decls = [];
775
+ for (const [key, value] of Object.entries(brand.palette ?? {})) {
776
+ if (typeof value === "string" && value.trim()) decls.push(`${paletteVar(key)}: ${cleanValue(value)};`);
777
+ }
778
+ const fonts = brand.fonts;
779
+ if (fonts?.display) decls.push(`--ui-font-display: ${cleanValue(fonts.display)};`);
780
+ if (fonts?.body) decls.push(`--ui-font-sans: ${cleanValue(fonts.body)};`);
781
+ if (fonts?.numeral) decls.push(`--ui-font-numeral: ${cleanValue(fonts.numeral)};`);
782
+ return decls.length ? `:root {
783
+ ${decls.join("\n ")}
784
+ }
785
+ ` : "";
786
+ }
787
+
788
+ // src/ui/brand-style.tsx
789
+ import { jsx as jsx9 } from "react/jsx-runtime";
790
+ function BrandStyle(props) {
791
+ const css = brandTokens(props.brand);
792
+ if (!css) return null;
793
+ return /* @__PURE__ */ jsx9("style", { children: css });
794
+ }
214
795
  export {
215
- ChapterAdmin
796
+ BrandStyle,
797
+ ChapterAdmin,
798
+ JoinIsland,
799
+ MembersArea,
800
+ PaymentStep,
801
+ Rescheduler,
802
+ SlotPicker,
803
+ dayKey,
804
+ dayLabel,
805
+ fmtDate,
806
+ fmtMoney,
807
+ fullLabel,
808
+ groupSlotsByDay,
809
+ peopleSection,
810
+ timeLabel,
811
+ tzShort
216
812
  };
217
813
  //# sourceMappingURL=index.js.map