2025-04-13 22:38:33 +01:00
|
|
|
"use client";
|
2025-05-12 21:38:46 +01:00
|
|
|
import Image from "next/image";
|
2025-05-27 13:48:32 +01:00
|
|
|
import { Dispatch, SetStateAction, useCallback, useState, useEffect } from "react";
|
|
|
|
|
import BottomFooter from "@components/BottomFooter";
|
2025-04-28 19:03:29 +01:00
|
|
|
|
2025-05-19 18:05:40 +01:00
|
|
|
import { ExtendedArtefact } from "@appTypes/ApiTypes";
|
2025-05-12 21:38:46 +01:00
|
|
|
import { Currency } from "@appTypes/StoreModel";
|
|
|
|
|
import { useStoreState } from "@hooks/store";
|
2025-04-13 22:38:33 +01:00
|
|
|
|
2025-03-17 13:21:02 +00:00
|
|
|
export default function Shop() {
|
2025-05-27 13:48:32 +01:00
|
|
|
const [artefacts, setArtefacts] = useState<ExtendedArtefact[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
// 3. Fetch from your API route and map data to fit your existing fields
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
async function fetchArtefacts() {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
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
|
|
|
|
|
earthquakeID: a.earthquakeId?.toString() ?? "",
|
|
|
|
|
observatory: a.type ?? "", // if you want to display 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
|
|
|
|
|
}));
|
|
|
|
|
setArtefacts(transformed);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Optionally handle error
|
|
|
|
|
console.error("Failed to fetch artefacts", e);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fetchArtefacts();
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-05-12 21:38:46 +01:00
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
2025-05-13 10:00:09 +01:00
|
|
|
const [selectedArtefact, setSelectedArtefact] = useState<Artefact | null>(null);
|
2025-05-19 13:32:20 +01:00
|
|
|
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
|
|
|
|
const [artefactToBuy, setArtefactToBuy] = useState<Artefact | null>(null);
|
|
|
|
|
const [showThankYouModal, setShowThankYouModal] = useState(false);
|
|
|
|
|
const [orderNumber, setOrderNumber] = useState<string | null>(null);
|
|
|
|
|
|
2025-05-13 10:00:09 +01:00
|
|
|
const artefactsPerPage = 12;
|
|
|
|
|
const indexOfLastArtefact = currentPage * artefactsPerPage;
|
|
|
|
|
const indexOfFirstArtefact = indexOfLastArtefact - artefactsPerPage;
|
|
|
|
|
const currentArtefacts = artefacts.slice(indexOfFirstArtefact, indexOfLastArtefact);
|
2025-04-13 22:38:33 +01:00
|
|
|
|
2025-05-12 21:38:46 +01:00
|
|
|
const selectedCurrency = useStoreState((state) => state.currency.selectedCurrency);
|
|
|
|
|
const conversionRates = useStoreState((state) => state.currency.conversionRates);
|
|
|
|
|
const currencyTickers = useStoreState((state) => state.currency.tickers);
|
2025-04-13 22:38:33 +01:00
|
|
|
|
2025-05-19 13:32:20 +01:00
|
|
|
const convertPrice = useCallback(
|
|
|
|
|
(price: number, currency: Currency) => (price * conversionRates[currency]).toFixed(2),
|
|
|
|
|
[conversionRates]
|
|
|
|
|
);
|
2025-04-14 13:50:13 +01:00
|
|
|
|
2025-05-12 21:38:46 +01:00
|
|
|
const handleNextPage = () => {
|
2025-05-19 13:32:20 +01:00
|
|
|
if (indexOfLastArtefact < artefacts.length) setCurrentPage((prev) => prev + 1);
|
2025-05-12 21:38:46 +01:00
|
|
|
};
|
|
|
|
|
const handlePreviousPage = () => {
|
2025-05-19 13:32:20 +01:00
|
|
|
if (currentPage > 1) setCurrentPage((prev) => prev - 1);
|
2025-05-12 21:38:46 +01:00
|
|
|
};
|
2025-04-14 13:50:13 +01:00
|
|
|
|
2025-05-22 17:59:48 +01:00
|
|
|
function ArtefactCard({ artefact }: { artefact: ExtendedArtefact }) {
|
2025-05-19 13:32:20 +01:00
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform"
|
|
|
|
|
onClick={() => setSelectedArtefact(artefact)}
|
|
|
|
|
>
|
|
|
|
|
<Image src={artefact.image} alt={artefact.name} width={500} height={300} className="w-full h-56 object-cover" />
|
|
|
|
|
<div className="p-4">
|
|
|
|
|
<h3 className="text-lg font-semibold">{artefact.name}</h3>
|
|
|
|
|
<p className="text-neutral-500 mb-2">{artefact.location}</p>
|
|
|
|
|
<p className="text-neutral-500 mb-2">{artefact.earthquakeID}</p>
|
|
|
|
|
<p className="text-black font-bold text-md mt-2">
|
|
|
|
|
{currencyTickers[selectedCurrency]}
|
|
|
|
|
{convertPrice(artefact.price, selectedCurrency)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-05-22 17:59:48 +01:00
|
|
|
function Modal({ artefact }: { artefact: ExtendedArtefact }) {
|
2025-05-13 10:00:09 +01:00
|
|
|
if (!artefact) return null;
|
2025-05-19 13:32:20 +01:00
|
|
|
const handleOverlayClick = (e: React.MouseEvent) => {
|
|
|
|
|
if (e.target === e.currentTarget) setSelectedArtefact(null);
|
2025-05-12 21:38:46 +01:00
|
|
|
};
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
|
|
|
|
|
onClick={handleOverlayClick}
|
|
|
|
|
>
|
|
|
|
|
<div className="bg-white rounded-xl shadow-2xl max-w-lg w-full p-6">
|
2025-05-13 10:00:09 +01:00
|
|
|
<h3 className="text-2xl font-bold mb-4">{artefact.name}</h3>
|
2025-05-12 21:38:46 +01:00
|
|
|
<Image
|
2025-05-13 10:00:09 +01:00
|
|
|
src={artefact.image}
|
|
|
|
|
alt={artefact.name}
|
2025-05-19 13:32:20 +01:00
|
|
|
width={500}
|
|
|
|
|
height={300}
|
2025-05-12 21:38:46 +01:00
|
|
|
className="w-full h-64 object-cover rounded-md"
|
|
|
|
|
/>
|
|
|
|
|
<p className="text-xl font-bold">
|
|
|
|
|
{currencyTickers[selectedCurrency]}
|
2025-05-13 10:00:09 +01:00
|
|
|
{convertPrice(artefact.price, selectedCurrency)}
|
2025-05-12 21:38:46 +01:00
|
|
|
</p>
|
2025-05-13 10:00:09 +01:00
|
|
|
<p className="text-neutral-600 mt-2">{artefact.description}</p>
|
|
|
|
|
<p className="text-neutral-500 font-bold mt-1">Location: {artefact.location}</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.dateReleased}</p>
|
2025-05-12 21:38:46 +01:00
|
|
|
<div className="flex justify-end gap-4 mt-4 mr-2">
|
|
|
|
|
<button
|
2025-05-19 13:32:20 +01:00
|
|
|
onClick={() => {
|
|
|
|
|
setArtefactToBuy(artefact); // Set artefact for payment modal
|
|
|
|
|
setShowPaymentModal(true); // Show payment modal
|
|
|
|
|
setSelectedArtefact(null); // Close this modal
|
|
|
|
|
}}
|
2025-05-12 21:38:46 +01:00
|
|
|
className="px-10 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
|
|
|
>
|
|
|
|
|
Buy
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-04-13 22:38:33 +01:00
|
|
|
|
2025-05-19 13:32:20 +01:00
|
|
|
function PaymentModal({ artefact, onClose }: { artefact: Artefact; onClose: () => void }) {
|
|
|
|
|
const [cardNumber, setCardNumber] = useState("");
|
|
|
|
|
const [expiry, setExpiry] = useState("");
|
|
|
|
|
const [cvc, setCvc] = useState("");
|
|
|
|
|
const [name, setName] = useState("");
|
|
|
|
|
const [email, setEmail] = useState("");
|
|
|
|
|
const [remember, setRemember] = useState(false);
|
|
|
|
|
const [error, setError] = useState("");
|
|
|
|
|
|
|
|
|
|
function validateEmail(email: string) {
|
|
|
|
|
return (
|
|
|
|
|
email.includes("@") &&
|
|
|
|
|
(email.endsWith(".com") || email.endsWith(".co.uk") || email.endsWith(".org") || email.endsWith(".org.uk"))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
function validateCardNumber(number: string) {
|
|
|
|
|
return /^\d{12,19}$/.test(number.replace(/\s/g, "")); // 12-19 digits
|
|
|
|
|
}
|
|
|
|
|
function validateCVC(number: string) {
|
|
|
|
|
return /^\d{3,4}$/.test(number);
|
|
|
|
|
}
|
|
|
|
|
function validateExpiry(exp: string) {
|
|
|
|
|
return /^\d{2}\/\d{2}$/.test(exp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handlePay() {
|
|
|
|
|
setError("");
|
|
|
|
|
if (!validateEmail(email)) {
|
|
|
|
|
setError("Please enter a valid email ending");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!validateCardNumber(cardNumber)) {
|
|
|
|
|
setError("Card number must be 12-19 digits.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!validateExpiry(expiry)) {
|
|
|
|
|
setError("Expiry must be in MM/YY format.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!validateCVC(cvc)) {
|
|
|
|
|
setError("CVC must be 3 or 4 digits.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-05-20 13:53:25 +01:00
|
|
|
// todo create receiving api route
|
|
|
|
|
// todo handle sending to api route
|
|
|
|
|
// todo remove order number generation - we don't need one
|
|
|
|
|
// todo add option to save details in new account
|
2025-05-19 13:32:20 +01:00
|
|
|
const genOrder = () => "#" + Math.random().toString(36).substring(2, 10).toUpperCase();
|
|
|
|
|
setOrderNumber(genOrder());
|
|
|
|
|
onClose();
|
|
|
|
|
setShowThankYouModal(true);
|
|
|
|
|
}
|
|
|
|
|
const handleOverlayClick = (e: React.MouseEvent) => {
|
|
|
|
|
if (e.target === e.currentTarget) onClose();
|
|
|
|
|
};
|
2025-05-12 21:38:46 +01:00
|
|
|
return (
|
|
|
|
|
<div
|
2025-05-19 13:32:20 +01:00
|
|
|
className="fixed inset-0 bg-neutral-900 bg-opacity-60 flex justify-center items-center z-10"
|
|
|
|
|
onClick={handleOverlayClick}
|
2025-05-12 21:38:46 +01:00
|
|
|
>
|
2025-05-19 13:32:20 +01:00
|
|
|
<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>
|
|
|
|
|
{/* ...Image... */}
|
|
|
|
|
<form
|
|
|
|
|
onSubmit={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handlePay();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
className="w-full mb-2 px-3 py-2 border rounded"
|
|
|
|
|
placeholder="Email Address"
|
|
|
|
|
value={email}
|
|
|
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
|
|
|
type="email"
|
|
|
|
|
required
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
<input
|
|
|
|
|
className="w-full mb-2 px-3 py-2 border rounded"
|
|
|
|
|
placeholder="Cardholder Name"
|
|
|
|
|
value={name}
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
<input
|
|
|
|
|
className="w-full mb-2 px-3 py-2 border rounded"
|
|
|
|
|
placeholder="Card Number"
|
|
|
|
|
value={cardNumber}
|
|
|
|
|
onChange={(e) => setCardNumber(e.target.value.replace(/\D/g, ""))}
|
|
|
|
|
maxLength={19}
|
|
|
|
|
required
|
|
|
|
|
inputMode="numeric"
|
|
|
|
|
pattern="\d*"
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<input
|
|
|
|
|
className="w-1/2 mb-2 px-3 py-2 border rounded"
|
|
|
|
|
placeholder="MM/YY"
|
|
|
|
|
value={expiry}
|
|
|
|
|
onChange={(e) => setExpiry(e.target.value.replace(/[^0-9/]/g, ""))}
|
|
|
|
|
maxLength={5}
|
|
|
|
|
required
|
|
|
|
|
inputMode="numeric"
|
|
|
|
|
/>
|
|
|
|
|
<input
|
|
|
|
|
className="w-1/2 mb-2 px-3 py-2 border rounded"
|
|
|
|
|
placeholder="CVC"
|
|
|
|
|
value={cvc}
|
|
|
|
|
onChange={(e) => setCvc(e.target.value.replace(/\D/g, ""))}
|
|
|
|
|
maxLength={4}
|
|
|
|
|
required
|
|
|
|
|
inputMode="numeric"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<label className="inline-flex items-center mb-4">
|
|
|
|
|
<input type="checkbox" checked={remember} onChange={() => setRemember((r) => !r)} className="mr-2" />
|
|
|
|
|
Remember me
|
|
|
|
|
</label>
|
|
|
|
|
{error && <p className="text-red-600 mb-2">{error}</p>}
|
|
|
|
|
<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">
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
<button type="submit" className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
|
|
|
|
|
Pay
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
2025-05-12 21:38:46 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-04-29 16:52:03 +01:00
|
|
|
|
2025-05-19 13:32:20 +01:00
|
|
|
function ThankYouModal({ orderNumber, onClose }: { orderNumber: string; onClose: () => void }) {
|
|
|
|
|
const handleOverlayClick = (e: React.MouseEvent) => {
|
|
|
|
|
if (e.target === e.currentTarget) onClose();
|
|
|
|
|
};
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 bg-neutral-900 bg-opacity-60 flex justify-center items-center z-50"
|
|
|
|
|
onClick={handleOverlayClick}
|
|
|
|
|
>
|
|
|
|
|
<div className="bg-white rounded-xl shadow-2xl max-w-md w-full p-8 text-center">
|
|
|
|
|
<h2 className="text-3xl font-bold mb-4">Thank you for your purchase!</h2>
|
|
|
|
|
<p className="mb-4">Your order number is:</p>
|
|
|
|
|
<p className="text-2xl font-mono font-bold mb-6">{orderNumber}</p>
|
|
|
|
|
<button className="px-8 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700" onClick={onClose}>
|
|
|
|
|
Close
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-05-12 21:38:46 +01:00
|
|
|
return (
|
|
|
|
|
<div
|
2025-05-27 13:48:32 +01:00
|
|
|
className="min-h-screen bg-blue-50 relative flex flex-col"
|
2025-05-12 21:38:46 +01:00
|
|
|
style={{
|
2025-05-27 13:48:32 +01:00
|
|
|
//backgroundImage: "url('/BlueBackground.png')",
|
2025-05-12 21:38:46 +01:00
|
|
|
backgroundSize: "cover",
|
|
|
|
|
backgroundPosition: "center",
|
|
|
|
|
}}
|
|
|
|
|
>
|
2025-05-27 13:48:32 +01:00
|
|
|
<div className="absolute inset-0 bg-black bg-opacity-0 z-0"></div>
|
2025-05-12 21:38:46 +01:00
|
|
|
<div className="relative z-10 flex flex-col items-center w-full px-2 py-12">
|
2025-05-27 13:48:32 +01:00
|
|
|
<h1 className="text-4xl md:text-4xl font-bold text-center text-blue-800 mb-2 tracking-tight drop-shadow-lg">
|
2025-05-13 10:00:09 +01:00
|
|
|
Artefact Shop
|
2025-05-12 21:38:46 +01:00
|
|
|
</h1>
|
2025-05-27 13:48:32 +01:00
|
|
|
<p className="text-lg md:text-xl text-center text-gray-700 mb-10 drop-shadow-md max-w-2xl">
|
|
|
|
|
Discover extraordinary artefacts and collectibles from major seismic events from around the world - Previously studied by our scientists, now
|
2025-05-12 21:38:46 +01:00
|
|
|
available for purchase.
|
|
|
|
|
</p>
|
|
|
|
|
<div className="w-full max-w-7xl grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 p-2">
|
2025-05-13 10:00:09 +01:00
|
|
|
{currentArtefacts.map((artefact) => (
|
|
|
|
|
<ArtefactCard key={artefact.id} artefact={artefact} />
|
2025-05-12 21:38:46 +01:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<footer className="mt-10 bg-white bg-opacity-90 border-neutral-300 py-3 text-center flex justify-center items-center w-100 max-w-7xl rounded-lg">
|
|
|
|
|
<button
|
|
|
|
|
onClick={handlePreviousPage}
|
|
|
|
|
disabled={currentPage === 1}
|
|
|
|
|
className={`mx-2 px-4 py-1 bg-blue-500 text-white rounded-md font-bold shadow-md ${
|
|
|
|
|
currentPage === 1 ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
← Previous
|
|
|
|
|
</button>
|
|
|
|
|
<p className="mx-3 text-lg font-bold">{currentPage}</p>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleNextPage}
|
2025-05-13 10:00:09 +01:00
|
|
|
disabled={indexOfLastArtefact >= artefacts.length}
|
2025-05-12 21:38:46 +01:00
|
|
|
className={`mx-2 px-4 py-1 bg-blue-500 text-white rounded-md font-bold shadow-md ${
|
2025-05-13 10:00:09 +01:00
|
|
|
indexOfLastArtefact >= artefacts.length ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600"
|
2025-05-12 21:38:46 +01:00
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
Next →
|
|
|
|
|
</button>
|
|
|
|
|
</footer>
|
|
|
|
|
</div>
|
2025-05-13 10:00:09 +01:00
|
|
|
{selectedArtefact && <Modal artefact={selectedArtefact} />}
|
2025-05-19 13:32:20 +01:00
|
|
|
{artefactToBuy && showPaymentModal && (
|
|
|
|
|
<PaymentModal
|
|
|
|
|
artefact={artefactToBuy}
|
|
|
|
|
onClose={() => {
|
|
|
|
|
setShowPaymentModal(false);
|
|
|
|
|
setArtefactToBuy(null);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{showThankYouModal && orderNumber && (
|
|
|
|
|
<ThankYouModal orderNumber={orderNumber} onClose={() => setShowThankYouModal(false)} />
|
|
|
|
|
)}
|
2025-05-27 13:48:32 +01:00
|
|
|
{!selectedArtefact && !showPaymentModal && !showThankYouModal && (
|
|
|
|
|
<div className="relative z-50">
|
|
|
|
|
<BottomFooter />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|