// 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 = { devlet: "Devlet", vakif: "Vakıf (burslu olursa)", farketmez: "Fark etmez", }; export type UniversiteTipi = UniturGrubu | "farketmez"; export const PUAN_TURU_ETIKET: Record = { 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; 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; 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 & { secimler?: SihirbazSecimleri; }, ekstra?: Record, ): 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"); }