"use client"; import { useState, useCallback, useRef, useTransition } from "react"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { createProduct } from "@/actions/products/create-product"; import { updateProduct } from "@/actions/products/update-product"; import { deleteProduct } from "@/actions/products"; import { uploadProductImage } from "@/actions/products/upload-image"; import GlassModal from "@/components/admin/GlassModal"; import { PageHeader, AdminButton, AdminIconButton, AdminSearchInput, AdminFilterTabs, AdminViewModeTabs, useToast, Skeleton } from "@/components/admin/design-system"; type Product = { id: string; name: string; description: string; price: number; type: string; active: boolean; image_url?: string | null; brand_id: string; is_taxable: boolean; }; type FormData = { name: string; description: string; price: string; type: string; active: boolean; image_url: string; is_taxable: boolean; }; type ViewMode = "table" | "cards"; // Icons const Icons = { search: (className: string) => ( ), plus: (className: string) => ( ), package: (className: string) => ( ), upload: (className: string) => ( ), }; // Page header icon const PackageIconHeader = () => ( ); export default function ProductsClient({ products, brandId }: { products: Product[]; brandId?: string }) { const router = useRouter(); const { success: showSuccess, error: showError } = useToast(); const [, startTransition] = useTransition(); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState<"all" | "active" | "inactive">("all"); const [viewMode, setViewMode] = useState("table"); const [showModal, setShowModal] = useState(false); const [editingProduct, setEditingProduct] = useState(null); const [formData, setFormData] = useState({ name: "", description: "", price: "", type: "pickup", active: true, image_url: "", is_taxable: true, }); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState(null); const [deletingId, setDeletingId] = useState(null); const [isLoading, setIsLoading] = useState(false); // Image upload states const [imagePreview, setImagePreview] = useState(null); const [uploading, setUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const [pendingImageUrl, setPendingImageUrl] = useState(null); const fileInputRef = useRef(null); const filtered = products.filter((p) => { const matchesSearch = !search || p.name.toLowerCase().includes(search.toLowerCase()) || p.description.toLowerCase().includes(search.toLowerCase()); const matchesStatus = statusFilter === "all" || (statusFilter === "active" && p.active) || (statusFilter === "inactive" && !p.active); return matchesSearch && matchesStatus; }); const activeCount = products.filter((p) => p.active).length; const inactiveCount = products.filter((p) => !p.active).length; async function resizeImage(file: File, maxWidth: number): Promise { return new Promise((resolve, reject) => { const img = document.createElement("img"); img.onload = () => { let { width, height } = img; if (width > maxWidth) { height = Math.round(height * (maxWidth / width)); width = maxWidth; } const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d")!; ctx.drawImage(img, 0, 0, width, height); canvas.toBlob((blob) => { if (!blob) { reject(new Error("Failed to resize image")); return; } blob.arrayBuffer().then((buf) => resolve(buf)); }, "image/jpeg", 0.85); }; img.onerror = reject; img.src = URL.createObjectURL(file); }); } async function handleFileSelect(file: File) { const validTypes = ["image/png", "image/jpeg", "image/webp"]; if (!validTypes.includes(file.type)) { setUploadError("Only PNG, JPEG, and WebP images are allowed."); return; } if (file.size > 5 * 1024 * 1024) { setUploadError("Image must be under 5MB."); return; } setUploadError(null); setUploading(true); const resizedBuffer = await resizeImage(file, 1200); const resizedFile = new File([resizedBuffer], file.name, { type: "image/jpeg" }); const productIdForUpload = editingProduct ? editingProduct.id : "__NEW__"; const result = await uploadProductImage(productIdForUpload, resizedFile); setUploading(false); if (result.success) { setPendingImageUrl(result.imageUrl); setImagePreview(result.imageUrl); setFormData({ ...formData, image_url: result.imageUrl }); } else { setUploadError(result.error ?? "Upload failed."); } } const openAddModal = useCallback(() => { setEditingProduct(null); setFormData({ name: "", description: "", price: "", type: "pickup", active: true, image_url: "", is_taxable: true, }); setImagePreview(null); setPendingImageUrl(null); setUploadError(null); setError(null); setShowModal(true); }, []); const openEditModal = useCallback((product: Product) => { setEditingProduct(product); setFormData({ name: product.name, description: product.description || "", price: String(product.price), type: product.type, active: product.active, image_url: product.image_url || "", is_taxable: product.is_taxable, }); setImagePreview(product.image_url || null); setPendingImageUrl(null); setUploadError(null); setError(null); setShowModal(true); }, []); const closeModal = useCallback(() => { setShowModal(false); setEditingProduct(null); setImagePreview(null); setPendingImageUrl(null); setUploadError(null); setError(null); }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setSaving(true); setIsLoading(true); const price = parseFloat(formData.price); if (isNaN(price) || price < 0) { setError("Please enter a valid price"); setSaving(false); setIsLoading(false); return; } if (!formData.name.trim()) { setError("Product name is required"); setSaving(false); setIsLoading(false); return; } try { if (!brandId) { setError("Brand ID is required"); setSaving(false); setIsLoading(false); return; } let result; const imageUrl = pendingImageUrl || formData.image_url || null; if (editingProduct) { result = await updateProduct(editingProduct.id, brandId, { name: formData.name.trim(), description: formData.description.trim(), price, type: formData.type, active: formData.active, image_url: imageUrl, is_taxable: formData.is_taxable, }); } else { result = await createProduct(brandId, { name: formData.name.trim(), description: formData.description.trim(), price, type: formData.type, active: formData.active, image_url: imageUrl, is_taxable: formData.is_taxable, }); } if (!result.success) { setError(result.error ?? "Failed to save product"); showError("Failed to save product", result.error ?? "Please try again"); setSaving(false); setIsLoading(false); return; } showSuccess(editingProduct ? "Product updated" : "Product created", `${formData.name} has been saved`); closeModal(); setIsLoading(false); startTransition(() => router.refresh()); } catch { setError("Network error. Please try again."); showError("Network error", "Please check your connection and try again"); setSaving(false); setIsLoading(false); } }; const handleDelete = async (productId: string) => { setDeletingId(productId); const result = await deleteProduct(productId, null); setDeletingId(null); if (result.success) { setDeleteConfirm(null); showSuccess("Product deleted", "The product has been removed"); startTransition(() => router.refresh()); } else { showError("Failed to delete product", result.error ?? "Please try again"); } }; // Filter tabs configuration const filterTabs = [ { value: "all", label: "All", count: products.length }, { value: "active", label: "Active", count: activeCount }, { value: "inactive", label: "Inactive", count: inactiveCount }, ]; return (
{/* Page Header */} } title="Products" subtitle={`${filtered.length} product${filtered.length !== 1 ? "s" : ""}`} actions={ Add Product } /> {/* Stats Cards */}

