Files
netrunner-landing/app/[lang]/nodes/nodes-client.tsx
T
2026-07-02 23:13:24 +07:00

210 lines
7.1 KiB
TypeScript
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.
"use client";
import { motion } from "framer-motion";
import { Activity, Globe2, Server } from "lucide-react";
import { Dictionary } from "@/lib/IDict";
// Моковые данные серверов. В отличие от остальных разделов (blog/docs/download/pricing/faq),
// список нод пока не подключён к PocketBase или бэкенду — статус/пинг/загрузка захардкожены.
// При появлении реального API нод (вероятно, через proxy-ws) этот массив нужно заменить
// на клиентский fetch по аналогии с download-client.tsx / docs-client.tsx.
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",
},
];
/**
* Список VPN-серверов (нод) с индикацией статуса (online/full/maintenance),
* загрузки и пинга. См. предупреждение выше — данные пока моковые.
*/
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={`cursor-net-move 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>
);
}