feat: replace placeholder image with ProxyForwarding component and add ContentDistribution section

This commit is contained in:
2025-10-13 17:53:35 +02:00
parent ce162cd169
commit b320f3e506
2 changed files with 185 additions and 6 deletions

View File

@@ -0,0 +1,182 @@
'use client';
import * as React from 'react';
import { motion, useReducedMotion } from 'framer-motion';
type Props = {
className?: string; // e.g. "w-full h-80"
bg?: string; // default white
};
/** Palette */
const ACCENT = '#00b8db';
const STROKE = '#111827';
const GRAY = '#9CA3AF';
const GRAY_LT = '#E5E7EB';
/* ---------- small generic icons (no brands) ---------- */
const IconSquare = () => (
<rect x={-14} y={-14} width={28} height={28} rx={6} fill={ACCENT} stroke={STROKE} strokeWidth={3} />
);
const IconTriangle = () => (
<path d="M 0 -15 L 14 12 L -14 12 Z" fill="#fff" stroke={STROKE} strokeWidth={3} />
);
const IconHex = () => (
<path
d="M 0 -15 L 13 -7 L 13 7 L 0 15 L -13 7 L -13 -7 Z"
fill="#fff"
stroke={STROKE}
strokeWidth={3}
/>
);
const IconBolt = () => (
<path d="M -5 -14 L 4 -2 L -1 -2 L 5 14 L -6 1 L -1 1 Z" fill={ACCENT} stroke={STROKE} strokeWidth={3} />
);
const IconPlay = () => (
<circle r={15} fill="#fff" stroke={STROKE} strokeWidth={3} />
);
const IconDB = () => (
<>
<ellipse cx={0} cy={-10} rx={16} ry={8} fill="#fff" stroke={STROKE} strokeWidth={3} />
<rect x={-16} y={-10} width={32} height={20} fill="#fff" stroke={STROKE} strokeWidth={3} />
<ellipse cx={0} cy={10} rx={16} ry={8} fill="#fff" stroke={STROKE} strokeWidth={3} />
</>
);
/* icon inside white circular badge */
function Badge({ children }: { children: React.ReactNode }) {
return (
<>
<circle r={26} fill="#fff" stroke={GRAY_LT} strokeWidth={3} />
<g>{children}</g>
<filter id="shadow" x="-200%" y="-200%" width="400%" height="400%">
<feDropShadow dx="0" dy="2" stdDeviation="2" floodColor={GRAY} floodOpacity="0.25" />
</filter>
</>
);
}
/* ---------- central cloud ---------- */
function Cloud({ pulse = true }: { pulse?: boolean }) {
const prefersReduced = useReducedMotion();
return (
<g>
<g fill={STROKE}>
<circle cx={-18} cy={0} r={14} />
<circle cx={0} cy={-10} r={18} />
<circle cx={18} cy={0} r={16} />
<rect x={-30} y={0} width={54} height={16} rx={8} />
</g>
{/* subtle accent aura */}
<motion.circle
r={36}
fill="none"
stroke={ACCENT}
strokeWidth={4}
initial={{ opacity: 0.15, scale: 0.9 }}
animate={pulse && !prefersReduced ? { opacity: [0.15, 0.35, 0.15], scale: [0.9, 1.05, 0.9] } : {}}
transition={{ duration: 1.8, repeat: Infinity }}
/>
</g>
);
}
/* a small packet line from center to a node */
function Beam({
x2,
y2,
delay = 0,
}: {
x2: number;
y2: number;
delay?: number;
}) {
const prefersReduced = useReducedMotion();
return (
<motion.line
x1={0}
y1={0}
x2={x2}
y2={y2}
stroke={ACCENT}
strokeWidth={4}
strokeLinecap="round"
initial={{ pathLength: 0, opacity: 0.0 }}
animate={{ pathLength: 1, opacity: 0.9 }}
transition={{
duration: prefersReduced ? 0.01 : 0.9,
delay,
repeat: prefersReduced ? 0 : Infinity,
repeatDelay: 1.2,
repeatType: 'reverse',
ease: [0.22, 1, 0.36, 1],
}}
/>
);
}
export default function ContentDistribution({ className, bg = '#ffffff' }: Props) {
const W = 900;
const H = 560;
// ring radii
const rings = [110, 190, 270];
// positions (angle degrees) for badges on rings
const layout = [
{ r: rings[1], a: -20, icon: <IconSquare /> },
{ r: rings[2], a: 20, icon: <IconTriangle /> },
{ r: rings[0], a: 155, icon: <IconHex /> },
{ r: rings[2], a: -145, icon: <IconBolt /> },
{ r: rings[1], a: 210, icon: <IconDB /> },
{ r: rings[0], a: 60, icon: <IconPlay /> },
];
const prefersReduced = useReducedMotion();
return (
<div className={className} aria-hidden="true" role="img" style={{ background: bg }}>
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%">
{/* subtle radial background + rings */}
<defs>
<radialGradient id="fade" cx="50%" cy="50%" r="60%">
<stop offset="0%" stopColor="#ffffff" />
<stop offset="100%" stopColor="#ffffff" />
</radialGradient>
</defs>
<rect width={W} height={H} fill="url(#fade)" />
<g transform={`translate(${W / 2}, ${H / 2})`}>
{rings.map((r, i) => (
<circle key={i} r={r} fill="none" stroke={GRAY_LT} strokeWidth={2} />
))}
{/* central cloud */}
<Cloud />
{/* rotating layer with badges */}
<motion.g
initial={{ rotate: 0 }}
animate={{ rotate: prefersReduced ? 0 : 360 }}
transition={{ duration: 40, ease: 'linear', repeat: prefersReduced ? 0 : Infinity }}
>
{layout.map((n, i) => {
const rad = (n.a * Math.PI) / 180;
const x = n.r * Math.cos(rad);
const y = n.r * Math.sin(rad);
return (
<g key={i} transform={`translate(${x}, ${y})`} filter="url(#shadow)">
<circle r={34} fill="#fff" stroke={GRAY_LT} strokeWidth={3} />
<g transform="scale(1)">
{n.icon}
</g>
{/* beam from center to this node (animated) */}
<Beam x2={-x} y2={-y} delay={i * 0.15} />
</g>
);
})}
</motion.g>
</g>
</svg>
</div>
);
}

