refactor: katalog feature'ı — sayfalar senkron kompozisyona geçti (Faz 2)
- 8 katalog bileşeni src/features/katalog/components/ altına taşındı; katalog-queries.ts (server-only) lib/katalog'un feature kapısı oldu - /bolumler ve /universiteler'in sayfa içi fetch+inline bileşenleri BolumKatalogu/UniversiteKatalogu feature bileşenlerine çıkarıldı - 6 sayfa async'ten senkron'a: params.then() + sayfada Suspense, skeleton'lar feature bileşeninin yanında (BolumIcerikSkeleton vb.) - generateStaticParams artık *-queries yerine bileşen dosyasının helper'larını kullanıyor (bolumStaticSlugs, uniSayfaStaticParams) - Davranış değişikliği yok; route durumları aynı (◐), 1917 statik sayfa Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
221
src/features/katalog/components/program-netleri.tsx
Normal file
221
src/features/katalog/components/program-netleri.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
"use client";
|
||||
|
||||
// Program satırındaki "netler" butonu: YÖK Atlas'ın "yerleşen son kişinin
|
||||
// netleri" verisini (data/yokatlas.db → netler tablosu, /api/netler) yıl
|
||||
// sütunlu kompakt bir modalde gösterir. Kadın-erkek dağılımı bilinçli olarak
|
||||
// yok: YÖK Atlas 2026 SPA'sına geçerken cinsiyet panelini yayından kaldırdı
|
||||
// (bkz. scripts/detay.ts).
|
||||
|
||||
import { useState } from "react";
|
||||
import { ListChecks } from "lucide-react";
|
||||
import type { ProgramNetSatiri } from "@/types/yokatlas";
|
||||
import { olay } from "@/lib/analitik";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
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">;
|
||||
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 },
|
||||
];
|
||||
|
||||
const net = (n: number) =>
|
||||
n.toLocaleString("tr-TR", { maximumFractionDigits: 2 });
|
||||
|
||||
type Durum = "bekliyor" | "yukleniyor" | "hazir" | "hata";
|
||||
|
||||
export function ProgramNetleriButonu({
|
||||
programId,
|
||||
isim,
|
||||
universite,
|
||||
className = "",
|
||||
}: {
|
||||
programId: string;
|
||||
isim: string;
|
||||
universite?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [durum, setDurum] = useState<Durum>("bekliyor");
|
||||
const [netler, setNetler] = useState<ProgramNetSatiri[]>([]);
|
||||
|
||||
async function yukle() {
|
||||
setDurum("yukleniyor");
|
||||
try {
|
||||
const res = await fetch(`/api/netler?id=${programId}`);
|
||||
if (!res.ok) throw new Error(String(res.status));
|
||||
const veri = (await res.json()) as { netler: ProgramNetSatiri[] };
|
||||
setNetler(veri.netler);
|
||||
setDurum("hazir");
|
||||
} catch {
|
||||
setDurum("hata");
|
||||
}
|
||||
}
|
||||
|
||||
function acildi(acik: boolean) {
|
||||
if (!acik) return;
|
||||
olay("netler_goruntulendi");
|
||||
if (durum === "bekliyor" || durum === "hata") yukle();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={acildi}>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
title="Son yerleşenin netleri"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={`inline-flex size-11 min-h-11 min-w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-primary hover:bg-primary/5 hover:text-primary active:scale-[0.92] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${className}`}
|
||||
>
|
||||
<ListChecks className="size-4" aria-hidden />
|
||||
<span className="sr-only">{isim} — son yerleşenin netleri</span>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="sm:max-w-md"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="pr-10">Son yerleşenin netleri</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isim}
|
||||
{universite ? ` · ${universite}` : ""} — bölüme en düşük puanla
|
||||
yerleşen adayın sınav netleri (YÖK Atlas).
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<NetlerIcerik durum={durum} netler={netler} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function NetlerIcerik({
|
||||
durum,
|
||||
netler,
|
||||
}: {
|
||||
durum: Durum;
|
||||
netler: ProgramNetSatiri[];
|
||||
}) {
|
||||
if (durum === "yukleniyor" || durum === "bekliyor") {
|
||||
return (
|
||||
<div className="space-y-2" aria-hidden>
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="h-6 animate-pulse rounded-md bg-slate-100" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (durum === "hata") {
|
||||
return (
|
||||
<p className="py-4 text-center text-sm text-slate-500">
|
||||
Netler yüklenemedi. Modalı kapatıp yeniden açmayı dene.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (netler.length === 0) {
|
||||
return (
|
||||
<p className="py-4 text-center text-sm 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),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="-mx-1 overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="h-9">Ders</TableHead>
|
||||
{netler.map((y) => (
|
||||
<TableHead key={y.yil} className="h-9 text-right tabular-nums">
|
||||
{y.yil}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{dersler.map((d) => (
|
||||
<TableRow key={d.alan}>
|
||||
<TableCell className="py-2 text-xs text-slate-600">
|
||||
{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="py-2 text-right text-xs tabular-nums"
|
||||
>
|
||||
{deger != null ? net(deger) : "—"}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow>
|
||||
<TableCell className="py-2 text-xs font-medium text-slate-600">
|
||||
OBP
|
||||
</TableCell>
|
||||
{netler.map((y) => (
|
||||
<TableCell
|
||||
key={y.yil}
|
||||
className="py-2 text-right text-xs tabular-nums text-slate-500"
|
||||
>
|
||||
{y.obp != null ? net(y.obp) : "—"}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2 text-xs font-semibold text-slate-700">
|
||||
Yerleşme puanı
|
||||
</TableCell>
|
||||
{netler.map((y) => (
|
||||
<TableCell
|
||||
key={y.yil}
|
||||
className="py-2 text-right text-xs font-semibold tabular-nums text-slate-700"
|
||||
>
|
||||
{y.tabanPuan != null ? net(y.tabanPuan) : "—"}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user