Add countries and locations to WorldMap
This commit is contained in:
@@ -253,41 +253,57 @@ export const fetchNSApiNodes = async (): Promise<NS_NODE[]> => {
|
||||
return allNodes;
|
||||
};
|
||||
|
||||
export const fetchWorldMapCountries =
|
||||
async (): Promise<CountryDataResponse> => {
|
||||
// Fetch all nodes from the NS API
|
||||
const nodes = await fetchNSApiNodes();
|
||||
export const fetchWorldMapCountries = async (): Promise<{
|
||||
countries: CountryDataResponse;
|
||||
totalCountries: number;
|
||||
uniqueLocations: number;
|
||||
totalServers: number;
|
||||
}> => {
|
||||
// Fetch all nodes from the NS API
|
||||
const nodes = await fetchNSApiNodes();
|
||||
|
||||
// Create a map to count nodes by country
|
||||
const countryCounts: Record<string, number> = {};
|
||||
// Create a map to count nodes by country
|
||||
const countryCounts: Record<string, number> = {};
|
||||
// Set to track unique cities
|
||||
const uniqueCities = new Set<string>();
|
||||
|
||||
// Process each node
|
||||
for (const node of nodes) {
|
||||
// Get the 2-letter country code from the node's geoip data
|
||||
const twoLetterCode = node.geoip?.country;
|
||||
// Process each node
|
||||
for (const node of nodes) {
|
||||
// Get the 2-letter country code from the node's geoip data
|
||||
const twoLetterCode = node.geoip?.country;
|
||||
|
||||
if (twoLetterCode) {
|
||||
// Convert to 3-letter country code
|
||||
const threeLetterCode = countryCodeMap[twoLetterCode] || twoLetterCode;
|
||||
if (twoLetterCode) {
|
||||
// Convert to 3-letter country code
|
||||
const threeLetterCode = countryCodeMap[twoLetterCode] || twoLetterCode;
|
||||
|
||||
// Increment the count for this country
|
||||
countryCounts[threeLetterCode] =
|
||||
(countryCounts[threeLetterCode] || 0) + 1;
|
||||
} else {
|
||||
// If no geoip data, count it as unknown
|
||||
countryCounts[""] = (countryCounts[""] || 0) + 1;
|
||||
// Increment the count for this country
|
||||
countryCounts[threeLetterCode] =
|
||||
(countryCounts[threeLetterCode] || 0) + 1;
|
||||
|
||||
// Add city to unique cities set if it exists
|
||||
if (node.geoip?.city) {
|
||||
uniqueCities.add(node.geoip.city);
|
||||
}
|
||||
} else {
|
||||
// If no geoip data, count it as unknown
|
||||
countryCounts[""] = (countryCounts[""] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the counts to the required format
|
||||
const result: CountryDataResponse = {};
|
||||
// Convert the counts to the required format
|
||||
const result: CountryDataResponse = {};
|
||||
|
||||
for (const [threeLetterCode, count] of Object.entries(countryCounts)) {
|
||||
result[threeLetterCode] = {
|
||||
ISO3: threeLetterCode,
|
||||
nodes: count,
|
||||
};
|
||||
}
|
||||
for (const [threeLetterCode, count] of Object.entries(countryCounts)) {
|
||||
result[threeLetterCode] = {
|
||||
ISO3: threeLetterCode,
|
||||
nodes: count,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
return {
|
||||
countries: result,
|
||||
totalCountries: Object.keys(countryCounts).length,
|
||||
uniqueLocations: uniqueCities.size,
|
||||
totalServers: nodes.length,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,7 +33,17 @@ export const WorldMap = (): JSX.Element => {
|
||||
}>({ coordinates: [0, 0], zoom: 1 });
|
||||
|
||||
const {
|
||||
data: countries = [],
|
||||
data: {
|
||||
countries = [],
|
||||
totalCountries = 0,
|
||||
uniqueLocations = 0,
|
||||
totalServers = 0,
|
||||
} = {
|
||||
countries: [],
|
||||
totalCountries: 0,
|
||||
uniqueLocations: 0,
|
||||
totalServers: 0,
|
||||
},
|
||||
isLoading: isLoadingCountries,
|
||||
isError: isCountriesError,
|
||||
} = useQuery({
|
||||
@@ -129,221 +139,268 @@ export const WorldMap = (): JSX.Element => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ExplorerCard
|
||||
label="Nym server locations"
|
||||
sx={{
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
p: { xs: 2, sm: 3 },
|
||||
"& > .MuiCardContent-root": {
|
||||
height: {
|
||||
xs: "200px",
|
||||
sm: "auto",
|
||||
},
|
||||
aspectRatio: {
|
||||
xs: "unset",
|
||||
sm: "16/7",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
<>
|
||||
<ExplorerCard
|
||||
label="Nym server locations"
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
margin: "0 auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
p: { xs: 2, sm: 3 },
|
||||
"& > .MuiCardContent-root": {
|
||||
height: {
|
||||
xs: "200px",
|
||||
sm: "auto",
|
||||
},
|
||||
aspectRatio: {
|
||||
xs: "unset",
|
||||
sm: "16/7",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ComposableMap
|
||||
data-tip=""
|
||||
style={{
|
||||
backgroundColor: isDarkMode ? "#000000" : theme.palette.pine[25],
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
viewBox="0 0 800 400"
|
||||
projection="geoMercator"
|
||||
projectionConfig={{
|
||||
scale: 130,
|
||||
}}
|
||||
>
|
||||
<ZoomableGroup
|
||||
center={position.coordinates}
|
||||
zoom={position.zoom}
|
||||
minZoom={1}
|
||||
maxZoom={8}
|
||||
translateExtent={[
|
||||
[-800, -400],
|
||||
[800, 400],
|
||||
]}
|
||||
onMoveEnd={({
|
||||
coordinates,
|
||||
zoom,
|
||||
}: {
|
||||
coordinates: [number, number];
|
||||
zoom: number;
|
||||
}) => {
|
||||
setPosition({ coordinates, zoom });
|
||||
}}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Geographies geography={MAP_TOPOJSON}>
|
||||
{({ geographies }: { geographies: GeoJSON.Feature[] }) =>
|
||||
geographies.map((geo) => {
|
||||
const d = Array.isArray(countries)
|
||||
? { nodes: 0 }
|
||||
: (countries as CountryDataResponse)[
|
||||
geo.properties?.ISO_A3 as string
|
||||
] || { nodes: 0 };
|
||||
return (
|
||||
<Geography
|
||||
key={`${geo.properties?.ISO_A3 || ""}-${geo.id}-${
|
||||
geo.properties?.NAME_LONG || ""
|
||||
}`}
|
||||
geography={geo}
|
||||
fill={colorScale(d?.nodes || 0)}
|
||||
stroke={
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.pine[800]
|
||||
: theme.palette.pine[200]
|
||||
}
|
||||
strokeWidth={0.2}
|
||||
data-tooltip-id="map-tooltip"
|
||||
onMouseEnter={() => {
|
||||
const { NAME_LONG } = geo.properties as {
|
||||
NAME_LONG: string;
|
||||
};
|
||||
setTooltipContent(`${NAME_LONG} | ${d?.nodes || 0}`);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setTooltipContent("");
|
||||
}}
|
||||
style={{
|
||||
hover: countries
|
||||
? {
|
||||
fill: theme.palette.accent.main,
|
||||
outline: "white",
|
||||
cursor: "pointer",
|
||||
}
|
||||
: undefined,
|
||||
default: {
|
||||
outline: "none",
|
||||
},
|
||||
pressed: {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Geographies>
|
||||
</ZoomableGroup>
|
||||
</ComposableMap>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
left: 10,
|
||||
zIndex: 1000,
|
||||
overflow: "hidden",
|
||||
margin: "0 auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(0,0,0,0.5)"
|
||||
: "rgba(255,255,255,0.5)",
|
||||
padding: "4px",
|
||||
borderRadius: "4px",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setPosition((prev) => ({
|
||||
...prev,
|
||||
zoom: Math.min(prev.zoom + 0.5, 8),
|
||||
}))
|
||||
}
|
||||
sx={{
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.1)"
|
||||
: "rgba(0,0,0,0.1)",
|
||||
"&:hover": {
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
<ComposableMap
|
||||
data-tip=""
|
||||
style={{
|
||||
backgroundColor: isDarkMode ? "#000000" : theme.palette.pine[25],
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
viewBox="0 0 800 400"
|
||||
projection="geoMercator"
|
||||
projectionConfig={{
|
||||
scale: 130,
|
||||
}}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setPosition((prev) => ({
|
||||
...prev,
|
||||
zoom: Math.max(prev.zoom - 0.5, 1),
|
||||
}))
|
||||
}
|
||||
sx={{
|
||||
<ZoomableGroup
|
||||
center={position.coordinates}
|
||||
zoom={position.zoom}
|
||||
minZoom={1}
|
||||
maxZoom={8}
|
||||
translateExtent={[
|
||||
[-800, -400],
|
||||
[800, 400],
|
||||
]}
|
||||
onMoveEnd={({
|
||||
coordinates,
|
||||
zoom,
|
||||
}: {
|
||||
coordinates: [number, number];
|
||||
zoom: number;
|
||||
}) => {
|
||||
setPosition({ coordinates, zoom });
|
||||
}}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Geographies geography={MAP_TOPOJSON}>
|
||||
{({ geographies }: { geographies: GeoJSON.Feature[] }) =>
|
||||
geographies.map((geo) => {
|
||||
const d = Array.isArray(countries)
|
||||
? { nodes: 0 }
|
||||
: (countries as CountryDataResponse)[
|
||||
geo.properties?.ISO_A3 as string
|
||||
] || { nodes: 0 };
|
||||
return (
|
||||
<Geography
|
||||
key={`${geo.properties?.ISO_A3 || ""}-${geo.id}-${
|
||||
geo.properties?.NAME_LONG || ""
|
||||
}`}
|
||||
geography={geo}
|
||||
fill={colorScale(d?.nodes || 0)}
|
||||
stroke={
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.pine[800]
|
||||
: theme.palette.pine[200]
|
||||
}
|
||||
strokeWidth={0.2}
|
||||
data-tooltip-id="map-tooltip"
|
||||
onMouseEnter={() => {
|
||||
const { NAME_LONG } = geo.properties as {
|
||||
NAME_LONG: string;
|
||||
};
|
||||
setTooltipContent(`${NAME_LONG} | ${d?.nodes || 0}`);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setTooltipContent("");
|
||||
}}
|
||||
style={{
|
||||
hover: countries
|
||||
? {
|
||||
fill: theme.palette.accent.main,
|
||||
outline: "white",
|
||||
cursor: "pointer",
|
||||
}
|
||||
: undefined,
|
||||
default: {
|
||||
outline: "none",
|
||||
},
|
||||
pressed: {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Geographies>
|
||||
</ZoomableGroup>
|
||||
</ComposableMap>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
left: 10,
|
||||
zIndex: 1000,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.1)"
|
||||
: "rgba(0,0,0,0.1)",
|
||||
"&:hover": {
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
? "rgba(0,0,0,0.5)"
|
||||
: "rgba(255,255,255,0.5)",
|
||||
padding: "4px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<RemoveIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setPosition({ coordinates: [0, 0], zoom: 1 })}
|
||||
sx={{
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.1)"
|
||||
: "rgba(0,0,0,0.1)",
|
||||
"&:hover": {
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setPosition((prev) => ({
|
||||
...prev,
|
||||
zoom: Math.min(prev.zoom + 0.5, 8),
|
||||
}))
|
||||
}
|
||||
sx={{
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RestartAltIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Box>
|
||||
<Tooltip
|
||||
id="map-tooltip"
|
||||
content={tooltipContent}
|
||||
float={true}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
padding: "4px 8px",
|
||||
backgroundColor:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.pine[800]
|
||||
: theme.palette.pine[200],
|
||||
color:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.base.white
|
||||
: theme.palette.pine[950],
|
||||
borderRadius: "4px",
|
||||
zIndex: 9999,
|
||||
? "rgba(255,255,255,0.1)"
|
||||
: "rgba(0,0,0,0.1)",
|
||||
"&:hover": {
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setPosition((prev) => ({
|
||||
...prev,
|
||||
zoom: Math.max(prev.zoom - 0.5, 1),
|
||||
}))
|
||||
}
|
||||
sx={{
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.1)"
|
||||
: "rgba(0,0,0,0.1)",
|
||||
"&:hover": {
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RemoveIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setPosition({ coordinates: [0, 0], zoom: 1 })}
|
||||
sx={{
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.1)"
|
||||
: "rgba(0,0,0,0.1)",
|
||||
"&:hover": {
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RestartAltIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Box>
|
||||
<Tooltip
|
||||
id="map-tooltip"
|
||||
content={tooltipContent}
|
||||
float={true}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
padding: "4px 8px",
|
||||
backgroundColor:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.pine[800]
|
||||
: theme.palette.pine[200],
|
||||
color:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.base.white
|
||||
: theme.palette.pine[950],
|
||||
borderRadius: "4px",
|
||||
zIndex: 9999,
|
||||
}}
|
||||
/>
|
||||
</ExplorerCard>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: {
|
||||
xs: "1fr",
|
||||
sm: "repeat(3, 1fr)",
|
||||
},
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
}}
|
||||
/>
|
||||
</ExplorerCard>
|
||||
>
|
||||
<ExplorerCard label="Number of all servers">
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
{totalServers}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
<ExplorerCard label="Number of countries">
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
{totalCountries}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
<ExplorerCard label="Number of locations">
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
{uniqueLocations}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user