@odla-ai/chapter 0.22.1 → 0.24.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.
@@ -1,3 +1,8 @@
1
+ import {
2
+ formatChapterCopy,
3
+ useChapterCopy
4
+ } from "./chunk-WGQO4WDJ.js";
5
+
1
6
  // src/ui/slot-picker.tsx
2
7
  import { useMemo, useState } from "preact/hooks";
3
8
 
@@ -103,7 +108,9 @@ import { useEffect, useState as useState3 } from "preact/hooks";
103
108
  import { useState as useState2 } from "preact/hooks";
104
109
  import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
105
110
  function Rescheduler(props) {
106
- const { api, applicationId, timezone, onRescheduled, label = "Change your time" } = props;
111
+ const inheritedCopy = useChapterCopy();
112
+ const copy = props.copy ?? inheritedCopy.members.reschedule;
113
+ const { api, applicationId, timezone, onRescheduled, label = copy.changeTime } = props;
107
114
  const [open, setOpen] = useState2(false);
108
115
  const [slots, setSlots] = useState2(null);
109
116
  const [tz, setTz] = useState2(timezone);
@@ -117,14 +124,14 @@ function Rescheduler(props) {
117
124
  const r = await api("/api/schedule/slots");
118
125
  if (r.schedulingReady === false) {
119
126
  setSlots([]);
120
- setMsg("Scheduling is briefly unavailable. We'll reach out by email to arrange your call.");
127
+ setMsg(copy.unavailable);
121
128
  return;
122
129
  }
123
130
  setSlots(r.slots ?? []);
124
131
  if (r.timezone) setTz(r.timezone);
125
132
  } catch {
126
133
  setSlots([]);
127
- setMsg("Times are briefly unavailable. Please try again.");
134
+ setMsg(copy.loadFailed);
128
135
  }
129
136
  };
130
137
  const pick = async (slot) => {
@@ -135,7 +142,7 @@ function Rescheduler(props) {
135
142
  setOpen(false);
136
143
  await onRescheduled();
137
144
  } catch (e) {
138
- setMsg(e instanceof Error ? e.message : "That time is no longer available. Please pick another.");
145
+ setMsg(e instanceof Error ? e.message : copy.slotGone);
139
146
  } finally {
140
147
  setBusy(false);
141
148
  }
@@ -154,7 +161,7 @@ function Rescheduler(props) {
154
161
  ) });
155
162
  }
156
163
  return /* @__PURE__ */ jsxs2("div", { className: "msched", children: [
157
- slots === null ? /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: "Loading available times\u2026" }) : slots.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: msg ?? "No open times right now. Please check back soon." }) : /* @__PURE__ */ jsx2(
164
+ slots === null ? /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: copy.loading }) : slots.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: msg ?? copy.noTimes }) : /* @__PURE__ */ jsx2(
158
165
  SlotPicker,
159
166
  {
160
167
  slots,
@@ -163,7 +170,7 @@ function Rescheduler(props) {
163
170
  onPick: (s) => void pick(s)
164
171
  }
165
172
  ),
166
- busy ? /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: "Rescheduling\u2026" }) : null,
173
+ busy ? /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: copy.rescheduling }) : null,
167
174
  msg && slots && slots.length > 0 ? /* @__PURE__ */ jsx2("p", { className: "meeting-note error", children: msg }) : null,
168
175
  /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: /* @__PURE__ */ jsx2(
169
176
  "a",
@@ -173,7 +180,7 @@ function Rescheduler(props) {
173
180
  e.preventDefault();
174
181
  setOpen(false);
175
182
  },
176
- children: "Keep my current time"
183
+ children: copy.keepTime
177
184
  }
178
185
  ) })
179
186
  ] });
