Some checks failed
Deploy / deploy (push) Failing after 1h28m6s
- Added new scripts for database operations and temporary production in package.json. - Integrated KaydetBannerLazy component into the layout for improved user notifications. - Enhanced API error handling in the soru route to mark credit exhaustion. - Updated the IletisimPage for better button styling and user experience. - Refactored ListemPage to streamline user flow and improve session handling. - Removed unused ListePaneli component to clean up the codebase.
765 lines
28 KiB
TypeScript
765 lines
28 KiB
TypeScript
"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<AramaSonuclari>(BOS_SONUCLAR);
|
||
const [yukleniyor, setYukleniyor] = useState(false);
|
||
const [macMi, setMacMi] = useState(true);
|
||
const inputRef = useRef<HTMLInputElement>(null);
|
||
const kaydirmaRef = useRef<HTMLDivElement>(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 */}
|
||
<nav className="absolute left-1/2 hidden -translate-x-1/2 items-center gap-1 rounded-full border border-slate-200 bg-white p-1.5 text-base font-medium text-slate-600 lg:flex">
|
||
{profil ? (
|
||
<button
|
||
type="button"
|
||
onClick={profilDuzenlemeyiAc}
|
||
aria-label={`Sıralamanı ve tercihlerini değiştir (${profil.sira.toLocaleString("tr-TR")} · ${PUAN_TURU_ETIKET[profil.tur] ?? profil.tur})`}
|
||
className="group/profil flex cursor-pointer items-center gap-2 rounded-full py-1.5 pl-4 pr-1.5 transition-colors duration-150 hover:bg-slate-100 focus-visible:bg-slate-100 focus-visible:outline-none"
|
||
>
|
||
<Trophy className="size-4 shrink-0 text-orange-500" aria-hidden />
|
||
<span className="font-semibold tracking-tight text-slate-900">
|
||
<span className="tabular-nums">
|
||
{profil.sira.toLocaleString("tr-TR")}
|
||
</span>
|
||
<span className="font-medium text-slate-500">
|
||
{" "}
|
||
· {PUAN_TURU_ETIKET[profil.tur] ?? profil.tur}
|
||
</span>
|
||
</span>
|
||
<span
|
||
aria-hidden
|
||
className="flex max-w-0 items-center gap-1 overflow-hidden rounded-full bg-slate-100 px-0 py-1.5 text-xs font-medium text-slate-500 opacity-0 transition-[max-width,padding,opacity,background-color,color] duration-200 group-hover/profil:max-w-28 group-hover/profil:bg-white group-hover/profil:px-2.5 group-hover/profil:text-slate-900 group-hover/profil:opacity-100 group-focus-visible/profil:max-w-28 group-focus-visible/profil:bg-white group-focus-visible/profil:px-2.5 group-focus-visible/profil:text-slate-900 group-focus-visible/profil:opacity-100"
|
||
>
|
||
<SquarePen className="size-3 shrink-0" aria-hidden />
|
||
Değiştir
|
||
</span>
|
||
</button>
|
||
) : (
|
||
<>
|
||
<button
|
||
type="button"
|
||
onClick={siralamaGirVeyaSonucaGit}
|
||
className="flex cursor-pointer items-center gap-2 rounded-full px-4 py-1.5 text-slate-700 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900"
|
||
>
|
||
<Trophy className="size-4 text-orange-500" aria-hidden />
|
||
Sıralamanı gir
|
||
</button>
|
||
<Link
|
||
href="/#nasil-calisir"
|
||
className="rounded-full px-5 py-2 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900"
|
||
>
|
||
Nasıl çalışır?
|
||
</Link>
|
||
</>
|
||
)}
|
||
<button
|
||
type="button"
|
||
onClick={() => modalDegisti(true)}
|
||
className="flex cursor-pointer items-center gap-2 rounded-full bg-slate-900 px-5 py-2 text-white transition-[background-color,transform] duration-150 hover:bg-slate-700 active:scale-[0.97]"
|
||
>
|
||
<Search className="size-4" aria-hidden />
|
||
Ara
|
||
<KbdGroup aria-hidden className="ml-1">
|
||
<Kbd className="bg-white/15 text-white/85">
|
||
{macMi ? "⌘" : "Ctrl"}
|
||
</Kbd>
|
||
<Kbd className="bg-white/15 text-white/85">K</Kbd>
|
||
</KbdGroup>
|
||
</button>
|
||
</nav>
|
||
|
||
{/* lg altında 2. satır: sıralama + ara (header flex-wrap + order-3) */}
|
||
<div className="order-3 flex w-full basis-full items-center gap-1.5 lg:hidden">
|
||
{profil ? (
|
||
<button
|
||
type="button"
|
||
onClick={profilDuzenlemeyiAc}
|
||
aria-label={`Sıralamanı ve tercihlerini değiştir (${profil.sira.toLocaleString("tr-TR")} · ${PUAN_TURU_ETIKET[profil.tur] ?? profil.tur})`}
|
||
className="flex h-9 min-w-0 flex-1 cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-white py-1 pl-2.5 pr-1.5 text-xs font-semibold text-slate-900 transition-[background-color,transform] duration-150 hover:bg-slate-100 active:scale-[0.96] sm:h-12 sm:gap-2 sm:pl-3.5 sm:pr-2 sm:text-sm"
|
||
>
|
||
<Trophy
|
||
className="size-3.5 shrink-0 text-orange-500 sm:size-4"
|
||
aria-hidden
|
||
/>
|
||
<span className="truncate tracking-tight">
|
||
<span className="tabular-nums">
|
||
{profil.sira.toLocaleString("tr-TR")}
|
||
</span>
|
||
<span className="font-medium text-slate-500">
|
||
{" "}
|
||
· {PUAN_TURU_ETIKET[profil.tur] ?? profil.tur}
|
||
</span>
|
||
</span>
|
||
<span className="inline-flex shrink-0 items-center gap-0.5 rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-500 sm:text-xs">
|
||
<SquarePen className="size-2.5 sm:size-3" aria-hidden />
|
||
Değiştir
|
||
</span>
|
||
</button>
|
||
) : (
|
||
<button
|
||
type="button"
|
||
onClick={siralamaGirVeyaSonucaGit}
|
||
aria-label="Başarı sıralamanı gir"
|
||
className="flex h-9 min-w-0 flex-1 cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-white py-1 pl-2.5 pr-2.5 text-xs font-semibold text-slate-900 transition-[background-color,transform] duration-150 hover:bg-slate-100 active:scale-[0.96] sm:h-12 sm:gap-2 sm:pl-3.5 sm:pr-3 sm:text-sm"
|
||
>
|
||
<Trophy
|
||
className="size-3.5 shrink-0 text-orange-500 sm:size-4"
|
||
aria-hidden
|
||
/>
|
||
<span className="truncate">Sıralama gir</span>
|
||
</button>
|
||
)}
|
||
<button
|
||
type="button"
|
||
onClick={() => modalDegisti(true)}
|
||
aria-label="Bölüm, üniversite veya rehber ara"
|
||
className="flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-700 transition-[background-color,transform] duration-150 hover:bg-slate-100 active:scale-[0.96] sm:size-12"
|
||
>
|
||
<Search className="size-4" aria-hidden />
|
||
</button>
|
||
</div>
|
||
|
||
<Dialog open={acik} onOpenChange={modalDegisti}>
|
||
<DialogContent
|
||
showCloseButton={false}
|
||
onOpenAutoFocus={(event) => {
|
||
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"
|
||
>
|
||
<DialogTitle className="sr-only">
|
||
Bölüm, üniversite ve rehber ara
|
||
</DialogTitle>
|
||
<DialogDescription className="sr-only">
|
||
YKS bölümlerini, üniversiteleri ve rehber yazılarını tek yerden
|
||
ara.
|
||
</DialogDescription>
|
||
|
||
<div className="flex items-center border-b border-slate-200 px-4 sm:px-5">
|
||
<Search
|
||
className="mr-3 size-5 shrink-0 text-slate-400"
|
||
aria-hidden
|
||
/>
|
||
<input
|
||
ref={inputRef}
|
||
type="text"
|
||
value={sorgu}
|
||
onChange={(event) => {
|
||
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"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={kapat}
|
||
aria-label="Aramayı kapat"
|
||
className="flex size-9 cursor-pointer items-center justify-center rounded-full border border-slate-200 text-slate-500 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96]"
|
||
>
|
||
<X className="size-4" aria-hidden />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="relative min-h-0">
|
||
<div
|
||
ref={kaydirmaRef}
|
||
onScroll={kenarBlurGuncelle}
|
||
className="h-full max-h-full min-h-72 overflow-y-auto overscroll-contain p-3 sm:p-4"
|
||
aria-live="polite"
|
||
>
|
||
{!aramaAktif ? (
|
||
<BaslangicIcerigi onSec={kapat} />
|
||
) : yukleniyor ? (
|
||
<AramaIskeleti />
|
||
) : sonucVar ? (
|
||
<div className="space-y-5">
|
||
{sonuclar.bolumler.length > 0 ? (
|
||
<SonucGrubu
|
||
baslik="Bölümler"
|
||
ikon={<GraduationCap className="size-4" aria-hidden />}
|
||
>
|
||
{sonuclar.bolumler.map((bolum) => (
|
||
<SonucLinki
|
||
key={bolum.href}
|
||
href={bolum.href}
|
||
baslik={bolum.ad}
|
||
meta={`${bolum.seviye} · ${sayi(bolum.programSayisi)} program`}
|
||
detay={
|
||
bolum.enIyiSira
|
||
? `En iyi sıra ${sayi(bolum.enIyiSira)}`
|
||
: "Sıralama verisi yok"
|
||
}
|
||
onSec={kapat}
|
||
/>
|
||
))}
|
||
</SonucGrubu>
|
||
) : null}
|
||
|
||
{sonuclar.universiteler.length > 0 ? (
|
||
<SonucGrubu
|
||
baslik="Üniversiteler"
|
||
ikon={<Building2 className="size-4" aria-hidden />}
|
||
>
|
||
{sonuclar.universiteler.map((universite) => (
|
||
<SonucLinki
|
||
key={universite.href}
|
||
href={universite.href}
|
||
gorsel={
|
||
<UniLogo
|
||
slug={universite.href.split("/").pop() ?? ""}
|
||
ad={universite.ad}
|
||
boy="sm"
|
||
/>
|
||
}
|
||
baslik={universite.ad}
|
||
meta={
|
||
[universite.il, universite.tur]
|
||
.filter(Boolean)
|
||
.join(" · ") || "Üniversite"
|
||
}
|
||
detay={`${sayi(universite.programSayisi)} program · ${sayi(universite.fakulteSayisi)} fakülte`}
|
||
onSec={kapat}
|
||
/>
|
||
))}
|
||
</SonucGrubu>
|
||
) : null}
|
||
|
||
{rehberSonuclari.length > 0 ? (
|
||
<SonucGrubu
|
||
baslik="Rehber yazıları"
|
||
ikon={<BookOpenText className="size-4" aria-hidden />}
|
||
>
|
||
<div className="grid gap-2 sm:grid-cols-2">
|
||
{rehberSonuclari.map((yazi) => (
|
||
<RehberSonucKarti
|
||
key={yazi.slug}
|
||
yazi={yazi}
|
||
onSec={kapat}
|
||
/>
|
||
))}
|
||
</div>
|
||
</SonucGrubu>
|
||
) : null}
|
||
</div>
|
||
) : (
|
||
<div className="flex min-h-64 flex-col items-center justify-center px-6 text-center">
|
||
<div className="flex size-12 items-center justify-center rounded-2xl bg-slate-100 text-slate-500">
|
||
<Search className="size-5" aria-hidden />
|
||
</div>
|
||
<p className="mt-4 font-medium text-slate-900">
|
||
"{temizSorgu}" için sonuç bulunamadı
|
||
</p>
|
||
<p className="mt-1 max-w-sm text-sm leading-6 text-slate-500">
|
||
Bölüm, üniversite veya rehber konusunu farklı yazarak tekrar
|
||
dene.
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div
|
||
aria-hidden
|
||
className={`pointer-events-none absolute inset-x-0 top-0 h-14 transition-opacity duration-200 ${kenarBlur.ust ? "opacity-100" : "opacity-0"}`}
|
||
>
|
||
<ProgressiveBlur
|
||
position="top"
|
||
backgroundColor="#ffffff"
|
||
height="56px"
|
||
blurAmount="4px"
|
||
/>
|
||
</div>
|
||
<div
|
||
aria-hidden
|
||
className={`pointer-events-none absolute inset-x-0 bottom-0 h-14 transition-opacity duration-200 ${kenarBlur.alt ? "opacity-100" : "opacity-0"}`}
|
||
>
|
||
<ProgressiveBlur
|
||
position="bottom"
|
||
backgroundColor="#ffffff"
|
||
height="56px"
|
||
blurAmount="4px"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between border-t border-slate-200 bg-slate-50/80 px-4 py-3 text-xs text-slate-500 sm:px-5">
|
||
<span>En az 2 harf yazarak ara</span>
|
||
<span className="hidden items-center gap-1.5 sm:flex">
|
||
<Kbd>Esc</Kbd> ile kapat
|
||
</span>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function BaslangicIcerigi({ onSec }: { onSec: () => void }) {
|
||
return (
|
||
<div>
|
||
<p className="px-1 pb-2.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-400">
|
||
Keşfet
|
||
</p>
|
||
<div className="grid grid-cols-3 gap-2">
|
||
<KesfetLinki
|
||
href="/bolumler"
|
||
ikon={GraduationCap}
|
||
baslik="Tüm bölümler"
|
||
kisaBaslik="Bölümler"
|
||
aciklama="Lisans · önlisans"
|
||
ton="slate"
|
||
onSec={onSec}
|
||
/>
|
||
<KesfetLinki
|
||
href="/universiteler"
|
||
ikon={Building2}
|
||
baslik="Üniversiteler"
|
||
kisaBaslik="Üniversite"
|
||
aciklama="Devlet · vakıf"
|
||
ton="sky"
|
||
onSec={onSec}
|
||
/>
|
||
<KesfetLinki
|
||
href="/rehber"
|
||
ikon={BookOpenText}
|
||
baslik="Tercih rehberi"
|
||
kisaBaslik="Rehber"
|
||
aciklama="Sorularına yanıt"
|
||
ton="amber"
|
||
onSec={onSec}
|
||
/>
|
||
</div>
|
||
<div className="mt-5 flex gap-3 rounded-2xl bg-slate-50 px-3.5 py-3.5 sm:px-4">
|
||
<span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full bg-white text-slate-400 shadow-[0_0_0_1px_rgba(15,23,42,0.06)]">
|
||
<Search className="size-3.5" aria-hidden />
|
||
</span>
|
||
<div className="min-w-0">
|
||
<p className="text-sm font-semibold tracking-tight text-slate-900">
|
||
Neyi merak ediyorsun?
|
||
</p>
|
||
<p className="mt-0.5 text-xs leading-5 text-slate-500 sm:text-sm sm:leading-6">
|
||
“Psikoloji” veya “Boğaziçi” yaz; eşleşen sayfaları ve temel
|
||
bilgilerini burada önizle.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<AramaFunnelKarti onSec={onSec} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<Link
|
||
href={href}
|
||
onClick={onSec}
|
||
className="group/kesfet flex cursor-pointer flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white transition-transform duration-150 ease-out active:scale-[0.97]"
|
||
>
|
||
{/* Mini tarayıcı — sekme içinde tek büyük ikon, alta doğru maske ile solar */}
|
||
<div
|
||
aria-hidden
|
||
className="relative h-18 overflow-hidden bg-slate-50/80 sm:h-22"
|
||
>
|
||
<div
|
||
className={`absolute inset-x-2 -bottom-3 top-2 flex flex-col overflow-hidden rounded-t-xl border border-b-0 sm:inset-x-2.5 sm:top-2.5 ${palet.pencere}`}
|
||
>
|
||
<div className="flex h-3.5 shrink-0 items-center gap-1 border-b border-slate-100/90 bg-slate-50/90 px-2">
|
||
<span className={`size-1.5 rounded-full ${palet.nokta}`} />
|
||
<span className={`size-1.5 rounded-full ${palet.nokta} opacity-65`} />
|
||
<span className={`size-1.5 rounded-full ${palet.nokta} opacity-35`} />
|
||
<span className="ml-1.5 h-1.5 max-w-[55%] flex-1 rounded-full bg-white shadow-[0_0_0_1px_rgba(15,23,42,0.06)]" />
|
||
</div>
|
||
<div className="relative min-h-0 flex-1 overflow-hidden">
|
||
<div className="absolute inset-x-0 top-0 flex flex-col items-center gap-1.5 pt-2 sm:gap-2 sm:pt-2.5">
|
||
<span
|
||
className={`flex size-9 items-center justify-center rounded-xl border shadow-[0_6px_12px_-6px_rgba(15,23,42,0.18)] transition-transform duration-200 ease-out group-hover/kesfet:-translate-y-0.5 sm:size-10 ${palet.rozet}`}
|
||
>
|
||
<Ikon className="size-4.5 sm:size-5" strokeWidth={1.75} />
|
||
</span>
|
||
<span className={`h-1.5 w-2/5 rounded-full ${palet.cizgi}`} />
|
||
<span className="h-1.5 w-1/4 rounded-full bg-slate-100" />
|
||
</div>
|
||
<ProgressiveBlur
|
||
position="bottom"
|
||
backgroundColor="#ffffff"
|
||
height="45%"
|
||
blurAmount="2px"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-linear-to-t from-white to-transparent" />
|
||
</div>
|
||
|
||
<span className="min-w-0 px-2.5 pb-3 pt-2 text-center sm:px-3.5 sm:pb-3.5 sm:pt-2.5 sm:text-left">
|
||
<span className="block text-[12px] font-semibold leading-tight tracking-tight text-slate-900 sm:text-sm">
|
||
<span className="sm:hidden">{kisaBaslik}</span>
|
||
<span className="hidden sm:inline">{baslik}</span>
|
||
</span>
|
||
<span className="mt-0.5 block text-[10px] leading-snug text-slate-500 sm:text-xs">
|
||
{aciklama}
|
||
</span>
|
||
</span>
|
||
</Link>
|
||
);
|
||
}
|
||
|
||
function SonucGrubu({
|
||
baslik,
|
||
ikon,
|
||
children,
|
||
}: {
|
||
baslik: string;
|
||
ikon: React.ReactNode;
|
||
children: React.ReactNode;
|
||
}) {
|
||
return (
|
||
<section>
|
||
<h3 className="mb-2 flex items-center gap-2 px-2 text-xs font-semibold uppercase tracking-wider text-slate-400">
|
||
{ikon}
|
||
{baslik}
|
||
</h3>
|
||
<div className="space-y-1">{children}</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function SonucLinki({
|
||
href,
|
||
baslik,
|
||
meta,
|
||
detay,
|
||
gorsel,
|
||
onSec,
|
||
}: {
|
||
href: string;
|
||
baslik: string;
|
||
meta: string;
|
||
detay: string;
|
||
gorsel?: React.ReactNode;
|
||
onSec: () => void;
|
||
}) {
|
||
return (
|
||
<Link
|
||
href={href}
|
||
onClick={onSec}
|
||
className="group flex items-center gap-3 rounded-xl px-3 py-3 transition-colors duration-150 hover:bg-slate-100"
|
||
>
|
||
{gorsel}
|
||
<span className="min-w-0 flex-1">
|
||
<span className="block truncate text-sm font-semibold text-slate-900">
|
||
{baslik}
|
||
</span>
|
||
<span className="mt-1 block truncate text-xs text-slate-500">
|
||
{meta}
|
||
</span>
|
||
</span>
|
||
<span className="hidden shrink-0 text-right text-xs text-slate-400 sm:block">
|
||
{detay}
|
||
</span>
|
||
<ArrowRight
|
||
className="size-4 shrink-0 text-slate-300 transition-[color,transform] duration-150 group-hover:translate-x-0.5 group-hover:text-slate-600"
|
||
aria-hidden
|
||
/>
|
||
</Link>
|
||
);
|
||
}
|
||
|
||
function RehberSonucKarti({
|
||
yazi,
|
||
onSec,
|
||
}: {
|
||
yazi: RehberOzet;
|
||
onSec: () => void;
|
||
}) {
|
||
return (
|
||
<Link
|
||
href={`/rehber/${yazi.slug}`}
|
||
onClick={onSec}
|
||
className="group overflow-hidden rounded-xl border border-slate-200 bg-white p-1.5 transition-[border-color,background-color,transform] duration-150 hover:border-slate-300 hover:bg-slate-50 active:scale-[0.99]"
|
||
>
|
||
<RehberKapak baslik={yazi.baslik} compact className="rounded-lg" />
|
||
<span className="block px-2 pb-1.5 pt-2.5">
|
||
<span className="line-clamp-2 text-xs leading-5 text-slate-600">
|
||
{yazi.aciklama}
|
||
</span>
|
||
<span className="mt-2 inline-flex items-center gap-1 text-xs font-semibold text-primary">
|
||
Yazıyı oku
|
||
<ArrowRight
|
||
className="size-3.5 transition-transform duration-150 group-hover:translate-x-0.5"
|
||
aria-hidden
|
||
/>
|
||
</span>
|
||
</span>
|
||
</Link>
|
||
);
|
||
}
|
||
|
||
function AramaIskeleti() {
|
||
return (
|
||
<div className="space-y-2 px-2 pt-2" aria-label="Aranıyor">
|
||
{[0, 1, 2, 3].map((sira) => (
|
||
<div
|
||
key={sira}
|
||
className="flex h-16 animate-pulse items-center rounded-xl bg-slate-100 px-3 motion-reduce:animate-none"
|
||
>
|
||
<div className="h-4 w-2/5 rounded bg-slate-200" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|