Files
FullMoonBakehouse-archive-2025/frontend/pages/cart.js
2025-11-09 00:26:00 -06:00

337 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}