Merge branch 'main' into cloudflare

This commit is contained in:
hamster1963 2024-12-07 19:26:12 +08:00
commit 250f8f68dc
6 changed files with 166 additions and 48 deletions

View File

@ -8,6 +8,7 @@ import { geoJsonString } from "../../../lib/geo-json-string";
import GlobalInfo from "./GlobalInfo"; import GlobalInfo from "./GlobalInfo";
import GlobalLoading from "./GlobalLoading"; import GlobalLoading from "./GlobalLoading";
import { InteractiveMap } from "./InteractiveMap"; import { InteractiveMap } from "./InteractiveMap";
import { TooltipProvider } from "./TooltipContext";
export default function ServerGlobal() { export default function ServerGlobal() {
const { data: nezhaServerList, error } = useSWR<ServerApi>( const { data: nezhaServerList, error } = useSWR<ServerApi>(
@ -51,13 +52,16 @@ export default function ServerGlobal() {
<section className="flex flex-col gap-4 mt-[3.2px]"> <section className="flex flex-col gap-4 mt-[3.2px]">
<GlobalInfo countries={countryList} /> <GlobalInfo countries={countryList} />
<div className="w-full overflow-x-auto"> <div className="w-full overflow-x-auto">
<TooltipProvider>
<InteractiveMap <InteractiveMap
countries={countryList} countries={countryList}
serverCounts={serverCounts} serverCounts={serverCounts}
width={width} width={width}
height={height} height={height}
filteredFeatures={filteredFeatures} filteredFeatures={filteredFeatures}
nezhaServerList={nezhaServerList}
/> />
</TooltipProvider>
</div> </div>
</section> </section>
); );

View File

@ -2,9 +2,10 @@
import { countryCoordinates } from "@/lib/geo-limit"; import { countryCoordinates } from "@/lib/geo-limit";
import { geoEquirectangular, geoPath } from "d3-geo"; import { geoEquirectangular, geoPath } from "d3-geo";
import { AnimatePresence, m } from "framer-motion";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState } from "react";
import MapTooltip from "./MapTooltip";
import { useTooltip } from "./TooltipContext";
interface InteractiveMapProps { interface InteractiveMapProps {
countries: string[]; countries: string[];
@ -12,6 +13,7 @@ interface InteractiveMapProps {
width: number; width: number;
height: number; height: number;
filteredFeatures: any[]; filteredFeatures: any[];
nezhaServerList: any;
} }
export function InteractiveMap({ export function InteractiveMap({
@ -20,14 +22,10 @@ export function InteractiveMap({
width, width,
height, height,
filteredFeatures, filteredFeatures,
nezhaServerList,
}: InteractiveMapProps) { }: InteractiveMapProps) {
const t = useTranslations("Global"); const t = useTranslations("Global");
const { setTooltipData } = useTooltip();
const [tooltipData, setTooltipData] = useState<{
centroid: [number, number];
country: string;
count: number;
} | null>(null);
const projection = geoEquirectangular() const projection = geoEquirectangular()
.scale(140) .scale(140)
@ -37,7 +35,10 @@ export function InteractiveMap({
const path = geoPath().projection(projection); const path = geoPath().projection(projection);
return ( return (
<div className="relative w-full aspect-[2/1]"> <div
className="relative w-full aspect-[2/1]"
onMouseLeave={() => setTooltipData(null)}
>
<svg <svg
width={width} width={width}
height={height} height={height}
@ -51,6 +52,15 @@ export function InteractiveMap({
</pattern> </pattern>
</defs> </defs>
<g> <g>
{/* Background rect to handle mouse events in empty areas */}
<rect
x="0"
y="0"
width={width}
height={height}
fill="transparent"
onMouseEnter={() => setTooltipData(null)}
/>
{filteredFeatures.map((feature, index) => { {filteredFeatures.map((feature, index) => {
const isHighlighted = countries.includes( const isHighlighted = countries.includes(
feature.properties.iso_a2_eh, feature.properties.iso_a2_eh,
@ -72,15 +82,30 @@ export function InteractiveMap({
: "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]" : "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]"
} }
onMouseEnter={() => { 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.result
.filter(
(server: any) =>
server.host.CountryCode?.toUpperCase() ===
countryCode,
)
.map((server: any) => ({
name: server.name,
status: server.online_status,
}));
setTooltipData({ setTooltipData({
centroid: path.centroid(feature), centroid: path.centroid(feature),
country: feature.properties.name, country: feature.properties.name,
count: serverCount, count: serverCount,
servers: countryServers,
}); });
} }
}} }}
onMouseLeave={() => setTooltipData(null)}
/> />
); );
})} })}
@ -107,13 +132,22 @@ export function InteractiveMap({
<g <g
key={countryCode} key={countryCode}
onMouseEnter={() => { onMouseEnter={() => {
const countryServers = nezhaServerList.result
.filter(
(server: any) =>
server.host.CountryCode?.toUpperCase() === countryCode,
)
.map((server: any) => ({
name: server.name,
status: server.online_status,
}));
setTooltipData({ setTooltipData({
centroid: [x, y], centroid: [x, y],
country: coords.name, country: coords.name,
count: serverCount, count: serverCount,
servers: countryServers,
}); });
}} }}
onMouseLeave={() => setTooltipData(null)}
className="cursor-pointer" className="cursor-pointer"
> >
<circle <circle
@ -127,30 +161,7 @@ export function InteractiveMap({
})} })}
</g> </g>
</svg> </svg>
<AnimatePresence mode="wait"> <MapTooltip />
{tooltipData && (
<m.div
initial={{ opacity: 0, filter: "blur(10px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
className="absolute hidden lg:block pointer-events-none bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700"
key={tooltipData.country}
style={{
left: tooltipData.centroid[0],
top: tooltipData.centroid[1],
transform: "translate(-50%, -50%)",
}}
>
<p className="font-medium">
{tooltipData.country === "China"
? "Mainland China"
: tooltipData.country}
</p>
<p className="text-neutral-600 dark:text-neutral-400">
{tooltipData.count} {t("Servers")}
</p>
</m.div>
)}
</AnimatePresence>
</div> </div>
); );
} }

View File

@ -0,0 +1,65 @@
"use client";
import { AnimatePresence, m } from "framer-motion";
import { useTranslations } from "next-intl";
import { memo } from "react";
import { useTooltip } from "./TooltipContext";
const MapTooltip = memo(function MapTooltip() {
const { tooltipData } = useTooltip();
const t = useTranslations("Global");
if (!tooltipData) return null;
return (
<AnimatePresence mode="wait">
<m.div
initial={{ opacity: 0, filter: "blur(10px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, filter: "blur(10px)" }}
className="absolute hidden lg:block bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700 z-50"
key={tooltipData.country}
style={{
left: tooltipData.centroid[0],
top: tooltipData.centroid[1],
transform: "translate(-50%, -50%)",
}}
onMouseEnter={(e) => {
e.stopPropagation();
}}
>
<div>
<p className="font-medium">
{tooltipData.country === "China"
? "Mainland China"
: tooltipData.country}
</p>
<p className="text-neutral-600 dark:text-neutral-400 mb-1">
{tooltipData.count} {t("Servers")}
</p>
</div>
<div
className="border-t dark:border-neutral-700 pt-1"
style={{
maxHeight: "200px",
overflowY: "auto",
}}
>
{tooltipData.servers.map((server, index) => (
<div key={index} className="flex items-center gap-1.5 py-0.5">
<span
className={`w-1.5 h-1.5 shrink-0 rounded-full ${
server.status ? "bg-green-500" : "bg-red-500"
}`}
></span>
<span className="text-xs">{server.name}</span>
</div>
))}
</div>
</m.div>
</AnimatePresence>
);
});
export default MapTooltip;

View File

@ -0,0 +1,38 @@
"use client";
import { ReactNode, createContext, useContext, useState } 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;
}
const TooltipContext = createContext<TooltipContextType | undefined>(undefined);
export function TooltipProvider({ children }: { children: ReactNode }) {
const [tooltipData, setTooltipData] = useState<TooltipData | null>(null);
return (
<TooltipContext.Provider value={{ tooltipData, setTooltipData }}>
{children}
</TooltipContext.Provider>
);
}
export function useTooltip() {
const context = useContext(TooltipContext);
if (context === undefined) {
throw new Error("useTooltip must be used within a TooltipProvider");
}
return context;
}

BIN
bun.lockb

Binary file not shown.

View File

@ -1,6 +1,6 @@
{ {
"name": "nezha-dash", "name": "nezha-dash",
"version": "1.7.2", "version": "1.7.3",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3040", "dev": "next dev -p 3040",
@ -28,7 +28,7 @@
"@types/d3-geo": "^3.1.0", "@types/d3-geo": "^3.1.0",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@typescript-eslint/eslint-plugin": "^8.17.0", "@typescript-eslint/eslint-plugin": "^8.17.0",
"caniuse-lite": "^1.0.30001686", "caniuse-lite": "^1.0.30001687",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"country-flag-icons": "^1.5.13", "country-flag-icons": "^1.5.13",
@ -41,7 +41,7 @@
"luxon": "^3.5.0", "luxon": "^3.5.0",
"next": "^15.0.4", "next": "^15.0.4",
"next-auth": "^5.0.0-beta.25", "next-auth": "^5.0.0-beta.25",
"next-intl": "^3.25.3", "next-intl": "^3.26.0",
"next-runtime-env": "^3.2.2", "next-runtime-env": "^3.2.2",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"react": "19.0.0-rc-02c0e824-20241028", "react": "19.0.0-rc-02c0e824-20241028",
@ -58,7 +58,7 @@
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "^15.0.4", "@next/bundle-analyzer": "^15.0.4",
"@tailwindcss/postcss": "^4.0.0-beta.5", "@tailwindcss/postcss": "^4.0.0-beta.6",
"@types/node": "^22.10.1", "@types/node": "^22.10.1",
"@types/react": "npm:types-react@19.0.0-rc.1", "@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
@ -69,7 +69,7 @@
"postcss": "^8.4.49", "postcss": "^8.4.49",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.9", "prettier-plugin-tailwindcss": "^0.6.9",
"tailwindcss": "^4.0.0-beta.5", "tailwindcss": "^4.0.0-beta.6",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vercel": "39.1.1" "vercel": "39.1.1"
}, },