@@ -181,9 +188,9 @@ function Rescheduler(props) {
181
188
 
182
189
  // src/ui/members.tsx
183
190
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
184
- function card(kicker, body) {
191
+ function card(copy, kicker, body) {
185
192
  return /* @__PURE__ */ jsxs3("div", { className: "card", children: [
186
- /* @__PURE__ */ jsx3("div", { className: "card-label", children: "Your Application" }),
193
+ /* @__PURE__ */ jsx3("div", { className: "card-label", children: copy.provisional.cardLabel }),
187
194
  /* @__PURE__ */ jsxs3("div", { className: "meeting-block", children: [
188
195
  /* @__PURE__ */ jsx3("div", { className: "meeting-kicker", children: kicker }),
189
196
  body
@@ -191,47 +198,78 @@ function card(kicker, body) {
191
198
  ] });
192
199
  }
193
200
  function ProvisionalCard(props) {
194
- const { api, application, applyHref, onReschedule } = props;
201
+ const { api, application, applyHref, copy, onReschedule } = props;
195
202
  if (!application) {
196
203
  return card(
197
- "One step remains",
204
+ copy,
205
+ copy.provisional.applicationNeeded,
198
206
  /* @__PURE__ */ jsxs3(Fragment2, { children: [
199
- /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: "Your account is ready, and the application that completes it takes a few minutes." }),
200
- /* @__PURE__ */ jsx3("a", { className: "apply-link", href: applyHref, children: "Apply for membership" })
207
+ /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: copy.provisional.applicationNeededBody }),
208
+ /* @__PURE__ */ jsx3("a", { className: "apply-link", href: applyHref, children: copy.provisional.apply })
201
209
  ] })
202
210
  );
203
211
  }
204
212
  if (application.status === "refunded") {
205
- return card("Membership refunded", /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: "Your fee has been refunded in full and your membership is canceled." }));
213
+ return card(
214
+ copy,
215
+ copy.provisional.refunded,
216
+ /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: copy.provisional.refundedBody })
217
+ );
206
218
  }
207
- const membership = application.paid ? /* @__PURE__ */ jsxs3("div", { className: "meeting-note", children: [
208
- "Your membership is active",
209
- application.renewalAt ? ` and renews ${fmtDate(application.renewalAt)}` : "",
210
- "."
211
- ] }) : null;
219
+ const membership = application.paid ? /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: application.renewalAt ? formatChapterCopy(copy.provisional.renews, { date: fmtDate(application.renewalAt) }) : copy.provisional.active }) : null;
212
220
  if (application.meetingAt) {
213
221
  return card(
214
- "Your introduction call",
222
+ copy,
223
+ copy.provisional.introductionCall,
215
224
  /* @__PURE__ */ jsxs3(Fragment2, { children: [
216
225
  /* @__PURE__ */ jsx3("div", { className: "meeting-date", children: fullLabel(application.meetingAt, application.timezone) }),
217
- /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: "A calendar invitation with the video call link is in your email." }),
218
- application.meetUrl ? /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: /* @__PURE__ */ jsx3("a", { href: application.meetUrl, target: "_blank", rel: "noopener", children: "Join the video call" }) }) : null,
219
- /* @__PURE__ */ jsx3(Rescheduler, { api, applicationId: application.id, timezone: application.timezone, onRescheduled: onReschedule }),
226
+ /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: copy.provisional.calendarInvite }),
227
+ application.meetUrl ? /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: /* @__PURE__ */ jsx3("a", { href: application.meetUrl, target: "_blank", rel: "noopener", children: copy.provisional.joinCall }) }) : null,
228
+ /* @__PURE__ */ jsx3(
229
+ Rescheduler,
230
+ {
231
+ api,
232
+ applicationId: application.id,
233
+ timezone: application.timezone,
234
+ copy: copy.reschedule,
235
+ onRescheduled: onReschedule
236
+ }
237
+ ),
220
238
  membership
221
239
  ] })
222
240
  );
223
241
  }
