@medalsocial/booking 0.1.0 → 0.2.1
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 +19 -0
- package/README.md +2 -2
- package/dist/core/config.d.mts +7 -0
- package/dist/core/config.mjs +4 -2
- package/dist/core/config.mjs.map +1 -1
- package/dist/core/draft-store.d.mts +6 -0
- package/dist/core/draft-store.mjs +17 -1
- package/dist/core/draft-store.mjs.map +1 -1
- package/dist/core/index.d.mts +3 -2
- package/dist/core/index.mjs +3 -2
- package/dist/core/machine.d.mts +53 -6
- package/dist/core/machine.mjs +162 -52
- package/dist/core/machine.mjs.map +1 -1
- package/dist/core/party-slots.d.mts +9 -2
- package/dist/core/party-slots.mjs +18 -12
- package/dist/core/party-slots.mjs.map +1 -1
- package/dist/core/types.d.mts +4 -0
- package/dist/core/visit.d.mts +26 -0
- package/dist/core/visit.mjs +40 -0
- package/dist/core/visit.mjs.map +1 -0
- package/dist/next/catalogue.mjs +25 -7
- package/dist/next/catalogue.mjs.map +1 -1
- package/dist/next/medal.d.mts +7 -0
- package/dist/next/medal.mjs +2 -1
- package/dist/next/medal.mjs.map +1 -1
- package/dist/next/routes/catalogue-routes.mjs +18 -4
- package/dist/next/routes/catalogue-routes.mjs.map +1 -1
- package/dist/next/routes/create.mjs +47 -9
- package/dist/next/routes/create.mjs.map +1 -1
- package/dist/next/routes/manage.mjs.map +1 -1
- package/dist/next/routes/shared.mjs +29 -1
- package/dist/next/routes/shared.mjs.map +1 -1
- package/dist/react/BookingWizard.mjs +32 -11
- package/dist/react/BookingWizard.mjs.map +1 -1
- package/dist/react/labels/screens.d.mts +1 -1
- package/dist/react/labels/screens.mjs +12 -0
- package/dist/react/labels/screens.mjs.map +1 -1
- package/dist/react/labels/wizard.d.mts +7 -0
- package/dist/react/labels/wizard.mjs +2 -0
- package/dist/react/labels/wizard.mjs.map +1 -1
- package/dist/react/useBooking.d.mts +2 -0
- package/dist/react/useBooking.mjs +194 -53
- package/dist/react/useBooking.mjs.map +1 -1
- package/dist/react/wizard/adapters.mjs +7 -5
- package/dist/react/wizard/adapters.mjs.map +1 -1
- package/dist/react/wizard-error.mjs +29 -0
- package/dist/react/wizard-error.mjs.map +1 -0
- package/package.json +4 -4
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { fill, fillParts, labelText } from "../core/labels.mjs";
|
|
3
|
+
import { visitServicesOf } from "../core/visit.mjs";
|
|
3
4
|
import { SELF_KEY } from "../core/machine.mjs";
|
|
4
5
|
import { useBookingKit } from "./Provider.mjs";
|
|
5
6
|
import { screenLabels } from "./screen-labels.mjs";
|
|
6
7
|
import { LoginSheet as LoginSheet$1 } from "./LoginSheet.mjs";
|
|
7
8
|
import { useBooking } from "./useBooking.mjs";
|
|
8
9
|
import { addedChildEntries, confirmationProps, detailsLines, detailsScreenBase, familyEntries, guestChoices, partyPeople, partySizeWord, serviceFitsFor, serviceScreenBase, weekendNoteFor } from "./wizard/adapters.mjs";
|
|
9
|
-
import {
|
|
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";
|
|
10
12
|
import { Suspense, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
11
13
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
12
14
|
import { ArrowLeft } from "lucide-react";
|
|
@@ -172,6 +174,7 @@ function BookingWizardShell(props) {
|
|
|
172
174
|
focusAfterSignIn.current = true;
|
|
173
175
|
booking.login.signIn(who);
|
|
174
176
|
};
|
|
177
|
+
const multiSelect = state.step === "service" && kit.config.party.maxServicesPerPerson > 1;
|
|
175
178
|
const loginOffered = props.actions !== void 0 && kit.config.portal.enabled;
|
|
176
179
|
const loginRow = loginOffered ? /* @__PURE__ */ jsx(LoginRow, {
|
|
177
180
|
booking,
|
|
@@ -191,10 +194,10 @@ function BookingWizardShell(props) {
|
|
|
191
194
|
step
|
|
192
195
|
})
|
|
193
196
|
}),
|
|
194
|
-
state.error !== null && state.step !== "details" && state.step !== "when" && /* @__PURE__ */ jsx("p", {
|
|
197
|
+
state.error !== null && state.step !== "details" && state.step !== "when" && !multiSelect && /* @__PURE__ */ jsx("p", {
|
|
195
198
|
role: "alert",
|
|
196
199
|
className: resolved.classNames.wizard?.alert ?? "rounded-lg border border-destructive/40 bg-destructive/10 px-5 py-3 text-sm",
|
|
197
|
-
children: labels
|
|
200
|
+
children: wizardErrorText(labels, state.error, kit.config.party.maxServicesPerPerson)
|
|
198
201
|
}),
|
|
199
202
|
state.step === "details" && loginRow,
|
|
200
203
|
loginOffered && booking.login.vippsConfirm !== null && signedIn === null && /* @__PURE__ */ jsx(LoginSheet$1, {
|
|
@@ -218,7 +221,8 @@ function BookingWizardShell(props) {
|
|
|
218
221
|
}),
|
|
219
222
|
state.step === "service" && /* @__PURE__ */ jsx(ServiceStep, {
|
|
220
223
|
booking,
|
|
221
|
-
resolved
|
|
224
|
+
resolved,
|
|
225
|
+
multiSelect
|
|
222
226
|
}),
|
|
223
227
|
state.step === "when" && /* @__PURE__ */ jsx(WhenStep, {
|
|
224
228
|
booking,
|
|
@@ -230,7 +234,7 @@ function BookingWizardShell(props) {
|
|
|
230
234
|
})
|
|
231
235
|
]
|
|
232
236
|
}, state.step),
|
|
233
|
-
/* @__PURE__ */ jsx(SummaryBar, {
|
|
237
|
+
!multiSelect && /* @__PURE__ */ jsx(SummaryBar, {
|
|
234
238
|
line: booking.derived.summary,
|
|
235
239
|
canAdvance: booking.derived.canAdvance,
|
|
236
240
|
step: state.step,
|
|
@@ -271,10 +275,15 @@ function WhoStep({ booking, resolved, loginRow }) {
|
|
|
271
275
|
components: resolved.components.PersonCard ? { PersonCard: resolved.components.PersonCard } : void 0
|
|
272
276
|
});
|
|
273
277
|
}
|
|
274
|
-
function ServiceStep({ booking, resolved }) {
|
|
278
|
+
function ServiceStep({ booking, resolved, multiSelect }) {
|
|
275
279
|
const { kit, state, people, catalogue } = booking;
|
|
276
280
|
const ageOf = (person) => people.personAge(person, people.ageDayKey);
|
|
277
281
|
const single = state.people.length === 1 ? state.people[0] : null;
|
|
282
|
+
const pickFor = (index, service, oneTap) => {
|
|
283
|
+
const visit = state.choices[index] ? [state.choices[index], ...state.extras[index]] : [];
|
|
284
|
+
if (!multiSelect || visit.length === 0 || visit.length === 1 && visit[0].id === service.id) oneTap();
|
|
285
|
+
else if (!visit.some((ticked) => ticked.id === service.id)) booking.toggleServiceFor(index, service);
|
|
286
|
+
};
|
|
278
287
|
return /* @__PURE__ */ jsx(ServiceScreen, {
|
|
279
288
|
labels: screenLabels(kit.labels),
|
|
280
289
|
format: kit.format,
|
|
@@ -282,19 +291,31 @@ function ServiceStep({ booking, resolved }) {
|
|
|
282
291
|
...serviceScreenBase(kit),
|
|
283
292
|
serviceFits: serviceFitsFor(kit, state.people.map(ageOf)),
|
|
284
293
|
initialCategory: catalogue.deepLinkCategory,
|
|
285
|
-
onPick: booking.pickService,
|
|
294
|
+
onPick: (service) => pickFor(0, service, () => booking.pickService(service)),
|
|
286
295
|
suggestion: single ? people.suggestionFor(single) : null,
|
|
287
296
|
chosenId: state.choices[0]?.id ?? null,
|
|
288
297
|
childName: single ? single.name ?? null : null,
|
|
289
298
|
party: state.people.length > 1 ? {
|
|
290
299
|
people: partyPeople(kit, state.people, ageOf, people.suggestionFor),
|
|
291
300
|
choices: state.choices,
|
|
292
|
-
onPickFor: booking.pickServiceFor,
|
|
301
|
+
onPickFor: (index, service) => pickFor(index, service, () => booking.pickServiceFor(index, service)),
|
|
293
302
|
onRemove: (index) => booking.dispatch({
|
|
294
303
|
type: "choosePeople",
|
|
295
304
|
people: state.people.filter((_, position) => position !== index)
|
|
296
305
|
})
|
|
297
306
|
} : void 0,
|
|
307
|
+
selection: multiSelect ? {
|
|
308
|
+
lists: state.choices.map((first, index) => first ? [first, ...state.extras[index]] : []),
|
|
309
|
+
onToggle: booking.toggleServiceFor,
|
|
310
|
+
onContinue: booking.continueFromService,
|
|
311
|
+
total: state.items.length === 0 ? null : {
|
|
312
|
+
minutes: kit.wizard.visitMinutes(state.items, state.partyMode),
|
|
313
|
+
priceOre: booking.derived.total
|
|
314
|
+
},
|
|
315
|
+
canContinue: booking.derived.canAdvance,
|
|
316
|
+
notice: state.error === null ? null : labelText(wizardErrorText(kit.labels, state.error, kit.config.party.maxServicesPerPerson)),
|
|
317
|
+
labels: kit.labels
|
|
318
|
+
} : void 0,
|
|
298
319
|
classNames: resolved.classNames.service,
|
|
299
320
|
components: resolved.components.ServiceCard ? { ServiceCard: resolved.components.ServiceCard } : void 0
|
|
300
321
|
});
|
|
@@ -309,7 +330,7 @@ function WhenStep({ booking, resolved }) {
|
|
|
309
330
|
/* @__PURE__ */ jsx(StylistScreen, {
|
|
310
331
|
labels: screenLabels(labels),
|
|
311
332
|
format,
|
|
312
|
-
serviceIds: items.
|
|
333
|
+
serviceIds: items.flatMap((item) => visitServicesOf(item).map((service) => service.id)),
|
|
313
334
|
resources: catalogue.resources,
|
|
314
335
|
loading: !catalogue.resourcesKnown,
|
|
315
336
|
nextAvailableLoading: slots.nextAvailableLoading,
|
|
@@ -369,7 +390,7 @@ function WhenStep({ booking, resolved }) {
|
|
|
369
390
|
labels: screenLabels(labels),
|
|
370
391
|
days: slots.days.length,
|
|
371
392
|
monthView: true,
|
|
372
|
-
surchargeRow: items.some((item) => item.service.weekendSurchargePct > 0)
|
|
393
|
+
surchargeRow: items.some((item) => visitServicesOf(item).some((service) => service.weekendSurchargePct > 0))
|
|
373
394
|
}),
|
|
374
395
|
/* @__PURE__ */ jsx(TakenToast, {
|
|
375
396
|
labels: screenLabels(labels),
|
|
@@ -389,7 +410,7 @@ function DetailsStep({ booking, resolved }) {
|
|
|
389
410
|
const guardian = booking.login.guardian;
|
|
390
411
|
const lines = useMemo(() => detailsLines(kit, state), [kit, state]);
|
|
391
412
|
return /* @__PURE__ */ jsx(DetailsScreen, {
|
|
392
|
-
state,
|
|
413
|
+
state: forMedaScreen(state),
|
|
393
414
|
onChange: booking.dispatch,
|
|
394
415
|
onSubmit: booking.submit,
|
|
395
416
|
lines,
|
|
@@ -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 DETAILS_ERROR_LABEL_KEYS,\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 { 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';\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 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 && state.step !== 'details' && state.step !== 'when' && (\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 {labels[DETAILS_ERROR_LABEL_KEYS[state.error]]}\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' && <ServiceStep booking={booking} resolved={resolved} />}\n {state.step === 'when' && <WhenStep booking={booking} resolved={resolved} />}\n {state.step === 'details' && <DetailsStep booking={booking} resolved={resolved} />}\n </div>\n\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 </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 }: StepProps) {\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 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={booking.pickService}\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: booking.pickServiceFor,\n onRemove: (index) =>\n booking.dispatch({\n type: 'choosePeople',\n people: state.people.filter((_, position) => position !== index),\n }),\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 line.\n serviceIds={items.map((item) => item.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) => item.service.weekendSurchargePct > 0)}\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={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 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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"}
|
|
@@ -6,6 +6,6 @@ import { BookingLabels } from "@medalsocial/meda/booking";
|
|
|
6
6
|
* node, an array one per element (meda ≥ 3.4). The built-in packs are all
|
|
7
7
|
* strings.
|
|
8
8
|
*/
|
|
9
|
-
export type ScreenLabels = { [K in keyof BookingLabels]
|
|
9
|
+
export type ScreenLabels = { [K in keyof BookingLabels]-?: BookingLabel; };
|
|
10
10
|
//#endregion
|
|
11
11
|
//# sourceMappingURL=screens.d.mts.map
|
|
@@ -251,16 +251,22 @@ const SCREEN_LABELS_NB = {
|
|
|
251
251
|
"service.ageDivider.named": "Passer vanligvis ikke for alderen til {name}",
|
|
252
252
|
"service.ageDivider.unnamed": "Passer vanligvis ikke for alderen",
|
|
253
253
|
"service.categoriesLegend": "Kategorier",
|
|
254
|
+
"service.chooseFirst": "Velg minst én tjeneste",
|
|
254
255
|
"service.chooseOther": "Velg noe annet",
|
|
256
|
+
"service.continue": "Neste",
|
|
255
257
|
"service.duration": "{minutes} min",
|
|
256
258
|
"service.heading": "Hva vil du bestille?",
|
|
259
|
+
"service.multiHint": "Velg én eller flere",
|
|
257
260
|
"service.party.adultAlone": "{label} må bestilles som en egen time.",
|
|
258
261
|
"service.party.listLabel": "Tjenester for {label}",
|
|
262
|
+
"service.party.nothingBookable": "Ingen tjenester kan bestilles på nett for {label}.",
|
|
259
263
|
"service.party.nothingFits": "Ingen tjenester kan bestilles for {label} i en så stor gruppe.",
|
|
260
264
|
"service.party.remove": "Fjern {label} fra denne timen",
|
|
261
265
|
"service.phoneOnly": "Bestilles på telefon",
|
|
262
266
|
"service.phoneOnlyLinkLabel": "Bestilles på telefon – ring {phone}",
|
|
263
267
|
"service.sameAsLast": "Samme som sist",
|
|
268
|
+
"service.tabDone": "ferdig",
|
|
269
|
+
"service.total": "{minutes} min · {price}",
|
|
264
270
|
"stylist.firstAvailable": "Første ledige",
|
|
265
271
|
"stylist.firstAvailableBadge": "★",
|
|
266
272
|
"stylist.firstAvailableSubtitle": "Første stol som er ledig",
|
|
@@ -603,16 +609,22 @@ const SCREEN_LABELS_EN = {
|
|
|
603
609
|
"service.ageDivider.named": "Usually not suited to {nameGenitive} age",
|
|
604
610
|
"service.ageDivider.unnamed": "Usually not suited to this age",
|
|
605
611
|
"service.categoriesLegend": "Categories",
|
|
612
|
+
"service.chooseFirst": "Choose at least one service",
|
|
606
613
|
"service.chooseOther": "Choose something else",
|
|
614
|
+
"service.continue": "Next",
|
|
607
615
|
"service.duration": "{minutes} min",
|
|
608
616
|
"service.heading": "What would you like to book?",
|
|
617
|
+
"service.multiHint": "Choose one or more",
|
|
609
618
|
"service.party.adultAlone": "{label} needs a separate appointment.",
|
|
610
619
|
"service.party.listLabel": "Services for {label}",
|
|
620
|
+
"service.party.nothingBookable": "Nothing can be booked online for {label}.",
|
|
611
621
|
"service.party.nothingFits": "Nothing can be booked for {label} in a group this size.",
|
|
612
622
|
"service.party.remove": "Remove {label} from this appointment",
|
|
613
623
|
"service.phoneOnly": "Book by phone",
|
|
614
624
|
"service.phoneOnlyLinkLabel": "Book by phone – call {phone}",
|
|
615
625
|
"service.sameAsLast": "Same as last time",
|
|
626
|
+
"service.tabDone": "done",
|
|
627
|
+
"service.total": "{minutes} min · {price}",
|
|
616
628
|
"stylist.firstAvailable": "First available",
|
|
617
629
|
"stylist.firstAvailableBadge": "★",
|
|
618
630
|
"stylist.firstAvailableSubtitle": "We find the first free time",
|