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