@voyantjs/products-ui 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +13 -0
  2. package/dist/components/option-unit-dialog.d.ts +11 -0
  3. package/dist/components/option-unit-dialog.d.ts.map +1 -0
  4. package/dist/components/option-unit-dialog.js +13 -0
  5. package/dist/components/option-unit-form.d.ts +17 -0
  6. package/dist/components/option-unit-form.d.ts.map +1 -0
  7. package/dist/components/option-unit-form.js +114 -0
  8. package/dist/components/product-category-combobox.d.ts +10 -0
  9. package/dist/components/product-category-combobox.d.ts.map +1 -0
  10. package/dist/components/product-category-combobox.js +45 -0
  11. package/dist/components/product-category-dialog.d.ts +9 -0
  12. package/dist/components/product-category-dialog.d.ts.map +1 -0
  13. package/dist/components/product-category-dialog.js +13 -0
  14. package/dist/components/product-category-form.d.ts +15 -0
  15. package/dist/components/product-category-form.d.ts.map +1 -0
  16. package/dist/components/product-category-form.js +74 -0
  17. package/dist/components/product-category-list.d.ts +5 -0
  18. package/dist/components/product-category-list.d.ts.map +1 -0
  19. package/dist/components/product-category-list.js +42 -0
  20. package/dist/components/product-day-dialog.d.ts +11 -0
  21. package/dist/components/product-day-dialog.d.ts.map +1 -0
  22. package/dist/components/product-day-dialog.js +13 -0
  23. package/dist/components/product-day-form.d.ts +18 -0
  24. package/dist/components/product-day-form.d.ts.map +1 -0
  25. package/dist/components/product-day-form.js +67 -0
  26. package/dist/components/product-itinerary-dialog.d.ts +16 -0
  27. package/dist/components/product-itinerary-dialog.d.ts.map +1 -0
  28. package/dist/components/product-itinerary-dialog.js +77 -0
  29. package/dist/components/product-media-dialog.d.ts +11 -0
  30. package/dist/components/product-media-dialog.d.ts.map +1 -0
  31. package/dist/components/product-media-dialog.js +13 -0
  32. package/dist/components/product-media-form.d.ts +17 -0
  33. package/dist/components/product-media-form.d.ts.map +1 -0
  34. package/dist/components/product-media-form.js +92 -0
  35. package/dist/components/product-media-section.d.ts +27 -0
  36. package/dist/components/product-media-section.d.ts.map +1 -0
  37. package/dist/components/product-media-section.js +79 -0
  38. package/dist/components/product-option-dialog.d.ts +11 -0
  39. package/dist/components/product-option-dialog.d.ts.map +1 -0
  40. package/dist/components/product-option-dialog.js +13 -0
  41. package/dist/components/product-option-form.d.ts +17 -0
  42. package/dist/components/product-option-form.d.ts.map +1 -0
  43. package/dist/components/product-option-form.js +88 -0
  44. package/dist/components/product-options-section.d.ts +11 -0
  45. package/dist/components/product-options-section.d.ts.map +1 -0
  46. package/dist/components/product-options-section.js +87 -0
  47. package/dist/components/product-tag-dialog.d.ts +9 -0
  48. package/dist/components/product-tag-dialog.d.ts.map +1 -0
  49. package/dist/components/product-tag-dialog.js +13 -0
  50. package/dist/components/product-tag-form.d.ts +15 -0
  51. package/dist/components/product-tag-form.d.ts.map +1 -0
  52. package/dist/components/product-tag-form.js +44 -0
  53. package/dist/components/product-tag-list.d.ts +5 -0
  54. package/dist/components/product-tag-list.d.ts.map +1 -0
  55. package/dist/components/product-tag-list.js +40 -0
  56. package/dist/components/product-type-combobox.d.ts +9 -0
  57. package/dist/components/product-type-combobox.d.ts.map +1 -0
  58. package/dist/components/product-type-combobox.js +44 -0
  59. package/dist/components/product-version-dialog.d.ts +8 -0
  60. package/dist/components/product-version-dialog.d.ts.map +1 -0
  61. package/dist/components/product-version-dialog.js +37 -0
  62. package/dist/components/product-versions-section.d.ts +7 -0
  63. package/dist/components/product-versions-section.d.ts.map +1 -0
  64. package/dist/components/product-versions-section.js +14 -0
  65. package/dist/index.d.ts +22 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +21 -0
  68. package/package.json +68 -0
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @voyantjs/products-ui
2
+
3
+ Importable React UI components for Voyant products. Bundler-consumed (Vite, Next.js, webpack, etc.).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @voyantjs/products-ui @voyantjs/products-react @voyantjs/voyant-ui @tanstack/react-query react react-dom
9
+ ```
10
+
11
+ `@voyantjs/voyant-ui` provides the design-system primitives. `@voyantjs/products-react` provides the data-layer hooks. Both are required peers.
12
+
13
+ All components accept a `className` prop and merge it with `cn()`. Wrap or compose to extend; use the registry copy-paste path (`npx shadcn add @voyant/...`) for components you want to fork outright.
@@ -0,0 +1,11 @@
1
+ import type { OptionUnitRecord } from "@voyantjs/products-react";
2
+ export interface OptionUnitDialogProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ optionId: string;
6
+ unit?: OptionUnitRecord;
7
+ sortOrder?: number;
8
+ onSuccess?: (unit: OptionUnitRecord) => void;
9
+ }
10
+ export declare function OptionUnitDialog({ open, onOpenChange, optionId, unit, sortOrder, onSuccess, }: OptionUnitDialogProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=option-unit-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"option-unit-dialog.d.ts","sourceRoot":"","sources":["../../src/components/option-unit-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAYhE,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,gBAAgB,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,IAAI,CAAA;CAC7C;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,IAAI,EACJ,SAAS,EACT,SAAS,GACV,EAAE,qBAAqB,2CAyBvB"}
@@ -0,0 +1,13 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@voyantjs/voyant-ui/components/dialog";
4
+ import { OptionUnitForm } from "./option-unit-form";
5
+ export function OptionUnitDialog({ open, onOpenChange, optionId, unit, sortOrder, onSuccess, }) {
6
+ const isEdit = Boolean(unit);
7
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { "data-slot": "option-unit-dialog", className: "sm:max-w-[680px]", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: isEdit ? "Edit option unit" : "New option unit" }), _jsx(DialogDescription, { children: isEdit
8
+ ? "Update unit constraints, quantity limits, and occupancy rules."
9
+ : "Create a selectable unit under this option." })] }), _jsx(OptionUnitForm, { mode: unit ? { kind: "edit", unit } : { kind: "create", optionId, sortOrder }, onSuccess: (saved) => {
10
+ onSuccess?.(saved);
11
+ onOpenChange(false);
12
+ }, onCancel: () => onOpenChange(false) })] }) }));
13
+ }
@@ -0,0 +1,17 @@
1
+ import { type OptionUnitRecord } from "@voyantjs/products-react";
2
+ type Mode = {
3
+ kind: "create";
4
+ optionId: string;
5
+ sortOrder?: number;
6
+ } | {
7
+ kind: "edit";
8
+ unit: OptionUnitRecord;
9
+ };
10
+ export interface OptionUnitFormProps {
11
+ mode: Mode;
12
+ onSuccess?: (unit: OptionUnitRecord) => void;
13
+ onCancel?: () => void;
14
+ }
15
+ export declare function OptionUnitForm({ mode, onSuccess, onCancel }: OptionUnitFormProps): import("react/jsx-runtime").JSX.Element;
16
+ export {};
17
+ //# sourceMappingURL=option-unit-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"option-unit-form.d.ts","sourceRoot":"","sources":["../../src/components/option-unit-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,gBAAgB,EAEtB,MAAM,0BAA0B,CAAA;AAgBjC,KAAK,IAAI,GACL;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,gBAAgB,CAAA;CAAE,CAAA;AAE5C,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,IAAI,CAAA;IACV,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,IAAI,CAAA;IAC5C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AA6FD,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,mBAAmB,2CAuNhF"}
@@ -0,0 +1,114 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useOptionUnitMutation, } from "@voyantjs/products-react";
4
+ import { Button } from "@voyantjs/voyant-ui/components/button";
5
+ import { Input } from "@voyantjs/voyant-ui/components/input";
6
+ import { Label } from "@voyantjs/voyant-ui/components/label";
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/voyant-ui/components/select";
8
+ import { Switch } from "@voyantjs/voyant-ui/components/switch";
9
+ import { Textarea } from "@voyantjs/voyant-ui/components/textarea";
10
+ import { Loader2 } from "lucide-react";
11
+ import * as React from "react";
12
+ const UNIT_TYPES = [
13
+ { value: "person", label: "Person" },
14
+ { value: "group", label: "Group" },
15
+ { value: "room", label: "Room" },
16
+ { value: "vehicle", label: "Vehicle" },
17
+ { value: "service", label: "Service" },
18
+ { value: "other", label: "Other" },
19
+ ];
20
+ function initialState(mode) {
21
+ if (mode.kind === "edit") {
22
+ return {
23
+ name: mode.unit.name,
24
+ code: mode.unit.code ?? "",
25
+ description: mode.unit.description ?? "",
26
+ unitType: mode.unit.unitType,
27
+ minQuantity: mode.unit.minQuantity != null ? String(mode.unit.minQuantity) : "",
28
+ maxQuantity: mode.unit.maxQuantity != null ? String(mode.unit.maxQuantity) : "",
29
+ minAge: mode.unit.minAge != null ? String(mode.unit.minAge) : "",
30
+ maxAge: mode.unit.maxAge != null ? String(mode.unit.maxAge) : "",
31
+ occupancyMin: mode.unit.occupancyMin != null ? String(mode.unit.occupancyMin) : "",
32
+ occupancyMax: mode.unit.occupancyMax != null ? String(mode.unit.occupancyMax) : "",
33
+ isRequired: mode.unit.isRequired,
34
+ isHidden: mode.unit.isHidden,
35
+ sortOrder: String(mode.unit.sortOrder),
36
+ };
37
+ }
38
+ return {
39
+ name: "",
40
+ code: "",
41
+ description: "",
42
+ unitType: "person",
43
+ minQuantity: "",
44
+ maxQuantity: "",
45
+ minAge: "",
46
+ maxAge: "",
47
+ occupancyMin: "",
48
+ occupancyMax: "",
49
+ isRequired: false,
50
+ isHidden: false,
51
+ sortOrder: String(mode.sortOrder ?? 0),
52
+ };
53
+ }
54
+ function toOptionalString(value) {
55
+ const trimmed = value.trim();
56
+ return trimmed ? trimmed : null;
57
+ }
58
+ function toOptionalNumber(value) {
59
+ const trimmed = value.trim();
60
+ if (!trimmed)
61
+ return null;
62
+ const parsed = Number.parseInt(trimmed, 10);
63
+ return Number.isFinite(parsed) ? parsed : null;
64
+ }
65
+ function toPayload(state) {
66
+ return {
67
+ name: state.name.trim(),
68
+ code: toOptionalString(state.code),
69
+ description: toOptionalString(state.description),
70
+ unitType: state.unitType,
71
+ minQuantity: toOptionalNumber(state.minQuantity),
72
+ maxQuantity: toOptionalNumber(state.maxQuantity),
73
+ minAge: toOptionalNumber(state.minAge),
74
+ maxAge: toOptionalNumber(state.maxAge),
75
+ occupancyMin: toOptionalNumber(state.occupancyMin),
76
+ occupancyMax: toOptionalNumber(state.occupancyMax),
77
+ isRequired: state.isRequired,
78
+ isHidden: state.isHidden,
79
+ sortOrder: Number.parseInt(state.sortOrder || "0", 10) || 0,
80
+ };
81
+ }
82
+ export function OptionUnitForm({ mode, onSuccess, onCancel }) {
83
+ const [state, setState] = React.useState(() => initialState(mode));
84
+ const [error, setError] = React.useState(null);
85
+ const { create, update } = useOptionUnitMutation();
86
+ React.useEffect(() => {
87
+ setState(initialState(mode));
88
+ setError(null);
89
+ }, [mode]);
90
+ const isSubmitting = create.isPending || update.isPending;
91
+ const field = (key) => (value) => {
92
+ setState((prev) => ({ ...prev, [key]: value }));
93
+ };
94
+ const handleSubmit = async (event) => {
95
+ event.preventDefault();
96
+ setError(null);
97
+ if (!state.name.trim()) {
98
+ setError("Unit name is required.");
99
+ return;
100
+ }
101
+ try {
102
+ const unit = mode.kind === "create"
103
+ ? await create.mutateAsync({ optionId: mode.optionId, ...toPayload(state) })
104
+ : await update.mutateAsync({ id: mode.unit.id, input: toPayload(state) });
105
+ onSuccess?.(unit);
106
+ }
107
+ catch (err) {
108
+ setError(err instanceof Error ? err.message : "Failed to save option unit.");
109
+ }
110
+ };
111
+ const showAgeRange = state.unitType === "person";
112
+ const showOccupancy = state.unitType === "group" || state.unitType === "room" || state.unitType === "vehicle";
113
+ return (_jsxs("form", { "data-slot": "option-unit-form", onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_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: "option-unit-name", children: "Name" }), _jsx(Input, { id: "option-unit-name", required: true, autoFocus: true, value: state.name, onChange: (event) => field("name")(event.target.value), placeholder: "Adult" })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "option-unit-code", children: "Code" }), _jsx(Input, { id: "option-unit-code", value: state.code, onChange: (event) => field("code")(event.target.value), placeholder: "adult" })] })] }), _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, { children: "Unit type" }), _jsxs(Select, { items: UNIT_TYPES, value: state.unitType, onValueChange: (value) => value && field("unitType")(value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: UNIT_TYPES.map((type) => (_jsx(SelectItem, { value: type.value, children: type.label }, type.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "option-unit-sort-order", children: "Sort order" }), _jsx(Input, { id: "option-unit-sort-order", type: "number", value: state.sortOrder, onChange: (event) => field("sortOrder")(event.target.value) })] })] }), _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: "option-unit-min-quantity", children: "Min quantity" }), _jsx(Input, { id: "option-unit-min-quantity", type: "number", min: "0", value: state.minQuantity, onChange: (event) => field("minQuantity")(event.target.value) })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "option-unit-max-quantity", children: "Max quantity" }), _jsx(Input, { id: "option-unit-max-quantity", type: "number", min: "0", value: state.maxQuantity, onChange: (event) => field("maxQuantity")(event.target.value) })] })] }), showAgeRange ? (_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: "option-unit-min-age", children: "Min age" }), _jsx(Input, { id: "option-unit-min-age", type: "number", min: "0", value: state.minAge, onChange: (event) => field("minAge")(event.target.value) })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "option-unit-max-age", children: "Max age" }), _jsx(Input, { id: "option-unit-max-age", type: "number", min: "0", value: state.maxAge, onChange: (event) => field("maxAge")(event.target.value) })] })] })) : null, showOccupancy ? (_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: "option-unit-occupancy-min", children: "Occupancy min" }), _jsx(Input, { id: "option-unit-occupancy-min", type: "number", min: "0", value: state.occupancyMin, onChange: (event) => field("occupancyMin")(event.target.value) })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "option-unit-occupancy-max", children: "Occupancy max" }), _jsx(Input, { id: "option-unit-occupancy-max", type: "number", min: "0", value: state.occupancyMax, onChange: (event) => field("occupancyMax")(event.target.value) })] })] })) : null, _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "option-unit-description", children: "Description" }), _jsx(Textarea, { id: "option-unit-description", value: state.description, onChange: (event) => field("description")(event.target.value), placeholder: "Optional unit description" })] }), _jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: state.isRequired, onCheckedChange: (checked) => field("isRequired")(checked) }), _jsx(Label, { children: "Required" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: state.isHidden, onCheckedChange: (checked) => field("isHidden")(checked) }), _jsx(Label, { children: "Hidden" })] })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null, _jsxs("div", { className: "flex items-center justify-end gap-2", children: [onCancel ? (_jsx(Button, { type: "button", variant: "ghost", onClick: onCancel, disabled: isSubmitting, children: "Cancel" })) : null, _jsxs(Button, { type: "submit", disabled: isSubmitting, children: [isSubmitting ? (_jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": "true" })) : null, mode.kind === "create" ? "Create unit" : "Save changes"] })] })] }));
114
+ }
@@ -0,0 +1,10 @@
1
+ type Props = {
2
+ value: string | null | undefined;
3
+ onChange: (value: string | null) => void;
4
+ placeholder?: string;
5
+ disabled?: boolean;
6
+ excludeId?: string | null;
7
+ };
8
+ export declare function ProductCategoryCombobox({ value, onChange, placeholder, disabled, excludeId, }: Props): import("react/jsx-runtime").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=product-category-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-category-combobox.d.ts","sourceRoot":"","sources":["../../src/components/product-category-combobox.tsx"],"names":[],"mappings":"AAgBA,KAAK,KAAK,GAAG;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,CAAA;AAID,wBAAgB,uBAAuB,CAAC,EACtC,KAAK,EACL,QAAQ,EACR,WAAuC,EACvC,QAAQ,EACR,SAAS,GACV,EAAE,KAAK,2CAsEP"}
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useProductCategories, useProductCategory, } from "@voyantjs/products-react";
3
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/voyant-ui/components/combobox";
4
+ import * as React from "react";
5
+ const PAGE_SIZE = 25;
6
+ export function ProductCategoryCombobox({ value, onChange, placeholder = "Search parent category…", disabled, excludeId, }) {
7
+ const [search, setSearch] = React.useState("");
8
+ const listQuery = useProductCategories({ search: search || undefined, limit: PAGE_SIZE });
9
+ const selectedQuery = useProductCategory(value, { enabled: !!value });
10
+ const items = React.useMemo(() => {
11
+ const map = new Map();
12
+ for (const item of listQuery.data?.data ?? []) {
13
+ if (item.id !== excludeId)
14
+ map.set(item.id, item);
15
+ }
16
+ if (selectedQuery.data && selectedQuery.data.id !== excludeId) {
17
+ map.set(selectedQuery.data.id, selectedQuery.data);
18
+ }
19
+ return Array.from(map.values());
20
+ }, [excludeId, listQuery.data?.data, selectedQuery.data]);
21
+ const itemMap = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
22
+ const selected = value ? itemMap.get(value) : undefined;
23
+ const selectedLabel = selected ? selected.name : "";
24
+ const [inputValue, setInputValue] = React.useState(selectedLabel);
25
+ React.useEffect(() => {
26
+ setInputValue(selectedLabel);
27
+ }, [selectedLabel]);
28
+ return (_jsxs(Combobox, { items: items.map((item) => item.id), value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringValue: (id) => itemMap.get(id)?.name ?? "", onInputValueChange: (next) => {
29
+ setInputValue(next);
30
+ setSearch(next);
31
+ if (!next)
32
+ onChange(null);
33
+ }, onValueChange: (next) => {
34
+ const id = next ?? null;
35
+ onChange(id);
36
+ setInputValue(id ? (itemMap.get(id)?.name ?? "") : "");
37
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: !!value }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: listQuery.isPending || selectedQuery.isPending
38
+ ? "Loading…"
39
+ : "No product categories found." }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
40
+ const item = itemMap.get(id);
41
+ if (!item)
42
+ return null;
43
+ return (_jsx(ComboboxItem, { value: item.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: item.name }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: item.slug })] }) }, item.id));
44
+ } }) })] })] }));
45
+ }
@@ -0,0 +1,9 @@
1
+ import type { ProductCategoryRecord } from "@voyantjs/products-react";
2
+ export interface ProductCategoryDialogProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ category?: ProductCategoryRecord;
6
+ onSuccess?: (category: ProductCategoryRecord) => void;
7
+ }
8
+ export declare function ProductCategoryDialog({ open, onOpenChange, category, onSuccess, }: ProductCategoryDialogProps): import("react/jsx-runtime").JSX.Element;
9
+ //# sourceMappingURL=product-category-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-category-dialog.d.ts","sourceRoot":"","sources":["../../src/components/product-category-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAA;AAYrE,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,QAAQ,CAAC,EAAE,qBAAqB,CAAA;IAChC,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,qBAAqB,KAAK,IAAI,CAAA;CACtD;AAED,wBAAgB,qBAAqB,CAAC,EACpC,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,SAAS,GACV,EAAE,0BAA0B,2CAyB5B"}
@@ -0,0 +1,13 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@voyantjs/voyant-ui/components/dialog";
4
+ import { ProductCategoryForm } from "./product-category-form";
5
+ export function ProductCategoryDialog({ open, onOpenChange, category, onSuccess, }) {
6
+ const isEdit = Boolean(category);
7
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { "data-slot": "product-category-dialog", className: "sm:max-w-[640px]", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: isEdit ? "Edit product category" : "New product category" }), _jsx(DialogDescription, { children: isEdit
8
+ ? "Update category hierarchy, slug, and active state."
9
+ : "Create a category for organizing your product catalog." })] }), _jsx(ProductCategoryForm, { mode: category ? { kind: "edit", category } : { kind: "create" }, onSuccess: (saved) => {
10
+ onSuccess?.(saved);
11
+ onOpenChange(false);
12
+ }, onCancel: () => onOpenChange(false) })] }) }));
13
+ }
@@ -0,0 +1,15 @@
1
+ import { type ProductCategoryRecord } from "@voyantjs/products-react";
2
+ type Mode = {
3
+ kind: "create";
4
+ } | {
5
+ kind: "edit";
6
+ category: ProductCategoryRecord;
7
+ };
8
+ export interface ProductCategoryFormProps {
9
+ mode: Mode;
10
+ onSuccess?: (category: ProductCategoryRecord) => void;
11
+ onCancel?: () => void;
12
+ }
13
+ export declare function ProductCategoryForm({ mode, onSuccess, onCancel }: ProductCategoryFormProps): import("react/jsx-runtime").JSX.Element;
14
+ export {};
15
+ //# sourceMappingURL=product-category-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-category-form.d.ts","sourceRoot":"","sources":["../../src/components/product-category-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,qBAAqB,EAE3B,MAAM,0BAA0B,CAAA;AAUjC,KAAK,IAAI,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,qBAAqB,CAAA;CAAE,CAAA;AAElF,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,IAAI,CAAA;IACV,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,qBAAqB,KAAK,IAAI,CAAA;IACrD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AA6CD,wBAAgB,mBAAmB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,wBAAwB,2CAyH1F"}
@@ -0,0 +1,74 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useProductCategoryMutation, } from "@voyantjs/products-react";
4
+ import { Button } from "@voyantjs/voyant-ui/components/button";
5
+ import { Input } from "@voyantjs/voyant-ui/components/input";
6
+ import { Label } from "@voyantjs/voyant-ui/components/label";
7
+ import { Switch } from "@voyantjs/voyant-ui/components/switch";
8
+ import { Textarea } from "@voyantjs/voyant-ui/components/textarea";
9
+ import { Loader2 } from "lucide-react";
10
+ import * as React from "react";
11
+ import { ProductCategoryCombobox } from "./product-category-combobox";
12
+ function initialState(mode) {
13
+ if (mode.kind === "edit") {
14
+ const category = mode.category;
15
+ return {
16
+ name: category.name,
17
+ slug: category.slug,
18
+ parentId: category.parentId ?? "__none__",
19
+ description: category.description ?? "",
20
+ sortOrder: String(category.sortOrder),
21
+ active: category.active,
22
+ };
23
+ }
24
+ return {
25
+ name: "",
26
+ slug: "",
27
+ parentId: "__none__",
28
+ description: "",
29
+ sortOrder: "0",
30
+ active: true,
31
+ };
32
+ }
33
+ function toPayload(state) {
34
+ return {
35
+ name: state.name.trim(),
36
+ slug: state.slug.trim(),
37
+ parentId: state.parentId === "__none__" ? null : state.parentId,
38
+ description: state.description.trim() || null,
39
+ sortOrder: Number(state.sortOrder) || 0,
40
+ active: state.active,
41
+ };
42
+ }
43
+ export function ProductCategoryForm({ mode, onSuccess, onCancel }) {
44
+ const [state, setState] = React.useState(() => initialState(mode));
45
+ const [error, setError] = React.useState(null);
46
+ const { create, update } = useProductCategoryMutation();
47
+ React.useEffect(() => {
48
+ setState(initialState(mode));
49
+ setError(null);
50
+ }, [mode]);
51
+ const isSubmitting = create.isPending || update.isPending;
52
+ const handleSubmit = async (event) => {
53
+ event.preventDefault();
54
+ setError(null);
55
+ if (!state.name.trim()) {
56
+ setError("Category name is required.");
57
+ return;
58
+ }
59
+ if (!state.slug.trim()) {
60
+ setError("Category slug is required.");
61
+ return;
62
+ }
63
+ try {
64
+ const category = mode.kind === "create"
65
+ ? await create.mutateAsync(toPayload(state))
66
+ : await update.mutateAsync({ id: mode.category.id, input: toPayload(state) });
67
+ onSuccess?.(category);
68
+ }
69
+ catch (err) {
70
+ setError(err instanceof Error ? err.message : "Failed to save product category.");
71
+ }
72
+ };
73
+ return (_jsxs("form", { "data-slot": "product-category-form", onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-category-name", children: "Name" }), _jsx(Input, { id: "product-category-name", required: true, autoFocus: true, value: state.name, onChange: (event) => setState((prev) => ({ ...prev, name: event.target.value })), placeholder: "Adventure" })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-category-slug", children: "Slug" }), _jsx(Input, { id: "product-category-slug", required: true, value: state.slug, onChange: (event) => setState((prev) => ({ ...prev, slug: event.target.value })), placeholder: "adventure" })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: "Parent category" }), _jsx(ProductCategoryCombobox, { value: state.parentId === "__none__" ? null : state.parentId, onChange: (value) => setState((prev) => ({ ...prev, parentId: value ?? "__none__" })), excludeId: mode.kind === "edit" ? mode.category.id : null, placeholder: "Search parent category\u2026" })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-category-description", children: "Description" }), _jsx(Textarea, { id: "product-category-description", value: state.description, onChange: (event) => setState((prev) => ({ ...prev, description: event.target.value })), placeholder: "Category description..." })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-category-sort-order", children: "Sort order" }), _jsx(Input, { id: "product-category-sort-order", type: "number", value: state.sortOrder, onChange: (event) => setState((prev) => ({ ...prev, sortOrder: event.target.value })) })] }), _jsxs("div", { className: "flex items-center gap-2 pt-7", children: [_jsx(Switch, { checked: state.active, onCheckedChange: (active) => setState((prev) => ({ ...prev, active })) }), _jsx(Label, { children: "Active" })] })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null, _jsxs("div", { className: "flex items-center justify-end gap-2", children: [onCancel ? (_jsx(Button, { type: "button", variant: "ghost", onClick: onCancel, children: "Cancel" })) : null, _jsxs(Button, { type: "submit", disabled: isSubmitting, children: [isSubmitting ? (_jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": "true" })) : null, mode.kind === "edit" ? "Save changes" : "Create category"] })] })] }));
74
+ }
@@ -0,0 +1,5 @@
1
+ export interface ProductCategoryListProps {
2
+ pageSize?: number;
3
+ }
4
+ export declare function ProductCategoryList({ pageSize }?: ProductCategoryListProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=product-category-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-category-list.d.ts","sourceRoot":"","sources":["../../src/components/product-category-list.tsx"],"names":[],"mappings":"AA8BA,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,mBAAmB,CAAC,EAAE,QAAc,EAAE,GAAE,wBAA6B,2CA+JpF"}
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useProductCategories, useProductCategoryMutation, } from "@voyantjs/products-react";
4
+ import { Badge } from "@voyantjs/voyant-ui/components/badge";
5
+ import { Button } from "@voyantjs/voyant-ui/components/button";
6
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@voyantjs/voyant-ui/components/dropdown-menu";
7
+ import { Input } from "@voyantjs/voyant-ui/components/input";
8
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/voyant-ui/components/table";
9
+ import { CheckCircle2, Loader2, MoreHorizontal, Pencil, Plus, Search, Trash2 } from "lucide-react";
10
+ import * as React from "react";
11
+ import { ProductCategoryDialog } from "./product-category-dialog";
12
+ export function ProductCategoryList({ pageSize = 200 } = {}) {
13
+ const [search, setSearch] = React.useState("");
14
+ const [offset, setOffset] = React.useState(0);
15
+ const [dialogOpen, setDialogOpen] = React.useState(false);
16
+ const [editing, setEditing] = React.useState(undefined);
17
+ const { data, isPending, isError } = useProductCategories({
18
+ search: search || undefined,
19
+ limit: pageSize,
20
+ offset,
21
+ });
22
+ const { remove } = useProductCategoryMutation();
23
+ const categories = data?.data ?? [];
24
+ const total = data?.total ?? 0;
25
+ const page = Math.floor(offset / pageSize) + 1;
26
+ const pageCount = Math.max(1, Math.ceil(total / pageSize));
27
+ const categoryById = new Map(categories.map((category) => [category.id, category]));
28
+ return (_jsxs("div", { "data-slot": "product-category-list", className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { className: "relative w-full max-w-sm", children: [_jsx(Search, { className: "absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: "Search product categories\u2026", value: search, onChange: (event) => {
29
+ setSearch(event.target.value);
30
+ setOffset(0);
31
+ }, className: "pl-9" })] }), _jsxs(Button, { onClick: () => {
32
+ setEditing(undefined);
33
+ setDialogOpen(true);
34
+ }, children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), "Add category"] })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Name" }), _jsx(TableHead, { children: "Slug" }), _jsx(TableHead, { children: "Parent" }), _jsx(TableHead, { children: "Status" }), _jsx(TableHead, { className: "w-[80px] text-right", children: "Actions" })] }) }), _jsx(TableBody, { children: isPending ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "h-24 text-center", children: _jsx(Loader2, { className: "mx-auto size-4 animate-spin text-muted-foreground" }) }) })) : isError ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "h-24 text-center text-sm text-destructive", children: "Failed to load product categories." }) })) : categories.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "h-24 text-center text-sm text-muted-foreground", children: "No product categories found." }) })) : (categories.map((category) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: category.name }), _jsx(TableCell, { children: category.slug }), _jsx(TableCell, { children: category.parentId ? (categoryById.get(category.parentId)?.name ?? "—") : "—" }), _jsx(TableCell, { children: category.active ? (_jsxs(Badge, { variant: "default", className: "gap-1", children: [_jsx(CheckCircle2, { className: "size-3.5" }), "Active"] })) : (_jsx(Badge, { variant: "secondary", children: "Inactive" })) }), _jsx(TableCell, { className: "text-right", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { className: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground outline-hidden hover:bg-accent hover:text-accent-foreground", children: _jsx(MoreHorizontal, { className: "size-4" }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => {
35
+ setEditing(category);
36
+ setDialogOpen(true);
37
+ }, children: [_jsx(Pencil, { className: "size-4" }), "Edit"] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", onClick: () => {
38
+ if (confirm("Delete this product category?")) {
39
+ remove.mutate(category.id);
40
+ }
41
+ }, children: [_jsx(Trash2, { className: "size-4" }), "Delete"] })] })] }) })] }, category.id)))) })] }) }), _jsxs("div", { className: "flex items-center justify-between text-sm text-muted-foreground", children: [_jsxs("span", { children: ["Showing ", categories.length, " of ", total] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: offset === 0, onClick: () => setOffset((prev) => Math.max(0, prev - pageSize)), children: "Previous" }), _jsxs("span", { children: ["Page ", page, " / ", pageCount] }), _jsx(Button, { variant: "outline", size: "sm", disabled: offset + pageSize >= total, onClick: () => setOffset((prev) => prev + pageSize), children: "Next" })] })] }), _jsx(ProductCategoryDialog, { open: dialogOpen, onOpenChange: setDialogOpen, category: editing })] }));
42
+ }
@@ -0,0 +1,11 @@
1
+ import type { ProductDayRecord } from "@voyantjs/products-react";
2
+ export interface ProductDayDialogProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ productId: string;
6
+ day?: ProductDayRecord;
7
+ nextDayNumber?: number;
8
+ onSuccess?: (day: ProductDayRecord) => void;
9
+ }
10
+ export declare function ProductDayDialog({ open, onOpenChange, productId, day, nextDayNumber, onSuccess, }: ProductDayDialogProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=product-day-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-day-dialog.d.ts","sourceRoot":"","sources":["../../src/components/product-day-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAYhE,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,gBAAgB,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAA;CAC5C;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,GAAG,EACH,aAAa,EACb,SAAS,GACV,EAAE,qBAAqB,2CA2BvB"}
@@ -0,0 +1,13 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@voyantjs/voyant-ui/components/dialog";
4
+ import { ProductDayForm } from "./product-day-form";
5
+ export function ProductDayDialog({ open, onOpenChange, productId, day, nextDayNumber, onSuccess, }) {
6
+ const isEdit = Boolean(day);
7
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { "data-slot": "product-day-dialog", className: "sm:max-w-[640px]", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: isEdit ? "Edit itinerary day" : "Add itinerary day" }), _jsx(DialogDescription, { children: isEdit
8
+ ? "Update the title, location, and overview for this day."
9
+ : "Create a structured day in the product itinerary." })] }), _jsx(ProductDayForm, { mode: day ? { kind: "edit", productId, day } : { kind: "create", productId, nextDayNumber }, onSuccess: (savedDay) => {
10
+ onSuccess?.(savedDay);
11
+ onOpenChange(false);
12
+ }, onCancel: () => onOpenChange(false) })] }) }));
13
+ }
@@ -0,0 +1,18 @@
1
+ import { type ProductDayRecord } from "@voyantjs/products-react";
2
+ type Mode = {
3
+ kind: "create";
4
+ productId: string;
5
+ nextDayNumber?: number;
6
+ } | {
7
+ kind: "edit";
8
+ productId: string;
9
+ day: ProductDayRecord;
10
+ };
11
+ export interface ProductDayFormProps {
12
+ mode: Mode;
13
+ onSuccess?: (day: ProductDayRecord) => void;
14
+ onCancel?: () => void;
15
+ }
16
+ export declare function ProductDayForm({ mode, onSuccess, onCancel }: ProductDayFormProps): import("react/jsx-runtime").JSX.Element;
17
+ export {};
18
+ //# sourceMappingURL=product-day-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-day-form.d.ts","sourceRoot":"","sources":["../../src/components/product-day-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,gBAAgB,EAAyB,MAAM,0BAA0B,CAAA;AAQvF,KAAK,IAAI,GACL;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,gBAAgB,CAAA;CAAE,CAAA;AAE9D,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,IAAI,CAAA;IACV,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAA;IAC3C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AA2BD,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,mBAAmB,2CAiHhF"}
@@ -0,0 +1,67 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useProductDayMutation } from "@voyantjs/products-react";
4
+ import { Button } from "@voyantjs/voyant-ui/components/button";
5
+ import { Input } from "@voyantjs/voyant-ui/components/input";
6
+ import { Label } from "@voyantjs/voyant-ui/components/label";
7
+ import { Textarea } from "@voyantjs/voyant-ui/components/textarea";
8
+ import { Loader2 } from "lucide-react";
9
+ import * as React from "react";
10
+ function initialState(mode) {
11
+ if (mode.kind === "edit") {
12
+ return {
13
+ dayNumber: String(mode.day.dayNumber),
14
+ title: mode.day.title ?? "",
15
+ description: mode.day.description ?? "",
16
+ location: mode.day.location ?? "",
17
+ };
18
+ }
19
+ return {
20
+ dayNumber: String(mode.nextDayNumber ?? 1),
21
+ title: "",
22
+ description: "",
23
+ location: "",
24
+ };
25
+ }
26
+ export function ProductDayForm({ mode, onSuccess, onCancel }) {
27
+ const [state, setState] = React.useState(() => initialState(mode));
28
+ const [error, setError] = React.useState(null);
29
+ const { create, update } = useProductDayMutation();
30
+ React.useEffect(() => {
31
+ setState(initialState(mode));
32
+ setError(null);
33
+ }, [mode]);
34
+ const isSubmitting = create.isPending || update.isPending;
35
+ const field = (key) => (value) => {
36
+ setState((previous) => ({ ...previous, [key]: value }));
37
+ };
38
+ const handleSubmit = async (event) => {
39
+ event.preventDefault();
40
+ setError(null);
41
+ const dayNumber = Number.parseInt(state.dayNumber || "0", 10);
42
+ if (!Number.isFinite(dayNumber) || dayNumber < 1) {
43
+ setError("Day number must be at least 1.");
44
+ return;
45
+ }
46
+ const payload = {
47
+ dayNumber,
48
+ title: state.title.trim() ? state.title.trim() : null,
49
+ description: state.description.trim() ? state.description.trim() : null,
50
+ location: state.location.trim() ? state.location.trim() : null,
51
+ };
52
+ try {
53
+ const day = mode.kind === "create"
54
+ ? await create.mutateAsync({ productId: mode.productId, ...payload })
55
+ : await update.mutateAsync({
56
+ productId: mode.productId,
57
+ dayId: mode.day.id,
58
+ input: payload,
59
+ });
60
+ onSuccess?.(day);
61
+ }
62
+ catch (submissionError) {
63
+ setError(submissionError instanceof Error ? submissionError.message : "Failed to save day.");
64
+ }
65
+ };
66
+ return (_jsxs("form", { "data-slot": "product-day-form", onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_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: "Day number" }), _jsx(Input, { id: "product-day-number", type: "number", min: "1", autoFocus: true, required: true, value: state.dayNumber, onChange: (event) => field("dayNumber")(event.target.value) })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-day-location", children: "Location" }), _jsx(Input, { id: "product-day-location", value: state.location, onChange: (event) => field("location")(event.target.value), placeholder: "Dubrovnik" })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-day-title", children: "Title" }), _jsx(Input, { id: "product-day-title", value: state.title, onChange: (event) => field("title")(event.target.value), placeholder: "Arrival in Dubrovnik" })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-day-description", children: "Description" }), _jsx(Textarea, { id: "product-day-description", value: state.description, onChange: (event) => field("description")(event.target.value), placeholder: "Overview and activities for this day" })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null, _jsxs("div", { className: "flex items-center justify-end gap-2", children: [onCancel ? (_jsx(Button, { type: "button", variant: "ghost", onClick: onCancel, disabled: isSubmitting, children: "Cancel" })) : null, _jsxs(Button, { type: "submit", disabled: isSubmitting, children: [isSubmitting ? (_jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": "true" })) : null, mode.kind === "create" ? "Add day" : "Save day"] })] })] }));
67
+ }
@@ -0,0 +1,16 @@
1
+ type ItineraryData = {
2
+ id: string;
3
+ name: string;
4
+ isDefault: boolean;
5
+ };
6
+ export interface ProductItineraryDialogProps {
7
+ open: boolean;
8
+ onOpenChange: (open: boolean) => void;
9
+ productId: string;
10
+ itinerary?: ItineraryData;
11
+ itineraryCount: number;
12
+ onSuccess?: (itineraryId: string) => void;
13
+ }
14
+ export declare function ProductItineraryDialog({ open, onOpenChange, productId, itinerary, itineraryCount, onSuccess, }: ProductItineraryDialogProps): import("react/jsx-runtime").JSX.Element;
15
+ export {};
16
+ //# sourceMappingURL=product-itinerary-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-itinerary-dialog.d.ts","sourceRoot":"","sources":["../../src/components/product-itinerary-dialog.tsx"],"names":[],"mappings":"AAiBA,KAAK,aAAa,GAAG;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,aAAa,CAAA;IACzB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;CAC1C;AAED,wBAAgB,sBAAsB,CAAC,EACrC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,EACT,cAAc,EACd,SAAS,GACV,EAAE,2BAA2B,2CA4H7B"}