Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e85eac86fd | |||
| bc2b3d241d | |||
| 6b3788ba94 | |||
| f6b5a15c0d | |||
| e400fa136e | |||
| 8f29a36b53 | |||
| 556de0ecde | |||
| da4ec2f47c | |||
| 00584a1d4f | |||
| 7cff69690b | |||
| d48798e1b2 | |||
| 1bab3f3970 | |||
| 3bb2f03c5e | |||
| d3d32435eb | |||
| b6af4da5d8 | |||
| 0594b64cfa | |||
| 2c8efedae8 | |||
| 0a4f3411ac | |||
| 4a3404c5d6 | |||
| dc63decca6 | |||
| 83c1e1d82a | |||
| 33b5148223 | |||
| e67ad18b10 | |||
| 50a3150eb0 | |||
| 7054af8e0e | |||
| c683a12632 | |||
| 351873c4b4 | |||
| 2403481c12 | |||
| b2ab3c4107 | |||
| 35cec83ba5 | |||
| cf0bcfb2a5 | |||
| e8eafad650 | |||
| c5c06f9f26 | |||
| 33f9d0180b | |||
| 9a4f246016 | |||
| 18284e09b7 | |||
| 142497eedb | |||
| d53cf7da60 | |||
| 263e87b734 | |||
| 952da54721 | |||
| a7c5892cd8 | |||
| fc76992da8 | |||
| 2a26af8ce7 | |||
| 6ab1c42b08 | |||
| d9ea243858 | |||
| 235d478eca |
204
src/app/api/payment/process/route.ts
Normal file
204
src/app/api/payment/process/route.ts
Normal file
@@ -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<NextResponse<PaymentResponse>> {
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
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?_wi=2", 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>
|
||||
);
|
||||
}
|
||||
159
src/app/page.tsx
159
src/app/page.tsx
@@ -8,9 +8,21 @@ 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 } from 'lucide-react';
|
||||
import { Heart, Sparkles, Star, Camera, Clock, MapPin, Phone, Facebook, ShoppingCart } from 'lucide-react';
|
||||
|
||||
export default function LandingPage() {
|
||||
const handlePhoneClick = () => {
|
||||
window.location.href = 'tel:628984413';
|
||||
};
|
||||
|
||||
const handleFacebookClick = () => {
|
||||
window.open('https://facebook.com', '_blank');
|
||||
};
|
||||
|
||||
const handleOrderClick = () => {
|
||||
window.location.href = '/checkout';
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="icon-arrow"
|
||||
@@ -31,19 +43,20 @@ export default function LandingPage() {
|
||||
{ name: "Home", id: "home" },
|
||||
{ name: "Menu", id: "menu" },
|
||||
{ name: "Reviews", id: "reviews" },
|
||||
{ name: "Visit", id: "visit" }
|
||||
{ name: "Place", id: "visit" },
|
||||
{ name: "Checkout", id: "checkout" }
|
||||
]}
|
||||
button={{ text: "Order Now", href: "#contact" }}
|
||||
button={{ text: "Order Now", href: "/checkout" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="hero-section" data-section="hero-section">
|
||||
<HeroLogoBillboardSplit
|
||||
logoText="Fans Coffee & Bakery"
|
||||
description=""
|
||||
description="Premium Coffee & Artisanal Bakery | Calle del Dr. Esquerdo 180, Retiro Madrid | Near Pacífico Metro | €1-10"
|
||||
background={{ variant: "gradient-bars" }}
|
||||
buttons={[
|
||||
{ text: "Explore Our Menu", href: "#menu" },
|
||||
{ text: "Order Online", href: "/checkout" },
|
||||
{ text: "Get Directions", href: "#visit" }
|
||||
]}
|
||||
buttonAnimation="opacity"
|
||||
@@ -58,7 +71,7 @@ export default function LandingPage() {
|
||||
|
||||
<div id="signature-products" data-section="signature-products">
|
||||
<ProductCardThree
|
||||
title="Our Signature Favorites"
|
||||
title="OUR BEST PLATES "
|
||||
description=""
|
||||
tag="Most Loved"
|
||||
tagIcon={Heart}
|
||||
@@ -67,21 +80,28 @@ export default function LandingPage() {
|
||||
useInvertedBackground={false}
|
||||
products={[
|
||||
{
|
||||
id: "tres-leches", name: "Tarta tres leches", price: "€6.50", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AwJRpdoUBlhmoBlz25LKt9jMJl/uploaded-1773500286117-4ambvo9k.png", imageAlt: "Tres Leches cake slice", initialQuantity: 1
|
||||
id: "tres-leches", name: "Tarta Tres Leches", price: "€6.50", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AwJRpdoUBlhmoBlz25LKt9jMJl/uploaded-1773509807126-e9bn37wq.jpg", imageAlt: "Tres Leches cake slice - our signature dessert", initialQuantity: 1
|
||||
},
|
||||
{
|
||||
id: "cinnamon-roll", name: "Cinnamon Roll", price: "€4.50", imageSrc: "http://img.b2bpic.net/free-photo/traditional-cinnamon-rolls-dough-preparation-homemade-sweet-rolls_127032-2506.jpg", imageAlt: "Fresh cinnamon roll with frosting", initialQuantity: 1
|
||||
id: "palmeritas", name: "Palmeritas", price: "€3.00", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AwJRpdoUBlhmoBlz25LKt9jMJl/uploaded-1773509967040-g6gfn0n4.jpg", imageAlt: "Crispy Palmeritas pastry", initialQuantity: 1
|
||||
},
|
||||
{
|
||||
id: "specialty-coffee", name: "Frappuccino de galleta ", price: "€3.50", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AwJRpdoUBlhmoBlz25LKt9jMJl/uploaded-1773500356599-f2pk6pu9.png", imageAlt: "Espresso drink with latte art", initialQuantity: 1
|
||||
id: "cinnamon-roll", name: "Cinnamon Roll", price: "€4.50", imageSrc: "http://img.b2bpic.net/free-photo/traditional-cinnamon-rolls-dough-preparation-homemade-sweet-rolls_127032-2506.jpg?_wi=2", imageAlt: "Fresh cinnamon roll with frosting", initialQuantity: 1
|
||||
},
|
||||
{
|
||||
id: "breakfast-toast", name: "Tostada con salmón", price: "€5.00", imageSrc: "http://img.b2bpic.net/free-photo/plate-with-toast-vegetables-breakfast_23-2148574563.jpg", imageAlt: "Artisanal toast with premium toppings", initialQuantity: 1
|
||||
id: "specialty-coffee", name: "Frappuccino de Galleta", price: "€3.50", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AwJRpdoUBlhmoBlz25LKt9jMJl/uploaded-1773500356599-f2pk6pu9.png?_wi=1", imageAlt: "Specialty coffee drink with latte art", initialQuantity: 1
|
||||
},
|
||||
{
|
||||
id: "breakfast-toast", name: "Tostada con Jamón y Tomate", price: "€5.00", imageSrc: "http://img.b2bpic.net/free-photo/plate-with-toast-vegetables-breakfast_23-2148574563.jpg", imageAlt: "Breakfast toast with tomato and ham", initialQuantity: 1
|
||||
},
|
||||
{
|
||||
id: "coffee-drinks", name: "Premium Coffee Drinks", price: "€2.50-€4.00", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AwJRpdoUBlhmoBlz25LKt9jMJl/uploaded-1773510048778-46ij7zfs.webp", imageAlt: "Selection of specialty coffee drinks", initialQuantity: 1
|
||||
}
|
||||
]}
|
||||
gridVariant="two-columns-alternating-heights"
|
||||
animationType="slide-up"
|
||||
buttons={[
|
||||
{ text: "Go to Checkout", href: "/checkout" },
|
||||
{ text: "View Full Menu", href: "#menu" }
|
||||
]}
|
||||
buttonAnimation="opacity"
|
||||
@@ -91,7 +111,7 @@ export default function LandingPage() {
|
||||
<div id="why-fans" data-section="why-fans">
|
||||
<FeatureCardTwentyThree
|
||||
title="Why Fans Coffee & Bakery"
|
||||
description="More than just coffee and pastries – it's a neighborhood gathering place where quality meets warmth."
|
||||
description="Fans Coffee & Bakery It's one of the best business options because there's a wide variety of dishes, desserts, and drinks. Plus, it has the best service in all of Madrid because everyone here is happy, from the staff to the customers. "
|
||||
tag="What Makes Us Special"
|
||||
tagIcon={Sparkles}
|
||||
tagAnimation="opacity"
|
||||
@@ -99,16 +119,16 @@ export default function LandingPage() {
|
||||
useInvertedBackground={true}
|
||||
features={[
|
||||
{
|
||||
id: "exceptional-coffee", title: "Exceptional Coffee", tags: ["Premium Beans", "Expert Brewing"],
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/portrait-young-man-working-as-barista-making-coffee-restaurant-barista-apron-white-shirt-standing-with-cup-coffee-his-workplace-coffee-shop_574295-4957.jpg", imageAlt: "Barista preparing espresso"
|
||||
id: "exceptional-coffee", title: "Excellent Service", tags: ["", "", ""],
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/portrait-young-man-working-as-barista-making-coffee-restaurant-barista-apron-white-shirt-standing-with-cup-coffee-his-workplace-coffee-shop_574295-4957.jpg", imageAlt: "Barista preparing espresso with expertise"
|
||||
},
|
||||
{
|
||||
id: "artisanal-baking", title: "Artisanal Baking", tags: ["Daily Fresh", "Homemade Recipes"],
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/close-up-shot-caramel-vanilla-cupcakes-with-cream-chocolate-decoration-copyspace-food-eating-sugar-sweet-concept_7502-5464.jpg", imageAlt: "Pastry display"
|
||||
id: "artisanal-baking", title: "Artisanal Baking", tags: ["Daily Fresh", "Homemade Recipes", "Generous Portions"],
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/close-up-shot-caramel-vanilla-cupcakes-with-cream-chocolate-decoration-copyspace-food-eating-sugar-sweet-concept_7502-5464.jpg", imageAlt: "Fresh pastries and cakes display"
|
||||
},
|
||||
{
|
||||
id: "warm-service", title: "Warm Service", tags: ["Friendly Staff", "Community Vibe"],
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/man-white-long-sleeve-shirt-sitting-chair-cafe_250224-25.jpg", imageAlt: "Café interior"
|
||||
id: "cozy-atmosphere", title: "Cozy Atmosphere", tags: ["Friendly Staff", "Community Vibe", "Perfect After Health Center"],
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/man-white-long-sleeve-shirt-sitting-chair-cafe_250224-25.jpg", imageAlt: "Warm and welcoming café interior"
|
||||
}
|
||||
]}
|
||||
animationType="opacity"
|
||||
@@ -122,7 +142,7 @@ export default function LandingPage() {
|
||||
<div id="testimonials" data-section="testimonials">
|
||||
<TestimonialCardSixteen
|
||||
title="What Our Customers Love"
|
||||
description="Real words from real neighbors who make Fans part of their daily routine."
|
||||
description="Real reviews from Google Maps from our neighbors who visit us regularly for exceptional coffee, fresh pastries, and our warm, welcoming atmosphere near Pacífico metro."
|
||||
tag="Customer Reviews"
|
||||
tagIcon={Star}
|
||||
tagAnimation="opacity"
|
||||
@@ -130,28 +150,40 @@ export default function LandingPage() {
|
||||
useInvertedBackground={false}
|
||||
testimonials={[
|
||||
{
|
||||
id: "review-1", name: "María García", role: "Regular Customer", company: "Madrid Local", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/happy-woman-sitting-coffee-shop_273609-2867.jpg", imageAlt: "María García"
|
||||
id: "lucia-acosta", name: "Lucia Acosta", role: "", company: "\"A welcoming atmosphere perfect after health center visits\"", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/happy-woman-sitting-coffee-shop_273609-2867.jpg?_wi=1", imageAlt: "Lucia Acosta"
|
||||
},
|
||||
{
|
||||
id: "review-2", name: "Carlos López", role: "Coffee Enthusiast", company: "Pacífico Neighborhood", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/man-laughing-showing-ok-sign_23-2148859417.jpg", imageAlt: "Carlos López"
|
||||
id: "nacho-h", name: "Nacho H", role: "", company: "\"Excellent coffee and Tres Leches cake!\"", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/man-laughing-showing-ok-sign_23-2148859417.jpg?_wi=1", imageAlt: "Nacho H"
|
||||
},
|
||||
{
|
||||
id: "review-3", name: "Ana Martín", role: "Pastry Lover", company: "Weekend Visitor", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/smiley-woman-enjoying-cupcake_23-2148466342.jpg", imageAlt: "Ana Martín"
|
||||
id: "simena-arias", name: "Simena Arias", role: "", company: "\"Outstanding quality and service\"", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/smiley-woman-enjoying-cupcake_23-2148466342.jpg?_wi=1", imageAlt: "Simena Arias"
|
||||
},
|
||||
{
|
||||
id: "review-4", name: "Diego Ruiz", role: "Breakfast Regular", company: "Morning Commuter", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/man-kissing-her-wife-eating-kiwi-with-spoon_23-2147904407.jpg", imageAlt: "Diego Ruiz"
|
||||
id: "karina-boros", name: "Karina Boros", role: "", company: "\"The cinnamon rolls are divine!\"", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/man-kissing-her-wife-eating-kiwi-with-spoon_23-2147904407.jpg", imageAlt: "Karina Boros"
|
||||
},
|
||||
{
|
||||
id: "review-5", name: "Elena Fernández", role: "Cake Specialist", company: "Madrid Birthday Planner", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/smiley-beautiful-woman-enjoying-party_23-2148946123.jpg", imageAlt: "Elena Fernández"
|
||||
id: "silvia-quesada", name: "Silvia Quesada", role: "", company: "\"Breakfast with salmon and cheese perfection\"", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/smiley-beautiful-woman-enjoying-party_23-2148946123.jpg", imageAlt: "Silvia Quesada"
|
||||
},
|
||||
{
|
||||
id: "review-6", name: "Roberto Sánchez", role: "Comfort Seeker", company: "Post-Hospital Visitor", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/cheerful-young-student-casual-t-shirt-trendy-hat-listening-new-album-his-favourite-artist-earphones-using-online-music-app-cell-phone_273609-1964.jpg", imageAlt: "Roberto Sánchez"
|
||||
id: "guiomar-cas", name: "Guiomar Cas", role: "", company: "\"Love the Frapuccino and Palmeritas!\"", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/cheerful-young-student-casual-t-shirt-trendy-hat-listening-new-album-his-favourite-artist-earphones-using-online-music-app-cell-phone_273609-1964.jpg", imageAlt: "Guiomar Cas"
|
||||
},
|
||||
{
|
||||
id: "ingrid-bello", name: "Ingrid Bello", role: "", company: "\"Friendly staff and excellent toast quality\"", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/happy-woman-sitting-coffee-shop_273609-2867.jpg?_wi=2", imageAlt: "Ingrid Bello"
|
||||
},
|
||||
{
|
||||
id: "lola-nunez-prado", name: "Lola Núñez Prado", role: "", company: "\"Amazing coffee and bakery offerings!\"", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/man-laughing-showing-ok-sign_23-2148859417.jpg?_wi=2", imageAlt: "Lola Núñez Prado"
|
||||
},
|
||||
{
|
||||
id: "genesis-gonzalez", name: "genesis gonzalez", role: "", company: "\"Daniela and Maru provide excellent service!\"", rating: 5,
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/smiley-woman-enjoying-cupcake_23-2148466342.jpg?_wi=2", imageAlt: "genesis gonzalez"
|
||||
}
|
||||
]}
|
||||
kpiItems={[
|
||||
@@ -167,39 +199,14 @@ export default function LandingPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="gallery" data-section="gallery">
|
||||
<FeatureCardTwentyThree
|
||||
title="Our Space"
|
||||
description="A cozy corner near Pacífico where comfort meets elegance in every detail."
|
||||
tag="Gallery"
|
||||
tagIcon={Camera}
|
||||
tagAnimation="opacity"
|
||||
textboxLayout="default"
|
||||
useInvertedBackground={true}
|
||||
features={[
|
||||
{
|
||||
id: "interior-warm", title: "Warm Interior", tags: ["Cozy Seating", "Perfect Ambiance"],
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/ceiling-lamps_1203-791.jpg", imageAlt: "Warm café interior"
|
||||
},
|
||||
{
|
||||
id: "pastry-showcase", title: "Pastry Display", tags: ["Daily Fresh", "Premium Quality"],
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/mid-aged-couple-love-home_23-2147986279.jpg", imageAlt: "Pastry showcase"
|
||||
},
|
||||
{
|
||||
id: "coffee-bar", title: "Coffee Bar", tags: ["Expert Baristas", "Specialty Drinks"],
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/male-barista-coffee-shop-holding-cup-by-counter_1303-29323.jpg", imageAlt: "Coffee bar"
|
||||
}
|
||||
]}
|
||||
animationType="opacity"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="visit-location" data-section="visit-location">
|
||||
<ContactSplitForm
|
||||
title="Visit Us"
|
||||
description="Conveniently located near Pacífico metro. Stop by for your next coffee, cake, or breakfast."
|
||||
title="Visit Us Today"
|
||||
description="Located at Calle del Dr. Esquerdo 180, Retiro Madrid, conveniently near Pacífico metro. Perfect for ordering online, eating in, takeaway, or delivery. Open Mon-Fri 7am-8pm, Sat-Sun 8am-9pm. Call us at 628 98 44 13 or find us on Facebook."
|
||||
imageSrc="http://img.b2bpic.net/free-photo/woman-study-map-drinking-coffee-street-cafe-redhead-happy-girl-travel-canary-islands-looking-new-place-visit-sunny-day_1217-1756.jpg"
|
||||
imageAlt="Fans Coffee & Bakery location on map"
|
||||
imageAlt="Location map near Pacífico metro"
|
||||
mediaAnimation="blur-reveal"
|
||||
mediaPosition="right"
|
||||
useInvertedBackground={false}
|
||||
@@ -207,8 +214,8 @@ export default function LandingPage() {
|
||||
{ name: "name", type: "text", placeholder: "Your Name", required: true },
|
||||
{ name: "email", type: "email", placeholder: "Your Email", required: true }
|
||||
]}
|
||||
textarea={{ name: "message", placeholder: "Tell us what brings you in...", rows: 4 }}
|
||||
buttonText="Get Directions"
|
||||
textarea={{ name: "message", placeholder: "Tell us what brings you in (Dine-in, Takeaway, or Delivery)...", rows: 4 }}
|
||||
buttonText="Reserve / Order"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -217,32 +224,32 @@ export default function LandingPage() {
|
||||
columns={[
|
||||
{
|
||||
title: "Menu", items: [
|
||||
{ label: "Coffee", href: "#menu" },
|
||||
{ label: "Pastries", href: "#menu" },
|
||||
{ label: "Cakes", href: "#menu" },
|
||||
{ label: "Breakfast", href: "#menu" }
|
||||
{ label: "Coffee Drinks", href: "#menu" },
|
||||
{ label: "Pastries & Cakes", href: "#menu" },
|
||||
{ label: "Tres Leches", href: "#menu" },
|
||||
{ label: "Breakfast Toasts", href: "#menu" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "About", items: [
|
||||
{ label: "Our Story", href: "#about" },
|
||||
{ label: "Quality Promise", href: "#why-fans" },
|
||||
{ label: "Community", href: "#reviews" },
|
||||
{ label: "Careers", 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: "Pacífico, Madrid", href: "#visit" },
|
||||
{ label: "Mon-Fri 7am-8pm", href: "#" },
|
||||
{ label: "Sat-Sun 8am-9pm", href: "#" },
|
||||
{ label: "Instagram", href: "#" }
|
||||
{ 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."
|
||||
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