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>
171 lines
5.5 KiB
TypeScript
171 lines
5.5 KiB
TypeScript
// 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");
|
||
}
|