Files
homaroma/apps/website/src/components/CartDrawer.tsx
T

215 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react'
import { X, ShoppingBag, Plus, Minus, Trash2, Tag, ArrowRight } from 'lucide-react'
import { Link } from 'react-router-dom'
import { useCartStore } from '../store/cartStore'
import { formatPrice } from '@homaroma/types'
import { useTranslation } from 'react-i18next'
import { API_BASE } from '../types'
export default function CartDrawer() {
const { t } = useTranslation()
const { isCartOpen, toggleCart, items, updateQuantity, removeFromCart, getCartTotal } = useCartStore()
if (!isCartOpen) return null
return (
<div className="fixed inset-0 z-[100] flex justify-end">
{/* Backdrop */}
<div
className="absolute inset-0 bg-night-950/80 backdrop-blur-sm animate-fade-in"
onClick={() => toggleCart(false)}
/>
{/* Drawer */}
<div className="relative w-full max-w-md h-full bg-night-900 shadow-2xl flex flex-col border-l border-white/10 animate-slide-left">
{/* Header */}
<div className="p-6 border-b border-white/10 flex items-center justify-between bg-night-950/50">
<h2 className="text-xl font-bold flex items-center gap-2">
<ShoppingBag className="w-5 h-5 text-cyan-glow" />
{t('cart.title')}
<span className="text-sm font-normal text-slate-400 bg-white/5 px-2 py-0.5 rounded-full ml-2">
{t('cart.items', { count: items.length })}
</span>
</h2>
<button
onClick={() => toggleCart(false)}
className="p-2 hover:bg-white/5 rounded-full transition-colors"
>
<X className="w-5 h-5 text-slate-400 hover:text-white" />
</button>
</div>
{/* Cart Items */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{items.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-slate-500 space-y-4">
<ShoppingBag className="w-16 h-16 opacity-20" />
<p>{t('cart.empty')}</p>
<button
onClick={() => toggleCart(false)}
className="text-cyan-glow hover:underline tracking-widest uppercase text-xs font-bold"
>
{t('cart.continueShopping')}
</button>
</div>
) : (
items.map((item) => (
<div key={item.cartItemId} className="flex gap-4 p-4 rounded-2xl bg-white/5 border border-white/5 group relative">
<div className="w-18 h-22 rounded-lg bg-night-950 overflow-hidden flex-shrink-0" style={{ width: '72px', height: '88px' }}>
<img
src={item.productImage || '/images/oil_ozone.png'}
alt={item.productName}
className="w-full h-full object-cover opacity-80"
/>
</div>
<div className="flex-1 flex flex-col pt-1 space-y-1 relative min-w-0">
<h3 className="font-bold text-sm leading-tight pr-6 truncate">{item.productName}</h3>
{item.variantSize && (
<p className="text-xs text-slate-500">{item.variantSize}</p>
)}
{/* Price row */}
<div className="flex items-baseline gap-1.5 mt-0.5">
<span className="text-xs text-slate-500">{formatPrice(item.price)}</span>
<span className="text-xs text-slate-600">×</span>
<span className="text-xs text-slate-500">{item.quantity}</span>
<span className="text-xs text-slate-600">=</span>
<span className="text-sm font-bold text-cyan-glow">{formatPrice(item.price * item.quantity)}</span>
</div>
{/* Quantity controls */}
<div className="flex items-center gap-2 mt-1">
<div className="flex items-center gap-2 bg-night-950/70 rounded-lg border border-white/5">
<button
onClick={() => updateQuantity(item.cartItemId, item.quantity - 1)}
className="p-1.5 hover:text-cyan-glow transition-colors"
>
<Minus className="w-3 h-3" />
</button>
<span className="w-5 text-center text-xs font-bold">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.cartItemId, item.quantity + 1)}
className="p-1.5 hover:text-cyan-glow transition-colors"
>
<Plus className="w-3 h-3" />
</button>
</div>
</div>
<button
onClick={() => removeFromCart(item.cartItemId)}
className="absolute top-0 right-0 p-1.5 text-slate-600 hover:text-rose-500 transition-colors"
title="Remove item"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
))
)}
</div>
{/* Footer */}
{items.length > 0 && (
<CartDrawerFooter subtotal={getCartTotal()} onClose={() => toggleCart(false)} t={t} />
)}
</div>
</div>
)
}
function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: () => void; t?: any }) {
const [savedCoupon, setSavedCoupon] = React.useState<string | null>(null)
const [discountAmount, setDiscountAmount] = React.useState<number>(0)
const [isFreeShipping, setIsFreeShipping] = React.useState<boolean>(false)
const [couponValid, setCouponValid] = React.useState<boolean>(false)
React.useEffect(() => {
const code = localStorage.getItem('irelax_active_coupon')
setSavedCoupon(code)
if (code && subtotal > 0) {
fetch(`${API_BASE}/coupons/validate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, cartTotal: subtotal })
})
.then(r => r.json())
.then(data => {
if (data.valid) {
setCouponValid(true)
setDiscountAmount(data.discountAmount || 0)
setIsFreeShipping(data.isFreeShipping || false)
} else {
setCouponValid(false)
}
})
.catch(() => setCouponValid(false))
}
}, [subtotal])
const removeCoupon = () => {
localStorage.removeItem('irelax_active_coupon')
setSavedCoupon(null)
setDiscountAmount(0)
setIsFreeShipping(false)
setCouponValid(false)
}
return (
<div className="p-5 border-t border-white/10 bg-night-950/60 space-y-4">
{/* Totals */}
<div className="space-y-2 text-sm">
<div className="flex justify-between text-slate-400">
<span>ราคาสินค้า</span>
<span>{formatPrice(subtotal)}</span>
</div>
{(discountAmount > 0 || isFreeShipping) && savedCoupon && (
<div className="flex justify-between text-cyan-glow">
<span className="flex items-center gap-1.5">
ส่วนลดคูปอง
<span className="text-[10px] font-bold bg-cyan-glow/15 px-1.5 py-0.5 rounded tracking-widest">
{savedCoupon}
</span>
<button onClick={removeCoupon} className="text-slate-500 hover:text-rose-400 transition-colors" title="ยกเลิก">
<X className="w-3 h-3" />
</button>
</span>
<span>{isFreeShipping ? 'ส่งฟรี' : `- ${formatPrice(discountAmount)}`}</span>
</div>
)}
{savedCoupon && !couponValid && (
<div className="flex justify-between text-amber-400 text-xs">
<span className="flex items-center gap-1.5">
<Tag className="w-3 h-3" />
คูปอง <span className="font-bold tracking-widest">{savedCoupon}</span> (ยังไม่ถึงยอดขั้นต่ำ)
<button onClick={removeCoupon} className="text-slate-500 hover:text-rose-400 transition-colors">
<X className="w-3 h-3" />
</button>
</span>
</div>
)}
<div className="flex justify-between font-bold text-base pt-2 border-t border-white/10">
<span>รวมสุทธิ</span>
<span className="text-cyan-glow">{formatPrice(Math.max(0, subtotal - discountAmount))}</span>
</div>
</div>
{/* Checkout button */}
<Link
to="/checkout"
onClick={onClose}
className="flex items-center justify-center gap-2 w-full py-4 bg-white text-night-900 font-bold rounded-xl uppercase tracking-widest text-sm hover:bg-cyan-glow hover:shadow-[0_0_25px_rgba(102,252,241,0.4)] transition-all duration-500 group"
>
<ShoppingBag className="w-4 h-4" />
ดำเนินการชำระเงิน
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
)
}