Add src/app/checkout/page.tsx

This commit is contained in:
2026-03-14 16:55:24 +00:00
parent e8eafad650
commit cf0bcfb2a5

448
src/app/checkout/page.tsx Normal file
View File

@@ -0,0 +1,448 @@
"use client"
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
import { useState } from 'react';
import { ShoppingCart, Trash2, Plus, Minus } from 'lucide-react';
import FooterBaseReveal from '@/components/sections/footer/FooterBaseReveal';
interface CartItem {
id: string;
name: string;
price: string;
quantity: number;
imageSrc: string;
imageAlt: string;
}
interface PaymentFormData {
cardName: string;
cardNumber: string;
expiryDate: string;
cvv: string;
email: string;
phone: string;
}
export default function CheckoutPage() {
const [cartItems, setCartItems] = useState<CartItem[]>([
{
id: "tres-leches", name: "Tarta Tres Leches", price: "€6.50", quantity: 1,
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AwJRpdoUBlhmoBlz25LKt9jMJl/uploaded-1773500286117-4ambvo9k.png", imageAlt: "Tres Leches cake slice"
},
{
id: "specialty-coffee", name: "Frappuccino de Galleta", price: "€3.50", quantity: 2,
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AwJRpdoUBlhmoBlz25LKt9jMJl/uploaded-1773500356599-f2pk6pu9.png?_wi=1", imageAlt: "Specialty coffee drink"
}
]);
const [paymentFormData, setPaymentFormData] = useState<PaymentFormData>({
cardName: '',
cardNumber: '',
expiryDate: '',
cvv: '',
email: '',
phone: ''
});
const [isProcessing, setIsProcessing] = useState(false);
const [paymentStatus, setPaymentStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState('');
const extractNumericPrice = (priceStr: string): number => {
const match = priceStr.match(/\d+(\.\d+)?/);
return match ? parseFloat(match[0]) : 0;
};
const subtotal = cartItems.reduce((sum, item) => {
return sum + (extractNumericPrice(item.price) * item.quantity);
}, 0);
const tax = subtotal * 0.1;
const total = subtotal + tax;
const handleQuantityChange = (id: string, newQuantity: number) => {
if (newQuantity <= 0) return;
setCartItems(cartItems.map(item =>
item.id === id ? { ...item, quantity: newQuantity } : item
));
};
const handleRemoveItem = (id: string) => {
setCartItems(cartItems.filter(item => item.id !== id));
};
const handlePaymentInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setPaymentFormData(prev => ({
...prev,
[name]: value
}));
};
const validatePaymentForm = (): boolean => {
if (!paymentFormData.cardName.trim()) {
setErrorMessage('Cardholder name is required');
return false;
}
if (!paymentFormData.cardNumber.replace(/\s/g, '').match(/^\d{13,19}$/)) {
setErrorMessage('Invalid card number');
return false;
}
if (!paymentFormData.expiryDate.match(/^\d{2}\/\d{2}$/)) {
setErrorMessage('Expiry date must be MM/YY format');
return false;
}
if (!paymentFormData.cvv.match(/^\d{3,4}$/)) {
setErrorMessage('Invalid CVV');
return false;
}
if (!paymentFormData.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
setErrorMessage('Invalid email address');
return false;
}
if (!paymentFormData.phone.trim()) {
setErrorMessage('Phone number is required');
return false;
}
return true;
};
const handlePaymentSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErrorMessage('');
setPaymentStatus('idle');
if (!validatePaymentForm()) {
setPaymentStatus('error');
return;
}
if (cartItems.length === 0) {
setErrorMessage('Your cart is empty');
setPaymentStatus('error');
return;
}
setIsProcessing(true);
try {
const response = await fetch('/api/payment/process', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
paymentMethod: 'card',
cardName: paymentFormData.cardName,
cardNumber: paymentFormData.cardNumber.replace(/\s/g, ''),
expiryDate: paymentFormData.expiryDate,
cvv: paymentFormData.cvv,
email: paymentFormData.email,
phone: paymentFormData.phone,
amount: total,
currency: 'EUR',
items: cartItems,
orderDetails: {
subtotal: subtotal.toFixed(2),
tax: tax.toFixed(2),
total: total.toFixed(2)
}
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || 'Payment processing failed');
}
const data = await response.json();
setPaymentStatus('success');
setCartItems([]);
setPaymentFormData({ cardName: '', cardNumber: '', expiryDate: '', cvv: '', email: '', phone: '' });
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'Payment processing failed. Please try again.');
setPaymentStatus('error');
} finally {
setIsProcessing(false);
}
};
return (
<ThemeProvider
defaultButtonVariant="icon-arrow"
defaultTextAnimation="entrance-slide"
borderRadius="rounded"
contentWidth="mediumSmall"
sizing="largeSmallSizeLargeTitles"
background="blurBottom"
cardStyle="layered-gradient"
primaryButtonStyle="diagonal-gradient"
secondaryButtonStyle="layered"
headingFontWeight="semibold"
>
<div id="nav" data-section="nav">
<NavbarStyleCentered
brandName="Fans Coffee & Bakery"
navItems={[
{ name: "Home", id: "home" },
{ name: "Menu", id: "menu" },
{ name: "Reviews", id: "reviews" },
{ name: "Place", id: "visit" }
]}
button={{ text: "Back to Shop", href: "/" }}
/>
</div>
<div className="min-h-screen bg-gradient-to-b from-background to-card py-12 px-4">
<div className="max-w-6xl mx-auto">
<div className="mb-8">
<h1 className="text-4xl font-bold text-foreground mb-2 flex items-center gap-2">
<ShoppingCart className="w-8 h-8" />
Checkout
</h1>
<p className="text-foreground/70">Review your order and complete payment</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Cart Items Section */}
<div className="lg:col-span-2">
<div className="bg-card rounded-lg p-6 border border-foreground/10 shadow-lg">
<h2 className="text-2xl font-semibold text-foreground mb-4">Order Summary</h2>
{cartItems.length === 0 ? (
<div className="text-center py-8">
<ShoppingCart className="w-12 h-12 text-foreground/30 mx-auto mb-4" />
<p className="text-foreground/70">Your cart is empty</p>
</div>
) : (
<div className="space-y-4">
{cartItems.map(item => (
<div key={item.id} className="flex gap-4 bg-background/50 p-4 rounded-lg border border-foreground/5">
<img
src={item.imageSrc}
alt={item.imageAlt}
className="w-24 h-24 object-cover rounded-lg"
/>
<div className="flex-1">
<h3 className="font-semibold text-foreground">{item.name}</h3>
<p className="text-primary-cta font-bold">{item.price}</p>
<div className="flex items-center gap-2 mt-2">
<button
onClick={() => handleQuantityChange(item.id, item.quantity - 1)}
className="p-1 hover:bg-foreground/10 rounded"
aria-label="Decrease quantity"
>
<Minus className="w-4 h-4" />
</button>
<span className="w-8 text-center">{item.quantity}</span>
<button
onClick={() => handleQuantityChange(item.id, item.quantity + 1)}
className="p-1 hover:bg-foreground/10 rounded"
aria-label="Increase quantity"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
<div className="text-right flex flex-col justify-between">
<p className="font-semibold text-foreground">
{(extractNumericPrice(item.price) * item.quantity).toFixed(2)}
</p>
<button
onClick={() => handleRemoveItem(item.id)}
className="text-red-500 hover:text-red-600 p-1"
aria-label="Remove item"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Payment Form and Summary */}
<div className="lg:col-span-1">
{/* Order Totals */}
<div className="bg-card rounded-lg p-6 border border-foreground/10 shadow-lg mb-6">
<h3 className="text-xl font-semibold text-foreground mb-4">Order Total</h3>
<div className="space-y-2 mb-4 pb-4 border-b border-foreground/10">
<div className="flex justify-between text-foreground/70">
<span>Subtotal:</span>
<span>{subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between text-foreground/70">
<span>Tax (10%):</span>
<span>{tax.toFixed(2)}</span>
</div>
</div>
<div className="flex justify-between text-xl font-bold text-primary-cta">
<span>Total:</span>
<span>{total.toFixed(2)}</span>
</div>
</div>
{/* Payment Form */}
<form onSubmit={handlePaymentSubmit} className="bg-card rounded-lg p-6 border border-foreground/10 shadow-lg">
<h3 className="text-lg font-semibold text-foreground mb-4">Payment Details</h3>
{paymentStatus === 'success' && (
<div className="mb-4 p-4 bg-green-500/20 border border-green-500 rounded-lg text-green-700">
Payment processed successfully! Your order has been received.
</div>
)}
{paymentStatus === 'error' && errorMessage && (
<div className="mb-4 p-4 bg-red-500/20 border border-red-500 rounded-lg text-red-700">
{errorMessage}
</div>
)}
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-foreground mb-1">Cardholder Name</label>
<input
type="text"
name="cardName"
value={paymentFormData.cardName}
onChange={handlePaymentInputChange}
placeholder="John Doe"
className="w-full px-3 py-2 bg-background border border-foreground/20 rounded-lg text-foreground placeholder-foreground/50 focus:outline-none focus:border-primary-cta"
disabled={isProcessing}
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">Card Number</label>
<input
type="text"
name="cardNumber"
value={paymentFormData.cardNumber}
onChange={(e) => {
const value = e.target.value.replace(/\s/g, '').slice(0, 19);
const formatted = value.replace(/(\d{4})(?=\d)/g, '$1 ');
setPaymentFormData(prev => ({ ...prev, cardNumber: formatted }));
}}
placeholder="1234 5678 9012 3456"
className="w-full px-3 py-2 bg-background border border-foreground/20 rounded-lg text-foreground placeholder-foreground/50 focus:outline-none focus:border-primary-cta"
disabled={isProcessing}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-sm font-medium text-foreground mb-1">MM/YY</label>
<input
type="text"
name="expiryDate"
value={paymentFormData.expiryDate}
onChange={(e) => {
let value = e.target.value.replace(/\D/g, '').slice(0, 4);
if (value.length >= 2) {
value = value.slice(0, 2) + '/' + value.slice(2);
}
setPaymentFormData(prev => ({ ...prev, expiryDate: value }));
}}
placeholder="12/25"
className="w-full px-3 py-2 bg-background border border-foreground/20 rounded-lg text-foreground placeholder-foreground/50 focus:outline-none focus:border-primary-cta"
disabled={isProcessing}
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">CVV</label>
<input
type="text"
name="cvv"
value={paymentFormData.cvv}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 4);
setPaymentFormData(prev => ({ ...prev, cvv: value }));
}}
placeholder="123"
className="w-full px-3 py-2 bg-background border border-foreground/20 rounded-lg text-foreground placeholder-foreground/50 focus:outline-none focus:border-primary-cta"
disabled={isProcessing}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">Email</label>
<input
type="email"
name="email"
value={paymentFormData.email}
onChange={handlePaymentInputChange}
placeholder="customer@example.com"
className="w-full px-3 py-2 bg-background border border-foreground/20 rounded-lg text-foreground placeholder-foreground/50 focus:outline-none focus:border-primary-cta"
disabled={isProcessing}
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">Phone</label>
<input
type="tel"
name="phone"
value={paymentFormData.phone}
onChange={handlePaymentInputChange}
placeholder="+34 628 98 44 13"
className="w-full px-3 py-2 bg-background border border-foreground/20 rounded-lg text-foreground placeholder-foreground/50 focus:outline-none focus:border-primary-cta"
disabled={isProcessing}
/>
</div>
<button
type="submit"
disabled={isProcessing || cartItems.length === 0}
className="w-full mt-6 px-4 py-3 bg-primary-cta text-white font-semibold rounded-lg hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
>
{isProcessing ? 'Processing...' : `Pay €${total.toFixed(2)}`}
</button>
<p className="text-xs text-foreground/50 text-center mt-3">
💳 Secure payment. Your data is encrypted.
</p>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="footer" data-section="footer">
<FooterBaseReveal
columns={[
{
title: "Menu", items: [
{ label: "Coffee Drinks", href: "/" },
{ label: "Pastries & Cakes", href: "/" },
{ label: "Tres Leches", href: "/" },
{ label: "Breakfast Toasts", href: "/" }
]
},
{
title: "Visit", items: [
{ label: "Calle del Dr. Esquerdo 180, Retiro", href: "#visit" },
{ label: "Near Pacífico Metro", href: "#visit" },
{ label: "Mon-Fri 7am-8pm", href: "#" },
{ label: "Sat-Sun 8am-9pm", href: "#" }
]
},
{
title: "Contact", items: [
{ label: "Phone: 628 98 44 13", href: "tel:628984413" },
{ label: "Order Online", href: "/checkout" },
{ label: "Dine-In | Takeaway | Delivery", href: "#visit" },
{ label: "Follow on Facebook", href: "https://facebook.com" }
]
}
]}
copyrightText="© 2025 Fans Coffee & Bakery. All rights reserved. Quality Coffee & Artisanal Bakery near Pacífico, Madrid."
/>
</div>
</ThemeProvider>
);
}