initial commit

This commit is contained in:
2026-04-17 17:06:55 +07:00
parent fab77ed970
commit 6404495696
28 changed files with 9704 additions and 224 deletions
+40
View File
@@ -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>
);
}
+34
View File
@@ -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
View File
@@ -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; /* Момент "затухания" лампы */
} }
} }
-36
View File
@@ -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>
);
}
-65
View File
@@ -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>
);
}
+43
View File
@@ -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>
);
}
+30
View File
@@ -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>
);
}
+142
View File
@@ -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>
);
}
+89
View File
@@ -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>
);
}
+295
View File
@@ -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>
);
}
+121
View File
@@ -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>
);
}
+82
View File
@@ -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 };
+37
View File
@@ -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
View File
@@ -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 };
+76
View File
@@ -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,
};
+270
View File
@@ -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,
};
+33
View File
@@ -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 }
+123
View File
@@ -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."
}
}
+122
View File
@@ -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": "Сигналы не найдены."
}
}
+14
View File
@@ -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();
};
+44
View File
@@ -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 [];
}
}
+28
View File
@@ -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|.*\\..*).*)",
],
};
+4
View File
@@ -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": {
+7802
View File
File diff suppressed because it is too large Load Diff
+72
View File
@@ -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;