From 8fdcf1777d3b1220ffad1d23864c40523598a8c9 Mon Sep 17 00:00:00 2001 From: ehab-hassan Date: Tue, 18 Nov 2025 13:28:20 +0200 Subject: [PATCH 1/5] update statics to be dynamic --- src/pages/home/HomeMap.tsx | 102 +++++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 11 deletions(-) diff --git a/src/pages/home/HomeMap.tsx b/src/pages/home/HomeMap.tsx index c4ea97b..97cd2a8 100644 --- a/src/pages/home/HomeMap.tsx +++ b/src/pages/home/HomeMap.tsx @@ -1,15 +1,95 @@ "use client"; +import { useEffect, useState } from "react"; import WorldMap from "@/components/ui/world-map"; import { Eyebrow, H3, P } from "@/components/Texts"; -const stats = [ - { id: 1, name: 'CORES', value: '54,958', description: 'Total Central Processing Unit cores available on the grid.' }, - { id: 2, name: 'NODES', value: '1,493', description: 'Total number of nodes on the grid.' }, - { id: 3, name: 'SSD CAPACITY', value: '5,388,956', description: 'Total GB of storage (SSD, HDD, & RAM) on the grid.' }, - { id: 4, name: 'COUNTRIES', value: '44', description: 'Total number of countries with active nodes.' }, -] +type StatKey = "cores" | "nodes" | "ssd" | "countries"; + +type StatsData = Record; + +const STAT_API_URL = "https://stats.grid.tf/api/stats-summary"; + +const DEFAULT_STATS: StatsData = { + cores: "31,669", + nodes: "1157", + ssd: "4,199,303", + countries: "41", +}; + +const STAT_CARDS: Array<{ key: StatKey; title: string; description: string }> = [ + { + key: "ssd", + title: "SSD CAPACITY", + description: "Total GB of storage (SSD, HDD, & RAM) on the grid.", + }, + { + key: "cores", + title: "CORES", + description: "Total Central Processing Unit cores available on the grid.", + }, + { + key: "nodes", + title: "NODES", + description: "Total number of nodes on the grid.", + }, + + { + key: "countries", + title: "COUNTRIES", + description: "Total number of countries with active nodes.", + }, +]; export function HomeMap() { + const [stats, setStats] = useState(DEFAULT_STATS); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + let isMounted = true; + + const formatValue = (value: unknown, fallback: string) => { + if (typeof value === "number") { + return value.toLocaleString(); + } + if (typeof value === "string" && value.trim().length) { + const numeric = Number(value); + return Number.isNaN(numeric) ? value : numeric.toLocaleString(); + } + return fallback; + }; + + async function fetchStats() { + try { + const response = await fetch(STAT_API_URL); + if (!response.ok) { + throw new Error(`Request failed with ${response.status}`); + } + const data = await response.json(); + + if (!isMounted) return; + + setStats({ + cores: formatValue(data?.cores, DEFAULT_STATS.cores), + nodes: formatValue(data?.nodes, DEFAULT_STATS.nodes), + ssd: formatValue(data?.ssd, DEFAULT_STATS.ssd), + countries: formatValue(data?.countries, DEFAULT_STATS.countries), + }); + } catch (error) { + console.error("[HomeMap] Failed to load stats", error); + } finally { + if (isMounted) { + setIsLoading(false); + } + } + } + + fetchStats(); + + return () => { + isMounted = false; + }; + }, []); + return (
{/* βœ… Top horizontal line with spacing */} @@ -48,21 +128,21 @@ Configure it once. Your node takes over from there.
- {stats.map((stat) => ( + {STAT_CARDS.map(({ key, title, description }) => (
- {stat.name} + {title}
- {stat.value} + {isLoading ? "…" : stats[key]}

- {stat.description} + {description}

))} From 8dfc46cabe2a55372d960b2199e880c8f8664da1 Mon Sep 17 00:00:00 2001 From: ehab-hassan Date: Tue, 18 Nov 2025 16:43:23 +0200 Subject: [PATCH 2/5] Replace static actual grid data --- src/components/ui/DynamicMapContainer.tsx | 84 +++++++++++++++++++++++ src/components/ui/world-map.tsx | 75 ++++++++++++++------ src/pages/home/HomeMap.tsx | 13 +--- 3 files changed, 141 insertions(+), 31 deletions(-) create mode 100644 src/components/ui/DynamicMapContainer.tsx diff --git a/src/components/ui/DynamicMapContainer.tsx b/src/components/ui/DynamicMapContainer.tsx new file mode 100644 index 0000000..9bc988a --- /dev/null +++ b/src/components/ui/DynamicMapContainer.tsx @@ -0,0 +1,84 @@ +import { useState, useEffect } from 'react'; +import WorldMap from './world-map'; +import { motion } from 'framer-motion'; + +// Interface for the simplified data passed to WorldMap +interface GeoNode { + lat: number; + lng: number; + label?: string; + color?: string; +} + +// Interface for the raw data structure expected from the gridproxy API +interface RawNode { + node_id: number; + location: { + latitude: string; // API often returns these as strings + longitude: string; // API often returns these as strings + city: string; + country: string; + }; + // ... other raw fields you don't need +} + +function DynamicMapContainer() { + const [loading, setLoading] = useState(true); + const [nodes, setNodes] = useState([]); + const API_URL = "https://gridproxy.grid.tf/nodes?healthy=true"; + + useEffect(() => { + async function fetchNodeData() { + try { + const response = await fetch(API_URL); + const data: RawNode[] = await response.json(); // Type the incoming data + + // 🚨 Map the API response to your component's expected GeoNode format + const geoNodes: GeoNode[] = data + .filter((node: RawNode) => node.location && node.location.latitude && node.location.longitude) + .map((node: RawNode) => ({ + // Convert string coordinates to numbers + lat: parseFloat(node.location.latitude), + lng: parseFloat(node.location.longitude), + label: `${node.location.city}, ${node.location.country} (${node.node_id})`, + // Optionally set color based on some node property if available + })); + + setNodes(geoNodes); + setLoading(false); + } catch (error) { + console.error("Failed to fetch node data:", error); + setLoading(false); + } + } + + fetchNodeData(); + }, []); + + // --- RENDERING --- + + if (loading) { + // Show a loading state while data is being fetched + return ( +
+ + 🌎 + +

Loading nodes...

+
+ ); + } + + // Pass the dynamically fetched nodes to your WorldMap component + return ( + + ); +} + +export default DynamicMapContainer; \ No newline at end of file diff --git a/src/components/ui/world-map.tsx b/src/components/ui/world-map.tsx index 64e30c8..bb44360 100644 --- a/src/components/ui/world-map.tsx +++ b/src/components/ui/world-map.tsx @@ -1,7 +1,7 @@ "use client"; import { useRef } from "react"; -import { motion } from "motion/react"; +import { motion } from "framer-motion"; import DottedMap from "dotted-map"; interface MapProps { @@ -9,25 +9,29 @@ interface MapProps { start: { lat: number; lng: number; label?: string }; end: { lat: number; lng: number; label?: string }; }>; + // New prop for dynamic standalone nodes + nodes?: Array<{ lat: number; lng: number; label?: string; color?: string }>; lineColor?: string; } export default function WorldMap({ dots = [], - lineColor = "#06b6d4", // cyan-500 + nodes = [], + lineColor = "#06b6d4", }: MapProps) { const svgRef = useRef(null); - // βœ… Force dark-dotted map theme + // βœ… Force dark-dotted map theme const map = new DottedMap({ height: 100, grid: "diagonal" }); const svgMap = map.getSVG({ radius: 0.22, - color: "#06b6d480", // cyan-500 at 50% opacity + color: "#06b6d480", shape: "circle", backgroundColor: "#111111", }); - // βœ… Point projection stays the same + // βœ… Point projection stays the same + // Projects lat/lng to the SVG's 800x400 viewBox coordinates const projectPoint = (lat: number, lng: number) => { const x = (lng + 180) * (800 / 360); const y = (90 - lat) * (400 / 180); @@ -36,7 +40,8 @@ export default function WorldMap({ const createCurvedPath = (start: any, end: any) => { const midX = (start.x + end.x) / 2; - const midY = Math.min(start.y, end.y) - 50; + // Creates an arc that bows upward by 50 units + const midY = Math.min(start.y, end.y) - 50; return `M ${start.x} ${start.y} Q ${midX} ${midY} ${end.x} ${end.y}`; }; @@ -49,13 +54,53 @@ export default function WorldMap({ draggable={false} /> - {/* βœ… Lines + points */} + {/* βœ… Lines + points + new standalone nodes */} - {/* βœ… animated curved travel lines */} + {/* Glowing path gradient DEFS */} + + + + + + + + + + {/* βœ… DYNAMIC STANDALONE NODE DOTS (New Section) */} + {nodes.map((node, i) => { + const p = projectPoint(node.lat, node.lng); + const dotColor = node.color || lineColor; + + return ( + + {/* Outer pulsing circle */} + + + + + {/* Inner fixed circle */} + + + ); + })} + + {/* βœ… Animated curved travel lines (Existing Logic) */} {dots.map((dot, i) => { const startPoint = projectPoint(dot.start.lat, dot.start.lng); const endPoint = projectPoint(dot.end.lat, dot.end.lng); @@ -78,17 +123,7 @@ export default function WorldMap({ ); })} - {/* βœ… glowing path gradient */} - - - - - - - - - - {/* βœ… start & end points with pulsing cyan glow */} + {/* βœ… Start & end points with pulsing cyan glow (Existing Logic) */} {dots.map((dot, i) => { const s = projectPoint(dot.start.lat, dot.start.lng); const e = projectPoint(dot.end.lat, dot.end.lng); @@ -122,4 +157,4 @@ export default function WorldMap({
); -} +} \ No newline at end of file diff --git a/src/pages/home/HomeMap.tsx b/src/pages/home/HomeMap.tsx index 97cd2a8..95dfcf8 100644 --- a/src/pages/home/HomeMap.tsx +++ b/src/pages/home/HomeMap.tsx @@ -1,6 +1,6 @@ "use client"; import { useEffect, useState } from "react"; -import WorldMap from "@/components/ui/world-map"; +import DynamicMapContainer from "@/components/ui/DynamicMapContainer"; import { Eyebrow, H3, P } from "@/components/Texts"; type StatKey = "cores" | "nodes" | "ssd" | "countries"; @@ -112,16 +112,7 @@ Configure it once. Your node takes over from there.
{/* βœ… Match same side margins */}
- +
From cbbc87cc298d7098501a7724850529b88f430fa6 Mon Sep 17 00:00:00 2001 From: ehab-hassan Date: Wed, 19 Nov 2025 10:24:15 +0200 Subject: [PATCH 3/5] update map with nodes dynamic --- src/components/ui/DynamicMapContainer.tsx | 2 +- src/components/ui/world-map.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ui/DynamicMapContainer.tsx b/src/components/ui/DynamicMapContainer.tsx index 9bc988a..8e3da85 100644 --- a/src/components/ui/DynamicMapContainer.tsx +++ b/src/components/ui/DynamicMapContainer.tsx @@ -25,7 +25,7 @@ interface RawNode { function DynamicMapContainer() { const [loading, setLoading] = useState(true); const [nodes, setNodes] = useState([]); - const API_URL = "https://gridproxy.grid.tf/nodes?healthy=true"; + const API_URL = "https://gridproxy.grid.tf/nodes?healthy=true&size=99999"; useEffect(() => { async function fetchNodeData() { diff --git a/src/components/ui/world-map.tsx b/src/components/ui/world-map.tsx index bb44360..41ce284 100644 --- a/src/components/ui/world-map.tsx +++ b/src/components/ui/world-map.tsx @@ -34,7 +34,7 @@ export default function WorldMap({ // Projects lat/lng to the SVG's 800x400 viewBox coordinates const projectPoint = (lat: number, lng: number) => { const x = (lng + 180) * (800 / 360); - const y = (90 - lat) * (400 / 180); + const y = (90 - lat) * (400 / 180) + 45; return { x, y }; }; From fa30aeff437f81dba8e6e5cca1216d25e5eb6fd3 Mon Sep 17 00:00:00 2001 From: ehab-hassan Date: Wed, 19 Nov 2025 10:56:29 +0200 Subject: [PATCH 4/5] make map load fast --- src/components/ui/DynamicMapContainer.tsx | 49 ++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/components/ui/DynamicMapContainer.tsx b/src/components/ui/DynamicMapContainer.tsx index 8e3da85..4ea6596 100644 --- a/src/components/ui/DynamicMapContainer.tsx +++ b/src/components/ui/DynamicMapContainer.tsx @@ -22,6 +22,52 @@ interface RawNode { // ... other raw fields you don't need } +const clusterNodes = (nodeList: GeoNode[], cellSize = 2) => { + const buckets = new Map< + string, + { latSum: number; lngSum: number; count: number } + >(); + + nodeList.forEach((node) => { + const latBucket = Math.round(node.lat / cellSize) * cellSize; + const lngBucket = Math.round(node.lng / cellSize) * cellSize; + const key = `${latBucket}|${lngBucket}`; + + const bucket = buckets.get(key); + if (bucket) { + bucket.latSum += node.lat; + bucket.lngSum += node.lng; + bucket.count += 1; + } else { + buckets.set(key, { + latSum: node.lat, + lngSum: node.lng, + count: 1, + }); + } + }); + + return Array.from(buckets.values()).map((bucket) => { + const avgLat = bucket.latSum / bucket.count; + const avgLng = bucket.lngSum / bucket.count; + const count = bucket.count; + + let color = "#06b6d4"; + if (count > 20) { + color = "#0891b2"; + } else if (count > 5) { + color = "#22d3ee"; + } + + return { + lat: avgLat, + lng: avgLng, + color, + label: `${count} nodes`, + }; + }); +}; + function DynamicMapContainer() { const [loading, setLoading] = useState(true); const [nodes, setNodes] = useState([]); @@ -44,7 +90,8 @@ function DynamicMapContainer() { // Optionally set color based on some node property if available })); - setNodes(geoNodes); + const clusteredNodes = clusterNodes(geoNodes); + setNodes(clusteredNodes); setLoading(false); } catch (error) { console.error("Failed to fetch node data:", error); From 3f89d1c441d212e6ed145e40efeebbb48e4a128a Mon Sep 17 00:00:00 2001 From: ehab-hassan Date: Wed, 19 Nov 2025 11:20:12 +0200 Subject: [PATCH 5/5] add some padding at mobile --- src/pages/home/HomeMap.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/HomeMap.tsx b/src/pages/home/HomeMap.tsx index 95dfcf8..5770c30 100644 --- a/src/pages/home/HomeMap.tsx +++ b/src/pages/home/HomeMap.tsx @@ -96,7 +96,7 @@ export function HomeMap() {
-
+
PROJECT MYCELIUM IS LIVE.

Host a Node, Grow the Network