feat(spa): add cart coupon input and checkout mobile accordion

This commit is contained in:
Your Name committed 2026-06-06 20:12:43 +07:00
1 parent 769afa37fd
commit fd3abcf487
2 files changed
+57 -5

No files matched your search

+39
View File
@@ -126,6 +126,10 @@ function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: ()
const [couponValid, setCouponValid] = React.useState<boolean>(false) const [couponValid, setCouponValid] = React.useState<boolean>(false)
const [activePromos, setActivePromos] = React.useState<any[]>([]) const [activePromos, setActivePromos] = React.useState<any[]>([])
const [couponInput, setCouponInput] = React.useState<string>('')
const [couponLoading, setCouponLoading] = React.useState<boolean>(false)
const [couponError, setCouponError] = React.useState<string>('')
React.useEffect(() => { React.useEffect(() => {
fetch(`${API_BASE}/promotions/active`) fetch(`${API_BASE}/promotions/active`)
.then(res => res.json()) .then(res => res.json())
@@ -164,6 +168,21 @@ function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: ()
setDiscountAmount(0) setDiscountAmount(0)
setIsFreeShipping(false) setIsFreeShipping(false)
setCouponValid(false) setCouponValid(false)
setCouponError('')
setCouponInput('')
}
const handleApplyCoupon = async () => {
if (!couponInput.trim()) return
setCouponLoading(true)
setCouponError('')
// We must validate with subtotalAfterL2, but since we don't have it defined here yet,
// we'll calculate it inline or rely on the effect. Actually, we can just save it to localStorage and let the effect handle it.
localStorage.setItem('homaroma_active_coupon', couponInput.toUpperCase())
setSavedCoupon(couponInput.toUpperCase())
setCouponInput('')
setCouponLoading(false)
} }
// Calculate Level 1 Promo // Calculate Level 1 Promo
@@ -186,6 +205,26 @@ function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: ()
return ( return (
<div className="p-5 border-t border-spa-stone/10 bg-spa-light space-y-4"> <div className="p-5 border-t border-spa-stone/10 bg-spa-light space-y-4">
{/* Coupon Input */}
{!savedCoupon && (
<div className="flex gap-2">
<input
type="text"
value={couponInput}
onChange={e => setCouponInput(e.target.value.toUpperCase())}
placeholder={t('cart.couponPlaceholder', 'Discount Code')}
className="flex-1 bg-white border border-spa-stone/20 rounded-lg px-3 py-2 text-sm text-pop-black focus:border-spa-forest outline-none uppercase tracking-widest placeholder:text-spa-stone/50"
/>
<button
onClick={handleApplyCoupon}
disabled={couponLoading || !couponInput.trim()}
className="px-4 bg-spa-stone/10 hover:bg-spa-stone/20 border border-spa-stone/10 rounded-lg text-xs font-bold uppercase tracking-widest disabled:opacity-50 transition-all text-pop-black"
>
{couponLoading ? '...' : t('cart.applyCoupon', 'Apply')}
</button>
</div>
)}
{/* Totals */} {/* Totals */}
<div className="space-y-2 text-sm text-pop-black"> <div className="space-y-2 text-sm text-pop-black">
<div className="flex justify-between text-spa-stone"> <div className="flex justify-between text-spa-stone">
+18 -5
View File
@@ -23,6 +23,7 @@ export default function CheckoutPage() {
const [isFreeShipping, setIsFreeShipping] = useState<boolean>(false) const [isFreeShipping, setIsFreeShipping] = useState<boolean>(false)
const [couponLoading, setCouponLoading] = useState(false) const [couponLoading, setCouponLoading] = useState(false)
const [couponError, setCouponError] = useState('') const [couponError, setCouponError] = useState('')
const [isSummaryExpanded, setIsSummaryExpanded] = useState(true)
const [activePromos, setActivePromos] = useState<any[]>([]) const [activePromos, setActivePromos] = useState<any[]>([])
const [customerPoints, setCustomerPoints] = useState(0) const [customerPoints, setCustomerPoints] = useState(0)
@@ -293,9 +294,9 @@ export default function CheckoutPage() {
<main className="max-w-6xl mx-auto px-6 pt-32 pb-24 text-pop-black"> <main className="max-w-6xl mx-auto px-6 pt-32 pb-24 text-pop-black">
<h1 className="text-4xl font-bold mb-12">Checkout</h1> <h1 className="text-4xl font-bold mb-12">Checkout</h1>
<div className="flex flex-col lg:flex-row gap-12"> <div className="flex flex-col lg:flex-row gap-12 relative">
{/* Form Section */} {/* Form Section */}
<div className="flex-1 space-y-8"> <div className="flex-1 space-y-8 order-2 lg:order-1">
<form id="checkout-form" onSubmit={handleSubmit} className="space-y-6"> <form id="checkout-form" onSubmit={handleSubmit} className="space-y-6">
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 space-y-6 shadow-sm"> <div className="bg-white p-8 rounded-2xl border border-spa-stone/10 space-y-6 shadow-sm">
<h2 className="text-xl font-bold uppercase tracking-widest text-spa-forest border-b border-spa-stone/10 pb-4">Shipping Information</h2> <h2 className="text-xl font-bold uppercase tracking-widest text-spa-forest border-b border-spa-stone/10 pb-4">Shipping Information</h2>
@@ -357,10 +358,20 @@ export default function CheckoutPage() {
</div> </div>
{/* Summary Section */} {/* Summary Section */}
<div className="lg:w-96 space-y-6"> <div className="lg:w-96 space-y-6 order-1 lg:order-2">
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 shadow-sm sticky top-32"> <div className="bg-white p-6 lg:p-8 rounded-2xl border border-spa-stone/10 shadow-sm sticky top-32 transition-all">
<h2 className="text-xl font-bold uppercase tracking-widest text-spa-forest border-b border-spa-stone/10 pb-4 mb-6">Order Summary</h2> <button
type="button"
onClick={() => setIsSummaryExpanded(!isSummaryExpanded)}
className="w-full flex items-center justify-between text-xl font-bold uppercase tracking-widest text-spa-forest pb-4 border-b border-spa-stone/10 mb-6 lg:pointer-events-none"
>
<span>Order Summary</span>
<span className="lg:hidden">
<svg className={`w-5 h-5 transform transition-transform ${isSummaryExpanded ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path></svg>
</span>
</button>
<div className={`overflow-hidden transition-all duration-300 ${isSummaryExpanded ? 'max-h-[2000px] opacity-100' : 'max-h-0 opacity-0 lg:max-h-[2000px] lg:opacity-100'}`}>
{/* Coupon Section */} {/* Coupon Section */}
<div className="mb-6 pb-6 border-b border-spa-stone/10"> <div className="mb-6 pb-6 border-b border-spa-stone/10">
{!appliedCoupon ? ( {!appliedCoupon ? (
@@ -495,6 +506,8 @@ export default function CheckoutPage() {
</div> </div>
</div> </div>
</div> {/* End Accordion Content */}
<button <button
form="checkout-form" form="checkout-form"
type="submit" type="submit"