Enhance university-related components by adding logo support and improving search functionality. Introduced a new "logolar" script in package.json, updated KatalogArama to include rehber results, and integrated UniLogo component across various pages for better visual representation. Refactored UniversiteIcerik and TercihHaritasi to display logos conditionally, enhancing user experience. Additionally, made adjustments to styles and layout for improved responsiveness and clarity.
All checks were successful
Deploy / deploy (push) Successful in 16m24s

This commit is contained in:
bilalgursen
2026-08-01 20:17:48 +03:00
parent ff66480227
commit f27f3ffaac
254 changed files with 1035 additions and 154 deletions

View File

@@ -1,7 +1,11 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Link from "next/link";
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 {
ArrowRight,
BookOpenText,
@@ -16,6 +20,8 @@ import {
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;
@@ -40,18 +46,72 @@ type AramaSonuclari = {
};
const BOS_SONUCLAR: AramaSonuclari = { bolumler: [], universiteler: [] };
const REHBER_SONUC_SINIRI = 4;
const sayi = (deger: number) => deger.toLocaleString("tr-TR");
export function KatalogArama() {
export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
const [acik, setAcik] = useState(false);
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 });
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;
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;
@@ -81,6 +141,19 @@ export function KatalogArama() {
};
}, [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]);
function modalDegisti(yeniAcik: boolean) {
setAcik(yeniAcik);
if (!yeniAcik) {
@@ -95,7 +168,8 @@ export function KatalogArama() {
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">
{/* 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">
<Link
href="/#nasil-calisir"
className="rounded-full px-5 py-2 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900"
@@ -115,14 +189,20 @@ export function KatalogArama() {
>
<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>
<button
type="button"
onClick={() => setAcik(true)}
aria-label="Bölüm veya üniversite ara"
className="ml-auto 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:hidden"
aria-label="Bölüm, üniversite veya rehber ara"
className="ml-auto 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 lg:hidden"
>
<Search className="size-4" aria-hidden />
</button>
@@ -134,13 +214,14 @@ export function KatalogArama() {
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"
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 ve üniversite ara
Bölüm, üniversite ve rehber ara
</DialogTitle>
<DialogDescription className="sr-only">
YKS bölümlerini ve üniversiteleri tek yerden ara.
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">
@@ -158,8 +239,8 @@ export function KatalogArama() {
setSonuclar(BOS_SONUCLAR);
setYukleniyor(yeniSorgu.trim().length >= 2);
}}
placeholder="Bölüm veya üniversite ara..."
aria-label="Bölüm veya üniversite ara"
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"
/>
@@ -173,10 +254,13 @@ export function KatalogArama() {
</button>
</div>
<div
className="min-h-72 overflow-y-auto overscroll-contain p-3 sm:p-4"
aria-live="polite"
>
<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 ? (
@@ -214,6 +298,13 @@ export function KatalogArama() {
<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]
@@ -226,6 +317,23 @@ export function KatalogArama() {
))}
</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">
@@ -236,15 +344,42 @@ export function KatalogArama() {
&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 veya üniversite adını farklı yazarak tekrar dene.
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 sm:inline">Esc ile kapat</span>
<span className="hidden items-center gap-1.5 sm:flex">
<Kbd>Esc</Kbd> ile kapat
</span>
</div>
</DialogContent>
</Dialog>
@@ -349,12 +484,14 @@ function SonucLinki({
baslik,
meta,
detay,
gorsel,
onSec,
}: {
href: string;
baslik: string;
meta: string;
detay: string;
gorsel?: React.ReactNode;
onSec: () => void;
}) {
return (
@@ -363,6 +500,7 @@ function SonucLinki({
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}
@@ -382,6 +520,36 @@ function SonucLinki({
);
}
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">