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>
135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useRef, useState } from 'react'
|
||
import type { Category } from '@/data/menu'
|
||
|
||
type Props = {
|
||
categories: Category[]
|
||
lang: string
|
||
}
|
||
|
||
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[] = []
|
||
|
||
categories.forEach((cat) => {
|
||
const el = document.getElementById(cat.id)
|
||
if (!el) return
|
||
|
||
const observer = new IntersectionObserver(
|
||
(entries) => {
|
||
entries.forEach((entry) => {
|
||
if (entry.isIntersecting) setActiveId(cat.id)
|
||
})
|
||
},
|
||
{ rootMargin: '-15% 0px -80% 0px', threshold: 0 }
|
||
)
|
||
observer.observe(el)
|
||
observers.push(observer)
|
||
})
|
||
|
||
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="sticky z-30 bg-coffee-dark border-b border-white/[0.06]"
|
||
style={{ top: 'var(--header-h)' }}
|
||
aria-label="Kategori navigasyonu"
|
||
>
|
||
{/* 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}`}
|
||
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)',
|
||
}
|
||
}
|
||
>
|
||
{/* Emoji */}
|
||
<span
|
||
className="text-base leading-none"
|
||
style={{
|
||
filter: isActive
|
||
? 'drop-shadow(0 0 4px rgba(74,175,168,0.55))'
|
||
: 'none',
|
||
transition: 'filter 0.2s',
|
||
}}
|
||
>
|
||
{cat.emoji}
|
||
</span>
|
||
|
||
{/* Label */}
|
||
<span
|
||
className="font-body font-medium whitespace-nowrap"
|
||
style={{
|
||
fontSize: '0.72rem',
|
||
letterSpacing: '0.01em',
|
||
color: isActive ? '#4AAFA8' : 'rgba(245,240,232,0.45)',
|
||
transition: 'color 0.2s',
|
||
}}
|
||
>
|
||
{cat.label[l]}
|
||
</span>
|
||
</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>
|
||
)
|
||
}
|