feat(veri): bileşenler yılsız Program'a ve veri-yillari sözlüğüne geçti (P-B b)

- SiraSerisi = { siraGecmisi, ilkYil? }; yardımcılar dizinin kendi son iki
  elemanıyla çalışır, yıl ilkYil + indeks. raporSiraSerisi diziyi doldurmaz:
  5 elemanlı eski raporlar 2024→2025 trendini göstermeye devam eder.
- Tablo başlıkları "Son ${VERI_YIL_SAYISI} yıl"; rapor listesi başlığı kaydın
  kendi pencere uzunluğunu söyler (eski rapor: 5).
- siraYiliEtiketi SON_YIL/ONCEKI_YIL'dan; bölüm/üniversite sayfa metinleri ve
  metadata SON_YIL, TERCIH_YILI, BASLIK_YILI sözlüğünden.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 17:00:39 +03:00
parent ce40b7c5b3
commit ceeb70375d
10 changed files with 127 additions and 89 deletions

View File

@@ -25,6 +25,12 @@ import { VeriKaynagi } from "./veri-kaynagi";
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
import { SiteFooter } from "@/components/site-footer";
import { Skeleton } from "@/components/ui/skeleton";
import {
BASLIK_YILI,
ONCEKI_YIL,
SON_YIL,
TERCIH_YILI,
} from "@/lib/veri-yillari";
const sayi = (n: number) => n.toLocaleString("tr-TR");
@@ -65,20 +71,20 @@ function bolumSayfaDilimi(bolum: BolumDetay, sayfa: number) {
let minSira: number | null = null;
let maxSira: number | null = null;
let verisiz = 0;
// Sırası 2025'te olmayıp 2024'ten gelen satırlar: aralık metni yılı dürüstçe
// söylesin (tablonun Sıralama sütunu bu satırlarda "—" gösterir).
let yalniz2024 = 0;
// Sırası son yılda olmayıp bir önceki yıldan gelen satırlar: aralık metni
// yılı dürüstçe söylesin (tablonun Sıralama sütunu bu satırlarda "—" gösterir).
let yalnizOnceki = 0;
for (const p of dilim) {
const efektif = p.sira2025 ?? p.sira2024;
const efektif = p.efektifSira;
if (efektif == null) {
verisiz += 1;
continue;
}
if (p.sira2025 == null) yalniz2024 += 1;
if (p.siraSon == null) yalnizOnceki += 1;
if (minSira == null || efektif < minSira) minSira = efektif;
if (maxSira == null || efektif > maxSira) maxSira = efektif;
}
return { tumProgramlar, dilim, minSira, maxSira, verisiz, yalniz2024 };
return { tumProgramlar, dilim, minSira, maxSira, verisiz, yalnizOnceki };
}
/** <title> için kısa sıra: 2.425.061 → "2,43 mn", 95.400 → "95 bin". */
@@ -99,13 +105,13 @@ function siraAraligiMetni({
minSira,
maxSira,
verisiz,
yalniz2024,
yalnizOnceki,
}: ReturnType<typeof bolumSayfaDilimi>): string | null {
if (dilim.length === 0) return null;
if (minSira == null || maxSira == null) {
return "taban verisi olmayan programlar";
}
const yil = siraYiliEtiketi(yalniz2024, dilim.length - verisiz);
const yil = siraYiliEtiketi(yalnizOnceki, dilim.length - verisiz);
const aralik =
minSira === maxSira
? `${yil} taban sırası ${sayi(minSira)} olan programlar`
@@ -134,8 +140,8 @@ export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata {
const meta = sayfaMetadata({
title: baslik,
description: aralik
? `${bolum.ad} taban puanları, sayfa ${sayfa}: ${aralik}. Toplam ${stats.toplam} programın son yerleştirme verileri (2025; 2025'te verisi olmayanlarda 2024) — taban puanı, başarı sıralaması ve kontenjan.`
: `${bolum.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${stats.toplam} programın 2025 yerleştirme verileri.`,
? `${bolum.ad} taban puanları, sayfa ${sayfa}: ${aralik}. Toplam ${stats.toplam} programın son yerleştirme verileri (${SON_YIL}; o yıl verisi olmayanlarda ${ONCEKI_YIL}) — taban puanı, başarı sıralaması ve kontenjan.`
: `${bolum.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${stats.toplam} programın ${SON_YIL} yerleştirme verileri.`,
path: `/bolum/${slug}/sayfa/${sayfa}`,
openGraph: {
images: ustSegmentOgGorseli(
@@ -151,8 +157,8 @@ export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata {
? ` En iyi taban sıralaması ${sayi(stats.minSira)}.`
: "";
return sayfaMetadata({
title: `${bolum.ad} Taban Puanları ve Başarı Sıralamaları 2026`,
description: `${bolum.ad} 2025 yerleştirme verileri: ${stats.toplam} programın taban puanı ve sıralaması, ${stats.devlet} devlet / ${stats.vakif} vakıf.${enIyi} 2026 tercih rehberi.`,
title: `${bolum.ad} Taban Puanları ve Başarı Sıralamaları ${BASLIK_YILI}`,
description: `${bolum.ad} ${SON_YIL} yerleştirme verileri: ${stats.toplam} programın taban puanı ve sıralaması, ${stats.devlet} devlet / ${stats.vakif} vakıf.${enIyi} ${TERCIH_YILI} tercih rehberi.`,
path: `/bolum/${slug}`,
});
}
@@ -230,7 +236,7 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
</h1>
{ilkSayfa ? (
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
{sayi(tumProgramlar.length)} üniversite programının 2025 taban
{sayi(tumProgramlar.length)} üniversite programının {SON_YIL} taban
puanlarını, başarı sıralamalarını ve kontenjanlarını incele.
</p>
) : (

View File

@@ -5,6 +5,7 @@ import Link from "next/link";
import { ChevronDown, ChevronUp } from "lucide-react";
import type { Program } from "@/types/yokatlas";
import { trBaslikDuzeni } from "@/lib/slug";
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
import { Sayfalama, SAYFA_BOYU } from "./sayfalama";
import {
ProgramSiraGecmisi,
@@ -24,14 +25,14 @@ import {
const sayi = (n: number) => n.toLocaleString("tr-TR");
type SiralamaAlani = "sira2025" | "puan2025" | "kontenjan2025";
type SiralamaAlani = "siraSon" | "puanSon" | "kontenjanSon";
type SiralamaYonu = "artan" | "azalan";
export type BolumProgrami = Program & { universiteSlug: string | null };
const ALAN_ETIKETLERI: Record<SiralamaAlani, string> = {
sira2025: "Sıralama",
puan2025: "Puan",
kontenjan2025: "Kontenjan",
siraSon: "Sıralama",
puanSon: "Puan",
kontenjanSon: "Kontenjan",
};
function SiralamaDugmesi({
@@ -157,11 +158,11 @@ export function BolumProgramListesi({
<TableRow>
<TableHead>Üniversite</TableHead>
<TableHead className="hidden text-right md:table-cell">
Son 5 yıl
Son {VERI_YIL_SAYISI} yıl
</TableHead>
<TableHead className="hidden text-right lg:table-cell">
<SiralamaDugmesi
alan="puan2025"
alan="puanSon"
aktifAlan={aktifAlan}
yon={yon}
onSirala={sirala}
@@ -169,7 +170,7 @@ export function BolumProgramListesi({
</TableHead>
<TableHead className="hidden text-right sm:table-cell">
<SiralamaDugmesi
alan="kontenjan2025"
alan="kontenjanSon"
aktifAlan={aktifAlan}
yon={yon}
onSirala={sirala}
@@ -177,7 +178,7 @@ export function BolumProgramListesi({
</TableHead>
<TableHead className="text-right">
<SiralamaDugmesi
alan="sira2025"
alan="siraSon"
aktifAlan={aktifAlan}
yon={yon}
onSirala={sirala}
@@ -243,17 +244,17 @@ export function BolumProgramListesi({
<ProgramSiraGecmisi program={p} />
</TableCell>
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 lg:table-cell">
{p.puan2025 != null
? p.puan2025.toFixed(2).replace(".", ",")
{p.puanSon != null
? p.puanSon.toFixed(2).replace(".", ",")
: "—"}
</TableCell>
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell">
{p.kontenjan2025 != null ? `${sayi(p.kontenjan2025)} kişi` : "—"}
{p.kontenjanSon != null ? `${sayi(p.kontenjanSon)} kişi` : "—"}
</TableCell>
<TableCell className="text-right text-xs tabular-nums text-slate-500">
<span className="inline-flex items-center gap-1">
<ProgramTabanTrendi program={p} />
{p.sira2025 != null ? `~${sayi(p.sira2025)}.` : "—"}
{p.siraSon != null ? `~${sayi(p.siraSon)}.` : "—"}
</span>
</TableCell>
<TableCell className="pr-3 text-right">

View File

@@ -1,6 +1,7 @@
import type { ReactNode } from "react";
import Link from "next/link";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { ONCEKI_YIL, SON_YIL } from "@/lib/veri-yillari";
/** Programatik tablo sayfaları için satır dilimi boyutu. */
export const SAYFA_BOYU = 50;
@@ -13,10 +14,19 @@ export function sayfaDilimi<T>(liste: readonly T[], sayfa: number): T[] {
return liste.slice((sayfa - 1) * SAYFA_BOYU, sayfa * SAYFA_BOYU);
}
/** Aralıktaki sıraların hangi yıldan geldiği: hepsi 2025 / hepsi 2024 / karışık. */
export function siraYiliEtiketi(yalniz2024: number, siraliSayi: number): string {
if (yalniz2024 === 0) return "2025";
return yalniz2024 === siraliSayi ? "2024" : "2024–2025";
/**
* Aralıktaki sıraların hangi yıldan geldiği: hepsi son yıl / hepsi bir önceki
* yıl / karışık. `yalnizOnceki`: sırası son yılda olmayıp bir önceki yıldan
* gelen satır sayısı; `siraliSayi`: sırası olan toplam satır.
*/
export function siraYiliEtiketi(
yalnizOnceki: number,
siraliSayi: number,
): string {
if (yalnizOnceki === 0) return String(SON_YIL);
return yalnizOnceki === siraliSayi
? String(ONCEKI_YIL)
: `${ONCEKI_YIL}–${SON_YIL}`;
}
/**

View File

@@ -34,6 +34,7 @@ import { UniLogo } from "@/components/uni-logo";
import { uniLogoYolu } from "@/lib/uni-logolar";
import { Skeleton } from "@/components/ui/skeleton";
import type { Program } from "@/types/yokatlas";
import { BASLIK_YILI, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
const sayi = (n: number) => n.toLocaleString("tr-TR");
@@ -91,13 +92,13 @@ function fakulteOzeti({
if (adlar.length === 0) return null;
if (adlar.length === 1) {
const siralar = dilim
.map((p) => p.sira2025 ?? p.sira2024)
.map((p) => p.efektifSira)
.filter((n): n is number => n != null);
if (siralar.length === 0) return adlar[0];
const min = Math.min(...siralar);
const max = Math.max(...siralar);
const yil = siraYiliEtiketi(
dilim.filter((p) => p.sira2025 == null && p.sira2024 != null).length,
dilim.filter((p) => p.siraSon == null && p.efektifSira != null).length,
siralar.length,
);
return min === max
@@ -116,10 +117,10 @@ export function uniSayfaMetadata(slug: string, sayfa: number): Metadata {
// farklılaştırıcı (o sayfadaki birimler) açıklamaya ve gövdeye gider.
const birimler = fakulteOzeti(uniSayfaDilimi(uni, sayfa));
return sayfaMetadata({
title: `${uni.ad} Taban Puanları 2026 (Sayfa ${sayfa})`,
title: `${uni.ad} Taban Puanları ${BASLIK_YILI} (Sayfa ${sayfa})`,
description: birimler
? `${uni.ad} taban puanları, sayfa ${sayfa}: ${birimler}. Toplam ${uni.programSayisi} programın 2025 yerleştirme verileri — taban puanı, başarı sıralaması ve kontenjan.`
: `${uni.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${uni.programSayisi} programın 2025 yerleştirme verileri.`,
? `${uni.ad} taban puanları, sayfa ${sayfa}: ${birimler}. Toplam ${uni.programSayisi} programın ${SON_YIL} yerleştirme verileri — taban puanı, başarı sıralaması ve kontenjan.`
: `${uni.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${uni.programSayisi} programın ${SON_YIL} yerleştirme verileri.`,
path: `/universite/${slug}/sayfa/${sayfa}`,
openGraph: {
images: ustSegmentOgGorseli(
@@ -130,8 +131,8 @@ export function uniSayfaMetadata(slug: string, sayfa: number): Metadata {
});
}
return sayfaMetadata({
title: `${uni.ad} Taban Puanları ve Başarı Sıralamaları 2026`,
description: `${uni.ad} 2025 yerleştirme verileri: ${uni.programSayisi} programın taban puanları, başarı sıralamaları ve kontenjanları${uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}. 2026 tercih rehberi.`,
title: `${uni.ad} Taban Puanları ve Başarı Sıralamaları ${BASLIK_YILI}`,
description: `${uni.ad} ${SON_YIL} yerleştirme verileri: ${uni.programSayisi} programın taban puanları, başarı sıralamaları ve kontenjanları${uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}. ${TERCIH_YILI} tercih rehberi.`,
path: `/universite/${slug}`,
});
}
@@ -241,7 +242,7 @@ export function UniversiteIcerik({
<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
{uni.ad} Taban Puanları {BASLIK_YILI}
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
</h1>
</div>
@@ -250,7 +251,7 @@ export function UniversiteIcerik({
<>
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
{uni.ad}
{uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}, 2025 YKS
{uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}, {SON_YIL} YKS
yerleştirme sonuçlarına göre{" "}
<strong>{sayi(uni.programSayisi)} programda</strong> öğrenci aldı
{uni.fakulteSayisi > 1
@@ -267,10 +268,11 @@ export function UniversiteIcerik({
{uni.stats.toplamKontenjan > 0 ? (
<> Toplam kontenjan {sayi(uni.stats.toplamKontenjan)}.</>
) : null}{" "}
Aşağıdaki tablolar 2026 tercihleri için rehber niteliğindedir;{" "}
Aşağıdaki tablolar {TERCIH_YILI} tercihleri için rehber
niteliğindedir;{" "}
<em>
2026 taban puanları ve sıralamaları, yerleştirme sonuçları
açıklandığında netleşir.
{TERCIH_YILI} taban puanları ve sıralamaları, yerleştirme
sonuçları açıklandığında netleşir.
</em>
</p>

View File

@@ -2,6 +2,7 @@
import Link from "next/link";
import type { Program } from "@/types/yokatlas";
import { SON_YIL, VERI_YIL_SAYISI } from "@/lib/veri-yillari";
import {
ProgramSiraGecmisi,
ProgramTabanTrendi,
@@ -48,10 +49,10 @@ export function UniversiteProgramTablosu({
<TableRow>
<TableHead>Program</TableHead>
<TableHead className="hidden text-right md:table-cell">
Son 5 yıl
Son {VERI_YIL_SAYISI} yıl
</TableHead>
<TableHead className="hidden text-right lg:table-cell">
2025 puan
{SON_YIL} puan
</TableHead>
<TableHead className="hidden text-right sm:table-cell">
Kontenjan
@@ -93,13 +94,13 @@ export function UniversiteProgramTablosu({
<ProgramSiraGecmisi program={p} />
</TableCell>
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 lg:table-cell">
{p.puan2025 != null
? p.puan2025.toFixed(2).replace(".", ",")
{p.puanSon != null
? p.puanSon.toFixed(2).replace(".", ",")
: "—"}
</TableCell>
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell">
{p.kontenjan2025 != null
? `${sayi(p.kontenjan2025)} kişi`
{p.kontenjanSon != null
? `${sayi(p.kontenjanSon)} kişi`
: "—"}
</TableCell>
<TableCell className="text-right text-xs tabular-nums text-slate-500">

View File

@@ -24,8 +24,7 @@ type ProgramOzet = Pick<
| "il"
| "unitur"
| "tur"
| "sira2025"
| "sira2024"
| "efektifSira"
>;
const PROFILSIZ_EKLEME_TOAST_ID = "profilsiz-ekleme";

View File

@@ -22,6 +22,7 @@ import type {
} from "@/types/yokatlas";
import { DILIM_ETIKET, riskHesapla, type RiskSeviyesi } from "@/lib/risk";
import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug";
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
import {
ProgramSiraGecmisi,
ProgramTabanTrendi,
@@ -239,7 +240,7 @@ export function ProgramTablosu({
<TableHead className="w-8" />
<TableHead>Program</TableHead>
<TableHead className="hidden text-right md:table-cell">
Son 5 yıl
Son {VERI_YIL_SAYISI} yıl
</TableHead>
<TableHead className="hidden text-right sm:table-cell">
Kontenjan
@@ -357,19 +358,19 @@ function ProgramSatiri({
<TableCell
className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell"
title={
p.kontenjan2025 != null && p.yerlesen2025 != null
? `${p.kontenjan2025} kontenjan, ${p.yerlesen2025} yerleşen`
p.kontenjanSon != null && p.yerlesenSon != null
? `${p.kontenjanSon} kontenjan, ${p.yerlesenSon} yerleşen`
: undefined
}
>
{p.kontenjan2025 != null ? (
{p.kontenjanSon != null ? (
<span className="inline-flex items-center gap-1.5">
{p.yerlesen2025 != null && p.yerlesen2025 < p.kontenjan2025 ? (
{p.yerlesenSon != null && p.yerlesenSon < p.kontenjanSon ? (
<span className="rounded bg-amber-50 px-1 py-px text-[10px] font-medium leading-4 text-amber-600">
boş kaldı
</span>
) : null}
{p.kontenjan2025.toLocaleString("tr-TR")} kişi
{p.kontenjanSon.toLocaleString("tr-TR")} kişi
</span>
) : (
"—"

View File

@@ -2,7 +2,7 @@
// 24'lük listenin gövdesi. Görsel dil sonuç sayfasındaki ham program
// tablosuyla birebir aynıdır (bkz. program-tablosu.tsx): aynı beyaz kart,
// aynı satır düzeni, aynı "Son 5 yıl" / "Taban sıra" sütunları. Buradaki fark
// aynı satır düzeni, aynı "Son N yıl" / "Taban sıra" sütunları. Buradaki fark
// içerik: satır sırası tercih numarası, risk sütunu Yapay Zeka'dan gelir ve
// satır açıldığında gerekçe/risk notu görünür. Program adı ve tabanlar herkese
// açık; Yapay Zeka katmanı kilitliBaslangic sonrasında ücretlidir (metin
@@ -27,6 +27,7 @@ import {
TableRow,
} from "@/components/ui/table";
import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug";
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
import type { RaporSonuc } from "@/features/rapor/types/rapor";
import {
DILIM_ETIKET,
@@ -47,7 +48,7 @@ const RISK_STIL: Record<RiskSeviyesi, { nokta: string; icon: typeof Rocket }> =
guvenli: { nokta: "bg-emerald-500", icon: ShieldCheck },
};
// Sütun sayısı: # · Program · Son 5 yıl · Risk · Taban sıra · detay oku
// Sütun sayısı: # · Program · Son N yıl · Risk · Taban sıra · detay oku
const SUTUN_SAYISI = 6;
/**
@@ -161,6 +162,19 @@ export function RaporListesi({
return iller.size === 1 ? [...iller][0] : null;
}, [rapor]);
// Sütun başlığı kaydın KENDİ penceresini söyler: eski raporların sıra
// geçmişi üretildiği günün yıl sayısını taşır (dizi doldurulmaz), başlık
// çizgide olmayan bir yılı vaat etmesin. Geçmişi hiç olmayan kayıtta
// bugünkü pencereye düşer.
const yilSayisi = useMemo(() => {
let enUzun = 0;
for (const t of rapor.tercihler) {
const n = rapor.programlar[t.programId]?.siraGecmisi?.length ?? 0;
if (n > enUzun) enUzun = n;
}
return enUzun > 0 ? enUzun : VERI_YIL_SAYISI;
}, [rapor]);
const satir = (t: RaporSonuc["tercihler"][number], index: number) => {
const p = rapor.programlar[t.programId];
const analizKilitli = index >= kilitIdx;
@@ -168,7 +182,7 @@ export function RaporListesi({
(adaySira != null ? riskHesapla(p?.efektifSira, adaySira) : null) ??
dilimdenRisk(t.dilim);
const stil = RISK_STIL[risk];
const seri = raporSiraSerisi(p?.siraGecmisi);
const seri = raporSiraSerisi(p?.siraGecmisi, p?.ilkYil);
const taban = p?.efektifSira ?? programEtkinSira(seri);
const acikMi = acik.has(t.sira);
const devlet = p?.unitur === "DEVLET";
@@ -366,7 +380,7 @@ export function RaporListesi({
<TableHead className="w-8 pl-4 pr-0">#</TableHead>
<TableHead>Program</TableHead>
<TableHead className="hidden text-right md:table-cell">
Son 5 yıl
Son {yilSayisi} yıl
</TableHead>
<TableHead>
<span className="hidden sm:inline">Yapay Zeka </span>Risk

View File

@@ -105,7 +105,7 @@ export function TadimlikSatiri({
const p = satir.program;
const risk = riskHesapla(p.efektifSira, sira) ?? dilimdenRisk(satir.dilim);
const seri = raporSiraSerisi(p.siraGecmisi);
const seri = raporSiraSerisi(p.siraGecmisi, p.ilkYil);
const taban = p.efektifSira ?? programEtkinSira(seri);
const devlet = p.unitur === "DEVLET";
const girisUrl = `/giris?callback=${encodeURIComponent(sonucHref())}`;