View File

@@ -2,6 +2,7 @@ import Pathfinding from '@/components/Pathfinding'
import MessageBus from '@/components/MessageBus'
import ProxyDetection from '@/components/ProxyDetection'
import ProxyForwarding from '@/components/ProxyForwarding'
import ContentDistribution from '@/components/ContentDistribution'
export function Features() {
return (
@@ -75,11 +76,7 @@ export function Features() {
<div className="relative lg:col-span-2">
<div className="absolute inset-0 rounded-lg bg-white" />
<div className="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)]">
<img
alt=""
src="https://tailwindcss.com/plus-assets/img/component-images/bento-01-integrations.png"
className="h-80 object-cover"
/>
<ProxyForwarding className="h-80" />
<div className="p-10 pt-4">
<h3 className="text-sm/4 font-semibold text-cyan-500">Connectivity</h3>
<p className="mt-2 text-lg font-medium tracking-tight text-gray-950">
@@ -96,7 +93,7 @@ export function Features() {
<div className="relative lg:col-span-2">
<div className="absolute inset-0 rounded-lg bg-white max-lg:rounded-b-4xl lg:rounded-br-4xl" />
<div className="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] max-lg:rounded-b-[calc(2rem+1px)] lg:rounded-br-[calc(2rem+1px)]">
<ProxyForwarding />
<ContentDistribution className="h-80" />
<div className="p-10 pt-4">
<h3 className="text-sm/4 font-semibold text-cyan-500">Delivery</h3>
<p className="mt-2 text-lg font-medium tracking-tight text-gray-950">