feat: add nodes page with hosting information and benefits

- Created new NodePage with hero section explaining the Mycelium node network
- Added NodeBenefits component showcasing three key advantages of hosting nodes
- Integrated nodes route into navigation (header, footer, and routing configuration)
This commit is contained in:
2025-11-14 12:14:28 +01:00
parent 96a17a668a
commit 359afc3360
6 changed files with 187 additions and 0 deletions

View File

@@ -11,6 +11,7 @@ const ComputePage = lazy(() => import('./pages/compute/ComputePage'));
const StoragePage = lazy(() => import('./pages/storage/StoragePage')); const StoragePage = lazy(() => import('./pages/storage/StoragePage'));
const GpuPage = lazy(() => import('./pages/gpu/GpuPage')); const GpuPage = lazy(() => import('./pages/gpu/GpuPage'));
const PodsPage = lazy(() => import('./pages/pods/PodsPage')); const PodsPage = lazy(() => import('./pages/pods/PodsPage'));
const NodePage = lazy(() => import('./pages/node/NodePage'));
function App() { function App() {
return ( return (
@@ -27,6 +28,7 @@ function App() {
<Route path="storage" element={<StoragePage />} /> <Route path="storage" element={<StoragePage />} />
<Route path="gpu" element={<GpuPage />} /> <Route path="gpu" element={<GpuPage />} />
<Route path="pods" element={<PodsPage />} /> <Route path="pods" element={<PodsPage />} />
<Route path="nodes" element={<NodePage />} />
</Route> </Route>
</Routes> </Routes>
</Suspense> </Suspense>

View File

@@ -30,6 +30,9 @@ export function Footer() {
<Link to="/pods" className="text-sm text-gray-700 hover:text-cyan-500 transition-colors"> <Link to="/pods" className="text-sm text-gray-700 hover:text-cyan-500 transition-colors">
Pods Pods
</Link> </Link>
<Link to="/nodes" className="text-sm text-gray-700 hover:text-cyan-500 transition-colors">
Nodes
</Link>
</nav> </nav>
</div> </div>
<div className="group relative -mx-4 flex items-center self-stretch p-4 transition-colors hover:bg-gray-100 sm:self-auto sm:rounded-2xl lg:mx-0 lg:self-auto lg:p-6"> <div className="group relative -mx-4 flex items-center self-stretch p-4 transition-colors hover:bg-gray-100 sm:self-auto sm:rounded-2xl lg:mx-0 lg:self-auto lg:p-6">

View File

@@ -70,6 +70,12 @@ export function Header() {
> >
Pods Pods
</Link> </Link>
<Link
to="/nodes"
className="text-base/7 tracking-tight text-gray-700 hover:text-cyan-500 transition-colors"
>
Nodes
</Link>
</div> </div>
</div> </div>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
@@ -155,6 +161,13 @@ export function Header() {
> >
Pods Pods
</Link> </Link>
<Link
to="/nodes"
className="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-gray-900 hover:bg-gray-50"
onClick={() => setMobileMenuOpen(false)}
>
Nodes
</Link>
</div> </div>
<div className="py-6"> <div className="py-6">
<Button <Button

View File

@@ -0,0 +1,115 @@
"use client";
import { motion } from "framer-motion";
import { SectionHeader, P, Eyebrow, CT, CP } from "@/components/Texts";
import type { ComponentPropsWithoutRef } from "react";
type CircleIconProps = ComponentPropsWithoutRef<"svg">;
const CircleNumber1Icon = (props: CircleIconProps) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2zm.994 5.886c-.83-.777-1.755-.394-1.701.404l-.006.114v8l.007.117a1 1 0 001.986 0l.007-.117V9l-.006-.114z" />
</svg>
);
const CircleNumber2Icon = (props: CircleIconProps) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M12 2a10 10 0 110 20 10 10 0 010-20zm1 5h-3a1 1 0 100 2h3v2h-2a2 2 0 00-2 2v2a2 2 0 002 2h3a1 1 0 100-2h-3v-2h2a2 2 0 002-2V9a2 2 0 00-2-2z" />
</svg>
);
const CircleNumber3Icon = (props: CircleIconProps) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M12 2a10 10 0 110 20 10 10 0 010-20zm1 5h-2a2 2 0 00-2 2 1 1 0 102 0h2v2h-2c-1.39 0-2.103 1.67-1.11 2.588l.11.098h3v2h-2a1 1 0 100 2h2a2 2 0 002-2v-2a2 2 0 00-.25-.97l-.068-.115.068-.115A1.99 1.99 0 0015 11V9a2 2 0 00-2-2z" />
</svg>
);
const features = [
{
name: "Build Your Private Setup",
description:
"Run a node in your home or office and use it to host private workloads, models, and data. Experiment, deploy, or tinker locally, then offset your costs by sharing unused capacity with the network.",
icon: CircleNumber1Icon,
},
{
name: "Strengthen the Network",
description:
"Every node adds bandwidth, compute, and resilience to the Mycelium Grid. Together, hosts form the physical foundation for decentralized communication, storage, and AI.",
icon: CircleNumber2Icon,
},
{
name: "Earn Rewards",
description:
"Share storage, CPU, GPU, and bandwidth. When your resources are used, you earn network rewards based on real demand.",
icon: CircleNumber3Icon,
},
];
export function NodeBenefits() {
return (
<section className="bg-[#121212] w-full max-w-8xl mx-auto">
{/* Top spacing + border */}
<div className="max-w-7xl mx-auto py-6 border border-t-0 border-b-0 border-gray-800" />
<div className="w-full border-t border-l border-r border-gray-800" />
<div className="relative px-6 lg:px-12 py-12 bg-[#111111] border border-t-0 border-b-0 border-gray-800 max-w-7xl mx-auto">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.1 }}
className="mx-auto max-w-5xl text-center"
>
<Eyebrow color="accent">Host</Eyebrow>
<SectionHeader
className="text-3xl font-medium tracking-tight"
color="light"
>
Benefits of Hosting Nodes
</SectionHeader>
<P className="mt-6" color="light">
Hosting a node gives you private compute, contributes to the global
Mycelium infrastructure, and unlocks ways to earn from real network
usage all while keeping sovereignty and control.
</P>
</motion.div>
{/* Features */}
<motion.ul
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true, amount: 0.2 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="mx-auto mt-8 grid max-w-2xl grid-cols-1 gap-x-8 gap-y-8 text-base/7 sm:grid-cols-2 sm:gap-y-16 lg:mx-12 lg:mt-12 lg:max-w-7xl lg:grid-cols-3"
>
{features.map((feature, index) => (
<motion.li
key={feature.name}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.2 }}
transition={{
duration: 0.45,
delay: 0.3 + index * 0.15,
ease: "easeOut",
}}
className="rounded-2xl border border-gray-300 bg-white/5 p-8 transition-all duration-300 ease-in-out hover:scale-105 hover:border-cyan-500 hover:shadow-lg hover:shadow-cyan-500/20 backdrop-blur-md"
>
<feature.icon className="mb-4 h-8 w-8 text-white" />
<CT as="span" className="text-left font-semibold" color="light">
{feature.name}
</CT>
<CP className="mt-2 text-left text-sm" color="light">
{feature.description}
</CP>
</motion.li>
))}
</motion.ul>
</div>
<div className="w-full border-b border-gray-800 bg-[#121212]" />
</section>
);
}

View File

@@ -0,0 +1,40 @@
'use client'
import { Button } from '@/components/Button'
import { Eyebrow, H3, P } from '@/components/Texts'
export function NodeHero() {
return (
<div className="">
{/* Boxed container */}
<div
className="relative mx-auto max-w-7xl border border-t-0 border-b-0 border-gray-100 bg-white overflow-hidden"
>
{/* Inner padding */}
<div className="px-6 py-16 lg:py-16">
<div className="max-w-3xl mx-auto text-center">
<Eyebrow>MYCELIUM NODES</Eyebrow>
<H3 as="h1" className="mt-4">
Host a Node. Power the Network.
</H3>
<P className="mt-6 text-gray-800">
The Mycelium Network runs on nodes hosted by people and organizations around the world. Each node adds capacity, resilience, and sovereignty, expanding a global network for private, distributed compute and AI. 
</P>
<div className="mt-10 flex items-center justify-center gap-x-6">
<Button href="/host" variant="solid" color="cyan">
Host a Node
</Button>
<Button href="#" variant="outline">
Learn More
</Button>
</div>
</div>
</div>
</div>
{/* ✅ Bottom horizontal line with spacing */}
<div className="w-full border-b border-gray-100" />
<div className="max-w-7xl bg-transparent mx-auto py-6 border border-t-0 border-b-0 border-gray-100"></div>
</div>
)
}

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { NodeHero } from './NodeHero';
import { NodeBenefits } from './NodeBenefits';
const NodePage: React.FC = () => {
return (
<>
<NodeHero />
<NodeBenefits />
</>
);
};
export default NodePage;