From 7ba82efc3ca41609f7dc0f95876f447c11bde5a5 Mon Sep 17 00:00:00 2001 From: Emily Neighbour Date: Sat, 31 May 2025 21:58:05 +0100 Subject: [PATCH] cart feature added --- src/app/shop/page.tsx | 233 +++++++++++++++++++++++++++++++++--------- 1 file changed, 184 insertions(+), 49 deletions(-) diff --git a/src/app/shop/page.tsx b/src/app/shop/page.tsx index f39be36..a838498 100644 --- a/src/app/shop/page.tsx +++ b/src/app/shop/page.tsx @@ -1,43 +1,41 @@ "use client"; import Image from "next/image"; -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { ExtendedArtefact } from "@appTypes/ApiTypes"; import { Currency } from "@appTypes/StoreModel"; import BottomFooter from "@components/BottomFooter"; import { useStoreState } from "@hooks/store"; -// todo hide from shop after purchase - export default function Shop() { const [artefacts, setArtefacts] = useState([]); const [hiddenArtefactIds, setHiddenArtefactIds] = useState([]); const [loading, setLoading] = useState(true); + + const [cart, setCart] = useState([]); + const [showCartModal, setShowCartModal] = useState(false); + const user = useStoreState((state) => state.user); - // 3. Fetch from your API route and map data to fit your existing fields useEffect(() => { async function fetchArtefacts() { setLoading(true); try { - // todo only show only non-required artefacts const res = await fetch("/api/artefacts"); const data = await res.json(); - const transformed = data.artefact.map((a: any) => ({ id: a.id, name: a.name, description: a.description, - location: a.warehouseArea, // your database + location: a.warehouseArea, earthquakeID: a.earthquakeId?.toString() ?? "", - observatory: a.type ?? "", // if you want to display type + observatory: a.type ?? "", dateReleased: a.createdAt ? new Date(a.createdAt).toLocaleDateString() : "", image: "/artefactImages/" + (a.imageName || "NoImageFound.PNG"), - price: a.shopPrice ?? 100, // fallback price if not in DB + price: a.shopPrice ?? 100, })); setArtefacts(transformed); } catch (e) { - // Optionally handle error console.error("Failed to fetch artefacts", e); } finally { setLoading(false); @@ -47,9 +45,10 @@ export default function Shop() { }, []); const [currentPage, setCurrentPage] = useState(1); - const [selectedArtefact, setSelectedArtefact] = useState(null); + const [selectedArtefact, setSelectedArtefact] = useState(null); const [showPaymentModal, setShowPaymentModal] = useState(false); - const [artefactToBuy, setArtefactToBuy] = useState(null); + const [artefactToBuy, setArtefactToBuy] = useState(null); + const [cartCheckout, setCartCheckout] = useState(false); // true = checkout cart (not single artefact) const [showThankYouModal, setShowThankYouModal] = useState(false); const [orderNumber, setOrderNumber] = useState(null); @@ -93,11 +92,14 @@ export default function Shop() { ); } + function Modal({ artefact }: { artefact: ExtendedArtefact }) { if (!artefact) return null; const handleOverlayClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) setSelectedArtefact(null); }; + const inCart = cart.some((a) => a.id === artefact.id); + return (
{artefact.earthquakeID}

{artefact.observatory}

{artefact.dateReleased}

-
+
+
@@ -138,15 +156,99 @@ export default function Shop() { ); } - function PaymentModal({ artefact, onClose }: { artefact: Artefact; onClose: () => void }) { + function CartModal() { + const total = cart.reduce((sum, art) => sum + art.price, 0); + + const handleOverlayClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) setShowCartModal(false); + }; + + return ( +
+
+
+

Your Cart

+ +
+ {cart.length === 0 ? ( +

Your cart is empty.

+ ) : ( + <> +
    + {cart.map((art) => ( +
  • +
    + {art.name} +
    +
    +

    {art.name}

    +

    {art.location}

    +
    +

    + {currencyTickers[selectedCurrency]} + {convertPrice(art.price, selectedCurrency)} +

    + +
  • + ))} +
+
+ Total: + + {currencyTickers[selectedCurrency]} + {convertPrice(total, selectedCurrency)} + +
+
+ +
+ + )} +
+
+ ); + } + + function PaymentModal({ + artefact, + onClose, + cartItems, + }: { + artefact?: ExtendedArtefact; + onClose: () => void; + cartItems?: ExtendedArtefact[]; + }) { const [cardNumber, setCardNumber] = useState(""); const [expiry, setExpiry] = useState(""); const [cvc, setCvc] = useState(""); const [name, setName] = useState(""); - const [email, setEmail] = useState(""); + const [email, setEmail] = useState(user?.email || ""); const [remember, setRemember] = useState(false); const [error, setError] = useState(""); + const artefactsToBuy = artefact ? [artefact] : cartItems || []; + const total = artefactsToBuy.reduce((sum, art) => sum + art.price, 0); + function validateEmail(email: string) { return ( email.includes("@") && @@ -162,18 +264,13 @@ export default function Shop() { function validateExpiry(exp: string) { return /^\d{2}\/\d{2}$/.test(exp); } - function handlePay() { setError(""); - if (email || user?.email) { - if (!validateEmail(email)) { - setError("Please enter a valid email ending"); - return; - } - } else { + const paymentEmail = user?.email || email; + if (!validateEmail(paymentEmail)) { + setError("Please enter a valid email"); return; } - if (!validateCardNumber(cardNumber)) { setError("Card number must be 12-19 digits."); return; @@ -186,45 +283,51 @@ export default function Shop() { setError("CVC must be 3 or 4 digits."); return; } - - setHiddenArtefactIds((ids) => [...ids, artefact.id]); - + // remove all artefacts that were bought (works for both cart and single) + setHiddenArtefactIds((ids) => [...ids, ...artefactsToBuy.map((a) => a.id)]); // todo create receiving api route // todo handle sending to api route - // todo (optional) add create account button to auto-fill email in sign-up modal const genOrder = () => "#" + Math.random().toString(36).substring(2, 10).toUpperCase(); setOrderNumber(genOrder()); onClose(); setShowThankYouModal(true); + setCart((c) => c.filter((a) => !artefactsToBuy.map((x) => x.id).includes(a.id))); } const handleOverlayClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) onClose(); }; return (
-

Buy {artefact.name}

- {/* ...Image... */} +

+ Checkout {artefact ? artefact.name : artefactsToBuy.length + " item(s)"} + {!artefact && ({artefactsToBuy.map((x) => x.name).join(", ")})} +

{ e.preventDefault(); handlePay(); }} > - {!user ? ( - setEmail(e.target.value)} - type="email" - required - autoFocus - /> - ) : null} + {/* Email autofill */} + setEmail(e.target.value)} + type="email" + required + autoFocus + disabled={!!user?.email} + /> + {user?.email && ( +

+ Signed in as {user.email} +

+ )} {error &&

{error}

} +
+ Total: + + {currencyTickers[selectedCurrency]} + {convertPrice(total, selectedCurrency)} + +

Artefact Shop @@ -349,19 +481,22 @@ export default function Shop() {

{selectedArtefact && } - {artefactToBuy && showPaymentModal && ( + {showCartModal && } + {showPaymentModal && (cartCheckout || artefactToBuy) && ( { setShowPaymentModal(false); setArtefactToBuy(null); + setCartCheckout(false); }} /> )} {showThankYouModal && orderNumber && ( setShowThankYouModal(false)} /> )} - {!selectedArtefact && !showPaymentModal && !showThankYouModal && ( + {!selectedArtefact && !showPaymentModal && !showThankYouModal && !showCartModal && (