@medalsocial/booking 0.0.0-placeholder.0 → 0.2.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 +27 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +220 -1
- package/dist/core/age.d.mts +34 -0
- package/dist/core/age.mjs +95 -0
- package/dist/core/age.mjs.map +1 -0
- package/dist/core/attempt-store.d.mts +111 -0
- package/dist/core/attempt-store.mjs +159 -0
- package/dist/core/attempt-store.mjs.map +1 -0
- package/dist/core/categories.d.mts +27 -0
- package/dist/core/categories.mjs +42 -0
- package/dist/core/categories.mjs.map +1 -0
- package/dist/core/clock.d.mts +33 -0
- package/dist/core/clock.mjs +251 -0
- package/dist/core/clock.mjs.map +1 -0
- package/dist/core/config.d.mts +178 -0
- package/dist/core/config.mjs +394 -0
- package/dist/core/config.mjs.map +1 -0
- package/dist/core/consent.d.mts +14 -0
- package/dist/core/consent.mjs +17 -0
- package/dist/core/consent.mjs.map +1 -0
- package/dist/core/deep-link.d.mts +40 -0
- package/dist/core/deep-link.mjs +160 -0
- package/dist/core/deep-link.mjs.map +1 -0
- package/dist/core/display-name.d.mts +18 -0
- package/dist/core/display-name.mjs +30 -0
- package/dist/core/display-name.mjs.map +1 -0
- package/dist/core/draft-store.d.mts +89 -0
- package/dist/core/draft-store.mjs +131 -0
- package/dist/core/draft-store.mjs.map +1 -0
- package/dist/core/dto.d.mts +46 -0
- package/dist/core/dto.mjs +157 -0
- package/dist/core/dto.mjs.map +1 -0
- package/dist/core/ics.d.mts +63 -0
- package/dist/core/ics.mjs +130 -0
- package/dist/core/ics.mjs.map +1 -0
- package/dist/core/index.d.mts +35 -0
- package/dist/core/index.mjs +32 -0
- package/dist/core/labels.d.mts +88 -0
- package/dist/core/labels.mjs +93 -0
- package/dist/core/labels.mjs.map +1 -0
- package/dist/core/machine.d.mts +645 -0
- package/dist/core/machine.mjs +988 -0
- package/dist/core/machine.mjs.map +1 -0
- package/dist/core/money.d.mts +12 -0
- package/dist/core/money.mjs +51 -0
- package/dist/core/money.mjs.map +1 -0
- package/dist/core/next-available.d.mts +20 -0
- package/dist/core/next-available.mjs +36 -0
- package/dist/core/next-available.mjs.map +1 -0
- package/dist/core/next-free.d.mts +25 -0
- package/dist/core/next-free.mjs +44 -0
- package/dist/core/next-free.mjs.map +1 -0
- package/dist/core/party-slots.d.mts +79 -0
- package/dist/core/party-slots.mjs +229 -0
- package/dist/core/party-slots.mjs.map +1 -0
- package/dist/core/paths.d.mts +11 -0
- package/dist/core/paths.mjs +56 -0
- package/dist/core/paths.mjs.map +1 -0
- package/dist/core/phone.d.mts +17 -0
- package/dist/core/phone.mjs +58 -0
- package/dist/core/phone.mjs.map +1 -0
- package/dist/core/portal/cookies.d.mts +29 -0
- package/dist/core/portal/dto.d.mts +140 -0
- package/dist/core/portal/dto.mjs +142 -0
- package/dist/core/portal/dto.mjs.map +1 -0
- package/dist/core/portal/family.d.mts +39 -0
- package/dist/core/portal/family.mjs +39 -0
- package/dist/core/portal/family.mjs.map +1 -0
- package/dist/core/portal/login-input.d.mts +29 -0
- package/dist/core/portal/login-input.mjs +29 -0
- package/dist/core/portal/login-input.mjs.map +1 -0
- package/dist/core/portal/next-path.d.mts +12 -0
- package/dist/core/portal/next-path.mjs +98 -0
- package/dist/core/portal/next-path.mjs.map +1 -0
- package/dist/core/portal/return-path.d.mts +7 -0
- package/dist/core/portal/return-path.mjs +49 -0
- package/dist/core/portal/return-path.mjs.map +1 -0
- package/dist/core/portal/session-cookie.d.mts +27 -0
- package/dist/core/portal/session-cookie.mjs +30 -0
- package/dist/core/portal/session-cookie.mjs.map +1 -0
- package/dist/core/portal/vipps-link.d.mts +56 -0
- package/dist/core/portal/vipps-link.mjs +136 -0
- package/dist/core/portal/vipps-link.mjs.map +1 -0
- package/dist/core/portal/vipps-return.d.mts +39 -0
- package/dist/core/portal/vipps-return.mjs +69 -0
- package/dist/core/portal/vipps-return.mjs.map +1 -0
- package/dist/core/rebook-store.d.mts +28 -0
- package/dist/core/rebook-store.mjs +60 -0
- package/dist/core/rebook-store.mjs.map +1 -0
- package/dist/core/restore-gate.d.mts +40 -0
- package/dist/core/restore-gate.mjs +96 -0
- package/dist/core/restore-gate.mjs.map +1 -0
- package/dist/core/stores.d.mts +8 -0
- package/dist/core/stores.mjs +23 -0
- package/dist/core/stores.mjs.map +1 -0
- package/dist/core/types.d.mts +245 -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/core/wire.d.mts +37 -0
- package/dist/next/cache/memory.d.mts +14 -0
- package/dist/next/cache/memory.mjs +51 -0
- package/dist/next/cache/memory.mjs.map +1 -0
- package/dist/next/cache/next-data.d.mts +6 -0
- package/dist/next/cache/next-data.mjs +35 -0
- package/dist/next/cache/next-data.mjs.map +1 -0
- package/dist/next/cache/noop.d.mts +6 -0
- package/dist/next/cache/noop.mjs +15 -0
- package/dist/next/cache/noop.mjs.map +1 -0
- package/dist/next/cache/workers.d.mts +29 -0
- package/dist/next/cache/workers.mjs +74 -0
- package/dist/next/cache/workers.mjs.map +1 -0
- package/dist/next/catalogue.d.mts +38 -0
- package/dist/next/catalogue.mjs +215 -0
- package/dist/next/catalogue.mjs.map +1 -0
- package/dist/next/handler.d.mts +11 -0
- package/dist/next/handler.mjs +150 -0
- package/dist/next/handler.mjs.map +1 -0
- package/dist/next/index.d.mts +17 -0
- package/dist/next/index.mjs +14 -0
- package/dist/next/loaders.d.mts +79 -0
- package/dist/next/loaders.mjs +195 -0
- package/dist/next/loaders.mjs.map +1 -0
- package/dist/next/manage-dto.d.mts +6 -0
- package/dist/next/manage-dto.mjs +28 -0
- package/dist/next/manage-dto.mjs.map +1 -0
- package/dist/next/medal.d.mts +105 -0
- package/dist/next/medal.mjs +155 -0
- package/dist/next/medal.mjs.map +1 -0
- package/dist/next/options.d.mts +158 -0
- package/dist/next/options.mjs +65 -0
- package/dist/next/options.mjs.map +1 -0
- package/dist/next/portal/guardian.d.mts +42 -0
- package/dist/next/portal/guardian.mjs +76 -0
- package/dist/next/portal/guardian.mjs.map +1 -0
- package/dist/next/portal/medal-portal.d.mts +161 -0
- package/dist/next/portal/medal-portal.mjs +825 -0
- package/dist/next/portal/medal-portal.mjs.map +1 -0
- package/dist/next/portal/session.d.mts +38 -0
- package/dist/next/portal/session.mjs +40 -0
- package/dist/next/portal/session.mjs.map +1 -0
- package/dist/next/portal/vipps-flash.d.mts +14 -0
- package/dist/next/portal/vipps-flash.mjs +41 -0
- package/dist/next/portal/vipps-flash.mjs.map +1 -0
- package/dist/next/portal.d.mts +205 -0
- package/dist/next/portal.mjs +469 -0
- package/dist/next/portal.mjs.map +1 -0
- package/dist/next/request.d.mts +51 -0
- package/dist/next/request.mjs +123 -0
- package/dist/next/request.mjs.map +1 -0
- package/dist/next/routes/catalogue-routes.mjs +344 -0
- package/dist/next/routes/catalogue-routes.mjs.map +1 -0
- package/dist/next/routes/create.mjs +460 -0
- package/dist/next/routes/create.mjs.map +1 -0
- package/dist/next/routes/manage.mjs +191 -0
- package/dist/next/routes/manage.mjs.map +1 -0
- package/dist/next/routes/portal-routes.mjs +312 -0
- package/dist/next/routes/portal-routes.mjs.map +1 -0
- package/dist/next/routes/shared.mjs +162 -0
- package/dist/next/routes/shared.mjs.map +1 -0
- package/dist/next/routes/vipps-return.mjs +166 -0
- package/dist/next/routes/vipps-return.mjs.map +1 -0
- package/dist/next/runtime.d.mts +49 -0
- package/dist/next/runtime.mjs +94 -0
- package/dist/next/runtime.mjs.map +1 -0
- package/dist/next/seed.d.mts +71 -0
- package/dist/next/seed.mjs +495 -0
- package/dist/next/seed.mjs.map +1 -0
- package/dist/next/server.d.mts +26 -0
- package/dist/next/server.mjs +63 -0
- package/dist/next/server.mjs.map +1 -0
- package/dist/react/BookingLink.d.mts +61 -0
- package/dist/react/BookingLink.mjs +102 -0
- package/dist/react/BookingLink.mjs.map +1 -0
- package/dist/react/BookingWizard.d.mts +49 -0
- package/dist/react/BookingWizard.mjs +598 -0
- package/dist/react/BookingWizard.mjs.map +1 -0
- package/dist/react/LoginPageQuery.d.mts +37 -0
- package/dist/react/LoginPageQuery.mjs +106 -0
- package/dist/react/LoginPageQuery.mjs.map +1 -0
- package/dist/react/LoginSheet.d.mts +49 -0
- package/dist/react/LoginSheet.mjs +98 -0
- package/dist/react/LoginSheet.mjs.map +1 -0
- package/dist/react/ManageBooking.d.mts +50 -0
- package/dist/react/ManageBooking.mjs +197 -0
- package/dist/react/ManageBooking.mjs.map +1 -0
- package/dist/react/PortalDashboard.d.mts +10 -0
- package/dist/react/Provider.d.mts +82 -0
- package/dist/react/Provider.mjs +132 -0
- package/dist/react/Provider.mjs.map +1 -0
- package/dist/react/actions.d.mts +133 -0
- package/dist/react/actions.mjs +90 -0
- package/dist/react/actions.mjs.map +1 -0
- package/dist/react/index.d.mts +28 -0
- package/dist/react/index.mjs +20 -0
- package/dist/react/kit.d.mts +54 -0
- package/dist/react/kit.mjs +127 -0
- package/dist/react/kit.mjs.map +1 -0
- package/dist/react/labels/index.d.mts +23 -0
- package/dist/react/labels/index.mjs +44 -0
- package/dist/react/labels/index.mjs.map +1 -0
- package/dist/react/labels/login.d.mts +10 -0
- package/dist/react/labels/login.mjs +13 -0
- package/dist/react/labels/login.mjs.map +1 -0
- package/dist/react/labels/manage.d.mts +15 -0
- package/dist/react/labels/manage.mjs +19 -0
- package/dist/react/labels/manage.mjs.map +1 -0
- package/dist/react/labels/portal.d.mts +25 -0
- package/dist/react/labels/portal.mjs +41 -0
- package/dist/react/labels/portal.mjs.map +1 -0
- package/dist/react/labels/screens.d.mts +11 -0
- package/dist/react/labels/screens.mjs +721 -0
- package/dist/react/labels/screens.mjs.map +1 -0
- package/dist/react/labels/wizard.d.mts +76 -0
- package/dist/react/labels/wizard.mjs +97 -0
- package/dist/react/labels/wizard.mjs.map +1 -0
- package/dist/react/link/index.d.mts +5 -0
- package/dist/react/link/index.mjs +5 -0
- package/dist/react/portal/SessionTouch.d.mts +19 -0
- package/dist/react/portal/SessionTouch.mjs +40 -0
- package/dist/react/portal/SessionTouch.mjs.map +1 -0
- package/dist/react/portal/age-prompt.d.mts +10 -0
- package/dist/react/portal/age-prompt.mjs +21 -0
- package/dist/react/portal/age-prompt.mjs.map +1 -0
- package/dist/react/portal/book-again.d.mts +18 -0
- package/dist/react/portal/book-again.mjs +21 -0
- package/dist/react/portal/book-again.mjs.map +1 -0
- package/dist/react/portal/child-summary.d.mts +17 -0
- package/dist/react/portal/child-summary.mjs +45 -0
- package/dist/react/portal/child-summary.mjs.map +1 -0
- package/dist/react/portal/dashboard.d.mts +74 -0
- package/dist/react/portal/dashboard.mjs +204 -0
- package/dist/react/portal/dashboard.mjs.map +1 -0
- package/dist/react/portal/leave.d.mts +17 -0
- package/dist/react/portal/leave.mjs +21 -0
- package/dist/react/portal/leave.mjs.map +1 -0
- package/dist/react/portal/sections.d.mts +12 -0
- package/dist/react/portal/sections.mjs +48 -0
- package/dist/react/portal/sections.mjs.map +1 -0
- package/dist/react/portal/tabs.d.mts +23 -0
- package/dist/react/portal/tabs.mjs +36 -0
- package/dist/react/portal/tabs.mjs.map +1 -0
- package/dist/react/portal/vipps-flash.d.mts +11 -0
- package/dist/react/portal/vipps-flash.mjs +18 -0
- package/dist/react/portal/vipps-flash.mjs.map +1 -0
- package/dist/react/portal/wired.mjs +202 -0
- package/dist/react/portal/wired.mjs.map +1 -0
- package/dist/react/screen-labels.mjs +14 -0
- package/dist/react/screen-labels.mjs.map +1 -0
- package/dist/react/shared.d.mts +13 -0
- package/dist/react/shared.mjs +7 -0
- package/dist/react/use-next-free.d.mts +19 -0
- package/dist/react/use-next-free.mjs +56 -0
- package/dist/react/use-next-free.mjs.map +1 -0
- package/dist/react/useBooking.d.mts +179 -0
- package/dist/react/useBooking.mjs +1331 -0
- package/dist/react/useBooking.mjs.map +1 -0
- package/dist/react/wizard/adapters.mjs +189 -0
- package/dist/react/wizard/adapters.mjs.map +1 -0
- package/dist/react/wizard/index.d.mts +11 -0
- package/dist/react/wizard/index.mjs +6 -0
- package/dist/react/wizard-error.mjs +29 -0
- package/dist/react/wizard-error.mjs.map +1 -0
- package/package.json +126 -5
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { labelText } from "../../core/labels.mjs";
|
|
3
|
+
import { readAction } from "../actions.mjs";
|
|
4
|
+
import { screenLabels } from "../screen-labels.mjs";
|
|
5
|
+
import { agePromptCookie } from "./age-prompt.mjs";
|
|
6
|
+
import { bookAgainHref } from "./book-again.mjs";
|
|
7
|
+
import { leavePortal } from "./leave.mjs";
|
|
8
|
+
import { vippsFlashPath } from "./vipps-flash.mjs";
|
|
9
|
+
import { ChildCards, DataControls, FamilyEditor, LogoutButton, ProfileForm, RebookCards, VippsLinkRow } from "@medalsocial/meda/booking";
|
|
10
|
+
import { useRouter } from "next/navigation";
|
|
11
|
+
import { jsx } from "react/jsx-runtime";
|
|
12
|
+
//#region src/react/portal/wired.tsx
|
|
13
|
+
/**
|
|
14
|
+
* The portal's meda screens, each wired to the app's server actions with the
|
|
15
|
+
* behaviour the screens leave to their caller: reading an action's answer
|
|
16
|
+
* (plain or next-safe-action's envelope), sending a dead session to the
|
|
17
|
+
* login, leaving with one document load, the age cookie, «book again» links
|
|
18
|
+
* and the Vipps flash. Imported only by the client dashboard.
|
|
19
|
+
*/
|
|
20
|
+
/** An action's answer as a meda callback's: `ok(value)`, or the failure. */
|
|
21
|
+
function settleAction(answer, booking, unreachable, ok) {
|
|
22
|
+
const read = readAction(answer, {
|
|
23
|
+
unreachable,
|
|
24
|
+
invalidInput: labelText(booking.kit.labels["portal.invalidInput"])
|
|
25
|
+
});
|
|
26
|
+
if (read.ok) return ok(read.value);
|
|
27
|
+
return read.failure.kind === "session" ? {
|
|
28
|
+
ok: false,
|
|
29
|
+
kind: "session"
|
|
30
|
+
} : {
|
|
31
|
+
ok: false,
|
|
32
|
+
kind: "error",
|
|
33
|
+
message: read.failure.message
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** A dead session: back to the login, as a router navigation (the source's `router.push`). */
|
|
37
|
+
function useToLogin(booking) {
|
|
38
|
+
const router = useRouter();
|
|
39
|
+
return () => router.push(booking.kit.config.paths.portalLogin);
|
|
40
|
+
}
|
|
41
|
+
/** Whether next-safe-action pinned the rejection on `phone` — the one field a visitor can get wrong. */
|
|
42
|
+
function rejectedPhone(answer) {
|
|
43
|
+
const errors = answer.validationErrors;
|
|
44
|
+
return typeof errors === "object" && errors !== null && "phone" in errors;
|
|
45
|
+
}
|
|
46
|
+
function PortalProfileForm({ booking, profile, actions }) {
|
|
47
|
+
const { labels } = booking.kit;
|
|
48
|
+
const unreachable = labelText(labels["profileForm.unreachable"]);
|
|
49
|
+
return /* @__PURE__ */ jsx(ProfileForm, {
|
|
50
|
+
labels: screenLabels(labels),
|
|
51
|
+
profile,
|
|
52
|
+
classNames: booking.classNames.profileForm,
|
|
53
|
+
onSessionExpired: useToLogin(booking),
|
|
54
|
+
onSave: async ({ firstName, lastName, phone }) => {
|
|
55
|
+
const answer = await actions.updateProfile({
|
|
56
|
+
...firstName ? { first_name: firstName } : {},
|
|
57
|
+
...lastName ? { last_name: lastName } : {},
|
|
58
|
+
phone
|
|
59
|
+
});
|
|
60
|
+
const result = settleAction(answer, booking, unreachable, (value) => value);
|
|
61
|
+
return result.ok || result.kind === "session" || !rejectedPhone(answer) ? result : {
|
|
62
|
+
...result,
|
|
63
|
+
field: "phone"
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
onConsentChange: async (granted) => {
|
|
67
|
+
const result = settleAction(await actions.setMarketingConsent({ granted }), booking, unreachable, (value) => value);
|
|
68
|
+
return result.ok || result.kind === "session" ? result : {
|
|
69
|
+
ok: false,
|
|
70
|
+
kind: "error"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Which child, in the action's wire shape. */
|
|
76
|
+
function wireTarget(target) {
|
|
77
|
+
return {
|
|
78
|
+
person_id: target.personId,
|
|
79
|
+
...target.index === void 0 ? {} : { index: target.index }
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function PortalFamilyEditor({ booking, family, nouns, personDetails, stylists, dismissed, cookieName, currentYear, now, actions }) {
|
|
83
|
+
const { kit } = booking;
|
|
84
|
+
const settled = (answer) => settleAction(answer, booking, labelText(kit.labels["familyEditor.unreachable"]), (value) => ({
|
|
85
|
+
ok: true,
|
|
86
|
+
family: value.profile.family,
|
|
87
|
+
personId: value.personId,
|
|
88
|
+
fallback: value.fallback
|
|
89
|
+
}));
|
|
90
|
+
return /* @__PURE__ */ jsx(FamilyEditor, {
|
|
91
|
+
labels: screenLabels(kit.labels),
|
|
92
|
+
format: kit.format,
|
|
93
|
+
family,
|
|
94
|
+
nouns,
|
|
95
|
+
personDetails,
|
|
96
|
+
stylists,
|
|
97
|
+
currentYear,
|
|
98
|
+
now,
|
|
99
|
+
agePrompt: dismissed && { dismissed },
|
|
100
|
+
classNames: booking.classNames.familyEditor,
|
|
101
|
+
components: booking.components,
|
|
102
|
+
onSessionExpired: useToLogin(booking),
|
|
103
|
+
onAgeAnswered: (ids) => {
|
|
104
|
+
document.cookie = agePromptCookie(cookieName, kit.config.paths.portal ?? "/", ids);
|
|
105
|
+
},
|
|
106
|
+
onSavePerson: async ({ create, target, person }) => settled(await actions.savePerson({
|
|
107
|
+
create,
|
|
108
|
+
...wireTarget(target),
|
|
109
|
+
person: {
|
|
110
|
+
name: person.name,
|
|
111
|
+
birth_year: person.birthYear,
|
|
112
|
+
...person.birthMonth === void 0 ? {} : { birth_month: person.birthMonth },
|
|
113
|
+
...person.notes === void 0 ? {} : { notes: person.notes },
|
|
114
|
+
...person.preferredResourceId === void 0 ? {} : { preferred_resource_id: person.preferredResourceId }
|
|
115
|
+
}
|
|
116
|
+
})),
|
|
117
|
+
onRemovePerson: async (target) => settled(await actions.removePerson(wireTarget(target)))
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function PortalDataControls({ booking, actions, deletedHref }) {
|
|
121
|
+
const unreachable = labelText(booking.kit.labels["dataControls.unreachable"]);
|
|
122
|
+
return /* @__PURE__ */ jsx(DataControls, {
|
|
123
|
+
labels: screenLabels(booking.kit.labels),
|
|
124
|
+
classNames: booking.classNames.dataControls,
|
|
125
|
+
onSessionExpired: useToLogin(booking),
|
|
126
|
+
onExport: async () => settleAction(await actions.exportData(), booking, unreachable, (v) => v),
|
|
127
|
+
onDelete: async (confirm) => settleAction(await actions.deleteMe({ confirm }), booking, unreachable, (value) => {
|
|
128
|
+
leavePortal(deletedHref, { replace: true });
|
|
129
|
+
return value;
|
|
130
|
+
})
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function PortalLogoutButton({ booking, actions, logoutHref }) {
|
|
134
|
+
const { labels } = booking.kit;
|
|
135
|
+
return /* @__PURE__ */ jsx(LogoutButton, {
|
|
136
|
+
labels: screenLabels(labels),
|
|
137
|
+
classNames: booking.classNames.logout,
|
|
138
|
+
onLogout: async () => {
|
|
139
|
+
if (!readAction(await actions.logout(), {
|
|
140
|
+
unreachable: "",
|
|
141
|
+
invalidInput: ""
|
|
142
|
+
}).ok) return {
|
|
143
|
+
ok: false,
|
|
144
|
+
message: labelText(labels["logout.unreachable"])
|
|
145
|
+
};
|
|
146
|
+
leavePortal(logoutHref);
|
|
147
|
+
return { ok: true };
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function PortalVippsLinkRow({ booking, linked, flash, startVippsLink, onToast }) {
|
|
152
|
+
const { kit } = booking;
|
|
153
|
+
return /* @__PURE__ */ jsx(VippsLinkRow, {
|
|
154
|
+
labels: screenLabels(kit.labels),
|
|
155
|
+
linked,
|
|
156
|
+
flash,
|
|
157
|
+
announceSuccess: !onToast,
|
|
158
|
+
classNames: booking.classNames.vippsLink,
|
|
159
|
+
onStart: () => startVippsLink(null),
|
|
160
|
+
onSessionExpired: useToLogin(booking),
|
|
161
|
+
onFlash: (spent) => {
|
|
162
|
+
if (spent === "linked") onToast?.(labelText(kit.labels["vippsLink.success"]));
|
|
163
|
+
fetch(vippsFlashPath(kit.config), {
|
|
164
|
+
method: "DELETE",
|
|
165
|
+
keepalive: true
|
|
166
|
+
}).catch(() => void 0);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function PortalChildCards({ booking, kids, bookingHref, variant, empty, stylistNames }) {
|
|
171
|
+
const { kit } = booking;
|
|
172
|
+
return /* @__PURE__ */ jsx(ChildCards, {
|
|
173
|
+
kids,
|
|
174
|
+
variant,
|
|
175
|
+
empty,
|
|
176
|
+
stylistNames,
|
|
177
|
+
labels: screenLabels(kit.labels),
|
|
178
|
+
format: kit.format,
|
|
179
|
+
classNames: booking.classNames.childCards,
|
|
180
|
+
components: booking.components,
|
|
181
|
+
hrefFor: (child) => bookAgainHref(kit.config, bookingHref ?? kit.config.paths.booking, {
|
|
182
|
+
serviceId: child.serviceId,
|
|
183
|
+
resourceId: child.preferredResourceId ?? child.resourceId
|
|
184
|
+
}),
|
|
185
|
+
onBook: (child) => kit.rebook.stashRebookWho(child.name)
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function PortalRebookCards({ booking, suggestions, bookingHref }) {
|
|
189
|
+
const { kit } = booking;
|
|
190
|
+
return /* @__PURE__ */ jsx(RebookCards, {
|
|
191
|
+
suggestions,
|
|
192
|
+
labels: screenLabels(kit.labels),
|
|
193
|
+
classNames: booking.classNames.rebook,
|
|
194
|
+
components: booking.components,
|
|
195
|
+
hrefFor: (suggestion) => bookAgainHref(kit.config, bookingHref ?? kit.config.paths.booking, suggestion),
|
|
196
|
+
onRebook: (suggestion) => kit.rebook.stashRebookWho(suggestion.bookedForName)
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
export { PortalChildCards, PortalDataControls, PortalFamilyEditor, PortalLogoutButton, PortalProfileForm, PortalRebookCards, PortalVippsLinkRow };
|
|
201
|
+
|
|
202
|
+
//# sourceMappingURL=wired.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wired.mjs","names":[],"sources":["../../../src/react/portal/wired.tsx"],"sourcesContent":["/**\n * The portal's meda screens, each wired to the app's server actions with the\n * behaviour the screens leave to their caller: reading an action's answer\n * (plain or next-safe-action's envelope), sending a dead session to the\n * login, leaving with one document load, the age cookie, «book again» links\n * and the Vipps flash. Imported only by the client dashboard.\n */\n\nimport {\n ChildCards,\n type ChildCardsVariant,\n type ChildSummary,\n DataControls,\n FamilyEditor,\n LogoutButton,\n type PersonSaveResult,\n type PersonTarget,\n type PortalActionFailure,\n type PortalFamilyMemberDto,\n type PortalPersonNouns,\n type PortalProfileDto,\n ProfileForm,\n RebookCards,\n type RebookSuggestion,\n type VippsLinkFlash,\n VippsLinkRow,\n} from '@medalsocial/meda/booking';\nimport { useRouter } from 'next/navigation';\nimport type { ReactNode } from 'react';\nimport { labelText } from '../../core/labels';\nimport {\n type ActionAnswer,\n type PersonActionResult,\n type PortalActions,\n readAction,\n} from '../actions';\nimport type { ResolvedBooking } from '../Provider';\nimport { screenLabels } from '../screen-labels';\nimport { agePromptCookie } from './age-prompt';\nimport { bookAgainHref } from './book-again';\nimport { leavePortal } from './leave';\nimport { vippsFlashPath } from './vipps-flash';\n\ninterface Wired {\n booking: ResolvedBooking;\n}\n\ntype Ok<T> = Extract<T, { ok: true }>;\n\n/** An action's answer as a meda callback's: `ok(value)`, or the failure. */\nfunction settleAction<T extends object, R>(\n answer: ActionAnswer<T> | undefined,\n booking: ResolvedBooking,\n unreachable: string,\n ok: (value: Ok<T>) => R\n): R | PortalActionFailure {\n const read = readAction(answer, {\n unreachable,\n invalidInput: labelText(booking.kit.labels['portal.invalidInput']),\n });\n if (read.ok) return ok(read.value as Ok<T>);\n return read.failure.kind === 'session'\n ? { ok: false, kind: 'session' }\n : { ok: false, kind: 'error', message: read.failure.message };\n}\n\n/** A dead session: back to the login, as a router navigation (the source's `router.push`). */\nfunction useToLogin(booking: ResolvedBooking): () => void {\n const router = useRouter();\n return () => router.push(booking.kit.config.paths.portalLogin);\n}\n\n/** Whether next-safe-action pinned the rejection on `phone` — the one field a visitor can get wrong. */\nfunction rejectedPhone(answer: unknown): boolean {\n const errors = (answer as { validationErrors?: unknown }).validationErrors;\n return typeof errors === 'object' && errors !== null && 'phone' in errors;\n}\n\nexport function PortalProfileForm({\n booking,\n profile,\n actions,\n}: Wired & {\n profile: PortalProfileDto;\n actions: Pick<PortalActions, 'updateProfile' | 'setMarketingConsent'>;\n}) {\n const { labels } = booking.kit;\n const unreachable = labelText(labels['profileForm.unreachable']);\n return (\n <ProfileForm\n labels={screenLabels(labels)}\n profile={profile}\n classNames={booking.classNames.profileForm}\n onSessionExpired={useToLogin(booking)}\n onSave={async ({ firstName, lastName, phone }) => {\n const answer = await actions.updateProfile({\n ...(firstName ? { first_name: firstName } : {}),\n ...(lastName ? { last_name: lastName } : {}),\n phone,\n });\n const result = settleAction(answer, booking, unreachable, (value) => value);\n return result.ok || result.kind === 'session' || !rejectedPhone(answer)\n ? result\n : { ...result, field: 'phone' as const };\n }}\n onConsentChange={async (granted) => {\n const result = settleAction(\n await actions.setMarketingConsent({ granted }),\n booking,\n unreachable,\n (value) => value\n );\n // Whatever else went wrong, the box says «could not reach» (as the source did).\n return result.ok || result.kind === 'session' ? result : { ok: false, kind: 'error' };\n }}\n />\n );\n}\n\n/** Which child, in the action's wire shape. */\nfunction wireTarget(target: PersonTarget) {\n return {\n person_id: target.personId,\n ...(target.index === undefined ? {} : { index: target.index }),\n };\n}\n\nexport function PortalFamilyEditor({\n booking,\n family,\n nouns,\n personDetails,\n stylists,\n dismissed,\n cookieName,\n currentYear,\n now,\n actions,\n}: Wired & {\n family: PortalFamilyMemberDto[];\n nouns?: PortalPersonNouns;\n personDetails: boolean;\n stylists: ReadonlyArray<{ id: string; name: string }>;\n /** The person ids the age card has already been answered for (the cookie). Absent: no card. */\n dismissed?: readonly string[];\n cookieName: string;\n currentYear?: number;\n now?: number;\n actions: Pick<PortalActions, 'savePerson' | 'removePerson'>;\n}) {\n const { kit } = booking;\n const settled = (answer: ActionAnswer<PersonActionResult>): PersonSaveResult =>\n settleAction(answer, booking, labelText(kit.labels['familyEditor.unreachable']), (value) => ({\n ok: true,\n family: value.profile.family,\n personId: value.personId,\n fallback: value.fallback,\n }));\n return (\n <FamilyEditor\n labels={screenLabels(kit.labels)}\n format={kit.format}\n family={family}\n nouns={nouns}\n personDetails={personDetails}\n stylists={stylists}\n currentYear={currentYear}\n now={now}\n agePrompt={dismissed && { dismissed }}\n classNames={booking.classNames.familyEditor}\n components={booking.components}\n onSessionExpired={useToLogin(booking)}\n onAgeAnswered={(ids) => {\n // biome-ignore lint/suspicious/noDocumentCookie: a one-line, script-readable preference cookie; see `age-prompt.ts`.\n document.cookie = agePromptCookie(cookieName, kit.config.paths.portal ?? '/', ids);\n }}\n onSavePerson={async ({ create, target, person }) =>\n settled(\n await actions.savePerson({\n create,\n ...wireTarget(target),\n person: {\n name: person.name,\n birth_year: person.birthYear,\n ...(person.birthMonth === undefined ? {} : { birth_month: person.birthMonth }),\n ...(person.notes === undefined ? {} : { notes: person.notes }),\n ...(person.preferredResourceId === undefined\n ? {}\n : { preferred_resource_id: person.preferredResourceId }),\n },\n })\n )\n }\n onRemovePerson={async (target) => settled(await actions.removePerson(wireTarget(target)))}\n />\n );\n}\n\nexport function PortalDataControls({\n booking,\n actions,\n deletedHref,\n}: Wired & {\n actions: Pick<PortalActions, 'exportData' | 'deleteMe'>;\n /** Where a deleted visitor lands (one document load, replacing this entry). */\n deletedHref: string;\n}) {\n const unreachable = labelText(booking.kit.labels['dataControls.unreachable']);\n return (\n <DataControls\n labels={screenLabels(booking.kit.labels)}\n classNames={booking.classNames.dataControls}\n onSessionExpired={useToLogin(booking)}\n onExport={async () =>\n settleAction(await actions.exportData(), booking, unreachable, (v) => v)\n }\n onDelete={async (confirm) =>\n settleAction(await actions.deleteMe({ confirm }), booking, unreachable, (value) => {\n leavePortal(deletedHref, { replace: true });\n return value;\n })\n }\n />\n );\n}\n\nexport function PortalLogoutButton({\n booking,\n actions,\n logoutHref,\n}: Wired & {\n actions: Pick<PortalActions, 'logout'>;\n /** Where a logged-out visitor lands (one document load). */\n logoutHref: string;\n}) {\n const { labels } = booking.kit;\n return (\n <LogoutButton\n labels={screenLabels(labels)}\n classNames={booking.classNames.logout}\n onLogout={async () => {\n // Only an `ok` leaves: navigating away on a failure would SAY logged\n // out while the session may still be live.\n if (!readAction(await actions.logout(), { unreachable: '', invalidInput: '' }).ok) {\n return { ok: false, message: labelText(labels['logout.unreachable']) };\n }\n leavePortal(logoutHref);\n return { ok: true };\n }}\n />\n );\n}\n\nexport function PortalVippsLinkRow({\n booking,\n linked,\n flash,\n startVippsLink,\n onToast,\n}: Wired & {\n linked: boolean;\n flash: VippsLinkFlash | null;\n startVippsLink: NonNullable<PortalActions['startVippsLink']>;\n /** Shows the «linked» toast; without it the row says so inline. */\n onToast?: (message: string) => void;\n}) {\n const { kit } = booking;\n return (\n <VippsLinkRow\n labels={screenLabels(kit.labels)}\n linked={linked}\n flash={flash}\n announceSuccess={!onToast}\n classNames={booking.classNames.vippsLink}\n onStart={() => startVippsLink(null)}\n onSessionExpired={useToLogin(booking)}\n onFlash={(spent) => {\n if (spent === 'linked') onToast?.(labelText(kit.labels['vippsLink.success']));\n // Spend the flash. Best effort: it lapses on its own regardless.\n void fetch(vippsFlashPath(kit.config), { method: 'DELETE', keepalive: true }).catch(\n () => undefined\n );\n }}\n />\n );\n}\n\nexport function PortalChildCards({\n booking,\n kids,\n bookingHref,\n variant,\n empty,\n stylistNames,\n}: Wired & {\n kids: ChildSummary[];\n bookingHref?: string;\n variant?: ChildCardsVariant;\n empty?: ReactNode;\n stylistNames?: Readonly<Record<string, string>>;\n}) {\n const { kit } = booking;\n return (\n <ChildCards\n kids={kids}\n variant={variant}\n empty={empty}\n stylistNames={stylistNames}\n labels={screenLabels(kit.labels)}\n format={kit.format}\n classNames={booking.classNames.childCards}\n components={booking.components}\n // The last visit, with the child's usual stylist where they have one.\n hrefFor={(child) =>\n bookAgainHref(kit.config, bookingHref ?? kit.config.paths.booking, {\n serviceId: child.serviceId,\n resourceId: child.preferredResourceId ?? child.resourceId,\n })\n }\n // The name goes through the tab's session store, never the URL.\n onBook={(child) => kit.rebook.stashRebookWho(child.name)}\n />\n );\n}\n\nexport function PortalRebookCards({\n booking,\n suggestions,\n bookingHref,\n}: Wired & { suggestions: RebookSuggestion[]; bookingHref?: string }) {\n const { kit } = booking;\n return (\n <RebookCards\n suggestions={suggestions}\n labels={screenLabels(kit.labels)}\n classNames={booking.classNames.rebook}\n components={booking.components}\n hrefFor={(suggestion) =>\n bookAgainHref(kit.config, bookingHref ?? kit.config.paths.booking, suggestion)\n }\n // Every tap REPLACES the stash — `null` clears it.\n onRebook={(suggestion) => kit.rebook.stashRebookWho(suggestion.bookedForName)}\n />\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
//#region src/react/screen-labels.ts
|
|
3
|
+
/**
|
|
4
|
+
* A pack as meda's screens type their `labels` prop: the same object. meda
|
|
5
|
+
* 3.4 takes a string or an array for every key; this only narrows the type
|
|
6
|
+
* for a meda whose types still read `string`.
|
|
7
|
+
*/
|
|
8
|
+
function screenLabels(labels) {
|
|
9
|
+
return labels;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { screenLabels };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=screen-labels.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screen-labels.mjs","names":[],"sources":["../../src/react/screen-labels.ts"],"sourcesContent":["import type { BookingLabels as MedaBookingLabels } from '@medalsocial/meda/booking';\nimport type { BookingLabels } from './labels';\n\n/**\n * A pack as meda's screens type their `labels` prop: the same object. meda\n * 3.4 takes a string or an array for every key; this only narrows the type\n * for a meda whose types still read `string`.\n */\nexport function screenLabels(labels: Readonly<BookingLabels>): MedaBookingLabels {\n return labels as unknown as MedaBookingLabels;\n}\n"],"mappings":";;;;;;;AAQA,SAAgB,aAAa,QAAoD;CAC/E,OAAO;AACT"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BookingLabel } from "../core/labels.mjs";
|
|
2
|
+
import { LoginLabels } from "./labels/login.mjs";
|
|
3
|
+
import { ManageLabels } from "./labels/manage.mjs";
|
|
4
|
+
import { PortalLabels } from "./labels/portal.mjs";
|
|
5
|
+
import { ScreenLabels } from "./labels/screens.mjs";
|
|
6
|
+
import { WizardLabels } from "./labels/wizard.mjs";
|
|
7
|
+
import { BOOKING_LABELS, BookingLabels, BookingLabelsInput, mergeLabels } from "./labels/index.mjs";
|
|
8
|
+
import { agePromptCookieName, parseAgePromptDismissed } from "./portal/age-prompt.mjs";
|
|
9
|
+
import { bookAgainHref } from "./portal/book-again.mjs";
|
|
10
|
+
import { createChildSummaries } from "./portal/child-summary.mjs";
|
|
11
|
+
import { DEFAULT_PORTAL_TAB, DEFAULT_PORTAL_TAB_PARAM, DEFAULT_PORTAL_TAB_SLUGS, PORTAL_TABS, PortalTab, parsePortalTab } from "./portal/tabs.mjs";
|
|
12
|
+
import { VIPPS_LINK_FLASHES, VippsLinkFlash, vippsFlashPath, vippsLinkFlash } from "./portal/vipps-flash.mjs";
|
|
13
|
+
export { BOOKING_LABELS, type BookingLabel, BookingLabels, BookingLabelsInput, DEFAULT_PORTAL_TAB, DEFAULT_PORTAL_TAB_PARAM, DEFAULT_PORTAL_TAB_SLUGS, type LoginLabels, type ManageLabels, PORTAL_TABS, type PortalLabels, type PortalTab, type ScreenLabels, VIPPS_LINK_FLASHES, type VippsLinkFlash, type WizardLabels, agePromptCookieName, bookAgainHref, createChildSummaries, mergeLabels, parseAgePromptDismissed, parsePortalTab, vippsFlashPath, vippsLinkFlash };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { agePromptCookieName, parseAgePromptDismissed } from "./portal/age-prompt.mjs";
|
|
2
|
+
import { bookAgainHref } from "./portal/book-again.mjs";
|
|
3
|
+
import { createChildSummaries } from "./portal/child-summary.mjs";
|
|
4
|
+
import { DEFAULT_PORTAL_TAB, DEFAULT_PORTAL_TAB_PARAM, DEFAULT_PORTAL_TAB_SLUGS, PORTAL_TABS, parsePortalTab } from "./portal/tabs.mjs";
|
|
5
|
+
import { VIPPS_LINK_FLASHES, vippsFlashPath, vippsLinkFlash } from "./portal/vipps-flash.mjs";
|
|
6
|
+
import { BOOKING_LABELS, mergeLabels } from "./labels/index.mjs";
|
|
7
|
+
export { BOOKING_LABELS, DEFAULT_PORTAL_TAB, DEFAULT_PORTAL_TAB_PARAM, DEFAULT_PORTAL_TAB_SLUGS, PORTAL_TABS, VIPPS_LINK_FLASHES, agePromptCookieName, bookAgainHref, createChildSummaries, mergeLabels, parseAgePromptDismissed, parsePortalTab, vippsFlashPath, vippsLinkFlash };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BookingConfig } from "../core/config.mjs";
|
|
2
|
+
import { NextFree, NextFreeClient } from "../core/next-free.mjs";
|
|
3
|
+
//#region src/react/use-next-free.d.ts
|
|
4
|
+
/** The shared `createNextFree` client for this config's `paths`. */
|
|
5
|
+
export declare function nextFreeClient(config: Pick<BookingConfig, 'paths'>): NextFreeClient;
|
|
6
|
+
/**
|
|
7
|
+
* The business's next free time, fetched after the page is idle — never in
|
|
8
|
+
* the way of the LCP or the first tap — or `null` until then (and while
|
|
9
|
+
* nothing is free). Re-asked when the answer's minute is up or the quoted time
|
|
10
|
+
* starts, so a visitor who lingers never reads an hour that has passed.
|
|
11
|
+
* `enabled: false` asks nothing: a site whose bookings go to another provider
|
|
12
|
+
* must not quote Medal's diary.
|
|
13
|
+
*
|
|
14
|
+
* `config` is the site's, or the nearest `BookingProvider`'s.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useNextFree(enabled: boolean, config?: Readonly<BookingConfig>): NextFree | null;
|
|
17
|
+
//#endregion
|
|
18
|
+
export type { NextFree };
|
|
19
|
+
//# sourceMappingURL=use-next-free.d.mts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { NEXT_FREE_TTL_MS, createNextFree } from "../core/next-free.mjs";
|
|
3
|
+
import { useBookingKit } from "./Provider.mjs";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
//#region src/react/use-next-free.ts
|
|
6
|
+
/** One client per site (keyed on its `paths`), so every caller shares one answer. */
|
|
7
|
+
const CLIENTS = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
/** The shared `createNextFree` client for this config's `paths`. */
|
|
9
|
+
function nextFreeClient(config) {
|
|
10
|
+
let client = CLIENTS.get(config.paths);
|
|
11
|
+
if (client === void 0) {
|
|
12
|
+
client = createNextFree(config);
|
|
13
|
+
CLIENTS.set(config.paths, client);
|
|
14
|
+
}
|
|
15
|
+
return client;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The business's next free time, fetched after the page is idle — never in
|
|
19
|
+
* the way of the LCP or the first tap — or `null` until then (and while
|
|
20
|
+
* nothing is free). Re-asked when the answer's minute is up or the quoted time
|
|
21
|
+
* starts, so a visitor who lingers never reads an hour that has passed.
|
|
22
|
+
* `enabled: false` asks nothing: a site whose bookings go to another provider
|
|
23
|
+
* must not quote Medal's diary.
|
|
24
|
+
*
|
|
25
|
+
* `config` is the site's, or the nearest `BookingProvider`'s.
|
|
26
|
+
*/
|
|
27
|
+
function useNextFree(enabled, config) {
|
|
28
|
+
const client = nextFreeClient(useBookingKit({ config }).kit.config);
|
|
29
|
+
const [nextFree, setNextFree] = useState(null);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!enabled) return;
|
|
32
|
+
let alive = true;
|
|
33
|
+
let refresh = 0;
|
|
34
|
+
const run = () => {
|
|
35
|
+
client.fetchNextFree().then((answer) => {
|
|
36
|
+
if (!alive) return;
|
|
37
|
+
setNextFree(answer !== null && answer.startTs > Date.now() ? answer : null);
|
|
38
|
+
const until = answer === null ? NEXT_FREE_TTL_MS : Math.min(NEXT_FREE_TTL_MS, answer.startTs - Date.now());
|
|
39
|
+
refresh = window.setTimeout(run, Math.max(until, 1e3));
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
const { requestIdleCallback: idle, cancelIdleCallback: cancelIdle } = window;
|
|
43
|
+
const handle = idle ? idle(run) : window.setTimeout(run, 1500);
|
|
44
|
+
return () => {
|
|
45
|
+
alive = false;
|
|
46
|
+
window.clearTimeout(refresh);
|
|
47
|
+
if (idle && cancelIdle) cancelIdle(handle);
|
|
48
|
+
else window.clearTimeout(handle);
|
|
49
|
+
};
|
|
50
|
+
}, [enabled, client]);
|
|
51
|
+
return nextFree;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { nextFreeClient, useNextFree };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=use-next-free.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-next-free.mjs","names":[],"sources":["../../src/react/use-next-free.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport type { BookingConfig } from '../core/config';\nimport {\n createNextFree,\n NEXT_FREE_TTL_MS,\n type NextFree,\n type NextFreeClient,\n} from '../core/next-free';\nimport { useBookingKit } from './Provider';\n\nexport type { NextFree };\n\n/** One client per site (keyed on its `paths`), so every caller shares one answer. */\nconst CLIENTS = new WeakMap<object, NextFreeClient>();\n\n/** The shared `createNextFree` client for this config's `paths`. */\nexport function nextFreeClient(config: Pick<BookingConfig, 'paths'>): NextFreeClient {\n let client = CLIENTS.get(config.paths);\n if (client === undefined) {\n client = createNextFree(config);\n CLIENTS.set(config.paths, client);\n }\n return client;\n}\n\ntype IdleWindow = {\n requestIdleCallback?: (callback: () => void) => number;\n cancelIdleCallback?: (handle: number) => void;\n};\n\n/**\n * The business's next free time, fetched after the page is idle — never in\n * the way of the LCP or the first tap — or `null` until then (and while\n * nothing is free). Re-asked when the answer's minute is up or the quoted time\n * starts, so a visitor who lingers never reads an hour that has passed.\n * `enabled: false` asks nothing: a site whose bookings go to another provider\n * must not quote Medal's diary.\n *\n * `config` is the site's, or the nearest `BookingProvider`'s.\n */\nexport function useNextFree(enabled: boolean, config?: Readonly<BookingConfig>): NextFree | null {\n const client = nextFreeClient(useBookingKit({ config }).kit.config);\n const [nextFree, setNextFree] = useState<NextFree | null>(null);\n\n useEffect(() => {\n if (!enabled) return;\n let alive = true;\n let refresh = 0;\n\n const run = () => {\n void client.fetchNextFree().then((answer) => {\n if (!alive) return;\n // A quoted time that has started is not «next free» any more.\n setNextFree(answer !== null && answer.startTs > Date.now() ? answer : null);\n const until =\n answer === null\n ? NEXT_FREE_TTL_MS\n : Math.min(NEXT_FREE_TTL_MS, answer.startTs - Date.now());\n refresh = window.setTimeout(run, Math.max(until, 1_000));\n });\n };\n\n const { requestIdleCallback: idle, cancelIdleCallback: cancelIdle } = window as IdleWindow;\n const handle = idle ? idle(run) : window.setTimeout(run, 1_500);\n return () => {\n alive = false;\n window.clearTimeout(refresh);\n if (idle && cancelIdle) cancelIdle(handle);\n else window.clearTimeout(handle);\n };\n }, [enabled, client]);\n\n return nextFree;\n}\n"],"mappings":";;;;;;AAaA,MAAM,0BAAU,IAAI,QAAgC;;AAGpD,SAAgB,eAAe,QAAsD;CACnF,IAAI,SAAS,QAAQ,IAAI,OAAO,KAAK;CACrC,IAAI,WAAW,KAAA,GAAW;EACxB,SAAS,eAAe,MAAM;EAC9B,QAAQ,IAAI,OAAO,OAAO,MAAM;CAClC;CACA,OAAO;AACT;;;;;;;;;;;AAiBA,SAAgB,YAAY,SAAkB,QAAmD;CAC/F,MAAM,SAAS,eAAe,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,MAAM;CAClE,MAAM,CAAC,UAAU,eAAe,SAA0B,IAAI;CAE9D,gBAAgB;EACd,IAAI,CAAC,SAAS;EACd,IAAI,QAAQ;EACZ,IAAI,UAAU;EAEd,MAAM,YAAY;GAChB,OAAY,cAAc,CAAC,CAAC,MAAM,WAAW;IAC3C,IAAI,CAAC,OAAO;IAEZ,YAAY,WAAW,QAAQ,OAAO,UAAU,KAAK,IAAI,IAAI,SAAS,IAAI;IAC1E,MAAM,QACJ,WAAW,OACP,mBACA,KAAK,IAAI,kBAAkB,OAAO,UAAU,KAAK,IAAI,CAAC;IAC5D,UAAU,OAAO,WAAW,KAAK,KAAK,IAAI,OAAO,GAAK,CAAC;GACzD,CAAC;EACH;EAEA,MAAM,EAAE,qBAAqB,MAAM,oBAAoB,eAAe;EACtE,MAAM,SAAS,OAAO,KAAK,GAAG,IAAI,OAAO,WAAW,KAAK,IAAK;EAC9D,aAAa;GACX,QAAQ;GACR,OAAO,aAAa,OAAO;GAC3B,IAAI,QAAQ,YAAY,WAAW,MAAM;QACpC,OAAO,aAAa,MAAM;EACjC;CACF,GAAG,CAAC,SAAS,MAAM,CAAC;CAEpB,OAAO;AACT"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { AgeRange } from "../core/age.mjs";
|
|
2
|
+
import { WizardAction, WizardPerson, WizardService, WizardState } from "../core/machine.mjs";
|
|
3
|
+
import { BookingDayDto, BookingFamilyMember, BookingGuardian, BookingResourceDto, BookingServiceDto, BookingSlotDto, BookingSubmission } from "../core/types.mjs";
|
|
4
|
+
import { SubmittedVisit } from "../core/attempt-store.mjs";
|
|
5
|
+
import { PartySlot } from "../core/party-slots.mjs";
|
|
6
|
+
import { BookingKit } from "./kit.mjs";
|
|
7
|
+
import { BookingOverrides } from "./Provider.mjs";
|
|
8
|
+
//#region src/react/useBooking.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* What the booking page prefetched on the server. Structurally the `/next`
|
|
11
|
+
* loader's seed; every part but `services` may be missing, and whatever is
|
|
12
|
+
* missing is fetched on demand into the same place.
|
|
13
|
+
*/
|
|
14
|
+
export interface BookingSeed {
|
|
15
|
+
/** The full catalogue. */
|
|
16
|
+
services: BookingServiceDto[];
|
|
17
|
+
/** The stylists, or `null`/absent when they could not be read. */
|
|
18
|
+
resources?: BookingResourceDto[] | null;
|
|
19
|
+
/** Openings by service id, unfiltered by stylist. */
|
|
20
|
+
slots?: Record<string, BookingSlotDto[]>;
|
|
21
|
+
/** The business's hours over the window, by service id. */
|
|
22
|
+
schedules?: Record<string, BookingDayDto[]>;
|
|
23
|
+
/** «Next available» per stylist, by service id. */
|
|
24
|
+
nextAvailable?: Record<string, Record<string, number>>;
|
|
25
|
+
/** The instant the seed's window starts at; pinned once when absent. */
|
|
26
|
+
fromTs?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface UseBookingOptions extends BookingOverrides {
|
|
29
|
+
seed: BookingSeed;
|
|
30
|
+
/**
|
|
31
|
+
* The business's phone and address for this request, over `config.contact`
|
|
32
|
+
* — what `/next`'s `loadBookingPage` answers as `contact`.
|
|
33
|
+
*/
|
|
34
|
+
contact?: {
|
|
35
|
+
phone?: string | null;
|
|
36
|
+
address?: string | null;
|
|
37
|
+
};
|
|
38
|
+
/** The parent the page found a portal session for; `null` for a guest. */
|
|
39
|
+
guardian?: BookingGuardian | null;
|
|
40
|
+
/** How many days the window covers. Default `config.window.rangeDays`. */
|
|
41
|
+
rangeDays?: number;
|
|
42
|
+
}
|
|
43
|
+
/** A person's «same as last time», and the note when it was swapped for their age. */
|
|
44
|
+
export interface BookingSuggestion {
|
|
45
|
+
service: BookingServiceDto;
|
|
46
|
+
note?: string;
|
|
47
|
+
}
|
|
48
|
+
/** One booked line on the confirmation. */
|
|
49
|
+
export interface ConfirmedLine {
|
|
50
|
+
item: SubmittedVisit['items'][number];
|
|
51
|
+
bookingId: string;
|
|
52
|
+
stylistName: string | null;
|
|
53
|
+
manageHref: string | null;
|
|
54
|
+
}
|
|
55
|
+
/** The confirmation, once a booking came back. */
|
|
56
|
+
export interface BookingConfirmation {
|
|
57
|
+
bookings: Array<{
|
|
58
|
+
id: string;
|
|
59
|
+
manageHref: string | null;
|
|
60
|
+
}>;
|
|
61
|
+
submitted: SubmittedVisit;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Whether a create answer is a whole one: exactly one booking, with its own
|
|
65
|
+
* distinct id, per submitted line. Anything else — none, fewer, more, an id
|
|
66
|
+
* missing or repeated — is not a
|
|
67
|
+
* confirmation; the wizard shows it as the generic create failure
|
|
68
|
+
* (`upstreamError`, «we still do not know»), whose attempt survives for the
|
|
69
|
+
* replay under the same idempotency key.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isCompleteConfirmation(bookings: ReadonlyArray<{
|
|
72
|
+
id?: unknown;
|
|
73
|
+
}> | null | undefined, itemCount: number): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* The basket and the create response, zipped by INDEX — each line its OWN
|
|
76
|
+
* booking. Only a whole answer (`isCompleteConfirmation`) gets this far; a line
|
|
77
|
+
* the answer has no booking for is left out rather than handed a sibling's id,
|
|
78
|
+
* which would give two calendar entries one UID.
|
|
79
|
+
*/
|
|
80
|
+
export declare function confirmationLines(confirmation: BookingConfirmation): ConfirmedLine[];
|
|
81
|
+
/**
|
|
82
|
+
* A saved child as the person the machine seats: by `personId` when Medal has
|
|
83
|
+
* one, else by their PLACE in the profile with name and year beside it, so two
|
|
84
|
+
* «Emma 2018» are two seats and a reordered list matches nobody.
|
|
85
|
+
*/
|
|
86
|
+
export declare function personForChild(child: BookingFamilyMember, index: number): WizardPerson;
|
|
87
|
+
export declare function useBooking(options: UseBookingOptions): {
|
|
88
|
+
kit: BookingKit;
|
|
89
|
+
state: WizardState;
|
|
90
|
+
dispatch: (value: WizardAction) => void;
|
|
91
|
+
/** The parent: who arrived, who logged in from the sheet, and the merged answer. */
|
|
92
|
+
login: {
|
|
93
|
+
arrivedAs: BookingGuardian | null;
|
|
94
|
+
signedIn: {
|
|
95
|
+
guardian: BookingGuardian | null;
|
|
96
|
+
} | null;
|
|
97
|
+
guardian: BookingGuardian | null;
|
|
98
|
+
signIn: (who: BookingGuardian | null) => void;
|
|
99
|
+
vippsConfirm: {
|
|
100
|
+
to: string | null;
|
|
101
|
+
} | null;
|
|
102
|
+
resumePath: string;
|
|
103
|
+
};
|
|
104
|
+
people: {
|
|
105
|
+
family: BookingFamilyMember[] | null;
|
|
106
|
+
ageDayKey: string;
|
|
107
|
+
personAge: (person: WizardPerson, dayKey: string) => AgeRange | null;
|
|
108
|
+
suggestionFor: (person: WizardPerson) => BookingSuggestion | null;
|
|
109
|
+
choosePeople: (people: WizardPerson[], advance: boolean) => void;
|
|
110
|
+
addChild: (child: {
|
|
111
|
+
name: string;
|
|
112
|
+
birthYear: number;
|
|
113
|
+
birthMonth?: number;
|
|
114
|
+
notes?: string;
|
|
115
|
+
}) => Promise<{
|
|
116
|
+
ok: true;
|
|
117
|
+
} | {
|
|
118
|
+
ok: false;
|
|
119
|
+
message: string;
|
|
120
|
+
}>;
|
|
121
|
+
};
|
|
122
|
+
catalogue: {
|
|
123
|
+
services: BookingServiceDto[];
|
|
124
|
+
resources: BookingResourceDto[];
|
|
125
|
+
resourcesKnown: boolean;
|
|
126
|
+
resolveStylistName: (resourceId: string) => string | null;
|
|
127
|
+
chosenStylistName: string | null;
|
|
128
|
+
stylistNotice: string | null;
|
|
129
|
+
deepLinkCategory: string | null;
|
|
130
|
+
};
|
|
131
|
+
slots: {
|
|
132
|
+
fromTs: number;
|
|
133
|
+
toTs: number;
|
|
134
|
+
days: number[];
|
|
135
|
+
single: BookingSlotDto[];
|
|
136
|
+
party: PartySlot[];
|
|
137
|
+
alternatives: PartySlot[];
|
|
138
|
+
failed: boolean;
|
|
139
|
+
ready: boolean;
|
|
140
|
+
takenSlotTs: number | null;
|
|
141
|
+
nextAvailableTs: Record<string, number>;
|
|
142
|
+
nextAvailableLoading: boolean;
|
|
143
|
+
};
|
|
144
|
+
schedule: {
|
|
145
|
+
openDays: BookingDayDto[] | null;
|
|
146
|
+
settled: boolean;
|
|
147
|
+
};
|
|
148
|
+
derived: {
|
|
149
|
+
canAdvance: boolean;
|
|
150
|
+
party: boolean;
|
|
151
|
+
total: number;
|
|
152
|
+
summary: string;
|
|
153
|
+
visitEnd: number | null;
|
|
154
|
+
};
|
|
155
|
+
pickService: (service: BookingServiceDto) => void;
|
|
156
|
+
pickServiceFor: (index: number, service: WizardService) => void;
|
|
157
|
+
toggleServiceFor: (index: number, service: WizardService) => void;
|
|
158
|
+
continueFromService: () => void;
|
|
159
|
+
pickResource: (resourceId: string | null) => void;
|
|
160
|
+
pickSlot: (slot: BookingSlotDto) => void;
|
|
161
|
+
pickPartySlot: (slot: PartySlot) => void;
|
|
162
|
+
next: () => void;
|
|
163
|
+
submit: (submission: BookingSubmission) => Promise<void>;
|
|
164
|
+
submitting: boolean;
|
|
165
|
+
submissionNonce: string | undefined;
|
|
166
|
+
confirmed: BookingConfirmation | null;
|
|
167
|
+
startOver: () => void;
|
|
168
|
+
/** Ask for the openings again after a failure. */
|
|
169
|
+
refresh: () => void;
|
|
170
|
+
restore: {
|
|
171
|
+
hydrated: boolean;
|
|
172
|
+
restoring: boolean;
|
|
173
|
+
linked: boolean;
|
|
174
|
+
resuming: boolean;
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
export type BookingController = ReturnType<typeof useBooking>;
|
|
178
|
+
//#endregion
|
|
179
|
+
//# sourceMappingURL=useBooking.d.mts.map
|