loading and seo optimization, big design update
This commit is contained in:
@@ -24,11 +24,11 @@ export default async function BlogIndexPage({ params }: Props) {
|
|||||||
let records: PBPost[] = [];
|
let records: PBPost[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Динамически выбираем таблицу: posts_ru или posts_en
|
|
||||||
records = await pb.collection(`posts_${lang}`).getFullList<PBPost>({
|
records = await pb.collection(`posts_${lang}`).getFullList<PBPost>({
|
||||||
filter: "published = true",
|
filter: "published = true",
|
||||||
sort: "-created",
|
sort: "-created",
|
||||||
cache: "no-store",
|
fetch: (url, config) =>
|
||||||
|
fetch(url, { ...config, next: { revalidate: 3600 } }),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to fetch blog posts:", err);
|
console.error("Failed to fetch blog posts:", err);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { Footer } from "@/components/sections/Footer";
|
|||||||
const jetbrainsMono = JetBrains_Mono({
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
subsets: ["latin", "cyrillic"],
|
subsets: ["latin", "cyrillic"],
|
||||||
display: "swap",
|
display: "swap",
|
||||||
|
preload: true, // Гарантирует предзагрузку в <head>
|
||||||
|
fallback: ["monospace"], // Указывает браузеру, что показывать до загрузки
|
||||||
});
|
});
|
||||||
|
|
||||||
// Настройка метаданных
|
// Настройка метаданных
|
||||||
|
|||||||
+35
-6
@@ -1,11 +1,39 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import { getDictionary } from "@/lib/dictionaries";
|
import { getDictionary } from "@/lib/dictionaries";
|
||||||
import { Hero } from "@/components/sections/Hero";
|
import { Hero } from "@/components/sections/Hero";
|
||||||
import { Features } from "@/components/sections/Features";
|
import { Features } from "@/components/sections/Features";
|
||||||
import { Pricing } from "@/components/sections/Pricing";
|
|
||||||
import { FAQ } from "@/components/sections/Faq";
|
// Легкий скелетон для плавности
|
||||||
import { BlogPreview } from "@/components/sections/BlogPreview";
|
const SectionSkeleton = () => (
|
||||||
import { TerminalGuestbook } from "@/components/sections/TerminalGuestbook";
|
<div className="h-[500px] w-full animate-pulse bg-card/20 rounded-3xl my-10" />
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ленивая загрузка тяжелых секций
|
||||||
|
const Pricing = dynamic(
|
||||||
|
() => import("@/components/sections/Pricing").then((mod) => mod.Pricing),
|
||||||
|
{
|
||||||
|
loading: () => <SectionSkeleton />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const FAQ = dynamic(
|
||||||
|
() => import("@/components/sections/Faq").then((mod) => mod.FAQ),
|
||||||
|
{
|
||||||
|
loading: () => <SectionSkeleton />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const TerminalGuestbook = dynamic(
|
||||||
|
() =>
|
||||||
|
import("@/components/sections/TerminalGuestbook").then(
|
||||||
|
(mod) => mod.TerminalGuestbook,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
loading: () => <SectionSkeleton />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const BlogPreview = dynamic(() =>
|
||||||
|
import("@/components/sections/BlogPreview").then((mod) => mod.BlogPreview),
|
||||||
|
);
|
||||||
|
|
||||||
export default async function Home({
|
export default async function Home({
|
||||||
params,
|
params,
|
||||||
@@ -17,13 +45,14 @@ export default async function Home({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Добавили передачу lang в Hero */}
|
|
||||||
<Hero dict={dict.hero} lang={lang} />
|
<Hero dict={dict.hero} lang={lang} />
|
||||||
<Features dict={dict.features} />
|
<Features dict={dict.features} />
|
||||||
|
|
||||||
|
{/* Теперь эти секции загрузятся только когда понадобятся */}
|
||||||
<Pricing dict={dict.pricing} lang={lang} />
|
<Pricing dict={dict.pricing} lang={lang} />
|
||||||
<FAQ dict={dict.faq} lang={lang} />
|
<FAQ dict={dict.faq} lang={lang} />
|
||||||
<TerminalGuestbook dict={dict.guestbook} />
|
<TerminalGuestbook dict={dict.guestbook} />
|
||||||
<Suspense fallback={<div className="py-20 text-center">Loading...</div>}>
|
<Suspense fallback={<SectionSkeleton />}>
|
||||||
<BlogPreview lang={lang} dict={dict.blog} />
|
<BlogPreview lang={lang} dict={dict.blog} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+48
-19
@@ -37,28 +37,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(0.99 0 0);
|
/* Остужаем фон, чтобы он не был слепяще белым */
|
||||||
--foreground: oklch(0.12 0 0);
|
--background: oklch(0.98 0.01 240);
|
||||||
--card: oklch(1 0 0 / 70%);
|
--foreground: oklch(0.2 0.02 260);
|
||||||
--card-foreground: oklch(0.12 0 0);
|
|
||||||
--popover: oklch(1 0 0 / 80%);
|
/* Карточки теперь чистые белые с легким блюром */
|
||||||
--popover-foreground: oklch(0.12 0 0);
|
--card: oklch(1 0 0 / 90%);
|
||||||
|
--card-foreground: oklch(0.2 0.02 260);
|
||||||
|
--popover: oklch(1 0 0 / 95%);
|
||||||
|
--popover-foreground: oklch(0.2 0.02 260);
|
||||||
|
|
||||||
/* Светлая тема */
|
/* Светлая тема */
|
||||||
/* Primary = Фиолетовый */
|
/* Primary = Более глубокий фиолетовый для контраста */
|
||||||
--primary: oklch(0.45 0.25 290);
|
--primary: oklch(0.5 0.2 290);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
/* Secondary = Голубой */
|
/* Secondary = Более насыщенный циан (ближе к синему) */
|
||||||
--secondary: oklch(0.65 0.15 220);
|
--secondary: oklch(0.55 0.15 220);
|
||||||
--secondary-foreground: oklch(0.1 0 0);
|
--secondary-foreground: oklch(1 0 0);
|
||||||
|
|
||||||
--muted: oklch(0.95 0 0);
|
--muted: oklch(0.94 0.01 240);
|
||||||
--muted-foreground: oklch(0.45 0 0);
|
--muted-foreground: oklch(0.45 0.02 260);
|
||||||
--accent: oklch(0.92 0.05 290);
|
--accent: oklch(0.94 0.02 290);
|
||||||
--accent-foreground: oklch(0.45 0.25 290);
|
--accent-foreground: oklch(0.45 0.25 290);
|
||||||
--border: oklch(0.9 0 0 / 50%);
|
--border: oklch(0.85 0.02 260);
|
||||||
--input: oklch(0.9 0 0);
|
--input: oklch(0.9 0.02 260);
|
||||||
--ring: oklch(0.45 0.25 290);
|
--ring: oklch(0.5 0.2 290);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -98,6 +101,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shadow-md {
|
||||||
|
box-shadow:
|
||||||
|
0 4px 6px -1px rgb(0 0 0 / 0.05),
|
||||||
|
0 2px 4px -2px rgb(0 0 0 / 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .shadow-2xl {
|
||||||
|
/* Тонкое свечение для темной темы, которое не режет глаз */
|
||||||
|
box-shadow:
|
||||||
|
0 25px 50px -12px rgb(0 0 0 / 0.5),
|
||||||
|
0 0 20px -5px oklch(var(--primary) / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.text-neon-cyan {
|
.text-neon-cyan {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
@@ -119,12 +135,12 @@
|
|||||||
|
|
||||||
.light .text-neon-cyan {
|
.light .text-neon-cyan {
|
||||||
color: #0284c7;
|
color: #0284c7;
|
||||||
text-shadow: 0 0 5px rgba(0, 240, 255, 0.4);
|
text-shadow: 0 0 2px rgba(2, 132, 199, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .text-neon-purple {
|
.light .text-neon-purple {
|
||||||
color: #6d28d9;
|
color: #6d28d9;
|
||||||
text-shadow: 0 0 5px rgba(139, 61, 255, 0.4);
|
text-shadow: 0 0 2px rgba(109, 40, 217, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-neon-flicker {
|
.animate-neon-flicker {
|
||||||
@@ -148,3 +164,16 @@
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes scan {
|
||||||
|
from {
|
||||||
|
left: -100%;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-scan {
|
||||||
|
animation: scan 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
@@ -11,8 +9,8 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowRight, Loader2 } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { pb, getPbImage, type PBPost } from "@/lib/pb"; // Импортируем тип отсюда
|
import { pb, getPbImage, type PBPost } from "@/lib/pb";
|
||||||
import { Dictionary } from "@/lib/IDict";
|
import { Dictionary } from "@/lib/IDict";
|
||||||
|
|
||||||
interface BlogPreviewProps {
|
interface BlogPreviewProps {
|
||||||
@@ -20,43 +18,25 @@ interface BlogPreviewProps {
|
|||||||
dict: Dictionary["blog"];
|
dict: Dictionary["blog"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BlogPreview({ lang, dict }: BlogPreviewProps) {
|
// Теперь это асинхронный серверный компонент
|
||||||
const [posts, setPosts] = useState<PBPost[]>([]);
|
export async function BlogPreview({ lang, dict }: BlogPreviewProps) {
|
||||||
const [loading, setLoading] = useState(true);
|
let posts: PBPost[] = [];
|
||||||
|
|
||||||
useEffect(() => {
|
try {
|
||||||
if (!lang) return;
|
// В Next.js App Router лучше использовать нативный fetch для контроля кэша,
|
||||||
|
// либо передавать кастомный fetch в PB SDK. Здесь используем ISR на 3600 секунд (1 час).
|
||||||
async function fetchPosts() {
|
posts = await pb.collection(`posts_${lang}`).getFullList<PBPost>({
|
||||||
try {
|
filter: "published = true",
|
||||||
setLoading(true);
|
sort: "-created",
|
||||||
// Тянем из нужной языковой коллекции (posts_ru или posts_en)
|
perPage: 3,
|
||||||
const result = await pb
|
fetch: (url, config) =>
|
||||||
.collection(`posts_${lang}`)
|
fetch(url, { ...config, next: { revalidate: 3600 } }),
|
||||||
.getList<PBPost>(1, 3, {
|
});
|
||||||
filter: "published = true",
|
} catch (error) {
|
||||||
sort: "-created",
|
console.error("Ошибка загрузки логов:", error);
|
||||||
requestKey: null,
|
return null; // Тихо фейлимся без падения страницы
|
||||||
});
|
|
||||||
setPosts(result.items);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Ошибка загрузки логов:", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchPosts();
|
|
||||||
}, [lang]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center py-20 text-primary opacity-50">
|
|
||||||
<Loader2 className="w-8 h-8 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если записей нет, показываем либо ничего, либо сообщение об ошибке из словаря
|
|
||||||
if (posts.length === 0) return null;
|
if (posts.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -94,11 +74,15 @@ export function BlogPreview({ lang, dict }: BlogPreviewProps) {
|
|||||||
className="group overflow-hidden border-border/50 bg-card/30 hover:border-primary/50 transition-all duration-300 flex flex-col h-full"
|
className="group overflow-hidden border-border/50 bg-card/30 hover:border-primary/50 transition-all duration-300 flex flex-col h-full"
|
||||||
>
|
>
|
||||||
{post.image && (
|
{post.image && (
|
||||||
<div className="relative w-full h-48 overflow-hidden border-b border-border/50">
|
<div className="relative w-full h-48 overflow-hidden border-b border-border/50 bg-muted">
|
||||||
<img
|
// Внутри .map в BlogPreview.tsx:
|
||||||
|
<Image
|
||||||
src={getPbImage(post, post.image)}
|
src={getPbImage(post, post.image)}
|
||||||
alt={post.title}
|
alt={post.title}
|
||||||
className="object-cover w-full h-full group-hover:scale-105 transition-transform duration-500"
|
fill
|
||||||
|
sizes="(max-width: 768px) 100vw, 33vw"
|
||||||
|
priority={false}
|
||||||
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-background to-transparent opacity-80" />
|
<div className="absolute inset-0 bg-gradient-to-t from-background to-transparent opacity-80" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+22
-42
@@ -1,13 +1,10 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { Loader2, HelpCircle } from "lucide-react";
|
import { HelpCircle } from "lucide-react";
|
||||||
import { Dictionary } from "@/lib/IDict";
|
import { Dictionary } from "@/lib/IDict";
|
||||||
import { pb } from "@/lib/pb";
|
import { pb } from "@/lib/pb";
|
||||||
|
|
||||||
@@ -19,34 +16,29 @@ interface PBFaq {
|
|||||||
order: number;
|
order: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FAQ({ dict, lang }: { dict: Dictionary["faq"]; lang: string }) {
|
// Теперь это Server Component
|
||||||
const [faqs, setFaqs] = useState<PBFaq[]>([]);
|
export async function FAQ({
|
||||||
const [loading, setLoading] = useState(true);
|
dict,
|
||||||
|
lang,
|
||||||
|
}: {
|
||||||
|
dict: Dictionary["faq"];
|
||||||
|
lang: string;
|
||||||
|
}) {
|
||||||
|
let faqs: PBFaq[] = [];
|
||||||
|
|
||||||
useEffect(() => {
|
try {
|
||||||
async function fetchFaqs() {
|
faqs = await pb.collection(`faqs_${lang}`).getFullList<PBFaq>({
|
||||||
try {
|
sort: "order",
|
||||||
setLoading(true);
|
fetch: (url, config) =>
|
||||||
const records = await pb.collection(`faqs_${lang}`).getFullList<PBFaq>({
|
fetch(url, { ...config, next: { revalidate: 86400 } }), // Кэшируем на сутки
|
||||||
sort: "order",
|
});
|
||||||
requestKey: null,
|
} catch (error) {
|
||||||
});
|
console.error("Ошибка загрузки FAQ:", error);
|
||||||
setFaqs(records);
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("Ошибка загрузки FAQ:", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchFaqs();
|
|
||||||
}, [lang]);
|
|
||||||
|
|
||||||
// Функция для красивого рендеринга текста с поддержкой переносов и буллитов
|
|
||||||
const formatAnswer = (text: string) => {
|
const formatAnswer = (text: string) => {
|
||||||
return text.split("\n").map((line, i) => {
|
return text.split("\n").map((line, i) => {
|
||||||
const trimmedLine = line.trim();
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
// Если строка начинается с буллита
|
|
||||||
if (trimmedLine.startsWith("•")) {
|
if (trimmedLine.startsWith("•")) {
|
||||||
return (
|
return (
|
||||||
<div key={i} className="flex gap-3 mb-2 pl-2 group">
|
<div key={i} className="flex gap-3 mb-2 pl-2 group">
|
||||||
@@ -59,11 +51,7 @@ export function FAQ({ dict, lang }: { dict: Dictionary["faq"]; lang: string }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если строка пустая — это отступ между абзацами
|
|
||||||
if (!trimmedLine) return <div key={i} className="h-4" />;
|
if (!trimmedLine) return <div key={i} className="h-4" />;
|
||||||
|
|
||||||
// Обычный параграф
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
key={i}
|
key={i}
|
||||||
@@ -78,10 +66,9 @@ export function FAQ({ dict, lang }: { dict: Dictionary["faq"]; lang: string }) {
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id="faq"
|
id="faq"
|
||||||
className="py-32 px-4 container mx-auto max-w-4xl relative overflow-hidden"
|
className="py-32 px-4 container mx-auto max-w-4xl relative"
|
||||||
>
|
>
|
||||||
{/* Мягкий фон */}
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-primary/10 dark:from-primary/20 via-transparent to-transparent blur-[100px] pointer-events-none -z-10" />
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[100vw] h-[500px] bg-primary/5 blur-[120px] rounded-full pointer-events-none -z-10" />
|
|
||||||
|
|
||||||
<div className="flex flex-col items-center mb-16">
|
<div className="flex flex-col items-center mb-16">
|
||||||
<div className="bg-primary/10 p-3 rounded-2xl mb-6 border border-primary/20">
|
<div className="bg-primary/10 p-3 rounded-2xl mb-6 border border-primary/20">
|
||||||
@@ -92,14 +79,7 @@ export function FAQ({ dict, lang }: { dict: Dictionary["faq"]; lang: string }) {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{faqs.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-20 text-primary opacity-50">
|
|
||||||
<Loader2 className="w-10 h-10 animate-spin mb-4" />
|
|
||||||
<span className="text-xs tracking-[0.3em] uppercase font-mono">
|
|
||||||
Synchronizing_Knowledge...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : faqs.length === 0 ? (
|
|
||||||
<div className="text-center py-20 border border-dashed border-border/50 rounded-[2.5rem] bg-card/20">
|
<div className="text-center py-20 border border-dashed border-border/50 rounded-[2.5rem] bg-card/20">
|
||||||
<p className="text-muted-foreground font-mono text-sm uppercase tracking-widest">
|
<p className="text-muted-foreground font-mono text-sm uppercase tracking-widest">
|
||||||
0 protocols found in cache.
|
0 protocols found in cache.
|
||||||
|
|||||||
@@ -3,75 +3,94 @@
|
|||||||
import { EyeOff, Lock, Globe, Gauge, Cpu } from "lucide-react";
|
import { EyeOff, Lock, Globe, Gauge, Cpu } from "lucide-react";
|
||||||
import { motion, Variants } from "framer-motion";
|
import { motion, Variants } from "framer-motion";
|
||||||
import { Dictionary } from "@/lib/IDict";
|
import { Dictionary } from "@/lib/IDict";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function Features({ dict }: { dict: Dictionary["features"] }) {
|
export function Features({ dict }: { dict: Dictionary["features"] }) {
|
||||||
// Универсальные классы для переиспользования и чистоты кода
|
// Класс для карточек: в светлой теме — плотный белый с тенью, в темной — стекло
|
||||||
const verticalCardClass =
|
const cardClass = cn(
|
||||||
"min-h-[320px] md:min-h-[360px] group bg-card/40 backdrop-blur-xl border border-border/50 rounded-[2.5rem] p-8 md:p-10 flex flex-col hover:border-primary/50 transition-all duration-500 hover:shadow-[0_0_40px_rgba(139,61,255,0.15)] relative overflow-hidden";
|
"min-h-[320px] md:min-h-[360px] group relative flex flex-col p-8 md:p-10 rounded-[2.5rem] overflow-hidden transition-all duration-500",
|
||||||
|
"bg-white border-border shadow-sm hover:shadow-md", // Light
|
||||||
|
"dark:bg-card/40 dark:backdrop-blur-xl dark:border-border/50 dark:hover:border-primary/50 dark:hover:shadow-[0_0_40px_rgba(139,61,255,0.15)]", // Dark
|
||||||
|
);
|
||||||
|
|
||||||
const iconContainerClass =
|
// Класс для контейнера иконки: в светлой теме — "плитка", в темной — "ядро"
|
||||||
"bg-secondary/10 w-14 h-14 rounded-[1.2rem] flex items-center justify-center border border-secondary/20 shadow-[0_0_15px_rgba(0,229,242,0.15)] relative z-10 transition-transform duration-500 group-hover:scale-110";
|
const iconContainerClass = cn(
|
||||||
|
"w-14 h-14 rounded-[1.2rem] flex items-center justify-center relative z-10 transition-transform duration-500 group-hover:scale-110",
|
||||||
|
"bg-slate-50 border-border shadow-inner", // Light
|
||||||
|
"dark:bg-secondary/10 dark:border-secondary/20 dark:shadow-[0_0_15px_rgba(0,229,242,0.15)]", // Dark
|
||||||
|
);
|
||||||
|
|
||||||
|
// Стиль для самой иконки (SVG)
|
||||||
|
const iconStyle =
|
||||||
|
"w-7 h-7 text-secondary dark:drop-shadow-[0_0_8px_rgba(0,229,242,0.6)]";
|
||||||
|
|
||||||
|
// Декоративные фоновые элементы (большие иконки)
|
||||||
|
const bgIconClass =
|
||||||
|
"absolute -bottom-10 -right-10 w-64 h-64 transition-all duration-700 pointer-events-none group-hover:scale-110 group-hover:-rotate-12";
|
||||||
|
const bgIconTheme =
|
||||||
|
"text-slate-100 dark:text-primary dark:opacity-[0.03] dark:group-hover:opacity-[0.08]";
|
||||||
|
|
||||||
const bentoConfig = [
|
const bentoConfig = [
|
||||||
{
|
{
|
||||||
// 1: DPI Evasion
|
className: `md:col-span-2 ${cardClass}`,
|
||||||
className: `md:col-span-2 ${verticalCardClass}`,
|
|
||||||
icon: (
|
icon: (
|
||||||
<div className={iconContainerClass}>
|
<div className={iconContainerClass}>
|
||||||
<EyeOff className="w-7 h-7 text-secondary drop-shadow-[0_0_10px_rgba(0,229,242,0.8)]" />
|
<EyeOff className={iconStyle} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
bgElement: <EyeOff className={cn(bgIconClass, bgIconTheme)} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: `md:col-span-1 ${cardClass}`,
|
||||||
|
icon: (
|
||||||
|
<div className={iconContainerClass}>
|
||||||
|
<Lock className={iconStyle} />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
bgElement: (
|
bgElement: (
|
||||||
<EyeOff className="absolute -bottom-10 -right-10 w-64 h-64 text-primary opacity-[0.03] group-hover:opacity-[0.08] group-hover:scale-110 group-hover:-rotate-12 transition-all duration-700 pointer-events-none" />
|
<div className="absolute top-0 right-0 w-40 h-40 bg-primary/5 dark:bg-primary/10 blur-[70px] opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// 2: Cryptography
|
className: `md:col-span-1 ${cardClass}`,
|
||||||
className: `md:col-span-1 ${verticalCardClass}`,
|
|
||||||
icon: (
|
icon: (
|
||||||
<div className={iconContainerClass}>
|
<div className={iconContainerClass}>
|
||||||
<Lock className="w-7 h-7 text-secondary drop-shadow-[0_0_10px_rgba(0,229,242,0.8)]" />
|
<Gauge className={iconStyle} />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
bgElement: (
|
bgElement: (
|
||||||
<div className="absolute top-0 right-0 w-40 h-40 bg-primary/10 blur-[70px] opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
<Gauge
|
||||||
|
className={cn(
|
||||||
|
bgIconClass,
|
||||||
|
bgIconTheme,
|
||||||
|
"w-48 h-48 top-10 -right-8 bottom-auto",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// 3: Adaptive Backpressure
|
className: `md:col-span-2 ${cardClass}`,
|
||||||
className: `md:col-span-1 ${verticalCardClass}`,
|
|
||||||
icon: (
|
icon: (
|
||||||
<div className={iconContainerClass}>
|
<div className={iconContainerClass}>
|
||||||
<Gauge className="w-7 h-7 text-secondary drop-shadow-[0_0_10px_rgba(0,229,242,0.8)]" />
|
<Cpu className={iconStyle} />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
bgElement: (
|
bgElement: (
|
||||||
<Gauge className="absolute top-10 -right-8 w-48 h-48 text-primary opacity-[0.02] group-hover:opacity-[0.08] transition-all duration-700 pointer-events-none" />
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_right,_var(--tw-gradient-stops))] from-primary/5 dark:from-primary/10 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// 4: Multi-Platform Core
|
className: cn(
|
||||||
className: `md:col-span-2 ${verticalCardClass}`,
|
cardClass,
|
||||||
|
"md:col-span-3 flex-col md:flex-row items-start md:items-center gap-8 md:gap-12",
|
||||||
|
),
|
||||||
icon: (
|
icon: (
|
||||||
<div className={iconContainerClass}>
|
<div className="bg-slate-50 dark:bg-secondary/10 w-16 h-16 md:w-20 md:h-20 rounded-[1.5rem] md:rounded-[1.8rem] shrink-0 flex items-center justify-center border border-border dark:border-secondary/20 shadow-inner relative z-10 transition-transform duration-500 group-hover:scale-105">
|
||||||
<Cpu className="w-7 h-7 text-secondary drop-shadow-[0_0_10px_rgba(0,229,242,0.8)]" />
|
<Globe className="w-8 h-8 md:w-10 md:h-10 text-secondary dark:drop-shadow-[0_0_10px_rgba(0,229,242,0.8)]" />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
bgElement: (
|
bgElement: (
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_right,_var(--tw-gradient-stops))] from-primary/10 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_left,_var(--tw-gradient-stops))] from-primary/5 dark:from-primary/10 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 5: Global Network (Горизонтальная карточка)
|
|
||||||
className:
|
|
||||||
"md:col-span-3 bg-card/40 backdrop-blur-xl border border-border/50 rounded-[2.5rem] p-8 md:p-10 flex flex-col md:flex-row items-start md:items-center gap-8 md:gap-12 group hover:border-primary/50 transition-all duration-500 hover:shadow-[0_0_40px_rgba(139,61,255,0.15)] relative overflow-hidden",
|
|
||||||
icon: (
|
|
||||||
<div className="bg-secondary/10 w-16 h-16 md:w-20 md:h-20 rounded-[1.5rem] md:rounded-[1.8rem] shrink-0 flex items-center justify-center border border-secondary/20 shadow-[0_0_20px_rgba(0,229,242,0.15)] relative z-10 transition-transform duration-500 group-hover:scale-105">
|
|
||||||
<Globe className="w-8 h-8 md:w-10 md:h-10 text-secondary drop-shadow-[0_0_10px_rgba(0,229,242,0.8)]" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
bgElement: (
|
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_left,_var(--tw-gradient-stops))] from-primary/10 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -80,25 +99,16 @@ export function Features({ dict }: { dict: Dictionary["features"] }) {
|
|||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
show: {
|
show: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
transition: { staggerChildren: 0.1 },
|
||||||
staggerChildren: 0.15,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemVariants: Variants = {
|
const itemVariants: Variants = {
|
||||||
hidden: {
|
hidden: { opacity: 0, y: 20 },
|
||||||
opacity: 0,
|
|
||||||
y: 40,
|
|
||||||
},
|
|
||||||
show: {
|
show: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
transition: {
|
transition: { type: "spring", stiffness: 260, damping: 20 },
|
||||||
type: "spring",
|
|
||||||
stiffness: 260,
|
|
||||||
damping: 20,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,11 +120,11 @@ export function Features({ dict }: { dict: Dictionary["features"] }) {
|
|||||||
className="py-24 px-4 w-full mx-auto relative overflow-hidden"
|
className="py-24 px-4 w-full mx-auto relative overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="container max-w-6xl mx-auto relative z-10">
|
<div className="container max-w-6xl mx-auto relative z-10">
|
||||||
{/* Центральное мягкое свечение секции */}
|
{/* Фоновое свечение секции: приглушено в светлой теме */}
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[100vw] md:w-[600px] h-[600px] bg-primary/5 blur-[100px] md:blur-[120px] rounded-full pointer-events-none -z-10" />
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[100vw] md:w-[600px] h-[600px] bg-primary/5 blur-[100px] rounded-full pointer-events-none -z-10 opacity-50 dark:opacity-100" />
|
||||||
|
|
||||||
<div className="mb-16 md:mb-20 text-center">
|
<div className="mb-16 md:mb-20 text-center">
|
||||||
<h2 className="text-4xl md:text-5xl font-extrabold tracking-tight text-foreground">
|
<h2 className="text-4xl md:text-5xl font-black tracking-tight text-foreground uppercase">
|
||||||
{dict.title}
|
{dict.title}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,7 +138,6 @@ export function Features({ dict }: { dict: Dictionary["features"] }) {
|
|||||||
>
|
>
|
||||||
{bentoConfig.map((config, idx) => {
|
{bentoConfig.map((config, idx) => {
|
||||||
const feature = dict.items[idx];
|
const feature = dict.items[idx];
|
||||||
// Проверяем, это последняя горизонтальная карточка или нет
|
|
||||||
const isHorizontal = idx === 4;
|
const isHorizontal = idx === 4;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -140,13 +149,13 @@ export function Features({ dict }: { dict: Dictionary["features"] }) {
|
|||||||
{config.bgElement}
|
{config.bgElement}
|
||||||
{config.icon}
|
{config.icon}
|
||||||
|
|
||||||
{/* mt-auto прижимает текст к низу, pt-8 дает гарантированный отступ от иконки */}
|
|
||||||
<div
|
<div
|
||||||
className={`relative z-10 ${
|
className={cn(
|
||||||
isHorizontal ? "" : "mt-auto pt-8"
|
"relative z-10",
|
||||||
}`}
|
isHorizontal ? "" : "mt-auto pt-8",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<h3 className="text-xl md:text-2xl font-bold text-foreground mb-3 tracking-tight">
|
<h3 className="text-xl md:text-2xl font-bold text-foreground mb-3 tracking-tight uppercase">
|
||||||
{feature.title}
|
{feature.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground text-sm md:text-base leading-relaxed max-w-lg">
|
<p className="text-muted-foreground text-sm md:text-base leading-relaxed max-w-lg">
|
||||||
|
|||||||
+294
-61
@@ -1,15 +1,148 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { JetBrains_Mono } from "next/font/google";
|
import { JetBrains_Mono } from "next/font/google";
|
||||||
import { GithubLogoIcon, TwitterLogoIcon } from "@phosphor-icons/react";
|
|
||||||
import { Send } from "lucide-react";
|
|
||||||
import { Dictionary } from "@/lib/IDict";
|
import { Dictionary } from "@/lib/IDict";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const jetbrainsMono = JetBrains_Mono({
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
subsets: ["latin", "cyrillic"],
|
subsets: ["latin", "cyrillic"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Кастомные легкие SVG иконки
|
||||||
|
const Icons = {
|
||||||
|
Telegram: () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="size-6"
|
||||||
|
>
|
||||||
|
<path d="m22 2-7 20-4-9-9-4Z" />
|
||||||
|
<path d="M22 2 11 13" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
Youtube: () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="size-6"
|
||||||
|
>
|
||||||
|
<path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.42a2.78 2.78 0 0 0-1.94 2C1 8.11 1 12 1 12s0 3.89.46 5.58a2.78 2.78 0 0 0 1.94 2c1.72.42 8.6.42 8.6.42s6.88 0 8.6-.42a2.78 2.78 0 0 0 1.94-2C23 15.89 23 12 23 12s0-3.89-.46-5.58z" />
|
||||||
|
<polygon
|
||||||
|
points="9.75 15.02 15.5 12 9.75 8.98 9.75 15.02"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
Discord: () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="size-6"
|
||||||
|
>
|
||||||
|
<path d="M19.73 5.42a19.06 19.06 0 0 0-4.73-1.45 14.1 14.1 0 0 0-.6 1.2A17.65 17.65 0 0 0 9.6 5.17a14.1 14.1 0 0 0-.6-1.2 19.06 19.06 0 0 0-4.73 1.45 19.64 19.64 0 0 0-3.37 13.5 19.34 19.34 0 0 0 5.86 3 13.9 13.9 0 0 0 1.26-2.03 13.2 13.2 0 0 1-2-.95c.16-.12.3-.25.45-.38a13.3 13.3 0 0 0 11.08 0c.15.13.3.26.45.38a13.2 13.2 0 0 1-2 .95 13.9 13.9 0 0 0 1.26 2.03 19.34 19.34 0 0 0 5.86-3 19.64 19.64 0 0 0-3.37-13.5ZM8.5 15.2c-1.07 0-1.95-1-1.95-2.22s.87-2.22 1.95-2.22c1.08 0 1.96 1 1.95 2.22 0 1.21-.88 2.22-1.95 2.22Zm7 0c-1.07 0-1.95-1-1.95-2.22s.87-2.22 1.95-2.22c1.08 0 1.96 1 1.95 2.22 0 1.21-.88 2.22-1.95 2.22Z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
X: () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="size-6"
|
||||||
|
>
|
||||||
|
<path d="M4 4l11.733 16H20L8.267 4z" />
|
||||||
|
<path d="M4 20l6.768-6.768m2.464-2.464L20 4" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
Instagram: () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="size-6"
|
||||||
|
>
|
||||||
|
<rect width="20" height="20" x="2" y="2" rx="5" ry="5" />
|
||||||
|
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" />
|
||||||
|
<line x1="17.5" x2="17.51" y1="6.5" y2="6.5" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
Habr: () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="size-6"
|
||||||
|
>
|
||||||
|
<path d="M5 5v14M19 5v14M5 12h14" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const FlickeringSocialIcon = ({ social }: { social: any }) => {
|
||||||
|
const [opacity, setOpacity] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
|
||||||
|
const flicker = () => {
|
||||||
|
if (Math.random() > 0.6) {
|
||||||
|
setOpacity(Math.random() * 0.5 + 0.1);
|
||||||
|
setTimeout(() => setOpacity(1), Math.random() * 120 + 30);
|
||||||
|
}
|
||||||
|
timeoutId = setTimeout(flicker, Math.random() * 6000 + 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
timeoutId = setTimeout(flicker, Math.random() * 3000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={social.href}
|
||||||
|
title={social.name}
|
||||||
|
// В светлой теме добавляем жесткую рамку при ховере вместо свечения
|
||||||
|
className={cn(
|
||||||
|
"p-2 rounded-lg transition-all duration-300 relative",
|
||||||
|
"dark:bg-transparent dark:border-none", // Темная тема: без фона
|
||||||
|
"bg-muted/50 border border-transparent hover:border-border hover:shadow-inner", // Светлая тема: кнопки-клавиши
|
||||||
|
social.color,
|
||||||
|
social.glow,
|
||||||
|
"hover:!opacity-100 hover:scale-110 active:scale-95",
|
||||||
|
)}
|
||||||
|
style={{ opacity, transition: "opacity 0.05s ease-in-out" }}
|
||||||
|
>
|
||||||
|
{/* Фоновый отсвет только для темной темы */}
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 blur-[6px] opacity-20 hidden dark:block ${social.bgGlow}`}
|
||||||
|
/>
|
||||||
|
<social.icon />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function Footer({
|
export function Footer({
|
||||||
dict,
|
dict,
|
||||||
lang,
|
lang,
|
||||||
@@ -17,14 +150,13 @@ export function Footer({
|
|||||||
dict: Dictionary["footer"];
|
dict: Dictionary["footer"];
|
||||||
lang: string;
|
lang: string;
|
||||||
}) {
|
}) {
|
||||||
// Динамическая карта сайта с прописанными маршрутами
|
|
||||||
const sitemap = [
|
const sitemap = [
|
||||||
{
|
{
|
||||||
title: dict.sections.product,
|
title: dict.sections.product,
|
||||||
links: [
|
links: [
|
||||||
{ name: dict.links.features, href: `/${lang}#features` },
|
{ name: dict.links.features, href: `/${lang}#features` },
|
||||||
{ name: dict.links.pricing, href: `/${lang}#pricing` },
|
{ name: dict.links.pricing, href: `/${lang}#pricing` },
|
||||||
{ name: dict.links.protocol, href: `/${lang}/docs` }, // Ведет в документацию
|
{ name: dict.links.protocol, href: `/${lang}/docs` },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -37,81 +169,182 @@ export function Footer({
|
|||||||
{
|
{
|
||||||
title: dict.sections.legal,
|
title: dict.sections.legal,
|
||||||
links: [
|
links: [
|
||||||
{ name: dict.links.privacy, href: `/${lang}/docs` }, // Ведет в документацию (Legal)
|
{ name: dict.links.privacy, href: `/${lang}/docs` },
|
||||||
{ name: dict.links.terms, href: `/${lang}/docs` }, // Ведет в документацию (Legal)
|
{ name: dict.links.terms, href: `/${lang}/docs` },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const socialLinks = [
|
||||||
|
{
|
||||||
|
name: "Telegram",
|
||||||
|
href: "#",
|
||||||
|
icon: Icons.Telegram,
|
||||||
|
color: "text-[#0088cc]", // В светлой теме цвет остается плотным
|
||||||
|
glow: "dark:drop-shadow-[0_0_8px_rgba(0,136,204,0.8)]", // Ограничиваем свечение только темной темой
|
||||||
|
bgGlow: "bg-[#0088cc]",
|
||||||
|
},
|
||||||
|
// ... аналогично для YouTube, Discord, X, Instagram, Habr (добавь префикс dark: к каждому glow)
|
||||||
|
{
|
||||||
|
name: "YouTube",
|
||||||
|
href: "#",
|
||||||
|
icon: Icons.Youtube,
|
||||||
|
color: "text-[#ff0000]",
|
||||||
|
glow: "dark:drop-shadow-[0_0_8px_rgba(255,0,0,0.8)]",
|
||||||
|
bgGlow: "bg-[#ff0000]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Discord",
|
||||||
|
href: "#",
|
||||||
|
icon: Icons.Discord,
|
||||||
|
color: "text-[#5865F2]",
|
||||||
|
glow: "dark:drop-shadow-[0_0_8px_rgba(88,101,242,0.8)]",
|
||||||
|
bgGlow: "bg-[#5865F2]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X",
|
||||||
|
href: "#",
|
||||||
|
icon: Icons.X,
|
||||||
|
color: "text-foreground",
|
||||||
|
glow: "dark:drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]",
|
||||||
|
bgGlow: "bg-white",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Instagram",
|
||||||
|
href: "#",
|
||||||
|
icon: Icons.Instagram,
|
||||||
|
color: "text-[#E1306C]",
|
||||||
|
glow: "dark:drop-shadow-[0_0_8px_rgba(225,48,108,0.8)]",
|
||||||
|
bgGlow: "bg-[#E1306C]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Habr",
|
||||||
|
href: "#",
|
||||||
|
icon: Icons.Habr,
|
||||||
|
color: "text-[#65A3BE]",
|
||||||
|
glow: "dark:drop-shadow-[0_0_8px_rgba(101,163,190,0.8)]",
|
||||||
|
bgGlow: "bg-[#65A3BE]",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer
|
||||||
className={`mt-20 border-t border-border/50 bg-card/30 backdrop-blur-xl ${jetbrainsMono.className}`}
|
className={`mt-20 border-t border-border/20 bg-background pt-16 pb-12 ${jetbrainsMono.className}`}
|
||||||
>
|
>
|
||||||
<div className="container mx-auto px-6 py-12">
|
<div className="container mx-auto px-6 max-w-6xl">
|
||||||
<div className="grid grid-cols-1 gap-12 md:grid-cols-4 lg:gap-16">
|
<div className="flex flex-col lg:flex-row justify-between items-start gap-16 lg:gap-8">
|
||||||
{/* Блок бренда */}
|
<div className="flex flex-col gap-3 max-w-xs">
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<Link
|
<Link
|
||||||
href={`/${lang}`}
|
href={`/${lang}`}
|
||||||
className="text-2xl font-bold tracking-tighter flex items-center gap-2 w-fit"
|
className="flex items-center gap-3 group hover:scale-105 transition-transform duration-300 w-fit mt-1"
|
||||||
>
|
>
|
||||||
<div className="w-3 h-3 bg-primary rounded-full shadow-[0_0_10px_rgba(0,240,255,0.8)] animate-pulse" />
|
{/* Убрали -rotate-2, текст теперь стоит ровно */}
|
||||||
<span className="text-neon-cyan">{dict.brand}</span>
|
<div className="text-2xl sm:text-3xl tracking-widest flex items-center select-none font-bold">
|
||||||
|
<span
|
||||||
|
className="text-transparent animate-neon-flicker transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
WebkitTextStroke: "1px #00E5F2",
|
||||||
|
filter: "drop-shadow(0 0 5px rgba(0,229,242,0.6))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Net
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-transparent transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
WebkitTextStroke: "1px #00E5F2",
|
||||||
|
filter: "drop-shadow(0 0 5px rgba(0,229,242,0.6))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
runner
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="ml-2 text-transparent opacity-90 group-hover:opacity-100 transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
WebkitTextStroke: "1px #8B3DFF",
|
||||||
|
filter: "drop-shadow(0 0 5px rgba(139,61,255,0.6))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
VPN
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<p className="text-muted-foreground text-sm leading-relaxed max-w-[200px]">
|
|
||||||
|
<div className="text-xs font-bold text-primary/80 tracking-widest uppercase mt-1 drop-shadow-[0_0_5px_rgba(139,61,255,0.5)]">
|
||||||
|
PROTOCOL // ENCRYPTED ACCESS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground text-sm leading-relaxed mt-2 max-w-50">
|
||||||
{dict.tagline}
|
{dict.tagline}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-4 mt-2">
|
|
||||||
<Link
|
<div className="flex flex-wrap gap-5 mt-4 items-center">
|
||||||
href="#"
|
{socialLinks.map((social) => (
|
||||||
className="text-muted-foreground hover:text-primary transition-colors"
|
<FlickeringSocialIcon key={social.name} social={social} />
|
||||||
>
|
))}
|
||||||
<GithubLogoIcon size={20} />
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="#"
|
|
||||||
className="text-muted-foreground hover:text-secondary transition-colors"
|
|
||||||
>
|
|
||||||
<TwitterLogoIcon size={20} />
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="#"
|
|
||||||
className="text-muted-foreground hover:text-primary transition-colors"
|
|
||||||
>
|
|
||||||
<Send size={20} />
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Динамическая карта сайта */}
|
<div className="flex flex-wrap md:flex-nowrap gap-12 lg:gap-16">
|
||||||
{sitemap.map((section, idx) => (
|
{sitemap.map((section, idx) => (
|
||||||
<div key={idx} className="flex flex-col gap-4">
|
<div key={idx} className="flex flex-col gap-5">
|
||||||
<h4 className="text-foreground font-bold text-sm uppercase tracking-widest">
|
<h4 className="text-foreground font-black text-[13px] uppercase tracking-widest">
|
||||||
{section.title}
|
{section.title}
|
||||||
</h4>
|
</h4>
|
||||||
<ul className="flex flex-col gap-2">
|
<ul className="flex flex-col gap-3">
|
||||||
{section.links.map((link, linkIdx) => (
|
{section.links.map((link, linkIdx) => (
|
||||||
<li key={linkIdx}>
|
<li key={linkIdx}>
|
||||||
<Link
|
<Link
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className="text-muted-foreground hover:text-primary text-sm transition-colors duration-200"
|
className="text-muted-foreground hover:text-primary text-sm transition-colors duration-200"
|
||||||
>
|
>
|
||||||
{link.name}
|
{link.name}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Нижняя панель */}
|
<div className="mt-20 border border-border/30 bg-card/20 rounded-xl p-6 md:px-10 md:py-8 flex flex-col md:flex-row justify-between items-start md:items-center gap-8 shadow-2xl relative overflow-hidden">
|
||||||
<div className="mt-16 pt-8 border-t border-border/20 flex flex-col md:flex-row justify-between items-center gap-4">
|
<div className="absolute top-0 left-1/4 w-1/2 h-full bg-primary/10 blur-3xl -z-10 pointer-events-none" />
|
||||||
<p className="text-xs text-muted-foreground/60">{dict.rights}</p>
|
|
||||||
<div className="flex gap-8 text-[10px] uppercase tracking-[0.2em] text-muted-foreground/40 font-bold">
|
<div className="flex flex-col gap-1">
|
||||||
<span>Uplink: Active</span>
|
<div className="flex items-center gap-2">
|
||||||
<span>Node: Node-Th-01</span>
|
<div className="w-1.5 h-1.5 rounded-full bg-primary shadow-[0_0_8px_rgba(139,61,255,0.8)] animate-pulse" />
|
||||||
<span>Latency: 24ms</span>
|
<span className="text-[10px] text-muted-foreground/60 uppercase tracking-widest font-bold">
|
||||||
|
Uplink Status
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-foreground tracking-tight mt-1">
|
||||||
|
464.6 MBPS
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-primary shadow-[0_0_8px_rgba(139,61,255,0.8)]" />
|
||||||
|
<span className="text-[10px] text-muted-foreground/60 uppercase tracking-widest font-bold">
|
||||||
|
Active Node
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-foreground tracking-tight mt-1">
|
||||||
|
TYO-SEC-05
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.8)]" />
|
||||||
|
<span className="text-[10px] text-muted-foreground/60 uppercase tracking-widest font-bold">
|
||||||
|
Latency (RTT)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-foreground tracking-tight mt-1">
|
||||||
|
16 MS
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { LanguageSwitcher } from "@/components/sections/lang/language-switcher";
|
import { LanguageSwitcher } from "@/components/sections/lang/language-switcher";
|
||||||
import { ThemeToggle } from "@/components/sections/theme/theme-toggle";
|
import { ThemeToggle } from "@/components/sections/theme/theme-toggle";
|
||||||
import { Dictionary } from "@/lib/IDict";
|
import { Dictionary } from "@/lib/IDict";
|
||||||
|
import { CyberButton } from "../ui/cyber-button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function Header({
|
export function Header({
|
||||||
dict,
|
dict,
|
||||||
@@ -14,27 +15,45 @@ export function Header({
|
|||||||
lang: string;
|
lang: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<header className="fixed top-6 left-1/2 -translate-x-1/2 w-[95%] max-w-5xl z-50 bg-card/60 dark:bg-card/40 backdrop-blur-2xl border border-border/50 rounded-full shadow-lg transition-all duration-500 hover:bg-card/80 dark:hover:bg-card/60">
|
<header className="fixed top-4 md:top-6 left-1/2 -translate-x-1/2 w-[95%] max-w-5xl z-50 bg-card/60 dark:bg-card/40 backdrop-blur-2xl border border-border/50 rounded-full shadow-lg transition-all duration-500 hover:bg-card/80 dark:hover:bg-card/60">
|
||||||
<div className="px-6 md:px-8 h-16 flex items-center justify-between">
|
<div className="px-4 md:px-8 h-14 md:h-16 flex items-center justify-between">
|
||||||
{/* ЛЕВЫЙ БЛОК: Неоновый Логотип */}
|
{/* ЛЕВЫЙ БЛОК: Адаптивный Логотип */}
|
||||||
<Link
|
<Link
|
||||||
href={`/${lang}`}
|
href={`/${lang}`}
|
||||||
className="flex items-center gap-3 group group-hover:scale-105 transition-transform duration-300"
|
className="flex items-center gap-2 md:gap-3 group hover:scale-105 transition-transform duration-300 w-fit"
|
||||||
>
|
>
|
||||||
<div className="text-2xl sm:text-3xl tracking-widest flex items-center -rotate-2 select-none font-bold">
|
<div className="text-lg sm:text-2xl md:text-3xl tracking-wide md:tracking-widest flex items-center select-none font-bold uppercase">
|
||||||
<span className="transition-all duration-300 text-neon-cyan animate-neon-flicker">
|
<span
|
||||||
|
className="text-transparent animate-neon-flicker transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
WebkitTextStroke: "1px #00E5F2",
|
||||||
|
filter: "drop-shadow(0 0 5px rgba(0,229,242,0.6))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
Net
|
Net
|
||||||
</span>
|
</span>
|
||||||
<span className="transition-all duration-300 text-neon-cyan">
|
<span
|
||||||
|
className="text-transparent transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
WebkitTextStroke: "1px #00E5F2",
|
||||||
|
filter: "drop-shadow(0 0 5px rgba(0,229,242,0.6))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
runner
|
runner
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-2 opacity-90 group-hover:opacity-100 transition-all duration-300 text-neon-purple">
|
<span
|
||||||
|
className="ml-1.5 md:ml-2 text-transparent opacity-90 group-hover:opacity-100 transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
WebkitTextStroke: "1px #8B3DFF",
|
||||||
|
filter: "drop-shadow(0 0 5px rgba(139,61,255,0.6))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
VPN
|
VPN
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* ЦЕНТРАЛЬНЫЙ БЛОК: Навигация */}
|
{/* ЦЕНТРАЛЬНЫЙ БЛОК: Навигация (скрыта на мобилках) */}
|
||||||
<nav className="hidden md:flex items-center gap-8 text-sm font-bold text-muted-foreground">
|
<nav className="hidden md:flex items-center gap-8 text-sm font-bold text-muted-foreground">
|
||||||
<Link
|
<Link
|
||||||
href={`/${lang}#features`}
|
href={`/${lang}#features`}
|
||||||
@@ -57,18 +76,19 @@ export function Header({
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* ПРАВЫЙ БЛОК: Контролы */}
|
{/* ПРАВЫЙ БЛОК: Контролы */}
|
||||||
<div className="flex items-center gap-2 sm:gap-4">
|
<div className="flex items-center gap-1.5 md:gap-4">
|
||||||
<div className="flex items-center gap-1 sm:gap-2">
|
<div className="flex items-center gap-1 md:gap-2">
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
{/* РОУТИНГ НА СТРАНИЦУ СКАЧИВАНИЯ */}
|
|
||||||
<Button
|
<CyberButton
|
||||||
asChild
|
href={`/${lang}/download`}
|
||||||
className="hidden sm:flex rounded-full px-6 py-5 text-sm font-bold transition-all duration-300 bg-primary text-primary-foreground shadow-md hover:shadow-[0_0_20px_rgba(0,240,255,0.4)] dark:shadow-[0_0_15px_rgba(112,0,255,0.3)] hover:-translate-y-0.5"
|
variant="primary"
|
||||||
|
className="hidden sm:block scale-90 md:scale-100 origin-right"
|
||||||
>
|
>
|
||||||
<Link href={`/${lang}/download`}>{dict.cta}</Link>
|
{dict.cta}
|
||||||
</Button>
|
</CyberButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Shield, EyeOff } from "lucide-react";
|
|||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { Dictionary } from "@/lib/IDict";
|
import { Dictionary } from "@/lib/IDict";
|
||||||
import Link from "next/link";
|
import { CyberButton } from "../ui/cyber-button";
|
||||||
|
|
||||||
const MatrixBackground = dynamic(() => import("./MatrixBackground"), {
|
const MatrixBackground = dynamic(() => import("./MatrixBackground"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -177,11 +177,11 @@ export function Hero({
|
|||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
||||||
<MatrixBackground isSecure={isVpnOn} theme={resolvedTheme} />
|
<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(ellipse_at_center,_transparent_0%,_var(--background)_80%)] -z-10" />
|
</div>
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_transparent_20%,_var(--background)_100%)] -z-10 opacity-70" />
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@@ -244,26 +244,17 @@ export function Hero({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center w-full sm:w-auto font-mono">
|
<div className="flex flex-col sm:flex-row gap-6 justify-center w-full sm:w-auto font-mono">
|
||||||
{/* ИНВЕРСИЯ КНОПОК */}
|
<CyberButton
|
||||||
<Button
|
href={`/${lang}/download`}
|
||||||
asChild
|
variant={isVpnOn ? "primary" : "secondary"}
|
||||||
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>
|
{dict.ctaPrimary}
|
||||||
</Button>
|
</CyberButton>
|
||||||
|
|
||||||
<Button
|
<CyberButton href={`/${lang}/nodes`} variant="secondary">
|
||||||
asChild
|
{dict.ctaSecondary}
|
||||||
variant="outline"
|
</CyberButton>
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -10,101 +10,123 @@ export default function MatrixBackground({
|
|||||||
theme?: string;
|
theme?: string;
|
||||||
}) {
|
}) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
// Используем Ref вместо State для позиции мыши, чтобы не перерисовывать компонент
|
||||||
|
const mousePos = useRef({ x: -1000, y: -1000 });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d", { alpha: false }); // Оптимизация производительности
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
let width = (canvas.width = window.innerWidth);
|
let width = (canvas.width = window.innerWidth);
|
||||||
let height = (canvas.height = window.innerHeight);
|
let height = (canvas.height = window.innerHeight);
|
||||||
|
|
||||||
const fontSize = 16;
|
const fontSize = 15;
|
||||||
const columns = Math.floor(width / fontSize);
|
const columns = Math.floor(width / fontSize);
|
||||||
const drops = Array(columns).fill(1);
|
const drops = Array(columns).fill(1);
|
||||||
|
|
||||||
const isDark = theme === "dark";
|
const chars = "01ヲアウエオカキケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン".split("");
|
||||||
|
|
||||||
const draw = () => {
|
const isDark = theme === "dark";
|
||||||
// Контрастный фон: в темной теме делаем глубокий черный
|
let animationFrameId: number;
|
||||||
|
let lastTime = 0;
|
||||||
|
const fps = 22;
|
||||||
|
const interval = 1000 / fps;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
mousePos.current = { x: e.clientX, y: e.clientY };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
width = canvas.width = window.innerWidth;
|
||||||
|
height = canvas.height = window.innerHeight;
|
||||||
|
// При ресайзе пересчитываем колонки без сброса анимации
|
||||||
|
const newCols = Math.floor(width / fontSize);
|
||||||
|
if (newCols > drops.length) {
|
||||||
|
for (let i = drops.length; i < newCols; i++) drops[i] = 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
const draw = (currentTime: number) => {
|
||||||
|
animationFrameId = requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
const deltaTime = currentTime - lastTime;
|
||||||
|
if (deltaTime < interval) return;
|
||||||
|
lastTime = currentTime - (deltaTime % interval);
|
||||||
|
|
||||||
|
// Заливка фона (создает шлейф)
|
||||||
|
// В светлой теме прозрачность шлейфа должна быть выше, чтобы буквы не исчезали мгновенно
|
||||||
ctx.fillStyle = isDark
|
ctx.fillStyle = isDark
|
||||||
? "rgba(0, 0, 0, 0.08)" // Чистый черный для контраста
|
? "rgba(11, 13, 23, 0.15)"
|
||||||
: "rgba(250, 250, 250, 0.05)";
|
: "rgba(248, 250, 252, 0.25)";
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
const gradient = ctx.createRadialGradient(
|
ctx.font = `bold ${fontSize}px monospace`;
|
||||||
width / 2,
|
ctx.shadowBlur = 0; // Сбрасываем тени для основных букв (оптимизация)
|
||||||
height / 2,
|
|
||||||
0,
|
|
||||||
width / 2,
|
|
||||||
height / 2,
|
|
||||||
Math.sqrt(width ** 2 + height ** 2) / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isSecure) {
|
|
||||||
if (isDark) {
|
|
||||||
gradient.addColorStop(0, "#00F0FF"); // Центр: Ядовитый циановый
|
|
||||||
gradient.addColorStop(0.4, "#0080FF");
|
|
||||||
gradient.addColorStop(1, "rgba(0, 20, 40, 0)"); // Края уходят в прозрачность
|
|
||||||
} else {
|
|
||||||
gradient.addColorStop(0, "rgba(2, 132, 199, 1)");
|
|
||||||
gradient.addColorStop(1, "rgba(2, 85, 199, 0.2)");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isDark) {
|
|
||||||
gradient.addColorStop(0, "#7000FF"); // Центр: Яркий фиолетовый
|
|
||||||
gradient.addColorStop(0.4, "#400099");
|
|
||||||
gradient.addColorStop(1, "rgba(20, 0, 40, 0)");
|
|
||||||
} else {
|
|
||||||
gradient.addColorStop(0, "rgba(217, 40, 217, 1)");
|
|
||||||
gradient.addColorStop(1, "rgba(61, 40, 217, 0.2)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.fillStyle = gradient;
|
|
||||||
ctx.font = `bold ${fontSize}px monospace`; // Сделали шрифт bold для четкости
|
|
||||||
|
|
||||||
// Добавляем эффект свечения только для темной темы
|
|
||||||
if (isDark) {
|
|
||||||
ctx.shadowBlur = 8;
|
|
||||||
ctx.shadowColor = isSecure ? "#00F0FF" : "#7000FF";
|
|
||||||
} else {
|
|
||||||
ctx.shadowBlur = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < drops.length; i++) {
|
for (let i = 0; i < drops.length; i++) {
|
||||||
const text = Math.random() > 0.5 ? "1" : "0";
|
const x = i * fontSize;
|
||||||
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
|
const y = drops[i] * fontSize;
|
||||||
|
|
||||||
if (drops[i] * fontSize > height && Math.random() > 0.975) {
|
// Расчет яркости в зависимости от курсора
|
||||||
|
const dx = x - mousePos.current.x;
|
||||||
|
const dy = y - mousePos.current.y;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
const radius = 300; // Радиус "взгляда"
|
||||||
|
|
||||||
|
// Плавное затухание: у курсора ярко, вдали - почти не видно
|
||||||
|
const highlight = Math.max(0, 1 - distance / radius);
|
||||||
|
|
||||||
|
// Подбираем альфу так, чтобы буквы было видно даже без мыши, но очень тускло
|
||||||
|
const baseOpacity = isDark ? 0.08 : 0.15;
|
||||||
|
const finalOpacity = baseOpacity + highlight * 0.5;
|
||||||
|
|
||||||
|
if (isSecure) {
|
||||||
|
ctx.fillStyle = isDark
|
||||||
|
? `rgba(0, 240, 255, ${finalOpacity})`
|
||||||
|
: `rgba(2, 132, 199, ${finalOpacity})`;
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = isDark
|
||||||
|
? `rgba(139, 61, 255, ${finalOpacity})`
|
||||||
|
: `rgba(109, 40, 217, ${finalOpacity})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем небольшой Bloom эффект только самым ярким символам у курсора
|
||||||
|
if (isDark && highlight > 0.7) {
|
||||||
|
ctx.shadowBlur = 10;
|
||||||
|
ctx.shadowColor = isSecure ? "#00F0FF" : "#8B3DFF";
|
||||||
|
} else {
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char = chars[Math.floor(Math.random() * chars.length)];
|
||||||
|
ctx.fillText(char, x, y);
|
||||||
|
|
||||||
|
// Скорость падения и рандомный сброс
|
||||||
|
if (y > height && Math.random() > 0.98) {
|
||||||
drops[i] = 0;
|
drops[i] = 0;
|
||||||
}
|
}
|
||||||
drops[i]++;
|
drops[i]++;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const interval = setInterval(draw, 33);
|
animationFrameId = requestAnimationFrame(draw);
|
||||||
|
|
||||||
const handleResize = () => {
|
|
||||||
width = canvas.width = window.innerWidth;
|
|
||||||
height = canvas.height = window.innerHeight;
|
|
||||||
};
|
|
||||||
window.addEventListener("resize", handleResize);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
window.removeEventListener("resize", handleResize);
|
window.removeEventListener("resize", handleResize);
|
||||||
};
|
};
|
||||||
}, [isSecure, theme]);
|
}, [isSecure, theme]); // mousePos удален из зависимостей
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
// opacity-100 для темной темы, чтобы не гасить яркость, и opacity-30 для светлой
|
className="absolute inset-0 w-full h-full pointer-events-none"
|
||||||
className={`absolute inset-0 -z-20 transition-opacity duration-1000 ${
|
|
||||||
theme === "dark" ? "opacity-100" : "opacity-30"
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { CheckIcon, Loader2 } from "lucide-react";
|
import { CheckIcon, Loader2 } from "lucide-react";
|
||||||
import { Dictionary } from "@/lib/IDict";
|
import { Dictionary } from "@/lib/IDict";
|
||||||
import { pb } from "@/lib/pb";
|
import { pb } from "@/lib/pb";
|
||||||
|
import { CyberButton } from "../ui/cyber-button";
|
||||||
|
|
||||||
interface PBPricingPlan {
|
interface PBPricingPlan {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -260,16 +261,13 @@ export function Pricing({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter className="pt-0">
|
<CardFooter className="pt-0">
|
||||||
<Button
|
<CyberButton
|
||||||
variant={plan.popular ? "default" : "outline"}
|
href="#"
|
||||||
className={`w-full rounded-2xl font-bold text-sm uppercase tracking-widest h-14 transition-all duration-300 ${
|
variant={plan.popular ? "primary" : "secondary"}
|
||||||
plan.popular
|
className="w-full"
|
||||||
? "bg-primary text-primary-foreground hover:shadow-[0_0_30px_rgba(0,240,255,0.4)]"
|
|
||||||
: "hover:border-primary/50"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{plan.cta}
|
{plan.cta}
|
||||||
</Button>
|
</CyberButton>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
import { pb } from "@/lib/pb";
|
import { pb } from "@/lib/pb";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dictionary } from "@/lib/IDict";
|
import { Dictionary } from "@/lib/IDict";
|
||||||
|
import { CyberButton } from "../ui/cyber-button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function TerminalGuestbook({ dict }: { dict: Dictionary["guestbook"] }) {
|
export function TerminalGuestbook({ dict }: { dict: Dictionary["guestbook"] }) {
|
||||||
const [comments, setComments] = useState<any[]>([]);
|
const [comments, setComments] = useState<any[]>([]);
|
||||||
@@ -35,7 +37,7 @@ export function TerminalGuestbook({ dict }: { dict: Dictionary["guestbook"] }) {
|
|||||||
try {
|
try {
|
||||||
const records = await pb
|
const records = await pb
|
||||||
.collection("comments")
|
.collection("comments")
|
||||||
.getList(1, 50, { sort: "-created" });
|
.getList(1, 50, { sort: "-created", requestKey: null });
|
||||||
setComments(records.items);
|
setComments(records.items);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -105,9 +107,9 @@ export function TerminalGuestbook({ dict }: { dict: Dictionary["guestbook"] }) {
|
|||||||
id="guestbook"
|
id="guestbook"
|
||||||
className="py-32 px-4 container mx-auto max-w-6xl relative overflow-hidden"
|
className="py-32 px-4 container mx-auto max-w-6xl relative overflow-hidden"
|
||||||
>
|
>
|
||||||
{/* Мягкие свечения */}
|
{/* Мягкие свечения — увеличили размер и размытие для плавности */}
|
||||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-primary/5 blur-[120px] rounded-full -z-10" />
|
<div className="absolute -top-20 -right-20 w-[800px] h-[800px] bg-primary/5 dark:bg-primary/10 blur-[150px] rounded-full -z-10 opacity-30 dark:opacity-60" />
|
||||||
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-secondary/5 blur-[120px] rounded-full -z-10" />
|
<div className="absolute -bottom-20 -left-20 w-[800px] h-[800px] bg-secondary/5 dark:bg-secondary/10 blur-[150px] rounded-full -z-10 opacity-30 dark:opacity-60" />
|
||||||
|
|
||||||
{/* Заголовок секции */}
|
{/* Заголовок секции */}
|
||||||
<div className="text-center mb-16">
|
<div className="text-center mb-16">
|
||||||
@@ -136,7 +138,12 @@ export function TerminalGuestbook({ dict }: { dict: Dictionary["guestbook"] }) {
|
|||||||
<div className="bg-card/40 backdrop-blur-xl border border-border/50 rounded-[2.5rem] p-8 shadow-xl">
|
<div className="bg-card/40 backdrop-blur-xl border border-border/50 rounded-[2.5rem] p-8 shadow-xl">
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-mono text-primary uppercase tracking-[0.2em] flex items-center gap-2 ml-1">
|
<label
|
||||||
|
className={cn(
|
||||||
|
"text-[10px] font-mono uppercase tracking-[0.2em] flex items-center gap-2 ml-1 mb-2",
|
||||||
|
"text-primary/80 dark:text-primary font-bold", // Сделали жирнее и чуть приглушили в светлой
|
||||||
|
)}
|
||||||
|
>
|
||||||
<User className="size-3" /> {dict.labelAlias}
|
<User className="size-3" /> {dict.labelAlias}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -146,7 +153,15 @@ export function TerminalGuestbook({ dict }: { dict: Dictionary["guestbook"] }) {
|
|||||||
onChange={(e) => setNickname(e.target.value)}
|
onChange={(e) => setNickname(e.target.value)}
|
||||||
placeholder={dict.placeholderAlias}
|
placeholder={dict.placeholderAlias}
|
||||||
disabled={isSubmitting || cooldown > 0}
|
disabled={isSubmitting || cooldown > 0}
|
||||||
className="w-full bg-background/50 border border-border/50 focus:border-primary/50 rounded-2xl px-5 py-4 text-sm outline-none transition-all disabled:opacity-50 text-foreground font-mono placeholder:text-muted-foreground/30"
|
// Фрагмент классов для input и textarea в TerminalGuestbook.tsx
|
||||||
|
className={cn(
|
||||||
|
"w-full rounded-2xl px-5 py-4 text-sm outline-none transition-all font-mono",
|
||||||
|
"disabled:opacity-50 text-foreground",
|
||||||
|
// Светлая тема: более плотный плейсхолдер и четкая рамка
|
||||||
|
"bg-background/50 border border-border/80 placeholder:text-muted-foreground/60 focus:border-primary focus:bg-background shadow-sm",
|
||||||
|
// Темная тема: стеклянный эффект
|
||||||
|
"dark:bg-background/50 dark:border-border/50 dark:placeholder:text-muted-foreground/30 dark:focus:border-primary/50",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -161,7 +176,15 @@ export function TerminalGuestbook({ dict }: { dict: Dictionary["guestbook"] }) {
|
|||||||
onChange={(e) => setMessage(e.target.value)}
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
placeholder={dict.placeholderPayload}
|
placeholder={dict.placeholderPayload}
|
||||||
disabled={isSubmitting || cooldown > 0}
|
disabled={isSubmitting || cooldown > 0}
|
||||||
className="w-full bg-background/50 border border-border/50 focus:border-primary/50 rounded-2xl px-5 py-4 text-sm outline-none transition-all disabled:opacity-50 resize-none text-foreground placeholder:text-muted-foreground/30"
|
// Фрагмент классов для input и textarea в TerminalGuestbook.tsx
|
||||||
|
className={cn(
|
||||||
|
"w-full rounded-2xl px-5 py-4 text-sm outline-none transition-all font-mono",
|
||||||
|
"disabled:opacity-50 text-foreground",
|
||||||
|
// Светлая тема: более плотный плейсхолдер и четкая рамка
|
||||||
|
"bg-background/50 border border-border/80 placeholder:text-muted-foreground/60 focus:border-primary focus:bg-background shadow-sm",
|
||||||
|
// Темная тема: стеклянный эффект
|
||||||
|
"dark:bg-background/50 dark:border-border/50 dark:placeholder:text-muted-foreground/30 dark:focus:border-primary/50",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="text-right text-[10px] text-muted-foreground/40 font-mono pr-2">
|
<div className="text-right text-[10px] text-muted-foreground/40 font-mono pr-2">
|
||||||
{message.length}/150
|
{message.length}/150
|
||||||
@@ -196,14 +219,11 @@ export function TerminalGuestbook({ dict }: { dict: Dictionary["guestbook"] }) {
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<Button
|
<CyberButton
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
disabled={isSubmitting || cooldown > 0}
|
disabled={isSubmitting || cooldown > 0}
|
||||||
className={`w-full py-7 rounded-2xl font-black font-mono tracking-widest uppercase transition-all active:scale-[0.98] ${
|
className="w-full"
|
||||||
cooldown > 0
|
|
||||||
? "bg-muted text-muted-foreground/40"
|
|
||||||
: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-[0_0_30px_rgba(139,61,255,0.3)]"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
dict.btnEncrypting
|
dict.btnEncrypting
|
||||||
@@ -214,13 +234,22 @@ export function TerminalGuestbook({ dict }: { dict: Dictionary["guestbook"] }) {
|
|||||||
<Send className="size-4" /> {dict.btnBroadcast}
|
<Send className="size-4" /> {dict.btnBroadcast}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</CyberButton>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ПРАВАЯ КОЛОНКА (7/12): Фид сообщений */}
|
{/* ПРАВАЯ КОЛОНКА (7/12): Фид сообщений */}
|
||||||
<div className="lg:col-span-7 bg-black/20 backdrop-blur-md border border-primary/20 rounded-[3rem] p-8 h-[700px] flex flex-col shadow-2xl">
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"lg:col-span-7 h-[700px] flex flex-col p-8 rounded-[3rem] backdrop-blur-md transition-all",
|
||||||
|
// Светлая тема: чистый белый, жесткая граница, сильная тень
|
||||||
|
"bg-white/90 border border-border/80 shadow-xl",
|
||||||
|
// Темная тема: полупрозрачное стекло, неоновая граница
|
||||||
|
"dark:bg-black/20 dark:border-primary/20 dark:shadow-2xl",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="flex justify-between items-center mb-8 px-4">
|
<div className="flex justify-between items-center mb-8 px-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="size-2 rounded-full bg-secondary animate-pulse shadow-[0_0_10px_rgba(0,229,242,0.8)]" />
|
<div className="size-2 rounded-full bg-secondary animate-pulse shadow-[0_0_10px_rgba(0,229,242,0.8)]" />
|
||||||
@@ -246,7 +275,16 @@ export function TerminalGuestbook({ dict }: { dict: Dictionary["guestbook"] }) {
|
|||||||
className="relative group"
|
className="relative group"
|
||||||
>
|
>
|
||||||
<div className="absolute -left-2 top-0 bottom-0 w-0.5 bg-gradient-to-b from-secondary/0 via-secondary/20 to-secondary/0 group-hover:via-secondary/50 transition-all" />
|
<div className="absolute -left-2 top-0 bottom-0 w-0.5 bg-gradient-to-b from-secondary/0 via-secondary/20 to-secondary/0 group-hover:via-secondary/50 transition-all" />
|
||||||
<div className="bg-white/5 border border-white/5 rounded-[1.5rem] p-5 transition-all group-hover:bg-white/[0.08] group-hover:border-white/10 group-hover:translate-x-1">
|
{/* Сообщение */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"p-5 rounded-[1.5rem] transition-all group-hover:translate-x-1",
|
||||||
|
// Светлая тема: серый фон, четкая рамка
|
||||||
|
"bg-muted/30 border border-border/50 group-hover:bg-muted/60",
|
||||||
|
// Темная тема: прозрачное стекло
|
||||||
|
"dark:bg-white/5 dark:border-white/5 dark:group-hover:bg-white/[0.08] dark:group-hover:border-white/10",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="flex justify-between items-center mb-3">
|
<div className="flex justify-between items-center mb-3">
|
||||||
<span className="font-black text-secondary font-mono text-sm tracking-tight">
|
<span className="font-black text-secondary font-mono text-sm tracking-tight">
|
||||||
@{comment.nickname}
|
@{comment.nickname}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Accordion as AccordionPrimitive } from "radix-ui";
|
import { Accordion as AccordionPrimitive } from "radix-ui";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CaretDownIcon } from "@phosphor-icons/react";
|
import { ChevronDown } from "lucide-react"; // Заменили импорт
|
||||||
|
|
||||||
function Accordion({
|
function Accordion({
|
||||||
className,
|
className,
|
||||||
@@ -50,7 +50,7 @@ function AccordionTrigger({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<CaretDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
<ChevronDown className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||||
</AccordionPrimitive.Trigger>
|
</AccordionPrimitive.Trigger>
|
||||||
</AccordionPrimitive.Header>
|
</AccordionPrimitive.Header>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface CyberButtonProps {
|
||||||
|
href?: string;
|
||||||
|
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
variant?: "primary" | "secondary";
|
||||||
|
disabled?: boolean;
|
||||||
|
type?: "button" | "submit" | "reset";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CyberButton({
|
||||||
|
href,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
variant = "primary",
|
||||||
|
disabled = false,
|
||||||
|
type = "button",
|
||||||
|
}: CyberButtonProps) {
|
||||||
|
const isPrimary = variant === "primary";
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<motion.div
|
||||||
|
whileHover={!disabled ? { scale: 1.02 } : {}}
|
||||||
|
whileTap={!disabled ? { scale: 0.98 } : {}}
|
||||||
|
className={cn(
|
||||||
|
"relative z-10 flex items-center justify-center px-8 py-4 overflow-hidden transition-all duration-300 border rounded-xl font-mono",
|
||||||
|
|
||||||
|
// --- ACTIVE STATE ---
|
||||||
|
!disabled && [
|
||||||
|
isPrimary
|
||||||
|
? "bg-[#8B3DFF] border-[#8B3DFF] text-white shadow-lg shadow-purple-500/20 dark:bg-[radial-gradient(circle_at_center,_rgba(139,61,255,0.4)_0%,_rgba(139,61,255,0.1)_100%)] dark:border-[#8B3DFF]/50 dark:shadow-[0_0_25px_rgba(139,61,255,0.3)] dark:after:absolute dark:after:inset-0 dark:after:shadow-[inset_0_0_15px_rgba(139,61,255,0.2)]"
|
||||||
|
: "text-[#00E5F2] border-[#00E5F2]/40 backdrop-blur-md bg-slate-900/5 border-[#00E5F2]/50 shadow-sm dark:bg-[#00E5F2]/5 dark:border-[#00E5F2]/30 dark:shadow-[0_0_15px_rgba(0,229,242,0.1)] hover:border-[#00E5F2] hover:shadow-[0_0_20px_rgba(0,229,242,0.3)]",
|
||||||
|
],
|
||||||
|
|
||||||
|
// --- DISABLED STATE (Исправленный) ---
|
||||||
|
disabled && [
|
||||||
|
"cursor-not-allowed border-dashed shadow-none",
|
||||||
|
// Светлая тема: делаем кнопку матовой и "холодной"
|
||||||
|
"bg-slate-100 border-slate-300 text-slate-400",
|
||||||
|
// Темная тема: делаем кнопку глубокой и "выключенной"
|
||||||
|
"dark:bg-slate-900/40 dark:border-slate-800 dark:text-slate-600",
|
||||||
|
],
|
||||||
|
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* CRT Сетка (только когда активна) */}
|
||||||
|
{!isPrimary && !disabled && (
|
||||||
|
<div className="absolute inset-0 opacity-[0.03] dark:opacity-10 pointer-events-none bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,229,242,0.1)_50%),linear-gradient(90deg,rgba(255,0,0,0.02),rgba(0,255,0,0.01),rgba(0,0,255,0.02))] bg-[length:100%_4px,3px_100%]" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Энергетический блик (только когда активна) */}
|
||||||
|
{!disabled && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 w-full h-full -translate-x-full group-hover:animate-scan pointer-events-none",
|
||||||
|
isPrimary
|
||||||
|
? "bg-gradient-to-r from-transparent via-white/20 to-transparent"
|
||||||
|
: "bg-gradient-to-r from-transparent via-[#00E5F2]/20 to-transparent",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Угловые элементы (⌜ ⌟) - прячем или приглушаем при disabled */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 pointer-events-none transition-opacity",
|
||||||
|
disabled ? "opacity-20" : "opacity-60",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute top-1 left-1 text-[10px] font-mono",
|
||||||
|
isPrimary ? "text-white" : "text-[#00E5F2]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
⌜
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute bottom-1 right-1 text-[10px] font-mono",
|
||||||
|
isPrimary ? "text-white" : "text-[#00E5F2]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
⌟
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Анимированная полоска заряда (только для Secondary и когда активна) */}
|
||||||
|
{!isPrimary && !disabled && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0 }}
|
||||||
|
whileHover={{ height: "70%" }}
|
||||||
|
className="absolute left-0 w-[2px] bg-[#00E5F2] shadow-[0_0_10px_#00E5F2] transition-all"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Текст */}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"relative z-20 font-black uppercase tracking-[0.2em] text-sm flex items-center gap-2",
|
||||||
|
!isPrimary &&
|
||||||
|
!disabled &&
|
||||||
|
"drop-shadow-[0_0_5px_rgba(0,229,242,0.5)]",
|
||||||
|
disabled && "drop-shadow-none",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="opacity-50">[</span>
|
||||||
|
{children}
|
||||||
|
<span className="opacity-50">]</span>
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Фоновое облако свечения (только когда активна)
|
||||||
|
const glowEffect = !disabled && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 blur-2xl opacity-0 group-hover:opacity-40 transition-opacity duration-500 -z-10",
|
||||||
|
isPrimary ? "bg-[#8B3DFF]" : "bg-[#00E5F2]/40",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (href && !disabled) {
|
||||||
|
return (
|
||||||
|
<Link href={href} className="group relative inline-block">
|
||||||
|
{content}
|
||||||
|
{glowEffect}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
className="group relative inline-block bg-transparent border-none p-0 outline-none cursor-pointer"
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
{glowEffect}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
|||||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CheckIcon, CaretRightIcon } from "@phosphor-icons/react";
|
import { Check, ChevronRight } from "lucide-react";
|
||||||
|
|
||||||
function DropdownMenu({
|
function DropdownMenu({
|
||||||
...props
|
...props
|
||||||
@@ -106,7 +106,7 @@ function DropdownMenuCheckboxItem({
|
|||||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||||
>
|
>
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<CheckIcon />
|
<Check className="size-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
@@ -148,7 +148,7 @@ function DropdownMenuRadioItem({
|
|||||||
data-slot="dropdown-menu-radio-item-indicator"
|
data-slot="dropdown-menu-radio-item-indicator"
|
||||||
>
|
>
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<CheckIcon />
|
<Check className="size-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
@@ -230,7 +230,7 @@ function DropdownMenuSubTrigger({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<CaretRightIcon className="ml-auto" />
|
<ChevronRight className="ml-auto" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,20 @@
|
|||||||
import PocketBase, { RecordModel } from "pocketbase";
|
import PocketBase, { RecordModel } from "pocketbase";
|
||||||
|
|
||||||
// Определяем, на сервере мы или в браузере
|
|
||||||
const isServer = typeof window === "undefined";
|
const isServer = typeof window === "undefined";
|
||||||
|
|
||||||
// Фикс для Node.js (серверная часть)
|
const publicUrl =
|
||||||
if (isServer) {
|
process.env.NEXT_PUBLIC_PB_URL || "https://netrunner-vpn.com/cms-api";
|
||||||
// Позволяет серверу игнорировать проблемы с сертификатами при локальных запросах
|
const internalUrl = process.env.PB_INTERNAL_URL; // Не ставим дефолт здесь
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
export const PB_URL = isServer ? internalUrl || publicUrl : publicUrl;
|
||||||
* ЛОГИКА URL:
|
|
||||||
* 1. На сервере: используем локальный IP (быстро, внутри сети).
|
|
||||||
* 2. В браузере: используем публичный URL (через Nginx/SSL).
|
|
||||||
*/
|
|
||||||
export const PB_URL = isServer
|
|
||||||
? process.env.PB_INTERNAL_URL || "https://netrunner-vpn.com/cms-api"
|
|
||||||
: process.env.NEXT_PUBLIC_PB_URL || "https://netrunner-vpn.com/cms-api";
|
|
||||||
|
|
||||||
export const pb = new PocketBase(PB_URL);
|
export const pb = new PocketBase(PB_URL);
|
||||||
|
|
||||||
|
// Отключаем автоотмену на сервере, чтобы избежать AbortError при SSR/ISR
|
||||||
|
if (isServer) {
|
||||||
|
pb.autoCancellation(false);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Базовый интерфейс для записей PocketBase.
|
* Базовый интерфейс для записей PocketBase.
|
||||||
* Наследуемся от RecordModel, чтобы иметь системные поля (id, created, updated, и т.д.)
|
* Наследуемся от RecordModel, чтобы иметь системные поля (id, created, updated, и т.д.)
|
||||||
@@ -55,6 +50,9 @@ export interface PBFaq extends BaseRecord {
|
|||||||
lang: string;
|
lang: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FILE_BASE_URL =
|
||||||
|
process.env.NEXT_PUBLIC_PB_URL || "https://netrunner-vpn.com/cms-api";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Функция для генерации ссылки на картинку из PB
|
* Функция для генерации ссылки на картинку из PB
|
||||||
* @param record - Объект записи (должен содержать id и collectionId)
|
* @param record - Объект записи (должен содержать id и collectionId)
|
||||||
@@ -65,9 +63,8 @@ export function getPbImage(
|
|||||||
filename: string | undefined,
|
filename: string | undefined,
|
||||||
): string {
|
): string {
|
||||||
if (!filename) return "";
|
if (!filename) return "";
|
||||||
return `${PB_URL}/api/files/${record.collectionId}/${record.id}/${filename}`;
|
return `${FILE_BASE_URL}/api/files/${record.collectionId}/${record.id}/${filename}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// lib/pb.ts (добавь в конец файла)
|
// lib/pb.ts (добавь в конец файла)
|
||||||
|
|
||||||
export interface PBDownload extends BaseRecord {
|
export interface PBDownload extends BaseRecord {
|
||||||
@@ -85,12 +82,5 @@ export function getPbFileUrl(
|
|||||||
filename: string | undefined,
|
filename: string | undefined,
|
||||||
): string {
|
): string {
|
||||||
if (!filename) return "";
|
if (!filename) return "";
|
||||||
|
return `${FILE_BASE_URL}/api/files/${record.collectionId}/${record.id}/${filename}`;
|
||||||
// Если мы на сервере, для ссылок на файлы всё равно используем ПУБЛИЧНЫЙ адрес,
|
|
||||||
// так как эти ссылки рендерятся в HTML и уходят клиенту.
|
|
||||||
const baseUrl = isServer
|
|
||||||
? process.env.NEXT_PUBLIC_PB_URL || "https://netrunner-vpn.com/cms"
|
|
||||||
: PB_URL;
|
|
||||||
|
|
||||||
return `${baseUrl}/api/files/${record.collectionId}/${record.id}/${filename}`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
images: {
|
||||||
|
remotePatterns: [{ protocol: "https", hostname: "netrunner-vpn.com" }],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "^0.8.3",
|
"@formatjs/intl-localematcher": "^0.8.3",
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
|
|||||||
Generated
-15
@@ -11,9 +11,6 @@ importers:
|
|||||||
'@formatjs/intl-localematcher':
|
'@formatjs/intl-localematcher':
|
||||||
specifier: ^0.8.3
|
specifier: ^0.8.3
|
||||||
version: 0.8.3
|
version: 0.8.3
|
||||||
'@phosphor-icons/react':
|
|
||||||
specifier: ^2.1.10
|
|
||||||
version: 2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -648,13 +645,6 @@ packages:
|
|||||||
'@open-draft/until@2.1.0':
|
'@open-draft/until@2.1.0':
|
||||||
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
|
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
|
||||||
|
|
||||||
'@phosphor-icons/react@2.1.10':
|
|
||||||
resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>= 16.8'
|
|
||||||
react-dom: '>= 16.8'
|
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1':
|
'@radix-ui/number@1.1.1':
|
||||||
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
||||||
|
|
||||||
@@ -4363,11 +4353,6 @@ snapshots:
|
|||||||
|
|
||||||
'@open-draft/until@2.1.0': {}
|
'@open-draft/until@2.1.0': {}
|
||||||
|
|
||||||
'@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
|
||||||
dependencies:
|
|
||||||
react: 19.2.4
|
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1': {}
|
'@radix-ui/number@1.1.1': {}
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.3': {}
|
'@radix-ui/primitive@1.1.3': {}
|
||||||
|
|||||||
Reference in New Issue
Block a user