Some checks failed
Deploy / deploy (push) Has been cancelled
Profil çerezinin SSR'da kullanılmasını sağlayarak, sonuç sayfasındaki yönlendirmeleri güncelledik. Artık profil bilgileri URL parametreleri yerine çerezden alınarak işleniyor. Ayrıca, profil çerezi olmayan kullanıcılar için yönlendirme mantığı iyileştirildi. Çeşitli bileşenlerdeki URL oluşturma fonksiyonları güncellenerek, profil bilgileri çerezden okunacak şekilde düzenlendi. Bu değişiklikler, kullanıcı deneyimini artırmayı ve daha tutarlı bir veri akışı sağlamayı hedefliyor.
410 lines
13 KiB
TypeScript
410 lines
13 KiB
TypeScript
import "server-only";
|
||
import { getDb, SELECT_COLS, EFEKTIF_SIRA } from "./db";
|
||
import type { Program } from "@/types/yokatlas";
|
||
import { kategoriBul, type Kategori } from "./kategoriler";
|
||
import { uniAdiNormalize } from "./harita";
|
||
import { bolumBazAdi, turkishSlugify, trBaslikDuzeni } from "./slug";
|
||
|
||
/**
|
||
* Programatik SEO sayfalarının veri katmanı (/bolum, /universite, hub'lar).
|
||
*
|
||
* Slug haritaları modül-scope'ta lazy kurulur: DB readonly açıldığı için
|
||
* slugs tablosu yazılamaz; SELECT DISTINCT (1.5K satır) süreç başına bir kez
|
||
* <10ms sürer. Harita değerleri HAM isim/universite dizeleridir — sorgular
|
||
* `WHERE isim IN (?, ...)` ile birebir eşleşip index'e vurur (WHERE içinde
|
||
* TRIM/fonksiyon kullanmak index'i öldürür).
|
||
*/
|
||
|
||
// ---------- Bölüm tarafı ----------
|
||
|
||
export type BolumOzet = {
|
||
slug: string;
|
||
ad: string;
|
||
programSayisi: number;
|
||
kategoriler: Kategori[];
|
||
lisans: boolean;
|
||
onlisans: boolean;
|
||
/** MIN(COALESCE(sira2025, sira2024)) — en iyi (en küçük) taban sıralaması */
|
||
enIyiSira: number | null;
|
||
};
|
||
|
||
export type BolumStats = {
|
||
toplam: number;
|
||
devlet: number;
|
||
vakif: number;
|
||
/** unitur boş/NULL olan satırlar — vakıf SAYILMAZ, ayrı gösterilir */
|
||
bilinmeyenTur: number;
|
||
minSira: number | null;
|
||
maxSira: number | null;
|
||
minPuan: number | null;
|
||
maxPuan: number | null;
|
||
ilSayisi: number;
|
||
};
|
||
|
||
export type BolumDetay = BolumOzet & {
|
||
lisansProgramlari: Program[];
|
||
onlisansProgramlari: Program[];
|
||
stats: BolumStats;
|
||
};
|
||
|
||
type BolumKayit = { slug: string; ad: string; hamIsimler: string[] };
|
||
|
||
const globalKatalog = globalThis as unknown as {
|
||
bolumMap?: Map<string, BolumKayit>;
|
||
bolumOzetler?: BolumOzet[];
|
||
uniMap?: Map<string, UniKayit>;
|
||
uniOzetler?: UniOzet[];
|
||
};
|
||
|
||
function bolumMapGetir(): Map<string, BolumKayit> {
|
||
if (!globalKatalog.bolumMap) {
|
||
const rows = getDb()
|
||
.prepare("SELECT DISTINCT isim FROM programs")
|
||
.all() as { isim: string }[];
|
||
const map = new Map<string, BolumKayit>();
|
||
for (const { isim } of rows) {
|
||
const baz = bolumBazAdi(isim);
|
||
if (!baz) continue;
|
||
const slug = turkishSlugify(baz);
|
||
if (!slug) continue;
|
||
const kayit = map.get(slug);
|
||
if (kayit) kayit.hamIsimler.push(isim);
|
||
else map.set(slug, { slug, ad: baz, hamIsimler: [isim] });
|
||
}
|
||
if (map.size === 0) {
|
||
// generateStaticParams boş dizi dönerse build hatası — boş DB'yi erken yakala
|
||
throw new Error("katalog: programs tablosu boş görünüyor");
|
||
}
|
||
globalKatalog.bolumMap = map;
|
||
}
|
||
return globalKatalog.bolumMap;
|
||
}
|
||
|
||
export function getAllBolumler(): BolumOzet[] {
|
||
if (!globalKatalog.bolumOzetler) {
|
||
const map = bolumMapGetir();
|
||
// Tek taramada tüm bölümlerin özeti: 23K satır, süreç başına bir kez
|
||
const rows = getDb()
|
||
.prepare(
|
||
`SELECT isim, onlisans, ${EFEKTIF_SIRA} AS efektif FROM programs`,
|
||
)
|
||
.all() as { isim: string; onlisans: number; efektif: number | null }[];
|
||
|
||
const ozet = new Map<string, BolumOzet>();
|
||
const slugByIsim = new Map<string, string>();
|
||
for (const kayit of map.values()) {
|
||
for (const ham of kayit.hamIsimler) slugByIsim.set(ham, kayit.slug);
|
||
ozet.set(kayit.slug, {
|
||
slug: kayit.slug,
|
||
ad: kayit.ad,
|
||
programSayisi: 0,
|
||
kategoriler: kategoriBul(kayit.ad),
|
||
lisans: false,
|
||
onlisans: false,
|
||
enIyiSira: null,
|
||
});
|
||
}
|
||
for (const row of rows) {
|
||
const slug = slugByIsim.get(row.isim);
|
||
if (!slug) continue;
|
||
const b = ozet.get(slug)!;
|
||
b.programSayisi += 1;
|
||
if (row.onlisans === 1) b.onlisans = true;
|
||
else b.lisans = true;
|
||
if (row.efektif != null && (b.enIyiSira == null || row.efektif < b.enIyiSira)) {
|
||
b.enIyiSira = row.efektif;
|
||
}
|
||
}
|
||
globalKatalog.bolumOzetler = [...ozet.values()].sort((a, b) =>
|
||
a.ad.localeCompare(b.ad, "tr-TR"),
|
||
);
|
||
}
|
||
return globalKatalog.bolumOzetler;
|
||
}
|
||
|
||
export function getAllBolumSlugs(): string[] {
|
||
return [...bolumMapGetir().keys()];
|
||
}
|
||
|
||
export function getBolumBySlug(slug: string): BolumDetay | null {
|
||
const kayit = bolumMapGetir().get(slug);
|
||
if (!kayit) return null;
|
||
|
||
const placeholders = kayit.hamIsimler.map(() => "?").join(",");
|
||
const programlar = getDb()
|
||
.prepare(
|
||
`SELECT ${SELECT_COLS} FROM programs
|
||
WHERE isim IN (${placeholders})
|
||
ORDER BY (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`,
|
||
)
|
||
.all(...kayit.hamIsimler) as (Program & { onlisans?: number })[];
|
||
|
||
// onlisans SELECT_COLS içinde yok; tur üzerinden ayır (TYT = önlisans)
|
||
const lisansProgramlari = programlar.filter((p) => p.tur !== "TYT");
|
||
const onlisansProgramlari = programlar.filter((p) => p.tur === "TYT");
|
||
|
||
const iller = new Set<string>();
|
||
let devlet = 0;
|
||
let vakif = 0;
|
||
let bilinmeyenTur = 0;
|
||
let minSira: number | null = null;
|
||
let maxSira: number | null = null;
|
||
let minPuan: number | null = null;
|
||
let maxPuan: number | null = null;
|
||
for (const p of programlar) {
|
||
if (p.il) iller.add(p.il);
|
||
if (p.unitur === "DEVLET") devlet += 1;
|
||
else if (p.unitur) vakif += 1;
|
||
else bilinmeyenTur += 1;
|
||
const efektif = p.sira2025 ?? p.sira2024;
|
||
if (efektif != null) {
|
||
if (minSira == null || efektif < minSira) minSira = efektif;
|
||
if (maxSira == null || efektif > maxSira) maxSira = efektif;
|
||
}
|
||
if (p.puan2025 != null) {
|
||
if (minPuan == null || p.puan2025 < minPuan) minPuan = p.puan2025;
|
||
if (maxPuan == null || p.puan2025 > maxPuan) maxPuan = p.puan2025;
|
||
}
|
||
}
|
||
|
||
const ozet = getAllBolumler().find((b) => b.slug === slug)!;
|
||
return {
|
||
...ozet,
|
||
lisansProgramlari,
|
||
onlisansProgramlari,
|
||
stats: {
|
||
toplam: programlar.length,
|
||
devlet,
|
||
vakif,
|
||
bilinmeyenTur,
|
||
minSira,
|
||
maxSira,
|
||
minPuan,
|
||
maxPuan,
|
||
ilSayisi: iller.size,
|
||
},
|
||
};
|
||
}
|
||
|
||
/** Aynı kategorideki diğer bölümler, program sayısına göre (kendisi hariç). */
|
||
export function ilgiliBolumler(slug: string, limit = 8): BolumOzet[] {
|
||
const hepsi = getAllBolumler();
|
||
const kendi = hepsi.find((b) => b.slug === slug);
|
||
if (!kendi) return [];
|
||
const kendiKategoriler = new Set<string>(kendi.kategoriler);
|
||
return hepsi
|
||
.filter(
|
||
(b) =>
|
||
b.slug !== slug &&
|
||
(kendiKategoriler.size === 0 ||
|
||
b.kategoriler.some((k) => kendiKategoriler.has(k))),
|
||
)
|
||
.sort((a, b) => b.programSayisi - a.programSayisi)
|
||
.slice(0, limit);
|
||
}
|
||
|
||
// ---------- Üniversite tarafı ----------
|
||
|
||
export type UniOzet = {
|
||
slug: string;
|
||
/** tr-TR başlık düzeninde görünen ad (İstanbul Teknik Üniversitesi) */
|
||
ad: string;
|
||
il: string | null;
|
||
unitur: string | null;
|
||
programSayisi: number;
|
||
fakulteSayisi: number;
|
||
};
|
||
|
||
export type UniDetay = UniOzet & {
|
||
fakulteler: { fakulte: string; programlar: Program[] }[];
|
||
stats: {
|
||
minSira: number | null;
|
||
toplamKontenjan: number;
|
||
devletMi: boolean;
|
||
};
|
||
};
|
||
|
||
type UniKayit = { slug: string; ad: string; hamAdlar: string[] };
|
||
|
||
function uniMapGetir(): Map<string, UniKayit> {
|
||
if (!globalKatalog.uniMap) {
|
||
const rows = getDb()
|
||
.prepare("SELECT DISTINCT universite FROM programs")
|
||
.all() as { universite: string }[];
|
||
const map = new Map<string, UniKayit>();
|
||
for (const { universite } of rows) {
|
||
const normal = uniAdiNormalize(universite);
|
||
if (!normal) continue;
|
||
const slug = turkishSlugify(normal);
|
||
if (!slug) continue;
|
||
const kayit = map.get(slug);
|
||
if (kayit) kayit.hamAdlar.push(universite);
|
||
else
|
||
map.set(slug, {
|
||
slug,
|
||
ad: trBaslikDuzeni(normal),
|
||
hamAdlar: [universite],
|
||
});
|
||
}
|
||
if (map.size === 0) {
|
||
throw new Error("katalog: programs tablosunda üniversite yok");
|
||
}
|
||
globalKatalog.uniMap = map;
|
||
}
|
||
return globalKatalog.uniMap;
|
||
}
|
||
|
||
export function getAllUniversiteler(): UniOzet[] {
|
||
if (!globalKatalog.uniOzetler) {
|
||
const map = uniMapGetir();
|
||
const rows = getDb()
|
||
.prepare(
|
||
`SELECT universite, MAX(unitur) AS unitur, COUNT(*) AS adet
|
||
FROM programs GROUP BY universite`,
|
||
)
|
||
.all() as {
|
||
universite: string;
|
||
unitur: string | null;
|
||
adet: number;
|
||
}[];
|
||
// il: MAX(il) alfabetik en büyüğü seçip yanıltıyordu (ör. Ankara Sosyal
|
||
// Bilimler'in 12 KKTC satırı 47 Ankara satırını eziyordu); üniversitenin
|
||
// en çok programının bulunduğu il esas alınır.
|
||
const ilRows = getDb()
|
||
.prepare(
|
||
`SELECT universite, il, COUNT(*) AS adet FROM programs
|
||
WHERE il IS NOT NULL GROUP BY universite, il`,
|
||
)
|
||
.all() as { universite: string; il: string; adet: number }[];
|
||
// Fakülte sayısı: aynı üniversitenin iki yazımı ("X ÜNİV." / "X ÜNİV. (İL)")
|
||
// birleştiği için GROUP BY universite üzerinden saymak çift sayardı;
|
||
// (slug, fakulte) çifti üzerinden kesin sayılır.
|
||
const fakulteRows = getDb()
|
||
.prepare(
|
||
"SELECT DISTINCT universite, fakulte FROM programs WHERE fakulte IS NOT NULL",
|
||
)
|
||
.all() as { universite: string; fakulte: string }[];
|
||
|
||
const bySlug = new Map<string, UniOzet>();
|
||
const slugByHam = new Map<string, string>();
|
||
for (const kayit of map.values()) {
|
||
for (const ham of kayit.hamAdlar) slugByHam.set(ham, kayit.slug);
|
||
bySlug.set(kayit.slug, {
|
||
slug: kayit.slug,
|
||
ad: kayit.ad,
|
||
il: null,
|
||
unitur: null,
|
||
programSayisi: 0,
|
||
fakulteSayisi: 0,
|
||
});
|
||
}
|
||
for (const row of rows) {
|
||
const slug = slugByHam.get(row.universite);
|
||
if (!slug) continue;
|
||
const u = bySlug.get(slug)!;
|
||
u.programSayisi += row.adet;
|
||
if (!u.unitur && row.unitur) u.unitur = row.unitur;
|
||
}
|
||
const ilAdetleri = new Map<string, Map<string, number>>();
|
||
for (const row of ilRows) {
|
||
const slug = slugByHam.get(row.universite);
|
||
if (!slug) continue;
|
||
let sayac = ilAdetleri.get(slug);
|
||
if (!sayac) {
|
||
sayac = new Map();
|
||
ilAdetleri.set(slug, sayac);
|
||
}
|
||
sayac.set(row.il, (sayac.get(row.il) ?? 0) + row.adet);
|
||
}
|
||
for (const [slug, sayac] of ilAdetleri) {
|
||
let enIyi: string | null = null;
|
||
let enCok = 0;
|
||
for (const [il, adet] of sayac) {
|
||
if (adet > enCok) {
|
||
enIyi = il;
|
||
enCok = adet;
|
||
}
|
||
}
|
||
bySlug.get(slug)!.il = enIyi;
|
||
}
|
||
const fakulteSetleri = new Map<string, Set<string>>();
|
||
for (const row of fakulteRows) {
|
||
const slug = slugByHam.get(row.universite);
|
||
if (!slug) continue;
|
||
let set = fakulteSetleri.get(slug);
|
||
if (!set) {
|
||
set = new Set();
|
||
fakulteSetleri.set(slug, set);
|
||
}
|
||
set.add(row.fakulte);
|
||
}
|
||
for (const [slug, set] of fakulteSetleri) {
|
||
bySlug.get(slug)!.fakulteSayisi = set.size;
|
||
}
|
||
globalKatalog.uniOzetler = [...bySlug.values()].sort((a, b) =>
|
||
a.ad.localeCompare(b.ad, "tr-TR"),
|
||
);
|
||
}
|
||
return globalKatalog.uniOzetler;
|
||
}
|
||
|
||
export function getAllUniversiteSlugs(): string[] {
|
||
return [...uniMapGetir().keys()];
|
||
}
|
||
|
||
export function getUniversiteBySlug(slug: string): UniDetay | null {
|
||
const kayit = uniMapGetir().get(slug);
|
||
if (!kayit) return null;
|
||
|
||
const placeholders = kayit.hamAdlar.map(() => "?").join(",");
|
||
const programlar = getDb()
|
||
.prepare(
|
||
`SELECT ${SELECT_COLS} FROM programs
|
||
WHERE universite IN (${placeholders})
|
||
ORDER BY fakulte, (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`,
|
||
)
|
||
.all(...kayit.hamAdlar) as Program[];
|
||
|
||
const gruplar = new Map<string, Program[]>();
|
||
let minSira: number | null = null;
|
||
let toplamKontenjan = 0;
|
||
for (const p of programlar) {
|
||
const anahtar = p.fakulte ?? "Diğer";
|
||
const liste = gruplar.get(anahtar);
|
||
if (liste) liste.push(p);
|
||
else gruplar.set(anahtar, [p]);
|
||
const efektif = p.sira2025 ?? p.sira2024;
|
||
if (efektif != null && (minSira == null || efektif < minSira)) {
|
||
minSira = efektif;
|
||
}
|
||
toplamKontenjan += p.kontenjan2025 ?? 0;
|
||
}
|
||
|
||
const ozet = getAllUniversiteler().find((u) => u.slug === slug)!;
|
||
return {
|
||
...ozet,
|
||
fakulteler: [...gruplar.entries()]
|
||
.map(([fakulte, programlar]) => ({ fakulte, programlar }))
|
||
.sort((a, b) => a.fakulte.localeCompare(b.fakulte, "tr-TR")),
|
||
stats: {
|
||
minSira,
|
||
toplamKontenjan,
|
||
devletMi: ozet.unitur === "DEVLET",
|
||
},
|
||
};
|
||
}
|
||
|
||
// ---------- Yardımcılar ----------
|
||
|
||
/** Ham program isminden /bolum sayfası slug'ı (tablolarda çapraz link için). */
|
||
export function bolumSlugFromIsim(isim: string): string | null {
|
||
const slug = turkishSlugify(bolumBazAdi(isim));
|
||
return bolumMapGetir().has(slug) ? slug : null;
|
||
}
|
||
|
||
/** Ham üniversite adından /universite sayfası slug'ı. */
|
||
export function uniSlugFromAd(universite: string): string | null {
|
||
const slug = turkishSlugify(uniAdiNormalize(universite));
|
||
return uniMapGetir().has(slug) ? slug : null;
|
||
}
|