Files
kolaytercih/src/features/sihirbaz/sihirbaz-sabitler.ts
bilalgursen 5311459073 refactor(sihirbaz): listemUretHref tek kaynağa taşınır
Yerel fonksiyon sihirbaz-sabitler.ts'e (sonucHref'in yanına) taşındı ve
export edildi; `uret=1` parametre sırası sözleşmesinin yorumu da onunla
gitti. Davranış birebir aynı — tadımlık CTA'sı da aynı hedefi kullanacağı
için literal kopyanın dördüncü kez çoğalması engellendi.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 02:18:22 +03:00

201 lines
6.9 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 = [
"İş 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<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";
// 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<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
// 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<T>(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<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 };
}
/** /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, string>): 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");
}