diff --git a/src/components/arama-funnel-karti.tsx b/src/components/arama-funnel-karti.tsx index ec7da05..29b8c5a 100644 --- a/src/components/arama-funnel-karti.tsx +++ b/src/components/arama-funnel-karti.tsx @@ -102,18 +102,9 @@ export function AramaFunnelKarti({ onSec }: { onSec: () => void }) { return (
-
-
- -
+
void }) {
diff --git a/src/features/liste/components/tercih-haritasi.tsx b/src/features/liste/components/tercih-haritasi.tsx index cc68f7c..0af9813 100644 --- a/src/features/liste/components/tercih-haritasi.tsx +++ b/src/features/liste/components/tercih-haritasi.tsx @@ -25,7 +25,7 @@ import { HARITA_YUKSEKLIK, normalizeIlAdi, } from "@/lib/harita"; -import type { RiskSeviyesi } from "@/lib/risk"; +import { RISK_PIN_RENK as RISK_RENK, type RiskSeviyesi } from "@/lib/risk"; export type HaritaPin = { /** Üniversite bazlı benzersiz anahtar (kilitlilerde sentetik) */ @@ -38,16 +38,12 @@ export type HaritaPin = { y: number; /** Bu üniversitedeki tercihlerin liste sıraları (1-24) */ tercihSiralari: number[]; + /** tercihSiralari ile hizalı bölüm adları — kilitli pinlerde boş string */ + bolumler: string[]; risk: RiskSeviyesi; kilitli?: boolean; }; -const RISK_RENK: Record = { - guvenli: "oklch(0.696 0.17 162.5)", // emerald-500 - "az-riskli": "oklch(0.769 0.188 70.1)", // amber-500 - riskli: "oklch(0.637 0.237 25.3)", // red-500 -}; - const TAM_GORUNUM = { x: 0, y: HARITA_UST_KIRPMA, @@ -287,7 +283,13 @@ export function TercihHaritasi({ const seritTercihleri = useMemo( () => seciliPinler - .flatMap((p) => p.tercihSiralari.map((sira) => ({ pin: p, sira }))) + .flatMap((p) => + p.tercihSiralari.map((sira, i) => ({ + pin: p, + sira, + bolum: p.bolumler[i] ?? "", + })), + ) .sort((a, b) => a.sira - b.sira), [seciliPinler], ); @@ -500,18 +502,18 @@ export function TercihHaritasi({ {/* Seçili ilin tercih şeridi (yalnızca zoom'dayken) */} {zoomda && seciliPinler.length > 0 ? (
-

- {seciliIl!.toLocaleLowerCase("tr-TR")} tercihlerin -

-
    - {seritTercihleri.map(({ pin, sira }) => { +
      + {seritTercihleri.map(({ pin, sira, bolum }) => { const logo = pin.kilitli ? null : uniLogoYoluFromAd(pin.universite); return (
{pinVerisi.haritaDisi > 0 ? (

- {pinVerisi.haritaDisi} tercih harita dışında (KKTC vb.) + {pinVerisi.haritaDisi} tercih haritada gösterilemiyor

) : null}
diff --git a/src/lib/harita-pinler.ts b/src/lib/harita-pinler.ts index 346b0d1..be6f13d 100644 --- a/src/lib/harita-pinler.ts +++ b/src/lib/harita-pinler.ts @@ -3,6 +3,11 @@ import type { RaporSonuc } from "@/features/rapor/types/rapor"; import type { HaritaPin } from "@/features/liste/components/tercih-haritasi"; import { ilKonum, uniAdiNormalize, uniKonum } from "@/lib/harita"; +import { + yurtdisiIlKonum, + yurtdisiUniKonum, + type UlkeKodu, +} from "@/lib/harita-ulkeler"; import { dilimdenRisk, riskHesapla, type RiskSeviyesi } from "@/lib/risk"; const RISK_AGIRLIK: Record = { @@ -11,18 +16,71 @@ const RISK_AGIRLIK: Record = { riskli: 2, }; +/** Türkiye dışındaki tercihlerin ülke mini haritalarında şehir başına pini. */ +export type YurtdisiPin = { + id: string; + ulke: UlkeKodu; + ulkeAd: string; + sehir: string; + x: number; + y: number; + /** Bu şehirdeki tercihlerin liste sıraları (1-24) */ + tercihSiralari: number[]; + /** tercihSiralari ile hizalı — pin rengi çoğunluk riskiyle belirlenir */ + riskler: RiskSeviyesi[]; +}; + +/** + * Yurtdışı tercihini şehir pinine ekler; şehir çözülemezse false (harita + * dışı). Kilitli satırlarda universite null verilir — pin şehir bazlı + * olduğundan üniversite kimliği zaten görünmez. + */ +export function yurtdisiPinKaydet( + gruplar: Map, + universite: string | null, + il: string | null, + sira: number, + risk: RiskSeviyesi, +): boolean { + const konum = universite + ? yurtdisiUniKonum(universite, il) + : yurtdisiIlKonum(il); + if (!konum) return false; + const key = `${konum.ulke}|${konum.sehir}`; + const mevcut = gruplar.get(key); + if (mevcut) { + mevcut.tercihSiralari.push(sira); + mevcut.riskler.push(risk); + } else { + gruplar.set(key, { + id: key, + ...konum, + tercihSiralari: [sira], + riskler: [risk], + }); + } + return true; +} + /** * Rapordan harita pinleri türetir. Açık satırlar üniversite bazında gruplanır * (kampüs konumunda, adıyla); kilitli satırlar il başına tek adsız "?" pinine * toplanır — maskeli veride üniversite adı zaten client'a inmediği için - * harita paywall'ı delemez. Haritada karşılığı olmayan tercihler sayılır. + * harita paywall'ı delemez. Türkiye dışındaki tercihler ülke mini haritaları + * için şehir bazlı yurtdışı pinlerine ayrılır; orada da karşılığı olmayanlar + * sayılır. */ export function pinleriTuret( rapor: RaporSonuc, adaySira: number, kilitliBaslangic: number, -): { pinler: HaritaPin[]; haritaDisi: number } { +): { + pinler: HaritaPin[]; + yurtdisiPinler: YurtdisiPin[]; + haritaDisi: number; +} { const gruplar = new Map(); + const yurtdisi = new Map(); let haritaDisi = 0; rapor.tercihler.forEach((t, i) => { @@ -34,13 +92,16 @@ export function pinleriTuret( if (kilitli) { const konum = ilKonum(p.il); if (!konum) { - haritaDisi += 1; + if (!yurtdisiPinKaydet(yurtdisi, null, p.il, t.sira, risk)) + haritaDisi += 1; return; } const key = `kilit|${p.il}`; const mevcut = gruplar.get(key); if (mevcut) { mevcut.tercihSiralari.push(t.sira); + // Kilitli satırda bölüm adı da gösterilmez — şeritte "Kilitli tercih" + mevcut.bolumler.push(""); if (RISK_AGIRLIK[risk] > RISK_AGIRLIK[mevcut.risk]) mevcut.risk = risk; } else { // Açık pinlerle üst üste binmesin diye il merkezinin hafif altına @@ -51,6 +112,7 @@ export function pinleriTuret( x: konum.x, y: konum.y + 9, tercihSiralari: [t.sira], + bolumler: [""], risk, kilitli: true, }); @@ -60,13 +122,15 @@ export function pinleriTuret( const konum = uniKonum(p.universite, p.il); if (!konum) { - haritaDisi += 1; + if (!yurtdisiPinKaydet(yurtdisi, p.universite, p.il, t.sira, risk)) + haritaDisi += 1; return; } const key = `${uniAdiNormalize(p.universite)}|${p.il ?? ""}`; const mevcut = gruplar.get(key); if (mevcut) { mevcut.tercihSiralari.push(t.sira); + mevcut.bolumler.push(p.isim); if (RISK_AGIRLIK[risk] > RISK_AGIRLIK[mevcut.risk]) mevcut.risk = risk; } else { gruplar.set(key, { @@ -76,10 +140,15 @@ export function pinleriTuret( x: konum.x, y: konum.y, tercihSiralari: [t.sira], + bolumler: [p.isim], risk, }); } }); - return { pinler: [...gruplar.values()], haritaDisi }; + return { + pinler: [...gruplar.values()], + yurtdisiPinler: [...yurtdisi.values()], + haritaDisi, + }; } diff --git a/src/lib/harita-ulkeler.ts b/src/lib/harita-ulkeler.ts index d9359f7..6fb5154 100644 --- a/src/lib/harita-ulkeler.ts +++ b/src/lib/harita-ulkeler.ts @@ -6,6 +6,7 @@ import { type UlkeKodu, type UlkeHarita, } from "./harita-ulkeler-verisi"; +import type { LatLng } from "./uni-koordinatlar"; export { ULKE_HARITALARI, @@ -25,23 +26,41 @@ export const ULKE_ISO: Record = { arnavutluk: "AL", }; -type Sehir = { ulke: UlkeKodu; ad: string }; +type Sehir = { ulke: UlkeKodu; ad: string; koord: LatLng }; /** DB'deki il değerlerinden ve düzeltme tablosundan şehir anahtarları. */ const SEHIRLER: Record = { - GİRNE: { ulke: "kktc", ad: "Girne" }, - LEFKOŞA: { ulke: "kktc", ad: "Lefkoşa" }, - GAZİMAĞUSA: { ulke: "kktc", ad: "Gazimağusa" }, - GÜZELYURT: { ulke: "kktc", ad: "Güzelyurt" }, - LEFKE: { ulke: "kktc", ad: "Lefke" }, - "BAKÜ-AZERBAYCAN": { ulke: "azerbaycan", ad: "Bakü" }, - "BİŞKEK-KIRGIZİSTAN": { ulke: "kirgizistan", ad: "Bişkek" }, - "TÜRKİSTAN-KAZAKİSTAN": { ulke: "kazakistan", ad: "Türkistan" }, - "SARAYBOSNA - BOSNA - HERSEK": { ulke: "bosna-hersek", ad: "Saraybosna" }, - ÜSKÜP: { ulke: "makedonya", ad: "Üsküp" }, - TİRAN: { ulke: "arnavutluk", ad: "Tiran" }, + GİRNE: { ulke: "kktc", ad: "Girne", koord: [35.341, 33.319] }, + LEFKOŞA: { ulke: "kktc", ad: "Lefkoşa", koord: [35.19, 33.365] }, + GAZİMAĞUSA: { ulke: "kktc", ad: "Gazimağusa", koord: [35.125, 33.941] }, + GÜZELYURT: { ulke: "kktc", ad: "Güzelyurt", koord: [35.198, 32.994] }, + LEFKE: { ulke: "kktc", ad: "Lefke", koord: [35.111, 32.849] }, + "BAKÜ-AZERBAYCAN": { + ulke: "azerbaycan", + ad: "Bakü", + koord: [40.409, 49.867], + }, + "BİŞKEK-KIRGIZİSTAN": { + ulke: "kirgizistan", + ad: "Bişkek", + koord: [42.875, 74.603], + }, + "TÜRKİSTAN-KAZAKİSTAN": { + ulke: "kazakistan", + ad: "Türkistan", + koord: [43.297, 68.252], + }, + "SARAYBOSNA - BOSNA - HERSEK": { + ulke: "bosna-hersek", + ad: "Saraybosna", + koord: [43.856, 18.413], + }, + ÜSKÜP: { ulke: "makedonya", ad: "Üsküp", koord: [41.997, 21.428] }, + TİRAN: { ulke: "arnavutluk", ad: "Tiran", koord: [41.328, 19.818] }, }; +type SehirAnahtari = keyof typeof SEHIRLER; + /** * Kaynak verideki gürültü için üniversite adına göre düzeltme: * - Uluslararası Balkan Üniversitesi Üsküp'tedir; YÖK Atlas satırlarının @@ -51,7 +70,7 @@ const SEHIRLER: Record = { * null → Türkiye haritasında kalır. * Anahtar: uniAdiNormalize edilmiş adın tr-TR büyük harf hali. */ -const UNI_DUZELTME: Record = { +const UNI_DUZELTME: Record = { "ULUSLARARASI BALKAN ÜNİVERSİTESİ": "ÜSKÜP", "TİRAN NEW YORK ÜNİVERSİTESİ": "TİRAN", "KIBRIS AYDIN ÜNİVERSİTESİ": "GİRNE", // il alanı yalnızca "KIBRIS" @@ -62,6 +81,24 @@ const UNI_DUZELTME: Record = { /** il değeri Türkiye dışını mı gösteriyor? ("KIBRIS" genel değeri dahil) */ const YURTDISI_IL = new Set([...Object.keys(SEHIRLER), "KIBRIS"]); +/** Üniversite adı + il'den şehir anahtarı; şehri bilinmeyen KKTC'de ülke. */ +type Cozum = { anahtar: SehirAnahtari } | { ulke: UlkeKodu } | null; + +function coz(universite: string, il: string | null): Cozum { + const ad = universite + .replace(/\s*\([^)]*\)\s*$/, "") + .trim() + .toLocaleUpperCase("tr-TR"); + + if (ad in UNI_DUZELTME) { + const anahtar = UNI_DUZELTME[ad]; + return anahtar === null ? null : { anahtar }; + } + if (!il || !YURTDISI_IL.has(il)) return null; + if (il in SEHIRLER) return { anahtar: il as SehirAnahtari }; + return { ulke: "kktc" }; // il "KIBRIS": ülke bilinir, şehir bilinmez +} + export type UlkeKonum = { ulke: UlkeKodu; ulkeAd: string; @@ -71,45 +108,59 @@ export type UlkeKonum = { sehir: string | null; }; -function sehirdenKonum(anahtar: keyof typeof SEHIRLER): UlkeKonum { - const sehir = SEHIRLER[anahtar]; - const harita = ULKE_HARITALARI[sehir.ulke]; - return { - ulke: sehir.ulke, - ulkeAd: harita.ad, - iso: ULKE_ISO[sehir.ulke], - harita, - sehir: sehir.ad, - }; -} - /** - * Üniversite Türkiye dışındaysa ülke haritası + şehir konumu döner; + * Üniversite Türkiye dışındaysa ülke haritası + şehir adı döner; * Türkiye'deyse (veya çözülemiyorsa) null — Türkiye haritası kullanılır. */ export function uniUlkeKonum( universite: string, il: string | null, ): UlkeKonum | null { - const ad = universite - .replace(/\s*\([^)]*\)\s*$/, "") - .trim() - .toLocaleUpperCase("tr-TR"); - - if (ad in UNI_DUZELTME) { - const anahtar = UNI_DUZELTME[ad]; - return anahtar === null ? null : sehirdenKonum(anahtar); - } - if (!il || !YURTDISI_IL.has(il)) return null; - if (il in SEHIRLER) return sehirdenKonum(il as keyof typeof SEHIRLER); - - // il "KIBRIS": ülke bilinir, şehir bilinmez — haritayı işaretsiz göster - const harita = ULKE_HARITALARI.kktc; + const cozum = coz(universite, il); + if (!cozum) return null; + const ulke = "anahtar" in cozum ? SEHIRLER[cozum.anahtar].ulke : cozum.ulke; + const harita = ULKE_HARITALARI[ulke]; return { - ulke: "kktc", + ulke, ulkeAd: harita.ad, - iso: ULKE_ISO.kktc, + iso: ULKE_ISO[ulke], harita, - sehir: null, + sehir: "anahtar" in cozum ? SEHIRLER[cozum.anahtar].ad : null, }; } + +/** Yurtdışı şehir merkezinin kendi ülke haritasındaki viewBox konumu. */ +export type YurtdisiKonum = { + ulke: UlkeKodu; + ulkeAd: string; + sehir: string; + x: number; + y: number; +}; + +function sehirKonumu(anahtar: SehirAnahtari): YurtdisiKonum { + const sehir = SEHIRLER[anahtar]; + const { ad, proje } = ULKE_HARITALARI[sehir.ulke]; + const [lat, lng] = sehir.koord; + return { + ulke: sehir.ulke, + ulkeAd: ad, + sehir: sehir.ad, + x: proje.ax * lng + proje.bx, + y: proje.ay * lat + proje.by, + }; +} + +/** Yurtdışı üniversitenin şehir konumu; şehir çözülemiyorsa null. */ +export function yurtdisiUniKonum( + universite: string, + il: string | null, +): YurtdisiKonum | null { + const cozum = coz(universite, il); + return cozum && "anahtar" in cozum ? sehirKonumu(cozum.anahtar) : null; +} + +/** Yalnızca il değerinden (kilitli satırlar) yurtdışı şehir konumu. */ +export function yurtdisiIlKonum(il: string | null): YurtdisiKonum | null { + return il && il in SEHIRLER ? sehirKonumu(il as SehirAnahtari) : null; +} diff --git a/src/lib/risk.ts b/src/lib/risk.ts index 54121a8..4b7c151 100644 --- a/src/lib/risk.ts +++ b/src/lib/risk.ts @@ -12,6 +12,13 @@ export const RISK_ETIKET: Record = { riskli: "Riskli", }; +/** Harita pinlerinin SVG dolgu renkleri — risk renk dilinin oklch karşılığı. */ +export const RISK_PIN_RENK: Record = { + guvenli: "oklch(0.696 0.17 162.5)", // emerald-500 + "az-riskli": "oklch(0.769 0.188 70.1)", // amber-500 + riskli: "oklch(0.637 0.237 25.3)", // red-500 +}; + /** * Deterministik risk: programın efektif taban sıralaması (COALESCE 2025→2024) * adayın sıralamasıyla kıyaslanır. Eşikler db.ts'teki dilim aralıklarıyla