nomadweb/frontend/src/components/WorldMap.tsx
eric e23cf9a03c Close Explore/Grow loops: visa wizard, jobs, lessons, gigs notify, map.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 09:26:56 -05:00

134 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}