110 lines
4.5 KiB
TypeScript
110 lines
4.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useMemo } from "react";
|
|
import { useLocale, useTranslation } from "@/i18n/navigation";
|
|
import { useTheme } from "@/app/context/ThemeContext";
|
|
import { MembersMap } from "@/app/lib/charts";
|
|
import { MAP_CITIES } from "@/app/data/map-cities";
|
|
|
|
export default function MembersMapPage() {
|
|
const { t } = useTranslation("membersMap");
|
|
const locale = useLocale();
|
|
const { theme } = useTheme();
|
|
const [hoveredCity, setHoveredCity] = useState<typeof MAP_CITIES[0] | null>(null);
|
|
|
|
const totalMembers = useMemo(
|
|
() => MAP_CITIES.reduce((sum, c) => sum + c.nomadsNow, 0),
|
|
[]
|
|
);
|
|
|
|
const sortedCities = useMemo(
|
|
() => [...MAP_CITIES].sort((a, b) => b.nomadsNow - a.nomadsNow),
|
|
[]
|
|
);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
|
|
<main className="max-w-[1200px] mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
|
<header className="mb-8">
|
|
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
|
<span className="w-1 h-8 bg-[#ff4d4f] rounded-full" />
|
|
{t("title")}
|
|
</h1>
|
|
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm sm:text-base">
|
|
{t("subtitle")}
|
|
</p>
|
|
</header>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 lg:gap-8">
|
|
<div className="lg:col-span-2 bg-white dark:bg-gray-900 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-800 overflow-hidden p-4 sm:p-6">
|
|
<div className="rounded-xl overflow-hidden bg-gray-50 dark:bg-gray-800/50">
|
|
<MembersMap
|
|
data={MAP_CITIES}
|
|
locale={locale}
|
|
dark={theme === "dark"}
|
|
className="min-h-[400px]"
|
|
onCityHover={setHoveredCity}
|
|
/>
|
|
</div>
|
|
{hoveredCity && (
|
|
<div className="mt-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{locale === "zh" ? hoveredCity.name : hoveredCity.nameEn} {hoveredCity.emoji}
|
|
</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
{hoveredCity.nomadsNow} {t("nomadsInCity")}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-800 p-6">
|
|
<h3 className="text-sm font-bold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-2">
|
|
{t("totalMembers")}
|
|
</h3>
|
|
<p className="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
|
{totalMembers.toLocaleString()}+
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-800 p-6">
|
|
<h3 className="text-sm font-bold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-4">
|
|
{t("cities")}
|
|
</h3>
|
|
<ul className="space-y-2 max-h-[280px] overflow-y-auto">
|
|
{sortedCities.map((city) => (
|
|
<li
|
|
key={city.id}
|
|
className={`flex items-center justify-between py-2 px-3 rounded-lg cursor-pointer transition-colors ${
|
|
hoveredCity?.id === city.id
|
|
? "bg-[#ff4d4f]/10 text-[#ff4d4f]"
|
|
: "hover:bg-gray-50 dark:hover:bg-gray-800"
|
|
}`}
|
|
onMouseEnter={() => setHoveredCity(city)}
|
|
onMouseLeave={() => setHoveredCity(null)}
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
<span>{city.emoji}</span>
|
|
<span className="font-medium text-gray-900 dark:text-gray-100">
|
|
{locale === "zh" ? city.name : city.nameEn}
|
|
</span>
|
|
</span>
|
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
{city.nomadsNow}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-6 text-center">
|
|
{t("demoHint")}
|
|
</p>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|