Files
shokuninmarche/client/src/pages/CartPage.tsx
tester 325d2944fe feat: Order & Checkout System - cart, checkout, order history, tracking
- Backend: Add GET /api/orders/:id, PUT /api/orders/:id/cancel, GET /api/orders/:id/tracking with mock Yamato data
- Frontend: Zustand cart store with localStorage persistence (key 'cart')
- Frontend: CartPage (/cart) with quantity controls, item removal, JPY totals
- Frontend: CheckoutPage (/checkout) with shipping address form, auth guard, POST to /api/orders
- Frontend: OrderHistoryPage (/account/orders) with Japanese status labels, auth guard
- Frontend: OrderDetailPage (/account/orders/:id) with cancel button, tracking section, auth guard
- Updated App.tsx with all four new routes
- Updated ProductDetail.tsx to use cart store with View Cart link after adding
- Updated Navbar.tsx with cart icon badge (item count) and 注文履歴 order history link
2026-02-21 22:06:25 +00:00

115 lines
4.1 KiB
TypeScript

import { Link, useNavigate } from 'react-router-dom'
import { useCartStore } from '@/store/cart'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Minus, Plus, Trash2, ShoppingBag } from 'lucide-react'
function formatPrice(price: number): string {
return new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(price)
}
export function CartPage() {
const { items, total, removeItem, updateQuantity } = useCartStore()
const navigate = useNavigate()
if (items.length === 0) {
return (
<div className="container mx-auto px-4 py-24 text-center">
<ShoppingBag className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
<h1 className="text-2xl font-bold mb-2"></h1>
<p className="text-muted-foreground mb-6">Your cart is empty.</p>
<Link to="/products">
<Button></Button>
</Link>
</div>
)
}
return (
<div className="container mx-auto px-4 py-10 max-w-3xl">
<h1 className="text-2xl font-bold mb-8"></h1>
<div className="space-y-4 mb-8">
{items.map(item => (
<Card key={item.product_id}>
<CardContent className="p-4 flex items-center gap-4">
{/* Image */}
<div className="w-20 h-20 bg-amber-50 rounded-lg overflow-hidden flex-shrink-0 border border-border">
{item.image ? (
<img src={item.image} alt={item.name} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center text-2xl">🏺</div>
)}
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<p className="font-semibold truncate">{item.name}</p>
<p className="text-primary font-bold mt-1">{formatPrice(item.price)}</p>
</div>
{/* Quantity controls */}
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => updateQuantity(item.product_id, item.quantity - 1)}
>
<Minus className="h-3 w-3" />
</Button>
<span className="w-8 text-center font-medium">{item.quantity}</span>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => updateQuantity(item.product_id, item.quantity + 1)}
>
<Plus className="h-3 w-3" />
</Button>
</div>
{/* Item total */}
<div className="w-24 text-right font-bold">
{formatPrice(item.price * item.quantity)}
</div>
{/* Remove */}
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-destructive"
onClick={() => removeItem(item.product_id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</CardContent>
</Card>
))}
</div>
{/* Summary */}
<Card>
<CardContent className="p-6">
<div className="flex justify-between items-center text-xl font-bold mb-6">
<span></span>
<span className="text-primary">{formatPrice(total)}</span>
</div>
<Button
size="lg"
className="w-full"
onClick={() => navigate('/checkout')}
>
(Proceed to Checkout)
</Button>
<div className="mt-3 text-center">
<Link to="/products" className="text-sm text-muted-foreground hover:text-primary transition-colors">
</Link>
</div>
</CardContent>
</Card>
</div>
)
}