317 lines
12 KiB
TypeScript
317 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import Link from 'next/link';
|
|
import Image from 'next/image';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useCart } from '@/context/cart-context';
|
|
import { getAssetUrl } from '@/lib/directus';
|
|
import { PayPalCheckout } from '@/components/paypal-checkout';
|
|
import { getShippingCost } from '@/lib/shipping';
|
|
|
|
export default function CheckoutPage() {
|
|
const { items, subtotal, removeItem, itemCount, clearCart } = useCart();
|
|
const router = useRouter();
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [formValid, setFormValid] = useState(false);
|
|
const [formData, setFormData] = useState({
|
|
email: '',
|
|
firstName: '',
|
|
lastName: '',
|
|
address: '',
|
|
city: '',
|
|
postcode: '',
|
|
country: 'United Kingdom',
|
|
phone: '',
|
|
notes: '',
|
|
});
|
|
|
|
if (items.length === 0) {
|
|
return (
|
|
<div className="min-h-screen flex flex-col items-center justify-center py-20 pt-32">
|
|
<h1 className="font-serif text-2xl">Your cart is empty</h1>
|
|
<p className="mt-4 text-gray-600">Add some artwork to continue.</p>
|
|
<Link href="/store" className="mt-8 btn btn-primary">
|
|
Browse Store
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const handleFieldChange = (field: string, value: string) => {
|
|
const updated = { ...formData, [field]: value };
|
|
setFormData(updated);
|
|
const required = ['email', 'firstName', 'lastName', 'address', 'city', 'postcode'] as const;
|
|
setFormValid(required.every(f => updated[f].trim() !== ''));
|
|
};
|
|
|
|
const currency = items[0]?.artwork?.currency || 'GBP';
|
|
const currencySymbol = currency === 'GBP' ? '\u00a3' : '$';
|
|
const shippingCost = getShippingCost(formData.country, currency);
|
|
const total = subtotal + shippingCost;
|
|
|
|
const paypalItems = items.map(item => ({
|
|
artworkId: parseInt(item.id),
|
|
artworkName: item.title,
|
|
price: item.price,
|
|
currency: item.artwork.currency || 'GBP',
|
|
quantity: item.quantity,
|
|
}));
|
|
|
|
return (
|
|
<div className="min-h-screen bg-white pt-24">
|
|
{/* Header */}
|
|
<div className="border-b border-gray-200">
|
|
<div className="mx-auto max-w-7xl px-4 py-8">
|
|
<h1 className="font-serif text-3xl">Checkout</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mx-auto max-w-7xl px-4 py-12">
|
|
{error && (
|
|
<div className="mb-8 bg-red-50 border border-red-200 text-red-700 px-4 py-3 text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid gap-12 lg:grid-cols-2">
|
|
{/* Order Form */}
|
|
<div>
|
|
<h2 className="text-lg font-medium uppercase tracking-wider mb-6">
|
|
Contact Information
|
|
</h2>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm text-gray-600 mb-2">
|
|
Email Address
|
|
</label>
|
|
<input
|
|
type="email"
|
|
id="email"
|
|
required
|
|
value={formData.email}
|
|
onChange={(e) => handleFieldChange('email', e.target.value)}
|
|
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label htmlFor="firstName" className="block text-sm text-gray-600 mb-2">
|
|
First Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="firstName"
|
|
required
|
|
value={formData.firstName}
|
|
onChange={(e) => handleFieldChange('firstName', e.target.value)}
|
|
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="lastName" className="block text-sm text-gray-600 mb-2">
|
|
Last Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="lastName"
|
|
required
|
|
value={formData.lastName}
|
|
onChange={(e) => handleFieldChange('lastName', e.target.value)}
|
|
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 className="text-lg font-medium uppercase tracking-wider mt-12 mb-6">
|
|
Shipping Address
|
|
</h2>
|
|
|
|
<div>
|
|
<label htmlFor="address" className="block text-sm text-gray-600 mb-2">
|
|
Address
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="address"
|
|
required
|
|
value={formData.address}
|
|
onChange={(e) => handleFieldChange('address', e.target.value)}
|
|
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label htmlFor="city" className="block text-sm text-gray-600 mb-2">
|
|
City
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="city"
|
|
required
|
|
value={formData.city}
|
|
onChange={(e) => handleFieldChange('city', e.target.value)}
|
|
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="postcode" className="block text-sm text-gray-600 mb-2">
|
|
Postcode
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="postcode"
|
|
required
|
|
value={formData.postcode}
|
|
onChange={(e) => handleFieldChange('postcode', e.target.value)}
|
|
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="country" className="block text-sm text-gray-600 mb-2">
|
|
Country
|
|
</label>
|
|
<select
|
|
id="country"
|
|
value={formData.country}
|
|
onChange={(e) => handleFieldChange('country', e.target.value)}
|
|
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none bg-white"
|
|
>
|
|
<option>United Kingdom</option>
|
|
<option>Ireland</option>
|
|
<option>France</option>
|
|
<option>Germany</option>
|
|
<option>United States</option>
|
|
<option>Other</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="phone" className="block text-sm text-gray-600 mb-2">
|
|
Phone (optional)
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
id="phone"
|
|
value={formData.phone}
|
|
onChange={(e) => handleFieldChange('phone', e.target.value)}
|
|
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="notes" className="block text-sm text-gray-600 mb-2">
|
|
Order Notes (optional)
|
|
</label>
|
|
<textarea
|
|
id="notes"
|
|
rows={3}
|
|
value={formData.notes}
|
|
onChange={(e) => handleFieldChange('notes', e.target.value)}
|
|
className="w-full border border-gray-300 px-4 py-3 text-sm focus:border-gray-900 focus:outline-none resize-none"
|
|
placeholder="Any special instructions..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="pt-6">
|
|
{formValid ? (
|
|
<PayPalCheckout
|
|
items={paypalItems}
|
|
shipping={formData}
|
|
shippingCost={shippingCost}
|
|
currency={currency}
|
|
onSuccess={(orderId) => {
|
|
clearCart();
|
|
router.push(`/order-confirmation?id=${orderId}`);
|
|
}}
|
|
onError={(message) => {
|
|
setError(message);
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}}
|
|
/>
|
|
) : (
|
|
<div className="text-center py-4">
|
|
<p className="text-sm text-gray-500">
|
|
Please fill in all required fields above to proceed to payment.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Order Summary */}
|
|
<div className="lg:pl-8">
|
|
<div className="bg-gray-50 p-8">
|
|
<h2 className="text-lg font-medium uppercase tracking-wider mb-6">
|
|
Order Summary ({itemCount} {itemCount === 1 ? 'item' : 'items'})
|
|
</h2>
|
|
|
|
<ul className="divide-y divide-gray-200">
|
|
{items.map((item) => (
|
|
<li key={item.id} className="py-4 flex gap-4">
|
|
<div className="relative h-24 w-24 flex-shrink-0 bg-gray-100">
|
|
{item.image && (
|
|
<Image
|
|
src={getAssetUrl(item.image, { width: 200, quality: 80 })}
|
|
alt={item.title}
|
|
fill
|
|
className="object-cover"
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="flex-1">
|
|
<h3 className="text-sm font-medium">{item.title}</h3>
|
|
{item.artwork.medium && (
|
|
<p className="text-xs text-gray-500 mt-1">{item.artwork.medium}</p>
|
|
)}
|
|
<p className="text-sm font-medium mt-2">
|
|
{item.artwork.currency === 'GBP' ? '\u00a3' : '$'}{item.price.toLocaleString()}
|
|
</p>
|
|
<button
|
|
onClick={() => removeItem(item.id)}
|
|
className="text-xs text-gray-500 underline mt-2 hover:no-underline"
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<div className="border-t border-gray-200 mt-6 pt-6 space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span>Subtotal</span>
|
|
<span>{currencySymbol}{subtotal.toLocaleString()}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm text-gray-500">
|
|
<span>Shipping</span>
|
|
<span>{currencySymbol}{shippingCost.toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 mt-6 pt-6">
|
|
<div className="flex justify-between text-lg font-medium">
|
|
<span>Total</span>
|
|
<span>{currencySymbol}{total.toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 text-center">
|
|
<Link href="/store" className="text-sm underline underline-offset-2 hover:no-underline">
|
|
Continue Shopping
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|