forked from emre/www_projectmycelium_com
150 lines
4.7 KiB
TypeScript
150 lines
4.7 KiB
TypeScript
"use client";
|
||
import { useEffect, useState } from "react";
|
||
import DynamicMapContainer from "@/components/ui/DynamicMapContainer";
|
||
import { Eyebrow, H3, P } from "@/components/Texts";
|
||
|
||
type StatKey = "cores" | "nodes" | "ssd" | "countries";
|
||
|
||
type StatsData = Record<StatKey, string>;
|
||
|
||
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<StatsData>(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 (
|
||
<div className="bg-[#121212] w-full">
|
||
{/* ✅ Top horizontal line with spacing */}
|
||
<div className="max-w-7xl bg-transparent mx-auto py-6 border border-t-0 border-b-0 border-gray-800"></div>
|
||
<div className="w-full border-t border-l border-r border-gray-800" />
|
||
|
||
<div className="max-w-7xl mx-auto text-center pt-12 border border-t-0 border-b-0 border-gray-800">
|
||
<Eyebrow>PROJECT MYCELIUM IS LIVE. </Eyebrow>
|
||
<H3 className="text-white">Host a Node, Grow the Network</H3>
|
||
<P className="text-sm md:text-lg text-gray-200 max-w-3xl mx-auto py-4">
|
||
Mycelium runs on nodes hosted by people and organizations around the world.
|
||
Each node adds compute, storage, and bandwidth, expanding the network’s capacity and resilience.
|
||
</P>
|
||
<P className="text-sm md:text-lg text-gray-200 max-w-3xl mx-auto py-4">
|
||
You can share your idle resources and earn rewards when they are used.
|
||
Configure it once. Your node takes over from there.
|
||
</P>
|
||
|
||
</div>
|
||
<div className="max-w-7xl mx-auto border border-t-0 border-b-0 border-gray-800 ">
|
||
{/* ✅ Match same side margins */}
|
||
<div className="max-w-5xl mx-auto px-6 ">
|
||
<DynamicMapContainer />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mx-auto max-w-7xl px-6 lg:px-8 border border-t-0 border-b-0 border-gray-800 pb-12">
|
||
|
||
<dl className="pt-6 grid grid-cols-1 gap-0.5 overflow-hidden rounded-md text-center sm:grid-cols-2 lg:grid-cols-4">
|
||
{STAT_CARDS.map(({ key, title, description }) => (
|
||
<div
|
||
key={key}
|
||
className="flex flex-col bg-white/1 p-8"
|
||
>
|
||
<dt className="text-sm/6 font-semibold text-gray-300">
|
||
{title}
|
||
</dt>
|
||
|
||
<dd className="order-first text-3xl font-semibold tracking-tight text-white">
|
||
{isLoading ? "…" : stats[key]}
|
||
</dd>
|
||
|
||
<p className="mt-2 text-sm text-gray-400">
|
||
{description}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</dl>
|
||
|
||
</div>
|
||
|
||
{/* ✅ Bottom horizontal line with spacing */}
|
||
<div className="w-full border-b border-gray-800" />
|
||
<div className="max-w-7xl bg-transparent mx-auto py-6 border border-t-0 border-b-0 border-gray-800"></div>
|
||
</div>
|
||
);
|
||
}
|