@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,44 @@
|
|
|
1
|
+
import { Calendar } from "@voyantjs/ui/components/calendar";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
type CalendarProps = React.ComponentProps<typeof Calendar>;
|
|
4
|
+
type SinglePreset = {
|
|
5
|
+
label: string;
|
|
6
|
+
value: string | null;
|
|
7
|
+
};
|
|
8
|
+
type DateRangeValue = {
|
|
9
|
+
from: string | null;
|
|
10
|
+
to: string | null;
|
|
11
|
+
};
|
|
12
|
+
type RangePreset = {
|
|
13
|
+
label: string;
|
|
14
|
+
value: DateRangeValue | null;
|
|
15
|
+
};
|
|
16
|
+
type SharedProps = Omit<CalendarProps, "mode" | "selected" | "onSelect" | "disabled"> & {
|
|
17
|
+
placeholder?: React.ReactNode;
|
|
18
|
+
displayFormat?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
contentClassName?: string;
|
|
21
|
+
clearable?: boolean;
|
|
22
|
+
/** Disable the entire picker (prevents opening the popover). */
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/** Per-day disable matcher, forwarded to the underlying Calendar. */
|
|
25
|
+
dateDisabled?: CalendarProps["disabled"];
|
|
26
|
+
};
|
|
27
|
+
type DatePickerProps = SharedProps & {
|
|
28
|
+
value?: string | null;
|
|
29
|
+
defaultValue?: string | null;
|
|
30
|
+
onChange?: (value: string | null) => void;
|
|
31
|
+
presets?: SinglePreset[];
|
|
32
|
+
};
|
|
33
|
+
type DateRangePickerProps = SharedProps & {
|
|
34
|
+
value?: DateRangeValue | null;
|
|
35
|
+
defaultValue?: DateRangeValue | null;
|
|
36
|
+
onChange?: (value: DateRangeValue | null) => void;
|
|
37
|
+
presets?: RangePreset[];
|
|
38
|
+
};
|
|
39
|
+
export declare function DatePicker({ value, defaultValue, onChange, presets, placeholder, // i18n-literal-ok primitive default, overridden by callers
|
|
40
|
+
displayFormat, className, contentClassName, clearable, disabled, dateDisabled, ...calendarProps }: DatePickerProps): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
export declare function DateRangePicker({ value, defaultValue, onChange, presets, placeholder, // i18n-literal-ok primitive default, overridden by callers
|
|
42
|
+
displayFormat, className, contentClassName, clearable, disabled, dateDisabled, numberOfMonths, ...calendarProps }: DateRangePickerProps): import("react/jsx-runtime").JSX.Element;
|
|
43
|
+
export type { DatePickerProps, DateRangePickerProps, DateRangeValue };
|
|
44
|
+
//# sourceMappingURL=date-picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date-picker.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/date-picker.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAA;AAO3D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,KAAK,aAAa,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,QAAQ,CAAC,CAAA;AAE1D,KAAK,YAAY,GAAG;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB,CAAA;AAED,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;CAClB,CAAA;AAED,KAAK,WAAW,GAAG;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,cAAc,GAAG,IAAI,CAAA;CAC7B,CAAA;AAED,KAAK,WAAW,GAAG,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC,GAAG;IACtF,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,qEAAqE;IACrE,YAAY,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,CAAA;CACzC,CAAA;AAED,KAAK,eAAe,GAAG,WAAW,GAAG;IACnC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACzC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAA;CACzB,CAAA;AAED,KAAK,oBAAoB,GAAG,WAAW,GAAG;IACxC,KAAK,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAC7B,YAAY,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IACpC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,KAAK,IAAI,CAAA;IACjD,OAAO,CAAC,EAAE,WAAW,EAAE,CAAA;CACxB,CAAA;AAqJD,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,OAAY,EACZ,WAA2B,EAAE,2DAA2D;AACxF,aAAqB,EACrB,SAAS,EACT,gBAAgB,EAChB,SAAgB,EAChB,QAAQ,EACR,YAAY,EACZ,GAAG,aAAa,EACjB,EAAE,eAAe,2CAgEjB;AAED,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,OAAY,EACZ,WAAiC,EAAE,2DAA2D;AAC9F,aAA0B,EAC1B,SAAS,EACT,gBAAgB,EAChB,SAAgB,EAChB,QAAQ,EACR,YAAY,EACZ,cAAkB,EAClB,GAAG,aAAa,EACjB,EAAE,oBAAoB,2CAoFtB;AAED,YAAY,EAAE,eAAe,EAAE,oBAAoB,EAAE,cAAc,EAAE,CAAA"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from "@voyantjs/ui/components/button";
|
|
4
|
+
import { Calendar } from "@voyantjs/ui/components/calendar";
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@voyantjs/ui/components/popover";
|
|
6
|
+
import { Separator } from "@voyantjs/ui/components/separator";
|
|
7
|
+
import { cn } from "@voyantjs/ui/lib/utils";
|
|
8
|
+
import { format, isValid, parseISO } from "date-fns";
|
|
9
|
+
import { CalendarIcon, XIcon } from "lucide-react";
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
function parseDateValue(value) {
|
|
12
|
+
if (!value)
|
|
13
|
+
return undefined;
|
|
14
|
+
const parsed = parseISO(value);
|
|
15
|
+
return isValid(parsed) ? parsed : undefined;
|
|
16
|
+
}
|
|
17
|
+
function formatDateValue(value) {
|
|
18
|
+
return value ? format(value, "yyyy-MM-dd") : null;
|
|
19
|
+
}
|
|
20
|
+
function hasRangeValue(value) {
|
|
21
|
+
return Boolean(value?.from || value?.to);
|
|
22
|
+
}
|
|
23
|
+
function formatRangeLabel(value, displayFormat, placeholder) {
|
|
24
|
+
if (!value?.from && !value?.to)
|
|
25
|
+
return placeholder;
|
|
26
|
+
const from = parseDateValue(value?.from);
|
|
27
|
+
const to = parseDateValue(value?.to);
|
|
28
|
+
if (from && to) {
|
|
29
|
+
return `${format(from, displayFormat)} - ${format(to, displayFormat)}`;
|
|
30
|
+
}
|
|
31
|
+
if (from) {
|
|
32
|
+
return `${format(from, displayFormat)} -`;
|
|
33
|
+
}
|
|
34
|
+
if (to) {
|
|
35
|
+
return `- ${format(to, displayFormat)}`;
|
|
36
|
+
}
|
|
37
|
+
return placeholder;
|
|
38
|
+
}
|
|
39
|
+
function DatePickerTrigger({ className, empty, disabled, children, ...props }) {
|
|
40
|
+
return (_jsxs(Button
|
|
41
|
+
// Spread base-ui's PopoverTrigger props (onClick, ref, aria/data state)
|
|
42
|
+
// first so the popover actually toggles; explicit styling props follow.
|
|
43
|
+
, { ...props, variant: "outline", "data-empty": empty, disabled: disabled, className: cn("w-full justify-start text-left font-normal data-[empty=true]:text-muted-foreground", className), children: [_jsx(CalendarIcon, { className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: children })] }));
|
|
44
|
+
}
|
|
45
|
+
function DatePickerFooter({ clearable, hasValue, onClear }) {
|
|
46
|
+
if (!clearable || !hasValue)
|
|
47
|
+
return null;
|
|
48
|
+
return (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx("div", { className: "flex justify-end p-2", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: onClear, children: [_jsx(XIcon, { className: "h-4 w-4" }), "Clear"] }) })] }));
|
|
49
|
+
}
|
|
50
|
+
function SinglePresets({ presets, onSelect }) {
|
|
51
|
+
if (presets.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex flex-wrap gap-2 p-3 pb-0", children: presets.map((preset) => (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onSelect(preset.value), children: preset.label }, preset.label))) }), _jsx("div", { className: "px-3 pt-3", children: _jsx(Separator, {}) })] }));
|
|
54
|
+
}
|
|
55
|
+
function RangePresets({ presets, onSelect }) {
|
|
56
|
+
if (presets.length === 0)
|
|
57
|
+
return null;
|
|
58
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex flex-wrap gap-2 p-3 pb-0", children: presets.map((preset) => (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onSelect(preset.value), children: preset.label }, preset.label))) }), _jsx("div", { className: "px-3 pt-3", children: _jsx(Separator, {}) })] }));
|
|
59
|
+
}
|
|
60
|
+
export function DatePicker({ value, defaultValue, onChange, presets = [], placeholder = "Pick a date", // i18n-literal-ok primitive default, overridden by callers
|
|
61
|
+
displayFormat = "PPP", className, contentClassName, clearable = true, disabled, dateDisabled, ...calendarProps }) {
|
|
62
|
+
const [open, setOpen] = React.useState(false);
|
|
63
|
+
const [internalValue, setInternalValue] = React.useState(defaultValue ?? null);
|
|
64
|
+
const isControlled = value !== undefined;
|
|
65
|
+
const selectedValue = isControlled ? (value ?? null) : internalValue;
|
|
66
|
+
const selectedDate = parseDateValue(selectedValue);
|
|
67
|
+
const handleChange = React.useCallback((nextValue) => {
|
|
68
|
+
if (!isControlled) {
|
|
69
|
+
setInternalValue(nextValue);
|
|
70
|
+
}
|
|
71
|
+
onChange?.(nextValue);
|
|
72
|
+
}, [isControlled, onChange]);
|
|
73
|
+
const handleSelect = (nextDate) => {
|
|
74
|
+
handleChange(formatDateValue(nextDate));
|
|
75
|
+
if (nextDate) {
|
|
76
|
+
setOpen(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const label = selectedDate ? format(selectedDate, displayFormat) : placeholder;
|
|
80
|
+
return (_jsxs(Popover, { open: disabled ? false : open, onOpenChange: disabled ? undefined : setOpen, children: [_jsx(PopoverTrigger, { render: _jsx(DatePickerTrigger, { className: className, empty: !selectedDate, disabled: disabled }), children: label }), _jsxs(PopoverContent, { align: "start", className: cn("w-auto p-0", contentClassName), children: [_jsx(SinglePresets, { presets: presets, onSelect: (presetValue) => {
|
|
81
|
+
handleChange(presetValue);
|
|
82
|
+
setOpen(false);
|
|
83
|
+
} }), _jsx(Calendar, { mode: "single", selected: selectedDate, onSelect: handleSelect, defaultMonth: selectedDate, disabled: dateDisabled, ...calendarProps }), _jsx(DatePickerFooter, { clearable: clearable, hasValue: Boolean(selectedDate), onClear: () => {
|
|
84
|
+
handleChange(null);
|
|
85
|
+
setOpen(false);
|
|
86
|
+
} })] })] }));
|
|
87
|
+
}
|
|
88
|
+
export function DateRangePicker({ value, defaultValue, onChange, presets = [], placeholder = "Pick a date range", // i18n-literal-ok primitive default, overridden by callers
|
|
89
|
+
displayFormat = "LLL d, y", className, contentClassName, clearable = true, disabled, dateDisabled, numberOfMonths = 2, ...calendarProps }) {
|
|
90
|
+
const [open, setOpen] = React.useState(false);
|
|
91
|
+
const [internalValue, setInternalValue] = React.useState(defaultValue ?? null);
|
|
92
|
+
const isControlled = value !== undefined;
|
|
93
|
+
const selectedValue = isControlled ? (value ?? null) : internalValue;
|
|
94
|
+
const selectedRange = selectedValue
|
|
95
|
+
? {
|
|
96
|
+
from: parseDateValue(selectedValue.from),
|
|
97
|
+
to: parseDateValue(selectedValue.to),
|
|
98
|
+
}
|
|
99
|
+
: undefined;
|
|
100
|
+
const handleChange = React.useCallback((nextValue) => {
|
|
101
|
+
if (!isControlled) {
|
|
102
|
+
setInternalValue(nextValue);
|
|
103
|
+
}
|
|
104
|
+
onChange?.(nextValue);
|
|
105
|
+
}, [isControlled, onChange]);
|
|
106
|
+
const handleSelect = (nextRange) => {
|
|
107
|
+
const nextValue = nextRange?.from || nextRange?.to
|
|
108
|
+
? {
|
|
109
|
+
from: formatDateValue(nextRange.from),
|
|
110
|
+
to: formatDateValue(nextRange.to),
|
|
111
|
+
}
|
|
112
|
+
: null;
|
|
113
|
+
handleChange(nextValue);
|
|
114
|
+
if (nextRange?.from && nextRange.to) {
|
|
115
|
+
setOpen(false);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
return (_jsxs(Popover, { open: disabled ? false : open, onOpenChange: disabled ? undefined : setOpen, children: [_jsx(PopoverTrigger, { render: _jsx(DatePickerTrigger, { className: className, empty: !hasRangeValue(selectedValue), disabled: disabled }), children: formatRangeLabel(selectedValue, displayFormat, placeholder) }), _jsxs(PopoverContent, { align: "start", className: cn("w-auto p-0", contentClassName), children: [_jsx(RangePresets, { presets: presets, onSelect: (presetValue) => {
|
|
119
|
+
handleChange(presetValue);
|
|
120
|
+
setOpen(false);
|
|
121
|
+
} }), _jsx(Calendar, { mode: "range", selected: selectedRange, onSelect: handleSelect, defaultMonth: selectedRange?.from, numberOfMonths: numberOfMonths, disabled: dateDisabled, ...calendarProps }), _jsx(DatePickerFooter, { clearable: clearable, hasValue: hasRangeValue(selectedValue), onClear: () => {
|
|
122
|
+
handleChange(null);
|
|
123
|
+
setOpen(false);
|
|
124
|
+
} })] })] }));
|
|
125
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { OperatorAdminMessages } from "@voyantjs/i18n";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { ProductMediaUploadHandler } from "../product-media-section.js";
|
|
4
|
+
/**
|
|
5
|
+
* The product-detail page and its sections are transport- and app-agnostic.
|
|
6
|
+
* The host application injects everything app-specific — localized messages, a
|
|
7
|
+
* REST client, the active locale, navigation, and a media upload handler — via
|
|
8
|
+
* this context so the same page can be mounted by any template.
|
|
9
|
+
*/
|
|
10
|
+
export type ProductDetailMessages = OperatorAdminMessages;
|
|
11
|
+
export interface ProductDetailApi {
|
|
12
|
+
get: <T = unknown>(path: string) => Promise<T>;
|
|
13
|
+
post: <T = unknown>(path: string, body?: unknown) => Promise<T>;
|
|
14
|
+
patch: <T = unknown>(path: string, body?: unknown) => Promise<T>;
|
|
15
|
+
delete: <T = unknown>(path: string) => Promise<T>;
|
|
16
|
+
}
|
|
17
|
+
export interface ProductDetailNavigation {
|
|
18
|
+
toProducts: () => void;
|
|
19
|
+
toProduct: (productId: string) => void;
|
|
20
|
+
toNewBooking: (productId: string) => void;
|
|
21
|
+
toAvailability: (slotId: string) => void;
|
|
22
|
+
}
|
|
23
|
+
export interface ProductDetailBreadcrumb {
|
|
24
|
+
label: string;
|
|
25
|
+
href?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ProductDetailHostValue {
|
|
28
|
+
messages: ProductDetailMessages;
|
|
29
|
+
api: ProductDetailApi;
|
|
30
|
+
locale: string;
|
|
31
|
+
navigate: ProductDetailNavigation;
|
|
32
|
+
uploadMedia?: ProductMediaUploadHandler;
|
|
33
|
+
/** Optional app-shell breadcrumb sink (e.g. the operator admin shell). */
|
|
34
|
+
setBreadcrumbs?: (items: ProductDetailBreadcrumb[]) => void;
|
|
35
|
+
/** Optional extra content rendered under each product option (e.g. an app-specific resource panel). */
|
|
36
|
+
renderOptionExtras?: (productId: string, optionId: string) => ReactNode;
|
|
37
|
+
}
|
|
38
|
+
export declare function ProductDetailHostProvider({ value, children, }: {
|
|
39
|
+
value: ProductDetailHostValue;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
42
|
+
export declare function useProductDetailHost(): ProductDetailHostValue;
|
|
43
|
+
/**
|
|
44
|
+
* Components keep their `messages.products.*` (and occasional sibling-namespace)
|
|
45
|
+
* access verbatim — the only change at the call site is the hook name
|
|
46
|
+
* (`useAdminMessages` → `useProductDetailMessages`).
|
|
47
|
+
*/
|
|
48
|
+
export type ProductMessagesRoot = ProductDetailMessages;
|
|
49
|
+
export declare function useProductDetailMessages(): ProductMessagesRoot;
|
|
50
|
+
export declare function useProductDetailApi(): ProductDetailApi;
|
|
51
|
+
/** The active locale string (BCP-47), e.g. for `toLocaleString`/`Intl`. */
|
|
52
|
+
export declare function useProductLocale(): string;
|
|
53
|
+
//# sourceMappingURL=host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/host.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EAAiB,KAAK,SAAS,EAAc,MAAM,OAAO,CAAA;AAEjE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAA;AAE5E;;;;;GAKG;AAKH,MAAM,MAAM,qBAAqB,GAAG,qBAAqB,CAAA;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;IAC9C,IAAI,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;IAC/D,KAAK,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;IAChE,MAAM,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;CAClD;AAED,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;CACzC;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,GAAG,EAAE,gBAAgB,CAAA;IACrB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,uBAAuB,CAAA;IACjC,WAAW,CAAC,EAAE,yBAAyB,CAAA;IACvC,0EAA0E;IAC1E,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,EAAE,KAAK,IAAI,CAAA;IAC3D,uGAAuG;IACvG,kBAAkB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,SAAS,CAAA;CACxE;AAID,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,sBAAsB,CAAA;IAC7B,QAAQ,EAAE,SAAS,CAAA;CACpB,2CAIA;AAED,wBAAgB,oBAAoB,IAAI,sBAAsB,CAM7D;AAED;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,CAAA;AAEvD,wBAAgB,wBAAwB,IAAI,mBAAmB,CAE9D;AAED,wBAAgB,mBAAmB,IAAI,gBAAgB,CAEtD;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
const ProductDetailHostContext = createContext(null);
|
|
5
|
+
export function ProductDetailHostProvider({ value, children, }) {
|
|
6
|
+
return (_jsx(ProductDetailHostContext.Provider, { value: value, children: children }));
|
|
7
|
+
}
|
|
8
|
+
export function useProductDetailHost() {
|
|
9
|
+
const context = useContext(ProductDetailHostContext);
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error("useProductDetailHost must be used within a ProductDetailHostProvider");
|
|
12
|
+
}
|
|
13
|
+
return context;
|
|
14
|
+
}
|
|
15
|
+
export function useProductDetailMessages() {
|
|
16
|
+
return useProductDetailHost().messages;
|
|
17
|
+
}
|
|
18
|
+
export function useProductDetailApi() {
|
|
19
|
+
return useProductDetailHost().api;
|
|
20
|
+
}
|
|
21
|
+
/** The active locale string (BCP-47), e.g. for `toLocaleString`/`Intl`. */
|
|
22
|
+
export function useProductLocale() {
|
|
23
|
+
return useProductDetailHost().locale;
|
|
24
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { type ProductDetailApi, type ProductDetailBreadcrumb, ProductDetailHostProvider, type ProductDetailHostValue, type ProductDetailMessages, type ProductDetailNavigation, useProductDetailHost, } from "./host.js";
|
|
2
|
+
export { ProductDetailPage } from "./product-detail-page.js";
|
|
3
|
+
export { getChannelsQueryOptions, getProductChannelMappingsQueryOptions, getProductMediaQueryOptions, getProductRulesQueryOptions, getProductSlotsQueryOptions, } from "./product-detail-shared.js";
|
|
4
|
+
export { ProductDetailSkeleton } from "./product-detail-skeleton.js";
|
|
5
|
+
export { getPricingCategoriesQueryOptions, getProductOptionsQueryOptions, } from "./product-options-shared.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,oBAAoB,GACrB,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,EACL,uBAAuB,EACvB,qCAAqC,EACrC,2BAA2B,EAC3B,2BAA2B,EAC3B,2BAA2B,GAC5B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AACpE,OAAO,EACL,gCAAgC,EAChC,6BAA6B,GAC9B,MAAM,6BAA6B,CAAA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ProductDetailHostProvider, useProductDetailHost, } from "./host.js";
|
|
2
|
+
export { ProductDetailPage } from "./product-detail-page.js";
|
|
3
|
+
export { getChannelsQueryOptions, getProductChannelMappingsQueryOptions, getProductMediaQueryOptions, getProductRulesQueryOptions, getProductSlotsQueryOptions, } from "./product-detail-shared.js";
|
|
4
|
+
export { ProductDetailSkeleton } from "./product-detail-skeleton.js";
|
|
5
|
+
export { getPricingCategoriesQueryOptions, getProductOptionsQueryOptions, } from "./product-options-shared.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product-activity-section.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-activity-section.tsx"],"names":[],"mappings":"AAoBA,wBAAgB,sBAAsB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,2CAuE1E"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useProductActionLedger, } from "@voyantjs/products-react";
|
|
3
|
+
import { Badge, Button } from "@voyantjs/ui/components";
|
|
4
|
+
import { Activity, Loader2 } from "lucide-react";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { actionLedgerRiskVariant, actionLedgerStatusVariant, } from "../product-action-ledger-card.js";
|
|
7
|
+
import { useProductDetailMessages, useProductLocale } from "./host.js";
|
|
8
|
+
import { Section } from "./product-detail-sections.js";
|
|
9
|
+
function formatActionLedgerName(actionName) {
|
|
10
|
+
const label = actionName.replace(/^product\./, "").replace(/[._-]/g, " ");
|
|
11
|
+
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
12
|
+
}
|
|
13
|
+
export function ProductActivitySection({ productId }) {
|
|
14
|
+
const messages = useProductDetailMessages();
|
|
15
|
+
const productMessages = messages.products.core;
|
|
16
|
+
const resolvedLocale = useProductLocale();
|
|
17
|
+
const [cursor, setCursor] = useState(null);
|
|
18
|
+
const ledgerQuery = useProductActionLedger(productId, { cursor, limit: 20 });
|
|
19
|
+
const [pages, setPages] = useState([]);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const page = ledgerQuery.data;
|
|
22
|
+
if (!page)
|
|
23
|
+
return;
|
|
24
|
+
setPages((current) => {
|
|
25
|
+
if (current.some((entry) => entry.data[0]?.id === page.data[0]?.id))
|
|
26
|
+
return current;
|
|
27
|
+
return cursor ? [...current, page] : [page];
|
|
28
|
+
});
|
|
29
|
+
}, [ledgerQuery.data, cursor]);
|
|
30
|
+
const entries = pages.flatMap((page) => page.data);
|
|
31
|
+
const nextCursor = pages.at(-1)?.pageInfo.nextCursor ?? null;
|
|
32
|
+
const formatTimestamp = (iso) => new Date(iso).toLocaleString(resolvedLocale, { dateStyle: "medium", timeStyle: "short" });
|
|
33
|
+
return (_jsx(Section, { title: productMessages.activityTitle, contentClassName: "p-0", children: ledgerQuery.isPending && entries.length === 0 ? (_jsx("div", { className: "flex justify-center py-8", children: _jsx(Loader2, { className: "size-5 animate-spin text-muted-foreground", "aria-hidden": "true" }) })) : ledgerQuery.isError && entries.length === 0 ? (_jsx("p", { className: "py-8 text-center text-sm text-muted-foreground", children: productMessages.activityLoadFailed })) : entries.length === 0 ? (_jsx("p", { className: "py-8 text-center text-sm text-muted-foreground", children: productMessages.activityEmpty })) : (_jsxs("div", { className: "max-h-[420px] overflow-y-auto", children: [_jsx("ul", { className: "divide-y", children: entries.map((entry) => (_jsx(ActivityRow, { entry: entry, timestamp: formatTimestamp(entry.occurredAt) }, entry.id))) }), nextCursor ? (_jsx("div", { className: "border-t p-2", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "w-full", disabled: ledgerQuery.isFetching, onClick: () => setCursor(nextCursor), children: [ledgerQuery.isFetching ? (_jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": "true" })) : null, productMessages.activityLoadMore] }) })) : null] })) }));
|
|
34
|
+
}
|
|
35
|
+
function ActivityRow({ entry, timestamp, }) {
|
|
36
|
+
return (_jsxs("li", { className: "flex items-start gap-2.5 px-4 py-2.5", children: [_jsx(Activity, { className: "mt-0.5 size-3.5 shrink-0 text-muted-foreground", "aria-hidden": "true" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "truncate text-xs font-medium", children: formatActionLedgerName(entry.actionName) }), _jsx(Badge, { variant: actionLedgerStatusVariant[entry.status] ?? "secondary", className: "px-1.5 py-0 text-[10px]", children: entry.status.replace(/_/g, " ") }), _jsx(Badge, { variant: actionLedgerRiskVariant[entry.evaluatedRisk] ?? "outline", className: "px-1.5 py-0 text-[10px]", children: entry.evaluatedRisk })] }), _jsxs("p", { className: "mt-0.5 truncate text-[11px] text-muted-foreground", children: [entry.principalType, ":", entry.principalId, " \u00B7 ", timestamp] }), entry.mutationSummary ? (_jsx("p", { className: "mt-0.5 truncate text-[11px] text-muted-foreground", children: entry.mutationSummary })) : null] })] }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ProductDayRecord } from "@voyantjs/products-react";
|
|
2
|
+
import type { ProductMediaUploadHandler } from "../product-media-section.js";
|
|
3
|
+
export interface ProductDaySheetProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onOpenChange: (open: boolean) => void;
|
|
6
|
+
productId: string;
|
|
7
|
+
itineraryId?: string;
|
|
8
|
+
day?: ProductDayRecord;
|
|
9
|
+
nextDayNumber?: number;
|
|
10
|
+
uploadMedia?: ProductMediaUploadHandler;
|
|
11
|
+
onSuccess?: (day: ProductDayRecord) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function ProductDaySheet({ open, onOpenChange, productId, itineraryId, day, nextDayNumber, uploadMedia, onSuccess, }: ProductDaySheetProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
//# sourceMappingURL=product-day-sheet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product-day-sheet.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-day-sheet.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAqC,MAAM,0BAA0B,CAAA;AAenG,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAA;AAK5E,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,GAAG,CAAC,EAAE,gBAAgB,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,WAAW,CAAC,EAAE,yBAAyB,CAAA;IACvC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAA;CAC5C;AAED,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,WAAW,EACX,GAAG,EACH,aAAa,EACb,WAAW,EACX,SAAS,GACV,EAAE,oBAAoB,2CAkLtB"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useProduct, useProductDayMutation } from "@voyantjs/products-react";
|
|
3
|
+
import { Button, Input, Label, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, } from "@voyantjs/ui/components";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { ProductsUiMessagesProvider } from "../../i18n/index.js";
|
|
7
|
+
import { ProductDayMediaTray } from "../product-day-media-tray.js";
|
|
8
|
+
import { useProductDetailMessages, useProductLocale } from "./host.js";
|
|
9
|
+
import { DayTranslatableField, useProductDayTranslationDrafts } from "./product-day-translation.js";
|
|
10
|
+
import { ContentLanguageSwitcher, richTextHasContent } from "./product-translation-popover.js";
|
|
11
|
+
export function ProductDaySheet({ open, onOpenChange, productId, itineraryId, day, nextDayNumber, uploadMedia, onSuccess, }) {
|
|
12
|
+
const messages = useProductDetailMessages();
|
|
13
|
+
const productMessages = messages.products.core;
|
|
14
|
+
const resolvedLocale = useProductLocale();
|
|
15
|
+
const isEdit = !!day;
|
|
16
|
+
const productQuery = useProduct(productId);
|
|
17
|
+
const adminBaseLocale = resolvedLocale.split("-")[0]?.toLowerCase() || "en";
|
|
18
|
+
const defaultLanguageTag = productQuery.data?.defaultLanguageTag?.trim() || adminBaseLocale;
|
|
19
|
+
const [dayNumber, setDayNumber] = useState("1");
|
|
20
|
+
const [title, setTitle] = useState("");
|
|
21
|
+
const [description, setDescription] = useState("");
|
|
22
|
+
const [location, setLocation] = useState("");
|
|
23
|
+
const [error, setError] = useState(null);
|
|
24
|
+
const dayMutation = useProductDayMutation();
|
|
25
|
+
const translations = useProductDayTranslationDrafts(productId, day?.id ?? null);
|
|
26
|
+
const [activeLanguage, setActiveLanguage] = useState(defaultLanguageTag);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
setActiveLanguage(defaultLanguageTag);
|
|
29
|
+
}, [defaultLanguageTag]);
|
|
30
|
+
// Reset the base fields whenever the sheet opens for a different day.
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!open)
|
|
33
|
+
return;
|
|
34
|
+
setDayNumber(String(day?.dayNumber ?? nextDayNumber ?? 1));
|
|
35
|
+
setTitle(day?.title ?? "");
|
|
36
|
+
setDescription(day?.description ?? "");
|
|
37
|
+
setLocation(day?.location ?? "");
|
|
38
|
+
setError(null);
|
|
39
|
+
}, [open, day, nextDayNumber]);
|
|
40
|
+
const isSubmitting = dayMutation.create.isPending || dayMutation.update.isPending;
|
|
41
|
+
const handleSubmit = async (event) => {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
setError(null);
|
|
44
|
+
const parsedDayNumber = Number.parseInt(dayNumber || "0", 10);
|
|
45
|
+
if (!Number.isFinite(parsedDayNumber) || parsedDayNumber < 1) {
|
|
46
|
+
setError(productMessages.dayNumberMin);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const payload = {
|
|
50
|
+
dayNumber: parsedDayNumber,
|
|
51
|
+
title: title.trim() ? title.trim() : null,
|
|
52
|
+
description: richTextHasContent(description) ? description : null,
|
|
53
|
+
location: location.trim() ? location.trim() : null,
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
const savedDay = isEdit
|
|
57
|
+
? await dayMutation.update.mutateAsync({ productId, dayId: day.id, input: payload })
|
|
58
|
+
: await dayMutation.create.mutateAsync({ productId, itineraryId, ...payload });
|
|
59
|
+
await translations.persist(productId, savedDay.id, defaultLanguageTag);
|
|
60
|
+
onSuccess?.(savedDay);
|
|
61
|
+
onOpenChange(false);
|
|
62
|
+
}
|
|
63
|
+
catch (submissionError) {
|
|
64
|
+
setError(submissionError instanceof Error ? submissionError.message : productMessages.daySaveFailed);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: isEdit ? productMessages.daySheetEditTitle : productMessages.daySheetNewTitle }) }), _jsx(SheetBody, { children: _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsx(ContentLanguageSwitcher, { activeLanguage: activeLanguage, defaultLanguageTag: defaultLanguageTag, languageTags: translations.drafts.map((draft) => draft.languageTag), messages: productMessages, onSelect: setActiveLanguage, onAddLanguage: (code) => {
|
|
68
|
+
translations.addLanguage(code);
|
|
69
|
+
setActiveLanguage(code);
|
|
70
|
+
}, onRemoveLanguage: (code) => {
|
|
71
|
+
translations.removeLanguage(code);
|
|
72
|
+
if (activeLanguage === code)
|
|
73
|
+
setActiveLanguage(defaultLanguageTag);
|
|
74
|
+
} }), _jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-day-number", children: productMessages.dayNumberLabel }), _jsx(Input, { id: "product-day-number", type: "number", min: "1", required: true, value: dayNumber, onChange: (event) => setDayNumber(event.target.value) })] }), _jsx(DayTranslatableField, { field: "location", type: "text", label: productMessages.dayLocationLabel, activeLanguage: activeLanguage, defaultLanguageTag: defaultLanguageTag, base: { value: location, onChange: setLocation }, drafts: translations, messages: productMessages, placeholder: productMessages.dayLocationPlaceholder })] }), _jsx(DayTranslatableField, { field: "title", type: "text", label: productMessages.dayTitleLabel, activeLanguage: activeLanguage, defaultLanguageTag: defaultLanguageTag, base: { value: title, onChange: setTitle }, drafts: translations, messages: productMessages, placeholder: productMessages.dayTitlePlaceholder, autoFocus: true }), _jsx(DayTranslatableField, { field: "description", type: "richtext", label: productMessages.dayDescriptionLabel, activeLanguage: activeLanguage, defaultLanguageTag: defaultLanguageTag, base: { value: description, onChange: setDescription }, drafts: translations, messages: productMessages, placeholder: productMessages.dayDescriptionPlaceholder }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null, isEdit ? (_jsx("div", { className: "border-t pt-4", children: _jsx(ProductsUiMessagesProvider, { locale: resolvedLocale, children: _jsx(ProductDayMediaTray, { productId: productId, dayId: day.id, uploadMedia: uploadMedia }) }) })) : null, _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), disabled: isSubmitting, children: productMessages.cancel }), _jsxs(Button, { type: "submit", disabled: isSubmitting, children: [isSubmitting ? (_jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": "true" })) : null, isEdit ? productMessages.saveDay : productMessages.addDay] })] })] }) })] }) }));
|
|
75
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { useProductDetailMessages } from "./host.js";
|
|
2
|
+
type ProductCoreMessages = ReturnType<typeof useProductDetailMessages>["products"]["core"];
|
|
3
|
+
export type DayTranslatableField = "title" | "description" | "location";
|
|
4
|
+
export type DayTranslationDraft = {
|
|
5
|
+
id: string | null;
|
|
6
|
+
languageTag: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
location: string;
|
|
10
|
+
};
|
|
11
|
+
export interface ProductDayTranslationDrafts {
|
|
12
|
+
drafts: DayTranslationDraft[];
|
|
13
|
+
setFieldValue: (languageTag: string, field: DayTranslatableField, value: string) => void;
|
|
14
|
+
addLanguage: (languageTag: string) => void;
|
|
15
|
+
removeLanguage: (languageTag: string) => void;
|
|
16
|
+
persist: (productId: string, dayId: string, defaultLanguageTag: string) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* In-memory drafts for a day's translations. The default language's content
|
|
20
|
+
* lives in the base day columns (title/description/location all have base
|
|
21
|
+
* columns), so this only manages non-default-language translation rows.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useProductDayTranslationDrafts(productId: string | null, dayId: string | null): ProductDayTranslationDrafts;
|
|
24
|
+
export interface DayTranslatableFieldProps {
|
|
25
|
+
label: string;
|
|
26
|
+
type: "text" | "richtext";
|
|
27
|
+
field: DayTranslatableField;
|
|
28
|
+
activeLanguage: string;
|
|
29
|
+
defaultLanguageTag: string;
|
|
30
|
+
base: {
|
|
31
|
+
value: string;
|
|
32
|
+
onChange: (value: string) => void;
|
|
33
|
+
};
|
|
34
|
+
drafts: ProductDayTranslationDrafts;
|
|
35
|
+
messages: ProductCoreMessages;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
autoFocus?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare function DayTranslatableField({ label, type, field, activeLanguage, defaultLanguageTag, base, drafts, messages, placeholder, autoFocus, }: DayTranslatableFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=product-day-translation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product-day-translation.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-day-translation.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAA;AAGzD,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;AAE1F,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,aAAa,GAAG,UAAU,CAAA;AAEvE,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAqBD,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,mBAAmB,EAAE,CAAA;IAC7B,aAAa,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACxF,WAAW,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,cAAc,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7C,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACzF;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,KAAK,EAAE,MAAM,GAAG,IAAI,GACnB,2BAA2B,CA2F7B;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,UAAU,CAAA;IACzB,KAAK,EAAE,oBAAoB,CAAA;IAC3B,cAAc,EAAE,MAAM,CAAA;IACtB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAA;IAC1D,MAAM,EAAE,2BAA2B,CAAA;IACnC,QAAQ,EAAE,mBAAmB,CAAA;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,IAAI,EACJ,KAAK,EACL,cAAc,EACd,kBAAkB,EAClB,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,WAAW,EACX,SAAS,GACV,EAAE,yBAAyB,2CAmC3B"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useProductDayTranslationMutation, useProductDayTranslations, } from "@voyantjs/products-react";
|
|
3
|
+
import { Input, Label } from "@voyantjs/ui/components";
|
|
4
|
+
import { RichTextEditor } from "@voyantjs/ui/components/rich-text-editor";
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { richTextHasContent, TranslationIndicator } from "./product-translation-popover.js";
|
|
7
|
+
function recordToDraft(record) {
|
|
8
|
+
return {
|
|
9
|
+
id: record.id,
|
|
10
|
+
languageTag: record.languageTag,
|
|
11
|
+
title: record.title ?? "",
|
|
12
|
+
description: record.description ?? "",
|
|
13
|
+
location: record.location ?? "",
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function emptyDraft(languageTag) {
|
|
17
|
+
return { id: null, languageTag, title: "", description: "", location: "" };
|
|
18
|
+
}
|
|
19
|
+
function fieldHasContent(draft, field) {
|
|
20
|
+
if (field === "description")
|
|
21
|
+
return richTextHasContent(draft.description);
|
|
22
|
+
return draft[field].trim().length > 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* In-memory drafts for a day's translations. The default language's content
|
|
26
|
+
* lives in the base day columns (title/description/location all have base
|
|
27
|
+
* columns), so this only manages non-default-language translation rows.
|
|
28
|
+
*/
|
|
29
|
+
export function useProductDayTranslationDrafts(productId, dayId) {
|
|
30
|
+
const query = useProductDayTranslations(productId ?? undefined, dayId ?? undefined, {
|
|
31
|
+
enabled: !!productId && !!dayId,
|
|
32
|
+
});
|
|
33
|
+
const mutations = useProductDayTranslationMutation();
|
|
34
|
+
const [drafts, setDrafts] = useState([]);
|
|
35
|
+
const seededKey = useRef(null);
|
|
36
|
+
const existingRef = useRef([]);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const key = `${productId ?? ""}:${dayId ?? "__new__"}`;
|
|
39
|
+
if (productId && dayId && query.isPending)
|
|
40
|
+
return;
|
|
41
|
+
if (seededKey.current === key)
|
|
42
|
+
return;
|
|
43
|
+
const records = query.data?.data ?? [];
|
|
44
|
+
existingRef.current = records;
|
|
45
|
+
setDrafts(records.map(recordToDraft));
|
|
46
|
+
seededKey.current = key;
|
|
47
|
+
}, [productId, dayId, query.isPending, query.data]);
|
|
48
|
+
const setFieldValue = useCallback((languageTag, field, value) => {
|
|
49
|
+
setDrafts((prev) => {
|
|
50
|
+
if (prev.some((draft) => draft.languageTag === languageTag)) {
|
|
51
|
+
return prev.map((draft) => draft.languageTag === languageTag ? { ...draft, [field]: value } : draft);
|
|
52
|
+
}
|
|
53
|
+
return [...prev, { ...emptyDraft(languageTag), [field]: value }];
|
|
54
|
+
});
|
|
55
|
+
}, []);
|
|
56
|
+
const addLanguage = useCallback((languageTag) => {
|
|
57
|
+
setDrafts((prev) => prev.some((draft) => draft.languageTag === languageTag)
|
|
58
|
+
? prev
|
|
59
|
+
: [...prev, emptyDraft(languageTag)]);
|
|
60
|
+
}, []);
|
|
61
|
+
const removeLanguage = useCallback((languageTag) => {
|
|
62
|
+
setDrafts((prev) => prev.filter((draft) => draft.languageTag !== languageTag));
|
|
63
|
+
}, []);
|
|
64
|
+
const persist = useCallback(async (resolvedProductId, resolvedDayId, defaultLanguageTag) => {
|
|
65
|
+
const original = existingRef.current;
|
|
66
|
+
const nonDefault = drafts.filter((draft) => draft.languageTag !== defaultLanguageTag);
|
|
67
|
+
const currentLanguages = new Set(nonDefault.map((draft) => draft.languageTag));
|
|
68
|
+
const deletes = original
|
|
69
|
+
.filter((record) => !currentLanguages.has(record.languageTag))
|
|
70
|
+
.map((record) => mutations.remove.mutateAsync({
|
|
71
|
+
productId: resolvedProductId,
|
|
72
|
+
dayId: resolvedDayId,
|
|
73
|
+
translationId: record.id,
|
|
74
|
+
}));
|
|
75
|
+
const upserts = nonDefault.map((draft) => {
|
|
76
|
+
const title = draft.title.trim() ? draft.title.trim() : null;
|
|
77
|
+
const description = richTextHasContent(draft.description) ? draft.description : null;
|
|
78
|
+
const location = draft.location.trim() ? draft.location.trim() : null;
|
|
79
|
+
if (draft.id) {
|
|
80
|
+
return mutations.update.mutateAsync({
|
|
81
|
+
productId: resolvedProductId,
|
|
82
|
+
dayId: resolvedDayId,
|
|
83
|
+
translationId: draft.id,
|
|
84
|
+
input: { title, description, location },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (!title && !description && !location)
|
|
88
|
+
return Promise.resolve(null);
|
|
89
|
+
return mutations.create.mutateAsync({
|
|
90
|
+
productId: resolvedProductId,
|
|
91
|
+
dayId: resolvedDayId,
|
|
92
|
+
input: { languageTag: draft.languageTag, title, description, location },
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
await Promise.all([...deletes, ...upserts]);
|
|
96
|
+
seededKey.current = null;
|
|
97
|
+
}, [drafts, mutations]);
|
|
98
|
+
return { drafts, setFieldValue, addLanguage, removeLanguage, persist };
|
|
99
|
+
}
|
|
100
|
+
export function DayTranslatableField({ label, type, field, activeLanguage, defaultLanguageTag, base, drafts, messages, placeholder, autoFocus, }) {
|
|
101
|
+
const usesBase = activeLanguage === defaultLanguageTag;
|
|
102
|
+
const activeDraft = drafts.drafts.find((draft) => draft.languageTag === activeLanguage);
|
|
103
|
+
const value = usesBase ? base.value : (activeDraft?.[field] ?? "");
|
|
104
|
+
const handleChange = usesBase
|
|
105
|
+
? base.onChange
|
|
106
|
+
: (next) => drafts.setFieldValue(activeLanguage, field, next);
|
|
107
|
+
const translatedLanguages = drafts.drafts
|
|
108
|
+
.filter((draft) => draft.languageTag !== defaultLanguageTag && fieldHasContent(draft, field))
|
|
109
|
+
.map((draft) => draft.languageTag);
|
|
110
|
+
return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Label, { children: label }), _jsx(TranslationIndicator, { languages: translatedLanguages, messages: messages })] }), type === "richtext" ? (_jsx(RichTextEditor, { value: value, onChange: handleChange, placeholder: placeholder, editorClassName: "max-h-[320px] overflow-y-auto" })) : (_jsx(Input, { value: value, onChange: (event) => handleChange(event.target.value), placeholder: placeholder, autoFocus: autoFocus }))] }));
|
|
111
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type DepartureSlot } from "./product-departure-form.js";
|
|
2
|
+
export type { DepartureSlot };
|
|
3
|
+
type DepartureDialogProps = {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onOpenChange: (open: boolean) => void;
|
|
6
|
+
productId: string;
|
|
7
|
+
slot?: DepartureSlot;
|
|
8
|
+
onSuccess: () => void;
|
|
9
|
+
};
|
|
10
|
+
export declare function DepartureDialog({ open, onOpenChange, productId, slot, onSuccess, }: DepartureDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=product-departure-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product-departure-dialog.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-departure-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAE/E,YAAY,EAAE,aAAa,EAAE,CAAA;AAE7B,KAAK,oBAAoB,GAAG;IAC1B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,aAAa,CAAA;IACpB,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,IAAI,EACJ,SAAS,GACV,EAAE,oBAAoB,2CAwBtB"}
|
|
@@ -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 { DepartureForm } from "./product-departure-form.js";
|
|
5
|
+
export function DepartureDialog({ open, onOpenChange, productId, slot, onSuccess, }) {
|
|
6
|
+
const messages = useProductDetailMessages();
|
|
7
|
+
const departureMessages = messages.products.operations.departures;
|
|
8
|
+
const isEditing = !!slot;
|
|
9
|
+
return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: isEditing ? departureMessages.editTitle : departureMessages.newTitle }) }), _jsx(SheetBody, { children: _jsx(DepartureForm, { productId: productId, slot: slot, onSuccess: onSuccess, onCancel: () => onOpenChange(false) }) })] }) }));
|
|
10
|
+
}
|