cart feature added
This commit is contained in:
parent
8d3575591d
commit
7ba82efc3c
@ -1,43 +1,41 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Image from "next/image";
|
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 { ExtendedArtefact } from "@appTypes/ApiTypes";
|
||||||
import { Currency } from "@appTypes/StoreModel";
|
import { Currency } from "@appTypes/StoreModel";
|
||||||
import BottomFooter from "@components/BottomFooter";
|
import BottomFooter from "@components/BottomFooter";
|
||||||
import { useStoreState } from "@hooks/store";
|
import { useStoreState } from "@hooks/store";
|
||||||
|
|
||||||
// todo hide from shop after purchase
|
|
||||||
|
|
||||||
export default function Shop() {
|
export default function Shop() {
|
||||||
const [artefacts, setArtefacts] = useState<ExtendedArtefact[]>([]);
|
const [artefacts, setArtefacts] = useState<ExtendedArtefact[]>([]);
|
||||||
const [hiddenArtefactIds, setHiddenArtefactIds] = useState<number[]>([]);
|
const [hiddenArtefactIds, setHiddenArtefactIds] = useState<number[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const [cart, setCart] = useState<ExtendedArtefact[]>([]);
|
||||||
|
const [showCartModal, setShowCartModal] = useState(false);
|
||||||
|
|
||||||
const user = useStoreState((state) => state.user);
|
const user = useStoreState((state) => state.user);
|
||||||
|
|
||||||
// 3. Fetch from your API route and map data to fit your existing fields
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchArtefacts() {
|
async function fetchArtefacts() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// todo only show only non-required artefacts
|
|
||||||
const res = await fetch("/api/artefacts");
|
const res = await fetch("/api/artefacts");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
const transformed = data.artefact.map((a: any) => ({
|
const transformed = data.artefact.map((a: any) => ({
|
||||||
id: a.id,
|
id: a.id,
|
||||||
name: a.name,
|
name: a.name,
|
||||||
description: a.description,
|
description: a.description,
|
||||||
location: a.warehouseArea, // your database
|
location: a.warehouseArea,
|
||||||
earthquakeID: a.earthquakeId?.toString() ?? "",
|
earthquakeID: a.earthquakeId?.toString() ?? "",
|
||||||
observatory: a.type ?? "", // if you want to display type
|
observatory: a.type ?? "",
|
||||||
dateReleased: a.createdAt ? new Date(a.createdAt).toLocaleDateString() : "",
|
dateReleased: a.createdAt ? new Date(a.createdAt).toLocaleDateString() : "",
|
||||||
image: "/artefactImages/" + (a.imageName || "NoImageFound.PNG"),
|
image: "/artefactImages/" + (a.imageName || "NoImageFound.PNG"),
|
||||||
price: a.shopPrice ?? 100, // fallback price if not in DB
|
price: a.shopPrice ?? 100,
|
||||||
}));
|
}));
|
||||||
setArtefacts(transformed);
|
setArtefacts(transformed);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Optionally handle error
|
|
||||||
console.error("Failed to fetch artefacts", e);
|
console.error("Failed to fetch artefacts", e);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -47,9 +45,10 @@ export default function Shop() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [selectedArtefact, setSelectedArtefact] = useState<Artefact | null>(null);
|
const [selectedArtefact, setSelectedArtefact] = useState<ExtendedArtefact | null>(null);
|
||||||
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
||||||
const [artefactToBuy, setArtefactToBuy] = useState<Artefact | null>(null);
|
const [artefactToBuy, setArtefactToBuy] = useState<ExtendedArtefact | null>(null);
|
||||||
|
const [cartCheckout, setCartCheckout] = useState(false); // true = checkout cart (not single artefact)
|
||||||
const [showThankYouModal, setShowThankYouModal] = useState(false);
|
const [showThankYouModal, setShowThankYouModal] = useState(false);
|
||||||
const [orderNumber, setOrderNumber] = useState<string | null>(null);
|
const [orderNumber, setOrderNumber] = useState<string | null>(null);
|
||||||
|
|
||||||
@ -93,11 +92,14 @@ export default function Shop() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Modal({ artefact }: { artefact: ExtendedArtefact }) {
|
function Modal({ artefact }: { artefact: ExtendedArtefact }) {
|
||||||
if (!artefact) return null;
|
if (!artefact) return null;
|
||||||
const handleOverlayClick = (e: React.MouseEvent) => {
|
const handleOverlayClick = (e: React.MouseEvent) => {
|
||||||
if (e.target === e.currentTarget) setSelectedArtefact(null);
|
if (e.target === e.currentTarget) setSelectedArtefact(null);
|
||||||
};
|
};
|
||||||
|
const inCart = cart.some((a) => a.id === artefact.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
|
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
|
||||||
@ -121,16 +123,32 @@ export default function Shop() {
|
|||||||
<p className="text-neutral-500 mb-2">{artefact.earthquakeID}</p>
|
<p className="text-neutral-500 mb-2">{artefact.earthquakeID}</p>
|
||||||
<p className="text-neutral-500 mb-2">{artefact.observatory}</p>
|
<p className="text-neutral-500 mb-2">{artefact.observatory}</p>
|
||||||
<p className="text-neutral-500 mb-2">{artefact.dateReleased}</p>
|
<p className="text-neutral-500 mb-2">{artefact.dateReleased}</p>
|
||||||
<div className="flex justify-end gap-4 mt-4 mr-2">
|
<div className="flex flex-col sm:flex-row justify-end gap-4 mt-4 mr-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setArtefactToBuy(artefact); // Set artefact for payment modal
|
if (!inCart) setCart((cart) => [...cart, artefact]);
|
||||||
setShowPaymentModal(true); // Show payment modal
|
|
||||||
setSelectedArtefact(null); // Close this modal
|
|
||||||
}}
|
}}
|
||||||
className="px-10 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
disabled={inCart}
|
||||||
|
className={`px-6 py-2 rounded-md font-bold border
|
||||||
|
${
|
||||||
|
inCart
|
||||||
|
? "bg-gray-300 text-gray-400 cursor-not-allowed"
|
||||||
|
: "bg-green-500 hover:bg-green-600 text-white"
|
||||||
|
}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
Buy
|
{inCart ? "In Cart" : "Add to Cart"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setArtefactToBuy(artefact);
|
||||||
|
setShowPaymentModal(true);
|
||||||
|
setCartCheckout(false);
|
||||||
|
setSelectedArtefact(null);
|
||||||
|
}}
|
||||||
|
className="px-8 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Buy Now
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -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 (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-neutral-900 bg-opacity-60 flex justify-center items-center z-[999]"
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-xl shadow-2xl max-w-2xl w-full p-8">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-2xl font-bold">Your Cart</h2>
|
||||||
|
<button onClick={() => setShowCartModal(false)} className="text-xl font-bold px-2 py-1 rounded">
|
||||||
|
✖
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{cart.length === 0 ? (
|
||||||
|
<p className="text-neutral-500">Your cart is empty.</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ul className="mb-4">
|
||||||
|
{cart.map((art) => (
|
||||||
|
<li key={art.id} className="flex items-center border-b py-2">
|
||||||
|
<div className="flex-shrink-0 mr-3">
|
||||||
|
<Image src={art.image} alt={art.name} width={60} height={40} className="rounded" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<p className="font-bold">{art.name}</p>
|
||||||
|
<p className="text-neutral-500 text-sm">{art.location}</p>
|
||||||
|
</div>
|
||||||
|
<p className="font-bold mr-2">
|
||||||
|
{currencyTickers[selectedCurrency]}
|
||||||
|
{convertPrice(art.price, selectedCurrency)}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="px-3 py-1 bg-red-400 hover:bg-red-500 text-white rounded"
|
||||||
|
onClick={() => setCart((c) => c.filter((a) => a.id !== art.id))}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="font-bold">Total:</span>
|
||||||
|
<span className="text-lg font-bold">
|
||||||
|
{currencyTickers[selectedCurrency]}
|
||||||
|
{convertPrice(total, selectedCurrency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<button
|
||||||
|
className="px-8 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||||
|
onClick={() => {
|
||||||
|
setShowCartModal(false);
|
||||||
|
setArtefactToBuy(null);
|
||||||
|
setShowPaymentModal(true);
|
||||||
|
setCartCheckout(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Checkout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaymentModal({
|
||||||
|
artefact,
|
||||||
|
onClose,
|
||||||
|
cartItems,
|
||||||
|
}: {
|
||||||
|
artefact?: ExtendedArtefact;
|
||||||
|
onClose: () => void;
|
||||||
|
cartItems?: ExtendedArtefact[];
|
||||||
|
}) {
|
||||||
const [cardNumber, setCardNumber] = useState("");
|
const [cardNumber, setCardNumber] = useState("");
|
||||||
const [expiry, setExpiry] = useState("");
|
const [expiry, setExpiry] = useState("");
|
||||||
const [cvc, setCvc] = useState("");
|
const [cvc, setCvc] = useState("");
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState(user?.email || "");
|
||||||
const [remember, setRemember] = useState(false);
|
const [remember, setRemember] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const artefactsToBuy = artefact ? [artefact] : cartItems || [];
|
||||||
|
const total = artefactsToBuy.reduce((sum, art) => sum + art.price, 0);
|
||||||
|
|
||||||
function validateEmail(email: string) {
|
function validateEmail(email: string) {
|
||||||
return (
|
return (
|
||||||
email.includes("@") &&
|
email.includes("@") &&
|
||||||
@ -162,18 +264,13 @@ export default function Shop() {
|
|||||||
function validateExpiry(exp: string) {
|
function validateExpiry(exp: string) {
|
||||||
return /^\d{2}\/\d{2}$/.test(exp);
|
return /^\d{2}\/\d{2}$/.test(exp);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePay() {
|
function handlePay() {
|
||||||
setError("");
|
setError("");
|
||||||
if (email || user?.email) {
|
const paymentEmail = user?.email || email;
|
||||||
if (!validateEmail(email)) {
|
if (!validateEmail(paymentEmail)) {
|
||||||
setError("Please enter a valid email ending");
|
setError("Please enter a valid email");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validateCardNumber(cardNumber)) {
|
if (!validateCardNumber(cardNumber)) {
|
||||||
setError("Card number must be 12-19 digits.");
|
setError("Card number must be 12-19 digits.");
|
||||||
return;
|
return;
|
||||||
@ -186,45 +283,51 @@ export default function Shop() {
|
|||||||
setError("CVC must be 3 or 4 digits.");
|
setError("CVC must be 3 or 4 digits.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// remove all artefacts that were bought (works for both cart and single)
|
||||||
setHiddenArtefactIds((ids) => [...ids, artefact.id]);
|
setHiddenArtefactIds((ids) => [...ids, ...artefactsToBuy.map((a) => a.id)]);
|
||||||
|
|
||||||
// todo create receiving api route
|
// todo create receiving api route
|
||||||
// todo handle sending to 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();
|
const genOrder = () => "#" + Math.random().toString(36).substring(2, 10).toUpperCase();
|
||||||
setOrderNumber(genOrder());
|
setOrderNumber(genOrder());
|
||||||
onClose();
|
onClose();
|
||||||
setShowThankYouModal(true);
|
setShowThankYouModal(true);
|
||||||
|
setCart((c) => c.filter((a) => !artefactsToBuy.map((x) => x.id).includes(a.id)));
|
||||||
}
|
}
|
||||||
const handleOverlayClick = (e: React.MouseEvent) => {
|
const handleOverlayClick = (e: React.MouseEvent) => {
|
||||||
if (e.target === e.currentTarget) onClose();
|
if (e.target === e.currentTarget) onClose();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-neutral-900 bg-opacity-60 flex justify-center items-center z-10"
|
className="fixed inset-0 bg-neutral-900 bg-opacity-60 flex justify-center items-center z-[12000]"
|
||||||
onClick={handleOverlayClick}
|
onClick={handleOverlayClick}
|
||||||
>
|
>
|
||||||
<div className="bg-white rounded-xl shadow-2xl max-w-md w-full p-6">
|
<div className="bg-white rounded-xl shadow-2xl max-w-md w-full p-6">
|
||||||
<h2 className="text-2xl font-bold mb-4">Buy {artefact.name}</h2>
|
<h2 className="text-2xl font-bold mb-4">
|
||||||
{/* ...Image... */}
|
Checkout {artefact ? artefact.name : artefactsToBuy.length + " item(s)"}
|
||||||
|
{!artefact && <span className="ml-1">({artefactsToBuy.map((x) => x.name).join(", ")})</span>}
|
||||||
|
</h2>
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handlePay();
|
handlePay();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!user ? (
|
{/* Email autofill */}
|
||||||
<input
|
<input
|
||||||
className="w-full mb-2 px-3 py-2 border rounded"
|
className="w-full mb-2 px-3 py-2 border rounded"
|
||||||
placeholder="Email Address"
|
placeholder="Email Address"
|
||||||
value={email}
|
value={user?.email ? user.email : email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
|
disabled={!!user?.email}
|
||||||
/>
|
/>
|
||||||
) : null}
|
{user?.email && (
|
||||||
|
<p className="text-sm text-gray-500 mb-2">
|
||||||
|
Signed in as <span className="font-bold">{user.email}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
className="w-full mb-2 px-3 py-2 border rounded"
|
className="w-full mb-2 px-3 py-2 border rounded"
|
||||||
placeholder="Cardholder Name"
|
placeholder="Cardholder Name"
|
||||||
@ -267,6 +370,13 @@ export default function Shop() {
|
|||||||
Remember me
|
Remember me
|
||||||
</label>
|
</label>
|
||||||
{error && <p className="text-red-600 mb-2">{error}</p>}
|
{error && <p className="text-red-600 mb-2">{error}</p>}
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="font-bold">Total:</span>
|
||||||
|
<span className="text-lg font-bold">
|
||||||
|
{currencyTickers[selectedCurrency]}
|
||||||
|
{convertPrice(total, selectedCurrency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="flex justify-end gap-2 mt-2">
|
<div className="flex justify-end gap-2 mt-2">
|
||||||
<button type="button" onClick={onClose} className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md mr-2">
|
<button type="button" onClick={onClose} className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md mr-2">
|
||||||
Cancel
|
Cancel
|
||||||
@ -311,6 +421,28 @@ export default function Shop() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-black bg-opacity-0 z-0"></div>
|
<div className="absolute inset-0 bg-black bg-opacity-0 z-0"></div>
|
||||||
|
{/* --- Cart Button fixed at top right --- */}
|
||||||
|
<button
|
||||||
|
className="absolute top-6 right-6 z-[11000] bg-white border border-blue-500 shadow-lg rounded-full p-3 hover:bg-blue-100 flex flex-row items-center"
|
||||||
|
onClick={() => setShowCartModal(true)}
|
||||||
|
aria-label="Open your cart"
|
||||||
|
>
|
||||||
|
<span className="mr-2 font-bold">{cart.length || ""}</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-7 h-7 text-blue-600"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13l-1.35 2.7a1 1 0 00.9 1.45h12.2M7 13l1.2-2.4M3 3l.01 0"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<div className="relative z-10 flex flex-col items-center w-full px-2 py-12">
|
<div className="relative z-10 flex flex-col items-center w-full px-2 py-12">
|
||||||
<h1 className="text-4xl md:text-4xl font-bold text-center text-blue-300 mb-2 tracking-tight drop-shadow-lg">
|
<h1 className="text-4xl md:text-4xl font-bold text-center text-blue-300 mb-2 tracking-tight drop-shadow-lg">
|
||||||
Artefact Shop
|
Artefact Shop
|
||||||
@ -349,19 +481,22 @@ export default function Shop() {
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
{selectedArtefact && <Modal artefact={selectedArtefact} />}
|
{selectedArtefact && <Modal artefact={selectedArtefact} />}
|
||||||
{artefactToBuy && showPaymentModal && (
|
{showCartModal && <CartModal />}
|
||||||
|
{showPaymentModal && (cartCheckout || artefactToBuy) && (
|
||||||
<PaymentModal
|
<PaymentModal
|
||||||
artefact={artefactToBuy}
|
artefact={cartCheckout ? undefined : artefactToBuy!}
|
||||||
|
cartItems={cartCheckout ? cart : undefined}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowPaymentModal(false);
|
setShowPaymentModal(false);
|
||||||
setArtefactToBuy(null);
|
setArtefactToBuy(null);
|
||||||
|
setCartCheckout(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showThankYouModal && orderNumber && (
|
{showThankYouModal && orderNumber && (
|
||||||
<ThankYouModal orderNumber={orderNumber} onClose={() => setShowThankYouModal(false)} />
|
<ThankYouModal orderNumber={orderNumber} onClose={() => setShowThankYouModal(false)} />
|
||||||
)}
|
)}
|
||||||
{!selectedArtefact && !showPaymentModal && !showThankYouModal && (
|
{!selectedArtefact && !showPaymentModal && !showThankYouModal && !showCartModal && (
|
||||||
<div className="relative z-50">
|
<div className="relative z-50">
|
||||||
<BottomFooter />
|
<BottomFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user