chore: initial import

This commit is contained in:
2025-11-09 00:26:00 -06:00
commit 67fb60e6ca
76 changed files with 3925 additions and 0 deletions

336
frontend/pages/cart.js Normal file
View File

@@ -0,0 +1,336 @@
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>
);
}