From 0c6c9da56cf2cb49bc0340b7fc54005614d8a3a3 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Sun, 9 Aug 2026 00:12:08 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20/sonuc=20ham=20tablosuna=20sihirbaz=20i?= =?UTF-8?q?l=20+=20devlet/vak=C4=B1f=20filtresi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/api/programlar/route.ts | 10 +++ src/app/sonuc/page.tsx | 28 ++++++- .../liste/components/program-tablosu.tsx | 80 +++++++++++++++++-- .../components/sonuc-program-tablosu.tsx | 18 ++++- src/features/liste/liste-queries.ts | 16 +++- src/features/sihirbaz/sihirbaz-sabitler.ts | 19 ++++- 6 files changed, 152 insertions(+), 19 deletions(-) diff --git a/src/app/api/programlar/route.ts b/src/app/api/programlar/route.ts index c5950cd..9da54f3 100644 --- a/src/app/api/programlar/route.ts +++ b/src/app/api/programlar/route.ts @@ -36,9 +36,19 @@ export function GET(request: NextRequest) { 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, { limit: SAYFA_BOYU, offset, + iller, + uniturGrubu, }); // Tablo satırının kullanmadığı alanlar payload'a girmesin return NextResponse.json({ programlar: programlar.map(programOzetle), toplam }); diff --git a/src/app/sonuc/page.tsx b/src/app/sonuc/page.tsx index 9f033ef..9dc8be8 100644 --- a/src/app/sonuc/page.tsx +++ b/src/app/sonuc/page.tsx @@ -28,6 +28,8 @@ export default function SonucPage({ tur?: string; sihirbaz?: string; hazir?: string; + il?: string | string[]; + tip?: string; }>; }) { return ( @@ -51,6 +53,22 @@ export default function SonucPage({ params.tur && params.tur in PUAN_TURLERI ? (params.tur as PuanTuruKey) : "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) { return ( @@ -71,8 +89,14 @@ export default function SonucPage({ {/* 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 - rozetinden değiştirilir */} - + rozetinden değiştirilir. Sihirbaz seçimleri (il, devlet/ + vakıf) URL paramlarıyla tabloya uygulanır. */} + {/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek. Havuzda içerik yoksa bileşen hiç render edilmez. */} diff --git a/src/features/liste/components/program-tablosu.tsx b/src/features/liste/components/program-tablosu.tsx index 129f87c..207ed60 100644 --- a/src/features/liste/components/program-tablosu.tsx +++ b/src/features/liste/components/program-tablosu.tsx @@ -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(() => 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(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 ? ( +
+ + Filtreler: + + {filtreTip ? ( + + ) : null} + {iller.map((il) => ( + + ))} +
+ ) : null} + setAktifDilim(v as DilimKey)} diff --git a/src/features/liste/components/sonuc-program-tablosu.tsx b/src/features/liste/components/sonuc-program-tablosu.tsx index dae1317..b34ca89 100644 --- a/src/features/liste/components/sonuc-program-tablosu.tsx +++ b/src/features/liste/components/sonuc-program-tablosu.tsx @@ -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 ; + const sonuclar = siraylaProgramAra(sira, tur, { iller, uniturGrubu }); + return ( + + ); } export function SonucProgramTablosuSkeleton() { diff --git a/src/features/liste/liste-queries.ts b/src/features/liste/liste-queries.ts index 597344b..569f366 100644 --- a/src/features/liste/liste-queries.ts +++ b/src/features/liste/liste-queries.ts @@ -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), diff --git a/src/features/sihirbaz/sihirbaz-sabitler.ts b/src/features/sihirbaz/sihirbaz-sabitler.ts index 1e6a948..f923335 100644 --- a/src/features/sihirbaz/sihirbaz-sabitler.ts +++ b/src/features/sihirbaz/sihirbaz-sabitler.ts @@ -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, + profil: Pick & { + secimler?: SihirbazSecimleri; + }, ekstra?: Record, ): 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}`; }