Files
netrunner-landing/repomix-output.xml
T

5921 lines
277 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file is a merged representation of the entire codebase, combined into a single document by Repomix.
<file_summary>
This section contains a summary of this file.
<purpose>
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.
</purpose>
<file_format>
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Repository files (if enabled)
5. Multiple file entries, each consisting of:
- File path as an attribute
- Full contents of the file
</file_format>
<usage_guidelines>
- This file should be treated as read-only. Any changes should be made to the
original repository files, not this packed version.
- When processing this file, use the file path to distinguish
between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
the same level of security as you would the original repository.
</usage_guidelines>
<notes>
- Some files may have been excluded based on .gitignore rules and Repomix's configuration
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
- Files matching patterns in .gitignore are excluded
- Files matching default ignore patterns are excluded
- Files are sorted by Git change count (files with more changes are at the bottom)
</notes>
</file_summary>
<directory_structure>
.github/
workflows/
deploy.yml
app/
[lang]/
blog/
[slug]/
page.tsx
page.tsx
docs/
docs-client.tsx
page.tsx
download/
download-client.tsx
page.tsx
nodes/
nodes-client.tsx
page.tsx
layout.tsx
loading.tsx
page.tsx
favicon.ico
globals.css
not-found.tsx
components/
sections/
lang/
language-switcher.tsx
theme/
theme-provider.tsx
theme-toggle.tsx
BlogPreview.tsx
Faq.tsx
Features.tsx
Footer.tsx
Header.tsx
Hero.tsx
MatrixBackground.tsx
Pricing.tsx
TerminalGuestbook.tsx
ui/
accordion.tsx
badge.tsx
button.tsx
card.tsx
cyber-button.tsx
dropdown-menu.tsx
switch.tsx
dictionaries/
en.json
ru.json
lib/
dictionaries.ts
document.ts
IDict.ts
pb.ts
utils.ts
proxy-ws/
src/
main.rs
Cargo.toml
public/
logo.svg
.dockerignore
.gitignore
AGENTS.md
CLAUDE.md
collect_code.js
components.json
docker-compose.yml
Dockerfile
ecosystem.config.js
eslint.config.mjs
next.config.ts
package.json
postcss.config.mjs
proxy.ts
README.md
tailwind.config.ts
tsconfig.json
upload.mjs
</directory_structure>
<files>
This section contains the contents of the repository's files.
<file path="proxy-ws/src/main.rs">
use axum::{Json, Router, http::StatusCode, response::IntoResponse, routing::post};
use regex::Regex;
use reqwest::Client;
use rustrict::CensorStr;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
// 1. Описываем, что ждем от Next.js
#[derive(Deserialize, Serialize, Debug)]
struct CommentPayload {
nickname: String,
message: String,
}
// 2. Статичный Regex для кастомного мусора (русские маты, наркотики, насилие и т.д.)
// LazyLock гарантирует, что регулярка скомпилируется один раз при старте сервера
static BAD_WORDS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(порно|наркотик|убить|насилие|heroin|meth|murder|gore|соли|закладки)").unwrap()
});
#[tokio::main]
async fn main() {
// Собираем роуты. Здесь будет и твой WebSocket, и REST методы
let app = Router::new()
// .route("/ws", get(ws_handler)) // Твой будущий вебсокет
.route("/api/comments", post(create_comment));
println!("🚀 Netrunner Gateway started on 0.0.0.0:3001");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
// 3. Хендлер: проверяет и пересылает
async fn create_comment(Json(payload): Json<CommentPayload>) -> impl IntoResponse {
// --- ЭТАП 1: ЖЕСТКИЙ ФИЛЬТР ---
// Проверка через встроенный крейт (отлично кроет английский мусор и обходные написания типа f*ck)
if payload.message.is_inappropriate() || payload.nickname.is_inappropriate() {
println!("🛡️ Blocked by rustrict: {}", payload.nickname);
return (
StatusCode::NOT_ACCEPTABLE,
"Inappropriate content detected.",
);
}
// Проверка по нашему кастомному словарю
if BAD_WORDS_REGEX.is_match(&payload.message) || BAD_WORDS_REGEX.is_match(&payload.nickname) {
println!("🛡️ Blocked by custom regex: {}", payload.nickname);
return (StatusCode::NOT_ACCEPTABLE, "Content policy violation.");
}
// --- ЭТАП 2: ОТПРАВКА В POCKETBASE ---
let client = Client::new();
let pb_url = "http://netrunner-cms:8090/api/collections/comments/records";
// Пересылаем чистый JSON в PocketBase
let pb_response = client.post(pb_url).json(&payload).send().await;
match pb_response {
Ok(res) if res.status().is_success() => {
(StatusCode::OK, "Transmission secured and delivered.")
}
Ok(res) => {
println!("⚠️ PocketBase rejected: {:?}", res.status());
(StatusCode::BAD_REQUEST, "Database rejected transmission.")
}
Err(e) => {
println!("💥 PB Connection error: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Uplink offline.")
}
}
}
</file>
<file path="proxy-ws/Cargo.toml">
[package]
name = "proxy-ws"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rustrict = "0.7" # Готовый быстрый фильтр (в основном англ)
regex = "1.10" # Для кастомного словаря треша
</file>
<file path="app/[lang]/docs/page.tsx">
import { getDictionary } from "@/lib/dictionaries";
import { DocsClient } from "./docs-client";
export default async function DocsPage({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
const dict = await getDictionary(lang);
// Передаем весь словарь или нужную часть, плюс текущий язык
return <DocsClient lang={lang} />;
}
</file>
<file path="app/[lang]/nodes/nodes-client.tsx">
"use client";
import { motion } from "framer-motion";
import { Activity, Globe2, Server } from "lucide-react";
import { Dictionary } from "@/lib/IDict";
// Моковые данные серверов
const MOCK_NODES = [
{
id: "eu-1",
region: "Europe",
country: "Germany",
city: "Frankfurt",
flag: "🇩🇪",
ping: 24,
load: 35,
status: "online",
},
{
id: "eu-2",
region: "Europe",
country: "Netherlands",
city: "Amsterdam",
flag: "🇳🇱",
ping: 31,
load: 88,
status: "online",
},
{
id: "eu-3",
region: "Europe",
country: "Switzerland",
city: "Zurich",
flag: "🇨🇭",
ping: 45,
load: 100,
status: "full",
},
{
id: "us-1",
region: "Americas",
country: "United States",
city: "New York",
flag: "🇺🇸",
ping: 112,
load: 45,
status: "online",
},
{
id: "us-2",
region: "Americas",
country: "Canada",
city: "Toronto",
flag: "🇨🇦",
ping: 120,
load: 12,
status: "online",
},
{
id: "as-1",
region: "Asia",
country: "Singapore",
city: "Singapore",
flag: "🇸🇬",
ping: 185,
load: 0,
status: "maintenance",
},
{
id: "as-2",
region: "Asia",
country: "Japan",
city: "Tokyo",
flag: "🇯🇵",
ping: 160,
load: 60,
status: "online",
},
];
export function NodesClient({ dict }: { dict: Dictionary["nodes"] }) {
// Вспомогательная функция для получения статуса
const getStatusText = (status: string) => {
switch (status) {
case "online":
return dict.status.online;
case "full":
return dict.status.full;
case "maintenance":
return dict.status.maintenance;
default:
return status;
}
};
return (
<div className="min-h-screen pt-32 pb-24 px-4 container mx-auto max-w-5xl relative">
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-[100vw] md:w-[600px] h-[400px] bg-secondary/5 blur-[120px] rounded-full pointer-events-none -z-10" />
<div className="text-center mb-16">
<h1 className="text-4xl md:text-5xl font-extrabold tracking-tight mb-4 text-foreground flex items-center justify-center gap-4">
<Globe2 className="w-10 h-10 text-primary" />
{dict.title}
</h1>
<p className="text-muted-foreground text-lg max-w-2xl mx-auto font-mono">
{dict.subtitle}
</p>
</div>
<motion.div
initial="hidden"
animate="show"
variants={{
hidden: { opacity: 0 },
show: { opacity: 1, transition: { staggerChildren: 0.1 } },
}}
className="grid grid-cols-1 md:grid-cols-2 gap-6"
>
{MOCK_NODES.map((node) => (
<motion.div
key={node.id}
variants={{
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0, transition: { type: "spring" } },
}}
className={`bg-card/40 backdrop-blur-xl border rounded-[2rem] p-6 relative overflow-hidden transition-all duration-300 ${
node.status === "online"
? "border-border/50 hover:border-primary/50 hover:shadow-[0_0_30px_rgba(0,240,255,0.1)]"
: node.status === "full"
? "border-secondary/30 opacity-80"
: "border-destructive/30 opacity-60 grayscale-[50%]"
}`}
>
<div className="flex justify-between items-start mb-6">
<div className="flex items-center gap-3">
<span className="text-4xl drop-shadow-md">{node.flag}</span>
<div>
<h3 className="text-xl font-bold text-foreground">
{node.city}
</h3>
<p className="text-sm text-muted-foreground font-mono">
{node.country}
</p>
</div>
</div>
<div className="flex flex-col items-end">
<span
className={`text-xs font-bold uppercase tracking-wider px-3 py-1 rounded-full border ${
node.status === "online"
? "bg-primary/10 text-primary border-primary/20"
: node.status === "full"
? "bg-secondary/10 text-secondary border-secondary/20"
: "bg-destructive/10 text-destructive border-destructive/20"
}`}
>
{getStatusText(node.status)}
</span>
<span className="text-[10px] text-muted-foreground font-mono mt-2">
ID: {node.id.toUpperCase()}
</span>
</div>
</div>
<div className="space-y-4">
{/* Load Bar */}
<div>
<div className="flex justify-between text-xs font-mono mb-1">
<span className="text-muted-foreground flex items-center gap-1">
<Server className="w-3 h-3" /> {dict.labels.load}
</span>
<span
className={
node.load > 80 ? "text-secondary" : "text-primary"
}
>
{node.load}%
</span>
</div>
<div className="w-full bg-background/50 rounded-full h-2 overflow-hidden border border-border/50">
<div
className={`h-full rounded-full transition-all duration-1000 ${node.load > 80 ? "bg-secondary shadow-[0_0_10px_#7000FF]" : "bg-primary shadow-[0_0_10px_#00F0FF]"}`}
style={{ width: `${node.load}%` }}
/>
</div>
</div>
{/* Ping */}
<div className="flex justify-between items-center pt-2 border-t border-border/20">
<span className="text-xs text-muted-foreground font-mono flex items-center gap-1">
<Activity className="w-3 h-3" /> {dict.labels.latency}
</span>
<span className="text-sm font-bold font-mono text-foreground">
{node.status === "maintenance" ? "---" : `${node.ping} ms`}
</span>
</div>
</div>
</motion.div>
))}
</motion.div>
</div>
);
}
</file>
<file path="app/[lang]/nodes/page.tsx">
// app/[lang]/nodes/page.tsx
import { getDictionary } from "@/lib/dictionaries";
import { NodesClient } from "./nodes-client";
export default async function NodesPage({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
const dict = await getDictionary(lang);
return <NodesClient dict={dict.nodes} />;
}
</file>
<file path="app/[lang]/loading.tsx">
import { Loader2 } from "lucide-react";
export default function Loading() {
return (
<div className="fixed inset-0 z-[100] flex flex-col items-center justify-center bg-background text-primary">
<div className="relative">
<div className="absolute inset-0 bg-primary/20 blur-xl rounded-full" />
<Loader2 className="w-12 h-12 animate-spin relative z-10 drop-shadow-[0_0_15px_rgba(0,240,255,0.8)]" />
</div>
<div className="mt-8 flex flex-col items-center gap-2 font-mono">
<p className="text-sm font-bold uppercase tracking-[0.3em] animate-pulse">
Establishing connection
</p>
<div className="flex gap-2">
<div
className="w-1 h-1 bg-primary rounded-full animate-bounce"
style={{ animationDelay: "0ms" }}
/>
<div
className="w-1 h-1 bg-primary rounded-full animate-bounce"
style={{ animationDelay: "150ms" }}
/>
<div
className="w-1 h-1 bg-primary rounded-full animate-bounce"
style={{ animationDelay: "300ms" }}
/>
</div>
</div>
</div>
);
}
</file>
<file path="app/not-found.tsx">
import Link from "next/link";
import { Terminal, Home } from "lucide-react";
import { Button } from "@/components/ui/button";
import "@/app/globals.css"; // Убедись, что путь правильный
export default function NotFound() {
return (
<html lang="en">
<body className="bg-[#0A0A0B] text-[#E2E8F0] font-mono">
<div className="min-h-screen flex flex-col items-center justify-center px-4 relative overflow-hidden">
{/* Декоративный фон */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[80vw] h-[80vw] max-w-[600px] max-h-[600px] bg-red-500/10 blur-[120px] rounded-full -z-10" />
<div className="flex flex-col items-center text-center z-10">
<Terminal className="w-20 h-20 text-red-500 mb-8 animate-pulse" />
<h1 className="text-8xl font-black tracking-tighter mb-4">404</h1>
<div className="bg-red-500/10 border border-red-500/30 text-red-500 px-6 py-2 rounded-lg text-sm uppercase tracking-widest mb-12">
[ Error: Node Not Found ]
</div>
<p className="text-slate-400 mb-12 max-w-md leading-relaxed">
The routing table does not contain the requested node. The
transmission was dropped. Return to the secure network.
</p>
<Button
asChild
className="gap-2 rounded-2xl px-8 h-14 bg-white text-black hover:bg-slate-200 transition-colors"
>
<Link href="/">
<Home className="size-4" />
Return Home
</Link>
</Button>
</div>
</div>
</body>
</html>
);
}
</file>
<file path="components/sections/lang/language-switcher.tsx">
"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>
);
}
</file>
<file path="components/sections/theme/theme-provider.tsx">
"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>;
}
</file>
<file path="components/sections/theme/theme-toggle.tsx">
"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>
);
}
</file>
<file path="components/ui/badge.tsx">
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 };
</file>
<file path="components/ui/card.tsx">
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,
};
</file>
<file path="components/ui/cyber-button.tsx">
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
interface CyberButtonProps {
href?: string;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
className?: string;
variant?: "primary" | "secondary";
disabled?: boolean;
type?: "button" | "submit" | "reset";
}
export function CyberButton({
href,
onClick,
children,
className,
variant = "primary",
disabled = false,
type = "button",
}: CyberButtonProps) {
const isPrimary = variant === "primary";
const content = (
<motion.div
whileHover={!disabled ? { scale: 1.02 } : {}}
whileTap={!disabled ? { scale: 0.98 } : {}}
className={cn(
"relative z-10 flex items-center justify-center px-8 py-4 overflow-hidden transition-all duration-300 border rounded-xl font-mono",
// --- ACTIVE STATE ---
!disabled && [
isPrimary
? "bg-[#8B3DFF] border-[#8B3DFF] text-white shadow-lg shadow-purple-500/20 dark:bg-[radial-gradient(circle_at_center,_rgba(139,61,255,0.4)_0%,_rgba(139,61,255,0.1)_100%)] dark:border-[#8B3DFF]/50 dark:shadow-[0_0_25px_rgba(139,61,255,0.3)] dark:after:absolute dark:after:inset-0 dark:after:shadow-[inset_0_0_15px_rgba(139,61,255,0.2)]"
: "text-[#00E5F2] border-[#00E5F2]/40 backdrop-blur-md bg-slate-900/5 border-[#00E5F2]/50 shadow-sm dark:bg-[#00E5F2]/5 dark:border-[#00E5F2]/30 dark:shadow-[0_0_15px_rgba(0,229,242,0.1)] hover:border-[#00E5F2] hover:shadow-[0_0_20px_rgba(0,229,242,0.3)]",
],
// --- DISABLED STATE (Исправленный) ---
disabled && [
"cursor-not-allowed border-dashed shadow-none",
// Светлая тема: делаем кнопку матовой и "холодной"
"bg-slate-100 border-slate-300 text-slate-400",
// Темная тема: делаем кнопку глубокой и "выключенной"
"dark:bg-slate-900/40 dark:border-slate-800 dark:text-slate-600",
],
className,
)}
>
{/* CRT Сетка (только когда активна) */}
{!isPrimary && !disabled && (
<div className="absolute inset-0 opacity-[0.03] dark:opacity-10 pointer-events-none bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,229,242,0.1)_50%),linear-gradient(90deg,rgba(255,0,0,0.02),rgba(0,255,0,0.01),rgba(0,0,255,0.02))] bg-[length:100%_4px,3px_100%]" />
)}
{/* Энергетический блик (только когда активна) */}
{!disabled && (
<div
className={cn(
"absolute inset-0 w-full h-full -translate-x-full group-hover:animate-scan pointer-events-none",
isPrimary
? "bg-gradient-to-r from-transparent via-white/20 to-transparent"
: "bg-gradient-to-r from-transparent via-[#00E5F2]/20 to-transparent",
)}
/>
)}
{/* Угловые элементы (⌜ ⌟) - прячем или приглушаем при disabled */}
<div
className={cn(
"absolute inset-0 pointer-events-none transition-opacity",
disabled ? "opacity-20" : "opacity-60",
)}
>
<span
className={cn(
"absolute top-1 left-1 text-[10px] font-mono",
isPrimary ? "text-white" : "text-[#00E5F2]",
)}
>
</span>
<span
className={cn(
"absolute bottom-1 right-1 text-[10px] font-mono",
isPrimary ? "text-white" : "text-[#00E5F2]",
)}
>
</span>
</div>
{/* Анимированная полоска заряда (только для Secondary и когда активна) */}
{!isPrimary && !disabled && (
<motion.div
initial={{ height: 0 }}
whileHover={{ height: "70%" }}
className="absolute left-0 w-[2px] bg-[#00E5F2] shadow-[0_0_10px_#00E5F2] transition-all"
/>
)}
{/* Текст */}
<span
className={cn(
"relative z-20 font-black uppercase tracking-[0.2em] text-sm flex items-center gap-2",
!isPrimary &&
!disabled &&
"drop-shadow-[0_0_5px_rgba(0,229,242,0.5)]",
disabled && "drop-shadow-none",
)}
>
<span className="opacity-50">[</span>
{children}
<span className="opacity-50">]</span>
</span>
</motion.div>
);
// Фоновое облако свечения (только когда активна)
const glowEffect = !disabled && (
<div
className={cn(
"absolute inset-0 blur-2xl opacity-0 group-hover:opacity-40 transition-opacity duration-500 -z-10",
isPrimary ? "bg-[#8B3DFF]" : "bg-[#00E5F2]/40",
)}
/>
);
if (href && !disabled) {
return (
<Link href={href} className="group relative inline-block">
{content}
{glowEffect}
</Link>
);
}
return (
<button
type={type}
disabled={disabled}
onClick={onClick}
className="group relative inline-block bg-transparent border-none p-0 outline-none cursor-pointer"
>
{content}
{glowEffect}
</button>
);
}
</file>
<file path="components/ui/switch.tsx">
"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 }
</file>
<file path="lib/dictionaries.ts">
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();
};
</file>
<file path="lib/document.ts">
export interface PBDocument {
id: string;
collectionId: string;
collectionName: string;
created: string;
updated: string;
// Твои кастомные поля из PocketBase:
title: string;
category: "Core" | "Security" | "Network" | "Legal" | "All";
status: "Encrypted" | "Public" | "Dynamic" | "Internal";
content: string;
date: string; // можно использовать системное 'created', если не нужно ручное
file?: string; // если будешь прикреплять файлы
}
</file>
<file path="lib/utils.ts">
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
</file>
<file path="public/logo.svg">
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_57_121)">
<rect width="1024" height="1024" fill="black"/>
<path d="M54 109C49.5333 109 45.6333 108.133 42.3 106.4C39.0333 104.6 36.5 102.1 34.7 98.9C32.9 95.6333 32 91.8333 32 87.5V55.5C32 51.1 32.9 47.3 34.7 44.1C36.5 40.9 39.0333 38.4333 42.3 36.7C45.6333 34.9 49.5333 34 54 34C58.5333 34 62.4333 34.9 65.7 36.7C68.9667 38.4333 71.5 40.9 73.3 44.1C75.1 47.3 76 51.1 76 55.5V87.5C76 91.8333 75.1 95.6333 73.3 98.9C71.5 102.1 68.9333 104.6 65.6 106.4C62.3333 108.133 58.4667 109 54 109ZM54 101.3C58 101.3 61.2 100.033 63.6 97.5C66.0667 94.9 67.3 91.5667 67.3 87.5V55.5C67.3 51.4333 66.0667 48.1333 63.6 45.6C61.2 43 58 41.7 54 41.7C50 41.7 46.7667 43 44.3 45.6C41.9 48.1333 40.7 51.4333 40.7 55.5V87.5C40.7 91.5667 41.9 94.9 44.3 97.5C46.7667 100.033 50 101.3 54 101.3ZM54 77.4C52.2 77.4 50.7333 76.8333 49.6 75.7C48.5333 74.5667 48 73.0667 48 71.2C48 69.4 48.5333 67.9667 49.6 66.9C50.7333 65.7667 52.2 65.2 54 65.2C55.8 65.2 57.2333 65.7667 58.3 66.9C59.4333 67.9667 60 69.4 60 71.2C60 73.0667 59.4333 74.5667 58.3 75.7C57.2333 76.8333 55.8 77.4 54 77.4ZM33 198V189.8H52.8V132.5L33 147.3V137.3L49.5 125H61.8V189.8H78V198H33Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M54 303C49.5333 303 45.6333 302.133 42.3 300.4C39.0333 298.6 36.5 296.1 34.7 292.9C32.9 289.633 32 285.833 32 281.5V249.5C32 245.1 32.9 241.3 34.7 238.1C36.5 234.9 39.0333 232.433 42.3 230.7C45.6333 228.9 49.5333 228 54 228C58.5333 228 62.4333 228.9 65.7 230.7C68.9667 232.433 71.5 234.9 73.3 238.1C75.1 241.3 76 245.1 76 249.5V281.5C76 285.833 75.1 289.633 73.3 292.9C71.5 296.1 68.9333 298.6 65.6 300.4C62.3333 302.133 58.4667 303 54 303ZM54 295.3C58 295.3 61.2 294.033 63.6 291.5C66.0667 288.9 67.3 285.567 67.3 281.5V249.5C67.3 245.433 66.0667 242.133 63.6 239.6C61.2 237 58 235.7 54 235.7C50 235.7 46.7667 237 44.3 239.6C41.9 242.133 40.7 245.433 40.7 249.5V281.5C40.7 285.567 41.9 288.9 44.3 291.5C46.7667 294.033 50 295.3 54 295.3ZM54 271.4C52.2 271.4 50.7333 270.833 49.6 269.7C48.5333 268.567 48 267.067 48 265.2C48 263.4 48.5333 261.967 49.6 260.9C50.7333 259.767 52.2 259.2 54 259.2C55.8 259.2 57.2333 259.767 58.3 260.9C59.4333 261.967 60 263.4 60 265.2C60 267.067 59.4333 268.567 58.3 269.7C57.2333 270.833 55.8 271.4 54 271.4ZM33 392V383.8H52.8V326.5L33 341.3V331.3L49.5 319H61.8V383.8H78V392H33Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M54 497C49.5333 497 45.6333 496.133 42.3 494.4C39.0333 492.6 36.5 490.1 34.7 486.9C32.9 483.633 32 479.833 32 475.5V443.5C32 439.1 32.9 435.3 34.7 432.1C36.5 428.9 39.0333 426.433 42.3 424.7C45.6333 422.9 49.5333 422 54 422C58.5333 422 62.4333 422.9 65.7 424.7C68.9667 426.433 71.5 428.9 73.3 432.1C75.1 435.3 76 439.1 76 443.5V475.5C76 479.833 75.1 483.633 73.3 486.9C71.5 490.1 68.9333 492.6 65.6 494.4C62.3333 496.133 58.4667 497 54 497ZM54 489.3C58 489.3 61.2 488.033 63.6 485.5C66.0667 482.9 67.3 479.567 67.3 475.5V443.5C67.3 439.433 66.0667 436.133 63.6 433.6C61.2 431 58 429.7 54 429.7C50 429.7 46.7667 431 44.3 433.6C41.9 436.133 40.7 439.433 40.7 443.5V475.5C40.7 479.567 41.9 482.9 44.3 485.5C46.7667 488.033 50 489.3 54 489.3ZM54 465.4C52.2 465.4 50.7333 464.833 49.6 463.7C48.5333 462.567 48 461.067 48 459.2C48 457.4 48.5333 455.967 49.6 454.9C50.7333 453.767 52.2 453.2 54 453.2C55.8 453.2 57.2333 453.767 58.3 454.9C59.4333 455.967 60 457.4 60 459.2C60 461.067 59.4333 462.567 58.3 463.7C57.2333 464.833 55.8 465.4 54 465.4ZM33 586V577.8H52.8V520.5L33 535.3V525.3L49.5 513H61.8V577.8H78V586H33Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M54 691C49.5333 691 45.6333 690.133 42.3 688.4C39.0333 686.6 36.5 684.1 34.7 680.9C32.9 677.633 32 673.833 32 669.5V637.5C32 633.1 32.9 629.3 34.7 626.1C36.5 622.9 39.0333 620.433 42.3 618.7C45.6333 616.9 49.5333 616 54 616C58.5333 616 62.4333 616.9 65.7 618.7C68.9667 620.433 71.5 622.9 73.3 626.1C75.1 629.3 76 633.1 76 637.5V669.5C76 673.833 75.1 677.633 73.3 680.9C71.5 684.1 68.9333 686.6 65.6 688.4C62.3333 690.133 58.4667 691 54 691ZM54 683.3C58 683.3 61.2 682.033 63.6 679.5C66.0667 676.9 67.3 673.567 67.3 669.5V637.5C67.3 633.433 66.0667 630.133 63.6 627.6C61.2 625 58 623.7 54 623.7C50 623.7 46.7667 625 44.3 627.6C41.9 630.133 40.7 633.433 40.7 637.5V669.5C40.7 673.567 41.9 676.9 44.3 679.5C46.7667 682.033 50 683.3 54 683.3ZM54 659.4C52.2 659.4 50.7333 658.833 49.6 657.7C48.5333 656.567 48 655.067 48 653.2C48 651.4 48.5333 649.967 49.6 648.9C50.7333 647.767 52.2 647.2 54 647.2C55.8 647.2 57.2333 647.767 58.3 648.9C59.4333 649.967 60 651.4 60 653.2C60 655.067 59.4333 656.567 58.3 657.7C57.2333 658.833 55.8 659.4 54 659.4ZM33 780V771.8H52.8V714.5L33 729.3V719.3L49.5 707H61.8V771.8H78V780H33Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M54 885C49.5333 885 45.6333 884.133 42.3 882.4C39.0333 880.6 36.5 878.1 34.7 874.9C32.9 871.633 32 867.833 32 863.5V831.5C32 827.1 32.9 823.3 34.7 820.1C36.5 816.9 39.0333 814.433 42.3 812.7C45.6333 810.9 49.5333 810 54 810C58.5333 810 62.4333 810.9 65.7 812.7C68.9667 814.433 71.5 816.9 73.3 820.1C75.1 823.3 76 827.1 76 831.5V863.5C76 867.833 75.1 871.633 73.3 874.9C71.5 878.1 68.9333 880.6 65.6 882.4C62.3333 884.133 58.4667 885 54 885ZM54 877.3C58 877.3 61.2 876.033 63.6 873.5C66.0667 870.9 67.3 867.567 67.3 863.5V831.5C67.3 827.433 66.0667 824.133 63.6 821.6C61.2 819 58 817.7 54 817.7C50 817.7 46.7667 819 44.3 821.6C41.9 824.133 40.7 827.433 40.7 831.5V863.5C40.7 867.567 41.9 870.9 44.3 873.5C46.7667 876.033 50 877.3 54 877.3ZM54 853.4C52.2 853.4 50.7333 852.833 49.6 851.7C48.5333 850.567 48 849.067 48 847.2C48 845.4 48.5333 843.967 49.6 842.9C50.7333 841.767 52.2 841.2 54 841.2C55.8 841.2 57.2333 841.767 58.3 842.9C59.4333 843.967 60 845.4 60 847.2C60 849.067 59.4333 850.567 58.3 851.7C57.2333 852.833 55.8 853.4 54 853.4ZM33 974V965.8H52.8V908.5L33 923.3V913.3L49.5 901H61.8V965.8H78V974H33Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M662 109C657.533 109 653.633 108.133 650.3 106.4C647.033 104.6 644.5 102.1 642.7 98.9C640.9 95.6333 640 91.8333 640 87.5V55.5C640 51.1 640.9 47.3 642.7 44.1C644.5 40.9 647.033 38.4333 650.3 36.7C653.633 34.9 657.533 34 662 34C666.533 34 670.433 34.9 673.7 36.7C676.967 38.4333 679.5 40.9 681.3 44.1C683.1 47.3 684 51.1 684 55.5V87.5C684 91.8333 683.1 95.6333 681.3 98.9C679.5 102.1 676.933 104.6 673.6 106.4C670.333 108.133 666.467 109 662 109ZM662 101.3C666 101.3 669.2 100.033 671.6 97.5C674.067 94.9 675.3 91.5667 675.3 87.5V55.5C675.3 51.4333 674.067 48.1333 671.6 45.6C669.2 43 666 41.7 662 41.7C658 41.7 654.767 43 652.3 45.6C649.9 48.1333 648.7 51.4333 648.7 55.5V87.5C648.7 91.5667 649.9 94.9 652.3 97.5C654.767 100.033 658 101.3 662 101.3ZM662 77.4C660.2 77.4 658.733 76.8333 657.6 75.7C656.533 74.5667 656 73.0667 656 71.2C656 69.4 656.533 67.9667 657.6 66.9C658.733 65.7667 660.2 65.2 662 65.2C663.8 65.2 665.233 65.7667 666.3 66.9C667.433 67.9667 668 69.4 668 71.2C668 73.0667 667.433 74.5667 666.3 75.7C665.233 76.8333 663.8 77.4 662 77.4ZM641 198V189.8H660.8V132.5L641 147.3V137.3L657.5 125H669.8V189.8H686V198H641Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M662 303C657.533 303 653.633 302.133 650.3 300.4C647.033 298.6 644.5 296.1 642.7 292.9C640.9 289.633 640 285.833 640 281.5V249.5C640 245.1 640.9 241.3 642.7 238.1C644.5 234.9 647.033 232.433 650.3 230.7C653.633 228.9 657.533 228 662 228C666.533 228 670.433 228.9 673.7 230.7C676.967 232.433 679.5 234.9 681.3 238.1C683.1 241.3 684 245.1 684 249.5V281.5C684 285.833 683.1 289.633 681.3 292.9C679.5 296.1 676.933 298.6 673.6 300.4C670.333 302.133 666.467 303 662 303ZM662 295.3C666 295.3 669.2 294.033 671.6 291.5C674.067 288.9 675.3 285.567 675.3 281.5V249.5C675.3 245.433 674.067 242.133 671.6 239.6C669.2 237 666 235.7 662 235.7C658 235.7 654.767 237 652.3 239.6C649.9 242.133 648.7 245.433 648.7 249.5V281.5C648.7 285.567 649.9 288.9 652.3 291.5C654.767 294.033 658 295.3 662 295.3ZM662 271.4C660.2 271.4 658.733 270.833 657.6 269.7C656.533 268.567 656 267.067 656 265.2C656 263.4 656.533 261.967 657.6 260.9C658.733 259.767 660.2 259.2 662 259.2C663.8 259.2 665.233 259.767 666.3 260.9C667.433 261.967 668 263.4 668 265.2C668 267.067 667.433 268.567 666.3 269.7C665.233 270.833 663.8 271.4 662 271.4ZM641 392V383.8H660.8V326.5L641 341.3V331.3L657.5 319H669.8V383.8H686V392H641Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M662 497C657.533 497 653.633 496.133 650.3 494.4C647.033 492.6 644.5 490.1 642.7 486.9C640.9 483.633 640 479.833 640 475.5V443.5C640 439.1 640.9 435.3 642.7 432.1C644.5 428.9 647.033 426.433 650.3 424.7C653.633 422.9 657.533 422 662 422C666.533 422 670.433 422.9 673.7 424.7C676.967 426.433 679.5 428.9 681.3 432.1C683.1 435.3 684 439.1 684 443.5V475.5C684 479.833 683.1 483.633 681.3 486.9C679.5 490.1 676.933 492.6 673.6 494.4C670.333 496.133 666.467 497 662 497ZM662 489.3C666 489.3 669.2 488.033 671.6 485.5C674.067 482.9 675.3 479.567 675.3 475.5V443.5C675.3 439.433 674.067 436.133 671.6 433.6C669.2 431 666 429.7 662 429.7C658 429.7 654.767 431 652.3 433.6C649.9 436.133 648.7 439.433 648.7 443.5V475.5C648.7 479.567 649.9 482.9 652.3 485.5C654.767 488.033 658 489.3 662 489.3ZM662 465.4C660.2 465.4 658.733 464.833 657.6 463.7C656.533 462.567 656 461.067 656 459.2C656 457.4 656.533 455.967 657.6 454.9C658.733 453.767 660.2 453.2 662 453.2C663.8 453.2 665.233 453.767 666.3 454.9C667.433 455.967 668 457.4 668 459.2C668 461.067 667.433 462.567 666.3 463.7C665.233 464.833 663.8 465.4 662 465.4ZM641 586V577.8H660.8V520.5L641 535.3V525.3L657.5 513H669.8V577.8H686V586H641Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M662 691C657.533 691 653.633 690.133 650.3 688.4C647.033 686.6 644.5 684.1 642.7 680.9C640.9 677.633 640 673.833 640 669.5V637.5C640 633.1 640.9 629.3 642.7 626.1C644.5 622.9 647.033 620.433 650.3 618.7C653.633 616.9 657.533 616 662 616C666.533 616 670.433 616.9 673.7 618.7C676.967 620.433 679.5 622.9 681.3 626.1C683.1 629.3 684 633.1 684 637.5V669.5C684 673.833 683.1 677.633 681.3 680.9C679.5 684.1 676.933 686.6 673.6 688.4C670.333 690.133 666.467 691 662 691ZM662 683.3C666 683.3 669.2 682.033 671.6 679.5C674.067 676.9 675.3 673.567 675.3 669.5V637.5C675.3 633.433 674.067 630.133 671.6 627.6C669.2 625 666 623.7 662 623.7C658 623.7 654.767 625 652.3 627.6C649.9 630.133 648.7 633.433 648.7 637.5V669.5C648.7 673.567 649.9 676.9 652.3 679.5C654.767 682.033 658 683.3 662 683.3ZM662 659.4C660.2 659.4 658.733 658.833 657.6 657.7C656.533 656.567 656 655.067 656 653.2C656 651.4 656.533 649.967 657.6 648.9C658.733 647.767 660.2 647.2 662 647.2C663.8 647.2 665.233 647.767 666.3 648.9C667.433 649.967 668 651.4 668 653.2C668 655.067 667.433 656.567 666.3 657.7C665.233 658.833 663.8 659.4 662 659.4ZM641 780V771.8H660.8V714.5L641 729.3V719.3L657.5 707H669.8V771.8H686V780H641Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M662 885C657.533 885 653.633 884.133 650.3 882.4C647.033 880.6 644.5 878.1 642.7 874.9C640.9 871.633 640 867.833 640 863.5V831.5C640 827.1 640.9 823.3 642.7 820.1C644.5 816.9 647.033 814.433 650.3 812.7C653.633 810.9 657.533 810 662 810C666.533 810 670.433 810.9 673.7 812.7C676.967 814.433 679.5 816.9 681.3 820.1C683.1 823.3 684 827.1 684 831.5V863.5C684 867.833 683.1 871.633 681.3 874.9C679.5 878.1 676.933 880.6 673.6 882.4C670.333 884.133 666.467 885 662 885ZM662 877.3C666 877.3 669.2 876.033 671.6 873.5C674.067 870.9 675.3 867.567 675.3 863.5V831.5C675.3 827.433 674.067 824.133 671.6 821.6C669.2 819 666 817.7 662 817.7C658 817.7 654.767 819 652.3 821.6C649.9 824.133 648.7 827.433 648.7 831.5V863.5C648.7 867.567 649.9 870.9 652.3 873.5C654.767 876.033 658 877.3 662 877.3ZM662 853.4C660.2 853.4 658.733 852.833 657.6 851.7C656.533 850.567 656 849.067 656 847.2C656 845.4 656.533 843.967 657.6 842.9C658.733 841.767 660.2 841.2 662 841.2C663.8 841.2 665.233 841.767 666.3 842.9C667.433 843.967 668 845.4 668 847.2C668 849.067 667.433 850.567 666.3 851.7C665.233 852.833 663.8 853.4 662 853.4ZM641 974V965.8H660.8V908.5L641 923.3V913.3L657.5 901H669.8V965.8H686V974H641Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M358 109C353.533 109 349.633 108.133 346.3 106.4C343.033 104.6 340.5 102.1 338.7 98.9C336.9 95.6333 336 91.8333 336 87.5V55.5C336 51.1 336.9 47.3 338.7 44.1C340.5 40.9 343.033 38.4333 346.3 36.7C349.633 34.9 353.533 34 358 34C362.533 34 366.433 34.9 369.7 36.7C372.967 38.4333 375.5 40.9 377.3 44.1C379.1 47.3 380 51.1 380 55.5V87.5C380 91.8333 379.1 95.6333 377.3 98.9C375.5 102.1 372.933 104.6 369.6 106.4C366.333 108.133 362.467 109 358 109ZM358 101.3C362 101.3 365.2 100.033 367.6 97.5C370.067 94.9 371.3 91.5667 371.3 87.5V55.5C371.3 51.4333 370.067 48.1333 367.6 45.6C365.2 43 362 41.7 358 41.7C354 41.7 350.767 43 348.3 45.6C345.9 48.1333 344.7 51.4333 344.7 55.5V87.5C344.7 91.5667 345.9 94.9 348.3 97.5C350.767 100.033 354 101.3 358 101.3ZM358 77.4C356.2 77.4 354.733 76.8333 353.6 75.7C352.533 74.5667 352 73.0667 352 71.2C352 69.4 352.533 67.9667 353.6 66.9C354.733 65.7667 356.2 65.2 358 65.2C359.8 65.2 361.233 65.7667 362.3 66.9C363.433 67.9667 364 69.4 364 71.2C364 73.0667 363.433 74.5667 362.3 75.7C361.233 76.8333 359.8 77.4 358 77.4ZM337 198V189.8H356.8V132.5L337 147.3V137.3L353.5 125H365.8V189.8H382V198H337Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M358 303C353.533 303 349.633 302.133 346.3 300.4C343.033 298.6 340.5 296.1 338.7 292.9C336.9 289.633 336 285.833 336 281.5V249.5C336 245.1 336.9 241.3 338.7 238.1C340.5 234.9 343.033 232.433 346.3 230.7C349.633 228.9 353.533 228 358 228C362.533 228 366.433 228.9 369.7 230.7C372.967 232.433 375.5 234.9 377.3 238.1C379.1 241.3 380 245.1 380 249.5V281.5C380 285.833 379.1 289.633 377.3 292.9C375.5 296.1 372.933 298.6 369.6 300.4C366.333 302.133 362.467 303 358 303ZM358 295.3C362 295.3 365.2 294.033 367.6 291.5C370.067 288.9 371.3 285.567 371.3 281.5V249.5C371.3 245.433 370.067 242.133 367.6 239.6C365.2 237 362 235.7 358 235.7C354 235.7 350.767 237 348.3 239.6C345.9 242.133 344.7 245.433 344.7 249.5V281.5C344.7 285.567 345.9 288.9 348.3 291.5C350.767 294.033 354 295.3 358 295.3ZM358 271.4C356.2 271.4 354.733 270.833 353.6 269.7C352.533 268.567 352 267.067 352 265.2C352 263.4 352.533 261.967 353.6 260.9C354.733 259.767 356.2 259.2 358 259.2C359.8 259.2 361.233 259.767 362.3 260.9C363.433 261.967 364 263.4 364 265.2C364 267.067 363.433 268.567 362.3 269.7C361.233 270.833 359.8 271.4 358 271.4ZM337 392V383.8H356.8V326.5L337 341.3V331.3L353.5 319H365.8V383.8H382V392H337Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M358 497C353.533 497 349.633 496.133 346.3 494.4C343.033 492.6 340.5 490.1 338.7 486.9C336.9 483.633 336 479.833 336 475.5V443.5C336 439.1 336.9 435.3 338.7 432.1C340.5 428.9 343.033 426.433 346.3 424.7C349.633 422.9 353.533 422 358 422C362.533 422 366.433 422.9 369.7 424.7C372.967 426.433 375.5 428.9 377.3 432.1C379.1 435.3 380 439.1 380 443.5V475.5C380 479.833 379.1 483.633 377.3 486.9C375.5 490.1 372.933 492.6 369.6 494.4C366.333 496.133 362.467 497 358 497ZM358 489.3C362 489.3 365.2 488.033 367.6 485.5C370.067 482.9 371.3 479.567 371.3 475.5V443.5C371.3 439.433 370.067 436.133 367.6 433.6C365.2 431 362 429.7 358 429.7C354 429.7 350.767 431 348.3 433.6C345.9 436.133 344.7 439.433 344.7 443.5V475.5C344.7 479.567 345.9 482.9 348.3 485.5C350.767 488.033 354 489.3 358 489.3ZM358 465.4C356.2 465.4 354.733 464.833 353.6 463.7C352.533 462.567 352 461.067 352 459.2C352 457.4 352.533 455.967 353.6 454.9C354.733 453.767 356.2 453.2 358 453.2C359.8 453.2 361.233 453.767 362.3 454.9C363.433 455.967 364 457.4 364 459.2C364 461.067 363.433 462.567 362.3 463.7C361.233 464.833 359.8 465.4 358 465.4ZM337 586V577.8H356.8V520.5L337 535.3V525.3L353.5 513H365.8V577.8H382V586H337Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M358 691C353.533 691 349.633 690.133 346.3 688.4C343.033 686.6 340.5 684.1 338.7 680.9C336.9 677.633 336 673.833 336 669.5V637.5C336 633.1 336.9 629.3 338.7 626.1C340.5 622.9 343.033 620.433 346.3 618.7C349.633 616.9 353.533 616 358 616C362.533 616 366.433 616.9 369.7 618.7C372.967 620.433 375.5 622.9 377.3 626.1C379.1 629.3 380 633.1 380 637.5V669.5C380 673.833 379.1 677.633 377.3 680.9C375.5 684.1 372.933 686.6 369.6 688.4C366.333 690.133 362.467 691 358 691ZM358 683.3C362 683.3 365.2 682.033 367.6 679.5C370.067 676.9 371.3 673.567 371.3 669.5V637.5C371.3 633.433 370.067 630.133 367.6 627.6C365.2 625 362 623.7 358 623.7C354 623.7 350.767 625 348.3 627.6C345.9 630.133 344.7 633.433 344.7 637.5V669.5C344.7 673.567 345.9 676.9 348.3 679.5C350.767 682.033 354 683.3 358 683.3ZM358 659.4C356.2 659.4 354.733 658.833 353.6 657.7C352.533 656.567 352 655.067 352 653.2C352 651.4 352.533 649.967 353.6 648.9C354.733 647.767 356.2 647.2 358 647.2C359.8 647.2 361.233 647.767 362.3 648.9C363.433 649.967 364 651.4 364 653.2C364 655.067 363.433 656.567 362.3 657.7C361.233 658.833 359.8 659.4 358 659.4ZM337 780V771.8H356.8V714.5L337 729.3V719.3L353.5 707H365.8V771.8H382V780H337Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M358 885C353.533 885 349.633 884.133 346.3 882.4C343.033 880.6 340.5 878.1 338.7 874.9C336.9 871.633 336 867.833 336 863.5V831.5C336 827.1 336.9 823.3 338.7 820.1C340.5 816.9 343.033 814.433 346.3 812.7C349.633 810.9 353.533 810 358 810C362.533 810 366.433 810.9 369.7 812.7C372.967 814.433 375.5 816.9 377.3 820.1C379.1 823.3 380 827.1 380 831.5V863.5C380 867.833 379.1 871.633 377.3 874.9C375.5 878.1 372.933 880.6 369.6 882.4C366.333 884.133 362.467 885 358 885ZM358 877.3C362 877.3 365.2 876.033 367.6 873.5C370.067 870.9 371.3 867.567 371.3 863.5V831.5C371.3 827.433 370.067 824.133 367.6 821.6C365.2 819 362 817.7 358 817.7C354 817.7 350.767 819 348.3 821.6C345.9 824.133 344.7 827.433 344.7 831.5V863.5C344.7 867.567 345.9 870.9 348.3 873.5C350.767 876.033 354 877.3 358 877.3ZM358 853.4C356.2 853.4 354.733 852.833 353.6 851.7C352.533 850.567 352 849.067 352 847.2C352 845.4 352.533 843.967 353.6 842.9C354.733 841.767 356.2 841.2 358 841.2C359.8 841.2 361.233 841.767 362.3 842.9C363.433 843.967 364 845.4 364 847.2C364 849.067 363.433 850.567 362.3 851.7C361.233 852.833 359.8 853.4 358 853.4ZM337 974V965.8H356.8V908.5L337 923.3V913.3L353.5 901H365.8V965.8H382V974H337Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M966 109C961.533 109 957.633 108.133 954.3 106.4C951.033 104.6 948.5 102.1 946.7 98.9C944.9 95.6333 944 91.8333 944 87.5V55.5C944 51.1 944.9 47.3 946.7 44.1C948.5 40.9 951.033 38.4333 954.3 36.7C957.633 34.9 961.533 34 966 34C970.533 34 974.433 34.9 977.7 36.7C980.967 38.4333 983.5 40.9 985.3 44.1C987.1 47.3 988 51.1 988 55.5V87.5C988 91.8333 987.1 95.6333 985.3 98.9C983.5 102.1 980.933 104.6 977.6 106.4C974.333 108.133 970.467 109 966 109ZM966 101.3C970 101.3 973.2 100.033 975.6 97.5C978.067 94.9 979.3 91.5667 979.3 87.5V55.5C979.3 51.4333 978.067 48.1333 975.6 45.6C973.2 43 970 41.7 966 41.7C962 41.7 958.767 43 956.3 45.6C953.9 48.1333 952.7 51.4333 952.7 55.5V87.5C952.7 91.5667 953.9 94.9 956.3 97.5C958.767 100.033 962 101.3 966 101.3ZM966 77.4C964.2 77.4 962.733 76.8333 961.6 75.7C960.533 74.5667 960 73.0667 960 71.2C960 69.4 960.533 67.9667 961.6 66.9C962.733 65.7667 964.2 65.2 966 65.2C967.8 65.2 969.233 65.7667 970.3 66.9C971.433 67.9667 972 69.4 972 71.2C972 73.0667 971.433 74.5667 970.3 75.7C969.233 76.8333 967.8 77.4 966 77.4ZM945 198V189.8H964.8V132.5L945 147.3V137.3L961.5 125H973.8V189.8H990V198H945Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M966 303C961.533 303 957.633 302.133 954.3 300.4C951.033 298.6 948.5 296.1 946.7 292.9C944.9 289.633 944 285.833 944 281.5V249.5C944 245.1 944.9 241.3 946.7 238.1C948.5 234.9 951.033 232.433 954.3 230.7C957.633 228.9 961.533 228 966 228C970.533 228 974.433 228.9 977.7 230.7C980.967 232.433 983.5 234.9 985.3 238.1C987.1 241.3 988 245.1 988 249.5V281.5C988 285.833 987.1 289.633 985.3 292.9C983.5 296.1 980.933 298.6 977.6 300.4C974.333 302.133 970.467 303 966 303ZM966 295.3C970 295.3 973.2 294.033 975.6 291.5C978.067 288.9 979.3 285.567 979.3 281.5V249.5C979.3 245.433 978.067 242.133 975.6 239.6C973.2 237 970 235.7 966 235.7C962 235.7 958.767 237 956.3 239.6C953.9 242.133 952.7 245.433 952.7 249.5V281.5C952.7 285.567 953.9 288.9 956.3 291.5C958.767 294.033 962 295.3 966 295.3ZM966 271.4C964.2 271.4 962.733 270.833 961.6 269.7C960.533 268.567 960 267.067 960 265.2C960 263.4 960.533 261.967 961.6 260.9C962.733 259.767 964.2 259.2 966 259.2C967.8 259.2 969.233 259.767 970.3 260.9C971.433 261.967 972 263.4 972 265.2C972 267.067 971.433 268.567 970.3 269.7C969.233 270.833 967.8 271.4 966 271.4ZM945 392V383.8H964.8V326.5L945 341.3V331.3L961.5 319H973.8V383.8H990V392H945Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M966 497C961.533 497 957.633 496.133 954.3 494.4C951.033 492.6 948.5 490.1 946.7 486.9C944.9 483.633 944 479.833 944 475.5V443.5C944 439.1 944.9 435.3 946.7 432.1C948.5 428.9 951.033 426.433 954.3 424.7C957.633 422.9 961.533 422 966 422C970.533 422 974.433 422.9 977.7 424.7C980.967 426.433 983.5 428.9 985.3 432.1C987.1 435.3 988 439.1 988 443.5V475.5C988 479.833 987.1 483.633 985.3 486.9C983.5 490.1 980.933 492.6 977.6 494.4C974.333 496.133 970.467 497 966 497ZM966 489.3C970 489.3 973.2 488.033 975.6 485.5C978.067 482.9 979.3 479.567 979.3 475.5V443.5C979.3 439.433 978.067 436.133 975.6 433.6C973.2 431 970 429.7 966 429.7C962 429.7 958.767 431 956.3 433.6C953.9 436.133 952.7 439.433 952.7 443.5V475.5C952.7 479.567 953.9 482.9 956.3 485.5C958.767 488.033 962 489.3 966 489.3ZM966 465.4C964.2 465.4 962.733 464.833 961.6 463.7C960.533 462.567 960 461.067 960 459.2C960 457.4 960.533 455.967 961.6 454.9C962.733 453.767 964.2 453.2 966 453.2C967.8 453.2 969.233 453.767 970.3 454.9C971.433 455.967 972 457.4 972 459.2C972 461.067 971.433 462.567 970.3 463.7C969.233 464.833 967.8 465.4 966 465.4ZM945 586V577.8H964.8V520.5L945 535.3V525.3L961.5 513H973.8V577.8H990V586H945Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M966 691C961.533 691 957.633 690.133 954.3 688.4C951.033 686.6 948.5 684.1 946.7 680.9C944.9 677.633 944 673.833 944 669.5V637.5C944 633.1 944.9 629.3 946.7 626.1C948.5 622.9 951.033 620.433 954.3 618.7C957.633 616.9 961.533 616 966 616C970.533 616 974.433 616.9 977.7 618.7C980.967 620.433 983.5 622.9 985.3 626.1C987.1 629.3 988 633.1 988 637.5V669.5C988 673.833 987.1 677.633 985.3 680.9C983.5 684.1 980.933 686.6 977.6 688.4C974.333 690.133 970.467 691 966 691ZM966 683.3C970 683.3 973.2 682.033 975.6 679.5C978.067 676.9 979.3 673.567 979.3 669.5V637.5C979.3 633.433 978.067 630.133 975.6 627.6C973.2 625 970 623.7 966 623.7C962 623.7 958.767 625 956.3 627.6C953.9 630.133 952.7 633.433 952.7 637.5V669.5C952.7 673.567 953.9 676.9 956.3 679.5C958.767 682.033 962 683.3 966 683.3ZM966 659.4C964.2 659.4 962.733 658.833 961.6 657.7C960.533 656.567 960 655.067 960 653.2C960 651.4 960.533 649.967 961.6 648.9C962.733 647.767 964.2 647.2 966 647.2C967.8 647.2 969.233 647.767 970.3 648.9C971.433 649.967 972 651.4 972 653.2C972 655.067 971.433 656.567 970.3 657.7C969.233 658.833 967.8 659.4 966 659.4ZM945 780V771.8H964.8V714.5L945 729.3V719.3L961.5 707H973.8V771.8H990V780H945Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M966 885C961.533 885 957.633 884.133 954.3 882.4C951.033 880.6 948.5 878.1 946.7 874.9C944.9 871.633 944 867.833 944 863.5V831.5C944 827.1 944.9 823.3 946.7 820.1C948.5 816.9 951.033 814.433 954.3 812.7C957.633 810.9 961.533 810 966 810C970.533 810 974.433 810.9 977.7 812.7C980.967 814.433 983.5 816.9 985.3 820.1C987.1 823.3 988 827.1 988 831.5V863.5C988 867.833 987.1 871.633 985.3 874.9C983.5 878.1 980.933 880.6 977.6 882.4C974.333 884.133 970.467 885 966 885ZM966 877.3C970 877.3 973.2 876.033 975.6 873.5C978.067 870.9 979.3 867.567 979.3 863.5V831.5C979.3 827.433 978.067 824.133 975.6 821.6C973.2 819 970 817.7 966 817.7C962 817.7 958.767 819 956.3 821.6C953.9 824.133 952.7 827.433 952.7 831.5V863.5C952.7 867.567 953.9 870.9 956.3 873.5C958.767 876.033 962 877.3 966 877.3ZM966 853.4C964.2 853.4 962.733 852.833 961.6 851.7C960.533 850.567 960 849.067 960 847.2C960 845.4 960.533 843.967 961.6 842.9C962.733 841.767 964.2 841.2 966 841.2C967.8 841.2 969.233 841.767 970.3 842.9C971.433 843.967 972 845.4 972 847.2C972 849.067 971.433 850.567 970.3 851.7C969.233 852.833 967.8 853.4 966 853.4ZM945 974V965.8H964.8V908.5L945 923.3V913.3L961.5 901H973.8V965.8H990V974H945Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M206 109C201.533 109 197.633 108.133 194.3 106.4C191.033 104.6 188.5 102.1 186.7 98.9C184.9 95.6333 184 91.8333 184 87.5V55.5C184 51.1 184.9 47.3 186.7 44.1C188.5 40.9 191.033 38.4333 194.3 36.7C197.633 34.9 201.533 34 206 34C210.533 34 214.433 34.9 217.7 36.7C220.967 38.4333 223.5 40.9 225.3 44.1C227.1 47.3 228 51.1 228 55.5V87.5C228 91.8333 227.1 95.6333 225.3 98.9C223.5 102.1 220.933 104.6 217.6 106.4C214.333 108.133 210.467 109 206 109ZM206 101.3C210 101.3 213.2 100.033 215.6 97.5C218.067 94.9 219.3 91.5667 219.3 87.5V55.5C219.3 51.4333 218.067 48.1333 215.6 45.6C213.2 43 210 41.7 206 41.7C202 41.7 198.767 43 196.3 45.6C193.9 48.1333 192.7 51.4333 192.7 55.5V87.5C192.7 91.5667 193.9 94.9 196.3 97.5C198.767 100.033 202 101.3 206 101.3ZM206 77.4C204.2 77.4 202.733 76.8333 201.6 75.7C200.533 74.5667 200 73.0667 200 71.2C200 69.4 200.533 67.9667 201.6 66.9C202.733 65.7667 204.2 65.2 206 65.2C207.8 65.2 209.233 65.7667 210.3 66.9C211.433 67.9667 212 69.4 212 71.2C212 73.0667 211.433 74.5667 210.3 75.7C209.233 76.8333 207.8 77.4 206 77.4ZM185 198V189.8H204.8V132.5L185 147.3V137.3L201.5 125H213.8V189.8H230V198H185Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M206 303C201.533 303 197.633 302.133 194.3 300.4C191.033 298.6 188.5 296.1 186.7 292.9C184.9 289.633 184 285.833 184 281.5V249.5C184 245.1 184.9 241.3 186.7 238.1C188.5 234.9 191.033 232.433 194.3 230.7C197.633 228.9 201.533 228 206 228C210.533 228 214.433 228.9 217.7 230.7C220.967 232.433 223.5 234.9 225.3 238.1C227.1 241.3 228 245.1 228 249.5V281.5C228 285.833 227.1 289.633 225.3 292.9C223.5 296.1 220.933 298.6 217.6 300.4C214.333 302.133 210.467 303 206 303ZM206 295.3C210 295.3 213.2 294.033 215.6 291.5C218.067 288.9 219.3 285.567 219.3 281.5V249.5C219.3 245.433 218.067 242.133 215.6 239.6C213.2 237 210 235.7 206 235.7C202 235.7 198.767 237 196.3 239.6C193.9 242.133 192.7 245.433 192.7 249.5V281.5C192.7 285.567 193.9 288.9 196.3 291.5C198.767 294.033 202 295.3 206 295.3ZM206 271.4C204.2 271.4 202.733 270.833 201.6 269.7C200.533 268.567 200 267.067 200 265.2C200 263.4 200.533 261.967 201.6 260.9C202.733 259.767 204.2 259.2 206 259.2C207.8 259.2 209.233 259.767 210.3 260.9C211.433 261.967 212 263.4 212 265.2C212 267.067 211.433 268.567 210.3 269.7C209.233 270.833 207.8 271.4 206 271.4ZM185 392V383.8H204.8V326.5L185 341.3V331.3L201.5 319H213.8V383.8H230V392H185Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M206 497C201.533 497 197.633 496.133 194.3 494.4C191.033 492.6 188.5 490.1 186.7 486.9C184.9 483.633 184 479.833 184 475.5V443.5C184 439.1 184.9 435.3 186.7 432.1C188.5 428.9 191.033 426.433 194.3 424.7C197.633 422.9 201.533 422 206 422C210.533 422 214.433 422.9 217.7 424.7C220.967 426.433 223.5 428.9 225.3 432.1C227.1 435.3 228 439.1 228 443.5V475.5C228 479.833 227.1 483.633 225.3 486.9C223.5 490.1 220.933 492.6 217.6 494.4C214.333 496.133 210.467 497 206 497ZM206 489.3C210 489.3 213.2 488.033 215.6 485.5C218.067 482.9 219.3 479.567 219.3 475.5V443.5C219.3 439.433 218.067 436.133 215.6 433.6C213.2 431 210 429.7 206 429.7C202 429.7 198.767 431 196.3 433.6C193.9 436.133 192.7 439.433 192.7 443.5V475.5C192.7 479.567 193.9 482.9 196.3 485.5C198.767 488.033 202 489.3 206 489.3ZM206 465.4C204.2 465.4 202.733 464.833 201.6 463.7C200.533 462.567 200 461.067 200 459.2C200 457.4 200.533 455.967 201.6 454.9C202.733 453.767 204.2 453.2 206 453.2C207.8 453.2 209.233 453.767 210.3 454.9C211.433 455.967 212 457.4 212 459.2C212 461.067 211.433 462.567 210.3 463.7C209.233 464.833 207.8 465.4 206 465.4ZM185 586V577.8H204.8V520.5L185 535.3V525.3L201.5 513H213.8V577.8H230V586H185Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M206 691C201.533 691 197.633 690.133 194.3 688.4C191.033 686.6 188.5 684.1 186.7 680.9C184.9 677.633 184 673.833 184 669.5V637.5C184 633.1 184.9 629.3 186.7 626.1C188.5 622.9 191.033 620.433 194.3 618.7C197.633 616.9 201.533 616 206 616C210.533 616 214.433 616.9 217.7 618.7C220.967 620.433 223.5 622.9 225.3 626.1C227.1 629.3 228 633.1 228 637.5V669.5C228 673.833 227.1 677.633 225.3 680.9C223.5 684.1 220.933 686.6 217.6 688.4C214.333 690.133 210.467 691 206 691ZM206 683.3C210 683.3 213.2 682.033 215.6 679.5C218.067 676.9 219.3 673.567 219.3 669.5V637.5C219.3 633.433 218.067 630.133 215.6 627.6C213.2 625 210 623.7 206 623.7C202 623.7 198.767 625 196.3 627.6C193.9 630.133 192.7 633.433 192.7 637.5V669.5C192.7 673.567 193.9 676.9 196.3 679.5C198.767 682.033 202 683.3 206 683.3ZM206 659.4C204.2 659.4 202.733 658.833 201.6 657.7C200.533 656.567 200 655.067 200 653.2C200 651.4 200.533 649.967 201.6 648.9C202.733 647.767 204.2 647.2 206 647.2C207.8 647.2 209.233 647.767 210.3 648.9C211.433 649.967 212 651.4 212 653.2C212 655.067 211.433 656.567 210.3 657.7C209.233 658.833 207.8 659.4 206 659.4ZM185 780V771.8H204.8V714.5L185 729.3V719.3L201.5 707H213.8V771.8H230V780H185Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M206 885C201.533 885 197.633 884.133 194.3 882.4C191.033 880.6 188.5 878.1 186.7 874.9C184.9 871.633 184 867.833 184 863.5V831.5C184 827.1 184.9 823.3 186.7 820.1C188.5 816.9 191.033 814.433 194.3 812.7C197.633 810.9 201.533 810 206 810C210.533 810 214.433 810.9 217.7 812.7C220.967 814.433 223.5 816.9 225.3 820.1C227.1 823.3 228 827.1 228 831.5V863.5C228 867.833 227.1 871.633 225.3 874.9C223.5 878.1 220.933 880.6 217.6 882.4C214.333 884.133 210.467 885 206 885ZM206 877.3C210 877.3 213.2 876.033 215.6 873.5C218.067 870.9 219.3 867.567 219.3 863.5V831.5C219.3 827.433 218.067 824.133 215.6 821.6C213.2 819 210 817.7 206 817.7C202 817.7 198.767 819 196.3 821.6C193.9 824.133 192.7 827.433 192.7 831.5V863.5C192.7 867.567 193.9 870.9 196.3 873.5C198.767 876.033 202 877.3 206 877.3ZM206 853.4C204.2 853.4 202.733 852.833 201.6 851.7C200.533 850.567 200 849.067 200 847.2C200 845.4 200.533 843.967 201.6 842.9C202.733 841.767 204.2 841.2 206 841.2C207.8 841.2 209.233 841.767 210.3 842.9C211.433 843.967 212 845.4 212 847.2C212 849.067 211.433 850.567 210.3 851.7C209.233 852.833 207.8 853.4 206 853.4ZM185 974V965.8H204.8V908.5L185 923.3V913.3L201.5 901H213.8V965.8H230V974H185Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M814 109C809.533 109 805.633 108.133 802.3 106.4C799.033 104.6 796.5 102.1 794.7 98.9C792.9 95.6333 792 91.8333 792 87.5V55.5C792 51.1 792.9 47.3 794.7 44.1C796.5 40.9 799.033 38.4333 802.3 36.7C805.633 34.9 809.533 34 814 34C818.533 34 822.433 34.9 825.7 36.7C828.967 38.4333 831.5 40.9 833.3 44.1C835.1 47.3 836 51.1 836 55.5V87.5C836 91.8333 835.1 95.6333 833.3 98.9C831.5 102.1 828.933 104.6 825.6 106.4C822.333 108.133 818.467 109 814 109ZM814 101.3C818 101.3 821.2 100.033 823.6 97.5C826.067 94.9 827.3 91.5667 827.3 87.5V55.5C827.3 51.4333 826.067 48.1333 823.6 45.6C821.2 43 818 41.7 814 41.7C810 41.7 806.767 43 804.3 45.6C801.9 48.1333 800.7 51.4333 800.7 55.5V87.5C800.7 91.5667 801.9 94.9 804.3 97.5C806.767 100.033 810 101.3 814 101.3ZM814 77.4C812.2 77.4 810.733 76.8333 809.6 75.7C808.533 74.5667 808 73.0667 808 71.2C808 69.4 808.533 67.9667 809.6 66.9C810.733 65.7667 812.2 65.2 814 65.2C815.8 65.2 817.233 65.7667 818.3 66.9C819.433 67.9667 820 69.4 820 71.2C820 73.0667 819.433 74.5667 818.3 75.7C817.233 76.8333 815.8 77.4 814 77.4ZM793 198V189.8H812.8V132.5L793 147.3V137.3L809.5 125H821.8V189.8H838V198H793Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M814 303C809.533 303 805.633 302.133 802.3 300.4C799.033 298.6 796.5 296.1 794.7 292.9C792.9 289.633 792 285.833 792 281.5V249.5C792 245.1 792.9 241.3 794.7 238.1C796.5 234.9 799.033 232.433 802.3 230.7C805.633 228.9 809.533 228 814 228C818.533 228 822.433 228.9 825.7 230.7C828.967 232.433 831.5 234.9 833.3 238.1C835.1 241.3 836 245.1 836 249.5V281.5C836 285.833 835.1 289.633 833.3 292.9C831.5 296.1 828.933 298.6 825.6 300.4C822.333 302.133 818.467 303 814 303ZM814 295.3C818 295.3 821.2 294.033 823.6 291.5C826.067 288.9 827.3 285.567 827.3 281.5V249.5C827.3 245.433 826.067 242.133 823.6 239.6C821.2 237 818 235.7 814 235.7C810 235.7 806.767 237 804.3 239.6C801.9 242.133 800.7 245.433 800.7 249.5V281.5C800.7 285.567 801.9 288.9 804.3 291.5C806.767 294.033 810 295.3 814 295.3ZM814 271.4C812.2 271.4 810.733 270.833 809.6 269.7C808.533 268.567 808 267.067 808 265.2C808 263.4 808.533 261.967 809.6 260.9C810.733 259.767 812.2 259.2 814 259.2C815.8 259.2 817.233 259.767 818.3 260.9C819.433 261.967 820 263.4 820 265.2C820 267.067 819.433 268.567 818.3 269.7C817.233 270.833 815.8 271.4 814 271.4ZM793 392V383.8H812.8V326.5L793 341.3V331.3L809.5 319H821.8V383.8H838V392H793Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M814 497C809.533 497 805.633 496.133 802.3 494.4C799.033 492.6 796.5 490.1 794.7 486.9C792.9 483.633 792 479.833 792 475.5V443.5C792 439.1 792.9 435.3 794.7 432.1C796.5 428.9 799.033 426.433 802.3 424.7C805.633 422.9 809.533 422 814 422C818.533 422 822.433 422.9 825.7 424.7C828.967 426.433 831.5 428.9 833.3 432.1C835.1 435.3 836 439.1 836 443.5V475.5C836 479.833 835.1 483.633 833.3 486.9C831.5 490.1 828.933 492.6 825.6 494.4C822.333 496.133 818.467 497 814 497ZM814 489.3C818 489.3 821.2 488.033 823.6 485.5C826.067 482.9 827.3 479.567 827.3 475.5V443.5C827.3 439.433 826.067 436.133 823.6 433.6C821.2 431 818 429.7 814 429.7C810 429.7 806.767 431 804.3 433.6C801.9 436.133 800.7 439.433 800.7 443.5V475.5C800.7 479.567 801.9 482.9 804.3 485.5C806.767 488.033 810 489.3 814 489.3ZM814 465.4C812.2 465.4 810.733 464.833 809.6 463.7C808.533 462.567 808 461.067 808 459.2C808 457.4 808.533 455.967 809.6 454.9C810.733 453.767 812.2 453.2 814 453.2C815.8 453.2 817.233 453.767 818.3 454.9C819.433 455.967 820 457.4 820 459.2C820 461.067 819.433 462.567 818.3 463.7C817.233 464.833 815.8 465.4 814 465.4ZM793 586V577.8H812.8V520.5L793 535.3V525.3L809.5 513H821.8V577.8H838V586H793Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M814 691C809.533 691 805.633 690.133 802.3 688.4C799.033 686.6 796.5 684.1 794.7 680.9C792.9 677.633 792 673.833 792 669.5V637.5C792 633.1 792.9 629.3 794.7 626.1C796.5 622.9 799.033 620.433 802.3 618.7C805.633 616.9 809.533 616 814 616C818.533 616 822.433 616.9 825.7 618.7C828.967 620.433 831.5 622.9 833.3 626.1C835.1 629.3 836 633.1 836 637.5V669.5C836 673.833 835.1 677.633 833.3 680.9C831.5 684.1 828.933 686.6 825.6 688.4C822.333 690.133 818.467 691 814 691ZM814 683.3C818 683.3 821.2 682.033 823.6 679.5C826.067 676.9 827.3 673.567 827.3 669.5V637.5C827.3 633.433 826.067 630.133 823.6 627.6C821.2 625 818 623.7 814 623.7C810 623.7 806.767 625 804.3 627.6C801.9 630.133 800.7 633.433 800.7 637.5V669.5C800.7 673.567 801.9 676.9 804.3 679.5C806.767 682.033 810 683.3 814 683.3ZM814 659.4C812.2 659.4 810.733 658.833 809.6 657.7C808.533 656.567 808 655.067 808 653.2C808 651.4 808.533 649.967 809.6 648.9C810.733 647.767 812.2 647.2 814 647.2C815.8 647.2 817.233 647.767 818.3 648.9C819.433 649.967 820 651.4 820 653.2C820 655.067 819.433 656.567 818.3 657.7C817.233 658.833 815.8 659.4 814 659.4ZM793 780V771.8H812.8V714.5L793 729.3V719.3L809.5 707H821.8V771.8H838V780H793Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M814 885C809.533 885 805.633 884.133 802.3 882.4C799.033 880.6 796.5 878.1 794.7 874.9C792.9 871.633 792 867.833 792 863.5V831.5C792 827.1 792.9 823.3 794.7 820.1C796.5 816.9 799.033 814.433 802.3 812.7C805.633 810.9 809.533 810 814 810C818.533 810 822.433 810.9 825.7 812.7C828.967 814.433 831.5 816.9 833.3 820.1C835.1 823.3 836 827.1 836 831.5V863.5C836 867.833 835.1 871.633 833.3 874.9C831.5 878.1 828.933 880.6 825.6 882.4C822.333 884.133 818.467 885 814 885ZM814 877.3C818 877.3 821.2 876.033 823.6 873.5C826.067 870.9 827.3 867.567 827.3 863.5V831.5C827.3 827.433 826.067 824.133 823.6 821.6C821.2 819 818 817.7 814 817.7C810 817.7 806.767 819 804.3 821.6C801.9 824.133 800.7 827.433 800.7 831.5V863.5C800.7 867.567 801.9 870.9 804.3 873.5C806.767 876.033 810 877.3 814 877.3ZM814 853.4C812.2 853.4 810.733 852.833 809.6 851.7C808.533 850.567 808 849.067 808 847.2C808 845.4 808.533 843.967 809.6 842.9C810.733 841.767 812.2 841.2 814 841.2C815.8 841.2 817.233 841.767 818.3 842.9C819.433 843.967 820 845.4 820 847.2C820 849.067 819.433 850.567 818.3 851.7C817.233 852.833 815.8 853.4 814 853.4ZM793 974V965.8H812.8V908.5L793 923.3V913.3L809.5 901H821.8V965.8H838V974H793Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M510 109C505.533 109 501.633 108.133 498.3 106.4C495.033 104.6 492.5 102.1 490.7 98.9C488.9 95.6333 488 91.8333 488 87.5V55.5C488 51.1 488.9 47.3 490.7 44.1C492.5 40.9 495.033 38.4333 498.3 36.7C501.633 34.9 505.533 34 510 34C514.533 34 518.433 34.9 521.7 36.7C524.967 38.4333 527.5 40.9 529.3 44.1C531.1 47.3 532 51.1 532 55.5V87.5C532 91.8333 531.1 95.6333 529.3 98.9C527.5 102.1 524.933 104.6 521.6 106.4C518.333 108.133 514.467 109 510 109ZM510 101.3C514 101.3 517.2 100.033 519.6 97.5C522.067 94.9 523.3 91.5667 523.3 87.5V55.5C523.3 51.4333 522.067 48.1333 519.6 45.6C517.2 43 514 41.7 510 41.7C506 41.7 502.767 43 500.3 45.6C497.9 48.1333 496.7 51.4333 496.7 55.5V87.5C496.7 91.5667 497.9 94.9 500.3 97.5C502.767 100.033 506 101.3 510 101.3ZM510 77.4C508.2 77.4 506.733 76.8333 505.6 75.7C504.533 74.5667 504 73.0667 504 71.2C504 69.4 504.533 67.9667 505.6 66.9C506.733 65.7667 508.2 65.2 510 65.2C511.8 65.2 513.233 65.7667 514.3 66.9C515.433 67.9667 516 69.4 516 71.2C516 73.0667 515.433 74.5667 514.3 75.7C513.233 76.8333 511.8 77.4 510 77.4ZM489 198V189.8H508.8V132.5L489 147.3V137.3L505.5 125H517.8V189.8H534V198H489Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M510 303C505.533 303 501.633 302.133 498.3 300.4C495.033 298.6 492.5 296.1 490.7 292.9C488.9 289.633 488 285.833 488 281.5V249.5C488 245.1 488.9 241.3 490.7 238.1C492.5 234.9 495.033 232.433 498.3 230.7C501.633 228.9 505.533 228 510 228C514.533 228 518.433 228.9 521.7 230.7C524.967 232.433 527.5 234.9 529.3 238.1C531.1 241.3 532 245.1 532 249.5V281.5C532 285.833 531.1 289.633 529.3 292.9C527.5 296.1 524.933 298.6 521.6 300.4C518.333 302.133 514.467 303 510 303ZM510 295.3C514 295.3 517.2 294.033 519.6 291.5C522.067 288.9 523.3 285.567 523.3 281.5V249.5C523.3 245.433 522.067 242.133 519.6 239.6C517.2 237 514 235.7 510 235.7C506 235.7 502.767 237 500.3 239.6C497.9 242.133 496.7 245.433 496.7 249.5V281.5C496.7 285.567 497.9 288.9 500.3 291.5C502.767 294.033 506 295.3 510 295.3ZM510 271.4C508.2 271.4 506.733 270.833 505.6 269.7C504.533 268.567 504 267.067 504 265.2C504 263.4 504.533 261.967 505.6 260.9C506.733 259.767 508.2 259.2 510 259.2C511.8 259.2 513.233 259.767 514.3 260.9C515.433 261.967 516 263.4 516 265.2C516 267.067 515.433 268.567 514.3 269.7C513.233 270.833 511.8 271.4 510 271.4ZM489 392V383.8H508.8V326.5L489 341.3V331.3L505.5 319H517.8V383.8H534V392H489Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M510 497C505.533 497 501.633 496.133 498.3 494.4C495.033 492.6 492.5 490.1 490.7 486.9C488.9 483.633 488 479.833 488 475.5V443.5C488 439.1 488.9 435.3 490.7 432.1C492.5 428.9 495.033 426.433 498.3 424.7C501.633 422.9 505.533 422 510 422C514.533 422 518.433 422.9 521.7 424.7C524.967 426.433 527.5 428.9 529.3 432.1C531.1 435.3 532 439.1 532 443.5V475.5C532 479.833 531.1 483.633 529.3 486.9C527.5 490.1 524.933 492.6 521.6 494.4C518.333 496.133 514.467 497 510 497ZM510 489.3C514 489.3 517.2 488.033 519.6 485.5C522.067 482.9 523.3 479.567 523.3 475.5V443.5C523.3 439.433 522.067 436.133 519.6 433.6C517.2 431 514 429.7 510 429.7C506 429.7 502.767 431 500.3 433.6C497.9 436.133 496.7 439.433 496.7 443.5V475.5C496.7 479.567 497.9 482.9 500.3 485.5C502.767 488.033 506 489.3 510 489.3ZM510 465.4C508.2 465.4 506.733 464.833 505.6 463.7C504.533 462.567 504 461.067 504 459.2C504 457.4 504.533 455.967 505.6 454.9C506.733 453.767 508.2 453.2 510 453.2C511.8 453.2 513.233 453.767 514.3 454.9C515.433 455.967 516 457.4 516 459.2C516 461.067 515.433 462.567 514.3 463.7C513.233 464.833 511.8 465.4 510 465.4ZM489 586V577.8H508.8V520.5L489 535.3V525.3L505.5 513H517.8V577.8H534V586H489Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M510 691C505.533 691 501.633 690.133 498.3 688.4C495.033 686.6 492.5 684.1 490.7 680.9C488.9 677.633 488 673.833 488 669.5V637.5C488 633.1 488.9 629.3 490.7 626.1C492.5 622.9 495.033 620.433 498.3 618.7C501.633 616.9 505.533 616 510 616C514.533 616 518.433 616.9 521.7 618.7C524.967 620.433 527.5 622.9 529.3 626.1C531.1 629.3 532 633.1 532 637.5V669.5C532 673.833 531.1 677.633 529.3 680.9C527.5 684.1 524.933 686.6 521.6 688.4C518.333 690.133 514.467 691 510 691ZM510 683.3C514 683.3 517.2 682.033 519.6 679.5C522.067 676.9 523.3 673.567 523.3 669.5V637.5C523.3 633.433 522.067 630.133 519.6 627.6C517.2 625 514 623.7 510 623.7C506 623.7 502.767 625 500.3 627.6C497.9 630.133 496.7 633.433 496.7 637.5V669.5C496.7 673.567 497.9 676.9 500.3 679.5C502.767 682.033 506 683.3 510 683.3ZM510 659.4C508.2 659.4 506.733 658.833 505.6 657.7C504.533 656.567 504 655.067 504 653.2C504 651.4 504.533 649.967 505.6 648.9C506.733 647.767 508.2 647.2 510 647.2C511.8 647.2 513.233 647.767 514.3 648.9C515.433 649.967 516 651.4 516 653.2C516 655.067 515.433 656.567 514.3 657.7C513.233 658.833 511.8 659.4 510 659.4ZM489 780V771.8H508.8V714.5L489 729.3V719.3L505.5 707H517.8V771.8H534V780H489Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M510 885C505.533 885 501.633 884.133 498.3 882.4C495.033 880.6 492.5 878.1 490.7 874.9C488.9 871.633 488 867.833 488 863.5V831.5C488 827.1 488.9 823.3 490.7 820.1C492.5 816.9 495.033 814.433 498.3 812.7C501.633 810.9 505.533 810 510 810C514.533 810 518.433 810.9 521.7 812.7C524.967 814.433 527.5 816.9 529.3 820.1C531.1 823.3 532 827.1 532 831.5V863.5C532 867.833 531.1 871.633 529.3 874.9C527.5 878.1 524.933 880.6 521.6 882.4C518.333 884.133 514.467 885 510 885ZM510 877.3C514 877.3 517.2 876.033 519.6 873.5C522.067 870.9 523.3 867.567 523.3 863.5V831.5C523.3 827.433 522.067 824.133 519.6 821.6C517.2 819 514 817.7 510 817.7C506 817.7 502.767 819 500.3 821.6C497.9 824.133 496.7 827.433 496.7 831.5V863.5C496.7 867.567 497.9 870.9 500.3 873.5C502.767 876.033 506 877.3 510 877.3ZM510 853.4C508.2 853.4 506.733 852.833 505.6 851.7C504.533 850.567 504 849.067 504 847.2C504 845.4 504.533 843.967 505.6 842.9C506.733 841.767 508.2 841.2 510 841.2C511.8 841.2 513.233 841.767 514.3 842.9C515.433 843.967 516 845.4 516 847.2C516 849.067 515.433 850.567 514.3 851.7C513.233 852.833 511.8 853.4 510 853.4ZM489 974V965.8H508.8V908.5L489 923.3V913.3L505.5 901H517.8V965.8H534V974H489Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M109 108V99.8H128.8V42.5L109 57.3V47.3L125.5 35H137.8V99.8H154V108H109ZM130 199C125.533 199 121.633 198.133 118.3 196.4C115.033 194.6 112.5 192.1 110.7 188.9C108.9 185.633 108 181.833 108 177.5V145.5C108 141.1 108.9 137.3 110.7 134.1C112.5 130.9 115.033 128.433 118.3 126.7C121.633 124.9 125.533 124 130 124C134.533 124 138.433 124.9 141.7 126.7C144.967 128.433 147.5 130.9 149.3 134.1C151.1 137.3 152 141.1 152 145.5V177.5C152 181.833 151.1 185.633 149.3 188.9C147.5 192.1 144.933 194.6 141.6 196.4C138.333 198.133 134.467 199 130 199ZM130 191.3C134 191.3 137.2 190.033 139.6 187.5C142.067 184.9 143.3 181.567 143.3 177.5V145.5C143.3 141.433 142.067 138.133 139.6 135.6C137.2 133 134 131.7 130 131.7C126 131.7 122.767 133 120.3 135.6C117.9 138.133 116.7 141.433 116.7 145.5V177.5C116.7 181.567 117.9 184.9 120.3 187.5C122.767 190.033 126 191.3 130 191.3ZM130 167.4C128.2 167.4 126.733 166.833 125.6 165.7C124.533 164.567 124 163.067 124 161.2C124 159.4 124.533 157.967 125.6 156.9C126.733 155.767 128.2 155.2 130 155.2C131.8 155.2 133.233 155.767 134.3 156.9C135.433 157.967 136 159.4 136 161.2C136 163.067 135.433 164.567 134.3 165.7C133.233 166.833 131.8 167.4 130 167.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M109 302V293.8H128.8V236.5L109 251.3V241.3L125.5 229H137.8V293.8H154V302H109ZM130 393C125.533 393 121.633 392.133 118.3 390.4C115.033 388.6 112.5 386.1 110.7 382.9C108.9 379.633 108 375.833 108 371.5V339.5C108 335.1 108.9 331.3 110.7 328.1C112.5 324.9 115.033 322.433 118.3 320.7C121.633 318.9 125.533 318 130 318C134.533 318 138.433 318.9 141.7 320.7C144.967 322.433 147.5 324.9 149.3 328.1C151.1 331.3 152 335.1 152 339.5V371.5C152 375.833 151.1 379.633 149.3 382.9C147.5 386.1 144.933 388.6 141.6 390.4C138.333 392.133 134.467 393 130 393ZM130 385.3C134 385.3 137.2 384.033 139.6 381.5C142.067 378.9 143.3 375.567 143.3 371.5V339.5C143.3 335.433 142.067 332.133 139.6 329.6C137.2 327 134 325.7 130 325.7C126 325.7 122.767 327 120.3 329.6C117.9 332.133 116.7 335.433 116.7 339.5V371.5C116.7 375.567 117.9 378.9 120.3 381.5C122.767 384.033 126 385.3 130 385.3ZM130 361.4C128.2 361.4 126.733 360.833 125.6 359.7C124.533 358.567 124 357.067 124 355.2C124 353.4 124.533 351.967 125.6 350.9C126.733 349.767 128.2 349.2 130 349.2C131.8 349.2 133.233 349.767 134.3 350.9C135.433 351.967 136 353.4 136 355.2C136 357.067 135.433 358.567 134.3 359.7C133.233 360.833 131.8 361.4 130 361.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M109 496V487.8H128.8V430.5L109 445.3V435.3L125.5 423H137.8V487.8H154V496H109ZM130 587C125.533 587 121.633 586.133 118.3 584.4C115.033 582.6 112.5 580.1 110.7 576.9C108.9 573.633 108 569.833 108 565.5V533.5C108 529.1 108.9 525.3 110.7 522.1C112.5 518.9 115.033 516.433 118.3 514.7C121.633 512.9 125.533 512 130 512C134.533 512 138.433 512.9 141.7 514.7C144.967 516.433 147.5 518.9 149.3 522.1C151.1 525.3 152 529.1 152 533.5V565.5C152 569.833 151.1 573.633 149.3 576.9C147.5 580.1 144.933 582.6 141.6 584.4C138.333 586.133 134.467 587 130 587ZM130 579.3C134 579.3 137.2 578.033 139.6 575.5C142.067 572.9 143.3 569.567 143.3 565.5V533.5C143.3 529.433 142.067 526.133 139.6 523.6C137.2 521 134 519.7 130 519.7C126 519.7 122.767 521 120.3 523.6C117.9 526.133 116.7 529.433 116.7 533.5V565.5C116.7 569.567 117.9 572.9 120.3 575.5C122.767 578.033 126 579.3 130 579.3ZM130 555.4C128.2 555.4 126.733 554.833 125.6 553.7C124.533 552.567 124 551.067 124 549.2C124 547.4 124.533 545.967 125.6 544.9C126.733 543.767 128.2 543.2 130 543.2C131.8 543.2 133.233 543.767 134.3 544.9C135.433 545.967 136 547.4 136 549.2C136 551.067 135.433 552.567 134.3 553.7C133.233 554.833 131.8 555.4 130 555.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M109 690V681.8H128.8V624.5L109 639.3V629.3L125.5 617H137.8V681.8H154V690H109ZM130 781C125.533 781 121.633 780.133 118.3 778.4C115.033 776.6 112.5 774.1 110.7 770.9C108.9 767.633 108 763.833 108 759.5V727.5C108 723.1 108.9 719.3 110.7 716.1C112.5 712.9 115.033 710.433 118.3 708.7C121.633 706.9 125.533 706 130 706C134.533 706 138.433 706.9 141.7 708.7C144.967 710.433 147.5 712.9 149.3 716.1C151.1 719.3 152 723.1 152 727.5V759.5C152 763.833 151.1 767.633 149.3 770.9C147.5 774.1 144.933 776.6 141.6 778.4C138.333 780.133 134.467 781 130 781ZM130 773.3C134 773.3 137.2 772.033 139.6 769.5C142.067 766.9 143.3 763.567 143.3 759.5V727.5C143.3 723.433 142.067 720.133 139.6 717.6C137.2 715 134 713.7 130 713.7C126 713.7 122.767 715 120.3 717.6C117.9 720.133 116.7 723.433 116.7 727.5V759.5C116.7 763.567 117.9 766.9 120.3 769.5C122.767 772.033 126 773.3 130 773.3ZM130 749.4C128.2 749.4 126.733 748.833 125.6 747.7C124.533 746.567 124 745.067 124 743.2C124 741.4 124.533 739.967 125.6 738.9C126.733 737.767 128.2 737.2 130 737.2C131.8 737.2 133.233 737.767 134.3 738.9C135.433 739.967 136 741.4 136 743.2C136 745.067 135.433 746.567 134.3 747.7C133.233 748.833 131.8 749.4 130 749.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M109 884V875.8H128.8V818.5L109 833.3V823.3L125.5 811H137.8V875.8H154V884H109ZM130 975C125.533 975 121.633 974.133 118.3 972.4C115.033 970.6 112.5 968.1 110.7 964.9C108.9 961.633 108 957.833 108 953.5V921.5C108 917.1 108.9 913.3 110.7 910.1C112.5 906.9 115.033 904.433 118.3 902.7C121.633 900.9 125.533 900 130 900C134.533 900 138.433 900.9 141.7 902.7C144.967 904.433 147.5 906.9 149.3 910.1C151.1 913.3 152 917.1 152 921.5V953.5C152 957.833 151.1 961.633 149.3 964.9C147.5 968.1 144.933 970.6 141.6 972.4C138.333 974.133 134.467 975 130 975ZM130 967.3C134 967.3 137.2 966.033 139.6 963.5C142.067 960.9 143.3 957.567 143.3 953.5V921.5C143.3 917.433 142.067 914.133 139.6 911.6C137.2 909 134 907.7 130 907.7C126 907.7 122.767 909 120.3 911.6C117.9 914.133 116.7 917.433 116.7 921.5V953.5C116.7 957.567 117.9 960.9 120.3 963.5C122.767 966.033 126 967.3 130 967.3ZM130 943.4C128.2 943.4 126.733 942.833 125.6 941.7C124.533 940.567 124 939.067 124 937.2C124 935.4 124.533 933.967 125.6 932.9C126.733 931.767 128.2 931.2 130 931.2C131.8 931.2 133.233 931.767 134.3 932.9C135.433 933.967 136 935.4 136 937.2C136 939.067 135.433 940.567 134.3 941.7C133.233 942.833 131.8 943.4 130 943.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M717 108V99.8H736.8V42.5L717 57.3V47.3L733.5 35H745.8V99.8H762V108H717ZM738 199C733.533 199 729.633 198.133 726.3 196.4C723.033 194.6 720.5 192.1 718.7 188.9C716.9 185.633 716 181.833 716 177.5V145.5C716 141.1 716.9 137.3 718.7 134.1C720.5 130.9 723.033 128.433 726.3 126.7C729.633 124.9 733.533 124 738 124C742.533 124 746.433 124.9 749.7 126.7C752.967 128.433 755.5 130.9 757.3 134.1C759.1 137.3 760 141.1 760 145.5V177.5C760 181.833 759.1 185.633 757.3 188.9C755.5 192.1 752.933 194.6 749.6 196.4C746.333 198.133 742.467 199 738 199ZM738 191.3C742 191.3 745.2 190.033 747.6 187.5C750.067 184.9 751.3 181.567 751.3 177.5V145.5C751.3 141.433 750.067 138.133 747.6 135.6C745.2 133 742 131.7 738 131.7C734 131.7 730.767 133 728.3 135.6C725.9 138.133 724.7 141.433 724.7 145.5V177.5C724.7 181.567 725.9 184.9 728.3 187.5C730.767 190.033 734 191.3 738 191.3ZM738 167.4C736.2 167.4 734.733 166.833 733.6 165.7C732.533 164.567 732 163.067 732 161.2C732 159.4 732.533 157.967 733.6 156.9C734.733 155.767 736.2 155.2 738 155.2C739.8 155.2 741.233 155.767 742.3 156.9C743.433 157.967 744 159.4 744 161.2C744 163.067 743.433 164.567 742.3 165.7C741.233 166.833 739.8 167.4 738 167.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M717 302V293.8H736.8V236.5L717 251.3V241.3L733.5 229H745.8V293.8H762V302H717ZM738 393C733.533 393 729.633 392.133 726.3 390.4C723.033 388.6 720.5 386.1 718.7 382.9C716.9 379.633 716 375.833 716 371.5V339.5C716 335.1 716.9 331.3 718.7 328.1C720.5 324.9 723.033 322.433 726.3 320.7C729.633 318.9 733.533 318 738 318C742.533 318 746.433 318.9 749.7 320.7C752.967 322.433 755.5 324.9 757.3 328.1C759.1 331.3 760 335.1 760 339.5V371.5C760 375.833 759.1 379.633 757.3 382.9C755.5 386.1 752.933 388.6 749.6 390.4C746.333 392.133 742.467 393 738 393ZM738 385.3C742 385.3 745.2 384.033 747.6 381.5C750.067 378.9 751.3 375.567 751.3 371.5V339.5C751.3 335.433 750.067 332.133 747.6 329.6C745.2 327 742 325.7 738 325.7C734 325.7 730.767 327 728.3 329.6C725.9 332.133 724.7 335.433 724.7 339.5V371.5C724.7 375.567 725.9 378.9 728.3 381.5C730.767 384.033 734 385.3 738 385.3ZM738 361.4C736.2 361.4 734.733 360.833 733.6 359.7C732.533 358.567 732 357.067 732 355.2C732 353.4 732.533 351.967 733.6 350.9C734.733 349.767 736.2 349.2 738 349.2C739.8 349.2 741.233 349.767 742.3 350.9C743.433 351.967 744 353.4 744 355.2C744 357.067 743.433 358.567 742.3 359.7C741.233 360.833 739.8 361.4 738 361.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M717 496V487.8H736.8V430.5L717 445.3V435.3L733.5 423H745.8V487.8H762V496H717ZM738 587C733.533 587 729.633 586.133 726.3 584.4C723.033 582.6 720.5 580.1 718.7 576.9C716.9 573.633 716 569.833 716 565.5V533.5C716 529.1 716.9 525.3 718.7 522.1C720.5 518.9 723.033 516.433 726.3 514.7C729.633 512.9 733.533 512 738 512C742.533 512 746.433 512.9 749.7 514.7C752.967 516.433 755.5 518.9 757.3 522.1C759.1 525.3 760 529.1 760 533.5V565.5C760 569.833 759.1 573.633 757.3 576.9C755.5 580.1 752.933 582.6 749.6 584.4C746.333 586.133 742.467 587 738 587ZM738 579.3C742 579.3 745.2 578.033 747.6 575.5C750.067 572.9 751.3 569.567 751.3 565.5V533.5C751.3 529.433 750.067 526.133 747.6 523.6C745.2 521 742 519.7 738 519.7C734 519.7 730.767 521 728.3 523.6C725.9 526.133 724.7 529.433 724.7 533.5V565.5C724.7 569.567 725.9 572.9 728.3 575.5C730.767 578.033 734 579.3 738 579.3ZM738 555.4C736.2 555.4 734.733 554.833 733.6 553.7C732.533 552.567 732 551.067 732 549.2C732 547.4 732.533 545.967 733.6 544.9C734.733 543.767 736.2 543.2 738 543.2C739.8 543.2 741.233 543.767 742.3 544.9C743.433 545.967 744 547.4 744 549.2C744 551.067 743.433 552.567 742.3 553.7C741.233 554.833 739.8 555.4 738 555.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M717 690V681.8H736.8V624.5L717 639.3V629.3L733.5 617H745.8V681.8H762V690H717ZM738 781C733.533 781 729.633 780.133 726.3 778.4C723.033 776.6 720.5 774.1 718.7 770.9C716.9 767.633 716 763.833 716 759.5V727.5C716 723.1 716.9 719.3 718.7 716.1C720.5 712.9 723.033 710.433 726.3 708.7C729.633 706.9 733.533 706 738 706C742.533 706 746.433 706.9 749.7 708.7C752.967 710.433 755.5 712.9 757.3 716.1C759.1 719.3 760 723.1 760 727.5V759.5C760 763.833 759.1 767.633 757.3 770.9C755.5 774.1 752.933 776.6 749.6 778.4C746.333 780.133 742.467 781 738 781ZM738 773.3C742 773.3 745.2 772.033 747.6 769.5C750.067 766.9 751.3 763.567 751.3 759.5V727.5C751.3 723.433 750.067 720.133 747.6 717.6C745.2 715 742 713.7 738 713.7C734 713.7 730.767 715 728.3 717.6C725.9 720.133 724.7 723.433 724.7 727.5V759.5C724.7 763.567 725.9 766.9 728.3 769.5C730.767 772.033 734 773.3 738 773.3ZM738 749.4C736.2 749.4 734.733 748.833 733.6 747.7C732.533 746.567 732 745.067 732 743.2C732 741.4 732.533 739.967 733.6 738.9C734.733 737.767 736.2 737.2 738 737.2C739.8 737.2 741.233 737.767 742.3 738.9C743.433 739.967 744 741.4 744 743.2C744 745.067 743.433 746.567 742.3 747.7C741.233 748.833 739.8 749.4 738 749.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M717 884V875.8H736.8V818.5L717 833.3V823.3L733.5 811H745.8V875.8H762V884H717ZM738 975C733.533 975 729.633 974.133 726.3 972.4C723.033 970.6 720.5 968.1 718.7 964.9C716.9 961.633 716 957.833 716 953.5V921.5C716 917.1 716.9 913.3 718.7 910.1C720.5 906.9 723.033 904.433 726.3 902.7C729.633 900.9 733.533 900 738 900C742.533 900 746.433 900.9 749.7 902.7C752.967 904.433 755.5 906.9 757.3 910.1C759.1 913.3 760 917.1 760 921.5V953.5C760 957.833 759.1 961.633 757.3 964.9C755.5 968.1 752.933 970.6 749.6 972.4C746.333 974.133 742.467 975 738 975ZM738 967.3C742 967.3 745.2 966.033 747.6 963.5C750.067 960.9 751.3 957.567 751.3 953.5V921.5C751.3 917.433 750.067 914.133 747.6 911.6C745.2 909 742 907.7 738 907.7C734 907.7 730.767 909 728.3 911.6C725.9 914.133 724.7 917.433 724.7 921.5V953.5C724.7 957.567 725.9 960.9 728.3 963.5C730.767 966.033 734 967.3 738 967.3ZM738 943.4C736.2 943.4 734.733 942.833 733.6 941.7C732.533 940.567 732 939.067 732 937.2C732 935.4 732.533 933.967 733.6 932.9C734.733 931.767 736.2 931.2 738 931.2C739.8 931.2 741.233 931.767 742.3 932.9C743.433 933.967 744 935.4 744 937.2C744 939.067 743.433 940.567 742.3 941.7C741.233 942.833 739.8 943.4 738 943.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M413 108V99.8H432.8V42.5L413 57.3V47.3L429.5 35H441.8V99.8H458V108H413ZM434 199C429.533 199 425.633 198.133 422.3 196.4C419.033 194.6 416.5 192.1 414.7 188.9C412.9 185.633 412 181.833 412 177.5V145.5C412 141.1 412.9 137.3 414.7 134.1C416.5 130.9 419.033 128.433 422.3 126.7C425.633 124.9 429.533 124 434 124C438.533 124 442.433 124.9 445.7 126.7C448.967 128.433 451.5 130.9 453.3 134.1C455.1 137.3 456 141.1 456 145.5V177.5C456 181.833 455.1 185.633 453.3 188.9C451.5 192.1 448.933 194.6 445.6 196.4C442.333 198.133 438.467 199 434 199ZM434 191.3C438 191.3 441.2 190.033 443.6 187.5C446.067 184.9 447.3 181.567 447.3 177.5V145.5C447.3 141.433 446.067 138.133 443.6 135.6C441.2 133 438 131.7 434 131.7C430 131.7 426.767 133 424.3 135.6C421.9 138.133 420.7 141.433 420.7 145.5V177.5C420.7 181.567 421.9 184.9 424.3 187.5C426.767 190.033 430 191.3 434 191.3ZM434 167.4C432.2 167.4 430.733 166.833 429.6 165.7C428.533 164.567 428 163.067 428 161.2C428 159.4 428.533 157.967 429.6 156.9C430.733 155.767 432.2 155.2 434 155.2C435.8 155.2 437.233 155.767 438.3 156.9C439.433 157.967 440 159.4 440 161.2C440 163.067 439.433 164.567 438.3 165.7C437.233 166.833 435.8 167.4 434 167.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M413 302V293.8H432.8V236.5L413 251.3V241.3L429.5 229H441.8V293.8H458V302H413ZM434 393C429.533 393 425.633 392.133 422.3 390.4C419.033 388.6 416.5 386.1 414.7 382.9C412.9 379.633 412 375.833 412 371.5V339.5C412 335.1 412.9 331.3 414.7 328.1C416.5 324.9 419.033 322.433 422.3 320.7C425.633 318.9 429.533 318 434 318C438.533 318 442.433 318.9 445.7 320.7C448.967 322.433 451.5 324.9 453.3 328.1C455.1 331.3 456 335.1 456 339.5V371.5C456 375.833 455.1 379.633 453.3 382.9C451.5 386.1 448.933 388.6 445.6 390.4C442.333 392.133 438.467 393 434 393ZM434 385.3C438 385.3 441.2 384.033 443.6 381.5C446.067 378.9 447.3 375.567 447.3 371.5V339.5C447.3 335.433 446.067 332.133 443.6 329.6C441.2 327 438 325.7 434 325.7C430 325.7 426.767 327 424.3 329.6C421.9 332.133 420.7 335.433 420.7 339.5V371.5C420.7 375.567 421.9 378.9 424.3 381.5C426.767 384.033 430 385.3 434 385.3ZM434 361.4C432.2 361.4 430.733 360.833 429.6 359.7C428.533 358.567 428 357.067 428 355.2C428 353.4 428.533 351.967 429.6 350.9C430.733 349.767 432.2 349.2 434 349.2C435.8 349.2 437.233 349.767 438.3 350.9C439.433 351.967 440 353.4 440 355.2C440 357.067 439.433 358.567 438.3 359.7C437.233 360.833 435.8 361.4 434 361.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M413 496V487.8H432.8V430.5L413 445.3V435.3L429.5 423H441.8V487.8H458V496H413ZM434 587C429.533 587 425.633 586.133 422.3 584.4C419.033 582.6 416.5 580.1 414.7 576.9C412.9 573.633 412 569.833 412 565.5V533.5C412 529.1 412.9 525.3 414.7 522.1C416.5 518.9 419.033 516.433 422.3 514.7C425.633 512.9 429.533 512 434 512C438.533 512 442.433 512.9 445.7 514.7C448.967 516.433 451.5 518.9 453.3 522.1C455.1 525.3 456 529.1 456 533.5V565.5C456 569.833 455.1 573.633 453.3 576.9C451.5 580.1 448.933 582.6 445.6 584.4C442.333 586.133 438.467 587 434 587ZM434 579.3C438 579.3 441.2 578.033 443.6 575.5C446.067 572.9 447.3 569.567 447.3 565.5V533.5C447.3 529.433 446.067 526.133 443.6 523.6C441.2 521 438 519.7 434 519.7C430 519.7 426.767 521 424.3 523.6C421.9 526.133 420.7 529.433 420.7 533.5V565.5C420.7 569.567 421.9 572.9 424.3 575.5C426.767 578.033 430 579.3 434 579.3ZM434 555.4C432.2 555.4 430.733 554.833 429.6 553.7C428.533 552.567 428 551.067 428 549.2C428 547.4 428.533 545.967 429.6 544.9C430.733 543.767 432.2 543.2 434 543.2C435.8 543.2 437.233 543.767 438.3 544.9C439.433 545.967 440 547.4 440 549.2C440 551.067 439.433 552.567 438.3 553.7C437.233 554.833 435.8 555.4 434 555.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M413 690V681.8H432.8V624.5L413 639.3V629.3L429.5 617H441.8V681.8H458V690H413ZM434 781C429.533 781 425.633 780.133 422.3 778.4C419.033 776.6 416.5 774.1 414.7 770.9C412.9 767.633 412 763.833 412 759.5V727.5C412 723.1 412.9 719.3 414.7 716.1C416.5 712.9 419.033 710.433 422.3 708.7C425.633 706.9 429.533 706 434 706C438.533 706 442.433 706.9 445.7 708.7C448.967 710.433 451.5 712.9 453.3 716.1C455.1 719.3 456 723.1 456 727.5V759.5C456 763.833 455.1 767.633 453.3 770.9C451.5 774.1 448.933 776.6 445.6 778.4C442.333 780.133 438.467 781 434 781ZM434 773.3C438 773.3 441.2 772.033 443.6 769.5C446.067 766.9 447.3 763.567 447.3 759.5V727.5C447.3 723.433 446.067 720.133 443.6 717.6C441.2 715 438 713.7 434 713.7C430 713.7 426.767 715 424.3 717.6C421.9 720.133 420.7 723.433 420.7 727.5V759.5C420.7 763.567 421.9 766.9 424.3 769.5C426.767 772.033 430 773.3 434 773.3ZM434 749.4C432.2 749.4 430.733 748.833 429.6 747.7C428.533 746.567 428 745.067 428 743.2C428 741.4 428.533 739.967 429.6 738.9C430.733 737.767 432.2 737.2 434 737.2C435.8 737.2 437.233 737.767 438.3 738.9C439.433 739.967 440 741.4 440 743.2C440 745.067 439.433 746.567 438.3 747.7C437.233 748.833 435.8 749.4 434 749.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M413 884V875.8H432.8V818.5L413 833.3V823.3L429.5 811H441.8V875.8H458V884H413ZM434 975C429.533 975 425.633 974.133 422.3 972.4C419.033 970.6 416.5 968.1 414.7 964.9C412.9 961.633 412 957.833 412 953.5V921.5C412 917.1 412.9 913.3 414.7 910.1C416.5 906.9 419.033 904.433 422.3 902.7C425.633 900.9 429.533 900 434 900C438.533 900 442.433 900.9 445.7 902.7C448.967 904.433 451.5 906.9 453.3 910.1C455.1 913.3 456 917.1 456 921.5V953.5C456 957.833 455.1 961.633 453.3 964.9C451.5 968.1 448.933 970.6 445.6 972.4C442.333 974.133 438.467 975 434 975ZM434 967.3C438 967.3 441.2 966.033 443.6 963.5C446.067 960.9 447.3 957.567 447.3 953.5V921.5C447.3 917.433 446.067 914.133 443.6 911.6C441.2 909 438 907.7 434 907.7C430 907.7 426.767 909 424.3 911.6C421.9 914.133 420.7 917.433 420.7 921.5V953.5C420.7 957.567 421.9 960.9 424.3 963.5C426.767 966.033 430 967.3 434 967.3ZM434 943.4C432.2 943.4 430.733 942.833 429.6 941.7C428.533 940.567 428 939.067 428 937.2C428 935.4 428.533 933.967 429.6 932.9C430.733 931.767 432.2 931.2 434 931.2C435.8 931.2 437.233 931.767 438.3 932.9C439.433 933.967 440 935.4 440 937.2C440 939.067 439.433 940.567 438.3 941.7C437.233 942.833 435.8 943.4 434 943.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M261 108V99.8H280.8V42.5L261 57.3V47.3L277.5 35H289.8V99.8H306V108H261ZM282 199C277.533 199 273.633 198.133 270.3 196.4C267.033 194.6 264.5 192.1 262.7 188.9C260.9 185.633 260 181.833 260 177.5V145.5C260 141.1 260.9 137.3 262.7 134.1C264.5 130.9 267.033 128.433 270.3 126.7C273.633 124.9 277.533 124 282 124C286.533 124 290.433 124.9 293.7 126.7C296.967 128.433 299.5 130.9 301.3 134.1C303.1 137.3 304 141.1 304 145.5V177.5C304 181.833 303.1 185.633 301.3 188.9C299.5 192.1 296.933 194.6 293.6 196.4C290.333 198.133 286.467 199 282 199ZM282 191.3C286 191.3 289.2 190.033 291.6 187.5C294.067 184.9 295.3 181.567 295.3 177.5V145.5C295.3 141.433 294.067 138.133 291.6 135.6C289.2 133 286 131.7 282 131.7C278 131.7 274.767 133 272.3 135.6C269.9 138.133 268.7 141.433 268.7 145.5V177.5C268.7 181.567 269.9 184.9 272.3 187.5C274.767 190.033 278 191.3 282 191.3ZM282 167.4C280.2 167.4 278.733 166.833 277.6 165.7C276.533 164.567 276 163.067 276 161.2C276 159.4 276.533 157.967 277.6 156.9C278.733 155.767 280.2 155.2 282 155.2C283.8 155.2 285.233 155.767 286.3 156.9C287.433 157.967 288 159.4 288 161.2C288 163.067 287.433 164.567 286.3 165.7C285.233 166.833 283.8 167.4 282 167.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M261 302V293.8H280.8V236.5L261 251.3V241.3L277.5 229H289.8V293.8H306V302H261ZM282 393C277.533 393 273.633 392.133 270.3 390.4C267.033 388.6 264.5 386.1 262.7 382.9C260.9 379.633 260 375.833 260 371.5V339.5C260 335.1 260.9 331.3 262.7 328.1C264.5 324.9 267.033 322.433 270.3 320.7C273.633 318.9 277.533 318 282 318C286.533 318 290.433 318.9 293.7 320.7C296.967 322.433 299.5 324.9 301.3 328.1C303.1 331.3 304 335.1 304 339.5V371.5C304 375.833 303.1 379.633 301.3 382.9C299.5 386.1 296.933 388.6 293.6 390.4C290.333 392.133 286.467 393 282 393ZM282 385.3C286 385.3 289.2 384.033 291.6 381.5C294.067 378.9 295.3 375.567 295.3 371.5V339.5C295.3 335.433 294.067 332.133 291.6 329.6C289.2 327 286 325.7 282 325.7C278 325.7 274.767 327 272.3 329.6C269.9 332.133 268.7 335.433 268.7 339.5V371.5C268.7 375.567 269.9 378.9 272.3 381.5C274.767 384.033 278 385.3 282 385.3ZM282 361.4C280.2 361.4 278.733 360.833 277.6 359.7C276.533 358.567 276 357.067 276 355.2C276 353.4 276.533 351.967 277.6 350.9C278.733 349.767 280.2 349.2 282 349.2C283.8 349.2 285.233 349.767 286.3 350.9C287.433 351.967 288 353.4 288 355.2C288 357.067 287.433 358.567 286.3 359.7C285.233 360.833 283.8 361.4 282 361.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M261 496V487.8H280.8V430.5L261 445.3V435.3L277.5 423H289.8V487.8H306V496H261ZM282 587C277.533 587 273.633 586.133 270.3 584.4C267.033 582.6 264.5 580.1 262.7 576.9C260.9 573.633 260 569.833 260 565.5V533.5C260 529.1 260.9 525.3 262.7 522.1C264.5 518.9 267.033 516.433 270.3 514.7C273.633 512.9 277.533 512 282 512C286.533 512 290.433 512.9 293.7 514.7C296.967 516.433 299.5 518.9 301.3 522.1C303.1 525.3 304 529.1 304 533.5V565.5C304 569.833 303.1 573.633 301.3 576.9C299.5 580.1 296.933 582.6 293.6 584.4C290.333 586.133 286.467 587 282 587ZM282 579.3C286 579.3 289.2 578.033 291.6 575.5C294.067 572.9 295.3 569.567 295.3 565.5V533.5C295.3 529.433 294.067 526.133 291.6 523.6C289.2 521 286 519.7 282 519.7C278 519.7 274.767 521 272.3 523.6C269.9 526.133 268.7 529.433 268.7 533.5V565.5C268.7 569.567 269.9 572.9 272.3 575.5C274.767 578.033 278 579.3 282 579.3ZM282 555.4C280.2 555.4 278.733 554.833 277.6 553.7C276.533 552.567 276 551.067 276 549.2C276 547.4 276.533 545.967 277.6 544.9C278.733 543.767 280.2 543.2 282 543.2C283.8 543.2 285.233 543.767 286.3 544.9C287.433 545.967 288 547.4 288 549.2C288 551.067 287.433 552.567 286.3 553.7C285.233 554.833 283.8 555.4 282 555.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M261 690V681.8H280.8V624.5L261 639.3V629.3L277.5 617H289.8V681.8H306V690H261ZM282 781C277.533 781 273.633 780.133 270.3 778.4C267.033 776.6 264.5 774.1 262.7 770.9C260.9 767.633 260 763.833 260 759.5V727.5C260 723.1 260.9 719.3 262.7 716.1C264.5 712.9 267.033 710.433 270.3 708.7C273.633 706.9 277.533 706 282 706C286.533 706 290.433 706.9 293.7 708.7C296.967 710.433 299.5 712.9 301.3 716.1C303.1 719.3 304 723.1 304 727.5V759.5C304 763.833 303.1 767.633 301.3 770.9C299.5 774.1 296.933 776.6 293.6 778.4C290.333 780.133 286.467 781 282 781ZM282 773.3C286 773.3 289.2 772.033 291.6 769.5C294.067 766.9 295.3 763.567 295.3 759.5V727.5C295.3 723.433 294.067 720.133 291.6 717.6C289.2 715 286 713.7 282 713.7C278 713.7 274.767 715 272.3 717.6C269.9 720.133 268.7 723.433 268.7 727.5V759.5C268.7 763.567 269.9 766.9 272.3 769.5C274.767 772.033 278 773.3 282 773.3ZM282 749.4C280.2 749.4 278.733 748.833 277.6 747.7C276.533 746.567 276 745.067 276 743.2C276 741.4 276.533 739.967 277.6 738.9C278.733 737.767 280.2 737.2 282 737.2C283.8 737.2 285.233 737.767 286.3 738.9C287.433 739.967 288 741.4 288 743.2C288 745.067 287.433 746.567 286.3 747.7C285.233 748.833 283.8 749.4 282 749.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M261 884V875.8H280.8V818.5L261 833.3V823.3L277.5 811H289.8V875.8H306V884H261ZM282 975C277.533 975 273.633 974.133 270.3 972.4C267.033 970.6 264.5 968.1 262.7 964.9C260.9 961.633 260 957.833 260 953.5V921.5C260 917.1 260.9 913.3 262.7 910.1C264.5 906.9 267.033 904.433 270.3 902.7C273.633 900.9 277.533 900 282 900C286.533 900 290.433 900.9 293.7 902.7C296.967 904.433 299.5 906.9 301.3 910.1C303.1 913.3 304 917.1 304 921.5V953.5C304 957.833 303.1 961.633 301.3 964.9C299.5 968.1 296.933 970.6 293.6 972.4C290.333 974.133 286.467 975 282 975ZM282 967.3C286 967.3 289.2 966.033 291.6 963.5C294.067 960.9 295.3 957.567 295.3 953.5V921.5C295.3 917.433 294.067 914.133 291.6 911.6C289.2 909 286 907.7 282 907.7C278 907.7 274.767 909 272.3 911.6C269.9 914.133 268.7 917.433 268.7 921.5V953.5C268.7 957.567 269.9 960.9 272.3 963.5C274.767 966.033 278 967.3 282 967.3ZM282 943.4C280.2 943.4 278.733 942.833 277.6 941.7C276.533 940.567 276 939.067 276 937.2C276 935.4 276.533 933.967 277.6 932.9C278.733 931.767 280.2 931.2 282 931.2C283.8 931.2 285.233 931.767 286.3 932.9C287.433 933.967 288 935.4 288 937.2C288 939.067 287.433 940.567 286.3 941.7C285.233 942.833 283.8 943.4 282 943.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M869 108V99.8H888.8V42.5L869 57.3V47.3L885.5 35H897.8V99.8H914V108H869ZM890 199C885.533 199 881.633 198.133 878.3 196.4C875.033 194.6 872.5 192.1 870.7 188.9C868.9 185.633 868 181.833 868 177.5V145.5C868 141.1 868.9 137.3 870.7 134.1C872.5 130.9 875.033 128.433 878.3 126.7C881.633 124.9 885.533 124 890 124C894.533 124 898.433 124.9 901.7 126.7C904.967 128.433 907.5 130.9 909.3 134.1C911.1 137.3 912 141.1 912 145.5V177.5C912 181.833 911.1 185.633 909.3 188.9C907.5 192.1 904.933 194.6 901.6 196.4C898.333 198.133 894.467 199 890 199ZM890 191.3C894 191.3 897.2 190.033 899.6 187.5C902.067 184.9 903.3 181.567 903.3 177.5V145.5C903.3 141.433 902.067 138.133 899.6 135.6C897.2 133 894 131.7 890 131.7C886 131.7 882.767 133 880.3 135.6C877.9 138.133 876.7 141.433 876.7 145.5V177.5C876.7 181.567 877.9 184.9 880.3 187.5C882.767 190.033 886 191.3 890 191.3ZM890 167.4C888.2 167.4 886.733 166.833 885.6 165.7C884.533 164.567 884 163.067 884 161.2C884 159.4 884.533 157.967 885.6 156.9C886.733 155.767 888.2 155.2 890 155.2C891.8 155.2 893.233 155.767 894.3 156.9C895.433 157.967 896 159.4 896 161.2C896 163.067 895.433 164.567 894.3 165.7C893.233 166.833 891.8 167.4 890 167.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M869 302V293.8H888.8V236.5L869 251.3V241.3L885.5 229H897.8V293.8H914V302H869ZM890 393C885.533 393 881.633 392.133 878.3 390.4C875.033 388.6 872.5 386.1 870.7 382.9C868.9 379.633 868 375.833 868 371.5V339.5C868 335.1 868.9 331.3 870.7 328.1C872.5 324.9 875.033 322.433 878.3 320.7C881.633 318.9 885.533 318 890 318C894.533 318 898.433 318.9 901.7 320.7C904.967 322.433 907.5 324.9 909.3 328.1C911.1 331.3 912 335.1 912 339.5V371.5C912 375.833 911.1 379.633 909.3 382.9C907.5 386.1 904.933 388.6 901.6 390.4C898.333 392.133 894.467 393 890 393ZM890 385.3C894 385.3 897.2 384.033 899.6 381.5C902.067 378.9 903.3 375.567 903.3 371.5V339.5C903.3 335.433 902.067 332.133 899.6 329.6C897.2 327 894 325.7 890 325.7C886 325.7 882.767 327 880.3 329.6C877.9 332.133 876.7 335.433 876.7 339.5V371.5C876.7 375.567 877.9 378.9 880.3 381.5C882.767 384.033 886 385.3 890 385.3ZM890 361.4C888.2 361.4 886.733 360.833 885.6 359.7C884.533 358.567 884 357.067 884 355.2C884 353.4 884.533 351.967 885.6 350.9C886.733 349.767 888.2 349.2 890 349.2C891.8 349.2 893.233 349.767 894.3 350.9C895.433 351.967 896 353.4 896 355.2C896 357.067 895.433 358.567 894.3 359.7C893.233 360.833 891.8 361.4 890 361.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M869 496V487.8H888.8V430.5L869 445.3V435.3L885.5 423H897.8V487.8H914V496H869ZM890 587C885.533 587 881.633 586.133 878.3 584.4C875.033 582.6 872.5 580.1 870.7 576.9C868.9 573.633 868 569.833 868 565.5V533.5C868 529.1 868.9 525.3 870.7 522.1C872.5 518.9 875.033 516.433 878.3 514.7C881.633 512.9 885.533 512 890 512C894.533 512 898.433 512.9 901.7 514.7C904.967 516.433 907.5 518.9 909.3 522.1C911.1 525.3 912 529.1 912 533.5V565.5C912 569.833 911.1 573.633 909.3 576.9C907.5 580.1 904.933 582.6 901.6 584.4C898.333 586.133 894.467 587 890 587ZM890 579.3C894 579.3 897.2 578.033 899.6 575.5C902.067 572.9 903.3 569.567 903.3 565.5V533.5C903.3 529.433 902.067 526.133 899.6 523.6C897.2 521 894 519.7 890 519.7C886 519.7 882.767 521 880.3 523.6C877.9 526.133 876.7 529.433 876.7 533.5V565.5C876.7 569.567 877.9 572.9 880.3 575.5C882.767 578.033 886 579.3 890 579.3ZM890 555.4C888.2 555.4 886.733 554.833 885.6 553.7C884.533 552.567 884 551.067 884 549.2C884 547.4 884.533 545.967 885.6 544.9C886.733 543.767 888.2 543.2 890 543.2C891.8 543.2 893.233 543.767 894.3 544.9C895.433 545.967 896 547.4 896 549.2C896 551.067 895.433 552.567 894.3 553.7C893.233 554.833 891.8 555.4 890 555.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M869 690V681.8H888.8V624.5L869 639.3V629.3L885.5 617H897.8V681.8H914V690H869ZM890 781C885.533 781 881.633 780.133 878.3 778.4C875.033 776.6 872.5 774.1 870.7 770.9C868.9 767.633 868 763.833 868 759.5V727.5C868 723.1 868.9 719.3 870.7 716.1C872.5 712.9 875.033 710.433 878.3 708.7C881.633 706.9 885.533 706 890 706C894.533 706 898.433 706.9 901.7 708.7C904.967 710.433 907.5 712.9 909.3 716.1C911.1 719.3 912 723.1 912 727.5V759.5C912 763.833 911.1 767.633 909.3 770.9C907.5 774.1 904.933 776.6 901.6 778.4C898.333 780.133 894.467 781 890 781ZM890 773.3C894 773.3 897.2 772.033 899.6 769.5C902.067 766.9 903.3 763.567 903.3 759.5V727.5C903.3 723.433 902.067 720.133 899.6 717.6C897.2 715 894 713.7 890 713.7C886 713.7 882.767 715 880.3 717.6C877.9 720.133 876.7 723.433 876.7 727.5V759.5C876.7 763.567 877.9 766.9 880.3 769.5C882.767 772.033 886 773.3 890 773.3ZM890 749.4C888.2 749.4 886.733 748.833 885.6 747.7C884.533 746.567 884 745.067 884 743.2C884 741.4 884.533 739.967 885.6 738.9C886.733 737.767 888.2 737.2 890 737.2C891.8 737.2 893.233 737.767 894.3 738.9C895.433 739.967 896 741.4 896 743.2C896 745.067 895.433 746.567 894.3 747.7C893.233 748.833 891.8 749.4 890 749.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M869 884V875.8H888.8V818.5L869 833.3V823.3L885.5 811H897.8V875.8H914V884H869ZM890 975C885.533 975 881.633 974.133 878.3 972.4C875.033 970.6 872.5 968.1 870.7 964.9C868.9 961.633 868 957.833 868 953.5V921.5C868 917.1 868.9 913.3 870.7 910.1C872.5 906.9 875.033 904.433 878.3 902.7C881.633 900.9 885.533 900 890 900C894.533 900 898.433 900.9 901.7 902.7C904.967 904.433 907.5 906.9 909.3 910.1C911.1 913.3 912 917.1 912 921.5V953.5C912 957.833 911.1 961.633 909.3 964.9C907.5 968.1 904.933 970.6 901.6 972.4C898.333 974.133 894.467 975 890 975ZM890 967.3C894 967.3 897.2 966.033 899.6 963.5C902.067 960.9 903.3 957.567 903.3 953.5V921.5C903.3 917.433 902.067 914.133 899.6 911.6C897.2 909 894 907.7 890 907.7C886 907.7 882.767 909 880.3 911.6C877.9 914.133 876.7 917.433 876.7 921.5V953.5C876.7 957.567 877.9 960.9 880.3 963.5C882.767 966.033 886 967.3 890 967.3ZM890 943.4C888.2 943.4 886.733 942.833 885.6 941.7C884.533 940.567 884 939.067 884 937.2C884 935.4 884.533 933.967 885.6 932.9C886.733 931.767 888.2 931.2 890 931.2C891.8 931.2 893.233 931.767 894.3 932.9C895.433 933.967 896 935.4 896 937.2C896 939.067 895.433 940.567 894.3 941.7C893.233 942.833 891.8 943.4 890 943.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M565 108V99.8H584.8V42.5L565 57.3V47.3L581.5 35H593.8V99.8H610V108H565ZM586 199C581.533 199 577.633 198.133 574.3 196.4C571.033 194.6 568.5 192.1 566.7 188.9C564.9 185.633 564 181.833 564 177.5V145.5C564 141.1 564.9 137.3 566.7 134.1C568.5 130.9 571.033 128.433 574.3 126.7C577.633 124.9 581.533 124 586 124C590.533 124 594.433 124.9 597.7 126.7C600.967 128.433 603.5 130.9 605.3 134.1C607.1 137.3 608 141.1 608 145.5V177.5C608 181.833 607.1 185.633 605.3 188.9C603.5 192.1 600.933 194.6 597.6 196.4C594.333 198.133 590.467 199 586 199ZM586 191.3C590 191.3 593.2 190.033 595.6 187.5C598.067 184.9 599.3 181.567 599.3 177.5V145.5C599.3 141.433 598.067 138.133 595.6 135.6C593.2 133 590 131.7 586 131.7C582 131.7 578.767 133 576.3 135.6C573.9 138.133 572.7 141.433 572.7 145.5V177.5C572.7 181.567 573.9 184.9 576.3 187.5C578.767 190.033 582 191.3 586 191.3ZM586 167.4C584.2 167.4 582.733 166.833 581.6 165.7C580.533 164.567 580 163.067 580 161.2C580 159.4 580.533 157.967 581.6 156.9C582.733 155.767 584.2 155.2 586 155.2C587.8 155.2 589.233 155.767 590.3 156.9C591.433 157.967 592 159.4 592 161.2C592 163.067 591.433 164.567 590.3 165.7C589.233 166.833 587.8 167.4 586 167.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M565 302V293.8H584.8V236.5L565 251.3V241.3L581.5 229H593.8V293.8H610V302H565ZM586 393C581.533 393 577.633 392.133 574.3 390.4C571.033 388.6 568.5 386.1 566.7 382.9C564.9 379.633 564 375.833 564 371.5V339.5C564 335.1 564.9 331.3 566.7 328.1C568.5 324.9 571.033 322.433 574.3 320.7C577.633 318.9 581.533 318 586 318C590.533 318 594.433 318.9 597.7 320.7C600.967 322.433 603.5 324.9 605.3 328.1C607.1 331.3 608 335.1 608 339.5V371.5C608 375.833 607.1 379.633 605.3 382.9C603.5 386.1 600.933 388.6 597.6 390.4C594.333 392.133 590.467 393 586 393ZM586 385.3C590 385.3 593.2 384.033 595.6 381.5C598.067 378.9 599.3 375.567 599.3 371.5V339.5C599.3 335.433 598.067 332.133 595.6 329.6C593.2 327 590 325.7 586 325.7C582 325.7 578.767 327 576.3 329.6C573.9 332.133 572.7 335.433 572.7 339.5V371.5C572.7 375.567 573.9 378.9 576.3 381.5C578.767 384.033 582 385.3 586 385.3ZM586 361.4C584.2 361.4 582.733 360.833 581.6 359.7C580.533 358.567 580 357.067 580 355.2C580 353.4 580.533 351.967 581.6 350.9C582.733 349.767 584.2 349.2 586 349.2C587.8 349.2 589.233 349.767 590.3 350.9C591.433 351.967 592 353.4 592 355.2C592 357.067 591.433 358.567 590.3 359.7C589.233 360.833 587.8 361.4 586 361.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M565 496V487.8H584.8V430.5L565 445.3V435.3L581.5 423H593.8V487.8H610V496H565ZM586 587C581.533 587 577.633 586.133 574.3 584.4C571.033 582.6 568.5 580.1 566.7 576.9C564.9 573.633 564 569.833 564 565.5V533.5C564 529.1 564.9 525.3 566.7 522.1C568.5 518.9 571.033 516.433 574.3 514.7C577.633 512.9 581.533 512 586 512C590.533 512 594.433 512.9 597.7 514.7C600.967 516.433 603.5 518.9 605.3 522.1C607.1 525.3 608 529.1 608 533.5V565.5C608 569.833 607.1 573.633 605.3 576.9C603.5 580.1 600.933 582.6 597.6 584.4C594.333 586.133 590.467 587 586 587ZM586 579.3C590 579.3 593.2 578.033 595.6 575.5C598.067 572.9 599.3 569.567 599.3 565.5V533.5C599.3 529.433 598.067 526.133 595.6 523.6C593.2 521 590 519.7 586 519.7C582 519.7 578.767 521 576.3 523.6C573.9 526.133 572.7 529.433 572.7 533.5V565.5C572.7 569.567 573.9 572.9 576.3 575.5C578.767 578.033 582 579.3 586 579.3ZM586 555.4C584.2 555.4 582.733 554.833 581.6 553.7C580.533 552.567 580 551.067 580 549.2C580 547.4 580.533 545.967 581.6 544.9C582.733 543.767 584.2 543.2 586 543.2C587.8 543.2 589.233 543.767 590.3 544.9C591.433 545.967 592 547.4 592 549.2C592 551.067 591.433 552.567 590.3 553.7C589.233 554.833 587.8 555.4 586 555.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M565 690V681.8H584.8V624.5L565 639.3V629.3L581.5 617H593.8V681.8H610V690H565ZM586 781C581.533 781 577.633 780.133 574.3 778.4C571.033 776.6 568.5 774.1 566.7 770.9C564.9 767.633 564 763.833 564 759.5V727.5C564 723.1 564.9 719.3 566.7 716.1C568.5 712.9 571.033 710.433 574.3 708.7C577.633 706.9 581.533 706 586 706C590.533 706 594.433 706.9 597.7 708.7C600.967 710.433 603.5 712.9 605.3 716.1C607.1 719.3 608 723.1 608 727.5V759.5C608 763.833 607.1 767.633 605.3 770.9C603.5 774.1 600.933 776.6 597.6 778.4C594.333 780.133 590.467 781 586 781ZM586 773.3C590 773.3 593.2 772.033 595.6 769.5C598.067 766.9 599.3 763.567 599.3 759.5V727.5C599.3 723.433 598.067 720.133 595.6 717.6C593.2 715 590 713.7 586 713.7C582 713.7 578.767 715 576.3 717.6C573.9 720.133 572.7 723.433 572.7 727.5V759.5C572.7 763.567 573.9 766.9 576.3 769.5C578.767 772.033 582 773.3 586 773.3ZM586 749.4C584.2 749.4 582.733 748.833 581.6 747.7C580.533 746.567 580 745.067 580 743.2C580 741.4 580.533 739.967 581.6 738.9C582.733 737.767 584.2 737.2 586 737.2C587.8 737.2 589.233 737.767 590.3 738.9C591.433 739.967 592 741.4 592 743.2C592 745.067 591.433 746.567 590.3 747.7C589.233 748.833 587.8 749.4 586 749.4Z" fill="#8200DB" fill-opacity="0.8"/>
<path d="M565 884V875.8H584.8V818.5L565 833.3V823.3L581.5 811H593.8V875.8H610V884H565ZM586 975C581.533 975 577.633 974.133 574.3 972.4C571.033 970.6 568.5 968.1 566.7 964.9C564.9 961.633 564 957.833 564 953.5V921.5C564 917.1 564.9 913.3 566.7 910.1C568.5 906.9 571.033 904.433 574.3 902.7C577.633 900.9 581.533 900 586 900C590.533 900 594.433 900.9 597.7 902.7C600.967 904.433 603.5 906.9 605.3 910.1C607.1 913.3 608 917.1 608 921.5V953.5C608 957.833 607.1 961.633 605.3 964.9C603.5 968.1 600.933 970.6 597.6 972.4C594.333 974.133 590.467 975 586 975ZM586 967.3C590 967.3 593.2 966.033 595.6 963.5C598.067 960.9 599.3 957.567 599.3 953.5V921.5C599.3 917.433 598.067 914.133 595.6 911.6C593.2 909 590 907.7 586 907.7C582 907.7 578.767 909 576.3 911.6C573.9 914.133 572.7 917.433 572.7 921.5V953.5C572.7 957.567 573.9 960.9 576.3 963.5C578.767 966.033 582 967.3 586 967.3ZM586 943.4C584.2 943.4 582.733 942.833 581.6 941.7C580.533 940.567 580 939.067 580 937.2C580 935.4 580.533 933.967 581.6 932.9C582.733 931.767 584.2 931.2 586 931.2C587.8 931.2 589.233 931.767 590.3 932.9C591.433 933.967 592 935.4 592 937.2C592 939.067 591.433 940.567 590.3 941.7C589.233 942.833 587.8 943.4 586 943.4Z" fill="#8200DB" fill-opacity="0.8"/>
<g filter="url(#filter0_f_57_121)">
<path d="M315.313 559.434V583.592H283V559.434H315.313Z" stroke="url(#paint0_linear_57_121)" stroke-width="20"/>
</g>
<path d="M312.288 560.377H286.131V582.456H312.288V560.377Z" fill="url(#paint1_linear_57_121)"/>
<g filter="url(#filter1_f_57_121)">
<path d="M740.099 614.877V622.599H722.164V614.877H740.099Z" stroke="url(#paint2_linear_57_121)" stroke-width="20"/>
</g>
<path d="M740.653 611.747H721.686V625.607H740.653V611.747Z" fill="url(#paint3_linear_57_121)"/>
<g filter="url(#filter2_f_57_121)">
<path d="M515.41 651.352V675.51H458.812V651.352H515.41Z" stroke="url(#paint4_linear_57_121)" stroke-width="20"/>
</g>
<path d="M506.338 652.295H468.039V674.374H506.338V652.295Z" fill="url(#paint5_linear_57_121)"/>
<g filter="url(#filter3_f_57_121)">
<path d="M610.976 570.377V594.534H554.377V570.377H610.976Z" stroke="url(#paint6_linear_57_121)" stroke-width="20"/>
</g>
<path d="M601.903 571.32H563.604V593.398H601.903V571.32Z" fill="url(#paint7_linear_57_121)"/>
<g filter="url(#filter4_f_57_121)">
<path d="M621.517 734L621.517 594H693L693 734H621.517Z" stroke="url(#paint8_linear_57_121)" stroke-width="40"/>
<path d="M432.483 594L432.483 734H361V594H432.483Z" stroke="url(#paint9_linear_57_121)" stroke-width="40"/>
</g>
<g filter="url(#filter5_f_57_121)">
<path d="M432.483 555H361V296.96H432.483V380.542H447.574L432.483 397.258C432.483 362.764 442.809 336.23 463.459 317.657C484.11 298.552 512.703 289 549.239 289C593.188 289 628.136 302.532 654.081 329.597C680.027 356.661 693 393.543 693 440.243L693 555H621.517L621.517 448.203C621.517 416.893 613.045 393.013 596.1 376.562C579.686 359.58 557.182 351.089 528.588 351.089C498.936 351.089 475.373 360.111 457.9 378.154C440.955 396.197 432.483 422.2 432.483 456.163V555Z" stroke="url(#paint10_linear_57_121)" stroke-width="40"/>
</g>
<path d="M0 0H1024V1024H0V0Z" fill="url(#paint11_linear_57_121)" fill-opacity="0.2"/>
<path d="M0 0H1024V1024H0V0Z" fill="black" fill-opacity="0.2"/>
<g filter="url(#filter7_f_57_121)">
<path d="M621.517 734L621.517 594H693L693 734H621.517Z" stroke="url(#paint12_linear_57_121)" stroke-width="40"/>
<path d="M432.483 594L432.483 734H361V594H432.483Z" stroke="url(#paint13_linear_57_121)" stroke-width="40"/>
</g>
<path d="M437.483 589V739H356V589H437.483ZM698 589V739H616.517V589H698Z" fill="#05050A" fill-opacity="0.4"/>
<path d="M437.483 589V739H356V589H437.483ZM698 589V739H616.517V589H698Z" fill="url(#paint14_linear_57_121)" fill-opacity="0.2"/>
<path d="M437.483 589V739H356V589H437.483ZM698 589V739H616.517V589H698Z" stroke="url(#paint15_linear_57_121)" stroke-width="10"/>
<g filter="url(#filter9_f_57_121)">
<path d="M432.483 555H361V296.96H432.483V380.542H447.574L432.483 397.258C432.483 362.764 442.809 336.23 463.459 317.657C484.11 298.552 512.703 289 549.239 289C593.188 289 628.136 302.532 654.081 329.597C680.027 356.661 693 393.543 693 440.243L693 555H621.517L621.517 448.203C621.517 416.893 613.045 393.013 596.1 376.562C579.686 359.58 557.182 351.089 528.588 351.089C498.936 351.089 475.373 360.111 457.9 378.154C440.955 396.197 432.483 422.2 432.483 456.163V555Z" stroke="url(#paint16_linear_57_121)" stroke-width="40"/>
</g>
<path d="M549.239 284C594.296 284 630.639 297.919 657.69 326.137C684.735 354.347 698 392.587 698 440.243V560H616.517V448.203C616.517 417.787 608.306 395.382 592.617 380.149L592.561 380.094L592.506 380.037C577.227 364.23 556.133 356.089 528.589 356.089C500.045 356.089 477.883 364.725 461.523 381.599C445.744 398.415 437.483 423.007 437.483 456.163V560H356V291.96H437.483V345.092C442.838 333.118 450.364 322.71 460.115 313.939C481.935 293.781 511.861 284 549.239 284Z" fill="#05050A" fill-opacity="0.4"/>
<path d="M549.239 284C594.296 284 630.639 297.919 657.69 326.137C684.735 354.347 698 392.587 698 440.243V560H616.517V448.203C616.517 417.787 608.306 395.382 592.617 380.149L592.561 380.094L592.506 380.037C577.227 364.23 556.133 356.089 528.589 356.089C500.045 356.089 477.883 364.725 461.523 381.599C445.744 398.415 437.483 423.007 437.483 456.163V560H356V291.96H437.483V345.092C442.838 333.118 450.364 322.71 460.115 313.939C481.935 293.781 511.861 284 549.239 284Z" fill="url(#paint17_linear_57_121)" fill-opacity="0.2"/>
<path d="M549.239 284C594.296 284 630.639 297.919 657.69 326.137C684.735 354.347 698 392.587 698 440.243V560H616.517V448.203C616.517 417.787 608.306 395.382 592.617 380.149L592.561 380.094L592.506 380.037C577.227 364.23 556.133 356.089 528.589 356.089C500.045 356.089 477.883 364.725 461.523 381.599C445.744 398.415 437.483 423.007 437.483 456.163V560H356V291.96H437.483V345.092C442.838 333.118 450.364 322.71 460.115 313.939C481.935 293.781 511.861 284 549.239 284Z" stroke="url(#paint18_linear_57_121)" stroke-width="10"/>
</g>
<defs>
<filter id="filter0_f_57_121" x="223" y="499.434" width="152.313" height="144.157" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="25" result="effect1_foregroundBlur_57_121"/>
</filter>
<filter id="filter1_f_57_121" x="662.164" y="554.877" width="137.935" height="127.721" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="25" result="effect1_foregroundBlur_57_121"/>
</filter>
<filter id="filter2_f_57_121" x="398.812" y="591.352" width="176.598" height="144.157" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="25" result="effect1_foregroundBlur_57_121"/>
</filter>
<filter id="filter3_f_57_121" x="494.377" y="510.377" width="176.598" height="144.157" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="25" result="effect1_foregroundBlur_57_121"/>
</filter>
<filter id="filter4_f_57_121" x="291" y="524" width="472" height="280" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="25" result="effect1_foregroundBlur_57_121"/>
</filter>
<filter id="filter5_f_57_121" x="291" y="219" width="472" height="406" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="25" result="effect1_foregroundBlur_57_121"/>
</filter>
<filter id="filter7_f_57_121" x="291" y="524" width="472" height="280" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="25" result="effect1_foregroundBlur_57_121"/>
</filter>
<filter id="filter9_f_57_121" x="291" y="219" width="472" height="406" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="25" result="effect1_foregroundBlur_57_121"/>
</filter>
<linearGradient id="paint0_linear_57_121" x1="273" y1="549.434" x2="316.53" y2="601.005" gradientUnits="userSpaceOnUse">
<stop stop-color="#8200DB"/>
<stop offset="1" stop-color="#FF00FF"/>
</linearGradient>
<linearGradient id="paint1_linear_57_121" x1="286.131" y1="560.377" x2="307.897" y2="586.163" gradientUnits="userSpaceOnUse">
<stop stop-color="#8200DB"/>
<stop offset="1" stop-color="#FF00FF"/>
</linearGradient>
<linearGradient id="paint2_linear_57_121" x1="712.164" y1="604.877" x2="738.575" y2="641.019" gradientUnits="userSpaceOnUse">
<stop stop-color="#8200DB"/>
<stop offset="1" stop-color="#FF00FF"/>
</linearGradient>
<linearGradient id="paint3_linear_57_121" x1="721.686" y1="611.747" x2="734.891" y2="629.818" gradientUnits="userSpaceOnUse">
<stop stop-color="#8200DB"/>
<stop offset="1" stop-color="#FF00FF"/>
</linearGradient>
<linearGradient id="paint4_linear_57_121" x1="448.812" y1="641.352" x2="487.024" y2="707.638" gradientUnits="userSpaceOnUse">
<stop stop-color="#8200DB"/>
<stop offset="1" stop-color="#FF00FF"/>
</linearGradient>
<linearGradient id="paint5_linear_57_121" x1="468.039" y1="652.295" x2="487.145" y2="685.438" gradientUnits="userSpaceOnUse">
<stop stop-color="#8200DB"/>
<stop offset="1" stop-color="#FF00FF"/>
</linearGradient>
<linearGradient id="paint6_linear_57_121" x1="544.377" y1="560.377" x2="582.589" y2="626.663" gradientUnits="userSpaceOnUse">
<stop stop-color="#8200DB"/>
<stop offset="1" stop-color="#FF00FF"/>
</linearGradient>
<linearGradient id="paint7_linear_57_121" x1="563.604" y1="571.32" x2="582.71" y2="604.463" gradientUnits="userSpaceOnUse">
<stop stop-color="#8200DB"/>
<stop offset="1" stop-color="#FF00FF"/>
</linearGradient>
<linearGradient id="paint8_linear_57_121" x1="1000.56" y1="831.344" x2="190.828" y2="325.684" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF00FF"/>
<stop offset="1" stop-color="#8200DB"/>
</linearGradient>
<linearGradient id="paint9_linear_57_121" x1="1000.56" y1="831.344" x2="190.828" y2="325.684" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF00FF"/>
<stop offset="1" stop-color="#8200DB"/>
</linearGradient>
<linearGradient id="paint10_linear_57_121" x1="1000.56" y1="832.327" x2="189.832" y2="327.155" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF00FF"/>
<stop offset="1" stop-color="#8200DB"/>
</linearGradient>
<linearGradient id="paint11_linear_57_121" x1="-163" y1="-65.5" x2="1222" y2="1130.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#303030"/>
<stop offset="1" stop-color="#1F1F1F"/>
</linearGradient>
<linearGradient id="paint12_linear_57_121" x1="1000.56" y1="831.344" x2="190.828" y2="325.684" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF00FF"/>
<stop offset="1" stop-color="#8200DB"/>
</linearGradient>
<linearGradient id="paint13_linear_57_121" x1="1000.56" y1="831.344" x2="190.828" y2="325.684" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF00FF"/>
<stop offset="1" stop-color="#8200DB"/>
</linearGradient>
<linearGradient id="paint14_linear_57_121" x1="802.452" y1="186.747" x2="-8.50068" y2="1179.57" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint15_linear_57_121" x1="1000.56" y1="831.344" x2="190.828" y2="325.684" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF00FF"/>
<stop offset="1" stop-color="#8200DB"/>
</linearGradient>
<linearGradient id="paint16_linear_57_121" x1="1000.56" y1="832.327" x2="189.832" y2="327.155" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF00FF"/>
<stop offset="1" stop-color="#8200DB"/>
</linearGradient>
<linearGradient id="paint17_linear_57_121" x1="802.452" y1="186.314" x2="-10.6374" y2="1179.57" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint18_linear_57_121" x1="1000.56" y1="832.327" x2="189.832" y2="327.155" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF00FF"/>
<stop offset="1" stop-color="#8200DB"/>
</linearGradient>
<clipPath id="clip0_57_121">
<rect width="1024" height="1024" fill="white"/>
</clipPath>
</defs>
</svg>
</file>
<file path=".dockerignore">
node_modules
.next
.git
.env.local
.vercel
README.md
</file>
<file path=".gitignore">
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
</file>
<file path="AGENTS.md">
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
</file>
<file path="CLAUDE.md">
@AGENTS.md
</file>
<file path="components.json">
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-lyra",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "phosphor",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
</file>
<file path="Dockerfile">
# Stage 1: Установка зависимостей
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
# Stage 2: Сборка проекта
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm install -g pnpm && pnpm run build
# Stage 3: Продакшен сервер (Только самое необходимое)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Копируем публичные файлы и собранный проект
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["npm", "start"]
</file>
<file path="ecosystem.config.js">
module.exports = {
apps: [
{
name: "netrunner-landing",
script: "node_modules/next/dist/bin/next",
args: "start -p 3000",
exec_mode: "fork",
env: {
NODE_ENV: "production",
},
},
],
};
</file>
<file path="eslint.config.mjs">
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
</file>
<file path="postcss.config.mjs">
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
</file>
<file path="proxy.ts">
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { match as matchLocale } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
const locales = ["en", "ru"];
const defaultLocale = "en";
function getLocale(request: NextRequest): string {
const negotiatorHeaders: Record<string, string> = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
const languages = new Negotiator({ headers: negotiatorHeaders }).languages();
try {
return matchLocale(languages, locales, defaultLocale);
} catch (e) {
return defaultLocale;
}
}
// Изменили название с middleware на proxy и добавили export
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
);
if (pathnameHasLocale) return;
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
// Конфиг остается прежним
export const config = {
matcher: ["/((?!_next|api|favicon.ico|logo.svg|.*\\..*).*)"],
};
</file>
<file path="README.md">
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
</file>
<file path="tsconfig.json">
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}
</file>
<file path="app/[lang]/blog/[slug]/page.tsx">
import { pb, getPbImage } from "@/lib/pb";
import { notFound } from "next/navigation";
import Link from "next/link";
import { ArrowLeft, Calendar, Clock, ShieldCheck } from "lucide-react";
interface PBPost {
id: string;
collectionId: string;
title: string;
slug: string;
content: string;
image: string;
lang: string;
created: string;
published: boolean;
}
interface Props {
params: Promise<{ lang: string; slug: string }>;
}
export default async function BlogPostPage({ params }: Props) {
const { lang, slug } = await params;
const isRu = lang === "ru";
let post: PBPost;
try {
// Ищем в соответствующей коллекции по слагу
post = await pb
.collection(`posts_${lang}`)
.getFirstListItem<PBPost>(`slug = "${slug}" && published = true`, {
cache: "no-store",
});
} catch (error) {
notFound();
}
return (
<main className="min-h-screen pt-32 pb-20 bg-background">
<div className="container mx-auto max-w-3xl px-4">
{/* Хлебные крошки / Назад */}
<Link
href={`/${lang}/blog`}
className="inline-flex items-center gap-2 text-sm font-mono text-muted-foreground hover:text-primary mb-12 transition-colors group"
>
<ArrowLeft
size={16}
className="group-hover:-translate-x-1 transition-transform"
/>
<span className="opacity-50">root@netrunner:~#</span>
<span>cd ../logs</span>
</Link>
<article className="relative">
{/* Обложка с неоновым свечением */}
{post.image && (
<div className="w-full h-[300px] md:h-[450px] rounded-3xl overflow-hidden mb-12 border border-border/50 shadow-2xl shadow-primary/10 relative">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getPbImage(post, post.image)}
alt={post.title}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-transparent opacity-60" />
</div>
)}
{/* Шапка статьи */}
<div className="flex flex-wrap items-center gap-6 text-[10px] font-mono text-primary mb-8 uppercase tracking-[0.2em]">
<span className="flex items-center gap-2 px-3 py-1 bg-primary/10 rounded-full border border-primary/20">
<Calendar size={12} />
{new Date(post.created).toLocaleDateString(
isRu ? "ru-RU" : "en-US",
)}
</span>
<span className="flex items-center gap-2 text-muted-foreground">
<Clock size={12} />
{isRu ? "Чтение: 4 мин" : "Read: 4 min"}
</span>
<span className="flex items-center gap-2 text-green-500/70">
<ShieldCheck size={12} />
{isRu ? "Проверено: Netrunner" : "Verified: Netrunner"}
</span>
</div>
<h1 className="text-4xl md:text-6xl font-extrabold tracking-tighter mb-10 leading-[1.1] text-foreground">
{post.title}
</h1>
{/* Тело статьи с Tailwind Typography (prose) */}
<div
className="prose prose-invert prose-p:text-muted-foreground/90 prose-p:leading-relaxed prose-headings:text-foreground prose-headings:tracking-tighter prose-a:text-primary prose-strong:text-foreground prose-code:text-primary max-w-none text-lg selection:bg-primary/30"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
{/* Футер статьи */}
<div className="mt-20 pt-10 border-t border-border/30 flex justify-between items-center">
<div className="text-[10px] font-mono text-muted-foreground/40 uppercase tracking-widest">
End of transmission // node_th_01
</div>
<div className="flex gap-4">
<div className="w-2 h-2 bg-primary rounded-full animate-ping" />
<div className="w-2 h-2 bg-primary rounded-full opacity-50" />
<div className="w-2 h-2 bg-primary rounded-full opacity-20" />
</div>
</div>
</article>
</div>
</main>
);
}
</file>
<file path="app/[lang]/docs/docs-client.tsx">
"use client";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Search,
FileText,
Shield,
Network,
Scale,
ChevronLeft,
TerminalSquare,
Loader2,
} from "lucide-react";
import { pb } from "@/lib/pb";
// --- МОДЕЛЬ ДАННЫХ POCKETBASE ---
interface PBDocument {
id: string;
title: string;
category: "Core" | "Security" | "Network" | "Legal";
status: "Encrypted" | "Public" | "Internal" | "Dynamic";
content: string;
created: string; // Системное поле PocketBase
}
const CATEGORIES = ["All", "Core", "Security", "Network", "Legal"];
export function DocsClient({ lang }: { lang: string }) {
const [docs, setDocs] = useState<PBDocument[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [activeCategory, setActiveCategory] = useState("All");
const [selectedDoc, setSelectedDoc] = useState<PBDocument | null>(null);
useEffect(() => {
async function fetchDocs() {
try {
setLoading(true);
// Обращаемся к documents_ru или documents_en
const records = await pb
.collection(`documents_${lang}`)
.getFullList<PBDocument>({
sort: "-created",
});
setDocs(records);
} catch (error) {
console.error("Vault access error:", error);
} finally {
setLoading(false);
}
}
fetchDocs();
}, [lang]);
const filteredDocs = docs.filter((doc) => {
const matchesSearch =
doc.title.toLowerCase().includes(search.toLowerCase()) ||
doc.content.toLowerCase().includes(search.toLowerCase());
const matchesCategory =
activeCategory === "All" || doc.category === activeCategory;
return matchesSearch && matchesCategory;
});
const getCategoryIcon = (cat: string) => {
switch (cat) {
case "Core":
return <TerminalSquare className="w-4 h-4 text-primary" />;
case "Security":
return <Shield className="w-4 h-4 text-secondary" />;
case "Network":
return <Network className="w-4 h-4 text-primary" />;
case "Legal":
return <Scale className="w-4 h-4 text-muted-foreground" />;
default:
return <FileText className="w-4 h-4" />;
}
};
const formatDate = (dateStr: string) => dateStr.split(" ")[0];
return (
// Добавлен overflow-x-hidden для защиты от горизонтального скролла на мобилках
<div className="min-h-screen pt-24 md:pt-32 pb-24 px-4 sm:px-6 container mx-auto max-w-6xl relative font-mono overflow-x-hidden md:overflow-x-visible">
{/* Исправлено свечение: теперь max-w-full, чтобы не распирать экран */}
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-full max-w-[800px] h-[300px] md:h-[500px] bg-primary/10 md:bg-primary/5 blur-[100px] md:blur-[150px] rounded-full pointer-events-none -z-10" />
<AnimatePresence mode="wait">
{loading ? (
<motion.div
key="loader"
className="flex flex-col items-center justify-center py-40 text-primary"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<Loader2 className="w-8 h-8 animate-spin mb-4" />
<p className="text-xs uppercase tracking-[0.3em] animate-pulse text-center">
Establishing_secure_link...
</p>
</motion.div>
) : !selectedDoc ? (
<motion.div
key="grid"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20, filter: "blur(10px)" }}
transition={{ duration: 0.3 }}
>
{/* Заголовок и поиск: центрирование на мобильных (items-center text-center) */}
<div className="mb-10 md:mb-12 flex flex-col md:flex-row gap-6 items-center md:items-end justify-between text-center md:text-left">
<div>
<h1 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground flex items-center justify-center md:justify-start gap-3">
<div className="w-3 h-3 bg-primary animate-pulse shadow-[0_0_10px_rgba(0,240,255,0.8)]" />
DATA_VAULT
</h1>
<p className="text-muted-foreground mt-2 text-xs md:text-sm uppercase tracking-widest">
System logs & documentation // V4.2
</p>
</div>
<div className="relative w-full max-w-md md:max-w-none md:w-96 group mx-auto md:mx-0">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground group-focus-within:text-primary transition-colors" />
<input
type="text"
placeholder="root@nrxp:~# grep -i ..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full bg-card/50 backdrop-blur-md border border-border/50 rounded-xl py-3 pl-12 pr-4 text-sm text-foreground focus:outline-none focus:border-primary/50 focus:shadow-[0_0_15px_rgba(0,240,255,0.15)] transition-all placeholder:text-muted-foreground/50"
/>
</div>
</div>
{/* Фильтры: центрирование на мобильных (justify-center) */}
<div className="flex flex-wrap justify-center md:justify-start gap-2 mb-8">
{CATEGORIES.map((cat) => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`px-3 md:px-4 py-2 rounded-lg text-xs md:text-sm font-bold transition-all duration-300 border ${
activeCategory === cat
? "bg-primary/10 border-primary/50 text-primary shadow-[0_0_10px_rgba(0,240,255,0.2)]"
: "bg-card/30 border-border/50 text-muted-foreground hover:bg-card/60 hover:text-foreground"
}`}
>
{cat}
</button>
))}
</div>
{/* Сетка документов */}
<motion.div
layout
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
>
<AnimatePresence>
{filteredDocs.map((doc) => (
<motion.div
layout
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
key={doc.id}
onClick={() => setSelectedDoc(doc)}
className="group bg-card/30 backdrop-blur-sm border border-border/50 hover:border-primary/40 rounded-2xl p-5 md:p-6 cursor-pointer transition-all duration-300 hover:shadow-[0_0_30px_rgba(0,240,255,0.1)] hover:-translate-y-1 flex flex-col h-48"
>
<div className="flex justify-between items-start mb-4">
<div className="p-2 rounded-lg bg-background/50 border border-border/50 group-hover:bg-primary/10 group-hover:border-primary/30 transition-colors">
{getCategoryIcon(doc.category)}
</div>
<span
className={`text-[9px] md:text-[10px] uppercase font-bold tracking-wider px-2 py-1 rounded border ${
doc.status === "Encrypted"
? "text-primary border-primary/30 bg-primary/5"
: doc.status === "Public"
? "text-secondary border-secondary/30 bg-secondary/5"
: "text-muted-foreground border-border bg-muted/20"
}`}
>
{doc.status}
</span>
</div>
<h3 className="text-base md:text-lg font-bold text-foreground mb-1 line-clamp-2">
{doc.title}
</h3>
<div className="mt-auto flex items-center justify-between text-[10px] md:text-xs text-muted-foreground">
<span>{doc.category}</span>
<span>{formatDate(doc.created)}</span>
</div>
</motion.div>
))}
</AnimatePresence>
</motion.div>
{filteredDocs.length === 0 && (
<div className="text-center py-20 text-muted-foreground">
<TerminalSquare className="w-12 h-12 mx-auto mb-4 opacity-20" />
<p>0 records found for query {`"${search}"`}</p>
</div>
)}
</motion.div>
) : (
// --- РЕЖИМ ЧТЕНИЯ ---
<motion.div
key="reader"
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -20 }}
className="w-full max-w-4xl mx-auto"
>
{/* Кнопка "Назад": центрируем на мобилках */}
<button
onClick={() => setSelectedDoc(null)}
className="mb-6 md:mb-8 flex items-center justify-center md:justify-start gap-2 w-full md:w-auto text-muted-foreground hover:text-primary transition-colors text-sm font-bold uppercase tracking-wider group"
>
<ChevronLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
Close Session
</button>
<div className="bg-card/40 backdrop-blur-2xl border border-border/50 rounded-2xl md:rounded-3xl overflow-hidden shadow-2xl">
{/* Шапка терминала: перенос на новую строку на мобильных */}
<div className="bg-background/80 border-b border-border/50 px-4 md:px-6 py-3 md:py-4 flex flex-col sm:flex-row items-center gap-3 sm:justify-between text-center sm:text-left">
<div className="flex items-center gap-3 md:gap-4">
<div className="hidden sm:flex gap-2">
<div className="w-3 h-3 rounded-full bg-red-500/50" />
<div className="w-3 h-3 rounded-full bg-yellow-500/50" />
<div className="w-3 h-3 rounded-full bg-green-500/50" />
</div>
<span className="text-muted-foreground text-[10px] md:text-xs uppercase tracking-widest sm:border-l border-border/50 sm:pl-4">
{selectedDoc.id}.txt
</span>
</div>
<div className="flex items-center gap-2 text-[10px] md:text-xs font-bold text-primary">
{getCategoryIcon(selectedDoc.category)}
{selectedDoc.category}
</div>
</div>
{/* Тело документа: уменьшены отступы для телефона (p-5 вместо p-8) */}
<div className="p-5 sm:p-8 md:p-12 relative">
<h2 className="text-xl md:text-3xl font-bold text-foreground mb-6 md:mb-8 pb-4 md:pb-6 border-b border-border/30">
{selectedDoc.title}
</h2>
<div className="text-muted-foreground leading-relaxed whitespace-pre-wrap text-sm md:text-base">
{selectedDoc.content || "NO_CONTENT_FOUND"}
</div>
<div className="mt-8 flex items-center gap-2 text-primary text-xs md:text-sm">
<span>EOF</span>
<div className="w-2 h-4 bg-primary animate-pulse" />
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
</file>
<file path="app/[lang]/download/download-client.tsx">
"use client";
import { useState, useEffect } from "react";
import { motion, Variants } from "framer-motion";
import { Button } from "@/components/ui/button";
import {
Monitor,
Smartphone,
Terminal,
Apple,
Download,
ExternalLink,
Bell,
Loader2,
Clock,
} from "lucide-react";
import { Dictionary } from "@/lib/IDict";
import { pb, type PBDownload, getPbFileUrl } from "@/lib/pb";
interface DownloadClientProps {
dict: Dictionary["download"];
lang: string;
}
export function DownloadClient({ dict, lang }: DownloadClientProps) {
const [platforms, setPlatforms] = useState<PBDownload[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchDownloads() {
try {
setLoading(true);
const records = await pb
.collection(`downloads_${lang}`)
.getFullList<PBDownload>({
sort: "order",
requestKey: null,
});
setPlatforms(records);
} catch (error) {
console.error("Ошибка загрузки данных платформы:", error);
} finally {
setLoading(false);
}
}
fetchDownloads();
}, [lang]);
// Единый конфиг стилей: Primary (Фиолетовый) для базы, Secondary (Циан) для акцентов
const getPlatformIcon = (platformId: string) => {
const props = {
className:
"size-8 text-secondary drop-shadow-[0_0_10px_rgba(0,229,242,0.8)]",
};
switch (platformId) {
case "windows":
return <Monitor {...props} />;
case "linux":
return <Terminal {...props} />;
case "android":
return <Smartphone {...props} />;
case "macos":
case "ios":
return <Apple {...props} />;
default:
return <Download {...props} />;
}
};
const containerVariants: Variants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.1 },
},
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 40 },
show: {
opacity: 1,
y: 0,
transition: { type: "spring", stiffness: 300, damping: 24 },
},
};
return (
<div className="min-h-screen pt-32 pb-24 px-4 container mx-auto max-w-5xl relative">
{/* Мягкое фоновое свечение */}
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-full max-w-[800px] h-[400px] bg-primary/5 blur-[120px] rounded-full pointer-events-none -z-10" />
<div className="text-center mb-20">
<h1 className="text-4xl md:text-6xl font-black tracking-tighter mb-6 text-foreground">
{dict.title}
</h1>
<p className="text-muted-foreground text-lg max-w-2xl mx-auto font-medium">
{dict.subtitle}
</p>
</div>
{loading ? (
<div className="flex flex-col items-center justify-center py-20 text-primary opacity-50">
<Loader2 className="w-10 h-10 animate-spin mb-4" />
<span className="font-mono text-xs uppercase tracking-[0.3em]">
Accessing_Build_Server...
</span>
</div>
) : (
<motion.div
variants={containerVariants}
initial="hidden"
animate="show"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8"
>
{platforms.map((platform) => (
<motion.div
key={platform.id}
variants={itemVariants}
className={`bg-card/40 backdrop-blur-xl border border-border/50 rounded-[2.5rem] p-8 md:p-10 flex flex-col transition-all duration-500 relative overflow-hidden group hover:border-primary/40 hover:shadow-[0_0_40px_rgba(139,61,255,0.15)] ${
platform.status === "dev" ? "opacity-60 grayscale-[50%]" : ""
}`}
>
{/* Статус бейдж */}
<div className="absolute top-8 right-8">
{platform.status === "active" ? (
<div className="px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary text-[10px] font-bold uppercase tracking-widest shadow-sm">
{dict.badges.active}
</div>
) : (
<div className="px-3 py-1 rounded-full bg-muted border border-border text-muted-foreground text-[10px] font-bold uppercase tracking-widest">
{dict.badges.dev}
</div>
)}
</div>
{/* Иконка в Циановом (Secondary) стиле */}
<div className="mb-10 p-5 rounded-[1.5rem] bg-secondary/10 inline-flex border border-secondary/20 shadow-[0_0_20px_rgba(0,229,242,0.15)] w-fit transition-transform duration-500 group-hover:scale-110">
{getPlatformIcon(platform.platformId)}
</div>
<div className="mb-10">
<h3 className="text-2xl font-bold text-foreground mb-2 tracking-tight">
{platform.name}
</h3>
<p className="text-sm text-muted-foreground font-mono">
{platform.version}
</p>
</div>
{/* Кнопки действий: Основная всегда Фиолетовая (Primary) */}
<div className="mt-auto flex flex-col gap-3">
{platform.status === "active" ? (
<>
{/* Прямое скачивание */}
{platform.file ? (
<Button
asChild
className="w-full h-14 rounded-2xl font-bold gap-3 bg-primary text-primary-foreground hover:bg-primary/90 shadow-[0_0_20px_rgba(139,61,255,0.3)] transition-all active:scale-[0.98]"
>
<a
href={getPbFileUrl(platform, platform.file)}
download
>
<Download className="size-5" />
{dict.buttons.direct}
</a>
</Button>
) : (
<Button
disabled
variant="outline"
className="w-full h-14 rounded-2xl font-bold gap-3 opacity-50 bg-background/50 border-dashed"
>
<Download className="size-5" />
Build Pending
</Button>
)}
{/* Ссылка на магазин (Вместо GitHub) */}
{platform.storeLink ? (
<Button
asChild
variant="outline"
className="w-full h-14 rounded-2xl gap-3 border-border/50 bg-background/30 text-muted-foreground hover:text-foreground hover:border-primary/30 transition-all"
>
<a
href={platform.storeLink}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="size-4" />
{dict.buttons.store}
</a>
</Button>
) : (
<Button
variant="outline"
disabled
className="w-full h-14 rounded-2xl gap-3 border-border/20 bg-background/20 text-muted-foreground/40 cursor-not-allowed"
>
<Clock className="size-4" />
{dict.buttons.soon}
</Button>
)}
</>
) : (
// В разработке
<Button
variant="secondary"
disabled
className="w-full h-14 rounded-2xl gap-3 opacity-40 cursor-not-allowed bg-muted"
>
<Bell className="size-4" />
{dict.buttons.notify}
</Button>
)}
</div>
</motion.div>
))}
</motion.div>
)}
</div>
);
}
</file>
<file path="app/[lang]/download/page.tsx">
import { getDictionary } from "@/lib/dictionaries";
import { DownloadClient } from "./download-client";
export default async function DownloadPage({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
const dict = await getDictionary(lang);
// Добавили передачу lang
return <DownloadClient dict={dict.download} lang={lang} />;
}
</file>
<file path="components/sections/Footer.tsx">
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { JetBrains_Mono } from "next/font/google";
import { Dictionary } from "@/lib/IDict";
import { cn } from "@/lib/utils";
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin", "cyrillic"],
});
// Кастомные легкие SVG иконки
const Icons = {
Telegram: () => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-6"
>
<path d="m22 2-7 20-4-9-9-4Z" />
<path d="M22 2 11 13" />
</svg>
),
Youtube: () => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-6"
>
<path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.42a2.78 2.78 0 0 0-1.94 2C1 8.11 1 12 1 12s0 3.89.46 5.58a2.78 2.78 0 0 0 1.94 2c1.72.42 8.6.42 8.6.42s6.88 0 8.6-.42a2.78 2.78 0 0 0 1.94-2C23 15.89 23 12 23 12s0-3.89-.46-5.58z" />
<polygon
points="9.75 15.02 15.5 12 9.75 8.98 9.75 15.02"
fill="currentColor"
/>
</svg>
),
Discord: () => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-6"
>
<path d="M19.73 5.42a19.06 19.06 0 0 0-4.73-1.45 14.1 14.1 0 0 0-.6 1.2A17.65 17.65 0 0 0 9.6 5.17a14.1 14.1 0 0 0-.6-1.2 19.06 19.06 0 0 0-4.73 1.45 19.64 19.64 0 0 0-3.37 13.5 19.34 19.34 0 0 0 5.86 3 13.9 13.9 0 0 0 1.26-2.03 13.2 13.2 0 0 1-2-.95c.16-.12.3-.25.45-.38a13.3 13.3 0 0 0 11.08 0c.15.13.3.26.45.38a13.2 13.2 0 0 1-2 .95 13.9 13.9 0 0 0 1.26 2.03 19.34 19.34 0 0 0 5.86-3 19.64 19.64 0 0 0-3.37-13.5ZM8.5 15.2c-1.07 0-1.95-1-1.95-2.22s.87-2.22 1.95-2.22c1.08 0 1.96 1 1.95 2.22 0 1.21-.88 2.22-1.95 2.22Zm7 0c-1.07 0-1.95-1-1.95-2.22s.87-2.22 1.95-2.22c1.08 0 1.96 1 1.95 2.22 0 1.21-.88 2.22-1.95 2.22Z" />
</svg>
),
X: () => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-6"
>
<path d="M4 4l11.733 16H20L8.267 4z" />
<path d="M4 20l6.768-6.768m2.464-2.464L20 4" />
</svg>
),
Instagram: () => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-6"
>
<rect width="20" height="20" x="2" y="2" rx="5" ry="5" />
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" />
<line x1="17.5" x2="17.51" y1="6.5" y2="6.5" />
</svg>
),
Habr: () => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
className="size-6"
>
<path d="M5 5v14M19 5v14M5 12h14" />
</svg>
),
};
const FlickeringSocialIcon = ({ social }: { social: any }) => {
const [opacity, setOpacity] = useState(1);
useEffect(() => {
let timeoutId: NodeJS.Timeout;
const flicker = () => {
if (Math.random() > 0.6) {
setOpacity(Math.random() * 0.5 + 0.1);
setTimeout(() => setOpacity(1), Math.random() * 120 + 30);
}
timeoutId = setTimeout(flicker, Math.random() * 6000 + 1000);
};
timeoutId = setTimeout(flicker, Math.random() * 3000);
return () => clearTimeout(timeoutId);
}, []);
return (
<Link
href={social.href}
title={social.name}
// В светлой теме добавляем жесткую рамку при ховере вместо свечения
className={cn(
"p-2 rounded-lg transition-all duration-300 relative",
"dark:bg-transparent dark:border-none", // Темная тема: без фона
"bg-muted/50 border border-transparent hover:border-border hover:shadow-inner", // Светлая тема: кнопки-клавиши
social.color,
social.glow,
"hover:!opacity-100 hover:scale-110 active:scale-95",
)}
style={{ opacity, transition: "opacity 0.05s ease-in-out" }}
>
{/* Фоновый отсвет только для темной темы */}
<div
className={`absolute inset-0 blur-[6px] opacity-20 hidden dark:block ${social.bgGlow}`}
/>
<social.icon />
</Link>
);
};
export function Footer({
dict,
lang,
}: {
dict: Dictionary["footer"];
lang: string;
}) {
const sitemap = [
{
title: dict.sections.product,
links: [
{ name: dict.links.features, href: `/${lang}#features` },
{ name: dict.links.pricing, href: `/${lang}#pricing` },
{ name: dict.links.protocol, href: `/${lang}/docs` },
],
},
{
title: dict.sections.resources,
links: [
{ name: dict.links.faq, href: `/${lang}#faq` },
{ name: dict.links.comms, href: `/${lang}/blog` },
],
},
{
title: dict.sections.legal,
links: [
{ name: dict.links.privacy, href: `/${lang}/docs` },
{ name: dict.links.terms, href: `/${lang}/docs` },
],
},
];
const socialLinks = [
{
name: "Telegram",
href: "#",
icon: Icons.Telegram,
color: "text-[#0088cc]", // В светлой теме цвет остается плотным
glow: "dark:drop-shadow-[0_0_8px_rgba(0,136,204,0.8)]", // Ограничиваем свечение только темной темой
bgGlow: "bg-[#0088cc]",
},
// ... аналогично для YouTube, Discord, X, Instagram, Habr (добавь префикс dark: к каждому glow)
{
name: "YouTube",
href: "#",
icon: Icons.Youtube,
color: "text-[#ff0000]",
glow: "dark:drop-shadow-[0_0_8px_rgba(255,0,0,0.8)]",
bgGlow: "bg-[#ff0000]",
},
{
name: "Discord",
href: "#",
icon: Icons.Discord,
color: "text-[#5865F2]",
glow: "dark:drop-shadow-[0_0_8px_rgba(88,101,242,0.8)]",
bgGlow: "bg-[#5865F2]",
},
{
name: "X",
href: "#",
icon: Icons.X,
color: "text-foreground",
glow: "dark:drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]",
bgGlow: "bg-white",
},
{
name: "Instagram",
href: "#",
icon: Icons.Instagram,
color: "text-[#E1306C]",
glow: "dark:drop-shadow-[0_0_8px_rgba(225,48,108,0.8)]",
bgGlow: "bg-[#E1306C]",
},
{
name: "Habr",
href: "#",
icon: Icons.Habr,
color: "text-[#65A3BE]",
glow: "dark:drop-shadow-[0_0_8px_rgba(101,163,190,0.8)]",
bgGlow: "bg-[#65A3BE]",
},
];
return (
<footer
className={`mt-20 border-t border-border/20 bg-background pt-16 pb-12 ${jetbrainsMono.className}`}
>
<div className="container mx-auto px-6 max-w-6xl">
<div className="flex flex-col lg:flex-row justify-between items-start gap-16 lg:gap-8">
<div className="flex flex-col gap-3 max-w-xs">
<Link
href={`/${lang}`}
className="flex items-center gap-3 group hover:scale-105 transition-transform duration-300 w-fit mt-1"
>
{/* Убрали -rotate-2, текст теперь стоит ровно */}
<div className="text-2xl sm:text-3xl tracking-widest flex items-center select-none font-bold">
<span
className="text-transparent animate-neon-flicker transition-all duration-300"
style={{
WebkitTextStroke: "1px #00E5F2",
filter: "drop-shadow(0 0 5px rgba(0,229,242,0.6))",
}}
>
Net
</span>
<span
className="text-transparent transition-all duration-300"
style={{
WebkitTextStroke: "1px #00E5F2",
filter: "drop-shadow(0 0 5px rgba(0,229,242,0.6))",
}}
>
runner
</span>
<span
className="ml-2 text-transparent opacity-90 group-hover:opacity-100 transition-all duration-300"
style={{
WebkitTextStroke: "1px #8B3DFF",
filter: "drop-shadow(0 0 5px rgba(139,61,255,0.6))",
}}
>
VPN
</span>
</div>
</Link>
<div className="text-xs font-bold text-primary/80 tracking-widest uppercase mt-1 drop-shadow-[0_0_5px_rgba(139,61,255,0.5)]">
PROTOCOL // ENCRYPTED ACCESS
</div>
<p className="text-muted-foreground text-sm leading-relaxed mt-2 max-w-50">
{dict.tagline}
</p>
<div className="flex flex-wrap gap-5 mt-4 items-center">
{socialLinks.map((social) => (
<FlickeringSocialIcon key={social.name} social={social} />
))}
</div>
</div>
<div className="flex flex-wrap md:flex-nowrap gap-12 lg:gap-16">
{sitemap.map((section, idx) => (
<div key={idx} className="flex flex-col gap-5">
<h4 className="text-foreground font-black text-[13px] uppercase tracking-widest">
{section.title}
</h4>
<ul className="flex flex-col gap-3">
{section.links.map((link, linkIdx) => (
<li key={linkIdx}>
<Link
href={link.href}
className="text-muted-foreground hover:text-primary text-sm transition-colors duration-200"
>
{link.name}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
<div className="mt-20 border border-border/30 bg-card/20 rounded-xl p-6 md:px-10 md:py-8 flex flex-col md:flex-row justify-between items-start md:items-center gap-8 shadow-2xl relative overflow-hidden">
<div className="absolute top-0 left-1/4 w-1/2 h-full bg-primary/10 blur-3xl -z-10 pointer-events-none" />
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-primary shadow-[0_0_8px_rgba(139,61,255,0.8)] animate-pulse" />
<span className="text-[10px] text-muted-foreground/60 uppercase tracking-widest font-bold">
Uplink Status
</span>
</div>
<div className="text-2xl font-black text-foreground tracking-tight mt-1">
464.6 MBPS
</div>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-primary shadow-[0_0_8px_rgba(139,61,255,0.8)]" />
<span className="text-[10px] text-muted-foreground/60 uppercase tracking-widest font-bold">
Active Node
</span>
</div>
<div className="text-2xl font-black text-foreground tracking-tight mt-1">
TYO-SEC-05
</div>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.8)]" />
<span className="text-[10px] text-muted-foreground/60 uppercase tracking-widest font-bold">
Latency (RTT)
</span>
</div>
<div className="text-2xl font-black text-foreground tracking-tight mt-1">
16 MS
</div>
</div>
</div>
</div>
</footer>
);
}
</file>
<file path="components/sections/TerminalGuestbook.tsx">
"use client";
import { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Terminal,
Send,
ShieldAlert,
CheckCircle2,
User,
MessageSquare,
Zap,
} from "lucide-react";
import { pb } from "@/lib/pb";
import { Button } from "@/components/ui/button";
import { Dictionary } from "@/lib/IDict";
import { CyberButton } from "../ui/cyber-button";
import { cn } from "@/lib/utils";
export function TerminalGuestbook({ dict }: { dict: Dictionary["guestbook"] }) {
const [comments, setComments] = useState<any[]>([]);
const [nickname, setNickname] = useState("");
const [message, setMessage] = useState("");
const [honeypot, setHoneypot] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [cooldown, setCooldown] = useState(0);
const [status, setStatus] = useState<{
type: "success" | "error" | null;
text: string;
}>({ type: null, text: "" });
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
pb.autoCancellation(false);
// Начальная загрузка
const fetchComments = async () => {
try {
const records = await pb
.collection("comments")
.getList(1, 50, { sort: "-created", requestKey: null });
setComments(records.items);
} catch (e) {
console.error(e);
}
};
fetchComments();
// Кулдаун
const lastPost = localStorage.getItem("last_comment_time");
if (lastPost) {
const timePassed = Math.floor((Date.now() - parseInt(lastPost)) / 1000);
if (timePassed < 60) setCooldown(60 - timePassed);
}
// Реалтайм
pb.collection("comments").subscribe("*", (e) => {
if (e.action === "create") {
setComments((prev) => [e.record, ...prev].slice(0, 50));
}
});
return () => {
pb.collection("comments").unsubscribe("*");
};
}, []);
useEffect(() => {
if (cooldown > 0) {
const timer = setTimeout(() => setCooldown((c) => c - 1), 1000);
return () => clearTimeout(timer);
}
}, [cooldown]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (honeypot)
return setStatus({ type: "error", text: dict.statusErrorBreach });
if (!nickname.trim() || !message.trim())
return setStatus({ type: "error", text: dict.statusErrorEmpty });
if (nickname.length > 20 || message.length > 150)
return setStatus({ type: "error", text: dict.statusErrorSize });
if (cooldown > 0)
return setStatus({
type: "error",
text: dict.statusErrorFirewall.replace("{{time}}", cooldown.toString()),
});
try {
setIsSubmitting(true);
// Стучимся в наш Rust API Gateway
const res = await fetch("http://localhost:3001/api/comments", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
nickname: nickname.trim(),
message: message.trim(),
}),
});
if (!res.ok) {
// Если Rust вернул 406 Not Acceptable (грязь в тексте)
if (res.status === 406) {
throw new Error(dict.statusErrorBreach); // или кастомная ошибка "Мат запрещен"
}
throw new Error("Server error");
}
setNickname("");
setMessage("");
setStatus({ type: "success", text: dict.statusSuccess });
setCooldown(60);
localStorage.setItem("last_comment_time", Date.now().toString());
} catch (err) {
setStatus({ type: "error", text: dict.statusErrorOffline });
} finally {
setIsSubmitting(false);
setTimeout(() => setStatus({ type: null, text: "" }), 5000);
}
};
return (
<section
id="guestbook"
className="py-32 px-4 container mx-auto max-w-6xl relative overflow-hidden"
>
{/* Мягкие свечения — увеличили размер и размытие для плавности */}
<div className="absolute -top-20 -right-20 w-[800px] h-[800px] bg-primary/5 dark:bg-primary/10 blur-[150px] rounded-full -z-10 opacity-30 dark:opacity-60" />
<div className="absolute -bottom-20 -left-20 w-[800px] h-[800px] bg-secondary/5 dark:bg-secondary/10 blur-[150px] rounded-full -z-10 opacity-30 dark:opacity-60" />
{/* Заголовок секции */}
<div className="text-center mb-16">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-secondary/30 bg-secondary/10 text-secondary text-xs font-bold tracking-[0.2em] mb-4 uppercase">
<Zap className="size-3 fill-current" /> {dict.subtitle}
</div>
<h2 className="text-4xl md:text-6xl font-black tracking-tighter text-foreground font-mono">
{dict.title}
</h2>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
{/* ЛЕВАЯ КОЛОНКА (4/12): Философия и Форма */}
<div className="lg:col-span-5 space-y-8">
{/* Блок философии (Тот самый "Плюс") */}
<div className="bg-primary/5 border-l-4 border-primary p-6 rounded-r-2xl backdrop-blur-sm">
<h3 className="text-xl font-bold text-primary mb-3 flex items-center gap-2 font-mono uppercase tracking-tight">
<ShieldAlert className="size-5" /> {dict.philosophyTitle}
</h3>
<p className="text-muted-foreground leading-relaxed text-sm antialiased italic">
"{dict.philosophyText}"
</p>
</div>
{/* Форма отправки */}
<div className="bg-card/40 backdrop-blur-xl border border-border/50 rounded-[2.5rem] p-8 shadow-xl">
<form onSubmit={handleSubmit} className="space-y-5">
<div className="space-y-2">
<label
className={cn(
"text-[10px] font-mono uppercase tracking-[0.2em] flex items-center gap-2 ml-1 mb-2",
"text-primary/80 dark:text-primary font-bold", // Сделали жирнее и чуть приглушили в светлой
)}
>
<User className="size-3" /> {dict.labelAlias}
</label>
<input
type="text"
maxLength={20}
value={nickname}
onChange={(e) => setNickname(e.target.value)}
placeholder={dict.placeholderAlias}
disabled={isSubmitting || cooldown > 0}
// Фрагмент классов для input и textarea в TerminalGuestbook.tsx
className={cn(
"w-full rounded-2xl px-5 py-4 text-sm outline-none transition-all font-mono",
"disabled:opacity-50 text-foreground",
// Светлая тема: более плотный плейсхолдер и четкая рамка
"bg-background/50 border border-border/80 placeholder:text-muted-foreground/60 focus:border-primary focus:bg-background shadow-sm",
// Темная тема: стеклянный эффект
"dark:bg-background/50 dark:border-border/50 dark:placeholder:text-muted-foreground/30 dark:focus:border-primary/50",
)}
/>
</div>
<div className="space-y-2">
<label className="text-[10px] font-mono text-primary uppercase tracking-[0.2em] flex items-center gap-2 ml-1">
<MessageSquare className="size-3" /> {dict.labelPayload}
</label>
<textarea
maxLength={150}
rows={4}
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder={dict.placeholderPayload}
disabled={isSubmitting || cooldown > 0}
// Фрагмент классов для input и textarea в TerminalGuestbook.tsx
className={cn(
"w-full rounded-2xl px-5 py-4 text-sm outline-none transition-all font-mono",
"disabled:opacity-50 text-foreground",
// Светлая тема: более плотный плейсхолдер и четкая рамка
"bg-background/50 border border-border/80 placeholder:text-muted-foreground/60 focus:border-primary focus:bg-background shadow-sm",
// Темная тема: стеклянный эффект
"dark:bg-background/50 dark:border-border/50 dark:placeholder:text-muted-foreground/30 dark:focus:border-primary/50",
)}
/>
<div className="text-right text-[10px] text-muted-foreground/40 font-mono pr-2">
{message.length}/150
</div>
</div>
<input
type="text"
name="website"
tabIndex={-1}
autoComplete="off"
value={honeypot}
onChange={(e) => setHoneypot(e.target.value)}
style={{ position: "absolute", left: "-9999px" }}
/>
<AnimatePresence mode="wait">
{status.text && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className={`p-4 rounded-2xl text-xs font-mono flex items-start gap-3 border ${status.type === "error" ? "bg-destructive/10 border-destructive/20 text-destructive" : "bg-secondary/10 border-secondary/20 text-secondary"}`}
>
{status.type === "error" ? (
<ShieldAlert className="size-4 shrink-0" />
) : (
<CheckCircle2 className="size-4 shrink-0" />
)}
{status.text}
</motion.div>
)}
</AnimatePresence>
<CyberButton
type="submit"
variant="primary"
disabled={isSubmitting || cooldown > 0}
className="w-full"
>
{isSubmitting ? (
dict.btnEncrypting
) : cooldown > 0 ? (
dict.cooldown.replace("{{time}}", cooldown.toString())
) : (
<span className="flex items-center gap-2">
<Send className="size-4" /> {dict.btnBroadcast}
</span>
)}
</CyberButton>
</form>
</div>
</div>
{/* ПРАВАЯ КОЛОНКА (7/12): Фид сообщений */}
<div
className={cn(
"lg:col-span-7 h-[700px] flex flex-col p-8 rounded-[3rem] backdrop-blur-md transition-all",
// Светлая тема: чистый белый, жесткая граница, сильная тень
"bg-white/90 border border-border/80 shadow-xl",
// Темная тема: полупрозрачное стекло, неоновая граница
"dark:bg-black/20 dark:border-primary/20 dark:shadow-2xl",
)}
>
<div className="flex justify-between items-center mb-8 px-4">
<div className="flex items-center gap-3">
<div className="size-2 rounded-full bg-secondary animate-pulse shadow-[0_0_10px_rgba(0,229,242,0.8)]" />
<span className="font-mono text-xs text-secondary uppercase tracking-[0.3em] font-bold">
{dict.liveFeed}
</span>
</div>
<span className="text-[10px] font-mono text-muted-foreground/40 uppercase">
Encrypted_Uplink_v2.4
</span>
</div>
<div
ref={scrollRef}
className="flex-1 overflow-y-auto pr-4 space-y-6 custom-scrollbar-cyber"
>
<AnimatePresence initial={false}>
{comments.map((comment) => (
<motion.div
key={comment.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="relative group"
>
<div className="absolute -left-2 top-0 bottom-0 w-0.5 bg-gradient-to-b from-secondary/0 via-secondary/20 to-secondary/0 group-hover:via-secondary/50 transition-all" />
{/* Сообщение */}
<div
className={cn(
"p-5 rounded-[1.5rem] transition-all group-hover:translate-x-1",
// Светлая тема: серый фон, четкая рамка
"bg-muted/30 border border-border/50 group-hover:bg-muted/60",
// Темная тема: прозрачное стекло
"dark:bg-white/5 dark:border-white/5 dark:group-hover:bg-white/[0.08] dark:group-hover:border-white/10",
)}
>
<div className="flex justify-between items-center mb-3">
<span className="font-black text-secondary font-mono text-sm tracking-tight">
@{comment.nickname}
</span>
<span className="text-[10px] text-muted-foreground/40 font-mono">
{new Date(comment.created).toLocaleTimeString()}
</span>
</div>
<p className="text-muted-foreground text-[15px] leading-relaxed break-words font-medium">
{comment.message}
</p>
</div>
</motion.div>
))}
</AnimatePresence>
{comments.length === 0 && (
<div className="h-full flex flex-col items-center justify-center text-muted-foreground/20 font-mono space-y-4">
<Terminal className="size-12 opacity-10" />
<span className="text-xs uppercase tracking-[0.2em]">
{dict.noTransmissions}
</span>
</div>
)}
</div>
</div>
</div>
</section>
);
}
</file>
<file path="components/ui/accordion.tsx">
"use client";
import * as React from "react";
import { Accordion as AccordionPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
import { ChevronDown } from "lucide-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}
<ChevronDown 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 };
</file>
<file path="components/ui/button.tsx">
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "radix-ui";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"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: {
variant: {
default:
"bg-primary text-primary-foreground shadow-lg shadow-primary/20 hover:bg-primary/90",
outline:
"border-border bg-background/50 backdrop-blur-sm hover:bg-muted hover:text-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-muted/50 hover:text-foreground",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-11 px-6 py-2",
sm: "h-9 rounded-xl px-4 text-xs",
lg: "h-14 rounded-3xl px-8 text-base",
icon: "size-11 rounded-2xl",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };
</file>
<file path="components/ui/dropdown-menu.tsx">
"use client";
import * as React from "react";
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
import { Check, ChevronRight } from "lucide-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>
<Check className="size-4" />
</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>
<Check className="size-4" />
</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}
<ChevronRight 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,
};
</file>
<file path="collect_code.js">
import fs from "fs";
import path from "path";
const CONFIG = {
// Папки для сканирования
includeDirs: ["app", "components", "lib", "types"],
// Исключения
excludeDirs: ["node_modules", ".next", ".git", "public"],
// Только TypeScript файлы
extensions: [".ts", ".tsx"],
outputFile: "project_dump.txt",
};
let outputContent = "";
function readFilesRecursively(dir) {
const files = fs.readdirSync(dir);
files.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
if (!CONFIG.excludeDirs.includes(file)) {
readFilesRecursively(filePath);
}
} else {
const ext = path.extname(file);
if (CONFIG.extensions.includes(ext)) {
const content = fs.readFileSync(filePath, "utf8");
outputContent += `\n\n--- FILE: ${filePath} ---\n\n`;
outputContent += content;
}
}
});
}
console.log("🚀 Сборка TypeScript кода началась...");
// Обрабатываем основные папки
CONFIG.includeDirs.forEach((dir) => {
if (fs.existsSync(dir)) {
readFilesRecursively(dir);
}
});
// Дополнительно проверяем только ts/tsx файлы в корне (если они есть)
const rootFiles = fs
.readdirSync(".")
.filter(
(f) =>
fs.statSync(f).isFile() && CONFIG.extensions.includes(path.extname(f)),
);
rootFiles.forEach((file) => {
const content = fs.readFileSync(file, "utf8");
outputContent += `\n\n--- FILE: ${file} ---\n\n`;
outputContent += content;
});
fs.writeFileSync(CONFIG.outputFile, outputContent);
console.log(`\n✅ Готово!`);
console.log(`📄 Файл: ${CONFIG.outputFile}`);
console.log(`💡 Теперь в дампе только чистый код .ts и .tsx`);
</file>
<file path="next.config.ts">
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [{ protocol: "https", hostname: "netrunner-vpn.com" }],
},
};
export default nextConfig;
</file>
<file path="tailwind.config.ts">
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: "#0B0D17",
foreground: "#F8FAFC",
// ТЕПЕРЬ PRIMARY = ФИОЛЕТОВЫЙ
primary: {
DEFAULT: "#8B3DFF", // Яркий, сочный фиолетовый
foreground: "#FFFFFF", // Белый текст на фиолетовом фоне
},
// ТЕПЕРЬ SECONDARY = CYAN
secondary: {
DEFAULT: "#00E5F2", // Светлый циан
foreground: "#000000", // Черный текст на голубом фоне
},
muted: {
DEFAULT: "#1E2136",
foreground: "#A0AEC0", // Светлый серый для читаемости
},
border: "#2A2E45",
card: {
DEFAULT: "rgba(22, 25, 43, 0.6)",
foreground: "#F8FAFC",
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"pulse-primary":
"pulse-primary 2s cubic-bezier(0.4, 0, 0.6, 1) infinite", // Переименовал в pulse-primary
},
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-primary": {
"0%, 100%": {
opacity: "1",
boxShadow: "0 0 10px #8B3DFF, 0 0 20px #8B3DFF",
},
"50%": {
opacity: ".5",
boxShadow: "0 0 5px #8B3DFF, 0 0 10px #8B3DFF",
},
},
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;
export default config;
</file>
<file path="upload.mjs">
import * as dotenv from "dotenv";
dotenv.config({ path: ".env.local" });
import PocketBase from "pocketbase";
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
const PB_URL = process.env.NEXT_PUBLIC_PB_URL;
const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL;
const ADMIN_PASS = process.env.PB_ADMIN_PASS;
const pb = new PocketBase(PB_URL);
// --- ДАННЫЕ ДЛЯ ЗАГРУЗКИ ---
const rawDownloads = [
// --- RU ---
{
lang: "ru",
name: "Windows Клиент",
platformId: "windows",
version: "v1.2.4-stable",
status: "active",
storeLink: "https://github.com/netrunner-vpn/desktop",
order: 1,
},
{
lang: "ru",
name: "Android APK",
platformId: "android",
version: "v1.1.0-release",
status: "active",
storeLink: "", // Оставляем пустым для теста "Скоро в Store"
order: 2,
},
{
lang: "ru",
name: "Linux Core",
platformId: "linux",
version: "v1.2.4-build",
status: "active",
storeLink: "https://github.com/netrunner-vpn/core",
order: 3,
},
{
lang: "ru",
name: "macOS Desktop",
platformId: "macos",
version: "В разработке",
status: "dev",
storeLink: "",
order: 4,
},
{
lang: "ru",
name: "iOS App",
platformId: "ios",
version: "Бета-тест",
status: "dev",
storeLink: "",
order: 5,
},
// --- EN ---
{
lang: "en",
name: "Windows Client",
platformId: "windows",
version: "v1.2.4-stable",
status: "active",
storeLink: "https://github.com/netrunner-vpn/desktop",
order: 1,
},
{
lang: "en",
name: "Android APK",
platformId: "android",
version: "v1.1.0-release",
status: "active",
storeLink: "",
order: 2,
},
{
lang: "en",
name: "Linux Core",
platformId: "linux",
version: "v1.2.4-build",
status: "active",
storeLink: "https://github.com/netrunner-vpn/core",
order: 3,
},
{
lang: "en",
name: "macOS Desktop",
platformId: "macos",
version: "Coming soon",
status: "dev",
storeLink: "",
order: 4,
},
{
lang: "en",
name: "iOS App",
platformId: "ios",
version: "Under development",
status: "dev",
storeLink: "",
order: 5,
},
];
async function migrateDownloadsOnly() {
try {
console.log("🚀 Auth in progress...");
const authData = await pb.send("/api/admins/auth-with-password", {
method: "POST",
body: { identity: ADMIN_EMAIL, password: ADMIN_PASS },
});
pb.authStore.save(authData.token, authData.admin);
console.log("🔑 Access Granted. Syncing downloads...\n");
for (let item of rawDownloads) {
const { lang, ...cleanData } = item;
const targetTable = `downloads_${lang}`;
try {
// Создаем запись
await pb.collection(targetTable).create(cleanData);
console.log(
`✅ [${targetTable}] Created: ${cleanData.name} (${cleanData.platformId})`,
);
} catch (err) {
console.error(
`❌ [${targetTable}] Failed ${cleanData.name}:`,
err.message,
);
}
}
console.log("\n🏁 All downloads synchronized.");
} catch (err) {
console.error("💥 Global Error:", err.message);
}
}
migrateDownloadsOnly();
</file>
<file path="app/[lang]/blog/page.tsx">
import { pb } from "@/lib/pb";
import Link from "next/link";
import { Badge } from "@/components/ui/badge";
// Типизация записи из PocketBase
interface PBPost {
id: string;
title: string;
slug: string;
excerpt: string;
lang: string;
created: string;
published: boolean;
}
interface Props {
params: Promise<{ lang: string }>;
}
export default async function BlogIndexPage({ params }: Props) {
const { lang } = await params;
const isRu = lang === "ru";
let records: PBPost[] = [];
try {
records = await pb.collection(`posts_${lang}`).getFullList<PBPost>({
filter: "published = true",
sort: "-created",
fetch: (url, config) =>
fetch(url, { ...config, next: { revalidate: 3600 } }),
});
} catch (err) {
console.error("Failed to fetch blog posts:", err);
}
return (
<main className="min-h-screen pt-32 pb-20 container mx-auto max-w-5xl px-4">
<Badge
variant="outline"
className="mb-6 border-primary/30 text-primary uppercase tracking-widest"
>
{isRu ? "Архив терминала" : "Terminal Archive"}
</Badge>
<h1 className="text-5xl md:text-6xl font-extrabold tracking-tighter mb-12">
{isRu ? "Data Logs" : "System Logs"}
</h1>
{records.length === 0 ? (
<div className="p-20 border border-dashed border-border/50 rounded-3xl text-center bg-card/10">
<p className="text-muted-foreground font-mono italic">
{isRu
? "[Записи не найдены в этом секторе]"
: "[No records found in this sector]"}
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{records.map((post) => (
<Link
href={`/${lang}/blog/${post.slug}`}
key={post.id}
className="group block"
>
<article className="p-8 border border-border/50 rounded-2xl bg-card/20 hover:bg-card/40 hover:border-primary/50 transition-all duration-300 h-full flex flex-col">
<div className="text-xs text-primary font-mono mb-4 flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-primary rounded-full animate-pulse" />
{new Date(post.created).toLocaleDateString(
isRu ? "ru-RU" : "en-US",
)}
</div>
<h2 className="text-2xl font-bold mb-3 group-hover:text-primary transition-colors duration-300">
{post.title}
</h2>
<p className="text-muted-foreground text-sm leading-relaxed line-clamp-3">
{post.excerpt}
</p>
<div className="mt-8 pt-4 border-t border-border/30 text-xs font-bold uppercase tracking-widest text-primary/50 group-hover:text-primary transition-colors">
{isRu ? "Дешифровать →" : "Decrypt entry →"}
</div>
</article>
</Link>
))}
</div>
)}
</main>
);
}
</file>
<file path="app/[lang]/layout.tsx">
import { Metadata } from "next"; // Импортируем тип Metadata
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";
import { Footer } from "@/components/sections/Footer";
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin", "cyrillic"],
display: "swap",
preload: true, // Гарантирует предзагрузку в <head>
fallback: ["monospace"], // Указывает браузеру, что показывать до загрузки
});
// Настройка метаданных
export const metadata: Metadata = {
title: {
template: "%s | Netrunner VPN",
default: "Netrunner VPN — Система следит за всеми, кроме тебя.",
},
description:
"Цифровой след скрыт. Логи уничтожены. Скорость не ограничена. Ваш персональный защищенный туннель Netrunner.",
icons: {
icon: [
{
url: "/logo.svg",
href: "/logo.svg",
},
],
},
};
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} />
{/* УБРАЛИ mt-32 ТУТ */}
<main className="flex-1">{children}</main>
<Footer dict={dict.footer} lang={lang} />
</ThemeProvider>
</body>
</html>
);
}
</file>
<file path="components/sections/Header.tsx">
"use client";
import { useState } from "react";
import Link from "next/link";
import { LanguageSwitcher } from "@/components/sections/lang/language-switcher";
import { ThemeToggle } from "@/components/sections/theme/theme-toggle";
import { Dictionary } from "@/lib/IDict";
import { CyberButton } from "../ui/cyber-button";
import { MenuSquare, X, Terminal, FolderOpen, ArrowRight } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";
export function Header({
dict,
lang,
}: {
dict: Dictionary["nav"];
lang: string;
}) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
// Ссылки для мобильного меню (расширенный список, как в футере)
const mobileLinks = [
{ name: dict.features, href: `/${lang}#features`, icon: Terminal },
{ name: dict.pricing, href: `/${lang}#pricing`, icon: FolderOpen },
{ name: dict.faq, href: `/${lang}#faq`, icon: FolderOpen },
{ name: dict.blog, href: `/${lang}/blog`, icon: FolderOpen },
];
return (
<>
<header className="fixed top-4 md:top-6 left-1/2 -translate-x-1/2 w-[95%] max-w-5xl z-50 bg-card/30 dark:bg-card/20 backdrop-blur-xl border border-border/50 rounded-full shadow-lg transition-all duration-500 hover:bg-card/50 dark:hover:bg-card/40">
<div className="px-4 md:px-8 h-14 md:h-16 flex items-center justify-between">
{/* ЛЕВЫЙ БЛОК: Адаптивный Логотип */}
<Link
href={`/${lang}`}
className="flex items-center gap-2 md:gap-3 group hover:scale-105 transition-transform duration-300 w-fit relative z-50"
onClick={() => setIsMenuOpen(false)}
>
<div className="text-lg sm:text-2xl md:text-3xl tracking-wide md:tracking-widest flex items-center select-none font-bold uppercase">
<span
className="text-transparent animate-neon-flicker transition-all duration-300"
style={{
WebkitTextStroke: "1px #00E5F2",
filter: "drop-shadow(0 0 5px rgba(0,229,242,0.6))",
}}
>
Net
</span>
<span
className="text-transparent transition-all duration-300"
style={{
WebkitTextStroke: "1px #00E5F2",
filter: "drop-shadow(0 0 5px rgba(0,229,242,0.6))",
}}
>
runner
</span>
<span
className="ml-1.5 md:ml-2 text-transparent opacity-90 group-hover:opacity-100 transition-all duration-300"
style={{
WebkitTextStroke: "1px #8B3DFF",
filter: "drop-shadow(0 0 5px rgba(139,61,255,0.6))",
}}
>
VPN
</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-1.5 md:gap-4 relative z-50">
<div className="flex items-center gap-1 md:gap-2">
<LanguageSwitcher />
<ThemeToggle />
</div>
<CyberButton
href={`/${lang}/download`}
variant="primary"
className="hidden sm:block scale-75 md:scale-90 origin-right"
>
{dict.cta}
</CyberButton>
{/* КНОПКА МОБИЛЬНОГО МЕНЮ */}
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="sm:hidden p-2 rounded-xl bg-background/50 border border-border text-foreground hover:bg-muted transition-colors"
aria-label="Toggle System Menu"
>
{isMenuOpen ? (
<X className="size-5" />
) : (
<MenuSquare className="size-5" />
)}
</button>
</div>
</div>
</header>
{/* ВСПЛЫВАЮЩЕЕ ОКНО (Мобильное меню в стиле ретро-ОС) */}
<AnimatePresence>
{isMenuOpen && (
<motion.div
initial={{ opacity: 0, y: -20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.95 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
className="fixed top-24 left-4 right-4 z-40 sm:hidden origin-top"
>
{/* Окно-папка */}
<div className="bg-[#E5E5E5] dark:bg-[#1E1E1E] border-2 border-t-white border-l-white border-b-gray-500 border-r-gray-500 dark:border-t-gray-600 dark:border-l-gray-600 dark:border-b-black dark:border-r-black p-1 shadow-[4px_4px_0_0_rgba(0,0,0,0.5)]">
{/* Заголовок окна (Title bar) */}
<div className="bg-blue-800 text-white px-2 py-1 flex items-center justify-between mb-2 font-mono text-xs font-bold tracking-wider">
<span className="flex items-center gap-2">
<Terminal className="size-3" /> C:\NETRUNNER\MENU.EXE
</span>
</div>
{/* Содержимое папки (Ссылки) */}
<div className="bg-white dark:bg-black border-2 border-inset border-t-gray-500 border-l-gray-500 border-b-white border-r-white dark:border-t-black dark:border-l-black dark:border-b-gray-700 dark:border-r-gray-700 p-2 font-mono flex flex-col gap-1 max-h-[70vh] overflow-y-auto custom-scrollbar-retro">
{mobileLinks.map((link) => (
<Link
key={link.name}
href={link.href}
onClick={() => setIsMenuOpen(false)}
className="flex items-center gap-3 p-3 hover:bg-blue-800 hover:text-white dark:hover:bg-primary/20 dark:hover:text-primary transition-colors group cursor-pointer text-sm text-black dark:text-white shrink-0"
>
<link.icon className="size-5 text-gray-500 group-hover:text-white dark:group-hover:text-primary transition-colors fill-yellow-200 dark:fill-transparent" />
<span className="flex-1">{link.name}</span>
<span className="opacity-0 group-hover:opacity-100 transition-opacity">
_
</span>
</Link>
))}
{/* Разделитель */}
<div className="my-2 border-t border-dashed border-gray-400 dark:border-gray-700 shrink-0" />
{/* Доп ссылки из футера */}
<Link
href={`/${lang}/docs`}
onClick={() => setIsMenuOpen(false)}
className="flex items-center gap-3 p-3 hover:bg-blue-800 hover:text-white dark:hover:bg-primary/20 dark:hover:text-primary transition-colors group cursor-pointer text-xs text-gray-600 dark:text-gray-400 shrink-0"
>
<Terminal className="size-4" />
<span className="flex-1 uppercase tracking-widest">
Documentation
</span>
</Link>
{/* Кнопка теперь идет СРАЗУ за списком, а не прибита к низу h-[60vh] */}
<div className="pt-4 pb-2 px-2 shrink-0">
<CyberButton
href={`/${lang}/download`}
variant="primary"
className="w-full text-xs"
>
<span className="flex items-center justify-center gap-2">
{dict.cta} <ArrowRight className="size-3" />
</span>
</CyberButton>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Оверлей для закрытия меню по клику вне его */}
{isMenuOpen && (
<div
className="fixed inset-0 z-30 sm:hidden bg-black/20 backdrop-blur-sm"
onClick={() => setIsMenuOpen(false)}
/>
)}
</>
);
}
</file>
<file path="components/sections/MatrixBackground.tsx">
"use client";
import { useEffect, useRef } from "react";
export default function MatrixBackground({
isSecure,
theme,
}: {
isSecure: boolean;
theme?: string;
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const mousePos = useRef({ x: -1000, y: -1000 });
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d", { alpha: false });
if (!ctx) return;
let width = (canvas.width = window.innerWidth);
let height = (canvas.height = window.innerHeight);
const fontSize = 15;
const columns = Math.floor(width / fontSize);
const drops = Array(columns).fill(1);
// Хакерский набор символов
const chars = "01ヲアウエオカキケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン".split("");
const isDark = theme === "dark";
let animationFrameId: number;
let lastTime = 0;
const fps = 22;
const interval = 1000 / fps;
const handleMouseMove = (e: MouseEvent) => {
mousePos.current = { x: e.clientX, y: e.clientY };
};
const handleResize = () => {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
};
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("resize", handleResize);
const draw = (currentTime: number) => {
animationFrameId = requestAnimationFrame(draw);
const deltaTime = currentTime - lastTime;
if (deltaTime < interval) return;
lastTime = currentTime - (deltaTime % interval);
// Рисуем шлейф: чем меньше alpha, тем длиннее "хвосты" у цифр
ctx.fillStyle = isDark
? "rgba(11, 13, 23, 0.25)"
: "rgba(248, 250, 252, 0.30)";
ctx.fillRect(0, 0, width, height);
ctx.font = `${fontSize}px monospace`;
for (let i = 0; i < drops.length; i++) {
const x = i * fontSize;
const y = drops[i] * fontSize;
const dx = x - mousePos.current.x;
const dy = y - mousePos.current.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const radius = 350; // Радиус подсветки за курсором
const highlight = Math.max(0, 1 - distance / radius);
// Базовая яркость очень низкая (0.05), чтобы фон не бил в глаза
const baseOpacity = isDark ? 0.15 : 0.2;
const finalOpacity = baseOpacity + highlight * 0.6;
// Основной цвет
if (isSecure) {
ctx.fillStyle = isDark
? `rgba(0, 240, 255, ${finalOpacity})`
: `rgba(2, 132, 199, ${finalOpacity})`;
} else {
ctx.fillStyle = isDark
? `rgba(139, 61, 255, ${finalOpacity})`
: `rgba(109, 40, 217, ${finalOpacity})`;
}
// Мягкое свечение только там, где "взгляд" (highlight)
if (isDark && highlight > 0.8) {
ctx.shadowBlur = 10;
ctx.shadowColor = isSecure ? "#00F0FF" : "#8B3DFF";
} else {
ctx.shadowBlur = 0;
}
const char = chars[Math.floor(Math.random() * chars.length)];
ctx.fillText(char, x, y);
if (y > height && Math.random() > 0.985) {
drops[i] = 0;
}
drops[i]++;
}
};
animationFrameId = requestAnimationFrame(draw);
return () => {
cancelAnimationFrame(animationFrameId);
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("resize", handleResize);
};
}, [isSecure, theme]);
return (
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full pointer-events-none"
/>
);
}
</file>
<file path="lib/IDict.ts">
export interface Dictionary {
nav: {
features: string;
pricing: string;
faq: string;
blog: string;
cta: string;
};
hero: {
badge: string;
badgeSecure: string;
titleStart: string;
titleHighlight: string;
subtitle: string;
switchLabel: string;
ctaPrimary: string;
ctaSecondary: string;
};
features: {
title: string;
items: { title: string; description: string }[];
};
pricing: {
badge: string;
title: string;
perMonth: string;
recommend: string;
};
faq: {
title: string;
empty: string;
};
blog: {
badge: string;
title: string;
more: string;
readMore: string;
empty: string;
};
// ДОБАВЬ ЭТОТ БЛОК:
footer: {
brand: string;
tagline: string;
sections: {
product: string;
resources: string;
legal: string;
};
links: {
features: string;
pricing: string;
faq: string;
protocol: string;
comms: string;
privacy: string;
terms: string;
};
rights: string;
};
download: {
title: string;
subtitle: string;
badges: { active: string; dev: string };
buttons: {
direct: string;
store: string;
github: string;
notify: string;
soon: string; // <--- ДОБАВИЛИ
};
platforms: {
windows: string;
android: string;
linux: string;
macos: string;
ios: string;
};
};
loading: {
establishing: string;
};
notFound: {
errorBadge: string;
message: string;
returnHome: string;
};
nodes: {
title: string;
subtitle: string;
status: {
online: string;
full: string;
maintenance: string;
};
labels: {
load: string;
latency: string;
};
};
guestbook: {
title: string;
subtitle: string;
description: string;
philosophyTitle: string;
philosophyText: string;
liveFeed: string;
noTransmissions: string;
labelAlias: string;
labelPayload: string;
placeholderAlias: string;
placeholderPayload: string;
statusSuccess: string;
statusErrorBreach: string;
statusErrorEmpty: string;
statusErrorSize: string;
statusErrorFirewall: string;
statusErrorOffline: string;
btnEncrypting: string;
btnBroadcast: string;
cooldown: string;
};
}
</file>
<file path=".github/workflows/deploy.yml">
name: Deploy Frontend & CMS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Copy files to VPS
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.VPS_IP }}
username: root
key: ${{ secrets.SSH_PRIVATE_KEY }}
# Копируем всё, кроме тяжелого мусора
source: ".,!.git,!node_modules,!.next"
target: "/root/netrunner-frontend"
- name: Docker Run
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.VPS_IP }}
username: root
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /root/netrunner-frontend
# 1. Штатно тушим текущий проект
docker compose down || true
# 2. ЖЕСТКАЯ ЗАЧИСТКА: Убиваем контейнеры по именам,
# даже если они зависли от старых деплоев или других папок.
# || true гарантирует, что пайплайн не упадет, если контейнеров уже нет.
docker rm -f netrunner-landing netrunner-cms || true
# 3. Поднимаем всё заново с пересборкой
docker compose up -d --build
# 4. Чистим мусор: удаляем dangling образы (без тегов),
# чтобы сервер не задохнулся от нехватки места на диске.
docker image prune -f
</file>
<file path="components/sections/BlogPreview.tsx">
import Link from "next/link";
import Image from "next/image";
import {
Card,
CardHeader,
CardTitle,
CardContent,
CardFooter,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ArrowRight } from "lucide-react";
import { pb, getPbImage, type PBPost } from "@/lib/pb";
import { Dictionary } from "@/lib/IDict";
interface BlogPreviewProps {
lang: string;
dict: Dictionary["blog"];
}
// Теперь это асинхронный серверный компонент
export async function BlogPreview({ lang, dict }: BlogPreviewProps) {
let posts: PBPost[] = [];
try {
// В Next.js App Router лучше использовать нативный fetch для контроля кэша,
// либо передавать кастомный fetch в PB SDK. Здесь используем ISR на 3600 секунд (1 час).
posts = await pb.collection(`posts_${lang}`).getFullList<PBPost>({
filter: "published = true",
sort: "-created",
perPage: 3,
fetch: (url, config) =>
fetch(url, { ...config, next: { revalidate: 3600 } }),
});
} catch (error) {
console.error("Ошибка загрузки логов:", error);
return null; // Тихо фейлимся без падения страницы
}
if (posts.length === 0) return null;
return (
<section id="blog" className="py-24 px-4 container mx-auto max-w-7xl">
<div className="flex flex-col md:flex-row justify-between items-end mb-12 gap-6">
<div>
<Badge
variant="outline"
className="mb-4 text-primary border-primary/30"
>
{dict.badge}
</Badge>
<h2 className="text-4xl font-bold tracking-tighter text-foreground">
{dict.title}
</h2>
</div>
<Button
asChild
variant="ghost"
className="text-muted-foreground hover:text-primary transition-colors"
>
<Link
href={`/${lang}/blog`}
className="flex items-center gap-2 font-bold"
>
{dict.more} <ArrowRight size={16} />
</Link>
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{posts.map((post) => (
<Card
key={post.id}
className="group overflow-hidden border-border/50 bg-card/30 hover:border-primary/50 transition-all duration-300 flex flex-col h-full"
>
{post.image && (
<div className="relative w-full h-48 overflow-hidden border-b border-border/50 bg-muted">
// Внутри .map в BlogPreview.tsx:
<Image
src={getPbImage(post, post.image)}
alt={post.title}
fill
sizes="(max-width: 768px) 100vw, 33vw"
priority={false}
className="object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background to-transparent opacity-80" />
</div>
)}
<CardHeader className="flex-none">
<div className="text-xs font-mono text-muted-foreground mb-2">
{new Date(post.created).toLocaleDateString(
lang === "ru" ? "ru-RU" : "en-US",
)}
</div>
<CardTitle className="text-xl group-hover:text-primary transition-colors line-clamp-2 h-14 flex items-start">
{post.title}
</CardTitle>
</CardHeader>
<CardContent className="flex-1">
<p className="text-sm text-muted-foreground line-clamp-3 leading-relaxed">
{post.excerpt}
</p>
</CardContent>
<CardFooter className="pt-0">
<Button
asChild
variant="link"
className="px-0 text-primary font-bold hover:no-underline group/link"
>
<Link
href={`/${lang}/blog/${post.slug}`}
className="flex items-center gap-1"
>
{dict.readMore}
<ArrowRight
size={14}
className="group-hover/link:translate-x-1 transition-transform"
/>
</Link>
</Button>
</CardFooter>
</Card>
))}
</div>
</section>
);
}
</file>
<file path="components/sections/Features.tsx">
"use client";
import { EyeOff, Lock, Globe, Gauge, Cpu } from "lucide-react";
import { motion, Variants } from "framer-motion";
import { Dictionary } from "@/lib/IDict";
import { cn } from "@/lib/utils";
export function Features({ dict }: { dict: Dictionary["features"] }) {
// Класс для карточек: в светлой теме — плотный белый с тенью, в темной — стекло
const cardClass = cn(
"min-h-[320px] md:min-h-[360px] group relative flex flex-col p-8 md:p-10 rounded-[2.5rem] overflow-hidden transition-all duration-500",
"bg-white border-border shadow-sm hover:shadow-md", // Light
"dark:bg-card/40 dark:backdrop-blur-xl dark:border-border/50 dark:hover:border-primary/50 dark:hover:shadow-[0_0_40px_rgba(139,61,255,0.15)]", // Dark
);
// Класс для контейнера иконки: в светлой теме — "плитка", в темной — "ядро"
const iconContainerClass = cn(
"w-14 h-14 rounded-[1.2rem] flex items-center justify-center relative z-10 transition-transform duration-500 group-hover:scale-110",
"bg-slate-50 border-border shadow-inner", // Light
"dark:bg-secondary/10 dark:border-secondary/20 dark:shadow-[0_0_15px_rgba(0,229,242,0.15)]", // Dark
);
// Стиль для самой иконки (SVG)
const iconStyle =
"w-7 h-7 text-secondary dark:drop-shadow-[0_0_8px_rgba(0,229,242,0.6)]";
// Декоративные фоновые элементы (большие иконки)
const bgIconClass =
"absolute -bottom-10 -right-10 w-64 h-64 transition-all duration-700 pointer-events-none group-hover:scale-110 group-hover:-rotate-12";
const bgIconTheme =
"text-slate-100 dark:text-primary dark:opacity-[0.03] dark:group-hover:opacity-[0.08]";
const bentoConfig = [
{
className: `md:col-span-2 ${cardClass}`,
icon: (
<div className={iconContainerClass}>
<EyeOff className={iconStyle} />
</div>
),
bgElement: <EyeOff className={cn(bgIconClass, bgIconTheme)} />,
},
{
className: `md:col-span-1 ${cardClass}`,
icon: (
<div className={iconContainerClass}>
<Lock className={iconStyle} />
</div>
),
bgElement: (
<div className="absolute top-0 right-0 w-40 h-40 bg-primary/5 dark:bg-primary/10 blur-[70px] opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
),
},
{
className: `md:col-span-1 ${cardClass}`,
icon: (
<div className={iconContainerClass}>
<Gauge className={iconStyle} />
</div>
),
bgElement: (
<Gauge
className={cn(
bgIconClass,
bgIconTheme,
"w-48 h-48 top-10 -right-8 bottom-auto",
)}
/>
),
},
{
className: `md:col-span-2 ${cardClass}`,
icon: (
<div className={iconContainerClass}>
<Cpu className={iconStyle} />
</div>
),
bgElement: (
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_right,_var(--tw-gradient-stops))] from-primary/5 dark:from-primary/10 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
),
},
{
className: cn(
cardClass,
"md:col-span-3 flex-col md:flex-row items-start md:items-center gap-8 md:gap-12",
),
icon: (
<div className="bg-slate-50 dark:bg-secondary/10 w-16 h-16 md:w-20 md:h-20 rounded-[1.5rem] md:rounded-[1.8rem] shrink-0 flex items-center justify-center border border-border dark:border-secondary/20 shadow-inner relative z-10 transition-transform duration-500 group-hover:scale-105">
<Globe className="w-8 h-8 md:w-10 md:h-10 text-secondary dark:drop-shadow-[0_0_10px_rgba(0,229,242,0.8)]" />
</div>
),
bgElement: (
<div className="absolute inset-0 bg-[radial-gradient(circle_at_left,_var(--tw-gradient-stops))] from-primary/5 dark:from-primary/10 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
),
},
];
const containerVariants: Variants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.1 },
},
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 20 },
show: {
opacity: 1,
y: 0,
transition: { type: "spring", stiffness: 260, damping: 20 },
},
};
if (!dict || !dict.items || dict.items.length < 5) return null;
return (
<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-primary/5 blur-[100px] rounded-full pointer-events-none -z-10 opacity-50 dark:opacity-100" />
<div className="mb-16 md:mb-20 text-center">
<h2 className="text-4xl md:text-5xl font-black tracking-tight text-foreground uppercase">
{dict.title}
</h2>
</div>
<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];
const isHorizontal = idx === 4;
return (
<motion.div
key={idx}
variants={itemVariants}
className={config.className}
>
{config.bgElement}
{config.icon}
<div
className={cn(
"relative z-10",
isHorizontal ? "" : "mt-auto pt-8",
)}
>
<h3 className="text-xl md:text-2xl font-bold text-foreground mb-3 tracking-tight uppercase">
{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>
);
}
</file>
<file path="components/sections/Hero.tsx">
"use client";
import { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { motion, Variants } from "framer-motion";
import { Shield, EyeOff } from "lucide-react";
import { useTheme } from "next-themes";
import dynamic from "next/dynamic";
import { Dictionary } from "@/lib/IDict";
import { CyberButton } from "../ui/cyber-button";
const MatrixBackground = dynamic(() => import("./MatrixBackground"), {
ssr: false,
});
function CyberEye({ isClosed, vpnOn }: { isClosed: boolean; vpnOn: boolean }) {
const eyeRef = useRef<HTMLDivElement>(null);
const [offset, setOffset] = useState({ x: 0, y: 0 });
const [direction, setDirection] = useState("center");
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (isClosed || !eyeRef.current) return;
const rect = eyeRef.current.getBoundingClientRect();
const eyeCenterX = rect.left + rect.width / 2;
const eyeCenterY = rect.top + rect.height / 2;
const dx = e.clientX - eyeCenterX;
const dy = e.clientY - eyeCenterY;
if (Math.abs(dx) < 30 && Math.abs(dy) < 30) {
setDirection("center");
} else if (Math.abs(dx) > Math.abs(dy)) {
setDirection(dx > 0 ? "right" : "left");
} else {
setDirection(dy > 0 ? "down" : "up");
}
const angle = Math.atan2(dy, dx);
const maxRadius = 22;
const distance = Math.min(maxRadius, Math.hypot(dx, dy) * 0.12);
setOffset({
x: Math.cos(angle) * distance,
y: Math.sin(angle) * distance,
});
};
if (!isClosed) window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, [isClosed]);
const eyeVariants: Variants = {
open: {
height: 72,
width: 120,
borderRadius: 36,
borderWidth: 4,
transition: { type: "spring", stiffness: 450, damping: 12 },
},
closed: {
height: 6,
width: 140,
borderRadius: 8,
borderWidth: 3,
transition: { type: "spring", stiffness: 600, damping: 14 },
},
};
// ИНВЕРСИЯ: Активный VPN (vpnOn) теперь фиолетовый (Primary), выключенный - циан (Secondary)
const borderColor = vpnOn
? "border-purple-600 dark:border-primary shadow-[0_0_15px_rgba(139,61,255,0.3)] dark:shadow-[0_0_20px_rgba(139,61,255,0.4)]"
: "border-cyan-600 dark:border-secondary shadow-[0_0_15px_rgba(0,229,242,0.3)] dark:shadow-[0_0_20px_rgba(0,229,242,0.4)]";
return (
<motion.div
ref={eyeRef}
variants={eyeVariants}
initial="open"
animate={isClosed ? "closed" : "open"}
className={`relative flex items-center justify-center overflow-hidden bg-background ${borderColor} z-10`}
>
<motion.div
animate={{
x: isClosed ? 0 : offset.x,
y: isClosed ? 0 : offset.y,
scale: isClosed ? 0.3 : 1,
opacity: isClosed ? 0 : 1,
}}
transition={{ type: "tween", ease: "easeOut", duration: 0.1 }}
className="relative flex items-center justify-center text-foreground"
>
<span
className={`absolute -top-4 text-xs transition-opacity duration-200 ${direction === "up" ? "opacity-100 text-current scale-125" : "opacity-20"}`}
>
</span>
<span
className={`absolute -bottom-4 text-xs transition-opacity duration-200 ${direction === "down" ? "opacity-100 text-current scale-125" : "opacity-20"}`}
>
</span>
<span
className={`absolute -left-5 text-xs transition-opacity duration-200 ${direction === "left" ? "opacity-100 text-current scale-125" : "opacity-20"}`}
>
</span>
<span
className={`absolute -right-5 text-xs transition-opacity duration-200 ${direction === "right" ? "opacity-100 text-current scale-125" : "opacity-20"}`}
>
</span>
<span className="text-3xl z-10 drop-shadow-md"></span>
</motion.div>
{/* ИНВЕРСИЯ ЦВЕТА ПОЛОСКИ */}
<motion.div
className={`absolute h-0.5 w-full ${vpnOn ? "bg-purple-600 dark:bg-primary" : "bg-cyan-600 dark:bg-secondary"}`}
initial={{ opacity: 0 }}
animate={{ opacity: isClosed ? 1 : 0 }}
transition={{ duration: 0.1 }}
/>
</motion.div>
);
}
function CyberWatcherEyes({ vpnOn }: { vpnOn: boolean }) {
const [isBlinking, setIsBlinking] = useState(false);
const blinkTimerRef = useRef<NodeJS.Timeout | null>(null);
const unblinkTimerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (vpnOn) {
if (blinkTimerRef.current) clearTimeout(blinkTimerRef.current);
if (unblinkTimerRef.current) clearTimeout(unblinkTimerRef.current);
return;
}
const scheduleNextBlink = () => {
const delay = Math.random() * 3500 + 1500;
blinkTimerRef.current = setTimeout(() => {
setIsBlinking(true);
unblinkTimerRef.current = setTimeout(() => {
setIsBlinking(false);
scheduleNextBlink();
}, 140);
}, delay);
};
scheduleNextBlink();
return () => {
if (blinkTimerRef.current) clearTimeout(blinkTimerRef.current);
if (unblinkTimerRef.current) clearTimeout(unblinkTimerRef.current);
};
}, [vpnOn]);
const shouldBeClosed = vpnOn || isBlinking;
return (
<div className="flex gap-6 md:gap-10 justify-center mb-12 h-20 items-center select-none">
<CyberEye isClosed={shouldBeClosed} vpnOn={vpnOn} />
<CyberEye isClosed={shouldBeClosed} vpnOn={vpnOn} />
</div>
);
}
export function Hero({
dict,
lang,
}: {
dict: Dictionary["hero"];
lang: string;
}) {
const [isVpnOn, setIsVpnOn] = useState(false);
const { resolvedTheme } = useTheme();
return (
<section className="relative pt-32 pb-20 md:pt-48 md:pb-32 flex flex-col items-center text-center px-4 min-h-[95vh] justify-center overflow-hidden">
{/* КОНТЕЙНЕР ФОНА: Теперь он inset-0, без верхних границ */}
<div className="absolute inset-0 -z-10 bg-background pointer-events-none">
<MatrixBackground isSecure={isVpnOn} theme={resolvedTheme} />
{/* ВИНЬЕТКА: Сделаем ее погуще сверху, чтобы хедер читался идеально */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_transparent_10%,_var(--background)_110%)] opacity-80" />
{/* Дополнительный мягкий градиент сверху для "воздуха" */}
<div className="absolute inset-x-0 top-0 h-40 bg-gradient-to-b from-background via-transparent to-transparent opacity-90" />
</div>
<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 pt-40 md:pt-48 pb-20"
>
{/* ИНВЕРСИЯ БЕЙДЖА */}
<motion.div
layout
className={`inline-flex items-center gap-2 px-4 py-1.5 rounded-full border text-sm font-bold mb-8 transition-colors duration-500 shadow-sm ${
isVpnOn
? "bg-purple-100 border-purple-300 text-purple-800 dark:bg-primary/20 dark:border-primary dark:text-primary-foreground dark:drop-shadow-[0_0_5px_rgba(139,61,255,0.4)]"
: "bg-cyan-100 border-cyan-300 text-cyan-800 dark:bg-secondary/20 dark:border-secondary dark:text-secondary-foreground dark:drop-shadow-[0_0_5px_rgba(0,229,242,0.4)]"
}`}
>
{isVpnOn ? (
<Shield className="w-4 h-4" />
) : (
<EyeOff className="w-4 h-4" />
)}
<span>{isVpnOn ? dict.badgeSecure : dict.badge}</span>
</motion.div>
<CyberWatcherEyes vpnOn={isVpnOn} />
{/* ИНВЕРСИЯ ЗАГОЛОВКА */}
<h1 className="text-4xl md:text-6xl font-extrabold tracking-tight text-slate-900 dark:text-white dark:drop-shadow-md mb-6 transition-colors duration-500 font-mono">
{dict.titleStart}{" "}
<span
className={`transition-all duration-500 ${
isVpnOn
? "text-purple-600 dark:text-primary dark:drop-shadow-[0_0_15px_rgba(139,61,255,0.6)]"
: "text-cyan-600 dark:text-secondary dark:drop-shadow-[0_0_15px_rgba(0,229,242,0.6)]"
}`}
>
{dict.titleHighlight}
</span>
</h1>
<p className="text-muted-foreground font-medium text-lg md:text-xl max-w-2xl mx-auto mb-12">
{dict.subtitle}
</p>
<div className="flex flex-col items-center gap-8 mb-12">
{/* ИНВЕРСИЯ SWITCH'А */}
<div className="flex items-center gap-4 bg-card/90 backdrop-blur-xl border border-border/60 p-4 rounded-3xl shadow-md transition-colors duration-300">
<Switch
checked={isVpnOn}
onCheckedChange={setIsVpnOn}
className="data-[state=checked]:bg-purple-600 dark:data-[state=checked]:bg-primary data-[state=unchecked]:bg-cyan-600 dark:data-[state=unchecked]:bg-secondary"
/>
<span
className={`text-base font-bold transition-colors font-mono ${
isVpnOn
? "text-purple-700 dark:text-primary dark:drop-shadow-[0_0_5px_rgba(139,61,255,0.8)]"
: "text-foreground"
}`}
>
Netrunner VPN
</span>
</div>
<div className="flex flex-col sm:flex-row gap-6 justify-center w-full sm:w-auto font-mono">
<CyberButton
href={`/${lang}/download`}
variant={isVpnOn ? "primary" : "secondary"}
>
{dict.ctaPrimary}
</CyberButton>
<CyberButton href={`/${lang}/nodes`} variant="secondary">
{dict.ctaSecondary}
</CyberButton>
</div>
</div>
</motion.div>
</section>
);
}
</file>
<file path="components/sections/Pricing.tsx">
"use client";
import { useState, useEffect } from "react";
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, Loader2 } from "lucide-react";
import { Dictionary } from "@/lib/IDict";
import { pb } from "@/lib/pb";
import { CyberButton } from "../ui/cyber-button";
interface PBPricingPlan {
id: string;
name: string;
level: string;
tagline: string;
description: string;
price1m: number;
price6m: number;
price1y: number;
features: string;
popular: boolean;
cta: string;
lang: string;
order: number;
}
type Period = "1m" | "6m" | "1y";
interface PeriodOption {
id: Period;
label: string;
badge?: string;
}
export function Pricing({
dict,
lang,
}: {
dict: Dictionary["pricing"];
lang: string;
}) {
const [period, setPeriod] = useState<Period>("1y");
const [plans, setPlans] = useState<PBPricingPlan[]>([]);
const [loading, setLoading] = useState(!!lang);
useEffect(() => {
if (!lang) return;
async function fetchPlans() {
try {
setLoading(true);
const records = await pb
.collection(`pricing_${lang}`)
.getFullList<PBPricingPlan>({
sort: "order",
requestKey: null,
});
setPlans(records);
} catch (error) {
console.error("Ошибка загрузки тарифов:", error);
} finally {
setLoading(false);
}
}
fetchPlans();
}, [lang]);
const isRu = lang === "ru";
const currencySign = isRu ? "₽" : "$";
const periods: PeriodOption[] = [
{ id: "1m", label: isRu ? "1 месяц" : "1 month" },
{ id: "6m", label: isRu ? "6 месяцев" : "6 months" },
{
id: "1y",
label: isRu ? "1 год" : "1 year",
badge: isRu ? "Выгодно" : "Best Value",
},
];
return (
<section
id="pricing"
className="py-32 px-4 container mx-auto max-w-7xl relative min-h-[600px]"
>
<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="inline-flex items-center justify-center p-1.5 bg-card/50 border border-border/50 rounded-2xl backdrop-blur-sm relative">
{periods.map((p: PeriodOption) => (
<button
key={p.id}
onClick={() => setPeriod(p.id)}
// Исправлено: фиксированная высота h-14, flex-col, элементы по центру
className={`relative flex flex-col items-center justify-center gap-0.5 px-6 min-w-[120px] h-14 text-sm font-bold rounded-xl transition-all duration-300 ${
period === p.id
? "bg-primary text-primary-foreground shadow-lg shadow-primary/25"
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
}`}
>
<span>{p.label}</span>
{/* Исправлено: бейджик стал мелким, курсивным, под основным текстом */}
{p.badge && (
<span
className={`text-[9px] uppercase tracking-widest italic leading-none font-black ${
period === p.id
? "text-primary-foreground/70"
: "text-primary/70"
}`}
>
{p.badge}
</span>
)}
</button>
))}
</div>
</div>
{loading ? (
<div className="flex flex-col items-center justify-center py-20 text-primary opacity-50">
<Loader2 className="animate-spin mb-4" />
<span className="text-sm uppercase font-mono tracking-widest">
Syncing_Nodes...
</span>
</div>
) : plans.length === 0 ? (
<p className="text-center text-muted-foreground font-mono">
0 uplinks found.
</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 items-stretch">
{plans.map((plan) => {
const months = period === "1m" ? 1 : period === "6m" ? 6 : 12;
const totalPrice =
period === "1m"
? plan.price1m
: period === "6m"
? plan.price6m
: plan.price1y;
const originalTotalPrice = plan.price1m * months;
const isDiscounted = totalPrice < originalTotalPrice;
const monthlyEquivalent = Math.round(totalPrice / months);
const formatPrice = (price: number) =>
isRu ? price.toLocaleString("ru-RU") : price.toString();
return (
<Card
key={plan.id}
className={`relative flex flex-col h-full transition-all duration-300 ${
plan.popular
? "border-primary/50 shadow-2xl shadow-primary/10 scale-[1.02] z-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">
{isRu ? "Рекомендуем" : "Recommended"}
</Badge>
</div>
)}
<CardHeader className="pb-0 flex-none">
{" "}
{/* Фиксируем шапку */}
<div className="text-[10px] font-bold text-primary uppercase tracking-[0.2em] mb-2 h-4">
{plan.level}
</div>
{/* Фиксированная высота заголовка */}
<CardTitle className="text-3xl font-black text-foreground uppercase tracking-tight h-10 flex items-center">
{plan.name}
</CardTitle>
{/* Цитата: фиксируем высоту, чтобы не прыгало */}
<div className="text-sm font-medium italic text-muted-foreground mt-4 border-l-2 border-primary/30 pl-3 min-h-[40px] flex items-center">
«{plan.tagline}»
</div>
{/* Описание: запас на 3 строки текста */}
<CardDescription className="mt-4 text-sm leading-relaxed min-h-[80px]">
{plan.description}
</CardDescription>
{/* Блок цены: жестко привязан к позиции */}
<div className="mt-8 mb-6">
<div className="flex items-center gap-2 h-6 mb-1">
{isDiscounted ? (
<>
<span className="text-sm text-muted-foreground/50 line-through">
{formatPrice(originalTotalPrice)}
{currencySign}
</span>
<Badge
variant="secondary"
className="bg-green-500/10 text-green-500 border-none text-[10px] px-2 py-0"
>
{Math.round(
(1 - totalPrice / originalTotalPrice) * 100,
)}
% OFF
</Badge>
</>
) : (
<div className="h-6" /> // Spacer
)}
</div>
<div className="flex items-baseline gap-1">
<span className="text-5xl font-black tracking-tighter text-foreground">
{isRu ? "" : currencySign}
{formatPrice(monthlyEquivalent)}
{isRu ? currencySign : ""}
</span>
<span className="text-muted-foreground text-sm font-bold">
/mo
</span>
</div>
<div className="h-5 mt-1 text-[10px] font-mono text-muted-foreground/60 uppercase tracking-wider">
{period !== "1m" &&
`${isRu ? "Total billed:" : "Total billed:"} ${formatPrice(totalPrice)}${currencySign}`}
</div>
</div>
</CardHeader>
{/* flex-1 заставляет контент расширяться, толкая футер вниз */}
<CardContent className="flex-1 pb-8">
<ul className="space-y-4 pt-6 border-t border-border/20">
{plan.features
.split("\n")
.filter((f) => f.trim())
.map((feature, i) => (
<li
key={i}
className="flex items-start gap-3 text-sm text-foreground/80 group"
>
<div className="bg-primary/10 w-5 h-5 rounded-full flex items-center justify-center mt-0.5 shrink-0 transition-colors group-hover:bg-primary/20">
<CheckIcon className="w-3 h-3 text-primary stroke-[3]" />
</div>
<span className="leading-tight">
{feature.trim()}
</span>
</li>
))}
</ul>
</CardContent>
<CardFooter className="pt-0">
<CyberButton
href="#"
variant={plan.popular ? "primary" : "secondary"}
className="w-full"
>
{plan.cta}
</CyberButton>
</CardFooter>
</Card>
);
})}
</div>
)}
</section>
);
}
</file>
<file path="dictionaries/en.json">
{
"nav": {
"features": "Features",
"pricing": "Pricing",
"faq": "FAQ",
"blog": "Logs",
"cta": "Get Netrunner"
},
"hero": {
"badge": "System is watching",
"badgeSecure": "Secure tunnel established",
"titleStart": "The system watches everyone,",
"titleHighlight": "except you.",
"subtitle": "Digital footprint hidden. Logs destroyed. Speed unlimited.",
"switchLabel": "Netrunner VPN",
"ctaPrimary": "Download Client",
"ctaSecondary": "Node List"
},
"features": {
"title": "Access Technologies",
"items": [
{
"title": "DPI Evasion & uTLS",
"description": "Netrunner masks traffic as standard HTTPS. Deep Packet Inspection systems see only white noise."
},
{
"title": "ChaCha20-Poly1305",
"description": "State-of-the-art stream encryption with Perfect Forward Secrecy. Session keys are generated dynamically."
},
{
"title": "Adaptive Backpressure",
"description": "Buffer sizes dynamically scale based on system RTT. Prevents latency spikes automatically."
},
{
"title": "Multi-Platform Core",
"description": "Low-level Rust core ensures flawless compatibility and zero overhead across all major OS."
},
{
"title": "Global Network",
"description": "Private node network with flow multiplexing, providing ultimate anonymity and stable connection."
}
]
},
"pricing": {
"badge": "Pricing Protocols",
"title": "Select your Uplink",
"perMonth": "/mo",
"recommend": "Recommended"
},
"faq": {
"title": "System Queries (FAQ)",
"empty": "No records found."
},
"blog": {
"badge": "Data Logs",
"title": "Network Chronicles",
"more": "All entries",
"readMore": "Read more",
"empty": "No signals found."
},
"footer": {
"brand": "Netrunner VPN",
"tagline": "The system watches everyone, except you.",
"sections": {
"product": "Product",
"resources": "Resources",
"legal": "Legal"
},
"links": {
"features": "Features",
"pricing": "Pricing",
"faq": "FAQ",
"protocol": "Protocol",
"comms": "Blog",
"privacy": "Privacy",
"terms": "Terms"
},
"rights": "© 2026 Netrunner VPN. All packets encrypted."
},
"download": {
"title": "Node Deployment",
"subtitle": "Select your platform to deploy the Netrunner client.",
"badges": {
"active": "Stable Build",
"dev": "In Development"
},
"buttons": {
"direct": "Direct Download",
"store": "Store Link",
"github": "Source Code",
"notify": "Notify Me",
"soon": "Soon in Store"
},
"platforms": {
"windows": "Windows 10 / 11",
"android": "Android 8.0+",
"linux": "Ubuntu / Debian / Arch",
"macos": "macOS 12.0+",
"ios": "iOS 15.0+"
}
},
"loading": {
"establishing": "Establishing connection"
},
"notFound": {
"errorBadge": "[ Error: Node Not Found ]",
"message": "The routing table does not contain the requested node. The transmission was dropped. Return to the secure network.",
"returnHome": "Return Home"
},
"nodes": {
"title": "Global Node Network",
"subtitle": "Real-time topology of Netrunner servers.",
"status": {
"online": "Online",
"full": "Full",
"maintenance": "Maintenance"
},
"labels": {
"load": "Server Load",
"latency": "Latency"
}
},
"guestbook": {
"title": "COMM-LINK_",
"subtitle": "Communication Terminal",
"description": "Direct uplink to Netrunner nodes.",
"philosophyTitle": "Zero Censorship",
"philosophyText": "We built Netrunner to bypass all barriers, so we don't fear your words. This feed is a living testament to free speech. While others hide feedback, we stream it in real-time. If our protocol stands against DPI, it will stand against your honesty.",
"liveFeed": "Live Feed (Last 50)",
"noTransmissions": "No active transmissions found.",
"labelAlias": "Alias",
"labelPayload": "Payload",
"placeholderAlias": "Guest_77",
"placeholderPayload": "Enter transmission data...",
"statusSuccess": "Payload delivered successfully.",
"statusErrorBreach": "Security breach. Connection dropped.",
"statusErrorEmpty": "Requires alias and payload.",
"statusErrorSize": "Payload exceeds maximum size.",
"statusErrorFirewall": "Firewall active. Wait {{time}}s.",
"statusErrorOffline": "Transmission failed. Node offline.",
"btnEncrypting": "Encrypting...",
"btnBroadcast": "Broadcast",
"cooldown": "Cooldown: {{time}}s"
}
}
</file>
<file path="dictionaries/ru.json">
{
"nav": {
"features": "Возможности",
"pricing": "Тарифы",
"faq": "Вопросы",
"blog": "Логи",
"cta": "Скачать Netrunner"
},
"hero": {
"badge": "Система следит за всеми",
"badgeSecure": "Защищенный туннель установлен",
"titleStart": "Система следит за всеми,",
"titleHighlight": "кроме тебя.",
"subtitle": "Цифровой след скрыт. Логи уничтожены. Скорость не ограничена.",
"switchLabel": "Netrunner VPN",
"ctaPrimary": "Скачать клиент",
"ctaSecondary": "Список узлов"
},
"features": {
"title": "Технологии доступа",
"items": [
{
"title": "Обход DPI и uTLS",
"description": "Протокол Netrunner маскирует трафик под обычные HTTPS-запросы. Системы глубокого анализа пакетов видят лишь белый шум."
},
{
"title": "Криптография ChaCha20",
"description": "Высокоскоростной шифр потоковой передачи с Perfect Forward Secrecy. Ключи сессий меняются динамически."
},
{
"title": "Adaptive Backpressure",
"description": "Размер буфера масштабируется в зависимости от пинга. При плохой связи буфер сжимается для устранения лагов."
},
{
"title": "Multi-Platform Core",
"description": "Низкоуровневое сетевое ядро на Rust гарантирует нулевой оверхед на iOS, Android, Windows и macOS."
},
{
"title": "Global Network",
"description": "Собственная сеть узлов по всему миру с мультиплексированием потоков для обеспечения максимальной анонимности."
}
]
},
"pricing": {
"badge": "Тарифные протоколы",
"title": "Выберите ваш Uplink",
"perMonth": "/мес",
"recommend": "Рекомендуем"
},
"faq": {
"title": "Системные запросы (FAQ)",
"empty": "База знаний пуста."
},
"blog": {
"badge": "Data Logs",
"title": "Сетевые Хроники",
"more": "Все записи",
"readMore": "Читать далее",
"empty": "Сигналы не найдены."
},
"footer": {
"brand": "Netrunner VPN",
"tagline": "Система следит за всеми, кроме тебя.",
"sections": {
"product": "Продукт",
"resources": "Ресурсы",
"legal": "Право"
},
"links": {
"features": "Возможности",
"pricing": "Тарифы",
"faq": "FAQ",
"protocol": "Протокол",
"comms": "Блог",
"privacy": "Приватность",
"terms": "Условия"
},
"rights": "© 2026 Netrunner VPN. Все пакеты зашифрованы."
},
"download": {
"title": "Установка Узла",
"subtitle": "Выберите вашу платформу для загрузки клиента Netrunner.",
"badges": {
"active": "Стабильная",
"dev": "В разработке"
},
"buttons": {
"direct": "Скачать напрямую",
"store": "В Store",
"github": "Source Code",
"notify": "Уведомить",
"soon": "Скоро в Store"
},
"platforms": {
"windows": "Windows 10 / 11",
"android": "Android 8.0+",
"linux": "Ubuntu / Debian / Arch",
"macos": "macOS 12.0+",
"ios": "iOS 15.0+"
}
},
"loading": {
"establishing": "Установка соединения"
},
"notFound": {
"errorBadge": "[ Ошибка: Узел не найден ]",
"message": "В таблице маршрутизации нет запрашиваемого узла. Передача прервана. Вернитесь в защищенную сеть.",
"returnHome": "Вернуться на базу"
},
"nodes": {
"title": "Глобальная Сеть Узлов",
"subtitle": "Топология серверов Netrunner в реальном времени.",
"status": {
"online": "В сети",
"full": "Перегружен",
"maintenance": "Обслуживание"
},
"labels": {
"load": "Нагрузка",
"latency": "Задержка"
}
},
"guestbook": {
"title": "COMM-LINK_",
"subtitle": "Терминал связи",
"description": "Прямой канал связи с узлами Netrunner.",
"philosophyTitle": "Нам не нужна модерация",
"philosophyText": "Мы создали Netrunner для обхода любых барьеров, поэтому мы не боимся того, что вы напишете. Этот чат — живое доказательство свободы слова. Пока другие прячут негатив, мы выводим его в реальном времени. Если наш протокол выдерживает атаки DPI, он выдержит и вашу честность.",
"liveFeed": "Прямой эфир (Последние 50)",
"noTransmissions": "Активных передач не обнаружено.",
"labelAlias": "Позывной",
"labelPayload": "Данные",
"placeholderAlias": "Guest_77",
"placeholderPayload": "Введите сообщение...",
"statusSuccess": "Данные успешно доставлены в сеть.",
"statusErrorBreach": "Нарушение безопасности. Соединение разорвано.",
"statusErrorEmpty": "Требуется позывной и пакет данных.",
"statusErrorSize": "Размер пакета превышает лимит.",
"statusErrorFirewall": "Файрвол активен. Подождите {{time}} сек.",
"statusErrorOffline": "Передача прервана. Узел недоступен.",
"btnEncrypting": "Шифрование...",
"btnBroadcast": "Транслировать",
"cooldown": "Кулдаун: {{time}}с"
}
}
</file>
<file path="app/[lang]/page.tsx">
import { Suspense } from "react";
import dynamic from "next/dynamic";
import { getDictionary } from "@/lib/dictionaries";
import { Hero } from "@/components/sections/Hero";
import { Features } from "@/components/sections/Features";
// Легкий скелетон для плавности
const SectionSkeleton = () => (
<div className="h-[500px] w-full animate-pulse bg-card/20 rounded-3xl my-10" />
);
// Ленивая загрузка тяжелых секций
const Pricing = dynamic(
() => import("@/components/sections/Pricing").then((mod) => mod.Pricing),
{
loading: () => <SectionSkeleton />,
},
);
const FAQ = dynamic(
() => import("@/components/sections/Faq").then((mod) => mod.FAQ),
{
loading: () => <SectionSkeleton />,
},
);
const TerminalGuestbook = dynamic(
() =>
import("@/components/sections/TerminalGuestbook").then(
(mod) => mod.TerminalGuestbook,
),
{
loading: () => <SectionSkeleton />,
},
);
const BlogPreview = dynamic(() =>
import("@/components/sections/BlogPreview").then((mod) => mod.BlogPreview),
);
export default async function Home({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
const dict = await getDictionary(lang);
return (
<>
<Hero dict={dict.hero} lang={lang} />
<Features dict={dict.features} />
{/* Теперь эти секции загрузятся только когда понадобятся */}
<Pricing dict={dict.pricing} lang={lang} />
<FAQ dict={dict.faq} lang={lang} />
<TerminalGuestbook dict={dict.guestbook} />
<Suspense fallback={<SectionSkeleton />}>
<BlogPreview lang={lang} dict={dict.blog} />
</Suspense>
</>
);
}
</file>
<file path="components/sections/Faq.tsx">
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { HelpCircle } from "lucide-react";
import { Dictionary } from "@/lib/IDict";
import { pb } from "@/lib/pb";
interface PBFaq {
id: string;
question: string;
answer: string;
lang: string;
order: number;
}
// Теперь это Server Component
export async function FAQ({
dict,
lang,
}: {
dict: Dictionary["faq"];
lang: string;
}) {
let faqs: PBFaq[] = [];
try {
faqs = await pb.collection(`faqs_${lang}`).getFullList<PBFaq>({
sort: "order",
fetch: (url, config) =>
fetch(url, { ...config, next: { revalidate: 86400 } }), // Кэшируем на сутки
});
} catch (error) {
console.error("Ошибка загрузки FAQ:", error);
}
const formatAnswer = (text: string) => {
return text.split("\n").map((line, i) => {
const trimmedLine = line.trim();
if (trimmedLine.startsWith("•")) {
return (
<div key={i} className="flex gap-3 mb-2 pl-2 group">
<span className="text-secondary font-bold group-hover:animate-pulse">
</span>
<span className="text-muted-foreground/90">
{trimmedLine.replace("•", "").trim()}
</span>
</div>
);
}
if (!trimmedLine) return <div key={i} className="h-4" />;
return (
<p
key={i}
className="mb-3 last:mb-0 text-muted-foreground/90 leading-relaxed"
>
{line}
</p>
);
});
};
return (
<section
id="faq"
className="py-32 px-4 container mx-auto max-w-4xl relative"
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-primary/10 dark:from-primary/20 via-transparent to-transparent blur-[100px] pointer-events-none -z-10" />
<div className="flex flex-col items-center mb-16">
<div className="bg-primary/10 p-3 rounded-2xl mb-6 border border-primary/20">
<HelpCircle className="w-8 h-8 text-primary" />
</div>
<h2 className="text-4xl md:text-5xl font-black tracking-tight text-center">
{dict.title}
</h2>
</div>
{faqs.length === 0 ? (
<div className="text-center py-20 border border-dashed border-border/50 rounded-[2.5rem] bg-card/20">
<p className="text-muted-foreground font-mono text-sm uppercase tracking-widest">
0 protocols found in cache.
</p>
</div>
) : (
<Accordion type="single" collapsible className="w-full space-y-4">
{faqs.map((faq) => (
<AccordionItem
key={faq.id}
value={`item-${faq.id}`}
className="border border-border/40 bg-card/30 backdrop-blur-md rounded-[1.5rem] px-6 transition-all duration-300 hover:border-primary/30 data-[state=open]:border-primary/50 data-[state=open]:bg-card/50 shadow-sm"
>
<AccordionTrigger className="text-left text-foreground hover:no-underline hover:text-primary transition-colors font-bold text-lg md:text-xl py-6">
{faq.question}
</AccordionTrigger>
<AccordionContent className="pb-8 text-base antialiased">
<div className="border-t border-border/30 pt-6">
{formatAnswer(faq.answer)}
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)}
</section>
);
}
</file>
<file path="docker-compose.yml">
services:
landing:
build: .
container_name: netrunner-landing
restart: always
ports:
- "127.0.0.1:3000:3000"
environment:
- NODE_ENV=production
- NEXT_PUBLIC_PB_URL=https://netrunner-vpn.com/cms-api
netrunner-cms:
image: ghcr.io/muchobien/pocketbase:latest
container_name: netrunner-cms
restart: unless-stopped
# Указываем только аргументы, так как путь к бинарнику уже в ENTRYPOINT
command:
- serve
- --http=0.0.0.0:8090
- --dir=/pb_data
ports:
- "127.0.0.1:8090:8090"
volumes:
- ./pb_data:/pb_data
ws-proxy:
build: ./proxy-ws
container_name: netrunner-ws
restart: always
ports:
- "127.0.0.1:3001:3001"
environment:
- PB_URL=http://netrunner-cms:8090
depends_on:
- netrunner-cms
</file>
<file path="app/globals.css">
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-mono);
--font-heading: var(--font-mono);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-primary-foreground: var(--primary-foreground);
--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: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: 0.75rem;
--radius-md: 1rem;
--radius-lg: 1.5rem;
--radius-xl: 2rem;
--radius-2xl: 2.5rem;
--radius-3xl: 3rem;
}
:root {
/* Остужаем фон, чтобы он не был слепяще белым */
--background: oklch(0.98 0.01 240);
--foreground: oklch(0.2 0.02 260);
/* Карточки теперь чистые белые с легким блюром */
--card: oklch(1 0 0 / 90%);
--card-foreground: oklch(0.2 0.02 260);
--popover: oklch(1 0 0 / 95%);
--popover-foreground: oklch(0.2 0.02 260);
/* Светлая тема */
/* Primary = Более глубокий фиолетовый для контраста */
--primary: oklch(0.5 0.2 290);
--primary-foreground: oklch(0.985 0 0);
/* Secondary = Более насыщенный циан (ближе к синему) */
--secondary: oklch(0.55 0.15 220);
--secondary-foreground: oklch(1 0 0);
--muted: oklch(0.94 0.01 240);
--muted-foreground: oklch(0.45 0.02 260);
--accent: oklch(0.94 0.02 290);
--accent-foreground: oklch(0.45 0.25 290);
--border: oklch(0.85 0.02 260);
--input: oklch(0.9 0.02 260);
--ring: oklch(0.5 0.2 290);
}
.dark {
--background: oklch(0.1 0.01 260);
--foreground: oklch(0.98 0 0);
--card: oklch(0.14 0.02 260 / 50%);
--card-foreground: oklch(0.98 0 0);
--popover: oklch(0.12 0.02 260 / 80%);
--popover-foreground: oklch(0.98 0 0);
/* ТЕПЕРЬ PRIMARY = ФИОЛЕТОВЫЙ */
--primary: oklch(0.65 0.25 290);
--primary-foreground: oklch(0.98 0 0);
/* ТЕПЕРЬ SECONDARY = CYAN */
--secondary: oklch(0.85 0.15 190);
--secondary-foreground: oklch(0.1 0.01 260);
--muted: oklch(0.18 0.02 260);
--muted-foreground: oklch(0.85 0.02 260);
--accent: oklch(0.2 0.04 260);
--accent-foreground: oklch(0.98 0 0);
--border: oklch(0.25 0.03 260 / 60%);
--input: oklch(0.2 0.03 260 / 50%);
--ring: oklch(0.65 0.25 290);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
.shadow-md {
box-shadow:
0 4px 6px -1px rgb(0 0 0 / 0.05),
0 2px 4px -2px rgb(0 0 0 / 0.05);
}
.dark .shadow-2xl {
/* Тонкое свечение для темной темы, которое не режет глаз */
box-shadow:
0 25px 50px -12px rgb(0 0 0 / 0.5),
0 0 20px -5px oklch(var(--primary) / 0.1);
}
@layer utilities {
.text-neon-cyan {
color: #ffffff;
text-shadow:
0 0 2px #fff,
0 0 8px #00f0ff,
0 0 15px #00f0ff,
0 0 30px #00f0ff;
}
.text-neon-purple {
color: #ffffff;
text-shadow:
0 0 2px #fff,
0 0 8px #8b3dff,
0 0 15px #8b3dff,
0 0 30px #8b3dff;
}
.light .text-neon-cyan {
color: #0284c7;
text-shadow: 0 0 2px rgba(2, 132, 199, 0.2);
}
.light .text-neon-purple {
color: #6d28d9;
text-shadow: 0 0 2px rgba(109, 40, 217, 0.2);
}
.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;
}
}
@keyframes scan {
from {
left: -100%;
}
to {
left: 100%;
}
}
.animate-scan {
animation: scan 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@layer utilities {
.custom-scrollbar-retro::-webkit-scrollbar {
width: 16px;
}
.custom-scrollbar-retro::-webkit-scrollbar-track {
background: #dfdfdf;
box-shadow:
inset 1px 1px #fff,
inset -1px -1px #808080;
}
.custom-scrollbar-retro::-webkit-scrollbar-thumb {
background: #c0c0c0;
border: 2px solid #dfdfdf;
box-shadow:
1px 1px #808080,
-1px -1px #fff;
}
.dark .custom-scrollbar-retro::-webkit-scrollbar-track {
background: #1a1a1a;
}
.dark .custom-scrollbar-retro::-webkit-scrollbar-thumb {
background: #333;
}
}
</file>
<file path="lib/pb.ts">
import PocketBase, { RecordModel } from "pocketbase";
const isServer = typeof window === "undefined";
const publicUrl =
process.env.NEXT_PUBLIC_PB_URL || "https://netrunner-vpn.com/cms-api";
const internalUrl = process.env.PB_INTERNAL_URL; // Не ставим дефолт здесь
export const PB_URL = isServer ? internalUrl || publicUrl : publicUrl;
export const pb = new PocketBase(PB_URL);
// Отключаем автоотмену на сервере, чтобы избежать AbortError при SSR/ISR
if (isServer) {
pb.autoCancellation(false);
}
if (typeof window !== "undefined") {
// Фильтруем ошибки абортов (CANCELLED), которые генерит PocketBase
const originalError = console.error;
console.error = (...args) => {
if (args[0]?.message?.includes("The request was autocancelled")) return;
originalError.apply(console, args);
};
}
/**
* Базовый интерфейс для записей PocketBase.
* Наследуемся от RecordModel, чтобы иметь системные поля (id, created, updated, и т.д.)
*/
export interface BaseRecord extends RecordModel {
id: string;
collectionId: string;
collectionName: string;
created: string;
updated: string;
}
/**
* Типизация для постов блога
*/
export interface PBPost extends BaseRecord {
title: string;
slug: string;
excerpt: string;
content: string;
image: string; // имя файла
lang: "ru" | "en";
published: boolean;
}
/**
* Типизация для FAQ
*/
export interface PBFaq extends BaseRecord {
question: string;
answer: string;
order: number;
lang: string;
}
const FILE_BASE_URL =
process.env.NEXT_PUBLIC_PB_URL || "https://netrunner-vpn.com/cms-api";
/**
* Функция для генерации ссылки на картинку из PB
* @param record - Объект записи (должен содержать id и collectionId)
* @param filename - Имя файла из поля записи
*/
export function getPbImage(
record: Pick<BaseRecord, "id" | "collectionId">,
filename: string | undefined,
): string {
if (!filename) return "";
return `${FILE_BASE_URL}/api/files/${record.collectionId}/${record.id}/${filename}`;
}
// lib/pb.ts (добавь в конец файла)
export interface PBDownload extends BaseRecord {
name: string; // Имя (напр. "Windows 11")
platformId: "windows" | "android" | "linux" | "macos" | "ios"; // Строгий ID для иконок
version: string; // Строка версии
status: "active" | "dev"; // Статус разработки
file: string; // Сам бинарник (файл)
storeLink: string; // URL на магазин или github
order: number; // Для сортировки (1, 2, 3...)
}
export function getPbFileUrl(
record: Pick<BaseRecord, "id" | "collectionId">,
filename: string | undefined,
): string {
if (!filename) return "";
return `${FILE_BASE_URL}/api/files/${record.collectionId}/${record.id}/${filename}`;
}
</file>
<file path="package.json">
{
"name": "netrunner-landing",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.8.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.4.2",
"framer-motion": "^12.38.0",
"install": "^0.13.0",
"lucide-react": "^1.8.0",
"negotiator": "^1.0.0",
"next": "16.2.4",
"next-themes": "^0.4.6",
"pocketbase": "^0.26.8",
"radix-ui": "^1.4.3",
"react": "19.2.4",
"react-dom": "19.2.4",
"shadcn": "^4.3.0",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/negotiator": "^0.6.4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.4",
"tailwindcss": "^4",
"typescript": "^5"
}
}
</file>
</files>