@medalsocial/meda 3.11.0 → 3.13.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.
|
@@ -83,10 +83,36 @@ export interface ConfirmationProps {
|
|
|
83
83
|
/** The visit's total in minor units. */
|
|
84
84
|
totalOre: number;
|
|
85
85
|
address?: string | null;
|
|
86
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* The calendar link: a calendar file the site serves (`text/calendar`, which
|
|
88
|
+
* iOS and macOS open straight into «Add to Calendar»), a calendar's own
|
|
89
|
+
* add-event page (Google Calendar), or a `data:text/calendar` URL. `null`
|
|
90
|
+
* draws no link.
|
|
91
|
+
*/
|
|
87
92
|
calendarHref?: string | null;
|
|
88
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* `download` filename for the calendar link. Only a `data:` or `blob:` URL
|
|
95
|
+
* gets it: browsers refuse to navigate to one, so the attribute is what makes
|
|
96
|
+
* it do anything. On a real URL it would force a download, and an iPhone
|
|
97
|
+
* would save the file to Files instead of opening Calendar.
|
|
98
|
+
*/
|
|
89
99
|
calendarFileName?: string;
|
|
100
|
+
/**
|
|
101
|
+
* The calendar link is another site's page (Google Calendar's add-event
|
|
102
|
+
* form): it opens in a new tab. A calendar file — relative or absolute —
|
|
103
|
+
* opens in place, which is what lets iOS show «Add to Calendar».
|
|
104
|
+
*/
|
|
105
|
+
calendarNewTab?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* A second way into a calendar, drawn as a small link: Google Calendar beside
|
|
108
|
+
* a calendar file, or the file beside Google Calendar. `newTab` as for
|
|
109
|
+
* `calendarNewTab`. `null` draws none.
|
|
110
|
+
*/
|
|
111
|
+
calendarAlt?: {
|
|
112
|
+
href: string;
|
|
113
|
+
label: string;
|
|
114
|
+
newTab?: boolean;
|
|
115
|
+
} | null;
|
|
90
116
|
/** Where all of the visitor's bookings are listed. `null` draws no link. */
|
|
91
117
|
portalHref?: string | null;
|
|
92
118
|
/** A way back to an empty wizard. Absent, the button is not drawn. */
|
|
@@ -109,4 +135,4 @@ export interface ConfirmationProps {
|
|
|
109
135
|
classNames?: SlotClassNames<ConfirmationSlot>;
|
|
110
136
|
components?: ConfirmationComponents;
|
|
111
137
|
}
|
|
112
|
-
export declare function Confirmation({ lines: rawLines, startTs, totalOre, address, calendarHref, calendarFileName, portalHref, onStartOver, now, layout, directionsHref, freeChangeUntilTs, format, labels, classNames, components, }: ConfirmationProps): import("react").JSX.Element;
|
|
138
|
+
export declare function Confirmation({ lines: rawLines, startTs, totalOre, address, calendarHref, calendarFileName, calendarAlt, calendarNewTab, portalHref, onStartOver, now, layout, directionsHref, freeChangeUntilTs, format, labels, classNames, components, }: ConfirmationProps): import("react").JSX.Element;
|
|
@@ -68,10 +68,22 @@ export function DefaultPartyLine({ line, text, manageLabel, manageAriaLabel, lin
|
|
|
68
68
|
return (_jsxs("li", { className: "flex flex-wrap items-baseline gap-x-3", children: [_jsx("span", { className: "tabular-nums", children: text }), line.manageHref && (_jsx("a", { href: line.manageHref, "aria-label": manageAriaLabel, className: linkClassName ?? 'text-sm font-semibold text-primary underline underline-offset-4', children: manageLabel }))] }));
|
|
69
69
|
}
|
|
70
70
|
const SEPARATOR = ' · ';
|
|
71
|
-
|
|
71
|
+
/** `download` only where it is needed (see `calendarFileName`). */
|
|
72
|
+
function downloadFor(href, fileName) {
|
|
73
|
+
return /^(data|blob):/i.test(href) ? fileName : undefined;
|
|
74
|
+
}
|
|
75
|
+
/** A calendar link: the file in place, another calendar's page in a new tab. */
|
|
76
|
+
function calendarLinkProps(href, fileName, newTab = false) {
|
|
77
|
+
return {
|
|
78
|
+
href,
|
|
79
|
+
download: downloadFor(href, fileName),
|
|
80
|
+
...(newTab ? { target: '_blank', rel: 'noopener' } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function Confirmation({ lines: rawLines, startTs, totalOre, address = null, calendarHref = null, calendarFileName = 'booking.ics', calendarAlt = null, calendarNewTab = false, portalHref = null, onStartOver, now = Date.now(), layout = 'card', directionsHref = null, freeChangeUntilTs = null, format, labels, classNames, components, }) {
|
|
72
84
|
const { clock } = format;
|
|
73
85
|
if (layout === 'hero') {
|
|
74
|
-
return (_jsx(HeroConfirmation, { lines: rawLines, startTs: startTs, totalOre: totalOre, address: address, calendarHref: calendarHref, calendarFileName: calendarFileName, portalHref: portalHref, onStartOver: onStartOver, now: now, directionsHref: directionsHref, freeChangeUntilTs: freeChangeUntilTs, format: format, labels: labels, classNames: classNames, components: components }));
|
|
86
|
+
return (_jsx(HeroConfirmation, { lines: rawLines, startTs: startTs, totalOre: totalOre, address: address, calendarHref: calendarHref, calendarFileName: calendarFileName, calendarAlt: calendarAlt, calendarNewTab: calendarNewTab, portalHref: portalHref, onStartOver: onStartOver, now: now, directionsHref: directionsHref, freeChangeUntilTs: freeChangeUntilTs, format: format, labels: labels, classNames: classNames, components: components }));
|
|
75
87
|
}
|
|
76
88
|
// Cleaned here as well as upstream: a restored confirmation carries whatever
|
|
77
89
|
// name was resolved when it was stored.
|
|
@@ -96,7 +108,7 @@ export function Confirmation({ lines: rawLines, startTs, totalOre, address = nul
|
|
|
96
108
|
// This line's own price, which for one person is the whole total.
|
|
97
109
|
format.price(first.priceOre),
|
|
98
110
|
], SEPARATOR) })) : null;
|
|
99
|
-
return (_jsxs("section", { "aria-labelledby": "booking-confirmed-heading", className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h2", { id: "booking-confirmed-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['confirmation.heading'] }), lines.length > 1 ? (_jsx(PartyCard, { lines: lines, totalOre: totalOre, now: now, format: format, labels: labels, classNames: classNames, PartyLine: components?.PartyLine ?? DefaultPartyLine })) : (singleCard), address && (_jsx("p", { className: slotClass(classNames, 'address', 'text-sm text-muted-foreground'), children: address })), _jsxs("div", { className: "flex flex-wrap gap-4", children: [calendarHref && (_jsx("a", {
|
|
111
|
+
return (_jsxs("section", { "aria-labelledby": "booking-confirmed-heading", className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h2", { id: "booking-confirmed-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['confirmation.heading'] }), lines.length > 1 ? (_jsx(PartyCard, { lines: lines, totalOre: totalOre, now: now, format: format, labels: labels, classNames: classNames, PartyLine: components?.PartyLine ?? DefaultPartyLine })) : (singleCard), address && (_jsx("p", { className: slotClass(classNames, 'address', 'text-sm text-muted-foreground'), children: address })), _jsxs("div", { className: "flex flex-wrap gap-4", children: [calendarHref && (_jsx("a", { ...calendarLinkProps(calendarHref, calendarFileName, calendarNewTab), className: linkClass, children: labels['confirmation.calendar'] })), calendarAlt && (_jsx("a", { ...calendarLinkProps(calendarAlt.href, calendarFileName, calendarAlt.newTab), className: linkClass, children: calendarAlt.label })), lines.length === 1 && first?.manageHref && (_jsx("a", { href: first.manageHref, className: linkClass, children: labels['confirmation.manage'] }))] }), portalHref && (_jsx("p", { className: "text-sm", children: _jsx("a", { href: portalHref, className: linkClass, children: labels['confirmation.portal'] }) })), onStartOver && (_jsx("div", { children: _jsx(BookingButton, { variant: "outline", className: slotClass(classNames, 'startOver'), onClick: onStartOver, children: labels['confirmation.startOver'] }) }))] }));
|
|
100
112
|
}
|
|
101
113
|
/**
|
|
102
114
|
* The family card: one line per person in the order they sit down, each with
|
|
@@ -139,7 +151,7 @@ function visitMinutes(item) {
|
|
|
139
151
|
* booked, and the two things a parent does next — put it in the calendar and
|
|
140
152
|
* find the way — as buttons rather than links.
|
|
141
153
|
*/
|
|
142
|
-
function HeroConfirmation({ lines: rawLines, startTs, totalOre, address, calendarHref, calendarFileName, portalHref, onStartOver, now, directionsHref, freeChangeUntilTs, format, labels, classNames, components, }) {
|
|
154
|
+
function HeroConfirmation({ lines: rawLines, startTs, totalOre, address, calendarHref, calendarFileName, calendarAlt, calendarNewTab, portalHref, onStartOver, now, directionsHref, freeChangeUntilTs, format, labels, classNames, components, }) {
|
|
143
155
|
const { clock } = format;
|
|
144
156
|
const lines = rawLines.map((line) => ({
|
|
145
157
|
...line,
|
|
@@ -201,11 +213,11 @@ function HeroConfirmation({ lines: rawLines, startTs, totalOre, address, calenda
|
|
|
201
213
|
? fillLabel(label('confirmation.leafTime'), {
|
|
202
214
|
time: clock.formatTime(startTs),
|
|
203
215
|
})
|
|
204
|
-
: clock.formatTime(startTs) })] }), _jsx("h2", { id: "booking-confirmed-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['confirmation.heading'] }), _jsx("p", { className: "sr-only", children: when }), label('confirmation.sent') && (_jsx("p", { className: "text-sm text-muted-foreground", children: renderLabel(label('confirmation.sent')) }))] }), plainLine, lines.length > 1 && (_jsx(PartyCard, { lines: lines, totalOre: totalOre, now: now, format: format, labels: labels, classNames: classNames, PartyLine: components?.PartyLine ?? DefaultPartyLine })), rows.length > 0 && (_jsx("dl", { className: slotClass(classNames, 'card', 'divide-y divide-border rounded-lg border border-border bg-card px-5'), children: rows.map((row) => (_jsxs("div", { className: "flex items-center gap-4 py-3 text-sm", children: [_jsx("dt", { className: "w-16 shrink-0 text-muted-foreground", children: renderLabel(row.name) }), _jsx("dd", { className: "min-w-0 font-medium", children: row.value })] }, labelText(row.name)))) })), (calendarHref || (directionsHref && label('confirmation.directions'))) && (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [calendarHref && (_jsxs("a", {
|
|
216
|
+
: clock.formatTime(startTs) })] }), _jsx("h2", { id: "booking-confirmed-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['confirmation.heading'] }), _jsx("p", { className: "sr-only", children: when }), label('confirmation.sent') && (_jsx("p", { className: "text-sm text-muted-foreground", children: renderLabel(label('confirmation.sent')) }))] }), plainLine, lines.length > 1 && (_jsx(PartyCard, { lines: lines, totalOre: totalOre, now: now, format: format, labels: labels, classNames: classNames, PartyLine: components?.PartyLine ?? DefaultPartyLine })), rows.length > 0 && (_jsx("dl", { className: slotClass(classNames, 'card', 'divide-y divide-border rounded-lg border border-border bg-card px-5'), children: rows.map((row) => (_jsxs("div", { className: "flex items-center gap-4 py-3 text-sm", children: [_jsx("dt", { className: "w-16 shrink-0 text-muted-foreground", children: renderLabel(row.name) }), _jsx("dd", { className: "min-w-0 font-medium", children: row.value })] }, labelText(row.name)))) })), (calendarHref || (directionsHref && label('confirmation.directions'))) && (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [calendarHref && (_jsxs("a", { ...calendarLinkProps(calendarHref, calendarFileName, calendarNewTab), className: bookingButtonClass({ size: 'lg', className: 'gap-2' }), children: [_jsx(CalendarPlus, { "aria-hidden": "true", className: "size-4" }), labels['confirmation.calendar']] })), directionsHref && label('confirmation.directions') && (_jsxs("a", { href: directionsHref, target: "_blank", rel: "noopener", className: bookingButtonClass({
|
|
205
217
|
variant: 'outline',
|
|
206
218
|
size: 'lg',
|
|
207
219
|
className: cn('gap-2', !calendarHref && 'col-span-2'),
|
|
208
|
-
}), children: [_jsx(MapPin, { "aria-hidden": "true", className: "size-4" }), renderLabel(label('confirmation.directions'))] }))] })), label('confirmation.tip.heading') && label('confirmation.tip.body') && (_jsxs("div", { className: "rounded-lg bg-muted px-5 py-4 text-sm", children: [_jsx("p", { className: "font-semibold", children: renderLabel(label('confirmation.tip.heading')) }), _jsx("p", { className: "text-muted-foreground", children: renderLabel(label('confirmation.tip.body')) })] })), lines.length === 1 && first?.manageHref && (_jsxs("p", { className: "text-center text-sm", children: [_jsx("a", { href: first.manageHref, className: linkClass, children: labels['confirmation.manage'] }), manageUntil && freeChangeUntilTs !== null && freeChangeUntilTs !== undefined && (_jsxs("span", { className: "text-muted-foreground", children: [SEPARATOR, fillLabel(manageUntil, {
|
|
220
|
+
}), children: [_jsx(MapPin, { "aria-hidden": "true", className: "size-4" }), renderLabel(label('confirmation.directions'))] }))] })), calendarAlt && (_jsx("p", { className: "-mt-2 text-center text-sm", children: _jsx("a", { ...calendarLinkProps(calendarAlt.href, calendarFileName, calendarAlt.newTab), className: linkClass, children: calendarAlt.label }) })), label('confirmation.tip.heading') && label('confirmation.tip.body') && (_jsxs("div", { className: "rounded-lg bg-muted px-5 py-4 text-sm", children: [_jsx("p", { className: "font-semibold", children: renderLabel(label('confirmation.tip.heading')) }), _jsx("p", { className: "text-muted-foreground", children: renderLabel(label('confirmation.tip.body')) })] })), lines.length === 1 && first?.manageHref && (_jsxs("p", { className: "text-center text-sm", children: [_jsx("a", { href: first.manageHref, className: linkClass, children: labels['confirmation.manage'] }), manageUntil && freeChangeUntilTs !== null && freeChangeUntilTs !== undefined && (_jsxs("span", { className: "text-muted-foreground", children: [SEPARATOR, fillLabel(manageUntil, {
|
|
209
221
|
day: clock.dayLabel(freeChangeUntilTs, now),
|
|
210
222
|
time: clock.formatTime(freeChangeUntilTs),
|
|
211
223
|
})] }))] })), portalHref && (_jsx("p", { className: "text-center text-sm", children: _jsx("a", { href: portalHref, className: linkClass, children: labels['confirmation.portal'] }) })), onStartOver && (_jsx("div", { className: "flex justify-center", children: _jsx(BookingButton, { variant: "outline", className: slotClass(classNames, 'startOver'), onClick: onStartOver, children: labels['confirmation.startOver'] }) }))] }));
|
|
@@ -31,7 +31,7 @@ import { WHO_SCREEN_LABEL_KEYS, WHO_SCREEN_PARTY_LABEL_KEYS } from './who-screen
|
|
|
31
31
|
* (Screens that embed another screen already include its keys, e.g. the
|
|
32
32
|
* login sheet carries the login panel's, OTP slots' and Vipps button's.)
|
|
33
33
|
*/
|
|
34
|
-
export declare const BOOKING_LABEL_KEYS: readonly ("addChild.trigger" | "addChild.title" | "addChild.description.saved" | "addChild.description.local" | "addChild.name" | "addChild.birthYear" | "addChild.birthYearPlaceholder" | "addChild.birthMonth" | "addChild.birthMonthUnknown" | "addChild.notes" | "addChild.missing" | "addChild.submit" | "addChild.close" | "panel.heading" | "panel.who" | "panel.service" | "panel.time" | "panel.stylist" | "panel.notChosen" | "panel.firstAvailable" | "panel.alsoFree" | "panel.swap" | "panel.swapTo" | "panel.total" | "panel.continue" | "recap.label" | "recap.time" | "recap.line" | "recap.lineFor" | "recap.anyStylist" | "recap.total" | "recap.edit" | "recap.editLabel" | "recap.alsoFree" | "recap.swapTo" | "bookingSkeleton.opening" | "confirmation.heading" | "confirmation.serviceFor" | "confirmation.when" | "confirmation.party.heading" | "confirmation.party.line" | "confirmation.party.stylist" | "confirmation.party.total" | "confirmation.calendar" | "confirmation.manage" | "confirmation.manageFor" | "confirmation.portal" | "confirmation.startOver" | "confirmation.sent" | "confirmation.leafTime" | "confirmation.detail.service" | "confirmation.detail.stylist" | "confirmation.detail.where" | "confirmation.duration" | "confirmation.directions" | "confirmation.tip.heading" | "confirmation.tip.body" | "confirmation.manageUntil" | "details.heading" | "details.phone.label" | "details.phone.prefix" | "details.phone.error" | "details.name.label" | "details.known.label" | "details.known.for" | "details.known.self" | "details.known.adult" | "details.known.child" | "details.child.name" | "details.child.year" | "details.child.yearPlaceholder" | "details.email.label" | "details.email.help" | "details.email.lockedHelp" | "details.email.error" | "details.notes.label" | "details.notes.placeholder" | "details.terms.text" | "details.terms.link" | "details.marketing.text" | "details.error.maxParty" | "details.error.slotTaken" | "details.error.conflict" | "details.error.invalidInput" | "details.error.unconfigured" | "details.error.upstreamError" | "details.error.inProgress" | "details.call.link" | "details.call.none" | "details.submit" | "otp.label" | "vipps.button" | "vipps.unavailable" | "vipps.throttled" | "login.heading" | "login.codeHeading" | "login.or" | "login.continueEmail" | "login.emailIntro" | "login.emailLabel" | "login.sendCode" | "login.codeLabel" | "login.submitCode" | "login.codeHelp" | "login.codeHelpVipps" | "login.vippsSentTo" | "login.vippsSentToUnknown" | "login.cooldownWait" | "login.cooldownReady" | "login.resendIn" | "login.resend" | "login.switchEmail" | "login.useEmailInstead" | "login.notice.sent" | "login.notice.resent" | "login.notice.unreachable" | "login.notice.badEmail" | "login.notice.invalid" | "login.notice.throttled" | "login.notice.vippsInvalid" | "login.notice.vippsConflict" | "loginSheet.prompt" | "loginSheet.trigger" | "loginSheet.intro" | "loginSheet.vipps" | "loginSheet.close" | "time.heading" | "time.dayStrip.legend" | "time.nearest" | "time.current" | "time.nextFree" | "time.shortNotice" | "time.weekend.pct" | "time.weekend.mixed" | "time.month.show" | "time.month.hide" | "time.month.previous" | "time.month.next" | "time.month.heading" | "time.month.note" | "time.party.sequential" | "time.party.parallel" | "time.party.seat" | "time.party.seatSeparator" | "time.parallel.offer.two" | "time.parallel.offer.other" | "time.parallel.accept" | "time.empty.closed" | "time.empty.over" | "time.empty.full" | "time.empty.stylist" | "time.empty.unknown" | "time.empty.callLink" | "time.empty.callLinkAria" | "time.empty.callSuffix" | "time.empty.callPlain" | "time.loading" | "time.skeleton.dayPlaceholder" | "time.taken.message" | "time.taken.close" | "time.soonest.heading" | "time.soonest.pick" | "time.soonest.with" | "time.dayChip.free" | "time.dayChip.freeOne" | "time.dayChip.full" | "time.week.none" | "time.week.more" | "manage.heading" | "manage.serviceFallback" | "manage.card.named" | "manage.card.when" | "manage.card.whenWithStylist" | "manage.appointment.named" | "manage.appointment.namedSibilant" | "manage.appointment.unnamed" | "manage.ringUs" | "manage.ringUsOn" | "manage.callUs" | "manage.callUsOn" | "manage.error.windowPassed" | "manage.error.slotTaken" | "manage.error.notFound" | "manage.error.conflict" | "manage.error.invalidInput" | "manage.error.unavailable" | "manage.freeUntil" | "manage.remaining.in" | "manage.remaining.daysAndHours" | "manage.remaining.underMinute" | "manage.remaining.day.one" | "manage.remaining.day.other" | "manage.remaining.hour.one" | "manage.remaining.hour.other" | "manage.remaining.minute.one" | "manage.remaining.minute.other" | "manage.change" | "manage.cancel" | "manage.callToChange" | "manage.callToCancel" | "manage.blocked" | "manage.blocked.suffix" | "manage.blocked.plain" | "manage.calendar" | "manage.closed.cancelled" | "manage.closed.completed" | "manage.closed.noShow" | "manage.closed.pending" | "manage.closed.pendingCall" | "manage.closed.pendingCallPlain" | "manage.bookNew" | "manage.party.heading" | "manage.party.justThisOne" | "manage.party.unnamed" | "manage.party.note" | "manage.party.noteSuffix" | "manage.back" | "manage.reschedule.lead" | "manage.reschedule.leadWithStylist" | "manage.reschedule.failed" | "manage.reschedule.failedSuffix" | "manage.reschedule.loading" | "manage.move.question" | "manage.move.questionWithStylist" | "manage.move.surcharge" | "manage.move.confirm" | "manage.move.pickAnother" | "manage.cancel.question" | "manage.cancel.reasonLegend" | "manage.cancel.reason.illness" | "manage.cancel.reason.noLongerSuits" | "manage.cancel.reason.other" | "manage.cancel.confirm" | "manage.cancel.keep" | "manage.cancelled" | "manage.cancelled.findNew" | "manage.moved" | "manage.movedTo" | "manage.moved.view" | "manage.moved.noLink" | "manage.moved.noLinkSuffix" | "manage.portal" | "childCards.empty" | "childCards.neverVisited" | "childCards.lastVisit" | "childCards.lastVisitNoService" | "childCards.book" | "childCards.stylist" | "childCards.next" | "childCards.none" | "dataControls.heading" | "dataControls.export" | "dataControls.delete" | "dataControls.confirmHeading" | "dataControls.consequences" | "dataControls.confirmLabel" | "dataControls.confirmHelp" | "dataControls.wrongWord" | "dataControls.deleteForever" | "dataControls.cancel" | "dataControls.unreachable" | "dataControls.confirmWord" | "ageConfirm.heading" | "ageConfirm.lead" | "ageConfirm.legend" | "ageConfirm.birthYear" | "ageConfirm.birthMonth" | "ageConfirm.birthMonthUnknown" | "ageConfirm.confirm" | "ageConfirm.dismiss" | "familyEditor.heading" | "familyEditor.lead" | "familyEditor.empty" | "familyEditor.legend" | "familyEditor.add" | "familyEditor.name" | "familyEditor.birthYear" | "familyEditor.birthYearPlaceholder" | "familyEditor.birthMonth" | "familyEditor.birthMonthUnknown" | "familyEditor.stylist" | "familyEditor.stylistNone" | "familyEditor.stylistFormer" | "familyEditor.notes" | "familyEditor.notesPlaceholder" | "familyEditor.newRow" | "familyEditor.save" | "familyEditor.remove" | "familyEditor.removeNamed" | "familyEditor.removeRow" | "familyEditor.saved" | "familyEditor.savedNoDetails" | "familyEditor.partial" | "familyEditor.removed" | "familyEditor.unreachable" | "familyEditor.ageConfirmed" | "familyEditor.ageSaved" | "logout.button" | "logout.unreachable" | "portalShell.nav" | "portalShell.navBar" | "portalUnreachable.heading" | "portalUnreachable.body" | "portalUnreachable.retry" | "portalUnreachable.call" | "profileForm.heading" | "profileForm.firstName" | "profileForm.lastName" | "profileForm.phone" | "profileForm.email" | "profileForm.emailHelp" | "profileForm.save" | "profileForm.saved" | "profileForm.restored" | "profileForm.marketing" | "profileForm.unreachable" | "rebook.heading" | "rebook.empty" | "rebook.serviceFallback" | "rebook.anyStylist" | "rebook.cta" | "upcoming.heading" | "upcoming.empty" | "upcoming.book" | "upcoming.next" | "upcoming.today" | "upcoming.tomorrow" | "upcoming.inDays" | "upcoming.when" | "upcoming.serviceFallback" | "upcoming.anyStylist" | "upcoming.manage" | "upcoming.noManage" | "upcoming.noManageWithPhone" | "upcoming.call" | "vippsLink.heading" | "vippsLink.linked" | "vippsLink.pitch" | "vippsLink.button" | "vippsLink.success" | "vippsLink.conflict" | "vippsLink.failed" | "vippsLink.unavailable" | "vippsLink.throttled" | "history.heading" | "history.empty" | "history.serviceFallback" | "history.count.one" | "history.count.other" | "history.unknownAmount" | "service.multiHint" | "service.total" | "service.chooseFirst" | "service.continue" | "service.party.nothingBookable" | "service.tabDone" | "service.heading" | "service.categoriesLegend" | "service.sameAsLast" | "service.chooseOther" | "service.duration" | "service.phoneOnly" | "service.phoneOnlyLinkLabel" | "service.ageDivider.named" | "service.ageDivider.unnamed" | "service.party.listLabel" | "service.party.adultAlone" | "service.party.nothingFits" | "service.party.remove" | "service.search" | "service.searchLabel" | "service.searchEmpty" | "stylist.heading" | "stylist.firstAvailable" | "stylist.firstAvailableSubtitle" | "stylist.firstAvailableBadge" | "stylist.pendingName" | "stylist.nextAvailable" | "stylist.loading" | "stylist.party.legend" | "stylist.party.sequential" | "stylist.party.sequentialMinutes" | "stylist.party.parallel.two" | "stylist.party.parallel.three" | "stylist.party.parallel.other" | "stylist.party.parallelMinutes" | "stylist.party.parallelNote.two" | "stylist.party.parallelNote.three" | "stylist.party.parallelNote.other" | "stylist.party.seatsHeading" | "stylist.party.seat" | "summary.placeholder.who" | "summary.placeholder.service" | "summary.next" | "summary.hint.who" | "summary.hint.service" | "summary.hint.when" | "who.heading" | "who.guest.groupLabel" | "who.guest.children.one" | "who.guest.children.other" | "who.guest.adult" | "who.guest.addedList" | "who.family.legend" | "who.family.self" | "who.family.limit" | "who.party.children" | "who.party.childrenNote" | "who.party.adult" | "who.party.adultNote" | "who.party.fewer" | "who.party.more" | "who.party.count" | "who.party.adultFewer" | "who.party.adultMore" | "who.party.adultCount" | "who.party.lead" | "who.party.togetherHint")[];
|
|
34
|
+
export declare const BOOKING_LABEL_KEYS: readonly ("addChild.trigger" | "addChild.title" | "addChild.description.saved" | "addChild.description.local" | "addChild.name" | "addChild.birthYear" | "addChild.birthYearPlaceholder" | "addChild.birthMonth" | "addChild.birthMonthUnknown" | "addChild.notes" | "addChild.missing" | "addChild.submit" | "addChild.close" | "panel.heading" | "panel.who" | "panel.service" | "panel.time" | "panel.stylist" | "panel.notChosen" | "panel.firstAvailable" | "panel.alsoFree" | "panel.swap" | "panel.swapTo" | "panel.total" | "panel.continue" | "recap.label" | "recap.time" | "recap.line" | "recap.lineFor" | "recap.anyStylist" | "recap.total" | "recap.edit" | "recap.editLabel" | "recap.alsoFree" | "recap.swapTo" | "bookingSkeleton.opening" | "confirmation.heading" | "confirmation.serviceFor" | "confirmation.when" | "confirmation.party.heading" | "confirmation.party.line" | "confirmation.party.stylist" | "confirmation.party.total" | "confirmation.calendar" | "confirmation.manage" | "confirmation.manageFor" | "confirmation.portal" | "confirmation.startOver" | "confirmation.sent" | "confirmation.leafTime" | "confirmation.detail.service" | "confirmation.detail.stylist" | "confirmation.detail.where" | "confirmation.duration" | "confirmation.directions" | "confirmation.tip.heading" | "confirmation.tip.body" | "confirmation.manageUntil" | "details.heading" | "details.phone.label" | "details.phone.prefix" | "details.phone.error" | "details.name.label" | "details.known.label" | "details.known.for" | "details.known.self" | "details.known.adult" | "details.known.child" | "details.child.name" | "details.child.year" | "details.child.yearPlaceholder" | "details.email.label" | "details.email.help" | "details.email.lockedHelp" | "details.email.error" | "details.notes.label" | "details.notes.placeholder" | "details.terms.text" | "details.terms.link" | "details.marketing.text" | "details.error.maxParty" | "details.error.slotTaken" | "details.error.conflict" | "details.error.invalidInput" | "details.error.unconfigured" | "details.error.upstreamError" | "details.error.inProgress" | "details.call.link" | "details.call.none" | "details.submit" | "otp.label" | "vipps.button" | "vipps.unavailable" | "vipps.throttled" | "login.heading" | "login.codeHeading" | "login.or" | "login.continueEmail" | "login.emailIntro" | "login.emailLabel" | "login.sendCode" | "login.codeLabel" | "login.submitCode" | "login.codeHelp" | "login.codeHelpVipps" | "login.vippsSentTo" | "login.vippsSentToUnknown" | "login.cooldownWait" | "login.cooldownReady" | "login.resendIn" | "login.resend" | "login.switchEmail" | "login.useEmailInstead" | "login.notice.sent" | "login.notice.resent" | "login.notice.unreachable" | "login.notice.badEmail" | "login.notice.invalid" | "login.notice.throttled" | "login.notice.vippsInvalid" | "login.notice.vippsConflict" | "loginSheet.prompt" | "loginSheet.trigger" | "loginSheet.intro" | "loginSheet.vipps" | "loginSheet.close" | "time.heading" | "time.dayStrip.legend" | "time.nearest" | "time.current" | "time.nextFree" | "time.shortNotice" | "time.weekend.pct" | "time.weekend.mixed" | "time.month.show" | "time.month.hide" | "time.month.previous" | "time.month.next" | "time.month.heading" | "time.month.note" | "time.party.sequential" | "time.party.parallel" | "time.party.seat" | "time.party.seatSeparator" | "time.parallel.offer.two" | "time.parallel.offer.other" | "time.parallel.accept" | "time.empty.closed" | "time.empty.over" | "time.empty.full" | "time.empty.stylist" | "time.empty.unknown" | "time.empty.callLink" | "time.empty.callLinkAria" | "time.empty.callSuffix" | "time.empty.callPlain" | "time.loading" | "time.skeleton.dayPlaceholder" | "time.taken.message" | "time.taken.close" | "time.soonest.heading" | "time.soonest.pick" | "time.soonest.with" | "time.dayChip.free" | "time.dayChip.freeOne" | "time.dayChip.full" | "time.week.none" | "time.week.more" | "manage.heading" | "manage.serviceFallback" | "manage.card.named" | "manage.card.when" | "manage.card.whenWithStylist" | "manage.appointment.named" | "manage.appointment.namedSibilant" | "manage.appointment.unnamed" | "manage.ringUs" | "manage.ringUsOn" | "manage.callUs" | "manage.callUsOn" | "manage.error.windowPassed" | "manage.error.slotTaken" | "manage.error.notFound" | "manage.error.conflict" | "manage.error.invalidInput" | "manage.error.unavailable" | "manage.freeUntil" | "manage.remaining.in" | "manage.remaining.daysAndHours" | "manage.remaining.underMinute" | "manage.remaining.day.one" | "manage.remaining.day.other" | "manage.remaining.hour.one" | "manage.remaining.hour.other" | "manage.remaining.minute.one" | "manage.remaining.minute.other" | "manage.change" | "manage.cancel" | "manage.callToChange" | "manage.callToCancel" | "manage.blocked" | "manage.blocked.suffix" | "manage.blocked.plain" | "manage.calendar" | "manage.closed.cancelled" | "manage.closed.completed" | "manage.closed.noShow" | "manage.closed.pending" | "manage.closed.pendingCall" | "manage.closed.pendingCallPlain" | "manage.bookNew" | "manage.party.heading" | "manage.party.justThisOne" | "manage.party.unnamed" | "manage.party.note" | "manage.party.noteSuffix" | "manage.back" | "manage.reschedule.lead" | "manage.reschedule.leadWithStylist" | "manage.reschedule.failed" | "manage.reschedule.failedSuffix" | "manage.reschedule.loading" | "manage.move.question" | "manage.move.questionWithStylist" | "manage.move.surcharge" | "manage.move.confirm" | "manage.move.pickAnother" | "manage.cancel.question" | "manage.cancel.reasonLegend" | "manage.cancel.reason.illness" | "manage.cancel.reason.noLongerSuits" | "manage.cancel.reason.other" | "manage.cancel.confirm" | "manage.cancel.keep" | "manage.cancelled" | "manage.cancelled.findNew" | "manage.moved" | "manage.movedTo" | "manage.moved.view" | "manage.moved.noLink" | "manage.moved.noLinkSuffix" | "manage.portal" | "childCards.empty" | "childCards.neverVisited" | "childCards.lastVisit" | "childCards.lastVisitNoService" | "childCards.book" | "childCards.stylist" | "childCards.next" | "childCards.none" | "dataControls.heading" | "dataControls.export" | "dataControls.delete" | "dataControls.confirmHeading" | "dataControls.consequences" | "dataControls.confirmLabel" | "dataControls.confirmHelp" | "dataControls.wrongWord" | "dataControls.deleteForever" | "dataControls.cancel" | "dataControls.unreachable" | "dataControls.confirmWord" | "ageConfirm.heading" | "ageConfirm.lead" | "ageConfirm.legend" | "ageConfirm.birthYear" | "ageConfirm.birthMonth" | "ageConfirm.birthMonthUnknown" | "ageConfirm.confirm" | "ageConfirm.dismiss" | "familyEditor.heading" | "familyEditor.lead" | "familyEditor.empty" | "familyEditor.legend" | "familyEditor.add" | "familyEditor.name" | "familyEditor.birthYear" | "familyEditor.birthYearPlaceholder" | "familyEditor.birthMonth" | "familyEditor.birthMonthUnknown" | "familyEditor.stylist" | "familyEditor.stylistNone" | "familyEditor.stylistFormer" | "familyEditor.notes" | "familyEditor.notesPlaceholder" | "familyEditor.newRow" | "familyEditor.save" | "familyEditor.remove" | "familyEditor.removeNamed" | "familyEditor.removeRow" | "familyEditor.saved" | "familyEditor.savedNoDetails" | "familyEditor.partial" | "familyEditor.removed" | "familyEditor.unreachable" | "familyEditor.ageConfirmed" | "familyEditor.ageSaved" | "logout.button" | "logout.unreachable" | "portalShell.nav" | "portalShell.navBar" | "portalUnreachable.heading" | "portalUnreachable.body" | "portalUnreachable.retry" | "portalUnreachable.call" | "profileForm.heading" | "profileForm.firstName" | "profileForm.lastName" | "profileForm.phone" | "profileForm.email" | "profileForm.emailHelp" | "profileForm.save" | "profileForm.saved" | "profileForm.restored" | "profileForm.marketing" | "profileForm.unreachable" | "rebook.heading" | "rebook.empty" | "rebook.serviceFallback" | "rebook.anyStylist" | "rebook.cta" | "upcoming.heading" | "upcoming.empty" | "upcoming.book" | "upcoming.next" | "upcoming.today" | "upcoming.tomorrow" | "upcoming.inDays" | "upcoming.when" | "upcoming.serviceFallback" | "upcoming.anyStylist" | "upcoming.manage" | "upcoming.noManage" | "upcoming.noManageWithPhone" | "upcoming.call" | "vippsLink.heading" | "vippsLink.linked" | "vippsLink.pitch" | "vippsLink.button" | "vippsLink.success" | "vippsLink.conflict" | "vippsLink.failed" | "vippsLink.unavailable" | "vippsLink.throttled" | "history.heading" | "history.empty" | "history.serviceFallback" | "history.count.one" | "history.count.other" | "history.unknownAmount" | "service.multiHint" | "service.total" | "service.chooseFirst" | "service.continue" | "service.party.nothingBookable" | "service.tabDone" | "service.heading" | "service.categoriesLegend" | "service.sameAsLast" | "service.chooseOther" | "service.duration" | "service.phoneOnly" | "service.phoneOnlyLinkLabel" | "service.ageDivider.named" | "service.ageDivider.unnamed" | "service.party.listLabel" | "service.party.adultAlone" | "service.party.nothingFits" | "service.party.remove" | "service.search" | "service.searchLabel" | "service.searchEmpty" | "stylist.heading" | "stylist.firstAvailable" | "stylist.firstAvailableSubtitle" | "stylist.firstAvailableBadge" | "stylist.pendingName" | "stylist.nextAvailable" | "stylist.loading" | "stylist.party.legend" | "stylist.party.sequential" | "stylist.party.sequentialMinutes" | "stylist.party.parallel.two" | "stylist.party.parallel.three" | "stylist.party.parallel.other" | "stylist.party.parallelMinutes" | "stylist.party.parallelNote.two" | "stylist.party.parallelNote.three" | "stylist.party.parallelNote.other" | "stylist.party.seatsHeading" | "stylist.party.seat" | "summary.placeholder.who" | "summary.placeholder.service" | "summary.next" | "summary.hint.who" | "summary.hint.service" | "summary.hint.when" | "who.heading" | "who.guest.groupLabel" | "who.guest.children.one" | "who.guest.children.other" | "who.guest.adult" | "who.guest.addedList" | "who.family.legend" | "who.family.self" | "who.family.limit" | "who.party.children" | "who.party.childrenNote" | "who.party.adult" | "who.party.adultNote" | "who.party.fewer" | "who.party.more" | "who.party.count" | "who.party.adultFewer" | "who.party.adultMore" | "who.party.adultCount" | "who.party.lead" | "who.party.togetherHint" | "who.family.adults")[];
|
|
35
35
|
export type BookingLabelKey = BookingRequiredLabelKey | BookingOptionalLabelKey;
|
|
36
36
|
/**
|
|
37
37
|
* Keys added after a screen first shipped, for a feature a caller opts into.
|
|
@@ -41,7 +41,7 @@ export declare const WHO_SCREEN_LABEL_KEYS: readonly ["who.heading", "who.guest.
|
|
|
41
41
|
* stepper buttons' accessible names; `who.party.count` `{count}` is read out
|
|
42
42
|
* when the number changes.
|
|
43
43
|
*/
|
|
44
|
-
export declare const WHO_SCREEN_PARTY_LABEL_KEYS: readonly ["who.party.children", "who.party.childrenNote", "who.party.adult", "who.party.adultNote", "who.party.fewer", "who.party.more", "who.party.count", "who.party.adultFewer", "who.party.adultMore", "who.party.adultCount", "who.party.lead", "who.party.togetherHint"];
|
|
44
|
+
export declare const WHO_SCREEN_PARTY_LABEL_KEYS: readonly ["who.party.children", "who.party.childrenNote", "who.party.adult", "who.party.adultNote", "who.party.fewer", "who.party.more", "who.party.count", "who.party.adultFewer", "who.party.adultMore", "who.party.adultCount", "who.party.lead", "who.party.togetherHint", "who.family.adults"];
|
|
45
45
|
export type WhoScreenLabels = Record<(typeof WHO_SCREEN_LABEL_KEYS)[number], BookingLabel> & Partial<Record<(typeof WHO_SCREEN_PARTY_LABEL_KEYS)[number], BookingLabel>> & AddChildSheetLabels;
|
|
46
46
|
/**
|
|
47
47
|
* - `root` — the `<section>`
|
|
@@ -143,6 +143,11 @@ export interface WhoScreenProps {
|
|
|
143
143
|
* rather than one check row: two parents, or a parent and a grandparent,
|
|
144
144
|
* in one booking. Seat 1 must be `adult`. Needs the `who.party.adultFewer`,
|
|
145
145
|
* `adultMore` and `adultCount` labels; without them the check row stays.
|
|
146
|
+
*
|
|
147
|
+
* The one part of `guestParty` a logged-in parent's list reads too: with
|
|
148
|
+
* `who.family.adults` as well, its «myself» row is the same stepper, seat 1
|
|
149
|
+
* being `selfKey` and seats 2, 3 these — so a second grown-up survives a
|
|
150
|
+
* login. `isGuestSeat` must then not claim seats 2 and up.
|
|
146
151
|
*/
|
|
147
152
|
adults?: (seat: number) => WizardPerson & {
|
|
148
153
|
adult: true;
|
|
@@ -82,6 +82,18 @@ export const WHO_SCREEN_PARTY_LABEL_KEYS = [
|
|
|
82
82
|
'who.party.lead',
|
|
83
83
|
/** Under the steppers while there are children and no grown-up. */
|
|
84
84
|
'who.party.togetherHint',
|
|
85
|
+
/**
|
|
86
|
+
* A logged-in parent's grown-ups row, e.g. «Grown-ups», when
|
|
87
|
+
* `guestParty.adults` makes it a stepper: «myself» is seat 1, the rest are
|
|
88
|
+
* the guest's seats 2, 3. Without it the «myself» check row stays.
|
|
89
|
+
*/
|
|
90
|
+
'who.family.adults',
|
|
91
|
+
];
|
|
92
|
+
/** The labels a «Voksen» stepper cannot draw without. */
|
|
93
|
+
const ADULT_STEPPER_LABEL_KEYS = [
|
|
94
|
+
'who.party.adultFewer',
|
|
95
|
+
'who.party.adultMore',
|
|
96
|
+
'who.party.adultCount',
|
|
85
97
|
];
|
|
86
98
|
/** The `who.party.*` keys `guestParty` cannot draw without; the two notes are extras. */
|
|
87
99
|
const PARTY_REQUIRED_LABEL_KEYS = [
|
|
@@ -138,6 +150,27 @@ export function WhoScreen({ labels, format, people, family, guestChoices, addedC
|
|
|
138
150
|
const full = seated.length >= maxPeople;
|
|
139
151
|
const self = { key: selfKey, adult: true };
|
|
140
152
|
const selfChecked = chosen.has(selfKey);
|
|
153
|
+
// «Voksne» as a number under a family too: «myself» first, then the guest's
|
|
154
|
+
// seats 2, 3 — a second grown-up has no profile card to tick.
|
|
155
|
+
const moreAdults = guestParty?.adults !== undefined &&
|
|
156
|
+
labels['who.family.adults'] !== undefined &&
|
|
157
|
+
ADULT_STEPPER_LABEL_KEYS.every((key) => labels[key] !== undefined)
|
|
158
|
+
? guestParty.adults
|
|
159
|
+
: undefined;
|
|
160
|
+
const otherAdultKeys = new Set(moreAdults ? Array.from({ length: maxPeople - 1 }, (_, index) => moreAdults(index + 2).key) : []);
|
|
161
|
+
const isGrownUp = (person) => person.key === selfKey || otherAdultKeys.has(person.key);
|
|
162
|
+
const grownUps = seated.filter(isGrownUp).length;
|
|
163
|
+
const setGrownUps = (count) => {
|
|
164
|
+
if (!moreAdults)
|
|
165
|
+
return;
|
|
166
|
+
const seats = Array.from({ length: count }, (_, index) => index === 0 ? self : moreAdults(index + 1));
|
|
167
|
+
onChoose([...seated.filter((person) => !isGrownUp(person)), ...seats], false);
|
|
168
|
+
};
|
|
169
|
+
const partyText = (key, values = {}) => {
|
|
170
|
+
const label = labels[key];
|
|
171
|
+
return label ? fillLabel(label, values) : '';
|
|
172
|
+
};
|
|
173
|
+
const adultNote = labels['who.party.adultNote'];
|
|
141
174
|
function toggle(person) {
|
|
142
175
|
if (chosen.has(person.key)) {
|
|
143
176
|
onChoose(seated.filter((other) => other.key !== person.key), false);
|
|
@@ -150,7 +183,7 @@ export function WhoScreen({ labels, format, people, family, guestChoices, addedC
|
|
|
150
183
|
return (_jsxs("section", { "aria-labelledby": HEADING_ID, className: slotClass(classNames, 'root', 'space-y-6'), children: [heading, _jsxs("fieldset", { className: "space-y-4", children: [_jsx("legend", { className: "mb-2 text-sm text-muted-foreground", children: renderLabel(labels['who.family.legend'], { max: maxPeople }) }), _jsxs("ul", { className: "space-y-2", children: [family.map((entry, index) => {
|
|
151
184
|
const selected = chosen.has(entry.person.key);
|
|
152
185
|
return (_jsx("li", { children: _jsx(PersonCard, { name: entry.person.name ?? '', line: entry.line, index: index, selected: selected, disabled: !selected && full, describedBy: !selected && full ? LIMIT_ID : undefined, onToggle: () => toggle(entry.person), ...cardClasses }) }, entry.person.key));
|
|
153
|
-
}), _jsx("li", { children: sheet(full, true) })] }), _jsxs(CheckRow, { checked: selfChecked, disabled: !selfChecked && full, describedBy: !selfChecked && full ? LIMIT_ID : undefined, onToggle: () => toggle(self), className: cn('h-14 justify-between px-5 text-sm', classNames?.card), selectedClassName: classNames?.cardSelected, children: [_jsx("span", { className: "font-medium", children: labels['who.family.self'] }), _jsx(CheckCircle, { checked: selfChecked })] }), _jsx("p", { id: LIMIT_ID, "aria-live": "polite", className: slotClass(classNames, 'limit', 'min-h-5 text-sm text-muted-foreground'), children: full ? renderLabel(labels['who.family.limit'], { max: maxPeople }) : '' })] })] }));
|
|
186
|
+
}), _jsx("li", { children: sheet(full, true) })] }), moreAdults ? (_jsx("div", { className: cn('rounded-lg border border-border bg-card', classNames?.card), children: _jsx(StepperRow, { title: partyText('who.family.adults'), note: adultNote ? renderLabel(adultNote) : null, value: grownUps, countLabel: partyText('who.party.adultCount', { count: grownUps }), fewerLabel: partyText('who.party.adultFewer'), moreLabel: partyText('who.party.adultMore'), canFewer: grownUps > 0, canMore: !full, limitId: full ? LIMIT_ID : undefined, onFewer: () => setGrownUps(grownUps - 1), onMore: () => setGrownUps(grownUps + 1) }) })) : (_jsxs(CheckRow, { checked: selfChecked, disabled: !selfChecked && full, describedBy: !selfChecked && full ? LIMIT_ID : undefined, onToggle: () => toggle(self), className: cn('h-14 justify-between px-5 text-sm', classNames?.card), selectedClassName: classNames?.cardSelected, children: [_jsx("span", { className: "font-medium", children: labels['who.family.self'] }), _jsx(CheckCircle, { checked: selfChecked })] })), _jsx("p", { id: LIMIT_ID, "aria-live": "polite", className: slotClass(classNames, 'limit', 'min-h-5 text-sm text-muted-foreground'), children: full ? renderLabel(labels['who.family.limit'], { max: maxPeople }) : '' })] })] }));
|
|
154
187
|
}
|
|
155
188
|
/**
|
|
156
189
|
* A guest's party: how many children (a stepper) and whether the grown-up
|
|
@@ -161,9 +194,7 @@ export function WhoScreen({ labels, format, people, family, guestChoices, addedC
|
|
|
161
194
|
*/
|
|
162
195
|
function GuestParty({ party, people, maxPeople, labels, classNames, onChoose, }) {
|
|
163
196
|
const adultStepper = party.adults !== undefined &&
|
|
164
|
-
labels[
|
|
165
|
-
labels['who.party.adultMore'] !== undefined &&
|
|
166
|
-
labels['who.party.adultCount'] !== undefined;
|
|
197
|
+
ADULT_STEPPER_LABEL_KEYS.every((key) => labels[key] !== undefined);
|
|
167
198
|
const adultSeat = (seat) => seat === 1 || !party.adults ? party.adult : party.adults(seat);
|
|
168
199
|
const adultKeys = new Set(Array.from({ length: maxPeople }, (_, index) => adultSeat(index + 1).key));
|
|
169
200
|
const isAdultSeat = (person) => adultKeys.has(person.key);
|