Update routing in next.config.ts to redirect "/tercih-robotu" to the hero form, add simple-icons dependency in package.json, and enhance database indexing for improved query performance. Remove unused SVG files and update metadata across various pages for better SEO and clarity.
All checks were successful
Deploy / deploy (push) Successful in 14m14s
All checks were successful
Deploy / deploy (push) Successful in 14m14s
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
"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 "+"
|
||||
// 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";
|
||||
@@ -9,6 +10,7 @@ import Link from "next/link";
|
||||
import { useLinkStatus } from "next/link";
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
Plus,
|
||||
Rocket,
|
||||
Scale,
|
||||
@@ -16,8 +18,9 @@ import {
|
||||
TrendingDown,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import type { Program, PuanTuruKey, RankResults } from "@/lib/db";
|
||||
import type { DilimKey, Program, PuanTuruKey, RankResults } from "@/lib/db";
|
||||
import { riskHesapla, type RiskSeviyesi } from "@/lib/risk";
|
||||
import { turkishSlugify } from "@/lib/slug";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -33,9 +36,6 @@ import {
|
||||
useManuelListe,
|
||||
type ManuelTercih,
|
||||
} from "@/components/manuel-liste/store";
|
||||
import { ManuelHarita } from "@/components/manuel-liste/manuel-harita";
|
||||
|
||||
type DilimKey = keyof RankResults;
|
||||
|
||||
const DILIMLER: {
|
||||
key: DilimKey;
|
||||
@@ -144,6 +144,20 @@ function tercihYap(p: Program, adaySira: number): ManuelTercih {
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -154,14 +168,59 @@ export function ProgramTablosu({
|
||||
tur: PuanTuruKey;
|
||||
}) {
|
||||
const liste = useManuelListe();
|
||||
const [aktifDilim, setAktifDilim] = useState<DilimKey>("dengeli");
|
||||
const [aktifDilim, setAktifDilim] = useState<DilimKey>(() =>
|
||||
varsayilanDilim(sonuclar),
|
||||
);
|
||||
const listedekiler = new Set(liste.map((t) => t.id));
|
||||
const doldu = liste.length >= MANUEL_LISTE_MAX;
|
||||
|
||||
// "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");
|
||||
}
|
||||
}
|
||||
|
||||
function eklemeyiDene(p: Program) {
|
||||
ekle(tercihYap(p, adaySira));
|
||||
}
|
||||
|
||||
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">
|
||||
<div>
|
||||
@@ -177,11 +236,6 @@ export function ProgramTablosu({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Listendeki üniversitelerin konumu — ekledikçe canlı dolar */}
|
||||
<div className="mt-5">
|
||||
<ManuelHarita />
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
value={aktifDilim}
|
||||
onValueChange={(v) => setAktifDilim(v as DilimKey)}
|
||||
@@ -197,7 +251,7 @@ export function ProgramTablosu({
|
||||
<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}
|
||||
{sonuclar.toplam[d.key].toLocaleString("tr-TR")}
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
@@ -211,49 +265,70 @@ export function ProgramTablosu({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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="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-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>
|
||||
))}
|
||||
{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-16 text-right">
|
||||
<span className="sr-only">Listeye ekle</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{satirlar.map((p) => (
|
||||
<ProgramSatiri
|
||||
key={p.id}
|
||||
program={p}
|
||||
adaySira={adaySira}
|
||||
listede={listedekiler.has(p.id)}
|
||||
doldu={doldu}
|
||||
onEkle={eklemeyiDene}
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
@@ -339,6 +414,14 @@ function TabanTrendi({ p }: { p: Program }) {
|
||||
);
|
||||
}
|
||||
|
||||
// /universite/[slug] sayfa slug'ı: harita.ts'teki uniAdiNormalize ile aynı
|
||||
// kural (sondaki "(İL)" eki atılır) — client bundle'a harita verisi çekmemek
|
||||
// için burada tekrarlanır. Katalog haritası DB'deki tüm universite
|
||||
// değerlerinden kurulduğu için bu slug her zaman var olan bir sayfaya çıkar.
|
||||
function uniSayfaSlug(ad: string): string {
|
||||
return turkishSlugify(ad.replace(/\s*\([^)]*\)\s*$/, "").trim());
|
||||
}
|
||||
|
||||
function ProgramSatiri({
|
||||
program: p,
|
||||
adaySira,
|
||||
@@ -379,7 +462,15 @@ function ProgramSatiri({
|
||||
{devlet ? "Devlet" : "Vakıf"}
|
||||
</span>
|
||||
<span className="truncate">
|
||||
{p.universite}
|
||||
<Link
|
||||
href={`/universite/${uniSayfaSlug(p.universite)}`}
|
||||
className="hover:text-primary hover:underline"
|
||||
title={`${p.universite} taban puanları sayfası`}
|
||||
// Satırdaki "+" ekleme akışıyla karışmasın: tıklama satıra yayılmaz
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{p.universite}
|
||||
</Link>
|
||||
{p.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""}
|
||||
</span>
|
||||
{p.sure ? (
|
||||
|
||||
Reference in New Issue
Block a user