feat: refactor pods page with simplified content and improved animations

- Added agent1.png image asset
- Refactored InfiniteMovingCards component with cleaner animation logic and improved duplication handling
- Changed default speed from "fast" to "slow" and simplified animation setup
- Updated AgentBento title from "Augmented Intelligence Fabric" to "Intelligence Fabric"
- Increased Homepod vertical padding on large screens (lg:py-16 to lg:py-24)
- Removed "Feature" label from PodsFeatures use
This commit is contained in:
2025-11-17 14:39:23 +01:00
parent 6ff539b3fc
commit 57c39a8b2b
9 changed files with 154 additions and 165 deletions

BIN
public/images/agent1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 KiB

View File

@@ -1,12 +1,12 @@
"use client"; "use client";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import React, { useCallback, useEffect, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
export const InfiniteMovingCards = ({ export const InfiniteMovingCards = ({
items, items,
direction = "left", direction = "left",
speed = "fast", speed = "slow",
pauseOnHover = true, pauseOnHover = true,
className, className,
}: { }: {
@@ -15,43 +15,39 @@ export const InfiniteMovingCards = ({
speed?: "fast" | "normal" | "slow"; speed?: "fast" | "normal" | "slow";
pauseOnHover?: boolean; pauseOnHover?: boolean;
className?: string; className?: string;
}): JSX.Element => { }) => {
const containerRef = React.useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const scrollerRef = React.useRef<HTMLUListElement>(null); const scrollerRef = useRef<HTMLUListElement>(null);
const [start, setStart] = useState(false); const [isReady, setIsReady] = useState(false);
const getSpeed = useCallback(() => {
if (containerRef.current) {
if (speed === "fast") {
containerRef.current.style.setProperty("--animation-duration", "20s");
} else if (speed === "normal") {
containerRef.current.style.setProperty("--animation-duration", "40s");
} else {
containerRef.current.style.setProperty("--animation-duration", "80s");
}
}
}, [speed]);
const addAnimation = useCallback(() => {
if (containerRef.current && scrollerRef.current) {
const scrollerContent = Array.from(scrollerRef.current.children);
scrollerContent.forEach((item) => {
const duplicatedItem = item.cloneNode(true);
if (scrollerRef.current) {
scrollerRef.current.appendChild(duplicatedItem);
}
});
getSpeed();
setStart(true);
}
}, [getSpeed]);
useEffect(() => { useEffect(() => {
addAnimation(); if (!scrollerRef.current) return;
}, [addAnimation]);
const children = Array.from(scrollerRef.current.children);
// duplicate each item ONCE
children.forEach((item) => {
const clone = item.cloneNode(true);
scrollerRef.current!.appendChild(clone);
});
// set speed variable
const duration =
speed === "fast" ? "20s" : speed === "normal" ? "40s" : "80s";
containerRef.current?.style.setProperty(
"--animation-duration",
duration
);
// set direction variable
containerRef.current?.style.setProperty(
"--animation-direction",
direction === "left" ? "forwards" : "reverse"
);
setIsReady(true);
}, [direction, speed]);
return ( return (
<div <div
@@ -61,16 +57,13 @@ export const InfiniteMovingCards = ({
<ul <ul
ref={scrollerRef} ref={scrollerRef}
className={cn( className={cn(
"flex min-w-full shrink-0 gap-16 py-4 w-max flex-nowrap", "flex w-max shrink-0 gap-16 py-4",
start && "animate-scroll", isReady && "animate-infinite-scroll",
pauseOnHover && "hover:[animation-play-state:paused]", pauseOnHover && "hover:[animation-play-state:paused]"
)} )}
style={{
"--animation-direction": direction === "left" ? "forwards" : "reverse",
} as React.CSSProperties}
> >
{items.map((item, idx) => ( {items.map((item, i) => (
<li className="relative flex-shrink-0" key={idx}> <li key={i} className="flex-shrink-0">
{item} {item}
</li> </li>
))} ))}

View File

@@ -23,7 +23,7 @@ const bentos: {
{ {
id: "core", id: "core",
eyebrow: "ARCHITECTURE", eyebrow: "ARCHITECTURE",
title: "Augmented Intelligence Fabric", title: "Intelligence Fabric",
description: description:
"The sovereign substrate for autonomous AI. Stateless, geo-aware, end-to-end encrypted—and verifiable from intent to execution.", "The sovereign substrate for autonomous AI. Stateless, geo-aware, end-to-end encrypted—and verifiable from intent to execution.",
animation: null, animation: null,

View File

@@ -13,7 +13,7 @@ export default function Homepod() {
style={{ backgroundImage: "url('/images/computehero11.webp')", backgroundSize: "contain" }} style={{ backgroundImage: "url('/images/computehero11.webp')", backgroundSize: "contain" }}
> >
{/* Inner padding */} {/* Inner padding */}
<div className="px-6 py-16 lg:py-16"> <div className="px-6 py-16 lg:py-24">
<div className="max-w-2xl lg:pl-6"> <div className="max-w-2xl lg:pl-6">
<Eyebrow> <Eyebrow>
MYCELIUM PODS MYCELIUM PODS

View File

@@ -106,9 +106,6 @@ export function PodsFeatures() {
<h3 className="font-semibold text-gray-900"> <h3 className="font-semibold text-gray-900">
{useCase.title} {useCase.title}
</h3> </h3>
<Small className="uppercase tracking-[0.25em] text-cyan-500">
Feature
</Small>
</div> </div>
{/* Short description */} {/* Short description */}

View File

@@ -1,57 +1,70 @@
"use client"; "use client";
import { Eyebrow, H3, P } from "@/components/Texts"; import { Eyebrow, H4, H5 } from "@/components/Texts";
import PodsFlow from "./animations/PodsFlow"; import PodsFlow from "./animations/PodsFlow";
import { useEffect, useState } from "react";
import { IconClockHour5 } from "@tabler/icons-react";
const phrases = [
"everything runs directly from your Pod.",
"Messages travel from Pod to Pod, never through a server.",
"Calls are hosted on your Pod, not in a data center.",
"Files stay encrypted, available, and always yours.",
];
export function PodsHow() { export function PodsHow() {
const [index, setIndex] = useState(0);
const [fade, setFade] = useState(true);
useEffect(() => {
const interval = setInterval(() => {
setFade(false);
setTimeout(() => {
setIndex((prev) => (prev + 1) % phrases.length);
setFade(true);
}, 300);
}, 3000);
return () => clearInterval(interval);
}, []);
return ( return (
<section className="relative w-full bg-[#121212] overflow-hidden"> <section className="relative w-full bg-[#121212] overflow-hidden">
{/* ✅ Top horizontal line with spacing */}
<div className="max-w-7xl bg-[#121212] mx-auto py-6 border border-t-0 border-b-0 border-gray-800"></div> <div className="max-w-7xl bg-[#121212] 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="w-full border-t border-l border-r border-gray-800" />
<div className="max-w-7xl mx-auto px-6 lg:px-8 py-12 border border-t-0 border-b-0 border-gray-800 bg-[#111111] overflow-hidden"> <div className="max-w-7xl mx-auto px-6 lg:px-8 py-12 border border-t-0 border-b-0 border-gray-800 bg-[#111111] overflow-hidden">
{/* Two-column layout */} {/* Two-column layout */}
<div className="flex flex-col lg:flex-row-reverse gap-8"> <div className="flex flex-col lg:flex-row-reverse gap-8">
{/* Right side animation */} {/* Right: Animation */}
<div className="w-full lg:w-4/9"> <div className="w-full lg:w-4/9">
<PodsFlow /> <PodsFlow />
</div> </div>
{/* Left side content */} {/* Left: JUST the H3 with auto-changing sentence */}
<div className="w-full lg:w-5/9 text-white"> <div className="w-full lg:w-5/9 text-white flex items-center">
<Eyebrow color="accent" className="">
How it works <div>
</Eyebrow> <Eyebrow color="accent">How it works</Eyebrow>
<H3 color="white" className="mt-6"> <H4 color="white">A Pod in Action</H4>
A Pod in Action <H5 color="white" className="mt-4">
</H3> When you use Mycelium,&nbsp;
<P className="max-w-4xl text-gray-400 mt-6"> <span
When you use Mycelium, everything runs directly from your Pod. className={`inline-block transition-opacity duration-300 ${
</P> fade ? "opacity-100" : "opacity-0"
<ul className="max-w-4xl text-gray-400 mt-6 space-y-2 ml-6"> }`}
<li className="flex items-start gap-2"> >
<span className="mt-1 inline-block size-2 rounded-full bg-cyan-400" /> {phrases[index]}
<span>When you message someone, it goes Pod to Pod, not through a central server.</span> </span>
</li> </H5>
<li className="flex items-start gap-3"> </div>
<span className="mt-1 inline-block size-2 rounded-full bg-cyan-400" />
<span>When you host a call, it runs on your Pod. No third-party data centers involved.</span>
</li>
<li className="flex items-start gap-3">
<span className="mt-1 inline-block size-2 rounded-full bg-cyan-400" />
<span>When you save a file, it stays on your Pod, encrypted and always available.</span>
</li>
</ul>
<P className="max-w-3xl text-gray-400 mt-4">
No one else can read it, rent it, or switch it off.
You dont log in to the internet, you are part of it.
</P>
</div> </div>
</div> </div>
</div> </div>
<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-b border-gray-800" /> <div className="w-full border-b border-gray-800" />
</section> </section>

View File

@@ -7,45 +7,27 @@ import {
GlobeAltIcon, GlobeAltIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import { Eyebrow, H3 } from "@/components/Texts"; import { Eyebrow, H3, P } from "@/components/Texts";
const podCards = [ const podCards = [
{ {
id: "intro", id: "tools",
eyebrow: "Capabilities", title: "Runs communication, storage, and collaboration tools",
title: "What is a Pod?",
description: null,
icon: null,
custom: true,
noBorder: true,
colSpan: "lg:col-span-4",
},
{
id: "home",
title: "Your private digital home on the decentralized internet",
description:
"Your Pod is a private digital home where apps, data, and identity live independently of Big Tech and central servers.",
icon: ServerIcon, icon: ServerIcon,
}, },
{ {
id: "control", id: "p2p",
title: "An always-on space you fully control", title: "Operates peer to peer on the network",
description:
"A dedicated, always-on environment you fully command. Your own sovereign slice of the network that never goes offline.",
icon: ShieldCheckIcon, icon: ShieldCheckIcon,
}, },
{ {
id: "tools", id: "encrypted",
title: "Runs communication, storage, and collaboration tools", title: "Uses encrypted identities and storage",
description:
"Runs your communication, storage, and collaboration tools in a secure local environment without reliance on outside platforms.",
icon: BoltIcon, icon: BoltIcon,
}, },
{ {
id: "networking", id: "fabric",
title: "Fully encrypted, federated peer-to-peer network", title: "Connects directly to other Pods through the network fabric",
description:
"Encrypted, federated peer-to-peer networking that links your Pod directly with trusted devices without intermediaries.",
icon: GlobeAltIcon, icon: GlobeAltIcon,
}, },
]; ];
@@ -60,38 +42,31 @@ export function PodsWhat() {
{/* Content container */} {/* Content container */}
<div className="mx-auto bg-[#111111] max-w-7xl px-6 lg:px-10 border border-t-0 border-b-0 border-gray-800"> <div className="mx-auto bg-[#111111] max-w-7xl px-6 lg:px-10 border border-t-0 border-b-0 border-gray-800">
{/* Intro heading */}
<div className="pt-12 pb-10 max-w-3xl">
<Eyebrow>What</Eyebrow>
<H3 className="mt-2 text-white">What is a Pod?</H3>
<P className="mt-4 text-gray-300">
A Pod is an always-on digital environment that gives you a secure place to run your
applications and data.
</P>
</div>
{/* 4-column grid */} {/* 4-column grid */}
<div className="grid grid-cols-1 gap-12 pt-12 lg:grid-cols-4 pb-20"> <div className="grid grid-cols-1 gap-12 lg:grid-cols-4 pb-20">
{podCards.map((card) => { {podCards.map((card) => {
const Icon = card.icon; const Icon = card.icon;
return ( return (
<div <div
key={card.id} key={card.id}
className={`${card.colSpan || ""} flex flex-col ${ className="flex flex-col transition-transform duration-300 hover:scale-[1.02]"
card.custom ? "" : "transition-transform duration-300 hover:scale-[1.02]"
}`}
> >
{/* Custom Intro Card */} {/* TITLE WITH ICON */}
{card.custom ? ( <dt className="flex items-center gap-x-3 text-base font-semibold text-white">
<> {Icon && <Icon className="h-6 w-6 text-cyan-500" aria-hidden="true" />}
<Eyebrow>{card.eyebrow}</Eyebrow> {card.title}
<H3 className="mt-2 text-white">{card.title}</H3> </dt>
</>
) : (
<>
{/* TITLE WITH ICON (matching the TL example) */}
<dt className="flex items-center gap-x-3 text-base font-semibold text-white">
{Icon && <Icon className="h-6 w-6 text-cyan-500" aria-hidden="true" />}
{card.title}
</dt>
{/* DESCRIPTION */}
<dd className="mt-4 text-base text-gray-300">
{card.description}
</dd>
</>
)}
</div> </div>
); );
})} })}

View File

@@ -9,22 +9,23 @@ type Props = {
gridStroke?: string; gridStroke?: string;
}; };
// ▸ NEW: Cropped dimensions
const W = 760; const W = 760;
const H = 420; const H = 300; // was 420
export default function PodsFlow({ export default function PodsFlow({
className, className,
accent = "#00b8db", accent = "#00b8db",
gridStroke = "#2b2a2a", gridStroke = "#3a3a3a", // lighter grid stroke
}: Props) { }: Props) {
const pods = [ const pods = [
{ x: 100, y: 180, label: "Pod 1" }, { x: 100, y: 120, label: "Pod 1" }, // moved slightly up for new height
{ x: 260, y: 180, label: "Pod 2" }, { x: 260, y: 120, label: "Pod 2" },
{ x: 420, y: 180, label: "Pod 3" }, { x: 420, y: 120, label: "Pod 3" },
{ x: 580, y: 180, label: "Pod 4" }, { x: 580, y: 120, label: "Pod 4" },
]; ];
// Pulse path // Pulse path (unchanged)
const path = ` const path = `
M ${pods[0].x + 80} ${pods[0].y + 40} M ${pods[0].x + 80} ${pods[0].y + 40}
L ${pods[1].x - 10} ${pods[1].y + 40} L ${pods[1].x - 10} ${pods[1].y + 40}
@@ -34,7 +35,7 @@ export default function PodsFlow({
L ${pods[3].x - 10} ${pods[3].y + 40} L ${pods[3].x - 10} ${pods[3].y + 40}
`; `;
// Arrow segments // Line segments
const arrows = [ const arrows = [
{ {
d: `M ${pods[0].x + 80} ${pods[0].y + 40} L ${pods[1].x - 6} ${pods[1].y + 40}`, d: `M ${pods[0].x + 80} ${pods[0].y + 40} L ${pods[1].x - 6} ${pods[1].y + 40}`,
@@ -54,15 +55,13 @@ export default function PodsFlow({
<div <div
className={clsx("relative overflow-hidden", className)} className={clsx("relative overflow-hidden", className)}
aria-hidden="true" aria-hidden="true"
role="img"
aria-label="Pod-to-Pod signal transfer animation"
style={{ background: "transparent" }} style={{ background: "transparent" }}
> >
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full"> <svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full">
{/* GRID BG */} {/* GRID BACKGROUND */}
<defs> <defs>
<pattern id="pods-grid" width="28" height="28" patternUnits="userSpaceOnUse"> <pattern id="pods-grid" width="26" height="26" patternUnits="userSpaceOnUse">
<path d="M 28 0 L 0 0 0 28" fill="none" stroke={gridStroke} strokeWidth="1" opacity="0.6" /> <path d="M 26 0 L 0 0 0 26" fill="none" stroke={gridStroke} strokeWidth="1" opacity="0.9" />
</pattern> </pattern>
<filter id="pods-glow"> <filter id="pods-glow">
@@ -85,8 +84,8 @@ export default function PodsFlow({
width={80} width={80}
height={80} height={80}
rx={14} rx={14}
fill="#0d0d0d" fill="#0f0f0f"
stroke="#1a1a1a" stroke="#fff" // brighter stroke
strokeWidth={2} strokeWidth={2}
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 0.9 }} animate={{ opacity: 0.9 }}
@@ -94,7 +93,7 @@ export default function PodsFlow({
/> />
))} ))}
{/* POD LABELS */} {/* POD TEXT */}
{pods.map((p, i) => ( {pods.map((p, i) => (
<motion.text <motion.text
key={i} key={i}
@@ -103,26 +102,26 @@ export default function PodsFlow({
textAnchor="middle" textAnchor="middle"
fontSize="14" fontSize="14"
fontFamily="Inter, sans-serif" fontFamily="Inter, sans-serif"
fill="#9ca3af" fill="#fff" // lighter text
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 0.9 }} animate={{ opacity: 0.95 }}
transition={{ delay: 0.1 + i * 0.1, duration: 0.6 }} transition={{ delay: 0.1 + i * 0.1 }}
> >
{p.label} {p.label}
</motion.text> </motion.text>
))} ))}
{/* GREY LINES */} {/* GREY LINES (lighter) */}
{arrows.map((a, i) => ( {arrows.map((a, i) => (
<motion.path <motion.path
key={`grey-${i}`} key={`grey-${i}`}
d={a.d} d={a.d}
stroke="#333" stroke="#444" // lighter grey
strokeWidth={4} strokeWidth={4}
strokeLinecap="round" strokeLinecap="round"
fill="none" fill="none"
initial={{ pathLength: 0, opacity: 0 }} initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 0.8 }} animate={{ pathLength: 1, opacity: 0.9 }}
transition={{ delay: 0.2 * i, duration: 0.7 }} transition={{ delay: 0.2 * i, duration: 0.7 }}
/> />
))} ))}
@@ -143,7 +142,7 @@ export default function PodsFlow({
/> />
))} ))}
{/* NEW: CYAN ENDPOINT PULSES */} {/* ENDPOINT PULSES */}
{arrows.map((a, i) => ( {arrows.map((a, i) => (
<motion.circle <motion.circle
key={`endpoint-${i}`} key={`endpoint-${i}`}
@@ -151,23 +150,21 @@ export default function PodsFlow({
cy={a.end.y} cy={a.end.y}
r={10} r={10}
fill={accent} fill={accent}
opacity={0.12} opacity={0.16} // slightly stronger
filter="url(#pods-glow)" filter="url(#pods-glow)"
initial={{ scale: 0.8, opacity: 0 }}
animate={{ animate={{
scale: [1, 1.2, 1], scale: [1, 1.2, 1],
opacity: [0.05, 0.25, 0.05], opacity: [0.08, 0.28, 0.08],
}} }}
transition={{ transition={{
duration: 1.5, duration: 1.4,
delay: i * 0.2, delay: i * 0.2,
repeat: Infinity, repeat: Infinity,
repeatType: "mirror",
}} }}
/> />
))} ))}
{/* MAIN MOVING CYAN PULSE */} {/* PULSE DOT */}
<motion.circle <motion.circle
r={8} r={8}
fill={accent} fill={accent}
@@ -178,10 +175,10 @@ export default function PodsFlow({
initial={{ offsetDistance: "0%", opacity: 0.4 }} initial={{ offsetDistance: "0%", opacity: 0.4 }}
animate={{ animate={{
offsetDistance: ["0%", "100%"], offsetDistance: ["0%", "100%"],
opacity: [0.4, 1, 0.4], opacity: [0.5, 1, 0.5],
}} }}
transition={{ transition={{
duration: 2.6, duration: 2.4,
repeat: Infinity, repeat: Infinity,
ease: "linear", ease: "linear",
}} }}

View File

@@ -228,3 +228,17 @@
filter: drop-shadow(0 0 6px #90f6ff); filter: drop-shadow(0 0 6px #90f6ff);
transition: opacity 1s ease-out; transition: opacity 1s ease-out;
} }
@keyframes infinite-scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.animate-infinite-scroll {
animation: infinite-scroll var(--animation-duration, 40s) linear infinite;
animation-direction: var(--animation-direction, forwards);
}