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:
mstfyldz
2026-06-03 03:21:35 +03:00
co-authored by Claude Sonnet 4.6
parent bc8bd747ff
commit d369c9e258
5 changed files with 392 additions and 271 deletions
+66 -29
View File
@@ -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
View File
@@ -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>
)
}