refactor: optimize AramaFunnelKarti styles and enhance city data structure
All checks were successful
Deploy / deploy (push) Successful in 7m30s
All checks were successful
Deploy / deploy (push) Successful in 7m30s
Removed unnecessary elements and optimized styles in the AramaFunnelKarti component for improved layout. Updated the city data structure in harita-ulkeler.ts to include coordinates, enhancing geographical data handling. Additionally, refactored risk color definitions in risk.ts for better clarity and maintainability.
This commit is contained in:
@@ -102,18 +102,9 @@ export function AramaFunnelKarti({ onSec }: { onSec: () => void }) {
|
||||
return (
|
||||
<section
|
||||
aria-label="Tercih listeni kur"
|
||||
className="relative mt-5 overflow-hidden rounded-2xl border border-blue-400/20 bg-[#0b1f3a] px-4 py-5 text-white shadow-[0_20px_50px_-28px_rgba(15,71,150,0.65)] sm:px-5"
|
||||
className="mt-5 overflow-hidden rounded-2xl border border-blue-400/20 bg-[#0b1f3a] px-4 py-5 text-white sm:px-5"
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -right-12 -top-16 size-44 rounded-full bg-blue-500/25 blur-3xl"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -bottom-14 left-1/3 size-36 rounded-full bg-cyan-400/10 blur-3xl"
|
||||
/>
|
||||
|
||||
<div className="relative grid items-center gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:gap-5">
|
||||
<div className="grid items-center gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:gap-5">
|
||||
<div className="min-w-0">
|
||||
<TypingAnimation
|
||||
as="h3"
|
||||
@@ -141,7 +132,7 @@ export function AramaFunnelKarti({ onSec }: { onSec: () => void }) {
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={sihirbazaGit}
|
||||
className="w-full shrink-0 cursor-pointer bg-orange-500 font-semibold text-white shadow-[0_8px_24px_rgba(249,115,22,0.28)] transition-[background-color,transform,box-shadow] duration-200 hover:bg-orange-400 hover:shadow-[0_10px_28px_rgba(249,115,22,0.36)] active:scale-[0.98] focus-visible:ring-white/70 sm:w-auto"
|
||||
className="w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 focus-visible:ring-white/70 sm:w-auto"
|
||||
>
|
||||
Yapay Zeka listemi kur
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { TercihHaritasiLazy } from "@/features/liste/components/tercih-haritasi-lazy";
|
||||
import { YurtdisiHaritalarLazy } from "@/features/liste/components/yurtdisi-haritalar-lazy";
|
||||
import type { HaritaPin } from "@/features/liste/components/tercih-haritasi";
|
||||
import { ilKonum, uniAdiNormalize, uniKonum } from "@/lib/harita";
|
||||
import { yurtdisiPinKaydet, type YurtdisiPin } from "@/lib/harita-pinler";
|
||||
import type { RiskSeviyesi } from "@/lib/risk";
|
||||
import { useManuelListe } from "../liste-store";
|
||||
|
||||
@@ -22,21 +24,24 @@ export function ManuelHarita() {
|
||||
const liste = useManuelListe();
|
||||
const [seciliIl, setSeciliIl] = useState<string | null>(null);
|
||||
|
||||
const { pinler, haritaDisi } = useMemo(() => {
|
||||
const { pinler, yurtdisiPinler, haritaDisi } = useMemo(() => {
|
||||
const gruplar = new Map<string, HaritaPin>();
|
||||
const yurtdisi = new Map<string, YurtdisiPin>();
|
||||
let haritaDisi = 0;
|
||||
|
||||
liste.forEach((t, i) => {
|
||||
const risk = t.risk ?? "az-riskli";
|
||||
const konum = uniKonum(t.universite, t.il) ?? ilKonum(t.il);
|
||||
if (!konum) {
|
||||
haritaDisi += 1;
|
||||
if (!yurtdisiPinKaydet(yurtdisi, t.universite, t.il, i + 1, risk))
|
||||
haritaDisi += 1;
|
||||
return;
|
||||
}
|
||||
const risk = t.risk ?? "az-riskli";
|
||||
const key = `${uniAdiNormalize(t.universite)}|${t.il ?? ""}`;
|
||||
const mevcut = gruplar.get(key);
|
||||
if (mevcut) {
|
||||
mevcut.tercihSiralari.push(i + 1);
|
||||
mevcut.bolumler.push(t.isim);
|
||||
if (RISK_AGIRLIK[risk] > RISK_AGIRLIK[mevcut.risk]) mevcut.risk = risk;
|
||||
} else {
|
||||
gruplar.set(key, {
|
||||
@@ -46,12 +51,17 @@ export function ManuelHarita() {
|
||||
x: konum.x,
|
||||
y: konum.y,
|
||||
tercihSiralari: [i + 1],
|
||||
bolumler: [t.isim],
|
||||
risk,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { pinler: [...gruplar.values()], haritaDisi };
|
||||
return {
|
||||
pinler: [...gruplar.values()],
|
||||
yurtdisiPinler: [...yurtdisi.values()],
|
||||
haritaDisi,
|
||||
};
|
||||
}, [liste]);
|
||||
|
||||
// Liste boşken 197 KB'lık il geometrisi hiç inmesin: aynı oranlı
|
||||
@@ -76,9 +86,12 @@ export function ManuelHarita() {
|
||||
seciliIl={seciliIl}
|
||||
onSeciliIlDegisti={setSeciliIl}
|
||||
/>
|
||||
{yurtdisiPinler.length > 0 ? (
|
||||
<YurtdisiHaritalarLazy pinler={yurtdisiPinler} />
|
||||
) : null}
|
||||
{haritaDisi > 0 ? (
|
||||
<p className="mt-1 px-1 text-xs text-slate-500">
|
||||
{haritaDisi} tercih harita dışında (KKTC vb.)
|
||||
{haritaDisi} tercih haritada gösterilemiyor
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -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<RiskSeviyesi, string> = {
|
||||
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 ? (
|
||||
<div className="mt-2">
|
||||
<p className="px-1 pb-1 text-xs font-semibold text-slate-500">
|
||||
{seciliIl!.toLocaleLowerCase("tr-TR")} tercihlerin
|
||||
</p>
|
||||
<ul className="flex gap-2 overflow-x-auto pb-1">
|
||||
{seritTercihleri.map(({ pin, sira }) => {
|
||||
<ul
|
||||
aria-label={`${seciliIl!.toLocaleUpperCase("tr-TR")} tercihlerin`}
|
||||
className="flex gap-2 overflow-x-auto pb-1"
|
||||
>
|
||||
{seritTercihleri.map(({ pin, sira, bolum }) => {
|
||||
const logo = pin.kilitli ? null : uniLogoYoluFromAd(pin.universite);
|
||||
return (
|
||||
<li key={sira} className="shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onTercihSec ? () => onTercihSec(sira) : undefined}
|
||||
className="flex cursor-pointer items-center gap-2 rounded-full border border-slate-200 bg-white py-1 pl-1.5 pr-3 text-xs transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50"
|
||||
className="flex cursor-pointer items-center gap-2 rounded-full border border-slate-200 bg-white py-1.5 pl-1.5 pr-3 text-xs transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50"
|
||||
>
|
||||
<span
|
||||
className="flex size-5 items-center justify-center rounded-full font-heading text-[10px] font-bold text-white"
|
||||
@@ -525,19 +527,26 @@ export function TercihHaritasi({
|
||||
Kilitli tercih
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex min-w-0 items-center gap-1.5">
|
||||
<span className="inline-flex min-w-0 items-center gap-2">
|
||||
{logo ? (
|
||||
<Image
|
||||
src={logo}
|
||||
alt=""
|
||||
width={16}
|
||||
height={16}
|
||||
width={20}
|
||||
height={20}
|
||||
aria-hidden
|
||||
className="size-4 shrink-0 object-contain"
|
||||
className="size-5 shrink-0 object-contain"
|
||||
/>
|
||||
) : null}
|
||||
<span className="max-w-40 truncate font-medium text-slate-700">
|
||||
{kisaUniAdi(pin.universite)}
|
||||
<span className="flex min-w-0 flex-col items-start leading-tight">
|
||||
<span className="max-w-48 truncate font-medium text-slate-700">
|
||||
{bolum || kisaUniAdi(pin.universite)}
|
||||
</span>
|
||||
{bolum ? (
|
||||
<span className="max-w-48 truncate text-[10px] text-slate-500">
|
||||
{kisaUniAdi(pin.universite)}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
11
src/features/liste/components/yurtdisi-haritalar-lazy.tsx
Normal file
11
src/features/liste/components/yurtdisi-haritalar-lazy.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
// Ülke kontür verisi (~70 KB) yalnızca listede yurtdışı tercih varsa insin.
|
||||
// Çağıran taraf pinler boşken hiç render etmemeli ki chunk boşuna inmesin.
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
export const YurtdisiHaritalarLazy = dynamic(
|
||||
() => import("./yurtdisi-haritalar").then((m) => m.YurtdisiHaritalar),
|
||||
{ ssr: false },
|
||||
);
|
||||
134
src/features/liste/components/yurtdisi-haritalar.tsx
Normal file
134
src/features/liste/components/yurtdisi-haritalar.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
// Türkiye dışındaki tercihlerin ülke mini haritaları: /sonuc ve /listem'de
|
||||
// Türkiye haritasının altında, listede geçen her ülke için bir kontür ve
|
||||
// şehir başına risk renkli toplu pin. Renk dili Türkiye haritasıyla aynı:
|
||||
// ülke zemini "tercih içeren il" mavisi, pin rengi çoğunluk riski.
|
||||
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
ULKE_HARITALARI,
|
||||
ULKE_HARITA_GENISLIK,
|
||||
ULKE_HARITA_YUKSEKLIK,
|
||||
type UlkeKodu,
|
||||
} from "@/lib/harita-ulkeler";
|
||||
import type { YurtdisiPin } from "@/lib/harita-pinler";
|
||||
import { RISK_PIN_RENK, type RiskSeviyesi } from "@/lib/risk";
|
||||
|
||||
/** Pinin çoğunluk riski (eşitlikte güvenli taraf) — Türkiye il pinleriyle aynı kural. */
|
||||
function baskinRisk(riskler: RiskSeviyesi[]): RiskSeviyesi {
|
||||
const sayac: Record<RiskSeviyesi, number> = {
|
||||
guvenli: 0,
|
||||
"az-riskli": 0,
|
||||
riskli: 0,
|
||||
};
|
||||
for (const r of riskler) sayac[r] += 1;
|
||||
return (["guvenli", "az-riskli", "riskli"] as const).reduce((a, b) =>
|
||||
sayac[b] > sayac[a] ? b : a,
|
||||
);
|
||||
}
|
||||
|
||||
export function YurtdisiHaritalar({
|
||||
pinler,
|
||||
onTercihSec,
|
||||
}: {
|
||||
pinler: YurtdisiPin[];
|
||||
/** Pine dokununca ilgili ilk tercihe in (liste satırına) */
|
||||
onTercihSec?: (sira: number) => void;
|
||||
}) {
|
||||
const ulkeler = useMemo(() => {
|
||||
const gruplar = new Map<UlkeKodu, YurtdisiPin[]>();
|
||||
for (const p of pinler) {
|
||||
const grup = gruplar.get(p.ulke);
|
||||
if (grup) grup.push(p);
|
||||
else gruplar.set(p.ulke, [p]);
|
||||
}
|
||||
return [...gruplar.entries()];
|
||||
}, [pinler]);
|
||||
|
||||
if (ulkeler.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-3 flex flex-wrap items-end justify-center gap-x-6 gap-y-2">
|
||||
{ulkeler.map(([ulke, ulkePinleri]) => {
|
||||
const harita = ULKE_HARITALARI[ulke];
|
||||
const adet = ulkePinleri.reduce(
|
||||
(top, p) => top + p.tercihSiralari.length,
|
||||
0,
|
||||
);
|
||||
return (
|
||||
<figure key={ulke} className="w-52 max-w-full">
|
||||
<svg
|
||||
viewBox={`0 0 ${ULKE_HARITA_GENISLIK} ${ULKE_HARITA_YUKSEKLIK}`}
|
||||
className="h-auto w-full"
|
||||
role="img"
|
||||
aria-label={`${harita.ad} haritasında ${adet} tercih`}
|
||||
>
|
||||
{harita.baglam.map((d, i) => (
|
||||
<path
|
||||
key={`b${i}`}
|
||||
d={d}
|
||||
fill="oklch(0.929 0.013 255.5)" // slate-200
|
||||
stroke="white"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
))}
|
||||
{harita.ulke.map((d, i) => (
|
||||
<path
|
||||
key={`u${i}`}
|
||||
d={d}
|
||||
fill="oklch(0.882 0.059 254.1)" // blue-200: tercih içeren zemin
|
||||
stroke="white"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<title>{harita.ad}</title>
|
||||
</path>
|
||||
))}
|
||||
{ulkePinleri.map((p) => {
|
||||
const ilkSira = Math.min(...p.tercihSiralari);
|
||||
return (
|
||||
<g
|
||||
key={p.id}
|
||||
className={onTercihSec ? "cursor-pointer" : undefined}
|
||||
onClick={
|
||||
onTercihSec ? () => onTercihSec(ilkSira) : undefined
|
||||
}
|
||||
role={onTercihSec ? "button" : undefined}
|
||||
aria-label={`${p.sehir}: ${p.tercihSiralari.length} tercih`}
|
||||
>
|
||||
{/* r ≤ 26: en yakın şehir çifti (Girne–Lefkoşa ~55 birim)
|
||||
çakışmadan sığsın */}
|
||||
<circle
|
||||
cx={p.x}
|
||||
cy={p.y}
|
||||
r={26}
|
||||
fill={RISK_PIN_RENK[baskinRisk(p.riskler)]}
|
||||
stroke="white"
|
||||
strokeWidth={5}
|
||||
/>
|
||||
<text
|
||||
x={p.x}
|
||||
y={p.y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize={26}
|
||||
fontWeight={700}
|
||||
fill="white"
|
||||
className="pointer-events-none select-none"
|
||||
>
|
||||
{p.tercihSiralari.length}
|
||||
</text>
|
||||
<title>{`${p.sehir}: ${p.tercihSiralari.length} tercih`}</title>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
<figcaption className="text-center text-xs font-medium text-slate-500">
|
||||
{harita.ad}
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { RaporListesi } from "./rapor-listesi";
|
||||
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
||||
import { TercihHaritasiLazy } from "@/features/liste/components/tercih-haritasi-lazy";
|
||||
import { YurtdisiHaritalarLazy } from "@/features/liste/components/yurtdisi-haritalar-lazy";
|
||||
import { pinleriTuret } from "@/lib/harita-pinler";
|
||||
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
||||
import { SohbetClient, type DanismanOzeti, type Mesaj } from "./sohbet-client";
|
||||
@@ -98,10 +99,16 @@ export function ListemGovde({
|
||||
onSeciliIlDegisti={setSeciliIl}
|
||||
onTercihSec={setAcikSira}
|
||||
/>
|
||||
{pinVerisi.yurtdisiPinler.length > 0 ? (
|
||||
<YurtdisiHaritalarLazy
|
||||
pinler={pinVerisi.yurtdisiPinler}
|
||||
onTercihSec={setAcikSira}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{pinVerisi.haritaDisi > 0 ? (
|
||||
<p className="mt-1 text-center text-xs text-slate-500">
|
||||
{pinVerisi.haritaDisi} tercih harita dışında (KKTC vb.)
|
||||
{pinVerisi.haritaDisi} tercih haritada gösterilemiyor
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
@@ -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<RiskSeviyesi, number> = {
|
||||
@@ -11,18 +16,71 @@ const RISK_AGIRLIK: Record<RiskSeviyesi, number> = {
|
||||
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<string, YurtdisiPin>,
|
||||
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<string, HaritaPin>();
|
||||
const yurtdisi = new Map<string, YurtdisiPin>();
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<UlkeKodu, string> = {
|
||||
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<string, Sehir> = {
|
||||
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<string, Sehir> = {
|
||||
* null → Türkiye haritasında kalır.
|
||||
* Anahtar: uniAdiNormalize edilmiş adın tr-TR büyük harf hali.
|
||||
*/
|
||||
const UNI_DUZELTME: Record<string, keyof typeof SEHIRLER | null> = {
|
||||
const UNI_DUZELTME: Record<string, SehirAnahtari | null> = {
|
||||
"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<string, keyof typeof SEHIRLER | null> = {
|
||||
/** 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;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,13 @@ export const RISK_ETIKET: Record<RiskSeviyesi, string> = {
|
||||
riskli: "Riskli",
|
||||
};
|
||||
|
||||
/** Harita pinlerinin SVG dolgu renkleri — risk renk dilinin oklch karşılığı. */
|
||||
export const RISK_PIN_RENK: Record<RiskSeviyesi, string> = {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user