- 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
115 lines
4.1 KiB
TypeScript
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>
|
|
)
|
|
}
|