feat: replace static images with MessageBus and ProxyDetection components in Features section
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user