Files
ihaleist/src/components/TopNavigation.jsx
T

183 lines
10 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useRef, useState, useEffect, useCallback, useTransition } from 'react';
import { useApp } from '../context/AppContext';
import { Newspaper, Trophy, CalendarDays, FileCheck2, Calculator, TrendingUp, LayoutGrid, Kanban, Radar, FolderKanban, BarChart3, Webhook, Smartphone, Cpu, ShieldAlert, Globe2, Gavel, CreditCard, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
// Her sekmenin kendi rengi — pasifken ikon tonu, aktifken dolgu. Tailwind JIT
// için sınıf adları tam yazılıdır.
const RENK = {
list: { ikon: 'text-sky-600', aktif: 'border-sky-300 bg-sky-100 text-sky-900', hover: 'hover:bg-sky-50' },
bulten: { ikon: 'text-amber-600', aktif: 'border-amber-300 bg-amber-100 text-amber-900', hover: 'hover:bg-amber-50' },
sonuc: { ikon: 'text-lime-600', aktif: 'border-lime-300 bg-lime-100 text-lime-900', hover: 'hover:bg-lime-50' },
takvim: { ikon: 'text-rose-600', aktif: 'border-rose-300 bg-rose-100 text-rose-900', hover: 'hover:bg-rose-50' },
sozlesme: { ikon: 'text-emerald-600', aktif: 'border-emerald-300 bg-emerald-100 text-emerald-900', hover: 'hover:bg-emerald-50' },
kdv: { ikon: 'text-teal-600', aktif: 'border-teal-300 bg-teal-100 text-teal-900', hover: 'hover:bg-teal-50' },
sinir: { ikon: 'text-orange-600', aktif: 'border-orange-300 bg-orange-100 text-orange-900', hover: 'hover:bg-orange-50' },
fiyat: { ikon: 'text-green-600', aktif: 'border-green-300 bg-green-100 text-green-900', hover: 'hover:bg-green-50' },
kanban: { ikon: 'text-violet-600', aktif: 'border-violet-300 bg-violet-100 text-violet-900', hover: 'hover:bg-violet-50' },
rakip_radar: { ikon: 'text-red-600', aktif: 'border-red-300 bg-red-100 text-red-900', hover: 'hover:bg-red-50' },
dokuman_deposu: { ikon: 'text-indigo-600', aktif: 'border-indigo-300 bg-indigo-100 text-indigo-900', hover: 'hover:bg-indigo-50' },
analitik_dashboard: { ikon: 'text-cyan-600', aktif: 'border-cyan-300 bg-cyan-100 text-cyan-900', hover: 'hover:bg-cyan-50' },
kirmizi_bayrak: { ikon: 'text-rose-700', aktif: 'border-rose-400 bg-rose-100 text-rose-900', hover: 'hover:bg-rose-50' },
kik_kararlari: { ikon: 'text-stone-600', aktif: 'border-stone-400 bg-stone-200 text-stone-900', hover: 'hover:bg-stone-100' },
ted_eu: { ikon: 'text-blue-600', aktif: 'border-blue-300 bg-blue-100 text-blue-900', hover: 'hover:bg-blue-50' },
planlar: { ikon: 'text-pink-600', aktif: 'border-pink-300 bg-pink-100 text-pink-900', hover: 'hover:bg-pink-50' },
api_webhooks: { ikon: 'text-slate-600', aktif: 'border-slate-400 bg-slate-200 text-slate-900', hover: 'hover:bg-slate-100' },
ai_ayarlari: { ikon: 'text-purple-600', aktif: 'border-purple-300 bg-purple-100 text-purple-900', hover: 'hover:bg-purple-50' },
pwa_settings: { ikon: 'text-fuchsia-600', aktif: 'border-fuchsia-300 bg-fuchsia-100 text-fuchsia-900', hover: 'hover:bg-fuchsia-50' },
};
const RENK_VARSAYILAN = { ikon: 'text-slate-500', aktif: 'border-blue-200 bg-blue-50 text-blue-700', hover: 'hover:bg-slate-50' };
const NAV_GROUPS = [
{id:'discover',label:'Keşfet',tabs:['list','bulten','takvim','ted_eu']},
{id:'prepare',label:'Hazırlan',tabs:['kanban','dokuman_deposu','kdv','sinir','kik_kararlari']},
{id:'market',label:'Pazar',tabs:['sonuc','sozlesme','fiyat','rakip_radar','analitik_dashboard','kirmizi_bayrak']},
{id:'manage',label:'Yönetim',tabs:['planlar','api_webhooks','pwa_settings','ai_ayarlari']},
];
export const TopNavigation = () => {
const { activeTab, setActiveTab, isAdmin } = useApp();
// Ağır sekmeler (10 bin+ kayıtlı havuzda Kanban, analiz vb.) tıklamadan
// sonra saniyelerce hiçbir tepki vermiyordu — kullanıcı "tıklanmıyor" sanıyordu.
// Sekme değişimi geçiş (transition) olarak yapılır: tıklanan sekme ANINDA
// vurgulanır ve dönen simge gösterilir, ağır görünüm arka planda hazırlanır.
const [isPending, startTransition] = useTransition();
const [hedefTab, setHedefTab] = useState(null);
const sekmeSec = (id) => {
if (id === activeTab) return;
setHedefTab(id);
startTransition(() => setActiveTab(id));
};
const bekleyen = isPending ? hedefTab : null;
const navItems = [
{ id: 'list', label: 'Ana İhale Listesi', icon: LayoutGrid },
{ id: 'bulten', label: 'Günlük Bülten', icon: Newspaper },
{ id: 'sonuc', label: 'Sonuç Sorgula', icon: Trophy },
{ id: 'takvim', label: 'İhale Takvimi', icon: CalendarDays },
{ id: 'sozlesme', label: 'Sözleşme Sorgula', icon: FileCheck2 },
{ id: 'kdv', label: 'KDV Hesapla', icon: Calculator },
{ id: 'sinir', label: 'Sınır Değer', icon: Calculator },
{ id: 'fiyat', label: 'Fiyat Analizi', icon: TrendingUp },
{ id: 'kanban', label: 'Kanban Pano', icon: Kanban, badge: 'Sürükle Takip' },
{ id: 'rakip_radar', label: 'Rakip Radar', icon: Radar },
{ id: 'dokuman_deposu', label: 'Doküman Deposu', icon: FolderKanban },
{ id: 'analitik_dashboard', label: 'Analitik Dashboard', icon: BarChart3 },
{ id: 'kirmizi_bayrak', label: 'Kırmızı Bayrak', icon: ShieldAlert, badge: 'Şeffaflık' },
{ id: 'kik_kararlari', label: 'KİK Kurul Kararları', icon: Gavel },
{ id: 'ted_eu', label: 'AB İhaleleri (TED)', icon: Globe2 },
{ id: 'planlar', label: 'Planlar', icon: CreditCard },
{ id: 'api_webhooks', label: 'API & Webhooks', icon: Webhook },
// Sadece admin: AI Model Ayarları
...(isAdmin ? [{ id: 'ai_ayarlari', label: 'AI Model Ayarları', icon: Cpu, badge: 'Admin' }] : []),
{ id: 'pwa_settings', label: 'Mobil PWA', icon: Smartphone }
];
const activeGroup=NAV_GROUPS.find(group=>group.tabs.includes(bekleyen || activeTab)) || NAV_GROUPS[0];
const visibleItems=activeGroup.tabs.map(id=>navItems.find(item=>item.id===id)).filter(Boolean);
// 18 sekme çoğu ekrana sığmıyor. Kaydırma çubuğu gizli olduğu için sondaki
// sekmeler (Planlar, API & Webhooks, Mobil PWA) görünmez kalıyordu; taşma
// olduğunda ok düğmeleri gösterilir ve etkin sekme görünür alana kaydırılır.
const seritRef = useRef(null);
const [tasma, setTasma] = useState({ sol: false, sag: false });
const tasmaHesapla = useCallback(() => {
const el = seritRef.current;
if (!el) return;
setTasma({
sol: el.scrollLeft > 4,
sag: el.scrollLeft + el.clientWidth < el.scrollWidth - 4,
});
}, []);
useEffect(() => {
tasmaHesapla();
const el = seritRef.current;
if (!el) return;
el.addEventListener('scroll', tasmaHesapla, { passive: true });
window.addEventListener('resize', tasmaHesapla);
return () => {
el.removeEventListener('scroll', tasmaHesapla);
window.removeEventListener('resize', tasmaHesapla);
};
}, [tasmaHesapla]);
// Etkin sekme (ör. menüden 'Planlar' seçilince) görünür alana getirilir.
useEffect(() => {
const el = seritRef.current?.querySelector(`[data-tab="${activeTab}"]`);
el?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
tasmaHesapla();
}, [activeTab, activeGroup.id, tasmaHesapla]);
const kaydir = (yon) => {
seritRef.current?.scrollBy({ left: yon * 280, behavior: 'smooth' });
};
return (
<div className="bg-white border-b border-slate-200 shadow-xs sticky top-16 z-30">
<nav aria-label="Çalışma alanları" className="max-w-[1600px] mx-auto px-4 pt-2 flex flex-wrap gap-2">
{NAV_GROUPS.map(group=><button key={group.id} type="button" aria-pressed={group.id===activeGroup.id}
onClick={()=>{if(group.id!==activeGroup.id)sekmeSec(group.tabs.find(id=>navItems.some(item=>item.id===id)));}}
className={`px-4 py-2 rounded-lg text-sm font-bold border ${group.id===activeGroup.id ? 'bg-slate-900 text-white border-slate-900' : 'border-slate-200 text-slate-700 hover:bg-slate-50'}`}>
{group.label}
</button>)}
</nav>
<div className="max-w-[1600px] mx-auto px-4 relative">
{tasma.sol && (
<button
type="button"
onClick={() => kaydir(-1)}
aria-label="Önceki sekmeler"
className="absolute left-1 top-1/2 -translate-y-1/2 z-10 w-7 h-7 rounded-full bg-white border border-slate-200 shadow-md flex items-center justify-center text-slate-600 hover:text-slate-900 hover:bg-slate-50"
>
<ChevronLeft className="w-4 h-4" />
</button>
)}
{tasma.sag && (
<button
type="button"
onClick={() => kaydir(1)}
aria-label="Sonraki sekmeler"
className="absolute right-1 top-1/2 -translate-y-1/2 z-10 w-7 h-7 rounded-full bg-white border border-slate-200 shadow-md flex items-center justify-center text-slate-600 hover:text-slate-900 hover:bg-slate-50"
>
<ChevronRight className="w-4 h-4" />
</button>
)}
<nav aria-label={`${activeGroup.label} araçları`} ref={seritRef} className="flex items-center gap-1 overflow-x-auto py-1 scrollbar-none scroll-smooth">
{visibleItems.map(item => {
const Icon = item.icon;
const renk = RENK[item.id] || RENK_VARSAYILAN;
const isActive = bekleyen ? bekleyen === item.id : activeTab === item.id;
const yukleniyor = bekleyen === item.id;
return (
<button
key={item.id}
data-tab={item.id}
aria-current={isActive ? 'page' : undefined}
aria-busy={yukleniyor || undefined}
onClick={() => sekmeSec(item.id)}
className={`flex items-center gap-2 px-3.5 py-2 rounded-lg border text-xs font-semibold whitespace-nowrap transition-all select-none ${
isActive
? `${renk.aktif} shadow-2xs`
: `border-transparent text-slate-700 hover:text-slate-900 ${renk.hover}`
}`}
>
{yukleniyor
? <Loader2 className={`w-4 h-4 animate-spin ${renk.ikon}`} />
: <Icon className={`w-4 h-4 ${renk.ikon}`} />}
<span>{item.label}</span>
{item.badge && (
<span className="bg-amber-100 text-amber-800 font-bold text-[10px] px-1.5 py-0.5 rounded-full border border-amber-200">
{item.badge}
</span>
)}
</button>
);
})}
</nav>
</div>
</div>
);
};