Total

{isLoading ? : products.length}

Active

{isLoading ? : activeCount}

Inactive

{isLoading ? : inactiveCount}

{/* Filters */}
{/* Status Tabs */} setStatusFilter(value as "all" | "active" | "inactive")} tabs={filterTabs} size="md" /> {/* Search */} setSearch(e.target.value)} onClear={() => setSearch("")} placeholder="Search products..." containerClassName="flex-1" /> {/* View Toggle */} setViewMode(value as ViewMode)} />
{/* Content */} {viewMode === "table" ? ( setDeleteConfirm(id)} deleteConfirm={deleteConfirm} onDeleteConfirm={(id) => handleDelete(id)} onDeleteCancel={() => setDeleteConfirm(null)} deletingId={deletingId} /> ) : ( setDeleteConfirm(id)} deleteConfirm={deleteConfirm} onDeleteConfirm={(id) => handleDelete(id)} onDeleteCancel={() => setDeleteConfirm(null)} deletingId={deletingId} /> )} {/* Modal */} {showModal && (
{error && (
{error}
)}
setFormData({ ...formData, name: e.target.value })} placeholder="Product name" className={`w-full rounded-xl border px-3 py-2.5 text-sm text-[var(--admin-text-primary)] outline-none focus:ring-2 focus:ring-[var(--admin-accent)]/20 placeholder:text-[var(--admin-text-muted)] transition-colors ${ error && !formData.name.trim() ? "border-red-400 bg-red-50" : "border-[var(--admin-border)] bg-white focus:border-[var(--admin-accent)]" }`} required />