From e8eafad65050ebdd2d9022158bc36dd325c1b532 Mon Sep 17 00:00:00 2001 From: bender Date: Sat, 14 Mar 2026 16:55:23 +0000 Subject: [PATCH 1/3] Add src/app/api/payment/process/route.ts --- src/app/api/payment/process/route.ts | 204 +++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 src/app/api/payment/process/route.ts diff --git a/src/app/api/payment/process/route.ts b/src/app/api/payment/process/route.ts new file mode 100644 index 0000000..8017575 --- /dev/null +++ b/src/app/api/payment/process/route.ts @@ -0,0 +1,204 @@ +import { NextRequest, NextResponse } from 'next/server'; + +interface PaymentRequest { + paymentMethod: string; + cardName: string; + cardNumber: string; + expiryDate: string; + cvv: string; + email: string; + phone: string; + amount: number; + currency: string; + items: Array<{ + id: string; + name: string; + price: string; + quantity: number; + }>; + orderDetails: { + subtotal: string; + tax: string; + total: string; + }; +} + +interface PaymentResponse { + success: boolean; + transactionId?: string; + message: string; + order?: { + id: string; + amount: number; + currency: string; + status: string; + }; +} + +function validatePaymentData(data: PaymentRequest): { valid: boolean; error?: string } { + if (!data.cardName?.trim()) { + return { valid: false, error: 'Cardholder name is required' }; + } + + const cardNumberDigits = data.cardNumber.replace(/\D/g, ''); + if (!/^\d{13,19}$/.test(cardNumberDigits)) { + return { valid: false, error: 'Invalid card number' }; + } + + if (!/^\d{2}\/\d{2}$/.test(data.expiryDate)) { + return { valid: false, error: 'Invalid expiry date format' }; + } + + if (!/^\d{3,4}$/.test(data.cvv)) { + return { valid: false, error: 'Invalid CVV' }; + } + + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) { + return { valid: false, error: 'Invalid email address' }; + } + + if (!data.phone?.trim()) { + return { valid: false, error: 'Phone number is required' }; + } + + if (data.amount <= 0) { + return { valid: false, error: 'Invalid payment amount' }; + } + + return { valid: true }; +} + +function luhnCheck(cardNumber: string): boolean { + const digits = cardNumber.replace(/\D/g, ''); + let sum = 0; + let isEven = false; + + for (let i = digits.length - 1; i >= 0; i--) { + let digit = parseInt(digits[i], 10); + + if (isEven) { + digit *= 2; + if (digit > 9) { + digit -= 9; + } + } + + sum += digit; + isEven = !isEven; + } + + return sum % 10 === 0; +} + +function generateTransactionId(): string { + return `TXN-${Date.now()}-${Math.random().toString(36).substr(2, 9).toUpperCase()}`; +} + +export async function POST(request: NextRequest): Promise> { + try { + const data: PaymentRequest = await request.json(); + + // Validate payment data + const validation = validatePaymentData(data); + if (!validation.valid) { + return NextResponse.json( + { + success: false, + message: validation.error || 'Validation failed' + }, + { status: 400 } + ); + } + + // Validate card using Luhn algorithm + if (!luhnCheck(data.cardNumber)) { + return NextResponse.json( + { + success: false, + message: 'Invalid card number' + }, + { status: 400 } + ); + } + + // Validate expiry date + const [month, year] = data.expiryDate.split('/'); + const expiryDate = new Date(2000 + parseInt(year), parseInt(month) - 1); + if (expiryDate < new Date()) { + return NextResponse.json( + { + success: false, + message: 'Card has expired' + }, + { status: 400 } + ); + } + + // Validate items and amount + if (!data.items || data.items.length === 0) { + return NextResponse.json( + { + success: false, + message: 'No items in order' + }, + { status: 400 } + ); + } + + // In production, integrate with actual payment gateway here + // This is a mock processing example + const transactionId = generateTransactionId(); + + // Simulate payment processing delay + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Mock fraud check - reject cards ending in 0002 + const lastFourDigits = data.cardNumber.replace(/\D/g, '').slice(-4); + if (lastFourDigits === '0002') { + return NextResponse.json( + { + success: false, + message: 'Payment declined. Please try another card.' + }, + { status: 402 } + ); + } + + // Log order for email notification + console.log('Payment Processing Log:', { + transactionId, + amount: data.amount, + currency: data.currency, + cardLast4: lastFourDigits, + email: data.email, + phone: data.phone, + itemCount: data.items.length, + timestamp: new Date().toISOString() + }); + + return NextResponse.json( + { + success: true, + transactionId, + message: 'Payment processed successfully', + order: { + id: transactionId, + amount: data.amount, + currency: data.currency, + status: 'completed' + } + }, + { status: 200 } + ); + } catch (error) { + console.error('Payment processing error:', error); + + return NextResponse.json( + { + success: false, + message: 'Payment processing failed. Please try again.' + }, + { status: 500 } + ); + } +} -- 2.49.1 From cf0bcfb2a586d7c15b967a1a09eb4def88bfa178 Mon Sep 17 00:00:00 2001 From: bender Date: Sat, 14 Mar 2026 16:55:24 +0000 Subject: [PATCH 2/3] Add src/app/checkout/page.tsx --- src/app/checkout/page.tsx | 448 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 448 insertions(+) create mode 100644 src/app/checkout/page.tsx diff --git a/src/app/checkout/page.tsx b/src/app/checkout/page.tsx new file mode 100644 index 0000000..f4e1ce1 --- /dev/null +++ b/src/app/checkout/page.tsx @@ -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([ + { + 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({ + 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) => { + 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) => { + 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 ( + + + +
+
+
+

+ + Checkout +

+

Review your order and complete payment

+
+ +
+ {/* Cart Items Section */} +
+
+

Order Summary

+ + {cartItems.length === 0 ? ( +
+ +

Your cart is empty

+
+ ) : ( +
+ {cartItems.map(item => ( +
+ {item.imageAlt} +
+

{item.name}

+

{item.price}

+
+ + {item.quantity} + +
+
+
+

+ €{(extractNumericPrice(item.price) * item.quantity).toFixed(2)} +

+ +
+
+ ))} +
+ )} +
+
+ + {/* Payment Form and Summary */} +
+ {/* Order Totals */} +
+

Order Total

+
+
+ Subtotal: + €{subtotal.toFixed(2)} +
+
+ Tax (10%): + €{tax.toFixed(2)} +
+
+
+ Total: + €{total.toFixed(2)} +
+
+ + {/* Payment Form */} +
+

Payment Details

+ + {paymentStatus === 'success' && ( +
+ ✓ Payment processed successfully! Your order has been received. +
+ )} + + {paymentStatus === 'error' && errorMessage && ( +
+ ✗ {errorMessage} +
+ )} + +
+
+ + +
+ +
+ + { + 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} + /> +
+ +
+
+ + { + 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} + /> +
+
+ + { + 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} + /> +
+
+ +
+ + +
+ +
+ + +
+ + + +

+ 💳 Secure payment. Your data is encrypted. +

+
+
+
+
+
+
+ + +
+ ); +} \ No newline at end of file -- 2.49.1 From 35cec83ba580c5be8223cb66e8ed057e0b5315ad Mon Sep 17 00:00:00 2001 From: bender Date: Sat, 14 Mar 2026 16:55:24 +0000 Subject: [PATCH 3/3] Update src/app/page.tsx --- src/app/page.tsx | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index b6fb977..6eff646 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,7 +8,7 @@ import FeatureCardTwentyThree from '@/components/sections/feature/FeatureCardTwe import TestimonialCardSixteen from '@/components/sections/testimonial/TestimonialCardSixteen'; import ContactSplitForm from '@/components/sections/contact/ContactSplitForm'; import FooterBaseReveal from '@/components/sections/footer/FooterBaseReveal'; -import { Heart, Sparkles, Star, Camera, Clock, MapPin, Phone, Facebook } from 'lucide-react'; +import { Heart, Sparkles, Star, Camera, Clock, MapPin, Phone, Facebook, ShoppingCart } from 'lucide-react'; export default function LandingPage() { const handlePhoneClick = () => { @@ -19,6 +19,10 @@ export default function LandingPage() { window.open('https://facebook.com', '_blank'); }; + const handleOrderClick = () => { + window.location.href = '/checkout'; + }; + return ( @@ -51,7 +56,7 @@ export default function LandingPage() { description="Premium Coffee & Artisanal Bakery | Calle del Dr. Esquerdo 180, Retiro Madrid | Near Pacífico Metro | €1-10" background={{ variant: "gradient-bars" }} buttons={[ - { text: "Order Online", href: "#order" }, + { text: "Order Online", href: "/checkout" }, { text: "Get Directions", href: "#visit" } ]} buttonAnimation="opacity" @@ -67,7 +72,7 @@ export default function LandingPage() {
); -} +} \ No newline at end of file -- 2.49.1