Files
kolaytercih/src/components/program-tablosu.tsx
bilalgursen e62c6f07df
Some checks failed
Deploy / deploy (push) Failing after 1h28m6s
Update package.json scripts, enhance layout with new components, and improve API error handling
- Added new scripts for database operations and temporary production in package.json.
- Integrated KaydetBannerLazy component into the layout for improved user notifications.
- Enhanced API error handling in the soru route to mark credit exhaustion.
- Updated the IletisimPage for better button styling and user experience.
- Refactored ListemPage to streamline user flow and improve session handling.
- Removed unused ListePaneli component to clean up the codebase.
2026-08-06 02:27:42 +03:00

343 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
// Sonuç sayfasının Yapay Zeka'sız ham listesi: yalnızca başarı sıralamasıyla
// yokatlas'tan gelen programlar, dilim sekmeleri halinde ve yakından uzağa
// sayfalanır ("daha fazla göster" /api/programlar'dan akıtır). Her satırın "+"
// butonu programı manuel 24'lük listeye ekler (bkz. manuel-liste/store).
import { useState } from "react";
import Link from "next/link";
import {
ChevronDown,
Rocket,
Scale,
ShieldCheck,
} from "lucide-react";
import type { DilimKey, Program, PuanTuruKey, RankResults } from "@/lib/db";
import { riskHesapla, type RiskSeviyesi } from "@/lib/risk";
import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug";
import {
ProgramSiraGecmisi,
ProgramTabanTrendi,
programEtkinSira,
} from "@/components/program-liste-verileri";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ProgramEkleButonu } from "@/components/manuel-liste/program-ekle-butonu";
import { ProgramNetleriButonu } from "@/components/program-netleri";
import { UniLogo } from "@/components/uni-logo";
const DILIMLER: {
key: DilimKey;
etiket: string;
aciklama: string;
icon: typeof Rocket;
renk: string;
}[] = [
{
key: "hayal",
etiket: "Hayal",
aciklama: "Tabanı sıralamanın üstünde — şansını denediğin satırlar.",
icon: Rocket,
renk: "text-red-500",
},
{
key: "dengeli",
etiket: "Dengeli",
aciklama: "Tabanı sıralamana denk — listenin bel kemiği.",
icon: Scale,
renk: "text-amber-500",
},
{
key: "garanti",
etiket: "Garanti",
aciklama: "Tabanı belirgin şekilde altında — güvenli bölge.",
icon: ShieldCheck,
renk: "text-emerald-600",
},
];
const RISK_NOKTA: Record<RiskSeviyesi, string> = {
guvenli: "bg-emerald-500",
"az-riskli": "bg-amber-500",
riskli: "bg-red-500",
};
const BOS_EKSTRA: Record<DilimKey, Program[]> = {
hayal: [],
dengeli: [],
garanti: [],
};
// Dengeli ve hayal dilimleri tamamen boşsa (sıralama çok gerideyse) boş bir
// "dengeli" sekmesi yerine dolu olan "garanti" ile açılır.
function varsayilanDilim(sonuclar: RankResults): DilimKey {
return sonuclar.toplam.dengeli === 0 && sonuclar.toplam.hayal === 0
? "garanti"
: "dengeli";
}
export function ProgramTablosu({
sonuclar,
adaySira,
tur,
}: {
sonuclar: RankResults;
adaySira: number;
tur: PuanTuruKey;
}) {
const [aktifDilim, setAktifDilim] = useState<DilimKey>(() =>
varsayilanDilim(sonuclar),
);
// "Daha fazla göster" ile sayfalanan devam satırları. Sıra/tür değişince
// (puan türü seçici aynı bileşeni yeniden render eder, remount etmez)
// render sırasında sıfırlanır ki eski türün satırları yenisine karışmasın.
const [ekstra, setEkstra] = useState(BOS_EKSTRA);
const [yuklenen, setYuklenen] = useState<DilimKey | null>(null);
const veriAnahtari = `${adaySira}-${tur}`;
const [oncekiAnahtar, setOncekiAnahtar] = useState(veriAnahtari);
if (veriAnahtari !== oncekiAnahtar) {
setOncekiAnahtar(veriAnahtari);
setEkstra(BOS_EKSTRA);
// Yeni sıra/türde dengeli+hayal boş kaldıysa boş sekmede bırakma
if (varsayilanDilim(sonuclar) === "garanti" && aktifDilim !== "garanti") {
setAktifDilim("garanti");
}
}
async function dahaFazlaYukle(dilim: DilimKey, offset: number) {
setYuklenen(dilim);
try {
const res = await fetch(
`/api/programlar?sira=${adaySira}&tur=${tur}&dilim=${dilim}&offset=${offset}`,
);
if (!res.ok) return;
const veri = (await res.json()) as { programlar: Program[] };
// Aynı sayfanın iki kez istenmesine karşı id bazında tekilleştir
setEkstra((mevcut) => {
const eldekiler = new Set(
[...sonuclar[dilim], ...mevcut[dilim]].map((p) => p.id),
);
return {
...mevcut,
[dilim]: [
...mevcut[dilim],
...veri.programlar.filter((p) => !eldekiler.has(p.id)),
],
};
});
} catch {
} finally {
setYuklenen(null);
}
}
return (
<section className="mt-16" aria-label="Sıralamanla açılan programlar">
<Tabs
value={aktifDilim}
onValueChange={(v) => setAktifDilim(v as DilimKey)}
>
{/* Dilim sekmeleri tablonun hemen üstünde; puan türü burada değil,
navbar'daki sıralama rozetinden değiştirilir. */}
<TabsList>
{DILIMLER.map((d) => (
<TabsTrigger key={d.key} value={d.key} className="cursor-pointer">
<d.icon className={`size-3.5 ${d.renk}`} aria-hidden />
{d.etiket}
<span className="text-xs tabular-nums text-slate-400">
{sonuclar.toplam[d.key].toLocaleString("tr-TR")}
</span>
</TabsTrigger>
))}
</TabsList>
{DILIMLER.map((d) => {
const satirlar = [...sonuclar[d.key], ...ekstra[d.key]];
const toplam = sonuclar.toplam[d.key];
return (
<TabsContent key={d.key} value={d.key}>
<p className="mt-2 text-xs text-slate-500">{d.aciklama}</p>
<div className="mt-3 overflow-hidden rounded-2xl border border-slate-200 bg-white">
{satirlar.length === 0 ? (
<p className="px-5 py-8 text-center text-sm text-slate-500">
Bu dilimde filtrene uyan program bulunamadı.
</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-8" />
<TableHead>Program</TableHead>
<TableHead className="hidden text-right md:table-cell">
Son 5 yıl
</TableHead>
<TableHead className="hidden text-right sm:table-cell">
Kontenjan
</TableHead>
<TableHead className="text-right">Taban sıra</TableHead>
<TableHead className="w-24 text-right">
<span className="sr-only">Netler ve listeye ekle</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{satirlar.map((p) => (
<ProgramSatiri
key={p.id}
program={p}
adaySira={adaySira}
tur={tur}
/>
))}
</TableBody>
</Table>
)}
</div>
{satirlar.length < toplam ? (
<div className="mt-3 flex flex-col items-center gap-1">
<button
type="button"
disabled={yuklenen === d.key}
onClick={() => dahaFazlaYukle(d.key, satirlar.length)}
className="inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition-colors duration-150 hover:border-slate-300 hover:bg-slate-50 disabled:cursor-default disabled:opacity-60"
>
<ChevronDown className="size-4" aria-hidden />
{yuklenen === d.key ? "Yükleniyor…" : "Daha fazla göster"}
</button>
<span className="text-xs tabular-nums text-slate-400">
{satirlar.length.toLocaleString("tr-TR")} /{" "}
{toplam.toLocaleString("tr-TR")} program
</span>
</div>
) : null}
</TabsContent>
);
})}
</Tabs>
</section>
);
}
function ProgramSatiri({
program: p,
adaySira,
tur,
}: {
program: Program;
adaySira: number;
tur: PuanTuruKey;
}) {
const taban = programEtkinSira(p);
const risk = riskHesapla(taban, adaySira);
const devlet = p.unitur === "DEVLET";
return (
<TableRow>
<TableCell className="pl-4 pr-0">
{risk ? (
<span
className={`block size-2.5 rounded-full ${RISK_NOKTA[risk]}`}
aria-hidden
/>
) : null}
</TableCell>
<TableCell className="max-w-0 w-full">
<span className="flex min-w-0 items-center gap-2.5">
<UniLogo ad={p.universite} boy="sm" />
<span className="min-w-0 flex-1">
<Link
href={`/bolum/${bolumSayfaSlug(p.isim)}`}
className="block truncate text-sm font-semibold hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
title={`${p.isim} bölüm sayfası`}
onClick={(e) => e.stopPropagation()}
>
{p.isim}
</Link>
<span className="mt-0.5 flex items-center gap-1.5 text-xs text-slate-500">
<span
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
devlet
? "bg-slate-100 text-slate-600"
: "bg-violet-50 text-violet-600"
}`}
>
{devlet ? "Devlet" : "Vakıf"}
</span>
<span className="truncate">
<Link
href={`/universite/${uniSayfaSlug(p.universite)}`}
className="hover:text-primary hover:underline"
title={`${p.universite} taban puanları sayfası`}
onClick={(e) => e.stopPropagation()}
>
{p.universite}
</Link>
{p.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""}
</span>
{p.sure ? (
<span className="shrink-0 text-slate-400">· {p.sure} yıl</span>
) : null}
</span>
</span>
</span>
</TableCell>
<TableCell className="hidden text-right text-xs md:table-cell">
<ProgramSiraGecmisi program={p} />
</TableCell>
<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`
: undefined
}
>
{p.kontenjan2025 != null ? (
<span className="inline-flex items-center gap-1.5">
{p.yerlesen2025 != null && p.yerlesen2025 < p.kontenjan2025 ? (
<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
</span>
) : (
"—"
)}
</TableCell>
<TableCell className="text-right text-xs tabular-nums text-slate-500">
<span className="inline-flex items-center gap-1">
<ProgramTabanTrendi program={p} />
{taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"}
</span>
</TableCell>
<TableCell className="pr-4 text-right">
<span className="inline-flex items-center gap-1.5">
<ProgramNetleriButonu
programId={p.id}
isim={p.isim}
universite={p.universite}
className="size-8 min-h-8 min-w-8"
/>
<ProgramEkleButonu
program={p}
kaynak="sonuc"
adaySira={adaySira}
adayTur={tur}
className="size-8 min-h-8 min-w-8"
/>
</span>
</TableCell>
</TableRow>
);
}