feat: /sonuc ham tablosuna sihirbaz il + devlet/vakıf filtresi
Vakıf seçen kullanıcı sonuç tablosunda devlet üniversiteleri görüyordu: tablo sihirbaz seçimlerini hiç kullanmıyordu. sonucHref artık profildeki seçimlerden il (tekrarlı) ve tip paramlarını URL'e yazar; tablo SSR'da bu paramlarla filtrelenir, "daha fazla göster" sayfalaması aynı filtreleri taşır. Sekmelerin üstünde kaldırılabilir filtre çipleri görünür — çip kaldırmak yalnızca URL'i günceller, localStorage profiline dokunmaz. Kategori tabloya bilinçli uygulanmaz (DB'de kategori kolonu yok, JS filtresi offset/toplam semantiğini bozar); kategori AI listesi ve tadımlıkta uygulanır. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,11 +7,13 @@
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import {
|
||||
ChevronDown,
|
||||
Rocket,
|
||||
Scale,
|
||||
ShieldCheck,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type {
|
||||
DilimKey,
|
||||
@@ -20,7 +22,7 @@ import type {
|
||||
RankResultsListe,
|
||||
} from "@/types/yokatlas";
|
||||
import { riskHesapla, type RiskSeviyesi } from "@/lib/risk";
|
||||
import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug";
|
||||
import { bolumSayfaSlug, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
|
||||
import {
|
||||
ProgramSiraGecmisi,
|
||||
ProgramTabanTrendi,
|
||||
@@ -94,21 +96,44 @@ export function ProgramTablosu({
|
||||
sonuclar,
|
||||
adaySira,
|
||||
tur,
|
||||
filtreIller,
|
||||
filtreTip,
|
||||
}: {
|
||||
sonuclar: RankResultsListe;
|
||||
adaySira: number;
|
||||
tur: PuanTuruKey;
|
||||
filtreIller?: string[];
|
||||
filtreTip?: "devlet" | "vakif";
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const arama = useSearchParams();
|
||||
const [aktifDilim, setAktifDilim] = useState<DilimKey>(() =>
|
||||
varsayilanDilim(sonuclar),
|
||||
);
|
||||
const iller = useMemo(() => filtreIller ?? [], [filtreIller]);
|
||||
|
||||
// "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.
|
||||
// Filtre çipini kaldırmak yalnızca URL'i günceller (tek doğruluk kaynağı
|
||||
// URL); localStorage'taki sihirbaz profiline DOKUNULMAZ — kullanıcı
|
||||
// tabloda filtreyi kaldırıp tüm sonuçları görebilir, AI listesi seçimleri
|
||||
// etkilenmez.
|
||||
function filtreKaldir(tip: "il" | "tip", deger?: string) {
|
||||
const q = new URLSearchParams(arama.toString());
|
||||
if (tip === "tip") {
|
||||
q.delete("tip");
|
||||
} else {
|
||||
const kalanlar = q.getAll("il").filter((i) => i !== deger);
|
||||
q.delete("il");
|
||||
for (const i of kalanlar) q.append("il", i);
|
||||
}
|
||||
router.replace(`?${q}`, { scroll: false });
|
||||
}
|
||||
|
||||
// "Daha fazla göster" ile sayfalanan devam satırları. Sıra/tür/filtre
|
||||
// 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 kümenin satırları karışmasın.
|
||||
const [ekstra, setEkstra] = useState(BOS_EKSTRA);
|
||||
const [yuklenen, setYuklenen] = useState<DilimKey | null>(null);
|
||||
const veriAnahtari = `${adaySira}-${tur}`;
|
||||
const veriAnahtari = `${adaySira}-${tur}-${iller.join(",")}-${filtreTip ?? ""}`;
|
||||
const [oncekiAnahtar, setOncekiAnahtar] = useState(veriAnahtari);
|
||||
if (veriAnahtari !== oncekiAnahtar) {
|
||||
setOncekiAnahtar(veriAnahtari);
|
||||
@@ -129,9 +154,15 @@ export function ProgramTablosu({
|
||||
async function dahaFazlaYukle(dilim: DilimKey, offset: number) {
|
||||
setYuklenen(dilim);
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/programlar?sira=${adaySira}&tur=${tur}&dilim=${dilim}&offset=${offset}`,
|
||||
);
|
||||
const q = new URLSearchParams({
|
||||
sira: String(adaySira),
|
||||
tur,
|
||||
dilim,
|
||||
offset: String(offset),
|
||||
});
|
||||
for (const il of iller) q.append("il", il);
|
||||
if (filtreTip) q.set("tip", filtreTip);
|
||||
const res = await fetch(`/api/programlar?${q}`);
|
||||
if (!res.ok) return;
|
||||
const veri = (await res.json()) as { programlar: ProgramListeSatiri[] };
|
||||
// Aynı sayfanın iki kez istenmesine karşı id bazında tekilleştir
|
||||
@@ -162,6 +193,39 @@ export function ProgramTablosu({
|
||||
onPointerEnter={preloadTercihHaritasi}
|
||||
onFocusCapture={preloadTercihHaritasi}
|
||||
>
|
||||
{/* Sihirbaz seçimlerinden gelen aktif filtreler — görünür ve tek tek
|
||||
kaldırılabilir; sayılar neden azaldı sorusunu burada yanıtlar. */}
|
||||
{iller.length > 0 || filtreTip ? (
|
||||
<div className="mb-3 flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-semibold text-slate-400">
|
||||
Filtreler:
|
||||
</span>
|
||||
{filtreTip ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => filtreKaldir("tip")}
|
||||
title="Üniversite tipi filtresini kaldır"
|
||||
className="inline-flex cursor-pointer items-center gap-1 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 transition-colors duration-150 hover:border-slate-300 hover:bg-slate-50"
|
||||
>
|
||||
{filtreTip === "devlet" ? "Devlet" : "Vakıf"}
|
||||
<X className="size-3" aria-hidden />
|
||||
</button>
|
||||
) : null}
|
||||
{iller.map((il) => (
|
||||
<button
|
||||
key={il}
|
||||
type="button"
|
||||
onClick={() => filtreKaldir("il", il)}
|
||||
title={`${trBaslikDuzeni(il)} filtresini kaldır`}
|
||||
className="inline-flex cursor-pointer items-center gap-1 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 transition-colors duration-150 hover:border-slate-300 hover:bg-slate-50"
|
||||
>
|
||||
{trBaslikDuzeni(il)}
|
||||
<X className="size-3" aria-hidden />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Tabs
|
||||
value={aktifDilim}
|
||||
onValueChange={(v) => setAktifDilim(v as DilimKey)}
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
import { siraylaProgramAra } from "../liste-queries";
|
||||
import { ProgramTablosu } from "./program-tablosu";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { PuanTuruKey } from "@/types/yokatlas";
|
||||
import type { PuanTuruKey, UniturGrubu } from "@/types/yokatlas";
|
||||
|
||||
// /sonuc'un ham tablosunun sunucu sarmalayıcısı: okuma burada yaşar, sayfa
|
||||
// yalnızca kompoze eder.
|
||||
export function SonucProgramTablosu({
|
||||
sira,
|
||||
tur,
|
||||
iller,
|
||||
uniturGrubu,
|
||||
}: {
|
||||
sira: number;
|
||||
tur: PuanTuruKey;
|
||||
iller?: string[];
|
||||
uniturGrubu?: UniturGrubu;
|
||||
}) {
|
||||
const sonuclar = siraylaProgramAra(sira, tur);
|
||||
return <ProgramTablosu sonuclar={sonuclar} adaySira={sira} tur={tur} />;
|
||||
const sonuclar = siraylaProgramAra(sira, tur, { iller, uniturGrubu });
|
||||
return (
|
||||
<ProgramTablosu
|
||||
sonuclar={sonuclar}
|
||||
adaySira={sira}
|
||||
tur={tur}
|
||||
filtreIller={iller}
|
||||
filtreTip={uniturGrubu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SonucProgramTablosuSkeleton() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
ProgramListeSatiri,
|
||||
PuanTuruKey,
|
||||
RankResultsListe,
|
||||
UniturGrubu,
|
||||
} from "@/types/yokatlas";
|
||||
|
||||
// Tabloda kullanılmayan alanlar (fakulte, puan2025) client'a serileşmesin.
|
||||
@@ -29,13 +30,22 @@ export function programOzetle(p: Program): ProgramListeSatiri {
|
||||
};
|
||||
}
|
||||
|
||||
// /sonuc'un ham tablosu: yalnızca sıralamayla ulaşılabilen programlar,
|
||||
// hayal/dengeli/garanti dilimli, dilim başına ilk 20 satır.
|
||||
// /sonuc'un ham tablosu: sıralamayla ulaşılabilen programlar,
|
||||
// hayal/dengeli/garanti dilimli, dilim başına ilk 20 satır. Sihirbaz
|
||||
// seçimlerinden il + üniversite tipi URL üzerinden buraya taşınır ve SQL'de
|
||||
// filtrelenir; kategori bilinçli olarak uygulanmaz (DB'de kategori kolonu
|
||||
// yok, JS filtresi offset/toplam semantiğini bozar — kategori AI listesi ve
|
||||
// tadımlıkta uygulanır).
|
||||
export function siraylaProgramAra(
|
||||
sira: number,
|
||||
tur: PuanTuruKey,
|
||||
opts: { iller?: string[]; uniturGrubu?: UniturGrubu } = {},
|
||||
): RankResultsListe {
|
||||
const ham = searchByRank(sira, tur, { limitPerBucket: 20 });
|
||||
const ham = searchByRank(sira, tur, {
|
||||
limitPerBucket: 20,
|
||||
iller: opts.iller,
|
||||
uniturGrubu: opts.uniturGrubu,
|
||||
});
|
||||
return {
|
||||
hayal: ham.hayal.map(programOzetle),
|
||||
dengeli: ham.dengeli.map(programOzetle),
|
||||
|
||||
@@ -126,16 +126,29 @@ export function tercihProfiliDogrula(girdi: unknown): TercihProfili | null {
|
||||
return { sira, tur: p.tur, secimler };
|
||||
}
|
||||
|
||||
/** Profildeki sıra/tur ile sonuç sayfası URL'i — tüm uygulama aynı sorguyu kullanır. */
|
||||
/**
|
||||
* Profildeki sıra/tur ile sonuç sayfası URL'i — tüm uygulama aynı sorguyu
|
||||
* kullanır. Profilde sihirbaz seçimleri varsa il + üniversite tipi de URL'e
|
||||
* yazılır: /sonuc'un ham tablosu SSR'da bu paramlarla filtrelenir (tek
|
||||
* doğruluk kaynağı URL; localStorage profili yalnızca URL'i üretir).
|
||||
*/
|
||||
export function sonucHref(
|
||||
profil: Pick<TercihProfili, "sira" | "tur">,
|
||||
profil: Pick<TercihProfili, "sira" | "tur"> & {
|
||||
secimler?: SihirbazSecimleri;
|
||||
},
|
||||
ekstra?: Record<string, string>,
|
||||
): string {
|
||||
const q = new URLSearchParams({
|
||||
sira: String(profil.sira),
|
||||
tur: profil.tur,
|
||||
...ekstra,
|
||||
});
|
||||
if (profil.secimler) {
|
||||
for (const il of profil.secimler.iller) q.append("il", il);
|
||||
if (profil.secimler.universiteTipi !== "farketmez") {
|
||||
q.set("tip", profil.secimler.universiteTipi);
|
||||
}
|
||||
}
|
||||
for (const [k, v] of Object.entries(ekstra ?? {})) q.set(k, v);
|
||||
return `/sonuc?${q}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user