188 lines
7.3 KiB
TypeScript
188 lines
7.3 KiB
TypeScript
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'
|
|
import EmptyState from '../components/EmptyState'
|
|
import DynamicPageRenderer from '../components/DynamicPageRenderer'
|
|
import { API_BASE, type Category, type Product, type PageSection } from '../types'
|
|
|
|
export default function SearchPage() {
|
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
const query = searchParams.get('q') ?? ''
|
|
|
|
const [input, setInput] = useState(query)
|
|
const [categories, setCategories] = useState<Category[]>([])
|
|
const [products, setProducts] = useState<Product[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [searched, setSearched] = useState(false)
|
|
const [cmsSections, setCmsSections] = useState<PageSection[]>([])
|
|
const { i18n } = useTranslation()
|
|
|
|
useEffect(() => {
|
|
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?lang=${i18n.language}`)
|
|
.then((r) => {
|
|
if (!r.ok) throw new Error('Page not found');
|
|
return r.json();
|
|
})
|
|
.then((data) => {
|
|
if (data && data.sections) {
|
|
setCmsSections(data.sections);
|
|
}
|
|
})
|
|
.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)}&lang=${i18n.language}`)
|
|
const paginated = await res.json()
|
|
setProducts(paginated.data || [])
|
|
} catch (err) {
|
|
console.error(err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [i18n.language])
|
|
|
|
// Search when URL query changes
|
|
useEffect(() => {
|
|
setInput(query)
|
|
doSearch(query)
|
|
}, [query, doSearch])
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setSearchParams(input.trim() ? { q: input.trim() } : {})
|
|
}
|
|
|
|
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'));
|
|
const bottomSections = cmsSections.filter(s => s.isActive && s.type.toUpperCase() !== 'ANNOUNCEMENT_BAR' && s.type.toUpperCase() !== 'HERO' && s.type.toUpperCase() !== 'BANNER');
|
|
|
|
return (
|
|
<div className="min-h-screen bg-pop-bg">
|
|
{announcementSections.length > 0 && (
|
|
<DynamicPageRenderer
|
|
sections={announcementSections}
|
|
categories={categories}
|
|
categoriesLoading={loading}
|
|
/>
|
|
)}
|
|
|
|
<Navbar categories={categories} />
|
|
|
|
{/* Search Header */}
|
|
{heroSections.length > 0 ? (
|
|
<div className="pt-20">
|
|
<DynamicPageRenderer
|
|
sections={heroSections}
|
|
categories={categories}
|
|
categoriesLoading={loading}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="pt-32 pb-16 px-6 bg-spa-light border-b border-spa-stone/10">
|
|
<div className="max-w-3xl mx-auto space-y-6 text-center">
|
|
<p className="text-spa-forest uppercase tracking-[0.4em] text-xs font-bold opacity-80">
|
|
Discover
|
|
</p>
|
|
<h1 className="text-5xl font-bold text-pop-black">Search Products</h1>
|
|
|
|
<form onSubmit={handleSubmit} className="relative">
|
|
<div className="flex bg-spa-light border border-spa-stone/20 rounded-2xl overflow-hidden shadow-sm focus-within:border-spa-forest focus-within:ring-1 focus-within:ring-spa-forest transition-all">
|
|
<svg className="w-5 h-5 text-spa-stone m-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
<input
|
|
type="text"
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
placeholder="Search by scent name, category..."
|
|
className="bg-transparent border-none outline-none text-base p-4 pl-0 w-full text-pop-black placeholder-spa-stone/60"
|
|
autoFocus
|
|
/>
|
|
<button
|
|
type="submit"
|
|
className="bg-spa-forest text-spa-light px-8 font-bold text-xs uppercase tracking-widest hover:bg-spa-stone transition-colors shrink-0"
|
|
>
|
|
Search
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
{/* Quick category links */}
|
|
<div className="flex flex-wrap justify-center gap-2 pt-2">
|
|
{categories.map((cat) => (
|
|
<Link
|
|
key={cat.id}
|
|
to={`/search?q=${encodeURIComponent(typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[i18n.language] || (cat.name as any)?.en || '')}`}
|
|
className="bg-spa-light border border-spa-stone/20 px-4 py-1.5 rounded-full text-xs font-medium text-spa-stone hover:border-spa-forest hover:text-spa-forest shadow-sm transition-all duration-300"
|
|
>
|
|
{typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[i18n.language] || (cat.name as any)?.en || ''}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Results */}
|
|
<div className="max-w-7xl mx-auto px-6 py-16 space-y-8">
|
|
{!searched && !query && (
|
|
<div className="text-center py-20 space-y-3 text-spa-stone">
|
|
<p className="text-5xl">🔍</p>
|
|
<p className="text-lg font-medium text-pop-black">Type something to discover our collection</p>
|
|
</div>
|
|
)}
|
|
|
|
{searched && (
|
|
<p className="text-spa-stone text-sm font-medium">
|
|
{loading ? 'Searching...' : `${products.length} result${products.length !== 1 ? 's' : ''} for `}
|
|
{!loading && <span className="text-pop-black font-bold">"{query}"</span>}
|
|
</p>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
<div key={i} className="rounded-2xl bg-spa-stone/10 animate-pulse h-96" />
|
|
))}
|
|
</div>
|
|
) : searched && products.length === 0 ? (
|
|
<EmptyState
|
|
title="No results found"
|
|
message="Try a different keyword or browse our categories"
|
|
actionText="Browse All Products"
|
|
actionLink="/shop"
|
|
/>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
{products.map((p) => <ProductCard key={p.id} product={p} />)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{bottomSections.length > 0 && (
|
|
<DynamicPageRenderer
|
|
sections={bottomSections}
|
|
categories={categories}
|
|
categoriesLoading={loading}
|
|
/>
|
|
)}
|
|
|
|
<Footer categories={categories} />
|
|
</div>
|
|
)
|
|
}
|