134 lines
6.0 KiB
TypeScript
134 lines
6.0 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import Link from "next/link";
|
||
import { useRouter } from "next/navigation";
|
||
import { compareUrl } from "@/lib/compareScore";
|
||
import { mergeDestinationsIntoTrip } from "@/lib/tripActions";
|
||
import { useToast } from "@/lib/toast";
|
||
import { useI18n } from "@/lib/i18n";
|
||
import { meetupCityHref } from "@/lib/meetupLinks";
|
||
import type { Destination } from "@/lib/types";
|
||
|
||
interface Props { destinations: Destination[] }
|
||
|
||
export default function WorldMap({ destinations }: Props) {
|
||
const { t } = useI18n();
|
||
const { toast } = useToast();
|
||
const router = useRouter();
|
||
const [selected, setSelected] = useState<Destination | null>(null);
|
||
|
||
const addToPlan = () => {
|
||
if (!selected) return;
|
||
const { added } = mergeDestinationsIntoTrip([selected], 1);
|
||
toast(
|
||
added
|
||
? `${selected.emoji} ${selected.name} ${t.plan.addedToPlan}`
|
||
: t.common.alreadyInPlan,
|
||
added ? "success" : "info",
|
||
{ href: "/plan", label: t.strip.openPlan }
|
||
);
|
||
if (added) router.push("/plan");
|
||
};
|
||
|
||
return (
|
||
<section className="section map-section section-compact" id="map">
|
||
<div className="container">
|
||
<div className="section-header reveal">
|
||
<span className="section-tag">{t.home.mapTag}</span>
|
||
<h2>{t.home.mapTitle}</h2>
|
||
<p>{t.home.mapSubtitle}</p>
|
||
</div>
|
||
<div className="map-wrapper reveal">
|
||
<div className="map-svg-container">
|
||
<svg className="world-map" viewBox="0 0 1000 500" aria-hidden>
|
||
<defs>
|
||
<radialGradient id="mapGlow" cx="50%" cy="50%" r="50%">
|
||
<stop offset="0%" stopColor="#4ECDC4" stopOpacity="0.15" />
|
||
<stop offset="100%" stopColor="#4ECDC4" stopOpacity="0" />
|
||
</radialGradient>
|
||
<filter id="pinGlow">
|
||
<feGaussianBlur stdDeviation="3" result="blur" />
|
||
<feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
|
||
</filter>
|
||
</defs>
|
||
<rect width="1000" height="500" fill="url(#mapGlow)" rx="20" />
|
||
<g className="continents" opacity="0.25">
|
||
<path d="M180 120 Q220 100 280 110 Q320 130 310 170 Q290 200 250 210 Q200 220 170 190 Q150 160 180 120Z" fill="#4ECDC4" />
|
||
<path d="M420 100 Q480 90 530 110 Q560 140 550 180 Q520 210 470 200 Q430 180 420 140Z" fill="#4ECDC4" />
|
||
<path d="M530 130 Q600 120 660 140 Q700 170 690 210 Q650 240 590 230 Q540 210 530 170Z" fill="#4ECDC4" />
|
||
<path d="M700 160 Q780 150 830 180 Q860 220 840 260 Q790 280 730 260 Q690 230 700 190Z" fill="#4ECDC4" />
|
||
<path d="M200 260 Q250 250 290 280 Q310 320 280 360 Q240 380 200 360 Q170 330 180 290Z" fill="#4ECDC4" />
|
||
</g>
|
||
{destinations.map((d) => (
|
||
<g
|
||
key={d.slug}
|
||
className={`map-pin${selected?.slug === d.slug ? " active" : ""}`}
|
||
transform={`translate(${d.map_x}, ${d.map_y})`}
|
||
onClick={() => setSelected(d)}
|
||
style={{ cursor: "pointer" }}
|
||
role="button"
|
||
tabIndex={0}
|
||
aria-label={d.name}
|
||
onKeyDown={(e) => { if (e.key === "Enter") setSelected(d); }}
|
||
>
|
||
<circle r="20" fill="rgba(255,107,107,0.15)" className="pin-pulse" />
|
||
<circle r="8" fill="#FF6B6B" filter="url(#pinGlow)" />
|
||
<text y="-16" textAnchor="middle" fontSize="14">{d.emoji}</text>
|
||
</g>
|
||
))}
|
||
</svg>
|
||
</div>
|
||
<div className="map-info-panel">
|
||
{selected ? (
|
||
<div className="map-info-city">
|
||
<div className="city-emoji">{selected.emoji}</div>
|
||
<h3>{selected.name}, {selected.country}</h3>
|
||
<span className="modal-tag">{selected.tag}</span>
|
||
<p className="city-desc">{selected.description}</p>
|
||
<div className="map-info-stats">
|
||
<div className="map-stat"><strong>¥{selected.cost.toLocaleString()}</strong>{t.home.mapCost}</div>
|
||
<div className="map-stat"><strong>{selected.speed}Mbps</strong>{t.home.mapSpeed}</div>
|
||
<div className="map-stat"><strong>{selected.temperature}°C</strong>{t.home.mapTemp}</div>
|
||
<div className="map-stat"><strong>⭐ {selected.rating}</strong>{t.home.mapRating}</div>
|
||
</div>
|
||
<div className="map-info-actions">
|
||
<Link href={`/destinations/${selected.slug}`} className="btn btn-primary btn-sm">
|
||
{t.common.detail}
|
||
</Link>
|
||
<button type="button" className="btn btn-sm" onClick={addToPlan}>
|
||
🗓️ {t.nav.plan}
|
||
</button>
|
||
<Link href={meetupCityHref(selected.name)} className="btn btn-sm">
|
||
{t.common.cityMeetups}
|
||
</Link>
|
||
<Link href={compareUrl([selected.slug])} className="btn btn-ghost btn-sm">
|
||
⚖️ {t.nav.compare}
|
||
</Link>
|
||
<Link href="/next-stop" className="btn btn-ghost btn-sm">
|
||
🧭 {t.nav.nextStop}
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="map-info-default">
|
||
<span className="map-info-icon">👆</span>
|
||
<h3>{t.home.mapPick}</h3>
|
||
<p>{t.home.mapPickHint}</p>
|
||
<div className="dating-empty-actions">
|
||
<a href="#destinations" className="btn btn-ghost btn-sm map-browse-link">
|
||
{t.home.mapBrowse}
|
||
</a>
|
||
<Link href="/map" className="btn btn-sm map-browse-link">
|
||
{t.home.mapMembers}
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|