@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.
- package/README.md +99 -26
- package/dist/{chunk-4COPR4SY.js → chunk-GMGTJIY3.js} +292 -97
- package/dist/chunk-GMGTJIY3.js.map +1 -0
- package/dist/chunk-WGQO4WDJ.js +412 -0
- package/dist/chunk-WGQO4WDJ.js.map +1 -0
- package/dist/{chunk-C4XQSRW7.js → chunk-ZPAJL7KG.js} +225 -190
- package/dist/chunk-ZPAJL7KG.js.map +1 -0
- package/dist/{types-CgWUQ2MH.d.ts → copy-context-Dp5hvj5W.d.ts} +85 -20
- package/dist/index.cjs +340 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -24
- package/dist/index.d.ts +111 -24
- package/dist/index.js +340 -9
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +5 -2
- package/dist/ui/admin/index.js +8 -3
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +11 -5
- package/dist/ui/member/index.d.ts +118 -2
- package/dist/ui/member/index.js +10 -4
- package/dist/worker/index.cjs +274 -213
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +74 -19
- package/dist/worker/index.d.ts +74 -19
- package/dist/worker/index.js +274 -213
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/adopt-existing.md +23 -5
- package/runbooks/greenfield.md +85 -4
- package/dist/chunk-3QKGKUTX.js +0 -91
- package/dist/chunk-3QKGKUTX.js.map +0 -1
- package/dist/chunk-4COPR4SY.js.map +0 -1
- package/dist/chunk-C4XQSRW7.js.map +0 -1
|
@@ -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
|
|
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(
|
|
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(
|
|
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 :
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
204
|
+
copy,
|
|
205
|
+
copy.provisional.applicationNeeded,
|
|
198
206
|
/* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
199
|
-
/* @__PURE__ */ jsx3("div", { className: "meeting-note", children:
|
|
200
|
-
/* @__PURE__ */ jsx3("a", { className: "apply-link", href: applyHref, children:
|
|
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(
|
|
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__ */
|
|
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
|
-
|
|
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:
|
|
218
|
-
application.meetUrl ? /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: /* @__PURE__ */ jsx3("a", { href: application.meetUrl, target: "_blank", rel: "noopener", children:
|
|
219
|
-
/* @__PURE__ */ jsx3(
|
|
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
|
-
|
|
243
|
+
copy,
|
|
244
|
+
copy.provisional.bookCall,
|
|
226
245
|
/* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
227
|
-
/* @__PURE__ */ jsx3("div", { className: "meeting-note", children:
|
|
228
|
-
/* @__PURE__ */ jsx3(
|
|
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
|
|
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:
|
|
248
|
-
if (!me) return /* @__PURE__ */ jsx3("p", { className: "meeting-note", children:
|
|
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:
|
|
258
|
-
role === "admin" ? /* @__PURE__ */ jsx3("a", { className: "admin-console-link", href: adminHref, children:
|
|
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" ?
|
|
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
|
|
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
|
|
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(
|
|
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({
|
|
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 :
|
|
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
|
-
|
|
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 ??
|
|
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(
|
|
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("
|
|
342
|
-
/* @__PURE__ */ jsx4(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
431
|
+
),
|
|
432
|
+
copy.consent ? /* @__PURE__ */ jsx4("span", { children: copy.consent }) : null
|
|
433
|
+
] })
|
|
353
434
|
] }),
|
|
354
|
-
status === "loading" ? /* @__PURE__ */ jsx4("p", { className: "pay-status", children:
|
|
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" ?
|
|
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/
|
|
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(
|
|
471
|
+
setMsg(copy.unavailable);
|
|
377
472
|
return;
|
|
378
473
|
}
|
|
379
474
|
setState({ slots: data.slots, timezone: data.timezone ?? "UTC" });
|
|
380
475
|
} catch {
|
|
381
|
-
setMsg(
|
|
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(
|
|
495
|
+
setMsg(copy.slotTaken);
|
|
401
496
|
await load();
|
|
402
497
|
return;
|
|
403
498
|
}
|
|
404
|
-
if (!res.ok) throw new Error(data.error ??
|
|
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 :
|
|
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 ??
|
|
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 ?
|
|
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
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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 ??
|
|
452
|
-
|
|
453
|
-
|
|
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 :
|
|
613
|
+
setError(err instanceof Error ? err.message : copy.form.unexpectedFailure);
|
|
456
614
|
} finally {
|
|
457
615
|
setSubmitting(false);
|
|
458
616
|
}
|
|
459
617
|
};
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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 === "
|
|
469
|
-
return /* @__PURE__ */
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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 === "
|
|
481
|
-
return /* @__PURE__ */
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
/* @__PURE__ */
|
|
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-
|
|
700
|
+
//# sourceMappingURL=chunk-GMGTJIY3.js.map
|