@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.
Files changed (220) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +36 -0
  3. package/dist/admin/catalog-vertical-host.d.ts +45 -0
  4. package/dist/admin/catalog-vertical-host.d.ts.map +1 -0
  5. package/dist/admin/catalog-vertical-host.js +230 -0
  6. package/dist/admin/cruise-detail-host.d.ts +11 -0
  7. package/dist/admin/cruise-detail-host.d.ts.map +1 -0
  8. package/dist/admin/cruise-detail-host.js +33 -0
  9. package/dist/admin/dynamic-catalog-host.d.ts +13 -0
  10. package/dist/admin/dynamic-catalog-host.d.ts.map +1 -0
  11. package/dist/admin/dynamic-catalog-host.js +17 -0
  12. package/dist/admin/index.d.ts +133 -0
  13. package/dist/admin/index.d.ts.map +1 -0
  14. package/dist/admin/index.js +144 -0
  15. package/dist/admin/open-in-new-tab.d.ts +7 -0
  16. package/dist/admin/open-in-new-tab.d.ts.map +1 -0
  17. package/dist/admin/open-in-new-tab.js +10 -0
  18. package/dist/admin/pages/catalog-accommodations-detail-page.d.ts +4 -0
  19. package/dist/admin/pages/catalog-accommodations-detail-page.d.ts.map +1 -0
  20. package/dist/admin/pages/catalog-accommodations-detail-page.js +7 -0
  21. package/dist/admin/pages/catalog-accommodations-index-page.d.ts +8 -0
  22. package/dist/admin/pages/catalog-accommodations-index-page.d.ts.map +1 -0
  23. package/dist/admin/pages/catalog-accommodations-index-page.js +17 -0
  24. package/dist/admin/pages/catalog-cruises-detail-page.d.ts +4 -0
  25. package/dist/admin/pages/catalog-cruises-detail-page.d.ts.map +1 -0
  26. package/dist/admin/pages/catalog-cruises-detail-page.js +7 -0
  27. package/dist/admin/pages/catalog-cruises-index-page.d.ts +8 -0
  28. package/dist/admin/pages/catalog-cruises-index-page.d.ts.map +1 -0
  29. package/dist/admin/pages/catalog-cruises-index-page.js +19 -0
  30. package/dist/admin/pages/catalog-excursions-detail-page.d.ts +4 -0
  31. package/dist/admin/pages/catalog-excursions-detail-page.d.ts.map +1 -0
  32. package/dist/admin/pages/catalog-excursions-detail-page.js +7 -0
  33. package/dist/admin/pages/catalog-excursions-index-page.d.ts +8 -0
  34. package/dist/admin/pages/catalog-excursions-index-page.d.ts.map +1 -0
  35. package/dist/admin/pages/catalog-excursions-index-page.js +12 -0
  36. package/dist/admin/pages/catalog-products-detail-page.d.ts +8 -0
  37. package/dist/admin/pages/catalog-products-detail-page.d.ts.map +1 -0
  38. package/dist/admin/pages/catalog-products-detail-page.js +12 -0
  39. package/dist/admin/pages/catalog-products-index-page.d.ts +8 -0
  40. package/dist/admin/pages/catalog-products-index-page.d.ts.map +1 -0
  41. package/dist/admin/pages/catalog-products-index-page.js +12 -0
  42. package/dist/admin/pages/catalog-tours-detail-page.d.ts +4 -0
  43. package/dist/admin/pages/catalog-tours-detail-page.d.ts.map +1 -0
  44. package/dist/admin/pages/catalog-tours-detail-page.js +7 -0
  45. package/dist/admin/pages/catalog-tours-index-page.d.ts +8 -0
  46. package/dist/admin/pages/catalog-tours-index-page.d.ts.map +1 -0
  47. package/dist/admin/pages/catalog-tours-index-page.js +12 -0
  48. package/dist/admin/product-detail-host.d.ts +18 -0
  49. package/dist/admin/product-detail-host.d.ts.map +1 -0
  50. package/dist/admin/product-detail-host.js +40 -0
  51. package/dist/admin/scheduled-catalog-host.d.ts +15 -0
  52. package/dist/admin/scheduled-catalog-host.d.ts.map +1 -0
  53. package/dist/admin/scheduled-catalog-host.js +19 -0
  54. package/dist/admin/vertical-detail-host.d.ts +13 -0
  55. package/dist/admin/vertical-detail-host.d.ts.map +1 -0
  56. package/dist/admin/vertical-detail-host.js +62 -0
  57. package/dist/booking-engine/index.d.ts +26 -0
  58. package/dist/booking-engine/index.d.ts.map +1 -0
  59. package/dist/booking-engine/index.js +25 -0
  60. package/dist/booking-engine/use-booking-commit.d.ts +61 -0
  61. package/dist/booking-engine/use-booking-commit.d.ts.map +1 -0
  62. package/dist/booking-engine/use-booking-commit.js +47 -0
  63. package/dist/booking-engine/use-booking-draft-shape.d.ts +20 -0
  64. package/dist/booking-engine/use-booking-draft-shape.d.ts.map +1 -0
  65. package/dist/booking-engine/use-booking-draft-shape.js +9 -0
  66. package/dist/booking-engine/use-booking-draft.d.ts +102 -0
  67. package/dist/booking-engine/use-booking-draft.d.ts.map +1 -0
  68. package/dist/booking-engine/use-booking-draft.js +93 -0
  69. package/dist/booking-engine/use-booking-hold.d.ts +30 -0
  70. package/dist/booking-engine/use-booking-hold.d.ts.map +1 -0
  71. package/dist/booking-engine/use-booking-hold.js +44 -0
  72. package/dist/booking-engine/use-booking-journey-api.d.ts +23 -0
  73. package/dist/booking-engine/use-booking-journey-api.d.ts.map +1 -0
  74. package/dist/booking-engine/use-booking-journey-api.js +24 -0
  75. package/dist/booking-engine/use-booking-quote.d.ts +711 -0
  76. package/dist/booking-engine/use-booking-quote.d.ts.map +1 -0
  77. package/dist/booking-engine/use-booking-quote.js +122 -0
  78. package/dist/catalog-enrichment-mappers.d.ts +162 -0
  79. package/dist/catalog-enrichment-mappers.d.ts.map +1 -0
  80. package/dist/catalog-enrichment-mappers.js +190 -0
  81. package/dist/catalog-enrichment.d.ts +203 -0
  82. package/dist/catalog-enrichment.d.ts.map +1 -0
  83. package/dist/catalog-enrichment.js +130 -0
  84. package/dist/catalog-offers-client.d.ts +58 -0
  85. package/dist/catalog-offers-client.d.ts.map +1 -0
  86. package/dist/catalog-offers-client.js +61 -0
  87. package/dist/catalog-search-params.d.ts +45 -0
  88. package/dist/catalog-search-params.d.ts.map +1 -0
  89. package/dist/catalog-search-params.js +30 -0
  90. package/dist/catalog-surfaces.d.ts +17 -0
  91. package/dist/catalog-surfaces.d.ts.map +1 -0
  92. package/dist/catalog-surfaces.js +26 -0
  93. package/dist/client.d.ts +20 -0
  94. package/dist/client.d.ts.map +1 -0
  95. package/dist/client.js +65 -0
  96. package/dist/components/availability-calendar.d.ts +33 -0
  97. package/dist/components/availability-calendar.d.ts.map +1 -0
  98. package/dist/components/availability-calendar.js +65 -0
  99. package/dist/components/catalog-browse-page.d.ts +41 -0
  100. package/dist/components/catalog-browse-page.d.ts.map +1 -0
  101. package/dist/components/catalog-browse-page.js +47 -0
  102. package/dist/components/catalog-card.d.ts +68 -0
  103. package/dist/components/catalog-card.d.ts.map +1 -0
  104. package/dist/components/catalog-card.js +52 -0
  105. package/dist/components/catalog-detail-cruise-cards.d.ts +16 -0
  106. package/dist/components/catalog-detail-cruise-cards.d.ts.map +1 -0
  107. package/dist/components/catalog-detail-cruise-cards.js +54 -0
  108. package/dist/components/catalog-detail-departures.d.ts +25 -0
  109. package/dist/components/catalog-detail-departures.d.ts.map +1 -0
  110. package/dist/components/catalog-detail-departures.js +240 -0
  111. package/dist/components/catalog-detail-parts.d.ts +70 -0
  112. package/dist/components/catalog-detail-parts.d.ts.map +1 -0
  113. package/dist/components/catalog-detail-parts.js +282 -0
  114. package/dist/components/catalog-detail-sheet.d.ts +93 -0
  115. package/dist/components/catalog-detail-sheet.d.ts.map +1 -0
  116. package/dist/components/catalog-detail-sheet.js +68 -0
  117. package/dist/components/catalog-detail-view.d.ts +39 -0
  118. package/dist/components/catalog-detail-view.d.ts.map +1 -0
  119. package/dist/components/catalog-detail-view.js +157 -0
  120. package/dist/components/catalog-enrichment-fetchers.d.ts +8 -0
  121. package/dist/components/catalog-enrichment-fetchers.d.ts.map +1 -0
  122. package/dist/components/catalog-enrichment-fetchers.js +7 -0
  123. package/dist/components/catalog-faceted-filter.d.ts +30 -0
  124. package/dist/components/catalog-faceted-filter.d.ts.map +1 -0
  125. package/dist/components/catalog-faceted-filter.js +24 -0
  126. package/dist/components/catalog-filter-rail.d.ts +25 -0
  127. package/dist/components/catalog-filter-rail.d.ts.map +1 -0
  128. package/dist/components/catalog-filter-rail.js +88 -0
  129. package/dist/components/catalog-gallery.d.ts +27 -0
  130. package/dist/components/catalog-gallery.d.ts.map +1 -0
  131. package/dist/components/catalog-gallery.js +66 -0
  132. package/dist/components/catalog-hit.d.ts +27 -0
  133. package/dist/components/catalog-hit.d.ts.map +1 -0
  134. package/dist/components/catalog-hit.js +57 -0
  135. package/dist/components/catalog-page-cards.d.ts +21 -0
  136. package/dist/components/catalog-page-cards.d.ts.map +1 -0
  137. package/dist/components/catalog-page-cards.js +174 -0
  138. package/dist/components/catalog-page-config.d.ts +17 -0
  139. package/dist/components/catalog-page-config.d.ts.map +1 -0
  140. package/dist/components/catalog-page-config.js +369 -0
  141. package/dist/components/catalog-page.d.ts +88 -0
  142. package/dist/components/catalog-page.d.ts.map +1 -0
  143. package/dist/components/catalog-page.js +148 -0
  144. package/dist/components/catalog-range-filter.d.ts +34 -0
  145. package/dist/components/catalog-range-filter.d.ts.map +1 -0
  146. package/dist/components/catalog-range-filter.js +72 -0
  147. package/dist/components/catalog-search-page.d.ts +239 -0
  148. package/dist/components/catalog-search-page.d.ts.map +1 -0
  149. package/dist/components/catalog-search-page.js +63 -0
  150. package/dist/components/catalog-search-tab-panel.d.ts +42 -0
  151. package/dist/components/catalog-search-tab-panel.d.ts.map +1 -0
  152. package/dist/components/catalog-search-tab-panel.js +199 -0
  153. package/dist/components/catalog-vertical-detail-page.d.ts +33 -0
  154. package/dist/components/catalog-vertical-detail-page.d.ts.map +1 -0
  155. package/dist/components/catalog-vertical-detail-page.js +100 -0
  156. package/dist/components/cruise-detail-page-parts.d.ts +72 -0
  157. package/dist/components/cruise-detail-page-parts.d.ts.map +1 -0
  158. package/dist/components/cruise-detail-page-parts.js +146 -0
  159. package/dist/components/cruise-detail-page.d.ts +21 -0
  160. package/dist/components/cruise-detail-page.d.ts.map +1 -0
  161. package/dist/components/cruise-detail-page.js +201 -0
  162. package/dist/components/dynamic-catalog-page-parts.d.ts +40 -0
  163. package/dist/components/dynamic-catalog-page-parts.d.ts.map +1 -0
  164. package/dist/components/dynamic-catalog-page-parts.js +43 -0
  165. package/dist/components/dynamic-catalog-page.d.ts +23 -0
  166. package/dist/components/dynamic-catalog-page.d.ts.map +1 -0
  167. package/dist/components/dynamic-catalog-page.js +270 -0
  168. package/dist/components/media-gallery.d.ts +13 -0
  169. package/dist/components/media-gallery.d.ts.map +1 -0
  170. package/dist/components/media-gallery.js +42 -0
  171. package/dist/components/product-detail-page-parts.d.ts +106 -0
  172. package/dist/components/product-detail-page-parts.d.ts.map +1 -0
  173. package/dist/components/product-detail-page-parts.js +130 -0
  174. package/dist/components/product-detail-page.d.ts +57 -0
  175. package/dist/components/product-detail-page.d.ts.map +1 -0
  176. package/dist/components/product-detail-page.js +175 -0
  177. package/dist/components/scheduled-catalog-page.d.ts +34 -0
  178. package/dist/components/scheduled-catalog-page.d.ts.map +1 -0
  179. package/dist/components/scheduled-catalog-page.js +6 -0
  180. package/dist/hooks/index.d.ts +3 -0
  181. package/dist/hooks/index.d.ts.map +1 -0
  182. package/dist/hooks/index.js +2 -0
  183. package/dist/hooks/use-catalog-offers.d.ts +186 -0
  184. package/dist/hooks/use-catalog-offers.d.ts.map +1 -0
  185. package/dist/hooks/use-catalog-offers.js +105 -0
  186. package/dist/hooks/use-catalog-search.d.ts +109 -0
  187. package/dist/hooks/use-catalog-search.d.ts.map +1 -0
  188. package/dist/hooks/use-catalog-search.js +52 -0
  189. package/dist/i18n/en.d.ts +397 -0
  190. package/dist/i18n/en.d.ts.map +1 -0
  191. package/dist/i18n/en.js +396 -0
  192. package/dist/i18n/index.d.ts +5 -0
  193. package/dist/i18n/index.d.ts.map +1 -0
  194. package/dist/i18n/index.js +3 -0
  195. package/dist/i18n/messages.d.ts +335 -0
  196. package/dist/i18n/messages.d.ts.map +1 -0
  197. package/dist/i18n/messages.js +1 -0
  198. package/dist/i18n/provider.d.ts +816 -0
  199. package/dist/i18n/provider.d.ts.map +1 -0
  200. package/dist/i18n/provider.js +44 -0
  201. package/dist/i18n/ro.d.ts +397 -0
  202. package/dist/i18n/ro.d.ts.map +1 -0
  203. package/dist/i18n/ro.js +396 -0
  204. package/dist/index.d.ts +9 -0
  205. package/dist/index.d.ts.map +1 -0
  206. package/dist/index.js +17 -0
  207. package/dist/provider.d.ts +2 -0
  208. package/dist/provider.d.ts.map +1 -0
  209. package/dist/provider.js +1 -0
  210. package/dist/schemas-catalog-offers.d.ts +290 -0
  211. package/dist/schemas-catalog-offers.d.ts.map +1 -0
  212. package/dist/schemas-catalog-offers.js +155 -0
  213. package/dist/schemas.d.ts +143 -0
  214. package/dist/schemas.d.ts.map +1 -0
  215. package/dist/schemas.js +76 -0
  216. package/dist/ui.d.ts +19 -0
  217. package/dist/ui.d.ts.map +1 -0
  218. package/dist/ui.js +18 -0
  219. package/package.json +150 -0
  220. package/src/styles.css +11 -0
