This file is a merged representation of the entire codebase, combined into a single document by Repomix. This section contains a summary of this file. 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. 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 - 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. - 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) .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 This section contains the contents of the repository's files. 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 = 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) -> 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.") } } } [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" # Для кастомного словаря треша 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 ; } "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 (

{dict.title}

{dict.subtitle}

{MOCK_NODES.map((node) => (
{node.flag}

{node.city}

{node.country}

{getStatusText(node.status)} ID: {node.id.toUpperCase()}
{/* Load Bar */}
{dict.labels.load} 80 ? "text-secondary" : "text-primary" } > {node.load}%
80 ? "bg-secondary shadow-[0_0_10px_#7000FF]" : "bg-primary shadow-[0_0_10px_#00F0FF]"}`} style={{ width: `${node.load}%` }} />
{/* Ping */}
{dict.labels.latency} {node.status === "maintenance" ? "---" : `${node.ping} ms`}
))}
); } // 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 ; } import { Loader2 } from "lucide-react"; export default function Loading() { return (

Establishing connection

); } 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 (
{/* Декоративный фон */}

404

[ Error: Node Not Found ]

The routing table does not contain the requested node. The transmission was dropped. Return to the secure network.

); } "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 ( switchLanguage("en")}> English switchLanguage("ru")}> Русский ); } "use client"; import * as React from "react"; import { ThemeProvider as NextThemesProvider, ThemeProviderProps, } from "next-themes"; export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return {children}; } "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 ( ); } 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 & { asChild?: boolean }) { const Comp = asChild ? Slot.Root : "span"; return ( ); } export { Badge, badgeVariants }; import * as React from "react"; import { cn } from "@/lib/utils"; function Card({ className, ...props }: React.ComponentProps<"div">) { return (
); } function CardHeader({ className, ...props }: React.ComponentProps<"div">) { // Убираем горизонтальные отступы здесь, так как они уже есть в Card return (
); } function CardTitle({ className, ...props }: React.ComponentProps<"div">) { return (
); } function CardDescription({ className, ...props }: React.ComponentProps<"div">) { return (
); } function CardContent({ className, ...props }: React.ComponentProps<"div">) { return (
); } function CardFooter({ className, ...props }: React.ComponentProps<"div">) { // CardFooter прозрачный, без верхней границы return (
); } export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent, }; "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) => 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 = ( {/* CRT Сетка (только когда активна) */} {!isPrimary && !disabled && (
)} {/* Энергетический блик (только когда активна) */} {!disabled && (
)} {/* Угловые элементы (⌜ ⌟) - прячем или приглушаем при disabled */}
{/* Анимированная полоска заряда (только для Secondary и когда активна) */} {!isPrimary && !disabled && ( )} {/* Текст */} [ {children} ] ); // Фоновое облако свечения (только когда активна) const glowEffect = !disabled && (
); if (href && !disabled) { return ( {content} {glowEffect} ); } return ( ); } "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 & { size?: "sm" | "default" }) { return ( ) } export { Switch } 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(); }; 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; // если будешь прикреплять файлы } import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } node_modules .next .git .env.local .vercel README.md # 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 # 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. @AGENTS.md { "$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": {} } # 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"] module.exports = { apps: [ { name: "netrunner-landing", script: "node_modules/next/dist/bin/next", args: "start -p 3000", exec_mode: "fork", env: { NODE_ENV: "production", }, }, ], }; 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; const config = { plugins: { "@tailwindcss/postcss": {}, }, }; export default config; 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 = {}; 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|.*\\..*).*)"], }; 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. { "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"] } 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(`slug = "${slug}" && published = true`, { cache: "no-store", }); } catch (error) { notFound(); } return (
{/* Хлебные крошки / Назад */} root@netrunner:~# cd ../logs
{/* Обложка с неоновым свечением */} {post.image && (
{/* eslint-disable-next-line @next/next/no-img-element */} {post.title}
)} {/* Шапка статьи */}
{new Date(post.created).toLocaleDateString( isRu ? "ru-RU" : "en-US", )} {isRu ? "Чтение: 4 мин" : "Read: 4 min"} {isRu ? "Проверено: Netrunner" : "Verified: Netrunner"}

{post.title}

{/* Тело статьи с Tailwind Typography (prose) */}
{/* Футер статьи */}
End of transmission // node_th_01
); }
"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([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(""); const [activeCategory, setActiveCategory] = useState("All"); const [selectedDoc, setSelectedDoc] = useState(null); useEffect(() => { async function fetchDocs() { try { setLoading(true); // Обращаемся к documents_ru или documents_en const records = await pb .collection(`documents_${lang}`) .getFullList({ 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 ; case "Security": return ; case "Network": return ; case "Legal": return ; default: return ; } }; const formatDate = (dateStr: string) => dateStr.split(" ")[0]; return ( // Добавлен overflow-x-hidden для защиты от горизонтального скролла на мобилках
{/* Исправлено свечение: теперь max-w-full, чтобы не распирать экран */}
{loading ? (

Establishing_secure_link...

) : !selectedDoc ? ( {/* Заголовок и поиск: центрирование на мобильных (items-center text-center) */}

DATA_VAULT

System logs & documentation // V4.2

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" />
{/* Фильтры: центрирование на мобильных (justify-center) */}
{CATEGORIES.map((cat) => ( ))}
{/* Сетка документов */} {filteredDocs.map((doc) => ( 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" >
{getCategoryIcon(doc.category)}
{doc.status}

{doc.title}

{doc.category} {formatDate(doc.created)}
))}
{filteredDocs.length === 0 && (

0 records found for query {`"${search}"`}

)}
) : ( // --- РЕЖИМ ЧТЕНИЯ --- {/* Кнопка "Назад": центрируем на мобилках */}
{/* Шапка терминала: перенос на новую строку на мобильных */}
{selectedDoc.id}.txt
{getCategoryIcon(selectedDoc.category)} {selectedDoc.category}
{/* Тело документа: уменьшены отступы для телефона (p-5 вместо p-8) */}

{selectedDoc.title}

{selectedDoc.content || "NO_CONTENT_FOUND"}
EOF
)}
); } "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([]); const [loading, setLoading] = useState(true); useEffect(() => { async function fetchDownloads() { try { setLoading(true); const records = await pb .collection(`downloads_${lang}`) .getFullList({ 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 ; case "linux": return ; case "android": return ; case "macos": case "ios": return ; default: return ; } }; 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 (
{/* Мягкое фоновое свечение */}

{dict.title}

{dict.subtitle}

{loading ? (
Accessing_Build_Server...
) : ( {platforms.map((platform) => ( {/* Статус бейдж */}
{platform.status === "active" ? (
{dict.badges.active}
) : (
{dict.badges.dev}
)}
{/* Иконка в Циановом (Secondary) стиле */}
{getPlatformIcon(platform.platformId)}

{platform.name}

{platform.version}

{/* Кнопки действий: Основная всегда Фиолетовая (Primary) */}
{platform.status === "active" ? ( <> {/* Прямое скачивание */} {platform.file ? ( ) : ( )} {/* Ссылка на магазин (Вместо GitHub) */} {platform.storeLink ? ( ) : ( )} ) : ( // В разработке )}
))}
)}
); } 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 ; } "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: () => ( ), Youtube: () => ( ), Discord: () => ( ), X: () => ( ), Instagram: () => ( ), Habr: () => ( ), }; 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 ( {/* Фоновый отсвет только для темной темы */}