@voyant-travel/catalog-react 0.117.2
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/LICENSE +201 -0
- package/README.md +36 -0
- package/dist/admin/catalog-vertical-host.d.ts +45 -0
- package/dist/admin/catalog-vertical-host.d.ts.map +1 -0
- package/dist/admin/catalog-vertical-host.js +230 -0
- package/dist/admin/cruise-detail-host.d.ts +11 -0
- package/dist/admin/cruise-detail-host.d.ts.map +1 -0
- package/dist/admin/cruise-detail-host.js +33 -0
- package/dist/admin/dynamic-catalog-host.d.ts +13 -0
- package/dist/admin/dynamic-catalog-host.d.ts.map +1 -0
- package/dist/admin/dynamic-catalog-host.js +17 -0
- package/dist/admin/index.d.ts +133 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +144 -0
- package/dist/admin/open-in-new-tab.d.ts +7 -0
- package/dist/admin/open-in-new-tab.d.ts.map +1 -0
- package/dist/admin/open-in-new-tab.js +10 -0
- package/dist/admin/pages/catalog-accommodations-detail-page.d.ts +4 -0
- package/dist/admin/pages/catalog-accommodations-detail-page.d.ts.map +1 -0
- package/dist/admin/pages/catalog-accommodations-detail-page.js +7 -0
- package/dist/admin/pages/catalog-accommodations-index-page.d.ts +8 -0
- package/dist/admin/pages/catalog-accommodations-index-page.d.ts.map +1 -0
- package/dist/admin/pages/catalog-accommodations-index-page.js +17 -0
- package/dist/admin/pages/catalog-cruises-detail-page.d.ts +4 -0
- package/dist/admin/pages/catalog-cruises-detail-page.d.ts.map +1 -0
- package/dist/admin/pages/catalog-cruises-detail-page.js +7 -0
- package/dist/admin/pages/catalog-cruises-index-page.d.ts +8 -0
- package/dist/admin/pages/catalog-cruises-index-page.d.ts.map +1 -0
- package/dist/admin/pages/catalog-cruises-index-page.js +19 -0
- package/dist/admin/pages/catalog-excursions-detail-page.d.ts +4 -0
- package/dist/admin/pages/catalog-excursions-detail-page.d.ts.map +1 -0
- package/dist/admin/pages/catalog-excursions-detail-page.js +7 -0
- package/dist/admin/pages/catalog-excursions-index-page.d.ts +8 -0
- package/dist/admin/pages/catalog-excursions-index-page.d.ts.map +1 -0
- package/dist/admin/pages/catalog-excursions-index-page.js +12 -0
- package/dist/admin/pages/catalog-products-detail-page.d.ts +8 -0
- package/dist/admin/pages/catalog-products-detail-page.d.ts.map +1 -0
- package/dist/admin/pages/catalog-products-detail-page.js +12 -0
- package/dist/admin/pages/catalog-products-index-page.d.ts +8 -0
- package/dist/admin/pages/catalog-products-index-page.d.ts.map +1 -0
- package/dist/admin/pages/catalog-products-index-page.js +12 -0
- package/dist/admin/pages/catalog-tours-detail-page.d.ts +4 -0
- package/dist/admin/pages/catalog-tours-detail-page.d.ts.map +1 -0
- package/dist/admin/pages/catalog-tours-detail-page.js +7 -0
- package/dist/admin/pages/catalog-tours-index-page.d.ts +8 -0
- package/dist/admin/pages/catalog-tours-index-page.d.ts.map +1 -0
- package/dist/admin/pages/catalog-tours-index-page.js +12 -0
- package/dist/admin/product-detail-host.d.ts +18 -0
- package/dist/admin/product-detail-host.d.ts.map +1 -0
- package/dist/admin/product-detail-host.js +40 -0
- package/dist/admin/scheduled-catalog-host.d.ts +15 -0
- package/dist/admin/scheduled-catalog-host.d.ts.map +1 -0
- package/dist/admin/scheduled-catalog-host.js +19 -0
- package/dist/admin/vertical-detail-host.d.ts +13 -0
- package/dist/admin/vertical-detail-host.d.ts.map +1 -0
- package/dist/admin/vertical-detail-host.js +62 -0
- package/dist/booking-engine/index.d.ts +26 -0
- package/dist/booking-engine/index.d.ts.map +1 -0
- package/dist/booking-engine/index.js +25 -0
- package/dist/booking-engine/use-booking-commit.d.ts +61 -0
- package/dist/booking-engine/use-booking-commit.d.ts.map +1 -0
- package/dist/booking-engine/use-booking-commit.js +47 -0
- package/dist/booking-engine/use-booking-draft-shape.d.ts +20 -0
- package/dist/booking-engine/use-booking-draft-shape.d.ts.map +1 -0
- package/dist/booking-engine/use-booking-draft-shape.js +9 -0
- package/dist/booking-engine/use-booking-draft.d.ts +102 -0
- package/dist/booking-engine/use-booking-draft.d.ts.map +1 -0
- package/dist/booking-engine/use-booking-draft.js +93 -0
- package/dist/booking-engine/use-booking-hold.d.ts +30 -0
- package/dist/booking-engine/use-booking-hold.d.ts.map +1 -0
- package/dist/booking-engine/use-booking-hold.js +44 -0
- package/dist/booking-engine/use-booking-journey-api.d.ts +23 -0
- package/dist/booking-engine/use-booking-journey-api.d.ts.map +1 -0
- package/dist/booking-engine/use-booking-journey-api.js +24 -0
- package/dist/booking-engine/use-booking-quote.d.ts +711 -0
- package/dist/booking-engine/use-booking-quote.d.ts.map +1 -0
- package/dist/booking-engine/use-booking-quote.js +122 -0
- package/dist/catalog-enrichment-mappers.d.ts +162 -0
- package/dist/catalog-enrichment-mappers.d.ts.map +1 -0
- package/dist/catalog-enrichment-mappers.js +190 -0
- package/dist/catalog-enrichment.d.ts +203 -0
- package/dist/catalog-enrichment.d.ts.map +1 -0
- package/dist/catalog-enrichment.js +130 -0
- package/dist/catalog-offers-client.d.ts +58 -0
- package/dist/catalog-offers-client.d.ts.map +1 -0
- package/dist/catalog-offers-client.js +61 -0
- package/dist/catalog-search-params.d.ts +45 -0
- package/dist/catalog-search-params.d.ts.map +1 -0
- package/dist/catalog-search-params.js +30 -0
- package/dist/catalog-surfaces.d.ts +17 -0
- package/dist/catalog-surfaces.d.ts.map +1 -0
- package/dist/catalog-surfaces.js +26 -0
- package/dist/client.d.ts +20 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +65 -0
- package/dist/components/availability-calendar.d.ts +33 -0
- package/dist/components/availability-calendar.d.ts.map +1 -0
- package/dist/components/availability-calendar.js +65 -0
- package/dist/components/catalog-browse-page.d.ts +41 -0
- package/dist/components/catalog-browse-page.d.ts.map +1 -0
- package/dist/components/catalog-browse-page.js +47 -0
- package/dist/components/catalog-card.d.ts +68 -0
- package/dist/components/catalog-card.d.ts.map +1 -0
- package/dist/components/catalog-card.js +52 -0
- package/dist/components/catalog-detail-cruise-cards.d.ts +16 -0
- package/dist/components/catalog-detail-cruise-cards.d.ts.map +1 -0
- package/dist/components/catalog-detail-cruise-cards.js +54 -0
- package/dist/components/catalog-detail-departures.d.ts +25 -0
- package/dist/components/catalog-detail-departures.d.ts.map +1 -0
- package/dist/components/catalog-detail-departures.js +240 -0
- package/dist/components/catalog-detail-parts.d.ts +70 -0
- package/dist/components/catalog-detail-parts.d.ts.map +1 -0
- package/dist/components/catalog-detail-parts.js +282 -0
- package/dist/components/catalog-detail-sheet.d.ts +93 -0
- package/dist/components/catalog-detail-sheet.d.ts.map +1 -0
- package/dist/components/catalog-detail-sheet.js +68 -0
- package/dist/components/catalog-detail-view.d.ts +39 -0
- package/dist/components/catalog-detail-view.d.ts.map +1 -0
- package/dist/components/catalog-detail-view.js +157 -0
- package/dist/components/catalog-enrichment-fetchers.d.ts +8 -0
- package/dist/components/catalog-enrichment-fetchers.d.ts.map +1 -0
- package/dist/components/catalog-enrichment-fetchers.js +7 -0
- package/dist/components/catalog-faceted-filter.d.ts +30 -0
- package/dist/components/catalog-faceted-filter.d.ts.map +1 -0
- package/dist/components/catalog-faceted-filter.js +24 -0
- package/dist/components/catalog-filter-rail.d.ts +25 -0
- package/dist/components/catalog-filter-rail.d.ts.map +1 -0
- package/dist/components/catalog-filter-rail.js +88 -0
- package/dist/components/catalog-gallery.d.ts +27 -0
- package/dist/components/catalog-gallery.d.ts.map +1 -0
- package/dist/components/catalog-gallery.js +66 -0
- package/dist/components/catalog-hit.d.ts +27 -0
- package/dist/components/catalog-hit.d.ts.map +1 -0
- package/dist/components/catalog-hit.js +57 -0
- package/dist/components/catalog-page-cards.d.ts +21 -0
- package/dist/components/catalog-page-cards.d.ts.map +1 -0
- package/dist/components/catalog-page-cards.js +174 -0
- package/dist/components/catalog-page-config.d.ts +17 -0
- package/dist/components/catalog-page-config.d.ts.map +1 -0
- package/dist/components/catalog-page-config.js +369 -0
- package/dist/components/catalog-page.d.ts +88 -0
- package/dist/components/catalog-page.d.ts.map +1 -0
- package/dist/components/catalog-page.js +148 -0
- package/dist/components/catalog-range-filter.d.ts +34 -0
- package/dist/components/catalog-range-filter.d.ts.map +1 -0
- package/dist/components/catalog-range-filter.js +72 -0
- package/dist/components/catalog-search-page.d.ts +239 -0
- package/dist/components/catalog-search-page.d.ts.map +1 -0
- package/dist/components/catalog-search-page.js +63 -0
- package/dist/components/catalog-search-tab-panel.d.ts +42 -0
- package/dist/components/catalog-search-tab-panel.d.ts.map +1 -0
- package/dist/components/catalog-search-tab-panel.js +199 -0
- package/dist/components/catalog-vertical-detail-page.d.ts +33 -0
- package/dist/components/catalog-vertical-detail-page.d.ts.map +1 -0
- package/dist/components/catalog-vertical-detail-page.js +100 -0
- package/dist/components/cruise-detail-page-parts.d.ts +72 -0
- package/dist/components/cruise-detail-page-parts.d.ts.map +1 -0
- package/dist/components/cruise-detail-page-parts.js +146 -0
- package/dist/components/cruise-detail-page.d.ts +21 -0
- package/dist/components/cruise-detail-page.d.ts.map +1 -0
- package/dist/components/cruise-detail-page.js +201 -0
- package/dist/components/dynamic-catalog-page-parts.d.ts +40 -0
- package/dist/components/dynamic-catalog-page-parts.d.ts.map +1 -0
- package/dist/components/dynamic-catalog-page-parts.js +43 -0
- package/dist/components/dynamic-catalog-page.d.ts +23 -0
- package/dist/components/dynamic-catalog-page.d.ts.map +1 -0
- package/dist/components/dynamic-catalog-page.js +270 -0
- package/dist/components/media-gallery.d.ts +13 -0
- package/dist/components/media-gallery.d.ts.map +1 -0
- package/dist/components/media-gallery.js +42 -0
- package/dist/components/product-detail-page-parts.d.ts +106 -0
- package/dist/components/product-detail-page-parts.d.ts.map +1 -0
- package/dist/components/product-detail-page-parts.js +130 -0
- package/dist/components/product-detail-page.d.ts +57 -0
- package/dist/components/product-detail-page.d.ts.map +1 -0
- package/dist/components/product-detail-page.js +175 -0
- package/dist/components/scheduled-catalog-page.d.ts +34 -0
- package/dist/components/scheduled-catalog-page.d.ts.map +1 -0
- package/dist/components/scheduled-catalog-page.js +6 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/use-catalog-offers.d.ts +186 -0
- package/dist/hooks/use-catalog-offers.d.ts.map +1 -0
- package/dist/hooks/use-catalog-offers.js +105 -0
- package/dist/hooks/use-catalog-search.d.ts +109 -0
- package/dist/hooks/use-catalog-search.d.ts.map +1 -0
- package/dist/hooks/use-catalog-search.js +52 -0
- package/dist/i18n/en.d.ts +397 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +396 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +3 -0
- package/dist/i18n/messages.d.ts +335 -0
- package/dist/i18n/messages.d.ts.map +1 -0
- package/dist/i18n/messages.js +1 -0
- package/dist/i18n/provider.d.ts +816 -0
- package/dist/i18n/provider.d.ts.map +1 -0
- package/dist/i18n/provider.js +44 -0
- package/dist/i18n/ro.d.ts +397 -0
- package/dist/i18n/ro.d.ts.map +1 -0
- package/dist/i18n/ro.js +396 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +1 -0
- package/dist/schemas-catalog-offers.d.ts +290 -0
- package/dist/schemas-catalog-offers.d.ts.map +1 -0
- package/dist/schemas-catalog-offers.js +155 -0
- package/dist/schemas.d.ts +143 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +76 -0
- package/dist/ui.d.ts +19 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +18 -0
- package/package.json +150 -0
- package/src/styles.css +11 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { CatalogDeparturePricingRow, CatalogDetailEnrichment, CatalogSearchHit } from "../index.js";
|
|
3
|
+
export { CatalogDetailView, type CatalogDetailViewProps } from "./catalog-detail-view.js";
|
|
4
|
+
export interface CatalogDetailAction {
|
|
5
|
+
label: string;
|
|
6
|
+
onClick: (hit: CatalogSearchHit) => void;
|
|
7
|
+
variant?: "default" | "secondary" | "outline" | "ghost";
|
|
8
|
+
/**
|
|
9
|
+
* Optional predicate to hide the action for specific hits — e.g. only
|
|
10
|
+
* show "Open editor" when the hit is an owned product. Defaults to
|
|
11
|
+
* always-visible when omitted.
|
|
12
|
+
*/
|
|
13
|
+
visible?: (hit: CatalogSearchHit) => boolean;
|
|
14
|
+
}
|
|
15
|
+
export type { CatalogDetailEnrichment };
|
|
16
|
+
export type CatalogDetailSheetWidth = "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "6xl" | string;
|
|
17
|
+
export type CatalogDetailItineraryDay = NonNullable<CatalogDetailEnrichment["itinerary"]>[number];
|
|
18
|
+
export type CatalogDetailRenderSlot = (hit: CatalogSearchHit, enrichment: CatalogDetailEnrichment | null) => ReactNode;
|
|
19
|
+
export interface CatalogDetailSheetProps {
|
|
20
|
+
hit: CatalogSearchHit | null;
|
|
21
|
+
onOpenChange: (open: boolean) => void;
|
|
22
|
+
formatters?: Record<string, (value: unknown) => ReactNode>;
|
|
23
|
+
actions?: CatalogDetailAction[];
|
|
24
|
+
imageField?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The catalog vertical this sheet renders. Used for per-vertical labels —
|
|
27
|
+
* e.g. cruises label the "Options" tab "Cabins" since the options are cabin
|
|
28
|
+
* categories.
|
|
29
|
+
*/
|
|
30
|
+
vertical?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Sheet max-width token or class name. Defaults to `5xl` so rich catalog
|
|
33
|
+
* details can use two columns on wide operator screens.
|
|
34
|
+
*/
|
|
35
|
+
width?: CatalogDetailSheetWidth;
|
|
36
|
+
/** Secondary header content such as print/download icon buttons. */
|
|
37
|
+
headerExtras?: ReactNode | CatalogDetailRenderSlot;
|
|
38
|
+
/**
|
|
39
|
+
* Called once when the sheet opens for a hit. The result is rendered
|
|
40
|
+
* in dedicated sections (description, highlights, itinerary, media,
|
|
41
|
+
* options, policies). Errors swallowed silently — the sheet still
|
|
42
|
+
* shows the indexed-projection fallback.
|
|
43
|
+
*/
|
|
44
|
+
onLoadDetail?: (hit: CatalogSearchHit) => Promise<CatalogDetailEnrichment | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Called lazily when a departure (cruise sailing) row expands, to fetch live
|
|
47
|
+
* per-cabin pricing + availability. Returns `null` when unavailable. The
|
|
48
|
+
* caller binds the vertical; the sheet passes the cruise hit + sailing ref.
|
|
49
|
+
*/
|
|
50
|
+
onLoadDeparturePricing?: (hit: CatalogSearchHit, sailingRef: string) => Promise<CatalogDeparturePricingRow[] | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Called when the operator clicks a per-departure Book button. When
|
|
53
|
+
* omitted, departures render without a book affordance — the
|
|
54
|
+
* sheet-level `actions` (e.g. "Book this") still apply.
|
|
55
|
+
*/
|
|
56
|
+
onBookDeparture?: (hit: CatalogSearchHit, departure: NonNullable<CatalogDetailEnrichment["departures"]>[number]) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Per-option book affordance. When set, the expanded departure panel
|
|
59
|
+
* renders a Book button on each option row; the callback receives the
|
|
60
|
+
* departure plus the chosen option so the booking journey can
|
|
61
|
+
* pre-select it. Falls back to `onBookDeparture` when omitted.
|
|
62
|
+
*/
|
|
63
|
+
onBookOption?: (hit: CatalogSearchHit, departure: NonNullable<CatalogDetailEnrichment["departures"]>[number], option: NonNullable<CatalogDetailEnrichment["options"]>[number]) => void;
|
|
64
|
+
/** Dedicated brochure/print section rendered above media. */
|
|
65
|
+
renderBrochure?: CatalogDetailRenderSlot;
|
|
66
|
+
/** Consumer-provided media rendering, replacing the default thumbnail grid. */
|
|
67
|
+
renderMedia?: CatalogDetailRenderSlot;
|
|
68
|
+
/** Consumer-provided itinerary day renderer for richer day cards. */
|
|
69
|
+
renderItineraryDay?: (day: CatalogDetailItineraryDay, hit: CatalogSearchHit, enrichment: CatalogDetailEnrichment) => ReactNode;
|
|
70
|
+
/** Additional consumer sections rendered above the footer actions. */
|
|
71
|
+
renderExtraSections?: CatalogDetailRenderSlot;
|
|
72
|
+
/**
|
|
73
|
+
* Render a clickable supplier link in the Attributes tab. When set,
|
|
74
|
+
* the `supplierId` row uses this renderer instead of the plain
|
|
75
|
+
* formatter — typically a router-aware Link to the supplier detail
|
|
76
|
+
* page. Receives the supplier id and the display name (already
|
|
77
|
+
* resolved by `formatters.supplierId`).
|
|
78
|
+
*/
|
|
79
|
+
renderSupplierLink?: (supplierId: string, displayName: string) => ReactNode;
|
|
80
|
+
/**
|
|
81
|
+
* When provided, the Tags row in the Overview tab swaps its read-only
|
|
82
|
+
* chips for an inline add/remove editor. The callback is invoked with
|
|
83
|
+
* the *next* tag list (after the local optimistic update); rejecting
|
|
84
|
+
* the promise reverts to the indexed tags.
|
|
85
|
+
*
|
|
86
|
+
* Only the `tags` field — the operator-authored jsonb list — is
|
|
87
|
+
* editable. Index-derived arrays (categories, regions, etc.) stay
|
|
88
|
+
* read-only because they're computed from elsewhere.
|
|
89
|
+
*/
|
|
90
|
+
onTagsChange?: (hit: CatalogSearchHit, tags: string[]) => Promise<void> | void;
|
|
91
|
+
}
|
|
92
|
+
export declare function CatalogDetailSheet({ hit, onOpenChange, formatters, actions, imageField, width, vertical, headerExtras, onLoadDetail, onLoadDeparturePricing, onBookDeparture, onBookOption, renderBrochure, renderMedia, renderItineraryDay, renderExtraSections, renderSupplierLink, onTagsChange, }: CatalogDetailSheetProps): import("react/jsx-runtime").JSX.Element;
|
|
93
|
+
//# sourceMappingURL=catalog-detail-sheet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-detail-sheet.d.ts","sourceRoot":"","sources":["../../src/components/catalog-detail-sheet.tsx"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAA;AAG3D,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,gBAAgB,EACjB,MAAM,aAAa,CAAA;AAWpB,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AAEzF,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAA;IACxC,OAAO,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAA;IACvD;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAA;CAC7C;AAKD,YAAY,EAAE,uBAAuB,EAAE,CAAA;AAEvC,MAAM,MAAM,uBAAuB,GAC/B,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,KAAK,GACL,KAAK,GACL,KAAK,GACL,KAAK,GACL,KAAK,GACL,MAAM,CAAA;AAEV,MAAM,MAAM,yBAAyB,GAAG,WAAW,CAAC,uBAAuB,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AAEjG,MAAM,MAAM,uBAAuB,GAAG,CACpC,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,uBAAuB,GAAG,IAAI,KACvC,SAAS,CAAA;AAEd,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC5B,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,CAAC,CAAA;IAC1D,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAA;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,uBAAuB,CAAA;IAC/B,oEAAoE;IACpE,YAAY,CAAC,EAAE,SAAS,GAAG,uBAAuB,CAAA;IAClD;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAA;IACjF;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,CACvB,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,0BAA0B,EAAE,GAAG,IAAI,CAAC,CAAA;IACjD;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAChB,GAAG,EAAE,gBAAgB,EACrB,SAAS,EAAE,WAAW,CAAC,uBAAuB,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,KAClE,IAAI,CAAA;IACT;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CACb,GAAG,EAAE,gBAAgB,EACrB,SAAS,EAAE,WAAW,CAAC,uBAAuB,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,EACrE,MAAM,EAAE,WAAW,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAC5D,IAAI,CAAA;IACT,6DAA6D;IAC7D,cAAc,CAAC,EAAE,uBAAuB,CAAA;IACxC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,uBAAuB,CAAA;IACrC,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,CACnB,GAAG,EAAE,yBAAyB,EAC9B,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,uBAAuB,KAChC,SAAS,CAAA;IACd,sEAAsE;IACtE,mBAAmB,CAAC,EAAE,uBAAuB,CAAA;IAC7C;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,SAAS,CAAA;IAC3E;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC/E;AAED,wBAAgB,kBAAkB,CAAC,EACjC,GAAG,EACH,YAAY,EACZ,UAAU,EACV,OAAO,EACP,UAA2B,EAC3B,KAAa,EACb,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,eAAe,EACf,YAAY,EACZ,cAAc,EACd,WAAW,EACX,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,YAAY,GACb,EAAE,uBAAuB,2CAyKzB"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Badge } from "@voyant-travel/ui/components/badge";
|
|
4
|
+
import { Button } from "@voyant-travel/ui/components/button";
|
|
5
|
+
import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, } from "@voyant-travel/ui/components/sheet";
|
|
6
|
+
import { cn } from "@voyant-travel/ui/lib/utils";
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
import { useCatalogUiMessagesOrDefault } from "../i18n/index.js";
|
|
9
|
+
import { formatTemplate, IdChip, initialsOf, ProductPriceFrom, sheetWidthClass, stringOr, } from "./catalog-detail-parts.js";
|
|
10
|
+
import { CatalogDetailView } from "./catalog-detail-view.js";
|
|
11
|
+
export { CatalogDetailView } from "./catalog-detail-view.js";
|
|
12
|
+
export function CatalogDetailSheet({ hit, onOpenChange, formatters, actions, imageField = "thumbnailUrl", width = "5xl", vertical, headerExtras, onLoadDetail, onLoadDeparturePricing, onBookDeparture, onBookOption, renderBrochure, renderMedia, renderItineraryDay, renderExtraSections, renderSupplierLink, onTagsChange, }) {
|
|
13
|
+
const catalogMessages = useCatalogUiMessagesOrDefault().catalogPage;
|
|
14
|
+
const messages = catalogMessages.detail;
|
|
15
|
+
const open = hit != null;
|
|
16
|
+
const fields = hit?.document.fields ?? {};
|
|
17
|
+
const name = stringOr(fields.name, catalogMessages.fallbacks.detailName);
|
|
18
|
+
const status = stringOr(fields.status, null);
|
|
19
|
+
// Enrichment fetch — fires when the sheet opens for a hit. The
|
|
20
|
+
// useEffect dep list includes hit.id so re-opening a different row
|
|
21
|
+
// refetches; the result is local to the open instance, so closing
|
|
22
|
+
// the sheet clears it.
|
|
23
|
+
const [enrichment, setEnrichment] = useState(null);
|
|
24
|
+
const [enrichmentLoading, setEnrichmentLoading] = useState(false);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!hit || !onLoadDetail) {
|
|
27
|
+
setEnrichment(null);
|
|
28
|
+
setEnrichmentLoading(false);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
let cancelled = false;
|
|
32
|
+
setEnrichmentLoading(true);
|
|
33
|
+
setEnrichment(null);
|
|
34
|
+
onLoadDetail(hit)
|
|
35
|
+
.then((result) => {
|
|
36
|
+
if (!cancelled) {
|
|
37
|
+
setEnrichment(result ?? null);
|
|
38
|
+
setEnrichmentLoading(false);
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
.catch(() => {
|
|
42
|
+
if (!cancelled) {
|
|
43
|
+
setEnrichment(null);
|
|
44
|
+
setEnrichmentLoading(false);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
cancelled = true;
|
|
49
|
+
};
|
|
50
|
+
}, [hit, onLoadDetail]);
|
|
51
|
+
const imageUrl = stringOr(enrichment?.heroImageUrl, null) ?? stringOr(fields[imageField], null);
|
|
52
|
+
const resolvedHeaderExtras = hit && headerExtras
|
|
53
|
+
? typeof headerExtras === "function"
|
|
54
|
+
? headerExtras(hit, enrichment)
|
|
55
|
+
: headerExtras
|
|
56
|
+
: null;
|
|
57
|
+
return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsx(SheetContent, { className: cn("w-full p-0", sheetWidthClass(width)), children: _jsxs("div", { className: "flex h-full flex-col", children: [_jsx(SheetHeader, { className: "border-b bg-muted/20 px-6 py-5", children: _jsxs("div", { className: "flex items-start justify-between gap-4 pr-8", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-4", children: [imageUrl ? (_jsx("img", { src: imageUrl, alt: name, className: "h-16 w-16 shrink-0 rounded-lg object-cover ring-1 ring-border", loading: "lazy" })) : (_jsx("div", { className: "flex h-16 w-16 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-sky-500 to-indigo-600 text-lg font-medium text-white ring-1 ring-border", children: initialsOf(name) })), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [_jsx(SheetTitle, { className: "text-base leading-snug", children: name }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [status && (_jsx(Badge, { variant: status === "active" || status === "live" ? "default" : "secondary", className: "capitalize", children: status })), hit?.id && _jsx(IdChip, { id: hit.id }), enrichment?.servedLocale && (_jsx(Badge, { variant: "outline", className: "font-normal", children: enrichment.servedLocale })), enrichment?.matchKind && enrichment.matchKind !== "exact" && (_jsx(Badge, { variant: "outline", className: "font-normal", children: formatTemplate(messages.matchPrefix, { kind: enrichment.matchKind }) })), enrichment?.source && (_jsx(Badge, { variant: enrichment.source === "synthesized" ? "outline" : "secondary", className: "font-normal", children: enrichment.source })), enrichment?.servedStale && (_jsx(Badge, { variant: "outline", className: "font-normal", children: messages.stale })), enrichment?.machineTranslated && (_jsx(Badge, { variant: "outline", className: "font-normal", children: "MT" }))] })] })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-3", children: [_jsx(ProductPriceFrom, { enrichment: enrichment, fields: fields, messages: messages }), resolvedHeaderExtras && (_jsx("div", { className: "flex items-center gap-2", children: resolvedHeaderExtras }))] })] }) }), _jsx(CatalogDetailView, { hit: hit, enrichment: enrichment, enrichmentLoading: enrichmentLoading, vertical: vertical, formatters: formatters, renderBrochure: renderBrochure, renderMedia: renderMedia, renderItineraryDay: renderItineraryDay, renderExtraSections: renderExtraSections, renderSupplierLink: renderSupplierLink, onLoadDeparturePricing: onLoadDeparturePricing, onBookDeparture: onBookDeparture, onBookOption: onBookOption, onTagsChange: onTagsChange, className: "flex-1 overflow-y-auto px-6 py-5" }), (() => {
|
|
58
|
+
if (!hit || !actions || actions.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
const visibleActions = actions.filter((a) => (a.visible ? a.visible(hit) : true));
|
|
61
|
+
if (visibleActions.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
return (_jsx(SheetFooter, { className: "border-t bg-muted/20 px-6 py-3", children: _jsx("div", { className: "flex flex-wrap justify-end gap-2", children: visibleActions.map((a) => (_jsx(Button, { variant: a.variant ?? "default", size: "sm", onClick: () => {
|
|
64
|
+
a.onClick(hit);
|
|
65
|
+
onOpenChange(false);
|
|
66
|
+
}, children: a.label }, a.label))) }) }));
|
|
67
|
+
})()] }) }) }));
|
|
68
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { CatalogDeparturePricingRow, CatalogDetailEnrichment, CatalogSearchHit } from "../index.js";
|
|
3
|
+
import type { CatalogDetailItineraryDay, CatalogDetailRenderSlot } from "./catalog-detail-sheet.js";
|
|
4
|
+
/**
|
|
5
|
+
* Right-side detail sheet for any catalog hit. Header shows the entity's
|
|
6
|
+
* primary image, name, status, and id. Body is split into Description,
|
|
7
|
+
* Highlights/Tags (when present), and a clean two-column attribute grid.
|
|
8
|
+
* System provenance fields are tucked into a collapsible at the bottom.
|
|
9
|
+
*/
|
|
10
|
+
export interface CatalogDetailViewProps {
|
|
11
|
+
hit: CatalogSearchHit | null;
|
|
12
|
+
enrichment: CatalogDetailEnrichment | null;
|
|
13
|
+
/** Show the "loading full content" hint above the tabs. */
|
|
14
|
+
enrichmentLoading?: boolean;
|
|
15
|
+
vertical?: string;
|
|
16
|
+
formatters?: Record<string, (value: unknown) => ReactNode>;
|
|
17
|
+
renderBrochure?: CatalogDetailRenderSlot;
|
|
18
|
+
renderMedia?: CatalogDetailRenderSlot;
|
|
19
|
+
renderItineraryDay?: (day: CatalogDetailItineraryDay, hit: CatalogSearchHit, enrichment: CatalogDetailEnrichment) => ReactNode;
|
|
20
|
+
renderExtraSections?: CatalogDetailRenderSlot;
|
|
21
|
+
renderSupplierLink?: (supplierId: string, displayName: string) => ReactNode;
|
|
22
|
+
onLoadDeparturePricing?: (hit: CatalogSearchHit, sailingRef: string) => Promise<CatalogDeparturePricingRow[] | null>;
|
|
23
|
+
onBookDeparture?: (hit: CatalogSearchHit, departure: NonNullable<CatalogDetailEnrichment["departures"]>[number]) => void;
|
|
24
|
+
onBookOption?: (hit: CatalogSearchHit, departure: NonNullable<CatalogDetailEnrichment["departures"]>[number], option: NonNullable<CatalogDetailEnrichment["options"]>[number]) => void;
|
|
25
|
+
onTagsChange?: (hit: CatalogSearchHit, tags: string[]) => Promise<void> | void;
|
|
26
|
+
/** Wrapper className — the sheet scrolls (`flex-1 overflow-y-auto px-6 py-5`),
|
|
27
|
+
* a full-page host can pass its own (or nothing). */
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The tabbed detail BODY for a catalog hit — overview, itinerary, ship,
|
|
32
|
+
* cabins/rooms/options, departures/sailings (with live cabin pricing), media,
|
|
33
|
+
* policies, attributes. Layout-agnostic: it owns no modal/page chrome, so it's
|
|
34
|
+
* shared by {@link CatalogDetailSheet} (in a Sheet) and the operator's
|
|
35
|
+
* full-page, new-tab detail route. Enrichment is passed in (the host owns the
|
|
36
|
+
* fetch) since the host header also needs it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function CatalogDetailView({ hit, enrichment, enrichmentLoading, vertical, formatters, renderBrochure, renderMedia, renderItineraryDay, renderExtraSections, renderSupplierLink, onLoadDeparturePricing, onBookDeparture, onBookOption, onTagsChange, className, }: CatalogDetailViewProps): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
//# sourceMappingURL=catalog-detail-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-detail-view.d.ts","sourceRoot":"","sources":["../../src/components/catalog-detail-view.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAW,MAAM,OAAO,CAAA;AAE/C,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,gBAAgB,EACjB,MAAM,aAAa,CAAA;AAcpB,OAAO,KAAK,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAA;AA6DnG;;;;;GAKG;AAEH,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC5B,UAAU,EAAE,uBAAuB,GAAG,IAAI,CAAA;IAC1C,2DAA2D;IAC3D,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,CAAC,CAAA;IAC1D,cAAc,CAAC,EAAE,uBAAuB,CAAA;IACxC,WAAW,CAAC,EAAE,uBAAuB,CAAA;IACrC,kBAAkB,CAAC,EAAE,CACnB,GAAG,EAAE,yBAAyB,EAC9B,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,uBAAuB,KAChC,SAAS,CAAA;IACd,mBAAmB,CAAC,EAAE,uBAAuB,CAAA;IAC7C,kBAAkB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,SAAS,CAAA;IAC3E,sBAAsB,CAAC,EAAE,CACvB,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,0BAA0B,EAAE,GAAG,IAAI,CAAC,CAAA;IACjD,eAAe,CAAC,EAAE,CAChB,GAAG,EAAE,gBAAgB,EACrB,SAAS,EAAE,WAAW,CAAC,uBAAuB,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,KAClE,IAAI,CAAA;IACT,YAAY,CAAC,EAAE,CACb,GAAG,EAAE,gBAAgB,EACrB,SAAS,EAAE,WAAW,CAAC,uBAAuB,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,EACrE,MAAM,EAAE,WAAW,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAC5D,IAAI,CAAA;IACT,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC9E;0DACsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,GAAG,EACH,UAAU,EACV,iBAAyB,EACzB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,WAAW,EACX,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,SAAS,GACV,EAAE,sBAAsB,2CAmRxB"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@voyant-travel/ui/components/tabs";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
import { useCatalogUiMessagesOrDefault } from "../i18n/index.js";
|
|
7
|
+
import { CabinCard, ShipCard } from "./catalog-detail-cruise-cards.js";
|
|
8
|
+
import { DeparturesTable } from "./catalog-detail-departures.js";
|
|
9
|
+
import { ArrayBadges, AttributeList, DefaultMediaGrid, formatTemplate, humanize, InlineTagsEditor, Section, stringOr, toStringArray, } from "./catalog-detail-parts.js";
|
|
10
|
+
import { MediaGallery } from "./media-gallery.js";
|
|
11
|
+
const HIDDEN_FIELDS = new Set([
|
|
12
|
+
"id",
|
|
13
|
+
"name",
|
|
14
|
+
"description",
|
|
15
|
+
"shortDescription",
|
|
16
|
+
"status",
|
|
17
|
+
"text_embedding",
|
|
18
|
+
"embedding_model_id",
|
|
19
|
+
]);
|
|
20
|
+
const SYSTEM_FIELD_PREFIXES = ["source.", "seller."];
|
|
21
|
+
const ARRAY_FIELDS = new Set(["tags", "highlights", "regions", "themes", "defaultBookingModes"]);
|
|
22
|
+
/**
|
|
23
|
+
* Array fields the index emits that don't carry operator-facing value in
|
|
24
|
+
* the Tags & themes section — either duplicates of a friendlier sibling
|
|
25
|
+
* (`destinationSlugs` ↔ `regions`/`countries`/`cities`; `categorySlugs`
|
|
26
|
+
* ↔ `categoryIds`; `tagIds` ↔ `tagLabels`/`tags`) or noise derived from
|
|
27
|
+
* other rendered fields (`departureMonths` is a roll-up of
|
|
28
|
+
* `departureDates`). Hidden at render time; the underlying index still
|
|
29
|
+
* carries them for facets/filters.
|
|
30
|
+
*/
|
|
31
|
+
const HIDDEN_ARRAY_FIELDS = new Set([
|
|
32
|
+
// Departure surface lives in its own Departures tab now — the
|
|
33
|
+
// raw date / month chip lists in Tags & themes are redundant.
|
|
34
|
+
"departureDates",
|
|
35
|
+
"departureMonths",
|
|
36
|
+
"destinationIds",
|
|
37
|
+
"destinationSlugs",
|
|
38
|
+
// Three category projections cover the same relation: keep the
|
|
39
|
+
// friendly localized `categories` list, drop the id + slug mirrors.
|
|
40
|
+
"categoryIds",
|
|
41
|
+
"categorySlugs",
|
|
42
|
+
// `tagIds` + `tagLabels` mirror the relational tags table; keep only
|
|
43
|
+
// the operator-authored `tags` jsonb column.
|
|
44
|
+
"tagIds",
|
|
45
|
+
"tagLabels",
|
|
46
|
+
// Canonical geography id mirrors — the resolved name lists
|
|
47
|
+
// (`countries`/`regions`/`ports`/`waterways`) carry the display values, so
|
|
48
|
+
// the raw id columns are noise in the overview.
|
|
49
|
+
"country_iso",
|
|
50
|
+
"region_ids",
|
|
51
|
+
"port_ids",
|
|
52
|
+
"waterway_ids",
|
|
53
|
+
]);
|
|
54
|
+
/**
|
|
55
|
+
* Array-field label overrides for the Tags & themes section. Falls
|
|
56
|
+
* through to `humanize(key)` when not present. Localized variants come
|
|
57
|
+
* in via `messages.detail.arrayLabels` per locale — this stays as a
|
|
58
|
+
* stable fallback for any call path that hasn't been wired yet.
|
|
59
|
+
*/
|
|
60
|
+
const ARRAY_LABEL_OVERRIDES = {
|
|
61
|
+
// i18n-literal-ok: fallback when no localized override is provided
|
|
62
|
+
categories: "Category",
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The tabbed detail BODY for a catalog hit — overview, itinerary, ship,
|
|
66
|
+
* cabins/rooms/options, departures/sailings (with live cabin pricing), media,
|
|
67
|
+
* policies, attributes. Layout-agnostic: it owns no modal/page chrome, so it's
|
|
68
|
+
* shared by {@link CatalogDetailSheet} (in a Sheet) and the operator's
|
|
69
|
+
* full-page, new-tab detail route. Enrichment is passed in (the host owns the
|
|
70
|
+
* fetch) since the host header also needs it.
|
|
71
|
+
*/
|
|
72
|
+
export function CatalogDetailView({ hit, enrichment, enrichmentLoading = false, vertical, formatters, renderBrochure, renderMedia, renderItineraryDay, renderExtraSections, renderSupplierLink, onLoadDeparturePricing, onBookDeparture, onBookOption, onTagsChange, className, }) {
|
|
73
|
+
const catalogMessages = useCatalogUiMessagesOrDefault().catalogPage;
|
|
74
|
+
const messages = catalogMessages.detail;
|
|
75
|
+
// Cruises sell cabin categories, not generic options — label the tab "Cabins".
|
|
76
|
+
const optionsLabel = vertical === "cruises" ? messages.cabins : messages.options;
|
|
77
|
+
// In the cruise industry a scheduled departure is a "sailing".
|
|
78
|
+
const departuresLabel = vertical === "cruises" ? messages.sailings : messages.departures;
|
|
79
|
+
const fields = hit?.document.fields ?? {};
|
|
80
|
+
const name = stringOr(fields.name, catalogMessages.fallbacks.detailName);
|
|
81
|
+
const description = stringOr(enrichment?.description, null) ?? stringOr(fields.description, null);
|
|
82
|
+
const shortDescription = stringOr(enrichment?.shortDescription, null) ?? stringOr(fields.shortDescription, null);
|
|
83
|
+
const brochureContent = hit && renderBrochure ? renderBrochure(hit, enrichment) : null;
|
|
84
|
+
const hasCustomMediaRenderer = hit != null && renderMedia != null;
|
|
85
|
+
const mediaContent = hit && renderMedia ? renderMedia(hit, enrichment) : null;
|
|
86
|
+
const shouldRenderMediaSection = hasCustomMediaRenderer
|
|
87
|
+
? mediaContent !== null && mediaContent !== undefined && mediaContent !== false
|
|
88
|
+
: enrichment?.media != null && enrichment.media.length > 0;
|
|
89
|
+
const extraSections = hit && renderExtraSections ? renderExtraSections(hit, enrichment) : null;
|
|
90
|
+
const { arrayEntries, attributeEntries, systemEntries } = useMemo(() => {
|
|
91
|
+
const allEntries = Object.entries(fields).filter(([k]) => !HIDDEN_FIELDS.has(k));
|
|
92
|
+
const array = [];
|
|
93
|
+
const attrs = [];
|
|
94
|
+
const system = [];
|
|
95
|
+
for (const [k, v] of allEntries) {
|
|
96
|
+
if (SYSTEM_FIELD_PREFIXES.some((p) => k.startsWith(p)))
|
|
97
|
+
system.push([k, v]);
|
|
98
|
+
else if (ARRAY_FIELDS.has(k) || Array.isArray(v)) {
|
|
99
|
+
if (HIDDEN_ARRAY_FIELDS.has(k))
|
|
100
|
+
continue;
|
|
101
|
+
if (k !== "tags" && Array.isArray(v) && v.length === 0)
|
|
102
|
+
continue;
|
|
103
|
+
array.push([k, v]);
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
attrs.push([k, v]);
|
|
107
|
+
}
|
|
108
|
+
return { arrayEntries: array, attributeEntries: attrs, systemEntries: system };
|
|
109
|
+
}, [fields]);
|
|
110
|
+
const overviewGalleryImages = useMemo(() => {
|
|
111
|
+
const urls = [];
|
|
112
|
+
if (enrichment?.heroImageUrl)
|
|
113
|
+
urls.push(enrichment.heroImageUrl);
|
|
114
|
+
for (const option of enrichment?.options ?? []) {
|
|
115
|
+
const cover = option.images?.[0];
|
|
116
|
+
if (cover)
|
|
117
|
+
urls.push(cover);
|
|
118
|
+
}
|
|
119
|
+
return Array.from(new Set(urls));
|
|
120
|
+
}, [enrichment]);
|
|
121
|
+
const reshapedAttributeEntries = (() => {
|
|
122
|
+
const map = new Map(attributeEntries);
|
|
123
|
+
const out = [];
|
|
124
|
+
let didAmount = false;
|
|
125
|
+
for (const [k, v] of attributeEntries) {
|
|
126
|
+
if (k === "pax")
|
|
127
|
+
continue;
|
|
128
|
+
if (k === "sellCurrency")
|
|
129
|
+
continue;
|
|
130
|
+
if (k === "sellAmountCents") {
|
|
131
|
+
const currency = stringOr(map.get("sellCurrency"), "USD");
|
|
132
|
+
out.push(["sellAmount", { amountCents: v, currency }]);
|
|
133
|
+
didAmount = true;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
out.push([k, v]);
|
|
137
|
+
}
|
|
138
|
+
if (!didAmount && map.has("sellCurrency")) {
|
|
139
|
+
out.push(["sellCurrency", map.get("sellCurrency")]);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
})();
|
|
143
|
+
const hasItinerary = (enrichment?.itinerary?.length ?? 0) > 0;
|
|
144
|
+
const hasShip = enrichment?.ship != null;
|
|
145
|
+
const hasOptions = (enrichment?.options?.length ?? 0) > 0;
|
|
146
|
+
const hasDepartures = (enrichment?.departures?.length ?? 0) > 0;
|
|
147
|
+
const hasPolicies = (enrichment?.policies?.length ?? 0) > 0;
|
|
148
|
+
const hasAttributes = reshapedAttributeEntries.length > 0 || arrayEntries.length > 0 || systemEntries.length > 0;
|
|
149
|
+
return (_jsxs("div", { className: className, children: [enrichmentLoading && (_jsxs("div", { className: "mb-4 flex items-center gap-2 text-xs text-muted-foreground", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), messages.loadingFullContent] })), _jsxs(Tabs, { defaultValue: "overview", className: "gap-4", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "overview", children: messages.tabs.overview }), hasItinerary && _jsx(TabsTrigger, { value: "itinerary", children: messages.itinerary }), hasShip && _jsx(TabsTrigger, { value: "ship", children: messages.ship }), hasOptions && _jsx(TabsTrigger, { value: "options", children: optionsLabel }), hasDepartures && _jsx(TabsTrigger, { value: "departures", children: departuresLabel }), shouldRenderMediaSection && _jsx(TabsTrigger, { value: "media", children: messages.media }), hasPolicies && _jsx(TabsTrigger, { value: "policies", children: messages.policies }), hasAttributes && _jsx(TabsTrigger, { value: "attributes", children: messages.attributes })] }), _jsxs(TabsContent, { value: "overview", className: "flex flex-col gap-6", children: [overviewGalleryImages.length > 1 && (_jsx(Section, { title: messages.media, children: _jsx(MediaGallery, { images: overviewGalleryImages, alt: name, className: "w-full max-w-lg", imageClassName: "h-56 w-full" }) })), (shortDescription || description) && (_jsxs(Section, { children: [shortDescription && (_jsx("p", { className: "text-sm font-medium leading-relaxed text-foreground", children: shortDescription })), description && (_jsx("p", { className: "whitespace-pre-line text-sm leading-relaxed text-muted-foreground", children: description }))] })), enrichment?.highlights && enrichment.highlights.length > 0 && (_jsx(Section, { title: messages.highlights, children: _jsx("ul", { className: "ml-4 list-disc space-y-1 text-sm text-muted-foreground", children: enrichment.highlights.map((h) => (_jsx("li", { children: h }, h))) }) })), enrichment?.supplier && (_jsx(Section, { title: messages.supplier, children: _jsx("p", { className: "text-sm text-foreground", children: enrichment.supplier }) })), arrayEntries.length > 0 && (_jsx(Section, { title: messages.tagsThemes, children: _jsx("div", { className: "flex flex-col gap-3", children: arrayEntries.map(([key, value]) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx("span", { className: "text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: ARRAY_LABEL_OVERRIDES[key] ?? humanize(key) }), key === "tags" && hit && onTagsChange ? (_jsx(InlineTagsEditor, { hit: hit, value: toStringArray(value), onChange: onTagsChange, placeholder: messages.tagsInputPlaceholder })) : (_jsx(ArrayBadges, { value: value }))] }, key))) }) })), brochureContent && _jsx(Section, { title: messages.brochure, children: brochureContent }), extraSections] }), hasItinerary && (_jsx(TabsContent, { value: "itinerary", className: "flex flex-col gap-2", children: _jsx("ol", { className: "space-y-2", children: enrichment?.itinerary?.map((d) => (_jsx("li", { children: renderItineraryDay && hit ? (renderItineraryDay(d, hit, enrichment)) : (_jsx(DefaultItineraryDay, { day: d, dayLabel: messages.day })) }, d.dayNumber))) }) })), hasShip && enrichment?.ship && (_jsx(TabsContent, { value: "ship", children: _jsx(ShipCard, { ship: enrichment.ship, messages: messages }) })), hasOptions && (_jsx(TabsContent, { value: "options", children: _jsx("ul", { className: "space-y-3", children: enrichment?.options?.map((o) => (_jsx(CabinCard, { cabin: o, messages: messages }, o.id))) }) })), hasDepartures && enrichment?.departures && (_jsx(TabsContent, { value: "departures", children: _jsx(DeparturesTable, { hit: hit, vertical: vertical, departures: enrichment.departures, options: enrichment?.options ?? [], onLoadDeparturePricing: onLoadDeparturePricing, productSellAmountCents: typeof fields.sellAmountCents === "number"
|
|
150
|
+
? fields.sellAmountCents
|
|
151
|
+
: typeof fields.sellAmountCents === "string"
|
|
152
|
+
? Number(fields.sellAmountCents) || null
|
|
153
|
+
: null, productSellCurrency: typeof fields.sellCurrency === "string" ? fields.sellCurrency : null, onBookDeparture: onBookDeparture, onBookOption: onBookOption, messages: messages }) })), shouldRenderMediaSection && (_jsx(TabsContent, { value: "media", children: mediaContent ?? _jsx(DefaultMediaGrid, { media: enrichment?.media ?? [] }) })), hasPolicies && (_jsx(TabsContent, { value: "policies", children: _jsx("dl", { className: "space-y-2 text-sm", children: enrichment?.policies?.map((p, idx) => (_jsxs("div", { children: [_jsx("dt", { className: "text-xs font-medium uppercase tracking-wider text-muted-foreground", children: p.kind.replace(/_/g, " ") }), _jsx("dd", { className: "mt-0.5 whitespace-pre-line text-muted-foreground", children: p.body })] }, `${p.kind}-${idx}`))) }) })), hasAttributes && (_jsxs(TabsContent, { value: "attributes", className: "flex flex-col gap-6", children: [reshapedAttributeEntries.length > 0 && (_jsx(AttributeList, { entries: reshapedAttributeEntries, formatters: formatters, messages: catalogMessages, renderSupplierLink: renderSupplierLink })), systemEntries.length > 0 && (_jsxs("details", { className: "group rounded-lg border bg-muted/20", children: [_jsx("summary", { className: "cursor-pointer list-none px-4 py-2.5 text-xs font-medium uppercase tracking-wider text-muted-foreground hover:bg-muted/40 group-open:border-b", children: messages.system }), _jsx("div", { className: "px-4 py-3", children: _jsx(AttributeList, { entries: systemEntries, formatters: formatters, messages: catalogMessages }) })] }))] }))] })] }));
|
|
154
|
+
}
|
|
155
|
+
function DefaultItineraryDay({ day, dayLabel, }) {
|
|
156
|
+
return (_jsxs("div", { className: "flex gap-3 rounded-md border border-border bg-muted/10 p-3 text-sm", children: [day.heroImageUrl ? (_jsx("img", { src: day.heroImageUrl, alt: day.title ?? formatTemplate(dayLabel, { day: day.dayNumber }), className: "h-20 w-28 shrink-0 rounded-md object-cover ring-1 ring-border", loading: "lazy" })) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [_jsxs("div", { className: "flex flex-wrap items-baseline gap-2", children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: formatTemplate(dayLabel, { day: day.dayNumber }) }), day.title && _jsx("span", { className: "font-medium", children: day.title }), day.location && _jsxs("span", { className: "text-xs text-muted-foreground", children: ["\u00B7 ", day.location] })] }), day.description && (_jsx("p", { className: "text-xs leading-relaxed text-muted-foreground", children: day.description }))] })] }));
|
|
157
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Back-compat re-export. The catalog detail enrichment client + its
|
|
3
|
+
* `CatalogDetailEnrichment` view-model moved to the data layer
|
|
4
|
+
* (`@voyant-travel/catalog-react`); this shim preserves the
|
|
5
|
+
* `@voyant-travel/catalog-react/ui` import surface.
|
|
6
|
+
*/
|
|
7
|
+
export { __resetEnrichmentFetcherWarnings, type CatalogDeparturePricingRow, type CatalogDetailEnrichment, type CatalogEnrichmentFetchers, type CatalogEnrichmentFetchersOptions, type CatalogSlotAvailability, createCatalogEnrichmentFetchers, } from "../index.js";
|
|
8
|
+
//# sourceMappingURL=catalog-enrichment-fetchers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-enrichment-fetchers.d.ts","sourceRoot":"","sources":["../../src/components/catalog-enrichment-fetchers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,gCAAgC,EAChC,KAAK,0BAA0B,EAC/B,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACrC,KAAK,uBAAuB,EAC5B,+BAA+B,GAChC,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Back-compat re-export. The catalog detail enrichment client + its
|
|
3
|
+
* `CatalogDetailEnrichment` view-model moved to the data layer
|
|
4
|
+
* (`@voyant-travel/catalog-react`); this shim preserves the
|
|
5
|
+
* `@voyant-travel/catalog-react/ui` import surface.
|
|
6
|
+
*/
|
|
7
|
+
export { __resetEnrichmentFetcherWarnings, createCatalogEnrichmentFetchers, } from "../index.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CatalogFacetBucket } from "../index.js";
|
|
2
|
+
export interface CatalogFacetedFilterProps {
|
|
3
|
+
/** Field name (matches the indexer document field). */
|
|
4
|
+
field: string;
|
|
5
|
+
/** Display label for the filter trigger. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Live facet buckets — values + counts from the search response. */
|
|
8
|
+
buckets: CatalogFacetBucket[];
|
|
9
|
+
/** Currently-selected values for this field. */
|
|
10
|
+
selected: Array<string | number>;
|
|
11
|
+
/** Toggle a value on/off. */
|
|
12
|
+
onToggle: (value: string | number) => void;
|
|
13
|
+
/** Clear all selections for this field. */
|
|
14
|
+
onClear: () => void;
|
|
15
|
+
/**
|
|
16
|
+
* Optional value formatter. Used to display human-readable labels for
|
|
17
|
+
* fields that store IDs — e.g. `lineSupplierId` resolved against a
|
|
18
|
+
* `Map<id, name>`. Falls back to `String(value)` when not provided.
|
|
19
|
+
*/
|
|
20
|
+
formatValue?: (value: string | number) => string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Faceted filter dropdown — modeled after shadcn's data-table-faceted-filter.
|
|
24
|
+
* Renders as a dashed-border button showing the field label and any active
|
|
25
|
+
* selections as compact badges. Opens a searchable command list with
|
|
26
|
+
* checkbox-style selection per bucket; counts come from the live facet
|
|
27
|
+
* response so the UI never lies about what's available.
|
|
28
|
+
*/
|
|
29
|
+
export declare function CatalogFacetedFilter({ label, buckets, selected, onToggle, onClear, formatValue, }: CatalogFacetedFilterProps): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
//# sourceMappingURL=catalog-faceted-filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-faceted-filter.d.ts","sourceRoot":"","sources":["../../src/components/catalog-faceted-filter.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAErD,MAAM,WAAW,yBAAyB;IACxC,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAA;IACb,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAA;IACb,qEAAqE;IACrE,OAAO,EAAE,kBAAkB,EAAE,CAAA;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAA;IAChC,6BAA6B;IAC7B,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAA;IAC1C,2CAA2C;IAC3C,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,MAAM,CAAA;CACjD;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,WAAW,GACZ,EAAE,yBAAyB,2CA0D3B"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from "@voyant-travel/ui/components/button";
|
|
4
|
+
import { Checkbox } from "@voyant-travel/ui/components/checkbox";
|
|
5
|
+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@voyant-travel/ui/components/command";
|
|
6
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@voyant-travel/ui/components/popover";
|
|
7
|
+
import { PlusCircle } from "lucide-react";
|
|
8
|
+
import { useCatalogUiMessagesOrDefault } from "../i18n/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* Faceted filter dropdown — modeled after shadcn's data-table-faceted-filter.
|
|
11
|
+
* Renders as a dashed-border button showing the field label and any active
|
|
12
|
+
* selections as compact badges. Opens a searchable command list with
|
|
13
|
+
* checkbox-style selection per bucket; counts come from the live facet
|
|
14
|
+
* response so the UI never lies about what's available.
|
|
15
|
+
*/
|
|
16
|
+
export function CatalogFacetedFilter({ label, buckets, selected, onToggle, onClear, formatValue, }) {
|
|
17
|
+
const messages = useCatalogUiMessagesOrDefault().catalogPage.filtersUi;
|
|
18
|
+
const selectedSet = new Set(selected);
|
|
19
|
+
const display = (v) => (formatValue ? formatValue(v) : String(v));
|
|
20
|
+
return (_jsxs(Popover, { children: [_jsxs(PopoverTrigger, { render: _jsx(Button, { variant: "outline", size: "sm", className: "h-8 gap-2 border-dashed" }), children: [_jsx(PlusCircle, { className: "h-3.5 w-3.5" }), _jsx("span", { children: label }), selected.length > 0 && (_jsx("span", { className: "-mr-0.5 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-[11px] font-medium tabular-nums text-primary-foreground", children: selected.length }))] }), _jsx(PopoverContent, { className: "w-[220px] p-0", align: "start", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: label }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: messages.noResults }), _jsx(CommandGroup, { children: buckets.map((bucket) => {
|
|
21
|
+
const isSelected = selectedSet.has(bucket.value);
|
|
22
|
+
return (_jsxs(CommandItem, { onSelect: () => onToggle(bucket.value), children: [_jsx(Checkbox, { checked: isSelected, tabIndex: -1, "aria-hidden": true, className: "mr-2 pointer-events-none" }), _jsx("span", { className: "flex-1 truncate capitalize", children: display(bucket.value) }), _jsx("span", { className: "ml-2 text-muted-foreground text-xs", children: bucket.count })] }, String(bucket.value)));
|
|
23
|
+
}) }), selected.length > 0 && (_jsxs(_Fragment, { children: [_jsx(CommandSeparator, {}), _jsx(CommandGroup, { children: _jsx(CommandItem, { onSelect: onClear, className: "justify-center text-center text-muted-foreground", children: messages.clearFilter }) })] }))] })] }) })] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CatalogFacetBucket } from "../index.js";
|
|
2
|
+
import type { CatalogRangeFilterValue } from "./catalog-range-filter.js";
|
|
3
|
+
import type { CatalogFilterField } from "./catalog-search-page.js";
|
|
4
|
+
export interface CatalogFilterRailProps {
|
|
5
|
+
/** Visible filter fields (facets with buckets/selection + all ranges). */
|
|
6
|
+
fields: CatalogFilterField[];
|
|
7
|
+
/** Live facet buckets keyed by field. */
|
|
8
|
+
facetGroups: Record<string, CatalogFacetBucket[]>;
|
|
9
|
+
selectedFacets: Record<string, Array<string | number>>;
|
|
10
|
+
selectedRanges: Record<string, CatalogRangeFilterValue>;
|
|
11
|
+
onToggleFacet: (field: string, value: string | number) => void;
|
|
12
|
+
onClearFacet: (field: string) => void;
|
|
13
|
+
onSetRange: (field: string, value: CatalogRangeFilterValue | undefined) => void;
|
|
14
|
+
onClearAll: () => void;
|
|
15
|
+
hasSelections: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Persistent, sectioned left filter rail (Booking.com-style). Facets render
|
|
19
|
+
* as inline checkbox lists with live counts; numeric ranges render as inline
|
|
20
|
+
* min/max inputs. Replaces the wrapping chip row for a scannable, always-on
|
|
21
|
+
* filtering surface. Selection state is owned by the parent
|
|
22
|
+
* (`CatalogSearchPage`); this component is presentational.
|
|
23
|
+
*/
|
|
24
|
+
export declare function CatalogFilterRail({ fields, facetGroups, selectedFacets, selectedRanges, onToggleFacet, onClearFacet, onSetRange, onClearAll, hasSelections, }: CatalogFilterRailProps): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
//# sourceMappingURL=catalog-filter-rail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-filter-rail.d.ts","sourceRoot":"","sources":["../../src/components/catalog-filter-rail.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAA;AACxE,OAAO,KAAK,EAEV,kBAAkB,EAEnB,MAAM,0BAA0B,CAAA;AAKjC,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,MAAM,EAAE,kBAAkB,EAAE,CAAA;IAC5B,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAAA;IACjD,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAA;IACtD,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAA;IACvD,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAA;IAC9D,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,uBAAuB,GAAG,SAAS,KAAK,IAAI,CAAA;IAC/E,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,aAAa,EAAE,OAAO,CAAA;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,WAAW,EACX,cAAc,EACd,cAAc,EACd,aAAa,EACb,YAAY,EACZ,UAAU,EACV,UAAU,EACV,aAAa,GACd,EAAE,sBAAsB,2CAyDxB"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from "@voyant-travel/ui/components/button";
|
|
4
|
+
import { Checkbox } from "@voyant-travel/ui/components/checkbox";
|
|
5
|
+
import { Input } from "@voyant-travel/ui/components/input";
|
|
6
|
+
import { Separator } from "@voyant-travel/ui/components/separator";
|
|
7
|
+
import { X } from "lucide-react";
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
import { useCatalogUiMessagesOrDefault } from "../i18n/index.js";
|
|
10
|
+
/** How many facet values show before the "Show all" toggle. */
|
|
11
|
+
const DEFAULT_VISIBLE = 6;
|
|
12
|
+
/**
|
|
13
|
+
* Persistent, sectioned left filter rail (Booking.com-style). Facets render
|
|
14
|
+
* as inline checkbox lists with live counts; numeric ranges render as inline
|
|
15
|
+
* min/max inputs. Replaces the wrapping chip row for a scannable, always-on
|
|
16
|
+
* filtering surface. Selection state is owned by the parent
|
|
17
|
+
* (`CatalogSearchPage`); this component is presentational.
|
|
18
|
+
*/
|
|
19
|
+
export function CatalogFilterRail({ fields, facetGroups, selectedFacets, selectedRanges, onToggleFacet, onClearFacet, onSetRange, onClearAll, hasSelections, }) {
|
|
20
|
+
const messages = useCatalogUiMessagesOrDefault().catalogPage;
|
|
21
|
+
return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "font-medium text-sm", children: messages.view.filters }), hasSelections && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: onClearAll, className: "h-7 px-2 text-muted-foreground text-xs hover:text-foreground", children: [_jsx(X, { className: "mr-1 h-3 w-3" }), messages.search.clearAll] }))] }), fields.map((field, index) => {
|
|
22
|
+
const isRange = (field.kind ?? "facet") === "range";
|
|
23
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [index > 0 && _jsx(Separator, { className: "mb-1" }), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "font-medium text-muted-foreground text-xs uppercase tracking-wide", children: field.label }), !isRange && (selectedFacets[field.field]?.length ?? 0) > 0 && (_jsx("button", { type: "button", onClick: () => onClearFacet(field.field), className: "text-muted-foreground text-xs hover:text-foreground", children: messages.filtersUi.clear }))] }), isRange ? (_jsx(RangeSection, { field: field, value: selectedRanges[field.field], onChange: (next) => onSetRange(field.field, next) })) : (_jsx(FacetSection, { field: field, buckets: facetGroups[field.field] ?? [], selected: selectedFacets[field.field] ?? [], onToggle: (value) => onToggleFacet(field.field, value) }))] }, field.field));
|
|
24
|
+
})] }));
|
|
25
|
+
}
|
|
26
|
+
function FacetSection({ field, buckets, selected, onToggle, }) {
|
|
27
|
+
const messages = useCatalogUiMessagesOrDefault().catalogPage;
|
|
28
|
+
const [expanded, setExpanded] = useState(false);
|
|
29
|
+
const selectedSet = new Set(selected);
|
|
30
|
+
const display = (value) => field.formatValue ? field.formatValue(value) : String(value);
|
|
31
|
+
if (buckets.length === 0) {
|
|
32
|
+
return _jsx("span", { className: "text-muted-foreground text-xs", children: messages.filtersUi.noResults });
|
|
33
|
+
}
|
|
34
|
+
const ordered = sortBuckets(buckets, field.sortValues);
|
|
35
|
+
const visible = expanded ? ordered : ordered.slice(0, DEFAULT_VISIBLE);
|
|
36
|
+
return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [visible.map((bucket) => {
|
|
37
|
+
const checked = selectedSet.has(bucket.value);
|
|
38
|
+
return (_jsxs("button", { type: "button", onClick: () => onToggle(bucket.value), className: "flex w-full items-center gap-2 text-left text-sm", children: [_jsx(Checkbox, { checked: checked, "aria-hidden": true, tabIndex: -1, className: "pointer-events-none" }), _jsx("span", { className: "flex-1 truncate capitalize", children: display(bucket.value) }), _jsx("span", { className: "text-muted-foreground text-xs tabular-nums", children: bucket.count })] }, String(bucket.value)));
|
|
39
|
+
}), buckets.length > DEFAULT_VISIBLE && (_jsx("button", { type: "button", onClick: () => setExpanded((prev) => !prev), className: "self-start text-primary text-xs hover:underline", children: expanded
|
|
40
|
+
? messages.view.showLess
|
|
41
|
+
: messages.view.showAll.replace("{count}", String(buckets.length)) }))] }));
|
|
42
|
+
}
|
|
43
|
+
// Order facet buckets. Default keeps the index's count-descending order;
|
|
44
|
+
// "value-desc"/"value-asc" sort numerically when the values parse as numbers
|
|
45
|
+
// (star ratings 5 → 0), else lexicographically.
|
|
46
|
+
function sortBuckets(buckets, sort) {
|
|
47
|
+
if (!sort || sort === "count")
|
|
48
|
+
return buckets;
|
|
49
|
+
const dir = sort === "value-asc" ? 1 : -1;
|
|
50
|
+
return [...buckets].sort((a, b) => {
|
|
51
|
+
const an = Number(a.value);
|
|
52
|
+
const bn = Number(b.value);
|
|
53
|
+
if (Number.isFinite(an) && Number.isFinite(bn))
|
|
54
|
+
return (an - bn) * dir;
|
|
55
|
+
return String(a.value).localeCompare(String(b.value)) * dir;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function RangeSection({ field, value, onChange, }) {
|
|
59
|
+
const messages = useCatalogUiMessagesOrDefault().catalogPage.filtersUi;
|
|
60
|
+
const [minText, setMinText] = useState(value?.gte != null ? String(value.gte) : "");
|
|
61
|
+
const [maxText, setMaxText] = useState(value?.lte != null ? String(value.lte) : "");
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
setMinText(value?.gte != null ? String(value.gte) : "");
|
|
64
|
+
setMaxText(value?.lte != null ? String(value.lte) : "");
|
|
65
|
+
}, [value]);
|
|
66
|
+
const apply = () => {
|
|
67
|
+
const gte = parseNumber(minText);
|
|
68
|
+
const lte = parseNumber(maxText);
|
|
69
|
+
if (gte == null && lte == null) {
|
|
70
|
+
onChange(undefined);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
onChange({ gte: gte ?? undefined, lte: lte ?? undefined });
|
|
74
|
+
};
|
|
75
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "number", inputMode: "decimal", step: field.step, placeholder: field.minPlaceholder ?? messages.min, value: minText, onChange: (e) => setMinText(e.target.value), onBlur: apply, onKeyDown: (e) => {
|
|
76
|
+
if (e.key === "Enter")
|
|
77
|
+
apply();
|
|
78
|
+
}, className: "h-8" }), _jsx("span", { className: "text-muted-foreground text-xs", children: messages.to }), _jsx(Input, { type: "number", inputMode: "decimal", step: field.step, placeholder: field.maxPlaceholder ?? messages.max, value: maxText, onChange: (e) => setMaxText(e.target.value), onBlur: apply, onKeyDown: (e) => {
|
|
79
|
+
if (e.key === "Enter")
|
|
80
|
+
apply();
|
|
81
|
+
}, className: "h-8" })] }));
|
|
82
|
+
}
|
|
83
|
+
function parseNumber(s) {
|
|
84
|
+
if (!s.trim())
|
|
85
|
+
return null;
|
|
86
|
+
const n = Number(s);
|
|
87
|
+
return Number.isFinite(n) ? n : null;
|
|
88
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared catalog gallery + lightbox — the Booking.com-style mosaic and
|
|
3
|
+
* full-screen viewer used by every catalog detail page (packages, cruises, …)
|
|
4
|
+
* so they all look identical. Layout/lightbox logic lives here once.
|
|
5
|
+
*/
|
|
6
|
+
export interface GalleryImage {
|
|
7
|
+
src: string;
|
|
8
|
+
caption?: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface GalleryLightboxLabels {
|
|
11
|
+
close: string;
|
|
12
|
+
prev: string;
|
|
13
|
+
next: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function Gallery({ images, photosLabel, onOpen, }: {
|
|
16
|
+
images: GalleryImage[];
|
|
17
|
+
photosLabel: string;
|
|
18
|
+
onOpen: (index: number) => void;
|
|
19
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export declare function GalleryLightbox({ images, index, onIndex, onClose, labels, }: {
|
|
21
|
+
images: GalleryImage[];
|
|
22
|
+
index: number;
|
|
23
|
+
onIndex: (i: number) => void;
|
|
24
|
+
onClose: () => void;
|
|
25
|
+
labels: GalleryLightboxLabels;
|
|
26
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
27
|
+
//# sourceMappingURL=catalog-gallery.d.ts.map
|