initial commit
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
import { ThemeProvider } from "@/components/sections/theme/theme-provider";
|
||||||
|
import "../globals.css";
|
||||||
|
import { getDictionary } from "@/lib/dictionaries";
|
||||||
|
import { Header } from "@/components/sections/Header";
|
||||||
|
import { JetBrains_Mono } from "next/font/google"; // Импортируем шрифт
|
||||||
|
|
||||||
|
// Настраиваем JetBrains Mono
|
||||||
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
|
subsets: ["latin", "cyrillic"], // Важно добавить cyrillic для русского языка
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
params: Promise<{ lang: string }>;
|
||||||
|
}) {
|
||||||
|
const { lang } = await params;
|
||||||
|
const dict = await getDictionary(lang);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang={lang} suppressHydrationWarning>
|
||||||
|
<body
|
||||||
|
className={`${jetbrainsMono.className} bg-background text-foreground min-h-screen flex flex-col transition-colors duration-300`}
|
||||||
|
>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="dark"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
<Header dict={dict.nav} lang={lang} />
|
||||||
|
<main className="flex-1 mt-32">{children}</main>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import { getDictionary } from "@/lib/dictionaries";
|
||||||
|
import { Hero } from "@/components/sections/Hero";
|
||||||
|
import { Features } from "@/components/sections/Features";
|
||||||
|
import { Pricing } from "@/components/sections/Pricing";
|
||||||
|
import { FAQ } from "@/components/sections/Faq";
|
||||||
|
import { BlogPreview } from "@/components/sections/BlogPreview";
|
||||||
|
|
||||||
|
export default async function Home({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ lang: string }>;
|
||||||
|
}) {
|
||||||
|
// РАЗВОРАЧИВАЕМ PARAMS
|
||||||
|
const { lang } = await params;
|
||||||
|
|
||||||
|
// Получаем словарь, используя проверенную строку
|
||||||
|
const dict = await getDictionary(lang);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Hero dict={dict.hero} />
|
||||||
|
<Features dict={dict.features} />
|
||||||
|
<Pricing dict={dict.pricing} />
|
||||||
|
<FAQ dict={dict.faq} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
<Suspense fallback={<div className="py-20 text-center">Loading...</div>}>
|
||||||
|
<BlogPreview dict={dict.blog} lang={lang} />
|
||||||
|
</Suspense>
|
||||||
|
*/}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+101
-88
@@ -10,111 +10,81 @@
|
|||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-mono);
|
--font-mono: var(--font-mono);
|
||||||
--font-heading: var(--font-mono);
|
--font-heading: var(--font-mono);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-chart-5: var(--chart-5);
|
|
||||||
--color-chart-4: var(--chart-4);
|
|
||||||
--color-chart-3: var(--chart-3);
|
|
||||||
--color-chart-2: var(--chart-2);
|
|
||||||
--color-chart-1: var(--chart-1);
|
|
||||||
--color-ring: var(--ring);
|
--color-ring: var(--ring);
|
||||||
--color-input: var(--input);
|
--color-input: var(--input);
|
||||||
--color-border: var(--border);
|
--color-border: var(--border);
|
||||||
--color-destructive: var(--destructive);
|
--color-destructive: var(--destructive);
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
--color-primary: var(--primary);
|
--color-primary: var(--primary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
--color-popover: var(--popover);
|
--color-popover: var(--popover);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--radius-sm: calc(var(--radius) * 0.6);
|
|
||||||
--radius-md: calc(var(--radius) * 0.8);
|
--radius-sm: 0.75rem;
|
||||||
--radius-lg: var(--radius);
|
--radius-md: 1rem;
|
||||||
--radius-xl: calc(var(--radius) * 1.4);
|
--radius-lg: 1.5rem;
|
||||||
--radius-2xl: calc(var(--radius) * 1.8);
|
--radius-xl: 2rem;
|
||||||
--radius-3xl: calc(var(--radius) * 2.2);
|
--radius-2xl: 2.5rem;
|
||||||
--radius-4xl: calc(var(--radius) * 2.6);
|
--radius-3xl: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(0.98 0 0);
|
||||||
--foreground: oklch(0.145 0 0);
|
--foreground: oklch(0.15 0 0);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0 / 70%);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.15 0 0);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0 / 80%);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.15 0 0);
|
||||||
--primary: oklch(0.205 0 0);
|
|
||||||
|
--primary: oklch(0.45 0.25 290);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
--secondary: oklch(0.97 0 0);
|
--secondary: oklch(0.95 0.05 290);
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--secondary-foreground: oklch(0.35 0.2 290);
|
||||||
--muted: oklch(0.97 0 0);
|
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
--muted: oklch(0.96 0 0);
|
||||||
--accent: oklch(0.97 0 0);
|
--muted-foreground: oklch(0.5 0 0);
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--accent: oklch(0.96 0.05 290);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--accent-foreground: oklch(0.45 0.25 290);
|
||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.9 0 0 / 50%);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.9 0 0);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: oklch(0.45 0.25 290);
|
||||||
--chart-1: oklch(0.87 0 0);
|
|
||||||
--chart-2: oklch(0.556 0 0);
|
|
||||||
--chart-3: oklch(0.439 0 0);
|
|
||||||
--chart-4: oklch(0.371 0 0);
|
|
||||||
--chart-5: oklch(0.269 0 0);
|
|
||||||
--radius: 0.625rem;
|
|
||||||
--sidebar: oklch(0.985 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
/* Глубокий темный фон */
|
||||||
--foreground: oklch(0.985 0 0);
|
--background: oklch(0.12 0.02 290);
|
||||||
--card: oklch(0.205 0 0);
|
/* Слегка смягченный белый */
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.98 0 0);
|
||||||
--popover: oklch(0.205 0 0);
|
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
/* Более темные стеклянные карточки */
|
||||||
--primary: oklch(0.922 0 0);
|
--card: oklch(0.16 0.02 290 / 30%);
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--card-foreground: oklch(0.98 0 0);
|
||||||
--secondary: oklch(0.269 0 0);
|
--popover: oklch(0.15 0.03 290 / 60%);
|
||||||
|
--popover-foreground: oklch(0.98 0 0);
|
||||||
|
|
||||||
|
/* Cyber Purple */
|
||||||
|
--primary: oklch(0.6 0.28 290);
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
|
||||||
|
/* Secondary - приглушенный */
|
||||||
|
--secondary: oklch(0.25 0.08 290);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: oklch(0.269 0 0);
|
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--muted: oklch(0.2 0.05 290);
|
||||||
--accent: oklch(0.269 0 0);
|
--muted-foreground: oklch(0.65 0.03 290);
|
||||||
|
--accent: oklch(0.25 0.08 290);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--border: oklch(0.3 0.04 290 / 40%);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--input: oklch(0.3 0.05 290 / 50%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--ring: oklch(0.6 0.28 290);
|
||||||
--ring: oklch(0.556 0 0);
|
|
||||||
--chart-1: oklch(0.87 0 0);
|
|
||||||
--chart-2: oklch(0.556 0 0);
|
|
||||||
--chart-3: oklch(0.439 0 0);
|
|
||||||
--chart-4: oklch(0.371 0 0);
|
|
||||||
--chart-5: oklch(0.269 0 0);
|
|
||||||
--sidebar: oklch(0.205 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -124,7 +94,50 @@
|
|||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
html {
|
}
|
||||||
@apply font-mono;
|
|
||||||
|
@layer utilities {
|
||||||
|
.text-neon-cyan {
|
||||||
|
/* В темной теме текст белый, в светлой - глубокий синий */
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow:
|
||||||
|
0 0 2px #fff,
|
||||||
|
0 0 8px #00f0ff,
|
||||||
|
0 0 20px rgba(0, 240, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Добавляем поддержку светлой темы для неона */
|
||||||
|
:global(.light) .text-neon-cyan {
|
||||||
|
color: #0284c7;
|
||||||
|
text-shadow: 0 0 2px rgba(0, 240, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-neon-purple {
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow:
|
||||||
|
0 0 2px #fff,
|
||||||
|
0 0 8px #7000ff,
|
||||||
|
0 0 20px rgba(112, 0, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-neon-flicker {
|
||||||
|
animation: neon-flicker 4s infinite alternate ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes neon-flicker {
|
||||||
|
0%,
|
||||||
|
18%,
|
||||||
|
22%,
|
||||||
|
25%,
|
||||||
|
53%,
|
||||||
|
57%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
20%,
|
||||||
|
24%,
|
||||||
|
55% {
|
||||||
|
opacity: 0.6; /* Момент "затухания" лампы */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import type { Metadata } from "next";
|
|
||||||
import { Geist, Geist_Mono, JetBrains_Mono } from "next/font/google";
|
|
||||||
import "./globals.css";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const jetbrainsMono = JetBrains_Mono({subsets:['latin'],variable:'--font-mono'});
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
|
||||||
variable: "--font-geist-sans",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Create Next App",
|
|
||||||
description: "Generated by create next app",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) {
|
|
||||||
return (
|
|
||||||
<html
|
|
||||||
lang="en"
|
|
||||||
className={cn("h-full", "antialiased", geistSans.variable, geistMono.variable, "font-mono", jetbrainsMono.variable)}
|
|
||||||
>
|
|
||||||
<body className="min-h-full flex flex-col">{children}</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
|
||||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={100}
|
|
||||||
height={20}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the page.tsx file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { fetchPosts } from "@/lib/strapi";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
|
import { Calendar } from "lucide-react";
|
||||||
|
|
||||||
|
export async function BlogPreview({ dict, lang }: { dict: any; lang: string }) {
|
||||||
|
const posts = await fetchPosts();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="blog" className="py-20 px-4 container mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold mb-10 text-center">{dict.title}</h2>
|
||||||
|
|
||||||
|
{posts.length === 0 ? (
|
||||||
|
<div className="text-center text-muted-foreground py-10 border border-dashed border-border">
|
||||||
|
{dict.empty}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
||||||
|
{posts.map((post) => (
|
||||||
|
<Card
|
||||||
|
key={post.id}
|
||||||
|
className="bg-transparent border-border hover:border-secondary transition-colors cursor-pointer group"
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-3">
|
||||||
|
<Calendar className="w-3 h-3" />
|
||||||
|
{new Date(post.publishedAt).toLocaleDateString(lang)}
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-xl group-hover:text-secondary transition-colors">
|
||||||
|
{post.title}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
{post.description}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
|
|
||||||
|
export function FAQ({ dict }: { dict: any }) {
|
||||||
|
return (
|
||||||
|
<section id="faq" className="py-20 px-4 container mx-auto max-w-3xl">
|
||||||
|
<h2 className="text-3xl font-bold mb-10 text-center">{dict.title}</h2>
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
{dict.items.map((faq: any, index: number) => (
|
||||||
|
<AccordionItem
|
||||||
|
key={index}
|
||||||
|
value={`item-${index}`}
|
||||||
|
className="border-border"
|
||||||
|
>
|
||||||
|
<AccordionTrigger className="text-left text-foreground hover:text-primary transition-colors">
|
||||||
|
{faq.q}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="text-muted-foreground">
|
||||||
|
{faq.a}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { EyeOff, Lock, Globe, Gauge, Cpu } from "lucide-react";
|
||||||
|
import { motion, Variants } from "framer-motion";
|
||||||
|
|
||||||
|
export function Features({ dict }: { dict: any }) {
|
||||||
|
// Настройки для 5 карточек в стильной Bento-сетке
|
||||||
|
// md:col-span-2 = широкая карточка (2/3 ширины)
|
||||||
|
// md:col-span-1 = узкая/квадратная карточка (1/3 ширины)
|
||||||
|
const bentoConfig = [
|
||||||
|
{
|
||||||
|
// 1: Широкая сверху
|
||||||
|
className:
|
||||||
|
"md:col-span-2 min-h-[300px] group overflow-hidden bg-card/40 backdrop-blur-xl border border-border/50 rounded-[2.5rem] p-6 md:p-10 flex flex-col justify-end hover:border-primary/50 transition-all duration-500 hover:shadow-[0_0_40px_rgba(0,240,255,0.1)] relative",
|
||||||
|
icon: <EyeOff className="w-8 h-8 text-primary relative z-10 mb-4" />,
|
||||||
|
bgElement: (
|
||||||
|
<EyeOff className="absolute -bottom-10 -right-10 w-64 h-64 text-primary opacity-[0.03] group-hover:opacity-10 group-hover:scale-110 group-hover:-rotate-12 transition-all duration-700 pointer-events-none" />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 2: Узкая справа
|
||||||
|
className:
|
||||||
|
"md:col-span-1 min-h-[300px] bg-card/40 backdrop-blur-xl border border-border/50 rounded-[2.5rem] p-6 md:p-10 flex flex-col group hover:border-secondary/60 transition-all duration-500 hover:shadow-[0_0_40px_rgba(112,0,255,0.15)] relative overflow-hidden",
|
||||||
|
icon: (
|
||||||
|
<div className="bg-secondary/10 w-14 h-14 rounded-[1.2rem] flex items-center justify-center mb-auto border border-secondary/20 shadow-[0_0_15px_rgba(112,0,255,0.2)] relative z-10">
|
||||||
|
<Lock className="w-7 h-7 text-secondary" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
bgElement: (
|
||||||
|
<div className="absolute top-0 right-0 w-32 h-32 bg-secondary/20 blur-[60px] opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 3: Узкая слева в среднем ряду
|
||||||
|
className:
|
||||||
|
"md:col-span-1 min-h-[300px] bg-card/40 backdrop-blur-xl border border-border/50 rounded-[2.5rem] p-6 md:p-10 flex flex-col group hover:border-primary/50 transition-all duration-500 hover:shadow-[0_0_30px_rgba(0,240,255,0.1)] relative overflow-hidden",
|
||||||
|
icon: (
|
||||||
|
<div className="bg-primary/10 w-14 h-14 rounded-[1.2rem] flex items-center justify-center mb-auto border border-primary/20 shadow-[0_0_15px_rgba(0,240,255,0.2)] relative z-10">
|
||||||
|
<Gauge className="w-7 h-7 text-primary" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
bgElement: (
|
||||||
|
<Gauge className="absolute top-10 -right-4 w-40 h-40 text-primary opacity-[0.03] group-hover:opacity-10 transition-all duration-700 pointer-events-none" />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 4: Широкая справа в среднем ряду
|
||||||
|
className:
|
||||||
|
"md:col-span-2 min-h-[300px] bg-gradient-to-br from-card/40 to-background/40 backdrop-blur-xl border border-border/50 rounded-[2.5rem] p-6 md:p-10 flex flex-col justify-end group hover:border-secondary/50 transition-all duration-500 hover:shadow-[0_0_30px_rgba(112,0,255,0.1)] relative overflow-hidden",
|
||||||
|
icon: <Cpu className="w-8 h-8 text-secondary relative z-10 mb-4" />,
|
||||||
|
bgElement: (
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_right,_var(--tw-gradient-stops))] from-secondary/10 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 5: На всю ширину снизу
|
||||||
|
className:
|
||||||
|
"md:col-span-3 bg-gradient-to-r from-card/40 to-background/40 backdrop-blur-xl border border-border/50 rounded-[2.5rem] p-6 md:p-10 flex flex-col md:flex-row items-start md:items-center gap-6 md:gap-8 group hover:border-primary/50 transition-all duration-500 hover:shadow-[0_0_40px_rgba(0,240,255,0.1)] relative overflow-hidden",
|
||||||
|
icon: (
|
||||||
|
<div className="bg-primary/10 w-16 h-16 md:w-20 md:h-20 rounded-[1.5rem] md:rounded-[2rem] shrink-0 flex items-center justify-center border border-primary/20 shadow-[0_0_20px_rgba(0,240,255,0.2)] relative z-10">
|
||||||
|
<Globe className="w-8 h-8 md:w-10 md:h-10 text-primary" />
|
||||||
|
</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" />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants: Variants = {
|
||||||
|
hidden: {
|
||||||
|
opacity: 0,
|
||||||
|
y: 40,
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 260,
|
||||||
|
damping: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Проверка на случай, если переводы еще не подгрузились (предотвращает краш)
|
||||||
|
if (!dict || !dict.items || dict.items.length < 5) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
// overflow-hidden и w-full здесь чинят горизонтальный скролл на мобильных телефонах
|
||||||
|
<section
|
||||||
|
id="features"
|
||||||
|
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="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[100vw] md:w-[600px] h-[600px] bg-secondary/10 blur-[100px] md:blur-[120px] rounded-full pointer-events-none -z-10" />
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="show"
|
||||||
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-3 gap-6"
|
||||||
|
>
|
||||||
|
{bentoConfig.map((config, idx) => {
|
||||||
|
const feature = dict.items[idx];
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
variants={itemVariants}
|
||||||
|
className={config.className}
|
||||||
|
>
|
||||||
|
{config.bgElement}
|
||||||
|
{config.icon}
|
||||||
|
<div className="relative z-10 mt-6 md:mt-0">
|
||||||
|
<h3 className="text-xl md:text-2xl font-bold text-foreground mb-3 tracking-tight">
|
||||||
|
{feature.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground text-sm md:text-base leading-relaxed max-w-lg">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { LanguageSwitcher } from "@/components/sections/lang/language-switcher";
|
||||||
|
import { ThemeToggle } from "@/components/sections/theme/theme-toggle";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
|
export function Header({ dict, lang }: { dict: any; lang: string }) {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
// Ждем монтирования клиента, чтобы избежать ошибок гидратации при использовании классов тем
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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">
|
||||||
|
<div className="px-6 md:px-8 h-16 flex items-center justify-between">
|
||||||
|
{/* ЛЕВЫЙ БЛОК: Неоновый Логотип */}
|
||||||
|
<Link
|
||||||
|
href={`/${lang}`}
|
||||||
|
className="flex items-center gap-3 group group-hover:scale-105 transition-transform duration-300"
|
||||||
|
>
|
||||||
|
<div className="text-2xl sm:text-3xl tracking-widest flex items-center -rotate-2 select-none font-bold">
|
||||||
|
{/* Используем классы из globals.css через шаблонную строку */}
|
||||||
|
<span
|
||||||
|
className={`transition-all duration-300 ${
|
||||||
|
mounted ? "text-neon-cyan animate-neon-flicker" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Net
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`transition-all duration-300 ${
|
||||||
|
mounted ? "text-neon-cyan" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
runner
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`ml-2 opacity-90 group-hover:opacity-100 transition-all duration-300 ${
|
||||||
|
mounted ? "text-neon-purple" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
VPN
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* ЦЕНТРАЛЬНЫЙ БЛОК: Навигация */}
|
||||||
|
<nav className="hidden md:flex items-center gap-8 text-sm font-bold text-muted-foreground">
|
||||||
|
<Link
|
||||||
|
href={`/${lang}#features`}
|
||||||
|
className="hover:text-primary transition-colors duration-200"
|
||||||
|
>
|
||||||
|
{dict.features}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={`/${lang}#pricing`}
|
||||||
|
className="hover:text-primary transition-colors duration-200"
|
||||||
|
>
|
||||||
|
{dict.pricing}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={`/${lang}#faq`}
|
||||||
|
className="hover:text-primary transition-colors duration-200"
|
||||||
|
>
|
||||||
|
{dict.faq}
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* ПРАВЫЙ БЛОК: Контролы */}
|
||||||
|
<div className="flex items-center gap-2 sm:gap-4">
|
||||||
|
<div className="flex items-center gap-1 sm:gap-2">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
<Button 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">
|
||||||
|
{dict.cta}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
"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";
|
||||||
|
|
||||||
|
// --- ФОНОВЫЙ ЭФФЕКТ МАТРИЦЫ ---
|
||||||
|
function MatrixBackground({
|
||||||
|
isSecure,
|
||||||
|
theme,
|
||||||
|
}: {
|
||||||
|
isSecure: boolean;
|
||||||
|
theme?: string;
|
||||||
|
}) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
let width = (canvas.width = window.innerWidth);
|
||||||
|
let height = (canvas.height = window.innerHeight);
|
||||||
|
|
||||||
|
const fontSize = 16;
|
||||||
|
const columns = Math.floor(width / fontSize);
|
||||||
|
const drops = Array(columns).fill(1);
|
||||||
|
|
||||||
|
const isDark = theme === "dark";
|
||||||
|
|
||||||
|
const draw = () => {
|
||||||
|
ctx.fillStyle = isDark
|
||||||
|
? "rgba(10, 10, 11, 0.1)"
|
||||||
|
: "rgba(250, 250, 250, 0.15)";
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// В светлой теме цвета темнее для контраста
|
||||||
|
const colorSecure = isDark ? "#00F0FF" : "#0284c7";
|
||||||
|
const colorInsecure = isDark ? "#7000FF" : "#6d28d9";
|
||||||
|
|
||||||
|
ctx.fillStyle = isSecure ? colorSecure : colorInsecure;
|
||||||
|
ctx.font = `${fontSize}px monospace`;
|
||||||
|
|
||||||
|
for (let i = 0; i < drops.length; i++) {
|
||||||
|
const text = Math.random() > 0.5 ? "1" : "0";
|
||||||
|
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
|
||||||
|
|
||||||
|
if (drops[i] * fontSize > height && Math.random() > 0.975) {
|
||||||
|
drops[i] = 0;
|
||||||
|
}
|
||||||
|
drops[i]++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const interval = setInterval(draw, 33);
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
width = canvas.width = window.innerWidth;
|
||||||
|
height = canvas.height = window.innerHeight;
|
||||||
|
};
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
};
|
||||||
|
}, [isSecure, theme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className="absolute inset-0 -z-20 opacity-30 dark:opacity-20 transition-opacity duration-1000"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- КОМПОНЕНТ ГЛАЗ ---
|
||||||
|
function WatcherEyes({ isClosed }: { isClosed: boolean }) {
|
||||||
|
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: any }) {
|
||||||
|
const [isVpnOn, setIsVpnOn] = useState(false);
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => setMounted(true), []);
|
||||||
|
|
||||||
|
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">
|
||||||
|
{/* Матричный фон */}
|
||||||
|
{mounted && <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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { CheckIcon } from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
export function Pricing({ dict }: { dict: any }) {
|
||||||
|
const [isYearly, setIsYearly] = useState(true);
|
||||||
|
|
||||||
|
// Комбинируем переводы с логикой цен
|
||||||
|
const plans = [
|
||||||
|
{ ...dict.plans[0], priceMonthly: "4", priceYearly: "7" },
|
||||||
|
{ ...dict.plans[1], priceMonthly: "8", priceYearly: "12", popular: true },
|
||||||
|
{ ...dict.plans[2], priceMonthly: "15", priceYearly: "20" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="pricing"
|
||||||
|
className="py-32 px-4 container mx-auto max-w-7xl relative"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_left,_var(--tw-gradient-stops))] from-secondary/10 via-transparent to-transparent -z-10" />
|
||||||
|
|
||||||
|
<div className="text-center mb-20 flex flex-col items-center">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="mb-4 border-primary/30 text-primary"
|
||||||
|
>
|
||||||
|
{dict.badge}
|
||||||
|
</Badge>
|
||||||
|
<h2 className="text-4xl md:text-5xl font-bold tracking-tighter mb-8">
|
||||||
|
{dict.title}
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center justify-center gap-4 bg-card border border-border/50 rounded-full p-2 backdrop-blur-sm">
|
||||||
|
<span
|
||||||
|
className={`text-sm font-medium px-4 ${!isYearly ? "text-foreground" : "text-muted-foreground"}`}
|
||||||
|
>
|
||||||
|
{dict.monthly}
|
||||||
|
</span>
|
||||||
|
<Switch checked={isYearly} onCheckedChange={setIsYearly} />
|
||||||
|
<span
|
||||||
|
className={`text-sm font-medium px-4 flex items-center gap-2 ${isYearly ? "text-foreground" : "text-muted-foreground"}`}
|
||||||
|
>
|
||||||
|
{dict.yearly}{" "}
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="bg-primary/10 text-primary rounded-full px-2 text-[10px]"
|
||||||
|
>
|
||||||
|
{dict.discount}
|
||||||
|
</Badge>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 items-stretch">
|
||||||
|
{plans.map((plan) => (
|
||||||
|
<Card
|
||||||
|
key={plan.name}
|
||||||
|
className={`relative flex flex-col transition-all duration-300 ${plan.popular ? "border-primary/50 shadow-2xl shadow-primary/10" : "border-border/50"}`}
|
||||||
|
>
|
||||||
|
{plan.popular && (
|
||||||
|
<div className="absolute top-0 right-10 -translate-y-1/2">
|
||||||
|
<Badge className="bg-primary text-primary-foreground rounded-full px-4 py-1 uppercase text-xs font-bold tracking-widest shadow-lg shadow-primary/30">
|
||||||
|
{dict.mostPopular}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-foreground">{plan.name}</CardTitle>
|
||||||
|
<CardDescription>{plan.description}</CardDescription>
|
||||||
|
<div className="mt-8 flex items-baseline gap-1">
|
||||||
|
<span className="text-5xl font-extrabold tracking-tighter text-primary">
|
||||||
|
${isYearly ? plan.priceYearly : plan.priceMonthly}
|
||||||
|
</span>
|
||||||
|
<span className="text-base font-medium text-muted-foreground">
|
||||||
|
{dict.perMonth}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ul className="space-y-4 pt-6 border-t border-border/30">
|
||||||
|
{plan.features.map((feature: string, i: number) => (
|
||||||
|
<li
|
||||||
|
key={i}
|
||||||
|
className="flex items-start gap-3.5 text-base text-foreground/90"
|
||||||
|
>
|
||||||
|
<div className="bg-primary/10 w-6 h-6 rounded-full flex items-center justify-center mt-0.5 border border-primary/20 shrink-0">
|
||||||
|
<CheckIcon
|
||||||
|
className="w-3.5 h-3.5 text-primary"
|
||||||
|
weight="bold"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="leading-tight">{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button
|
||||||
|
variant={plan.popular ? "default" : "outline"}
|
||||||
|
className="w-full rounded-2xl text-base h-12"
|
||||||
|
>
|
||||||
|
{plan.cta}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Globe } from "lucide-react";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
|
export function LanguageSwitcher() {
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const switchLanguage = (lang: string) => {
|
||||||
|
const currentPath = pathname.replace(/^\/[a-z]{2}/, "");
|
||||||
|
router.push(`/${lang}${currentPath === "" ? "/" : currentPath}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-full border-border bg-background/50 backdrop-blur-md"
|
||||||
|
>
|
||||||
|
<Globe className="h-[1.2rem] w-[1.2rem]" />
|
||||||
|
<span className="sr-only">Switch language</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => switchLanguage("en")}>
|
||||||
|
English
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => switchLanguage("ru")}>
|
||||||
|
Русский
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
ThemeProvider as NextThemesProvider,
|
||||||
|
ThemeProviderProps,
|
||||||
|
} from "next-themes";
|
||||||
|
|
||||||
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Moon, Sun } from "lucide-react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const { setTheme, theme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||||
|
className="rounded-full border-border bg-background/50 backdrop-blur-md"
|
||||||
|
>
|
||||||
|
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Accordion as AccordionPrimitive } from "radix-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { CaretDownIcon } from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
function Accordion({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Root
|
||||||
|
data-slot="accordion"
|
||||||
|
className={cn("flex w-full flex-col gap-3", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
data-slot="accordion-item"
|
||||||
|
className={cn(
|
||||||
|
"border border-border/50 bg-card/40 backdrop-blur-md rounded-2xl px-5 overflow-hidden transition-colors hover:border-border",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
className={cn(
|
||||||
|
"group/accordion-trigger flex flex-1 items-center justify-between py-4 text-left text-sm font-medium transition-all outline-none hover:text-primary [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<CaretDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
data-slot="accordion-content"
|
||||||
|
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"pb-4 pt-1 text-muted-foreground leading-relaxed",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { Slot } from "radix-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full border border-transparent px-3 py-1 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
|
outline:
|
||||||
|
"text-foreground border-border/50 bg-background/50 backdrop-blur-sm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: { variant: "default" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot.Root : "span";
|
||||||
|
return (
|
||||||
|
<Comp className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants };
|
||||||
+23
-34
@@ -1,65 +1,54 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { Slot } from "radix-ui"
|
import { Slot } from "radix-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"group/button inline-flex shrink-0 items-center justify-center rounded-2xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow-lg shadow-primary/20 hover:bg-primary/90",
|
||||||
outline:
|
outline:
|
||||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
"border-border bg-background/50 backdrop-blur-sm hover:bg-muted hover:text-foreground",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost:
|
ghost: "hover:bg-muted/50 hover:text-foreground",
|
||||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
"bg-destructive/10 text-destructive hover:bg-destructive/20",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default:
|
default: "h-11 px-6 py-2",
|
||||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
sm: "h-9 rounded-xl px-4 text-xs",
|
||||||
xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
lg: "h-14 rounded-3xl px-8 text-base",
|
||||||
sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
icon: "size-11 rounded-2xl",
|
||||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
||||||
icon: "size-8",
|
|
||||||
"icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3",
|
|
||||||
"icon-sm": "size-7 rounded-none",
|
|
||||||
"icon-lg": "size-9",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Button({
|
function Button({
|
||||||
className,
|
className,
|
||||||
variant = "default",
|
variant,
|
||||||
size = "default",
|
size,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"button"> &
|
}: React.ComponentProps<"button"> &
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
|
||||||
asChild?: boolean
|
const Comp = asChild ? Slot.Root : "button";
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot.Root : "button"
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
data-variant={variant}
|
|
||||||
data-size={size}
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants };
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"group/card flex flex-col rounded-[2.5rem] bg-card backdrop-blur-xl p-8 text-card-foreground ring-1 ring-border shadow-sm transition-all duration-300 hover:ring-border/80 hover:shadow-xl hover:shadow-primary/5",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
// Убираем горизонтальные отступы здесь, так как они уже есть в Card
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn("flex flex-col gap-1.5 pb-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("font-bold tracking-tight text-3xl", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-base text-muted-foreground pt-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("flex-1 pb-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
// CardFooter прозрачный, без верхней границы
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { CheckIcon, CaretRightIcon } from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
function DropdownMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
align = "start",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
align={align}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-32 overflow-hidden rounded-2xl border border-border/50 bg-popover/80 backdrop-blur-xl p-2 text-popover-foreground shadow-xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
variant?: "default" | "destructive";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-xl px-3 py-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||||
|
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||||
|
>
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||||
|
data-slot="dropdown-menu-radio-item-indicator"
|
||||||
|
>
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-2 text-xs text-muted-foreground data-inset:pl-7",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
className={cn("-mx-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<CaretRightIcon className="ml-auto" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-none bg-popover text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Switch as SwitchPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"features": "Features",
|
||||||
|
"pricing": "Pricing",
|
||||||
|
"faq": "FAQ",
|
||||||
|
"blog": "Comms",
|
||||||
|
"cta": "Get Netrunner"
|
||||||
|
},
|
||||||
|
"hero": {
|
||||||
|
"badge": "System watching you",
|
||||||
|
"badgeSecure": "Secure tunnel established",
|
||||||
|
"titleStart": "The system watches everyone,",
|
||||||
|
"titleHighlight": "except you.",
|
||||||
|
"subtitle": "Digital footprint erased. Logs destroyed. Uncapped velocity.",
|
||||||
|
"switchLabel": "Enable Netrunner VPN",
|
||||||
|
"ctaPrimary": "Download Client",
|
||||||
|
"ctaSecondary": "View Nodes"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "DPI Evasion & uTLS Obfuscation",
|
||||||
|
"description": "The Netrunner protocol masks your traffic as standard Chrome or Firefox HTTPS requests. Deep Packet Inspection (DPI) systems see only white noise, allowing you to bypass strict firewalls undetected."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "ChaCha20-Poly1305 Cryptography",
|
||||||
|
"description": "We utilize state-of-the-art stream encryption combined with Perfect Forward Secrecy via ECDH handshakes. Your session keys are generated dynamically, leaving zero vectors for compromise."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Adaptive Backpressure",
|
||||||
|
"description": "Buffer sizes dynamically scale based on your system's current RTT (ping). When connection quality drops, buffers shrink automatically to prevent latency spikes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Multi-Platform Core",
|
||||||
|
"description": "Our low-level Rust networking core with FFI bindings (uniffi) ensures flawless compatibility and zero overhead across iOS, Android, Windows, and macOS."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Native TUN Engine & FakeDNS",
|
||||||
|
"description": "Traffic is intercepted at the network level by our custom TUN interface. An integrated Fake IP resolver prevents DNS leaks, while multiplexed streams guarantee ultra-low latency."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"badge": "Pricing Protocols",
|
||||||
|
"title": "Choose Your Uplink Plan",
|
||||||
|
"monthly": "Monthly",
|
||||||
|
"yearly": "Yearly",
|
||||||
|
"discount": "-40%",
|
||||||
|
"perMonth": "/mo",
|
||||||
|
"mostPopular": "Most Popular",
|
||||||
|
"plans": [
|
||||||
|
{
|
||||||
|
"name": "Ghost",
|
||||||
|
"description": "Basic uplink for standard browsing and simple firewall bypass.",
|
||||||
|
"features": [
|
||||||
|
"1 Active Device",
|
||||||
|
"ChaCha20-Poly1305 Cipher",
|
||||||
|
"Standard Buffer Profiles",
|
||||||
|
"Unrestricted P2P Traffic"
|
||||||
|
],
|
||||||
|
"cta": "Select Ghost Plan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Netrunner",
|
||||||
|
"description": "Advanced obfuscation for strict networks and Deep Packet Inspection.",
|
||||||
|
"features": [
|
||||||
|
"5 Active Devices",
|
||||||
|
"uTLS Browser Mimicry",
|
||||||
|
"Multiplexed Tunnels (Muxer)",
|
||||||
|
"Adaptive Backpressure",
|
||||||
|
"FakeDNS Leak Protection"
|
||||||
|
],
|
||||||
|
"cta": "Initiate Netrunner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Overlord",
|
||||||
|
"description": "Maximum throughput and dedicated resources for heavy grid users.",
|
||||||
|
"features": [
|
||||||
|
"10 Active Devices",
|
||||||
|
"Dedicated Static IP",
|
||||||
|
"Heavy Network Buffer Profiles",
|
||||||
|
"Raw L3 TUN Access",
|
||||||
|
"24/7 Priority Support"
|
||||||
|
],
|
||||||
|
"cta": "Select Overlord Plan"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"faq": {
|
||||||
|
"title": "System Queries (FAQ)",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"q": "What is the Netrunner protocol?",
|
||||||
|
"a": "It is our custom network protocol built from the ground up using Rust. Unlike standard solutions, it obfuscates traffic to look like regular HTTPS, making it nearly invisible to Deep Packet Inspection (DPI) systems and allowing it to bypass even the toughest network restrictions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "Do you store any logs or user data?",
|
||||||
|
"a": "No. We operate under a strict No-Logs policy. Our system is engineered so that session metadata is never written to persistent storage, and temporary identifiers exist only in volatile memory during an active connection."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "Will using NRXP affect my internet speed?",
|
||||||
|
"a": "Thanks to the Netrunner architecture, which minimizes encryption overhead, latency (ping) remains minimal. In some cases, you may even notice a speed increase if your ISP throttles traffic to specific services."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "Is P2P and Torrenting allowed?",
|
||||||
|
"a": "Yes, we do not restrict any traffic types. All our nodes support high-speed data transfer with no bandwidth caps or data limits."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "Which devices support Netrunner?",
|
||||||
|
"a": "We are building a cross-platform application using the Tauri framework, providing native support for Windows, macOS, and Linux. Mobile versions are currently in active development."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "Is it safe to pay with cryptocurrency?",
|
||||||
|
"a": "Absolutely. We have integrated a custom crypto-billing system, allowing you to purchase subscriptions completely anonymously without linking any credit cards."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"blog": {
|
||||||
|
"title": "Comm-Link Updates",
|
||||||
|
"empty": "No signals found."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"features": "Возможности",
|
||||||
|
"pricing": "Тарифы",
|
||||||
|
"faq": "Вопросы",
|
||||||
|
"blog": "Связь",
|
||||||
|
"cta": "Скачать Netrunner"
|
||||||
|
},
|
||||||
|
"hero": {
|
||||||
|
"badge": "Система следит за вами",
|
||||||
|
"badgeSecure": "Защищенный туннель установлен",
|
||||||
|
"titleStart": "Система следит за всеми,",
|
||||||
|
"titleHighlight": "кроме тебя.",
|
||||||
|
"subtitle": "Цифровой след скрыт. Логи уничтожены. Скорость не ограничена.",
|
||||||
|
"switchLabel": "Включить Netrunner VPN",
|
||||||
|
"ctaPrimary": "Скачать клиент",
|
||||||
|
"ctaSecondary": "Список узлов"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Обход DPI и uTLS-обфускация",
|
||||||
|
"description": "Протокол Netrunner маскирует трафик под обычные HTTPS-запросы Chrome или Firefox. Системы глубокого анализа пакетов (DPI) видят лишь белый шум, что позволяет обходить самые жесткие файрволы."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Криптография ChaCha20-Poly1305",
|
||||||
|
"description": "Вместо устаревших стандартов мы используем высокоскоростной шифр потоковой передачи с Perfect Forward Secrecy на базе ECDH. Ваши ключи сессий меняются динамически."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Адаптивный Backpressure",
|
||||||
|
"description": "Размер буфера динамически масштабируется в зависимости от текущего RTT (пинга) системы. При ухудшении связи буфер автоматически сжимается для предотвращения задержек."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Мультиплатформенное ядро",
|
||||||
|
"description": "Низкоуровневое сетевое ядро на Rust с FFI-привязками (uniffi) гарантирует идеальную совместимость и нулевой оверхед на iOS, Android, Windows и macOS."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "TUN-движок и защита FakeDNS",
|
||||||
|
"description": "Трафик перехватывается на сетевом уровне локальным TUN-интерфейсом. Встроенный резолвер с Fake IP блокирует любые утечки DNS, а мультиплексирование потоков гарантирует минимальный пинг."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"badge": "Тарифные протоколы",
|
||||||
|
"title": "Выберите ваш Uplink",
|
||||||
|
"monthly": "Ежемесячно",
|
||||||
|
"yearly": "Ежегодно",
|
||||||
|
"discount": "-40%",
|
||||||
|
"perMonth": "/мес",
|
||||||
|
"mostPopular": "Рекомендуемый",
|
||||||
|
"plans": [
|
||||||
|
{
|
||||||
|
"name": "Ghost",
|
||||||
|
"description": "Базовый канал для серфинга и обхода простых блокировок.",
|
||||||
|
"features": [
|
||||||
|
"1 активное устройство",
|
||||||
|
"Шифрование ChaCha20-Poly1305",
|
||||||
|
"Стандартные сетевые буферы",
|
||||||
|
"P2P трафик без ограничений"
|
||||||
|
],
|
||||||
|
"cta": "Выбрать Ghost"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Netrunner",
|
||||||
|
"description": "Продвинутая обфускация для жестких сетей и систем DPI.",
|
||||||
|
"features": [
|
||||||
|
"До 5 устройств одновременно",
|
||||||
|
"uTLS-мимикрия (под Chrome/Firefox)",
|
||||||
|
"Мультиплексирование туннелей",
|
||||||
|
"Адаптивный буфер (низкий пинг)",
|
||||||
|
"Встроенная защита FakeDNS"
|
||||||
|
],
|
||||||
|
"cta": "Активировать Netrunner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Overlord",
|
||||||
|
"description": "Максимальная пропускная способность и выделенные ресурсы.",
|
||||||
|
"features": [
|
||||||
|
"10 устройств",
|
||||||
|
"Выделенный статический IP",
|
||||||
|
"Тяжелые профили буферов (Heavy)",
|
||||||
|
"Прямой доступ к L3 TUN-интерфейсу",
|
||||||
|
"Приоритетный саппорт 24/7"
|
||||||
|
],
|
||||||
|
"cta": "Выбрать Overlord"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"title": "Системные запросы (FAQ)",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"q": "Что такое протокол Netrunner?",
|
||||||
|
"a": "Это наш кастомный сетевой протокол, разработанный с нуля на языке Rust. В отличие от стандартных решений, он имитирует обычный HTTPS-трафик, что делает его практически невидимым для систем глубокого анализа пакетов (DPI) и позволяет обходить самые сложные сетевые ограничения."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "Хранятся ли мои данные или логи?",
|
||||||
|
"a": "Нет. Мы придерживаемся строгой политики отсутствия логов (No-Logs). Наша система спроектирована так, что метаданные сессий не записываются на постоянные носители, а временные идентификаторы существуют только в оперативной памяти во время активного соединения."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "Повлияет ли использование NRXP на скорость интернета?",
|
||||||
|
"a": "Благодаря архитектуре Netrunner, минимизирующей накладные расходы на шифрование, задержки (ping) остаются минимальными. В ряде случаев скорость может даже вырасти, если ваш провайдер ограничивает трафик к определенным ресурсам."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "Могу ли я использовать P2P и торренты?",
|
||||||
|
"a": "Да, мы не ограничиваем типы трафика. Все наши узлы поддерживают высокоскоростную передачу данных без лимитов на объем скачиваемой информации."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "На каких устройствах работает Netrunner?",
|
||||||
|
"a": "Мы разрабатываем кроссплатформенное приложение на базе Tauri, которое обеспечит нативную поддержку Windows, macOS и Linux. Мобильные версии также находятся в активной разработке."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "Безопасно ли использовать криптовалюту для оплаты?",
|
||||||
|
"a": "Абсолютно. Мы интегрировали собственную систему верификации крипто-транзакций, что позволяет вам приобретать подписку полностью анонимно, без привязки банковских карт."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"blog": {
|
||||||
|
"title": "Обновления Comm-Link",
|
||||||
|
"empty": "Сигналы не найдены."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
const dictionaries = {
|
||||||
|
en: () => import("@/dictionaries/en.json").then((module) => module.default),
|
||||||
|
ru: () => import("@/dictionaries/ru.json").then((module) => module.default),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Исправляем типизацию и доступ
|
||||||
|
export const getDictionary = async (locale: string) => {
|
||||||
|
// Проверяем, существует ли локаль в нашем списке, иначе отдаем дефолтную 'en'
|
||||||
|
const loader =
|
||||||
|
dictionaries[locale as keyof typeof dictionaries] || dictionaries.en;
|
||||||
|
return loader();
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
export interface Post {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
cover?: string;
|
||||||
|
slug: string;
|
||||||
|
publishedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STRAPI_URL =
|
||||||
|
process.env.NEXT_PUBLIC_STRAPI_API_URL || "http://localhost:1337";
|
||||||
|
|
||||||
|
export async function fetchPosts(): Promise<Post[]> {
|
||||||
|
try {
|
||||||
|
// Пример запроса. Раскомментируйте, когда Strapi будет запущен
|
||||||
|
// const res = await fetch(`${STRAPI_URL}/api/posts?populate=*`, {
|
||||||
|
// next: { revalidate: 3600 }
|
||||||
|
// });
|
||||||
|
// if (!res.ok) throw new Error("Failed to fetch posts");
|
||||||
|
// const data = await res.json();
|
||||||
|
// return data.data;
|
||||||
|
|
||||||
|
// Моковые данные для разработки:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Netrunner Protocol V2: What's New?",
|
||||||
|
description: "Explore the next generation of zero-knowledge routing.",
|
||||||
|
slug: "netrunner-v2",
|
||||||
|
publishedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Why Military-Grade Encryption Matters",
|
||||||
|
description: "How AES-256 and ChaCha20 secure your digital footprint.",
|
||||||
|
slug: "encryption-matters",
|
||||||
|
publishedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Strapi fetch error:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// src/middleware.ts
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
const locales = ["en", "ru"];
|
||||||
|
const defaultLocale = "en";
|
||||||
|
|
||||||
|
export function middleware(request: NextRequest) {
|
||||||
|
const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
|
// Проверяем, есть ли уже локаль в URL (например, /ru/pricing или /en)
|
||||||
|
const pathnameHasLocale = locales.some(
|
||||||
|
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pathnameHasLocale) return;
|
||||||
|
|
||||||
|
// Если локали нет, перенаправляем на дефолтную (например, / -> /en)
|
||||||
|
request.nextUrl.pathname = `/${defaultLocale}${pathname}`;
|
||||||
|
return NextResponse.redirect(request.nextUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
// Пропускаем внутренние пути Next.js и статику, чтобы middleware их не трогал
|
||||||
|
"/((?!_next|api|favicon.ico|images|.*\\..*).*)",
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -12,12 +12,16 @@
|
|||||||
"@phosphor-icons/react": "^2.1.10",
|
"@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",
|
||||||
|
"framer-motion": "^12.38.0",
|
||||||
|
"lucide-react": "^1.8.0",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"shadcn": "^4.3.0",
|
"shadcn": "^4.3.0",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tw-animate-css": "^1.4.0"
|
"tw-animate-css": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Generated
+7802
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
darkMode: "class",
|
||||||
|
content: [
|
||||||
|
"./pages/**/*.{ts,tsx}",
|
||||||
|
"./components/**/*.{ts,tsx}",
|
||||||
|
"./app/**/*.{ts,tsx}",
|
||||||
|
"./src/**/*.{ts,tsx}",
|
||||||
|
],
|
||||||
|
prefix: "",
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
background: "#0A0A0B", // Глубокий черный
|
||||||
|
foreground: "#E2E8F0", // Основной текст
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "#00F0FF", // Electric Cyan
|
||||||
|
foreground: "#0A0A0B",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "#7000FF", // Cyber Purple
|
||||||
|
foreground: "#E2E8F0",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "#1E293B",
|
||||||
|
foreground: "#94A3B8", // Второстепенный текст
|
||||||
|
},
|
||||||
|
border: "#1E293B",
|
||||||
|
card: {
|
||||||
|
DEFAULT: "rgba(30, 41, 59, 0.3)", // Полупрозрачный фон карточек
|
||||||
|
foreground: "#E2E8F0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
"pulse-cyan": "pulse-cyan 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: "0" },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: "0" },
|
||||||
|
},
|
||||||
|
"pulse-cyan": {
|
||||||
|
"0%, 100%": {
|
||||||
|
opacity: "1",
|
||||||
|
boxShadow: "0 0 10px #00F0FF, 0 0 20px #00F0FF",
|
||||||
|
},
|
||||||
|
"50%": {
|
||||||
|
opacity: ".5",
|
||||||
|
boxShadow: "0 0 5px #00F0FF, 0 0 10px #00F0FF",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
} satisfies Config;
|
||||||
|
|
||||||
|
export default config;
|
||||||
Reference in New Issue
Block a user