design fixes, tables seperate by language, loading and 404 pages, downloads from cms

This commit is contained in:
Трапезников Кирилл Иванович
2026-04-19 16:05:05 +10:00
parent cb4ae3bc25
commit 7acc40ec46
27 changed files with 1260 additions and 727 deletions
+202
View File
@@ -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>
);
}
+13
View File
@@ -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} />;
}