feat: replace static images with MessageBus and ProxyDetection components in Features section
This commit is contained in:
		@@ -1,4 +1,7 @@
 | 
			
		||||
import Pathfinding from '@/components/Pathfinding'
 | 
			
		||||
import MessageBus from '@/components/MessageBus'
 | 
			
		||||
import ProxyDetection from '@/components/ProxyDetection'
 | 
			
		||||
import ProxyForwarding from '@/components/ProxyForwarding'
 | 
			
		||||
 | 
			
		||||
export function Features() {
 | 
			
		||||
  return (
 | 
			
		||||
@@ -38,11 +41,7 @@ export function Features() {
 | 
			
		||||
          <div className="relative lg:col-span-3">
 | 
			
		||||
            <div className="absolute inset-0 rounded-lg bg-white lg:rounded-tr-4xl" />
 | 
			
		||||
            <div className="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] lg:rounded-tr-[calc(2rem+1px)]">
 | 
			
		||||
              <img
 | 
			
		||||
                alt=""
 | 
			
		||||
                src="https://tailwindcss.com/plus-assets/img/component-images/bento-01-releases.png"
 | 
			
		||||
                className="h-80 object-cover object-left lg:object-right"
 | 
			
		||||
              />
 | 
			
		||||
              <MessageBus />
 | 
			
		||||
              <div className="p-10 pt-4">
 | 
			
		||||
                <h3 className="text-sm/4 font-semibold text-cyan-500">Communication</h3>
 | 
			
		||||
                <p className="mt-2 text-lg font-medium tracking-tight text-gray-950">
 | 
			
		||||
@@ -59,11 +58,7 @@ export function Features() {
 | 
			
		||||
          <div className="relative lg:col-span-2">
 | 
			
		||||
            <div className="absolute inset-0 rounded-lg bg-white lg:rounded-bl-4xl" />
 | 
			
		||||
            <div className="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] lg:rounded-bl-[calc(2rem+1px)]">
 | 
			
		||||
              <img
 | 
			
		||||
                alt=""
 | 
			
		||||
                src="https://tailwindcss.com/plus-assets/img/component-images/bento-01-speed.png"
 | 
			
		||||
                className="h-80 object-cover object-left"
 | 
			
		||||
              />
 | 
			
		||||
              <ProxyDetection className="h-80" />
 | 
			
		||||
              <div className="p-10 pt-4">
 | 
			
		||||
                <h3 className="text-sm/4 font-semibold text-cyan-500">Discovery</h3>
 | 
			
		||||
                <p className="mt-2 text-lg font-medium tracking-tight text-gray-950">
 | 
			
		||||
@@ -101,11 +96,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)]">
 | 
			
		||||
              <img
 | 
			
		||||
                alt=""
 | 
			
		||||
                src="https://tailwindcss.com/plus-assets/img/component-images/bento-01-network.png"
 | 
			
		||||
                className="h-80 object-cover"
 | 
			
		||||
              />
 | 
			
		||||
              <ProxyForwarding />
 | 
			
		||||
              <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">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										150
									
								
								src/components/MessageBus.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								src/components/MessageBus.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,150 @@
 | 
			
		||||
'use client';
 | 
			
		||||
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import { motion, useReducedMotion } from 'framer-motion';
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  className?: string;      // e.g. "w-full h-72"
 | 
			
		||||
  bg?: string;             // default white
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Palette (gray/black + accent only) */
 | 
			
		||||
const ACCENT = '#00b8db';
 | 
			
		||||
const STROKE = '#111827';   // black-ish
 | 
			
		||||
const GRAY   = '#9CA3AF';
 | 
			
		||||
const GRAY_LT = '#E5E7EB';
 | 
			
		||||
 | 
			
		||||
function Envelope({
 | 
			
		||||
  x, y, w = 88, h = 56, fill = GRAY_LT, accent = false, delay = 0, duration = 1.6,
 | 
			
		||||
  path = 'none', // 'left1' | 'left2' | 'rightTop' | 'rightBottom' | 'none'
 | 
			
		||||
  reverse = false,
 | 
			
		||||
}: {
 | 
			
		||||
  x: number; y: number; w?: number; h?: number; fill?: string; accent?: boolean;
 | 
			
		||||
  delay?: number; duration?: number; path?: 'left1'|'left2'|'rightTop'|'rightBottom'|'none'; reverse?: boolean;
 | 
			
		||||
}) {
 | 
			
		||||
  const prefersReduced = useReducedMotion();
 | 
			
		||||
 | 
			
		||||
  // simple keyframe paths (straight line segments)
 | 
			
		||||
  const paths: Record<string, { x: number[]; y: number[] }> = {
 | 
			
		||||
    left1: { x: [x, 380], y: [y, 220] },
 | 
			
		||||
    left2: { x: [x, 380], y: [y, 220] },
 | 
			
		||||
    rightTop: { x: [380, 720], y: [220, 150] },
 | 
			
		||||
    rightBottom: { x: [380, 720], y: [220, 290] },
 | 
			
		||||
    none: { x: [x], y: [y] },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const k = paths[path];
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <motion.g
 | 
			
		||||
      initial={{ opacity: 0, scale: 0.98 }}
 | 
			
		||||
      animate={{
 | 
			
		||||
        opacity: 1,
 | 
			
		||||
        scale: 1,
 | 
			
		||||
        x: prefersReduced ? 0 : (reverse ? [...k.x].reverse() : k.x),
 | 
			
		||||
        y: prefersReduced ? 0 : (reverse ? [...k.y].reverse() : k.y),
 | 
			
		||||
      }}
 | 
			
		||||
      transition={{
 | 
			
		||||
        delay,
 | 
			
		||||
        duration: prefersReduced ? 0.01 : duration,
 | 
			
		||||
        ease: [0.22, 1, 0.36, 1],
 | 
			
		||||
        repeat: prefersReduced ? 0 : Infinity,
 | 
			
		||||
        repeatDelay: 0.6,
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {/* envelope body */}
 | 
			
		||||
      <rect x={-w / 2} y={-h / 2} width={w} height={h} rx={8} fill={fill} stroke={STROKE} strokeWidth={3} />
 | 
			
		||||
      {/* flap */}
 | 
			
		||||
      <path
 | 
			
		||||
        d={`M ${-w/2+4} ${-h/2+6} L 0 ${-h/2+26} L ${w/2-4} ${-h/2+6}`}
 | 
			
		||||
        fill="none"
 | 
			
		||||
        stroke={accent ? ACCENT : STROKE}
 | 
			
		||||
        strokeWidth={4}
 | 
			
		||||
        strokeLinecap="round"
 | 
			
		||||
        strokeLinejoin="round"
 | 
			
		||||
      />
 | 
			
		||||
    </motion.g>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function MessageBus({ className, bg = '#ffffff' }: Props) {
 | 
			
		||||
  const W = 900;
 | 
			
		||||
  const H = 460;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={className} aria-hidden="true" role="img" style={{ background: bg }}>
 | 
			
		||||
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%">
 | 
			
		||||
 | 
			
		||||
        {/* subtle grid */}
 | 
			
		||||
        <defs>
 | 
			
		||||
          <pattern id="grid" width="24" height="24" patternUnits="userSpaceOnUse">
 | 
			
		||||
            <path d="M 24 0 L 0 0 0 24" fill="none" stroke={GRAY_LT} strokeWidth="1" />
 | 
			
		||||
          </pattern>
 | 
			
		||||
        </defs>
 | 
			
		||||
        <rect width={W} height={H} fill="url(#grid)" />
 | 
			
		||||
 | 
			
		||||
        {/* producers (left) */}
 | 
			
		||||
        {[{cx:140,cy:120},{cx:140,cy:340}].map((n,i)=>(
 | 
			
		||||
          <g key={i}>
 | 
			
		||||
            <circle cx={n.cx} cy={n.cy} r={44} fill="#fff" stroke={STROKE} strokeWidth={4}/>
 | 
			
		||||
            {/* arrows toward queue */}
 | 
			
		||||
            <motion.path
 | 
			
		||||
              d={`M ${n.cx+48} ${n.cy} L 320 ${n.cy>200?260:180}`}
 | 
			
		||||
              fill="none" stroke={STROKE} strokeWidth={4} strokeLinecap="round"
 | 
			
		||||
              initial={{ pathLength: 0, opacity: 0.3 }}
 | 
			
		||||
              animate={{ pathLength: 1, opacity: 1 }}
 | 
			
		||||
              transition={{ duration: 0.9, delay: 0.1 + i*0.1, ease: [0.22,1,0.36,1] }}
 | 
			
		||||
            />
 | 
			
		||||
          </g>
 | 
			
		||||
        ))}
 | 
			
		||||
 | 
			
		||||
        {/* consumers (right) */}
 | 
			
		||||
        {[{cx:760,cy:120},{cx:760,cy:340}].map((n,i)=>(
 | 
			
		||||
          <g key={i}>
 | 
			
		||||
            <circle cx={n.cx} cy={n.cy} r={44} fill="#fff" stroke={STROKE} strokeWidth={4}/>
 | 
			
		||||
            {/* arrows out from queue */}
 | 
			
		||||
            <motion.path
 | 
			
		||||
              d={`M 560 ${i===0?180:260} L ${n.cx-48} ${n.cy}`}
 | 
			
		||||
              fill="none" stroke={STROKE} strokeWidth={4} strokeLinecap="round"
 | 
			
		||||
              initial={{ pathLength: 0, opacity: 0.3 }}
 | 
			
		||||
              animate={{ pathLength: 1, opacity: 1 }}
 | 
			
		||||
              transition={{ duration: 0.9, delay: 0.2 + i*0.1, ease: [0.22,1,0.36,1] }}
 | 
			
		||||
            />
 | 
			
		||||
          </g>
 | 
			
		||||
        ))}
 | 
			
		||||
 | 
			
		||||
        {/* central queue container */}
 | 
			
		||||
        <rect x={330} y={150} width={240} height={140} rx={24} fill="#fff" stroke={STROKE} strokeWidth={4} />
 | 
			
		||||
        {/* inner slots (visual only) */}
 | 
			
		||||
        {[0,1,2].map(i=>(
 | 
			
		||||
          <rect key={i} x={350 + i*76} y={170} width={64} height={100} rx={12} fill="none" stroke={GRAY} strokeWidth={3}/>
 | 
			
		||||
        ))}
 | 
			
		||||
 | 
			
		||||
        {/* inbound envelopes (from left nodes to queue) */}
 | 
			
		||||
        <Envelope x={200} y={120} accent fill="#fff" path="left1" delay={0.0} duration={2.0}/>
 | 
			
		||||
        <Envelope x={200} y={340} fill={GRAY_LT} path="left2" delay={0.4} duration={2.2}/>
 | 
			
		||||
        <Envelope x={200} y={340} accent fill="#fff" path="left2" delay={0.9} duration={2.0}/>
 | 
			
		||||
 | 
			
		||||
        {/* queue “processing” envelopes (pulse inside slots) */}
 | 
			
		||||
        {[0,1,2].map((i)=>(
 | 
			
		||||
          <motion.g key={i} transform={`translate(${382 + i*76} 220)`}>
 | 
			
		||||
            <motion.rect
 | 
			
		||||
              x={-28} y={-18} width={56} height={36} rx={8}
 | 
			
		||||
              fill={i===2 ? ACCENT : GRAY_LT}
 | 
			
		||||
              stroke={STROKE} strokeWidth={3}
 | 
			
		||||
              initial={{ opacity: 0.6 }}
 | 
			
		||||
              animate={{ opacity: [0.6, 1, 0.6] }}
 | 
			
		||||
              transition={{ duration: 1.8, repeat: Infinity, delay: i*0.2 }}
 | 
			
		||||
            />
 | 
			
		||||
            <path d="M -24 -12 L 0 0 L 24 -12" fill="none" stroke={i===2 ? STROKE : STROKE} strokeWidth={4} strokeLinecap="round" />
 | 
			
		||||
          </motion.g>
 | 
			
		||||
        ))}
 | 
			
		||||
 | 
			
		||||
        {/* outbound envelopes (from queue to right nodes) */}
 | 
			
		||||
        <Envelope x={560} y={180} accent fill="#fff" path="rightTop" delay={0.6} duration={2.1}/>
 | 
			
		||||
        <Envelope x={560} y={260} fill={GRAY_LT} path="rightBottom" delay={1.0} duration={2.3}/>
 | 
			
		||||
        <Envelope x={560} y={260} accent fill="#fff" path="rightBottom" delay={1.5} duration={2.0}/>
 | 
			
		||||
      </svg>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										194
									
								
								src/components/ProxyDetection.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								src/components/ProxyDetection.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,194 @@
 | 
			
		||||
'use client';
 | 
			
		||||
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import { motion, useReducedMotion } from 'framer-motion';
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  className?: string;   // e.g. "w-full h-64"
 | 
			
		||||
  bg?: string;          // defaults to white
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Palette (only these)
 | 
			
		||||
const ACCENT = '#00b8db';
 | 
			
		||||
const STROKE = '#111827';
 | 
			
		||||
const GRAY = '#9CA3AF';
 | 
			
		||||
const GRAY_LT = '#E5E7EB';
 | 
			
		||||
 | 
			
		||||
function Magnifier({
 | 
			
		||||
  x = 0,
 | 
			
		||||
  y = 0,
 | 
			
		||||
  flip = false,
 | 
			
		||||
  delay = 0,
 | 
			
		||||
  duration = 3,
 | 
			
		||||
}: {
 | 
			
		||||
  x?: number;
 | 
			
		||||
  y?: number;
 | 
			
		||||
  flip?: boolean;      // rotate handle direction
 | 
			
		||||
  delay?: number;
 | 
			
		||||
  duration?: number;
 | 
			
		||||
}) {
 | 
			
		||||
  const prefersReduced = useReducedMotion();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <motion.g
 | 
			
		||||
      initial={{ x: 0 }}
 | 
			
		||||
      animate={{ x: [0, 520] }}
 | 
			
		||||
      transition={{
 | 
			
		||||
        delay,
 | 
			
		||||
        duration: prefersReduced ? 0.01 : duration,
 | 
			
		||||
        ease: [0.22, 1, 0.36, 1],
 | 
			
		||||
        repeat: prefersReduced ? 0 : Infinity,
 | 
			
		||||
        repeatType: 'reverse',
 | 
			
		||||
        repeatDelay: 0.4,
 | 
			
		||||
      }}
 | 
			
		||||
      transform={`translate(${x}, ${y})`}
 | 
			
		||||
    >
 | 
			
		||||
      {/* glass */}
 | 
			
		||||
      <circle cx={0} cy={0} r={38} fill="#fff" stroke={STROKE} strokeWidth={6} />
 | 
			
		||||
      {/* subtle scanning pulse inside the glass */}
 | 
			
		||||
      <motion.circle
 | 
			
		||||
        cx={0}
 | 
			
		||||
        cy={0}
 | 
			
		||||
        r={26}
 | 
			
		||||
        fill="none"
 | 
			
		||||
        stroke={ACCENT}
 | 
			
		||||
        strokeWidth={4}
 | 
			
		||||
        initial={{ opacity: 0.15, scale: 0.8 }}
 | 
			
		||||
        animate={{ opacity: [0.15, 0.35, 0.15], scale: [0.8, 1.05, 0.8] }}
 | 
			
		||||
        transition={{ duration: 1.6, repeat: Infinity }}
 | 
			
		||||
      />
 | 
			
		||||
      {/* handle */}
 | 
			
		||||
      <g transform={`rotate(${flip ? 40 : -40}) translate(35, 10)`}>
 | 
			
		||||
        <rect x={0} y={-6} width={80} height={12} rx={6} fill={STROKE} />
 | 
			
		||||
        <rect x={0} y={-12} width={14} height={24} rx={6} fill={GRAY} />
 | 
			
		||||
      </g>
 | 
			
		||||
    </motion.g>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ServerBox({
 | 
			
		||||
  x,
 | 
			
		||||
  y,
 | 
			
		||||
  w = 88,
 | 
			
		||||
  h = 50,
 | 
			
		||||
  delay = 0,
 | 
			
		||||
  accentPulse = false,
 | 
			
		||||
}: {
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  w?: number;
 | 
			
		||||
  h?: number;
 | 
			
		||||
  delay?: number;
 | 
			
		||||
  accentPulse?: boolean;
 | 
			
		||||
}) {
 | 
			
		||||
  const prefersReduced = useReducedMotion();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <motion.g
 | 
			
		||||
      transform={`translate(${x}, ${y})`}
 | 
			
		||||
      initial={{ opacity: 0.6 }}
 | 
			
		||||
      animate={{
 | 
			
		||||
        opacity: 1,
 | 
			
		||||
      }}
 | 
			
		||||
      transition={{ delay, duration: 0.4 }}
 | 
			
		||||
    >
 | 
			
		||||
      {/* outer box */}
 | 
			
		||||
      <rect
 | 
			
		||||
        x={-w / 2}
 | 
			
		||||
        y={-h / 2}
 | 
			
		||||
        width={w}
 | 
			
		||||
        height={h}
 | 
			
		||||
        rx={10}
 | 
			
		||||
        fill="#fff"
 | 
			
		||||
        stroke={STROKE}
 | 
			
		||||
        strokeWidth={3}
 | 
			
		||||
      />
 | 
			
		||||
      {/* top bar */}
 | 
			
		||||
      <rect
 | 
			
		||||
        x={-w / 2 + 6}
 | 
			
		||||
        y={-h / 2 + 8}
 | 
			
		||||
        width={w - 12}
 | 
			
		||||
        height={12}
 | 
			
		||||
        rx={6}
 | 
			
		||||
        fill={GRAY_LT}
 | 
			
		||||
      />
 | 
			
		||||
      {/* activity line */}
 | 
			
		||||
      <motion.rect
 | 
			
		||||
        x={-w / 2 + 10}
 | 
			
		||||
        y={-h / 2 + 26}
 | 
			
		||||
        width={w - 20}
 | 
			
		||||
        height={10}
 | 
			
		||||
        rx={5}
 | 
			
		||||
        fill={GRAY_LT}
 | 
			
		||||
        initial={{ width: w * 0.2 }}
 | 
			
		||||
        animate={{ width: [w * 0.2, w - 20, w * 0.2] }}
 | 
			
		||||
        transition={{
 | 
			
		||||
          delay,
 | 
			
		||||
          duration: prefersReduced ? 0.01 : 1.8,
 | 
			
		||||
          repeat: prefersReduced ? 0 : Infinity,
 | 
			
		||||
          ease: [0.22, 1, 0.36, 1],
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
      {/* “detected” indicator */}
 | 
			
		||||
      <motion.circle
 | 
			
		||||
        cx={w / 2 - 14}
 | 
			
		||||
        cy={h / 2 - 14}
 | 
			
		||||
        r={6}
 | 
			
		||||
        fill={accentPulse ? ACCENT : GRAY}
 | 
			
		||||
        initial={{ scale: 0.9, opacity: 0.8 }}
 | 
			
		||||
        animate={
 | 
			
		||||
          accentPulse && !prefersReduced
 | 
			
		||||
            ? { scale: [0.9, 1.15, 0.9], opacity: [0.8, 1, 0.8] }
 | 
			
		||||
            : { scale: 1, opacity: 0.9 }
 | 
			
		||||
        }
 | 
			
		||||
        transition={{ duration: 1.4, repeat: accentPulse && !prefersReduced ? Infinity : 0 }}
 | 
			
		||||
      />
 | 
			
		||||
    </motion.g>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ProxyDetection({ className, bg = '#ffffff' }: Props) {
 | 
			
		||||
  // Canvas
 | 
			
		||||
  const W = 900;
 | 
			
		||||
  const H = 180;
 | 
			
		||||
 | 
			
		||||
  // Layout: a row of proxy servers
 | 
			
		||||
  const rowY = H / 2;
 | 
			
		||||
  const xs = [180, 320, 460, 600, 740];
 | 
			
		||||
 | 
			
		||||
  // Sequence timings so boxes light up as magnifier passes
 | 
			
		||||
  const delays = [0.8, 0.6, 0.4, 0.2, 0.0];
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={className}
 | 
			
		||||
      aria-hidden="true"
 | 
			
		||||
      role="img"
 | 
			
		||||
      style={{ background: bg }}
 | 
			
		||||
    >
 | 
			
		||||
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%">
 | 
			
		||||
        {/* subtle grid */}
 | 
			
		||||
        <defs>
 | 
			
		||||
          <pattern id="grid" width="24" height="24" patternUnits="userSpaceOnUse">
 | 
			
		||||
            <path d="M 24 0 L 0 0 0 24" fill="none" stroke={GRAY_LT} strokeWidth="1" />
 | 
			
		||||
          </pattern>
 | 
			
		||||
        </defs>
 | 
			
		||||
        <rect width={W} height={H} fill="url(#grid)" />
 | 
			
		||||
 | 
			
		||||
        {/* Server row (right -> left sweep) */}
 | 
			
		||||
        {xs.map((x, i) => (
 | 
			
		||||
          <ServerBox
 | 
			
		||||
            key={`b-${i}`}
 | 
			
		||||
            x={x}
 | 
			
		||||
            y={rowY}
 | 
			
		||||
            delay={delays[i]}
 | 
			
		||||
            accentPulse
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
 | 
			
		||||
        {/* Magnifier scanning across the row (opposite direction) */}
 | 
			
		||||
        <Magnifier x={120} y={rowY} flip={true} delay={0.25} duration={3.2} />
 | 
			
		||||
      </svg>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										175
									
								
								src/components/ProxyForwarding.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								src/components/ProxyForwarding.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,175 @@
 | 
			
		||||
'use client';
 | 
			
		||||
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import { motion, useReducedMotion } from 'framer-motion';
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  className?: string;   // e.g. "w-full h-72"
 | 
			
		||||
  bg?: string;          // default white
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Palette */
 | 
			
		||||
const ACCENT = '#00b8db';
 | 
			
		||||
const STROKE = '#111827';   // black-ish
 | 
			
		||||
const GRAY = '#9CA3AF';
 | 
			
		||||
const GRAY_LT = '#E5E7EB';
 | 
			
		||||
 | 
			
		||||
function Laptop({ x, y }: { x: number; y: number }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <g transform={`translate(${x}, ${y})`}>
 | 
			
		||||
      {/* screen */}
 | 
			
		||||
      <rect x={-48} y={-32} width={96} height={64} rx={8} fill="#fff" stroke={STROKE} strokeWidth={3} />
 | 
			
		||||
      <rect x={-44} y={-28} width={88} height={40} rx={6} fill={GRAY_LT} />
 | 
			
		||||
      {/* base */}
 | 
			
		||||
      <rect x={-56} y={32} width={112} height={10} rx={5} fill={STROKE} />
 | 
			
		||||
    </g>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ServerStack({ x, y }: { x: number; y: number }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <g transform={`translate(${x}, ${y})`}>
 | 
			
		||||
      {[0, 1, 2].map((i) => (
 | 
			
		||||
        <g key={i} transform={`translate(0, ${-38 + i * 28})`}>
 | 
			
		||||
          <rect x={-56} y={-12} width={112} height={24} rx={8} fill="#fff" stroke={STROKE} strokeWidth={3} />
 | 
			
		||||
          <rect x={-46} y={-6} width={56} height={12} rx={6} fill={GRAY_LT} />
 | 
			
		||||
          <circle cx={20} cy={0} r={4} fill={GRAY} />
 | 
			
		||||
          <circle cx={30} cy={0} r={4} fill={ACCENT} />
 | 
			
		||||
          <circle cx={40} cy={0} r={4} fill={GRAY} />
 | 
			
		||||
        </g>
 | 
			
		||||
      ))}
 | 
			
		||||
      {/* tiny rack base */}
 | 
			
		||||
      <rect x={-18} y={48} width={36} height={6} rx={3} fill={GRAY} />
 | 
			
		||||
      <rect x={-10} y={54} width={20} height={6} rx={3} fill={ACCENT} />
 | 
			
		||||
    </g>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Cloud({ x, y }: { x: number; y: number }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <g transform={`translate(${x}, ${y})`} fill={STROKE}>
 | 
			
		||||
      <circle cx={-30} cy={0} r={18} fill={STROKE} />
 | 
			
		||||
      <circle cx={-8} cy={-10} r={22} fill={STROKE} />
 | 
			
		||||
      <circle cx={16} cy={0} r={20} fill={STROKE} />
 | 
			
		||||
      <rect x={-40} y={0} width={72} height={20} rx={10} fill={STROKE} />
 | 
			
		||||
      <rect x={-46} y={18} width={88} height={6} rx={3} fill={STROKE} />
 | 
			
		||||
    </g>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Arrow({ d, delay = 0 }: { d: string; delay?: number }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <motion.path
 | 
			
		||||
      d={d}
 | 
			
		||||
      fill="none"
 | 
			
		||||
      stroke={STROKE}
 | 
			
		||||
      strokeWidth={4}
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      initial={{ pathLength: 0, opacity: 0.3 }}
 | 
			
		||||
      animate={{ pathLength: 1, opacity: 1 }}
 | 
			
		||||
      transition={{ duration: 0.8, delay, ease: [0.22, 1, 0.36, 1] }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Small packet traveling along keyframe x/y arrays */
 | 
			
		||||
function Packet({
 | 
			
		||||
  xs,
 | 
			
		||||
  ys,
 | 
			
		||||
  delay = 0,
 | 
			
		||||
  color = ACCENT,
 | 
			
		||||
  duration = 2.2,
 | 
			
		||||
}: {
 | 
			
		||||
  xs: number[];
 | 
			
		||||
  ys: number[];
 | 
			
		||||
  delay?: number;
 | 
			
		||||
  color?: string;
 | 
			
		||||
  duration?: number;
 | 
			
		||||
}) {
 | 
			
		||||
  const prefersReduced = useReducedMotion();
 | 
			
		||||
  return (
 | 
			
		||||
    <motion.circle
 | 
			
		||||
      r={6}
 | 
			
		||||
      fill={color}
 | 
			
		||||
      initial={{ x: xs[0], y: ys[0], opacity: 0 }}
 | 
			
		||||
      animate={{
 | 
			
		||||
        x: prefersReduced ? xs[0] : xs,
 | 
			
		||||
        y: prefersReduced ? ys[0] : ys,
 | 
			
		||||
        opacity: 1,
 | 
			
		||||
      }}
 | 
			
		||||
      transition={{
 | 
			
		||||
        delay,
 | 
			
		||||
        duration: prefersReduced ? 0.01 : duration,
 | 
			
		||||
        ease: [0.22, 1, 0.36, 1],
 | 
			
		||||
        repeat: prefersReduced ? 0 : Infinity,
 | 
			
		||||
        repeatDelay: 0.6,
 | 
			
		||||
      }}
 | 
			
		||||
      stroke="#fff"
 | 
			
		||||
      strokeWidth={2}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ProxyForwarding({ className, bg = '#ffffff' }: Props) {
 | 
			
		||||
  const W = 1000;
 | 
			
		||||
  const H = 420;
 | 
			
		||||
 | 
			
		||||
  // Key points
 | 
			
		||||
  const C1 = { x: 140, y: 90 };
 | 
			
		||||
  const C2 = { x: 140, y: 210 };
 | 
			
		||||
  const C3 = { x: 140, y: 330 };
 | 
			
		||||
 | 
			
		||||
  const PROXY = { x: 420, y: 210 };
 | 
			
		||||
  const CLOUD = { x: 640, y: 210 };
 | 
			
		||||
  const DEST = { x: 860, y: 210 };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={className} aria-hidden="true" role="img" style={{ background: bg }}>
 | 
			
		||||
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%">
 | 
			
		||||
        {/* subtle grid bg */}
 | 
			
		||||
        <defs>
 | 
			
		||||
          <pattern id="grid" width="24" height="24" patternUnits="userSpaceOnUse">
 | 
			
		||||
            <path d="M 24 0 L 0 0 0 24" fill="none" stroke={GRAY_LT} strokeWidth="1" />
 | 
			
		||||
          </pattern>
 | 
			
		||||
        </defs>
 | 
			
		||||
        <rect width={W} height={H} fill="url(#grid)" />
 | 
			
		||||
 | 
			
		||||
        {/* Clients */}
 | 
			
		||||
        <Laptop x={C1.x} y={C1.y} />
 | 
			
		||||
        <Laptop x={C2.x} y={C2.y} />
 | 
			
		||||
        <Laptop x={C3.x} y={C3.y} />
 | 
			
		||||
 | 
			
		||||
        {/* Proxy (stack) */}
 | 
			
		||||
        <ServerStack x={PROXY.x} y={PROXY.y} />
 | 
			
		||||
 | 
			
		||||
        {/* Cloud / Internet */}
 | 
			
		||||
        <Cloud x={CLOUD.x} y={CLOUD.y} />
 | 
			
		||||
 | 
			
		||||
        {/* Destination servers */}
 | 
			
		||||
        <ServerStack x={DEST.x} y={DEST.y} />
 | 
			
		||||
 | 
			
		||||
        {/* Arrows: clients -> proxy */}
 | 
			
		||||
        <Arrow d={`M ${C1.x + 70} ${C1.y} C 260 ${C1.y}, 320 150, ${PROXY.x - 80} 170`} delay={0.05} />
 | 
			
		||||
        <Arrow d={`M ${C2.x + 70} ${C2.y} L ${PROXY.x - 80} ${PROXY.y}`} delay={0.1} />
 | 
			
		||||
        <Arrow d={`M ${C3.x + 70} ${C3.y} C 260 ${C3.y}, 320 270, ${PROXY.x - 80} 250`} delay={0.15} />
 | 
			
		||||
 | 
			
		||||
        {/* Arrow: proxy -> cloud -> destination */}
 | 
			
		||||
        <Arrow d={`M ${PROXY.x + 80} ${PROXY.y} L ${CLOUD.x - 60} ${CLOUD.y}`} delay={0.2} />
 | 
			
		||||
        <Arrow d={`M ${CLOUD.x + 60} ${CLOUD.y} L ${DEST.x - 80} ${DEST.y}`} delay={0.25} />
 | 
			
		||||
 | 
			
		||||
        {/* Packets flowing from clients to proxy */}
 | 
			
		||||
        <Packet xs={[C1.x + 70, PROXY.x - 80]} ys={[C1.y, 170]} delay={0.0} />
 | 
			
		||||
        <Packet xs={[C2.x + 70, PROXY.x - 80]} ys={[C2.y, PROXY.y]} delay={0.3} color={GRAY} />
 | 
			
		||||
        <Packet xs={[C3.x + 70, PROXY.x - 80]} ys={[C3.y, 250]} delay={0.6} />
 | 
			
		||||
 | 
			
		||||
        {/* Packets moving through proxy to cloud */}
 | 
			
		||||
        <Packet xs={[PROXY.x + 80, CLOUD.x - 60]} ys={[PROXY.y, CLOUD.y]} delay={0.4} />
 | 
			
		||||
        <Packet xs={[PROXY.x + 80, CLOUD.x - 60]} ys={[PROXY.y, CLOUD.y]} delay={0.9} color={GRAY} />
 | 
			
		||||
 | 
			
		||||
        {/* Packets leaving cloud to destination */}
 | 
			
		||||
        <Packet xs={[CLOUD.x + 60, DEST.x - 80]} ys={[CLOUD.y, DEST.y]} delay={0.7} />
 | 
			
		||||
        <Packet xs={[CLOUD.x + 60, DEST.x - 80]} ys={[CLOUD.y, DEST.y]} delay={1.1} color={GRAY} />
 | 
			
		||||
      </svg>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user