151 lines
7.7 KiB
TypeScript
151 lines
7.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Navbar from "@/components/Navbar";
|
|
|
|
const NODES = [
|
|
{ id: "n1", name: "Void Exchange", onion: "voidexxxxx…onion", category: "Market", status: "up", uptime: "99.1%", last: "12s ago", guard: true },
|
|
{ id: "n2", name: "Phantom Library", onion: "phantomll…onion", category: "Wiki", status: "up", uptime: "97.4%", last: "2m ago", guard: false },
|
|
{ id: "n3", name: "Ghost Relay #7", onion: "ghostrly7…onion", category: "Relay", status: "up", uptime: "99.8%", last: "5s ago", guard: true },
|
|
{ id: "n4", name: "Ember Forum", onion: "emberfrum…onion", category: "Forum", status: "down", uptime: "81.2%", last: "4h ago", guard: false },
|
|
{ id: "n5", name: "Cipher Drop", onion: "ciphdrpxx…onion", category: "Drop", status: "up", uptime: "95.5%", last: "30s ago", guard: false },
|
|
{ id: "n6", name: "Zero Wiki", onion: "zerowikxx…onion", category: "Wiki", status: "degraded", uptime: "91.0%", last: "8m ago", guard: true },
|
|
{ id: "n7", name: "Dark Market", onion: "drkmarket…onion", category: "Market", status: "up", uptime: "98.3%", last: "18s ago", guard: true },
|
|
{ id: "n8", name: "Nexus Gate", onion: "nexusgte…onion", category: "Relay", status: "up", uptime: "99.9%", last: "2s ago", guard: true },
|
|
];
|
|
|
|
const STATUS_COLORS: Record<string, string> = {
|
|
up: "text-neon-green bg-neon-green/10 border-neon-green/30",
|
|
down: "text-red-400 bg-red-500/10 border-red-500/30",
|
|
degraded: "text-yellow-400 bg-yellow-500/10 border-yellow-500/30",
|
|
};
|
|
const STATUS_DOT: Record<string, string> = {
|
|
up: "bg-neon-green",
|
|
down: "bg-red-500",
|
|
degraded: "bg-yellow-400",
|
|
};
|
|
|
|
const CATS = ["All", "Market", "Wiki", "Forum", "Relay", "Drop"];
|
|
|
|
export default function NexusPage() {
|
|
const [cat, setCat] = useState("All");
|
|
const [search, setSearch] = useState("");
|
|
|
|
const filtered = NODES.filter((n) => {
|
|
const matchCat = cat === "All" || n.category === cat;
|
|
const matchSearch = !search || n.name.toLowerCase().includes(search.toLowerCase()) || n.onion.includes(search.toLowerCase());
|
|
return matchCat && matchSearch;
|
|
});
|
|
|
|
const up = NODES.filter((n) => n.status === "up").length;
|
|
const down = NODES.filter((n) => n.status === "down").length;
|
|
const degraded = NODES.filter((n) => n.status === "degraded").length;
|
|
|
|
return (
|
|
<>
|
|
<Navbar />
|
|
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
|
|
<section className="container mx-auto max-w-6xl px-4 mb-10">
|
|
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">nexus · hidden service network tracker</p>
|
|
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">
|
|
THE <span className="text-neon-cyan">NEXUS</span>
|
|
</h1>
|
|
<p className="mt-3 max-w-xl text-foreground/60">
|
|
Real-time monitoring of trusted hidden services across the Tor network. Submit your node to join the monitored ring.
|
|
</p>
|
|
</section>
|
|
|
|
{/* Status overview */}
|
|
<section className="container mx-auto max-w-6xl px-4 mb-8">
|
|
<div className="flex flex-wrap gap-4">
|
|
{[
|
|
{ label: "Online", count: up, color: "neon-green" },
|
|
{ label: "Degraded", count: degraded, color: "yellow-400" },
|
|
{ label: "Offline", count: down, color: "red-400" },
|
|
].map((s) => (
|
|
<div key={s.label} className="flex items-center gap-3 rounded-xl border border-zinc-700/40 bg-black/40 px-5 py-3">
|
|
<span className={`h-2.5 w-2.5 rounded-full bg-${s.color} animate-pulse`} />
|
|
<div>
|
|
<p className="text-xl font-bold font-orbitron">{s.count}</p>
|
|
<p className="text-xs text-foreground/40">{s.label}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Filters */}
|
|
<section className="container mx-auto max-w-6xl px-4 mb-6">
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<input
|
|
className="rounded-lg border border-zinc-700 bg-zinc-900 px-4 py-2 text-sm text-foreground placeholder-foreground/30 w-56 focus:border-neon-cyan/40 outline-none"
|
|
placeholder="Search nodes…"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
/>
|
|
<div className="flex flex-wrap gap-2">
|
|
{CATS.map((c) => (
|
|
<button
|
|
key={c}
|
|
onClick={() => setCat(c)}
|
|
className={`rounded-full px-3 py-1.5 text-xs font-semibold border transition ${cat === c ? "border-neon-cyan/50 bg-neon-cyan/10 text-neon-cyan" : "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"}`}
|
|
>
|
|
{c}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Node table */}
|
|
<section className="container mx-auto max-w-6xl px-4">
|
|
<div className="overflow-hidden rounded-xl border border-zinc-800">
|
|
<table className="w-full text-sm">
|
|
<thead className="border-b border-zinc-800 bg-black/60">
|
|
<tr>
|
|
{["Node", "Category", "Onion", "Status", "Uptime", "Last seen", "Guard"].map((h) => (
|
|
<th key={h} className="px-4 py-3 text-left text-[10px] uppercase tracking-wider text-foreground/40">{h}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filtered.map((node, i) => (
|
|
<tr key={node.id} className={`border-b border-zinc-800/50 ${i % 2 === 0 ? "bg-black/20" : "bg-black/40"} hover:bg-white/5 transition`}>
|
|
<td className="px-4 py-3 font-semibold text-foreground">{node.name}</td>
|
|
<td className="px-4 py-3">
|
|
<span className="rounded-full border border-zinc-600 bg-zinc-800 px-2 py-0.5 text-[9px] uppercase tracking-wider text-zinc-400">{node.category}</span>
|
|
</td>
|
|
<td className="px-4 py-3 font-mono text-xs text-foreground/50">{node.onion}</td>
|
|
<td className="px-4 py-3">
|
|
<span className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-bold ${STATUS_COLORS[node.status]}`}>
|
|
<span className={`h-1.5 w-1.5 rounded-full ${STATUS_DOT[node.status]}`} />
|
|
{node.status}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 font-mono text-xs text-foreground/70">{node.uptime}</td>
|
|
<td className="px-4 py-3 text-xs text-foreground/40">{node.last}</td>
|
|
<td className="px-4 py-3 text-center">{node.guard ? <span className="text-neon-cyan text-base">✓</span> : <span className="text-foreground/20">—</span>}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Submit */}
|
|
<section className="container mx-auto max-w-6xl px-4 mt-12">
|
|
<div className="rounded-2xl border border-neon-cyan/20 bg-neon-cyan/5 p-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<h2 className="font-orbitron text-lg font-bold">Submit your node</h2>
|
|
<p className="text-sm text-foreground/60 mt-1">Add your hidden service to the Nexus monitoring ring. Requires a signed verification message.</p>
|
|
</div>
|
|
<button className="shrink-0 rounded-lg border border-neon-cyan/40 bg-neon-cyan/10 px-5 py-2.5 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/20 transition">
|
|
Submit Node →
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|