From 869e6c77b7966d351304584ee6f93838cf336566 Mon Sep 17 00:00:00 2001 From: notshop Date: Sun, 26 Apr 2026 16:36:11 +0000 Subject: [PATCH] chore: add client/src/pages/product.tsx --- client/src/pages/product.tsx | 190 +++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 client/src/pages/product.tsx diff --git a/client/src/pages/product.tsx b/client/src/pages/product.tsx new file mode 100644 index 0000000..a8f3040 --- /dev/null +++ b/client/src/pages/product.tsx @@ -0,0 +1,190 @@ +import { useState } from "react"; +import { useParams, Link } from "wouter"; +import { useQuery } from "@tanstack/react-query"; +import { ChevronLeft, ShoppingBag, Shield, Package } from "lucide-react"; +import { useCart } from "../hooks/use-cart"; +import { toast } from "../hooks/use-toast"; + +interface Variant { + id: number; + title: string; + price: string; + compareAtPrice?: string; + inventoryQty: number; + inventoryPolicy: string; + imageUrl?: string; +} + +interface Product { + id: number; + handle: string; + title: string; + description?: string; + descriptionHtml?: string; + price: string; + compareAtPrice?: string; + images?: string[]; + tags?: string[]; + hsaFsa?: boolean; + ingredients?: string; + vendor?: string; +} + +export default function ProductPage() { + const { handle } = useParams<{ handle: string }>(); + const { addItem } = useCart(); + const [selectedVariantId, setSelectedVariantId] = useState(); + const [selectedImage, setSelectedImage] = useState(0); + const [adding, setAdding] = useState(false); + + const { data, isLoading, error } = useQuery<{ product: Product; variants: Variant[] }>({ + queryKey: ["/api/products", handle], + queryFn: async () => { + const res = await fetch(`/api/products/${handle}`); + if (!res.ok) throw new Error("Product not found"); + return res.json(); + }, + }); + + if (isLoading) return ( +
+
+
+
+
+
+
+
+
+
+
+ ); + + if (error || !data) return ( +
+

Product not found

+ ← Back to shop +
+ ); + + const { product, variants } = data; + const images = product.images || []; + const selectedVariant = variants.find((v) => v.id === selectedVariantId); + const price = selectedVariant?.price || product.price; + const compareAtPrice = selectedVariant?.compareAtPrice || product.compareAtPrice; + const isOnSale = compareAtPrice && parseFloat(compareAtPrice) > parseFloat(price); + const displayImage = selectedVariant?.imageUrl || images[selectedImage]; + + const inStock = !selectedVariant || selectedVariant.inventoryPolicy === "continue" || selectedVariant.inventoryQty > 0; + + const handleAdd = async () => { + setAdding(true); + try { + await addItem(product.id, selectedVariantId); + toast({ title: "Added to cart", description: product.title }); + } catch { + toast({ title: "Error", description: "Could not add to cart", variant: "destructive" }); + } finally { + setAdding(false); + } + }; + + return ( +
+ + Back to shop + + +
+
+
+ {displayImage ? ( + {product.title} + ) : ( +
📦
+ )} +
+ {images.length > 1 && ( +
+ {images.map((img, i) => ( + + ))} +
+ )} +
+ +
+ {product.vendor &&

{product.vendor}

} +

{product.title}

+ +
+ ${parseFloat(price).toFixed(2)} + {isOnSale && ${parseFloat(compareAtPrice!).toFixed(2)}} + {product.hsaFsa && ( + + HSA/FSA Eligible + + )} +
+ + {variants.length > 1 && ( +
+

Option

+
+ {variants.map((v) => ( + + ))} +
+
+ )} + + {variants.length > 1 && !selectedVariantId && ( +

Please select an option

+ )} + + + +
+ + Free shipping on orders over $75 +
+ + {(product.description || product.descriptionHtml) && ( +
+ {product.descriptionHtml ? ( +
+ ) : ( +

{product.description}

+ )} +
+ )} + + {product.ingredients && ( +
+

Ingredients

+

{product.ingredients}

+
+ )} +
+
+
+ ); +}