224
242
  return card(
225
- "Book your introduction call",
243
+ copy,
244
+ copy.provisional.bookCall,
226
245
  /* @__PURE__ */ jsxs3(Fragment2, { children: [
227
- /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: "Your application is in. Choose a time below, and a calendar invitation will reach your email." }),
228
- /* @__PURE__ */ jsx3(Rescheduler, { api, applicationId: application.id, timezone: application.timezone, onRescheduled: onReschedule, label: "Choose a time" }),
246
+ /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: copy.provisional.bookCallBody }),
247
+ /* @__PURE__ */ jsx3(
248
+ Rescheduler,
249
+ {
250
+ api,
251
+ applicationId: application.id,
252
+ timezone: application.timezone,
253
+ copy: copy.reschedule,
254
+ onRescheduled: onReschedule,
255
+ label: copy.provisional.chooseTime
256
+ }
257
+ ),
229
258
  membership
230
259
  ] })
231
260
  );
232
261
  }
233
262
  function MembersArea(props) {
234
- const { api, signOut, adminHref = "/admin/", applyHref = "/join.html", memberContent } = props;
263
+ const inheritedCopy = useChapterCopy();
264
+ const {
265
+ api,
266
+ signOut,
267
+ adminHref = "/admin/",
268
+ applyHref = "/join.html",
269
+ memberContent,
270
+ renderProvisional
271
+ } = props;
272
+ const copy = props.copy ?? inheritedCopy.members;
235
273
  const [me, setMe] = useState3(null);
236
274
  const [error, setError] = useState3(false);
237
275
  const reload = async () => {
@@ -244,8 +282,8 @@ function MembersArea(props) {
244
282
  useEffect(() => {
245
283
  void reload();
246
284
  }, []);
247
- if (error) return /* @__PURE__ */ jsx3("p", { className: "meeting-note", children: "Sign in is briefly unavailable. Please refresh." });
248
- if (!me) return /* @__PURE__ */ jsx3("p", { className: "meeting-note", children: "Loading\u2026" });
285
+ if (error) return /* @__PURE__ */ jsx3("p", { className: "meeting-note", children: copy.loadFailed });
286
+ if (!me) return /* @__PURE__ */ jsx3("p", { className: "meeting-note", children: inheritedCopy.common.loading });
249
287
  const role = me.role || "provisional";
250
288
  return /* @__PURE__ */ jsxs3(Fragment2, { children: [
251
289
  /* @__PURE__ */ jsxs3("div", { className: "card", children: [
@@ -254,16 +292,34 @@ function MembersArea(props) {
254
292
  /* @__PURE__ */ jsx3("span", { className: "role-badge " + role, children: role })
255
293
  ] }),
256
294
  /* @__PURE__ */ jsxs3("div", { className: "account-actions", children: [
257
- /* @__PURE__ */ jsx3("button", { className: "btn secondary mini", onClick: signOut, children: "Sign out" }),
258
- role === "admin" ? /* @__PURE__ */ jsx3("a", { className: "admin-console-link", href: adminHref, children: "Admin console" }) : null
295
+ /* @__PURE__ */ jsx3("button", { className: "btn secondary mini", onClick: signOut, children: copy.account.signOut }),
296
+ role === "admin" ? /* @__PURE__ */ jsx3("a", { className: "admin-console-link", href: adminHref, children: copy.account.adminConsole }) : null
259
297
  ] })
260
298
  ] }),
261
- role === "provisional" ? /* @__PURE__ */ jsx3(ProvisionalCard, { api, application: me.application, applyHref, onReschedule: reload }) : memberContent ?? /* @__PURE__ */ jsx3("div", { className: "card", children: /* @__PURE__ */ jsx3("div", { className: "card-label", children: "Welcome back." }) })
299
+ role === "provisional" ? (() => {
300
+ const defaultContent = /* @__PURE__ */ jsx3(
301
+ ProvisionalCard,
302
+ {
303
+ api,
304
+ application: me.application,
305
+ applyHref,
306
+ copy,
307
+ onReschedule: reload
308
+ }
309
+ );
310
+ return renderProvisional?.({
311
+ api,
312
+ application: me.application,
313
+ applyHref,
314
+ reload,
315
+ defaultContent
316
+ }) ?? defaultContent;
317
+ })() : memberContent ?? /* @__PURE__ */ jsx3("div", { className: "card", children: /* @__PURE__ */ jsx3("div", { className: "card-label", children: copy.full.welcome }) })
262
318
  ] });
263
319
  }
