Update Next.js configuration and package dependencies
All checks were successful
Deploy / deploy (push) Successful in 9m29s
All checks were successful
Deploy / deploy (push) Successful in 9m29s
- Modified redirects in next.config.ts to change the destination for "/sohbet" to "/listem". - Added new dependencies in package.json: marked, motion, react-markdown, remark-breaks, remark-gfm, shiki, and use-stick-to-bottom. - Updated pnpm-lock.yaml to reflect the new package versions and dependencies. - Removed obsolete stack files for Flutter, Nuxt, Nuxt.js, React Native, and Svelte from the UI/UX Pro Max skill data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
253
src/components/program-tablosu.tsx
Normal file
253
src/components/program-tablosu.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
"use client";
|
||||
|
||||
// Sonuç sayfasının AI'sız ham listesi: yalnızca başarı sıralamasıyla
|
||||
// yokatlas'tan gelen programlar, dilim sekmeleri halinde. Her satırın "+"
|
||||
// butonu programı manuel 24'lük listeye ekler; pill navbar'daki (ya da açık
|
||||
// çekmecedeki) Listem hedefine uçar (bkz. manuel-liste/liste-cekmecesi).
|
||||
|
||||
import { useState } from "react";
|
||||
import { Check, Plus, Rocket, Scale, ShieldCheck } from "lucide-react";
|
||||
import type { Program, RankResults } from "@/lib/db";
|
||||
import { riskHesapla, type RiskSeviyesi } from "@/lib/risk";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
MANUEL_LISTE_MAX,
|
||||
ekle,
|
||||
ucusBaslat,
|
||||
useManuelListe,
|
||||
type ManuelTercih,
|
||||
} from "@/components/manuel-liste/store";
|
||||
import { ManuelHarita } from "@/components/manuel-liste/manuel-harita";
|
||||
|
||||
type DilimKey = keyof RankResults;
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
function efektifSira(p: Program): number | null {
|
||||
return p.sira2025 ?? p.sira2024;
|
||||
}
|
||||
|
||||
function tercihYap(p: Program, adaySira: number): ManuelTercih {
|
||||
const taban = efektifSira(p);
|
||||
return {
|
||||
id: p.id,
|
||||
isim: p.isim,
|
||||
universite: p.universite,
|
||||
il: p.il,
|
||||
efektifSira: taban,
|
||||
risk: riskHesapla(taban, adaySira),
|
||||
};
|
||||
}
|
||||
|
||||
export function ProgramTablosu({
|
||||
sonuclar,
|
||||
adaySira,
|
||||
}: {
|
||||
sonuclar: RankResults;
|
||||
adaySira: number;
|
||||
}) {
|
||||
const liste = useManuelListe();
|
||||
const [aktifDilim, setAktifDilim] = useState<DilimKey>("dengeli");
|
||||
const listedekiler = new Set(liste.map((t) => t.id));
|
||||
const doldu = liste.length >= MANUEL_LISTE_MAX;
|
||||
|
||||
function eklemeyiDene(p: Program, buton: HTMLElement) {
|
||||
const tercih = tercihYap(p, adaySira);
|
||||
if (!ekle(tercih)) return;
|
||||
const r = buton.getBoundingClientRect();
|
||||
ucusBaslat({
|
||||
tercih,
|
||||
kaynak: { x: r.left + r.width / 2, y: r.top + r.height / 2 },
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="mt-16" aria-label="Sıralamanla açılan programlar">
|
||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="font-heading text-2xl font-bold">
|
||||
Sıralamanla açılan programlar
|
||||
</h2>
|
||||
<p className="mt-1 max-w-2xl text-sm text-slate-500">
|
||||
Yapay zekâsız, ham YÖK Atlas verisi: geçen yılın taban
|
||||
sıralamalarına göre ulaşabildiğin bölümler.{" "}
|
||||
<span className="font-medium text-slate-700">
|
||||
+ ile beğendiklerini kendi 24'lük listene at.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Listendeki üniversitelerin konumu — ekledikçe canlı dolar */}
|
||||
<div className="mt-5">
|
||||
<ManuelHarita />
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
value={aktifDilim}
|
||||
onValueChange={(v) => setAktifDilim(v as DilimKey)}
|
||||
className="mt-5"
|
||||
>
|
||||
<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[d.key].length}
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{DILIMLER.map((d) => (
|
||||
<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">
|
||||
{sonuclar[d.key].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="text-right">Taban sıra</TableHead>
|
||||
<TableHead className="w-16 text-right">
|
||||
<span className="sr-only">Listeye ekle</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sonuclar[d.key].map((p) => (
|
||||
<ProgramSatiri
|
||||
key={p.id}
|
||||
program={p}
|
||||
adaySira={adaySira}
|
||||
listede={listedekiler.has(p.id)}
|
||||
doldu={doldu}
|
||||
onEkle={eklemeyiDene}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ProgramSatiri({
|
||||
program: p,
|
||||
adaySira,
|
||||
listede,
|
||||
doldu,
|
||||
onEkle,
|
||||
}: {
|
||||
program: Program;
|
||||
adaySira: number;
|
||||
listede: boolean;
|
||||
doldu: boolean;
|
||||
onEkle: (p: Program, buton: HTMLElement) => void;
|
||||
}) {
|
||||
const taban = efektifSira(p);
|
||||
const risk = riskHesapla(taban, adaySira);
|
||||
|
||||
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="block truncate text-sm font-semibold">{p.isim}</span>
|
||||
<span className="block truncate text-xs text-slate-500">
|
||||
{p.universite}
|
||||
{p.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-xs tabular-nums text-slate-500">
|
||||
{taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"}
|
||||
</TableCell>
|
||||
<TableCell className="pr-4 text-right">
|
||||
<button
|
||||
type="button"
|
||||
disabled={listede || doldu}
|
||||
onClick={(e) => onEkle(p, e.currentTarget)}
|
||||
title={
|
||||
listede
|
||||
? "Zaten listende"
|
||||
: doldu
|
||||
? `Liste dolu (${MANUEL_LISTE_MAX}/${MANUEL_LISTE_MAX})`
|
||||
: "Listeme ekle"
|
||||
}
|
||||
className={`inline-flex size-8 cursor-pointer items-center justify-center rounded-full border transition-[background-color,border-color,color,transform] duration-150 active:scale-[0.92] disabled:cursor-default ${
|
||||
listede
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-600"
|
||||
: "border-slate-200 bg-white text-slate-500 hover:border-primary hover:bg-primary/5 hover:text-primary disabled:opacity-40"
|
||||
}`}
|
||||
>
|
||||
{listede ? (
|
||||
<Check className="size-4" aria-hidden />
|
||||
) : (
|
||||
<Plus className="size-4" aria-hidden />
|
||||
)}
|
||||
<span className="sr-only">
|
||||
{listede ? `${p.isim} listende` : `${p.isim} listeye ekle`}
|
||||
</span>
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user