Rename ssr to website, website to spa, add spa nginx config

This commit is contained in:
Your Name committed 2026-06-03 09:38:15 +07:00
1 parent fc6bf0218e
commit 8514342c2f
103 files changed
+801 -768

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Search Products | iRelax Aroma',
description: 'Search our collection of premium aroma products by scent or product name.',
}
export default function SearchLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>
}
+136
View File
@@ -0,0 +1,136 @@
"use client"
import { useState, useEffect, useRef, Suspense } from 'react'
import { useSearchParams, useRouter } from 'next/navigation'
import Link from 'next/link'
import ProductCard from '../../components/ProductCard'
import { type Product } from '@homaroma/types'
import { API_BASE } from '../../lib/api'
function SearchResults() {
const searchParams = useSearchParams()
const router = useRouter()
const q = searchParams.get('q') || ''
const [query, setQuery] = useState(q)
const [products, setProducts] = useState<Product[]>([])
const [loading, setLoading] = useState(false)
const [displayCount, setDisplayCount] = useState(24)
const loaderRef = useRef<HTMLDivElement>(null)
// Sync input → URL
useEffect(() => {
const timer = setTimeout(() => {
if (query.trim()) router.replace(`/search?q=${encodeURIComponent(query.trim())}`)
}, 400)
return () => clearTimeout(timer)
}, [query, router])
// Fetch on URL change
useEffect(() => {
if (!q.trim()) { setProducts([]); return }
setLoading(true)
setDisplayCount(24)
fetch(`${API_BASE}/products?search=${encodeURIComponent(q)}&limit=1000`)
.then(r => r.json())
.then(data => setProducts(data.data || []))
.catch(console.error)
.finally(() => setLoading(false))
}, [q])
// 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])
return (
<div className="min-h-screen">
{/* 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">Search</p>
<h1 className="text-5xl font-bold tracking-tight">Find Your Scent</h1>
<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"
autoFocus
placeholder="Search by scent or product name..."
value={query}
onChange={e => setQuery(e.target.value)}
className="bg-transparent border-none outline-none text-sm p-2 w-full text-white placeholder-slate-500"
/>
{query && (
<button
onClick={() => { setQuery(''); router.replace('/search') }}
className="px-3 text-slate-400 hover:text-white transition-colors"
>✕</button>
)}
</div>
</div>
{!loading && q && (
<p className="text-slate-500 text-sm">
{products.length > 0
? `Found ${products.length} result${products.length !== 1 ? 's' : ''} for "${q}"`
: `No results for "${q}"`}
</p>
)}
</div>
<div className="max-w-7xl mx-auto px-6 py-12">
{!q.trim() ? (
<div className="text-center py-24 text-slate-500 space-y-3">
<p className="text-4xl">🔍</p>
<p className="text-lg">Type something to search</p>
<Link href="/shop" className="text-cyan-glow text-sm hover:underline">Browse All Products →</Link>
</div>
) : loading ? (
<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-4">
<p className="text-4xl">🌿</p>
<p className="text-lg font-semibold">No products found</p>
<p className="text-sm">Try a different scent name or keyword</p>
<Link href="/shop" className="text-cyan-glow text-sm hover:underline">Browse All Products →</Link>
</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>
{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>
</div>
)
}
export default function SearchPage() {
return (
<Suspense>
<SearchResults />
</Suspense>
)
}