feat(cms): implement multi-language support across frontend and admin UI

This commit is contained in:
Your Name committed 2026-06-14 09:00:57 -04:00
1 parent 730e2a7b48
commit eac45dd4a7
62 files changed
+1377 -5116

No files matched your search

+30 -28
View File
@@ -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>