@voyantjs/products-ui 0.101.0 → 0.101.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 (137) hide show
  1. package/dist/components/option-unit-form.d.ts.map +1 -1
  2. package/dist/components/option-unit-form.js +6 -4
  3. package/dist/components/product-detail/date-picker.d.ts +44 -0
  4. package/dist/components/product-detail/date-picker.d.ts.map +1 -0
  5. package/dist/components/product-detail/date-picker.js +125 -0
  6. package/dist/components/product-detail/host.d.ts +53 -0
  7. package/dist/components/product-detail/host.d.ts.map +1 -0
  8. package/dist/components/product-detail/host.js +24 -0
  9. package/dist/components/product-detail/index.d.ts +6 -0
  10. package/dist/components/product-detail/index.d.ts.map +1 -0
  11. package/dist/components/product-detail/index.js +5 -0
  12. package/dist/components/product-detail/product-activity-section.d.ts +4 -0
  13. package/dist/components/product-detail/product-activity-section.d.ts.map +1 -0
  14. package/dist/components/product-detail/product-activity-section.js +37 -0
  15. package/dist/components/product-detail/product-day-sheet.d.ts +14 -0
  16. package/dist/components/product-detail/product-day-sheet.d.ts.map +1 -0
  17. package/dist/components/product-detail/product-day-sheet.js +75 -0
  18. package/dist/components/product-detail/product-day-translation.d.ts +41 -0
  19. package/dist/components/product-detail/product-day-translation.d.ts.map +1 -0
  20. package/dist/components/product-detail/product-day-translation.js +111 -0
  21. package/dist/components/product-detail/product-departure-dialog.d.ts +11 -0
  22. package/dist/components/product-detail/product-departure-dialog.d.ts.map +1 -0
  23. package/dist/components/product-detail/product-departure-dialog.js +10 -0
  24. package/dist/components/product-detail/product-departure-form.d.ts +25 -0
  25. package/dist/components/product-detail/product-departure-form.d.ts.map +1 -0
  26. package/dist/components/product-detail/product-departure-form.js +217 -0
  27. package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts +8 -0
  28. package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts.map +1 -0
  29. package/dist/components/product-detail/product-departure-pricing-override-dialog.js +125 -0
  30. package/dist/components/product-detail/product-detail-day-row.d.ts +14 -0
  31. package/dist/components/product-detail/product-detail-day-row.d.ts.map +1 -0
  32. package/dist/components/product-detail/product-detail-day-row.js +43 -0
  33. package/dist/components/product-detail/product-detail-dialog.d.ts +10 -0
  34. package/dist/components/product-detail/product-detail-dialog.d.ts.map +1 -0
  35. package/dist/components/product-detail/product-detail-dialog.js +10 -0
  36. package/dist/components/product-detail/product-detail-form.d.ts +19 -0
  37. package/dist/components/product-detail/product-detail-form.d.ts.map +1 -0
  38. package/dist/components/product-detail/product-detail-form.js +177 -0
  39. package/dist/components/product-detail/product-detail-header.d.ts +12 -0
  40. package/dist/components/product-detail/product-detail-header.d.ts.map +1 -0
  41. package/dist/components/product-detail/product-detail-header.js +19 -0
  42. package/dist/components/product-detail/product-detail-itinerary-section.d.ts +4 -0
  43. package/dist/components/product-detail/product-detail-itinerary-section.d.ts.map +1 -0
  44. package/dist/components/product-detail/product-detail-itinerary-section.js +201 -0
  45. package/dist/components/product-detail/product-detail-page.d.ts +4 -0
  46. package/dist/components/product-detail/product-detail-page.d.ts.map +1 -0
  47. package/dist/components/product-detail/product-detail-page.js +97 -0
  48. package/dist/components/product-detail/product-detail-sections.d.ts +63 -0
  49. package/dist/components/product-detail/product-detail-sections.d.ts.map +1 -0
  50. package/dist/components/product-detail/product-detail-sections.js +143 -0
  51. package/dist/components/product-detail/product-detail-shared.d.ts +264 -0
  52. package/dist/components/product-detail/product-detail-shared.d.ts.map +1 -0
  53. package/dist/components/product-detail/product-detail-shared.js +157 -0
  54. package/dist/components/product-detail/product-detail-skeleton.d.ts +9 -0
  55. package/dist/components/product-detail/product-detail-skeleton.d.ts.map +1 -0
  56. package/dist/components/product-detail/product-detail-skeleton.js +53 -0
  57. package/dist/components/product-detail/product-extras-section.d.ts +4 -0
  58. package/dist/components/product-detail/product-extras-section.d.ts.map +1 -0
  59. package/dist/components/product-detail/product-extras-section.js +141 -0
  60. package/dist/components/product-detail/product-itinerary-form.d.ts +16 -0
  61. package/dist/components/product-detail/product-itinerary-form.d.ts.map +1 -0
  62. package/dist/components/product-detail/product-itinerary-form.js +38 -0
  63. package/dist/components/product-detail/product-market-rules-section.d.ts +6 -0
  64. package/dist/components/product-detail/product-market-rules-section.d.ts.map +1 -0
  65. package/dist/components/product-detail/product-market-rules-section.js +81 -0
  66. package/dist/components/product-detail/product-media-gallery.d.ts +19 -0
  67. package/dist/components/product-detail/product-media-gallery.d.ts.map +1 -0
  68. package/dist/components/product-detail/product-media-gallery.js +114 -0
  69. package/dist/components/product-detail/product-option-price-rule-dialog.d.ts +12 -0
  70. package/dist/components/product-detail/product-option-price-rule-dialog.d.ts.map +1 -0
  71. package/dist/components/product-detail/product-option-price-rule-dialog.js +10 -0
  72. package/dist/components/product-detail/product-option-price-rule-form.d.ts +29 -0
  73. package/dist/components/product-detail/product-option-price-rule-form.d.ts.map +1 -0
  74. package/dist/components/product-detail/product-option-price-rule-form.js +125 -0
  75. package/dist/components/product-detail/product-options-pricing.d.ts +6 -0
  76. package/dist/components/product-detail/product-options-pricing.d.ts.map +1 -0
  77. package/dist/components/product-detail/product-options-pricing.js +363 -0
  78. package/dist/components/product-detail/product-options-shared.d.ts +609 -0
  79. package/dist/components/product-detail/product-options-shared.d.ts.map +1 -0
  80. package/dist/components/product-detail/product-options-shared.js +34 -0
  81. package/dist/components/product-detail/product-payment-policy-section.d.ts +17 -0
  82. package/dist/components/product-detail/product-payment-policy-section.d.ts.map +1 -0
  83. package/dist/components/product-detail/product-payment-policy-section.js +58 -0
  84. package/dist/components/product-detail/product-schedule-dialog.d.ts +11 -0
  85. package/dist/components/product-detail/product-schedule-dialog.d.ts.map +1 -0
  86. package/dist/components/product-detail/product-schedule-dialog.js +10 -0
  87. package/dist/components/product-detail/product-schedule-form.d.ts +17 -0
  88. package/dist/components/product-detail/product-schedule-form.d.ts.map +1 -0
  89. package/dist/components/product-detail/product-schedule-form.js +222 -0
  90. package/dist/components/product-detail/product-service-dialog.d.ts +12 -0
  91. package/dist/components/product-detail/product-service-dialog.d.ts.map +1 -0
  92. package/dist/components/product-detail/product-service-dialog.js +10 -0
  93. package/dist/components/product-detail/product-service-form.d.ts +22 -0
  94. package/dist/components/product-detail/product-service-form.d.ts.map +1 -0
  95. package/dist/components/product-detail/product-service-form.js +154 -0
  96. package/dist/components/product-detail/product-translation-popover.d.ts +91 -0
  97. package/dist/components/product-detail/product-translation-popover.d.ts.map +1 -0
  98. package/dist/components/product-detail/product-translation-popover.js +217 -0
  99. package/dist/components/product-detail/product-unit-dialog.d.ts +12 -0
  100. package/dist/components/product-detail/product-unit-dialog.d.ts.map +1 -0
  101. package/dist/components/product-detail/product-unit-dialog.js +10 -0
  102. package/dist/components/product-detail/product-unit-form.d.ts +26 -0
  103. package/dist/components/product-detail/product-unit-form.d.ts.map +1 -0
  104. package/dist/components/product-detail/product-unit-form.js +109 -0
  105. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts +16 -0
  106. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts.map +1 -0
  107. package/dist/components/product-detail/product-unit-price-rule-dialog.js +10 -0
  108. package/dist/components/product-detail/product-unit-price-rule-form.d.ts +28 -0
  109. package/dist/components/product-detail/product-unit-price-rule-form.d.ts.map +1 -0
  110. package/dist/components/product-detail/product-unit-price-rule-form.js +126 -0
  111. package/dist/components/product-detail/timezone-options.d.ts +9 -0
  112. package/dist/components/product-detail/timezone-options.d.ts.map +1 -0
  113. package/dist/components/product-detail/timezone-options.js +28 -0
  114. package/dist/components/product-detail/use-product-detail-data.d.ts +41 -0
  115. package/dist/components/product-detail/use-product-detail-data.d.ts.map +1 -0
  116. package/dist/components/product-detail/use-product-detail-data.js +143 -0
  117. package/dist/components/product-detail/use-product-detail-dialogs.d.ts +24 -0
  118. package/dist/components/product-detail/use-product-detail-dialogs.d.ts.map +1 -0
  119. package/dist/components/product-detail/use-product-detail-dialogs.js +40 -0
  120. package/dist/components/product-detail/zod-resolver.d.ts +4 -0
  121. package/dist/components/product-detail/zod-resolver.d.ts.map +1 -0
  122. package/dist/components/product-detail/zod-resolver.js +39 -0
  123. package/dist/components/product-option-form.js +1 -1
  124. package/dist/components/product-options-section.d.ts +3 -1
  125. package/dist/components/product-options-section.d.ts.map +1 -1
  126. package/dist/components/product-options-section.js +102 -5
  127. package/dist/i18n/en.d.ts +21 -0
  128. package/dist/i18n/en.d.ts.map +1 -1
  129. package/dist/i18n/en.js +54 -33
  130. package/dist/i18n/messages.d.ts +21 -0
  131. package/dist/i18n/messages.d.ts.map +1 -1
  132. package/dist/i18n/provider.d.ts +42 -0
  133. package/dist/i18n/provider.d.ts.map +1 -1
  134. package/dist/i18n/ro.d.ts +21 -0
  135. package/dist/i18n/ro.d.ts.map +1 -1
  136. package/dist/i18n/ro.js +53 -32
  137. package/package.json +38 -19
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Card, CardContent, CardHeader } from "@voyantjs/ui/components/card";
3
+ import { Skeleton } from "@voyantjs/ui/components/skeleton";
4
+ /**
5
+ * Layout-matched placeholder for ProductDetailPage. Mirrors the real shell:
6
+ * - Header: title + status + action buttons
7
+ * - 2-column body (1fr + 320px)
8
+ * - Left stack: Details, Departures, Schedules, Itinerary (3 days), Options
9
+ * - Right stack: Channels, Organize, Media (3x3 grid)
10
+ */
11
+ export function ProductDetailSkeleton() {
12
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsx(ProductDetailHeaderSkeleton, {}), _jsxs("div", { className: "grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_320px]", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-6", children: [_jsx(ProductDetailsSectionSkeleton, {}), _jsx(ProductDeparturesSectionSkeleton, {}), _jsx(ProductSchedulesSectionSkeleton, {}), _jsx(ProductItinerarySectionSkeleton, {}), _jsx(ProductOptionsSectionSkeleton, {})] }), _jsxs("div", { className: "flex min-w-0 flex-col gap-6", children: [_jsx(ProductChannelsSectionSkeleton, {}), _jsx(ProductOrganizeSectionSkeleton, {}), _jsx(ProductMediaSectionSkeleton, {})] })] })] }));
13
+ }
14
+ // ---------- Header ----------
15
+ function ProductDetailHeaderSkeleton() {
16
+ return (_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Skeleton, { className: "h-8 w-64" }), _jsx(Skeleton, { className: "h-5 w-16 rounded-full" })] }), _jsx(Skeleton, { className: "h-4 w-96" })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Skeleton, { className: "h-9 w-9 rounded-md" }), _jsx(Skeleton, { className: "h-9 w-28" }), _jsx(Skeleton, { className: "h-9 w-36" })] })] }));
17
+ }
18
+ // ---------- Left column sections ----------
19
+ /** Product details: 2-column grid of label/value pairs. */
20
+ function ProductDetailsSectionSkeleton() {
21
+ return (_jsx(SectionShell, { titleWidth: "w-36", action: true, children: _jsx("div", { className: "grid grid-cols-2 gap-x-6 gap-y-4", children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-3 w-24" }), _jsx(Skeleton, { className: "h-4 w-32" })] }, i))) }) }));
22
+ }
23
+ /** Departures: row-per-slot list. */
24
+ function ProductDeparturesSectionSkeleton() {
25
+ return (_jsx(SectionShell, { titleWidth: "w-28", action: true, children: _jsx("div", { className: "flex flex-col gap-2", children: Array.from({ length: 3 }).map((_, i) => (_jsxs("div", { className: "flex items-center gap-4 rounded-md border px-4 py-3", children: [_jsx(Skeleton, { className: "h-4 w-28" }), _jsx(Skeleton, { className: "h-5 w-16 rounded-full" }), _jsx("div", { className: "flex-1" }), _jsx(Skeleton, { className: "h-4 w-20" }), _jsx(Skeleton, { className: "h-8 w-8 rounded" })] }, i))) }) }));
26
+ }
27
+ /** Recurring schedules: sparse list, often empty. */
28
+ function ProductSchedulesSectionSkeleton() {
29
+ return (_jsx(SectionShell, { titleWidth: "w-44", action: true, children: _jsx("div", { className: "rounded-md border px-4 py-6 text-center", children: _jsx(Skeleton, { className: "mx-auto h-4 w-80" }) }) }));
30
+ }
31
+ /** Itinerary: 3 day rows with cover thumbnail + title. */
32
+ function ProductItinerarySectionSkeleton() {
33
+ return (_jsx(SectionShell, { titleWidth: "w-24", action: true, children: _jsx("div", { className: "flex flex-col gap-2", children: Array.from({ length: 3 }).map((_, i) => (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border px-4 py-3", children: [_jsx(Skeleton, { className: "h-4 w-4" }), _jsx(Skeleton, { className: "h-10 w-14 rounded" }), _jsx(Skeleton, { className: "h-4 w-40" }), _jsx("div", { className: "flex-1" }), _jsx(Skeleton, { className: "h-5 w-16 rounded-full" }), _jsx(Skeleton, { className: "h-8 w-8 rounded" })] }, i))) }) }));
34
+ }
35
+ /** Options: one option card with Units + Pricing sub-sections. */
36
+ function ProductOptionsSectionSkeleton() {
37
+ return (_jsx(SectionShell, { titleWidth: "w-20", action: true, children: _jsxs("div", { className: "rounded-lg border", children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [_jsx(Skeleton, { className: "h-4 w-4" }), _jsx(Skeleton, { className: "h-4 w-24" }), _jsx(Skeleton, { className: "h-5 w-10 rounded-full" }), _jsx(Skeleton, { className: "h-5 w-14 rounded-full" }), _jsx("div", { className: "flex-1" }), _jsx(Skeleton, { className: "h-8 w-8 rounded" })] }), _jsxs("div", { className: "space-y-4 border-t px-4 py-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Skeleton, { className: "h-3 w-12" }), _jsx(Skeleton, { className: "h-7 w-24" })] }), Array.from({ length: 3 }).map((_, i) => (_jsxs("div", { className: "flex items-center gap-3 rounded-md border px-3 py-2", children: [_jsx(Skeleton, { className: "h-4 w-20" }), _jsx(Skeleton, { className: "h-5 w-14 rounded-full" }), _jsx("div", { className: "flex-1" }), _jsx(Skeleton, { className: "h-4 w-20" })] }, i)))] }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Skeleton, { className: "h-3 w-16" }), _jsx(Skeleton, { className: "h-7 w-32" })] }), _jsxs("div", { className: "rounded-md border px-3 py-3 space-y-2", children: [_jsx(Skeleton, { className: "h-4 w-48" }), _jsx(Skeleton, { className: "h-3 w-56" })] })] })] })] }) }));
38
+ }
39
+ // ---------- Right column sections ----------
40
+ function ProductChannelsSectionSkeleton() {
41
+ return (_jsx(SectionShell, { titleWidth: "w-28", children: _jsx("div", { className: "space-y-2", children: Array.from({ length: 3 }).map((_, i) => (_jsxs("div", { className: "flex items-center justify-between rounded-md border px-3 py-2", children: [_jsx(Skeleton, { className: "h-4 w-24" }), _jsx(Skeleton, { className: "h-4 w-4" })] }, i))) }) }));
42
+ }
43
+ function ProductOrganizeSectionSkeleton() {
44
+ return (_jsx(SectionShell, { titleWidth: "w-20", children: _jsx("div", { className: "space-y-4", children: Array.from({ length: 3 }).map((_, i) => (_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-3 w-20" }), _jsx(Skeleton, { className: "h-4 w-2/3" })] }, i))) }) }));
45
+ }
46
+ /** Media gallery: 3x2 thumbnail grid. */
47
+ function ProductMediaSectionSkeleton() {
48
+ return (_jsx(SectionShell, { titleWidth: "w-14", action: true, children: _jsx("div", { className: "grid grid-cols-3 gap-2", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Skeleton, { className: "aspect-square w-full rounded-md" }, i))) }) }));
49
+ }
50
+ // ---------- Shared shell ----------
51
+ function SectionShell({ titleWidth, action = false, children, }) {
52
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsx(Skeleton, { className: `h-5 ${titleWidth}` }), action ? _jsx(Skeleton, { className: "h-8 w-8 rounded" }) : null] }), _jsx(CardContent, { children: children })] }));
53
+ }
@@ -0,0 +1,4 @@
1
+ export declare function ProductExtrasSection({ productId }: {
2
+ productId: string;
3
+ }): import("react/jsx-runtime").JSX.Element;
4
+ //# sourceMappingURL=product-extras-section.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-extras-section.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-extras-section.tsx"],"names":[],"mappings":"AAmEA,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,2CAiQxE"}
@@ -0,0 +1,141 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useProductExtraMutation, useProductExtras, } from "@voyantjs/extras-react";
3
+ import { formatMessage } from "@voyantjs/i18n";
4
+ import { Badge, Button, Checkbox, Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/ui/components";
5
+ import { Pencil, Plus, Trash2 } from "lucide-react";
6
+ import * as React from "react";
7
+ import { useProductDetailMessages } from "./host.js";
8
+ import { ActionMenu, EmptyState, Section } from "./product-detail-sections.js";
9
+ const selectionTypes = ["optional", "required", "default_selected", "unavailable"];
10
+ const pricingModes = [
11
+ "included",
12
+ "per_person",
13
+ "per_booking",
14
+ "quantity_based",
15
+ "on_request",
16
+ "free",
17
+ ];
18
+ const emptyForm = {
19
+ name: "",
20
+ code: "",
21
+ description: "",
22
+ selectionType: "optional",
23
+ pricingMode: "per_booking",
24
+ pricedPerPerson: false,
25
+ minQuantity: "",
26
+ maxQuantity: "",
27
+ defaultQuantity: "",
28
+ active: true,
29
+ };
30
+ export function ProductExtrasSection({ productId }) {
31
+ const messages = useProductDetailMessages();
32
+ const extraMessages = messages.products.operations.extras;
33
+ const [open, setOpen] = React.useState(false);
34
+ const [editing, setEditing] = React.useState(null);
35
+ const [form, setForm] = React.useState(emptyForm);
36
+ const { data, isPending, refetch } = useProductExtras({ productId, limit: 100 });
37
+ const { create, update, remove } = useProductExtraMutation();
38
+ const rows = data?.data ?? [];
39
+ const startCreate = () => {
40
+ setEditing(null);
41
+ setForm(emptyForm);
42
+ setOpen(true);
43
+ };
44
+ const startEdit = (extra) => {
45
+ setEditing(extra);
46
+ setForm({
47
+ name: extra.name,
48
+ code: extra.code ?? "",
49
+ description: extra.description ?? "",
50
+ selectionType: extra.selectionType,
51
+ pricingMode: extra.pricingMode,
52
+ pricedPerPerson: extra.pricedPerPerson,
53
+ minQuantity: extra.minQuantity == null ? "" : String(extra.minQuantity),
54
+ maxQuantity: extra.maxQuantity == null ? "" : String(extra.maxQuantity),
55
+ defaultQuantity: extra.defaultQuantity == null ? "" : String(extra.defaultQuantity),
56
+ active: extra.active,
57
+ });
58
+ setOpen(true);
59
+ };
60
+ const save = async () => {
61
+ const payload = {
62
+ productId,
63
+ name: form.name.trim(),
64
+ code: form.code.trim() || null,
65
+ description: form.description.trim() || null,
66
+ selectionType: form.selectionType,
67
+ pricingMode: form.pricingMode,
68
+ pricedPerPerson: form.pricedPerPerson,
69
+ minQuantity: parseNullableInt(form.minQuantity),
70
+ maxQuantity: parseNullableInt(form.maxQuantity),
71
+ defaultQuantity: parseNullableInt(form.defaultQuantity),
72
+ active: form.active,
73
+ sortOrder: editing?.sortOrder ?? rows.length,
74
+ };
75
+ if (!payload.name)
76
+ return;
77
+ if (editing)
78
+ await update.mutateAsync({ id: editing.id, input: payload });
79
+ else
80
+ await create.mutateAsync(payload);
81
+ setOpen(false);
82
+ setEditing(null);
83
+ setForm(emptyForm);
84
+ void refetch();
85
+ };
86
+ return (_jsxs(Section, { title: extraMessages.sectionTitle, actions: _jsxs(Button, { variant: "outline", size: "sm", onClick: startCreate, children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), extraMessages.addAction] }), contentClassName: "px-6 py-4", children: [rows.length === 0 ? (_jsx(EmptyState, { message: isPending ? extraMessages.loading : extraMessages.empty })) : (_jsx("div", { className: "flex flex-col gap-2", children: rows.map((extra) => (_jsxs("div", { className: "flex items-start justify-between gap-3 rounded-md border px-3 py-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-medium text-sm", children: extra.name }), _jsx(Badge, { variant: extra.active ? "default" : "outline", children: extra.active ? extraMessages.activeBadge : extraMessages.inactiveBadge }), _jsx(Badge, { variant: "secondary", children: getPricingModeLabel(extra.pricingMode, extraMessages) }), extra.pricedPerPerson ? (_jsx(Badge, { variant: "outline", children: extraMessages.perTravelerBadge })) : null] }), extra.description ? (_jsx("p", { className: "text-muted-foreground text-xs", children: extra.description })) : null] }), _jsxs(ActionMenu, { children: [_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 px-2 py-1.5 text-sm", onClick: () => startEdit(extra), children: [_jsx(Pencil, { className: "h-4 w-4" }), extraMessages.editAction] }), _jsxs("button", { type: "button", className: "flex w-full items-center gap-2 px-2 py-1.5 text-destructive text-sm", onClick: () => {
87
+ if (confirm(formatMessage(extraMessages.deleteConfirm, { name: extra.name })))
88
+ remove.mutate(extra.id, { onSuccess: () => void refetch() });
89
+ }, children: [_jsx(Trash2, { className: "h-4 w-4" }), extraMessages.deleteAction] })] })] }, extra.id))) })), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { size: "lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: editing ? extraMessages.editTitle : extraMessages.newTitle }), _jsx(DialogDescription, { children: extraMessages.dialogDescription })] }), _jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [_jsx(Field, { label: extraMessages.nameLabel, children: _jsx(Input, { value: form.name, onChange: (event) => setForm({ ...form, name: event.target.value }) }) }), _jsx(Field, { label: extraMessages.codeLabel, children: _jsx(Input, { value: form.code, onChange: (event) => setForm({ ...form, code: event.target.value }) }) })] }), _jsx(Field, { label: extraMessages.descriptionLabel, children: _jsx(Textarea, { value: form.description, onChange: (event) => setForm({ ...form, description: event.target.value }) }) }), _jsxs("div", { className: "grid gap-3 md:grid-cols-3", children: [_jsx(Field, { label: extraMessages.selectionLabel, children: _jsxs(Select, { value: form.selectionType, onValueChange: (value) => setForm({
90
+ ...form,
91
+ selectionType: (value ?? "optional"),
92
+ }), items: selectionTypes.map((type) => ({
93
+ value: type,
94
+ label: getSelectionTypeLabel(type, extraMessages),
95
+ })), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: selectionTypes.map((type) => (_jsx(SelectItem, { value: type, children: getSelectionTypeLabel(type, extraMessages) }, type))) })] }) }), _jsx(Field, { label: extraMessages.pricingLabel, children: _jsxs(Select, { value: form.pricingMode, onValueChange: (value) => setForm({
96
+ ...form,
97
+ pricingMode: (value ?? "per_booking"),
98
+ }), items: pricingModes.map((mode) => ({
99
+ value: mode,
100
+ label: getPricingModeLabel(mode, extraMessages),
101
+ })), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: pricingModes.map((mode) => (_jsx(SelectItem, { value: mode, children: getPricingModeLabel(mode, extraMessages) }, mode))) })] }) }), _jsx(Field, { label: extraMessages.defaultQuantityLabel, children: _jsx(Input, { value: form.defaultQuantity, type: "number", min: "0", onChange: (event) => setForm({ ...form, defaultQuantity: event.target.value }) }) }), _jsx(Field, { label: extraMessages.minQuantityLabel, children: _jsx(Input, { value: form.minQuantity, type: "number", min: "0", onChange: (event) => setForm({ ...form, minQuantity: event.target.value }) }) }), _jsx(Field, { label: extraMessages.maxQuantityLabel, children: _jsx(Input, { value: form.maxQuantity, type: "number", min: "0", onChange: (event) => setForm({ ...form, maxQuantity: event.target.value }) }) })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-6", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Checkbox, { id: "product-extra-priced-per-person", checked: form.pricedPerPerson, onCheckedChange: (checked) => setForm({ ...form, pricedPerPerson: checked === true }) }), _jsx(Label, { htmlFor: "product-extra-priced-per-person", children: extraMessages.perTravelerLabel })] }), _jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Checkbox, { id: "product-extra-active", checked: form.active, onCheckedChange: (checked) => setForm({ ...form, active: checked === true }) }), _jsx(Label, { htmlFor: "product-extra-active", children: extraMessages.activeLabel })] })] })] }), _jsxs(DialogFooter, { className: "-mx-6 -mb-6", children: [_jsx(Button, { variant: "ghost", onClick: () => setOpen(false), children: extraMessages.cancel }), _jsx(Button, { onClick: () => void save(), disabled: !form.name.trim(), children: editing ? extraMessages.saveChanges : extraMessages.create })] })] }) })] }));
102
+ }
103
+ function Field({ label, children }) {
104
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: label }), children] }));
105
+ }
106
+ function parseNullableInt(value) {
107
+ const parsed = Number.parseInt(value, 10);
108
+ return Number.isFinite(parsed) ? parsed : null;
109
+ }
110
+ function getSelectionTypeLabel(value, messages) {
111
+ switch (value) {
112
+ case "optional":
113
+ return messages.selectionOptional;
114
+ case "required":
115
+ return messages.selectionRequired;
116
+ case "default_selected":
117
+ return messages.selectionDefaultSelected;
118
+ case "unavailable":
119
+ return messages.selectionUnavailable;
120
+ default:
121
+ return value;
122
+ }
123
+ }
124
+ function getPricingModeLabel(value, messages) {
125
+ switch (value) {
126
+ case "included":
127
+ return messages.pricingIncluded;
128
+ case "per_person":
129
+ return messages.pricingPerPerson;
130
+ case "per_booking":
131
+ return messages.pricingPerBooking;
132
+ case "quantity_based":
133
+ return messages.pricingQuantityBased;
134
+ case "on_request":
135
+ return messages.pricingOnRequest;
136
+ case "free":
137
+ return messages.pricingFree;
138
+ default:
139
+ return value;
140
+ }
141
+ }
@@ -0,0 +1,16 @@
1
+ export type ItineraryData = {
2
+ id: string;
3
+ name: string;
4
+ isDefault: boolean;
5
+ };
6
+ export interface ProductItineraryFormProps {
7
+ itinerary?: ItineraryData;
8
+ isFirstItinerary?: boolean;
9
+ onSubmit: (values: {
10
+ name: string;
11
+ isDefault: boolean;
12
+ }) => Promise<void> | void;
13
+ onCancel?: () => void;
14
+ }
15
+ export declare function ProductItineraryForm({ itinerary, isFirstItinerary, onSubmit, onCancel, }: ProductItineraryFormProps): import("react/jsx-runtime").JSX.Element;
16
+ //# sourceMappingURL=product-itinerary-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-itinerary-form.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-itinerary-form.tsx"],"names":[],"mappings":"AAsBA,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,MAAM,WAAW,yBAAyB;IACxC,SAAS,CAAC,EAAE,aAAa,CAAA;IACzB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAChF,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,wBAAgB,oBAAoB,CAAC,EACnC,SAAS,EACT,gBAAwB,EACxB,QAAQ,EACR,QAAQ,GACT,EAAE,yBAAyB,2CA6E3B"}
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Checkbox, Input, Label } from "@voyantjs/ui/components";
3
+ import { Loader2 } from "lucide-react";
4
+ import { useEffect } from "react";
5
+ import { useForm } from "react-hook-form";
6
+ import { z } from "zod/v4";
7
+ import { useProductDetailMessages } from "./host.js";
8
+ import { zodResolver } from "./zod-resolver.js";
9
+ const buildItineraryFormSchema = (messages) => z.object({
10
+ name: z.string().min(1, messages.validationNameRequired).max(255),
11
+ isDefault: z.boolean(),
12
+ });
13
+ export function ProductItineraryForm({ itinerary, isFirstItinerary = false, onSubmit, onCancel, }) {
14
+ const messages = useProductDetailMessages();
15
+ const itineraryMessages = messages.products.operations.itineraries;
16
+ const isEditing = !!itinerary;
17
+ const schema = buildItineraryFormSchema(itineraryMessages);
18
+ const form = useForm({
19
+ resolver: zodResolver(schema),
20
+ defaultValues: itinerary
21
+ ? { name: itinerary.name, isDefault: itinerary.isDefault }
22
+ : { name: "", isDefault: isFirstItinerary },
23
+ });
24
+ useEffect(() => {
25
+ if (itinerary) {
26
+ form.reset({ name: itinerary.name, isDefault: itinerary.isDefault });
27
+ }
28
+ else {
29
+ form.reset({ name: "", isDefault: isFirstItinerary });
30
+ }
31
+ }, [itinerary, isFirstItinerary, form]);
32
+ const isDefault = form.watch("isDefault");
33
+ const defaultLocked = isEditing && itinerary?.isDefault === true;
34
+ const handleSubmit = async (values) => {
35
+ await onSubmit(values);
36
+ };
37
+ return (_jsxs("form", { onSubmit: form.handleSubmit(handleSubmit), className: "flex flex-col gap-4", children: [_jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "product-itinerary-name", children: itineraryMessages.nameLabel }), _jsx(Input, { id: "product-itinerary-name", autoFocus: true, placeholder: itineraryMessages.namePlaceholder, ...form.register("name") }), form.formState.errors.name && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.name.message }))] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx(Checkbox, { id: "product-itinerary-default", checked: isDefault, disabled: defaultLocked || isFirstItinerary, onCheckedChange: (checked) => form.setValue("isDefault", checked === true, { shouldDirty: true }) }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { htmlFor: "product-itinerary-default", className: "text-sm font-normal", children: itineraryMessages.setDefaultLabel }), defaultLocked ? (_jsx("p", { className: "text-xs text-muted-foreground", children: itineraryMessages.defaultLockHint })) : isFirstItinerary ? (_jsx("p", { className: "text-xs text-muted-foreground", children: itineraryMessages.firstDefaultHint })) : null] })] })] }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [onCancel ? (_jsx(Button, { type: "button", variant: "ghost", onClick: onCancel, children: itineraryMessages.cancel })) : null, _jsxs(Button, { type: "submit", disabled: form.formState.isSubmitting, children: [form.formState.isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), isEditing ? itineraryMessages.save : itineraryMessages.create] })] })] }));
38
+ }
@@ -0,0 +1,6 @@
1
+ interface ProductMarketRulesSectionProps {
2
+ productId: string;
3
+ }
4
+ export declare function ProductMarketRulesSection({ productId }: ProductMarketRulesSectionProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
6
+ //# sourceMappingURL=product-market-rules-section.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-market-rules-section.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-market-rules-section.tsx"],"names":[],"mappings":"AAiCA,UAAU,8BAA8B;IACtC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,yBAAyB,CAAC,EAAE,SAAS,EAAE,EAAE,8BAA8B,2CAuItF"}
@@ -0,0 +1,81 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { formatMessage } from "@voyantjs/i18n";
4
+ import { useMarketProductRuleMutation, useMarketProductRules, useMarkets, } from "@voyantjs/markets-react";
5
+ import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components";
6
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components/select";
7
+ import { Globe2, Loader2, Plus, Trash2 } from "lucide-react";
8
+ import { useMemo, useState } from "react";
9
+ import { toast } from "sonner";
10
+ import { useProductDetailMessages } from "./host.js";
11
+ const SELLABILITY = ["sellable", "on_request", "unavailable"];
12
+ const VISIBILITY = ["public", "private", "hidden"];
13
+ export function ProductMarketRulesSection({ productId }) {
14
+ const t = useProductDetailMessages().products.operations.marketRules;
15
+ const marketsQuery = useMarkets({ status: "active", limit: 100 });
16
+ const rulesQuery = useMarketProductRules({ productId, limit: 200 });
17
+ const mutations = useMarketProductRuleMutation();
18
+ const markets = marketsQuery.data?.data ?? [];
19
+ const productRules = (rulesQuery.data?.data ?? []).filter((rule) => !rule.optionId);
20
+ const marketById = useMemo(() => new Map(markets.map((market) => [market.id, market])), [markets]);
21
+ const existingMarketIds = new Set(productRules.map((rule) => rule.marketId));
22
+ const availableMarkets = markets.filter((market) => !existingMarketIds.has(market.id));
23
+ const [selectedMarketId, setSelectedMarketId] = useState("");
24
+ const isLoading = marketsQuery.isPending || rulesQuery.isPending;
25
+ const isMutating = mutations.create.isPending || mutations.update.isPending || mutations.remove.isPending;
26
+ const addRule = async () => {
27
+ if (!selectedMarketId)
28
+ return;
29
+ try {
30
+ await mutations.create.mutateAsync({
31
+ marketId: selectedMarketId,
32
+ productId,
33
+ optionId: null,
34
+ priceCatalogId: null,
35
+ visibility: "public",
36
+ sellability: "sellable",
37
+ channelScope: "all",
38
+ active: true,
39
+ availableFrom: null,
40
+ availableTo: null,
41
+ notes: null,
42
+ });
43
+ setSelectedMarketId("");
44
+ }
45
+ catch (error) {
46
+ toast.error(error instanceof Error ? error.message : t.addFailed);
47
+ }
48
+ };
49
+ const updateRule = async (rule, input) => {
50
+ try {
51
+ await mutations.update.mutateAsync({ id: rule.id, input });
52
+ }
53
+ catch (error) {
54
+ toast.error(error instanceof Error ? error.message : t.updateFailed);
55
+ }
56
+ };
57
+ const removeRule = async (rule) => {
58
+ const marketName = marketById.get(rule.marketId)?.name ?? rule.marketId;
59
+ if (!confirm(formatMessage(t.removeConfirm, { market: marketName })))
60
+ return;
61
+ try {
62
+ await mutations.remove.mutateAsync(rule.id);
63
+ }
64
+ catch (error) {
65
+ toast.error(error instanceof Error ? error.message : t.removeFailed);
66
+ }
67
+ };
68
+ return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(Globe2, { className: "h-4 w-4" }), t.title] }), _jsx("p", { className: "mt-1 text-muted-foreground text-sm", children: t.description })] }), isLoading ? _jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" }) : null] }) }), _jsxs(CardContent, { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "grid gap-2 sm:grid-cols-[minmax(0,1fr)_auto]", children: [_jsxs(Select, { value: selectedMarketId, onValueChange: (value) => setSelectedMarketId(value ?? ""), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: t.addMarketPlaceholder }) }), _jsx(SelectContent, { children: availableMarkets.map((market) => (_jsxs(SelectItem, { value: market.id, children: [market.name, " \u00B7 ", market.defaultLanguageTag] }, market.id))) })] }), _jsxs(Button, { type: "button", onClick: () => void addRule(), disabled: !selectedMarketId || isMutating, children: [mutations.create.isPending ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Plus, { className: "mr-2 h-4 w-4" })), t.addMarketButton] })] }), productRules.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-4 text-muted-foreground text-sm", children: t.empty })) : (_jsxs("div", { className: "rounded-md border", children: [_jsxs("div", { className: "hidden gap-3 border-b px-3 py-2 text-muted-foreground text-xs font-medium md:grid md:grid-cols-[minmax(0,1fr)_160px_150px_130px_40px] md:items-center", children: [_jsx("span", {}), _jsx("span", { children: t.sellabilityLabel }), _jsx("span", { children: t.visibilityLabel }), _jsx("span", { children: t.statusLabel }), _jsx("span", {})] }), _jsx("div", { className: "divide-y", children: productRules.map((rule) => (_jsx(MarketRuleRow, { rule: rule, marketName: marketById.get(rule.marketId)?.name ?? rule.marketId, languageTag: marketById.get(rule.marketId)?.defaultLanguageTag ?? null, disabled: isMutating, onUpdate: updateRule, onRemove: removeRule, messages: t }, rule.id))) })] }))] })] }));
69
+ }
70
+ function MarketRuleRow({ rule, marketName, languageTag, disabled, onUpdate, onRemove, messages, }) {
71
+ return (_jsxs("div", { className: "grid gap-3 p-3 md:grid-cols-[minmax(0,1fr)_160px_150px_130px_40px] md:items-center", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate font-medium text-sm", children: marketName }), _jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-2", children: [languageTag ? _jsx(Badge, { variant: "secondary", children: languageTag }) : null, _jsx(Badge, { variant: rule.active ? "default" : "outline", children: rule.active ? messages.activeBadge : messages.inactiveBadge })] })] }), _jsxs(Select, { value: rule.sellability, onValueChange: (value) => {
72
+ if (value)
73
+ onUpdate(rule, { sellability: value });
74
+ }, disabled: disabled, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: SELLABILITY.map((value) => (_jsx(SelectItem, { value: value, children: messages.sellabilityOptions[value] }, value))) })] }), _jsxs(Select, { value: rule.visibility, onValueChange: (value) => {
75
+ if (value)
76
+ onUpdate(rule, { visibility: value });
77
+ }, disabled: disabled, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: VISIBILITY.map((value) => (_jsx(SelectItem, { value: value, children: messages.visibilityOptions[value] }, value))) })] }), _jsxs(Select, { value: rule.active ? "active" : "inactive", onValueChange: (value) => {
78
+ if (value)
79
+ onUpdate(rule, { active: value === "active" });
80
+ }, disabled: disabled, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "active", children: messages.activeStatus }), _jsx(SelectItem, { value: "inactive", children: messages.inactiveStatus })] })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => onRemove(rule), disabled: disabled, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }));
81
+ }
@@ -0,0 +1,19 @@
1
+ import type { ProductMediaItem } from "./product-detail-shared.js";
2
+ interface ProductMediaGalleryProps {
3
+ productId: string;
4
+ media: ProductMediaItem[];
5
+ isUploading: boolean;
6
+ onUpload: (file: File) => void;
7
+ onSetCover: (mediaId: string) => void;
8
+ onDelete: (mediaId: string) => void;
9
+ onReorderLocal?: (items: ProductMediaItem[]) => void;
10
+ }
11
+ /**
12
+ * Media block for product + itinerary-day photos.
13
+ * - Featured carousel with prev/next, click-to-lightbox
14
+ * - Reorder-mode toggle exposes draggable thumbnail row (motion Reorder)
15
+ * - Per-tile actions: set cover, delete
16
+ */
17
+ export declare function ProductMediaGallery({ productId, media, isUploading, onUpload, onSetCover, onDelete, onReorderLocal, }: ProductMediaGalleryProps): import("react/jsx-runtime").JSX.Element;
18
+ export {};
19
+ //# sourceMappingURL=product-media-gallery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-media-gallery.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-media-gallery.tsx"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAElE,UAAU,wBAAwB;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,gBAAgB,EAAE,CAAA;IACzB,WAAW,EAAE,OAAO,CAAA;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IAC9B,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAA;CACrD;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,KAAK,EACL,WAAW,EACX,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,cAAc,GACf,EAAE,wBAAwB,2CAyM1B"}
@@ -0,0 +1,114 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useMutation } from "@tanstack/react-query";
4
+ import { formatMessage } from "@voyantjs/i18n";
5
+ import { Button } from "@voyantjs/ui/components";
6
+ import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, } from "@voyantjs/ui/components/carousel";
7
+ import { Dialog, DialogContent, DialogTitle } from "@voyantjs/ui/components/dialog";
8
+ import { cn } from "@voyantjs/ui/lib/utils";
9
+ import { ChevronLeft, ChevronRight, GripVertical, Loader2, Star, Trash2, Upload, X, } from "lucide-react";
10
+ import { Reorder } from "motion/react";
11
+ import { useEffect, useRef, useState } from "react";
12
+ import { useProductDetailApi, useProductDetailMessages } from "./host.js";
13
+ /**
14
+ * Media block for product + itinerary-day photos.
15
+ * - Featured carousel with prev/next, click-to-lightbox
16
+ * - Reorder-mode toggle exposes draggable thumbnail row (motion Reorder)
17
+ * - Per-tile actions: set cover, delete
18
+ */
19
+ export function ProductMediaGallery({ productId, media, isUploading, onUpload, onSetCover, onDelete, onReorderLocal, }) {
20
+ const messages = useProductDetailMessages();
21
+ const api = useProductDetailApi();
22
+ const mediaMessages = messages.products.operations.media;
23
+ const fileInputRef = useRef(null);
24
+ const [carouselApi, setCarouselApi] = useState();
25
+ const [active, setActive] = useState(0);
26
+ const [lightboxIndex, setLightboxIndex] = useState(null);
27
+ const [reorderMode, setReorderMode] = useState(false);
28
+ const [localOrder, setLocalOrder] = useState(media);
29
+ // Keep local order in sync with props unless we're actively reordering.
30
+ useEffect(() => {
31
+ if (!reorderMode)
32
+ setLocalOrder(media);
33
+ }, [media, reorderMode]);
34
+ useEffect(() => {
35
+ if (!carouselApi)
36
+ return;
37
+ const onSelect = () => setActive(carouselApi.selectedScrollSnap());
38
+ onSelect();
39
+ carouselApi.on("select", onSelect);
40
+ return () => {
41
+ carouselApi.off("select", onSelect);
42
+ };
43
+ }, [carouselApi]);
44
+ const reorderMutation = useMutation({
45
+ mutationFn: (items) => api_post_reorder(api, productId, items),
46
+ });
47
+ const commitReorder = () => {
48
+ const items = localOrder.map((m, i) => ({ id: m.id, sortOrder: i }));
49
+ reorderMutation.mutate(items, {
50
+ onSuccess: () => {
51
+ setReorderMode(false);
52
+ onReorderLocal?.(localOrder);
53
+ },
54
+ });
55
+ };
56
+ const cancelReorder = () => {
57
+ setLocalOrder(media);
58
+ setReorderMode(false);
59
+ };
60
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: media.length === 0
61
+ ? mediaMessages.emptySummary
62
+ : formatMessage(mediaMessages.itemCount, {
63
+ count: media.length,
64
+ suffix: media.length === 1 ? "" : "s",
65
+ }) }), _jsxs("div", { className: "flex items-center gap-2", children: [media.length > 1 ? (reorderMode ? (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-8 text-xs", onClick: cancelReorder, disabled: reorderMutation.isPending, children: mediaMessages.cancelReorder }), _jsxs(Button, { type: "button", size: "sm", className: "h-8 text-xs", onClick: commitReorder, disabled: reorderMutation.isPending, children: [reorderMutation.isPending ? (_jsx(Loader2, { className: "mr-1.5 h-3.5 w-3.5 animate-spin" })) : null, mediaMessages.saveOrder] })] })) : (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-8 text-xs", onClick: () => setReorderMode(true), children: [_jsx(GripVertical, { className: "mr-1.5 h-3.5 w-3.5" }), mediaMessages.reorder] }))) : null, _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-8 text-xs", disabled: isUploading || reorderMode, onClick: () => fileInputRef.current?.click(), children: [isUploading ? (_jsx(Loader2, { className: "mr-1.5 h-3.5 w-3.5 animate-spin" })) : (_jsx(Upload, { className: "mr-1.5 h-3.5 w-3.5" })), mediaMessages.upload] }), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*,video/*", className: "hidden", onChange: (event) => {
66
+ const file = event.target.files?.[0];
67
+ if (file) {
68
+ onUpload(file);
69
+ event.target.value = "";
70
+ }
71
+ } })] })] }), media.length === 0 ? (_jsx("div", { className: "flex h-40 items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground", children: mediaMessages.emptyState })) : reorderMode ? (_jsx(ReorderGrid, { items: localOrder, onReorder: setLocalOrder, mediaMessages: mediaMessages })) : (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs(Carousel, { setApi: setCarouselApi, opts: { align: "start" }, className: "w-full", children: [_jsx(CarouselContent, { children: media.map((item, idx) => (_jsx(CarouselItem, { children: _jsx(MediaPreview, { item: item, onOpen: () => setLightboxIndex(idx), onSetCover: onSetCover, onDelete: onDelete, mediaMessages: mediaMessages }) }, item.id))) }), media.length > 1 ? (_jsxs(_Fragment, { children: [_jsx(CarouselPrevious, { className: "left-2 size-8" }), _jsx(CarouselNext, { className: "right-2 size-8" })] })) : null] }), media.length > 1 ? (_jsx("div", { className: "flex gap-2 overflow-x-auto pb-1", children: media.map((item, idx) => (_jsxs("button", { type: "button", onClick: () => carouselApi?.scrollTo(idx), className: cn("relative size-16 flex-shrink-0 overflow-hidden rounded border transition-all", idx === active
72
+ ? "border-primary ring-2 ring-primary/30"
73
+ : "border-border opacity-60 hover:opacity-100"), children: [item.mediaType === "image" ? (_jsx("img", { src: item.url, alt: item.altText ?? item.name, className: "h-full w-full object-cover" })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center bg-muted text-[9px] uppercase text-muted-foreground", children: item.mediaType })), item.isCover ? (_jsx(Star, { className: "absolute left-0.5 top-0.5 h-3 w-3 fill-yellow-400 text-yellow-400" })) : null] }, item.id))) })) : null] })), _jsx(Lightbox, { media: media, index: lightboxIndex, onClose: () => setLightboxIndex(null), mediaMessages: mediaMessages })] }));
74
+ }
75
+ function MediaPreview({ item, onOpen, onSetCover, onDelete, mediaMessages, }) {
76
+ return (_jsxs("div", { className: "group relative aspect-[16/9] w-full overflow-hidden rounded-lg border bg-muted", children: [item.mediaType === "image" ? (_jsx("button", { type: "button", onClick: onOpen, className: "h-full w-full", children: _jsx("img", { src: item.url, alt: item.altText ?? item.name, className: "h-full w-full cursor-zoom-in object-cover" }) })) : item.mediaType === "video" ? (_jsx("video", { src: item.url, controls: true, className: "h-full w-full object-cover" })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center text-xs uppercase text-muted-foreground", children: item.mediaType })), item.isCover ? (_jsxs("div", { className: "pointer-events-none absolute left-3 top-3 flex items-center gap-1 rounded-full bg-black/70 px-2.5 py-1 text-[11px] font-medium text-white", children: [_jsx(Star, { className: "h-3 w-3 fill-yellow-400 text-yellow-400" }), mediaMessages.cover] })) : null, _jsxs("div", { className: "pointer-events-none absolute inset-0 flex items-end justify-end gap-1.5 bg-gradient-to-t from-black/60 via-black/0 to-black/0 p-3 opacity-0 transition-opacity group-hover:opacity-100", children: [!item.isCover && item.mediaType === "image" ? (_jsxs(Button, { type: "button", size: "sm", variant: "secondary", className: "pointer-events-auto h-8 text-xs", onClick: (e) => {
77
+ e.stopPropagation();
78
+ onSetCover(item.id);
79
+ }, children: [_jsx(Star, { className: "mr-1 h-3 w-3" }), mediaMessages.setCover] })) : null, _jsxs(Button, { type: "button", size: "sm", variant: "destructive", className: "pointer-events-auto h-8 text-xs", onClick: (e) => {
80
+ e.stopPropagation();
81
+ if (confirm(mediaMessages.deleteConfirm))
82
+ onDelete(item.id);
83
+ }, children: [_jsx(Trash2, { className: "mr-1 h-3 w-3" }), mediaMessages.delete] })] })] }));
84
+ }
85
+ function ReorderGrid({ items, onReorder, mediaMessages, }) {
86
+ return (_jsx(Reorder.Group, { axis: "x", values: items, onReorder: onReorder, as: "ul", className: "flex flex-wrap gap-3", children: items.map((item) => (_jsxs(Reorder.Item, { value: item, as: "li", className: "relative size-28 cursor-grab overflow-hidden rounded-lg border bg-muted active:cursor-grabbing", whileDrag: { scale: 1.05, zIndex: 10 }, children: [item.mediaType === "image" ? (_jsx("img", { src: item.url, alt: item.altText ?? item.name, draggable: false, className: "pointer-events-none h-full w-full object-cover" })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center bg-muted text-[10px] uppercase text-muted-foreground", children: item.mediaType })), item.isCover ? (_jsx("div", { className: "pointer-events-none absolute left-1 top-1 rounded-full bg-black/70 p-1", children: _jsx(Star, { className: "h-3 w-3 fill-yellow-400 text-yellow-400" }) })) : null, _jsxs("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 flex items-center justify-center gap-1 bg-black/60 py-1 text-[10px] text-white", children: [_jsx(GripVertical, { className: "h-3 w-3" }), mediaMessages.drag] })] }, item.id))) }));
87
+ }
88
+ function Lightbox({ media, index, onClose, mediaMessages, }) {
89
+ const [current, setCurrent] = useState(index ?? 0);
90
+ useEffect(() => {
91
+ if (index != null)
92
+ setCurrent(index);
93
+ }, [index]);
94
+ const item = media[current];
95
+ const open = index != null && item != null;
96
+ const canPrev = current > 0;
97
+ const canNext = current < media.length - 1;
98
+ useEffect(() => {
99
+ if (!open)
100
+ return;
101
+ const onKey = (e) => {
102
+ if (e.key === "ArrowLeft" && canPrev)
103
+ setCurrent((c) => c - 1);
104
+ if (e.key === "ArrowRight" && canNext)
105
+ setCurrent((c) => c + 1);
106
+ };
107
+ window.addEventListener("keydown", onKey);
108
+ return () => window.removeEventListener("keydown", onKey);
109
+ }, [open, canPrev, canNext]);
110
+ return (_jsx(Dialog, { open: open, onOpenChange: (o) => !o && onClose(), children: _jsxs(DialogContent, { className: "h-screen w-screen max-w-none translate-x-0 translate-y-0 top-0 left-0 gap-0 rounded-none border-0 bg-black/90 p-0 shadow-none ring-0 sm:max-w-none", showCloseButton: false, children: [_jsx(DialogTitle, { className: "sr-only", children: mediaMessages.viewerTitle }), item ? (_jsxs("div", { className: "relative flex h-full w-full items-center justify-center", children: [_jsx("button", { type: "button", onClick: onClose, className: "absolute right-2 top-2 z-10 rounded-full bg-black/60 p-2 text-white hover:bg-black/80", "aria-label": mediaMessages.close, children: _jsx(X, { className: "h-5 w-5" }) }), canPrev ? (_jsx("button", { type: "button", onClick: () => setCurrent((c) => c - 1), className: "absolute left-2 z-10 rounded-full bg-black/60 p-2 text-white hover:bg-black/80", "aria-label": mediaMessages.previous, children: _jsx(ChevronLeft, { className: "h-6 w-6" }) })) : null, canNext ? (_jsx("button", { type: "button", onClick: () => setCurrent((c) => c + 1), className: "absolute right-2 z-10 rounded-full bg-black/60 p-2 text-white hover:bg-black/80", "aria-label": mediaMessages.next, children: _jsx(ChevronRight, { className: "h-6 w-6" }) })) : null, item.mediaType === "image" ? (_jsx("img", { src: item.url, alt: item.altText ?? item.name, className: "max-h-[95vh] max-w-[95vw] object-contain" })) : item.mediaType === "video" ? (_jsx("video", { src: item.url, controls: true, autoPlay: true, className: "max-h-[95vh] max-w-[95vw]" })) : (_jsx("div", { className: "rounded-lg bg-background p-8 text-sm text-muted-foreground", children: item.name }))] })) : null] }) }));
111
+ }
112
+ async function api_post_reorder(api, productId, items) {
113
+ return api.post(`/v1/products/${productId}/media/reorder`, { items });
114
+ }
@@ -0,0 +1,12 @@
1
+ import { type OptionPriceRuleData } from "./product-option-price-rule-form.js";
2
+ export type { OptionPriceRuleData };
3
+ type OptionPriceRuleDialogProps = {
4
+ open: boolean;
5
+ onOpenChange: (open: boolean) => void;
6
+ productId: string;
7
+ optionId: string;
8
+ rule?: OptionPriceRuleData;
9
+ onSuccess: () => void;
10
+ };
11
+ export declare function OptionPriceRuleDialog({ open, onOpenChange, productId, optionId, rule, onSuccess, }: OptionPriceRuleDialogProps): import("react/jsx-runtime").JSX.Element;
12
+ //# sourceMappingURL=product-option-price-rule-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-option-price-rule-dialog.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-option-price-rule-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,mBAAmB,EAAuB,MAAM,qCAAqC,CAAA;AAEnG,YAAY,EAAE,mBAAmB,EAAE,CAAA;AAEnC,KAAK,0BAA0B,GAAG;IAChC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,mBAAmB,CAAA;IAC1B,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,qBAAqB,CAAC,EACpC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,IAAI,EACJ,SAAS,GACV,EAAE,0BAA0B,2CAyB5B"}
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle } from "@voyantjs/ui/components";
3
+ import { useProductDetailMessages } from "./host.js";
4
+ import { OptionPriceRuleForm } from "./product-option-price-rule-form.js";
5
+ export function OptionPriceRuleDialog({ open, onOpenChange, productId, optionId, rule, onSuccess, }) {
6
+ const messages = useProductDetailMessages();
7
+ const priceRuleMessages = messages.products.operations.priceRules;
8
+ const isEditing = !!rule;
9
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: isEditing ? priceRuleMessages.editTitle : priceRuleMessages.newTitle }) }), _jsx(SheetBody, { children: _jsx(OptionPriceRuleForm, { productId: productId, optionId: optionId, rule: rule, onSuccess: onSuccess, onCancel: () => onOpenChange(false) }) })] }) }));
10
+ }
@@ -0,0 +1,29 @@
1
+ export type OptionPriceRuleData = {
2
+ id: string;
3
+ productId: string;
4
+ optionId: string;
5
+ priceCatalogId: string;
6
+ priceScheduleId: string | null;
7
+ cancellationPolicyId: string | null;
8
+ name: string;
9
+ code: string | null;
10
+ description: string | null;
11
+ pricingMode: "per_person" | "per_booking" | "starting_from" | "free" | "on_request";
12
+ baseSellAmountCents: number | null;
13
+ baseCostAmountCents: number | null;
14
+ minPerBooking: number | null;
15
+ maxPerBooking: number | null;
16
+ allPricingCategories: boolean;
17
+ isDefault: boolean;
18
+ active: boolean;
19
+ notes: string | null;
20
+ };
21
+ export interface OptionPriceRuleFormProps {
22
+ productId: string;
23
+ optionId: string;
24
+ rule?: OptionPriceRuleData;
25
+ onSuccess: () => void;
26
+ onCancel?: () => void;
27
+ }
28
+ export declare function OptionPriceRuleForm({ productId, optionId, rule, onSuccess, onCancel, }: OptionPriceRuleFormProps): import("react/jsx-runtime").JSX.Element;
29
+ //# sourceMappingURL=product-option-price-rule-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-option-price-rule-form.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-option-price-rule-form.tsx"],"names":[],"mappings":"AAkDA,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,YAAY,GAAG,aAAa,GAAG,eAAe,GAAG,MAAM,GAAG,YAAY,CAAA;IACnF,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,oBAAoB,EAAE,OAAO,CAAA;IAC7B,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB,CAAA;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,mBAAmB,CAAA;IAC1B,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAyCD,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,QAAQ,EACR,IAAI,EACJ,SAAS,EACT,QAAQ,GACT,EAAE,wBAAwB,2CA+M1B"}