Files
kolaytercih/src/features/sihirbaz/sihirbaz-sabitler.ts
bilalgursen 0c6c9da56c 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>
2026-08-09 00:12:08 +03:00

171 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Sihirbazın seçim şeması, doğrulaması ve prompt özeti — izomorfik katman:
// hem client bileşenleri hem server action'ları/AI motoru buradan okur.
// localStorage'a dokunan profil yardımcıları sihirbaz-profil.ts'te yaşar.
// Kategoriler/iller sıralamaya göre dinamik üretilir (bkz. rankWindowFacets);
// öncelikler statik kalır (veriye bağlı değil, tercih niyeti).
import { KATEGORILER } from "@/lib/kategoriler";
import type { UniturGrubu } from "@/types/yokatlas";
export const ONCELIKLER = [
"İş garantisi",
"Yüksek maaş potansiyeli",
"Sevdiğim işi yapmak",
"Akademik kariyer",
"Yurtdışı imkânı",
"Prestijli üniversite",
"Kolay yerleşme",
] as const;
export const UNIVERSITE_TIPI_ETIKET: Record<UniturGrubu | "farketmez", string> = {
devlet: "Devlet",
vakif: "Vakıf (burslu olursa)",
farketmez: "Fark etmez",
};
export type UniversiteTipi = UniturGrubu | "farketmez";
export const PUAN_TURU_ETIKET: Record<string, string> = {
say: "Sayısal",
ea: "Eşit Ağırlık",
soz: "Sözel",
dil: "Dil",
tyt: "TYT",
};
// Girişsiz kullanıcının modalda yaptığı seçimler, giriş sonrası /sonuc'a
// dönene kadar burada bekler.
export const SIHIRBAZ_STORAGE_KEY = "kolaytercih.sihirbaz";
// storage olayı yalnızca diğer sekmelerde tetiklenir; aynı sekmedeki
// aboneler (ör. navbar sıralama rozeti) bu olayla haberdar edilir.
export const PROFIL_DEGISTI_EVENT = "kolaytercih:profil-degisti";
// Arama modalındaki funnel CTA'sı /sonuc üzerindeyken sihirbaz
// orkestratörünü (SihirbazBolumu) bu olayla açar.
export const SIHIRBAZ_AC_EVENT = "kolaytercih:sihirbaz-ac";
export interface SihirbazSecimleri {
kategoriler: string[];
iller: string[];
universiteTipi: UniversiteTipi;
oncelikler: string[];
}
export interface TercihProfili {
sira: number;
tur: string;
secimler: SihirbazSecimleri;
}
const UNI_TIPLERI: UniversiteTipi[] = ["devlet", "vakif", "farketmez"];
/**
* Client'tan gelen seçimleri güvene alır. Kategoriler KATEGORILER allowlist'ine
* göre süzülür; iller serbest string (yalnızca sanity — DB IN filtresi zaten
* eşleşmeyeni yok sayar), en fazla 5 il; öncelikler ONCELIKLER'e göre süzülür.
* En az bir kategori zorunlu.
*/
export function sihirbazDogrula(girdi: unknown): SihirbazSecimleri | null {
if (typeof girdi !== "object" || girdi === null) return null;
const p = girdi as Record<string, unknown>;
const kategoriler = Array.isArray(p.kategoriler)
? [
...new Set(
p.kategoriler.filter(
(x): x is string =>
typeof x === "string" &&
(KATEGORILER as readonly string[]).includes(x),
),
),
]
: [];
if (kategoriler.length === 0) return null;
const iller = Array.isArray(p.iller)
? [
...new Set(
p.iller.filter(
(x): x is string => typeof x === "string" && x.trim().length > 0,
),
),
].slice(0, 5)
: [];
const universiteTipi =
typeof p.universiteTipi === "string" &&
UNI_TIPLERI.includes(p.universiteTipi as UniversiteTipi)
? (p.universiteTipi as UniversiteTipi)
: "farketmez";
const oncelikler = Array.isArray(p.oncelikler)
? [
...new Set(
p.oncelikler.filter(
(x): x is string =>
typeof x === "string" &&
(ONCELIKLER as readonly string[]).includes(x),
),
),
]
: [];
return { kategoriler, iller, universiteTipi, oncelikler };
}
/** localStorage'taki ortak sıra + sihirbaz profilini güvenle doğrular. */
export function tercihProfiliDogrula(girdi: unknown): TercihProfili | null {
if (typeof girdi !== "object" || girdi === null) return null;
const p = girdi as Record<string, unknown>;
const sira = typeof p.sira === "number" ? p.sira : Number(p.sira);
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) return null;
if (typeof p.tur !== "string" || p.tur.trim().length === 0) return null;
const secimler = sihirbazDogrula(p.secimler);
if (!secimler) return null;
return { sira, tur: p.tur, secimler };
}
/**
* 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"> & {
secimler?: SihirbazSecimleri;
},
ekstra?: Record<string, string>,
): string {
const q = new URLSearchParams({
sira: String(profil.sira),
tur: profil.tur,
});
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}`;
}
/** Prompt'a enjekte edilecek Türkçe özet. */
export function secimOzeti(secimler: SihirbazSecimleri): string {
const satirlar = [
`- İlgi alanları: ${secimler.kategoriler.join(", ")}`,
];
if (secimler.iller.length > 0) {
satirlar.push(`- Tercih ettiği iller: ${secimler.iller.join(", ")}`);
}
satirlar.push(
`- Üniversite tipi tercihi: ${UNIVERSITE_TIPI_ETIKET[secimler.universiteTipi]}`,
);
if (secimler.oncelikler.length > 0) {
satirlar.push(`- Öncelikleri: ${secimler.oncelikler.join(", ")}`);
}
return satirlar.join("\n");
}