Initial commit for homaroma

This commit is contained in:
Your Name committed 2026-06-03 08:39:12 +07:00
commit f2179cfe57
269 files changed
+22635

No files matched your search

+210
View File
@@ -0,0 +1,210 @@
import { useState, useEffect, useRef } from 'react'
import { Link, useLocation } from 'react-router-dom'
import Navbar from '../components/Navbar'
import Footer from '../components/Footer'
import ProductCard from '../components/ProductCard'
import { API_BASE, type Category, type Product } from '../types'
export default function ShopPage() {
const [categories, setCategories] = useState<Category[]>([])
const [products, setProducts] = useState<Product[]>([])
const [activeCategory, setActiveCategory] = useState<number | null>(null)
const [searchInput, setSearchInput] = useState('')
const [searchQuery, setSearchQuery] = useState('')
const location = useLocation()
// Local progressive rendering
const [displayCount, setDisplayCount] = useState(24)
const loaderRef = useRef<HTMLDivElement>(null)
const [totalProducts, setTotalProducts] = useState(0)
const [loading, setLoading] = useState(true)
const [productsLoading, setProductsLoading] = useState(false)
// Handle coupon from URL
useEffect(() => {
const params = new URLSearchParams(location.search)
const coupon = params.get('coupon')
if (coupon) {
localStorage.setItem('irelax_active_coupon', coupon)
// Optionally clean URL
window.history.replaceState({}, document.title, window.location.pathname)
}
}, [location])
// Fetch categories
useEffect(() => {
fetch(`${API_BASE}/categories`)
.then((r) => r.json())
.then((data: Category[]) => {
setCategories(data)
setLoading(false)
})
.catch(console.error)
}, [])
// Debounce search input
useEffect(() => {
const timer = setTimeout(() => {
setSearchQuery(searchInput)
}, 400)
return () => clearTimeout(timer)
}, [searchInput])
// Reset scroll and displayCount when category or search changes
useEffect(() => {
window.scrollTo({ top: 0, behavior: 'smooth' })
setDisplayCount(24)
}, [activeCategory, searchQuery])
// Fetch products
useEffect(() => {
setProductsLoading(true)
const params = new URLSearchParams()
params.append('limit', '1000') // fetch all matching items locally
if (activeCategory) params.append('categoryId', activeCategory.toString())
if (searchQuery.trim()) params.append('search', searchQuery.trim())
fetch(`${API_BASE}/products?${params.toString()}`)
.then((r) => r.json())
.then((data: { data: Product[], meta?: any }) => {
setProducts(data.data || [])
if (data.meta) {
setTotalProducts(data.meta.total || 0)
}
})
.catch(console.error)
.finally(() => setProductsLoading(false))
}, [activeCategory, searchQuery])
// Intersection Observer for infinite scroll
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && products.length > displayCount) {
setDisplayCount((prev) => Math.min(prev + 12, products.length))
}
},
{ threshold: 0.1, rootMargin: '200px' }
)
if (loaderRef.current) observer.observe(loaderRef.current)
return () => observer.disconnect()
}, [products.length, displayCount, productsLoading])
return (
<div className="min-h-screen">
<Navbar categories={categories} />
{/* Page Header */}
<div className="pt-32 pb-12 px-6 text-center space-y-4 border-b border-white/5 bg-night-800/30">
<p className="text-cyan-glow uppercase tracking-[0.4em] text-xs font-semibold opacity-70">
All Products
</p>
<h1 className="text-5xl font-bold tracking-tight">The Collection</h1>
<p className="text-slate-400 max-w-md mx-auto">
Browse our full range of premium aroma products — crafted to elevate every space.
</p>
{/* Search */}
<div className="max-w-md mx-auto mt-6">
<div className="flex glass border border-white/10 rounded-xl overflow-hidden">
<svg className="w-4 h-4 text-slate-400 m-3.5 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"
placeholder="Search by scent or category..."
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
className="bg-transparent border-none outline-none text-sm p-2 w-full text-white placeholder-slate-500"
/>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-6 py-12 space-y-10">
{/* Category Filter Tabs */}
<div className="flex flex-wrap gap-3">
<button
onClick={() => setActiveCategory(null)}
className={`px-5 py-2 rounded-full text-xs font-bold uppercase tracking-widest transition-all duration-300 ${
activeCategory === null
? 'bg-cyan-glow text-night-900 shadow-[0_0_15px_rgba(102,252,241,0.3)]'
: 'glass hover:bg-white/10'
}`}
>
All
</button>
{categories.map((cat) => (
<button
key={cat.id}
onClick={() => setActiveCategory(cat.id)}
className={`px-5 py-2 rounded-full text-xs font-bold uppercase tracking-widest transition-all duration-300 ${
activeCategory === cat.id
? 'bg-cyan-glow text-night-900 shadow-[0_0_15px_rgba(102,252,241,0.3)]'
: 'glass hover:bg-white/10'
}`}
>
{cat.name}
{cat._count && (
<span className="ml-2 opacity-60">({cat._count.products})</span>
)}
</button>
))}
</div>
{/* Results count */}
{(!loading) && (
<div className="flex items-center justify-between">
<p className="text-slate-500 text-sm">
{productsLoading ? 'Loading...' : `${totalProducts} products`}
</p>
{activeCategory && (
<Link
to={`/category/${activeCategory}`}
className="text-cyan-glow text-xs font-bold uppercase tracking-widest hover:underline"
>
View Category Page →
</Link>
)}
</div>
)}
{/* Products Grid */}
{loading || productsLoading ? (
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 md:gap-8">
{Array.from({ length: 9 }).map((_, i) => (
<div key={i} className="rounded-2xl bg-white/5 animate-pulse h-96" />
))}
</div>
) : products.length === 0 ? (
<div className="text-center py-24 text-slate-500 space-y-3">
<p className="text-4xl">🌿</p>
<p className="text-lg">No products found</p>
<p className="text-sm">Try adjusting your search or filter</p>
</div>
) : (
<div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 md:gap-8">
{products.slice(0, displayCount).map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
{/* Invisible target for IntersectionObserver */}
{displayCount < products.length && (
<div ref={loaderRef} className="py-12 flex justify-center">
<div className="w-8 h-8 rounded-full border-2 border-cyan-glow border-t-transparent animate-spin" />
</div>
)}
</div>
)}
</div>
<Footer categories={categories} />
</div>
)
}