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:
182
src/components/bolge-haritasi.tsx
Normal file
182
src/components/bolge-haritasi.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user