198 lines
8.4 KiB
TypeScript
198 lines
8.4 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"; // ИМПОРТИРУЕМ DYNAMIC
|
|
import { Dictionary } from "@/lib/IDict";
|
|
|
|
// ДИНАМИЧЕСКИЙ ИМПОРТ МАТРИЦЫ (ОТКЛЮЧАЕМ SSR)
|
|
const MatrixBackground = dynamic(() => import("./MatrixBackground"), {
|
|
ssr: false,
|
|
});
|
|
|
|
// --- КОМПОНЕНТ ГЛАЗ ---
|
|
function WatcherEyes({ isClosed }: { isClosed: boolean }) {
|
|
// ... (весь твой код WatcherEyes остается без изменений,
|
|
// он абсолютно безопасен для SSR)
|
|
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
|
const [isBlinking, setIsBlinking] = useState(false);
|
|
const blinkTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
const unblinkTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
useEffect(() => {
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
const x = (e.clientX / window.innerWidth) * 2 - 1;
|
|
const y = (e.clientY / window.innerHeight) * 2 - 1;
|
|
setMousePos({ x, y });
|
|
};
|
|
if (!isClosed) window.addEventListener("mousemove", handleMouseMove);
|
|
return () => window.removeEventListener("mousemove", handleMouseMove);
|
|
}, [isClosed]);
|
|
|
|
useEffect(() => {
|
|
if (isClosed) {
|
|
if (blinkTimerRef.current) clearTimeout(blinkTimerRef.current);
|
|
if (unblinkTimerRef.current) clearTimeout(unblinkTimerRef.current);
|
|
return;
|
|
}
|
|
const scheduleNextBlink = () => {
|
|
const delay = Math.random() * 4000 + 1500;
|
|
blinkTimerRef.current = setTimeout(() => {
|
|
setIsBlinking(true);
|
|
unblinkTimerRef.current = setTimeout(() => {
|
|
setIsBlinking(false);
|
|
scheduleNextBlink();
|
|
}, 150);
|
|
}, delay);
|
|
};
|
|
scheduleNextBlink();
|
|
return () => {
|
|
if (blinkTimerRef.current) clearTimeout(blinkTimerRef.current);
|
|
if (unblinkTimerRef.current) clearTimeout(unblinkTimerRef.current);
|
|
};
|
|
}, [isClosed]);
|
|
|
|
const maxOffset = 16;
|
|
const eyeVariants: Variants = {
|
|
open: {
|
|
height: "48px",
|
|
transition: { type: "spring", stiffness: 300, damping: 15, mass: 0.5 },
|
|
},
|
|
closed: { height: "0px", transition: { duration: 0.08, ease: "circIn" } },
|
|
};
|
|
const shouldBeClosed = isClosed || isBlinking;
|
|
|
|
return (
|
|
<div className="flex gap-8 justify-center mb-10 h-12 items-center">
|
|
{[1, 2].map((i) => (
|
|
<motion.div
|
|
key={i}
|
|
variants={eyeVariants}
|
|
initial="open"
|
|
animate={shouldBeClosed ? "closed" : "open"}
|
|
className={`w-28 relative overflow-hidden flex items-center justify-center rounded-[100%] transition-colors transition-shadow duration-500 ${
|
|
isClosed
|
|
? "bg-primary/20 border-primary/50 shadow-[0_0_15px_rgba(0,240,255,0.2)]"
|
|
: "bg-gray-100 dark:bg-[#050505] border-2 border-secondary/50 dark:border-secondary shadow-[0_0_20px_rgba(112,0,255,0.2)_inset,0_2px_10px_rgba(0,0,0,0.1)] dark:shadow-[0_0_30px_rgba(112,0,255,0.5)_inset,0_0_20px_rgba(112,0,255,0.4)]"
|
|
}`}
|
|
>
|
|
<div className="absolute inset-0 shadow-[inset_0px_8px_16px_rgba(0,0,0,0.2)] dark:shadow-[inset_0px_10px_20px_rgba(0,0,0,0.9)] rounded-[100%] z-10 pointer-events-none" />
|
|
{!isClosed && (
|
|
<motion.div
|
|
animate={{ x: mousePos.x * maxOffset, y: mousePos.y * maxOffset }}
|
|
transition={{ type: "tween", ease: "backOut", duration: 0.15 }}
|
|
className="w-12 h-12 rounded-full flex items-center justify-center relative z-0"
|
|
>
|
|
<div className="absolute inset-0 rounded-full border-[4px] border-secondary opacity-90 shadow-[0_0_10px_rgba(112,0,255,0.4)] dark:shadow-[0_0_15px_#7000FF,inset_0_0_10px_#7000FF]" />
|
|
<div className="w-5 h-5 bg-black rounded-full relative overflow-hidden flex items-center justify-center shadow-[0_4px_6px_rgba(0,0,0,0.5)] dark:shadow-[0_0_10px_#000]">
|
|
<div className="w-1.5 h-1.5 bg-white rounded-full shadow-[0_0_8px_#fff]" />
|
|
</div>
|
|
<div className="absolute top-1.5 right-2 w-2.5 h-2.5 bg-white/90 blur-[0.5px] rounded-full" />
|
|
<div className="absolute top-4 right-1.5 w-1 h-1 bg-white rounded-full" />
|
|
</motion.div>
|
|
)}
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// --- ГЛАВНЫЙ КОМПОНЕНТ HERO ---
|
|
export function Hero({ dict }: { dict: Dictionary["hero"] }) {
|
|
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-primary/20 border-primary/40 text-primary-foreground dark:text-primary"
|
|
: "bg-secondary/20 border-secondary/40 text-secondary-foreground dark:text-secondary"
|
|
}`}
|
|
>
|
|
{isVpnOn ? (
|
|
<Shield className="w-4 h-4" />
|
|
) : (
|
|
<EyeOff className="w-4 h-4" />
|
|
)}
|
|
<span>{isVpnOn ? dict.badgeSecure : dict.badge}</span>
|
|
</motion.div>
|
|
|
|
<WatcherEyes isClosed={isVpnOn} />
|
|
|
|
<h1 className="text-4xl md:text-6xl font-extrabold tracking-tight text-foreground mb-6 transition-colors duration-500">
|
|
{dict.titleStart}{" "}
|
|
<span
|
|
className={`transition-all duration-500 ${
|
|
isVpnOn
|
|
? "text-primary dark:drop-shadow-[0_0_20px_rgba(0,240,255,0.6)]"
|
|
: "text-secondary dark:drop-shadow-[0_0_20px_rgba(112,0,255,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">
|
|
<div className="flex items-center gap-4 bg-card/80 backdrop-blur-xl border border-border/60 p-4 rounded-3xl shadow-md dark:shadow-lg transition-colors duration-300">
|
|
<Switch
|
|
checked={isVpnOn}
|
|
onCheckedChange={setIsVpnOn}
|
|
className="data-[state=checked]:bg-primary data-[state=unchecked]:bg-secondary"
|
|
/>
|
|
<span
|
|
className={`text-base font-bold transition-colors ${
|
|
isVpnOn
|
|
? "text-primary dark:drop-shadow-[0_0_5px_rgba(0,240,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">
|
|
<Button
|
|
className={`text-lg font-bold px-8 py-6 rounded-2xl transition-all duration-300 w-full sm:w-auto ${
|
|
isVpnOn
|
|
? "bg-primary text-primary-foreground hover:bg-primary/90 shadow-[0_4px_14px_rgba(0,240,255,0.4)] dark:shadow-[0_0_20px_rgba(0,240,255,0.3)]"
|
|
: "bg-secondary text-secondary-foreground hover:bg-secondary/90 shadow-[0_4px_14px_rgba(112,0,255,0.4)] dark:shadow-[0_0_20px_rgba(112,0,255,0.3)]"
|
|
}`}
|
|
>
|
|
{dict.ctaPrimary}
|
|
</Button>
|
|
<Button
|
|
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"
|
|
>
|
|
{dict.ctaSecondary}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</section>
|
|
);
|
|
}
|