264
320
 
265
321
  // src/ui/join.tsx
266
- import { useEffect as useEffect2, useState as useState5 } from "preact/hooks";
322
+ import { useEffect as useEffect3, useState as useState6 } from "preact/hooks";
267
323
 
268
324
  // src/ui/payment-step.tsx
269
325
  import { useRef, useState as useState4 } from "preact/hooks";
@@ -288,9 +344,20 @@ function loadStripe() {
288
344
  return loader;
289
345
  }
290
346
  function PaymentStep(props) {
291
- const { applicationId, refundPolicyText, onPaid } = props;
347
+ const inheritedCopy = useChapterCopy();
348
+ const {
349
+ applicationId,
350
+ refundPolicyText,
351
+ onPaid,
352
+ appearance,
353
+ fonts,
354
+ renderPriceLines,
355
+ children
356
+ } = props;
357
+ const copy = props.copy ?? inheritedCopy.join.payment;
292
358
  const [status, setStatus] = useState4("idle");
293
359
  const [error, setError] = useState4(null);
360
+ const [lineItems, setLineItems] = useState4(null);
294
361
  const mountRef = useRef(null);
295
362
  const stripeRef = useRef(null);
296
363
  const elementsRef = useRef(null);
@@ -304,17 +371,22 @@ function PaymentStep(props) {
304
371
  body: JSON.stringify({ applicationId, refundPolicyAck: true })
305
372
  });
306
373
  const data = await res.json();
307
- if (!res.ok || !data.clientSecret || !data.publishableKey) throw new Error("Payment could not be set up. Please try again.");
374
+ if (!res.ok || !data.clientSecret || !data.publishableKey) throw new Error(copy.setupFailed);
308
375
  const stripe = (await loadStripe())(data.publishableKey);
309
- const elements = stripe.elements({ clientSecret: data.clientSecret });
376
+ const elements = stripe.elements({
377
+ clientSecret: data.clientSecret,
378
+ ...appearance ? { appearance } : {},
379
+ ...fonts ? { fonts } : {}
380
+ });
310
381
  const element = elements.create("payment");
311
382
  if (mountRef.current) element.mount(mountRef.current);
312
383
  stripeRef.current = stripe;
313
384
  elementsRef.current = elements;
385
+ setLineItems(data.lineItems ?? null);
314
386
  setStatus("ready");
315
387
  } catch (e) {
316
388
  setStatus("idle");
317
- setError(e instanceof Error ? e.message : "Payment could not be set up. Please try again.");
389
+ setError(e instanceof Error ? e.message : copy.setupFailed);
318
390
  }
319
391
  };
