From 0b748adfaa076b0229dfc9966511a61e1c23d154 Mon Sep 17 00:00:00 2001 From: default Date: Thu, 4 Jun 2026 20:20:09 +0000 Subject: [PATCH] Add seasonal availability dates to product form modal - Add available_from and available_until fields to ProductFormValues type - Add date picker inputs for seasonal availability in ProductFormModal - Wire availability dates through ProductsClient to modal initial values - Create migration 149 for database columns (already applied) --- src/components/admin/ElegantModal.tsx | 84 +++++++++++++++++++ src/components/admin/ProductFormModal.tsx | 45 ++++++++++ src/components/admin/ProductsClient.tsx | 2 + .../149_product_availability_dates.sql | 9 ++ 4 files changed, 140 insertions(+) create mode 100644 src/components/admin/ElegantModal.tsx create mode 100644 supabase/migrations/149_product_availability_dates.sql diff --git a/src/components/admin/ElegantModal.tsx b/src/components/admin/ElegantModal.tsx new file mode 100644 index 0000000..269a1f6 --- /dev/null +++ b/src/components/admin/ElegantModal.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useEffect } from "react"; + +type Props = { + title: string; + titleIcon?: React.ReactNode; + subtitle?: string; + onClose: () => void; + children: React.ReactNode; + maxWidth?: string; + variant?: "default" | "elegant"; +}; + +export default function ElegantModal({ title, titleIcon, subtitle, onClose, children, maxWidth = "max-w-lg", variant = "default" }: Props) { + useEffect(() => { + document.body.style.overflow = "hidden"; + return () => { document.body.style.overflow = ""; }; + }, []); + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", handleEscape); + return () => window.removeEventListener("keydown", handleEscape); + }, [onClose]); + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) onClose(); + }; + + const isElegant = variant === "elegant"; + + return ( +
+
+
+ +
+
+ {titleIcon && ( +
+ {titleIcon} +
+ )} +
+ {isElegant ? ( +

+ {title} +

+ ) : ( +

+ {title} +

+ )} + {subtitle && ( + + )} +
+
+ +
+ +
+ {children} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/admin/ProductFormModal.tsx b/src/components/admin/ProductFormModal.tsx index 09ac4f4..0e4fbf0 100644 --- a/src/components/admin/ProductFormModal.tsx +++ b/src/components/admin/ProductFormModal.tsx @@ -32,6 +32,8 @@ export type ProductFormValues = { active: boolean; is_taxable: boolean; image_url: string | null; + available_from?: string | null; + available_until?: string | null; }; const TYPE_OPTIONS: Array<{ @@ -99,6 +101,8 @@ export default function ProductFormModal({ ); const [active, setActive] = useState(initial?.active ?? true); const [isTaxable, setIsTaxable] = useState(initial?.is_taxable ?? true); + const [availableFrom, setAvailableFrom] = useState(initial?.available_from ? initial.available_from.split('T')[0] : ""); + const [availableUntil, setAvailableUntil] = useState(initial?.available_until ? initial.available_until.split('T')[0] : ""); // Image state const [imagePreview, setImagePreview] = useState(initialImageUrl); @@ -127,6 +131,8 @@ export default function ProductFormModal({ setBrandId(initial?.brand_id ?? lockedBrandId ?? brands[0]?.id ?? ""); setActive(initial?.active ?? true); setIsTaxable(initial?.is_taxable ?? true); + setAvailableFrom(initial?.available_from ? initial.available_from.split('T')[0] : ""); + setAvailableUntil(initial?.available_until ? initial.available_until.split('T')[0] : ""); setImagePreview(initialImageUrl); setImageUrl(initialImageUrl); setUploading(false); @@ -211,6 +217,8 @@ export default function ProductFormModal({ active, is_taxable: isTaxable, image_url: imageUrl, + available_from: availableFrom ? new Date(availableFrom).toISOString() : null, + available_until: availableUntil ? new Date(availableUntil).toISOString() : null, }); setSaving(false); @@ -611,6 +619,43 @@ export default function ProductFormModal({ Active products appear in the storefront. Taxable items add sales tax at checkout.

+ + {/* Seasonal Availability */} +
+ Seasonal Availability +

Set when this product is available (e.g., mid-July through mid-September)

+
+
+ + setAvailableFrom(e.target.value)} + className="atelier-input" + /> +
+
+ + setAvailableUntil(e.target.value)} + className="atelier-input" + /> +
+
+ {(availableFrom || availableUntil) && ( + + )} +
diff --git a/src/components/admin/ProductsClient.tsx b/src/components/admin/ProductsClient.tsx index 0214c3b..6d1ba99 100644 --- a/src/components/admin/ProductsClient.tsx +++ b/src/components/admin/ProductsClient.tsx @@ -351,6 +351,8 @@ export default function ProductsClient({ brand_id: editingProduct.brand_id, active: editingProduct.active, is_taxable: editingProduct.is_taxable, + available_from: editingProduct.available_from ?? null, + available_until: editingProduct.available_until ?? null, } : undefined } diff --git a/supabase/migrations/149_product_availability_dates.sql b/supabase/migrations/149_product_availability_dates.sql new file mode 100644 index 0000000..5948590 --- /dev/null +++ b/supabase/migrations/149_product_availability_dates.sql @@ -0,0 +1,9 @@ +-- Add seasonal availability date fields to products table +-- Allows products to have start/end availability dates (e.g., "Available mid-July through mid-September") + +ALTER TABLE products ADD COLUMN IF NOT EXISTS available_from TIMESTAMPTZ; +ALTER TABLE products ADD COLUMN IF NOT EXISTS available_until TIMESTAMPTZ; + +-- Add comment for documentation +COMMENT ON COLUMN products.available_from IS 'Start date for product availability (seasonal)'; +COMMENT ON COLUMN products.available_until IS 'End date for product availability (seasonal)'; \ No newline at end of file