@@ -0,0 +1,33 @@
1
+ export type CatalogVerticalDetailVertical = "products" | "cruises" | "accommodations";
2
+ export interface CatalogVerticalDetailBreadcrumb {
3
+ label: string;
4
+ href?: string;
5
+ }
6
+ export interface CatalogVerticalDetailPageProps {
7
+ id: string;
8
+ /** Content vertical backing the surface (excursions/tours → products). */
9
+ vertical: CatalogVerticalDetailVertical;
10
+ /** Localized surface name — header fallback + breadcrumb root. */
11
+ surfaceLabel: string;
12
+ /** Href of the surface's browse page, e.g. `/catalog/cruises`. */
13
+ surfaceHref: string;
14
+ /** BCP-47 locale forwarded to the content route. Defaults to the i18n locale. */
15
+ locale?: string;
16
+ /** Per-vertical content mount paths. Defaults to the operator admin routes. */
17
+ contentBasePathByVertical?: Record<string, string>;
18
+ /** Resolve a supplier id to a display name (host's supplier directory). */
19
+ formatSupplier?: (id: string) => string;
20
+ /** Route to the booking journey for this entity. `departureDate` + name/hero
21
+ * let the journey pre-fill the date and preview the panel rather than blank. */
22
+ onBook: (vertical: string, id: string, opts: {
23
+ departureId?: string;
24
+ optionId?: string;
25
+ departureDate?: string | null;
26
+ name?: string | null;
27
+ heroImageUrl?: string | null;
28
+ }) => void;
29
+ /** Publish breadcrumbs as the resolved name changes (host feeds its breadcrumb sink). */
30
+ onBreadcrumbs?: (crumbs: CatalogVerticalDetailBreadcrumb[]) => void;
31
+ }
32
+ export declare function CatalogVerticalDetailPage({ id, vertical, surfaceLabel, surfaceHref, locale, contentBasePathByVertical, formatSupplier, onBook, onBreadcrumbs, }: CatalogVerticalDetailPageProps): import("react/jsx-runtime").JSX.Element;
33
+ //# sourceMappingURL=catalog-vertical-detail-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-vertical-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/catalog-vertical-detail-page.tsx"],"names":[],"mappings":"AAkCA,MAAM,MAAM,6BAA6B,GAAG,UAAU,GAAG,SAAS,GAAG,gBAAgB,CAAA;AAErF,MAAM,WAAW,+BAA+B;IAC9C,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,8BAA8B;IAC7C,EAAE,EAAE,MAAM,CAAA;IACV,0EAA0E;IAC1E,QAAQ,EAAE,6BAA6B,CAAA;IACvC,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAA;IACpB,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAA;IACnB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+EAA+E;IAC/E,yBAAyB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClD,2EAA2E;IAC3E,cAAc,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAA;IACvC;qFACiF;IACjF,MAAM,EAAE,CACN,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;QACJ,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC7B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACpB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAC7B,KACE,IAAI,CAAA;IACT,yFAAyF;IACzF,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,+BAA+B,EAAE,KAAK,IAAI,CAAA;CACpE;AAED,wBAAgB,yBAAyB,CAAC,EACxC,EAAE,EACF,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,MAAM,EACN,yBAA4D,EAC5D,cAAc,EACd,MAAM,EACN,aAAa,GACd,EAAE,8BAA8B,2CAgJhC"}
@@ -0,0 +1,100 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Image as ImageIcon } from "lucide-react";
4
+ import { useEffect, useMemo, useState } from "react";
5
+ import { useCatalogUiI18nOrDefault, useCatalogUiMessagesOrDefault } from "../i18n/index.js";
6
+ import { fetchCatalogSlots, useVoyantCatalogContext } from "../index.js";
7
+ import { CatalogDetailView } from "./catalog-detail-sheet.js";
8
+ import { createCatalogEnrichmentFetchers, } from "./catalog-enrichment-fetchers.js";
9
+ /**
10
+ * Generic, full-page, URL-addressable detail page for the non-package catalog
11
+ * surfaces (cruises, accommodations, excursions, tours). Reads the rich content
12
+ * **directly from the content route by id** (the same source the detail sheet
13
+ * uses — keyed by the catalog id, so it works where a Typesense `id` filter
14
+ * doesn't) and renders the shared `CatalogDetailView`. Opened in a new tab from
15
+ * the surface's results. Packages keep their own bespoke detail page.
16
+ *
17
+ * Presentational: navigation (`onBook`), breadcrumbs (`onBreadcrumbs`) and
18
+ * supplier-name resolution (`formatSupplier`) are injected by the host; the
19
+ * content base URL + fetcher come from `VoyantCatalogProvider`.
20
+ */
21
+ const DEFAULT_CONTENT_BASE_BY_VERTICAL = {
22
+ products: "/v1/admin/products",
23
+ cruises: "/v1/admin/cruises",
24
+ accommodations: "/v1/admin/accommodations",
25
+ };
26
+ export function CatalogVerticalDetailPage({ id, vertical, surfaceLabel, surfaceHref, locale, contentBasePathByVertical = DEFAULT_CONTENT_BASE_BY_VERTICAL, formatSupplier, onBook, onBreadcrumbs, }) {
27
+ const { baseUrl, fetcher } = useVoyantCatalogContext();
28
+ const messages = useCatalogUiMessagesOrDefault().catalogBrowser;
29
+ const { locale: resolvedLocale } = useCatalogUiI18nOrDefault();
30
+ const contentLocale = locale ?? resolvedLocale;
31
+ const fetchers = useMemo(() => createCatalogEnrichmentFetchers({
32
+ baseUrl,
33
+ contentBasePathByVertical,
34
+ credentials: "include",
35
+ locale: contentLocale,
36
+ formatSupplier,
37
+ loadSlotAvailability: (productId) => fetchCatalogSlots({ baseUrl, fetcher }, { entityModule: "products", entityId: productId })
38
+ .then((res) => new Map(res.rows.map((slot) => [slot.id, slot])))
39
+ .catch(() => new Map()),
40
+ }), [baseUrl, fetcher, contentBasePathByVertical, contentLocale, formatSupplier]);
41
+ // Minimal hit — the detail body keys off `hit.id` for content + the slots
42
+ // call; the index projection isn't available by id, so fields stay empty and
43
+ // the rich content comes from the enrichment.
44
+ const hit = useMemo(() => ({ id, score: 0, document: { id, fields: {} } }), [id]);
45
+ const [enrichment, setEnrichment] = useState(null);
46
+ const [status, setStatus] = useState("loading");
47
+ useEffect(() => {
48
+ let cancelled = false;
49
+ setStatus("loading");
50
+ setEnrichment(null);
51
+ void (async () => {
52
+ try {
53
+ const enr = await fetchers.loadProductDetail(hit, vertical);
54
+ if (cancelled)
55
+ return;
56
+ if (!enr) {
57
+ setStatus("notfound");
58
+ return;
59
+ }
60
+ setEnrichment(enr);
61
+ setStatus("ready");
62
+ }
63
+ catch {
64
+ if (!cancelled)
65
+ setStatus("error");
66
+ }
67
+ })();
68
+ return () => {
69
+ cancelled = true;
70
+ };
71
+ }, [hit, vertical, fetchers]);
72
+ const name = enrichment?.name ?? null;
73
+ useEffect(() => {
74
+ if (!onBreadcrumbs)
75
+ return;
76
+ onBreadcrumbs(name
77
+ ? [{ label: surfaceLabel, href: surfaceHref }, { label: name }]
78
+ : [{ label: surfaceLabel, href: surfaceHref }]);
79
+ }, [name, surfaceLabel, surfaceHref, onBreadcrumbs]);
80
+ // Booking: route to the unified journey. entityModule = the content vertical.
81
+ // (enrichment is non-null whenever the detail view — the only caller — renders.)
82
+ const book = (departureId, optionId, departureDate) => {
83
+ onBook(vertical, id, {
84
+ ...(departureId ? { departureId } : {}),
85
+ ...(optionId ? { optionId } : {}),
86
+ departureDate: departureDate ?? null,
87
+ name: enrichment?.name ?? null,
88
+ heroImageUrl: enrichment?.heroImageUrl ?? null,
89
+ });
90
+ };
91
+ if (status === "loading") {
92
+ return (_jsxs("div", { className: "mx-auto w-full max-w-screen-2xl px-6 py-6 lg:px-8", children: [_jsx("div", { className: "h-7 w-1/3 animate-pulse rounded bg-muted/40" }), _jsx("div", { className: "mt-2 h-4 w-1/4 animate-pulse rounded bg-muted/20" }), _jsx("div", { className: "mt-6 h-72 w-full animate-pulse rounded-lg bg-muted/30" })] }));
93
+ }
94
+ if (status === "notfound" || status === "error" || !enrichment) {
95
+ return (_jsx("div", { className: "mx-auto w-full max-w-screen-2xl px-6 py-6 lg:px-8", children: _jsx("div", { className: "rounded-md border border-dashed p-8 text-center text-muted-foreground text-sm", children: status === "notfound" ? messages.detail.notFound : messages.detail.loadError }) }));
96
+ }
97
+ const heroUrl = enrichment.heroImageUrl;
98
+ const subtitle = enrichment.supplier;
99
+ return (_jsxs("div", { className: "mx-auto w-full max-w-screen-2xl px-6 py-6 lg:px-8", children: [_jsxs("div", { className: "flex items-start gap-4", children: [heroUrl ? (_jsx("img", { src: heroUrl, alt: name ?? "", className: "h-20 w-28 shrink-0 rounded-lg object-cover ring-1 ring-border", loading: "lazy" })) : (_jsx("div", { className: "flex h-20 w-28 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground", children: _jsx(ImageIcon, { className: "h-7 w-7", "aria-hidden": "true" }) })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "font-semibold text-2xl", children: name ?? surfaceLabel }), subtitle && _jsx("p", { className: "mt-1 text-muted-foreground text-sm", children: subtitle })] })] }), _jsx("div", { className: "mt-6", children: _jsx(CatalogDetailView, { hit: hit, enrichment: enrichment, vertical: vertical, onLoadDeparturePricing: (h, sailingRef) => fetchers.loadDeparturePricing(h, sailingRef, vertical), onBookDeparture: (_h, departure) => book(departure.sourceRef ?? departure.id, undefined, departure.startsAt), onBookOption: (_h, departure, option) => book(departure.sourceRef ?? departure.id, option.id, departure.startsAt) }) })] }));
100
+ }
@@ -0,0 +1,72 @@
1
+ import type { CatalogUiMessages } from "../i18n/messages.js";
2
+ export interface CruiseSailing {
3
+ id: string | null;
4
+ sourceRef: string | null;
5
+ startDate: string | null;
6
+ endDate: string | null;
7
+ nights: number | null;
8
+ status: string | null;
9
+ embarkationPort: string | null;
10
+ disembarkationPort: string | null;
11
+ lowestPriceCents: number | null;
12
+ currency: string | null;
13
+ }
14
+ export interface CruiseCabin {
15
+ id: string;
16
+ /** Provider cabin code (e.g. `omi_V`) — joins to live pricing rows. */
17
+ externalId: string | null;
18
+ name: string;
19
+ type: string | null;
20
+ view: string | null;
21
+ squareFeet: string | null;
22
+ capacityMin: number | null;
23
+ capacityMax: number | null;
24
+ images: string[];
25
+ inclusions: string[];
26
+ }
27
+ export interface CabinPrice {
28
+ code: string;
29
+ fromAmountMinor: number;
30
+ available: boolean;
31
+ }
32
+ export interface CruiseStop {
33
+ dayNumber: number | null;
34
+ date: string | null;
35
+ portName: string | null;
36
+ arrivalTime: string | null;
37
+ departureTime: string | null;
38
+ isAtSea: boolean;
39
+ description: string | null;
40
+ }
41
+ export interface CruiseDetail {
42
+ name: string | null;
43
+ description: string | null;
44
+ cruiseType: string | null;
45
+ cruiseLine: string | null;
46
+ nights: number | null;
47
+ heroImageUrl: string | null;
48
+ highlights: string[];
49
+ embarkationPort: string | null;
50
+ disembarkationPort: string | null;
51
+ ship: {
52
+ name: string | null;
53
+ shipType: string | null;
54
+ description: string | null;
55
+ capacity: number | null;
56
+ decks: number | null;
57
+ yearBuilt: number | null;
58
+ gallery: string[];
59
+ } | null;
60
+ sailings: CruiseSailing[];
61
+ cabins: CruiseCabin[];
62
+ itinerary: CruiseStop[];
63
+ }
64
+ export type SearchMessages = CatalogUiMessages["catalogBrowser"]["search"];
65
+ export declare function mapCruiseContent(content: unknown): CruiseDetail | null;
66
+ export declare function formatCruiseType(type: string | null, s: SearchMessages): string | null;
67
+ export declare function formatMoney(m: {
68
+ amountMinor: number;
69
+ currency: string;
70
+ }, locale?: string): string;
71
+ export declare function formatDay(iso: string | null, locale?: string): string;
72
+ //# sourceMappingURL=cruise-detail-page-parts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cruise-detail-page-parts.d.ts","sourceRoot":"","sources":["../../src/components/cruise-detail-page-parts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,146 @@
1
+ // ── Content mapping (Connect cruise content → render shape) ─────────────────
2
+ export function mapCruiseContent(content) {
3
+ const c = asRecord(content);
4
+ const cruise = asRecord(c?.cruise);
5
+ if (!cruise)
6
+ return null;
7
+ const ship = asRecord(c?.ship);
8
+ const sailingsRaw = Array.isArray(c?.sailings) ? c.sailings : [];
9
+ const cabinsRaw = Array.isArray(c?.cabin_categories) ? c.cabin_categories : [];
10
+ const stopsRaw = Array.isArray(c?.itinerary_stops) ? c.itinerary_stops : [];
11
+ return {
12
+ name: asStr(cruise.name),
13
+ description: asStr(cruise.description),
14
+ cruiseType: asStr(cruise.cruise_type),
15
+ cruiseLine: asStr(cruise.cruise_line),
16
+ nights: asNum(cruise.duration_nights),
17
+ heroImageUrl: asStr(cruise.hero_image_url),
18
+ highlights: asStrArray(cruise.highlights),
19
+ embarkationPort: asStr(cruise.embarkation_port),
20
+ disembarkationPort: asStr(cruise.disembarkation_port),
21
+ ship: ship
22
+ ? {
23
+ name: asStr(ship.name),
24
+ shipType: asStr(ship.ship_type),
25
+ description: asStr(ship.description),
26
+ capacity: asNum(ship.capacity),
27
+ decks: asNum(ship.decks),
28
+ yearBuilt: asNum(ship.year_built),
29
+ gallery: asStrArray(ship.gallery),
30
+ }
31
+ : null,
32
+ sailings: sailingsRaw.map((row) => {
33
+ const r = asRecord(row) ?? {};
34
+ return {
35
+ id: asStr(r.id),
36
+ sourceRef: asStr(r.source_ref),
37
+ startDate: asStr(r.start_date),
38
+ endDate: asStr(r.end_date),
39
+ nights: asNum(r.duration_nights),
40
+ status: asStr(r.status),
41
+ embarkationPort: asStr(r.embarkation_port),
42
+ disembarkationPort: asStr(r.disembarkation_port),
43
+ lowestPriceCents: asNum(r.lowest_price_cents),
44
+ currency: asStr(r.currency),
45
+ };
46
+ }),
47
+ cabins: cabinsRaw.map((row, i) => {
48
+ const r = asRecord(row) ?? {};
49
+ return {
50
+ id: asStr(r.id) ?? `cabin-${i}`,
51
+ externalId: decodeCatalogExternalId(asStr(r.id)),
52
+ // Pure mapper, no messages in scope; "Cabin" is a last-resort fallback for an unnamed upstream cabin, not chrome copy.
53
+ // i18n-literal-ok
54
+ name: asStr(r.name) ?? asStr(r.code) ?? "Cabin",
55
+ type: asStr(r.type),
56
+ view: asStr(r.view_type) ?? asStr(r.type),
57
+ squareFeet: asStr(r.square_feet),
58
+ capacityMin: asNum(r.capacity_min),
59
+ capacityMax: asNum(r.capacity_max),
60
+ images: asStrArray(r.images),
61
+ inclusions: asStrArray(r.inclusions),
62
+ };
63
+ }),
64
+ itinerary: stopsRaw.map((row) => {
65
+ const r = asRecord(row) ?? {};
66
+ return {
67
+ dayNumber: asNum(r.day_number),
68
+ date: asStr(r.date),
69
+ portName: asStr(r.port_name),
70
+ arrivalTime: asStr(r.arrival_time),
71
+ departureTime: asStr(r.departure_time),
72
+ isAtSea: r.is_at_sea === true,
73
+ description: asStr(r.description),
74
+ };
75
+ }),
76
+ };
77
+ }
78
+ export function formatCruiseType(type, s) {
79
+ if (type === "river")
80
+ return s.typeRiver;
81
+ if (type === "ocean")
82
+ return s.typeOcean;
83
+ return type;
84
+ }
85
+ export function formatMoney(m, locale) {
86
+ return new Intl.NumberFormat(locale, {
87
+ style: "currency",
88
+ currency: m.currency,
89
+ maximumFractionDigits: 0,
90
+ }).format(m.amountMinor / 100);
91
+ }
92
+ export function formatDay(iso, locale) {
93
+ if (!iso)
94
+ return "";
95
+ const d = new Date(iso);
96
+ if (Number.isNaN(d.getTime()))
97
+ return iso;
98
+ return new Intl.DateTimeFormat(locale, {
99
+ weekday: "short",
100
+ day: "numeric",
101
+ month: "short",
102
+ year: "numeric",
103
+ }).format(d);
104
+ }
105
+ // Catalog ids are `<prefix>_sr_<base64url(JSON{externalId,…})>`; pull the
106
+ // provider externalId so cabins can join to live pricing rows.
107
+ function decodeCatalogExternalId(id) {
108
+ if (!id)
109
+ return null;
110
+ const idx = id.indexOf("_sr_");
111
+ if (idx < 0)
112
+ return null;
113
+ try {
114
+ const b64 = id
115
+ .slice(idx + 4)
116
+ .replace(/-/g, "+")
117
+ .replace(/_/g, "/");
118
+ const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
119
+ const obj = JSON.parse(atob(padded));
120
+ return typeof obj.externalId === "string" ? obj.externalId : null;
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ function asRecord(value) {
127
+ return value && typeof value === "object" && !Array.isArray(value)
128
+ ? value
129
+ : undefined;
130
+ }
131
+ function asStr(value) {
132
+ return typeof value === "string" && value.length > 0 ? value : null;
133
+ }
134
+ function asNum(value) {
135
+ if (typeof value === "number" && Number.isFinite(value))
136
+ return value;
137
+ if (typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value))) {
138
+ return Number(value);
139
+ }
140
+ return null;
141
+ }
142
+ function asStrArray(value) {
143
+ return Array.isArray(value)
144
+ ? value.filter((v) => typeof v === "string" && v.length > 0)
145
+ : [];
146
+ }
@@ -0,0 +1,21 @@
1
+ import type { CatalogSurface } from "../index.js";
2
+ export interface CruiseDetailPageProps {
3
+ id: string;
4
+ locale?: string;
5
+ surface?: CatalogSurface;
6
+ cruisesLabel: string;
7
+ cruisesHref: string;
8
+ onBook: (cruiseId: string, opts: {
9
+ departureId?: string;
10
+ optionId?: string;
11
+ departureDate?: string | null;
12
+ name?: string | null;
13
+ heroImageUrl?: string | null;
14
+ }) => void;
15
+ onBreadcrumbs?: (crumbs: Array<{
16
+ label: string;
17
+ href?: string;
18
+ }>) => void;
19
+ }
20
+ export declare function CruiseDetailPage({ id, locale, surface, cruisesLabel, cruisesHref, onBook, onBreadcrumbs, }: CruiseDetailPageProps): import("react/jsx-runtime").JSX.Element;
21
+ //# sourceMappingURL=cruise-detail-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cruise-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/cruise-detail-page.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AA0BjD,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,cAAc,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,CACN,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;QACJ,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC7B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACpB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAC7B,KACE,IAAI,CAAA;IACT,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,KAAK,IAAI,CAAA;CAC1E;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,EAAE,EACF,MAAa,EACb,OAAiB,EACjB,YAAY,EACZ,WAAW,EACX,MAAM,EACN,aAAa,GACd,EAAE,qBAAqB,2CAohBvB"}
@@ -0,0 +1,201 @@
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 { Anchor, Check, Ship, Users } from "lucide-react";
6
+ import { useEffect, useMemo, useState } from "react";
7
+ import { useCatalogUiI18nOrDefault, useCatalogUiMessagesOrDefault } from "../i18n/index.js";
8
+ import { fetchCruiseContent, fetchCruisePrice, fetchCruiseSailingPricing, useVoyantCatalogContext, } from "../index.js";
9
+ import { AvailabilityCalendar, compareMonth, monthOfIso, shiftMonth, } from "./availability-calendar.js";
10
+ import { Gallery, GalleryLightbox } from "./catalog-gallery.js";
11
+ import { formatCruiseType, formatDay, formatMoney, mapCruiseContent, } from "./cruise-detail-page-parts.js";
12
+ export function CruiseDetailPage({ id, locale = "ro", surface = "admin", cruisesLabel, cruisesHref, onBook, onBreadcrumbs, }) {
13
+ const { baseUrl, fetcher } = useVoyantCatalogContext();
14
+ const browser = useCatalogUiMessagesOrDefault().catalogBrowser;
15
+ const t = browser.detail;
16
+ const s = browser.search;
17
+ const { locale: resolvedLocale } = useCatalogUiI18nOrDefault();
18
+ const [detail, setDetail] = useState(null);
19
+ // Cruise-level "from" price (from Connect; the content route carries none).
20
+ const [cruisePrice, setCruisePrice] = useState(null);
21
+ const [status, setStatus] = useState("loading");
22
+ const [monthCursor, setMonthCursor] = useState(null);
23
+ const [selected, setSelected] = useState(null);
24
+ const [lightbox, setLightbox] = useState(null);
25
+ const [cabinPricing, setCabinPricing] = useState(null);
26
+ const [pricingLoading, setPricingLoading] = useState(false);
27
+ useEffect(() => {
28
+ let cancelled = false;
29
+ setStatus("loading");
30
+ setDetail(null);
31
+ setCruisePrice(null);
32
+ void (async () => {
33
+ try {
34
+ const [contentJson, priceJson] = await Promise.all([
35
+ fetchCruiseContent({ baseUrl, fetcher, surface }, { cruiseId: id, locale }),
36
+ fetchCruisePrice({ baseUrl, fetcher, surface }, { cruiseId: id }).catch(() => null),
37
+ ]);
38
+ if (cancelled)
39
+ return;
40
+ const mapped = mapCruiseContent(contentJson.data?.content);
41
+ if (!mapped) {
42
+ setStatus("error");
43
+ return;
44
+ }
45
+ setDetail(mapped);
46
+ setStatus("ready");
47
+ if (priceJson) {
48
+ setCruisePrice({
49
+ fromAmountMinor: priceJson.fromAmountMinor ?? null,
50
+ currency: priceJson.currency ?? null,
51
+ });
52
+ }
53
+ const first = mapped.sailings
54
+ .map((sail) => sail.startDate)
55
+ .filter((d) => Boolean(d))
56
+ .sort()[0];
57
+ if (first) {
58
+ setMonthCursor(monthOfIso(first.slice(0, 10)));
59
+ setSelected(first.slice(0, 10));
60
+ }
61
+ }
62
+ catch {
63
+ if (!cancelled)
64
+ setStatus("error");
65
+ }
66
+ })();
67
+ return () => {
68
+ cancelled = true;
69
+ };
70
+ }, [id, locale, baseUrl, fetcher, surface]);
71
+ useEffect(() => {
72
+ if (!onBreadcrumbs)
73
+ return;
74
+ onBreadcrumbs(detail?.name
75
+ ? [{ label: cruisesLabel, href: cruisesHref }, { label: detail.name }]
76
+ : [{ label: cruisesLabel, href: cruisesHref }]);
77
+ }, [detail?.name, cruisesLabel, cruisesHref, onBreadcrumbs]);
78
+ const gallery = useMemo(() => {
79
+ if (!detail)
80
+ return [];
81
+ const urls = [
82
+ detail.heroImageUrl,
83
+ ...(detail.ship?.gallery ?? []),
84
+ ...detail.cabins.flatMap((c) => c.images.slice(0, 1)),
85
+ ].filter((u) => Boolean(u));
86
+ return [...new Set(urls)].map((src) => ({ src }));
87
+ }, [detail]);
88
+ // Per-day availability: sailings departing each day + from-price. Cruise
89
+ // sailings rarely carry a per-sailing price, so fall back to the cruise-level
90
+ // "from" price (Connect); the calendar omits the price line when there's none.
91
+ const byDate = useMemo(() => {
92
+ const fallback = cruisePrice?.fromAmountMinor ?? null;
93
+ const acc = new Map();
94
+ for (const sail of detail?.sailings ?? []) {
95
+ if (!sail.startDate)
96
+ continue;
97
+ const day = sail.startDate.slice(0, 10);
98
+ const cur = acc.get(day) ?? { count: 0, fromMinor: Number.POSITIVE_INFINITY };
99
+ cur.count += 1;
100
+ const price = sail.lowestPriceCents ?? fallback;
101
+ if (price != null)
102
+ cur.fromMinor = Math.min(cur.fromMinor, price);
103
+ acc.set(day, cur);
104
+ }
105
+ const out = new Map();
106
+ for (const [day, v] of acc)
107
+ out.set(day, v);
108
+ return out;
109
+ }, [detail, cruisePrice]);
110
+ const availableMonths = useMemo(() => {
111
+ const months = [...byDate.keys()].map(monthOfIso).filter((m) => m != null);
112
+ months.sort(compareMonth);
113
+ return months.filter((m, i) => i === 0 || compareMonth(m, months[i - 1]) !== 0);
114
+ }, [byDate]);
115
+ const currency = detail?.sailings.find((sail) => sail.currency)?.currency ?? cruisePrice?.currency ?? "USD";
116
+ const fromPrice = useMemo(() => {
117
+ const vals = [...byDate.values()]
118
+ .map((v) => v.fromMinor)
119
+ .filter((v) => Number.isFinite(v) && v > 0);
120
+ return vals.length ? Math.min(...vals) : null;
121
+ }, [byDate]);
122
+ const selectedSailings = useMemo(() => (detail?.sailings ?? [])
123
+ .filter((sail) => sail.startDate?.slice(0, 10) === selected)
124
+ .sort((a, b) => (a.lowestPriceCents ?? 0) - (b.lowestPriceCents ?? 0)), [detail, selected]);
125
+ const monthIndex = monthCursor
126
+ ? availableMonths.findIndex((m) => compareMonth(m, monthCursor) === 0)
127
+ : -1;
128
+ // Live per-cabin pricing for the selected sailing (Connect listSailingPricing).
129
+ const activeRef = selectedSailings[0]?.sourceRef ?? null;
130
+ useEffect(() => {
131
+ if (!activeRef) {
132
+ setCabinPricing(null);
133
+ return;
134
+ }
135
+ let cancelled = false;
136
+ setPricingLoading(true);
137
+ setCabinPricing(null);
138
+ void (async () => {
139
+ try {
140
+ const json = await fetchCruiseSailingPricing({ baseUrl, fetcher, surface }, { cruiseId: id, sailingRef: activeRef });
141
+ if (!cancelled) {
142
+ setCabinPricing({
143
+ sailingRef: activeRef,
144
+ cabins: json.cabins ?? [],
145
+ currency: json.currency ?? null,
146
+ });
147
+ }
148
+ }
149
+ catch {
150
+ if (!cancelled)
151
+ setCabinPricing({ sailingRef: activeRef, cabins: [], currency: null });
152
+ }
153
+ finally {
154
+ if (!cancelled)
155
+ setPricingLoading(false);
156
+ }
157
+ })();
158
+ return () => {
159
+ cancelled = true;
160
+ };
161
+ }, [activeRef, id, baseUrl, fetcher, surface]);
162
+ const book = (sail, cabinCode) => onBook(id, {
163
+ ...(sail.sourceRef || sail.id ? { departureId: sail.sourceRef ?? sail.id ?? "" } : {}),
164
+ ...(cabinCode ? { optionId: cabinCode } : {}),
165
+ departureDate: sail.startDate,
166
+ name: detail?.name ?? null,
167
+ heroImageUrl: detail?.heroImageUrl ?? null,
168
+ });
169
+ if (status === "loading") {
170
+ return (_jsxs("div", { className: "mx-auto w-full max-w-screen-2xl px-6 py-6 lg:px-8", children: [_jsxs("div", { className: "grid h-[340px] grid-cols-4 grid-rows-2 gap-2 sm:h-[440px]", children: [_jsx("div", { className: "col-span-2 row-span-2 animate-pulse rounded-xl bg-muted/40" }), Array.from({ length: 4 }).map((_, i) => (_jsx("div", { className: "animate-pulse rounded-lg bg-muted/30" }, i)))] }), _jsx("div", { className: "mt-5 h-7 w-1/3 animate-pulse rounded bg-muted/40" }), _jsx("div", { className: "mt-6 h-64 w-full animate-pulse rounded-lg bg-muted/30" })] }));
171
+ }
172
+ if (status === "error" || !detail) {
173
+ return (_jsx("div", { className: "mx-auto w-full max-w-screen-2xl px-6 py-6 lg:px-8", children: _jsx("div", { className: "rounded-md border border-dashed p-8 text-center text-muted-foreground text-sm", children: t.loadError }) }));
174
+ }
175
+ const route = [detail.embarkationPort, detail.disembarkationPort]
176
+ .filter((v) => Boolean(v))
177
+ .join(" → ");
178
+ const subtitle = [
179
+ formatCruiseType(detail.cruiseType, s),
180
+ detail.nights != null ? `${detail.nights} ${s.nights}` : null,
181
+ detail.cruiseLine,
182
+ ]
183
+ .filter((v) => Boolean(v))
184
+ .join(" · ");
185
+ return (_jsxs("div", { className: "mx-auto w-full max-w-screen-2xl px-6 py-6 lg:px-8", children: [_jsx(Gallery, { images: gallery, photosLabel: t.photos, onOpen: (i) => setLightbox(i) }), lightbox != null && gallery.length > 0 && (_jsx(GalleryLightbox, { images: gallery, index: lightbox, onIndex: setLightbox, onClose: () => setLightbox(null), labels: { close: t.close, prev: t.prevPhoto, next: t.nextPhoto } })), _jsxs("div", { className: "mt-5 flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "font-semibold text-2xl", children: detail.name ?? cruisesLabel }), _jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-muted-foreground text-sm", children: [subtitle && _jsx("span", { children: subtitle }), route && (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Anchor, { className: "h-3.5 w-3.5" }), " ", route] }))] })] }), fromPrice != null && (_jsxs("div", { className: "shrink-0 text-right", children: [_jsxs("span", { className: "text-muted-foreground text-xs", children: [t.from, " "] }), _jsx("span", { className: "font-semibold text-2xl", children: formatMoney({ amountMinor: fromPrice, currency }, resolvedLocale) })] }))] }), detail.highlights.length > 0 && (_jsx("div", { className: "mt-4 flex flex-wrap gap-1.5", children: detail.highlights.map((h) => (_jsxs(Badge, { variant: "outline", className: "gap-1 font-normal", children: [_jsx(Check, { className: "h-3 w-3 text-emerald-600 dark:text-emerald-400" }), h] }, h))) })), _jsx("h2", { className: "mt-8 mb-2 font-medium text-lg", children: t.datesAndPrices }), byDate.size === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-6 text-center text-muted-foreground text-sm", children: s.noSailings })) : (monthCursor && (_jsxs("div", { className: "grid gap-6 lg:grid-cols-[minmax(0,360px)_1fr]", children: [_jsx(AvailabilityCalendar, { cursor: monthCursor, byDate: byDate, currency: currency, selected: selected, onSelect: setSelected, onPrev: () => setMonthCursor((c) => (c ? shiftMonth(c, -1) : c)), onNext: () => setMonthCursor((c) => (c ? shiftMonth(c, 1) : c)), canPrev: monthIndex > 0, canNext: monthIndex >= 0 && monthIndex < availableMonths.length - 1 }), _jsx("div", { children: selected && selectedSailings.length > 0 ? (_jsx("div", { className: "flex flex-col gap-3", children: selectedSailings.map((sail) => {
186
+ const isActive = sail.sourceRef != null && sail.sourceRef === activeRef;
187
+ const fallbackPrice = sail.lowestPriceCents ?? cruisePrice?.fromAmountMinor;
188
+ return (_jsxs("div", { className: "overflow-hidden rounded-lg border", children: [_jsx("div", { className: "flex items-center justify-between gap-3 border-b bg-muted/20 px-3 py-2.5", children: _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "font-medium text-sm", children: [formatDay(sail.startDate, resolvedLocale), sail.endDate ? ` – ${formatDay(sail.endDate, resolvedLocale)}` : ""] }), _jsxs("div", { className: "flex flex-wrap items-center gap-x-2 text-muted-foreground text-xs", children: [sail.nights != null && (_jsxs("span", { children: [sail.nights, " ", s.nights] })), (sail.embarkationPort || sail.disembarkationPort) && (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Anchor, { className: "h-3 w-3" }), [sail.embarkationPort, sail.disembarkationPort]
189
+ .filter(Boolean)
190
+ .join(" → ")] }))] })] }) }), isActive && pricingLoading ? (_jsx("div", { className: "divide-y", children: Array.from({ length: 4 }).map((_, i) => (_jsxs("div", { className: "flex items-center justify-between px-3 py-3", children: [_jsx("div", { className: "h-4 w-40 animate-pulse rounded bg-muted/40" }), _jsx("div", { className: "h-7 w-24 animate-pulse rounded bg-muted/30" })] }, i))) })) : isActive && cabinPricing && cabinPricing.cabins.length > 0 ? (_jsx("ul", { className: "divide-y", children: cabinPricing.cabins.map((cab) => {
191
+ const cabin = detail.cabins.find((c) => c.externalId === cab.code);
192
+ return (_jsxs("li", { className: "flex items-center justify-between gap-3 px-3 py-2.5", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [_jsx("span", { className: "font-medium text-sm", children: cabin?.name ?? cab.code }), cabin?.view && (_jsx("span", { className: "text-muted-foreground text-xs capitalize", children: cabin.view })), !cab.available && (_jsx(Badge, { variant: "secondary", className: "font-normal", children: t.soldOut }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-3", children: [_jsxs("div", { className: "text-right", children: [_jsxs("span", { className: "text-muted-foreground text-xs", children: [t.from, " "] }), _jsxs("span", { className: "font-semibold text-sm tabular-nums", children: [formatMoney({
193
+ amountMinor: cab.fromAmountMinor,
194
+ currency: cabinPricing.currency ?? currency,
195
+ }, resolvedLocale), " ", t.perPerson] })] }), _jsx(Button, { size: "sm", disabled: !cab.available, onClick: () => book(sail, cab.code), children: t.book })] })] }, cab.code));
196
+ }) })) : (_jsxs("div", { className: "flex items-center justify-between gap-3 px-3 py-3", children: [fallbackPrice != null ? (_jsxs("div", { className: "text-right", children: [_jsxs("span", { className: "text-muted-foreground text-xs", children: [t.from, " "] }), _jsx("span", { className: "font-semibold text-sm tabular-nums", children: formatMoney({
197
+ amountMinor: fallbackPrice,
198
+ currency: sail.currency ?? currency,
199
+ }, resolvedLocale) })] })) : (_jsx("span", {})), _jsx(Button, { size: "sm", onClick: () => book(sail), children: t.book })] }))] }, sail.id ?? sail.sourceRef ?? sail.startDate));
200
+ }) })) : (_jsx("div", { className: "flex h-full items-center justify-center rounded-md border border-dashed p-6 text-center text-muted-foreground text-sm", children: t.selectDate })) })] }))), detail.cabins.length > 0 && (_jsxs("div", { className: "mt-10", children: [_jsx("h2", { className: "mb-3 font-medium text-lg", children: t.cabins }), _jsx("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-3", children: detail.cabins.map((cabin) => (_jsxs("div", { className: "flex flex-col overflow-hidden rounded-lg border", children: [_jsx("div", { className: "relative aspect-[4/3] w-full bg-muted", children: cabin.images[0] ? (_jsx("img", { src: cabin.images[0], alt: cabin.name, className: "h-full w-full object-cover", loading: "lazy" })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center text-muted-foreground/60", children: _jsx(Ship, { className: "h-7 w-7", "aria-hidden": "true" }) })) }), _jsxs("div", { className: "flex flex-1 flex-col gap-1.5 p-3", children: [_jsx("div", { className: "font-medium text-sm leading-tight", children: cabin.name }), _jsxs("div", { className: "flex flex-wrap gap-x-3 gap-y-0.5 text-muted-foreground text-xs", children: [cabin.view && _jsx("span", { className: "capitalize", children: cabin.view }), cabin.squareFeet && _jsxs("span", { children: [cabin.squareFeet, " ft\u00B2"] }), cabin.capacityMax != null && (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Users, { className: "h-3 w-3" }), " ", t.max, " ", cabin.capacityMax] }))] }), cabin.inclusions.length > 0 && (_jsx("p", { className: "mt-0.5 line-clamp-2 text-muted-foreground text-xs", children: cabin.inclusions.join(" · ") }))] })] }, cabin.id))) })] })), detail.itinerary.length > 0 && (_jsxs("div", { className: "mt-10", children: [_jsx("h2", { className: "mb-3 font-medium text-lg", children: t.itinerary }), _jsx("ol", { className: "space-y-2", children: detail.itinerary.map((stop) => (_jsxs("li", { className: "flex gap-3 rounded-lg border p-3", children: [_jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-secondary font-medium text-secondary-foreground text-xs", children: stop.dayNumber ?? "·" }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "font-medium text-sm", children: stop.isAtSea ? t.atSea : (stop.portName ?? t.atSea) }), _jsxs("div", { className: "flex flex-wrap gap-x-2 text-muted-foreground text-xs", children: [stop.arrivalTime && _jsx("span", { children: stop.arrivalTime }), stop.departureTime && _jsxs("span", { children: ["\u2192 ", stop.departureTime] })] }), stop.description && (_jsx("p", { className: "mt-0.5 text-muted-foreground text-xs", children: stop.description }))] })] }, `${stop.dayNumber}-${stop.portName ?? "sea"}`))) })] })), detail.ship && (_jsxs("div", { className: "mt-10", children: [_jsxs("h2", { className: "mb-3 flex items-center gap-1.5 font-medium text-lg", children: [_jsx(Ship, { className: "h-4 w-4 text-muted-foreground" }), detail.ship.name ?? t.ship] }), _jsxs("div", { className: "rounded-lg border p-4", children: [_jsxs("div", { className: "flex flex-wrap gap-x-6 gap-y-1 text-muted-foreground text-sm", children: [detail.ship.shipType && _jsx("span", { className: "capitalize", children: detail.ship.shipType }), detail.ship.capacity != null && (_jsxs("span", { children: [t.capacity, ": ", detail.ship.capacity] })), detail.ship.decks != null && (_jsxs("span", { children: [t.decks, ": ", detail.ship.decks] })), detail.ship.yearBuilt != null && _jsx("span", { children: detail.ship.yearBuilt })] }), detail.ship.description && (_jsx("p", { className: "mt-2 whitespace-pre-line text-muted-foreground text-sm", children: detail.ship.description }))] })] })), detail.description && (_jsxs("div", { className: "mt-10", children: [_jsx("h2", { className: "mb-3 font-medium text-lg", children: t.about }), _jsx("p", { className: "whitespace-pre-line text-muted-foreground text-sm", children: detail.description })] }))] }));
201
+ }
@@ -0,0 +1,40 @@
1
+ import type { CatalogUiMessages } from "../i18n/messages.js";
2
+ export interface SearchResultCard {
3
+ productId: string;
4
+ name: string | null;
5
+ image: string | null;
6
+ stars: string | number | null;
7
+ destination: string | null;
8
+ country: string | null;
9
+ board: string | null;
10
+ checkIn: string | null;
11
+ checkOut: string | null;
12
+ nights: number | null;
13
+ departureAirport: string | null;
14
+ arrivalAirport: string | null;
15
+ carrier: string | null;
16
+ perPerson: {
17
+ amountMinor: number;
18
+ currency: string;
19
+ } | null;
20
+ total: {
21
+ amountMinor: number;
22
+ currency: string;
23
+ } | null;
24
+ }
25
+ export interface AirportOption {
26
+ code: string;
27
+ label: string;
28
+ }
29
+ type SearchMessages = CatalogUiMessages["catalogBrowser"]["search"];
30
+ type BoardLabels = CatalogUiMessages["catalogBrowser"]["detail"]["boards"];
31
+ export declare function DynamicResultsSkeleton(): import("react/jsx-runtime").JSX.Element;
32
+ export declare function HolidayCard({ card, onOpen, s, boards, locale, }: {
33
+ card: SearchResultCard;
34
+ onOpen: (productId: string) => void;
35
+ s: SearchMessages;
36
+ boards: BoardLabels;
37
+ locale: string;
38
+ }): import("react/jsx-runtime").JSX.Element;
39
+ export {};
40
+ //# sourceMappingURL=dynamic-catalog-page-parts.d.ts.map