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
This commit is contained in:
2026-02-21 22:06:25 +00:00
parent cc695c6f28
commit 325d2944fe
10 changed files with 1045 additions and 43 deletions

70
client/src/store/cart.ts Normal file
View File

@@ -0,0 +1,70 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export interface CartItem {
product_id: string
name: string
price: number
quantity: number
image?: string
}
interface CartState {
items: CartItem[]
total: number
addItem: (item: CartItem) => void
removeItem: (product_id: string) => void
updateQuantity: (product_id: string, qty: number) => void
clearCart: () => void
}
function calcTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
export const useCartStore = create<CartState>()(
persist(
(set, get) => ({
items: [],
total: 0,
addItem: (item: CartItem) => {
const existing = get().items.find(i => i.product_id === item.product_id)
let newItems: CartItem[]
if (existing) {
newItems = get().items.map(i =>
i.product_id === item.product_id
? { ...i, quantity: i.quantity + item.quantity }
: i
)
} else {
newItems = [...get().items, item]
}
set({ items: newItems, total: calcTotal(newItems) })
},
removeItem: (product_id: string) => {
const newItems = get().items.filter(i => i.product_id !== product_id)
set({ items: newItems, total: calcTotal(newItems) })
},
updateQuantity: (product_id: string, qty: number) => {
if (qty <= 0) {
get().removeItem(product_id)
return
}
const newItems = get().items.map(i =>
i.product_id === product_id ? { ...i, quantity: qty } : i
)
set({ items: newItems, total: calcTotal(newItems) })
},
clearCart: () => {
set({ items: [], total: 0 })
},
}),
{
name: 'cart',
}
)
)