forked from emre/www_projectmycelium_com
feat: redesign node products section with interactive configuration selector
- Replaced static grid layout with dynamic two-column design featuring live product switching - Added configuration selector allowing users to toggle between AI Node and Compute Node options - Enhanced product information with detailed features, descriptions, and direct purchase/learn more CTAs
This commit is contained in:
@@ -1,90 +1,222 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { Eyebrow, SectionHeader, P, CT, CP } from "@/components/Texts";
|
import { Eyebrow, SectionHeader, P, CT, CP } from "@/components/Texts";
|
||||||
|
import {
|
||||||
|
QuestionMarkCircleIcon,
|
||||||
|
ShieldCheckIcon,
|
||||||
|
CheckIcon,
|
||||||
|
} from "@heroicons/react/20/solid";
|
||||||
|
|
||||||
const nodes = [
|
/* ------------------------------------------ */
|
||||||
{
|
/* PRODUCT DATA */
|
||||||
|
/* ------------------------------------------ */
|
||||||
|
|
||||||
|
const nodes = {
|
||||||
|
ainode: {
|
||||||
|
id: "ainode",
|
||||||
name: "Edge AI Node",
|
name: "Edge AI Node",
|
||||||
subtitle: "Based on Ryzen AI MAX+ 395 platform",
|
subtitle: "Based on Ryzen AI MAX+ 395 platform",
|
||||||
|
description:
|
||||||
|
"A compact AI-ready node designed for local inference, agent hosting, and sovereign edge compute. Equipped with a dedicated AI accelerator and optimized thermals.",
|
||||||
image: "/images/ainode.png",
|
image: "/images/ainode.png",
|
||||||
|
features: [
|
||||||
|
"Run local AI and cloud workloads",
|
||||||
|
"Host Mycelium Slices and earn SPORE",
|
||||||
|
"Experiment with decentralized apps and LLM agents",
|
||||||
|
"Participate in the global Mycelium Network",
|
||||||
|
],
|
||||||
|
buyUrl:
|
||||||
|
"https://www.gmktec.com/products/amd-ryzen%E2%84%A2-ai-max-395-evo-x2-ai-mini-pc?variant=6f7af17b-b907-4a9d-9c7e-afecfb41ed98",
|
||||||
|
learnUrl:
|
||||||
|
"https://threefold.info/mycelium_economics/docs/recommended_nodes/edge_ai_node",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
|
edgenode: {
|
||||||
|
id: "edgenode",
|
||||||
name: "Edge Compute Node",
|
name: "Edge Compute Node",
|
||||||
subtitle: "Based on GMKtec NUCBox M6 Ultra (Ryzen 5 7640HS)",
|
subtitle: "Based on GMKtec NUCBox M6 Ultra (Ryzen 5 7640HS)",
|
||||||
|
description:
|
||||||
|
"High-performance edge compute for networking, local models, and multiple agents. Excellent balance of efficiency and compute density.",
|
||||||
image: "/images/edgenode.png",
|
image: "/images/edgenode.png",
|
||||||
|
features: [
|
||||||
|
"Efficient 6-core / 12-thread Zen 4 architecture at up to 5.0 GHz boost. (CPU Monkey)",
|
||||||
|
"Low power consumption (35 W class HS chip) conducive to continuous operation. (LaptopMedia)",
|
||||||
|
"Compact size → easier placement, less cooling overhead.",
|
||||||
|
"Full node owner flexibility: use it for private workloads, host slices, or a hybrid of both.",
|
||||||
|
],
|
||||||
|
buyUrl:
|
||||||
|
"https://www.gmktec.com/products/amd-ryzen-5-7640hs-mini-pc-nucbox-m6-ultra?spm=..product_ba613c14-a120-431b-af10-c5c5ca575d55.header_1.1&variant=35ad4a9a-3f31-490c-a2d1-ef9ea3773fe9",
|
||||||
|
learnUrl:
|
||||||
|
"https://threefold.info/mycelium_economics/docs/recommended_nodes/edge_node",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const configOptions = [
|
||||||
|
{
|
||||||
|
id: "ainode",
|
||||||
|
name: "EDGE AI Node",
|
||||||
|
description: "Optimized for local inference + AI acceleration",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "edgenode",
|
||||||
|
name: "EDGE Compute Node",
|
||||||
|
description: "Optimized for general-purpose compute + agent workloads",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/* ------------------------------------------ */
|
||||||
|
/* MAIN COMPONENT */
|
||||||
|
/* ------------------------------------------ */
|
||||||
|
|
||||||
export function NodeProducts() {
|
export function NodeProducts() {
|
||||||
|
const [selectedNode, setSelectedNode] = useState(nodes.ainode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-[#121212] w-full max-w-8xl mx-auto">
|
<section className="bg-[#121212] w-full max-w-8xl mx-auto">
|
||||||
|
|
||||||
{/* Top spacing + border */}
|
{/* Top spacing + border */}
|
||||||
<div className="max-w-7xl mx-auto py-6 border border-t-0 border-b-0 border-gray-800" />
|
<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="w-full border-t border-l border-r border-gray-800" />
|
||||||
|
|
||||||
{/* MAIN CONTAINER */}
|
{/* MAIN SECTION */}
|
||||||
<div className="relative px-6 lg:px-12 py-16 bg-[#111111] border border-t-0 border-b-0 border-gray-800 max-w-7xl mx-auto">
|
<div className="relative px-6 lg:px-12 py-16 bg-[#111111] border border-t-0 border-b-0 border-gray-800 max-w-7xl mx-auto">
|
||||||
|
|
||||||
{/* Header */}
|
{/* --------------------------------------- */}
|
||||||
<motion.div
|
{/* SECTION TITLE + INTRO PARAGRAPH */}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
{/* --------------------------------------- */}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
<div className="text-center max-w-3xl mx-auto mb-16">
|
||||||
viewport={{ once: true }}
|
<Eyebrow color="accent">Hardware</Eyebrow>
|
||||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
<SectionHeader className="text-3xl font-medium" color="light">
|
||||||
className="mx-auto max-w-4xl text-center"
|
Recommended Nodes
|
||||||
>
|
|
||||||
<Eyebrow color="accent">Recommended</Eyebrow>
|
|
||||||
|
|
||||||
<SectionHeader
|
|
||||||
className="text-3xl font-medium tracking-tight"
|
|
||||||
color="light"
|
|
||||||
>
|
|
||||||
Recommended Nodes to Buy
|
|
||||||
</SectionHeader>
|
</SectionHeader>
|
||||||
|
<P className="mt-4 text-gray-300">
|
||||||
<P className="mt-6" color="light">
|
Below are some of the best-performing and most commonly recommended nodes
|
||||||
The best entry-level and performance-balanced hardware for hosting
|
for hosting agents, Mycelium workloads, and contributing compute to the network.
|
||||||
Mycelium nodes, running agents, and contributing compute to the network.
|
|
||||||
</P>
|
</P>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24 max-w-6xl mx-auto">
|
||||||
|
|
||||||
|
{/* ------------------------------ */}
|
||||||
|
{/* LEFT — TEXT AREA */}
|
||||||
|
{/* ------------------------------ */}
|
||||||
|
<motion.div
|
||||||
|
key={selectedNode.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4 }}
|
||||||
|
className="flex flex-col justify-center"
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl font-semibold text-white">
|
||||||
|
{selectedNode.name}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="mt-1 text-gray-400 text-base">
|
||||||
|
{selectedNode.subtitle}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="mt-6 text-gray-300 text-base leading-relaxed">
|
||||||
|
{selectedNode.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* FEATURES */}
|
||||||
|
<ul className="mt-6 space-y-2">
|
||||||
|
{selectedNode.features.map((f, i) => (
|
||||||
|
<li key={i} className="flex items-start">
|
||||||
|
<CheckIcon className="w-5 h-5 text-green-500 mt-0.5" />
|
||||||
|
<p className="ml-2 text-sm text-gray-300 leading-relaxed">
|
||||||
|
{f}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* CONFIG SELECTOR */}
|
||||||
|
<fieldset className="mt-10">
|
||||||
|
<legend className="text-sm font-medium text-gray-200">
|
||||||
|
Configuration
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
{configOptions.map((opt) => (
|
||||||
|
<label
|
||||||
|
key={opt.id}
|
||||||
|
className={`group relative flex flex-col border rounded-xl p-4 cursor-pointer transition
|
||||||
|
${
|
||||||
|
selectedNode.id === opt.id
|
||||||
|
? "border-cyan-500 bg-white/5"
|
||||||
|
: "border-gray-700 hover:border-gray-500"
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedNode(nodes[opt.id])}
|
||||||
|
>
|
||||||
|
<span className="text-white font-medium">{opt.name}</span>
|
||||||
|
<span className="mt-1 text-sm text-gray-400">{opt.description}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<a className="inline-flex text-sm text-gray-500 hover:text-gray-300 transition">
|
||||||
|
What config should I choose?
|
||||||
|
<QuestionMarkCircleIcon className="ml-1 w-5 h-5 text-gray-500" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ------------------------ */}
|
||||||
|
{/* BUTTONS AREA */}
|
||||||
|
{/* ------------------------ */}
|
||||||
|
<div className="mt-10 flex flex-col sm:flex-row gap-4">
|
||||||
|
|
||||||
|
{/* Outline Button */}
|
||||||
|
<a
|
||||||
|
href={selectedNode.learnUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex-1 sm:flex-none text-center border border-gray-700 hover:border-gray-500
|
||||||
|
text-gray-300 hover:text-white px-8 py-3 rounded-lg transition"
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Solid Cyan Button */}
|
||||||
|
<a
|
||||||
|
href={selectedNode.buyUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex-1 sm:flex-none text-center bg-cyan-600 hover:bg-cyan-700
|
||||||
|
text-white px-8 py-3 rounded-lg font-medium transition"
|
||||||
|
>
|
||||||
|
Purchase Node
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Guarantee */}
|
||||||
|
<div className="mt-6 flex items-center text-gray-400 text-sm">
|
||||||
|
<ShieldCheckIcon className="w-6 h-6 text-gray-500 mr-2" />
|
||||||
|
Lifetime Guarantee
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Node cards */}
|
{/* ------------------------------ */}
|
||||||
<div className="mx-auto mt-16 grid grid-cols-1 lg:grid-cols-2 gap-12 max-w-6xl">
|
{/* RIGHT — IMAGE */}
|
||||||
|
{/* ------------------------------ */}
|
||||||
{nodes.map((node, i) => (
|
|
||||||
<motion.div
|
<motion.div
|
||||||
key={node.name}
|
key={selectedNode.image}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, scale: 0.92 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
viewport={{ once: true }}
|
transition={{ duration: 0.35 }}
|
||||||
transition={{ duration: 0.45, delay: 0.15 * i }}
|
className="flex justify-center"
|
||||||
className="rounded-2xl border border-gray-800 bg-white/5 p-8 backdrop-blur-sm
|
|
||||||
hover:border-cyan-400 hover:shadow-xl hover:shadow-cyan-500/20
|
|
||||||
transition-all duration-300"
|
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={node.image}
|
src={selectedNode.image}
|
||||||
alt={node.name}
|
alt={selectedNode.name}
|
||||||
className="w-full rounded-xl border border-gray-700 object-cover"
|
className="w-full max-w-md rounded-2xl border border-gray-800 object-cover"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CT as="h3" className="mt-6 font-semibold text-white">
|
|
||||||
{node.name}
|
|
||||||
</CT>
|
|
||||||
|
|
||||||
<CP className="mt-2 text-sm text-gray-300">
|
|
||||||
{node.subtitle}
|
|
||||||
</CP>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="mt-6 w-full rounded-lg bg-indigo-600 hover:bg-indigo-700
|
|
||||||
text-white text-sm font-medium py-3 transition"
|
|
||||||
>
|
|
||||||
View Specs
|
|
||||||
</button>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user