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 && (
+
{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)
+
+ {(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