"use client"; // Program satırındaki "ayrıntı" düğmesi: programın tüm açık verisini alttan // açılan sayfada gösterir (DESIGN.md §4.10 çekmece, §4.19 program ayrıntı // çekmecesi). Veri /api/program-ayrinti'den gelir: künye (dil, öğretim türü, // burs/ücret, akreditasyon, baraj, kadro, kampüs, cinsiyet koşulu), yıllara // göre sıra/puan/kontenjan/yerleşen, kontenjan türleri, son yerleşenin // netleri, yerleşme koşulları, TÜİK mezun istihdamı, üniversite özeti. // Kadın-erkek dağılımı program düzeyinde bilinçli olarak yok: YÖK Atlas 2026 // SPA'sına geçerken cinsiyet panelini yayından kaldırdı (bkz. scripts/detay.ts). // Çekmece içerik ağırlıklı olduğu için liste çekmecesi gibi kaydırılır (§4.10 // tek ekran kuralının ikinci istisnası). import { SATIR_BASLIK, SATIR_DUGME_BOYU } from "@/components/liste-satiri"; import { useState } from "react"; import { BadgeCheck, Clock, Flag, GraduationCap, Info, Languages, MapPin, Monitor, Users, Wallet, X, } from "lucide-react"; import type { ProgramAyrintisi } from "@/app/api/program-ayrinti/route"; import type { KontenjanTuruAnahtari, ProgramNetSatiri } from "@/types/yokatlas"; import { olay } from "@/lib/analitik"; // Yalnız saf modüller: bu bileşen istemcide çalışır, db'ye dokunan // program-ozellik.ts import EDİLMEZ (kadro toplamı API'den gelir) import { CINSIYET_KOSULU_METNI, KONTENJAN_TURU_ADI } from "@/lib/ozel-durum"; import { trBaslikDuzeni } from "@/lib/slug"; import { UNITUR_ETIKET } from "@/lib/unitur"; import { cn } from "@/lib/utils"; import { SON_YIL } from "@/lib/veri-yillari"; import { AltSayfa, AltSayfaKapat, AltSayfaGovde, AltSayfaIcerik, AltSayfaAciklama, AltSayfaBaslik, AltSayfaTetik, } from "@/components/ui/alt-sayfa"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; // Ders sırası ÖSYM oturum sırasıyla; max = testteki soru sayısı. const DERSLER: { alan: keyof Omit; etiket: string; max: number; }[] = [ { alan: "tytTrkNet", etiket: "TYT Türkçe", max: 40 }, { alan: "tytSosNet", etiket: "TYT Sosyal Bilimler", max: 20 }, { alan: "tytMatNet", etiket: "TYT Matematik", max: 40 }, { alan: "tytFenNet", etiket: "TYT Fen Bilimleri", max: 20 }, { alan: "aytMatNet", etiket: "AYT Matematik", max: 40 }, { alan: "aytFizNet", etiket: "AYT Fizik", max: 14 }, { alan: "aytKimNet", etiket: "AYT Kimya", max: 13 }, { alan: "aytBioNet", etiket: "AYT Biyoloji", max: 13 }, { alan: "aytTdeNet", etiket: "AYT Türk Dili ve Edebiyatı", max: 24 }, { alan: "aytTrh1Net", etiket: "AYT Tarih-1", max: 10 }, { alan: "aytCog1Net", etiket: "AYT Coğrafya-1", max: 6 }, { alan: "aytTrh2Net", etiket: "AYT Tarih-2", max: 11 }, { alan: "aytCog2Net", etiket: "AYT Coğrafya-2", max: 11 }, { alan: "aytDinNet", etiket: "AYT Din Kültürü", max: 6 }, { alan: "aytFelNet", etiket: "AYT Felsefe Grubu", max: 12 }, { alan: "ydtYdilNet", etiket: "YDT Yabancı Dil", max: 80 }, ]; /** Meslek lisesi alan içi ek puanla yerleşmeyi gösteren OBP katsayısı. */ const EK_PUAN_KATSAYISI = 0.18; const KONTENJAN_TURU_SIRASI: KontenjanTuruAnahtari[] = [ "genel", "okulBirincisi", "sehitGaziYakini", "depremzede", "kadin34", ]; const sayi = (n: number) => n.toLocaleString("tr-TR"); const ondalik = (n: number) => n.toLocaleString("tr-TR", { maximumFractionDigits: 2 }); /** Satır kimlik hapı (DESIGN.md §4.12 ölçüsü, link değil). */ const HAP = "inline-flex h-10 items-center gap-2 rounded-full border border-slate-200 bg-card px-4 text-sm font-semibold text-slate-800"; const HAP_IKON = "size-4 shrink-0 text-primary"; const BOLUM_BASLIK = "mt-7 font-heading text-base font-bold text-slate-900"; const NOT = "mt-2 text-sm leading-relaxed text-slate-500"; type Durum = "bekliyor" | "yukleniyor" | "hazir" | "hata"; export function ProgramAyrintiButonu({ programId, isim, universite, className = "", varyant = "ikon", }: { programId: string; isim: string; universite?: string; className?: string; /** ikon: satır düğmesi (yuvarlak); metin: açık satır içindeki metin düğmesi ("Haritada gör" ile aynı kalıp) */ varyant?: "ikon" | "metin"; }) { const [durum, setDurum] = useState("bekliyor"); const [veri, setVeri] = useState(null); async function yukle() { setDurum("yukleniyor"); try { const res = await fetch(`/api/program-ayrinti?id=${programId}`); if (!res.ok) throw new Error(String(res.status)); setVeri((await res.json()) as ProgramAyrintisi); setDurum("hazir"); } catch { setDurum("hata"); } } function acildi(acik: boolean) { if (!acik) return; olay("program_ayrinti_goruntulendi"); if (durum === "bekliyor" || durum === "hata") yukle(); } return ( {varyant === "metin" ? ( ) : ( )} {/* Alt sayfa kabı tek kaynak (alt-sayfa.tsx), çarpı §4.3. İçerik uzun: sayfa ekranın %60'ında açılır (başlık + künye), yukarı çekince ya da kaydırınca tam boya çıkar (§4.19). */} e.stopPropagation()} > Kapat {isim} {veri ? [ trBaslikDuzeni(veri.program.universite), veri.program.il ? trBaslikDuzeni(veri.program.il) : null, veri.program.fakulte, ] .filter(Boolean) .join(" · ") : (universite ?? "Programın tüm açık verisi")}
); } function AyrintiIcerik({ durum, veri }: { durum: Durum; veri: ProgramAyrintisi | null }) { if (durum === "yukleniyor" || durum === "bekliyor" || (durum === "hazir" && !veri)) { return (
{[0, 1, 2, 3, 4, 5].map((i) => (
))}
); } if (durum === "hata" || !veri) { return (

Ayrıntı yüklenemedi. Paneli kapatıp yeniden açmayı dene.

); } return ( <>

Son yerleşenin netleri

Bölüme en düşük puanla yerleşen adayın sınav netleri (YÖK Atlas).

Kaynaklar: YÖK Atlas (yerleştirme, kontenjan, netler, koşullar; {SON_YIL}{" "} yerleştirmesi), TÜİK (mezun istihdamı), URAP ve YÖK İstatistik (üniversite).

); } function Kunye({ veri }: { veri: ProgramAyrintisi }) { const o = veri.ozellik; const haplar: { ikon: typeof Languages; metin: string }[] = []; const tur = veri.program.unitur ? UNITUR_ETIKET[veri.program.unitur]?.cip : null; if (veri.program.sure) { haplar.push({ ikon: Clock, metin: `${veri.program.sure} yıl${tur ? ` · ${tur}` : ""}`, }); } if (o?.ogrenimDili) haplar.push({ ikon: Languages, metin: o.ogrenimDili }); if (o?.ogrenimTuru && o.ogrenimTuru !== "Örgün Öğretim") { haplar.push({ ikon: Monitor, metin: o.ogrenimTuru }); } if (o?.bursOrani) { const ucret = o.ucret != null && o.bursOrani !== "Burslu" ? ` · yıllık ${sayi(o.ucret)} TL` : ""; haplar.push({ ikon: Wallet, metin: `${o.bursOrani}${ucret}` }); } if (o?.akreditasyon) haplar.push({ ikon: BadgeCheck, metin: `${o.akreditasyon} akreditasyonlu` }); if (o?.minBasariSirasi != null) { haplar.push({ ikon: Flag, metin: `Baraj ${sayi(o.minBasariSirasi)}. sıra` }); } const uye = veri.ogretimUyesiToplam; if (uye != null) haplar.push({ ikon: GraduationCap, metin: `${sayi(uye)} öğretim üyesi` }); if (o?.cinsiyetKosulu) { const m = CINSIYET_KOSULU_METNI[o.cinsiyetKosulu]; haplar.push({ ikon: Users, metin: m.charAt(0).toLocaleUpperCase("tr-TR") + m.slice(1) }); } if (o?.ilce) haplar.push({ ikon: MapPin, metin: `${trBaslikDuzeni(o.ilce)} kampüsü` }); if (haplar.length === 0) return null; return (
    {haplar.map((h) => (
  • {h.metin}
  • ))}
); } function YillaraGore({ veri }: { veri: ProgramAyrintisi }) { const satirlar = veri.gecmis.filter( (g) => g.sira != null || g.puan != null || g.kontenjan != null || g.yerlesen != null, ); if (satirlar.length === 0) return null; const sonYil = satirlar[0].yil; const hucre = (yil: number) => cn( "py-2.5 text-right text-sm tabular-nums", yil === sonYil ? "font-semibold text-slate-800" : "text-slate-500", ); return ( <>

Yıllara göre

Taban sırası (ve geniş ekranda puanı) o yıl yerleşen son adayınki; kontenjan ve yerleşen o yılın yerleştirmesi. Sırası boş yıl: program o yıl dolmadı ya da yoktu.

Yıl Taban sıra {/* Telefonda beş sütun 375 px'e sığmıyor (yatay kaydırma yok, §5.2.3): puan sm'den itibaren */} Puan Kontenjan Yerleşen {satirlar.map((g) => ( {g.yil} {g.sira != null ? sayi(g.sira) : "—"} {g.puan != null ? ondalik(g.puan) : "—"} {g.kontenjan != null ? sayi(g.kontenjan) : "—"} {g.yerlesen != null ? sayi(g.yerlesen) : "—"} ))}
); } function KontenjanTurleriBolumu({ veri }: { veri: ProgramAyrintisi }) { const k = veri.kontenjanTurleri; if (!k) return null; const satirlar = KONTENJAN_TURU_SIRASI.filter((t) => (k[t].kontenjan ?? 0) > 0); if (satirlar.length === 0) return null; const ozelVar = satirlar.some((t) => t !== "genel"); return ( <>

Kontenjan türleri ({k.yil})

{ozelVar ? "ÖSYM okul birincileri, şehit ve gazi yakınları, depremzedeler ve 34 yaş üstü kadınlar için ayrı kontenjan ayırır; bu kontenjanlar yalnız o durumdaki adaylara açıktır ve her birinin taban sırası ayrı yayımlanır — o sıra burada yok." : "Bu programda yalnız genel kontenjan var."}

{satirlar.map((t) => (
{KONTENJAN_TURU_ADI[t]}
{sayi(k[t].kontenjan ?? 0)} {" "} kontenjan · {k[t].yerlesen != null ? sayi(k[t].yerlesen) : "—"} yerleşen
))}
); } function NetlerIcerik({ netler }: { netler: ProgramNetSatiri[] }) { if (netler.length === 0) { return (

Bu program için YÖK Atlas'ta net verisi yayınlanmamış.

); } const dersler = DERSLER.filter((d) => netler.some((y) => y[d.alan] != null)); // En yeni yıl vurgulu (§1.1 "sık bakılan önde"); eski yıllar bağlam. const sonYil = Math.max(...netler.map((y) => y.yil)); const ekPuanli = netler.some((y) => y.katsayi != null && y.katsayi >= EK_PUAN_KATSAYISI); const hucre = (yil: number, vurgu = false) => cn( "py-2.5 text-right text-sm tabular-nums", yil === sonYil ? vurgu ? "font-semibold text-slate-900" : "font-semibold text-slate-800" : "text-slate-500", ); return ( <> Ders {netler.map((y) => ( {y.yil} ))} {dersler.map((d) => ( {/* Ders adı sarılır: 375 px'te üç yıl sütunu + "AYT Türk Dili ve Edebiyatı / 24" tek satırda 343 px'lik kaba sığmıyor, tablo yatay kayıyordu (6 Eki 2026 QA). Sayı hücreleri nowrap kalır. */} {d.etiket} / {d.max} {netler.map((y) => { const deger = y[d.alan]; return ( {deger != null ? ondalik(deger) : "—"} ); })} ))} OBP {netler.map((y) => ( {y.obp != null ? ondalik(y.obp) : "—"} ))} OBP katsayısı {netler.map((y) => ( {y.katsayi != null ? ondalik(y.katsayi) : "—"} ))} Yerleşme puanı {netler.map((y) => ( {y.tabanPuan != null ? ondalik(y.tabanPuan) : "—"} ))}
{ekPuanli ? (

OBP katsayısı {ondalik(EK_PUAN_KATSAYISI)} olan yılda son yerleşen, meslek lisesi alan içi ek puanıyla yerleşmiş; standart katsayı 0,12.

) : null} ); } function Kosullar({ veri }: { veri: ProgramAyrintisi }) { if (veri.kosullar.length === 0) return null; return (
Yerleşme koşulları ({veri.kosullar.length}) Göster Gizle
    {veri.kosullar.map((k) => (
  1. {k.kod} {k.metin}
  2. ))}

Koşul metinleri ÖSYM tercih kılavuzundan; tercih göndermeden önce güncel kılavuzdan doğrula.

); } function Istihdam({ veri }: { veri: ProgramAyrintisi }) { const v = veri.istihdam; if (!v) return null; const parcalar = [ v.istihdam != null ? `mezunların %${ondalik(v.istihdam)}'i kayıtlı çalışıyor` : null, v.isBulmaAy != null ? `ilk iş ortalama ${ondalik(v.isBulmaAy)} ayda` : null, v.kazanc ? `kazanç grubu ${v.kazanc.toLocaleLowerCase("tr-TR")}` : null, ].filter(Boolean); if (parcalar.length === 0) return null; const ort = v.ortalama.istihdam != null ? ` Türkiye ortalaması %${ondalik(v.ortalama.istihdam)}.` : ""; return ( <>

Mezun istihdamı

{parcalar.join(", ")} (TÜİK {v.yil}, ülke geneli, {v.duzey === "lisans" ? "lisans" : "ön lisans"}{" "} mezunları; üniversiteye göre değişir, kırılım yok).{ort}

); } function UniversiteOzeti({ veri }: { veri: ProgramAyrintisi }) { const u = veri.universite; if (!u || (!u.urap && !u.istatistik)) return null; const parcalar = [ u.urap ? `URAP araştırma sıralaması ${u.urap.sira}/${u.urap.toplam} (${u.urap.donem})` : null, u.istatistik ? `öğretim üyesi başına ${sayi(u.istatistik.hocaBasinaOgrenci)} öğrenci (sayfası olan üniversitelerin medyanı ${sayi(u.istatistik.medyan)}; YÖK İstatistik ${u.istatistik.donem})` : null, ].filter(Boolean); return ( <>

Üniversite

{parcalar.join(" · ")}.{" "} Üniversite sayfası

); }