diff --git a/client/src/pages/admin/products.tsx b/client/src/pages/admin/products.tsx new file mode 100644 index 0000000..3d7494c --- /dev/null +++ b/client/src/pages/admin/products.tsx @@ -0,0 +1,220 @@ +import { useState } from "react"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { queryClient, apiRequest } from "../../lib/queryClient"; +import { AdminNav } from "./dashboard"; +import { toast } from "../../hooks/use-toast"; +import { Plus, Pencil, Trash2, Search, X } from "lucide-react"; + +interface Product { + id: number; + handle: string; + title: string; + price: string; + status: string; + vendor?: string; + images?: string[]; + createdAt: string; +} + +interface ProductForm { + title: string; + handle: string; + description: string; + price: string; + compareAtPrice: string; + vendor: string; + status: string; + tags: string; +} + +const DEFAULT_FORM: ProductForm = { title: "", handle: "", description: "", price: "", compareAtPrice: "", vendor: "", status: "active", tags: "" }; + +function slugify(s: string) { + return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); +} + +function ProductModal({ product, onClose }: { product?: Product; onClose: () => void }) { + const isEdit = !!product; + const [form, setForm] = useState(product ? { + title: product.title, + handle: product.handle, + description: "", + price: product.price, + compareAtPrice: "", + vendor: product.vendor || "", + status: product.status, + tags: "", + } : DEFAULT_FORM); + + const mutation = useMutation({ + mutationFn: (body: object) => isEdit + ? apiRequest("PATCH", `/api/admin/products/${product!.id}`, body) + : apiRequest("POST", "/api/admin/products", body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/admin/products"] }); + toast({ title: isEdit ? "Product updated" : "Product created" }); + onClose(); + }, + onError: (err: any) => toast({ title: "Error", description: err.message, variant: "destructive" }), + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + mutation.mutate({ + ...form, + handle: form.handle || slugify(form.title), + tags: form.tags ? form.tags.split(",").map((t) => t.trim()).filter(Boolean) : [], + }); + }; + + const field = (label: string, key: keyof ProductForm, type = "text", placeholder = "") => ( +
+ + setForm({ ...form, [key]: e.target.value })} + placeholder={placeholder} + className="w-full px-3 py-2 border border-border rounded-md text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/30" + data-testid={`input-product-${key}`} /> +
+ ); + + return ( +
+
+
+

{isEdit ? "Edit product" : "New product"}

+ +
+
+ {field("Title", "title")} + {field("Handle (URL slug)", "handle", "text", "auto-generated from title")} +
+ +