@medalsocial/booking 0.0.0-placeholder.0 → 0.1.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 +14 -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 +171 -0
- package/dist/core/config.mjs +392 -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 +83 -0
- package/dist/core/draft-store.mjs +115 -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 +34 -0
- package/dist/core/index.mjs +31 -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 +598 -0
- package/dist/core/machine.mjs +878 -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 +72 -0
- package/dist/core/party-slots.mjs +223 -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 +241 -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 +197 -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 +98 -0
- package/dist/next/medal.mjs +154 -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 +330 -0
- package/dist/next/routes/catalogue-routes.mjs.map +1 -0
- package/dist/next/routes/create.mjs +422 -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 +134 -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 +577 -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 +709 -0
- package/dist/react/labels/screens.mjs.map +1 -0
- package/dist/react/labels/wizard.d.mts +69 -0
- package/dist/react/labels/wizard.mjs +95 -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 +177 -0
- package/dist/react/useBooking.mjs +1205 -0
- package/dist/react/useBooking.mjs.map +1 -0
- package/dist/react/wizard/adapters.mjs +187 -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/package.json +126 -5
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { PortalBookingDto, PortalProfileDto } from "../../core/portal/dto.mjs";
|
|
2
|
+
import { PortalActions } from "../actions.mjs";
|
|
3
|
+
import { BookingOverrides } from "../Provider.mjs";
|
|
4
|
+
import { PortalTab } from "./tabs.mjs";
|
|
5
|
+
import { PortalTabIcons } from "./sections.mjs";
|
|
6
|
+
import { VippsLinkFlash } from "@medalsocial/meda/booking";
|
|
7
|
+
import { ReactNode } from "react";
|
|
8
|
+
//#region src/react/portal/dashboard.d.ts
|
|
9
|
+
/** Where the visitor goes after logging out / deleting themselves. */
|
|
10
|
+
interface Exits {
|
|
11
|
+
/** Default `/`. */
|
|
12
|
+
logoutHref?: string;
|
|
13
|
+
/** Default `paths.portalLogin`. */
|
|
14
|
+
deletedHref?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface PortalDashboardProps extends BookingOverrides, Exits {
|
|
17
|
+
profile: PortalProfileDto;
|
|
18
|
+
bookings: {
|
|
19
|
+
upcoming: PortalBookingDto[];
|
|
20
|
+
past: PortalBookingDto[];
|
|
21
|
+
};
|
|
22
|
+
/** The «usual stylist» choices, display names, in the order to offer them. */
|
|
23
|
+
stylists?: ReadonlyArray<{
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
}>;
|
|
27
|
+
/** The business's number for «call us», or `null`. */
|
|
28
|
+
phone: string | null;
|
|
29
|
+
/** The app's `'use server'` wrappers (Decision 8). */
|
|
30
|
+
actions: PortalActions;
|
|
31
|
+
/** The section to open on (`parsePortalTab` on the query). A Vipps flash opens Profile. */
|
|
32
|
+
tab?: PortalTab;
|
|
33
|
+
/** The person ids the age prompt was already answered for (`parseAgePromptDismissed`). */
|
|
34
|
+
agePromptDismissed?: readonly string[];
|
|
35
|
+
/** The age prompt's cookie. Default `<portal.cookieName>_age_ok`, path `paths.portal`. */
|
|
36
|
+
agePromptCookie?: string;
|
|
37
|
+
/** How a «link Vipps» attempt that just returned ended (`vippsLinkFlash`), or `null`. */
|
|
38
|
+
vippsFlash?: VippsLinkFlash | null;
|
|
39
|
+
/** Shows the «Vipps linked» toast (e.g. sonner's `toast.success`); without it the row says so inline. */
|
|
40
|
+
onToast?: (message: string) => void;
|
|
41
|
+
/** Where «book» goes. Default `handoffUrl ?? paths.booking`; the ids ride along only to `paths.booking`. */
|
|
42
|
+
bookingHref?: string;
|
|
43
|
+
/** The render's clock (ages, «next appointment»). Default `Date.now()` at mount; pass the server's. */
|
|
44
|
+
now?: number;
|
|
45
|
+
/** Replaces the default header (eyebrow, greeting, «book» button). */
|
|
46
|
+
header?: ReactNode;
|
|
47
|
+
/** The query parameter the open section rides in (default `fane`) and each section's value. */
|
|
48
|
+
tabParam?: string;
|
|
49
|
+
tabSlugs?: Readonly<Record<PortalTab, string>>;
|
|
50
|
+
icons?: PortalTabIcons;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The customer portal: a rail on a desktop, a tab bar on a phone, and four
|
|
54
|
+
* sections — overview (next appointments, the family, «book again»), family
|
|
55
|
+
* (cards and the editor), history and profile (details, Vipps, data). Every
|
|
56
|
+
* section is mounted all the time, so a half-edited form survives a tab tap.
|
|
57
|
+
*
|
|
58
|
+
* The server page reads the profile, the bookings, the stylists, the phone,
|
|
59
|
+
* the age cookie and the Vipps flash, and hands them in; this renders and
|
|
60
|
+
* wires each form to its action. It renders no page wrapper.
|
|
61
|
+
*/
|
|
62
|
+
export declare function PortalDashboard(props: PortalDashboardProps): import("react").JSX.Element;
|
|
63
|
+
export interface PortalDashboardUnreachableProps extends BookingOverrides, Pick<Exits, 'logoutHref'> {
|
|
64
|
+
phone: string | null;
|
|
65
|
+
actions: Pick<PortalActions, 'logout'>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* What the portal shows when the backend did not answer: «try again» (this
|
|
69
|
+
* page) and the phone — and a logout, because this is the one screen a
|
|
70
|
+
* visitor could otherwise be held on.
|
|
71
|
+
*/
|
|
72
|
+
export declare function PortalDashboardUnreachable(props: PortalDashboardUnreachableProps): import("react").JSX.Element;
|
|
73
|
+
//#endregion
|
|
74
|
+
//# sourceMappingURL=dashboard.d.mts.map
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { fillParts, labelText } from "../../core/labels.mjs";
|
|
3
|
+
import { rebookSuggestions } from "../../core/portal/dto.mjs";
|
|
4
|
+
import { useBookingKit } from "../Provider.mjs";
|
|
5
|
+
import { screenLabels } from "../screen-labels.mjs";
|
|
6
|
+
import { agePromptCookieName } from "./age-prompt.mjs";
|
|
7
|
+
import { createChildSummaries } from "./child-summary.mjs";
|
|
8
|
+
import { SessionTouch } from "./SessionTouch.mjs";
|
|
9
|
+
import { PortalSections } from "./sections.mjs";
|
|
10
|
+
import { PortalChildCards, PortalDataControls, PortalFamilyEditor, PortalLogoutButton, PortalProfileForm, PortalRebookCards, PortalVippsLinkRow } from "./wired.mjs";
|
|
11
|
+
import { AccountCard, PortalUnreachable, UpcomingBookings, VisitHistory, bookingButtonClass } from "@medalsocial/meda/booking";
|
|
12
|
+
import { useState } from "react";
|
|
13
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
//#region src/react/portal/dashboard.tsx
|
|
15
|
+
/** A section heading as the dashboard draws it. */
|
|
16
|
+
function Heading({ id, children }) {
|
|
17
|
+
return /* @__PURE__ */ jsx("h2", {
|
|
18
|
+
id,
|
|
19
|
+
className: "font-sans text-xl font-bold md:text-2xl",
|
|
20
|
+
children
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The customer portal: a rail on a desktop, a tab bar on a phone, and four
|
|
25
|
+
* sections — overview (next appointments, the family, «book again»), family
|
|
26
|
+
* (cards and the editor), history and profile (details, Vipps, data). Every
|
|
27
|
+
* section is mounted all the time, so a half-edited form survives a tab tap.
|
|
28
|
+
*
|
|
29
|
+
* The server page reads the profile, the bookings, the stylists, the phone,
|
|
30
|
+
* the age cookie and the Vipps flash, and hands them in; this renders and
|
|
31
|
+
* wires each form to its action. It renders no page wrapper.
|
|
32
|
+
*/
|
|
33
|
+
function PortalDashboard(props) {
|
|
34
|
+
const booking = useBookingKit(props);
|
|
35
|
+
const { kit } = booking;
|
|
36
|
+
const { labels, config } = kit;
|
|
37
|
+
const { profile, bookings, actions, phone, stylists = [] } = props;
|
|
38
|
+
const [now] = useState(() => props.now ?? Date.now());
|
|
39
|
+
const bookingHref = props.bookingHref ?? config.handoffUrl ?? config.paths.booking;
|
|
40
|
+
const kids = createChildSummaries(kit.clock)(profile.family, bookings.past, now, bookings.upcoming);
|
|
41
|
+
const stylistNames = Object.fromEntries(stylists.map((stylist) => [stylist.id, stylist.name]));
|
|
42
|
+
const vippsFlash = props.vippsFlash ?? null;
|
|
43
|
+
const familyHeading = labels["portal.family.heading"];
|
|
44
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SessionTouch, { config }), /* @__PURE__ */ jsx(PortalSections, {
|
|
45
|
+
booking,
|
|
46
|
+
initialTab: vippsFlash !== null ? "profile" : props.tab,
|
|
47
|
+
tabParam: props.tabParam,
|
|
48
|
+
tabSlugs: props.tabSlugs,
|
|
49
|
+
icons: props.icons,
|
|
50
|
+
header: props.header ?? /* @__PURE__ */ jsxs("header", {
|
|
51
|
+
className: "flex flex-wrap items-end justify-between gap-4",
|
|
52
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
53
|
+
className: "space-y-1",
|
|
54
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
55
|
+
className: "text-xs font-bold tracking-widest text-muted-foreground",
|
|
56
|
+
children: labels["portal.eyebrow"]
|
|
57
|
+
}), /* @__PURE__ */ jsx("h1", {
|
|
58
|
+
className: "font-sans text-3xl font-bold md:text-5xl",
|
|
59
|
+
children: fillParts(labels["portal.greeting"], { name: profile.firstName ?? labelText(labels["portal.greetingFallback"]) })
|
|
60
|
+
})]
|
|
61
|
+
}), /* @__PURE__ */ jsxs("a", {
|
|
62
|
+
href: bookingHref,
|
|
63
|
+
className: bookingButtonClass({ size: "lg" }),
|
|
64
|
+
children: [labels["portal.book"], /* @__PURE__ */ jsx("svg", {
|
|
65
|
+
"aria-hidden": "true",
|
|
66
|
+
viewBox: "0 0 24 24",
|
|
67
|
+
fill: "none",
|
|
68
|
+
stroke: "currentColor",
|
|
69
|
+
strokeWidth: 2,
|
|
70
|
+
strokeLinecap: "round",
|
|
71
|
+
strokeLinejoin: "round",
|
|
72
|
+
className: "size-4",
|
|
73
|
+
children: /* @__PURE__ */ jsx("path", { d: "M5 12h14M12 5l7 7-7 7" })
|
|
74
|
+
})]
|
|
75
|
+
})]
|
|
76
|
+
}),
|
|
77
|
+
account: /* @__PURE__ */ jsx(AccountCard, {
|
|
78
|
+
profile,
|
|
79
|
+
compact: true,
|
|
80
|
+
classNames: booking.classNames.accountCard
|
|
81
|
+
}),
|
|
82
|
+
logout: /* @__PURE__ */ jsx(PortalLogoutButton, {
|
|
83
|
+
booking,
|
|
84
|
+
actions,
|
|
85
|
+
logoutHref: props.logoutHref ?? "/"
|
|
86
|
+
}),
|
|
87
|
+
overview: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
88
|
+
/* @__PURE__ */ jsx(UpcomingBookings, {
|
|
89
|
+
bookings: bookings.upcoming,
|
|
90
|
+
phone,
|
|
91
|
+
bookingHref,
|
|
92
|
+
now,
|
|
93
|
+
labels: screenLabels(labels),
|
|
94
|
+
format: kit.format,
|
|
95
|
+
classNames: booking.classNames.upcoming,
|
|
96
|
+
components: booking.components
|
|
97
|
+
}),
|
|
98
|
+
/* @__PURE__ */ jsxs("section", {
|
|
99
|
+
"aria-labelledby": "portal-children-summary",
|
|
100
|
+
className: "space-y-4",
|
|
101
|
+
children: [/* @__PURE__ */ jsx(Heading, {
|
|
102
|
+
id: "portal-children-summary",
|
|
103
|
+
children: familyHeading
|
|
104
|
+
}), /* @__PURE__ */ jsx(PortalChildCards, {
|
|
105
|
+
booking,
|
|
106
|
+
kids,
|
|
107
|
+
variant: "compact",
|
|
108
|
+
bookingHref
|
|
109
|
+
})]
|
|
110
|
+
}),
|
|
111
|
+
/* @__PURE__ */ jsx(PortalRebookCards, {
|
|
112
|
+
booking,
|
|
113
|
+
suggestions: rebookSuggestions(bookings.past),
|
|
114
|
+
bookingHref
|
|
115
|
+
})
|
|
116
|
+
] }),
|
|
117
|
+
family: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("section", {
|
|
118
|
+
"aria-labelledby": "portal-children-heading",
|
|
119
|
+
className: "space-y-4",
|
|
120
|
+
children: [
|
|
121
|
+
/* @__PURE__ */ jsx(Heading, {
|
|
122
|
+
id: "portal-children-heading",
|
|
123
|
+
children: familyHeading
|
|
124
|
+
}),
|
|
125
|
+
/* @__PURE__ */ jsx("p", {
|
|
126
|
+
className: "text-muted-foreground",
|
|
127
|
+
children: labels["portal.family.lead"]
|
|
128
|
+
}),
|
|
129
|
+
/* @__PURE__ */ jsx(PortalChildCards, {
|
|
130
|
+
booking,
|
|
131
|
+
kids,
|
|
132
|
+
bookingHref,
|
|
133
|
+
empty: null,
|
|
134
|
+
stylistNames
|
|
135
|
+
})
|
|
136
|
+
]
|
|
137
|
+
}), /* @__PURE__ */ jsx(PortalFamilyEditor, {
|
|
138
|
+
booking,
|
|
139
|
+
family: profile.family,
|
|
140
|
+
nouns: profile.labels,
|
|
141
|
+
personDetails: profile.personDetails,
|
|
142
|
+
stylists,
|
|
143
|
+
dismissed: props.agePromptDismissed ?? [],
|
|
144
|
+
cookieName: props.agePromptCookie ?? agePromptCookieName(config),
|
|
145
|
+
now,
|
|
146
|
+
actions
|
|
147
|
+
})] }),
|
|
148
|
+
history: /* @__PURE__ */ jsx(VisitHistory, {
|
|
149
|
+
past: bookings.past,
|
|
150
|
+
labels: screenLabels(labels),
|
|
151
|
+
format: kit.format,
|
|
152
|
+
classNames: booking.classNames.history,
|
|
153
|
+
components: booking.components
|
|
154
|
+
}),
|
|
155
|
+
profile: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
156
|
+
/* @__PURE__ */ jsx(AccountCard, {
|
|
157
|
+
profile,
|
|
158
|
+
classNames: booking.classNames.accountCard
|
|
159
|
+
}),
|
|
160
|
+
/* @__PURE__ */ jsx(PortalProfileForm, {
|
|
161
|
+
booking,
|
|
162
|
+
profile,
|
|
163
|
+
actions
|
|
164
|
+
}),
|
|
165
|
+
profile.vippsLinked !== void 0 && actions.startVippsLink && /* @__PURE__ */ jsx(PortalVippsLinkRow, {
|
|
166
|
+
booking,
|
|
167
|
+
linked: profile.vippsLinked,
|
|
168
|
+
flash: vippsFlash,
|
|
169
|
+
startVippsLink: actions.startVippsLink,
|
|
170
|
+
onToast: props.onToast
|
|
171
|
+
}),
|
|
172
|
+
/* @__PURE__ */ jsx(PortalDataControls, {
|
|
173
|
+
booking,
|
|
174
|
+
actions,
|
|
175
|
+
deletedHref: props.deletedHref ?? config.paths.portalLogin
|
|
176
|
+
})
|
|
177
|
+
] })
|
|
178
|
+
})] });
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* What the portal shows when the backend did not answer: «try again» (this
|
|
182
|
+
* page) and the phone — and a logout, because this is the one screen a
|
|
183
|
+
* visitor could otherwise be held on.
|
|
184
|
+
*/
|
|
185
|
+
function PortalDashboardUnreachable(props) {
|
|
186
|
+
const booking = useBookingKit(props);
|
|
187
|
+
const { kit } = booking;
|
|
188
|
+
return /* @__PURE__ */ jsx(PortalUnreachable, {
|
|
189
|
+
labels: screenLabels(kit.labels),
|
|
190
|
+
format: kit.format,
|
|
191
|
+
phone: props.phone,
|
|
192
|
+
retryHref: kit.config.paths.portal ?? "/",
|
|
193
|
+
classNames: booking.classNames.portalUnreachable,
|
|
194
|
+
logout: /* @__PURE__ */ jsx(PortalLogoutButton, {
|
|
195
|
+
booking,
|
|
196
|
+
actions: props.actions,
|
|
197
|
+
logoutHref: props.logoutHref ?? "/"
|
|
198
|
+
})
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
export { PortalDashboard, PortalDashboardUnreachable };
|
|
203
|
+
|
|
204
|
+
//# sourceMappingURL=dashboard.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard.mjs","names":[],"sources":["../../../src/react/portal/dashboard.tsx"],"sourcesContent":["import {\n AccountCard,\n bookingButtonClass,\n PortalUnreachable,\n UpcomingBookings,\n type VippsLinkFlash,\n VisitHistory,\n} from '@medalsocial/meda/booking';\nimport { type ReactNode, useState } from 'react';\nimport { fillParts, labelText } from '../../core/labels';\nimport type { PortalBookingDto, PortalProfileDto } from '../../core/portal/dto';\nimport { rebookSuggestions } from '../../core/portal/dto';\nimport type { PortalActions } from '../actions';\nimport { type BookingOverrides, useBookingKit } from '../Provider';\nimport { screenLabels } from '../screen-labels';\nimport { agePromptCookieName } from './age-prompt';\nimport { createChildSummaries } from './child-summary';\nimport { SessionTouch } from './SessionTouch';\nimport { PortalSections, type PortalTabIcons } from './sections';\nimport type { PortalTab } from './tabs';\nimport {\n PortalChildCards,\n PortalDataControls,\n PortalFamilyEditor,\n PortalLogoutButton,\n PortalProfileForm,\n PortalRebookCards,\n PortalVippsLinkRow,\n} from './wired';\n\n/** Where the visitor goes after logging out / deleting themselves. */\ninterface Exits {\n /** Default `/`. */\n logoutHref?: string;\n /** Default `paths.portalLogin`. */\n deletedHref?: string;\n}\n\nexport interface PortalDashboardProps extends BookingOverrides, Exits {\n profile: PortalProfileDto;\n bookings: { upcoming: PortalBookingDto[]; past: PortalBookingDto[] };\n /** The «usual stylist» choices, display names, in the order to offer them. */\n stylists?: ReadonlyArray<{ id: string; name: string }>;\n /** The business's number for «call us», or `null`. */\n phone: string | null;\n /** The app's `'use server'` wrappers (Decision 8). */\n actions: PortalActions;\n /** The section to open on (`parsePortalTab` on the query). A Vipps flash opens Profile. */\n tab?: PortalTab;\n /** The person ids the age prompt was already answered for (`parseAgePromptDismissed`). */\n agePromptDismissed?: readonly string[];\n /** The age prompt's cookie. Default `<portal.cookieName>_age_ok`, path `paths.portal`. */\n agePromptCookie?: string;\n /** How a «link Vipps» attempt that just returned ended (`vippsLinkFlash`), or `null`. */\n vippsFlash?: VippsLinkFlash | null;\n /** Shows the «Vipps linked» toast (e.g. sonner's `toast.success`); without it the row says so inline. */\n onToast?: (message: string) => void;\n /** Where «book» goes. Default `handoffUrl ?? paths.booking`; the ids ride along only to `paths.booking`. */\n bookingHref?: string;\n /** The render's clock (ages, «next appointment»). Default `Date.now()` at mount; pass the server's. */\n now?: number;\n /** Replaces the default header (eyebrow, greeting, «book» button). */\n header?: ReactNode;\n /** The query parameter the open section rides in (default `fane`) and each section's value. */\n tabParam?: string;\n tabSlugs?: Readonly<Record<PortalTab, string>>;\n icons?: PortalTabIcons;\n}\n\n/** A section heading as the dashboard draws it. */\nfunction Heading({ id, children }: { id: string; children: ReactNode }) {\n return (\n <h2 id={id} className=\"font-sans text-xl font-bold md:text-2xl\">\n {children}\n </h2>\n );\n}\n\n/**\n * The customer portal: a rail on a desktop, a tab bar on a phone, and four\n * sections — overview (next appointments, the family, «book again»), family\n * (cards and the editor), history and profile (details, Vipps, data). Every\n * section is mounted all the time, so a half-edited form survives a tab tap.\n *\n * The server page reads the profile, the bookings, the stylists, the phone,\n * the age cookie and the Vipps flash, and hands them in; this renders and\n * wires each form to its action. It renders no page wrapper.\n */\nexport function PortalDashboard(props: PortalDashboardProps) {\n const booking = useBookingKit(props);\n const { kit } = booking;\n const { labels, config } = kit;\n const { profile, bookings, actions, phone, stylists = [] } = props;\n const [now] = useState(() => props.now ?? Date.now());\n const bookingHref = props.bookingHref ?? config.handoffUrl ?? config.paths.booking;\n const kids = createChildSummaries(kit.clock)(\n profile.family,\n bookings.past,\n now,\n bookings.upcoming\n );\n const stylistNames = Object.fromEntries(stylists.map((stylist) => [stylist.id, stylist.name]));\n const vippsFlash = props.vippsFlash ?? null;\n const familyHeading = labels['portal.family.heading'];\n\n return (\n <>\n <SessionTouch config={config} />\n <PortalSections\n booking={booking}\n // A link attempt that just returned opens on Profile, where its message is.\n initialTab={vippsFlash !== null ? 'profile' : props.tab}\n tabParam={props.tabParam}\n tabSlugs={props.tabSlugs}\n icons={props.icons}\n header={\n props.header ?? (\n <header className=\"flex flex-wrap items-end justify-between gap-4\">\n <div className=\"space-y-1\">\n <p className=\"text-xs font-bold tracking-widest text-muted-foreground\">\n {labels['portal.eyebrow']}\n </p>\n <h1 className=\"font-sans text-3xl font-bold md:text-5xl\">\n {fillParts(labels['portal.greeting'], {\n name: profile.firstName ?? labelText(labels['portal.greetingFallback']),\n })}\n </h1>\n </div>\n <a href={bookingHref} className={bookingButtonClass({ size: 'lg' })}>\n {labels['portal.book']}\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"size-4\"\n >\n <path d=\"M5 12h14M12 5l7 7-7 7\" />\n </svg>\n </a>\n </header>\n )\n }\n account={\n <AccountCard profile={profile} compact classNames={booking.classNames.accountCard} />\n }\n logout={\n <PortalLogoutButton\n booking={booking}\n actions={actions}\n logoutHref={props.logoutHref ?? '/'}\n />\n }\n overview={\n <>\n <UpcomingBookings\n bookings={bookings.upcoming}\n phone={phone}\n bookingHref={bookingHref}\n now={now}\n labels={screenLabels(labels)}\n format={kit.format}\n classNames={booking.classNames.upcoming}\n components={booking.components}\n />\n {/* Always drawn — with a sentence when there is nobody yet — so the\n overview keeps its shape for a new visitor. */}\n <section aria-labelledby=\"portal-children-summary\" className=\"space-y-4\">\n <Heading id=\"portal-children-summary\">{familyHeading}</Heading>\n <PortalChildCards\n booking={booking}\n kids={kids}\n variant=\"compact\"\n bookingHref={bookingHref}\n />\n </section>\n <PortalRebookCards\n booking={booking}\n suggestions={rebookSuggestions(bookings.past)}\n bookingHref={bookingHref}\n />\n </>\n }\n family={\n <>\n <section aria-labelledby=\"portal-children-heading\" className=\"space-y-4\">\n <Heading id=\"portal-children-heading\">{familyHeading}</Heading>\n <p className=\"text-muted-foreground\">{labels['portal.family.lead']}</p>\n {/* No empty sentence: the editor right below says so, and is where one is added. */}\n <PortalChildCards\n booking={booking}\n kids={kids}\n bookingHref={bookingHref}\n empty={null}\n stylistNames={stylistNames}\n />\n </section>\n <PortalFamilyEditor\n booking={booking}\n family={profile.family}\n nouns={profile.labels}\n personDetails={profile.personDetails}\n stylists={stylists}\n dismissed={props.agePromptDismissed ?? []}\n cookieName={props.agePromptCookie ?? agePromptCookieName(config)}\n now={now}\n actions={actions}\n />\n </>\n }\n history={\n <VisitHistory\n past={bookings.past}\n labels={screenLabels(labels)}\n format={kit.format}\n classNames={booking.classNames.history}\n components={booking.components}\n />\n }\n profile={\n <>\n <AccountCard profile={profile} classNames={booking.classNames.accountCard} />\n <PortalProfileForm booking={booking} profile={profile} actions={actions} />\n {/* Only where the backend says whether Vipps is linked, and the app can start a link. */}\n {profile.vippsLinked !== undefined && actions.startVippsLink && (\n <PortalVippsLinkRow\n booking={booking}\n linked={profile.vippsLinked}\n flash={vippsFlash}\n startVippsLink={actions.startVippsLink}\n onToast={props.onToast}\n />\n )}\n <PortalDataControls\n booking={booking}\n actions={actions}\n deletedHref={props.deletedHref ?? config.paths.portalLogin}\n />\n </>\n }\n />\n </>\n );\n}\n\nexport interface PortalDashboardUnreachableProps\n extends BookingOverrides,\n Pick<Exits, 'logoutHref'> {\n phone: string | null;\n actions: Pick<PortalActions, 'logout'>;\n}\n\n/**\n * What the portal shows when the backend did not answer: «try again» (this\n * page) and the phone — and a logout, because this is the one screen a\n * visitor could otherwise be held on.\n */\nexport function PortalDashboardUnreachable(props: PortalDashboardUnreachableProps) {\n const booking = useBookingKit(props);\n const { kit } = booking;\n return (\n <PortalUnreachable\n labels={screenLabels(kit.labels)}\n format={kit.format}\n phone={props.phone}\n retryHref={kit.config.paths.portal ?? '/'}\n classNames={booking.classNames.portalUnreachable}\n logout={\n <PortalLogoutButton\n booking={booking}\n actions={props.actions}\n logoutHref={props.logoutHref ?? '/'}\n />\n }\n />\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/react/portal/leave.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Leave the portal with ONE full document load — after a logout or a delete.
|
|
4
|
+
*
|
|
5
|
+
* A `router.push` + `router.refresh` pair is two round-trips, and a push to a
|
|
6
|
+
* page under another root layout ends in a hard load anyway. A document load
|
|
7
|
+
* is a fresh page and drops the router cache holding the old dashboard in one
|
|
8
|
+
* go. `replace` for the delete, so Back cannot return to a dashboard whose
|
|
9
|
+
* account is gone; `assign` for the logout, where Back simply meets the login.
|
|
10
|
+
*
|
|
11
|
+
* Its own module so a test can stand in for `window.location`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function leavePortal(path: string, { replace }?: {
|
|
14
|
+
replace?: boolean;
|
|
15
|
+
}): void;
|
|
16
|
+
//#endregion
|
|
17
|
+
//# sourceMappingURL=leave.d.mts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
//#region src/react/portal/leave.ts
|
|
3
|
+
/**
|
|
4
|
+
* Leave the portal with ONE full document load — after a logout or a delete.
|
|
5
|
+
*
|
|
6
|
+
* A `router.push` + `router.refresh` pair is two round-trips, and a push to a
|
|
7
|
+
* page under another root layout ends in a hard load anyway. A document load
|
|
8
|
+
* is a fresh page and drops the router cache holding the old dashboard in one
|
|
9
|
+
* go. `replace` for the delete, so Back cannot return to a dashboard whose
|
|
10
|
+
* account is gone; `assign` for the logout, where Back simply meets the login.
|
|
11
|
+
*
|
|
12
|
+
* Its own module so a test can stand in for `window.location`.
|
|
13
|
+
*/
|
|
14
|
+
function leavePortal(path, { replace = false } = {}) {
|
|
15
|
+
if (replace) window.location.replace(path);
|
|
16
|
+
else window.location.assign(path);
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { leavePortal };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=leave.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leave.mjs","names":[],"sources":["../../../src/react/portal/leave.ts"],"sourcesContent":["/**\n * Leave the portal with ONE full document load — after a logout or a delete.\n *\n * A `router.push` + `router.refresh` pair is two round-trips, and a push to a\n * page under another root layout ends in a hard load anyway. A document load\n * is a fresh page and drops the router cache holding the old dashboard in one\n * go. `replace` for the delete, so Back cannot return to a dashboard whose\n * account is gone; `assign` for the logout, where Back simply meets the login.\n *\n * Its own module so a test can stand in for `window.location`.\n */\nexport function leavePortal(path: string, { replace = false }: { replace?: boolean } = {}): void {\n if (replace) window.location.replace(path);\n else window.location.assign(path);\n}\n"],"mappings":";;;;;;;;;;;;;AAWA,SAAgB,YAAY,MAAc,EAAE,UAAU,UAAiC,CAAC,GAAS;CAC/F,IAAI,SAAS,OAAO,SAAS,QAAQ,IAAI;MACpC,OAAO,SAAS,OAAO,IAAI;AAClC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import "../Provider.mjs";
|
|
2
|
+
import { PortalTab } from "./tabs.mjs";
|
|
3
|
+
import { PortalShellIcon } from "@medalsocial/meda/booking";
|
|
4
|
+
import { ReactNode } from "react";
|
|
5
|
+
//#region src/react/portal/sections.d.ts
|
|
6
|
+
/** The icons a tab may carry, rail and bottom bar. */
|
|
7
|
+
export type PortalTabIcons = Partial<Record<PortalTab, {
|
|
8
|
+
icon?: PortalShellIcon;
|
|
9
|
+
shortIcon?: PortalShellIcon;
|
|
10
|
+
}>>;
|
|
11
|
+
//#endregion
|
|
12
|
+
//# sourceMappingURL=sections.d.mts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { labelText } from "../../core/labels.mjs";
|
|
3
|
+
import { screenLabels } from "../screen-labels.mjs";
|
|
4
|
+
import { DEFAULT_PORTAL_TAB, DEFAULT_PORTAL_TAB_PARAM, DEFAULT_PORTAL_TAB_SLUGS } from "./tabs.mjs";
|
|
5
|
+
import { PortalShell } from "@medalsocial/meda/booking";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
//#region src/react/portal/sections.tsx
|
|
8
|
+
/**
|
|
9
|
+
* The portal's four sections in meda's shell, with the open one in the URL so
|
|
10
|
+
* a reload or a link lands on it. Written with `history.replaceState`, not a
|
|
11
|
+
* router navigation: the page is per-request, and a router replace would
|
|
12
|
+
* render it on the server again to move a `hidden` attribute. Replace, not
|
|
13
|
+
* push: a tab is not a page, and Back should leave the portal.
|
|
14
|
+
*/
|
|
15
|
+
function PortalSections({ booking, header, account, logout, initialTab = DEFAULT_PORTAL_TAB, tabParam = DEFAULT_PORTAL_TAB_PARAM, tabSlugs = DEFAULT_PORTAL_TAB_SLUGS, icons, ...sections }) {
|
|
16
|
+
const { labels } = booking.kit;
|
|
17
|
+
const tab = (id, short) => ({
|
|
18
|
+
id,
|
|
19
|
+
label: labelText(labels[`portal.tab.${id}`]),
|
|
20
|
+
short,
|
|
21
|
+
...icons?.[id],
|
|
22
|
+
content: sections[id]
|
|
23
|
+
});
|
|
24
|
+
return /* @__PURE__ */ jsx(PortalShell, {
|
|
25
|
+
labels: screenLabels(labels),
|
|
26
|
+
classNames: booking.classNames.portalShell,
|
|
27
|
+
header,
|
|
28
|
+
account,
|
|
29
|
+
logout,
|
|
30
|
+
defaultTab: initialTab,
|
|
31
|
+
onTabChange: (next) => {
|
|
32
|
+
const url = new URL(window.location.href);
|
|
33
|
+
if (next === "overview") url.searchParams.delete(tabParam);
|
|
34
|
+
else url.searchParams.set(tabParam, tabSlugs[next]);
|
|
35
|
+
window.history.replaceState(null, "", url);
|
|
36
|
+
},
|
|
37
|
+
tabs: [
|
|
38
|
+
tab("overview", labelText(labels["portal.tab.overviewShort"])),
|
|
39
|
+
tab("family"),
|
|
40
|
+
tab("history"),
|
|
41
|
+
tab("profile")
|
|
42
|
+
]
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { PortalSections };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=sections.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sections.mjs","names":[],"sources":["../../../src/react/portal/sections.tsx"],"sourcesContent":["import { PortalShell, type PortalShellIcon } from '@medalsocial/meda/booking';\nimport type { ReactNode } from 'react';\nimport { labelText } from '../../core/labels';\nimport type { ResolvedBooking } from '../Provider';\nimport { screenLabels } from '../screen-labels';\nimport {\n DEFAULT_PORTAL_TAB,\n DEFAULT_PORTAL_TAB_PARAM,\n DEFAULT_PORTAL_TAB_SLUGS,\n type PortalTab,\n} from './tabs';\n\n/** The icons a tab may carry, rail and bottom bar. */\nexport type PortalTabIcons = Partial<\n Record<PortalTab, { icon?: PortalShellIcon; shortIcon?: PortalShellIcon }>\n>;\n\nexport interface PortalSectionsProps extends Record<PortalTab, ReactNode> {\n booking: ResolvedBooking;\n header?: ReactNode;\n account?: ReactNode;\n logout?: ReactNode;\n initialTab?: PortalTab;\n /** The query parameter the open section rides in. Default `fane`. */\n tabParam?: string;\n tabSlugs?: Readonly<Record<PortalTab, string>>;\n icons?: PortalTabIcons;\n}\n\n/**\n * The portal's four sections in meda's shell, with the open one in the URL so\n * a reload or a link lands on it. Written with `history.replaceState`, not a\n * router navigation: the page is per-request, and a router replace would\n * render it on the server again to move a `hidden` attribute. Replace, not\n * push: a tab is not a page, and Back should leave the portal.\n */\nexport function PortalSections({\n booking,\n header,\n account,\n logout,\n initialTab = DEFAULT_PORTAL_TAB,\n tabParam = DEFAULT_PORTAL_TAB_PARAM,\n tabSlugs = DEFAULT_PORTAL_TAB_SLUGS,\n icons,\n ...sections\n}: PortalSectionsProps) {\n const { labels } = booking.kit;\n const tab = (id: PortalTab, short?: string) => ({\n id,\n label: labelText(labels[`portal.tab.${id}`]),\n short,\n ...icons?.[id],\n content: sections[id],\n });\n return (\n <PortalShell<PortalTab>\n labels={screenLabels(labels)}\n classNames={booking.classNames.portalShell}\n header={header}\n account={account}\n logout={logout}\n defaultTab={initialTab}\n onTabChange={(next) => {\n const url = new URL(window.location.href);\n if (next === DEFAULT_PORTAL_TAB) url.searchParams.delete(tabParam);\n else url.searchParams.set(tabParam, tabSlugs[next]);\n window.history.replaceState(null, '', url);\n }}\n tabs={[\n tab('overview', labelText(labels['portal.tab.overviewShort'])),\n tab('family'),\n tab('history'),\n tab('profile'),\n ]}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAoCA,SAAgB,eAAe,EAC7B,SACA,QACA,SACA,QACA,aAAa,oBACb,WAAW,0BACX,WAAW,0BACX,OACA,GAAG,YACmB;CACtB,MAAM,EAAE,WAAW,QAAQ;CAC3B,MAAM,OAAO,IAAe,WAAoB;EAC9C;EACA,OAAO,UAAU,OAAO,cAAc,KAAK;EAC3C;EACA,GAAG,QAAQ;EACX,SAAS,SAAS;CACpB;CACA,OACE,oBAAC,aAAD;EACE,QAAQ,aAAa,MAAM;EAC3B,YAAY,QAAQ,WAAW;EACvB;EACC;EACD;EACR,YAAY;EACZ,cAAc,SAAS;GACrB,MAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;GACxC,IAAI,SAAA,YAA6B,IAAI,aAAa,OAAO,QAAQ;QAC5D,IAAI,aAAa,IAAI,UAAU,SAAS,KAAK;GAClD,OAAO,QAAQ,aAAa,MAAM,IAAI,GAAG;EAC3C;EACA,MAAM;GACJ,IAAI,YAAY,UAAU,OAAO,2BAA2B,CAAC;GAC7D,IAAI,QAAQ;GACZ,IAAI,SAAS;GACb,IAAI,SAAS;EACf;CACD,CAAA;AAEL"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/react/portal/tabs.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The portal's four sections, and how they are spelled in the URL.
|
|
4
|
+
*
|
|
5
|
+
* Free of `'use client'`, so the server page can call `parsePortalTab` on the
|
|
6
|
+
* query and the FIRST paint is already the right section; the dashboard
|
|
7
|
+
* writes it back as the visitor moves between them.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PORTAL_TABS: readonly ['overview', 'family', 'history', 'profile'];
|
|
10
|
+
export type PortalTab = (typeof PORTAL_TABS)[number];
|
|
11
|
+
/** The section with no parameter — and the answer to any value that is not one. */
|
|
12
|
+
export declare const DEFAULT_PORTAL_TAB: PortalTab;
|
|
13
|
+
/** The query parameter the open section rides in (`?fane=…`). */
|
|
14
|
+
export declare const DEFAULT_PORTAL_TAB_PARAM = "fane";
|
|
15
|
+
/** Each section's value in that parameter. */
|
|
16
|
+
export declare const DEFAULT_PORTAL_TAB_SLUGS: Readonly<Record<PortalTab, string>>;
|
|
17
|
+
/**
|
|
18
|
+
* The parameter's value as a section. Exact spellings only; anything else — a
|
|
19
|
+
* typo, a repeated parameter, a value from an old link — opens the overview.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parsePortalTab(value: unknown, slugs?: Readonly<Record<PortalTab, string>>): PortalTab;
|
|
22
|
+
//#endregion
|
|
23
|
+
//# sourceMappingURL=tabs.d.mts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/react/portal/tabs.ts
|
|
2
|
+
/**
|
|
3
|
+
* The portal's four sections, and how they are spelled in the URL.
|
|
4
|
+
*
|
|
5
|
+
* Free of `'use client'`, so the server page can call `parsePortalTab` on the
|
|
6
|
+
* query and the FIRST paint is already the right section; the dashboard
|
|
7
|
+
* writes it back as the visitor moves between them.
|
|
8
|
+
*/
|
|
9
|
+
const PORTAL_TABS = [
|
|
10
|
+
"overview",
|
|
11
|
+
"family",
|
|
12
|
+
"history",
|
|
13
|
+
"profile"
|
|
14
|
+
];
|
|
15
|
+
/** The section with no parameter — and the answer to any value that is not one. */
|
|
16
|
+
const DEFAULT_PORTAL_TAB = "overview";
|
|
17
|
+
/** The query parameter the open section rides in (`?fane=…`). */
|
|
18
|
+
const DEFAULT_PORTAL_TAB_PARAM = "fane";
|
|
19
|
+
/** Each section's value in that parameter. */
|
|
20
|
+
const DEFAULT_PORTAL_TAB_SLUGS = {
|
|
21
|
+
overview: "oversikt",
|
|
22
|
+
family: "barn",
|
|
23
|
+
history: "historikk",
|
|
24
|
+
profile: "profil"
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The parameter's value as a section. Exact spellings only; anything else — a
|
|
28
|
+
* typo, a repeated parameter, a value from an old link — opens the overview.
|
|
29
|
+
*/
|
|
30
|
+
function parsePortalTab(value, slugs = DEFAULT_PORTAL_TAB_SLUGS) {
|
|
31
|
+
return PORTAL_TABS.find((tab) => typeof value === "string" && slugs[tab] === value) ?? "overview";
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { DEFAULT_PORTAL_TAB, DEFAULT_PORTAL_TAB_PARAM, DEFAULT_PORTAL_TAB_SLUGS, PORTAL_TABS, parsePortalTab };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=tabs.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabs.mjs","names":[],"sources":["../../../src/react/portal/tabs.ts"],"sourcesContent":["/**\n * The portal's four sections, and how they are spelled in the URL.\n *\n * Free of `'use client'`, so the server page can call `parsePortalTab` on the\n * query and the FIRST paint is already the right section; the dashboard\n * writes it back as the visitor moves between them.\n */\n\nexport const PORTAL_TABS = ['overview', 'family', 'history', 'profile'] as const;\n\nexport type PortalTab = (typeof PORTAL_TABS)[number];\n\n/** The section with no parameter — and the answer to any value that is not one. */\nexport const DEFAULT_PORTAL_TAB: PortalTab = 'overview';\n\n/** The query parameter the open section rides in (`?fane=…`). */\nexport const DEFAULT_PORTAL_TAB_PARAM = 'fane';\n\n/** Each section's value in that parameter. */\nexport const DEFAULT_PORTAL_TAB_SLUGS: Readonly<Record<PortalTab, string>> = {\n overview: 'oversikt',\n family: 'barn',\n history: 'historikk',\n profile: 'profil',\n};\n\n/**\n * The parameter's value as a section. Exact spellings only; anything else — a\n * typo, a repeated parameter, a value from an old link — opens the overview.\n */\nexport function parsePortalTab(\n value: unknown,\n slugs: Readonly<Record<PortalTab, string>> = DEFAULT_PORTAL_TAB_SLUGS\n): PortalTab {\n return (\n PORTAL_TABS.find((tab) => typeof value === 'string' && slugs[tab] === value) ??\n DEFAULT_PORTAL_TAB\n );\n}\n"],"mappings":";;;;;;;;AAQA,MAAa,cAAc;CAAC;CAAY;CAAU;CAAW;AAAS;;AAKtE,MAAa,qBAAgC;;AAG7C,MAAa,2BAA2B;;AAGxC,MAAa,2BAAgE;CAC3E,UAAU;CACV,QAAQ;CACR,SAAS;CACT,SAAS;AACX;;;;;AAMA,SAAgB,eACd,OACA,QAA6C,0BAClC;CACX,OACE,YAAY,MAAM,QAAQ,OAAO,UAAU,YAAY,MAAM,SAAS,KAAK,KAAA;AAG/E"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BookingConfig } from "../../core/config.mjs";
|
|
2
|
+
import { VippsLinkFlash as VippsLinkFlash$1 } from "@medalsocial/meda/booking";
|
|
3
|
+
//#region src/react/portal/vipps-flash.d.ts
|
|
4
|
+
export declare const VIPPS_LINK_FLASHES: readonly VippsLinkFlash$1[];
|
|
5
|
+
/** A cookie value as a flash, or `null` for anything else. */
|
|
6
|
+
export declare function vippsLinkFlash(value: unknown): VippsLinkFlash$1 | null;
|
|
7
|
+
/** Where the flash is spent (`DELETE`), so a reload says nothing again. */
|
|
8
|
+
export declare function vippsFlashPath(config: Pick<BookingConfig, 'paths'>): string;
|
|
9
|
+
//#endregion
|
|
10
|
+
export type { VippsLinkFlash$1 as VippsLinkFlash };
|
|
11
|
+
//# sourceMappingURL=vipps-flash.d.mts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/react/portal/vipps-flash.ts
|
|
2
|
+
const VIPPS_LINK_FLASHES = [
|
|
3
|
+
"linked",
|
|
4
|
+
"link_conflict",
|
|
5
|
+
"link_failed"
|
|
6
|
+
];
|
|
7
|
+
/** A cookie value as a flash, or `null` for anything else. */
|
|
8
|
+
function vippsLinkFlash(value) {
|
|
9
|
+
return VIPPS_LINK_FLASHES.find((flash) => flash === value) ?? null;
|
|
10
|
+
}
|
|
11
|
+
/** Where the flash is spent (`DELETE`), so a reload says nothing again. */
|
|
12
|
+
function vippsFlashPath(config) {
|
|
13
|
+
return `${config.paths.portalApi}/vipps/flash`;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { VIPPS_LINK_FLASHES, vippsFlashPath, vippsLinkFlash };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=vipps-flash.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vipps-flash.mjs","names":[],"sources":["../../../src/react/portal/vipps-flash.ts"],"sourcesContent":["import type { VippsLinkFlash } from '@medalsocial/meda/booking';\nimport type { BookingConfig } from '../../core/config';\n\n/**\n * How a «link Vipps» attempt ended, as the profile reads it. The outcome\n * never travels in a URL: the return route sets an httpOnly flash cookie and\n * lands on a bare portal page, the server page reads it and hands it to the\n * dashboard, and the dashboard spends it. Free of `'use client'`.\n */\n\nexport type { VippsLinkFlash };\n\nexport const VIPPS_LINK_FLASHES: readonly VippsLinkFlash[] = [\n 'linked',\n 'link_conflict',\n 'link_failed',\n];\n\n/** A cookie value as a flash, or `null` for anything else. */\nexport function vippsLinkFlash(value: unknown): VippsLinkFlash | null {\n return VIPPS_LINK_FLASHES.find((flash) => flash === value) ?? null;\n}\n\n/** Where the flash is spent (`DELETE`), so a reload says nothing again. */\nexport function vippsFlashPath(config: Pick<BookingConfig, 'paths'>): string {\n return `${config.paths.portalApi}/vipps/flash`;\n}\n"],"mappings":";AAYA,MAAa,qBAAgD;CAC3D;CACA;CACA;AACF;;AAGA,SAAgB,eAAe,OAAuC;CACpE,OAAO,mBAAmB,MAAM,UAAU,UAAU,KAAK,KAAK;AAChE;;AAGA,SAAgB,eAAe,QAA8C;CAC3E,OAAO,GAAG,OAAO,MAAM,UAAU;AACnC"}
|