Add il bazlı filtreleme ve Türkiye harita bileşeni

Sonuç sayfasına bölge haritası ile il seçimi eklendi; searchByRank
artık il filtresini destekliyor. Paket adı tercihai olarak güncellendi.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
bilalgursen
2026-07-21 12:05:29 +03:00
parent 87f265ec15
commit 871012e87d
5 changed files with 290 additions and 10 deletions

View File

@@ -0,0 +1,182 @@
"use client";
import { useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { cities } from "turkey-map-react/lib/data";
export type IlOzeti = {
hayal: number;
dengeli: number;
garanti: number;
};
type Props = {
/** DB'deki il adıyla (büyük harf Türkçe) program sayıları */
iller: Record<string, IlOzeti>;
sira: number;
turKey: string;
seciliIl: string | null;
};
// Harita adını DB formatına çevirir: "Hakkâri" → "HAKKARİ"
function normalizeIl(name: string): string {
return name
.toLocaleUpperCase("tr-TR")
.replace(/Â/g, "A")
.replace(/Î/g, "İ")
.replace(/Û/g, "U");
}
function toplam(o: IlOzeti): number {
return o.hayal + o.dengeli + o.garanti;
}
type Secim = {
name: string;
ozet: IlOzeti;
x: number;
y: number;
};
export function BolgeHaritasi({ iller, sira, turKey, seciliIl }: Props) {
const [secim, setSecim] = useState<Secim | null>(null);
const kapsayici = useRef<HTMLDivElement>(null);
const router = useRouter();
function filtrele(ilKey: string) {
const url =
ilKey === seciliIl
? `/sonuc?sira=${sira}&tur=${turKey}`
: `/sonuc?sira=${sira}&tur=${turKey}&il=${encodeURIComponent(ilKey)}`;
router.push(url, { scroll: false });
}
const maxToplam = Math.max(1, ...Object.values(iller).map((o) => toplam(o)));
// Haritada karşılığı olmayan yerler (KKTC, yurtdışı kampüsler)
const haritaIlleri = new Set(cities.map((c) => normalizeIl(c.name)));
const haritaDisi = Object.entries(iller).filter(
([il]) => !haritaIlleri.has(il)
);
function sec(el: SVGPathElement, name: string, ozet: IlOzeti) {
const kutu = kapsayici.current?.getBoundingClientRect();
const r = el.getBoundingClientRect();
if (!kutu) return;
setSecim({
name,
ozet,
x: r.x - kutu.x + r.width / 2,
y: r.y - kutu.y,
});
}
return (
<div ref={kapsayici} className="relative">
<svg
viewBox="0 0 1050 585"
className="h-auto w-full"
role="img"
aria-label="Önerilen programların illere dağılımı haritası"
onClick={(e) => {
if ((e.target as Element).tagName !== "path") setSecim(null);
}}
>
{/* Seçili il en son çizilir ki turuncu konturu komşu iller örtmesin */}
{[...cities]
.sort((a, b) =>
(normalizeIl(a.name) === seciliIl ? 1 : 0) -
(normalizeIl(b.name) === seciliIl ? 1 : 0)
)
.map((city) => {
const key = normalizeIl(city.name);
const ozet = iller[key];
const yogunluk = ozet ? toplam(ozet) / maxToplam : 0;
const secili = key === seciliIl;
return (
<path
key={city.id}
d={city.path}
className={
ozet
? "cursor-pointer transition-opacity duration-200 hover:opacity-80"
: "fill-slate-200 stroke-white"
}
style={
ozet
? {
fill: `oklch(0.623 0.188 259.8 / ${0.3 + 0.7 * yogunluk})`,
stroke: secili ? "oklch(0.705 0.213 47.6)" : "white",
strokeWidth: secili ? 2.5 : 1,
}
: undefined
}
strokeWidth={1}
onClick={
ozet ? () => filtrele(key) : () => setSecim(null)
}
onMouseEnter={
ozet
? (e) => sec(e.currentTarget, city.name, ozet)
: undefined
}
onMouseLeave={ozet ? () => setSecim(null) : undefined}
>
<title>
{ozet
? `${city.name}: ${toplam(ozet)} program — filtrelemek için tıkla`
: city.name}
</title>
</path>
);
})}
</svg>
{secim ? (
<div
data-testid="harita-tooltip"
className="pointer-events-none absolute z-10 -translate-x-1/2 -translate-y-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm shadow-md"
style={{ left: secim.x, top: secim.y - 6 }}
>
<p className="font-heading font-semibold">{secim.name}</p>
<p className="text-slate-600">
{toplam(secim.ozet)} program
<span className="block text-xs">
{secim.ozet.hayal} hayal · {secim.ozet.dengeli} dengeli ·{" "}
{secim.ozet.garanti} garanti
</span>
<span className="block text-xs font-medium text-primary">
{normalizeIl(secim.name) === seciliIl
? "Filtreyi kaldırmak için tıkla"
: "Filtrelemek için tıkla"}
</span>
</p>
</div>
) : null}
<div className="mt-2 flex items-center justify-end gap-2 text-xs text-slate-500">
<span>az</span>
<span
aria-hidden
className="h-2 w-24 rounded-full"
style={{
background:
"linear-gradient(to right, oklch(0.623 0.188 259.8 / 0.3), oklch(0.623 0.188 259.8))",
}}
/>
<span>çok program</span>
</div>
{haritaDisi.length > 0 ? (
<p className="mt-3 text-xs text-slate-500">
Harita dışı:{" "}
{haritaDisi
.map(
([il, ozet]) => `${il.toLocaleLowerCase("tr-TR")} (${toplam(ozet)})`
)
.join(", ")}
</p>
) : null}
</div>
);
}