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,33 +1,10 @@
import { NextResponse, type NextRequest } from "next/server";
import { aramaNormalize as normalize, eslesmePuani } from "@/lib/arama";
import { getAllBolumler, getAllUniversiteler } from "@/lib/katalog";
import { trBaslikDuzeni } from "@/lib/slug";
const SONUC_SINIRI = 6;
function normalize(metin: string): string {
return metin
.toLocaleLowerCase("tr-TR")
.replace(/ı/g, "i")
.replace(/ş/g, "s")
.replace(/ğ/g, "g")
.replace(/ü/g, "u")
.replace(/ö/g, "o")
.replace(/ç/g, "c")
.replace(/â/g, "a")
.replace(/î/g, "i")
.replace(/û/g, "u")
.normalize("NFKD")
.replace(/[̀-ͯ]/g, "");
}
function eslesmePuani(ad: string, sorgu: string): number {
const normalAd = normalize(ad);
if (normalAd === sorgu) return 0;
if (normalAd.startsWith(sorgu)) return 1;
if (normalAd.split(/\s+/).some((kelime) => kelime.startsWith(sorgu))) return 2;
return 3;
}
export function GET(request: NextRequest) {
const hamSorgu = request.nextUrl.searchParams.get("q")?.trim() ?? "";
const sorgu = normalize(hamSorgu).slice(0, 80);

View File

@@ -1,5 +1,8 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { getAllUniversiteler } from "@/lib/katalog";
import { trBaslikDuzeni } from "@/lib/slug";
import { uniLogoYolu } from "@/lib/uni-logolar";
import {
createOgSayfaImage,
OG_CONTENT_TYPE,
@@ -25,9 +28,24 @@ export default async function Image({
}
parcalar.push("2025 yerleştirme verileri");
// Logo dosyası varsa OG kartına data URI olarak gömülür; okuma hatası
// kartı düşürmesin diye sessizce logosuz karta düşülür.
let logo: string | undefined;
if (uniLogoYolu(slug)) {
try {
const buf = await readFile(
path.join(process.cwd(), "public", "uni-logo", `${slug}.png`),
);
logo = `data:image/png;base64,${buf.toString("base64")}`;
} catch {
logo = undefined;
}
}
return createOgSayfaImage({
eyebrow: "Üniversite",
title: `${ad} taban puanları`,
description: parcalar.join(" · "),
logo,
});
}

View File

@@ -11,6 +11,8 @@ import { Sayfalama, SAYFA_BOYU } from "@/components/sayfalama";
import { PagePixelDivider } from "@/components/pixel-decor";
import { UniversiteKonumHaritasi } from "@/components/universite-konum-haritasi";
import { UniversiteProgramTablosu } from "@/components/universite-program-tablosu";
import { UniLogo } from "@/components/uni-logo";
import { uniLogoYolu } from "@/lib/uni-logolar";
import type { Program } from "@/lib/db";
const sayi = (n: number) => n.toLocaleString("tr-TR");
@@ -89,6 +91,9 @@ export function UniversiteIcerik({
"@type": "CollegeOrUniversity",
name: uni.ad,
url: `${SITE_URL}/universite/${slug}`,
...(uniLogoYolu(slug)
? { logo: `${SITE_URL}${uniLogoYolu(slug)}` }
: {}),
...(uni.il
? {
address: {
@@ -133,10 +138,13 @@ export function UniversiteIcerik({
)}
</nav>
<h1 className="mt-3 font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
{uni.ad} Taban Puanları 2026
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
</h1>
<div className="mt-4 flex items-center gap-4">
<UniLogo slug={slug} ad={uni.ad} boy="lg" />
<h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
{uni.ad} Taban Puanları 2026
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
</h1>
</div>
{ilkSayfa ? (
<>

View File

@@ -6,6 +6,7 @@ import { JsonLd, breadcrumbJsonLd } from "@/lib/seo";
import { CtaSiraForm } from "@/components/cta-sira-form";
import { SiteFooter } from "@/components/site-footer";
import { PagePixelDivider } from "@/components/pixel-decor";
import { UniLogo } from "@/components/uni-logo";
export const metadata: Metadata = {
title: "Üniversitelerin Taban Puanları ve Sıralamaları 2026",
@@ -18,17 +19,22 @@ const sayi = (n: number) => n.toLocaleString("tr-TR");
function UniLinkleri({ uniler }: { uniler: UniOzet[] }) {
return (
<ul className="mt-3 grid gap-x-6 gap-y-1.5 sm:grid-cols-2">
<ul className="mt-4 grid gap-x-6 gap-y-2 sm:grid-cols-2">
{uniler.map((u) => (
<li key={u.slug}>
<Link
href={`/universite/${u.slug}`}
className="group inline-flex items-baseline gap-2 text-sm text-slate-700 hover:text-primary"
className="group flex items-center gap-3 text-sm text-slate-700 hover:text-primary"
>
<span className="group-hover:underline">{u.ad}</span>
<span className="text-xs text-slate-400">
{u.il ? `${trBaslikDuzeni(u.il)} · ` : ""}
{sayi(u.programSayisi)} program
<UniLogo slug={u.slug} ad={u.ad} boy="sm" />
<span className="min-w-0">
<span className="block truncate group-hover:underline">
{u.ad}
</span>
<span className="block text-xs text-slate-400">
{u.il ? `${trBaslikDuzeni(u.il)} · ` : ""}
{sayi(u.programSayisi)} program
</span>
</span>
</Link>
</li>

View File

@@ -10,6 +10,7 @@ import {
ProgramTabanTrendi,
} from "@/components/program-liste-verileri";
import { ProgramEkleButonu } from "@/components/manuel-liste/program-ekle-butonu";
import { UniLogo } from "@/components/uni-logo";
import {
Table,
TableBody,
@@ -162,39 +163,46 @@ export function BolumProgramListesi({
return (
<TableRow key={p.id}>
<TableCell className="max-w-0 w-full">
{p.universiteSlug ? (
<Link
href={`/universite/${p.universiteSlug}`}
className="block truncate text-sm font-semibold hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{uniAd}
</Link>
) : (
<span className="block truncate text-sm font-semibold">
{uniAd}
</span>
)}
<span className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-slate-500">
{p.unitur ? (
<span
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
devlet
? "bg-slate-100 text-slate-600"
: "bg-violet-50 text-violet-600"
}`}
>
{devlet ? "Devlet" : trBaslikDuzeni(p.unitur)}
</span>
<span className="flex min-w-0 items-center gap-2.5">
{p.universiteSlug ? (
<UniLogo slug={p.universiteSlug} ad={uniAd} boy="sm" />
) : null}
<span className="truncate">
{p.il ? trBaslikDuzeni(p.il) : null}
{varyant ? `${p.il ? " · " : ""}${varyant}` : ""}
</span>
{p.sure ? (
<span className="shrink-0 text-slate-400">
· {p.sure} yıl
<span className="min-w-0 flex-1">
{p.universiteSlug ? (
<Link
href={`/universite/${p.universiteSlug}`}
className="block truncate text-sm font-semibold hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{uniAd}
</Link>
) : (
<span className="block truncate text-sm font-semibold">
{uniAd}
</span>
)}
<span className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-slate-500">
{p.unitur ? (
<span
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
devlet
? "bg-slate-100 text-slate-600"
: "bg-violet-50 text-violet-600"
}`}
>
{devlet ? "Devlet" : trBaslikDuzeni(p.unitur)}
</span>
) : null}
<span className="truncate">
{p.il ? trBaslikDuzeni(p.il) : null}
{varyant ? `${p.il ? " · " : ""}${varyant}` : ""}
</span>
{p.sure ? (
<span className="shrink-0 text-slate-400">
· {p.sure} yıl
</span>
) : null}
</span>
) : null}
</span>
</span>
</TableCell>
<TableCell className="hidden text-right text-xs md:table-cell">

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">

View File

@@ -18,6 +18,7 @@
import { useEffect, useState } from "react";
import Link from "next/link";
import { UniLogo } from "@/components/uni-logo";
import {
GripVertical,
ListChecks,
@@ -332,6 +333,7 @@ function CekmeceSatiri({
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-slate-100 font-heading text-[11px] font-bold tabular-nums">
{sira}
</span>
<UniLogo ad={tercih.universite} boy="xs" />
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-semibold">
{tercih.isim}

View File

@@ -32,7 +32,8 @@ export function ListemButonu() {
className="relative inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-full border border-slate-200 bg-white px-3 text-xs font-semibold text-slate-700 transition-[color,background-color,border-color,transform] duration-200 hover:border-slate-300 hover:bg-slate-50 active:scale-[0.97] sm:h-12 sm:gap-2 sm:px-5 sm:text-sm"
>
<ListChecks className="size-3.5 text-primary sm:size-4" aria-hidden />
<span className="hidden sm:inline">Seçtiklerim</span>
{/* lgxl: ortadaki nav ile yer dar — metin gizli, ikon+rozet kalır */}
<span className="hidden sm:inline lg:hidden xl:inline">Seçtiklerim</span>
{liste.length > 0 ? (
// key=length: her eklemede rozet yeniden doğar ve spring ile oturur
<motion.span

View File

@@ -44,14 +44,11 @@ export function ProgramEkleButonu({
e.preventDefault();
e.stopPropagation();
const sonuc = programEkleIste({ program, kaynak, adaySira, adayTur });
if (sonuc.eklendi) {
toast.success("Program listene eklendi. Sıralamana ve tercihlerine göre değerlendirdik.");
} else if (sonuc.doldu) {
if (sonuc.doldu) {
toast.info(`Listen ${MANUEL_LISTE_MAX} tercihe ulaştı.`);
} else if (sonuc.zatenVar) {
toast.info("Bu program zaten seçtiklerinde.");
}
// kapiAcildi: modal açılır, toast yok
}
return (

View File

@@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
import { UniLogo } from "@/components/uni-logo";
import {
AlertTriangle,
CheckCircle2,
@@ -198,19 +199,22 @@ function SecimSatiri({
</span>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<p className="truncate font-semibold text-slate-900">
{tercih.isim}
</p>
<p className="truncate text-xs text-slate-500">
{tercih.universite}
{tercih.il
? ` · ${tercih.il.toLocaleLowerCase("tr-TR")}`
: ""}
{tercih.efektifSira != null
? ` · ~${tercih.efektifSira.toLocaleString("tr-TR")}.`
: ""}
</p>
<div className="flex min-w-0 items-center gap-2.5">
<UniLogo ad={tercih.universite} boy="sm" />
<div className="min-w-0">
<p className="truncate font-semibold text-slate-900">
{tercih.isim}
</p>
<p className="truncate text-xs text-slate-500">
{tercih.universite}
{tercih.il
? ` · ${tercih.il.toLocaleLowerCase("tr-TR")}`
: ""}
{tercih.efektifSira != null
? ` · ~${tercih.efektifSira.toLocaleString("tr-TR")}.`
: ""}
</p>
</div>
</div>
<button
type="button"

View File

@@ -152,11 +152,7 @@ function KapıIcerik({
toast.success(
"Tercihlerin güncellendi. Listendeki programları yeni seçimlerine göre yeniden değerlendirdik; hiçbir programı silmedik.",
);
} else if (bekleyenVar) {
toast.success(
"Program listene eklendi. Sıralamana ve tercihlerine göre değerlendirdik.",
);
} else {
} else if (!bekleyenVar) {
toast.success("Tercihlerin kaydedildi.");
}
}

View File

@@ -16,6 +16,7 @@ import {
TrendingDown,
TrendingUp,
} from "lucide-react";
import { UniLogo } from "@/components/uni-logo";
import type { RaporSonuc } from "@/lib/ai/rapor";
import {
RISK_ETIKET,
@@ -231,7 +232,11 @@ export function RaporListesi({
<span className="flex size-7 shrink-0 items-center justify-center rounded-full bg-slate-100 font-heading text-xs font-bold">
{t.sira}
</span>
<span className="min-w-0">
<span className="flex min-w-0 items-center gap-2.5">
{p?.universite ? (
<UniLogo ad={p.universite} boy="sm" className="hidden sm:flex" />
) : null}
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-semibold">
{p?.isim ?? t.programId}
</span>
@@ -244,6 +249,7 @@ export function RaporListesi({
<span>2025: {sira2025?.toLocaleString("tr-TR") ?? "—"}</span>
{trendG ? <span className={trendG.renk}>{trendG.etiket}</span> : null}
</span>
</span>
</span>
<span className="hidden text-right text-xs tabular-nums text-slate-600 xl:block">
{sira2024?.toLocaleString("tr-TR") ?? "—"}

View File

@@ -3,6 +3,7 @@ import Image from "next/image";
import Link from "next/link";
import { KatalogArama } from "@/components/katalog-arama";
import { UserNav, UserNavFallback } from "@/components/user-nav";
import { getAllRehberler } from "@/lib/rehber";
export function SiteHeader() {
return (
@@ -29,7 +30,7 @@ export function SiteHeader() {
</span>
</span>
</Link>
<KatalogArama />
<KatalogArama rehberler={getAllRehberler()} />
{/* UserNav session+DB bekler; Suspense dışına taşarsa tüm kabuk bloklanır */}
<Suspense fallback={<UserNavFallback />}>
<UserNav />

View File

@@ -14,9 +14,11 @@ import {
useRef,
useState,
} from "react";
import Image from "next/image";
import { ArrowLeft, Lock } from "lucide-react";
import { cities } from "turkey-map-react/lib/data";
import { Button } from "@/components/ui/button";
import { uniLogoYoluFromAd } from "@/components/uni-logo";
import {
HARITA_GENISLIK,
HARITA_UST_KIRPMA,
@@ -296,7 +298,10 @@ export function TercihHaritasi({
{/* İl seviyesi: üniversite pinleri */}
{zoomda
? acikPinler.map((p) => {
const r = (p.kilitli ? 7 : 8) * sf;
// Kilitli pinlerde logo da gösterilmez — üniversite kimliği
// client'a inmediği gibi görselden de sızmamalı.
const logo = p.kilitli ? null : uniLogoYoluFromAd(p.universite);
const r = (p.kilitli ? 7 : logo ? 11 : 8) * sf;
const etiket = p.kilitli ? null : kisaUniAdi(p.universite);
return (
<g
@@ -314,27 +319,71 @@ export function TercihHaritasi({
: `${etiket}: ${p.tercihSiralari.length} tercih`
}
>
<circle
cx={p.x}
cy={p.y}
r={r}
fill={p.kilitli ? "oklch(0.704 0.04 256.8)" : RISK_RENK[p.risk]}
stroke="white"
strokeWidth={2 * sf}
strokeDasharray={p.kilitli ? `${3 * sf} ${2.5 * sf}` : undefined}
/>
<text
x={p.x}
y={p.y}
textAnchor="middle"
dominantBaseline="central"
fontSize={9 * sf}
fontWeight={700}
fill="white"
className="pointer-events-none select-none"
>
{p.kilitli ? "?" : p.tercihSiralari.length}
</text>
{logo ? (
<>
<circle
cx={p.x}
cy={p.y}
r={r}
fill="white"
stroke={RISK_RENK[p.risk]}
strokeWidth={2 * sf}
/>
<image
href={logo}
x={p.x - 8 * sf}
y={p.y - 8 * sf}
width={16 * sf}
height={16 * sf}
preserveAspectRatio="xMidYMid meet"
className="pointer-events-none"
/>
<circle
cx={p.x + r * 0.78}
cy={p.y - r * 0.78}
r={4.5 * sf}
fill={RISK_RENK[p.risk]}
stroke="white"
strokeWidth={1.2 * sf}
/>
<text
x={p.x + r * 0.78}
y={p.y - r * 0.78}
textAnchor="middle"
dominantBaseline="central"
fontSize={6 * sf}
fontWeight={700}
fill="white"
className="pointer-events-none select-none"
>
{p.tercihSiralari.length}
</text>
</>
) : (
<>
<circle
cx={p.x}
cy={p.y}
r={r}
fill={p.kilitli ? "oklch(0.704 0.04 256.8)" : RISK_RENK[p.risk]}
stroke="white"
strokeWidth={2 * sf}
strokeDasharray={p.kilitli ? `${3 * sf} ${2.5 * sf}` : undefined}
/>
<text
x={p.x}
y={p.y}
textAnchor="middle"
dominantBaseline="central"
fontSize={9 * sf}
fontWeight={700}
fill="white"
className="pointer-events-none select-none"
>
{p.kilitli ? "?" : p.tercihSiralari.length}
</text>
</>
)}
{etiket ? (
<text
x={p.x}
@@ -401,8 +450,20 @@ export function TercihHaritasi({
Kilitli tercih
</span>
) : (
<span className="max-w-40 truncate font-medium text-slate-700">
{kisaUniAdi(pin.universite)}
<span className="inline-flex min-w-0 items-center gap-1.5">
{uniLogoYoluFromAd(pin.universite) ? (
<Image
src={uniLogoYoluFromAd(pin.universite)!}
alt=""
width={16}
height={16}
aria-hidden
className="size-4 shrink-0 object-contain"
/>
) : null}
<span className="max-w-40 truncate font-medium text-slate-700">
{kisaUniAdi(pin.universite)}
</span>
</span>
)}
</button>

26
src/components/ui/kbd.tsx Normal file
View File

@@ -0,0 +1,26 @@
import { cn } from "@/lib/utils"
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
data-slot="kbd"
className={cn(
"pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*='size-'])]:size-3",
className
)}
{...props}
/>
)
}
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<kbd
data-slot="kbd-group"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
)
}
export { Kbd, KbdGroup }

116
src/components/uni-logo.tsx Normal file
View File

@@ -0,0 +1,116 @@
import Image from "next/image";
import { cn } from "@/lib/utils";
import { uniLogoYolu } from "@/lib/uni-logolar";
import { turkishSlugify } from "@/lib/slug";
/**
* Üniversite logosu; dosyası olmayanlar için ad baş harflerinden monogram.
* Logolar 300×300 PNG (public/uni-logo, kaynak YÖK Atlas) — beyaz zeminli
* kapsayıcı hem şeffaf hem beyaz arka planlı logolarda tutarlı görünür.
*/
const BOYUT: Record<"xs" | "sm" | "md" | "lg", string> = {
xs: "size-7 rounded-md p-0.5",
sm: "size-9 rounded-lg p-1",
md: "size-12 rounded-xl p-1.5",
lg: "size-16 rounded-2xl p-2 sm:size-20",
};
const MONOGRAM_METIN: Record<"xs" | "sm" | "md" | "lg", string> = {
xs: "text-[10px]",
sm: "text-xs",
md: "text-sm",
lg: "text-lg sm:text-xl",
};
// "İstanbul Teknik Üniversitesi" → "İT"; tür sözcükleri baş harfe girmez
const TUR_SOZCUKLERI = new Set([
"üniversitesi",
"üniversite",
"meslek",
"yüksekokulu",
"yüksek",
"okulu",
"ve",
]);
function monogram(ad: string): string {
const harfler = ad
.split(/\s+/)
.filter((w) => w && !TUR_SOZCUKLERI.has(w.toLocaleLowerCase("tr-TR")))
.slice(0, 2)
.map((w) => w[0].toLocaleUpperCase("tr-TR"));
return harfler.join("") || ad.slice(0, 1).toLocaleUpperCase("tr-TR");
}
/**
* Ham DB adından logo yolu: "(İL)" eki atılıp slug'lanır (katalog.ts ile
* aynı anahtar). Haritalar gibi next/image kullanamayan SVG bağlamları için.
*/
export function uniLogoYoluFromAd(ad: string): string | null {
return uniLogoYolu(turkishSlugify(ad.replace(/\s*\([^)]*\)\s*$/, "").trim()));
}
// Slug'dan deterministik pastel ton — aynı üniversite her yerde aynı renk
function tonu(slug: string): number {
let h = 0;
for (let i = 0; i < slug.length; i++) h = (h * 31 + slug.charCodeAt(i)) % 360;
return h;
}
export function UniLogo({
slug,
ad,
boy = "sm",
className,
}: {
/** Verilmezse ham addan türetilir ("(İL)" eki atılıp slug'lanır) */
slug?: string;
ad: string;
boy?: "xs" | "sm" | "md" | "lg";
className?: string;
}) {
const anahtar =
slug ?? turkishSlugify(ad.replace(/\s*\([^)]*\)\s*$/, "").trim());
const yol = uniLogoYolu(anahtar);
const px = boy === "lg" ? 80 : boy === "md" ? 48 : boy === "sm" ? 36 : 28;
if (!yol) {
const h = tonu(anahtar);
return (
<span
aria-hidden="true"
className={cn(
"flex shrink-0 select-none items-center justify-center font-heading font-bold ring-1 ring-inset ring-slate-900/10",
BOYUT[boy],
MONOGRAM_METIN[boy],
className,
)}
style={{
backgroundColor: `oklch(0.95 0.03 ${h})`,
color: `oklch(0.45 0.09 ${h})`,
}}
>
{monogram(ad)}
</span>
);
}
return (
<span
className={cn(
"flex shrink-0 items-center justify-center bg-white ring-1 ring-inset ring-slate-900/10",
BOYUT[boy],
className,
)}
>
<Image
src={yol}
alt={`${ad} logosu`}
width={px}
height={px}
className="size-full object-contain"
/>
</span>
);
}

View File

@@ -4,7 +4,6 @@ import {
HARITA_UST_KIRPMA,
HARITA_YUKSEKLIK,
normalizeIlAdi,
uniKonum,
} from "@/lib/harita";
export function UniversiteKonumHaritasi({
@@ -14,8 +13,7 @@ export function UniversiteKonumHaritasi({
universite: string;
il: string | null;
}) {
const konum = uniKonum(universite, il);
if (!konum) return null;
if (!il) return null;
return (
<section
@@ -51,23 +49,6 @@ export function UniversiteKonumHaritasi({
</path>
);
})}
<g aria-hidden>
<circle
cx={konum.x}
cy={konum.y}
r={18}
fill="oklch(0.623 0.188 259.8 / 18%)"
/>
<circle
cx={konum.x}
cy={konum.y}
r={9}
fill="oklch(0.623 0.188 259.8)"
stroke="white"
strokeWidth={3}
/>
<circle cx={konum.x} cy={konum.y} r={2.5} fill="white" />
</g>
</svg>
</div>
</section>

View File

@@ -45,7 +45,10 @@ export async function UserNav() {
>
<Coins className="size-3.5 text-amber-500 sm:size-4" aria-hidden />
<span className="tabular-nums">{u.creditBalance}</span>
<span className="hidden sm:inline">kredi</span>
{/* Etiket yalnızca mdlg arası: daha darda logo kesiliyor, lg üstünde
ortadaki nav ile yer dar (konteyner 1152'de sabit). İkon+sayı ve
title her genişlikte anlamı taşıyor. */}
<span className="hidden md:inline lg:hidden">kredi</span>
</Link>
<SignOutButton />
</div>

30
src/lib/arama.ts Normal file
View File

@@ -0,0 +1,30 @@
/**
* Katalog aramasının paylaşılan metin eşleştirme yardımcıları.
* Hem /api/katalog-ara route'u hem de client'taki rehber araması kullanır —
* iki taraf aynı normalize kurallarıyla eşleşmeli.
*/
export function aramaNormalize(metin: string): string {
return metin
.toLocaleLowerCase("tr-TR")
.replace(/ı/g, "i")
.replace(/ş/g, "s")
.replace(/ğ/g, "g")
.replace(/ü/g, "u")
.replace(/ö/g, "o")
.replace(/ç/g, "c")
.replace(/â/g, "a")
.replace(/î/g, "i")
.replace(/û/g, "u")
.normalize("NFKD")
.replace(/[̀-ͯ]/g, "");
}
/** 0 = tam eşleşme, 1 = baş, 2 = kelime başı, 3 = geçiyor */
export function eslesmePuani(ad: string, sorgu: string): number {
const normalAd = aramaNormalize(ad);
if (normalAd === sorgu) return 0;
if (normalAd.startsWith(sorgu)) return 1;
if (normalAd.split(/\s+/).some((kelime) => kelime.startsWith(sorgu))) return 2;
return 3;
}

View File

@@ -44,9 +44,11 @@ export async function createOgSayfaImage(opts: {
eyebrow: string;
title: string;
description?: string;
/** data: URI — sağ üstte beyaz rozet içinde gösterilir (üniversite logosu). */
logo?: string;
}): Promise<ImageResponse> {
const fonts = await loadRehberOgFonts();
const { eyebrow, title, description } = opts;
const { eyebrow, title, description, logo } = opts;
return new ImageResponse(
(
@@ -136,6 +138,25 @@ export async function createOgSayfaImage(opts: {
}}
>
<span>{eyebrow}</span>
{logo ? (
<span
style={{
display: "flex",
marginLeft: "auto",
alignItems: "center",
justifyContent: "center",
width: 108,
height: 108,
borderRadius: 24,
background: "white",
border: "2px solid rgba(37,99,235,0.15)",
boxShadow: "0 8px 24px rgba(15,23,42,0.08)",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={logo} alt="" width={84} height={84} />
</span>
) : null}
</div>
<div

238
src/lib/uni-logolar.ts Normal file
View File

@@ -0,0 +1,238 @@
// Bu dosya scripts/logo-indir.ts tarafından üretilir — elle düzenleme.
// Logo dosyaları: public/uni-logo/<slug>.png (kaynak: YÖK Atlas)
export const UNI_LOGO_SLUGLARI: ReadonlySet<string> = new Set([
"abdullah-gul-universitesi",
"acibadem-mehmet-ali-aydinlar-universitesi",
"ada-kent-universitesi",
"adana-alparslan-turkes-bilim-ve-teknoloji-universitesi",
"adiyaman-universitesi",
"afyon-kocatepe-universitesi",
"afyonkarahisar-saglik-bilimleri-universitesi",
"agri-ibrahim-cecen-universitesi",
"akdeniz-karpaz-universitesi",
"akdeniz-universitesi",
"aksaray-universitesi",
"alanya-alaaddin-keykubat-universitesi",
"alanya-universitesi",
"altinbas-universitesi",
"amasya-universitesi",
"anadolu-universitesi",
"ankara-bilim-universitesi",
"ankara-haci-bayram-veli-universitesi",
"ankara-medipol-universitesi",
"ankara-muzik-ve-guzel-sanatlar-universitesi",
"ankara-sosyal-bilimler-universitesi",
"ankara-universitesi",
"ankara-yildirim-beyazit-universitesi",
"antalya-belek-universitesi",
"antalya-bilim-universitesi",
"ardahan-universitesi",
"arkin-yaratici-sanatlar-ve-tasarim-universitesi",
"artvin-coruh-universitesi",
"atasehir-adiguzel-meslek-yuksekokulu",
"ataturk-universitesi",
"atilim-universitesi",
"avrasya-universitesi",
"aydin-adnan-menderes-universitesi",
"azerbaycan-devlet-pedagoji-universitesi",
"bahcesehir-kibris-universitesi",
"bahcesehir-universitesi",
"balikesir-universitesi",
"bandirma-onyedi-eylul-universitesi",
"bartin-universitesi",
"baskent-universitesi",
"batman-universitesi",
"bayburt-universitesi",
"beykoz-universitesi",
"bezm-i-alem-vakif-universitesi",
"bilecik-seyh-edebali-universitesi",
"bingol-universitesi",
"biruni-universitesi",
"bitlis-eren-universitesi",
"bogazici-universitesi",
"bolu-abant-izzet-baysal-universitesi",
"burdur-mehmet-akif-ersoy-universitesi",
"bursa-teknik-universitesi",
"bursa-uludag-universitesi",
"cag-universitesi",
"canakkale-onsekiz-mart-universitesi",
"cankaya-universitesi",
"cankiri-karatekin-universitesi",
"cukurova-universitesi",
"demiroglu-bilim-universitesi",
"dicle-universitesi",
"dogu-akdeniz-universitesi",
"dogus-universitesi",
"dokuz-eylul-universitesi",
"duzce-universitesi",
"ege-universitesi",
"erciyes-universitesi",
"erzincan-binali-yildirim-universitesi",
"erzurum-teknik-universitesi",
"eskisehir-osmangazi-universitesi",
"eskisehir-teknik-universitesi",
"fatih-sultan-mehmet-vakif-universitesi",
"fenerbahce-universitesi",
"firat-universitesi",
"galatasaray-universitesi",
"gazi-universitesi",
"gaziantep-islam-bilim-ve-teknoloji-universitesi",
"gaziantep-universitesi",
"gebze-teknik-universitesi",
"giresun-universitesi",
"girne-amerikan-universitesi",
"girne-universitesi",
"gumushane-universitesi",
"hacettepe-universitesi",
"hakkari-universitesi",
"halic-universitesi",
"harran-universitesi",
"hasan-kalyoncu-universitesi",
"hatay-mustafa-kemal-universitesi",
"hitit-universitesi",
"hoca-ahmet-yesevi-uluslararasi-turk-kazak-universitesi",
"ibn-haldun-universitesi",
"icisleri-bakanligi-ve-milli-savunma-bakanligi-adina-saglik-bilimleri-universitesinde-egitim-alacaklar",
"igdir-universitesi",
"ihsan-dogramaci-bilkent-universitesi",
"inonu-universitesi",
"isik-universitesi",
"iskenderun-teknik-universitesi",
"isparta-uygulamali-bilimler-universitesi",
"istanbul-29-mayis-universitesi",
"istanbul-arel-universitesi",
"istanbul-atlas-universitesi",
"istanbul-aydin-universitesi",
"istanbul-beykent-universitesi",
"istanbul-bilgi-universitesi",
"istanbul-esenyurt-universitesi",
"istanbul-galata-universitesi",
"istanbul-gedik-universitesi",
"istanbul-gelisim-universitesi",
"istanbul-kent-universitesi",
"istanbul-kultur-universitesi",
"istanbul-medeniyet-universitesi",
"istanbul-medipol-universitesi",
"istanbul-nisantasi-universitesi",
"istanbul-okan-universitesi",
"istanbul-rumeli-universitesi",
"istanbul-sabahattin-zaim-universitesi",
"istanbul-saglik-ve-sosyal-bilimler-meslek-yuksekokulu",
"istanbul-saglik-ve-teknoloji-universitesi",
"istanbul-sisli-meslek-yuksekokulu",
"istanbul-teknik-universitesi",
"istanbul-ticaret-universitesi",
"istanbul-topkapi-universitesi",
"istanbul-universitesi",
"istanbul-universitesi-cerrahpasa",
"istanbul-yeni-yuzyil-universitesi",
"istinye-universitesi",
"izmir-bakircay-universitesi",
"izmir-demokrasi-universitesi",
"izmir-ekonomi-universitesi",
"izmir-katip-celebi-universitesi",
"izmir-kavram-meslek-yuksekokulu",
"izmir-tinaztepe-universitesi",
"izmir-yuksek-teknoloji-enstitusu",
"kadir-has-universitesi",
"kafkas-universitesi",
"kahramanmaras-istiklal-universitesi",
"kahramanmaras-sutcu-imam-universitesi",
"kapadokya-universitesi",
"karabuk-universitesi",
"karadeniz-teknik-universitesi",
"karamanoglu-mehmetbey-universitesi",
"kastamonu-universitesi",
"kayseri-universitesi",
"kibris-amerikan-universitesi",
"kibris-aydin-universitesi",
"kibris-bati-universitesi",
"kibris-ilim-universitesi",
"kibris-saglik-ve-toplum-bilimleri-universitesi",
"kilis-7-aralik-universitesi",
"kirgizistan-turkiye-manas-universitesi",
"kirikkale-universitesi",
"kirklareli-universitesi",
"kirsehir-ahi-evran-universitesi",
"koc-universitesi",
"kocaeli-saglik-ve-teknoloji-universitesi",
"kocaeli-universitesi",
"konya-gida-ve-tarim-universitesi",
"konya-teknik-universitesi",
"kto-karatay-universitesi",
"kutahya-dumlupinar-universitesi",
"kutahya-saglik-bilimleri-universitesi",
"lefke-avrupa-universitesi",
"lokman-hekim-universitesi",
"malatya-turgut-ozal-universitesi",
"maltepe-universitesi",
"manisa-celal-bayar-universitesi",
"mardin-artuklu-universitesi",
"marmara-universitesi",
"mef-universitesi",
"mersin-universitesi",
"mimar-sinan-guzel-sanatlar-universitesi",
"mudanya-universitesi",
"mugla-sitki-kocman-universitesi",
"munzur-universitesi",
"mus-alparslan-universitesi",
"necmettin-erbakan-universitesi",
"nevsehir-haci-bektas-veli-universitesi",
"nigde-omer-halisdemir-universitesi",
"nuh-naci-yazgan-universitesi",
"ondokuz-mayis-universitesi",
"ordu-universitesi",
"orta-dogu-teknik-universitesi",
"osmaniye-korkut-ata-universitesi",
"ostim-teknik-universitesi",
"ozyegin-universitesi",
"pamukkale-universitesi",
"piri-reis-universitesi",
"rauf-denktas-universitesi",
"recep-tayyip-erdogan-universitesi",
"sabanci-universitesi",
"saglik-bilimleri-universitesi",
"sakarya-universitesi",
"sakarya-uygulamali-bilimler-universitesi",
"samsun-universitesi",
"sanko-universitesi",
"selcuk-universitesi",
"siirt-universitesi",
"sinop-universitesi",
"sirnak-universitesi",
"sivas-bilim-ve-teknoloji-universitesi",
"sivas-cumhuriyet-universitesi",
"suleyman-demirel-universitesi",
"tarsus-universitesi",
"ted-universitesi",
"tekirdag-namik-kemal-universitesi",
"tiran-new-york-universitesi",
"tobb-ekonomi-ve-teknoloji-universitesi",
"tokat-gaziosmanpasa-universitesi",
"toros-universitesi",
"trabzon-universitesi",
"trakya-universitesi",
"turk-alman-universitesi",
"turk-hava-kurumu-universitesi",
"ufuk-universitesi",
"uluslararasi-balkan-universitesi",
"uluslararasi-final-universitesi",
"uluslararasi-kibris-universitesi",
"uluslararasi-saraybosna-universitesi",
"usak-universitesi",
"uskudar-universitesi",
"van-yuzuncu-yil-universitesi",
"yakin-dogu-universitesi",
"yalova-universitesi",
"yasar-universitesi",
"yeditepe-universitesi",
"yildiz-teknik-universitesi",
"yozgat-bozok-universitesi",
"yuksek-ihtisas-universitesi",
"zonguldak-bulent-ecevit-universitesi",
]);
export function uniLogoYolu(slug: string): string | null {
return UNI_LOGO_SLUGLARI.has(slug) ? `/uni-logo/${slug}.png` : null;
}