Redesign nav: sticky horizontal scrollable category strip
19 categories can't fit in a fixed bottom tab bar. Replace with a sticky horizontal pill strip that sits below the header, auto-scrolls active tab into view via Intersection Observer, and shows emoji + label at a readable size with teal active highlight. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
bc8bd747ff
commit
d369c9e258
+66
-29
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { Category } from '@/data/menu'
|
||||
|
||||
type Props = {
|
||||
@@ -11,7 +11,9 @@ type Props = {
|
||||
export default function BottomTabBar({ categories, lang }: Props) {
|
||||
const l = lang as 'tr' | 'en'
|
||||
const [activeId, setActiveId] = useState<string>(categories[0]?.id ?? '')
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
/* ── Intersection Observer: aktif kategoriyi takip et ── */
|
||||
useEffect(() => {
|
||||
const observers: IntersectionObserver[] = []
|
||||
|
||||
@@ -25,9 +27,8 @@ export default function BottomTabBar({ categories, lang }: Props) {
|
||||
if (entry.isIntersecting) setActiveId(cat.id)
|
||||
})
|
||||
},
|
||||
{ rootMargin: '-20% 0px -75% 0px', threshold: 0 }
|
||||
{ rootMargin: '-15% 0px -80% 0px', threshold: 0 }
|
||||
)
|
||||
|
||||
observer.observe(el)
|
||||
observers.push(observer)
|
||||
})
|
||||
@@ -35,36 +36,62 @@ export default function BottomTabBar({ categories, lang }: Props) {
|
||||
return () => observers.forEach((o) => o.disconnect())
|
||||
}, [categories])
|
||||
|
||||
/* ── Aktif tab görünüme kaydır ── */
|
||||
useEffect(() => {
|
||||
if (!scrollRef.current) return
|
||||
const activeEl = scrollRef.current.querySelector<HTMLElement>(
|
||||
`[data-id="${activeId}"]`
|
||||
)
|
||||
if (!activeEl) return
|
||||
activeEl.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' })
|
||||
}, [activeId])
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="tabbar-glass fixed bottom-0 left-0 right-0 z-40"
|
||||
style={{ height: 'var(--tabbar-h)' }}
|
||||
className="sticky z-30 bg-coffee-dark border-b border-white/[0.06]"
|
||||
style={{ top: 'var(--header-h)' }}
|
||||
aria-label="Kategori navigasyonu"
|
||||
>
|
||||
<div className="flex items-start justify-around pt-2 h-full px-1">
|
||||
{/* Scrollable pill strip */}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex gap-2 overflow-x-auto px-3 py-2.5"
|
||||
style={{
|
||||
scrollbarWidth: 'none',
|
||||
msOverflowStyle: 'none',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
}}
|
||||
>
|
||||
{categories.map((cat) => {
|
||||
const isActive = activeId === cat.id
|
||||
return (
|
||||
<a
|
||||
key={cat.id}
|
||||
href={`#${cat.id}`}
|
||||
className="tab-item flex flex-col items-center gap-0.5 flex-1 py-1 rounded-lg relative"
|
||||
data-id={cat.id}
|
||||
className="flex-shrink-0 flex items-center gap-1.5 px-3 py-1.5 rounded-full transition-all duration-200 select-none"
|
||||
style={
|
||||
isActive
|
||||
? {
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(74,175,168,0.28), rgba(92,138,92,0.20))',
|
||||
border: '1px solid rgba(74,175,168,0.50)',
|
||||
boxShadow: '0 0 10px rgba(74,175,168,0.18)',
|
||||
}
|
||||
: {
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
}
|
||||
}
|
||||
>
|
||||
{/* Active pill background */}
|
||||
{isActive && (
|
||||
<span
|
||||
className="tab-pill absolute inset-x-1 inset-y-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Emoji */}
|
||||
<span
|
||||
className="relative text-lg leading-none transition-transform duration-200"
|
||||
className="text-base leading-none"
|
||||
style={{
|
||||
filter: isActive
|
||||
? 'drop-shadow(0 0 6px rgba(74,175,168,0.60))'
|
||||
? 'drop-shadow(0 0 4px rgba(74,175,168,0.55))'
|
||||
: 'none',
|
||||
transform: isActive ? 'scale(1.15)' : 'scale(1)',
|
||||
transition: 'filter 0.2s',
|
||||
}}
|
||||
>
|
||||
{cat.emoji}
|
||||
@@ -72,26 +99,36 @@ export default function BottomTabBar({ categories, lang }: Props) {
|
||||
|
||||
{/* Label */}
|
||||
<span
|
||||
className="relative font-body font-medium leading-none tracking-wide text-center truncate w-full text-center transition-colors duration-200"
|
||||
className="font-body font-medium whitespace-nowrap"
|
||||
style={{
|
||||
fontSize: '0.5rem',
|
||||
color: isActive ? '#4AAFA8' : 'rgba(245,240,232,0.32)',
|
||||
fontSize: '0.72rem',
|
||||
letterSpacing: '0.01em',
|
||||
color: isActive ? '#4AAFA8' : 'rgba(245,240,232,0.45)',
|
||||
transition: 'color 0.2s',
|
||||
}}
|
||||
>
|
||||
{cat.label[l]}
|
||||
</span>
|
||||
|
||||
{/* Active dot */}
|
||||
{isActive && (
|
||||
<span
|
||||
className="relative w-1 h-1 rounded-full bg-teal"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Sağ ve sol fade maskesi */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 left-0 w-4"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(to right, rgba(31,20,10,0.9), transparent)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 right-0 w-6"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(to left, rgba(31,20,10,0.9), transparent)',
|
||||
}}
|
||||
/>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
+14
-10
@@ -28,7 +28,7 @@ export default function MenuItemRow({ item, lang, currency, index, isLast }: Pro
|
||||
boxShadow: 'inset 0 1px 2px rgba(255,255,255,0.8)',
|
||||
}}
|
||||
>
|
||||
{item.emoji}
|
||||
{item.emoji || '✨'}
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
@@ -39,18 +39,22 @@ export default function MenuItemRow({ item, lang, currency, index, isLast }: Pro
|
||||
>
|
||||
{item.name[l]}
|
||||
</p>
|
||||
<p
|
||||
className="font-body text-muted leading-relaxed mt-0.5 line-clamp-2"
|
||||
style={{ fontSize: '0.72rem' }}
|
||||
>
|
||||
{item.description[l]}
|
||||
</p>
|
||||
{item.description && (
|
||||
<p
|
||||
className="font-body text-muted leading-relaxed mt-0.5 line-clamp-2"
|
||||
style={{ fontSize: '0.72rem' }}
|
||||
>
|
||||
{item.description[l]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Price pill */}
|
||||
<span className="price-pill flex-shrink-0">
|
||||
{currency}{item.price}
|
||||
</span>
|
||||
{item.price !== undefined && (
|
||||
<span className="price-pill flex-shrink-0">
|
||||
{currency}{item.price}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user