Fixed shop and added sending to api route

This commit is contained in:
Tim Howitz 2025-06-02 07:57:50 +01:00
parent bfdb665c41
commit 976eaf7653

View File

@ -1,11 +1,12 @@
"use client";
import Image from 'next/image';
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
import axios from "axios";
import Image from "next/image";
import { Dispatch, SetStateAction, 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';
import { ExtendedArtefact } from "@appTypes/ApiTypes";
import { Currency } from "@appTypes/StoreModel";
import BottomFooter from "@components/BottomFooter";
import { useStoreState } from "@hooks/store";
interface SuperExtendedArtefact extends ExtendedArtefact {
location: string;
@ -18,9 +19,12 @@ export default function Shop() {
const [artefacts, setArtefacts] = useState<SuperExtendedArtefact[]>([]);
const [hiddenArtefactIds, setHiddenArtefactIds] = useState<number[]>([]);
const [loading, setLoading] = useState(true);
const [cart, setCart] = useState<SuperExtendedArtefact[]>([]);
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);
@ -50,6 +54,7 @@ export default function Shop() {
const [selectedArtefact, setSelectedArtefact] = useState<SuperExtendedArtefact | null>(null);
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [artefactToBuy, setArtefactToBuy] = useState<SuperExtendedArtefact | null>(null);
const [cartCheckout, setCartCheckout] = useState(false); // true = checkout cart (not single artefact)
const [showThankYouModal, setShowThankYouModal] = useState(false);
const [orderNumber, setOrderNumber] = useState<string | null>(null);
@ -93,11 +98,14 @@ export default function Shop() {
</div>
);
}
function Modal({ artefact }: { artefact: SuperExtendedArtefact }) {
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 (
<div
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
@ -121,16 +129,32 @@ export default function Shop() {
<p className="text-neutral-500 mb-2">{artefact.earthquakeCode}</p>
<p className="text-neutral-500 mb-2">{artefact.type}</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
onClick={() => {
setArtefactToBuy(artefact); // Set artefact for payment modal
setShowPaymentModal(true); // Show payment modal
setSelectedArtefact(null); // Close this modal
if (!inCart) setCart((cart) => [...cart, artefact]);
}}
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>
</div>
</div>
@ -138,15 +162,99 @@ export default function Shop() {
);
}
function PaymentModal({ artefact, onClose }: { artefact: SuperExtendedArtefact; 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?: SuperExtendedArtefact;
onClose: () => void;
cartItems?: SuperExtendedArtefact[];
}) {
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 +270,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");
const paymentEmail = user?.email || email;
if (!validateEmail(paymentEmail)) {
setError("Please enter a valid email");
return;
}
} else {
return;
}
if (!validateCardNumber(cardNumber)) {
setError("Card number must be 12-19 digits.");
return;
@ -186,44 +289,61 @@ export default function Shop() {
setError("CVC must be 3 or 4 digits.");
return;
}
setHiddenArtefactIds((ids) => [...ids, artefact.id]);
// todo!! create receiving api route
// todo!! handle sending to api route
// 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
const genOrder = () => "#" + Math.random().toString(36).substring(2, 10).toUpperCase();
setOrderNumber(genOrder());
const orderNum = genOrder();
// todo add display of error
(async () => {
try {
const response = await axios.post("/api/shop/purchase", artefactsToBuy);
setOrderNumber(orderNum);
onClose();
setShowThankYouModal(true);
setCart((c) => c.filter((a) => !artefactsToBuy.map((x) => x.id).includes(a.id)));
} catch (error) {
console.error("Error posting artefacts:", error);
throw error;
}
})();
}
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"
className="fixed inset-0 bg-neutral-900 bg-opacity-60 flex justify-center items-center z-[12000]"
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... */}
<h2 className="text-2xl font-bold mb-4">
Checkout {artefact ? artefact.name : artefactsToBuy.length + " item(s)"}
{!artefact && <span className="ml-1">({artefactsToBuy.map((x) => x.name).join(", ")})</span>}
</h2>
<form
onSubmit={(e) => {
e.preventDefault();
handlePay();
}}
>
{!user ? (
{/* Email autofill */}
<input
className="w-full mb-2 px-3 py-2 border rounded"
placeholder="Email Address"
value={email}
value={user?.email ? user.email : email}
onChange={(e) => setEmail(e.target.value)}
type="email"
required
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
className="w-full mb-2 px-3 py-2 border rounded"
placeholder="Cardholder Name"
@ -266,6 +386,13 @@ export default function Shop() {
Remember me
</label>
{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">
<button type="button" onClick={onClose} className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md mr-2">
Cancel
@ -310,6 +437,7 @@ export default function Shop() {
}}
>
<div className="absolute inset-0 bg-black bg-opacity-0 z-0"></div>
{/* --- Cart Button fixed at top right --- */}
<button
className="absolute top-6 left-6 z-40 bg-white border border-blue-500 shadow-lg rounded-full p-3 hover:bg-blue-100 flex flex-row items-center"
onClick={() => setShowCartModal(true)}
@ -369,19 +497,22 @@ export default function Shop() {
</footer>
</div>
{selectedArtefact && <Modal artefact={selectedArtefact} />}
{artefactToBuy && showPaymentModal && (
{showCartModal && <CartModal />}
{showPaymentModal && (cartCheckout || artefactToBuy) && (
<PaymentModal
artefact={artefactToBuy}
artefact={cartCheckout ? undefined : artefactToBuy!}
cartItems={cartCheckout ? cart : undefined}
onClose={() => {
setShowPaymentModal(false);
setArtefactToBuy(null);
setCartCheckout(false);
}}
/>
)}
{showThankYouModal && orderNumber && (
<ThankYouModal orderNumber={orderNumber} onClose={() => setShowThankYouModal(false)} />
)}
{!selectedArtefact && !showPaymentModal && !showThankYouModal && (
{!selectedArtefact && !showPaymentModal && !showThankYouModal && !showCartModal && (
<div className="relative z-50">
<BottomFooter />
</div>