Update routing in next.config.ts to redirect "/tercih-robotu" to the hero form, add simple-icons dependency in package.json, and enhance database indexing for improved query performance. Remove unused SVG files and update metadata across various pages for better SEO and clarity.
All checks were successful
Deploy / deploy (push) Successful in 14m14s
All checks were successful
Deploy / deploy (push) Successful in 14m14s
This commit is contained in:
398
src/components/katalog-arama.tsx
Normal file
398
src/components/katalog-arama.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowRight,
|
||||
BookOpenText,
|
||||
Building2,
|
||||
GraduationCap,
|
||||
Search,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
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 sayi = (deger: number) => deger.toLocaleString("tr-TR");
|
||||
|
||||
export function KatalogArama() {
|
||||
const [acik, setAcik] = useState(false);
|
||||
const [sorgu, setSorgu] = useState("");
|
||||
const [sonuclar, setSonuclar] = useState<AramaSonuclari>(BOS_SONUCLAR);
|
||||
const [yukleniyor, setYukleniyor] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const temizSorgu = sorgu.trim();
|
||||
const aramaAktif = temizSorgu.length >= 2;
|
||||
const sonucVar =
|
||||
sonuclar.bolumler.length > 0 || sonuclar.universiteler.length > 0;
|
||||
|
||||
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");
|
||||
setSonuclar((await response.json()) as AramaSonuclari);
|
||||
} 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]);
|
||||
|
||||
function modalDegisti(yeniAcik: boolean) {
|
||||
setAcik(yeniAcik);
|
||||
if (!yeniAcik) {
|
||||
setSorgu("");
|
||||
setSonuclar(BOS_SONUCLAR);
|
||||
}
|
||||
}
|
||||
|
||||
function kapat() {
|
||||
setAcik(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<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 sm:flex">
|
||||
<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>
|
||||
<Link
|
||||
href="/#karsilastirma"
|
||||
className="rounded-full px-5 py-2 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900"
|
||||
>
|
||||
Karşılaştır
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAcik(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
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAcik(true)}
|
||||
aria-label="Bölüm veya üniversite ara"
|
||||
className="ml-auto flex size-10 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:hidden"
|
||||
>
|
||||
<Search className="size-4" aria-hidden />
|
||||
</button>
|
||||
|
||||
<Dialog open={acik} onOpenChange={modalDegisti}>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
onOpenAutoFocus={(event) => {
|
||||
event.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
className="max-h-[min(720px,calc(100dvh-2rem))] 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 ve üniversite ara
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
YKS bölümlerini ve üniversiteleri 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 veya üniversite ara..."
|
||||
aria-label="Bölüm veya üniversite 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="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}
|
||||
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}
|
||||
</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 veya üniversite adını farklı yazarak tekrar dene.
|
||||
</p>
|
||||
</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 sm:inline">Esc ile kapat</span>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function BaslangicIcerigi({ onSec }: { onSec: () => void }) {
|
||||
return (
|
||||
<div>
|
||||
<p className="px-2 pb-2 text-xs font-semibold uppercase tracking-wider text-slate-400">
|
||||
Keşfet
|
||||
</p>
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
<KesfetLinki
|
||||
href="/bolumler"
|
||||
ikon={<GraduationCap className="size-5" aria-hidden />}
|
||||
baslik="Tüm bölümler"
|
||||
aciklama="Lisans ve önlisans"
|
||||
onSec={onSec}
|
||||
/>
|
||||
<KesfetLinki
|
||||
href="/universiteler"
|
||||
ikon={<Building2 className="size-5" aria-hidden />}
|
||||
baslik="Üniversiteler"
|
||||
aciklama="Devlet ve vakıf"
|
||||
onSec={onSec}
|
||||
/>
|
||||
<KesfetLinki
|
||||
href="/rehber"
|
||||
ikon={<BookOpenText className="size-5" aria-hidden />}
|
||||
baslik="Tercih rehberi"
|
||||
aciklama="Sorularına yanıtlar"
|
||||
onSec={onSec}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 rounded-xl border border-orange-100 bg-orange-50/70 p-4">
|
||||
<p className="text-sm font-medium text-slate-900">
|
||||
Neyi merak ediyorsun?
|
||||
</p>
|
||||
<p className="mt-1 text-sm leading-6 text-slate-600">
|
||||
“Psikoloji” veya “Boğaziçi” gibi bir ad yaz; eşleşen sayfaları ve
|
||||
temel bilgilerini burada önizle.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KesfetLinki({
|
||||
href,
|
||||
ikon,
|
||||
baslik,
|
||||
aciklama,
|
||||
onSec,
|
||||
}: {
|
||||
href: string;
|
||||
ikon: React.ReactNode;
|
||||
baslik: string;
|
||||
aciklama: string;
|
||||
onSec: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
onClick={onSec}
|
||||
className="group rounded-xl border border-slate-200 bg-white p-4 transition-[border-color,background-color,transform] duration-150 hover:border-slate-300 hover:bg-slate-50 active:scale-[0.98]"
|
||||
>
|
||||
<span className="flex size-9 items-center justify-center rounded-lg bg-slate-100 text-slate-600 transition-colors group-hover:bg-white">
|
||||
{ikon}
|
||||
</span>
|
||||
<span className="mt-3 block text-sm font-semibold text-slate-900">
|
||||
{baslik}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-xs text-slate-500">{aciklama}</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,
|
||||
onSec,
|
||||
}: {
|
||||
href: string;
|
||||
baslik: string;
|
||||
meta: string;
|
||||
detay: string;
|
||||
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"
|
||||
>
|
||||
<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 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user