337 lines
12 KiB
JavaScript
337 lines
12 KiB
JavaScript
import { useEffect, useRef, useState } from 'react';
|
||
import Head from 'next/head';
|
||
|
||
import SiteLayout from '../components/SiteLayout';
|
||
import { submitOrder } from '../lib/api';
|
||
import { useCart } from '../context/CartContext';
|
||
|
||
const INITIAL_CUSTOMER = {
|
||
name: '',
|
||
email: '',
|
||
phone: '',
|
||
fulfillment_method: 'pickup',
|
||
scheduled_date: '',
|
||
scheduled_time_slot: '',
|
||
delivery_address_line1: '',
|
||
delivery_city: '',
|
||
delivery_state: 'WI',
|
||
delivery_postal_code: '',
|
||
override_code: '',
|
||
notes: '',
|
||
};
|
||
|
||
export default function CartPage() {
|
||
const { items, updateQuantity, removeItem, clearCart, subtotal } = useCart();
|
||
const [customer, setCustomer] = useState(INITIAL_CUSTOMER);
|
||
const [notice, setNotice] = useState(null);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [confirmation, setConfirmation] = useState(null);
|
||
const timeoutRef = useRef(null);
|
||
|
||
useEffect(() => () => clearTimeout(timeoutRef.current), []);
|
||
|
||
const showNotice = (message, variant = 'info') => {
|
||
clearTimeout(timeoutRef.current);
|
||
setNotice({ message, variant });
|
||
timeoutRef.current = setTimeout(() => setNotice(null), 4000);
|
||
};
|
||
|
||
const handleQuantity = (id, delta) => {
|
||
const entry = items.find((item) => item.id === id);
|
||
if (!entry) return;
|
||
const limit = Number.isFinite(entry.limit) ? entry.limit : Infinity;
|
||
const next = Math.max(0, Math.min(entry.quantity + delta, limit));
|
||
updateQuantity(id, next);
|
||
};
|
||
|
||
const deliveryFee = customer.fulfillment_method === 'delivery' ? 5 : 0;
|
||
const total = subtotal + deliveryFee;
|
||
const hasCartItems = items.length > 0;
|
||
|
||
const updateCustomerField = (field, value) => {
|
||
setCustomer((prev) => ({ ...prev, [field]: value }));
|
||
};
|
||
|
||
const handleSubmit = async (event) => {
|
||
event.preventDefault();
|
||
if (!hasCartItems) {
|
||
showNotice('Add at least one item to your cart before checking out.', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!customer.name.trim() || !customer.email.trim()) {
|
||
showNotice('Name and email are required.', 'error');
|
||
return;
|
||
}
|
||
|
||
if (customer.fulfillment_method === 'delivery') {
|
||
if (!customer.delivery_address_line1.trim() || !customer.delivery_city.trim() || !customer.delivery_postal_code.trim()) {
|
||
showNotice('Delivery address, city, and postal code are required for delivery orders.', 'error');
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (items.some((item) => !item.id)) {
|
||
showNotice('Cart contains placeholder items. Add products from the live menu once available.', 'info');
|
||
return;
|
||
}
|
||
|
||
const payload = {
|
||
customer_name: customer.name.trim(),
|
||
customer_email: customer.email.trim(),
|
||
customer_phone: customer.phone.trim(),
|
||
fulfillment_method: customer.fulfillment_method,
|
||
scheduled_date: customer.scheduled_date || null,
|
||
scheduled_time_slot: customer.scheduled_time_slot,
|
||
delivery_address_line1:
|
||
customer.fulfillment_method === 'delivery' ? customer.delivery_address_line1.trim() : '',
|
||
delivery_address_line2: '',
|
||
delivery_city: customer.fulfillment_method === 'delivery' ? customer.delivery_city.trim() : '',
|
||
delivery_state: customer.fulfillment_method === 'delivery' ? customer.delivery_state.trim() : '',
|
||
delivery_postal_code:
|
||
customer.fulfillment_method === 'delivery' ? customer.delivery_postal_code.trim() : '',
|
||
delivery_override_code: customer.override_code.trim() || null,
|
||
notes: customer.notes,
|
||
items: items.map((item) => ({ product: item.id, quantity: item.quantity })),
|
||
};
|
||
|
||
try {
|
||
setSubmitting(true);
|
||
const order = await submitOrder(payload);
|
||
setConfirmation(order.id);
|
||
clearCart();
|
||
setCustomer(INITIAL_CUSTOMER);
|
||
showNotice('Order received! Check your email for confirmation.', 'success');
|
||
} catch (error) {
|
||
showNotice('We were unable to place your order. Please try again.', 'error');
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<SiteLayout>
|
||
<Head>
|
||
<title>Cart · Full Moon Bakehouse</title>
|
||
</Head>
|
||
|
||
<div className="wrapper cart-page">
|
||
<div className="section-header">
|
||
<div>
|
||
<h2>Your cart</h2>
|
||
<p>Add or adjust items, then submit your preorder request.</p>
|
||
</div>
|
||
</div>
|
||
|
||
{notice ? <div className={`flash ${notice.variant}`}>{notice.message}</div> : null}
|
||
|
||
<div className="cart-layout">
|
||
<div className="cart-card">
|
||
<div className="cart-header">
|
||
<h3>Items</h3>
|
||
{hasCartItems ? (
|
||
<button className="btn-small danger" type="button" onClick={clearCart}>
|
||
Clear cart
|
||
</button>
|
||
) : null}
|
||
</div>
|
||
|
||
{hasCartItems ? (
|
||
<div className="cart-list">
|
||
{items.map((item) => (
|
||
<div key={item.id} className="cart-item">
|
||
<div className="cart-item__row">
|
||
<strong>{item.name}</strong>
|
||
<span>${(item.price * item.quantity).toFixed(2)}</span>
|
||
</div>
|
||
<div className="cart-item__controls">
|
||
<button className="stepper" type="button" onClick={() => handleQuantity(item.id, -1)}>
|
||
–
|
||
</button>
|
||
<span>{item.quantity}</span>
|
||
<button className="stepper" type="button" onClick={() => handleQuantity(item.id, 1)}>
|
||
+
|
||
</button>
|
||
<button className="btn-small danger" type="button" onClick={() => removeItem(item.id)}>
|
||
Remove
|
||
</button>
|
||
</div>
|
||
{Number.isFinite(item.limit) ? (
|
||
<small className="cart-note">Max {item.limit} per order.</small>
|
||
) : null}
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="cart-empty">Your cart is empty. Browse the menu to get started.</div>
|
||
)}
|
||
|
||
<div className="cart-summary">
|
||
<div className="cart-item__row">
|
||
<span>Subtotal</span>
|
||
<span>${subtotal.toFixed(2)}</span>
|
||
</div>
|
||
<div className="cart-item__row" style={{ fontWeight: 500 }}>
|
||
<span>Delivery fee</span>
|
||
<span>{customer.fulfillment_method === 'delivery' ? '$5.00' : '$0.00'}</span>
|
||
</div>
|
||
<div className="cart-item__row" style={{ fontSize: '1.1rem' }}>
|
||
<span>Total</span>
|
||
<span>${total.toFixed(2)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<form className="cart-card checkout-form" onSubmit={handleSubmit}>
|
||
<h3>Checkout</h3>
|
||
{confirmation ? (
|
||
<div className="notice success">Order #{confirmation} received!</div>
|
||
) : null}
|
||
|
||
<div className="form-grid two-column">
|
||
<label className="form-field">
|
||
Name *
|
||
<input
|
||
className="input"
|
||
value={customer.name}
|
||
onChange={(event) => updateCustomerField('name', event.target.value)}
|
||
required
|
||
/>
|
||
</label>
|
||
<label className="form-field">
|
||
Email *
|
||
<input
|
||
type="email"
|
||
className="input"
|
||
value={customer.email}
|
||
onChange={(event) => updateCustomerField('email', event.target.value)}
|
||
required
|
||
/>
|
||
</label>
|
||
<label className="form-field">
|
||
Phone
|
||
<input
|
||
className="input"
|
||
value={customer.phone}
|
||
onChange={(event) => updateCustomerField('phone', event.target.value)}
|
||
/>
|
||
</label>
|
||
<label className="form-field">
|
||
Preferred pickup date
|
||
<input
|
||
type="date"
|
||
className="input"
|
||
value={customer.scheduled_date}
|
||
onChange={(event) => updateCustomerField('scheduled_date', event.target.value)}
|
||
/>
|
||
</label>
|
||
<label className="form-field">
|
||
Time window
|
||
<input
|
||
className="input"
|
||
placeholder="e.g., Thu 5:30-5:45pm"
|
||
value={customer.scheduled_time_slot}
|
||
onChange={(event) => updateCustomerField('scheduled_time_slot', event.target.value)}
|
||
/>
|
||
</label>
|
||
</div>
|
||
|
||
<div className="radio-group">
|
||
<span>Fulfillment method</span>
|
||
<label>
|
||
<input
|
||
type="radio"
|
||
name="fulfillment_method"
|
||
value="pickup"
|
||
checked={customer.fulfillment_method === 'pickup'}
|
||
onChange={(event) => updateCustomerField('fulfillment_method', event.target.value)}
|
||
/>{' '}
|
||
Pickup (free)
|
||
</label>
|
||
<label>
|
||
<input
|
||
type="radio"
|
||
name="fulfillment_method"
|
||
value="delivery"
|
||
checked={customer.fulfillment_method === 'delivery'}
|
||
onChange={(event) => updateCustomerField('fulfillment_method', event.target.value)}
|
||
/>{' '}
|
||
Delivery in Eau Claire or Altoona ($5)
|
||
</label>
|
||
</div>
|
||
|
||
{customer.fulfillment_method === 'delivery' ? (
|
||
<div className="form-grid two-column">
|
||
<label className="form-field">
|
||
Address line 1 *
|
||
<input
|
||
className="input"
|
||
value={customer.delivery_address_line1}
|
||
onChange={(event) =>
|
||
updateCustomerField('delivery_address_line1', event.target.value)
|
||
}
|
||
required
|
||
/>
|
||
</label>
|
||
<label className="form-field">
|
||
City *
|
||
<input
|
||
className="input"
|
||
value={customer.delivery_city}
|
||
onChange={(event) => updateCustomerField('delivery_city', event.target.value)}
|
||
required
|
||
/>
|
||
</label>
|
||
<label className="form-field">
|
||
State
|
||
<input
|
||
className="input"
|
||
value={customer.delivery_state}
|
||
onChange={(event) => updateCustomerField('delivery_state', event.target.value)}
|
||
/>
|
||
</label>
|
||
<label className="form-field">
|
||
Postal code *
|
||
<input
|
||
className="input"
|
||
value={customer.delivery_postal_code}
|
||
onChange={(event) =>
|
||
updateCustomerField('delivery_postal_code', event.target.value)
|
||
}
|
||
required
|
||
/>
|
||
</label>
|
||
</div>
|
||
) : null}
|
||
|
||
<label className="form-field">
|
||
Secret friend code
|
||
<input
|
||
className="input"
|
||
placeholder="Optional override code"
|
||
value={customer.override_code}
|
||
onChange={(event) => updateCustomerField('override_code', event.target.value)}
|
||
/>
|
||
</label>
|
||
|
||
<label className="form-field">
|
||
Notes
|
||
<textarea
|
||
rows={3}
|
||
className="textarea"
|
||
placeholder="Allergies, delivery notes, or special requests."
|
||
value={customer.notes}
|
||
onChange={(event) => updateCustomerField('notes', event.target.value)}
|
||
/>
|
||
</label>
|
||
|
||
<button className="btn btn-primary" type="submit" disabled={submitting || !hasCartItems}>
|
||
{submitting ? 'Submitting…' : 'Place order'}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</SiteLayout>
|
||
);
|
||
}
|