refactor: optimize AramaFunnelKarti styles and enhance city data structure
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:
bilalgursen
2026-08-09 03:55:56 +03:00
parent f752d27101
commit 696dc69763
9 changed files with 378 additions and 86 deletions

View File

@@ -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>

View File

@@ -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>
)}

View 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 },
);

View 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 (GirneLefkoş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>
);
}

View File

@@ -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>