feat(spa): add cart coupon input and checkout mobile accordion
This commit is contained in:
1 parent
769afa37fd
commit
fd3abcf487
2 files changed
+57
-5
No files matched your search
@@ -126,6 +126,10 @@ function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: ()
|
||||
const [couponValid, setCouponValid] = React.useState<boolean>(false)
|
||||
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(() => {
|
||||
fetch(`${API_BASE}/promotions/active`)
|
||||
.then(res => res.json())
|
||||
@@ -164,6 +168,21 @@ function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: ()
|
||||
setDiscountAmount(0)
|
||||
setIsFreeShipping(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
|
||||
@@ -186,6 +205,26 @@ function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: ()
|
||||
return (
|
||||
<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 */}
|
||||
<div className="space-y-2 text-sm text-pop-black">
|
||||
<div className="flex justify-between text-spa-stone">
|
||||
|
||||
@@ -23,6 +23,7 @@ export default function CheckoutPage() {
|
||||
const [isFreeShipping, setIsFreeShipping] = useState<boolean>(false)
|
||||
const [couponLoading, setCouponLoading] = useState(false)
|
||||
const [couponError, setCouponError] = useState('')
|
||||
const [isSummaryExpanded, setIsSummaryExpanded] = useState(true)
|
||||
|
||||
const [activePromos, setActivePromos] = useState<any[]>([])
|
||||
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">
|
||||
<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 */}
|
||||
<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">
|
||||
<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>
|
||||
@@ -357,10 +358,20 @@ export default function CheckoutPage() {
|
||||
</div>
|
||||
|
||||
{/* Summary Section */}
|
||||
<div className="lg:w-96 space-y-6">
|
||||
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 shadow-sm sticky top-32">
|
||||
<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>
|
||||
<div className="lg:w-96 space-y-6 order-1 lg:order-2">
|
||||
<div className="bg-white p-6 lg:p-8 rounded-2xl border border-spa-stone/10 shadow-sm sticky top-32 transition-all">
|
||||
<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 */}
|
||||
<div className="mb-6 pb-6 border-b border-spa-stone/10">
|
||||
{!appliedCoupon ? (
|
||||
@@ -495,6 +506,8 @@ export default function CheckoutPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> {/* End Accordion Content */}
|
||||
|
||||
<button
|
||||
form="checkout-form"
|
||||
type="submit"
|
||||
|
||||
Reference in new issue
Block a user