Add src/app/checkout/page.tsx
This commit is contained in:
448
src/app/checkout/page.tsx
Normal file
448
src/app/checkout/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user