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>
546 lines
21 KiB
TypeScript
546 lines
21 KiB
TypeScript
"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<ProgramNetSatiri, "yil" | "puanTuru" | "tabanPuan" | "obp" | "katsayi">;
|
||
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<Durum>("bekliyor");
|
||
const [veri, setVeri] = useState<ProgramAyrintisi | null>(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 (
|
||
<AltSayfa onOpenChange={acildi}>
|
||
<AltSayfaTetik asChild>
|
||
{varyant === "metin" ? (
|
||
<button
|
||
type="button"
|
||
onClick={(e) => e.stopPropagation()}
|
||
className={`inline-flex min-h-11 cursor-pointer items-center gap-1.5 text-sm font-semibold text-primary transition-colors duration-150 hover:text-orange-500 ${className}`}
|
||
>
|
||
<Info className="size-4" aria-hidden />
|
||
Program ayrıntısı
|
||
<span className="sr-only"> — {isim}</span>
|
||
</button>
|
||
) : (
|
||
<button
|
||
type="button"
|
||
title="Program ayrıntısı"
|
||
onClick={(e) => e.stopPropagation()}
|
||
className={`inline-flex ${SATIR_DUGME_BOYU} cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-card text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.92] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${className}`}
|
||
>
|
||
<Info className="size-4" aria-hidden />
|
||
<span className="sr-only">{isim} — program ayrıntısı</span>
|
||
</button>
|
||
)}
|
||
</AltSayfaTetik>
|
||
{/* 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). */}
|
||
<AltSayfaIcerik
|
||
duraklar={[0.6, 0.92]}
|
||
ilkDurak={0}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<AltSayfaKapat className="absolute top-3 right-3 z-10 flex size-9 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-popover text-slate-500 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96]">
|
||
<X className="size-4" aria-hidden />
|
||
<span className="sr-only">Kapat</span>
|
||
</AltSayfaKapat>
|
||
<AltSayfaGovde className="px-4 pt-3 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:px-6">
|
||
<AltSayfaBaslik className="px-10 text-center font-heading text-2xl font-bold text-slate-900">
|
||
{isim}
|
||
</AltSayfaBaslik>
|
||
<AltSayfaAciklama className="mx-auto mt-1 max-w-md px-4 text-center text-sm text-slate-500">
|
||
{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")}
|
||
</AltSayfaAciklama>
|
||
<div className="mt-5">
|
||
<AyrintiIcerik durum={durum} veri={veri} />
|
||
</div>
|
||
</AltSayfaGovde>
|
||
</AltSayfaIcerik>
|
||
</AltSayfa>
|
||
);
|
||
}
|
||
|
||
function AyrintiIcerik({ durum, veri }: { durum: Durum; veri: ProgramAyrintisi | null }) {
|
||
if (durum === "yukleniyor" || durum === "bekliyor" || (durum === "hazir" && !veri)) {
|
||
return (
|
||
<div className="space-y-2" aria-hidden>
|
||
{[0, 1, 2, 3, 4, 5].map((i) => (
|
||
<div key={i} className="h-10 animate-pulse rounded-lg bg-slate-100" />
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
if (durum === "hata" || !veri) {
|
||
return (
|
||
<p className="py-4 text-center text-base text-slate-500">
|
||
Ayrıntı yüklenemedi. Paneli kapatıp yeniden açmayı dene.
|
||
</p>
|
||
);
|
||
}
|
||
return (
|
||
<>
|
||
<Kunye veri={veri} />
|
||
<YillaraGore veri={veri} />
|
||
<KontenjanTurleriBolumu veri={veri} />
|
||
<h3 className={BOLUM_BASLIK}>Son yerleşenin netleri</h3>
|
||
<p className={NOT}>
|
||
Bölüme en düşük puanla yerleşen adayın sınav netleri (YÖK Atlas).
|
||
</p>
|
||
<div className="mt-3">
|
||
<NetlerIcerik netler={veri.netler} />
|
||
</div>
|
||
<Kosullar veri={veri} />
|
||
<Istihdam veri={veri} />
|
||
<UniversiteOzeti veri={veri} />
|
||
<p className="mt-7 border-t border-slate-100 pt-3 text-xs leading-5 text-slate-500">
|
||
Kaynaklar: YÖK Atlas (yerleştirme, kontenjan, netler, koşullar; {SON_YIL}{" "}
|
||
yerleştirmesi), TÜİK (mezun istihdamı), URAP ve YÖK İstatistik (üniversite).
|
||
</p>
|
||
</>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<ul className="flex flex-wrap justify-center gap-2">
|
||
{haplar.map((h) => (
|
||
<li key={h.metin} className={HAP}>
|
||
<h.ikon className={HAP_IKON} aria-hidden />
|
||
{h.metin}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<>
|
||
<h3 className={BOLUM_BASLIK}>Yıllara göre</h3>
|
||
<p className={NOT}>
|
||
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.
|
||
</p>
|
||
<Table className="mt-3">
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead className={cn(SATIR_BASLIK, "pl-0")}>Yıl</TableHead>
|
||
<TableHead className={cn(SATIR_BASLIK, "text-right")}>Taban sıra</TableHead>
|
||
{/* Telefonda beş sütun 375 px'e sığmıyor (yatay kaydırma yok, §5.2.3): puan sm'den itibaren */}
|
||
<TableHead className={cn(SATIR_BASLIK, "hidden text-right sm:table-cell")}>Puan</TableHead>
|
||
<TableHead className={cn(SATIR_BASLIK, "text-right")}>Kontenjan</TableHead>
|
||
<TableHead className={cn(SATIR_BASLIK, "text-right")}>Yerleşen</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{satirlar.map((g) => (
|
||
<TableRow key={g.yil}>
|
||
<TableCell
|
||
className={cn(
|
||
"py-2.5 pl-0 text-sm tabular-nums",
|
||
g.yil === sonYil ? "font-semibold text-slate-900" : "text-slate-700",
|
||
)}
|
||
>
|
||
{g.yil}
|
||
</TableCell>
|
||
<TableCell className={hucre(g.yil)}>{g.sira != null ? sayi(g.sira) : "—"}</TableCell>
|
||
<TableCell className={cn(hucre(g.yil), "hidden sm:table-cell")}>{g.puan != null ? ondalik(g.puan) : "—"}</TableCell>
|
||
<TableCell className={hucre(g.yil)}>{g.kontenjan != null ? sayi(g.kontenjan) : "—"}</TableCell>
|
||
<TableCell className={hucre(g.yil)}>{g.yerlesen != null ? sayi(g.yerlesen) : "—"}</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<>
|
||
<h3 className={BOLUM_BASLIK}>Kontenjan türleri ({k.yil})</h3>
|
||
<p className={NOT}>
|
||
{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."}
|
||
</p>
|
||
<dl className="mt-3 grid gap-x-6 gap-y-2 sm:grid-cols-2">
|
||
{satirlar.map((t) => (
|
||
<div key={t} className="flex items-baseline justify-between gap-3 border-b border-slate-100 py-2">
|
||
<dt className="text-sm text-slate-700">{KONTENJAN_TURU_ADI[t]}</dt>
|
||
<dd className="text-right text-sm tabular-nums text-slate-900">
|
||
<span className="font-semibold">{sayi(k[t].kontenjan ?? 0)}</span>
|
||
<span className="text-slate-500">
|
||
{" "}
|
||
kontenjan · {k[t].yerlesen != null ? sayi(k[t].yerlesen) : "—"} yerleşen
|
||
</span>
|
||
</dd>
|
||
</div>
|
||
))}
|
||
</dl>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function NetlerIcerik({ netler }: { netler: ProgramNetSatiri[] }) {
|
||
if (netler.length === 0) {
|
||
return (
|
||
<p className="py-2 text-base text-slate-500">
|
||
Bu program için YÖK Atlas'ta net verisi yayınlanmamış.
|
||
</p>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<>
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead className={cn(SATIR_BASLIK, "pl-0")}>Ders</TableHead>
|
||
{netler.map((y) => (
|
||
<TableHead
|
||
key={y.yil}
|
||
className={cn(
|
||
SATIR_BASLIK,
|
||
"text-right tabular-nums",
|
||
y.yil === sonYil && "text-slate-900",
|
||
)}
|
||
>
|
||
{y.yil}
|
||
</TableHead>
|
||
))}
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{dersler.map((d) => (
|
||
<TableRow key={d.alan}>
|
||
{/* 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. */}
|
||
<TableCell className="py-2.5 pl-0 pr-3 text-sm leading-snug whitespace-normal text-slate-700">
|
||
{d.etiket}
|
||
<span className="text-slate-400"> / {d.max}</span>
|
||
</TableCell>
|
||
{netler.map((y) => {
|
||
const deger = y[d.alan];
|
||
return (
|
||
<TableCell key={y.yil} className={hucre(y.yil)}>
|
||
{deger != null ? ondalik(deger) : "—"}
|
||
</TableCell>
|
||
);
|
||
})}
|
||
</TableRow>
|
||
))}
|
||
<TableRow>
|
||
<TableCell className="py-2.5 pl-0 text-sm text-slate-500">OBP</TableCell>
|
||
{netler.map((y) => (
|
||
<TableCell
|
||
key={y.yil}
|
||
className="py-2.5 text-right text-sm tabular-nums text-slate-500"
|
||
>
|
||
{y.obp != null ? ondalik(y.obp) : "—"}
|
||
</TableCell>
|
||
))}
|
||
</TableRow>
|
||
<TableRow>
|
||
<TableCell className="py-2.5 pl-0 text-sm text-slate-500">OBP katsayısı</TableCell>
|
||
{netler.map((y) => (
|
||
<TableCell
|
||
key={y.yil}
|
||
className="py-2.5 text-right text-sm tabular-nums text-slate-500"
|
||
>
|
||
{y.katsayi != null ? ondalik(y.katsayi) : "—"}
|
||
</TableCell>
|
||
))}
|
||
</TableRow>
|
||
<TableRow className="border-t border-slate-200">
|
||
<TableCell className="py-3 pl-0 text-sm font-semibold text-slate-900">
|
||
Yerleşme puanı
|
||
</TableCell>
|
||
{netler.map((y) => (
|
||
<TableCell key={y.yil} className={hucre(y.yil, true)}>
|
||
{y.tabanPuan != null ? ondalik(y.tabanPuan) : "—"}
|
||
</TableCell>
|
||
))}
|
||
</TableRow>
|
||
</TableBody>
|
||
</Table>
|
||
{ekPuanli ? (
|
||
<p className={NOT}>
|
||
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.
|
||
</p>
|
||
) : null}
|
||
</>
|
||
);
|
||
}
|
||
|
||
function Kosullar({ veri }: { veri: ProgramAyrintisi }) {
|
||
if (veri.kosullar.length === 0) return null;
|
||
return (
|
||
<details className="group mt-7">
|
||
<summary className="flex min-h-11 cursor-pointer list-none items-center justify-between gap-3 font-heading text-base font-bold text-slate-900 transition-colors hover:text-orange-500 [&::-webkit-details-marker]:hidden">
|
||
Yerleşme koşulları ({veri.kosullar.length})
|
||
<span className="text-sm font-semibold text-slate-500 group-open:hidden">Göster</span>
|
||
<span className="hidden text-sm font-semibold text-slate-500 group-open:inline">Gizle</span>
|
||
</summary>
|
||
<ol className="mt-2 space-y-3 text-sm leading-relaxed text-slate-600">
|
||
{veri.kosullar.map((k) => (
|
||
<li key={k.kod} className="flex gap-3">
|
||
<span className="shrink-0 tabular-nums text-slate-400">{k.kod}</span>
|
||
<span>{k.metin}</span>
|
||
</li>
|
||
))}
|
||
</ol>
|
||
<p className={NOT}>
|
||
Koşul metinleri ÖSYM tercih kılavuzundan; tercih göndermeden önce güncel
|
||
kılavuzdan doğrula.
|
||
</p>
|
||
</details>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<>
|
||
<h3 className={BOLUM_BASLIK}>Mezun istihdamı</h3>
|
||
<p className={NOT}>
|
||
{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}
|
||
</p>
|
||
</>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<>
|
||
<h3 className={BOLUM_BASLIK}>Üniversite</h3>
|
||
<p className={NOT}>
|
||
{parcalar.join(" · ")}.{" "}
|
||
<a
|
||
href={`/universite/${u.slug}`}
|
||
className="font-semibold text-primary transition-colors hover:text-orange-500 hover:underline"
|
||
>
|
||
Üniversite sayfası
|
||
</a>
|
||
</p>
|
||
</>
|
||
);
|
||
}
|