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:
384
src/lib/katalog.ts
Normal file
384
src/lib/katalog.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
import {
|
||||
getDb,
|
||||
SELECT_COLS,
|
||||
EFEKTIF_SIRA,
|
||||
type Program,
|
||||
} from "./db";
|
||||
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(il) AS il, MAX(unitur) AS unitur,
|
||||
COUNT(*) AS adet
|
||||
FROM programs GROUP BY universite`,
|
||||
)
|
||||
.all() as {
|
||||
universite: string;
|
||||
il: string | null;
|
||||
unitur: string | null;
|
||||
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.il && row.il) u.il = row.il;
|
||||
if (!u.unitur && row.unitur) u.unitur = row.unitur;
|
||||
}
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user