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

+202
View File
@@ -0,0 +1,202 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Navbar from '../components/Navbar'
import Footer from '../components/Footer'
import ProductCard from '../components/ProductCard'
import { API_BASE, getCategoryImage, type Category, type Product, type Hero } from '../types'
export default function HomePage() {
const [categories, setCategories] = useState<Category[]>([])
const [featuredProducts, setFeaturedProducts] = useState<Product[]>([])
const [hero, setHero] = useState<Hero | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const fetchData = async () => {
try {
const [catRes, prodRes, heroRes] = await Promise.all([
fetch(`${API_BASE}/categories`),
fetch(`${API_BASE}/products?categoryId=1`),
fetch(`${API_BASE}/hero`),
])
const cats: Category[] = await catRes.json()
const prodsJson = await prodRes.json()
const heroText = await heroRes.text()
const heroData: Hero | null = heroText ? JSON.parse(heroText) : null
const prods: Product[] = prodsJson.data || []
setCategories(cats)
setFeaturedProducts(prods.slice(0, 3))
setHero(heroData)
} catch (err) {
console.error('API error:', err)
} finally {
setLoading(false)
}
}
fetchData()
}, [])
return (
<div className="min-h-screen">
<Navbar categories={categories} />
{/* Hero Section */}
<section className="relative w-full min-h-[90vh] md:min-h-0 md:aspect-video flex items-center justify-center overflow-hidden bg-night-950">
<div className="absolute inset-0 z-0">
<img
src={hero?.imageUrl || "/images/hero.png"}
className="w-full h-full object-cover scale-[1.02] brightness-50"
alt="Hero Background"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none'
}}
/>
<div className="absolute inset-0 bg-gradient-to-t from-night-900 via-transparent to-night-900/50" />
</div>
<div className="relative z-10 text-center space-y-6 px-4 animate-fade-in">
<p className="text-cyan-glow uppercase tracking-[0.4em] text-sm font-semibold opacity-80">
{hero?.tagline || "A New Paradigm of Calm"}
</p>
<h1 className="text-6xl md:text-8xl font-bold max-w-4xl text-glow leading-[1.1]">
{hero ? (
hero.title.split('[').map((part, i) => {
if (part.includes(']')) {
const [accent, rest] = part.split(']')
return (
<span key={i}>
<span className="text-transparent bg-clip-text bg-gradient-to-r from-white via-silver-metallic to-white/50 italic font-serif">
{accent}
</span>
{rest}
</span>
)
}
return <span key={i}>{part}</span>
})
) : (
<>
Inhale{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-white via-silver-metallic to-white/50 italic font-serif">
Serenity
</span>
</>
)}
</h1>
<p className="text-slate-400 max-w-xl mx-auto text-lg md:text-xl font-light">
{hero?.description || "Crafting premium sensory experiences that transform your space into a sanctuary of peace."}
</p>
<div className="pt-8 flex flex-col sm:flex-row gap-4 justify-center">
<Link
to={hero?.buttonLink || "/shop"}
className="px-10 py-4 bg-white text-night-900 font-bold rounded-sm tracking-widest uppercase hover:bg-cyan-glow hover:shadow-[0_0_30px_rgba(102,252,241,0.5)] transition-all duration-500"
>
{hero?.buttonText || "Explore Collection"}
</Link>
<a
href="#categories"
className="px-10 py-4 glass text-white font-bold rounded-sm tracking-widest uppercase hover:bg-white/10 transition-all duration-300"
>
Learn More
</a>
</div>
</div>
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 animate-bounce opacity-40">
<div className="w-px h-16 bg-gradient-to-b from-white/0 via-white to-white/0" />
</div>
</section>
{/* Categories Showcase */}
<section id="categories" className="bg-night-900 py-24 px-6">
<div className="max-w-7xl mx-auto space-y-12">
<div className="text-center space-y-2">
<p className="text-cyan-glow uppercase tracking-[0.3em] text-xs font-semibold opacity-70">
Our Range
</p>
<h2 className="text-4xl font-bold tracking-tight uppercase">The Categories</h2>
<div className="w-12 h-px bg-cyan-glow mx-auto mt-3" />
</div>
{loading ? (
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="h-56 rounded-xl bg-white/5 animate-pulse" />
))}
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
{categories.map((cat) => (
<Link
key={cat.id}
to={`/category/${cat.id}`}
className="relative h-56 rounded-xl overflow-hidden group border-2 border-white/5 hover:border-cyan-glow/50 hover:shadow-[0_0_20px_rgba(102,252,241,0.15)] transition-all duration-500"
>
<img
src={getCategoryImage(cat.name)}
className="absolute inset-0 w-full h-full object-cover grayscale opacity-40 group-hover:grayscale-0 group-hover:scale-110 group-hover:opacity-70 transition-all duration-700"
alt={cat.name}
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
/>
<div className="absolute inset-0 bg-gradient-to-t from-night-900/90 via-night-900/20 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-4">
<span className="text-white font-bold tracking-wider uppercase text-xs block">
{cat.name}
</span>
{cat._count && (
<span className="text-cyan-glow/60 text-[10px] tracking-wider">
{cat._count.products} products
</span>
)}
</div>
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<svg className="w-5 h-5 text-cyan-glow" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</div>
</Link>
))}
</div>
)}
</div>
</section>
{/* Featured Products */}
<section className="bg-night-900 py-24 px-6 border-t border-white/5">
<div className="max-w-7xl mx-auto space-y-12">
<div className="flex justify-between items-end">
<div className="space-y-2">
<p className="text-cyan-glow uppercase tracking-[0.3em] text-xs font-semibold opacity-70">
Top Picks
</p>
<h2 className="text-4xl font-bold">Featured Products</h2>
</div>
<Link
to="/shop"
className="text-cyan-glow text-sm font-bold uppercase tracking-widest hover:underline decoration-cyan-glow/30"
>
View All →
</Link>
</div>
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="rounded-2xl bg-white/5 animate-pulse h-96" />
))}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{featuredProducts.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
)}
</div>
</section>
<Footer categories={categories} />
</div>
)
}