keystone-design-bootstrap 1.0.79 → 1.0.80
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/package.json
CHANGED
|
@@ -41,6 +41,15 @@ export interface PortalPageProps {
|
|
|
41
41
|
portalHref?: string;
|
|
42
42
|
bookingHref?: string | null;
|
|
43
43
|
bookingLabel?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Controls which tabs are rendered in the portal header.
|
|
46
|
+
* Default: ['book', 'services', 'packages', 'specials', 'messages']
|
|
47
|
+
*/
|
|
48
|
+
enabledTabs?: Tab[];
|
|
49
|
+
/**
|
|
50
|
+
* Optional per-tab label overrides, e.g. { messages: 'Chat with Us' }.
|
|
51
|
+
*/
|
|
52
|
+
tabLabels?: Partial<Record<Tab, string>>;
|
|
44
53
|
/**
|
|
45
54
|
* When true, always opens booking in a new tab even if the URL supports iframes.
|
|
46
55
|
* Useful when the booking page's iframe experience is broken or undesirable.
|
|
@@ -187,10 +196,12 @@ function EmptyState({ message }: { message: string }) {
|
|
|
187
196
|
function ServiceItemRow({
|
|
188
197
|
item,
|
|
189
198
|
isLoggedIn,
|
|
199
|
+
specialsEnabled,
|
|
190
200
|
specialsHref,
|
|
191
201
|
}: {
|
|
192
202
|
item: ServiceItem;
|
|
193
203
|
isLoggedIn: boolean;
|
|
204
|
+
specialsEnabled: boolean;
|
|
194
205
|
specialsHref: string;
|
|
195
206
|
}) {
|
|
196
207
|
const activeOffers = (item.offers ?? []).filter((o) => o.active !== false && !o.expired);
|
|
@@ -208,7 +219,7 @@ function ServiceItemRow({
|
|
|
208
219
|
{item.duration_minutes != null && item.duration_minutes > 0 && (
|
|
209
220
|
<p className="mt-0.5 text-xs text-quaternary">{item.duration_minutes} min</p>
|
|
210
221
|
)}
|
|
211
|
-
{activeOffers.length > 0 && (
|
|
222
|
+
{specialsEnabled && activeOffers.length > 0 && (
|
|
212
223
|
<div className="mt-1.5 flex flex-wrap gap-1">
|
|
213
224
|
{activeOffers.map((offer) =>
|
|
214
225
|
isLoggedIn ? (
|
|
@@ -252,10 +263,12 @@ function ServiceItemRow({
|
|
|
252
263
|
function ServicesPanel({
|
|
253
264
|
services,
|
|
254
265
|
isLoggedIn,
|
|
266
|
+
specialsEnabled,
|
|
255
267
|
portalHref,
|
|
256
268
|
}: {
|
|
257
269
|
services: Service[];
|
|
258
270
|
isLoggedIn: boolean;
|
|
271
|
+
specialsEnabled: boolean;
|
|
259
272
|
portalHref: string;
|
|
260
273
|
}) {
|
|
261
274
|
const activeServices = services.filter((s) => (s.service_items?.length ?? 0) > 0);
|
|
@@ -292,6 +305,7 @@ function ServicesPanel({
|
|
|
292
305
|
key={item.id}
|
|
293
306
|
item={item}
|
|
294
307
|
isLoggedIn={isLoggedIn}
|
|
308
|
+
specialsEnabled={specialsEnabled}
|
|
295
309
|
specialsHref={specialsHref}
|
|
296
310
|
/>
|
|
297
311
|
))}
|
|
@@ -309,10 +323,12 @@ function ServicesPanel({
|
|
|
309
323
|
function PackagesPanel({
|
|
310
324
|
packages,
|
|
311
325
|
isLoggedIn,
|
|
326
|
+
specialsEnabled,
|
|
312
327
|
portalHref,
|
|
313
328
|
}: {
|
|
314
329
|
packages: Package[];
|
|
315
330
|
isLoggedIn: boolean;
|
|
331
|
+
specialsEnabled: boolean;
|
|
316
332
|
portalHref: string;
|
|
317
333
|
}) {
|
|
318
334
|
const specialsHref = `${portalHref}?tab=specials`;
|
|
@@ -365,7 +381,7 @@ function PackagesPanel({
|
|
|
365
381
|
</ul>
|
|
366
382
|
)}
|
|
367
383
|
|
|
368
|
-
{activeOffers.length > 0 && (
|
|
384
|
+
{specialsEnabled && activeOffers.length > 0 && (
|
|
369
385
|
<div className="flex flex-wrap gap-1.5">
|
|
370
386
|
{activeOffers.map((offer) =>
|
|
371
387
|
isLoggedIn ? (
|
|
@@ -559,19 +575,20 @@ function BookPanel({
|
|
|
559
575
|
|
|
560
576
|
// ─── Main Portal Page (Self-Contained Async Server Component) ─────────────────
|
|
561
577
|
|
|
562
|
-
const
|
|
563
|
-
type Tab = (typeof
|
|
578
|
+
const ALL_TABS = ['services', 'packages', 'specials', 'messages', 'book'] as const;
|
|
579
|
+
type Tab = (typeof ALL_TABS)[number];
|
|
564
580
|
|
|
565
581
|
export async function PortalPage({
|
|
566
582
|
searchParams,
|
|
567
583
|
portalHref = '/portal',
|
|
568
584
|
bookingHref,
|
|
569
585
|
bookingLabel = 'Book Now',
|
|
586
|
+
enabledTabs,
|
|
587
|
+
tabLabels,
|
|
570
588
|
forceExternalBooking = false,
|
|
571
589
|
contactHref = '/contact',
|
|
572
590
|
}: PortalPageProps) {
|
|
573
591
|
const params = await searchParams ?? {};
|
|
574
|
-
const tabParam = VALID_TABS.includes(params.tab as Tab) ? (params.tab as Tab) : null;
|
|
575
592
|
const contactIdParsed = params.contact ? parseInt(params.contact, 10) : NaN;
|
|
576
593
|
const contactId = Number.isFinite(contactIdParsed) ? contactIdParsed : null;
|
|
577
594
|
|
|
@@ -591,15 +608,32 @@ export async function PortalPage({
|
|
|
591
608
|
const resolvedBookingHref =
|
|
592
609
|
bookingHref ?? companyInformation?.external_management_url ?? null;
|
|
593
610
|
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
611
|
+
const hasCustomTabOrder = Array.isArray(enabledTabs) && enabledTabs.length > 0;
|
|
612
|
+
const requestedTabs = (enabledTabs ?? ['book', 'services', 'packages', 'specials', 'messages']).filter((tab, index, arr) => (
|
|
613
|
+
ALL_TABS.includes(tab) && arr.indexOf(tab) === index
|
|
614
|
+
));
|
|
615
|
+
const normalizedTabs = requestedTabs.length > 0 ? requestedTabs : ['book', 'services', 'packages', 'specials', 'messages'];
|
|
616
|
+
const nonBookTabs = normalizedTabs.filter((tab) => tab !== 'book');
|
|
617
|
+
const includesBookTab = normalizedTabs.includes('book');
|
|
618
|
+
|
|
619
|
+
const labelForTab = (tab: Tab): string => {
|
|
620
|
+
if (tab === 'book') return tabLabels?.book ?? bookingLabel;
|
|
621
|
+
if (tab === 'services') return tabLabels?.services ?? 'Services';
|
|
622
|
+
if (tab === 'packages') return tabLabels?.packages ?? 'Packages';
|
|
623
|
+
if (tab === 'specials') return tabLabels?.specials ?? 'Specials';
|
|
624
|
+
return tabLabels?.messages ?? 'Messages';
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// Keep existing behavior: when booking is configured, "Book Now" appears first; otherwise last.
|
|
628
|
+
const tabs: Array<{ id: Tab; label: string }> = hasCustomTabOrder
|
|
629
|
+
? normalizedTabs.map((tab) => ({ id: tab, label: labelForTab(tab) }))
|
|
630
|
+
: [
|
|
631
|
+
...(includesBookTab && resolvedBookingHref ? [{ id: 'book' as Tab, label: labelForTab('book') }] : []),
|
|
632
|
+
...nonBookTabs.map((tab) => ({ id: tab, label: labelForTab(tab) })),
|
|
633
|
+
...(includesBookTab && !resolvedBookingHref ? [{ id: 'book' as Tab, label: labelForTab('book') }] : []),
|
|
634
|
+
];
|
|
635
|
+
const selectableTabIds = tabs.map((tab) => tab.id);
|
|
636
|
+
const tabParam = selectableTabIds.includes(params.tab as Tab) ? (params.tab as Tab) : null;
|
|
603
637
|
|
|
604
638
|
// Default to the first tab so tab order and default selection are always in sync.
|
|
605
639
|
const tab: Tab = tabParam ?? tabs[0].id;
|
|
@@ -641,6 +675,7 @@ export async function PortalPage({
|
|
|
641
675
|
const serviceList = services ?? [];
|
|
642
676
|
const packageList = packages ?? [];
|
|
643
677
|
const specials = aggregateSpecials(serviceList, packageList);
|
|
678
|
+
const specialsEnabled = selectableTabIds.includes('specials');
|
|
644
679
|
|
|
645
680
|
return (
|
|
646
681
|
<div className="min-h-screen bg-primary">
|
|
@@ -724,10 +759,20 @@ export async function PortalPage({
|
|
|
724
759
|
{/* Tab Content */}
|
|
725
760
|
<div className="mx-auto max-w-4xl px-4 py-8">
|
|
726
761
|
{tab === 'services' && (
|
|
727
|
-
<ServicesPanel
|
|
762
|
+
<ServicesPanel
|
|
763
|
+
services={serviceList}
|
|
764
|
+
isLoggedIn={isLoggedIn}
|
|
765
|
+
specialsEnabled={specialsEnabled}
|
|
766
|
+
portalHref={portalHref}
|
|
767
|
+
/>
|
|
728
768
|
)}
|
|
729
769
|
{tab === 'packages' && (
|
|
730
|
-
<PackagesPanel
|
|
770
|
+
<PackagesPanel
|
|
771
|
+
packages={packageList}
|
|
772
|
+
isLoggedIn={isLoggedIn}
|
|
773
|
+
specialsEnabled={specialsEnabled}
|
|
774
|
+
portalHref={portalHref}
|
|
775
|
+
/>
|
|
731
776
|
)}
|
|
732
777
|
{tab === 'specials' && (
|
|
733
778
|
isLoggedIn ? (
|