x
This commit is contained in:
1 parent
c449de2482
commit
7e4e89e33c
50 files changed
+306
-140
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ export const useWishlistStore = create<WishlistState>()(
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'homaroma-wishlist',
|
||||
name: 'app-wishlist',
|
||||
}
|
||||
)
|
||||
)
|
||||
Reference in new issue
Block a user