@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 @@
1
+ {"version":3,"file":"dynamic-catalog-page-parts.d.ts","sourceRoot":"","sources":["../../src/components/dynamic-catalog-page-parts.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,SAAS,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC3D,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CACxD;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,KAAK,cAAc,GAAG,iBAAiB,CAAC,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAA;AACnE,KAAK,WAAW,GAAG,iBAAiB,CAAC,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAA;AAK1E,wBAAgB,sBAAsB,4CA0CrC;AAED,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,MAAM,EACN,CAAC,EACD,MAAM,EACN,MAAM,GACP,EAAE;IACD,IAAI,EAAE,gBAAgB,CAAA;IACtB,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,CAAC,EAAE,cAAc,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACf,2CAmEA"}
@@ -0,0 +1,43 @@
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 { Image as ImageIcon, Plane } from "lucide-react";
5
+ // Loading state for a live availability search — mirrors the results layout
6
+ // (availability calendar + holiday cards) so there's no empty box or jump when
7
+ // offers arrive.
8
+ export function DynamicResultsSkeleton() {
9
+ return (_jsxs("div", { className: "mt-4 grid gap-6 lg:grid-cols-[minmax(0,360px)_1fr]", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("div", { className: "h-4 w-44 animate-pulse rounded bg-muted/30" }), _jsxs("div", { className: "rounded-lg border p-3", children: [_jsxs("div", { className: "mb-3 flex items-center justify-center gap-3", children: [_jsx("div", { className: "h-7 w-7 animate-pulse rounded bg-muted/30" }), _jsx("div", { className: "h-4 w-28 animate-pulse rounded bg-muted/30" }), _jsx("div", { className: "h-7 w-7 animate-pulse rounded bg-muted/30" })] }), _jsx("div", { className: "grid grid-cols-7 gap-1", children: Array.from({ length: 42 }).map((_, i) => (_jsx("div", { className: `rounded-md ${i < 7 ? "h-3 bg-muted/15" : "aspect-square animate-pulse bg-muted/20"}` }, i))) })] })] }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { className: "h-5 w-56 animate-pulse rounded bg-muted/30" }), _jsx("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "flex flex-col overflow-hidden rounded-lg border", children: [_jsx("div", { className: "aspect-[4/3] w-full animate-pulse bg-muted/40" }), _jsxs("div", { className: "flex flex-col gap-2 p-3", children: [_jsx("div", { className: "h-4 w-3/4 animate-pulse rounded bg-muted/40" }), _jsx("div", { className: "h-3 w-1/2 animate-pulse rounded bg-muted/20" }), _jsx("div", { className: "mt-2 h-5 w-1/3 animate-pulse rounded bg-muted/30" })] })] }, i))) })] })] }));
10
+ }
11
+ export function HolidayCard({ card, onOpen, s, boards, locale, }) {
12
+ const open = () => onOpen(card.productId);
13
+ const stars = formatStars(card.stars);
14
+ const subtitle = [stars, card.destination, formatCountry(card.country, locale)]
15
+ .filter((v) => Boolean(v))
16
+ .join(" · ");
17
+ return (_jsxs("button", { type: "button", onClick: open, className: "group flex flex-col overflow-hidden rounded-lg border text-left transition hover:border-primary/40 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_jsxs("div", { className: "relative aspect-[4/3] w-full overflow-hidden bg-muted", children: [card.image ? (_jsx("img", { src: card.image, alt: card.name ?? "", loading: "lazy", className: "h-full w-full object-cover transition duration-300 group-hover:scale-105" })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center text-muted-foreground", children: _jsx(ImageIcon, { className: "h-8 w-8", "aria-hidden": "true" }) })), card.nights != null && (_jsxs("span", { className: "absolute top-2 right-2 rounded-md bg-background/90 px-2 py-0.5 font-medium text-xs shadow-sm backdrop-blur", children: [card.nights, "n"] }))] }), _jsxs("div", { className: "flex flex-1 flex-col gap-2 p-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate font-medium leading-tight", children: card.name }), subtitle && _jsx("div", { className: "truncate text-muted-foreground text-sm", children: subtitle })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [card.board && (_jsx(Badge, { variant: "outline", className: "font-normal", children: boards[card.board.toUpperCase()] ?? card.board })), _jsxs("span", { className: "flex items-center gap-1 text-muted-foreground text-xs", children: [_jsx(Plane, { className: "h-3.5 w-3.5" }), card.departureAirport && card.arrivalAirport
18
+ ? `${card.departureAirport} → ${card.arrivalAirport}${card.carrier ? ` · ${card.carrier}` : ""}`
19
+ : s.flightIncluded] })] }), _jsxs("div", { className: "mt-auto flex items-end justify-between gap-2 pt-1", children: [_jsxs("div", { children: [card.total && (_jsx("div", { className: "font-semibold text-base", children: formatMoney(card.total, locale) })), card.perPerson && (_jsxs("div", { className: "text-muted-foreground text-xs", children: [formatMoney(card.perPerson, locale), " ", s.perPerson] }))] }), _jsx("span", { className: "shrink-0 rounded-md bg-secondary px-3 py-1.5 font-medium text-secondary-foreground text-sm", children: s.viewDates })] })] })] }));
20
+ }
21
+ function formatMoney(m, locale) {
22
+ return new Intl.NumberFormat(locale, {
23
+ style: "currency",
24
+ currency: m.currency,
25
+ maximumFractionDigits: 0,
26
+ }).format(m.amountMinor / 100);
27
+ }
28
+ function formatStars(value) {
29
+ const n = typeof value === "number" ? value : value ? Number(value) : Number.NaN;
30
+ if (!Number.isFinite(n) || n <= 0)
31
+ return null;
32
+ return `${Number.isInteger(n) ? n : n.toFixed(1)}★`;
33
+ }
34
+ function formatCountry(code, locale) {
35
+ if (!code || !/^[A-Za-z]{2}$/.test(code))
36
+ return code;
37
+ try {
38
+ return new Intl.DisplayNames(locale, { type: "region" }).of(code.toUpperCase()) ?? code;
39
+ }
40
+ catch {
41
+ return code;
42
+ }
43
+ }
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from "react";
2
+ import type { CatalogSearchParams, CatalogSurface } from "../index.js";
3
+ export interface DynamicCatalogPageProps {
4
+ search: CatalogSearchParams;
5
+ onSearchChange: (updater: (prev: CatalogSearchParams) => CatalogSearchParams, replace?: boolean) => void;
6
+ /** `/v1/admin/...` (default) vs `/v1/public/...`. */
7
+ surface?: CatalogSurface;
8
+ /** Localized "Packages" header title. */
9
+ productsLabel: string;
10
+ /** Localized header tagline. */
11
+ productsTagline: string;
12
+ /** Build the detail-page href for a holiday (opened in a new tab). */
13
+ buildDetailHref: (productId: string, ctx: {
14
+ adults: number;
15
+ nights: number;
16
+ }) => string;
17
+ /** Render the indexed browse grid shown when no live search is active. */
18
+ renderBrowseGrid: (locks: {
19
+ lockedFacets: Record<string, Array<string | number>>;
20
+ }) => ReactNode;
21
+ }
22
+ export declare function DynamicCatalogPage({ search, onSearchChange, surface, productsLabel, productsTagline, buildDetailHref, renderBrowseGrid, }: DynamicCatalogPageProps): import("react/jsx-runtime").JSX.Element;
23
+ //# sourceMappingURL=dynamic-catalog-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic-catalog-page.d.ts","sourceRoot":"","sources":["../../src/components/dynamic-catalog-page.tsx"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,SAAS,EAA6C,MAAM,OAAO,CAAA;AAEjF,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAmDtE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,mBAAmB,CAAA;IAC3B,cAAc,EAAE,CACd,OAAO,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,mBAAmB,EAC3D,OAAO,CAAC,EAAE,OAAO,KACd,IAAI,CAAA;IACT,qDAAqD;IACrD,OAAO,CAAC,EAAE,cAAc,CAAA;IACxB,yCAAyC;IACzC,aAAa,EAAE,MAAM,CAAA;IACrB,gCAAgC;IAChC,eAAe,EAAE,MAAM,CAAA;IACvB,sEAAsE;IACtE,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,CAAA;IACvF,0EAA0E;IAC1E,gBAAgB,EAAE,CAAC,KAAK,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAA;KAAE,KAAK,SAAS,CAAA;CACjG;AAaD,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,cAAc,EACd,OAAiB,EACjB,aAAa,EACb,eAAe,EACf,eAAe,EACf,gBAAgB,GACjB,EAAE,uBAAuB,2CA0ZzB"}
@@ -0,0 +1,270 @@
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 { Input } from "@voyant-travel/ui/components/input";
5
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyant-travel/ui/components/select";
6
+ import { Search, X } from "lucide-react";
7
+ import { useCallback, useEffect, useMemo, useState } from "react";
8
+ import { useCatalogUiI18nOrDefault, useCatalogUiMessagesOrDefault } from "../i18n/index.js";
9
+ import { fetchDepartureAirports, fetchPackageSearch, useCatalogSearch, useVoyantCatalogContext, } from "../index.js";
10
+ import { AvailabilityCalendar, compareMonth, monthOfIso, shiftMonth, } from "./availability-calendar.js";
11
+ import { DynamicResultsSkeleton, HolidayCard, } from "./dynamic-catalog-page-parts.js";
12
+ /**
13
+ * Dynamic (FIT) catalog surface — search-first, availability-driven.
14
+ *
15
+ * Flow: free-text + destination + duration → a live `packages/search` across
16
+ * that destination's dynamic hotels → an **availability calendar** (only days
17
+ * with real offers are selectable, each showing a "from" price + how many
18
+ * holidays depart that day) → pick a day → the holidays for that day. With no
19
+ * search active it falls back to the indexed browse grid (`renderBrowseGrid`).
20
+ *
21
+ * Presentational: the data comes from `VoyantCatalogProvider` + the catalog
22
+ * offer hooks/client; navigation (`buildDetailHref`), the embedded browse grid
23
+ * (`renderBrowseGrid`) and the localized header (`productsLabel`/Tagline) are
24
+ * injected by the host.
25
+ *
26
+ * See docs/architecture/catalog-supply-models.md (`dynamic` mechanic).
27
+ */
28
+ const ALL_AIRPORTS = "__all__";
29
+ const ANY_MONTH = "__any__";
30
+ // Search-bar select triggers, sized to match the h-9 inputs. The trigger's
31
+ // height comes from a `data-[size]` variant that out-specifies a plain `h-9`,
32
+ // so we set it through the same variant; `rounded-md` matches the inputs.
33
+ const TRIGGER_CLASS = "h-9 rounded-md data-[size=default]:h-9";
34
+ // How far ahead to scan for departures (the calendar spans this window).
35
+ const SEARCH_LEAD_DAYS = 10;
36
+ const SEARCH_WINDOW_DAYS = 230;
37
+ export function DynamicCatalogPage({ search, onSearchChange, surface = "admin", productsLabel, productsTagline, buildDetailHref, renderBrowseGrid, }) {
38
+ const { baseUrl, fetcher } = useVoyantCatalogContext();
39
+ const browser = useCatalogUiMessagesOrDefault().catalogBrowser;
40
+ const s = browser.search;
41
+ const boards = browser.detail.boards;
42
+ const { locale: resolvedLocale } = useCatalogUiI18nOrDefault();
43
+ const [destination, setDestination] = useState(null);
44
+ const [nights, setNights] = useState("7");
45
+ const [adults, setAdults] = useState(2);
46
+ const [month, setMonth] = useState(ANY_MONTH);
47
+ const [airport, setAirport] = useState(ALL_AIRPORTS);
48
+ const [airportOptions, setAirportOptions] = useState([]);
49
+ const [airportsLoading, setAirportsLoading] = useState(false);
50
+ // Destination picker, derived from the indexed country facet.
51
+ const countriesQuery = useCatalogSearch({
52
+ vertical: "products",
53
+ facets: [{ field: "countryCodes" }],
54
+ pagination: { limit: 1 },
55
+ surface,
56
+ });
57
+ const countries = useMemo(() => {
58
+ const region = new Intl.DisplayNames(resolvedLocale, { type: "region" });
59
+ return (countriesQuery.data?.facets?.countryCodes ?? [])
60
+ .map((b) => String(b.value))
61
+ .filter((code) => /^[A-Za-z]{2}$/.test(code))
62
+ .map((code) => ({ value: code, label: region.of(code.toUpperCase()) ?? code }))
63
+ .sort((a, b) => a.label.localeCompare(b.label));
64
+ }, [countriesQuery.data, resolvedLocale]);
65
+ // "When" options: any time, then the next 8 departure months.
66
+ const monthOptions = useMemo(() => {
67
+ const opts = [{ value: ANY_MONTH, label: s.anyTime }];
68
+ const now = new Date();
69
+ for (let i = 0; i < 8; i++) {
70
+ const d = new Date(now.getFullYear(), now.getMonth() + i, 1);
71
+ opts.push({
72
+ value: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`,
73
+ label: new Intl.DateTimeFormat(resolvedLocale, { month: "short", year: "numeric" }).format(d),
74
+ });
75
+ }
76
+ return opts;
77
+ }, [s.anyTime, resolvedLocale]);
78
+ // Length-of-stay options ("7 nights" / "10 nights" / "14 nights").
79
+ const durations = useMemo(() => [7, 10, 14].map((n) => ({ value: String(n), label: `${n} ${s.nights}` })), [s.nights]);
80
+ const [state, setState] = useState({ status: "browse" });
81
+ const [monthCursor, setMonthCursor] = useState(null);
82
+ const [selected, setSelected] = useState(null);
83
+ const query = (search.q ?? "").trim().toLowerCase();
84
+ // When the destination changes, probe its departure airports so the operator
85
+ // can pick where they fly from BEFORE running the full availability search.
86
+ useEffect(() => {
87
+ if (!destination) {
88
+ setAirportOptions([]);
89
+ setAirport(ALL_AIRPORTS);
90
+ setAirportsLoading(false);
91
+ return;
92
+ }
93
+ let cancelled = false;
94
+ setAirport(ALL_AIRPORTS);
95
+ setAirportOptions([]);
96
+ setAirportsLoading(true);
97
+ void (async () => {
98
+ try {
99
+ const json = await fetchDepartureAirports({ baseUrl, fetcher, surface }, { countryCode: destination });
100
+ if (!cancelled)
101
+ setAirportOptions(json.departureAirports ?? []);
102
+ }
103
+ catch {
104
+ if (!cancelled)
105
+ setAirportOptions([]);
106
+ }
107
+ finally {
108
+ if (!cancelled)
109
+ setAirportsLoading(false);
110
+ }
111
+ })();
112
+ return () => {
113
+ cancelled = true;
114
+ };
115
+ }, [destination, baseUrl, fetcher, surface]);
116
+ const runSearch = useCallback(async () => {
117
+ if (!destination)
118
+ return;
119
+ setState({ status: "loading" });
120
+ setSelected(null);
121
+ setMonthCursor(null);
122
+ const n = Number(nights) || 7;
123
+ const { from, to } = searchWindow(month);
124
+ try {
125
+ const json = await fetchPackageSearch({ baseUrl, fetcher, surface }, {
126
+ countryCode: destination,
127
+ departureDateFrom: from,
128
+ departureDateTo: to,
129
+ adults,
130
+ nights: { min: n, max: n },
131
+ });
132
+ const offers = json.offers ?? [];
133
+ // Refresh the picker from the full search (authoritative over the probe).
134
+ if (json.departureAirports && json.departureAirports.length > 0) {
135
+ setAirportOptions(json.departureAirports);
136
+ }
137
+ setState({
138
+ status: "results",
139
+ offers,
140
+ currency: json.currency ?? "EUR",
141
+ retryable: Boolean(json.retryable),
142
+ });
143
+ }
144
+ catch {
145
+ setState({ status: "error" });
146
+ }
147
+ }, [destination, nights, adults, month, baseUrl, fetcher, surface]);
148
+ const clearSearch = () => {
149
+ setState({ status: "browse" });
150
+ setSelected(null);
151
+ setMonthCursor(null);
152
+ setAirport(ALL_AIRPORTS);
153
+ };
154
+ // Offers filtered by the free-text box (hotel/destination) + departure airport.
155
+ const offers = useMemo(() => {
156
+ if (state.status !== "results")
157
+ return [];
158
+ return state.offers.filter((o) => {
159
+ if (airport !== ALL_AIRPORTS && o.departureAirport !== airport)
160
+ return false;
161
+ if (query && !`${o.name ?? ""} ${o.destination ?? ""}`.toLowerCase().includes(query)) {
162
+ return false;
163
+ }
164
+ return true;
165
+ });
166
+ }, [state, query, airport]);
167
+ // Per-day availability: how many holidays depart each day + the cheapest.
168
+ const byDate = useMemo(() => {
169
+ const map = new Map();
170
+ for (const o of offers) {
171
+ if (!o.checkIn || !o.total)
172
+ continue;
173
+ const day = o.checkIn.slice(0, 10);
174
+ const cur = map.get(day) ?? { hotels: new Set(), fromMinor: Number.POSITIVE_INFINITY };
175
+ cur.hotels.add(o.productId);
176
+ cur.fromMinor = Math.min(cur.fromMinor, o.total.amountMinor);
177
+ map.set(day, cur);
178
+ }
179
+ const out = new Map();
180
+ for (const [day, v] of map)
181
+ out.set(day, { count: v.hotels.size, fromMinor: v.fromMinor });
182
+ return out;
183
+ }, [offers]);
184
+ const availableMonths = useMemo(() => {
185
+ const months = [...byDate.keys()].map(monthOfIso).filter((m) => m != null);
186
+ months.sort(compareMonth);
187
+ // Dedupe consecutive equal months.
188
+ return months.filter((m, i) => i === 0 || compareMonth(m, months[i - 1]) !== 0);
189
+ }, [byDate]);
190
+ // Land the calendar on the first month that has departures + auto-select the
191
+ // earliest available day so the operator sees holidays immediately.
192
+ useEffect(() => {
193
+ if (state.status !== "results")
194
+ return;
195
+ const days = [...byDate.keys()].sort();
196
+ if (days.length === 0) {
197
+ setMonthCursor(null);
198
+ setSelected(null);
199
+ return;
200
+ }
201
+ const first = days[0];
202
+ setMonthCursor(monthOfIso(first));
203
+ setSelected((prev) => (prev && byDate.has(prev) ? prev : first));
204
+ }, [state.status, byDate]);
205
+ // i18n-literal-ok: "results" is a status discriminant and "EUR" a currency-code fallback, not UI copy.
206
+ const currency = state.status === "results" ? state.currency : "EUR";
207
+ const selectedOffers = useMemo(() => {
208
+ if (!selected)
209
+ return [];
210
+ const byHotel = new Map();
211
+ for (const o of offers) {
212
+ if (o.checkIn?.slice(0, 10) !== selected)
213
+ continue;
214
+ const cur = byHotel.get(o.productId);
215
+ if (!cur || (o.total?.amountMinor ?? 0) < (cur.total?.amountMinor ?? 0)) {
216
+ byHotel.set(o.productId, o);
217
+ }
218
+ }
219
+ return [...byHotel.values()].sort((a, b) => (a.total?.amountMinor ?? 0) - (b.total?.amountMinor ?? 0));
220
+ }, [offers, selected]);
221
+ const destinationLabel = useMemo(() => countries.find((c) => c.value === destination)?.label, [countries, destination]);
222
+ // Open the full detail page in a new tab — the live search stays put in this
223
+ // tab so the operator keeps their availability results. Occupancy + length of
224
+ // stay ride along so the detail page's offers match the search.
225
+ const openHoliday = (productId) => {
226
+ const href = buildDetailHref(productId, { adults, nights: Number(nights) || 7 });
227
+ if (typeof window !== "undefined")
228
+ window.open(href, "_blank", "noopener,noreferrer");
229
+ };
230
+ const monthIndex = monthCursor
231
+ ? availableMonths.findIndex((m) => compareMonth(m, monthCursor) === 0)
232
+ : -1;
233
+ return (_jsxs("div", { className: "mx-auto w-full max-w-screen-2xl px-6 py-6 lg:px-8", children: [_jsxs("div", { className: "mb-4", children: [_jsx("h1", { className: "font-semibold text-2xl", children: productsLabel }), _jsx("p", { className: "text-muted-foreground text-sm", children: productsTagline })] }), _jsxs("div", { className: "flex flex-wrap items-end gap-2 rounded-lg border bg-card p-3", children: [_jsxs("div", { className: "flex min-w-[200px] flex-1 flex-col gap-1", children: [_jsx("span", { className: "text-muted-foreground text-xs", children: s.searchLabel }), _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "-translate-y-1/2 absolute top-1/2 left-2.5 h-4 w-4 text-muted-foreground" }), _jsx(Input, { value: search.q ?? "", onChange: (e) => onSearchChange((prev) => ({ ...prev, q: e.target.value || undefined, page: 1 }), true), placeholder: s.searchPlaceholder, className: "h-9 pl-8" })] })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-muted-foreground text-xs", children: s.destination }), _jsxs(Select, { items: countries, value: destination ?? undefined, onValueChange: (value) => setDestination(value), children: [_jsx(SelectTrigger, { className: `${TRIGGER_CLASS} w-[190px]`, "aria-label": s.destination, children: _jsx(SelectValue, { placeholder: s.chooseCountry }) }), _jsx(SelectContent, { children: countries.map((c) => (_jsx(SelectItem, { value: c.value, children: c.label }, c.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-muted-foreground text-xs", children: s.when }), _jsxs(Select, { items: monthOptions, value: month, onValueChange: (v) => setMonth(v), children: [_jsx(SelectTrigger, { className: `${TRIGGER_CLASS} w-[140px]`, "aria-label": s.when, children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: monthOptions.map((m) => (_jsx(SelectItem, { value: m.value, children: m.label }, m.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("span", { className: "text-muted-foreground text-xs", children: [s.flyingFrom, airportsLoading ? ` · ${s.finding}` : ""] }), _jsxs(Select, { value: airport, onValueChange: (v) => setAirport(v || ALL_AIRPORTS), disabled: airportOptions.length === 0, children: [_jsx(SelectTrigger, { className: `${TRIGGER_CLASS} w-[180px]`, "aria-label": s.departureAirport, children: _jsx(SelectValue, { placeholder: destination ? s.loading : s.allAirports }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: ALL_AIRPORTS, children: s.allAirports }), airportOptions.map((a) => (_jsx(SelectItem, { value: a.code, children: a.label }, a.code)))] })] })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-muted-foreground text-xs", children: s.duration }), _jsxs(Select, { items: durations, value: nights, onValueChange: (v) => setNights(v), children: [_jsx(SelectTrigger, { className: `${TRIGGER_CLASS} w-[120px]`, "aria-label": s.duration, children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: durations.map((d) => (_jsx(SelectItem, { value: d.value, children: d.label }, d.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-muted-foreground text-xs", children: s.adults }), _jsx(Input, { type: "number", "aria-label": s.adults, min: 1, value: adults, onChange: (e) => setAdults(Math.max(1, Number(e.target.value) || 1)), className: "h-9 w-[80px]" })] }), _jsxs(Button, { onClick: () => void runSearch(), disabled: !destination || state.status === "loading", className: "h-9", children: [_jsx(Search, { className: "mr-1 h-4 w-4" }), state.status === "loading" ? s.searching : s.searchAvailability] }), state.status !== "browse" && (_jsxs(Button, { variant: "ghost", onClick: clearSearch, className: "h-9", children: [_jsx(X, { className: "mr-1 h-4 w-4" }), " ", s.clear] }))] }), state.status === "browse" ? (_jsx("div", { className: "mt-4", children: renderBrowseGrid({ lockedFacets: { supplyModel: ["dynamic"] } }) })) : state.status === "loading" ? (_jsx(DynamicResultsSkeleton, {})) : state.status === "error" ? (_jsx("div", { className: "mt-4 rounded-md border border-destructive/40 bg-destructive/5 p-6 text-center text-destructive text-sm", children: s.error })) : byDate.size === 0 ? (_jsx("div", { className: "mt-4 rounded-md border border-dashed p-6 text-center text-muted-foreground text-sm", children: state.retryable
234
+ ? s.availabilityUnavailable
235
+ : s.noDepartures
236
+ .replace("{nights}", nights)
237
+ .replace("{destination}", destinationLabel ?? s.thisDestination) })) : (_jsxs("div", { className: "mt-4 grid gap-6 lg:grid-cols-[minmax(0,360px)_1fr]", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "text-muted-foreground text-sm", children: [byDate.size, " ", byDate.size === 1 ? s.departureDate : s.departureDates, destinationLabel ? ` ${s.in} ${destinationLabel}` : ""] }), monthCursor && (_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 }))] }), _jsxs("div", { className: "flex flex-col gap-3", children: [selected && (_jsxs("div", { className: "font-medium text-lg", children: [selectedOffers.length, " ", selectedOffers.length === 1 ? s.holiday : s.holidays, " ", s.departing, " ", formatDay(selected, resolvedLocale)] })), selectedOffers.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-6 text-center text-muted-foreground text-sm", children: s.selectDay })) : (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-3", children: selectedOffers.map((card) => (_jsx(HolidayCard, { card: card, onOpen: openHoliday, s: s, boards: boards, locale: resolvedLocale }, card.productId))) }))] })] }))] }));
238
+ }
239
+ function formatDay(iso, locale) {
240
+ const d = new Date(iso);
241
+ if (Number.isNaN(d.getTime()))
242
+ return iso;
243
+ return new Intl.DateTimeFormat(locale, {
244
+ weekday: "short",
245
+ day: "numeric",
246
+ month: "short",
247
+ year: "numeric",
248
+ }).format(d);
249
+ }
250
+ function addDays(date, days) {
251
+ const next = new Date(date);
252
+ next.setDate(next.getDate() + days);
253
+ return next;
254
+ }
255
+ // Translate the "When" selection into a departure-date window. "Any time" scans
256
+ // the next several months; a specific month clamps to that month (never earlier
257
+ // than the booking lead time).
258
+ function searchWindow(month) {
259
+ const lead = addDays(new Date(), SEARCH_LEAD_DAYS);
260
+ if (month === ANY_MONTH) {
261
+ return { from: isoDate(lead), to: isoDate(addDays(new Date(), SEARCH_WINDOW_DAYS)) };
262
+ }
263
+ const [year, m] = month.split("-").map(Number);
264
+ const start = new Date(year, m - 1, 1);
265
+ const end = new Date(year, m, 0);
266
+ return { from: isoDate(start > lead ? start : lead), to: isoDate(end) };
267
+ }
268
+ function isoDate(date) {
269
+ return date.toISOString().slice(0, 10);
270
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Photo gallery: a carousel thumbnail that opens a full-screen lightbox
3
+ * carousel on click. Used for cruise cabins (Cabins tab) and the cruise
4
+ * Overview media gallery. Falls back to a placeholder when there are no images.
5
+ * Size is controlled by `className` (container) + `imageClassName` (thumbnail).
6
+ */
7
+ export declare function MediaGallery({ images, alt, className, imageClassName, }: {
8
+ images: string[];
9
+ alt: string;
10
+ className?: string;
11
+ imageClassName?: string;
12
+ }): import("react/jsx-runtime").JSX.Element;
13
+ //# sourceMappingURL=media-gallery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-gallery.d.ts","sourceRoot":"","sources":["../../src/components/media-gallery.tsx"],"names":[],"mappings":"AAeA;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,GAAG,EACH,SAAS,EACT,cAAc,GACf,EAAE;IACD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,2CAmEA"}
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, } from "@voyant-travel/ui/components/carousel";
4
+ import { Dialog, DialogContent, DialogTitle } from "@voyant-travel/ui/components/dialog";
5
+ import { cn } from "@voyant-travel/ui/lib/utils";
6
+ import { ImageIcon } from "lucide-react";
7
+ import { useEffect, useState } from "react";
8
+ /**
9
+ * Photo gallery: a carousel thumbnail that opens a full-screen lightbox
10
+ * carousel on click. Used for cruise cabins (Cabins tab) and the cruise
11
+ * Overview media gallery. Falls back to a placeholder when there are no images.
12
+ * Size is controlled by `className` (container) + `imageClassName` (thumbnail).
13
+ */
14
+ export function MediaGallery({ images, alt, className, imageClassName, }) {
15
+ const [open, setOpen] = useState(false);
16
+ const [startIndex, setStartIndex] = useState(0);
17
+ if (images.length === 0) {
18
+ return (_jsx("div", { className: cn("flex h-24 w-36 shrink-0 items-center justify-center rounded-md bg-muted", className), children: _jsx(ImageIcon, { className: "h-5 w-5 text-muted-foreground/50" }) }));
19
+ }
20
+ const openAt = (index) => {
21
+ setStartIndex(index);
22
+ setOpen(true);
23
+ };
24
+ return (_jsxs("div", { className: cn("w-36 shrink-0", className), children: [_jsxs(Carousel, { className: "group relative", opts: { loop: images.length > 1 }, children: [_jsx(CarouselContent, { children: images.map((src, i) => (_jsx(CarouselItem, { children: _jsx("button", { type: "button", onClick: () => openAt(i), className: "block w-full overflow-hidden rounded-md ring-1 ring-border", "aria-label": `${alt} — open photo ${i + 1} of ${images.length}`, children: _jsx("img", { src: src, alt: alt, className: cn("h-24 w-36 object-cover transition group-hover:opacity-90", imageClassName), loading: "lazy" }) }) }, src))) }), images.length > 1 && (_jsxs(_Fragment, { children: [_jsx(CarouselPrevious, { className: "left-1 h-6 w-6 opacity-0 transition group-hover:opacity-100" }), _jsx(CarouselNext, { className: "right-1 h-6 w-6 opacity-0 transition group-hover:opacity-100" }), _jsx("span", { className: "pointer-events-none absolute right-1 bottom-1 rounded bg-black/60 px-1.5 py-0.5 text-[10px] font-medium text-white tabular-nums", children: images.length })] }))] }), _jsx(CabinLightbox, { images: images, alt: alt, startIndex: startIndex, open: open, onOpenChange: setOpen })] }));
25
+ }
26
+ function CabinLightbox({ images, alt, startIndex, open, onOpenChange, }) {
27
+ const [api, setApi] = useState();
28
+ const [current, setCurrent] = useState(startIndex);
29
+ useEffect(() => {
30
+ if (!api)
31
+ return;
32
+ setCurrent(api.selectedScrollSnap());
33
+ const onSelect = () => setCurrent(api.selectedScrollSnap());
34
+ api.on("select", onSelect);
35
+ return () => {
36
+ api.off("select", onSelect);
37
+ };
38
+ }, [api]);
39
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "gap-2 border-0 bg-black/95 p-3 text-white ring-0 sm:max-w-5xl", children: [_jsx(DialogTitle, { className: "sr-only", children: alt }), _jsxs(Carousel
40
+ // Remount per-open so embla honors the clicked start index.
41
+ , { opts: { startIndex, loop: images.length > 1 }, setApi: setApi, className: "w-full", children: [_jsx(CarouselContent, { children: images.map((src, i) => (_jsx(CarouselItem, { className: "flex items-center justify-center", children: _jsx("img", { src: src, alt: `${alt} (${i + 1})`, className: "max-h-[78vh] w-full rounded object-contain" }) }, src))) }), images.length > 1 && (_jsxs(_Fragment, { children: [_jsx(CarouselPrevious, { className: "left-2 border-0 bg-white/15 text-white hover:bg-white/25" }), _jsx(CarouselNext, { className: "right-2 border-0 bg-white/15 text-white hover:bg-white/25" })] }))] }, `${startIndex}-${open}`), images.length > 1 && (_jsxs("div", { className: "text-center text-xs text-white/70 tabular-nums", children: [current + 1, " / ", images.length] }))] }) }));
42
+ }
@@ -0,0 +1,106 @@
1
+ import type { CatalogUiMessages } from "../i18n/messages.js";
2
+ export interface Offer {
3
+ id: string;
4
+ title: string | null;
5
+ checkIn: string | null;
6
+ checkOut: string | null;
7
+ nights: number | null;
8
+ board: string | null;
9
+ roomTypeId: string | null;
10
+ ratePlanId?: string | null;
11
+ perPerson: {
12
+ amountMinor: number;
13
+ currency: string;
14
+ } | null;
15
+ total: {
16
+ amountMinor: number;
17
+ currency: string;
18
+ } | null;
19
+ flights: Array<{
20
+ origin: string | null;
21
+ destination: string | null;
22
+ departureAt: string | null;
23
+ carrier: string | null;
24
+ flightNumber: string | null;
25
+ }>;
26
+ freeCancellationUntil: string | null;
27
+ }
28
+ export interface ProductMedia {
29
+ src: string;
30
+ rel: string | null;
31
+ caption: string | null;
32
+ }
33
+ export interface ProductDetail {
34
+ name: string | null;
35
+ stars: number | null;
36
+ city: string | null;
37
+ region: string | null;
38
+ countryCode: string | null;
39
+ category: string | null;
40
+ media: ProductMedia[];
41
+ sections: Array<{
42
+ title: string | null;
43
+ kind: string | null;
44
+ type: string | null;
45
+ lines: string[];
46
+ }>;
47
+ features: Array<{
48
+ code: string | null;
49
+ label: string | null;
50
+ type: string | null;
51
+ }>;
52
+ rooms: Array<{
53
+ code: string | null;
54
+ name: string | null;
55
+ area: number | null;
56
+ maxGuests: number | null;
57
+ view: string | null;
58
+ specifications: string[];
59
+ image: string | null;
60
+ }>;
61
+ reviews: {
62
+ source: string | null;
63
+ rating: number | null;
64
+ reviewsCount: number | null;
65
+ subratings: Array<{
66
+ name: string | null;
67
+ value: number | null;
68
+ }>;
69
+ } | null;
70
+ }
71
+ export type RoomDetail = ProductDetail["rooms"][number];
72
+ export type DetailMessages = CatalogUiMessages["catalogBrowser"]["detail"];
73
+ export interface OfferGroup {
74
+ /** Room code (offer.roomTypeId / room.code); null when the offer has none. */
75
+ code: string | null;
76
+ /** Matched content room (details, amenities, photo) — null if unmatched. */
77
+ room: RoomDetail | null;
78
+ /** Board/rate options for this room on the selected day, cheapest first. */
79
+ offers: Offer[];
80
+ }
81
+ export declare function sectionIconFor(kind: string | null | undefined): import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
82
+ export declare function ReviewsCard({ reviews, t, }: {
83
+ reviews: NonNullable<ProductDetail["reviews"]>;
84
+ t: DetailMessages;
85
+ }): import("react/jsx-runtime").JSX.Element;
86
+ export declare function RoomRateCard({ group, showImage, onBook, t, locale, }: {
87
+ group: OfferGroup;
88
+ showImage: boolean;
89
+ onBook: (offer: Offer) => void;
90
+ t: DetailMessages;
91
+ locale: string;
92
+ }): import("react/jsx-runtime").JSX.Element | null;
93
+ export declare function boardKeyOf(o: Offer | undefined): string;
94
+ export declare function boardLabel(board: string | null, t: DetailMessages): string;
95
+ export declare function roomCodeOf(roomTypeId: string | null): string | null;
96
+ export declare function humanizeFeature(value: string): string;
97
+ export declare function formatMoney(m: {
98
+ amountMinor: number;
99
+ currency: string;
100
+ }, locale?: string): string;
101
+ export declare function formatStars(value: number | null | undefined): string | null;
102
+ export declare function formatCountry(code: string | null | undefined, locale?: string): string | null;
103
+ export declare function formatDay(iso: string, locale?: string): string;
104
+ export declare function addDays(date: Date, days: number): Date;
105
+ export declare function isoDate(date: Date): string;
106
+ //# sourceMappingURL=product-detail-page-parts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-detail-page-parts.d.ts","sourceRoot":"","sources":["../../src/components/product-detail-page-parts.tsx"],"names":[],"mappings":"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"}