"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { AramaFunnelKarti } from "@/components/arama-funnel-karti"; import { RehberKapak } from "@/components/rehber-kapak"; import { UniLogo } from "@/components/uni-logo"; import { aramaNormalize, eslesmePuani } from "@/lib/arama"; import type { RehberOzet } from "@/lib/rehber"; import { profilDuzenlemeyiAc, sonucaGitIste, useTercihProfili, } from "@/components/manuel-liste/profil-kapisi-store"; import { PUAN_TURU_ETIKET } from "@/lib/sihirbaz"; import { olay } from "@/lib/analitik"; import { ArrowRight, BookOpenText, Building2, GraduationCap, Search, SquarePen, Trophy, X, } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogTitle, } from "@/components/ui/dialog"; import { Kbd, KbdGroup } from "@/components/ui/kbd"; import { ProgressiveBlur } from "@/components/ui/skiper-ui/skiper41"; type BolumSonucu = { href: string; ad: string; programSayisi: number; enIyiSira: number | null; seviye: string; }; type UniversiteSonucu = { href: string; ad: string; il: string | null; tur: string | null; programSayisi: number; fakulteSayisi: number; }; type AramaSonuclari = { bolumler: BolumSonucu[]; universiteler: UniversiteSonucu[]; }; const BOS_SONUCLAR: AramaSonuclari = { bolumler: [], universiteler: [] }; const REHBER_SONUC_SINIRI = 4; const sayi = (deger: number) => deger.toLocaleString("tr-TR"); const KESFET_TONLARI = { slate: { pencere: "border-slate-200/80 bg-white", nokta: "bg-slate-300", rozet: "border-slate-200 bg-slate-50 text-slate-600", cizgi: "bg-slate-200/90", }, sky: { pencere: "border-sky-200/70 bg-white", nokta: "bg-sky-300", rozet: "border-sky-200 bg-sky-50 text-sky-600", cizgi: "bg-sky-100", }, amber: { pencere: "border-amber-200/70 bg-white", nokta: "bg-amber-300", rozet: "border-amber-200 bg-amber-50 text-amber-600", cizgi: "bg-amber-100", }, } as const; export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) { const router = useRouter(); const profil = useTercihProfili(); const [acik, setAcik] = useState(false); function siralamaGirVeyaSonucaGit() { // Navbar CTA'sı "Sıralamanı gir" dediği için kapı tanıtım adımını // atlayıp doğrudan başarı sıralaması adımıyla açılır. const { href } = sonucaGitIste({ dogrudanSira: true }); if (href) router.push(href); } const [sorgu, setSorgu] = useState(""); const [sonuclar, setSonuclar] = useState(BOS_SONUCLAR); const [yukleniyor, setYukleniyor] = useState(false); const [macMi, setMacMi] = useState(true); const inputRef = useRef(null); const kaydirmaRef = useRef(null); const [kenarBlur, setKenarBlur] = useState({ ust: false, alt: false }); // Kapanışta gönderilecek son arama özeti (bkz. aşağıdaki fetch efekti) const sonAramaRef = useRef<{ sorgu_uzunlugu: number; sonuc_sayisi: number; } | null>(null); const temizSorgu = sorgu.trim(); const aramaAktif = temizSorgu.length >= 2; const kenarBlurGuncelle = useCallback(() => { const alan = kaydirmaRef.current; if (!alan) return; const ust = alan.scrollTop > 4; const alt = alan.scrollTop + alan.clientHeight < alan.scrollHeight - 4; setKenarBlur((onceki) => onceki.ust === ust && onceki.alt === alt ? onceki : { ust, alt }, ); }, []); // Rehber özetleri build sırasında server'dan prop olarak gelir (~10 yazı); // arama client'ta yapılır — prod imajında content/ dizini bulunmaz. const rehberSonuclari = useMemo(() => { if (!aramaAktif) return []; const normalSorgu = aramaNormalize(temizSorgu).slice(0, 80); return rehberler .filter((yazi) => aramaNormalize(`${yazi.baslik} ${yazi.aciklama}`).includes(normalSorgu), ) .sort( (a, b) => eslesmePuani(a.baslik, normalSorgu) - eslesmePuani(b.baslik, normalSorgu), ) .slice(0, REHBER_SONUC_SINIRI); }, [aramaAktif, temizSorgu, rehberler]); const sonucVar = sonuclar.bolumler.length > 0 || sonuclar.universiteler.length > 0 || rehberSonuclari.length > 0; useEffect(() => { const platform = (navigator as { userAgentData?: { platform?: string } }).userAgentData ?.platform ?? navigator.platform ?? ""; setMacMi(/mac|iphone|ipad/i.test(platform)); }, []); useEffect(() => { function kisayolDinle(event: KeyboardEvent) { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { event.preventDefault(); modalDegisti(!acik); } } window.addEventListener("keydown", kisayolDinle); return () => window.removeEventListener("keydown", kisayolDinle); }, [acik]); useEffect(() => { if (!acik || !aramaAktif) return; const controller = new AbortController(); const zamanlayici = window.setTimeout(async () => { setYukleniyor(true); try { const response = await fetch( `/api/katalog-ara?q=${encodeURIComponent(temizSorgu)}`, { signal: controller.signal }, ); if (!response.ok) throw new Error("Arama başarısız"); const veri = (await response.json()) as AramaSonuclari; setSonuclar(veri); // Olay burada değil kapanışta gönderilir; fetch her tuş vuruşunda // (160ms debounce) tetikleniyor, aramayı oturum başına bir kez sayarız. sonAramaRef.current = { sorgu_uzunlugu: temizSorgu.length, sonuc_sayisi: veri.bolumler.length + veri.universiteler.length, }; } catch (hata) { if (!(hata instanceof DOMException && hata.name === "AbortError")) { setSonuclar(BOS_SONUCLAR); } } finally { if (!controller.signal.aborted) setYukleniyor(false); } }, 160); return () => { window.clearTimeout(zamanlayici); controller.abort(); }; }, [acik, aramaAktif, temizSorgu]); // Sonuçlar, logolar ve kapak görselleri yüklendikçe scrollHeight değişir; // kenar blur görünürlüğünü scroll olayına ek olarak boyut değişiminde de tazele. useEffect(() => { if (!acik) return; const alan = kaydirmaRef.current; if (!alan) return; kenarBlurGuncelle(); const gozlemci = new ResizeObserver(kenarBlurGuncelle); gozlemci.observe(alan); for (const cocuk of alan.children) gozlemci.observe(cocuk); return () => gozlemci.disconnect(); }, [acik, aramaAktif, yukleniyor, sonucVar, kenarBlurGuncelle]); // Temizlik kapanışta değil açılışta yapılır: kapanış animasyonu sırasında // içerik "Keşfet" ekranına zıplamaz, modal her açılışta temiz başlar. // (X / sonuç linki `kapat` ile kapatır; orada temizlik yapılmadığı için // eski sorgu, bayat sonuçlar ve yarıda kesilen fetch'in takılı kalan // yükleniyor iskeleti bir sonraki açılışa taşınıyordu.) function modalDegisti(yeniAcik: boolean) { setAcik(yeniAcik); if (yeniAcik) { setSorgu(""); setSonuclar(BOS_SONUCLAR); setYukleniyor(false); setKenarBlur({ ust: false, alt: false }); } } // Arama oturumu kapandığında tek bir olay — hiç arama yapılmadıysa sessiz useEffect(() => { if (acik) return; const son = sonAramaRef.current; if (!son) return; sonAramaRef.current = null; olay("katalog_arama", son); }, [acik]); function kapat() { setAcik(false); } return ( <> {/* lg altında logo + sağ küme ile çakışır (absolute); o aralıkta gizli */} {/* lg altında 2. satır: sıralama + ara (header flex-wrap + order-3) */}
{profil ? ( ) : ( )}
{ event.preventDefault(); inputRef.current?.focus(); }} className="max-h-[min(720px,calc(100dvh-2rem))] grid-rows-[auto_minmax(0,1fr)_auto] gap-0 overflow-hidden rounded-2xl bg-white p-0 duration-200 sm:max-w-2xl motion-reduce:data-open:zoom-in-100 motion-reduce:data-closed:zoom-out-100" > Bölüm, üniversite ve rehber ara YKS bölümlerini, üniversiteleri ve rehber yazılarını tek yerden ara.
{ const yeniSorgu = event.target.value; setSorgu(yeniSorgu); setSonuclar(BOS_SONUCLAR); setYukleniyor(yeniSorgu.trim().length >= 2); }} placeholder="Bölüm, üniversite veya rehber ara..." aria-label="Bölüm, üniversite veya rehber ara" autoComplete="off" className="h-16 min-w-0 flex-1 bg-transparent text-base text-slate-950 outline-none placeholder:text-slate-400 sm:h-18 sm:text-lg" />
{!aramaAktif ? ( ) : yukleniyor ? ( ) : sonucVar ? (
{sonuclar.bolumler.length > 0 ? ( } > {sonuclar.bolumler.map((bolum) => ( ))} ) : null} {sonuclar.universiteler.length > 0 ? ( } > {sonuclar.universiteler.map((universite) => ( } baslik={universite.ad} meta={ [universite.il, universite.tur] .filter(Boolean) .join(" · ") || "Üniversite" } detay={`${sayi(universite.programSayisi)} program · ${sayi(universite.fakulteSayisi)} fakülte`} onSec={kapat} /> ))} ) : null} {rehberSonuclari.length > 0 ? ( } >
{rehberSonuclari.map((yazi) => ( ))}
) : null}
) : (

"{temizSorgu}" için sonuç bulunamadı

Bölüm, üniversite veya rehber konusunu farklı yazarak tekrar dene.

)}
En az 2 harf yazarak ara Esc ile kapat
); } function BaslangicIcerigi({ onSec }: { onSec: () => void }) { return (

Keşfet

Neyi merak ediyorsun?

“Psikoloji” veya “Boğaziçi” yaz; eşleşen sayfaları ve temel bilgilerini burada önizle.

); } function KesfetLinki({ href, ikon: Ikon, baslik, kisaBaslik, aciklama, ton, onSec, }: { href: string; ikon: typeof GraduationCap; baslik: string; kisaBaslik: string; aciklama: string; ton: keyof typeof KESFET_TONLARI; onSec: () => void; }) { const palet = KESFET_TONLARI[ton]; return ( {/* Mini tarayıcı — sekme içinde tek büyük ikon, alta doğru maske ile solar */}
{kisaBaslik} {baslik} {aciklama} ); } function SonucGrubu({ baslik, ikon, children, }: { baslik: string; ikon: React.ReactNode; children: React.ReactNode; }) { return (

{ikon} {baslik}

{children}
); } function SonucLinki({ href, baslik, meta, detay, gorsel, onSec, }: { href: string; baslik: string; meta: string; detay: string; gorsel?: React.ReactNode; onSec: () => void; }) { return ( {gorsel} {baslik} {meta} {detay} ); } function RehberSonucKarti({ yazi, onSec, }: { yazi: RehberOzet; onSec: () => void; }) { return ( {yazi.aciklama} Yazıyı oku ); } function AramaIskeleti() { return (
{[0, 1, 2, 3].map((sira) => (
))}
); }