Files
appcakes-builds/src/pages/Cart.tsx
T
2026-05-27 17:01:31 +00:00

279 lines
7.8 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import {
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonList,
IonLoading,
IonPage,
IonText,
IonTitle,
IonToolbar,
useIonViewWillEnter,
} from '@ionic/react';
import {
addOutline,
bagHandleOutline,
removeOutline,
trashOutline,
} from 'ionicons/icons';
import { useHistory } from 'react-router-dom';
import { supabase } from '../supabase';
import type { TablesInsert } from '../database.types';
import { CartItem, LAST_ORDER_STORAGE_KEY, readCart, writeCart } from './Home';
import {
setStatusBarBackground,
setStatusBarStyle,
Style,
} from '../utils/statusBar';
const Cart: React.FC = () => {
const history = useHistory();
const [cart, setCart] = useState<CartItem[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
useIonViewWillEnter(() => {
setStatusBarStyle(Style.Dark);
setStatusBarBackground('#ffffff');
});
useEffect(() => {
setCart(readCart());
}, []);
useEffect(() => {
const syncCartFromStorage = () => setCart(readCart());
window.addEventListener('storage', syncCartFromStorage);
window.addEventListener(
'cart:updated',
syncCartFromStorage as EventListener
);
return () => {
window.removeEventListener('storage', syncCartFromStorage);
window.removeEventListener(
'cart:updated',
syncCartFromStorage as EventListener
);
};
}, []);
const syncCart = (nextCart: CartItem[]) => {
setCart(nextCart);
writeCart(nextCart);
};
const showError = (message: string) => {
setErrorMessage(message);
window.setTimeout(() => setErrorMessage(''), 4000);
};
const itemCount = useMemo(
() => cart.reduce((sum, item) => sum + item.quantity, 0),
[cart]
);
const subtotal = useMemo(
() => cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
[cart]
);
const deliveryFee = cart.length > 0 ? 2.99 : 0;
const total = subtotal + deliveryFee;
const updateQuantity = (id: string, delta: number) => {
const nextCart = cart
.map((item) =>
item.id === id ? { ...item, quantity: item.quantity + delta } : item
)
.filter((item) => item.quantity > 0);
syncCart(nextCart);
};
const removeItem = (id: string) => {
syncCart(cart.filter((item) => item.id !== id));
};
const placeOrder = async () => {
if (cart.length === 0 || isSubmitting) {
return;
}
setIsSubmitting(true);
const orderNumber = `QC-${Date.now().toString().slice(-6)}`;
const orderPayload: TablesInsert<'orders'> = {
order_number: orderNumber,
subtotal,
delivery_fee: deliveryFee,
total,
item_count: itemCount,
status: 'confirmed',
};
const { data: order, error: orderError } = await supabase
.from('orders')
.insert(orderPayload)
.select()
.single();
if (orderError || !order) {
setIsSubmitting(false);
showError('We could not place your order. Please try again.');
return;
}
const itemsPayload: TablesInsert<'order_items'>[] = cart.map((item) => ({
order_id: order.id,
product_id: item.id,
product_name: item.name,
unit_price: item.price,
quantity: item.quantity,
}));
const { error: itemsError } = await supabase
.from('order_items')
.insert(itemsPayload);
if (itemsError) {
await supabase.from('orders').delete().eq('id', order.id);
setIsSubmitting(false);
showError('We could not save your order items. Please try again.');
return;
}
localStorage.setItem(
LAST_ORDER_STORAGE_KEY,
JSON.stringify({
id: order.id,
orderNumber: order.order_number,
total: Number(order.total),
itemCount: order.item_count,
status: order.status,
})
);
writeCart([]);
setCart([]);
setIsSubmitting(false);
history.replace('/order-confirmation');
};
return (
<IonPage>
<IonHeader className="ion-no-border">
<IonToolbar>
<IonButtons slot="start">
<IonButton fill="clear" onClick={() => history.replace('/home')}>
Back
</IonButton>
</IonButtons>
<IonTitle>Your Cart</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding">
<IonLoading isOpen={isSubmitting} message="Placing your order..." />
{errorMessage && (
<IonText color="danger">
<p>{errorMessage}</p>
</IonText>
)}
{cart.length === 0 ? (
<div className="empty-state cart-empty">
<IonIcon icon={bagHandleOutline} />
<h3>Your cart is empty</h3>
<p className="muted-text">
Add your first product to place an order.
</p>
<IonButton onClick={() => history.push('/home')}>
Browse products
</IonButton>
</div>
) : (
<>
<div className="cart-overview-card">
<div>
<p className="overview-label">Items in cart</p>
<h2>
{itemCount} item{itemCount === 1 ? '' : 's'}
</h2>
</div>
<IonText color="medium">
<p>Review your selections before placing the order.</p>
</IonText>
</div>
<IonList lines="none" className="cart-list">
{cart.map((item) => (
<IonItem key={item.id} className="cart-item">
<IonLabel>
<h2>{item.name}</h2>
<p>${item.price.toFixed(2)} each</p>
<strong>${(item.price * item.quantity).toFixed(2)}</strong>
</IonLabel>
<div className="cart-actions">
<IonButton
fill="clear"
onClick={() => updateQuantity(item.id, -1)}
>
<IonIcon icon={removeOutline} />
</IonButton>
<span>{item.quantity}</span>
<IonButton
fill="clear"
onClick={() => updateQuantity(item.id, 1)}
>
<IonIcon icon={addOutline} />
</IonButton>
<IonButton
fill="clear"
color="danger"
onClick={() => removeItem(item.id)}
>
<IonIcon icon={trashOutline} />
</IonButton>
</div>
</IonItem>
))}
</IonList>
<div className="summary-card">
<div className="summary-row">
<IonText color="medium">Items</IonText>
<strong>{itemCount}</strong>
</div>
<div className="summary-row">
<IonText color="medium">Subtotal</IonText>
<strong>${subtotal.toFixed(2)}</strong>
</div>
<div className="summary-row">
<IonText color="medium">Delivery</IonText>
<strong>${deliveryFee.toFixed(2)}</strong>
</div>
<div className="summary-row total-row">
<span>Total</span>
<strong>${total.toFixed(2)}</strong>
</div>
<IonButton
expand="block"
onClick={placeOrder}
disabled={isSubmitting}
>
Place order
</IonButton>
</div>
</>
)}
</IonContent>
</IonPage>
);
};
export default Cart;