feat(katalog+tasarim): şehir adları her yerde link; piksel yüzeylere beyaz ton

- ilSayfaHref: üniversite sayfası il hapı, /universiteler listesi, bölüm /
  sonuç / listem / tadımlık satırlarındaki il metası şehir sayfasına gider.
- Şehir kümeleri tüm (üniversite, il) çiftlerinden: DB'deki her il değerinin
  sayfası var (ODTÜ Ankara + KKTC); dev'de eski globalThis önbelleğine dayanıklı.
- PikselZemin ton="acik", CtaBandi/CtaSiraForm ton="beyaz": /universiteler
  altı ve /meraklisina kapanışı beyaz, diğer bantlar mavi (dönüşümlü).
- ilUlkeHaritasi: il değerinden ülke haritası + şehir konumu.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-24 14:56:11 +03:00
parent c27f24e6a6
commit 259534fa9c
13 changed files with 193 additions and 49 deletions

View File

@@ -11,6 +11,11 @@ import { cn } from "@/lib/utils";
* *
* - `buyuk` (varsayılan): sayfa kapanışı / panel — ortalı, iri boşluk. * - `buyuk` (varsayılan): sayfa kapanışı / panel — ortalı, iri boşluk.
* - `kompakt`: çekmece altı gibi dar yerler — sola yaslı, alçak. * - `kompakt`: çekmece altı gibi dar yerler — sola yaslı, alçak.
* - `ton="beyaz"`: aynı bant beyaz zeminde — slate kenarlık, koyu yazı,
* slate pikseller. Aynı sayfada üst üste iki mavi bant olmasın diye
* (Bilal, 24 Eyl 2026: "her kısımda mavi arka kullanmışsın, çeşitlendir").
* Çağıran paragraf renklerini tona göre verir (`text-white/90` ↔
* `text-slate-600`).
* *
* İçerik (başlık, paragraf, buton) çağıranındır; yalnız kabuk burada durur. * İçerik (başlık, paragraf, buton) çağıranındır; yalnız kabuk burada durur.
* Bilal'in kararı (24 Eyl 2026): koyu slate-950 yüzey yerine bu band. * Bilal'in kararı (24 Eyl 2026): koyu slate-950 yüzey yerine bu band.
@@ -20,6 +25,7 @@ export function CtaBandi({
className, className,
seed = 8, seed = 8,
boyut = "buyuk", boyut = "buyuk",
ton = "mavi",
etiket = "section", etiket = "section",
ref, ref,
...props ...props
@@ -27,6 +33,7 @@ export function CtaBandi({
/** Piksel dokusunun deseni; aynı sayfadaki iki band farklı olsun */ /** Piksel dokusunun deseni; aynı sayfadaki iki band farklı olsun */
seed?: number; seed?: number;
boyut?: "buyuk" | "kompakt"; boyut?: "buyuk" | "kompakt";
ton?: "mavi" | "beyaz";
/** Çekmece altı gibi başlıksız yerlerde `div` */ /** Çekmece altı gibi başlıksız yerlerde `div` */
etiket?: "section" | "div"; etiket?: "section" | "div";
ref?: Ref<HTMLElement>; ref?: Ref<HTMLElement>;
@@ -38,13 +45,20 @@ export function CtaBandi({
<Etiket <Etiket
ref={ref as Ref<HTMLDivElement>} ref={ref as Ref<HTMLDivElement>}
className={cn( className={cn(
"relative isolate overflow-hidden rounded-2xl bg-primary text-white", "relative isolate overflow-hidden rounded-2xl",
ton === "beyaz"
? "border border-slate-200 bg-white text-slate-900"
: "bg-primary text-white",
buyuk ? "px-5 py-8 text-center sm:p-12" : "px-5 py-4 text-left sm:py-5", buyuk ? "px-5 py-8 text-center sm:p-12" : "px-5 py-4 text-left sm:py-5",
className, className,
)} )}
{...props} {...props}
> >
<PikselZemin seed={seed} className={buyuk ? "-inset-y-10" : undefined} /> <PikselZemin
seed={seed}
ton={ton === "beyaz" ? "acik" : "koyu"}
className={buyuk ? "-inset-y-10" : undefined}
/>
{children} {children}
</Etiket> </Etiket>
); );

View File

@@ -21,8 +21,14 @@ export function PikselZemin({
yogunluk, yogunluk,
kenar, kenar,
haric, haric,
ton = "koyu",
className, className,
}: { }: {
/**
* `koyu`: renkli (mavi) zemin, kareler beyaz. `acik`: beyaz zemin, kareler
* slate (Bilal, 24 Eyl 2026: "beyaz arka planda piksel de güzel görünüyor").
*/
ton?: "koyu" | "acik";
/** Aynı ekrandaki iki yüzey farklı desen alsın */ /** Aynı ekrandaki iki yüzey farklı desen alsın */
seed?: number; seed?: number;
/** Doluluk çarpanı (1 varsayılan) */ /** Doluluk çarpanı (1 varsayılan) */
@@ -38,7 +44,8 @@ export function PikselZemin({
<div <div
aria-hidden aria-hidden
className={cn( className={cn(
"pointer-events-none absolute inset-x-0 -inset-y-6 -z-10 text-white", "pointer-events-none absolute inset-x-0 -inset-y-6 -z-10",
ton === "acik" ? "text-slate-400" : "text-white",
className, className,
)} )}
> >

View File

@@ -4,7 +4,8 @@ import { startTransition, useMemo, useRef, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { ChevronDown, ChevronUp } from "lucide-react"; import { ChevronDown, ChevronUp } from "lucide-react";
import type { Program } from "@/types/yokatlas"; import type { Program } from "@/types/yokatlas";
import { trBaslikDuzeni } from "@/lib/slug"; import { ilSayfaHref, trBaslikDuzeni } from "@/lib/slug";
import { cn } from "@/lib/utils";
import { UNITUR_ETIKET } from "@/lib/unitur"; import { UNITUR_ETIKET } from "@/lib/unitur";
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari"; import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
import { Sayfalama, SAYFA_BOYU } from "./sayfalama"; import { Sayfalama, SAYFA_BOYU } from "./sayfalama";
@@ -23,6 +24,7 @@ import {
SATIR_BUTON_KABI, SATIR_BUTON_KABI,
SATIR_CIP, SATIR_CIP,
SATIR_GRUP, SATIR_GRUP,
SATIR_IKINCIL_LINK,
SATIR_META, SATIR_META,
SATIR_MOBIL_SAYI, SATIR_MOBIL_SAYI,
} from "@/components/liste-satiri"; } from "@/components/liste-satiri";
@@ -247,9 +249,13 @@ export function BolumProgramListesi({
</span> </span>
) : null} ) : null}
{p.il ? ( {p.il ? (
<span className="shrink-0"> <Link
href={ilSayfaHref(p.il)}
className={cn("shrink-0", SATIR_IKINCIL_LINK)}
title={`${trBaslikDuzeni(p.il)} üniversiteleri`}
>
{trBaslikDuzeni(p.il)} {trBaslikDuzeni(p.il)}
</span> </Link>
) : null} ) : null}
{p.sure ? ( {p.sure ? (
<span className="shrink-0">{p.sure} yıl</span> <span className="shrink-0">{p.sure} yıl</span>

View File

@@ -18,7 +18,7 @@ import {
getUniversiteBySlug, getUniversiteBySlug,
bolumSlugFromIsim, bolumSlugFromIsim,
} from "../katalog-queries"; } from "../katalog-queries";
import { trBaslikDuzeni } from "@/lib/slug"; import { ilSayfaHref, trBaslikDuzeni } from "@/lib/slug";
import { UNITUR_ETIKET, uniturDevletMi } from "@/lib/unitur"; import { UNITUR_ETIKET, uniturDevletMi } from "@/lib/unitur";
import { import {
JsonLd, JsonLd,
@@ -579,10 +579,16 @@ export function UniversiteIcerik({
</span> </span>
) : null} ) : null}
{uni.il ? ( {uni.il ? (
<span className="inline-flex h-10 min-w-0 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800"> // İl hapı şehir sayfasına gider (Bilal, 24 Eyl 2026: "şehirlerin
// geçtiği her yer tıklanabilir"); hover §2.1 tek turuncu.
<Link
href={ilSayfaHref(uni.il)}
title={`${trBaslikDuzeni(uni.il)} üniversiteleri`}
className="inline-flex h-10 min-w-0 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]"
>
<MapPin className="size-4 shrink-0 text-primary" aria-hidden /> <MapPin className="size-4 shrink-0 text-primary" aria-hidden />
<span className="truncate">{trBaslikDuzeni(uni.il)}</span> <span className="truncate">{trBaslikDuzeni(uni.il)}</span>
</span> </Link>
) : null} ) : null}
</p> </p>
) : null} ) : null}

View File

@@ -4,7 +4,7 @@ import {
getAllUniversiteler, getAllUniversiteler,
type UniOzet, type UniOzet,
} from "../katalog-queries"; } from "../katalog-queries";
import { trBaslikDuzeni } from "@/lib/slug"; import { ilSayfaHref, trBaslikDuzeni } from "@/lib/slug";
import { SehirCipleri, oneCikanIller } from "./sehir-cipleri"; import { SehirCipleri, oneCikanIller } from "./sehir-cipleri";
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form"; import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
import { PagePixelDivider } from "@/components/pixel-decor"; import { PagePixelDivider } from "@/components/pixel-decor";
@@ -15,25 +15,41 @@ import { SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
const sayi = (n: number) => n.toLocaleString("tr-TR"); const sayi = (n: number) => n.toLocaleString("tr-TR");
export function UniLinkleri({ uniler }: { uniler: UniOzet[] }) { export function UniLinkleri({ uniler }: { uniler: UniOzet[] }) {
// Ad ve il ayrı linkler (iç içe <a> olmaz): logo + ad üniversiteye, alt
// satırdaki şehir adı şehir sayfasına (Bilal, 24 Eyl 2026: "şehirlerin
// geçtiği her yer tıklanabilir"). Hover §2.1 tek turuncu.
return ( return (
<ul className="mt-4 grid gap-x-6 gap-y-2 sm:grid-cols-2"> <ul className="mt-4 grid gap-x-6 gap-y-2 sm:grid-cols-2">
{uniler.map((u) => ( {uniler.map((u) => (
<li key={u.slug}> <li key={u.slug} className="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-x-3 text-sm">
<Link <Link
href={`/universite/${u.slug}`} href={`/universite/${u.slug}`}
className="group flex items-center gap-3 text-sm text-slate-700 transition-colors hover:text-orange-500" tabIndex={-1}
aria-hidden
className="row-span-2"
> >
<UniLogo slug={u.slug} ad={u.ad} boy="sm" /> <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> </Link>
<Link
href={`/universite/${u.slug}`}
className="block truncate text-slate-700 transition-colors hover:text-orange-500 hover:underline"
>
{u.ad}
</Link>
<span className="block text-xs text-slate-400">
{u.il ? (
<>
<Link
href={ilSayfaHref(u.il)}
className="transition-colors hover:text-orange-500 hover:underline"
>
{trBaslikDuzeni(u.il)}
</Link>
{" · "}
</>
) : null}
{sayi(u.programSayisi)} program
</span>
</li> </li>
))} ))}
</ul> </ul>
@@ -146,7 +162,7 @@ export function UniversiteKatalogu() {
</section> </section>
<div className="mt-12"> <div className="mt-12">
<CtaSiraForm /> <CtaSiraForm ton="beyaz" />
</div> </div>
</> </>
); );

View File

@@ -21,7 +21,8 @@ import type {
RankResultsListe, RankResultsListe,
} from "@/types/yokatlas"; } from "@/types/yokatlas";
import { DILIM_ETIKET, riskHesapla } from "@/lib/risk"; import { DILIM_ETIKET, riskHesapla } from "@/lib/risk";
import { bolumSayfaSlug, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug"; import { bolumSayfaSlug, ilSayfaHref, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
import { cn } from "@/lib/utils";
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari"; import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
import { import {
ProgramSiraGecmisi, ProgramSiraGecmisi,
@@ -374,7 +375,14 @@ function ProgramSatiri({
<span className={SATIR_META}> <span className={SATIR_META}>
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span> <span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
{p.il ? ( {p.il ? (
<span className="shrink-0">{trBaslikDuzeni(p.il)}</span> <Link
href={ilSayfaHref(p.il)}
className={cn("shrink-0", SATIR_IKINCIL_LINK)}
title={`${trBaslikDuzeni(p.il)} üniversiteleri`}
onClick={(e) => e.stopPropagation()}
>
{trBaslikDuzeni(p.il)}
</Link>
) : null} ) : null}
{p.sure ? ( {p.sure ? (
<span className="shrink-0">{p.sure} yıl</span> <span className="shrink-0">{p.sure} yıl</span>

View File

@@ -239,7 +239,7 @@ export function DemoKapanisCta() {
// §10.8; Bilal, 24 Eyl 2026: "burda piksel dokuları yok"). Tek başlık, tek // §10.8; Bilal, 24 Eyl 2026: "burda piksel dokuları yok"). Tek başlık, tek
// turuncu buton; sayfadaki diğer bantlardan farklı seed. // turuncu buton; sayfadaki diğer bantlardan farklı seed.
return ( return (
<CtaBandi etiket="div" seed={29}> <CtaBandi etiket="div" seed={29} ton="beyaz">
<p className="font-heading text-2xl font-bold text-balance sm:text-3xl"> <p className="font-heading text-2xl font-bold text-balance sm:text-3xl">
{profil {profil
? "Kuralları öğrendin — sayılar zaten senin sıralamanla hesaplandı." ? "Kuralları öğrendin — sayılar zaten senin sıralamanla hesaplandı."

View File

@@ -26,7 +26,8 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { bolumSayfaSlug, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug"; import { bolumSayfaSlug, ilSayfaHref, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
import { cn } from "@/lib/utils";
import { UNITUR_ETIKET } from "@/lib/unitur"; import { UNITUR_ETIKET } from "@/lib/unitur";
import { RiskEtiketi } from "@/components/risk-etiketi"; import { RiskEtiketi } from "@/components/risk-etiketi";
import { import {
@@ -266,7 +267,14 @@ export function RaporListesi({
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span> <span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
) : null} ) : null}
{p?.il ? ( {p?.il ? (
<span className="shrink-0">{trBaslikDuzeni(p.il)}</span> <Link
href={ilSayfaHref(p.il)}
className={cn("shrink-0", SATIR_IKINCIL_LINK)}
title={`${trBaslikDuzeni(p.il)} üniversiteleri`}
onClick={(e) => e.stopPropagation()}
>
{trBaslikDuzeni(p.il)}
</Link>
) : null} ) : null}
</span> </span>
</span> </span>

View File

@@ -24,12 +24,14 @@ import {
import { useGorunumOlayi } from "@/components/use-gorunum-olayi"; import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
import { olay, siraKovasi } from "@/lib/analitik"; import { olay, siraKovasi } from "@/lib/analitik";
import { dilimdenRisk, riskHesapla } from "@/lib/risk"; import { dilimdenRisk, riskHesapla } from "@/lib/risk";
import { trBaslikDuzeni, turkishSlugify } from "@/lib/slug"; import { ilSayfaHref, trBaslikDuzeni, turkishSlugify } from "@/lib/slug";
import { cn } from "@/lib/utils";
import { import {
SATIR_ALT_AD, SATIR_ALT_AD,
SATIR_AD_DUZ, SATIR_AD_DUZ,
SATIR_CIP, SATIR_CIP,
SATIR_ALT_GRUP, SATIR_ALT_GRUP,
SATIR_IKINCIL_LINK,
SATIR_META, SATIR_META,
} from "@/components/liste-satiri"; } from "@/components/liste-satiri";
import { listemUretHref, PROFIL_DEGISTI_EVENT } from "@/features/sihirbaz/sihirbaz-sabitler"; import { listemUretHref, PROFIL_DEGISTI_EVENT } from "@/features/sihirbaz/sihirbaz-sabitler";
@@ -166,7 +168,13 @@ export function TadimlikSatiri({
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span> <span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
) : null} ) : null}
{p.il ? ( {p.il ? (
<span className="shrink-0">{trBaslikDuzeni(p.il)}</span> <Link
href={ilSayfaHref(p.il)}
className={cn("shrink-0", SATIR_IKINCIL_LINK)}
title={`${trBaslikDuzeni(p.il)} üniversiteleri`}
>
{trBaslikDuzeni(p.il)}
</Link>
) : null} ) : null}
</span> </span>
</span> </span>

View File

@@ -101,6 +101,9 @@ function KayitliSira({
* tabloya bağlayan köprü cümlesini buradan verir. Başlık ve açıklamalar * tabloya bağlayan köprü cümlesini buradan verir. Başlık ve açıklamalar
* yerleşme vaadi içermez: ürün sıralamaya yakın programları listeler. * yerleşme vaadi içermez: ürün sıralamaya yakın programları listeler.
* *
* `ton="beyaz"`: kart varyantının beyaz zeminli bandı (aynı sayfada iki mavi
* bant üst üste gelmesin diye; DESIGN.md §10.8).
*
* `varyant="kompakt"`: katalog sayfalarında başlığın hemen altına, tablonun * `varyant="kompakt"`: katalog sayfalarında başlığın hemen altına, tablonun
* ÜSTÜNE konan tek satırlık sürüm. Sıralama alanı çekmecede değil doğrudan * ÜSTÜNE konan tek satırlık sürüm. Sıralama alanı çekmecede değil doğrudan
* sayfadadır (tek sayfada çıkan ziyaretçi alttaki karta hiç inmiyor); gönderince * sayfadadır (tek sayfada çıkan ziyaretçi alttaki karta hiç inmiyor); gönderince
@@ -113,10 +116,13 @@ export function CtaSiraForm({
aciklama, aciklama,
varyant = "kart", varyant = "kart",
konum = "alt", konum = "alt",
ton = "mavi",
}: { }: {
baslik?: string; baslik?: string;
aciklama?: string; aciklama?: string;
varyant?: "kart" | "kompakt"; varyant?: "kart" | "kompakt";
/** Yalnız kart varyantı: bandın zemini */
ton?: "mavi" | "beyaz";
/** Yalnız ölçüm içindir: formun sayfadaki yeri (tablonun üstü / altı). */ /** Yalnız ölçüm içindir: formun sayfadaki yeri (tablonun üstü / altı). */
konum?: "ust" | "alt"; konum?: "ust" | "alt";
}) { }) {
@@ -427,16 +433,18 @@ export function CtaSiraForm({
} }
// Kart varyantı sayfa sonundaki CTA bandıdır (DESIGN.md §10.8, CtaBandi). // Kart varyantı sayfa sonundaki CTA bandıdır (DESIGN.md §10.8, CtaBandi).
const beyaz = ton === "beyaz";
const paragrafSinifi = beyaz ? "text-slate-600" : "text-white/90";
if (profil) { if (profil) {
return ( return (
<CtaBandi seed={5}> <CtaBandi seed={5} ton={ton}>
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl"> <h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."} {baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
</h2> </h2>
<div className="mt-3"> <div className="mt-3">
<KayitliSira sira={profil.sira} tur={profil.tur} beyaz /> <KayitliSira sira={profil.sira} tur={profil.tur} beyaz={!beyaz} />
</div> </div>
<p className="mx-auto mt-2 max-w-xl text-base leading-relaxed text-white/90"> <p className={cn("mx-auto mt-2 max-w-xl text-base leading-relaxed", paragrafSinifi)}>
Gerçek YÖK Atlas verisiyle hayal, dengeli ve güvenli dilimlerindeki Gerçek YÖK Atlas verisiyle hayal, dengeli ve güvenli dilimlerindeki
programların hazır. programların hazır.
</p> </p>
@@ -456,11 +464,11 @@ export function CtaSiraForm({
return ( return (
<div ref={formRef}> <div ref={formRef}>
<CtaBandi seed={9}> <CtaBandi seed={9} ton={ton}>
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl"> <h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."} {baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
</h2> </h2>
<p className="mx-auto mt-3 max-w-xl text-base leading-relaxed text-white/90"> <p className={cn("mx-auto mt-3 max-w-xl text-base leading-relaxed", paragrafSinifi)}>
{aciklama ?? {aciklama ??
"YKS başarı sıralamanı gir; gerçek YÖK Atlas verisiyle sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez."} "YKS başarı sıralamanı gir; gerçek YÖK Atlas verisiyle sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez."}
</p> </p>

View File

@@ -180,6 +180,39 @@ export function yurtdisiUniKonum(
return cozum && "anahtar" in cozum ? sehirKonumu(cozum.anahtar) : null; return cozum && "anahtar" in cozum ? sehirKonumu(cozum.anahtar) : null;
} }
/**
* Yalnızca il değerinden ülke haritası (şehir sayfası). "KIBRIS" genel
* değeri ülke seviyesinde kalır; "ÜSKÜP-MAKEDONYA" gibi ülke ekli değerler
* ilk parçasıyla çözülür. Türkiye ili ya da bilinmeyen değer → null.
*/
export function ilUlkeHaritasi(il: string): {
ulke: UlkeKodu;
ulkeAd: string;
harita: UlkeHarita;
sehir: YurtdisiKonum | null;
} | null {
const anahtar =
il in SEHIRLER
? (il as SehirAnahtari)
: (() => {
const ilk = il.split("-")[0].trim();
return ilk in SEHIRLER ? (ilk as SehirAnahtari) : null;
})();
const ulke: UlkeKodu | null = anahtar
? SEHIRLER[anahtar].ulke
: il === "KIBRIS"
? "kktc"
: null;
if (!ulke) return null;
const harita = ULKE_HARITALARI[ulke];
return {
ulke,
ulkeAd: harita.ad,
harita,
sehir: anahtar ? sehirKonumu(anahtar) : null,
};
}
/** Yalnızca il değerinden (kilitli satırlar) yurtdışı şehir konumu. */ /** Yalnızca il değerinden (kilitli satırlar) yurtdışı şehir konumu. */
export function yurtdisiIlKonum(il: string | null): YurtdisiKonum | null { export function yurtdisiIlKonum(il: string | null): YurtdisiKonum | null {
return il && il in SEHIRLER ? sehirKonumu(il as SehirAnahtari) : null; return il && il in SEHIRLER ? sehirKonumu(il as SehirAnahtari) : null;

View File

@@ -64,6 +64,8 @@ const globalKatalog = globalThis as unknown as {
uniOzetler?: UniOzet[]; uniOzetler?: UniOzet[];
iller?: IlOzet[]; iller?: IlOzet[];
ilMap?: Map<string, IlOzet>; ilMap?: Map<string, IlOzet>;
/** slug → (il → o ildeki program sayısı); getAllUniversiteler doldurur */
uniIlAdetleri?: Map<string, Map<string, number>>;
}; };
function bolumMapGetir(): Map<string, BolumKayit> { function bolumMapGetir(): Map<string, BolumKayit> {
@@ -335,6 +337,7 @@ export function getAllUniversiteler(): UniOzet[] {
} }
sayac.set(row.il, (sayac.get(row.il) ?? 0) + row.adet); sayac.set(row.il, (sayac.get(row.il) ?? 0) + row.adet);
} }
globalKatalog.uniIlAdetleri = ilAdetleri;
for (const [slug, sayac] of ilAdetleri) { for (const [slug, sayac] of ilAdetleri) {
let enIyi: string | null = null; let enIyi: string | null = null;
let enCok = 0; let enCok = 0;
@@ -378,7 +381,9 @@ export type IlOzet = {
slug: string; slug: string;
/** Görünen ad: İstanbul, Bakü-Azerbaycan */ /** Görünen ad: İstanbul, Bakü-Azerbaycan */
ad: string; ad: string;
/** Bu ilde yer alan üniversiteler (UniOzet.il esas; ad sırasıyla) */ /** DB biçiminde ham il ("İSTANBUL"); harita anahtarı */
il: string;
/** Bu ilde programı olan üniversiteler (ad sırasıyla) */
uniler: UniOzet[]; uniler: UniOzet[];
programSayisi: number; programSayisi: number;
}; };
@@ -395,25 +400,42 @@ function ilGorunenAd(il: string): string {
/** /**
* Üniversiteleri iline göre kümeler; sıralama üniversite sayısına göre * Üniversiteleri iline göre kümeler; sıralama üniversite sayısına göre
* azalan, eşitlikte ad. Her üniversite tek ilde sayılır (UniOzet.il: en çok * azalan, eşitlikte ad. Bir üniversite **programı olan her ilde** sayılır
* programının bulunduğu il). İli olmayan (NULL/boş) üniversite hiçbir ile * (ODTÜ hem Ankara'da hem KKTC'de; öğrenci "KKTC'de okuyabileceğim
* girmez. KKTC ve yurt dışı şehirler de birer ildir (LEFKOŞA, BAKÜ-…). * üniversiteler" diye bakar) — böylece DB'deki her il değerinin sayfası
* vardır ve program satırlarındaki her şehir adı güvenle link olur
* (`ilSayfaHref`). `programSayisi` o ildeki program sayısıdır.
*/ */
export function getAllIller(): IlOzet[] { export function getAllIller(): IlOzet[] {
if (!globalKatalog.iller) { if (!globalKatalog.iller) {
// Dev'de globalThis önbelleği HMR'ı aşar: uniOzetler eski modül koduyla
// kurulduysa il sayacı yoktur; özetler yeniden kurulur.
if (!globalKatalog.uniIlAdetleri) globalKatalog.uniOzetler = undefined;
const uniler = getAllUniversiteler();
const ilAdetleri = globalKatalog.uniIlAdetleri!;
const bySlug = new Map<string, IlOzet>(); const bySlug = new Map<string, IlOzet>();
for (const u of getAllUniversiteler()) { for (const u of uniler) {
const il = u.il?.trim(); const sayac = ilAdetleri.get(u.slug);
if (!il) continue; if (!sayac) continue;
const slug = turkishSlugify(il); for (const [ham, adet] of sayac) {
if (!slug) continue; const il = ham.trim();
let kayit = bySlug.get(slug); if (!il) continue;
if (!kayit) { const slug = turkishSlugify(il);
kayit = { slug, ad: ilGorunenAd(il), uniler: [], programSayisi: 0 }; if (!slug) continue;
bySlug.set(slug, kayit); let kayit = bySlug.get(slug);
if (!kayit) {
kayit = {
slug,
ad: ilGorunenAd(il),
il,
uniler: [],
programSayisi: 0,
};
bySlug.set(slug, kayit);
}
kayit.uniler.push(u);
kayit.programSayisi += adet;
} }
kayit.uniler.push(u);
kayit.programSayisi += u.programSayisi;
} }
globalKatalog.ilMap = bySlug; globalKatalog.ilMap = bySlug;
globalKatalog.iller = [...bySlug.values()].sort( globalKatalog.iller = [...bySlug.values()].sort(

View File

@@ -52,6 +52,14 @@ export function bolumSayfaSlug(isim: string): string {
return turkishSlugify(bolumBazAdi(isim)); return turkishSlugify(bolumBazAdi(isim));
} }
/**
* /universiteler/[il] şehir sayfası adresi. Şehir sayfaları DB'deki her il
* değeri için üretildiğinden (katalog.ts getAllIller) adres her zaman vardır.
*/
export function ilSayfaHref(il: string): string {
return `/universiteler/${turkishSlugify(il)}`;
}
/** ALL-CAPS veritabanı adlarını tr-TR başlık düzenine çevirir (İSTANBUL → İstanbul). */ /** ALL-CAPS veritabanı adlarını tr-TR başlık düzenine çevirir (İSTANBUL → İstanbul). */
export function trBaslikDuzeni(s: string): string { export function trBaslikDuzeni(s: string): string {
return s return s