This commit is contained in:
Your Name committed 2026-06-21 01:52:57 -04:00
1 parent c449de2482
commit 7e4e89e33c
50 files changed
+306 -140

No files matched your search

+2
View File
@@ -19,6 +19,7 @@ import WishlistPage from './pages/WishlistPage'
import CustomPage from './pages/CustomPage'
import ProfilePage from './pages/ProfilePage'
import CartDrawer from './components/CartDrawer'
import ThemeInjector from './components/ThemeInjector'
function App() {
const { i18n } = useTranslation();
@@ -39,6 +40,7 @@ function App() {
return (
<BrowserRouter>
<ThemeInjector />
<ScrollToTop />
<CartDrawer />
<Routes>
+4 -4
View File
@@ -68,7 +68,7 @@ export default function CartDrawer() {
<div key={item.cartItemId} className="flex gap-4 p-4 rounded-2xl bg-white border border-spa-stone/10 shadow-sm group relative">
<div className="w-18 h-22 rounded-lg bg-spa-sage/30 overflow-hidden flex-shrink-0" style={{ width: '72px', height: '88px' }}>
<img
src={item.productImage || `${import.meta.env.VITE_API_URL.replace('/api', '')}/assets/oil_ozone.png`}
src={item.productImage || '/images/fallback-product.svg'}
alt={item.productName}
className="w-full h-full object-cover"
/>
@@ -151,7 +151,7 @@ function CartDrawerFooter({ subtotal, onClose, t }: { subtotal: number; onClose:
}, [])
React.useEffect(() => {
const code = localStorage.getItem('homaroma_active_coupon')
const code = localStorage.getItem('app_active_coupon')
setSavedCoupon(code)
if (code && subtotal > 0) {
fetch(`${API_BASE}/coupons/validate`, {
@@ -174,7 +174,7 @@ function CartDrawerFooter({ subtotal, onClose, t }: { subtotal: number; onClose:
}, [subtotal, activePromos])
const removeCoupon = () => {
localStorage.removeItem('homaroma_active_coupon')
localStorage.removeItem('app_active_coupon')
setSavedCoupon(null)
setDiscountAmount(0)
setIsFreeShipping(false)
@@ -190,7 +190,7 @@ function CartDrawerFooter({ subtotal, onClose, t }: { subtotal: number; onClose:
// 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())
localStorage.setItem('app_active_coupon', couponInput.toUpperCase())
setSavedCoupon(couponInput.toUpperCase())
setCouponInput('')
setCouponLoading(false)
@@ -60,7 +60,7 @@ export default function DynamicCategories({ payload, categories, loading }: Dyna
/>
<div className="absolute bottom-6 left-6 right-6 z-20 text-white">
<span className="mb-1 block text-xl font-semibold">
{getLocalizedField(cat.name, localStorage.getItem('homaroma_lang') || 'en')}
{getLocalizedField(cat.name, localStorage.getItem('app_lang') || 'en')}
</span>
{cat._count && (
<span className="text-sm font-medium text-white/80">
+2 -2
View File
@@ -24,7 +24,7 @@ export default function Footer({ categories = [] }: FooterProps) {
const brandName = settings?.brandName || ''
const translateMenuLabel = (item: MenuItemWithTranslations) => {
const lang = localStorage.getItem('homaroma_lang') || 'en'
const lang = localStorage.getItem('app_lang') || 'en'
if (lang !== 'en' && item.translations && item.translations.length > 0) {
const trans = item.translations.find((translation) => translation.langCode === lang)
if (trans && trans.label) {
@@ -128,7 +128,7 @@ export default function Footer({ categories = [] }: FooterProps) {
<div className="flex flex-col space-y-2 text-sm text-slate-400">
{categories.map((c) => (
<Link key={c.id} to={`/category/${c.id}`} className="hover:text-white transition-colors">
{getLocalizedField(c.name, localStorage.getItem('homaroma_lang') || 'en')}
{getLocalizedField(c.name, localStorage.getItem('app_lang') || 'en')}
</Link>
))}
</div>
+1 -1
View File
@@ -49,7 +49,7 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
const changeLanguage = (code: string) => {
i18n.changeLanguage(code)
localStorage.setItem('homaroma_lang', code)
localStorage.setItem('app_lang', code)
setLangSheetOpen(false)
}
+45
View File
@@ -0,0 +1,45 @@
import { useEffect } from 'react'
import { API_BASE } from '@homaroma/types'
export default function ThemeInjector() {
useEffect(() => {
const lang = localStorage.getItem('app_lang') || 'en'
fetch(`${API_BASE}/settings?lang=${lang}`)
.then(res => res.ok ? res.json() : null)
.then(settings => {
const tokens = settings?.themeTokens
if (!tokens || typeof tokens !== 'object') return
const root = document.documentElement
// Map tokens from the backend (which use brandPrimary etc.)
// to the CSS variables expected by SPA's tailwind.config.js
const mapping: Record<string, string> = {
'--color-spa-light': tokens.surfacePage,
'--color-spa-sage': tokens.brandSoft,
'--color-spa-stone': tokens.mutedText,
'--color-spa-forest': tokens.brandPrimary,
'--color-spa-gold': tokens.brandAccent,
'--color-pop-pink': tokens.brandPrimary,
'--color-pop-yellow': tokens.brandAccent,
'--color-pop-orange': tokens.brandWarm,
'--color-pop-cyan': tokens.brandCool,
'--color-pop-bg': tokens.surfacePage,
'--color-pop-black': tokens.textMain,
'--color-night-900': tokens.surfaceCard,
}
for (const [key, value] of Object.entries(mapping)) {
if (value && typeof value === 'string' && value.trim() !== '') {
root.style.setProperty(key, value)
}
}
})
.catch(err => console.error('Failed to inject theme in SPA:', err))
}, [])
return null
}
+1 -1
View File
@@ -9,7 +9,7 @@ import viTranslation from "./locales/vi/translation.json"
import koTranslation from "./locales/ko/translation.json"
// Retrieve default lang from localStorage if set
const savedLang = localStorage.getItem('homaroma_lang') || 'th'
const savedLang = localStorage.getItem('app_lang') || 'th'
i18n
.use(initReactI18next)
+2 -2
View File
@@ -136,7 +136,7 @@ export default function CategoryPage() {
<span>/</span>
<Link to="/shop" className="hover:text-white transition-colors">Shop</Link>
<span>/</span>
<span className="text-white">{typeof category?.name === 'string' ? category.name : (category?.name as any)?.[localStorage.getItem('homaroma_lang') || 'en'] || (category?.name as any)?.en || '...'}</span>
<span className="text-white">{typeof category?.name === 'string' ? category.name : (category?.name as any)?.[localStorage.getItem('app_lang') || 'en'] || (category?.name as any)?.en || '...'}</span>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-white">
{loading ? (
@@ -169,7 +169,7 @@ export default function CategoryPage() {
: 'bg-white text-spa-stone border-spa-stone/20 hover:border-spa-forest hover:text-spa-forest shadow-sm'
}`}
>
{typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[localStorage.getItem('homaroma_lang') || 'en'] || (cat.name as any)?.en || ''}
{typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[localStorage.getItem('app_lang') || 'en'] || (cat.name as any)?.en || ''}
</Link>
))}
</div>
+6 -6
View File
@@ -127,7 +127,7 @@ export default function CheckoutPage() {
.catch(console.error)
// Check if there's an active coupon saved in localStorage
const savedCoupon = localStorage.getItem('homaroma_active_coupon')
const savedCoupon = localStorage.getItem('app_active_coupon')
if (savedCoupon) {
setCouponCode(savedCoupon)
// We will trigger handleApplyCoupon but we need to wait for items/subtotal to be ready.
@@ -174,7 +174,7 @@ export default function CheckoutPage() {
// Auto-apply saved coupon if subtotal is ready
useEffect(() => {
const savedCoupon = localStorage.getItem('homaroma_active_coupon')
const savedCoupon = localStorage.getItem('app_active_coupon')
if (savedCoupon && subtotal > 0 && !appliedCoupon && !couponError && !couponLoading) {
setCouponCode(savedCoupon)
// trigger fetch to validate
@@ -190,7 +190,7 @@ export default function CheckoutPage() {
setDiscountAmount(data.discountAmount)
setIsFreeShipping(data.isFreeShipping)
} else {
localStorage.removeItem('homaroma_active_coupon') // Remove invalid
localStorage.removeItem('app_active_coupon') // Remove invalid
}
})
.catch(console.error)
@@ -254,7 +254,7 @@ export default function CheckoutPage() {
setDiscountAmount(0)
setIsFreeShipping(false)
setCouponError('')
localStorage.removeItem('homaroma_active_coupon')
localStorage.removeItem('app_active_coupon')
}
const handleSubmit = async (e: React.FormEvent) => {
@@ -309,10 +309,10 @@ export default function CheckoutPage() {
if (res.ok) {
clearCart()
const existingStr = localStorage.getItem('homaroma_local_orders')
const existingStr = localStorage.getItem('app_local_orders')
const existing = existingStr ? JSON.parse(existingStr) : []
if (!existing.includes(responseData.id)) {
localStorage.setItem('homaroma_local_orders', JSON.stringify([responseData.id, ...existing]))
localStorage.setItem('app_local_orders', JSON.stringify([responseData.id, ...existing]))
}
navigate(`/order/${responseData.id}`)
+1 -1
View File
@@ -82,7 +82,7 @@ export default function CouponsPage() {
}
const handleUseNow = (code: string) => {
localStorage.setItem('homaroma_active_coupon', code)
localStorage.setItem('app_active_coupon', code)
navigate('/shop')
}
+1 -1
View File
@@ -264,7 +264,7 @@ export default function OrderSuccessPage() {
{order.items.map(item => (
<div key={item.id} className="flex items-center gap-4 text-sm">
<div className="w-12 h-12 rounded-lg bg-spa-stone/10 flex-shrink-0 overflow-hidden border border-spa-stone/10">
<img src={item.productImage || `${import.meta.env.VITE_API_URL.replace('/api', '')}/assets/oil_ozone.png`} alt="Product" className="w-full h-full object-cover" />
<img src={item.productImage || '/images/fallback-product.svg'} alt="Product" className="w-full h-full object-cover" />
</div>
<div className="flex-1">
<p className="font-bold text-pop-black">{item.productName}</p>
+1 -1
View File
@@ -504,7 +504,7 @@ export default function ProfilePage() {
{order.items.map(item => (
<div key={item.id} className="flex items-center gap-3 text-xs">
<div className="w-10 h-10 rounded-md bg-spa-stone/10 overflow-hidden border border-spa-stone/15 flex-shrink-0">
<img src={item.productImage || `${import.meta.env.VITE_API_URL.replace('/api', '')}/assets/oil_ozone.png`} alt="Product" className="w-full h-full object-cover" />
<img src={item.productImage || '/images/fallback-product.svg'} alt="Product" className="w-full h-full object-cover" />
</div>
<div className="flex-1 min-w-0">
<p className="font-bold text-pop-black truncate">{item.productName}</p>
+1 -1
View File
@@ -32,7 +32,7 @@ export default function ShopPage() {
const params = new URLSearchParams(location.search)
const coupon = params.get('coupon')
if (coupon) {
localStorage.setItem('homaroma_active_coupon', coupon)
localStorage.setItem('app_active_coupon', coupon)
// Optionally clean URL
window.history.replaceState({}, document.title, window.location.pathname)
}
+2 -2
View File
@@ -64,7 +64,7 @@ export default function TrackOrderPage() {
.finally(() => setHistoryLoading(false))
} else {
// Load local orders if not logged in
const storedStr = localStorage.getItem('homaroma_local_orders')
const storedStr = localStorage.getItem('app_local_orders')
if (storedStr) {
setLocalOrders(JSON.parse(storedStr))
}
@@ -92,7 +92,7 @@ export default function TrackOrderPage() {
if (!localOrders.includes(data.orderId)) {
const newOrders = [data.orderId, ...localOrders]
setLocalOrders(newOrders)
localStorage.setItem('homaroma_local_orders', JSON.stringify(newOrders))
localStorage.setItem('app_local_orders', JSON.stringify(newOrders))
}
navigate(`/order/${data.orderId}`)
} else {
+1 -1
View File
@@ -31,7 +31,7 @@ export const useAuthStore = create<AuthState>()(
logout: () => set({ token: null, customer: null }),
}),
{
name: "homaroma-auth-storage",
name: "app-auth-storage",
partialize: (state) => ({ token: state.token, customer: state.customer }), // don't persist isLiffInitialized
}
)
+1 -1
View File
@@ -71,7 +71,7 @@ export const useCartStore = create<CartState>()(
}
}),
{
name: 'homaroma-cart-storage',
name: 'app-cart-storage',
partialize: (state) => ({ items: state.items }), // Only persist items, not UI state
}
)
+1 -1
View File
@@ -27,7 +27,7 @@ export const useWishlistStore = create<WishlistState>()(
}
}),
{
name: 'homaroma-wishlist',
name: 'app-wishlist',
}
)
)