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:
@@ -36,9 +36,19 @@ export function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: "Geçersiz offset." }, { status: 400 });
|
return NextResponse.json({ error: "Geçersiz offset." }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /sonuc tablosunun filtreleri sayfalamada da korunur (bkz. sonucHref)
|
||||||
|
const iller = [
|
||||||
|
...new Set(searchParams.getAll("il").filter((i) => i.trim().length > 0)),
|
||||||
|
].slice(0, 5);
|
||||||
|
const tipParam = searchParams.get("tip");
|
||||||
|
const uniturGrubu =
|
||||||
|
tipParam === "devlet" || tipParam === "vakif" ? tipParam : undefined;
|
||||||
|
|
||||||
const { programlar, toplam } = dilimAra(sira, tur, dilimParam as DilimKey, {
|
const { programlar, toplam } = dilimAra(sira, tur, dilimParam as DilimKey, {
|
||||||
limit: SAYFA_BOYU,
|
limit: SAYFA_BOYU,
|
||||||
offset,
|
offset,
|
||||||
|
iller,
|
||||||
|
uniturGrubu,
|
||||||
});
|
});
|
||||||
// Tablo satırının kullanmadığı alanlar payload'a girmesin
|
// Tablo satırının kullanmadığı alanlar payload'a girmesin
|
||||||
return NextResponse.json({ programlar: programlar.map(programOzetle), toplam });
|
return NextResponse.json({ programlar: programlar.map(programOzetle), toplam });
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export default function SonucPage({
|
|||||||
tur?: string;
|
tur?: string;
|
||||||
sihirbaz?: string;
|
sihirbaz?: string;
|
||||||
hazir?: string;
|
hazir?: string;
|
||||||
|
il?: string | string[];
|
||||||
|
tip?: string;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -51,6 +53,22 @@ export default function SonucPage({
|
|||||||
params.tur && params.tur in PUAN_TURLERI
|
params.tur && params.tur in PUAN_TURLERI
|
||||||
? (params.tur as PuanTuruKey)
|
? (params.tur as PuanTuruKey)
|
||||||
: "say";
|
: "say";
|
||||||
|
// Sihirbaz seçimlerinden gelen tablo filtreleri (bkz. sonucHref):
|
||||||
|
// il tekrarlı param, tip devlet|vakif allowlist'li
|
||||||
|
const iller = [
|
||||||
|
...new Set(
|
||||||
|
(Array.isArray(params.il)
|
||||||
|
? params.il
|
||||||
|
: params.il
|
||||||
|
? [params.il]
|
||||||
|
: []
|
||||||
|
).filter((i) => i.trim().length > 0),
|
||||||
|
),
|
||||||
|
].slice(0, 5);
|
||||||
|
const uniturGrubu =
|
||||||
|
params.tip === "devlet" || params.tip === "vakif"
|
||||||
|
? params.tip
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) {
|
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) {
|
||||||
return (
|
return (
|
||||||
@@ -71,8 +89,14 @@ export default function SonucPage({
|
|||||||
|
|
||||||
{/* Yapay Zeka'dan bağımsız ham tablo — haritanın hemen altında,
|
{/* Yapay Zeka'dan bağımsız ham tablo — haritanın hemen altında,
|
||||||
sayfanın ana gövdesi; puan türü navbar'daki sıralama
|
sayfanın ana gövdesi; puan türü navbar'daki sıralama
|
||||||
rozetinden değiştirilir */}
|
rozetinden değiştirilir. Sihirbaz seçimleri (il, devlet/
|
||||||
<SonucProgramTablosu sira={sira} tur={turKey} />
|
vakıf) URL paramlarıyla tabloya uygulanır. */}
|
||||||
|
<SonucProgramTablosu
|
||||||
|
sira={sira}
|
||||||
|
tur={turKey}
|
||||||
|
iller={iller}
|
||||||
|
uniturGrubu={uniturGrubu}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek.
|
{/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek.
|
||||||
Havuzda içerik yoksa bileşen hiç render edilmez. */}
|
Havuzda içerik yoksa bileşen hiç render edilmez. */}
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Rocket,
|
Rocket,
|
||||||
Scale,
|
Scale,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
DilimKey,
|
DilimKey,
|
||||||
@@ -20,7 +22,7 @@ import type {
|
|||||||
RankResultsListe,
|
RankResultsListe,
|
||||||
} from "@/types/yokatlas";
|
} from "@/types/yokatlas";
|
||||||
import { riskHesapla, type RiskSeviyesi } from "@/lib/risk";
|
import { riskHesapla, type RiskSeviyesi } from "@/lib/risk";
|
||||||
import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug";
|
import { bolumSayfaSlug, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
|
||||||
import {
|
import {
|
||||||
ProgramSiraGecmisi,
|
ProgramSiraGecmisi,
|
||||||
ProgramTabanTrendi,
|
ProgramTabanTrendi,
|
||||||
@@ -94,21 +96,44 @@ export function ProgramTablosu({
|
|||||||
sonuclar,
|
sonuclar,
|
||||||
adaySira,
|
adaySira,
|
||||||
tur,
|
tur,
|
||||||
|
filtreIller,
|
||||||
|
filtreTip,
|
||||||
}: {
|
}: {
|
||||||
sonuclar: RankResultsListe;
|
sonuclar: RankResultsListe;
|
||||||
adaySira: number;
|
adaySira: number;
|
||||||
tur: PuanTuruKey;
|
tur: PuanTuruKey;
|
||||||
|
filtreIller?: string[];
|
||||||
|
filtreTip?: "devlet" | "vakif";
|
||||||
}) {
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
const arama = useSearchParams();
|
||||||
const [aktifDilim, setAktifDilim] = useState<DilimKey>(() =>
|
const [aktifDilim, setAktifDilim] = useState<DilimKey>(() =>
|
||||||
varsayilanDilim(sonuclar),
|
varsayilanDilim(sonuclar),
|
||||||
);
|
);
|
||||||
|
const iller = useMemo(() => filtreIller ?? [], [filtreIller]);
|
||||||
|
|
||||||
// "Daha fazla göster" ile sayfalanan devam satırları. Sıra/tür değişince
|
// Filtre çipini kaldırmak yalnızca URL'i günceller (tek doğruluk kaynağı
|
||||||
// (puan türü seçici aynı bileşeni yeniden render eder, remount etmez)
|
// URL); localStorage'taki sihirbaz profiline DOKUNULMAZ — kullanıcı
|
||||||
// render sırasında sıfırlanır ki eski türün satırları yenisine karışmasın.
|
// 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 [ekstra, setEkstra] = useState(BOS_EKSTRA);
|
||||||
const [yuklenen, setYuklenen] = useState<DilimKey | null>(null);
|
const [yuklenen, setYuklenen] = useState<DilimKey | null>(null);
|
||||||
const veriAnahtari = `${adaySira}-${tur}`;
|
const veriAnahtari = `${adaySira}-${tur}-${iller.join(",")}-${filtreTip ?? ""}`;
|
||||||
const [oncekiAnahtar, setOncekiAnahtar] = useState(veriAnahtari);
|
const [oncekiAnahtar, setOncekiAnahtar] = useState(veriAnahtari);
|
||||||
if (veriAnahtari !== oncekiAnahtar) {
|
if (veriAnahtari !== oncekiAnahtar) {
|
||||||
setOncekiAnahtar(veriAnahtari);
|
setOncekiAnahtar(veriAnahtari);
|
||||||
@@ -129,9 +154,15 @@ export function ProgramTablosu({
|
|||||||
async function dahaFazlaYukle(dilim: DilimKey, offset: number) {
|
async function dahaFazlaYukle(dilim: DilimKey, offset: number) {
|
||||||
setYuklenen(dilim);
|
setYuklenen(dilim);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const q = new URLSearchParams({
|
||||||
`/api/programlar?sira=${adaySira}&tur=${tur}&dilim=${dilim}&offset=${offset}`,
|
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;
|
if (!res.ok) return;
|
||||||
const veri = (await res.json()) as { programlar: ProgramListeSatiri[] };
|
const veri = (await res.json()) as { programlar: ProgramListeSatiri[] };
|
||||||
// Aynı sayfanın iki kez istenmesine karşı id bazında tekilleştir
|
// Aynı sayfanın iki kez istenmesine karşı id bazında tekilleştir
|
||||||
@@ -162,6 +193,39 @@ export function ProgramTablosu({
|
|||||||
onPointerEnter={preloadTercihHaritasi}
|
onPointerEnter={preloadTercihHaritasi}
|
||||||
onFocusCapture={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
|
<Tabs
|
||||||
value={aktifDilim}
|
value={aktifDilim}
|
||||||
onValueChange={(v) => setAktifDilim(v as DilimKey)}
|
onValueChange={(v) => setAktifDilim(v as DilimKey)}
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
import { siraylaProgramAra } from "../liste-queries";
|
import { siraylaProgramAra } from "../liste-queries";
|
||||||
import { ProgramTablosu } from "./program-tablosu";
|
import { ProgramTablosu } from "./program-tablosu";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
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
|
// /sonuc'un ham tablosunun sunucu sarmalayıcısı: okuma burada yaşar, sayfa
|
||||||
// yalnızca kompoze eder.
|
// yalnızca kompoze eder.
|
||||||
export function SonucProgramTablosu({
|
export function SonucProgramTablosu({
|
||||||
sira,
|
sira,
|
||||||
tur,
|
tur,
|
||||||
|
iller,
|
||||||
|
uniturGrubu,
|
||||||
}: {
|
}: {
|
||||||
sira: number;
|
sira: number;
|
||||||
tur: PuanTuruKey;
|
tur: PuanTuruKey;
|
||||||
|
iller?: string[];
|
||||||
|
uniturGrubu?: UniturGrubu;
|
||||||
}) {
|
}) {
|
||||||
const sonuclar = siraylaProgramAra(sira, tur);
|
const sonuclar = siraylaProgramAra(sira, tur, { iller, uniturGrubu });
|
||||||
return <ProgramTablosu sonuclar={sonuclar} adaySira={sira} tur={tur} />;
|
return (
|
||||||
|
<ProgramTablosu
|
||||||
|
sonuclar={sonuclar}
|
||||||
|
adaySira={sira}
|
||||||
|
tur={tur}
|
||||||
|
filtreIller={iller}
|
||||||
|
filtreTip={uniturGrubu}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SonucProgramTablosuSkeleton() {
|
export function SonucProgramTablosuSkeleton() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
ProgramListeSatiri,
|
ProgramListeSatiri,
|
||||||
PuanTuruKey,
|
PuanTuruKey,
|
||||||
RankResultsListe,
|
RankResultsListe,
|
||||||
|
UniturGrubu,
|
||||||
} from "@/types/yokatlas";
|
} from "@/types/yokatlas";
|
||||||
|
|
||||||
// Tabloda kullanılmayan alanlar (fakulte, puan2025) client'a serileşmesin.
|
// 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,
|
// /sonuc'un ham tablosu: sıralamayla ulaşılabilen programlar,
|
||||||
// hayal/dengeli/garanti dilimli, dilim başına ilk 20 satır.
|
// 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(
|
export function siraylaProgramAra(
|
||||||
sira: number,
|
sira: number,
|
||||||
tur: PuanTuruKey,
|
tur: PuanTuruKey,
|
||||||
|
opts: { iller?: string[]; uniturGrubu?: UniturGrubu } = {},
|
||||||
): RankResultsListe {
|
): RankResultsListe {
|
||||||
const ham = searchByRank(sira, tur, { limitPerBucket: 20 });
|
const ham = searchByRank(sira, tur, {
|
||||||
|
limitPerBucket: 20,
|
||||||
|
iller: opts.iller,
|
||||||
|
uniturGrubu: opts.uniturGrubu,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
hayal: ham.hayal.map(programOzetle),
|
hayal: ham.hayal.map(programOzetle),
|
||||||
dengeli: ham.dengeli.map(programOzetle),
|
dengeli: ham.dengeli.map(programOzetle),
|
||||||
|
|||||||
@@ -126,16 +126,29 @@ export function tercihProfiliDogrula(girdi: unknown): TercihProfili | null {
|
|||||||
return { sira, tur: p.tur, secimler };
|
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(
|
export function sonucHref(
|
||||||
profil: Pick<TercihProfili, "sira" | "tur">,
|
profil: Pick<TercihProfili, "sira" | "tur"> & {
|
||||||
|
secimler?: SihirbazSecimleri;
|
||||||
|
},
|
||||||
ekstra?: Record<string, string>,
|
ekstra?: Record<string, string>,
|
||||||
): string {
|
): string {
|
||||||
const q = new URLSearchParams({
|
const q = new URLSearchParams({
|
||||||
sira: String(profil.sira),
|
sira: String(profil.sira),
|
||||||
tur: profil.tur,
|
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}`;
|
return `/sonuc?${q}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user