diff --git a/eslint.config.js b/eslint.config.js index c1d602f..86d5d48 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -24,6 +24,7 @@ export default tseslint.config( { allowConstantExport: true }, ], "react-hooks/exhaustive-deps": "off", + "@typescript-eslint/no-explicit-any": "off", }, }, ); diff --git a/src/components/GlobalMap.tsx b/src/components/GlobalMap.tsx index aaf5ef1..28c7238 100644 --- a/src/components/GlobalMap.tsx +++ b/src/components/GlobalMap.tsx @@ -1,22 +1,23 @@ import { geoJsonString } from "@/lib/geo-json-string"; import { NezhaServer } from "@/types/nezha-api"; -import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { AnimatePresence, m } from "framer-motion"; import { geoEquirectangular, geoPath } from "d3-geo"; import { countryCoordinates } from "@/lib/geo-limit"; +import MapTooltip from "./MapTooltip"; +import useTooltip from "@/hooks/use-tooltip"; +import { formatNezhaInfo } from "@/lib/utils"; export default function GlobalMap({ serverList, + now, }: { serverList: NezhaServer[]; + now: number; }) { const { t } = useTranslation(); const countryList: string[] = []; const serverCounts: { [key: string]: number } = {}; - console.log(serverList); - serverList.forEach((server) => { if (server.country_code) { const countryCode = server.country_code.toUpperCase(); @@ -48,6 +49,8 @@ export default function GlobalMap({ width={width} height={height} filteredFeatures={filteredFeatures} + nezhaServerList={serverList} + now={now} /> @@ -67,21 +70,20 @@ interface InteractiveMapProps { }; geometry: never; }[]; + nezhaServerList: NezhaServer[]; + now: number; } -function InteractiveMap({ +export function InteractiveMap({ countries, serverCounts, width, height, filteredFeatures, + nezhaServerList, + now, }: InteractiveMapProps) { - const { t } = useTranslation(); - const [tooltipData, setTooltipData] = useState<{ - centroid: [number, number]; - country: string; - count: number; - } | null>(null); + const { setTooltipData } = useTooltip(); const projection = geoEquirectangular() .scale(140) @@ -91,7 +93,10 @@ function InteractiveMap({ const path = geoPath().projection(projection); return ( -
+
setTooltipData(null)} + > + {/* Background rect to handle mouse events in empty areas */} + setTooltipData(null)} + /> {filteredFeatures.map((feature, index) => { const isHighlighted = countries.includes( feature.properties.iso_a2_eh, ); - if (isHighlighted) { - console.log(feature.properties.iso_a2_eh); - } - const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0; return ( @@ -126,15 +136,29 @@ function InteractiveMap({ : "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]" } onMouseEnter={() => { - if (isHighlighted && path.centroid(feature)) { + if (!isHighlighted) { + setTooltipData(null); + return; + } + if (path.centroid(feature)) { + const countryCode = feature.properties.iso_a2_eh; + const countryServers = nezhaServerList + .filter( + (server: NezhaServer) => + server.country_code?.toUpperCase() === countryCode, + ) + .map((server: NezhaServer) => ({ + name: server.name, + status: formatNezhaInfo(now, server).online, + })); setTooltipData({ centroid: path.centroid(feature), country: feature.properties.name, count: serverCount, + servers: countryServers, }); } }} - onMouseLeave={() => setTooltipData(null)} /> ); })} @@ -161,13 +185,23 @@ function InteractiveMap({ { + const countryServers = nezhaServerList + .filter( + (server: NezhaServer) => + server.country_code?.toUpperCase() === + countryCode.toUpperCase(), + ) + .map((server: NezhaServer) => ({ + name: server.name, + status: formatNezhaInfo(now, server).online, + })); setTooltipData({ centroid: [x, y], country: coords.name, count: serverCount, + servers: countryServers, }); }} - onMouseLeave={() => setTooltipData(null)} className="cursor-pointer" > - - {tooltipData && ( - -

- {tooltipData.country === "China" - ? "Mainland China" - : tooltipData.country} -

-

- {tooltipData.count} {t("map.Servers")} -

-
- )} -
+
); } diff --git a/src/components/MapTooltip.tsx b/src/components/MapTooltip.tsx new file mode 100644 index 0000000..3d6fbe5 --- /dev/null +++ b/src/components/MapTooltip.tsx @@ -0,0 +1,62 @@ +import useTooltip from "@/hooks/use-tooltip"; +import { AnimatePresence, m } from "framer-motion"; +import { memo } from "react"; +import { useTranslation } from "react-i18next"; + +const MapTooltip = memo(function MapTooltip() { + const { t } = useTranslation(); + const { tooltipData } = useTooltip(); + + if (!tooltipData) return null; + + return ( + + { + e.stopPropagation(); + }} + > +
+

+ {tooltipData.country === "China" + ? "Mainland China" + : tooltipData.country} +

+

+ {tooltipData.count} {t("map.Servers")} +

+
+
+ {tooltipData.servers.map((server, index: number) => ( +
+ + {server.name} +
+ ))} +
+
+
+ ); +}); + +export default MapTooltip; diff --git a/src/context/status-provider.tsx b/src/context/status-provider.tsx index 4b3d732..59e7e18 100644 --- a/src/context/status-provider.tsx +++ b/src/context/status-provider.tsx @@ -1,5 +1,3 @@ -"use client"; - import { ReactNode, useState } from "react"; import { Status, StatusContext } from "./status-context"; diff --git a/src/context/tooltip-context.ts b/src/context/tooltip-context.ts new file mode 100644 index 0000000..f942686 --- /dev/null +++ b/src/context/tooltip-context.ts @@ -0,0 +1,20 @@ +import { createContext } from "react"; + +export interface TooltipData { + centroid: [number, number]; + country: string; + count: number; + servers: Array<{ + name: string; + status: boolean; + }>; +} + +interface TooltipContextType { + tooltipData: TooltipData | null; + setTooltipData: (data: TooltipData | null) => void; +} + +export const TooltipContext = createContext( + undefined, +); diff --git a/src/context/tooltip-provider.tsx b/src/context/tooltip-provider.tsx new file mode 100644 index 0000000..aa23bec --- /dev/null +++ b/src/context/tooltip-provider.tsx @@ -0,0 +1,12 @@ +import { ReactNode, useState } from "react"; +import { TooltipContext, TooltipData } from "./tooltip-context"; + +export function TooltipProvider({ children }: { children: ReactNode }) { + const [tooltipData, setTooltipData] = useState(null); + + return ( + + {children} + + ); +} diff --git a/src/hooks/use-tooltip.tsx b/src/hooks/use-tooltip.tsx new file mode 100644 index 0000000..bf8754a --- /dev/null +++ b/src/hooks/use-tooltip.tsx @@ -0,0 +1,12 @@ +import { useContext } from "react"; +import { TooltipContext } from "@/context/tooltip-context"; + +export const useTooltip = () => { + const context = useContext(TooltipContext); + if (context === undefined) { + throw new Error("useTooltip must be used within a TooltipProvider"); + } + return context; +}; + +export default useTooltip; diff --git a/src/main.tsx b/src/main.tsx index 77d7ed6..7acb462 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -11,6 +11,7 @@ import { MotionProvider } from "./components/motion/motion-provider"; import { WebSocketProvider } from "./context/websocket-provider"; import { StatusProvider } from "./context/status-provider"; import { FilterProvider } from "./context/network-filter-context"; +import { TooltipProvider } from "./context/tooltip-provider"; const queryClient = new QueryClient(); @@ -22,19 +23,21 @@ ReactDOM.createRoot(document.getElementById("root")!).render( - - - + + + + + diff --git a/src/pages/Server.tsx b/src/pages/Server.tsx index 9633b2d..c394df0 100644 --- a/src/pages/Server.tsx +++ b/src/pages/Server.tsx @@ -243,7 +243,12 @@ export default function Servers() { setCurrentTab={setCurrentGroup} /> - {showMap === "1" && } + {showMap === "1" && ( + + )} {showServices === "1" && } {inline === "1" && (
diff --git a/src/types/css.d.ts b/src/types/css.d.ts index 18359a6..9d7b2e0 100644 --- a/src/types/css.d.ts +++ b/src/types/css.d.ts @@ -1,4 +1,4 @@ -declare module '*.css' { - const css: { [key: string]: string } - export default css +declare module "*.css" { + const css: { [key: string]: string }; + export default css; } diff --git a/src/types/nezha-api.ts b/src/types/nezha-api.ts index c072013..4ca6531 100644 --- a/src/types/nezha-api.ts +++ b/src/types/nezha-api.ts @@ -23,7 +23,6 @@ export interface NezhaServerHost { swap_total: number; arch: string; boot_time: number; - country_code: string; version: string; } diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 151aa68..11f02fe 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1 @@ -/// \ No newline at end of file +///