x
This commit is contained in:
1 parent
684fd6907b
commit
310e48fe6a
17 files changed
+424
-139
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { Link, NavLink, useNavigate } from 'react-router-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { API_BASE, type Category, type MenuItem } from '../types'
|
||||
import { ShoppingBag, MapPin, Menu as MenuIcon, X, Heart } from 'lucide-react'
|
||||
import { ShoppingBag, MapPin, Menu as MenuIcon, X, Heart, Globe, Check } from 'lucide-react'
|
||||
import { useCartStore } from '../store/cartStore'
|
||||
import { useWishlistStore } from '../store/wishlistStore'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
@@ -16,6 +16,7 @@ interface NavbarProps {
|
||||
export default function Navbar({ categories = [], topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const [langSheetOpen, setLangSheetOpen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const [localOrdersCount, setLocalOrdersCount] = useState(0)
|
||||
@@ -29,12 +30,20 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' }
|
||||
const { customer } = useAuthStore()
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const toggleLanguage = () => {
|
||||
const newLang = i18n.language.startsWith('en') ? 'th' : 'en'
|
||||
i18n.changeLanguage(newLang)
|
||||
localStorage.setItem('homaroma_lang', newLang)
|
||||
}
|
||||
const LANGUAGES = [
|
||||
{ code: 'th', label: 'ไทย', short: 'TH' },
|
||||
{ code: 'en', label: 'English', short: 'EN' },
|
||||
{ code: 'ja', label: '日本語', short: 'JP' },
|
||||
{ code: 'zh', label: '简体中文', short: 'ZH' },
|
||||
{ code: 'vi', label: 'Tiếng Việt', short: 'VN' },
|
||||
{ code: 'ko', label: '한국어', short: 'KO' },
|
||||
]
|
||||
|
||||
const changeLanguage = (code: string) => {
|
||||
i18n.changeLanguage(code)
|
||||
localStorage.setItem('homaroma_lang', code)
|
||||
setLangSheetOpen(false)
|
||||
}
|
||||
// Detect scroll and announcement bar presence
|
||||
useEffect(() => {
|
||||
const checkAnnouncement = () => {
|
||||
@@ -80,9 +89,9 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' }
|
||||
useEffect(() => { setMobileOpen(false) }, [navigate])
|
||||
// Prevent body scroll when overlays open
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = (searchOpen || mobileOpen) ? 'hidden' : ''
|
||||
document.body.style.overflow = (searchOpen || mobileOpen || langSheetOpen) ? 'hidden' : ''
|
||||
return () => { document.body.style.overflow = '' }
|
||||
}, [searchOpen, mobileOpen])
|
||||
}, [searchOpen, mobileOpen, langSheetOpen])
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -141,8 +150,8 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' }
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-4 w-52 bg-white/95 backdrop-blur-xl border border-black/5 rounded-xl overflow-hidden opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300 shadow-2xl">
|
||||
<div className="p-2 space-y-0.5">
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 pt-4 w-52 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300">
|
||||
<div className="bg-white/95 backdrop-blur-xl border border-black/5 rounded-xl overflow-hidden shadow-2xl p-2 space-y-0.5">
|
||||
{categories.map((cat) => (
|
||||
<Link
|
||||
key={cat.id}
|
||||
@@ -224,12 +233,29 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' }
|
||||
{t('nav.shopNow')}
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={toggleLanguage}
|
||||
className="px-3 py-2 rounded-full text-xs font-bold transition-all text-spa-forest border border-spa-forest/30 hover:bg-spa-sage bg-white"
|
||||
>
|
||||
{i18n.language.startsWith('en') ? 'EN' : 'TH'}
|
||||
</button>
|
||||
<div className="relative group flex items-center">
|
||||
<button className="px-3 py-2 rounded-full text-[10px] font-bold transition-all text-spa-forest border border-spa-forest/30 hover:bg-spa-sage bg-white flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Globe className="w-3.5 h-3.5" strokeWidth={1.5} />
|
||||
{LANGUAGES.find(l => i18n.language.startsWith(l.code))?.short || 'TH'}
|
||||
</button>
|
||||
<div className="absolute top-full right-0 pt-2 w-36 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300">
|
||||
<div className="bg-white/95 backdrop-blur-xl border border-spa-stone/10 rounded-xl overflow-hidden shadow-2xl p-1.5 space-y-0.5">
|
||||
{LANGUAGES.map(lang => {
|
||||
const isActive = i18n.language.startsWith(lang.code)
|
||||
return (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => changeLanguage(lang.code)}
|
||||
className={`w-full flex items-center justify-between px-3 py-2.5 text-xs rounded-lg transition-colors font-medium ${isActive ? 'text-spa-forest bg-spa-sage/60 font-bold' : 'text-spa-stone hover:bg-spa-light hover:text-spa-forest'}`}
|
||||
>
|
||||
<span>{lang.label}</span>
|
||||
{isActive && <Check className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{customer ? (
|
||||
<div className="relative group flex items-center">
|
||||
@@ -240,8 +266,8 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' }
|
||||
className="w-9 h-9 rounded-full border-2 border-spa-forest/30 cursor-pointer object-cover animate-fade-in"
|
||||
/>
|
||||
</Link>
|
||||
<div className="absolute top-full right-0 mt-4 w-36 bg-white/95 backdrop-blur-xl border border-spa-stone/10 rounded-xl overflow-hidden opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300 shadow-xl">
|
||||
<div className="p-1">
|
||||
<div className="absolute top-full right-0 pt-3 w-36 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300">
|
||||
<div className="bg-white/95 backdrop-blur-xl border border-spa-stone/10 rounded-xl overflow-hidden shadow-xl p-1">
|
||||
<div className="px-3 py-2 border-b border-spa-stone/10 mb-1">
|
||||
<p className="text-[10px] text-spa-stone/60 uppercase tracking-widest leading-tight truncate">Signed in as</p>
|
||||
<p className="text-xs text-spa-forest font-bold truncate">{customer.displayName}</p>
|
||||
@@ -377,8 +403,9 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' }
|
||||
{customer && (
|
||||
<img src={customer.pictureUrl || ''} className="w-6 h-6 rounded-full border-2 border-spa-forest/30" />
|
||||
)}
|
||||
<button onClick={toggleLanguage} className="px-2.5 py-1 rounded-md text-[10px] font-bold text-spa-forest border border-spa-forest/30 uppercase bg-spa-sage">
|
||||
{i18n.language.startsWith('en') ? 'EN' : 'TH'}
|
||||
<button onClick={() => setLangSheetOpen(true)} className="px-2.5 py-1 rounded-md text-[10px] font-bold text-spa-forest border border-spa-forest/30 uppercase bg-spa-sage flex items-center gap-1 shrink-0">
|
||||
<Globe className="w-3 h-3" />
|
||||
{LANGUAGES.find(l => i18n.language.startsWith(l.code))?.short || 'TH'}
|
||||
</button>
|
||||
<button onClick={() => setMobileOpen(false)} className="p-1.5 rounded-full hover:bg-spa-sage transition-all text-spa-stone">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -532,6 +559,48 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' }
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* ── MOBILE LANGUAGE BOTTOM SHEET ── */}
|
||||
<div
|
||||
className={`fixed inset-0 z-[110] md:hidden transition-all duration-300 ${langSheetOpen ? 'visible' : 'invisible'}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute inset-0 bg-spa-stone/40 backdrop-blur-sm transition-opacity duration-300 ${langSheetOpen ? 'opacity-100' : 'opacity-0'}`}
|
||||
onClick={() => setLangSheetOpen(false)}
|
||||
/>
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 bg-white rounded-t-3xl transition-transform duration-300 shadow-2xl pb-safe ${langSheetOpen ? 'translate-y-0' : 'translate-y-full'}`}
|
||||
>
|
||||
<div className="flex justify-center pt-3 pb-1">
|
||||
<div className="w-12 h-1.5 bg-spa-stone/20 rounded-full" />
|
||||
</div>
|
||||
<div className="px-6 pb-6 pt-2">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-sm font-bold uppercase tracking-widest text-spa-forest flex items-center gap-2">
|
||||
<Globe className="w-4 h-4" />
|
||||
Select Language
|
||||
</h3>
|
||||
<button onClick={() => setLangSheetOpen(false)} className="p-2 rounded-full hover:bg-spa-sage text-spa-stone">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{LANGUAGES.map(lang => {
|
||||
const isActive = i18n.language.startsWith(lang.code)
|
||||
return (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => changeLanguage(lang.code)}
|
||||
className={`w-full flex items-center justify-between px-4 py-4 rounded-xl transition-colors font-medium border border-transparent ${isActive ? 'bg-spa-sage border-spa-forest/20 text-spa-forest font-bold' : 'bg-spa-light/50 text-spa-stone hover:bg-spa-light'}`}
|
||||
>
|
||||
<span className="text-base">{lang.label}</span>
|
||||
{isActive && <Check className="w-5 h-5" />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Navbar from '../components/Navbar'
|
||||
import Footer from '../components/Footer'
|
||||
import ProductCard from '../components/ProductCard'
|
||||
@@ -13,6 +14,7 @@ export default function CategoryPage() {
|
||||
const [category, setCategory] = useState<Category | null>(null)
|
||||
const [products, setProducts] = useState<Product[]>([])
|
||||
const [cmsSections, setCmsSections] = useState<PageSection[]>([])
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
// Local progressive rendering
|
||||
const [displayCount, setDisplayCount] = useState(24)
|
||||
@@ -22,14 +24,14 @@ export default function CategoryPage() {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/categories`)
|
||||
fetch(`${API_BASE}/categories?lang=${i18n.language}`)
|
||||
.then((r) => r.json())
|
||||
.then(setCategories)
|
||||
.catch(console.error)
|
||||
}, [])
|
||||
}, [i18n.language])
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/pages/category-layout`)
|
||||
fetch(`${API_BASE}/pages/category-layout?lang=${i18n.language}`)
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error('Page not found');
|
||||
return r.json();
|
||||
@@ -42,7 +44,7 @@ export default function CategoryPage() {
|
||||
.catch((err) => {
|
||||
console.log('Category dynamic layout sections not loaded:', err.message);
|
||||
});
|
||||
}, [])
|
||||
}, [i18n.language])
|
||||
|
||||
// Reset state when category changes
|
||||
useEffect(() => {
|
||||
@@ -57,8 +59,8 @@ export default function CategoryPage() {
|
||||
|
||||
Promise.all([
|
||||
// Fetch up to 1000 items (essentially all) in one go
|
||||
fetch(`${API_BASE}/categories/${id}/products?limit=1000`),
|
||||
fetch(`${API_BASE}/categories`),
|
||||
fetch(`${API_BASE}/categories/${id}/products?limit=1000&lang=${i18n.language}`),
|
||||
fetch(`${API_BASE}/categories?lang=${i18n.language}`),
|
||||
])
|
||||
.then(async ([catRes, allCatsRes]) => {
|
||||
if (!catRes.ok) throw new Error('Category not found')
|
||||
@@ -71,7 +73,7 @@ export default function CategoryPage() {
|
||||
})
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false))
|
||||
}, [id])
|
||||
}, [id, i18n.language])
|
||||
|
||||
// Intersection Observer for infinite scroll
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Navbar from '../components/Navbar';
|
||||
import Footer from '../components/Footer';
|
||||
import DynamicPageRenderer from '../components/DynamicPageRenderer';
|
||||
@@ -13,6 +14,7 @@ export default function CustomPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [categoriesLoading, setCategoriesLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { i18n } = useTranslation();
|
||||
const announcementSections = sections.filter(
|
||||
(section) => section.isActive && section.type.toUpperCase() === 'ANNOUNCEMENT_BAR'
|
||||
);
|
||||
@@ -24,7 +26,7 @@ export default function CustomPage() {
|
||||
useEffect(() => {
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/categories`);
|
||||
const res = await fetch(`${API_BASE}/categories?lang=${i18n.language}`);
|
||||
const cats: Category[] = await res.json();
|
||||
setCategories(cats);
|
||||
} catch (err) {
|
||||
@@ -39,7 +41,7 @@ export default function CustomPage() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/pages/${slug}`);
|
||||
const res = await fetch(`${API_BASE}/pages/${slug}?lang=${i18n.language}`);
|
||||
if (!res.ok) {
|
||||
setError('Page not found');
|
||||
return;
|
||||
@@ -60,7 +62,7 @@ export default function CustomPage() {
|
||||
|
||||
fetchCategories();
|
||||
fetchPageLayout();
|
||||
}, [slug]);
|
||||
}, [slug, i18n.language]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#FDFDF9] text-[#1F3D2D]">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Navbar from '../components/Navbar'
|
||||
import Footer from '../components/Footer'
|
||||
import DynamicPageRenderer from '../components/DynamicPageRenderer'
|
||||
@@ -9,11 +10,12 @@ export default function HomePage() {
|
||||
const [sections, setSections] = useState<PageSection[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [categoriesLoading, setCategoriesLoading] = useState(true)
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/categories`)
|
||||
const res = await fetch(`${API_BASE}/categories?lang=${i18n.language}`)
|
||||
const cats: Category[] = await res.json()
|
||||
setCategories(cats)
|
||||
} catch (err) {
|
||||
@@ -25,7 +27,7 @@ export default function HomePage() {
|
||||
|
||||
const fetchPageLayout = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/pages/home`)
|
||||
const res = await fetch(`${API_BASE}/pages/home?lang=${i18n.language}`)
|
||||
const pageData = await res.json()
|
||||
if (pageData && pageData.sections) {
|
||||
setSections(pageData.sections)
|
||||
@@ -39,7 +41,7 @@ export default function HomePage() {
|
||||
|
||||
fetchCategories()
|
||||
fetchPageLayout()
|
||||
}, [])
|
||||
}, [i18n.language])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-pop-bg">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Navbar from '../components/Navbar'
|
||||
import Footer from '../components/Footer'
|
||||
import ProductCard from '../components/ProductCard'
|
||||
@@ -21,16 +22,17 @@ export default function ProductDetailPage() {
|
||||
const [cmsSections, setCmsSections] = useState<PageSection[]>([])
|
||||
|
||||
const { addToCart } = useCartStore()
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/categories`)
|
||||
fetch(`${API_BASE}/categories?lang=${i18n.language}`)
|
||||
.then((r) => r.json())
|
||||
.then(setCategories)
|
||||
.catch(console.error)
|
||||
}, [])
|
||||
}, [i18n.language])
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/pages/product-detail-layout`)
|
||||
fetch(`${API_BASE}/pages/product-detail-layout?lang=${i18n.language}`)
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error('Page not found');
|
||||
return r.json();
|
||||
@@ -43,7 +45,7 @@ export default function ProductDetailPage() {
|
||||
.catch((err) => {
|
||||
console.log('Product detail dynamic layout sections not loaded:', err.message);
|
||||
});
|
||||
}, [])
|
||||
}, [i18n.language])
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
@@ -51,7 +53,7 @@ export default function ProductDetailPage() {
|
||||
setError(null)
|
||||
setSelectedVariant(0)
|
||||
|
||||
fetch(`${API_BASE}/products/${id}`)
|
||||
fetch(`${API_BASE}/products/${id}?lang=${i18n.language}`)
|
||||
.then(async (r) => {
|
||||
if (!r.ok) throw new Error('Product not found')
|
||||
return r.json()
|
||||
@@ -59,14 +61,14 @@ export default function ProductDetailPage() {
|
||||
.then(async (data: Product) => {
|
||||
setProduct(data)
|
||||
// Fetch related (same category, different product)
|
||||
const relRes = await fetch(`${API_BASE}/products?categoryId=${data.category.id}`)
|
||||
const relRes = await fetch(`${API_BASE}/products?categoryId=${data.category.id}&lang=${i18n.language}`)
|
||||
const relJson = await relRes.json()
|
||||
const relData: Product[] = relJson.data || []
|
||||
setRelated(relData.filter((p) => p.id !== data.id).slice(0, 6))
|
||||
})
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false))
|
||||
}, [id])
|
||||
}, [id, i18n.language])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Navbar from '../components/Navbar'
|
||||
import Footer from '../components/Footer'
|
||||
import DynamicPageRenderer from '../components/DynamicPageRenderer'
|
||||
@@ -9,14 +10,15 @@ export default function ScentGuidePage() {
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
const [articles, setArticles] = useState<Article[]>([])
|
||||
const [cmsSections, setCmsSections] = useState<PageSection[]>([])
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/categories`).then(r => r.json()).then(setCategories).catch(console.error)
|
||||
fetch(`${API_BASE}/categories?lang=${i18n.language}`).then(r => r.json()).then(setCategories).catch(console.error)
|
||||
fetch(`${API_BASE}/articles?publishedOnly=true`).then(r => r.json()).then(setArticles).catch(console.error)
|
||||
}, [])
|
||||
}, [i18n.language])
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/pages/scent-guide-layout`)
|
||||
fetch(`${API_BASE}/pages/scent-guide-layout?lang=${i18n.language}`)
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error('Page not found');
|
||||
return r.json();
|
||||
@@ -29,7 +31,7 @@ export default function ScentGuidePage() {
|
||||
.catch((err) => {
|
||||
console.log('Scent guide dynamic layout sections not loaded:', err.message);
|
||||
});
|
||||
}, [])
|
||||
}, [i18n.language])
|
||||
|
||||
const announcementSections = cmsSections.filter(s => s.isActive && s.type.toUpperCase() === 'ANNOUNCEMENT_BAR');
|
||||
const heroSections = cmsSections.filter(s => s.isActive && (s.type.toUpperCase() === 'HERO' || s.type.toUpperCase() === 'BANNER'));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useSearchParams, Link } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Navbar from '../components/Navbar'
|
||||
import Footer from '../components/Footer'
|
||||
import ProductCard from '../components/ProductCard'
|
||||
@@ -17,13 +18,14 @@ export default function SearchPage() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [searched, setSearched] = useState(false)
|
||||
const [cmsSections, setCmsSections] = useState<PageSection[]>([])
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/categories`).then(r => r.json()).then(setCategories).catch(console.error)
|
||||
}, [])
|
||||
fetch(`${API_BASE}/categories?lang=${i18n.language}`).then(r => r.json()).then(setCategories).catch(console.error)
|
||||
}, [i18n.language])
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/pages/search-layout`)
|
||||
fetch(`${API_BASE}/pages/search-layout?lang=${i18n.language}`)
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error('Page not found');
|
||||
return r.json();
|
||||
@@ -36,14 +38,14 @@ export default function SearchPage() {
|
||||
.catch((err) => {
|
||||
console.log('Search dynamic layout sections not loaded:', err.message);
|
||||
});
|
||||
}, [])
|
||||
}, [i18n.language])
|
||||
|
||||
const doSearch = useCallback(async (q: string) => {
|
||||
if (!q.trim()) { setProducts([]); setSearched(false); return }
|
||||
setLoading(true)
|
||||
setSearched(true)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/products?search=${encodeURIComponent(q)}`)
|
||||
const res = await fetch(`${API_BASE}/products?search=${encodeURIComponent(q)}&lang=${i18n.language}`)
|
||||
const paginated = await res.json()
|
||||
setProducts(paginated.data || [])
|
||||
} catch (err) {
|
||||
@@ -51,7 +53,7 @@ export default function SearchPage() {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
}, [i18n.language])
|
||||
|
||||
// Search when URL query changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Navbar from '../components/Navbar'
|
||||
import Footer from '../components/Footer'
|
||||
import ProductCard from '../components/ProductCard'
|
||||
@@ -24,6 +25,7 @@ export default function ShopPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [productsLoading, setProductsLoading] = useState(false)
|
||||
const [cmsSections, setCmsSections] = useState<PageSection[]>([])
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
// Handle coupon from URL
|
||||
useEffect(() => {
|
||||
@@ -38,18 +40,18 @@ export default function ShopPage() {
|
||||
|
||||
// Fetch categories
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/categories`)
|
||||
fetch(`${API_BASE}/categories?lang=${i18n.language}`)
|
||||
.then((r) => r.json())
|
||||
.then((data: Category[]) => {
|
||||
setCategories(data)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(console.error)
|
||||
}, [])
|
||||
}, [i18n.language])
|
||||
|
||||
// Fetch CMS sections for shop-layout
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/pages/shop-layout`)
|
||||
fetch(`${API_BASE}/pages/shop-layout?lang=${i18n.language}`)
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error('Page not found');
|
||||
return r.json();
|
||||
@@ -62,7 +64,7 @@ export default function ShopPage() {
|
||||
.catch((err) => {
|
||||
console.log('Shop dynamic layout sections not loaded:', err.message);
|
||||
});
|
||||
}, [])
|
||||
}, [i18n.language])
|
||||
|
||||
// Debounce search input
|
||||
useEffect(() => {
|
||||
@@ -86,6 +88,7 @@ export default function ShopPage() {
|
||||
params.append('limit', '1000') // fetch all matching items locally
|
||||
if (activeCategory) params.append('categoryId', activeCategory.toString())
|
||||
if (searchQuery.trim()) params.append('search', searchQuery.trim())
|
||||
params.append('lang', i18n.language)
|
||||
|
||||
fetch(`${API_BASE}/products?${params.toString()}`)
|
||||
.then((r) => r.json())
|
||||
@@ -97,7 +100,7 @@ export default function ShopPage() {
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => setProductsLoading(false))
|
||||
}, [activeCategory, searchQuery])
|
||||
}, [activeCategory, searchQuery, i18n.language])
|
||||
|
||||
// Intersection Observer for infinite scroll
|
||||
useEffect(() => {
|
||||
|
||||
Reference in new issue
Block a user