feat(cms): implement multi-language support across frontend and admin UI
This commit is contained in:
1 parent
730e2a7b48
commit
eac45dd4a7
62 files changed
+1377
-5116
No files matched your search
@@ -6,8 +6,10 @@ import { API_BASE, formatPrice, type Category, type CustomerAddress } from '../t
|
||||
import { useCartStore } from '../store/cartStore'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function CheckoutPage() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { items, getCartTotal, clearCart } = useCartStore()
|
||||
const { customer } = useAuthStore()
|
||||
@@ -203,7 +205,7 @@ export default function CheckoutPage() {
|
||||
setIsFreeShipping(data.isFreeShipping)
|
||||
} else {
|
||||
removeCoupon()
|
||||
setCouponError(data.error || 'Coupon is no longer valid for this cart')
|
||||
setCouponError(data.error || t('checkout.invalidCoupon'))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -227,13 +229,13 @@ export default function CheckoutPage() {
|
||||
setDiscountAmount(data.discountAmount)
|
||||
setIsFreeShipping(data.isFreeShipping)
|
||||
} else {
|
||||
setCouponError(data.error || 'Invalid coupon')
|
||||
setCouponError(data.error || t('checkout.invalidCoupon'))
|
||||
setAppliedCoupon(null)
|
||||
setDiscountAmount(0)
|
||||
setIsFreeShipping(false)
|
||||
}
|
||||
} catch (err) {
|
||||
setCouponError('Error validating coupon')
|
||||
setCouponError(t('checkout.errorValidating'))
|
||||
} finally {
|
||||
setCouponLoading(false)
|
||||
}
|
||||
@@ -300,11 +302,11 @@ export default function CheckoutPage() {
|
||||
|
||||
navigate(`/order/${responseData.id}`)
|
||||
} else {
|
||||
alert(responseData.error || 'Failed to place order')
|
||||
alert(responseData.error || t('checkout.failedOrder'))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert('An error occurred. Please try again.')
|
||||
alert(t('checkout.errorOccurred'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -315,14 +317,14 @@ export default function CheckoutPage() {
|
||||
<Navbar categories={categories} />
|
||||
|
||||
<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">{t('checkout.title')}</h1>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-12 relative">
|
||||
{/* Form Section */}
|
||||
<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>
|
||||
<h2 className="text-xl font-bold uppercase tracking-widest text-spa-forest border-b border-spa-stone/10 pb-4">{t('checkout.shippingInfo')}</h2>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
@@ -338,7 +340,7 @@ export default function CheckoutPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="checkout-phone" className="text-xs uppercase tracking-widest text-spa-stone/80 font-bold">Phone Number *</label>
|
||||
<label htmlFor="checkout-phone" className="text-xs uppercase tracking-widest text-spa-stone/80 font-bold">{t('checkout.phoneNumber')}</label>
|
||||
<input
|
||||
id="checkout-phone"
|
||||
type="tel"
|
||||
@@ -352,7 +354,7 @@ export default function CheckoutPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<label htmlFor="checkout-email" className="text-xs uppercase tracking-widest text-spa-stone/80 font-bold">Email Address *</label>
|
||||
<label htmlFor="checkout-email" className="text-xs uppercase tracking-widest text-spa-stone/80 font-bold">{t('checkout.emailAddress')}</label>
|
||||
<input
|
||||
id="checkout-email"
|
||||
type="email"
|
||||
@@ -364,7 +366,7 @@ export default function CheckoutPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<label htmlFor="checkout-address" className="text-xs uppercase tracking-widest text-spa-stone/80 font-bold">Shipping Address *</label>
|
||||
<label htmlFor="checkout-address" className="text-xs uppercase tracking-widest text-spa-stone/80 font-bold">{t('checkout.shippingAddress')}</label>
|
||||
<textarea
|
||||
id="checkout-address"
|
||||
required
|
||||
@@ -376,7 +378,7 @@ export default function CheckoutPage() {
|
||||
/>
|
||||
{savedAddresses.length > 0 && (
|
||||
<div className="mt-2 space-y-2">
|
||||
<p className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/70">เลือกดึงที่อยู่จัดส่งจากโปรไฟล์:</p>
|
||||
<p className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/70">{t('checkout.selectProfileAddress')}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{savedAddresses.map(addr => {
|
||||
const formatted = `${addr.addressLine} ต.${addr.subdistrict} อ.${addr.district} จ.${addr.province} ${addr.zipcode}`
|
||||
@@ -410,7 +412,7 @@ export default function CheckoutPage() {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{loading && <Loader2 className="w-5 h-5 animate-spin" />}
|
||||
<span>Confirm Order</span>
|
||||
<span>{t('checkout.confirmOrder')}</span>
|
||||
</div>
|
||||
<span className="text-sm border-l border-white/20 pl-4">{formatPrice(total)}</span>
|
||||
</button>
|
||||
@@ -425,7 +427,7 @@ export default function CheckoutPage() {
|
||||
onClick={() => setIsSummaryExpanded(!isSummaryExpanded)}
|
||||
className={`w-full flex items-center justify-between text-xl font-bold uppercase tracking-widest text-spa-forest pb-4 ${isSummaryExpanded ? 'border-b border-spa-stone/10 mb-6' : ''} lg:pointer-events-none lg:border-b lg:border-spa-stone/10 lg:mb-6 transition-all`}
|
||||
>
|
||||
<span>Order Summary</span>
|
||||
<span>{t('checkout.orderSummary')}</span>
|
||||
<span className="lg:hidden flex items-center gap-3">
|
||||
{!isSummaryExpanded && <span className="text-sm font-bold text-rose-500 normal-case">{formatPrice(total)}</span>}
|
||||
<span className="text-spa-stone">
|
||||
@@ -439,13 +441,13 @@ export default function CheckoutPage() {
|
||||
<div className="mb-6 pb-6 border-b border-spa-stone/10">
|
||||
{!appliedCoupon ? (
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">Discount Code</label>
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">{t('checkout.discountCode')}</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={couponCode}
|
||||
onChange={e => setCouponCode(e.target.value.toUpperCase())}
|
||||
placeholder="e.g. SUMMER2026"
|
||||
placeholder={t('checkout.placeholderDiscount')}
|
||||
className="flex-1 bg-spa-light border border-spa-stone/20 rounded-lg px-3 text-sm text-pop-black focus:border-spa-forest outline-none uppercase tracking-widest placeholder:text-spa-stone/50"
|
||||
/>
|
||||
<button
|
||||
@@ -454,7 +456,7 @@ export default function CheckoutPage() {
|
||||
disabled={couponLoading || !couponCode.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 ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Apply'}
|
||||
{couponLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : t('checkout.apply')}
|
||||
</button>
|
||||
</div>
|
||||
{couponError && <p className="text-xs text-rose-500 mt-1 font-medium">{couponError}</p>}
|
||||
@@ -467,7 +469,7 @@ export default function CheckoutPage() {
|
||||
</svg>
|
||||
<span className="text-sm font-bold tracking-widest text-spa-forest">{appliedCoupon}</span>
|
||||
</div>
|
||||
<button type="button" onClick={removeCoupon} className="text-xs text-spa-stone/80 hover:text-rose-500 uppercase tracking-widest font-bold">Remove</button>
|
||||
<button type="button" onClick={removeCoupon} className="text-xs text-spa-stone/80 hover:text-rose-500 uppercase tracking-widest font-bold">{t('checkout.remove')}</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -477,9 +479,9 @@ export default function CheckoutPage() {
|
||||
<div className="mb-6 pb-6 border-b border-spa-stone/10 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80 flex items-center gap-2">
|
||||
<span className="text-purple-500 text-sm">⭐️</span> Use Loyalty Points
|
||||
<span className="text-purple-500 text-sm">⭐️</span> {t('checkout.usePoints')}
|
||||
</label>
|
||||
<span className="text-xs font-bold text-spa-forest">{customerPoints} Points Available</span>
|
||||
<span className="text-xs font-bold text-spa-forest">{t('checkout.pointsAvailable', { points: customerPoints })}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
@@ -492,7 +494,7 @@ export default function CheckoutPage() {
|
||||
}}
|
||||
className="w-4 h-4 text-spa-forest border-spa-stone/20 rounded focus:ring-spa-forest"
|
||||
/>
|
||||
<label htmlFor="use-points" className="text-sm text-pop-black font-medium">Apply points discount</label>
|
||||
<label htmlFor="use-points" className="text-sm text-pop-black font-medium">{t('checkout.applyPoints')}</label>
|
||||
</div>
|
||||
{usePoints && (
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
@@ -504,7 +506,7 @@ export default function CheckoutPage() {
|
||||
onChange={e => setPointsToUse(Math.min(Number(e.target.value), customerPoints, maxPointsAllowed))}
|
||||
className="w-24 bg-spa-light border border-spa-stone/20 rounded-lg px-3 py-1.5 text-sm text-pop-black focus:border-spa-forest outline-none"
|
||||
/>
|
||||
<span className="text-xs text-spa-stone/80 font-bold uppercase tracking-widest">Points (-฿{pointsToUse})</span>
|
||||
<span className="text-xs text-spa-stone/80 font-bold uppercase tracking-widest">{t('checkout.pointsDiscount', { points: pointsToUse })}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -524,18 +526,18 @@ export default function CheckoutPage() {
|
||||
|
||||
<div className="border-t border-spa-stone/10 pt-4 space-y-3 font-medium text-pop-black">
|
||||
<div className="flex justify-between text-sm text-spa-stone">
|
||||
<span>Subtotal</span>
|
||||
<span>{t('checkout.subtotal')}</span>
|
||||
<span>{formatPrice(subtotal)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-spa-stone">
|
||||
<span>Shipping Fee</span>
|
||||
<span>{t('checkout.shippingFee')}</span>
|
||||
{loadingSettings ? <Loader2 className="w-4 h-4 animate-spin" /> :
|
||||
<span className={isFreeShipping ? "line-through text-spa-stone/50" : ""}>{formatPrice(shippingFee)}</span>
|
||||
}
|
||||
</div>
|
||||
{isFreeShipping && (
|
||||
<div className="flex justify-between text-sm text-spa-forest">
|
||||
<span>Shipping Discount</span>
|
||||
<span>{t('checkout.shippingDiscount')}</span>
|
||||
<span>- {formatPrice(shippingFee)}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -553,18 +555,18 @@ export default function CheckoutPage() {
|
||||
)}
|
||||
{discountAmount > 0 && (
|
||||
<div className="flex justify-between text-sm text-rose-500">
|
||||
<span>Coupon ({appliedCoupon})</span>
|
||||
<span>{t('checkout.coupon')} ({appliedCoupon})</span>
|
||||
<span>- {formatPrice(discountAmount)}</span>
|
||||
</div>
|
||||
)}
|
||||
{pointsDiscount > 0 && (
|
||||
<div className="flex justify-between text-sm text-purple-500">
|
||||
<span>Points Redeemed</span>
|
||||
<span>{t('checkout.pointsRedeemed')}</span>
|
||||
<span>- {formatPrice(pointsDiscount)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-spa-stone/10 pt-3 mt-3 flex justify-between text-lg font-bold text-pop-black">
|
||||
<span>Total</span>
|
||||
<span>{t('checkout.total')}</span>
|
||||
<span className="text-rose-500">{formatPrice(total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -579,7 +581,7 @@ export default function CheckoutPage() {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{loading && <Loader2 className="w-5 h-5 animate-spin" />}
|
||||
<span>Confirm Order</span>
|
||||
<span>{t('checkout.confirmOrder')}</span>
|
||||
</div>
|
||||
<span className="text-sm border-l border-white/20 pl-4">{formatPrice(total)}</span>
|
||||
</button>
|
||||
|
||||
Reference in new issue
Block a user