feat(ui): Arc bileşenleri, landing bölümleri ve bildirim yığını

Sonner/vaul yerine kendi bildirim yığını ve alt sayfa; çip grubu, kart
destesi, kaydırma eylemi, yazı dönüşümü, bölme tutamacı; landing'e program
duvarı, süzülen haplar ve veri akışı; yaylar tek dosyada (lib/hareket.ts).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-07 17:12:53 +03:00
parent bc920f6111
commit 3c995402c6
38 changed files with 5474 additions and 697 deletions

View File

@@ -0,0 +1,129 @@
import "server-only";
// Ana sayfadaki iki etkileşimli parçanın verisi (sunucuda, build/istek anında):
// - Kart destesi demosu (DESIGN.md §4.27): örnek bir sıralamaya göre GERÇEK
// programlar. Uydurma taban sıralaması gösterilmez; veri okunamazsa bölüm
// hiç çizilmez.
// - Süzülen haplar (§4.30): yalnız katalogda sayfası olan bölüm ve şehirler.
import { dilimAra } from "@/lib/db";
import { bolumSlugFromIsim, getIlBySlug } from "@/lib/katalog";
import { ozelKontenjanSatiriMi } from "@/lib/rapor-havuzu";
import { riskHesapla, type RiskSeviyesi } from "@/lib/risk";
import { bolumBazAdi, ilSayfaHref, trBaslikDuzeni, turkishSlugify } from "@/lib/slug";
import type { DilimKey } from "@/types/yokatlas";
/** Demonun örnek öğrencisi: Sayısal, 85.000. sıra (§10.1'deki örnekle aynı). */
export const ORNEK_SIRA = 85_000;
const ORNEK_TUR = "say" as const;
export interface OrnekProgram {
id: string;
isim: string;
universite: string;
il: string | null;
tabanSira: number;
risk: RiskSeviyesi;
}
/**
* Her dilimden birkaç program: sınıra yapışık olanlar yerine dilimin içine
* yayılmış (riskli / az riskli / güvenli farkı kartta görünsün), her üniversite
* ve her bölüm adı en çok bir kez.
*/
export function ornekProgramlar(): OrnekProgram[] {
try {
const gorulenUni = new Set<string>();
const gorulenBolum = new Set<string>();
const sec = (dilim: DilimKey, adet: number, adim: number): OrnekProgram[] => {
const { programlar } = dilimAra(ORNEK_SIRA, ORNEK_TUR, dilim, {
uniturGrubu: "devlet",
barajDisiHaric: true,
limit: 600,
});
const secilen: OrnekProgram[] = [];
for (let i = adim; i < programlar.length && secilen.length < adet; i += adim) {
const p = programlar[i];
const bolum = bolumBazAdi(p.isim);
const risk = riskHesapla(p.efektifSira, ORNEK_SIRA);
if (
p.efektifSira == null ||
!risk ||
ozelKontenjanSatiriMi(p.isim) ||
/\(/.test(p.isim) ||
gorulenUni.has(p.universite) ||
gorulenBolum.has(bolum)
) {
// Elenen satırın bir sonrakine bak; adım bozulmasın diye geri al
i -= adim - 1;
continue;
}
gorulenUni.add(p.universite);
gorulenBolum.add(bolum);
secilen.push({
id: p.id,
isim: p.isim,
universite: trBaslikDuzeni(p.universite),
il: p.il ? trBaslikDuzeni(p.il) : null,
tabanSira: p.efektifSira,
risk,
});
}
return secilen;
};
const hayal = sec("hayal", 2, 60);
const dengeli = sec("dengeli", 3, 45);
const guvenli = sec("garanti", 2, 90);
// Sıra karışık: aynı risk art arda gelmesin
return [dengeli[0], hayal[0], guvenli[0], dengeli[1], hayal[1], dengeli[2], guvenli[1]].filter(
(p): p is OrnekProgram => Boolean(p),
);
} catch {
return [];
}
}
export interface KatalogHapi {
etiket: string;
href: string;
tur: "bolum" | "sehir";
}
const HAP_BOLUMLERI = [
"Tıp",
"Hukuk",
"Bilgisayar Mühendisliği",
"Psikoloji",
"Diş Hekimliği",
"Hemşirelik",
"Mimarlık",
"Yazılım Mühendisliği",
"Eczacılık",
"İşletme",
];
const HAP_SEHIRLERI = ["İstanbul", "Ankara", "İzmir", "Eskişehir", "Bursa", "Antalya"];
/** Bölüm ve şehir hapları; katalogda karşılığı olmayan ad sessizce düşer. */
export function katalogHaplari(): KatalogHapi[] {
try {
const bolumler: KatalogHapi[] = HAP_BOLUMLERI.flatMap((ad) => {
const slug = bolumSlugFromIsim(ad);
return slug ? [{ etiket: ad, href: `/bolum/${slug}`, tur: "bolum" as const }] : [];
});
const sehirler: KatalogHapi[] = HAP_SEHIRLERI.flatMap((ad) =>
getIlBySlug(turkishSlugify(ad))
? [{ etiket: ad, href: ilSayfaHref(ad), tur: "sehir" as const }]
: [],
);
// Bölüm ve şehir karışık dizilsin: her üç bölümden sonra bir şehir
const sonuc: KatalogHapi[] = [];
let s = 0;
bolumler.forEach((b, i) => {
sonuc.push(b);
if (i % 2 === 1 && s < sehirler.length) sonuc.push(sehirler[s++]);
});
return [...sonuc, ...sehirler.slice(s)];
} catch {
return [];
}
}