design fixes, tables seperate by language, loading and 404 pages, downloads from cms
This commit is contained in:
@@ -26,17 +26,15 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
let post: PBPost;
|
||||
|
||||
try {
|
||||
// Ищем в соответствующей коллекции по слагу
|
||||
post = await pb
|
||||
.collection("posts")
|
||||
.getFirstListItem<PBPost>(
|
||||
`slug = "${slug}" && lang = "${lang}" && published = true`,
|
||||
{ cache: "no-store" },
|
||||
);
|
||||
.collection(`posts_${lang}`)
|
||||
.getFirstListItem<PBPost>(`slug = "${slug}" && published = true`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
} catch (error) {
|
||||
// В продакшене лучше не логировать каждый 404, чтобы не забивать консоль
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen pt-32 pb-20 bg-background">
|
||||
<div className="container mx-auto max-w-3xl px-4">
|
||||
|
||||
@@ -24,11 +24,11 @@ export default async function BlogIndexPage({ params }: Props) {
|
||||
let records: PBPost[] = [];
|
||||
|
||||
try {
|
||||
// Возвращаем фильтрацию: только нужный язык и только опубликованные
|
||||
records = await pb.collection("posts").getFullList<PBPost>({
|
||||
filter: `lang = "${lang}" && published = true`,
|
||||
// Динамически выбираем таблицу: posts_ru или posts_en
|
||||
records = await pb.collection(`posts_${lang}`).getFullList<PBPost>({
|
||||
filter: "published = true",
|
||||
sort: "-created",
|
||||
cache: "no-store", // Чтобы новые посты из админки появлялись сразу
|
||||
cache: "no-store",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch blog posts:", err);
|
||||
|
||||
@@ -37,12 +37,11 @@ export function DocsClient({ lang }: { lang: string }) {
|
||||
async function fetchDocs() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const filterString = `lang = "${lang}"`;
|
||||
// Обращаемся к documents_ru или documents_en
|
||||
const records = await pb
|
||||
.collection("documents")
|
||||
.collection(`documents_${lang}`)
|
||||
.getFullList<PBDocument>({
|
||||
sort: "-created",
|
||||
filter: filterString,
|
||||
});
|
||||
setDocs(records);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
// Добавили импорт Variants
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion, Variants } from "framer-motion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -11,64 +11,62 @@ import {
|
||||
Download,
|
||||
ExternalLink,
|
||||
Bell,
|
||||
Loader2,
|
||||
Clock,
|
||||
} from "lucide-react";
|
||||
import { Dictionary } from "@/lib/IDict";
|
||||
import { pb, type PBDownload, getPbFileUrl } from "@/lib/pb";
|
||||
|
||||
export function DownloadClient({ dict }: { dict: Dictionary["download"] }) {
|
||||
const platforms = [
|
||||
{
|
||||
id: "windows",
|
||||
name: "Windows",
|
||||
version: dict.platforms.windows,
|
||||
icon: <Monitor className="w-8 h-8" />,
|
||||
status: "active",
|
||||
color: "text-primary",
|
||||
shadow: "shadow-[0_0_20px_rgba(0,240,255,0.2)]",
|
||||
borderHover: "hover:border-primary/50",
|
||||
},
|
||||
{
|
||||
id: "android",
|
||||
name: "Android",
|
||||
version: dict.platforms.android,
|
||||
icon: <Smartphone className="w-8 h-8" />,
|
||||
status: "active",
|
||||
color: "text-secondary",
|
||||
shadow: "shadow-[0_0_20px_rgba(112,0,255,0.2)]",
|
||||
borderHover: "hover:border-secondary/50",
|
||||
},
|
||||
{
|
||||
id: "linux",
|
||||
name: "Linux",
|
||||
version: dict.platforms.linux,
|
||||
icon: <Terminal className="w-8 h-8" />,
|
||||
status: "active",
|
||||
color: "text-primary",
|
||||
shadow: "shadow-[0_0_20px_rgba(0,240,255,0.2)]",
|
||||
borderHover: "hover:border-primary/50",
|
||||
},
|
||||
{
|
||||
id: "macos",
|
||||
name: "macOS",
|
||||
version: dict.platforms.macos,
|
||||
icon: <Apple className="w-8 h-8" />,
|
||||
status: "dev",
|
||||
color: "text-muted-foreground",
|
||||
shadow: "shadow-none",
|
||||
borderHover: "hover:border-border",
|
||||
},
|
||||
{
|
||||
id: "ios",
|
||||
name: "iOS",
|
||||
version: dict.platforms.ios,
|
||||
icon: <Apple className="w-8 h-8" />,
|
||||
status: "dev",
|
||||
color: "text-muted-foreground",
|
||||
shadow: "shadow-none",
|
||||
borderHover: "hover:border-border",
|
||||
},
|
||||
];
|
||||
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} />;
|
||||
}
|
||||
};
|
||||
|
||||
// Явно указываем тип Variants
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
@@ -77,7 +75,6 @@ export function DownloadClient({ dict }: { dict: Dictionary["download"] }) {
|
||||
},
|
||||
};
|
||||
|
||||
// Явно указываем тип Variants
|
||||
const itemVariants: Variants = {
|
||||
hidden: { opacity: 0, y: 40 },
|
||||
show: {
|
||||
@@ -89,89 +86,139 @@ export function DownloadClient({ dict }: { dict: Dictionary["download"] }) {
|
||||
|
||||
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-primary/5 blur-[120px] rounded-full pointer-events-none -z-10" />
|
||||
{/* Мягкое фоновое свечение */}
|
||||
<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-16">
|
||||
<h1 className="text-4xl md:text-5xl font-extrabold tracking-tight mb-4 text-foreground">
|
||||
<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">
|
||||
<p className="text-muted-foreground text-lg max-w-2xl mx-auto font-medium">
|
||||
{dict.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
>
|
||||
{platforms.map((platform) => (
|
||||
<motion.div
|
||||
key={platform.id}
|
||||
variants={itemVariants}
|
||||
className={`bg-card/40 backdrop-blur-xl border border-border/50 rounded-[2rem] p-8 flex flex-col transition-all duration-500 relative overflow-hidden group ${platform.borderHover}`}
|
||||
>
|
||||
{/* Статус бейдж */}
|
||||
<div className="absolute top-6 right-6">
|
||||
{platform.status === "active" ? (
|
||||
<div className="px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-bold uppercase tracking-wider">
|
||||
{dict.badges.active}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-3 py-1 rounded-full bg-muted border border-border text-muted-foreground text-xs font-bold uppercase tracking-wider">
|
||||
{dict.badges.dev}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Иконка */}
|
||||
<div
|
||||
className={`mb-6 p-4 rounded-2xl bg-background/50 inline-flex border border-border/50 ${platform.color} ${platform.shadow} transition-all duration-300`}
|
||||
{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%]" : ""
|
||||
}`}
|
||||
>
|
||||
{platform.icon}
|
||||
</div>
|
||||
{/* Статус бейдж */}
|
||||
<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>
|
||||
|
||||
<h3 className="text-2xl font-bold text-foreground mb-1">
|
||||
{platform.name}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-8">
|
||||
{platform.version}
|
||||
</p>
|
||||
{/* Иконка в Циановом (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="mt-auto flex flex-col gap-3">
|
||||
{platform.status === "active" ? (
|
||||
<>
|
||||
<Button className="w-full rounded-xl font-bold gap-2">
|
||||
<Download className="w-4 h-4" />
|
||||
{dict.buttons.direct}
|
||||
</Button>
|
||||
<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="outline"
|
||||
className="w-full rounded-xl gap-2 border-border/50 bg-background/50 text-muted-foreground hover:text-foreground transition-colors"
|
||||
variant="secondary"
|
||||
disabled
|
||||
className="w-full h-14 rounded-2xl gap-3 opacity-40 cursor-not-allowed bg-muted"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
{platform.id === "android"
|
||||
? dict.buttons.store
|
||||
: dict.buttons.github}
|
||||
<Bell className="size-4" />
|
||||
{dict.buttons.notify}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled
|
||||
className="w-full rounded-xl gap-2 opacity-50 cursor-not-allowed"
|
||||
>
|
||||
<Bell className="w-4 h-4" />
|
||||
{dict.buttons.notify}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@ export default async function DownloadPage({
|
||||
const { lang } = await params;
|
||||
const dict = await getDictionary(lang);
|
||||
|
||||
return <DownloadClient dict={dict.download} />;
|
||||
// Добавили передачу lang
|
||||
return <DownloadClient dict={dict.download} lang={lang} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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} />;
|
||||
}
|
||||
+5
-11
@@ -11,26 +11,20 @@ export default async function Home({
|
||||
}: {
|
||||
params: Promise<{ lang: string }>;
|
||||
}) {
|
||||
// РАЗВОРАЧИВАЕМ PARAMS
|
||||
const { lang } = await params;
|
||||
|
||||
// Получаем словарь, используя проверенную строку
|
||||
const dict = await getDictionary(lang);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hero dict={dict.hero} />
|
||||
{/* Добавили передачу lang в Hero */}
|
||||
<Hero dict={dict.hero} lang={lang} />
|
||||
<Features dict={dict.features} />
|
||||
<Pricing dict={dict.pricing} lang={lang} />
|
||||
<FAQ dict={dict.faq} lang={lang} />
|
||||
|
||||
{
|
||||
<Suspense
|
||||
fallback={<div className="py-20 text-center">Loading...</div>}
|
||||
>
|
||||
<BlogPreview lang={lang} />
|
||||
</Suspense>
|
||||
}
|
||||
<Suspense fallback={<div className="py-20 text-center">Loading...</div>}>
|
||||
<BlogPreview lang={lang} dict={dict.blog} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user