320
392
  const pay = async () => {
@@ -323,45 +395,68 @@ function PaymentStep(props) {
323
395
  if (!stripe || !elements) return;
324
396
  setStatus("confirming");
325
397
  setError(null);
326
- const returnUrl = typeof window !== "undefined" ? `${window.location.origin}${window.location.pathname}?redirect_status=succeeded` : void 0;
398
+ let returnUrl;
399
+ if (typeof window !== "undefined") {
400
+ const next = new URL(window.location.href);
401
+ next.searchParams.set("application", applicationId);
402
+ next.searchParams.set("redirect_status", "succeeded");
403
+ returnUrl = next.toString();
404
+ }
327
405
  const result = await stripe.confirmPayment({ elements, confirmParams: { return_url: returnUrl }, redirect: "if_required" });
328
406
  if (result.error) {
329
- setError(result.error.message ?? "The payment could not be completed.");
407
+ setError(result.error.message ?? copy.incomplete);
330
408
  setStatus("ready");
331
409
  return;
332
410
  }
333
411
  const paid = result.paymentIntent?.status;
334
412
  if (paid === "succeeded" || paid === "processing") onPaid();
335
413
  else {
336
- setError("The payment did not complete. Please try again.");
414
+ setError(copy.incomplete);
337
415
  setStatus("ready");
338
416
  }
339
417
  };
340
418
  return /* @__PURE__ */ jsxs4("div", { className: "join-pay", children: [
341
- /* @__PURE__ */ jsxs4("label", { className: "compliance-box", children: [
342
- /* @__PURE__ */ jsx4(
343
- "input",
344
- {
345
- type: "checkbox",
346
- disabled: status !== "idle",
347
- onChange: (e) => {
348
- if (e.currentTarget.checked) void begin();
419
+ /* @__PURE__ */ jsxs4("div", { className: "compliance-box", children: [
420
+ /* @__PURE__ */ jsx4("p", { className: "compliance-policy", children: refundPolicyText }),
421
+ /* @__PURE__ */ jsxs4("label", { className: "compliance-check", children: [
422
+ /* @__PURE__ */ jsx4(
423
+ "input",
424
+ {
425
+ type: "checkbox",
426
+ disabled: status !== "idle",
427
+ onChange: (e) => {
428
+ if (e.currentTarget.checked) void begin();
429
+ }
349
430
  }
350
- }
351
- ),
352
- /* @__PURE__ */ jsx4("span", { children: refundPolicyText })
431
+ ),
432
+ copy.consent ? /* @__PURE__ */ jsx4("span", { children: copy.consent }) : null
433
+ ] })
353
434
  ] }),
354
- status === "loading" ? /* @__PURE__ */ jsx4("p", { className: "pay-status", children: "Preparing secure payment\u2026" }) : null,
435
+ status === "loading" ? /* @__PURE__ */ jsx4("p", { className: "pay-status", children: copy.preparing }) : null,
436
+ lineItems && renderPriceLines ? renderPriceLines(lineItems) : null,
437
+ children,
355
438
  /* @__PURE__ */ jsx4("div", { ref: mountRef, hidden: status === "idle" || status === "loading" }),
356
- status === "ready" || status === "confirming" ? /* @__PURE__ */ jsx4("button", { className: "submit-btn", disabled: status === "confirming", onClick: () => void pay(), children: status === "confirming" ? "Processing\u2026" : "Pay and continue" }) : null,
439
+ status === "ready" || status === "confirming" ? /* @__PURE__ */ jsx4("button", { className: "submit-btn", disabled: status === "confirming", onClick: () => void pay(), children: status === "confirming" ? copy.processing : copy.payAndContinue }) : null,
357
440
  error ? /* @__PURE__ */ jsx4("p", { className: "pay-error", children: error }) : null
358
441
  ] });
359
442
  }
360
443
 
361
- // src/ui/join.tsx
444
+ // src/ui/form-fields.ts
445
+ function collectFormFields(form) {
446
+ const fields = {};
447
+ for (const key of new Set(form.keys())) {
448
+ const values = form.getAll(key).filter((v) => typeof v === "string");
449
+ if (values.length === 0) continue;
450
+ fields[key] = values.length > 1 ? values : values[0];
451
+ }
452
+ return fields;
453
+ }
454
+
455
+ // src/ui/join-booking.tsx
456
+ import { useEffect as useEffect2, useState as useState5 } from "preact/hooks";
362
457
  import { jsx as jsx5, jsxs as jsxs5 } from "preact/jsx-runtime";
363
458
  function JoinBooking(props) {
364
- const { applicationId, onBooked } = props;
459
+ const { applicationId, copy, onBooked } = props;
365
460
  const [state, setState] = useState5(null);
366
461
  const [msg, setMsg] = useState5(null);
367
462
  const [busy, setBusy] = useState5(false);
@@ -373,12 +468,12 @@ function JoinBooking(props) {
373
468
  const data = await res.json();
374
469
  if (data.schedulingReady === false || !data.slots?.length) {
375
470
  setState(null);
376
- setMsg("Scheduling is briefly unavailable \u2014 we'll reach out by email to arrange your call.");
471
+ setMsg(copy.unavailable);
377
472
  return;
378
473
  }
379
474
  setState({ slots: data.slots, timezone: data.timezone ?? "UTC" });
380
475
  } catch {
381
- setMsg("Times are briefly unavailable. Please try again.");
476
+ setMsg(copy.loadFailed);
382
477
  }
383
478
  };
384
479
  useEffect2(() => {
@@ -397,19 +492,19 @@ function JoinBooking(props) {
397
492
  const data = await res.json();
398
493
  if (res.status === 409 && data.code === "calendar_slot_unavailable") {
399
494
  setSelected(null);
400
- setMsg("That time was just taken. Here are the current openings.");
495
+ setMsg(copy.slotTaken);
401
496
  await load();
402
497
  return;
403
498
  }
404
- if (!res.ok) throw new Error(data.error ?? "The booking could not be completed.");
499
+ if (!res.ok) throw new Error(data.error ?? copy.failed);
405
500
  onBooked({ startAt: data.startAt ?? selected.startAt, timezone: state?.timezone ?? "UTC" });
406
501
  } catch (e) {
407
- setMsg(e instanceof Error ? e.message : "The booking could not be completed.");
502
+ setMsg(e instanceof Error ? e.message : copy.failed);
408
503
  } finally {
409
504
  setBusy(false);
410
505
  }
411
506
  };
412
- if (!state) return /* @__PURE__ */ jsx5("p", { className: "slots-status", children: msg ?? "Loading available times\u2026" });
507
+ if (!state) return /* @__PURE__ */ jsx5("p", { className: "slots-status", children: msg ?? copy.loading });
413
508
  return /* @__PURE__ */ jsxs5("div", { className: "join-book", children: [
414
509
  /* @__PURE__ */ jsx5(
415
510
  SlotPicker,
@@ -422,25 +517,86 @@ function JoinBooking(props) {
422
517
  }
423
518
  ),
424
519
  /* @__PURE__ */ jsxs5("div", { className: "slot-confirm", hidden: !selected, children: [
425
- /* @__PURE__ */ jsx5("button", { className: "submit-btn", disabled: busy, onClick: () => void book(), children: busy ? "Booking\u2026" : "Book this time" }),
520
+ /* @__PURE__ */ jsx5("button", { className: "submit-btn", disabled: busy, onClick: () => void book(), children: busy ? copy.booking : copy.book }),
426
521
  msg ? /* @__PURE__ */ jsx5("p", { className: "step2-note", children: msg }) : null
427
522
  ] })
428
523
  ] });
429
524
  }
525
+
526
+ // src/ui/join.tsx
527
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "preact/jsx-runtime";
528
+ async function loadJoinResume(applicationId, fetcher = fetch) {
529
+ const res = await fetcher(`/api/join/resume?application=${encodeURIComponent(applicationId)}`);
530
+ const data = await res.json();
531
+ if (!res.ok) throw new Error(data.error ?? "The application could not be resumed.");
532
+ if (data.state === "done" && data.booked) {
533
+ return { step: "done", applicationId: data.applicationId, booked: data.booked };
534
+ }
535
+ if (data.state === "paymentPending") {
536
+ return { step: "paymentPending", applicationId: data.applicationId };
537
+ }
538
+ if (data.state === "booking") {
539
+ return { step: "booking", applicationId: data.applicationId };
540
+ }
541
+ return { step: "payment", applicationId: data.applicationId };
542
+ }
430
543
  function JoinIsland(props) {
431
- const { config, children, membersHref = "/members/" } = props;
432
- const [step, setStep] = useState5("form");
433
- const [applicationId, setApplicationId] = useState5(null);
434
- const [error, setError] = useState5(null);
435
- const [submitting, setSubmitting] = useState5(false);
436
- const [booked, setBooked] = useState5(null);
544
+ const inheritedCopy = useChapterCopy();
545
+ const {
546
+ config,
547
+ children,
548
+ membersHref = "/members/",
549
+ renderStepHeader,
550
+ renderDone,
551
+ renderSubmit
552
+ } = props;
553
+ const copy = props.copy ?? config.copy ?? inheritedCopy.join;
554
+ const [state, setState] = useState6(props.initialState ?? { step: "form" });
555
+ const [error, setError] = useState6(null);
556
+ const [submitting, setSubmitting] = useState6(false);
557
+ useEffect3(() => {
558
+ const resolver = props.loadResume ?? (() => {
559
+ if (typeof window === "undefined") return Promise.resolve(null);
560
+ const applicationId = new URL(window.location.href).searchParams.get("application");
561
+ return applicationId ? loadJoinResume(applicationId) : Promise.resolve(null);
562
+ });
563
+ let live = true;
564
+ void resolver().then((resumed) => {
565
+ if (live && resumed) setState(resumed);
566
+ }).catch(() => {
567
+ if (live) setError(copy.form.unexpectedFailure);
568
+ });
569
+ return () => {
570
+ live = false;
571
+ };
572
+ }, [props.loadResume]);
573
+ useEffect3(() => {
574
+ if (state.step !== "paymentPending") return;
575
+ let live = true;
576
+ let timer;
577
+ const poll = async () => {
578
+ try {
579
+ const resumed = props.loadResume ? await props.loadResume() : await loadJoinResume(state.applicationId);
580
+ if (!live || !resumed) return;
581
+ setState(resumed);
582
+ if (resumed.step === "paymentPending") timer = setTimeout(() => void poll(), 1500);
583
+ } catch {
584
+ if (live) timer = setTimeout(() => void poll(), 2500);
585
+ }
586
+ };
587
+ timer = setTimeout(() => void poll(), 1e3);
588
+ return () => {
589
+ live = false;
590
+ if (timer) clearTimeout(timer);
591
+ };
592
+ }, [state, props.loadResume]);
437
593
  const submit = async (e) => {
438
594
  e.preventDefault();
595
+ if (submitting || props.submitDisabled === true) return;
439
596
  setSubmitting(true);
440
597
  setError(null);
441
598
  try {
442
- const fields = {};
443
- for (const [k, v] of new FormData(e.currentTarget).entries()) fields[k] = v;
599
+ const fields = collectFormFields(new FormData(e.currentTarget));
444
600
  fields.submissionId = crypto.randomUUID();
445
601
  const res = await fetch("/api/applications", {
446
602
  method: "POST",
@@ -448,42 +604,80 @@ function JoinIsland(props) {
448
604
  body: JSON.stringify(fields)
449
605
  });
450
606
  const data = await res.json();
451
- if (!res.ok || !data.id) throw new Error(data.error ?? "Your application could not be submitted.");
452
- setApplicationId(data.id);
453
- setStep(config.paymentsReady ? "pay" : "book");
607
+ if (!res.ok || !data.id) throw new Error(data.error ?? copy.form.submitFailed);
608
+ setState({
609
+ step: config.paymentsReady ? "payment" : "booking",
610
+ applicationId: data.id
611
+ });
454
612
  } catch (err) {
455
- setError(err instanceof Error ? err.message : "Something went wrong. Please try again.");
613
+ setError(err instanceof Error ? err.message : copy.form.unexpectedFailure);
456
614
  } finally {
457
615
  setSubmitting(false);
458
616
  }
459
617
  };
460
- if (step === "done" && booked) {
461
- return /* @__PURE__ */ jsxs5("div", { className: "join-done card", children: [
462
- /* @__PURE__ */ jsx5("div", { className: "card-label", children: "You're booked" }),
463
- /* @__PURE__ */ jsx5("p", { className: "meeting-date", children: fullLabel(booked.startAt, booked.timezone) }),
464
- /* @__PURE__ */ jsx5("p", { className: "meeting-note", children: "A calendar invitation with the video call link is on its way to your email." }),
465
- /* @__PURE__ */ jsx5("a", { className: "apply-link", href: membersHref, children: "Go to your member area" })
618
+ const header = renderStepHeader?.({
619
+ state,
620
+ ..."applicationId" in state ? { applicationId: state.applicationId } : {}
621
+ });
622
+ if (state.step === "done") {
623
+ const body = renderDone?.({ ...state, membersHref }) ?? /* @__PURE__ */ jsxs6("div", { className: "join-done card", children: [
624
+ /* @__PURE__ */ jsx6("div", { className: "card-label", children: copy.done.label }),
625
+ /* @__PURE__ */ jsx6("p", { className: "meeting-date", children: fullLabel(state.booked.startAt, state.booked.timezone) }),
626
+ /* @__PURE__ */ jsx6("p", { className: "meeting-note", children: copy.done.calendarInvite }),
627
+ /* @__PURE__ */ jsx6("a", { className: "apply-link", href: membersHref, children: copy.done.memberArea })
628
+ ] });
629
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
630
+ header,
631
+ body
466
632
  ] });
467
633
  }
468
- if (step === "book" && applicationId) {
469
- return /* @__PURE__ */ jsx5(
470
- JoinBooking,
471
- {
472
- applicationId,
473
- onBooked: (b) => {
474
- setBooked(b);
475
- setStep("done");
634
+ if (state.step === "booking") {
635
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
636
+ header,
637
+ /* @__PURE__ */ jsx6(
638
+ JoinBooking,
639
+ {
640
+ applicationId: state.applicationId,
641
+ copy: copy.booking,
642
+ onBooked: (booked) => {
643
+ setState({ step: "done", applicationId: state.applicationId, booked });
644
+ }
476
645
  }
477
- }
478
- );
646
+ )
647
+ ] });
648
+ }
649
+ if (state.step === "payment") {
650
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
651
+ header,
652
+ /* @__PURE__ */ jsx6(
653
+ PaymentStep,
654
+ {
655
+ applicationId: state.applicationId,
656
+ refundPolicyText: config.refundPolicyText ?? "",
657
+ copy: copy.payment,
658
+ appearance: props.payment?.appearance,
659
+ fonts: props.payment?.fonts,
660
+ renderPriceLines: props.payment?.renderPriceLines,
661
+ onPaid: () => setState({ step: "booking", applicationId: state.applicationId }),
662
+ children: props.payment?.children
663
+ }
664
+ )
665
+ ] });
479
666
  }
480
- if (step === "pay" && applicationId) {
481
- return /* @__PURE__ */ jsx5(PaymentStep, { applicationId, refundPolicyText: config.refundPolicyText ?? "", onPaid: () => setStep("book") });
667
+ if (state.step === "paymentPending") {
668
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
669
+ header,
670
+ /* @__PURE__ */ jsx6("p", { className: "pay-status", role: "status", children: copy.payment.pending })
671
+ ] });
482
672
  }
483
- return /* @__PURE__ */ jsxs5("form", { className: "join-form", onSubmit: (e) => void submit(e), children: [
484
- children,
485
- error ? /* @__PURE__ */ jsx5("p", { className: "join-error", children: error }) : null,
486
- /* @__PURE__ */ jsx5("button", { className: "submit-btn", type: "submit", disabled: submitting, children: submitting ? "Submitting\u2026" : "Submit application" })
673
+ const submitDisabled = submitting || props.submitDisabled === true;
674
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
675
+ header,
676
+ /* @__PURE__ */ jsxs6("form", { className: "join-form", onSubmit: (e) => void submit(e), children: [
677
+ children,
678
+ error ? /* @__PURE__ */ jsx6("p", { className: "join-error", children: error }) : null,
679
+ renderSubmit?.({ submitting, disabled: submitDisabled }) ?? /* @__PURE__ */ jsx6("button", { className: "submit-btn", type: "submit", disabled: submitDisabled, children: submitting ? copy.form.submitting : copy.form.submit })
680
+ ] })
487
681
  ] });
488
682
  }
489
683
 
@@ -500,6 +694,7 @@ export {
500
694
  Rescheduler,
501
695
  MembersArea,
502
696
  PaymentStep,
697
+ loadJoinResume,
503
698
  JoinIsland
504
699
  };
505
- //# sourceMappingURL=chunk-4COPR4SY.js.map
700
+ //# sourceMappingURL=chunk-GMGTJIY3.js.map