Feature: Add quantity adjustment and fix shipping selection
- Add quantity adjustment controls (+ and -) on checkout order summary - Prevent quantity from going below 1 with disabled minus button at qty=1 - Fix shipping selector not updating by wrapping callback in useCallback - Shipping option changes now properly update order total - Totals recalculate reactively when adjusting quantities or shipping Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
5538938020
commit
08038dffb1
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
interface AddToCartModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
productName: string;
|
||||
}
|
||||
|
||||
export default function AddToCartModal({ isOpen, onClose, productName }: AddToCartModalProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="mx-4 max-w-sm rounded border border-line bg-paper p-8 shadow-lg">
|
||||
<h2 className="font-display text-xl">Added to bag</h2>
|
||||
<p className="mt-3 text-sm text-muted">
|
||||
<strong>{productName}</strong> has been added to your bag.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 border-2 border-ink px-6 py-3 text-sm font-medium text-ink hover:border-clay hover:bg-clay hover:text-paper"
|
||||
>
|
||||
Continue shopping
|
||||
</button>
|
||||
<Link
|
||||
href="/checkout"
|
||||
onClick={onClose}
|
||||
className="flex-1 bg-clay px-6 py-3 text-center text-sm font-medium text-paper hover:bg-clay-dark"
|
||||
>
|
||||
Go to checkout
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { updateCustomerAddress } from '@/app/account/actions';
|
||||
|
||||
interface AddressFormSectionProps {
|
||||
customer: any;
|
||||
}
|
||||
|
||||
export default function AddressFormSection({ customer }: AddressFormSectionProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="mt-10">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2 text-sm text-clay hover:text-clay-dark font-medium"
|
||||
>
|
||||
{isOpen ? '✕' : '✎'} {isOpen ? 'Close' : 'Edit delivery address'}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<form action={updateCustomerAddress} className="mt-4 space-y-4 border border-line bg-surface p-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="phone" className="tag-label mb-2 block">
|
||||
Phone number
|
||||
</label>
|
||||
<input
|
||||
id="phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
defaultValue={customer.phone ?? ''}
|
||||
className="w-full border border-line px-3 py-2 text-sm"
|
||||
placeholder="e.g. +44 7700 900000"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="country" className="tag-label mb-2 block">
|
||||
Country
|
||||
</label>
|
||||
<select
|
||||
id="country"
|
||||
name="country"
|
||||
defaultValue={customer.country ?? 'GB'}
|
||||
className="w-full border border-line bg-paper px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="GB">United Kingdom</option>
|
||||
<option value="IE">Ireland</option>
|
||||
<option value="US">United States</option>
|
||||
<option value="CA">Canada</option>
|
||||
<option value="AU">Australia</option>
|
||||
<option value="NZ">New Zealand</option>
|
||||
<option value="DE">Germany</option>
|
||||
<option value="FR">France</option>
|
||||
<option value="NL">Netherlands</option>
|
||||
<option value="ES">Spain</option>
|
||||
<option value="IT">Italy</option>
|
||||
<option value="JP">Japan</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="addressLine1" className="tag-label mb-2 block">
|
||||
Address line 1
|
||||
</label>
|
||||
<input
|
||||
id="addressLine1"
|
||||
name="addressLine1"
|
||||
type="text"
|
||||
defaultValue={customer.addressLine1 ?? ''}
|
||||
className="w-full border border-line px-3 py-2 text-sm"
|
||||
placeholder="Street address"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="addressLine2" className="tag-label mb-2 block">
|
||||
Address line 2 (optional)
|
||||
</label>
|
||||
<input
|
||||
id="addressLine2"
|
||||
name="addressLine2"
|
||||
type="text"
|
||||
defaultValue={customer.addressLine2 ?? ''}
|
||||
className="w-full border border-line px-3 py-2 text-sm"
|
||||
placeholder="Apartment, suite, etc."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="city" className="tag-label mb-2 block">
|
||||
City
|
||||
</label>
|
||||
<input
|
||||
id="city"
|
||||
name="city"
|
||||
type="text"
|
||||
defaultValue={customer.city ?? ''}
|
||||
className="w-full border border-line px-3 py-2 text-sm"
|
||||
placeholder="London"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="postalCode" className="tag-label mb-2 block">
|
||||
Postal code
|
||||
</label>
|
||||
<input
|
||||
id="postalCode"
|
||||
name="postalCode"
|
||||
type="text"
|
||||
defaultValue={customer.postalCode ?? ''}
|
||||
className="w-full border border-line px-3 py-2 text-sm"
|
||||
placeholder="SW1A 1AA"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-clay px-6 py-3 text-sm font-medium text-paper hover:bg-clay-dark"
|
||||
>
|
||||
Save address
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface OlderOrdersProps {
|
||||
orders: any[];
|
||||
formatPrice: (cents: number) => string;
|
||||
statusLabel: (status: string) => string;
|
||||
statusClasses: (status: string) => string;
|
||||
}
|
||||
|
||||
export default function OlderOrders({ orders, formatPrice, statusLabel, statusClasses }: OlderOrdersProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2 text-sm text-clay hover:text-clay-dark font-medium"
|
||||
>
|
||||
{isOpen ? '✕' : '▼'} Older orders ({orders.length})
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="mt-4 divide-y divide-line border-t border-line">
|
||||
{orders.map((order) => (
|
||||
<div key={order.id} className="border-b border-line py-4 last:border-b-0">
|
||||
<Link
|
||||
href={`/account/orders/${order.id}`}
|
||||
className="flex flex-wrap items-center justify-between gap-2 hover:opacity-80"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm">
|
||||
{new Date(order.createdAt).toLocaleDateString(undefined, { dateStyle: 'medium' })} ·{' '}
|
||||
{order.items.length} item{order.items.length === 1 ? '' : 's'}
|
||||
</p>
|
||||
<span className={`tag-label mt-1 inline-block rounded-full px-3 py-1 ${statusClasses(order.status)}`}>
|
||||
{statusLabel(order.status)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono text-sm">{formatPrice(order.total)}</span>
|
||||
</Link>
|
||||
|
||||
{/* Tracking Information */}
|
||||
{order.carrier && order.trackingNumber && (
|
||||
<div className="mt-3 border-t border-line pt-3 text-sm">
|
||||
<p>
|
||||
<strong>Tracking:</strong> {order.carrier} · {order.trackingNumber}
|
||||
</p>
|
||||
{order.estimatedDeliveryDate && (
|
||||
<p className="text-muted">
|
||||
Est. delivery: {new Date(order.estimatedDeliveryDate).toLocaleDateString(undefined, { dateStyle: 'medium' })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { v4 as uuid } from 'uuid';
|
||||
import ProductMockup from './ProductMockup';
|
||||
import Price from './Price';
|
||||
import LoginPromptModal from './LoginPromptModal';
|
||||
import AddToCartModal from './AddToCartModal';
|
||||
import { useCart } from '@/lib/cartStore';
|
||||
import { getColorName } from '@/lib/colorNames';
|
||||
import { checkCustomerAuth } from '@/app/actions';
|
||||
@@ -17,6 +18,7 @@ export default function StandardProductView({ product }: { product: ProductDTO }
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [justAdded, setJustAdded] = useState(false);
|
||||
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||
const [showAddToCartModal, setShowAddToCartModal] = useState(false);
|
||||
const addItem = useCart((s) => s.addItem);
|
||||
|
||||
const hasBack = Boolean(product.imageUrlBack);
|
||||
@@ -48,11 +50,6 @@ export default function StandardProductView({ product }: { product: ProductDTO }
|
||||
}
|
||||
|
||||
const handleAddToBag = async () => {
|
||||
const { isAuthenticated } = await checkCustomerAuth();
|
||||
if (!isAuthenticated) {
|
||||
setShowLoginPrompt(true);
|
||||
}
|
||||
|
||||
addItem({
|
||||
cartItemId: uuid(),
|
||||
productId: product.id,
|
||||
@@ -69,8 +66,7 @@ export default function StandardProductView({ product }: { product: ProductDTO }
|
||||
placementPreviewUrl: product.colorPhotos[color] ?? product.imageUrl ?? null,
|
||||
placementPreviewUrlBack: product.imageUrlBack ?? null,
|
||||
});
|
||||
setJustAdded(true);
|
||||
setTimeout(() => setJustAdded(false), 2200);
|
||||
setShowAddToCartModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -188,10 +184,14 @@ export default function StandardProductView({ product }: { product: ProductDTO }
|
||||
Add to bag — <Price cents={product.effectivePrice * quantity} />
|
||||
</button>
|
||||
</div>
|
||||
{justAdded && <p className="text-sm text-pine">Added to your bag.</p>}
|
||||
</div>
|
||||
|
||||
<LoginPromptModal isOpen={showLoginPrompt} onClose={() => setShowLoginPrompt(false)} />
|
||||
<AddToCartModal
|
||||
isOpen={showAddToCartModal}
|
||||
onClose={() => setShowAddToCartModal(false)}
|
||||
productName={product.name}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user