@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
package/dist/client.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Re-exports the shared HTTP plumbing from `@voyant-travel/inventory-react/client`
3
+ * so the catalog-react package doesn't duplicate the fetcher / error-shaping
4
+ * logic. If/when this drifts, extract into `@voyant-travel/react/client` and
5
+ * point both packages at it.
6
+ */
7
+ export const defaultFetcher = (url, init) => fetch(url, { credentials: "include", ...init });
8
+ export class VoyantApiError extends Error {
9
+ status;
10
+ body;
11
+ constructor(message, status, body) {
12
+ super(message);
13
+ this.name = "VoyantApiError";
14
+ this.status = status;
15
+ this.body = body;
16
+ }
17
+ }
18
+ function extractErrorMessage(status, statusText, body) {
19
+ if (typeof body === "object" && body !== null && "error" in body) {
20
+ const err = body.error;
21
+ if (typeof err === "string")
22
+ return err;
23
+ if (typeof err === "object" && err !== null && "message" in err) {
24
+ return String(err.message);
25
+ }
26
+ }
27
+ return `Voyant API error: ${status} ${statusText}`;
28
+ }
29
+ export async function fetchWithValidation(path, schema, options, init) {
30
+ const url = joinUrl(options.baseUrl, path);
31
+ const headers = new Headers(init?.headers);
32
+ if (init?.body !== undefined && !headers.has("Content-Type")) {
33
+ headers.set("Content-Type", "application/json");
34
+ }
35
+ const response = await options.fetcher(url, { ...init, headers });
36
+ if (!response.ok) {
37
+ const body = await safeJson(response);
38
+ throw new VoyantApiError(extractErrorMessage(response.status, response.statusText, body), response.status, body);
39
+ }
40
+ if (response.status === 204) {
41
+ return schema.parse(undefined);
42
+ }
43
+ const body = await safeJson(response);
44
+ const parsed = schema.safeParse(body);
45
+ if (!parsed.success) {
46
+ throw new VoyantApiError(`Voyant API response failed validation: ${parsed.error.message}`, response.status, body);
47
+ }
48
+ return parsed.data;
49
+ }
50
+ async function safeJson(response) {
51
+ const text = await response.text();
52
+ if (!text)
53
+ return undefined;
54
+ try {
55
+ return JSON.parse(text);
56
+ }
57
+ catch {
58
+ return text;
59
+ }
60
+ }
61
+ function joinUrl(baseUrl, path) {
62
+ const trimmedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
63
+ const trimmedPath = path.startsWith("/") ? path : `/${path}`;
64
+ return `${trimmedBase}${trimmedPath}`;
65
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Availability-aware month calendar. Each day that has live offers is
3
+ * selectable and shows a "from" price plus how many offers/holidays depart
4
+ * that day; days with no inventory are dimmed and disabled — so the operator
5
+ * only ever picks a date that actually has something to sell.
6
+ *
7
+ * Shared by the Dynamic destination search (count = hotels available that day)
8
+ * and the individual product page (count = room/board offers that day).
9
+ */
10
+ export interface DayAvailability {
11
+ count: number;
12
+ fromMinor: number;
13
+ }
14
+ export interface MonthCursor {
15
+ year: number;
16
+ month: number;
17
+ }
18
+ export declare function AvailabilityCalendar({ cursor, byDate, currency, selected, onSelect, onPrev, onNext, canPrev, canNext, }: {
19
+ cursor: MonthCursor;
20
+ byDate: Map<string, DayAvailability>;
21
+ currency: string;
22
+ selected: string | null;
23
+ onSelect: (date: string) => void;
24
+ onPrev: () => void;
25
+ onNext: () => void;
26
+ canPrev?: boolean;
27
+ canNext?: boolean;
28
+ }): import("react/jsx-runtime").JSX.Element;
29
+ export declare function dateKey(year: number, month: number, day: number): string;
30
+ export declare function shiftMonth(c: MonthCursor, delta: number): MonthCursor;
31
+ export declare function monthOfIso(iso: string): MonthCursor | null;
32
+ export declare function compareMonth(a: MonthCursor, b: MonthCursor): number;
33
+ //# sourceMappingURL=availability-calendar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"availability-calendar.d.ts","sourceRoot":"","sources":["../../src/components/availability-calendar.tsx"],"names":[],"mappings":"AAQA;;;;;;;;GAQG;AAEH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,wBAAgB,oBAAoB,CAAC,EACnC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,MAAM,EACN,OAAc,EACd,OAAc,GACf,EAAE;IACD,MAAM,EAAE,WAAW,CAAA;IACnB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,2CA+FA;AAWD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAGrE;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAI1D;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,GAAG,MAAM,CAEnE"}
@@ -0,0 +1,65 @@
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 { ChevronLeft, ChevronRight } from "lucide-react";
5
+ import { useMemo } from "react";
6
+ import { useCatalogUiI18nOrDefault, useCatalogUiMessagesOrDefault } from "../i18n/index.js";
7
+ export function AvailabilityCalendar({ cursor, byDate, currency, selected, onSelect, onPrev, onNext, canPrev = true, canNext = true, }) {
8
+ const cal = useCatalogUiMessagesOrDefault().catalogBrowser.calendar;
9
+ const { locale: resolvedLocale } = useCatalogUiI18nOrDefault();
10
+ const { year, month } = cursor;
11
+ const firstWeekday = (new Date(year, month, 1).getDay() + 6) % 7; // Mon = 0
12
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
13
+ const cells = [];
14
+ for (let i = 0; i < firstWeekday; i++)
15
+ cells.push(null);
16
+ for (let d = 1; d <= daysInMonth; d++)
17
+ cells.push(d);
18
+ const monthLabel = new Intl.DateTimeFormat(resolvedLocale, {
19
+ month: "long",
20
+ year: "numeric",
21
+ }).format(new Date(year, month, 1));
22
+ // Localized short weekday names (Mon→Sun), derived from the app locale so we
23
+ // never hard-code English. 2024-01-01 is a Monday.
24
+ const weekdays = useMemo(() => {
25
+ const fmt = new Intl.DateTimeFormat(resolvedLocale, { weekday: "short" });
26
+ return Array.from({ length: 7 }, (_, i) => fmt.format(new Date(Date.UTC(2024, 0, 1 + i))));
27
+ }, [resolvedLocale]);
28
+ return (_jsxs("div", { className: "rounded-lg border p-3", children: [_jsxs("div", { className: "mb-3 flex items-center justify-center gap-3", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: onPrev, disabled: !canPrev, "aria-label": cal.prevMonth, children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsx("span", { className: "min-w-[10rem] text-center font-medium text-sm capitalize", children: monthLabel }), _jsx(Button, { variant: "outline", size: "sm", onClick: onNext, disabled: !canNext, "aria-label": cal.nextMonth, children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "grid grid-cols-7 gap-1 text-center", children: [weekdays.map((w) => (_jsx("div", { className: "py-1 text-muted-foreground text-xs", children: w }, w))), cells.map((day, i) => {
29
+ if (day == null) {
30
+ // biome-ignore lint/suspicious/noArrayIndexKey: fixed leading blanks -- owner: catalog-react; existing suppression is intentional pending typed cleanup.
31
+ return _jsx("div", {}, `blank-${i}`);
32
+ }
33
+ const key = dateKey(year, month, day);
34
+ const avail = byDate.get(key);
35
+ const isSelected = selected === key;
36
+ if (!avail) {
37
+ return (_jsx("div", { className: "rounded-md border border-transparent py-2.5 text-muted-foreground/30 text-sm", children: day }, key));
38
+ }
39
+ return (_jsxs("button", { type: "button", onClick: () => onSelect(key), className: `flex flex-col items-center gap-0.5 rounded-md border py-1.5 transition hover:border-primary ${isSelected ? "border-primary bg-primary/10" : "border-border"}`, children: [_jsx("span", { className: "font-medium text-sm leading-none", children: day }), Number.isFinite(avail.fromMinor) && avail.fromMinor > 0 && (_jsx("span", { className: "font-semibold text-[11px] text-primary leading-none", children: formatCompact(avail.fromMinor, currency, resolvedLocale) })), _jsxs("span", { className: "text-[10px] text-muted-foreground leading-none", children: [avail.count, " ", avail.count === 1 ? cal.offer : cal.offers] })] }, key));
40
+ })] })] }));
41
+ }
42
+ function formatCompact(amountMinor, currency, locale) {
43
+ return new Intl.NumberFormat(locale, {
44
+ style: "currency",
45
+ currency,
46
+ maximumFractionDigits: 0,
47
+ notation: "compact",
48
+ }).format(amountMinor / 100);
49
+ }
50
+ export function dateKey(year, month, day) {
51
+ return `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
52
+ }
53
+ export function shiftMonth(c, delta) {
54
+ const d = new Date(c.year, c.month + delta, 1);
55
+ return { year: d.getFullYear(), month: d.getMonth() };
56
+ }
57
+ export function monthOfIso(iso) {
58
+ const d = new Date(iso);
59
+ if (Number.isNaN(d.getTime()))
60
+ return null;
61
+ return { year: d.getFullYear(), month: d.getMonth() };
62
+ }
63
+ export function compareMonth(a, b) {
64
+ return a.year !== b.year ? a.year - b.year : a.month - b.month;
65
+ }
@@ -0,0 +1,41 @@
1
+ import { type ReactNode } from "react";
2
+ import type { CatalogSearchParams } from "../index.js";
3
+ import { type CatalogPageProps } from "./catalog-page.js";
4
+ /**
5
+ * Browse surface — the catalog results grid (cards + filter rail + sort/view +
6
+ * detail sheet) wired to a `CatalogSearchParams` URL state. Owns the reusable
7
+ * bits: merging always-on `lockedFacets`/`lockedRanges` into the user's filters
8
+ * (kept out of URL state) and adapting `onSearchChange` into the grid's
9
+ * query/page/view/sort/filter callbacks.
10
+ *
11
+ * Host-specific concerns stay injected: `scopeControls` (market/locale selects),
12
+ * `enrichmentFetchers`, `formatSupplier`, `renderSupplierLink`, booking + editor
13
+ * callbacks, and `onTagsChange`. The Dynamic/Scheduled surfaces embed this via
14
+ * `embedded` to drop its own search box/header/padding under their unified bar.
15
+ */
16
+ export interface CatalogBrowsePageProps extends Pick<CatalogPageProps, "formatSupplier" | "enrichmentFetchers" | "renderSupplierLink" | "onTagsChange" | "onBookHit" | "onBookDeparture" | "onBookOption" | "onOpenProductEditor" | "onOpenProductDetail"> {
17
+ vertical: string;
18
+ search: CatalogSearchParams;
19
+ onSearchChange: (updater: (prev: CatalogSearchParams) => CatalogSearchParams, replace?: boolean) => void;
20
+ /** Resolved display locale — the host resolves it from its markets. */
21
+ locale: string;
22
+ /**
23
+ * Embedded under another surface's unified search bar (the Dynamic/Scheduled
24
+ * page). Hides the built-in search box, header and outer padding so there is
25
+ * a single search experience; `search.q` is driven externally.
26
+ */
27
+ embedded?: boolean;
28
+ /** Facet filters always applied on this surface, never erased by the user. */
29
+ lockedFacets?: Record<string, Array<string | number>>;
30
+ /** Range filters always applied on this surface, never erased by the user. */
31
+ lockedRanges?: Record<string, {
32
+ gte?: number;
33
+ lte?: number;
34
+ }>;
35
+ /** Toolbar-end controls (market/locale selects). Hidden when embedded. */
36
+ scopeControls?: ReactNode;
37
+ /** Header. `false` suppresses catalog-ui's default. Ignored when embedded. */
38
+ title?: ReactNode | false;
39
+ }
40
+ export declare function CatalogBrowsePage({ vertical, search, onSearchChange, locale, embedded, lockedFacets, lockedRanges, scopeControls, title, ...forward }: CatalogBrowsePageProps): import("react/jsx-runtime").JSX.Element;
41
+ //# sourceMappingURL=catalog-browse-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-browse-page.d.ts","sourceRoot":"","sources":["../../src/components/catalog-browse-page.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAW,MAAM,OAAO,CAAA;AAC/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAEtD,OAAO,EAAE,KAAK,gBAAgB,EAAgC,MAAM,mBAAmB,CAAA;AAEvF;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,sBACf,SAAQ,IAAI,CACV,gBAAgB,EACd,gBAAgB,GAChB,oBAAoB,GACpB,oBAAoB,GACpB,cAAc,GACd,WAAW,GACX,iBAAiB,GACjB,cAAc,GACd,qBAAqB,GACrB,qBAAqB,CACxB;IACD,QAAQ,EAAE,MAAM,CAAA;IAChB,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,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAA;IACrD,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC7D,0EAA0E;IAC1E,aAAa,CAAC,EAAE,SAAS,CAAA;IACzB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,SAAS,GAAG,KAAK,CAAA;CAC1B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,MAAM,EACN,cAAc,EACd,MAAM,EACN,QAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,KAAK,EACL,GAAG,OAAO,EACX,EAAE,sBAAsB,2CAyExB"}
@@ -0,0 +1,47 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useMemo } from "react";
4
+ import { CatalogPage as CatalogUiPage } from "./catalog-page.js";
5
+ export function CatalogBrowsePage({ vertical, search, onSearchChange, locale, embedded = false, lockedFacets, lockedRanges, scopeControls, title, ...forward }) {
6
+ // Merge the always-on locked facets/ranges with the user's URL-driven filters.
7
+ // Memoized so locked surfaces hand a STABLE `filters` object to the tab panel:
8
+ // a fresh object every render reads as "selections changed" and resets back to
9
+ // page 1, breaking pagination. Key on the locked values' content (callers pass
10
+ // inline literals), not their identity. `search` is already stable (router).
11
+ const lockedFacetsKey = JSON.stringify(lockedFacets ?? null);
12
+ const lockedRangesKey = JSON.stringify(lockedRanges ?? null);
13
+ // biome-ignore lint/correctness/useExhaustiveDependencies: keyed on serialized locked filters, not their object identity -- owner: catalog-react; existing suppression is intentional pending typed cleanup.
14
+ const effectiveSearch = useMemo(() => lockedFacets || lockedRanges
15
+ ? {
16
+ ...search,
17
+ locale,
18
+ filters: {
19
+ ...search.filters,
20
+ facets: { ...(search.filters?.facets ?? {}), ...(lockedFacets ?? {}) },
21
+ ranges: { ...(search.filters?.ranges ?? {}), ...(lockedRanges ?? {}) },
22
+ },
23
+ }
24
+ : { ...search, locale }, [search, locale, lockedFacetsKey, lockedRangesKey]);
25
+ return (_jsx(CatalogUiPage, { ...forward, vertical: vertical, search: effectiveSearch, hideSearchInput: embedded, className: embedded ? "px-0 py-0 lg:px-0" : undefined,
26
+ // `false` (not `undefined`) so catalog-ui's `title ?? default` does NOT
27
+ // fall back to its generic header when an embedding surface renders its own.
28
+ title: embedded ? false : title, toolbarEnd: embedded ? undefined : scopeControls, onQueryChange: (q) => onSearchChange((prev) => ({ ...prev, q: q.length > 0 ? q : undefined, page: 1 }), true), onPageChange: (p) => onSearchChange((prev) => ({ ...prev, page: p }), true), onViewChange: (view) => onSearchChange((prev) => ({ ...prev, view }), true), onSortChange: (sort) => onSearchChange((prev) => ({ ...prev, sort }), true), onFiltersChange: (next) => {
29
+ // Prune empty selections so the URL stays clean; reset to page 1.
30
+ const facets = {};
31
+ for (const [field, values] of Object.entries(next.facets ?? {})) {
32
+ if (values.length > 0)
33
+ facets[field] = values;
34
+ }
35
+ const ranges = {};
36
+ for (const [field, range] of Object.entries(next.ranges ?? {})) {
37
+ if (range && (range.gte != null || range.lte != null))
38
+ ranges[field] = range;
39
+ }
40
+ const hasAny = Object.keys(facets).length > 0 || Object.keys(ranges).length > 0;
41
+ onSearchChange((prev) => ({
42
+ ...prev,
43
+ filters: hasAny ? { facets, ranges } : undefined,
44
+ page: 1,
45
+ }), true);
46
+ } }));
47
+ }
@@ -0,0 +1,68 @@
1
+ import type { CatalogSearchHit } from "../index.js";
2
+ import { type PriceUnit } from "./catalog-hit.js";
3
+ export interface CatalogCardBadge {
4
+ label: string;
5
+ variant?: "default" | "secondary" | "outline";
6
+ }
7
+ /**
8
+ * Declarative card mapping for one vertical. The grid view renders a
9
+ * `CatalogCard` per hit using these accessors so per-vertical packages own
10
+ * their merchandising language without the shell knowing field names.
11
+ *
12
+ * Accessors receive the indexer document's `fields` map and return ready-to-
13
+ * render strings; the shell formats price from the `*AmountField`/`*CurrencyField`
14
+ * pair (integer cents + ISO currency).
15
+ */
16
+ export interface CatalogCardConfig {
17
+ /** Field carrying the hero/thumbnail url. Falls back to the tab `imageField` then `thumbnailUrl`. */
18
+ imageField?: string;
19
+ /** Title field (default `name`). */
20
+ titleField?: string;
21
+ /**
22
+ * Integer-cents amount field(s) for the "from" price, tried in order — so a
23
+ * card can prefer the computed lowest price (`priceFromAmountCents`) and fall
24
+ * back to the headline (`sellAmountCents`).
25
+ */
26
+ priceAmountField?: string | string[];
27
+ /** ISO currency field(s) paired with the amount, tried in order. */
28
+ priceCurrencyField?: string | string[];
29
+ /**
30
+ * Whether the amount field holds integer minor units (cents, the default) or
31
+ * major currency units.
32
+ */
33
+ priceUnit?: PriceUnit;
34
+ /**
35
+ * Optional field carrying `minor` or `major`. When present it overrides
36
+ * `priceUnit`, which keeps legacy index documents display-compatible while
37
+ * letting newly indexed rows declare their units.
38
+ */
39
+ priceUnitField?: string;
40
+ /** Secondary line under the title — typically location (e.g. "Spain · Palma"). */
41
+ subtitle?: (fields: Record<string, unknown>) => string | null;
42
+ /** Compact overlay chip on the image — typically duration (e.g. "8d / 7n"). */
43
+ meta?: (fields: Record<string, unknown>) => string | null;
44
+ /** Muted footer line — typically next departure + count (e.g. "Next: 15 Aug · 12 departures"). */
45
+ footerNote?: (fields: Record<string, unknown>) => string | null;
46
+ /** Theme/category chips (rendered as outline badges; capped at 3). */
47
+ chips?: (fields: Record<string, unknown>) => string[];
48
+ /** Image-overlay badges — typically source/status. */
49
+ badges?: (fields: Record<string, unknown>) => CatalogCardBadge[];
50
+ }
51
+ export interface CatalogCardProps {
52
+ hit: CatalogSearchHit;
53
+ config: CatalogCardConfig;
54
+ /** Tab-level image field used when the card config omits its own. */
55
+ imageFallbackField?: string;
56
+ /** Title shown when the hit has no name. */
57
+ fallbackTitle: string;
58
+ /** Opens the detail sheet for this hit. */
59
+ onOpen: (hit: CatalogSearchHit) => void;
60
+ }
61
+ /**
62
+ * Booking.com-style merchandising card for one search hit. Pure projection
63
+ * of the indexed document via the tab's `CatalogCardConfig` — no extra fetch.
64
+ * The whole card is a button that opens the detail sheet (which carries the
65
+ * departures + Book actions).
66
+ */
67
+ export declare function CatalogCard({ hit, config, imageFallbackField, fallbackTitle, onOpen, }: CatalogCardProps): import("react/jsx-runtime").JSX.Element;
68
+ //# sourceMappingURL=catalog-card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-card.d.ts","sourceRoot":"","sources":["../../src/components/catalog-card.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAGL,KAAK,SAAS,EAGf,MAAM,kBAAkB,CAAA;AAEzB,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,CAAA;CAC9C;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,qGAAqG;IACrG,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACpC,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,GAAG,IAAI,CAAA;IAC7D,+EAA+E;IAC/E,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,GAAG,IAAI,CAAA;IACzD,kGAAkG;IAClG,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,GAAG,IAAI,CAAA;IAC/D,sEAAsE;IACtE,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,EAAE,CAAA;IACrD,sDAAsD;IACtD,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,gBAAgB,EAAE,CAAA;CACjE;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,gBAAgB,CAAA;IACrB,MAAM,EAAE,iBAAiB,CAAA;IACzB,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,4CAA4C;IAC5C,aAAa,EAAE,MAAM,CAAA;IACrB,2CAA2C;IAC3C,MAAM,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAA;CACxC;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAC1B,GAAG,EACH,MAAM,EACN,kBAAkB,EAClB,aAAa,EACb,MAAM,GACP,EAAE,gBAAgB,2CAqGlB"}
@@ -0,0 +1,52 @@
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 { Card } from "@voyant-travel/ui/components/card";
6
+ import { Image as ImageIcon } from "lucide-react";
7
+ import { useCatalogUiMessagesOrDefault } from "../i18n/index.js";
8
+ import { asString, formatHitPrice, resolveHitPriceUnit, stringField, } from "./catalog-hit.js";
9
+ /**
10
+ * Booking.com-style merchandising card for one search hit. Pure projection
11
+ * of the indexed document via the tab's `CatalogCardConfig` — no extra fetch.
12
+ * The whole card is a button that opens the detail sheet (which carries the
13
+ * departures + Book actions).
14
+ */
15
+ export function CatalogCard({ hit, config, imageFallbackField, fallbackTitle, onOpen, }) {
16
+ const card = useCatalogUiMessagesOrDefault().catalogPage.card;
17
+ const fields = hit.document.fields;
18
+ const imageField = config.imageField ?? imageFallbackField ?? "thumbnailUrl";
19
+ const imageUrl = asString(fields[imageField]);
20
+ const title = stringField(hit, config.titleField ?? "name", fallbackTitle);
21
+ const subtitle = config.subtitle?.(fields) ?? null;
22
+ const meta = config.meta?.(fields) ?? null;
23
+ const footerNote = config.footerNote?.(fields) ?? null;
24
+ const chips = config.chips?.(fields) ?? [];
25
+ const badges = config.badges?.(fields) ?? [];
26
+ const price = resolveCardPrice(hit, config.priceAmountField, config.priceCurrencyField, config.priceUnit, config.priceUnitField);
27
+ const open = () => onOpen(hit);
28
+ return (_jsxs(Card, { role: "button", tabIndex: 0, onClick: open, onKeyDown: (e) => {
29
+ if (e.key === "Enter" || e.key === " ") {
30
+ e.preventDefault();
31
+ open();
32
+ }
33
+ }, className: "group flex cursor-pointer flex-col gap-0 overflow-hidden p-0 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: [imageUrl ? (_jsx("img", { src: imageUrl, alt: title, 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" }) })), badges.length > 0 && (_jsx("div", { className: "absolute top-2 left-2 flex flex-wrap gap-1", children: badges.map((b) => (_jsx(Badge, { variant: b.variant ?? "secondary", className: "shadow-sm", children: b.label }, b.label))) })), meta && (_jsx("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: meta }))] }), _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: title }), subtitle && _jsx("div", { className: "truncate text-muted-foreground text-sm", children: subtitle })] }), chips.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1", children: chips.slice(0, 3).map((c) => (_jsx(Badge, { variant: "outline", className: "font-normal capitalize", children: c }, c))) })), footerNote && _jsx("div", { className: "text-muted-foreground text-xs", children: footerNote }), _jsxs("div", { className: "mt-auto flex items-end justify-between gap-2 pt-1", children: [_jsx("div", { className: "min-w-0", children: price ? (_jsxs("div", { className: "flex items-baseline gap-1", children: [_jsx("span", { className: "text-muted-foreground text-xs", children: card.from }), _jsx("span", { className: "font-semibold text-base", children: price })] })) : null }), _jsx(Button, { size: "sm", variant: "secondary", className: "shrink-0", onClick: (e) => {
34
+ e.stopPropagation();
35
+ open();
36
+ }, children: card.viewDetails })] })] })] }));
37
+ }
38
+ function resolveCardPrice(hit, amountField, currencyField, unit = "minor", unitField) {
39
+ if (!amountField || !currencyField)
40
+ return null;
41
+ const resolvedUnit = resolveHitPriceUnit(hit, unit, unitField);
42
+ const amounts = Array.isArray(amountField) ? amountField : [amountField];
43
+ const currencies = Array.isArray(currencyField) ? currencyField : [currencyField];
44
+ for (const amount of amounts) {
45
+ for (const currency of currencies) {
46
+ const formatted = formatHitPrice(hit, amount, currency, resolvedUnit);
47
+ if (formatted)
48
+ return formatted;
49
+ }
50
+ }
51
+ return null;
52
+ }
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from "react";
2
+ import type { CatalogUiMessages } from "../i18n/messages.js";
3
+ import type { CatalogDetailEnrichment } from "../index.js";
4
+ export declare function CabinCard({ cabin, messages, }: {
5
+ cabin: NonNullable<CatalogDetailEnrichment["options"]>[number];
6
+ messages: CatalogUiMessages["catalogPage"]["detail"];
7
+ }): ReactNode;
8
+ /**
9
+ * The vessel a cruise sails on: gallery + name/type, key specs (capacity,
10
+ * decks, year built) and a description.
11
+ */
12
+ export declare function ShipCard({ ship, messages, }: {
13
+ ship: NonNullable<CatalogDetailEnrichment["ship"]>;
14
+ messages: CatalogUiMessages["catalogPage"]["detail"];
15
+ }): ReactNode;
16
+ //# sourceMappingURL=catalog-detail-cruise-cards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-detail-cruise-cards.d.ts","sourceRoot":"","sources":["../../src/components/catalog-detail-cruise-cards.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAC5D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAI1D,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,WAAW,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;IAC9D,QAAQ,EAAE,iBAAiB,CAAC,aAAa,CAAC,CAAC,QAAQ,CAAC,CAAA;CACrD,GAAG,SAAS,CA+DZ;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,EACvB,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,IAAI,EAAE,WAAW,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC,CAAA;IAClD,QAAQ,EAAE,iBAAiB,CAAC,aAAa,CAAC,CAAC,QAAQ,CAAC,CAAA;CACrD,GAAG,SAAS,CA8FZ"}
@@ -0,0 +1,54 @@
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 { ExternalLink } from "lucide-react";
5
+ import { formatTemplate } from "./catalog-detail-parts.js";
6
+ import { MediaGallery } from "./media-gallery.js";
7
+ export function CabinCard({ cabin, messages, }) {
8
+ const desc = cabin.description?.trim() ?? "";
9
+ const meta = [
10
+ cabin.squareFeet ? `${cabin.squareFeet} sqft` : null,
11
+ cabin.capacityMax ? `sleeps ${cabin.capacityMax}` : null,
12
+ cabin.gradeCodes && cabin.gradeCodes.length > 0
13
+ ? `grades ${cabin.gradeCodes.join(", ")}`
14
+ : null,
15
+ ]
16
+ .filter(Boolean)
17
+ .join(" · ");
18
+ // The upstream amenity list leads with the cabin size + a copy of the
19
+ // description; drop those so the chips show genuine, non-duplicated perks.
20
+ const amenities = (cabin.amenities ?? []).filter((a) => a.trim() !== desc && !/^stateroom size/i.test(a.trim()));
21
+ return (_jsxs("li", { className: "flex gap-4 rounded-lg border border-border p-3", children: [_jsx(MediaGallery, { images: cabin.images ?? [], alt: cabin.name }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2", children: [_jsx("h4", { className: "font-medium text-sm", children: cabin.name }), meta && _jsx("span", { className: "text-xs text-muted-foreground", children: meta }), cabin.wheelchairAccessible && (_jsx(Badge, { variant: "outline", className: "text-[10px]", children: messages.wheelchairAccessible }))] }), desc && _jsx("p", { className: "mt-1 text-xs leading-relaxed text-muted-foreground", children: desc }), (cabin.floorplanImages?.length ?? 0) > 0 && (_jsxs("div", { className: "mt-3", children: [_jsx("div", { className: "mb-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: messages.floorPlan }), _jsx(MediaGallery, { images: cabin.floorplanImages ?? [], alt: `${cabin.name} floor plan`, className: "w-44", imageClassName: "h-28 w-44 object-contain bg-muted" })] })), amenities.length > 0 && (_jsxs("div", { className: "mt-2 flex flex-wrap gap-1", children: [amenities.slice(0, 6).map((a) => (_jsx("span", { className: "rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground", children: a }, a))), amenities.length > 6 && (_jsxs("span", { className: "px-1 text-[10px] text-muted-foreground", children: ["+", amenities.length - 6] }))] }))] })] }));
22
+ }
23
+ /**
24
+ * The vessel a cruise sails on: gallery + name/type, key specs (capacity,
25
+ * decks, year built) and a description.
26
+ */
27
+ export function ShipCard({ ship, messages, }) {
28
+ const desc = ship.description?.trim() ?? "";
29
+ const specs = [
30
+ ship.shipType ? { label: messages.shipSpecs.type, value: ship.shipType } : null,
31
+ ship.capacity
32
+ ? {
33
+ label: messages.shipSpecs.capacity,
34
+ value: formatTemplate(messages.shipSpecs.capacityGuests, { count: ship.capacity }),
35
+ }
36
+ : null,
37
+ ship.decks ? { label: messages.shipSpecs.decks, value: String(ship.decks) } : null,
38
+ ship.yearBuilt ? { label: messages.shipSpecs.yearBuilt, value: String(ship.yearBuilt) } : null,
39
+ ].filter((s) => s != null);
40
+ const images = ship.images ?? [];
41
+ const deckPlanUrls = [
42
+ ...(ship.deckPlanUrl ? [ship.deckPlanUrl] : []),
43
+ ...(ship.deckPlans ?? []).flatMap((deck) => (deck.imageUrl ? [deck.imageUrl] : [])),
44
+ ];
45
+ const deckPlanImages = Array.from(new Set(deckPlanUrls.filter(isRenderableImageUrl)));
46
+ const deckPlanDocuments = Array.from(new Set(deckPlanUrls.filter((url) => !isRenderableImageUrl(url))));
47
+ return (_jsxs("div", { className: "flex flex-col gap-4", children: [images.length > 0 && (_jsx(MediaGallery, { images: images, alt: ship.name, className: "w-full max-w-lg", imageClassName: "h-56 w-full" })), deckPlanImages.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "mb-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: messages.deckPlan }), _jsx(MediaGallery, { images: deckPlanImages, alt: `${ship.name} deck plan`, className: "w-full max-w-lg", imageClassName: "h-56 w-full object-contain bg-muted" })] })), deckPlanDocuments.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: deckPlanDocuments.map((url) => (_jsxs("a", { href: url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs text-foreground transition-colors hover:bg-muted", children: [_jsx(ExternalLink, { className: "h-3 w-3" }), messages.openDeckPlan] }, url))) })), _jsxs("div", { children: [_jsx("h3", { className: "text-base font-medium text-foreground", children: ship.name }), ship.shipType && _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: ship.shipType })] }), (ship.deckPlans?.length ?? 0) > 0 && (_jsx("div", { className: "flex flex-wrap gap-1", children: ship.deckPlans.map((deck) => (_jsx("span", { className: "rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground", children: deck.level != null ? `Deck ${deck.level}: ${deck.name}` : deck.name }, `${deck.level ?? ""}-${deck.name}`))) })), specs.length > 0 && (_jsx("dl", { className: "grid grid-cols-2 gap-x-6 gap-y-2 sm:grid-cols-3", children: specs.map((s) => (_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("dt", { className: "text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: s.label }), _jsx("dd", { className: "text-sm text-foreground", children: s.value })] }, s.label))) })), desc && (_jsx("p", { className: "whitespace-pre-line text-sm leading-relaxed text-muted-foreground", children: desc }))] }));
48
+ }
49
+ function isRenderableImageUrl(url) {
50
+ if (url.startsWith("data:image/"))
51
+ return true;
52
+ const path = url.split(/[?#]/, 1)[0]?.toLowerCase() ?? "";
53
+ return /\.(avif|gif|jpe?g|png|svg|webp)$/.test(path);
54
+ }
@@ -0,0 +1,25 @@
1
+ import type { CatalogUiMessages } from "../i18n/messages.js";
2
+ import type { CatalogDeparturePricingRow, CatalogDetailEnrichment, CatalogSearchHit } from "../index.js";
3
+ type DepartureEntry = NonNullable<CatalogDetailEnrichment["departures"]>[number];
4
+ type DepartureOption = NonNullable<CatalogDetailEnrichment["options"]>[number];
5
+ /**
6
+ * Flat departures table with sortable columns and filter controls
7
+ * (month/year, status, min-availability). Sold-out / closed / past
8
+ * rows render dimmed and are not clickable. Bookable rows expand to
9
+ * reveal a per-option row with its own remaining capacity and Book
10
+ * button.
11
+ */
12
+ export declare function DeparturesTable({ hit, vertical, departures, options, productSellAmountCents, productSellCurrency, onBookDeparture, onBookOption, onLoadDeparturePricing, messages, }: {
13
+ hit: CatalogSearchHit | null;
14
+ vertical?: string;
15
+ departures: ReadonlyArray<DepartureEntry>;
16
+ options: NonNullable<CatalogDetailEnrichment["options"]>;
17
+ productSellAmountCents: number | null;
18
+ productSellCurrency: string | null;
19
+ onBookDeparture?: (hit: CatalogSearchHit, departure: DepartureEntry) => void;
20
+ onBookOption?: (hit: CatalogSearchHit, departure: DepartureEntry, option: DepartureOption) => void;
21
+ onLoadDeparturePricing?: (hit: CatalogSearchHit, sailingRef: string) => Promise<CatalogDeparturePricingRow[] | null>;
22
+ messages: CatalogUiMessages["catalogPage"]["detail"];
23
+ }): import("react/jsx-runtime").JSX.Element;
24
+ export {};
25
+ //# sourceMappingURL=catalog-detail-departures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-detail-departures.d.ts","sourceRoot":"","sources":["../../src/components/catalog-detail-departures.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAC5D,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,gBAAgB,EACjB,MAAM,aAAa,CAAA;AAGpB,KAAK,cAAc,GAAG,WAAW,CAAC,uBAAuB,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AAChF,KAAK,eAAe,GAAG,WAAW,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AA8C9E;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,EAC9B,GAAG,EACH,QAAQ,EACR,UAAU,EACV,OAAO,EACP,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,EACf,YAAY,EACZ,sBAAsB,EACtB,QAAQ,GACT,EAAE;IACD,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,aAAa,CAAC,cAAc,CAAC,CAAA;IACzC,OAAO,EAAE,WAAW,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC,CAAA;IACxD,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,EAAE,SAAS,EAAE,cAAc,KAAK,IAAI,CAAA;IAC5E,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,EAAE,eAAe,KAAK,IAAI,CAAA;IAClG,sBAAsB,CAAC,EAAE,CACvB,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,0BAA0B,EAAE,GAAG,IAAI,CAAC,CAAA;IACjD,QAAQ,EAAE,iBAAiB,CAAC,aAAa,CAAC,CAAC,QAAQ,CAAC,CAAA;CACrD,2CAwQA"}