@voyantjs/products-ui 0.101.1 → 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.
- package/dist/components/product-detail/date-picker.d.ts +44 -0
- package/dist/components/product-detail/date-picker.d.ts.map +1 -0
- package/dist/components/product-detail/date-picker.js +125 -0
- package/dist/components/product-detail/host.d.ts +53 -0
- package/dist/components/product-detail/host.d.ts.map +1 -0
- package/dist/components/product-detail/host.js +24 -0
- package/dist/components/product-detail/index.d.ts +6 -0
- package/dist/components/product-detail/index.d.ts.map +1 -0
- package/dist/components/product-detail/index.js +5 -0
- package/dist/components/product-detail/product-activity-section.d.ts +4 -0
- package/dist/components/product-detail/product-activity-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-activity-section.js +37 -0
- package/dist/components/product-detail/product-day-sheet.d.ts +14 -0
- package/dist/components/product-detail/product-day-sheet.d.ts.map +1 -0
- package/dist/components/product-detail/product-day-sheet.js +75 -0
- package/dist/components/product-detail/product-day-translation.d.ts +41 -0
- package/dist/components/product-detail/product-day-translation.d.ts.map +1 -0
- package/dist/components/product-detail/product-day-translation.js +111 -0
- package/dist/components/product-detail/product-departure-dialog.d.ts +11 -0
- package/dist/components/product-detail/product-departure-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-departure-dialog.js +10 -0
- package/dist/components/product-detail/product-departure-form.d.ts +25 -0
- package/dist/components/product-detail/product-departure-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-departure-form.js +217 -0
- package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts +8 -0
- package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-departure-pricing-override-dialog.js +125 -0
- package/dist/components/product-detail/product-detail-day-row.d.ts +14 -0
- package/dist/components/product-detail/product-detail-day-row.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-day-row.js +43 -0
- package/dist/components/product-detail/product-detail-dialog.d.ts +10 -0
- package/dist/components/product-detail/product-detail-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-dialog.js +10 -0
- package/dist/components/product-detail/product-detail-form.d.ts +19 -0
- package/dist/components/product-detail/product-detail-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-form.js +177 -0
- package/dist/components/product-detail/product-detail-header.d.ts +12 -0
- package/dist/components/product-detail/product-detail-header.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-header.js +19 -0
- package/dist/components/product-detail/product-detail-itinerary-section.d.ts +4 -0
- package/dist/components/product-detail/product-detail-itinerary-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-itinerary-section.js +201 -0
- package/dist/components/product-detail/product-detail-page.d.ts +4 -0
- package/dist/components/product-detail/product-detail-page.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-page.js +97 -0
- package/dist/components/product-detail/product-detail-sections.d.ts +63 -0
- package/dist/components/product-detail/product-detail-sections.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-sections.js +143 -0
- package/dist/components/product-detail/product-detail-shared.d.ts +264 -0
- package/dist/components/product-detail/product-detail-shared.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-shared.js +157 -0
- package/dist/components/product-detail/product-detail-skeleton.d.ts +9 -0
- package/dist/components/product-detail/product-detail-skeleton.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-skeleton.js +53 -0
- package/dist/components/product-detail/product-extras-section.d.ts +4 -0
- package/dist/components/product-detail/product-extras-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-extras-section.js +141 -0
- package/dist/components/product-detail/product-itinerary-form.d.ts +16 -0
- package/dist/components/product-detail/product-itinerary-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-itinerary-form.js +38 -0
- package/dist/components/product-detail/product-market-rules-section.d.ts +6 -0
- package/dist/components/product-detail/product-market-rules-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-market-rules-section.js +81 -0
- package/dist/components/product-detail/product-media-gallery.d.ts +19 -0
- package/dist/components/product-detail/product-media-gallery.d.ts.map +1 -0
- package/dist/components/product-detail/product-media-gallery.js +114 -0
- package/dist/components/product-detail/product-option-price-rule-dialog.d.ts +12 -0
- package/dist/components/product-detail/product-option-price-rule-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-option-price-rule-dialog.js +10 -0
- package/dist/components/product-detail/product-option-price-rule-form.d.ts +29 -0
- package/dist/components/product-detail/product-option-price-rule-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-option-price-rule-form.js +125 -0
- package/dist/components/product-detail/product-options-pricing.d.ts +6 -0
- package/dist/components/product-detail/product-options-pricing.d.ts.map +1 -0
- package/dist/components/product-detail/product-options-pricing.js +363 -0
- package/dist/components/product-detail/product-options-shared.d.ts +609 -0
- package/dist/components/product-detail/product-options-shared.d.ts.map +1 -0
- package/dist/components/product-detail/product-options-shared.js +34 -0
- package/dist/components/product-detail/product-payment-policy-section.d.ts +17 -0
- package/dist/components/product-detail/product-payment-policy-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-payment-policy-section.js +58 -0
- package/dist/components/product-detail/product-schedule-dialog.d.ts +11 -0
- package/dist/components/product-detail/product-schedule-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-schedule-dialog.js +10 -0
- package/dist/components/product-detail/product-schedule-form.d.ts +17 -0
- package/dist/components/product-detail/product-schedule-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-schedule-form.js +222 -0
- package/dist/components/product-detail/product-service-dialog.d.ts +12 -0
- package/dist/components/product-detail/product-service-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-service-dialog.js +10 -0
- package/dist/components/product-detail/product-service-form.d.ts +22 -0
- package/dist/components/product-detail/product-service-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-service-form.js +154 -0
- package/dist/components/product-detail/product-translation-popover.d.ts +91 -0
- package/dist/components/product-detail/product-translation-popover.d.ts.map +1 -0
- package/dist/components/product-detail/product-translation-popover.js +217 -0
- package/dist/components/product-detail/product-unit-dialog.d.ts +12 -0
- package/dist/components/product-detail/product-unit-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-unit-dialog.js +10 -0
- package/dist/components/product-detail/product-unit-form.d.ts +26 -0
- package/dist/components/product-detail/product-unit-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-unit-form.js +109 -0
- package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts +16 -0
- package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-unit-price-rule-dialog.js +10 -0
- package/dist/components/product-detail/product-unit-price-rule-form.d.ts +28 -0
- package/dist/components/product-detail/product-unit-price-rule-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-unit-price-rule-form.js +126 -0
- package/dist/components/product-detail/timezone-options.d.ts +9 -0
- package/dist/components/product-detail/timezone-options.d.ts.map +1 -0
- package/dist/components/product-detail/timezone-options.js +28 -0
- package/dist/components/product-detail/use-product-detail-data.d.ts +41 -0
- package/dist/components/product-detail/use-product-detail-data.d.ts.map +1 -0
- package/dist/components/product-detail/use-product-detail-data.js +143 -0
- package/dist/components/product-detail/use-product-detail-dialogs.d.ts +24 -0
- package/dist/components/product-detail/use-product-detail-dialogs.d.ts.map +1 -0
- package/dist/components/product-detail/use-product-detail-dialogs.js +40 -0
- package/dist/components/product-detail/zod-resolver.d.ts +4 -0
- package/dist/components/product-detail/zod-resolver.d.ts.map +1 -0
- package/dist/components/product-detail/zod-resolver.js +39 -0
- 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 @@
|
|
|
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"}
|