Files
netrunner-landing/components/sections/Hero.tsx
T

270 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { motion, Variants } from "framer-motion";
import { Shield, EyeOff } from "lucide-react";
import { useTheme } from "next-themes";
import dynamic from "next/dynamic";
import { Dictionary } from "@/lib/IDict";
import { CyberButton } from "../ui/cyber-button";
const MatrixBackground = dynamic(() => import("./MatrixBackground"), {
ssr: false,
});
function CyberEye({ isClosed, vpnOn }: { isClosed: boolean; vpnOn: boolean }) {
const eyeRef = useRef<HTMLDivElement>(null);
const [offset, setOffset] = useState({ x: 0, y: 0 });
const [direction, setDirection] = useState("center");
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (isClosed || !eyeRef.current) return;
const rect = eyeRef.current.getBoundingClientRect();
const eyeCenterX = rect.left + rect.width / 2;
const eyeCenterY = rect.top + rect.height / 2;
const dx = e.clientX - eyeCenterX;
const dy = e.clientY - eyeCenterY;
if (Math.abs(dx) < 30 && Math.abs(dy) < 30) {
setDirection("center");
} else if (Math.abs(dx) > Math.abs(dy)) {
setDirection(dx > 0 ? "right" : "left");
} else {
setDirection(dy > 0 ? "down" : "up");
}
const angle = Math.atan2(dy, dx);
const maxRadius = 22;
const distance = Math.min(maxRadius, Math.hypot(dx, dy) * 0.12);
setOffset({
x: Math.cos(angle) * distance,
y: Math.sin(angle) * distance,
});
};
if (!isClosed) window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, [isClosed]);
const eyeVariants: Variants = {
open: {
height: 72,
width: 120,
borderRadius: 36,
borderWidth: 4,
transition: { type: "spring", stiffness: 450, damping: 12 },
},
closed: {
height: 6,
width: 140,
borderRadius: 8,
borderWidth: 3,
transition: { type: "spring", stiffness: 600, damping: 14 },
},
};
// ИНВЕРСИЯ: Активный VPN (vpnOn) теперь фиолетовый (Primary), выключенный - циан (Secondary)
const borderColor = vpnOn
? "border-purple-600 dark:border-primary shadow-[0_0_15px_rgba(139,61,255,0.3)] dark:shadow-[0_0_20px_rgba(139,61,255,0.4)]"
: "border-cyan-600 dark:border-secondary shadow-[0_0_15px_rgba(0,229,242,0.3)] dark:shadow-[0_0_20px_rgba(0,229,242,0.4)]";
return (
<motion.div
ref={eyeRef}
variants={eyeVariants}
initial="open"
animate={isClosed ? "closed" : "open"}
className={`relative flex items-center justify-center overflow-hidden bg-background ${borderColor} z-10`}
>
<motion.div
animate={{
x: isClosed ? 0 : offset.x,
y: isClosed ? 0 : offset.y,
scale: isClosed ? 0.3 : 1,
opacity: isClosed ? 0 : 1,
}}
transition={{ type: "tween", ease: "easeOut", duration: 0.1 }}
className="relative flex items-center justify-center text-foreground"
>
<span
className={`absolute -top-4 text-xs transition-opacity duration-200 ${direction === "up" ? "opacity-100 text-current scale-125" : "opacity-20"}`}
>
</span>
<span
className={`absolute -bottom-4 text-xs transition-opacity duration-200 ${direction === "down" ? "opacity-100 text-current scale-125" : "opacity-20"}`}
>
</span>
<span
className={`absolute -left-5 text-xs transition-opacity duration-200 ${direction === "left" ? "opacity-100 text-current scale-125" : "opacity-20"}`}
>
</span>
<span
className={`absolute -right-5 text-xs transition-opacity duration-200 ${direction === "right" ? "opacity-100 text-current scale-125" : "opacity-20"}`}
>
</span>
<span className="text-3xl z-10 drop-shadow-md"></span>
</motion.div>
{/* ИНВЕРСИЯ ЦВЕТА ПОЛОСКИ */}
<motion.div
className={`absolute h-0.5 w-full ${vpnOn ? "bg-purple-600 dark:bg-primary" : "bg-cyan-600 dark:bg-secondary"}`}
initial={{ opacity: 0 }}
animate={{ opacity: isClosed ? 1 : 0 }}
transition={{ duration: 0.1 }}
/>
</motion.div>
);
}
function CyberWatcherEyes({ vpnOn }: { vpnOn: boolean }) {
const [isBlinking, setIsBlinking] = useState(false);
const blinkTimerRef = useRef<NodeJS.Timeout | null>(null);
const unblinkTimerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (vpnOn) {
if (blinkTimerRef.current) clearTimeout(blinkTimerRef.current);
if (unblinkTimerRef.current) clearTimeout(unblinkTimerRef.current);
return;
}
const scheduleNextBlink = () => {
const delay = Math.random() * 3500 + 1500;
blinkTimerRef.current = setTimeout(() => {
setIsBlinking(true);
unblinkTimerRef.current = setTimeout(() => {
setIsBlinking(false);
scheduleNextBlink();
}, 140);
}, delay);
};
scheduleNextBlink();
return () => {
if (blinkTimerRef.current) clearTimeout(blinkTimerRef.current);
if (unblinkTimerRef.current) clearTimeout(unblinkTimerRef.current);
};
}, [vpnOn]);
const shouldBeClosed = vpnOn || isBlinking;
return (
<div className="flex gap-6 md:gap-10 justify-center mb-12 h-20 items-center select-none">
<CyberEye isClosed={shouldBeClosed} vpnOn={vpnOn} />
<CyberEye isClosed={shouldBeClosed} vpnOn={vpnOn} />
</div>
);
}
export function Hero({
dict,
lang,
}: {
dict: Dictionary["hero"];
lang: string;
}) {
const [isVpnOn, setIsVpnOn] = useState(false);
const { resolvedTheme } = useTheme();
return (
<section className="relative pt-32 pb-20 md:pt-48 md:pb-32 flex flex-col items-center text-center px-4 min-h-[95vh] justify-center overflow-hidden">
{/* КОНТЕЙНЕР ФОНА: Теперь он inset-0, без верхних границ */}
<div className="absolute inset-0 -z-10 bg-background pointer-events-none">
<MatrixBackground isSecure={isVpnOn} theme={resolvedTheme} />
{/* ВИНЬЕТКА: Сделаем ее погуще сверху, чтобы хедер читался идеально */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_transparent_10%,_var(--background)_110%)] opacity-80" />
{/* Дополнительный мягкий градиент сверху для "воздуха" */}
<div className="absolute inset-x-0 top-0 h-40 bg-gradient-to-b from-background via-transparent to-transparent opacity-90" />
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="relative z-10 w-full max-w-4xl mx-auto pt-40 md:pt-48 pb-20"
>
{/* ИНВЕРСИЯ БЕЙДЖА */}
<motion.div
layout
className={`inline-flex items-center gap-2 px-4 py-1.5 rounded-full border text-sm font-bold mb-8 transition-colors duration-500 shadow-sm ${
isVpnOn
? "bg-purple-100 border-purple-300 text-purple-800 dark:bg-primary/20 dark:border-primary dark:text-primary-foreground dark:drop-shadow-[0_0_5px_rgba(139,61,255,0.4)]"
: "bg-cyan-100 border-cyan-300 text-cyan-800 dark:bg-secondary/20 dark:border-secondary dark:text-secondary-foreground dark:drop-shadow-[0_0_5px_rgba(0,229,242,0.4)]"
}`}
>
{isVpnOn ? (
<Shield className="w-4 h-4" />
) : (
<EyeOff className="w-4 h-4" />
)}
<span>{isVpnOn ? dict.badgeSecure : dict.badge}</span>
</motion.div>
<CyberWatcherEyes vpnOn={isVpnOn} />
{/* ИНВЕРСИЯ ЗАГОЛОВКА */}
<h1 className="text-4xl md:text-6xl font-extrabold tracking-tight text-slate-900 dark:text-white dark:drop-shadow-md mb-6 transition-colors duration-500 font-mono">
{dict.titleStart}{" "}
<span
className={`transition-all duration-500 ${
isVpnOn
? "text-purple-600 dark:text-primary dark:drop-shadow-[0_0_15px_rgba(139,61,255,0.6)]"
: "text-cyan-600 dark:text-secondary dark:drop-shadow-[0_0_15px_rgba(0,229,242,0.6)]"
}`}
>
{dict.titleHighlight}
</span>
</h1>
<p className="text-muted-foreground font-medium text-lg md:text-xl max-w-2xl mx-auto mb-12">
{dict.subtitle}
</p>
<div className="flex flex-col items-center gap-8 mb-12">
{/* ИНВЕРСИЯ SWITCH'А */}
<div className="flex items-center gap-4 bg-card/90 backdrop-blur-xl border border-border/60 p-4 rounded-3xl shadow-md transition-colors duration-300">
<Switch
checked={isVpnOn}
onCheckedChange={setIsVpnOn}
className="data-[state=checked]:bg-purple-600 dark:data-[state=checked]:bg-primary data-[state=unchecked]:bg-cyan-600 dark:data-[state=unchecked]:bg-secondary"
/>
<span
className={`text-base font-bold transition-colors font-mono ${
isVpnOn
? "text-purple-700 dark:text-primary dark:drop-shadow-[0_0_5px_rgba(139,61,255,0.8)]"
: "text-foreground"
}`}
>
Netrunner VPN
</span>
</div>
<div className="flex flex-col sm:flex-row gap-6 justify-center w-full sm:w-auto font-mono">
<CyberButton
href={`/${lang}/download`}
variant={isVpnOn ? "primary" : "secondary"}
>
{dict.ctaPrimary}
</CyberButton>
<CyberButton href={`/${lang}/nodes`} variant="secondary">
{dict.ctaSecondary}
</CyberButton>
</div>
</div>
</motion.div>
</section>
);
}