Files
gitlab-instance-0a899031_cn…/app/[locale]/map/page.tsx
2026-06-06 04:29:46 -05:00

118 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, 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";
import { apiFetch } from "@/app/lib/api-client";
export default function MembersMapPage() {
const { t } = useTranslation("membersMap");
const locale = useLocale();
const { theme } = useTheme();
const [mapCities, setMapCities] = useState(MAP_CITIES);
const [hoveredCity, setHoveredCity] = useState<typeof MAP_CITIES[0] | null>(null);
useEffect(() => {
apiFetch<{ items: typeof MAP_CITIES }>("/api/stats/member-map")
.then((data) => data.items?.length && setMapCities(data.items))
.catch(() => setMapCities(MAP_CITIES));
}, []);
const totalMembers = useMemo(
() => mapCities.reduce((sum, c) => sum + c.nomadsNow, 0),
[mapCities]
);
const sortedCities = useMemo(
() => [...mapCities].sort((a, b) => b.nomadsNow - a.nomadsNow),
[mapCities]
);
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={mapCities}
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">
PocketBase
</p>
</main>
</div>
);
}