@medalsocial/booking 0.2.1 → 0.3.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/CHANGELOG.md +22 -0
- package/README.md +21 -3
- package/dist/core/attempt-store.d.mts +1 -0
- package/dist/core/attempt-store.mjs +13 -0
- package/dist/core/attempt-store.mjs.map +1 -1
- package/dist/core/config.d.mts +37 -0
- package/dist/core/config.mjs +29 -0
- package/dist/core/config.mjs.map +1 -1
- package/dist/core/machine.d.mts +27 -1
- package/dist/core/machine.mjs +52 -4
- package/dist/core/machine.mjs.map +1 -1
- package/dist/next/medal.d.mts +8 -1
- package/dist/next/medal.mjs +16 -5
- package/dist/next/medal.mjs.map +1 -1
- package/dist/next/portal/medal-portal.mjs +1 -62
- package/dist/next/portal/medal-portal.mjs.map +1 -1
- package/dist/next/redact.mjs +76 -0
- package/dist/next/redact.mjs.map +1 -0
- package/dist/next/routes/create.mjs +67 -9
- package/dist/next/routes/create.mjs.map +1 -1
- package/dist/next/routes/shared.mjs +3 -0
- package/dist/next/routes/shared.mjs.map +1 -1
- package/dist/react/BookingWizard.mjs +139 -11
- package/dist/react/BookingWizard.mjs.map +1 -1
- package/dist/react/LoginSheet.d.mts +19 -1
- package/dist/react/LoginSheet.mjs +15 -1
- package/dist/react/LoginSheet.mjs.map +1 -1
- package/dist/react/Provider.d.mts +2 -1
- package/dist/react/Provider.mjs.map +1 -1
- package/dist/react/actions.d.mts +16 -5
- package/dist/react/actions.mjs +5 -1
- package/dist/react/actions.mjs.map +1 -1
- package/dist/react/index.d.mts +2 -2
- package/dist/react/kit.d.mts +6 -1
- package/dist/react/kit.mjs +4 -3
- package/dist/react/kit.mjs.map +1 -1
- package/dist/react/labels/login.d.mts +2 -0
- package/dist/react/labels/login.mjs +4 -2
- package/dist/react/labels/login.mjs.map +1 -1
- package/dist/react/labels/screens.mjs +58 -2
- package/dist/react/labels/screens.mjs.map +1 -1
- package/dist/react/labels/wizard.d.mts +8 -0
- package/dist/react/labels/wizard.mjs +6 -0
- package/dist/react/labels/wizard.mjs.map +1 -1
- package/dist/react/screen-labels.mjs +5 -0
- package/dist/react/screen-labels.mjs.map +1 -1
- package/dist/react/useBooking.d.mts +19 -1
- package/dist/react/useBooking.mjs +269 -42
- package/dist/react/useBooking.mjs.map +1 -1
- package/dist/react/wizard/adapters.mjs +66 -3
- package/dist/react/wizard/adapters.mjs.map +1 -1
- package/dist/react/wizard/index.d.mts +2 -2
- package/package.json +4 -4
|
@@ -6,9 +6,9 @@ import { useBookingKit } from "./Provider.mjs";
|
|
|
6
6
|
import { screenLabels } from "./screen-labels.mjs";
|
|
7
7
|
import { LoginSheet as LoginSheet$1 } from "./LoginSheet.mjs";
|
|
8
8
|
import { useBooking } from "./useBooking.mjs";
|
|
9
|
-
import { addedChildEntries, confirmationProps, detailsLines, detailsScreenBase, familyEntries, guestChoices, partyPeople, partySizeWord, serviceFitsFor, serviceScreenBase, weekendNoteFor } from "./wizard/adapters.mjs";
|
|
9
|
+
import { GUEST_ADULT_SEAT, addedChildEntries, confirmationProps, detailsLines, detailsScreenBase, familyEntries, guestChoices, partyPeople, partySizeWord, recapProps, serviceFitsFor, serviceScreenBase, stylistFace, weekendNoteFor } from "./wizard/adapters.mjs";
|
|
10
10
|
import { forMedaScreen, wizardErrorText } from "./wizard-error.mjs";
|
|
11
|
-
import { BookingButton, Confirmation, DetailsScreen, LiveStatus, ServiceScreen, StylistScreen, SummaryBar, TakenToast, TimeScreen, TimeScreenSkeleton, WhoScreen } from "@medalsocial/meda/booking";
|
|
11
|
+
import { BookingButton, BookingRecap, Confirmation, DetailsScreen, LiveStatus, ServiceScreen, StylistScreen, SummaryBar, TakenToast, TimeScreen, TimeScreenSkeleton, WhoScreen } from "@medalsocial/meda/booking";
|
|
12
12
|
import { Suspense, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
13
13
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
14
14
|
import { ArrowLeft } from "lucide-react";
|
|
@@ -65,9 +65,17 @@ function BookingWizard(props) {
|
|
|
65
65
|
}
|
|
66
66
|
function BookingWizardShell(props) {
|
|
67
67
|
const resolved = useBookingKit(props, props.contact);
|
|
68
|
-
const
|
|
68
|
+
const loginOffered = props.actions !== void 0 && resolved.kit.config.portal.enabled;
|
|
69
|
+
const booking = useBooking({
|
|
70
|
+
...props,
|
|
71
|
+
loginOffered
|
|
72
|
+
});
|
|
69
73
|
const { kit } = booking;
|
|
70
74
|
const { labels } = kit;
|
|
75
|
+
const unreachableLogin = kit.config.account.required && !loginOffered;
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (unreachableLogin && typeof process !== "undefined" && process.env.NODE_ENV !== "production") console.warn("[booking] account.required without login actions: nobody can log in to book.");
|
|
78
|
+
}, [unreachableLogin]);
|
|
71
79
|
const { state, restore, confirmed, onEvent } = {
|
|
72
80
|
...booking,
|
|
73
81
|
onEvent: props.onEvent
|
|
@@ -114,6 +122,28 @@ function BookingWizardShell(props) {
|
|
|
114
122
|
focusAfterSignIn.current = false;
|
|
115
123
|
document.getElementById(STEP_HEADINGS[state.step])?.focus({ preventScroll: true });
|
|
116
124
|
}, [signedIn, state.step]);
|
|
125
|
+
/**
|
|
126
|
+
* A 401 put the gate back over the form: focus its heading (the details
|
|
127
|
+
* step's id), where the notice beside it says why. Once per loss, and only
|
|
128
|
+
* once the gate is actually on screen — a replay answers behind the skeleton.
|
|
129
|
+
*/
|
|
130
|
+
const sessionLost = booking.login.sessionLost;
|
|
131
|
+
const sessionLossFocused = useRef(false);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!sessionLost) {
|
|
134
|
+
sessionLossFocused.current = false;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (sessionLossFocused.current || restore.restoring || state.step !== "details") return;
|
|
138
|
+
sessionLossFocused.current = true;
|
|
139
|
+
const heading = document.getElementById(STEP_HEADINGS.details);
|
|
140
|
+
scrollToTop(rootRef.current);
|
|
141
|
+
heading?.focus({ preventScroll: true });
|
|
142
|
+
}, [
|
|
143
|
+
sessionLost,
|
|
144
|
+
restore.restoring,
|
|
145
|
+
state.step
|
|
146
|
+
]);
|
|
117
147
|
useEffect(() => {
|
|
118
148
|
if (!timeStepReady || !focusTimeWhenReady.current) return;
|
|
119
149
|
focusTimeWhenReady.current = false;
|
|
@@ -175,8 +205,13 @@ function BookingWizardShell(props) {
|
|
|
175
205
|
booking.login.signIn(who);
|
|
176
206
|
};
|
|
177
207
|
const multiSelect = state.step === "service" && kit.config.party.maxServicesPerPerson > 1;
|
|
178
|
-
|
|
179
|
-
|
|
208
|
+
/**
|
|
209
|
+
* `account.required`: every booking is a logged-in parent's. The login is
|
|
210
|
+
* not offered along the way — it IS «Bekreft», for a parent not logged in.
|
|
211
|
+
*/
|
|
212
|
+
const accountRequired = kit.config.account.required && loginOffered;
|
|
213
|
+
const gated = accountRequired && state.step === "details" && !booking.login.loggedIn;
|
|
214
|
+
const loginRow = loginOffered && !accountRequired ? /* @__PURE__ */ jsx(LoginRow, {
|
|
180
215
|
booking,
|
|
181
216
|
actions: props.actions,
|
|
182
217
|
overrides: props,
|
|
@@ -199,8 +234,12 @@ function BookingWizardShell(props) {
|
|
|
199
234
|
className: resolved.classNames.wizard?.alert ?? "rounded-lg border border-destructive/40 bg-destructive/10 px-5 py-3 text-sm",
|
|
200
235
|
children: wizardErrorText(labels, state.error, kit.config.party.maxServicesPerPerson)
|
|
201
236
|
}),
|
|
237
|
+
state.step === "details" && !gated && kit.config.screens.recap && /* @__PURE__ */ jsx(RecapStep, {
|
|
238
|
+
booking,
|
|
239
|
+
resolved
|
|
240
|
+
}),
|
|
202
241
|
state.step === "details" && loginRow,
|
|
203
|
-
loginOffered && booking.login.vippsConfirm !== null && signedIn === null && /* @__PURE__ */ jsx(LoginSheet$1, {
|
|
242
|
+
loginOffered && booking.login.vippsConfirm !== null && signedIn === null && !gated && /* @__PURE__ */ jsx(LoginSheet$1, {
|
|
204
243
|
...overridesOf(props),
|
|
205
244
|
actions: props.actions,
|
|
206
245
|
presentation: "sheet",
|
|
@@ -228,17 +267,24 @@ function BookingWizardShell(props) {
|
|
|
228
267
|
booking,
|
|
229
268
|
resolved
|
|
230
269
|
}),
|
|
231
|
-
state.step === "details" && /* @__PURE__ */ jsx(
|
|
270
|
+
state.step === "details" && (gated ? /* @__PURE__ */ jsx(AccountGate, {
|
|
271
|
+
booking,
|
|
272
|
+
resolved,
|
|
273
|
+
actions: props.actions,
|
|
274
|
+
overrides: props,
|
|
275
|
+
onSignedIn
|
|
276
|
+
}) : /* @__PURE__ */ jsx(DetailsStep, {
|
|
232
277
|
booking,
|
|
233
278
|
resolved
|
|
234
|
-
})
|
|
279
|
+
}))
|
|
235
280
|
]
|
|
236
281
|
}, state.step),
|
|
237
282
|
!multiSelect && /* @__PURE__ */ jsx(SummaryBar, {
|
|
238
|
-
line: booking.derived.summary,
|
|
283
|
+
...kit.config.screens.summaryDetail ? booking.derived.summaryParts : { line: booking.derived.summary },
|
|
239
284
|
canAdvance: booking.derived.canAdvance,
|
|
240
285
|
step: state.step,
|
|
241
286
|
onNext: booking.next,
|
|
287
|
+
hideNextWhenDisabled: kit.config.screens.hideDisabledNext,
|
|
242
288
|
labels: screenLabels(labels),
|
|
243
289
|
classNames: resolved.classNames.summary
|
|
244
290
|
})
|
|
@@ -269,6 +315,10 @@ function WhoStep({ booking, resolved, loginRow }) {
|
|
|
269
315
|
isGuestSeat: kit.wizard.isGuestSeat,
|
|
270
316
|
onChoose: people.choosePeople,
|
|
271
317
|
onAddChild: people.addChild,
|
|
318
|
+
guestParty: kit.config.screens.guestParty ? {
|
|
319
|
+
child: kit.wizard.guestChild,
|
|
320
|
+
adult: GUEST_ADULT_SEAT
|
|
321
|
+
} : void 0,
|
|
272
322
|
loginRow,
|
|
273
323
|
currentYear: Number(kit.clock.dayKey(booking.slots.fromTs).slice(0, 4)),
|
|
274
324
|
classNames: resolved.classNames.who,
|
|
@@ -289,7 +339,7 @@ function ServiceStep({ booking, resolved, multiSelect }) {
|
|
|
289
339
|
format: kit.format,
|
|
290
340
|
services: catalogue.services,
|
|
291
341
|
...serviceScreenBase(kit),
|
|
292
|
-
serviceFits: serviceFitsFor(kit, state.people.map(ageOf)),
|
|
342
|
+
serviceFits: serviceFitsFor(kit, state.people.map(ageOf), state.people),
|
|
293
343
|
initialCategory: catalogue.deepLinkCategory,
|
|
294
344
|
onPick: (service) => pickFor(0, service, () => booking.pickService(service)),
|
|
295
345
|
suggestion: single ? people.suggestionFor(single) : null,
|
|
@@ -339,6 +389,8 @@ function WhenStep({ booking, resolved }) {
|
|
|
339
389
|
pendingName: catalogue.chosenStylistName,
|
|
340
390
|
notice: catalogue.stylistNotice,
|
|
341
391
|
onPick: booking.pickResource,
|
|
392
|
+
firstAvailableFaces: config.screens.firstAvailableFaces,
|
|
393
|
+
edgeFade: config.screens.stylistEdgeFade,
|
|
342
394
|
party: party ? {
|
|
343
395
|
mode: state.partyMode,
|
|
344
396
|
size: items.length,
|
|
@@ -368,6 +420,8 @@ function WhenStep({ booking, resolved }) {
|
|
|
368
420
|
phone,
|
|
369
421
|
weekendNote: (dayTs) => weekendNoteFor(kit, party ? items : items.slice(0, 1), dayTs),
|
|
370
422
|
onPick: booking.pickSlot,
|
|
423
|
+
soonest: config.screens.soonest ? { resolveStylist: (resourceId) => stylistFace(catalogue.resources, resourceId) } : void 0,
|
|
424
|
+
dayFullness: config.screens.dayFullness,
|
|
371
425
|
party: party ? {
|
|
372
426
|
items,
|
|
373
427
|
mode: state.partyMode,
|
|
@@ -398,6 +452,36 @@ function WhenStep({ booking, resolved }) {
|
|
|
398
452
|
})
|
|
399
453
|
] });
|
|
400
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* The details step's recap (`config.screens.recap`): the day, hours, who and
|
|
457
|
+
* total of the booking the form below submits, «edit» back to the time step,
|
|
458
|
+
* and a swap to another stylist free at that minute.
|
|
459
|
+
*/
|
|
460
|
+
function RecapStep({ booking, resolved }) {
|
|
461
|
+
const { kit, state, catalogue, slots } = booking;
|
|
462
|
+
const recap = useMemo(() => recapProps(kit, state, catalogue.resources, slots.single), [
|
|
463
|
+
kit,
|
|
464
|
+
state,
|
|
465
|
+
catalogue.resources,
|
|
466
|
+
slots.single
|
|
467
|
+
]);
|
|
468
|
+
return /* @__PURE__ */ jsx(BookingRecap, {
|
|
469
|
+
lines: recap.lines,
|
|
470
|
+
totalOre: booking.derived.total,
|
|
471
|
+
format: kit.format,
|
|
472
|
+
labels: screenLabels(kit.labels),
|
|
473
|
+
onEdit: () => booking.dispatch({
|
|
474
|
+
type: "goToStep",
|
|
475
|
+
step: "when"
|
|
476
|
+
}),
|
|
477
|
+
alternatives: recap.alternatives,
|
|
478
|
+
onSwap: (resourceId) => booking.pickSlot({
|
|
479
|
+
startTs: state.startTs,
|
|
480
|
+
resourceId
|
|
481
|
+
}),
|
|
482
|
+
classNames: resolved.classNames.recap
|
|
483
|
+
});
|
|
484
|
+
}
|
|
401
485
|
function timeComponents({ components }) {
|
|
402
486
|
if (!components.DayChip && !components.TimeChip) return void 0;
|
|
403
487
|
return {
|
|
@@ -420,12 +504,56 @@ function DetailsStep({ booking, resolved }) {
|
|
|
420
504
|
submissionNonce: booking.submissionNonce,
|
|
421
505
|
family: guardian?.family,
|
|
422
506
|
guardianPhone: guardian?.phone ?? null,
|
|
507
|
+
emailReadOnly: kit.config.account.required && guardian !== null,
|
|
423
508
|
format: kit.format,
|
|
424
509
|
labels: screenLabels(kit.labels),
|
|
425
510
|
classNames: resolved.classNames.details,
|
|
426
511
|
components: resolved.components.FamilyChip ? { FamilyChip: resolved.components.FamilyChip } : void 0
|
|
427
512
|
});
|
|
428
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* «Bekreft» under `account.required` for a parent not logged in: «Nesten
|
|
516
|
+
* ferdig», a line, and the login — Vipps, or e-mail — in place of the form.
|
|
517
|
+
* The heading carries the details step's id, so the focus a step change moves
|
|
518
|
+
* lands here, and the form's heading takes it over once the parent is in.
|
|
519
|
+
*/
|
|
520
|
+
function AccountGate({ booking, resolved, actions, overrides, onSignedIn }) {
|
|
521
|
+
const { labels } = booking.kit;
|
|
522
|
+
const { classNames } = resolved;
|
|
523
|
+
const slot = (base, extra) => [base, extra].filter(Boolean).join(" ");
|
|
524
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
525
|
+
"aria-labelledby": STEP_HEADINGS.details,
|
|
526
|
+
className: slot("space-y-6", classNames.details?.root),
|
|
527
|
+
children: [
|
|
528
|
+
/* @__PURE__ */ jsx("h2", {
|
|
529
|
+
id: STEP_HEADINGS.details,
|
|
530
|
+
tabIndex: -1,
|
|
531
|
+
className: slot("font-sans text-2xl font-bold outline-none md:text-3xl", classNames.details?.heading),
|
|
532
|
+
children: labelText(labels["wizard.account.heading"])
|
|
533
|
+
}),
|
|
534
|
+
/* @__PURE__ */ jsx(LiveStatus, {
|
|
535
|
+
text: booking.login.sessionLost ? labelText(labels["wizard.account.sessionLost"]) : null,
|
|
536
|
+
className: "text-sm font-medium empty:hidden"
|
|
537
|
+
}),
|
|
538
|
+
/* @__PURE__ */ jsx("p", {
|
|
539
|
+
className: "text-muted-foreground",
|
|
540
|
+
children: labelText(labels["wizard.account.intro"])
|
|
541
|
+
}),
|
|
542
|
+
/* @__PURE__ */ jsx("p", {
|
|
543
|
+
className: slot("rounded-lg border border-border bg-card px-4 py-3 text-sm font-medium", classNames.summary?.line),
|
|
544
|
+
children: booking.derived.summary
|
|
545
|
+
}),
|
|
546
|
+
/* @__PURE__ */ jsx(LoginSheet$1, {
|
|
547
|
+
...overridesOf(overrides),
|
|
548
|
+
actions,
|
|
549
|
+
presentation: "gate",
|
|
550
|
+
resumePath: booking.login.resumePath,
|
|
551
|
+
vippsConfirm: booking.login.vippsConfirm,
|
|
552
|
+
onSignedIn
|
|
553
|
+
})
|
|
554
|
+
]
|
|
555
|
+
});
|
|
556
|
+
}
|
|
429
557
|
function ConfirmationStep({ booking, resolved, siteUrl }) {
|
|
430
558
|
const { kit } = booking;
|
|
431
559
|
const [now] = useState(() => Date.now());
|
|
@@ -449,7 +577,7 @@ function LoginRow({ booking, actions, overrides, onSignedIn }) {
|
|
|
449
577
|
const { arrivedAs, signedIn, resumePath } = booking.login;
|
|
450
578
|
const { labels } = booking.kit;
|
|
451
579
|
if (signedIn === null && arrivedAs !== null) return null;
|
|
452
|
-
const who = signedIn
|
|
580
|
+
const who = signedIn?.guardian ?? null;
|
|
453
581
|
const name = who === null ? null : [who.firstName, who.lastName].filter(Boolean).join(" ").trim() || who.email;
|
|
454
582
|
const confirmation = signedIn === null ? null : name === null ? labelText(labels["wizard.signedIn"]) : fill(labels["wizard.signedInAs"], { name });
|
|
455
583
|
return /* @__PURE__ */ jsxs("div", { children: [signedIn === null && /* @__PURE__ */ jsx(LoginSheet$1, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BookingWizard.mjs","names":["LoginSheet"],"sources":["../../src/react/BookingWizard.tsx"],"sourcesContent":["/**\n * `<BookingWizard>` — the booking flow, from «who is it for?» to the\n * confirmation, drawn with the `@medalsocial/meda/booking` screens over\n * `useBooking()`.\n *\n * The hook owns the machine, the fetches and the stores; this file owns the\n * page: which step is on screen (ONE at a time — the details step's nonce and\n * the submit flag depend on it), focus and scroll on every move, the restore\n * gate's pre-hydration script and skeleton, the header, and what each screen\n * is handed (prices, the weekend note, ages, the possessive, the calendar\n * file) out of the site's config.\n */\n\nimport {\n BookingButton,\n Confirmation,\n DetailsScreen,\n LiveStatus,\n ServiceScreen,\n StylistScreen,\n SummaryBar,\n TakenToast,\n TimeScreen,\n TimeScreenSkeleton,\n WhoScreen,\n} from '@medalsocial/meda/booking';\nimport { ArrowLeft } from 'lucide-react';\nimport {\n type ReactNode,\n Suspense,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { fill, fillParts, labelText } from '../core/labels';\nimport { SELF_KEY, type WizardPerson, type WizardState, type WizardStep } from '../core/machine';\nimport type { BookingServiceDto } from '../core/types';\nimport { visitServicesOf } from '../core/visit';\nimport type { PortalActions } from './actions';\nimport type { BookingKit } from './kit';\nimport { LoginSheet } from './LoginSheet';\nimport type { BookingOverrides, ResolvedBooking } from './Provider';\nimport { useBookingKit } from './Provider';\nimport { screenLabels } from './screen-labels';\nimport {\n type BookingConfirmation,\n type BookingController,\n type BookingSeed,\n useBooking,\n} from './useBooking';\nimport {\n addedChildEntries,\n confirmationProps,\n detailsLines,\n detailsScreenBase,\n familyEntries,\n guestChoices,\n partyPeople,\n partySizeWord,\n serviceFitsFor,\n serviceScreenBase,\n weekendNoteFor,\n} from './wizard/adapters';\nimport { forMedaScreen, wizardErrorText } from './wizard-error';\n\nexport interface BookingWizardProps extends BookingOverrides {\n /** What the page prefetched (the `/next` loader's seed). */\n seed: BookingSeed;\n /** This request's phone and address, over `config.contact` (the loader's `contact`). */\n contact?: { phone?: string | null; address?: string | null };\n /** The parent the page found a portal session for; `null` for a guest. */\n guardian?: BookingGuardianProp;\n /**\n * The app's server actions for the login sheet (Decision 8). Without them —\n * or with `config.portal.enabled` off — no login is offered.\n */\n actions?: Pick<PortalActions, 'startLogin' | 'startVipps'>;\n /** How many days the window covers. Default `config.window.rangeDays`. */\n rangeDays?: number;\n /**\n * The site's origin (`https://…`), so the manage links inside the calendar\n * file are absolute. Absent, they stay root-relative.\n */\n siteUrl?: string;\n /** A monitoring tap: the step the visitor is on, and a submission's outcome. */\n onEvent?: (event: BookingWizardEvent) => void;\n}\n\ntype BookingGuardianProp = Parameters<typeof useBooking>[0]['guardian'];\n\nexport type BookingWizardEvent =\n | { type: 'step'; step: WizardStep | 'confirmed' }\n | { type: 'submit_ok'; bookings: number }\n | { type: 'submit_error'; code: NonNullable<WizardState['error']> };\n\n/** The wizard's root, for the inline restore gate to find before hydration. */\nconst WIZARD_ROOT_ID = 'booking-wizard';\n\n/** The time step's heading, which a lost slot sends the visitor back to. */\nconst TIME_HEADING_ID = 'booking-time-heading';\n\n/** The heading each screen focuses when the visitor arrives on it. */\nconst STEP_HEADINGS: Record<WizardStep | 'confirmed', string> = {\n who: 'booking-who-heading',\n service: 'booking-service-heading',\n when: 'booking-stylist-heading',\n details: 'booking-details-heading',\n confirmed: 'booking-confirmed-heading',\n};\n\nconst STEPS: readonly WizardStep[] = ['who', 'service', 'when', 'details'];\n\n/** Smooth, or instant for a visitor who asked for reduced motion. */\nfunction scrollToTop(target: HTMLElement | null): void {\n if (!target || typeof target.scrollIntoView !== 'function') return;\n const reduced =\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n target.scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'start' });\n}\n\n/**\n * The wizard, behind its own Suspense boundary: `useSearchParams` throws to\n * the nearest one during a static prerender, and without ours that is the\n * root layout's — the whole page would ship empty until hydration.\n */\nexport function BookingWizard(props: BookingWizardProps) {\n return (\n <Suspense fallback={null}>\n <BookingWizardShell {...props} />\n </Suspense>\n );\n}\n\nfunction BookingWizardShell(props: BookingWizardProps) {\n const resolved = useBookingKit(props, props.contact);\n const booking = useBooking(props);\n const { kit } = booking;\n const { labels } = kit;\n const { state, restore, confirmed, onEvent } = { ...booking, onEvent: props.onEvent };\n\n /**\n * On every change of screen: the wizard's top into view and focus on the\n * new step's heading. Never on the first screen, and never for what the\n * restore gate reveals — that is the page arriving, not the visitor moving.\n */\n const rootRef = useRef<HTMLDivElement>(null);\n const screenKey: WizardStep | 'confirmed' = confirmed !== null ? 'confirmed' : state.step;\n const shownScreen = useRef<WizardStep | 'confirmed' | null>(null);\n const takenSlotTs = booking.slots.takenSlotTs;\n const timeStepReady = booking.slots.ready;\n const focusTimeWhenReady = useRef(false);\n useEffect(() => {\n if (restore.restoring) return;\n const previous = shownScreen.current;\n shownScreen.current = screenKey;\n if (previous === screenKey) return;\n onEvent?.({ type: 'step', step: screenKey });\n if (previous === null) return;\n // A lost slot sends the visitor back for the ALTERNATIVES, on the time half.\n const toTimes = screenKey === 'when' && takenSlotTs !== null;\n const headingId = toTimes ? TIME_HEADING_ID : STEP_HEADINGS[screenKey];\n const heading = document.getElementById(headingId);\n scrollToTop(toTimes ? heading : rootRef.current);\n heading?.focus({ preventScroll: true });\n // The heading focused now may be the skeleton's; focus follows the real one.\n focusTimeWhenReady.current = toTimes && !timeStepReady;\n }, [screenKey, restore.restoring, takenSlotTs, timeStepReady, onEvent]);\n\n /** After a login from the sheet the row that opened it is gone: focus the step's heading. */\n const focusAfterSignIn = useRef(false);\n const signedIn = booking.login.signedIn;\n useEffect(() => {\n if (signedIn === null || !focusAfterSignIn.current) return;\n focusAfterSignIn.current = false;\n document.getElementById(STEP_HEADINGS[state.step])?.focus({ preventScroll: true });\n }, [signedIn, state.step]);\n useEffect(() => {\n if (!timeStepReady || !focusTimeWhenReady.current) return;\n focusTimeWhenReady.current = false;\n const active = document.activeElement;\n if (active === null || active === document.body) {\n document.getElementById(TIME_HEADING_ID)?.focus({ preventScroll: true });\n }\n }, [timeStepReady]);\n\n // The monitoring tap for a submission's outcome.\n const lastError = useRef(state.error);\n useEffect(() => {\n if (state.error !== null && state.error !== lastError.current) {\n onEvent?.({ type: 'submit_error', code: state.error });\n }\n lastError.current = state.error;\n }, [state.error, onEvent]);\n const confirmedCount = confirmed?.bookings.length ?? 0;\n useEffect(() => {\n if (confirmedCount > 0) onEvent?.({ type: 'submit_ok', bookings: confirmedCount });\n }, [confirmedCount, onEvent]);\n\n // React owns the root from here on: drop the pre-hydration mark the inline\n // script may have set, and say so, so the script's fallback timer stands down.\n useLayoutEffect(() => {\n if (!restore.hydrated) return;\n rootRef.current?.removeAttribute('data-restoring');\n rootRef.current?.setAttribute('data-hydrated', '');\n }, [restore.hydrated]);\n\n // Steps animate in only once the visitor has moved between them.\n const [arrivalStep, setArrivalStep] = useState<WizardStep | null>(\n restore.restoring ? null : state.step\n );\n if (!restore.restoring && arrivalStep === null) setArrivalStep(state.step);\n const [stepsMovedOnce, setStepsMovedOnce] = useState(false);\n const movedNow = arrivalStep !== null && !restore.restoring && state.step !== arrivalStep;\n if (movedNow && !stepsMovedOnce) setStepsMovedOnce(true);\n const stepsMoved = stepsMovedOnce || movedNow;\n\n const restoreSkeleton = <RestoreSkeleton text={labelText(labels['wizard.restoring'])} />;\n /**\n * The root both screens share: the scroll target, and the element the\n * inline restore gate marks. The script and the hidden skeleton exist only\n * in the server's HTML and the hydration pass that adopts it.\n */\n const shell = (content: ReactNode) => (\n <div\n id={WIZARD_ROOT_ID}\n ref={rootRef}\n className={resolved.classNames.wizard?.root ?? 'group scroll-mt-28'}\n suppressHydrationWarning\n >\n {!restore.hydrated && (\n <>\n <script\n // A fixed script built from constants, run before hydration.\n // biome-ignore lint/security/noDangerouslySetInnerHtml: the restore gate must run before React does.\n dangerouslySetInnerHTML={{\n __html: kit.restoreGate.restoreGateScript(\n WIZARD_ROOT_ID,\n restore.linked,\n restore.resuming\n ),\n }}\n />\n <div className=\"hidden group-data-[restoring]:block\">{restoreSkeleton}</div>\n </>\n )}\n {restore.restoring && restore.hydrated ? (\n restoreSkeleton\n ) : (\n <div className=\"group-data-[restoring]:hidden\">{content}</div>\n )}\n </div>\n );\n\n if (confirmed !== null) {\n return shell(\n <ConfirmationStep booking={booking} resolved={resolved} siteUrl={props.siteUrl} />\n );\n }\n\n const onSignedIn = (who: Parameters<BookingController['login']['signIn']>[0]) => {\n focusAfterSignIn.current = true;\n booking.login.signIn(who);\n };\n // The multi-select service step carries its own total bar and refusal notice.\n const multiSelect = state.step === 'service' && kit.config.party.maxServicesPerPerson > 1;\n const loginOffered = props.actions !== undefined && kit.config.portal.enabled;\n const loginRow = loginOffered ? (\n <LoginRow\n booking={booking}\n actions={props.actions as NonNullable<BookingWizardProps['actions']>}\n overrides={props}\n onSignedIn={onSignedIn}\n />\n ) : null;\n\n return shell(\n <div className=\"space-y-8\">\n <WizardHeader\n kit={kit}\n state={state}\n submitting={booking.submitting}\n onGo={(step) => booking.dispatch({ type: 'goToStep', step })}\n />\n\n {/* The machine's error, on the steps that have nowhere else to put it. */}\n {state.error !== null &&\n state.step !== 'details' &&\n state.step !== 'when' &&\n !multiSelect && (\n <p\n role=\"alert\"\n className={\n resolved.classNames.wizard?.alert ??\n 'rounded-lg border border-destructive/40 bg-destructive/10 px-5 py-3 text-sm'\n }\n >\n {wizardErrorText(labels, state.error, kit.config.party.maxServicesPerPerson)}\n </p>\n )}\n\n {state.step === 'details' && loginRow}\n\n {loginOffered && booking.login.vippsConfirm !== null && signedIn === null && (\n <LoginSheet\n {...overridesOf(props)}\n actions={props.actions as NonNullable<BookingWizardProps['actions']>}\n presentation=\"sheet\"\n trigger={false}\n vippsConfirm={booking.login.vippsConfirm}\n resumePath={booking.login.resumePath}\n onSignedIn={onSignedIn}\n />\n )}\n\n {/* One box per step, keyed by it, so a step arriving is a mount. */}\n <div\n key={state.step}\n data-booking-step={state.step}\n data-animate={stepsMoved ? '' : undefined}\n className=\"space-y-8\"\n >\n {state.step === 'who' && (\n <WhoStep booking={booking} resolved={resolved} loginRow={loginRow} />\n )}\n {state.step === 'service' && (\n <ServiceStep booking={booking} resolved={resolved} multiSelect={multiSelect} />\n )}\n {state.step === 'when' && <WhenStep booking={booking} resolved={resolved} />}\n {state.step === 'details' && <DetailsStep booking={booking} resolved={resolved} />}\n </div>\n\n {!multiSelect && (\n <SummaryBar\n line={booking.derived.summary}\n canAdvance={booking.derived.canAdvance}\n step={state.step}\n onNext={booking.next}\n labels={screenLabels(labels)}\n classNames={resolved.classNames.summary}\n />\n )}\n </div>\n );\n}\n\n/** The overrides a nested booking component inherits from the wizard's props. */\nfunction overridesOf(props: BookingOverrides): BookingOverrides {\n return {\n config: props.config,\n labels: props.labels,\n classNames: props.classNames,\n components: props.components,\n };\n}\n\ninterface StepProps {\n booking: BookingController;\n resolved: ResolvedBooking;\n}\n\nfunction WhoStep({ booking, resolved, loginRow }: StepProps & { loginRow: ReactNode }) {\n const { kit, state, people } = booking;\n const guests = useMemo(() => guestChoices(kit), [kit]);\n return (\n <WhoScreen\n labels={screenLabels(kit.labels)}\n format={kit.format}\n people={state.people}\n family={people.family === null ? null : familyEntries(kit, people.family, people.ageDayKey)}\n guestChoices={guests}\n addedChildren={addedChildEntries(kit, state.people, people.ageDayKey)}\n maxPeople={kit.wizard.maxPeople}\n selfKey={SELF_KEY}\n isGuestSeat={kit.wizard.isGuestSeat}\n onChoose={people.choosePeople}\n onAddChild={people.addChild}\n loginRow={loginRow}\n currentYear={Number(kit.clock.dayKey(booking.slots.fromTs).slice(0, 4))}\n classNames={resolved.classNames.who}\n components={\n resolved.components.PersonCard ? { PersonCard: resolved.components.PersonCard } : undefined\n }\n />\n );\n}\n\nfunction ServiceStep({ booking, resolved, multiSelect }: StepProps & { multiSelect: boolean }) {\n const { kit, state, people, catalogue } = booking;\n const ageOf = (person: WizardPerson) => people.personAge(person, people.ageDayKey);\n const single = state.people.length === 1 ? state.people[0] : null;\n // «Samme som sist» stays a one-tap shortcut while the person's visit is\n // nothing, or just that service (a returning child is seated with it already\n // ticked). Once they have ticked something else, a one-tap pick would REPLACE\n // their visit — and, alone, jump to the time step — so the suggestion joins\n // the visit instead, and never unticks a service already in it.\n const pickFor = (index: number, service: BookingServiceDto, oneTap: () => void) => {\n const visit = state.choices[index] ? [state.choices[index], ...state.extras[index]] : [];\n if (!multiSelect || visit.length === 0 || (visit.length === 1 && visit[0].id === service.id)) {\n oneTap();\n } else if (!visit.some((ticked) => ticked.id === service.id)) {\n booking.toggleServiceFor(index, service);\n }\n };\n return (\n <ServiceScreen\n labels={screenLabels(kit.labels)}\n format={kit.format}\n services={catalogue.services}\n {...serviceScreenBase(kit)}\n serviceFits={serviceFitsFor(kit, state.people.map(ageOf))}\n initialCategory={catalogue.deepLinkCategory}\n onPick={(service) => pickFor(0, service, () => booking.pickService(service))}\n suggestion={single ? people.suggestionFor(single) : null}\n chosenId={state.choices[0]?.id ?? null}\n childName={single ? (single.name ?? null) : null}\n party={\n state.people.length > 1\n ? {\n people: partyPeople(kit, state.people, ageOf, people.suggestionFor),\n choices: state.choices,\n onPickFor: (index, service) =>\n pickFor(index, service, () => booking.pickServiceFor(index, service)),\n onRemove: (index) =>\n booking.dispatch({\n type: 'choosePeople',\n people: state.people.filter((_, position) => position !== index),\n }),\n }\n : undefined\n }\n selection={\n multiSelect\n ? {\n // Index = seated person; meda reads a missing list (nobody seated) as empty.\n lists: state.choices.map((first, index) =>\n first ? [first, ...state.extras[index]] : []\n ),\n onToggle: booking.toggleServiceFor,\n onContinue: booking.continueFromService,\n total:\n state.items.length === 0\n ? null\n : {\n minutes: kit.wizard.visitMinutes(state.items, state.partyMode),\n priceOre: booking.derived.total,\n },\n canContinue: booking.derived.canAdvance,\n notice:\n state.error === null\n ? null\n : labelText(\n wizardErrorText(\n kit.labels,\n state.error,\n kit.config.party.maxServicesPerPerson\n )\n ),\n labels: kit.labels,\n }\n : undefined\n }\n classNames={resolved.classNames.service}\n components={\n resolved.components.ServiceCard\n ? { ServiceCard: resolved.components.ServiceCard }\n : undefined\n }\n />\n );\n}\n\nfunction WhenStep({ booking, resolved }: StepProps) {\n const { kit, state, catalogue, slots, schedule } = booking;\n const { labels, format, wizard, config } = kit;\n const party = booking.derived.party;\n const items = state.items;\n const phone = config.contact.phone;\n return (\n <>\n <StylistScreen\n labels={screenLabels(labels)}\n format={format}\n // Not deduplicated: a named stylist has to cover every service of every\n // line — a person's extras included, as their visit is one stylist's.\n serviceIds={items.flatMap((item) => visitServicesOf(item).map((service) => service.id))}\n resources={catalogue.resources}\n loading={!catalogue.resourcesKnown}\n nextAvailableLoading={slots.nextAvailableLoading}\n selectedResourceId={state.resourceId}\n nextAvailableTs={slots.nextAvailableTs}\n pendingName={catalogue.chosenStylistName}\n notice={catalogue.stylistNotice}\n onPick={booking.pickResource}\n party={\n party\n ? {\n mode: state.partyMode,\n size: items.length,\n sizeWord: partySizeWord(labels, items.length),\n minutes: {\n sequential: wizard.visitMinutes(items, 'sequential'),\n parallel: wizard.visitMinutes(items, 'parallel'),\n },\n onMode: (mode) => booking.dispatch({ type: 'setPartyMode', mode }),\n }\n : undefined\n }\n classNames={resolved.classNames.stylist}\n components={\n resolved.components.StylistCard\n ? { StylistCard: resolved.components.StylistCard }\n : undefined\n }\n />\n\n {slots.ready ? (\n <TimeScreen\n labels={screenLabels(labels)}\n format={format}\n dayparts={kit.dayparts}\n monthView\n slots={slots.single}\n days={slots.days}\n openDays={schedule.openDays}\n stylistName={catalogue.chosenStylistName}\n takenSlotTs={slots.takenSlotTs}\n phone={phone}\n weekendNote={(dayTs) => weekendNoteFor(kit, party ? items : items.slice(0, 1), dayTs)}\n onPick={booking.pickSlot}\n party={\n party\n ? {\n items,\n mode: state.partyMode,\n slots: slots.party,\n chipEndTs: (slot) => wizard.visitEndTs(items, slot.startTs, 'sequential'),\n // Only a back-to-back family is offered the simultaneous rescue.\n alternativeFor:\n state.partyMode === 'sequential'\n ? (dayTs) =>\n kit.partySlots.partyAlternative({\n sequential: slots.party,\n parallel: slots.alternatives,\n dayTs,\n })\n : undefined,\n resolveStylistName: catalogue.resolveStylistName,\n onPick: booking.pickPartySlot,\n }\n : undefined\n }\n classNames={resolved.classNames.time}\n components={timeComponents(resolved)}\n />\n ) : slots.failed ? (\n <SlotsUnavailable kit={kit} onRetry={booking.refresh} />\n ) : (\n // The step's own shape, so nothing below it moves when it fills in.\n <TimeScreenSkeleton\n labels={screenLabels(labels)}\n days={slots.days.length}\n monthView\n surchargeRow={items.some((item) =>\n visitServicesOf(item).some((service) => service.weekendSurchargePct > 0)\n )}\n />\n )}\n\n {/* Once, outside the skeleton-or-step switch, so a re-read cannot remount it. */}\n <TakenToast labels={screenLabels(labels)} takenSlotTs={slots.takenSlotTs} />\n </>\n );\n}\n\nfunction timeComponents({ components }: ResolvedBooking) {\n if (!components.DayChip && !components.TimeChip) return undefined;\n return { DayChip: components.DayChip, TimeChip: components.TimeChip };\n}\n\nfunction DetailsStep({ booking, resolved }: StepProps) {\n const { kit, state } = booking;\n const guardian = booking.login.guardian;\n const lines = useMemo(() => detailsLines(kit, state), [kit, state]);\n return (\n <DetailsScreen\n state={forMedaScreen(state)}\n onChange={booking.dispatch}\n onSubmit={booking.submit}\n lines={lines}\n totalOre={booking.derived.total}\n {...detailsScreenBase(kit)}\n submitting={booking.submitting}\n submissionNonce={booking.submissionNonce}\n family={guardian?.family}\n guardianPhone={guardian?.phone ?? null}\n format={kit.format}\n labels={screenLabels(kit.labels)}\n classNames={resolved.classNames.details}\n components={\n resolved.components.FamilyChip ? { FamilyChip: resolved.components.FamilyChip } : undefined\n }\n />\n );\n}\n\nfunction ConfirmationStep({ booking, resolved, siteUrl }: StepProps & { siteUrl?: string }) {\n const { kit } = booking;\n // Read once, so «today» cannot become «tomorrow» halfway down the card.\n const [now] = useState(() => Date.now());\n const confirmation = booking.confirmed as BookingConfirmation;\n return (\n <Confirmation\n {...confirmationProps(kit, confirmation, siteUrl)}\n onStartOver={booking.startOver}\n now={now}\n format={kit.format}\n labels={screenLabels(kit.labels)}\n classNames={resolved.classNames.confirmation}\n components={\n resolved.components.PartyLine ? { PartyLine: resolved.components.PartyLine } : undefined\n }\n />\n );\n}\n\n/**\n * «Have an account? Log in» — or, once the parent is known, nothing at all\n * for one who arrived logged in, and one line saying so for one who logged in\n * from the sheet.\n */\nfunction LoginRow({\n booking,\n actions,\n overrides,\n onSignedIn,\n}: {\n booking: BookingController;\n actions: NonNullable<BookingWizardProps['actions']>;\n overrides: BookingOverrides;\n onSignedIn: (guardian: Parameters<BookingController['login']['signIn']>[0]) => void;\n}) {\n const { arrivedAs, signedIn, resumePath } = booking.login;\n const { labels } = booking.kit;\n if (signedIn === null && arrivedAs !== null) return null;\n const who = signedIn === null ? null : (signedIn.guardian ?? arrivedAs);\n const name =\n who === null\n ? null\n : [who.firstName, who.lastName].filter(Boolean).join(' ').trim() || who.email;\n const confirmation =\n signedIn === null\n ? null\n : name === null\n ? labelText(labels['wizard.signedIn'])\n : fill(labels['wizard.signedInAs'], { name });\n return (\n <div>\n {signedIn === null && (\n <LoginSheet\n {...overridesOf(overrides)}\n actions={actions}\n presentation=\"sheet\"\n resumePath={resumePath}\n onSignedIn={onSignedIn}\n />\n )}\n <LiveStatus text={confirmation} className=\"text-sm text-muted-foreground empty:hidden\" />\n </div>\n );\n}\n\n/** The neutral shell the restore gate shows, the header's and a step's shape. */\nfunction RestoreSkeleton({ text }: { text: string }) {\n return (\n <div data-testid=\"restore-skeleton\">\n {/* Outside the busy subtree: assistive tech may hold back what is inside it. */}\n <LiveStatus text={text} />\n <div aria-busy=\"true\" className=\"space-y-8\">\n <div aria-hidden=\"true\" className=\"space-y-3\">\n <div className=\"flex items-center gap-3\">\n <span className=\"size-9 shrink-0 animate-pulse rounded-full bg-muted\" />\n <span className=\"flex flex-col gap-1.5\">\n <span className=\"h-4 w-24 animate-pulse rounded bg-muted\" />\n <span className=\"h-3 w-36 animate-pulse rounded bg-muted\" />\n </span>\n </div>\n <span className=\"block h-1.5 w-full animate-pulse rounded-full bg-muted\" />\n </div>\n <div aria-hidden=\"true\" className=\"space-y-6\">\n <span className=\"block h-8 w-56 animate-pulse rounded bg-muted md:h-9\" />\n <div className=\"space-y-2\">\n {Array.from({ length: 4 }, (_, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: four identical placeholders.\n <span key={index} className=\"block h-24 animate-pulse rounded-lg bg-muted\" />\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * Where the visitor is, the way back, and how much is left: a back arrow, the\n * title and «step N of 4», and one segment per step that IS the button to it —\n * tappable only where the machine's `canGoToStep` says so, and frozen while a\n * submission is in flight.\n */\nfunction WizardHeader({\n kit,\n state,\n submitting,\n onGo,\n}: {\n kit: BookingKit;\n state: WizardState;\n submitting: boolean;\n onGo: (step: WizardStep) => void;\n}) {\n const { labels, wizard } = kit;\n const stepLabel = (step: WizardStep) => labelText(labels[`wizard.step.${step}`]);\n const index = STEPS.indexOf(state.step);\n const previous = index > 0 ? STEPS[index - 1] : null;\n const canGoBack = previous !== null && !submitting && wizard.canGoToStep(state, previous);\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-3\">\n {canGoBack ? (\n <button\n type=\"button\"\n aria-label={fill(labels['wizard.back'], { label: stepLabel(previous) })}\n onClick={() => onGo(previous)}\n className=\"flex size-9 shrink-0 items-center justify-center rounded-full border border-border text-foreground transition-colors hover:bg-muted\"\n >\n <ArrowLeft aria-hidden=\"true\" className=\"size-4\" />\n </button>\n ) : (\n // Held rather than dropped, so the title does not shuffle sideways.\n <span aria-hidden=\"true\" className=\"size-9 shrink-0\" />\n )}\n <p className=\"min-w-0\">\n <span className=\"block font-sans text-sm font-bold\">{labels['wizard.title']}</span>\n <span className=\"block truncate text-xs text-muted-foreground\">\n {fillParts(labels['wizard.progress'], {\n step: index + 1,\n total: STEPS.length,\n label: stepLabel(state.step),\n })}\n </span>\n </p>\n </div>\n\n <ol\n aria-label={labelText(labels['wizard.progressLabel'])}\n className=\"flex items-center gap-1.5\"\n >\n {STEPS.map((step, position) => {\n const isCurrent = step === state.step;\n const reachable = !submitting && !isCurrent && wizard.canGoToStep(state, step);\n return (\n <li key={step} className=\"flex-1\">\n <button\n type=\"button\"\n disabled={!reachable}\n aria-current={isCurrent ? 'step' : undefined}\n onClick={() => onGo(step)}\n className={`block h-1.5 w-full rounded-full transition-colors ${\n position <= index ? 'bg-primary' : 'bg-border'\n } ${reachable ? 'cursor-pointer' : 'cursor-default'}`}\n >\n <span className=\"sr-only\">{stepLabel(step)}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </div>\n );\n}\n\n/**\n * The openings could not be read — this site's failure, not a full day —\n * with the telephone as the rescue and a retry as the cheaper first try.\n */\nfunction SlotsUnavailable({ kit, onRetry }: { kit: BookingKit; onRetry: () => void }) {\n const { labels, config, format } = kit;\n const phone = config.contact.phone;\n return (\n <div className=\"space-y-3\">\n <p role=\"alert\" className=\"rounded-lg border border-border bg-card px-5 py-4 text-sm\">\n {labels['wizard.slotsUnavailable.lead']}\n {phone ? (\n <a\n href={format.telHref(phone)}\n aria-label={fill(labels['wizard.slotsUnavailable.callAria'], { phone })}\n className=\"font-semibold text-primary underline underline-offset-4\"\n >\n {fillParts(labels['wizard.slotsUnavailable.call'], { phone })}\n </a>\n ) : (\n labels['wizard.slotsUnavailable.callPlain']\n )}\n {labels['wizard.slotsUnavailable.suffix']}\n </p>\n <BookingButtonOutline onClick={onRetry}>{labels['wizard.retry']}</BookingButtonOutline>\n </div>\n );\n}\n\nfunction BookingButtonOutline({ onClick, children }: { onClick: () => void; children: ReactNode }) {\n return (\n <BookingButton variant=\"outline\" onClick={onClick}>\n {children}\n </BookingButton>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BookingWizard.mjs","names":["LoginSheet"],"sources":["../../src/react/BookingWizard.tsx"],"sourcesContent":["/**\n * `<BookingWizard>` — the booking flow, from «who is it for?» to the\n * confirmation, drawn with the `@medalsocial/meda/booking` screens over\n * `useBooking()`.\n *\n * The hook owns the machine, the fetches and the stores; this file owns the\n * page: which step is on screen (ONE at a time — the details step's nonce and\n * the submit flag depend on it), focus and scroll on every move, the restore\n * gate's pre-hydration script and skeleton, the header, and what each screen\n * is handed (prices, the weekend note, ages, the possessive, the calendar\n * file) out of the site's config.\n */\n\nimport {\n BookingButton,\n BookingRecap,\n Confirmation,\n DetailsScreen,\n LiveStatus,\n ServiceScreen,\n StylistScreen,\n SummaryBar,\n TakenToast,\n TimeScreen,\n TimeScreenSkeleton,\n WhoScreen,\n} from '@medalsocial/meda/booking';\nimport { ArrowLeft } from 'lucide-react';\nimport {\n type ReactNode,\n Suspense,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { fill, fillParts, labelText } from '../core/labels';\nimport { SELF_KEY, type WizardPerson, type WizardState, type WizardStep } from '../core/machine';\nimport type { BookingServiceDto } from '../core/types';\nimport { visitServicesOf } from '../core/visit';\nimport type { PortalActions } from './actions';\nimport type { BookingKit } from './kit';\nimport { LoginSheet } from './LoginSheet';\nimport type { BookingOverrides, ResolvedBooking } from './Provider';\nimport { useBookingKit } from './Provider';\nimport { screenLabels } from './screen-labels';\nimport {\n type BookingConfirmation,\n type BookingController,\n type BookingSeed,\n useBooking,\n} from './useBooking';\nimport {\n addedChildEntries,\n confirmationProps,\n detailsLines,\n detailsScreenBase,\n familyEntries,\n GUEST_ADULT_SEAT,\n guestChoices,\n partyPeople,\n partySizeWord,\n recapProps,\n serviceFitsFor,\n serviceScreenBase,\n stylistFace,\n weekendNoteFor,\n} from './wizard/adapters';\nimport { forMedaScreen, wizardErrorText } from './wizard-error';\n\nexport interface BookingWizardProps extends BookingOverrides {\n /** What the page prefetched (the `/next` loader's seed). */\n seed: BookingSeed;\n /** This request's phone and address, over `config.contact` (the loader's `contact`). */\n contact?: { phone?: string | null; address?: string | null };\n /** The parent the page found a portal session for; `null` for a guest. */\n guardian?: BookingGuardianProp;\n /**\n * The app's server actions for the login sheet (Decision 8). Without them —\n * or with `config.portal.enabled` off — no login is offered.\n */\n actions?: Pick<PortalActions, 'startLogin' | 'startVipps'>;\n /** How many days the window covers. Default `config.window.rangeDays`. */\n rangeDays?: number;\n /**\n * The site's origin (`https://…`), so the manage links inside the calendar\n * file are absolute. Absent, they stay root-relative.\n */\n siteUrl?: string;\n /** A monitoring tap: the step the visitor is on, and a submission's outcome. */\n onEvent?: (event: BookingWizardEvent) => void;\n}\n\ntype BookingGuardianProp = Parameters<typeof useBooking>[0]['guardian'];\n\nexport type BookingWizardEvent =\n | { type: 'step'; step: WizardStep | 'confirmed' }\n | { type: 'submit_ok'; bookings: number }\n | { type: 'submit_error'; code: NonNullable<WizardState['error']> };\n\n/** The bundler's build mode, which it inlines; absent outside a bundler. */\ndeclare const process: { env: { NODE_ENV?: string } } | undefined;\n\n/** The wizard's root, for the inline restore gate to find before hydration. */\nconst WIZARD_ROOT_ID = 'booking-wizard';\n\n/** The time step's heading, which a lost slot sends the visitor back to. */\nconst TIME_HEADING_ID = 'booking-time-heading';\n\n/** The heading each screen focuses when the visitor arrives on it. */\nconst STEP_HEADINGS: Record<WizardStep | 'confirmed', string> = {\n who: 'booking-who-heading',\n service: 'booking-service-heading',\n when: 'booking-stylist-heading',\n details: 'booking-details-heading',\n confirmed: 'booking-confirmed-heading',\n};\n\nconst STEPS: readonly WizardStep[] = ['who', 'service', 'when', 'details'];\n\n/** Smooth, or instant for a visitor who asked for reduced motion. */\nfunction scrollToTop(target: HTMLElement | null): void {\n if (!target || typeof target.scrollIntoView !== 'function') return;\n const reduced =\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n target.scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'start' });\n}\n\n/**\n * The wizard, behind its own Suspense boundary: `useSearchParams` throws to\n * the nearest one during a static prerender, and without ours that is the\n * root layout's — the whole page would ship empty until hydration.\n */\nexport function BookingWizard(props: BookingWizardProps) {\n return (\n <Suspense fallback={null}>\n <BookingWizardShell {...props} />\n </Suspense>\n );\n}\n\nfunction BookingWizardShell(props: BookingWizardProps) {\n const resolved = useBookingKit(props, props.contact);\n const loginOffered = props.actions !== undefined && resolved.kit.config.portal.enabled;\n const booking = useBooking({ ...props, loginOffered });\n const { kit } = booking;\n const { labels } = kit;\n\n // `account.required` with no way to log in: every «Bekreft» would be refused.\n const unreachableLogin = kit.config.account.required && !loginOffered;\n useEffect(() => {\n if (\n unreachableLogin &&\n typeof process !== 'undefined' &&\n process.env.NODE_ENV !== 'production'\n ) {\n console.warn('[booking] account.required without login actions: nobody can log in to book.');\n }\n }, [unreachableLogin]);\n const { state, restore, confirmed, onEvent } = { ...booking, onEvent: props.onEvent };\n\n /**\n * On every change of screen: the wizard's top into view and focus on the\n * new step's heading. Never on the first screen, and never for what the\n * restore gate reveals — that is the page arriving, not the visitor moving.\n */\n const rootRef = useRef<HTMLDivElement>(null);\n const screenKey: WizardStep | 'confirmed' = confirmed !== null ? 'confirmed' : state.step;\n const shownScreen = useRef<WizardStep | 'confirmed' | null>(null);\n const takenSlotTs = booking.slots.takenSlotTs;\n const timeStepReady = booking.slots.ready;\n const focusTimeWhenReady = useRef(false);\n useEffect(() => {\n if (restore.restoring) return;\n const previous = shownScreen.current;\n shownScreen.current = screenKey;\n if (previous === screenKey) return;\n onEvent?.({ type: 'step', step: screenKey });\n if (previous === null) return;\n // A lost slot sends the visitor back for the ALTERNATIVES, on the time half.\n const toTimes = screenKey === 'when' && takenSlotTs !== null;\n const headingId = toTimes ? TIME_HEADING_ID : STEP_HEADINGS[screenKey];\n const heading = document.getElementById(headingId);\n scrollToTop(toTimes ? heading : rootRef.current);\n heading?.focus({ preventScroll: true });\n // The heading focused now may be the skeleton's; focus follows the real one.\n focusTimeWhenReady.current = toTimes && !timeStepReady;\n }, [screenKey, restore.restoring, takenSlotTs, timeStepReady, onEvent]);\n\n /** After a login from the sheet the row that opened it is gone: focus the step's heading. */\n const focusAfterSignIn = useRef(false);\n const signedIn = booking.login.signedIn;\n useEffect(() => {\n if (signedIn === null || !focusAfterSignIn.current) return;\n focusAfterSignIn.current = false;\n document.getElementById(STEP_HEADINGS[state.step])?.focus({ preventScroll: true });\n }, [signedIn, state.step]);\n /**\n * A 401 put the gate back over the form: focus its heading (the details\n * step's id), where the notice beside it says why. Once per loss, and only\n * once the gate is actually on screen — a replay answers behind the skeleton.\n */\n const sessionLost = booking.login.sessionLost;\n const sessionLossFocused = useRef(false);\n useEffect(() => {\n if (!sessionLost) {\n sessionLossFocused.current = false;\n return;\n }\n if (sessionLossFocused.current || restore.restoring || state.step !== 'details') return;\n sessionLossFocused.current = true;\n const heading = document.getElementById(STEP_HEADINGS.details);\n scrollToTop(rootRef.current);\n heading?.focus({ preventScroll: true });\n }, [sessionLost, restore.restoring, state.step]);\n useEffect(() => {\n if (!timeStepReady || !focusTimeWhenReady.current) return;\n focusTimeWhenReady.current = false;\n const active = document.activeElement;\n if (active === null || active === document.body) {\n document.getElementById(TIME_HEADING_ID)?.focus({ preventScroll: true });\n }\n }, [timeStepReady]);\n\n // The monitoring tap for a submission's outcome.\n const lastError = useRef(state.error);\n useEffect(() => {\n if (state.error !== null && state.error !== lastError.current) {\n onEvent?.({ type: 'submit_error', code: state.error });\n }\n lastError.current = state.error;\n }, [state.error, onEvent]);\n const confirmedCount = confirmed?.bookings.length ?? 0;\n useEffect(() => {\n if (confirmedCount > 0) onEvent?.({ type: 'submit_ok', bookings: confirmedCount });\n }, [confirmedCount, onEvent]);\n\n // React owns the root from here on: drop the pre-hydration mark the inline\n // script may have set, and say so, so the script's fallback timer stands down.\n useLayoutEffect(() => {\n if (!restore.hydrated) return;\n rootRef.current?.removeAttribute('data-restoring');\n rootRef.current?.setAttribute('data-hydrated', '');\n }, [restore.hydrated]);\n\n // Steps animate in only once the visitor has moved between them.\n const [arrivalStep, setArrivalStep] = useState<WizardStep | null>(\n restore.restoring ? null : state.step\n );\n if (!restore.restoring && arrivalStep === null) setArrivalStep(state.step);\n const [stepsMovedOnce, setStepsMovedOnce] = useState(false);\n const movedNow = arrivalStep !== null && !restore.restoring && state.step !== arrivalStep;\n if (movedNow && !stepsMovedOnce) setStepsMovedOnce(true);\n const stepsMoved = stepsMovedOnce || movedNow;\n\n const restoreSkeleton = <RestoreSkeleton text={labelText(labels['wizard.restoring'])} />;\n /**\n * The root both screens share: the scroll target, and the element the\n * inline restore gate marks. The script and the hidden skeleton exist only\n * in the server's HTML and the hydration pass that adopts it.\n */\n const shell = (content: ReactNode) => (\n <div\n id={WIZARD_ROOT_ID}\n ref={rootRef}\n className={resolved.classNames.wizard?.root ?? 'group scroll-mt-28'}\n suppressHydrationWarning\n >\n {!restore.hydrated && (\n <>\n <script\n // A fixed script built from constants, run before hydration.\n // biome-ignore lint/security/noDangerouslySetInnerHtml: the restore gate must run before React does.\n dangerouslySetInnerHTML={{\n __html: kit.restoreGate.restoreGateScript(\n WIZARD_ROOT_ID,\n restore.linked,\n restore.resuming\n ),\n }}\n />\n <div className=\"hidden group-data-[restoring]:block\">{restoreSkeleton}</div>\n </>\n )}\n {restore.restoring && restore.hydrated ? (\n restoreSkeleton\n ) : (\n <div className=\"group-data-[restoring]:hidden\">{content}</div>\n )}\n </div>\n );\n\n if (confirmed !== null) {\n return shell(\n <ConfirmationStep booking={booking} resolved={resolved} siteUrl={props.siteUrl} />\n );\n }\n\n const onSignedIn = (who: Parameters<BookingController['login']['signIn']>[0]) => {\n focusAfterSignIn.current = true;\n booking.login.signIn(who);\n };\n // The multi-select service step carries its own total bar and refusal notice.\n const multiSelect = state.step === 'service' && kit.config.party.maxServicesPerPerson > 1;\n /**\n * `account.required`: every booking is a logged-in parent's. The login is\n * not offered along the way — it IS «Bekreft», for a parent not logged in.\n */\n const accountRequired = kit.config.account.required && loginOffered;\n const gated = accountRequired && state.step === 'details' && !booking.login.loggedIn;\n const loginRow =\n loginOffered && !accountRequired ? (\n <LoginRow\n booking={booking}\n actions={props.actions as NonNullable<BookingWizardProps['actions']>}\n overrides={props}\n onSignedIn={onSignedIn}\n />\n ) : null;\n\n return shell(\n <div className=\"space-y-8\">\n <WizardHeader\n kit={kit}\n state={state}\n submitting={booking.submitting}\n onGo={(step) => booking.dispatch({ type: 'goToStep', step })}\n />\n\n {/* The machine's error, on the steps that have nowhere else to put it. */}\n {state.error !== null &&\n state.step !== 'details' &&\n state.step !== 'when' &&\n !multiSelect && (\n <p\n role=\"alert\"\n className={\n resolved.classNames.wizard?.alert ??\n 'rounded-lg border border-destructive/40 bg-destructive/10 px-5 py-3 text-sm'\n }\n >\n {wizardErrorText(labels, state.error, kit.config.party.maxServicesPerPerson)}\n </p>\n )}\n\n {/* What is being booked, first: above the login offer and the form. */}\n {state.step === 'details' && !gated && kit.config.screens.recap && (\n <RecapStep booking={booking} resolved={resolved} />\n )}\n\n {state.step === 'details' && loginRow}\n\n {/* The gate takes a Vipps confirm code itself, in place of the form. */}\n {loginOffered && booking.login.vippsConfirm !== null && signedIn === null && !gated && (\n <LoginSheet\n {...overridesOf(props)}\n actions={props.actions as NonNullable<BookingWizardProps['actions']>}\n presentation=\"sheet\"\n trigger={false}\n vippsConfirm={booking.login.vippsConfirm}\n resumePath={booking.login.resumePath}\n onSignedIn={onSignedIn}\n />\n )}\n\n {/* One box per step, keyed by it, so a step arriving is a mount. */}\n <div\n key={state.step}\n data-booking-step={state.step}\n data-animate={stepsMoved ? '' : undefined}\n className=\"space-y-8\"\n >\n {state.step === 'who' && (\n <WhoStep booking={booking} resolved={resolved} loginRow={loginRow} />\n )}\n {state.step === 'service' && (\n <ServiceStep booking={booking} resolved={resolved} multiSelect={multiSelect} />\n )}\n {state.step === 'when' && <WhenStep booking={booking} resolved={resolved} />}\n {state.step === 'details' &&\n (gated ? (\n <AccountGate\n booking={booking}\n resolved={resolved}\n actions={props.actions as NonNullable<BookingWizardProps['actions']>}\n overrides={props}\n onSignedIn={onSignedIn}\n />\n ) : (\n <DetailsStep booking={booking} resolved={resolved} />\n ))}\n </div>\n\n {!multiSelect && (\n <SummaryBar\n {...(kit.config.screens.summaryDetail\n ? booking.derived.summaryParts\n : { line: booking.derived.summary })}\n canAdvance={booking.derived.canAdvance}\n step={state.step}\n onNext={booking.next}\n hideNextWhenDisabled={kit.config.screens.hideDisabledNext}\n labels={screenLabels(labels)}\n classNames={resolved.classNames.summary}\n />\n )}\n </div>\n );\n}\n\n/** The overrides a nested booking component inherits from the wizard's props. */\nfunction overridesOf(props: BookingOverrides): BookingOverrides {\n return {\n config: props.config,\n labels: props.labels,\n classNames: props.classNames,\n components: props.components,\n };\n}\n\ninterface StepProps {\n booking: BookingController;\n resolved: ResolvedBooking;\n}\n\nfunction WhoStep({ booking, resolved, loginRow }: StepProps & { loginRow: ReactNode }) {\n const { kit, state, people } = booking;\n const guests = useMemo(() => guestChoices(kit), [kit]);\n return (\n <WhoScreen\n labels={screenLabels(kit.labels)}\n format={kit.format}\n people={state.people}\n family={people.family === null ? null : familyEntries(kit, people.family, people.ageDayKey)}\n guestChoices={guests}\n addedChildren={addedChildEntries(kit, state.people, people.ageDayKey)}\n maxPeople={kit.wizard.maxPeople}\n selfKey={SELF_KEY}\n isGuestSeat={kit.wizard.isGuestSeat}\n onChoose={people.choosePeople}\n onAddChild={people.addChild}\n guestParty={\n kit.config.screens.guestParty\n ? { child: kit.wizard.guestChild, adult: GUEST_ADULT_SEAT }\n : undefined\n }\n loginRow={loginRow}\n currentYear={Number(kit.clock.dayKey(booking.slots.fromTs).slice(0, 4))}\n classNames={resolved.classNames.who}\n components={\n resolved.components.PersonCard ? { PersonCard: resolved.components.PersonCard } : undefined\n }\n />\n );\n}\n\nfunction ServiceStep({ booking, resolved, multiSelect }: StepProps & { multiSelect: boolean }) {\n const { kit, state, people, catalogue } = booking;\n const ageOf = (person: WizardPerson) => people.personAge(person, people.ageDayKey);\n const single = state.people.length === 1 ? state.people[0] : null;\n // «Samme som sist» stays a one-tap shortcut while the person's visit is\n // nothing, or just that service (a returning child is seated with it already\n // ticked). Once they have ticked something else, a one-tap pick would REPLACE\n // their visit — and, alone, jump to the time step — so the suggestion joins\n // the visit instead, and never unticks a service already in it.\n const pickFor = (index: number, service: BookingServiceDto, oneTap: () => void) => {\n const visit = state.choices[index] ? [state.choices[index], ...state.extras[index]] : [];\n if (!multiSelect || visit.length === 0 || (visit.length === 1 && visit[0].id === service.id)) {\n oneTap();\n } else if (!visit.some((ticked) => ticked.id === service.id)) {\n booking.toggleServiceFor(index, service);\n }\n };\n return (\n <ServiceScreen\n labels={screenLabels(kit.labels)}\n format={kit.format}\n services={catalogue.services}\n {...serviceScreenBase(kit)}\n serviceFits={serviceFitsFor(kit, state.people.map(ageOf), state.people)}\n initialCategory={catalogue.deepLinkCategory}\n onPick={(service) => pickFor(0, service, () => booking.pickService(service))}\n suggestion={single ? people.suggestionFor(single) : null}\n chosenId={state.choices[0]?.id ?? null}\n childName={single ? (single.name ?? null) : null}\n party={\n state.people.length > 1\n ? {\n people: partyPeople(kit, state.people, ageOf, people.suggestionFor),\n choices: state.choices,\n onPickFor: (index, service) =>\n pickFor(index, service, () => booking.pickServiceFor(index, service)),\n onRemove: (index) =>\n booking.dispatch({\n type: 'choosePeople',\n people: state.people.filter((_, position) => position !== index),\n }),\n }\n : undefined\n }\n selection={\n multiSelect\n ? {\n // Index = seated person; meda reads a missing list (nobody seated) as empty.\n lists: state.choices.map((first, index) =>\n first ? [first, ...state.extras[index]] : []\n ),\n onToggle: booking.toggleServiceFor,\n onContinue: booking.continueFromService,\n total:\n state.items.length === 0\n ? null\n : {\n minutes: kit.wizard.visitMinutes(state.items, state.partyMode),\n priceOre: booking.derived.total,\n },\n canContinue: booking.derived.canAdvance,\n notice:\n state.error === null\n ? null\n : labelText(\n wizardErrorText(\n kit.labels,\n state.error,\n kit.config.party.maxServicesPerPerson\n )\n ),\n labels: kit.labels,\n }\n : undefined\n }\n classNames={resolved.classNames.service}\n components={\n resolved.components.ServiceCard\n ? { ServiceCard: resolved.components.ServiceCard }\n : undefined\n }\n />\n );\n}\n\nfunction WhenStep({ booking, resolved }: StepProps) {\n const { kit, state, catalogue, slots, schedule } = booking;\n const { labels, format, wizard, config } = kit;\n const party = booking.derived.party;\n const items = state.items;\n const phone = config.contact.phone;\n return (\n <>\n <StylistScreen\n labels={screenLabels(labels)}\n format={format}\n // Not deduplicated: a named stylist has to cover every service of every\n // line — a person's extras included, as their visit is one stylist's.\n serviceIds={items.flatMap((item) => visitServicesOf(item).map((service) => service.id))}\n resources={catalogue.resources}\n loading={!catalogue.resourcesKnown}\n nextAvailableLoading={slots.nextAvailableLoading}\n selectedResourceId={state.resourceId}\n nextAvailableTs={slots.nextAvailableTs}\n pendingName={catalogue.chosenStylistName}\n notice={catalogue.stylistNotice}\n onPick={booking.pickResource}\n firstAvailableFaces={config.screens.firstAvailableFaces}\n edgeFade={config.screens.stylistEdgeFade}\n party={\n party\n ? {\n mode: state.partyMode,\n size: items.length,\n sizeWord: partySizeWord(labels, items.length),\n minutes: {\n sequential: wizard.visitMinutes(items, 'sequential'),\n parallel: wizard.visitMinutes(items, 'parallel'),\n },\n onMode: (mode) => booking.dispatch({ type: 'setPartyMode', mode }),\n }\n : undefined\n }\n classNames={resolved.classNames.stylist}\n components={\n resolved.components.StylistCard\n ? { StylistCard: resolved.components.StylistCard }\n : undefined\n }\n />\n\n {slots.ready ? (\n <TimeScreen\n labels={screenLabels(labels)}\n format={format}\n dayparts={kit.dayparts}\n monthView\n slots={slots.single}\n days={slots.days}\n openDays={schedule.openDays}\n stylistName={catalogue.chosenStylistName}\n takenSlotTs={slots.takenSlotTs}\n phone={phone}\n weekendNote={(dayTs) => weekendNoteFor(kit, party ? items : items.slice(0, 1), dayTs)}\n onPick={booking.pickSlot}\n soonest={\n config.screens.soonest\n ? {\n resolveStylist: (resourceId) => stylistFace(catalogue.resources, resourceId),\n }\n : undefined\n }\n dayFullness={config.screens.dayFullness}\n party={\n party\n ? {\n items,\n mode: state.partyMode,\n slots: slots.party,\n chipEndTs: (slot) => wizard.visitEndTs(items, slot.startTs, 'sequential'),\n // Only a back-to-back family is offered the simultaneous rescue.\n alternativeFor:\n state.partyMode === 'sequential'\n ? (dayTs) =>\n kit.partySlots.partyAlternative({\n sequential: slots.party,\n parallel: slots.alternatives,\n dayTs,\n })\n : undefined,\n resolveStylistName: catalogue.resolveStylistName,\n onPick: booking.pickPartySlot,\n }\n : undefined\n }\n classNames={resolved.classNames.time}\n components={timeComponents(resolved)}\n />\n ) : slots.failed ? (\n <SlotsUnavailable kit={kit} onRetry={booking.refresh} />\n ) : (\n // The step's own shape, so nothing below it moves when it fills in.\n <TimeScreenSkeleton\n labels={screenLabels(labels)}\n days={slots.days.length}\n monthView\n surchargeRow={items.some((item) =>\n visitServicesOf(item).some((service) => service.weekendSurchargePct > 0)\n )}\n />\n )}\n\n {/* Once, outside the skeleton-or-step switch, so a re-read cannot remount it. */}\n <TakenToast labels={screenLabels(labels)} takenSlotTs={slots.takenSlotTs} />\n </>\n );\n}\n\n/**\n * The details step's recap (`config.screens.recap`): the day, hours, who and\n * total of the booking the form below submits, «edit» back to the time step,\n * and a swap to another stylist free at that minute.\n */\nfunction RecapStep({ booking, resolved }: StepProps) {\n const { kit, state, catalogue, slots } = booking;\n const recap = useMemo(\n () => recapProps(kit, state, catalogue.resources, slots.single),\n [kit, state, catalogue.resources, slots.single]\n );\n return (\n <BookingRecap\n lines={recap.lines}\n totalOre={booking.derived.total}\n format={kit.format}\n labels={screenLabels(kit.labels)}\n onEdit={() => booking.dispatch({ type: 'goToStep', step: 'when' })}\n alternatives={recap.alternatives}\n // Alternatives exist only for a timed visit, so there is always a start to keep.\n onSwap={(resourceId) => booking.pickSlot({ startTs: state.startTs as number, resourceId })}\n classNames={resolved.classNames.recap}\n />\n );\n}\n\nfunction timeComponents({ components }: ResolvedBooking) {\n if (!components.DayChip && !components.TimeChip) return undefined;\n return { DayChip: components.DayChip, TimeChip: components.TimeChip };\n}\n\nfunction DetailsStep({ booking, resolved }: StepProps) {\n const { kit, state } = booking;\n const guardian = booking.login.guardian;\n const lines = useMemo(() => detailsLines(kit, state), [kit, state]);\n return (\n <DetailsScreen\n state={forMedaScreen(state)}\n onChange={booking.dispatch}\n onSubmit={booking.submit}\n lines={lines}\n totalOre={booking.derived.total}\n {...detailsScreenBase(kit)}\n submitting={booking.submitting}\n submissionNonce={booking.submissionNonce}\n family={guardian?.family}\n guardianPhone={guardian?.phone ?? null}\n // The address they logged in with: the booking is theirs, by that address.\n emailReadOnly={kit.config.account.required && guardian !== null}\n format={kit.format}\n labels={screenLabels(kit.labels)}\n classNames={resolved.classNames.details}\n components={\n resolved.components.FamilyChip ? { FamilyChip: resolved.components.FamilyChip } : undefined\n }\n />\n );\n}\n\n/**\n * «Bekreft» under `account.required` for a parent not logged in: «Nesten\n * ferdig», a line, and the login — Vipps, or e-mail — in place of the form.\n * The heading carries the details step's id, so the focus a step change moves\n * lands here, and the form's heading takes it over once the parent is in.\n */\nfunction AccountGate({\n booking,\n resolved,\n actions,\n overrides,\n onSignedIn,\n}: StepProps & {\n actions: NonNullable<BookingWizardProps['actions']>;\n overrides: BookingOverrides;\n onSignedIn: (guardian: Parameters<BookingController['login']['signIn']>[0]) => void;\n}) {\n const { labels } = booking.kit;\n const { classNames } = resolved;\n // The details screen's own root and heading slots, after the same defaults.\n const slot = (base: string, extra: string | undefined) => [base, extra].filter(Boolean).join(' ');\n return (\n <section\n aria-labelledby={STEP_HEADINGS.details}\n className={slot('space-y-6', classNames.details?.root)}\n >\n <h2\n id={STEP_HEADINGS.details}\n tabIndex={-1}\n className={slot(\n 'font-sans text-2xl font-bold outline-none md:text-3xl',\n classNames.details?.heading\n )}\n >\n {labelText(labels['wizard.account.heading'])}\n </h2>\n <LiveStatus\n text={booking.login.sessionLost ? labelText(labels['wizard.account.sessionLost']) : null}\n className=\"text-sm font-medium empty:hidden\"\n />\n <p className=\"text-muted-foreground\">{labelText(labels['wizard.account.intro'])}</p>\n {/* What the login is for: the hour held, in the summary bar's own words and slot. */}\n <p\n className={slot(\n 'rounded-lg border border-border bg-card px-4 py-3 text-sm font-medium',\n classNames.summary?.line\n )}\n >\n {booking.derived.summary}\n </p>\n <LoginSheet\n {...overridesOf(overrides)}\n actions={actions}\n presentation=\"gate\"\n resumePath={booking.login.resumePath}\n vippsConfirm={booking.login.vippsConfirm}\n onSignedIn={onSignedIn}\n />\n </section>\n );\n}\n\nfunction ConfirmationStep({ booking, resolved, siteUrl }: StepProps & { siteUrl?: string }) {\n const { kit } = booking;\n // Read once, so «today» cannot become «tomorrow» halfway down the card.\n const [now] = useState(() => Date.now());\n const confirmation = booking.confirmed as BookingConfirmation;\n return (\n <Confirmation\n {...confirmationProps(kit, confirmation, siteUrl)}\n onStartOver={booking.startOver}\n now={now}\n format={kit.format}\n labels={screenLabels(kit.labels)}\n classNames={resolved.classNames.confirmation}\n components={\n resolved.components.PartyLine ? { PartyLine: resolved.components.PartyLine } : undefined\n }\n />\n );\n}\n\n/**\n * «Have an account? Log in» — or, once the parent is known, nothing at all\n * for one who arrived logged in, and one line saying so for one who logged in\n * from the sheet.\n */\nfunction LoginRow({\n booking,\n actions,\n overrides,\n onSignedIn,\n}: {\n booking: BookingController;\n actions: NonNullable<BookingWizardProps['actions']>;\n overrides: BookingOverrides;\n onSignedIn: (guardian: Parameters<BookingController['login']['signIn']>[0]) => void;\n}) {\n const { arrivedAs, signedIn, resumePath } = booking.login;\n const { labels } = booking.kit;\n if (signedIn === null && arrivedAs !== null) return null;\n // Named only by the login itself: an unreadable profile is «Logget inn»,\n // never the name of whoever the page arrived as.\n const who = signedIn?.guardian ?? null;\n const name =\n who === null\n ? null\n : [who.firstName, who.lastName].filter(Boolean).join(' ').trim() || who.email;\n const confirmation =\n signedIn === null\n ? null\n : name === null\n ? labelText(labels['wizard.signedIn'])\n : fill(labels['wizard.signedInAs'], { name });\n return (\n <div>\n {signedIn === null && (\n <LoginSheet\n {...overridesOf(overrides)}\n actions={actions}\n presentation=\"sheet\"\n resumePath={resumePath}\n onSignedIn={onSignedIn}\n />\n )}\n <LiveStatus text={confirmation} className=\"text-sm text-muted-foreground empty:hidden\" />\n </div>\n );\n}\n\n/** The neutral shell the restore gate shows, the header's and a step's shape. */\nfunction RestoreSkeleton({ text }: { text: string }) {\n return (\n <div data-testid=\"restore-skeleton\">\n {/* Outside the busy subtree: assistive tech may hold back what is inside it. */}\n <LiveStatus text={text} />\n <div aria-busy=\"true\" className=\"space-y-8\">\n <div aria-hidden=\"true\" className=\"space-y-3\">\n <div className=\"flex items-center gap-3\">\n <span className=\"size-9 shrink-0 animate-pulse rounded-full bg-muted\" />\n <span className=\"flex flex-col gap-1.5\">\n <span className=\"h-4 w-24 animate-pulse rounded bg-muted\" />\n <span className=\"h-3 w-36 animate-pulse rounded bg-muted\" />\n </span>\n </div>\n <span className=\"block h-1.5 w-full animate-pulse rounded-full bg-muted\" />\n </div>\n <div aria-hidden=\"true\" className=\"space-y-6\">\n <span className=\"block h-8 w-56 animate-pulse rounded bg-muted md:h-9\" />\n <div className=\"space-y-2\">\n {Array.from({ length: 4 }, (_, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: four identical placeholders.\n <span key={index} className=\"block h-24 animate-pulse rounded-lg bg-muted\" />\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * Where the visitor is, the way back, and how much is left: a back arrow, the\n * title and «step N of 4», and one segment per step that IS the button to it —\n * tappable only where the machine's `canGoToStep` says so, and frozen while a\n * submission is in flight.\n */\nfunction WizardHeader({\n kit,\n state,\n submitting,\n onGo,\n}: {\n kit: BookingKit;\n state: WizardState;\n submitting: boolean;\n onGo: (step: WizardStep) => void;\n}) {\n const { labels, wizard } = kit;\n const stepLabel = (step: WizardStep) => labelText(labels[`wizard.step.${step}`]);\n const index = STEPS.indexOf(state.step);\n const previous = index > 0 ? STEPS[index - 1] : null;\n const canGoBack = previous !== null && !submitting && wizard.canGoToStep(state, previous);\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-3\">\n {canGoBack ? (\n <button\n type=\"button\"\n aria-label={fill(labels['wizard.back'], { label: stepLabel(previous) })}\n onClick={() => onGo(previous)}\n className=\"flex size-9 shrink-0 items-center justify-center rounded-full border border-border text-foreground transition-colors hover:bg-muted\"\n >\n <ArrowLeft aria-hidden=\"true\" className=\"size-4\" />\n </button>\n ) : (\n // Held rather than dropped, so the title does not shuffle sideways.\n <span aria-hidden=\"true\" className=\"size-9 shrink-0\" />\n )}\n <p className=\"min-w-0\">\n <span className=\"block font-sans text-sm font-bold\">{labels['wizard.title']}</span>\n <span className=\"block truncate text-xs text-muted-foreground\">\n {fillParts(labels['wizard.progress'], {\n step: index + 1,\n total: STEPS.length,\n label: stepLabel(state.step),\n })}\n </span>\n </p>\n </div>\n\n <ol\n aria-label={labelText(labels['wizard.progressLabel'])}\n className=\"flex items-center gap-1.5\"\n >\n {STEPS.map((step, position) => {\n const isCurrent = step === state.step;\n const reachable = !submitting && !isCurrent && wizard.canGoToStep(state, step);\n return (\n <li key={step} className=\"flex-1\">\n <button\n type=\"button\"\n disabled={!reachable}\n aria-current={isCurrent ? 'step' : undefined}\n onClick={() => onGo(step)}\n className={`block h-1.5 w-full rounded-full transition-colors ${\n position <= index ? 'bg-primary' : 'bg-border'\n } ${reachable ? 'cursor-pointer' : 'cursor-default'}`}\n >\n <span className=\"sr-only\">{stepLabel(step)}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </div>\n );\n}\n\n/**\n * The openings could not be read — this site's failure, not a full day —\n * with the telephone as the rescue and a retry as the cheaper first try.\n */\nfunction SlotsUnavailable({ kit, onRetry }: { kit: BookingKit; onRetry: () => void }) {\n const { labels, config, format } = kit;\n const phone = config.contact.phone;\n return (\n <div className=\"space-y-3\">\n <p role=\"alert\" className=\"rounded-lg border border-border bg-card px-5 py-4 text-sm\">\n {labels['wizard.slotsUnavailable.lead']}\n {phone ? (\n <a\n href={format.telHref(phone)}\n aria-label={fill(labels['wizard.slotsUnavailable.callAria'], { phone })}\n className=\"font-semibold text-primary underline underline-offset-4\"\n >\n {fillParts(labels['wizard.slotsUnavailable.call'], { phone })}\n </a>\n ) : (\n labels['wizard.slotsUnavailable.callPlain']\n )}\n {labels['wizard.slotsUnavailable.suffix']}\n </p>\n <BookingButtonOutline onClick={onRetry}>{labels['wizard.retry']}</BookingButtonOutline>\n </div>\n );\n}\n\nfunction BookingButtonOutline({ onClick, children }: { onClick: () => void; children: ReactNode }) {\n return (\n <BookingButton variant=\"outline\" onClick={onClick}>\n {children}\n </BookingButton>\n );\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { BookingOverrides } from "./Provider.mjs";
|
|
|
4
4
|
import { VippsConfirm } from "@medalsocial/meda/booking";
|
|
5
5
|
//#region src/react/LoginSheet.d.ts
|
|
6
6
|
/**
|
|
7
|
-
* The site's one login — Vipps, or a six-digit code by e-mail — in the
|
|
7
|
+
* The site's one login — Vipps, or a six-digit code by e-mail — in the three
|
|
8
8
|
* places a visitor takes it, over meda's `LoginSheet` / `LoginPanel`.
|
|
9
9
|
*
|
|
10
10
|
* - `presentation="sheet"` is the booking wizard's: an offer the wizard never
|
|
@@ -14,6 +14,16 @@ import { VippsConfirm } from "@medalsocial/meda/booking";
|
|
|
14
14
|
* `router.replace` to `returnPath` or the portal — the destination is a
|
|
15
15
|
* server page that reads the fresh cookie on the way in, and Back should not
|
|
16
16
|
* land on a login whose job is done.
|
|
17
|
+
* - `presentation="gate"` is the wizard's «Bekreft» under `account.required`:
|
|
18
|
+
* the panel in place of the form, never navigating. Vipps lands on the
|
|
19
|
+
* wizard's resume URL; a good code hands the parent back through
|
|
20
|
+
* `onSignedIn`, like the sheet.
|
|
21
|
+
*
|
|
22
|
+
* Where Vipps is offered, the page and the gate draw e-mail as a second button
|
|
23
|
+
* under it («Fortsett med Vipps» / «Fortsett med e-post») that opens the form.
|
|
24
|
+
* The sheet keeps its form open under Vipps: meda's `LoginSheet` does not take
|
|
25
|
+
* `emailCollapsed` (and under `account.required` the wizard draws no sheet but
|
|
26
|
+
* a Vipps confirm, which opens on the code).
|
|
17
27
|
*
|
|
18
28
|
* THE CODE IS CHECKED BY `fetch`, NOT BY A SERVER ACTION
|
|
19
29
|
* (`POST <portalApi>/login/verify`): in Next 16 an action that sets a cookie
|
|
@@ -37,6 +47,14 @@ export type LoginSheetProps = BookingOverrides & {
|
|
|
37
47
|
vippsConfirm?: VippsConfirm | null;
|
|
38
48
|
/** Draw the «have an account? Log in» row. Default `true`. */
|
|
39
49
|
trigger?: boolean;
|
|
50
|
+
} | {
|
|
51
|
+
presentation: 'gate';
|
|
52
|
+
/** Where a Vipps login should land: the wizard's resume URL. */
|
|
53
|
+
resumePath: string;
|
|
54
|
+
/** A good code; `guardian` is `null` when the read after it failed. */
|
|
55
|
+
onSignedIn: (guardian: BookingGuardian | null) => void;
|
|
56
|
+
/** A Vipps return asking for the e-mailed code: the gate opens on it. */
|
|
57
|
+
vippsConfirm?: VippsConfirm | null;
|
|
40
58
|
} | {
|
|
41
59
|
presentation: 'inline';
|
|
42
60
|
/** Already through `createReturnPath(config)`; `null` is the portal. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
+
import { labelText } from "../core/labels.mjs";
|
|
2
3
|
import { readStartLogin } from "./actions.mjs";
|
|
3
4
|
import { useBookingKit } from "./Provider.mjs";
|
|
4
5
|
import { screenLabels } from "./screen-labels.mjs";
|
|
@@ -67,8 +68,21 @@ function LoginSheet$1(props) {
|
|
|
67
68
|
otpClassNames: classNames.otp,
|
|
68
69
|
vippsClassNames: classNames.vipps
|
|
69
70
|
};
|
|
71
|
+
const vippsLabel = labelText(labels["login.continueVipps"]);
|
|
72
|
+
if (props.presentation === "gate") return /* @__PURE__ */ jsx(LoginPanel, {
|
|
73
|
+
...screen,
|
|
74
|
+
emailCollapsed: true,
|
|
75
|
+
vippsLabel,
|
|
76
|
+
vippsNext: props.resumePath,
|
|
77
|
+
classNames: classNames.loginPanel,
|
|
78
|
+
onSignedIn: props.onSignedIn
|
|
79
|
+
});
|
|
70
80
|
if (props.presentation === "inline") return /* @__PURE__ */ jsx(InlineLogin, {
|
|
71
|
-
screen
|
|
81
|
+
screen: {
|
|
82
|
+
...screen,
|
|
83
|
+
emailCollapsed: true,
|
|
84
|
+
vippsLabel
|
|
85
|
+
},
|
|
72
86
|
classNames: classNames.loginPanel,
|
|
73
87
|
target: props.returnPath ?? null,
|
|
74
88
|
fallback: config.paths.portal ?? "/"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LoginSheet.mjs","names":["LoginSheet","SheetScreen"],"sources":["../../src/react/LoginSheet.tsx"],"sourcesContent":["import {\n LoginPanel,\n type LoginPanelProps,\n type LoginStartResult,\n type LoginVerifyResult,\n LoginSheet as SheetScreen,\n type VippsConfirm,\n} from '@medalsocial/meda/booking';\nimport { useRouter } from 'next/navigation';\nimport type { BookingGuardian } from '../core/types';\nimport { type PortalActions, readStartLogin } from './actions';\nimport { type BookingOverrides, useBookingKit } from './Provider';\nimport { screenLabels } from './screen-labels';\n\nexport type { VippsConfirm };\n\n/**\n * The site's one login — Vipps, or a six-digit code by e-mail — in the
|
|
1
|
+
{"version":3,"file":"LoginSheet.mjs","names":["LoginSheet","SheetScreen"],"sources":["../../src/react/LoginSheet.tsx"],"sourcesContent":["import {\n LoginPanel,\n type LoginPanelProps,\n type LoginStartResult,\n type LoginVerifyResult,\n LoginSheet as SheetScreen,\n type VippsConfirm,\n} from '@medalsocial/meda/booking';\nimport { useRouter } from 'next/navigation';\nimport { labelText } from '../core/labels';\nimport type { BookingGuardian } from '../core/types';\nimport { type PortalActions, readStartLogin } from './actions';\nimport { type BookingOverrides, useBookingKit } from './Provider';\nimport { screenLabels } from './screen-labels';\n\nexport type { VippsConfirm };\n\n/**\n * The site's one login — Vipps, or a six-digit code by e-mail — in the three\n * places a visitor takes it, over meda's `LoginSheet` / `LoginPanel`.\n *\n * - `presentation=\"sheet\"` is the booking wizard's: an offer the wizard never\n * waits on, that never navigates. A good code hands the visitor back\n * through `onSignedIn` with the `guardian` the verify route answers with.\n * - `presentation=\"inline\"` is the login page's. A good code ends in ONE\n * `router.replace` to `returnPath` or the portal — the destination is a\n * server page that reads the fresh cookie on the way in, and Back should not\n * land on a login whose job is done.\n * - `presentation=\"gate\"` is the wizard's «Bekreft» under `account.required`:\n * the panel in place of the form, never navigating. Vipps lands on the\n * wizard's resume URL; a good code hands the parent back through\n * `onSignedIn`, like the sheet.\n *\n * Where Vipps is offered, the page and the gate draw e-mail as a second button\n * under it («Fortsett med Vipps» / «Fortsett med e-post») that opens the form.\n * The sheet keeps its form open under Vipps: meda's `LoginSheet` does not take\n * `emailCollapsed` (and under `account.required` the wizard draws no sheet but\n * a Vipps confirm, which opens on the code).\n *\n * THE CODE IS CHECKED BY `fetch`, NOT BY A SERVER ACTION\n * (`POST <portalApi>/login/verify`): in Next 16 an action that sets a cookie\n * makes the client re-fetch the route it was called from — the whole booking\n * page behind the sheet, or a second render racing the `replace`. Asking for\n * the code stays the app's action (`actions.startLogin`): it sets no cookie.\n *\n * Vipps is offered when the site has it (`config.portal.methods`) and the app\n * passed its start action.\n */\nexport type LoginSheetProps = BookingOverrides & {\n /** The app's server actions (Decision 8). */\n actions: Pick<PortalActions, 'startLogin' | 'startVipps'>;\n} & (\n | {\n presentation: 'sheet';\n /** Where a Vipps login should land: the wizard's resume URL. */\n resumePath: string;\n /** A good code; `guardian` is `null` when the read after it failed. */\n onSignedIn: (guardian: BookingGuardian | null) => void;\n /** A Vipps return asking for the e-mailed code: the sheet opens on it. */\n vippsConfirm?: VippsConfirm | null;\n /** Draw the «have an account? Log in» row. Default `true`. */\n trigger?: boolean;\n }\n | {\n presentation: 'gate';\n /** Where a Vipps login should land: the wizard's resume URL. */\n resumePath: string;\n /** A good code; `guardian` is `null` when the read after it failed. */\n onSignedIn: (guardian: BookingGuardian | null) => void;\n /** A Vipps return asking for the e-mailed code: the gate opens on it. */\n vippsConfirm?: VippsConfirm | null;\n }\n | {\n presentation: 'inline';\n /** Already through `createReturnPath(config)`; `null` is the portal. */\n returnPath?: string | null;\n vippsConfirm?: VippsConfirm | null;\n }\n );\n\n/**\n * Ask a verify route, and say what its answer means for the form. The\n * route's own reasons are the form's; anything else (a 403, a body that is\n * not the route's, the network failing) is `unreachable`.\n */\nasync function askVerify(\n path: string,\n payload: Record<string, string>\n): Promise<LoginVerifyResult> {\n let body: unknown;\n try {\n const response = await fetch(path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n cache: 'no-store',\n credentials: 'same-origin',\n });\n body = await response.json();\n } catch {\n return { ok: false, reason: 'unreachable' };\n }\n const answer = body as {\n ok?: unknown;\n guardian?: BookingGuardian | null;\n reason?: unknown;\n } | null;\n if (answer?.ok === true) return { ok: true, guardian: answer.guardian ?? null };\n const reason = answer?.reason;\n return reason === 'invalid' || reason === 'throttled' || reason === 'conflict'\n ? { ok: false, reason }\n : { ok: false, reason: 'unreachable' };\n}\n\nexport function LoginSheet(props: LoginSheetProps) {\n const { kit, classNames } = useBookingKit(props);\n const { config, labels } = kit;\n const { actions } = props;\n const api = config.paths.portalApi;\n const screen = {\n labels: screenLabels(labels),\n // «Sent» for every address; only a failed action is not (so the form is\n // never an oracle for who books here, and never claims a code it did not send).\n onStartLogin: async (email: string): Promise<LoginStartResult> => {\n try {\n return readStartLogin(await actions.startLogin({ email }));\n } catch {\n return { ok: false, reason: 'unreachable' };\n }\n },\n // The Vipps confirm code is checked by the code alone: the pending link is\n // in an httpOnly cookie the route reads.\n onVerify: (code: string, context: { mode: 'email' | 'vipps'; email: string }) =>\n context.mode === 'vipps'\n ? askVerify(`${api}/vipps/link/verify`, { code })\n : askVerify(`${api}/login/verify`, { email: context.email, code }),\n onVipps: config.portal.methods.includes('vipps') ? actions.startVipps : undefined,\n vippsConfirm: props.vippsConfirm ?? null,\n otpClassNames: classNames.otp,\n vippsClassNames: classNames.vipps,\n };\n const vippsLabel = labelText(labels['login.continueVipps']);\n if (props.presentation === 'gate') {\n return (\n <LoginPanel\n {...screen}\n emailCollapsed\n vippsLabel={vippsLabel}\n vippsNext={props.resumePath}\n classNames={classNames.loginPanel}\n onSignedIn={props.onSignedIn}\n />\n );\n }\n if (props.presentation === 'inline') {\n return (\n <InlineLogin\n screen={{ ...screen, emailCollapsed: true, vippsLabel }}\n classNames={classNames.loginPanel}\n target={props.returnPath ?? null}\n fallback={config.paths.portal ?? '/'}\n />\n );\n }\n return (\n <SheetScreen\n {...screen}\n resumePath={props.resumePath}\n onSignedIn={props.onSignedIn}\n trigger={props.trigger}\n classNames={classNames.loginSheet}\n panelClassNames={classNames.loginPanel}\n />\n );\n}\n\n/** Its own component so only the page login reads the router. */\nfunction InlineLogin({\n screen,\n classNames,\n target,\n fallback,\n}: {\n screen: Omit<LoginPanelProps, 'onSignedIn'>;\n classNames: LoginPanelProps['classNames'];\n target: string | null;\n fallback: string;\n}) {\n const router = useRouter();\n return (\n <LoginPanel\n {...screen}\n classNames={classNames}\n vippsNext={target}\n onSignedIn={() => router.replace(target ?? fallback)}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAqFA,eAAe,UACb,MACA,SAC4B;CAC5B,IAAI;CACJ,IAAI;EAQF,OAAO,OAAM,MAPU,MAAM,MAAM;GACjC,QAAQ;GACR,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C,MAAM,KAAK,UAAU,OAAO;GAC5B,OAAO;GACP,aAAa;EACf,CAAC,EAAA,CACqB,KAAK;CAC7B,QAAQ;EACN,OAAO;GAAE,IAAI;GAAO,QAAQ;EAAc;CAC5C;CACA,MAAM,SAAS;CAKf,IAAI,QAAQ,OAAO,MAAM,OAAO;EAAE,IAAI;EAAM,UAAU,OAAO,YAAY;CAAK;CAC9E,MAAM,SAAS,QAAQ;CACvB,OAAO,WAAW,aAAa,WAAW,eAAe,WAAW,aAChE;EAAE,IAAI;EAAO;CAAO,IACpB;EAAE,IAAI;EAAO,QAAQ;CAAc;AACzC;AAEA,SAAgBA,aAAW,OAAwB;CACjD,MAAM,EAAE,KAAK,eAAe,cAAc,KAAK;CAC/C,MAAM,EAAE,QAAQ,WAAW;CAC3B,MAAM,EAAE,YAAY;CACpB,MAAM,MAAM,OAAO,MAAM;CACzB,MAAM,SAAS;EACb,QAAQ,aAAa,MAAM;EAG3B,cAAc,OAAO,UAA6C;GAChE,IAAI;IACF,OAAO,eAAe,MAAM,QAAQ,WAAW,EAAE,MAAM,CAAC,CAAC;GAC3D,QAAQ;IACN,OAAO;KAAE,IAAI;KAAO,QAAQ;IAAc;GAC5C;EACF;EAGA,WAAW,MAAc,YACvB,QAAQ,SAAS,UACb,UAAU,GAAG,IAAI,qBAAqB,EAAE,KAAK,CAAC,IAC9C,UAAU,GAAG,IAAI,gBAAgB;GAAE,OAAO,QAAQ;GAAO;EAAK,CAAC;EACrE,SAAS,OAAO,OAAO,QAAQ,SAAS,OAAO,IAAI,QAAQ,aAAa,KAAA;EACxE,cAAc,MAAM,gBAAgB;EACpC,eAAe,WAAW;EAC1B,iBAAiB,WAAW;CAC9B;CACA,MAAM,aAAa,UAAU,OAAO,sBAAsB;CAC1D,IAAI,MAAM,iBAAiB,QACzB,OACE,oBAAC,YAAD;EACE,GAAI;EACJ,gBAAA;EACY;EACZ,WAAW,MAAM;EACjB,YAAY,WAAW;EACvB,YAAY,MAAM;CACnB,CAAA;CAGL,IAAI,MAAM,iBAAiB,UACzB,OACE,oBAAC,aAAD;EACE,QAAQ;GAAE,GAAG;GAAQ,gBAAgB;GAAM;EAAW;EACtD,YAAY,WAAW;EACvB,QAAQ,MAAM,cAAc;EAC5B,UAAU,OAAO,MAAM,UAAU;CAClC,CAAA;CAGL,OACE,oBAACC,YAAD;EACE,GAAI;EACJ,YAAY,MAAM;EAClB,YAAY,MAAM;EAClB,SAAS,MAAM;EACf,YAAY,WAAW;EACvB,iBAAiB,WAAW;CAC7B,CAAA;AAEL;;AAGA,SAAS,YAAY,EACnB,QACA,YACA,QACA,YAMC;CACD,MAAM,SAAS,UAAU;CACzB,OACE,oBAAC,YAAD;EACE,GAAI;EACQ;EACZ,WAAW;EACX,kBAAkB,OAAO,QAAQ,UAAU,QAAQ;CACpD,CAAA;AAEL"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BookingConfig } from "../core/config.mjs";
|
|
2
2
|
import { BookingLabelsInput } from "./labels/index.mjs";
|
|
3
3
|
import { BookingKit } from "./kit.mjs";
|
|
4
|
-
import { AccountCardSlot, AddChildSheetSlot, BookingCardProps, BookingSkeletonSlot, ChildCardProps, ChildCardsSlot, ConfirmationSlot, DataControlsSlot, DayChipProps, DetailsScreenSlot, FamilyChipProps, FamilyEditorSlot, FamilyMemberCardProps, LoginPanelSlot, LoginSheetSlot, LogoutButtonSlot, ManageScreenSlot, OtpSlotsSlot, PartyLineProps, PersonCardProps, PortalShellSlot, PortalUnreachableSlot, ProfileFormSlot, RebookCardProps, RebookCardsSlot, ServiceCardProps, ServiceScreenSlot, SlotClassNames, StylistCardProps, StylistScreenSlot, SummaryBarSlot, TimeChipProps, TimeScreenSlot, UpcomingBookingsSlot, VippsButtonSlot, VippsLinkRowSlot, VisitHistorySlot, VisitRowProps, WhoScreenSlot } from "@medalsocial/meda/booking";
|
|
4
|
+
import { AccountCardSlot, AddChildSheetSlot, BookingCardProps, BookingRecapSlot, BookingSkeletonSlot, ChildCardProps, ChildCardsSlot, ConfirmationSlot, DataControlsSlot, DayChipProps, DetailsScreenSlot, FamilyChipProps, FamilyEditorSlot, FamilyMemberCardProps, LoginPanelSlot, LoginSheetSlot, LogoutButtonSlot, ManageScreenSlot, OtpSlotsSlot, PartyLineProps, PersonCardProps, PortalShellSlot, PortalUnreachableSlot, ProfileFormSlot, RebookCardProps, RebookCardsSlot, ServiceCardProps, ServiceScreenSlot, SlotClassNames, StylistCardProps, StylistScreenSlot, SummaryBarSlot, TimeChipProps, TimeScreenSlot, UpcomingBookingsSlot, VippsButtonSlot, VippsLinkRowSlot, VisitHistorySlot, VisitRowProps, WhoScreenSlot } from "@medalsocial/meda/booking";
|
|
5
5
|
import { ComponentType, ReactNode } from "react";
|
|
6
6
|
//#region src/react/Provider.d.ts
|
|
7
7
|
/** Per-screen slot classes (see each meda screen's `…Slot` type for the slots). */
|
|
@@ -13,6 +13,7 @@ export interface BookingClassNames {
|
|
|
13
13
|
stylist?: SlotClassNames<StylistScreenSlot>;
|
|
14
14
|
time?: SlotClassNames<TimeScreenSlot>;
|
|
15
15
|
details?: SlotClassNames<DetailsScreenSlot>;
|
|
16
|
+
recap?: SlotClassNames<BookingRecapSlot>;
|
|
16
17
|
summary?: SlotClassNames<SummaryBarSlot>;
|
|
17
18
|
confirmation?: SlotClassNames<ConfirmationSlot>;
|
|
18
19
|
skeleton?: SlotClassNames<BookingSkeletonSlot>;
|