forked from emre/www_projectmycelium_com
Replace static actual grid data
This commit is contained in:
84
src/components/ui/DynamicMapContainer.tsx
Normal file
84
src/components/ui/DynamicMapContainer.tsx
Normal file
@@ -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<GeoNode[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className="flex justify-center items-center w-full aspect-[2/1] bg-[#111111] rounded-lg text-cyan-500">
|
||||||
|
<motion.span
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
||||||
|
className="text-4xl"
|
||||||
|
>
|
||||||
|
🌎
|
||||||
|
</motion.span>
|
||||||
|
<p className="ml-4">Loading nodes...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass the dynamically fetched nodes to your WorldMap component
|
||||||
|
return (
|
||||||
|
<WorldMap
|
||||||
|
nodes={nodes}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DynamicMapContainer;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "framer-motion";
|
||||||
import DottedMap from "dotted-map";
|
import DottedMap from "dotted-map";
|
||||||
|
|
||||||
interface MapProps {
|
interface MapProps {
|
||||||
@@ -9,12 +9,15 @@ interface MapProps {
|
|||||||
start: { lat: number; lng: number; label?: string };
|
start: { lat: number; lng: number; label?: string };
|
||||||
end: { 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;
|
lineColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WorldMap({
|
export default function WorldMap({
|
||||||
dots = [],
|
dots = [],
|
||||||
lineColor = "#06b6d4", // cyan-500
|
nodes = [],
|
||||||
|
lineColor = "#06b6d4",
|
||||||
}: MapProps) {
|
}: MapProps) {
|
||||||
const svgRef = useRef<SVGSVGElement>(null);
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
@@ -22,12 +25,13 @@ export default function WorldMap({
|
|||||||
const map = new DottedMap({ height: 100, grid: "diagonal" });
|
const map = new DottedMap({ height: 100, grid: "diagonal" });
|
||||||
const svgMap = map.getSVG({
|
const svgMap = map.getSVG({
|
||||||
radius: 0.22,
|
radius: 0.22,
|
||||||
color: "#06b6d480", // cyan-500 at 50% opacity
|
color: "#06b6d480",
|
||||||
shape: "circle",
|
shape: "circle",
|
||||||
backgroundColor: "#111111",
|
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 projectPoint = (lat: number, lng: number) => {
|
||||||
const x = (lng + 180) * (800 / 360);
|
const x = (lng + 180) * (800 / 360);
|
||||||
const y = (90 - lat) * (400 / 180);
|
const y = (90 - lat) * (400 / 180);
|
||||||
@@ -36,6 +40,7 @@ export default function WorldMap({
|
|||||||
|
|
||||||
const createCurvedPath = (start: any, end: any) => {
|
const createCurvedPath = (start: any, end: any) => {
|
||||||
const midX = (start.x + end.x) / 2;
|
const midX = (start.x + end.x) / 2;
|
||||||
|
// Creates an arc that bows upward by 50 units
|
||||||
const midY = Math.min(start.y, end.y) - 50;
|
const midY = Math.min(start.y, end.y) - 50;
|
||||||
return `M ${start.x} ${start.y} Q ${midX} ${midY} ${end.x} ${end.y}`;
|
return `M ${start.x} ${start.y} Q ${midX} ${midY} ${end.x} ${end.y}`;
|
||||||
};
|
};
|
||||||
@@ -49,13 +54,53 @@ export default function WorldMap({
|
|||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ✅ Lines + points */}
|
{/* ✅ Lines + points + new standalone nodes */}
|
||||||
<svg
|
<svg
|
||||||
ref={svgRef}
|
ref={svgRef}
|
||||||
viewBox="0 0 800 400"
|
viewBox="0 0 800 400"
|
||||||
className="w-full h-full absolute inset-0 pointer-events-none select-none"
|
className="w-full h-full absolute inset-0 pointer-events-none select-none"
|
||||||
>
|
>
|
||||||
{/* ✅ animated curved travel lines */}
|
{/* Glowing path gradient DEFS */}
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="path-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stopColor="black" stopOpacity="0" />
|
||||||
|
<stop offset="5%" stopColor={lineColor} stopOpacity="1" />
|
||||||
|
<stop offset="95%" stopColor={lineColor} stopOpacity="1" />
|
||||||
|
<stop offset="100%" stopColor="black" stopOpacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* ✅ DYNAMIC STANDALONE NODE DOTS (New Section) */}
|
||||||
|
{nodes.map((node, i) => {
|
||||||
|
const p = projectPoint(node.lat, node.lng);
|
||||||
|
const dotColor = node.color || lineColor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g key={`node-${i}`}>
|
||||||
|
{/* Outer pulsing circle */}
|
||||||
|
<circle cx={p.x} cy={p.y} r="2" fill={dotColor} opacity="0.5">
|
||||||
|
<animate
|
||||||
|
attributeName="r"
|
||||||
|
from="2"
|
||||||
|
to="7"
|
||||||
|
dur="1.4s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
from="0.6"
|
||||||
|
to="0"
|
||||||
|
dur="1.4s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
{/* Inner fixed circle */}
|
||||||
|
<circle cx={p.x} cy={p.y} r="2" fill={dotColor} />
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* ✅ Animated curved travel lines (Existing Logic) */}
|
||||||
{dots.map((dot, i) => {
|
{dots.map((dot, i) => {
|
||||||
const startPoint = projectPoint(dot.start.lat, dot.start.lng);
|
const startPoint = projectPoint(dot.start.lat, dot.start.lng);
|
||||||
const endPoint = projectPoint(dot.end.lat, dot.end.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 (Existing Logic) */}
|
||||||
<defs>
|
|
||||||
<linearGradient id="path-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
||||||
<stop offset="0%" stopColor="black" stopOpacity="0" />
|
|
||||||
<stop offset="5%" stopColor={lineColor} stopOpacity="1" />
|
|
||||||
<stop offset="95%" stopColor={lineColor} stopOpacity="1" />
|
|
||||||
<stop offset="100%" stopColor="black" stopOpacity="0" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
{/* ✅ start & end points with pulsing cyan glow */}
|
|
||||||
{dots.map((dot, i) => {
|
{dots.map((dot, i) => {
|
||||||
const s = projectPoint(dot.start.lat, dot.start.lng);
|
const s = projectPoint(dot.start.lat, dot.start.lng);
|
||||||
const e = projectPoint(dot.end.lat, dot.end.lng);
|
const e = projectPoint(dot.end.lat, dot.end.lng);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useState } from "react";
|
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";
|
import { Eyebrow, H3, P } from "@/components/Texts";
|
||||||
|
|
||||||
type StatKey = "cores" | "nodes" | "ssd" | "countries";
|
type StatKey = "cores" | "nodes" | "ssd" | "countries";
|
||||||
@@ -112,16 +112,7 @@ Configure it once. Your node takes over from there.
|
|||||||
<div className="max-w-7xl mx-auto border border-t-0 border-b-0 border-gray-800 ">
|
<div className="max-w-7xl mx-auto border border-t-0 border-b-0 border-gray-800 ">
|
||||||
{/* ✅ Match same side margins */}
|
{/* ✅ Match same side margins */}
|
||||||
<div className="max-w-5xl mx-auto px-6 ">
|
<div className="max-w-5xl mx-auto px-6 ">
|
||||||
<WorldMap
|
<DynamicMapContainer />
|
||||||
dots={[
|
|
||||||
{ start: { lat: 64.2008, lng: -149.4937 }, end: { lat: 34.0522, lng: -118.2437 } }, // Alaska → LA
|
|
||||||
{ start: { lat: 64.2008, lng: -149.4937 }, end: { lat: -15.7975, lng: -47.8919 } }, // Alaska → Brasília
|
|
||||||
{ start: { lat: -15.7975, lng: -47.8919 }, end: { lat: 38.7223, lng: -9.1393 } }, // Brasília → Lisbon
|
|
||||||
{ start: { lat: 51.5074, lng: -0.1278 }, end: { lat: 28.6139, lng: 77.209 } }, // London → New Delhi
|
|
||||||
{ start: { lat: 28.6139, lng: 77.209 }, end: { lat: 43.1332, lng: 131.9113 } }, // New Delhi → Vladivostok
|
|
||||||
{ start: { lat: 28.6139, lng: 77.209 }, end: { lat: -1.2921, lng: 36.8219 } }, // New Delhi → Nairobi
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user