feat: replace placeholder image with ProxyForwarding component and add ContentDistribution section
This commit is contained in:
182
src/components/ContentDistribution.tsx
Normal file
182
src/components/ContentDistribution.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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">
|
||||
|
Reference in New Issue
Block a user