273 lines
10 KiB
TypeScript
273 lines
10 KiB
TypeScript
"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 Link from "next/link";
|
||
|
||
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-[90vh] justify-center overflow-hidden">
|
||
<MatrixBackground isSecure={isVpnOn} theme={resolvedTheme} />
|
||
|
||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_transparent_0%,_var(--background)_80%)] -z-10" />
|
||
|
||
<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"
|
||
>
|
||
{/* ИНВЕРСИЯ БЕЙДЖА */}
|
||
<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-4 justify-center w-full sm:w-auto font-mono">
|
||
{/* ИНВЕРСИЯ КНОПОК */}
|
||
<Button
|
||
asChild
|
||
className={`text-lg font-bold px-8 py-6 rounded-2xl transition-all duration-300 w-full sm:w-auto ${
|
||
isVpnOn
|
||
? "bg-purple-600 text-white hover:bg-purple-700 dark:bg-primary dark:text-primary-foreground dark:hover:bg-primary/90 shadow-[0_4px_14px_rgba(139,61,255,0.3)] dark:shadow-[0_0_20px_rgba(139,61,255,0.3)]"
|
||
: "bg-cyan-600 text-white hover:bg-cyan-700 dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/90 shadow-[0_4px_14px_rgba(0,229,242,0.3)] dark:shadow-[0_0_20px_rgba(0,229,242,0.3)]"
|
||
}`}
|
||
>
|
||
<Link href={`/${lang}/download`}>{dict.ctaPrimary}</Link>
|
||
</Button>
|
||
|
||
<Button
|
||
asChild
|
||
variant="outline"
|
||
className="border-border/60 bg-background/60 backdrop-blur-md text-foreground hover:bg-muted font-semibold text-lg px-8 py-6 rounded-2xl w-full sm:w-auto shadow-sm"
|
||
>
|
||
<Link href={`/${lang}/nodes`}>{dict.ctaSecondary}</Link>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</section>
|
||
);
|
||
}
|