492 lines
15 KiB
TypeScript

"use client";
import Image from "next/image";
import { Dispatch, SetStateAction, useCallback, useState } from "react";
import { ExtendedArtefact } from "@appTypes/ApiTypes";
import { Currency } from "@appTypes/StoreModel";
import { useStoreState } from "@hooks/store";
const artefacts: ExtendedArtefact[] = [
{
id: 1,
name: "Golden Scarab",
description: "An ancient Egyptian artefact symbolizing rebirth.",
location: "Cairo, Egypt",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact1.jpg",
price: 150,
},
{
id: 2,
name: "Aztec Sunstone",
description: "A replica of the Aztec calendar (inscriptions intact).",
location: "Peru",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact2.jpg",
price: 200,
},
{
id: 3,
name: "Medieval Chalice",
description: "Used by royalty in medieval ceremonies.",
location: "Cambridge, England",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact3.jpg",
price: 120,
},
{
id: 4,
name: "Roman Coin",
description: "An authentic Roman coin from the 2nd century CE.",
location: "Rome, Italy",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact4.jpg",
price: 80,
},
{
id: 5,
name: "Samurai Mask",
description: "Replica of Japanese Samurai battle masks.",
location: "Tokyo, Japan",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact5.jpg",
price: 300,
},
{
id: 6,
name: "Ancient Greek Vase",
description: "Depicts Greek mythology, found in the Acropolis.",
location: "Athens, Greece",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact6.jpg",
price: 250,
},
{
id: 7,
name: "Incan Pendant",
description: "Represents the Sun God Inti.",
location: "India",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact7.jpg",
price: 175,
},
{
id: 8,
name: "Persian Carpet Fragment",
description: "Ancient Persian artistry.",
location: "Petra, Jordan",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact8.jpg",
price: 400,
},
{
id: 9,
name: "Stone Buddha",
description: "Authentic stone Buddha carving.",
location: "India",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact9.jpg",
price: 220,
},
{
id: 10,
name: "Victorian Brooch",
description: "A beautiful Victorian-era brooch with a ruby centre.",
location: "Oxford, England",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact10.jpg",
price: 150,
},
{
id: 11,
name: "Ancient Scroll",
description: "A mysterious scroll from ancient times.",
location: "Madrid, Spain",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact11.jpg",
price: 500,
},
{
id: 12,
name: "Ming Dynasty Porcelain",
description: "Porcelain from China's Ming Dynasty.",
location: "Beijing, China",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact12.jpg",
price: 300,
},
{
id: 13,
name: "African Tribal Mask",
description: "A unique tribal mask from Africa.",
location: "Nigeria",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact13.jpg",
price: 250,
},
{
id: 14,
name: "Crystal Skull",
description: "A mystical pre-Columbian artefact.",
location: "Colombia",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact14.jpg",
price: 1000,
},
{
id: 15,
name: "Medieval Armor Fragment",
description: "A fragment of medieval armor.",
location: "Normandy, France",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact15.jpg",
price: 400,
},
{
id: 16,
name: "Medieval Helmet Fragment",
description: "A fragment of a medieval helmet.",
location: "Normandy, France",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact16.jpg",
price: 500,
},
];
export default function Shop() {
const [currentPage, setCurrentPage] = useState(1);
const [selectedArtefact, setSelectedArtefact] = useState<Artefact | null>(null);
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [artefactToBuy, setArtefactToBuy] = useState<Artefact | null>(null);
const [showThankYouModal, setShowThankYouModal] = useState(false);
const [orderNumber, setOrderNumber] = useState<string | null>(null);
const artefactsPerPage = 12;
const indexOfLastArtefact = currentPage * artefactsPerPage;
const indexOfFirstArtefact = indexOfLastArtefact - artefactsPerPage;
const currentArtefacts = artefacts.slice(indexOfFirstArtefact, indexOfLastArtefact);
const selectedCurrency = useStoreState((state) => state.currency.selectedCurrency);
const conversionRates = useStoreState((state) => state.currency.conversionRates);
const currencyTickers = useStoreState((state) => state.currency.tickers);
const convertPrice = useCallback(
(price: number, currency: Currency) => (price * conversionRates[currency]).toFixed(2),
[conversionRates]
);
const handleNextPage = () => {
if (indexOfLastArtefact < artefacts.length) setCurrentPage((prev) => prev + 1);
};
const handlePreviousPage = () => {
if (currentPage > 1) setCurrentPage((prev) => prev - 1);
};
function ArtefactCard({ artefact }: { artefact: Artefact }) {
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>
);
}
function Modal({ artefact }: { artefact: Artefact }) {
if (!artefact) return null;
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) setSelectedArtefact(null);
};
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">
<h3 className="text-2xl font-bold mb-4">{artefact.name}</h3>
<Image
src={artefact.image}
alt={artefact.name}
width={500}
height={300}
className="w-full h-64 object-cover rounded-md"
/>
<p className="text-xl font-bold">
{currencyTickers[selectedCurrency]}
{convertPrice(artefact.price, selectedCurrency)}
</p>
<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>
<div className="flex justify-end gap-4 mt-4 mr-2">
<button
onClick={() => {
setArtefactToBuy(artefact); // Set artefact for payment modal
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"
>
Buy
</button>
</div>
</div>
</div>
);
}
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;
}
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();
};
return (
<div
className="fixed inset-0 bg-neutral-900 bg-opacity-60 flex justify-center items-center z-10"
onClick={handleOverlayClick}
>
<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>
</div>
</div>
);
}
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>
);
}
return (
<div
className="min-h-screen relative flex flex-col"
style={{
backgroundImage: "url('/artefacts.jpg')",
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
<div className="absolute inset-0 bg-black bg-opacity-50 z-0"></div>
<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-white mb-2 tracking-tight drop-shadow-lg">
Artefact Shop
</h1>
<p className="text-lg md:text-xl text-center text-white mb-10 drop-shadow-md max-w-2xl">
Discover extraordinary historical artefacts and collectibles from major seismic events from around the world - now
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">
{currentArtefacts.map((artefact) => (
<ArtefactCard key={artefact.id} artefact={artefact} />
))}
</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"
}`}
>
&larr; Previous
</button>
<p className="mx-3 text-lg font-bold">{currentPage}</p>
<button
onClick={handleNextPage}
disabled={indexOfLastArtefact >= artefacts.length}
className={`mx-2 px-4 py-1 bg-blue-500 text-white rounded-md font-bold shadow-md ${
indexOfLastArtefact >= artefacts.length ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600"
}`}
>
Next &rarr;
</button>
</footer>
</div>
{selectedArtefact && <Modal artefact={selectedArtefact} />}
{artefactToBuy && showPaymentModal && (
<PaymentModal
artefact={artefactToBuy}
onClose={() => {
setShowPaymentModal(false);
setArtefactToBuy(null);
}}
/>
)}
{showThankYouModal && orderNumber && (
<ThankYouModal orderNumber={orderNumber} onClose={() => setShowThankYouModal(false)} />
)}
</div>
);
}