// 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 = [ "İş bulma kolaylığı", "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"; // Profilin sunucuya giden kopyası: /sonuc tablosu SSR'da bu çerezden // filtrelenir, URL'de sira/il/tip paramı taşınmaz. localStorage client // tarafındaki tek doğruluk kaynağı olmayı sürdürür; çerez onun aynasıdır // (tercihProfiliYaz/Sil ikisini birlikte günceller). export const PROFIL_CEREZ_ADI = "kolaytercih.profil"; // 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 // Eski kayıtlı profiller: çip adı "İş garantisi" idi (vaat diliyle // çeliştiği için değişti); seçim kaybolmasın diye yeni ada eşlenir. .map(oncelikNormalize) .filter( (x): x is string => typeof x === "string" && (ONCELIKLER as readonly string[]).includes(x), ), ), ] : []; return { kategoriler, iller, universiteTipi, oncelikler }; } /** * Eski çip adı "İş garantisi" → "İş bulma kolaylığı". Kayıtlı profil ve eski * rapor parametreleri eski adı taşıyabilir; karşılaştırma/gösterim öncesi * yeni ada eşlenir ki ad değişikliği "tercihlerin değişmiş" diye okunmasın. */ export function oncelikNormalize(x: T): T | string { return x === "İş garantisi" ? "İş bulma kolaylığı" : x; } /** 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 }; } /** /sonuc çerezindeki ham değeri güvenle profile çözer (SSR tarafı). */ export function cerezdenTercihProfili( ham: string | undefined | null, ): TercihProfili | null { if (!ham) return null; try { return tercihProfiliDogrula(JSON.parse(decodeURIComponent(ham))); } catch { return null; } } /** * Sonuç sayfası URL'i. Profil verisi (sıra/tür/il/tip) URL'de taşınmaz — * /sonuc'un ham tablosu SSR'da PROFIL_CEREZ_ADI çerezinden filtrelenir. * `ekstra` yalnızca tek seferlik akış bayrakları içindir (sihirbaz=1, hazir=1). */ export function sonucHref(ekstra?: Record): string { const girdiler = Object.entries(ekstra ?? {}); if (girdiler.length === 0) return "/sonuc"; return `/sonuc?${new URLSearchParams(girdiler)}`; } /** * Üretimin koştuğu /listem URL'i. `kaynak` denemenin hangi yüzeyden * başladığını ölçer (izinli değerler: * rapor/components/uretim-basladi-olayi.tsx — yeni yüzeyi önce oraya ekle). * * `uret=1` HER ZAMAN ilk parametre kalmalı: giris-karti.tsx funnel metnini * `callbackURL.startsWith("/listem?uret=1")` ile seçiyor, sıra bozulursa * girişteki "Seçimlerin hazır" metni sessizce kaybolur. Bu sözleşmenin * literal kopyası çoğalmasın diye tek kaynak burası. */ export function listemUretHref(kaynak: string): string { return `/listem?uret=1&kaynak=${kaynak}`; } /** 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"); }