This commit is contained in:
Your Name committed 2026-06-13 23:10:33 -04:00
1 parent 684fd6907b
commit 310e48fe6a
17 files changed
+424 -139

No files matched your search

+89 -20
View File
@@ -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>
</>
)
}