Files
kolaytercih/src/components/katalog-arama.tsx
bilalgursen e62c6f07df
Some checks failed
Deploy / deploy (push) Failing after 1h28m6s
Update package.json scripts, enhance layout with new components, and improve API error handling
- 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.
2026-08-06 02:27:42 +03:00

765 lines
28 KiB
TypeScript
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.
"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">
&quot;{temizSorgu}&quot; 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>
);
}