added title and seo

This commit is contained in:
Kumaraguru 2025-01-10 15:42:15 +00:00
parent 130cac1280
commit 4a57d8668f
No known key found for this signature in database
GPG Key ID: 4E4555A84ECD28F7
2 changed files with 401 additions and 388 deletions

View File

@ -37,6 +37,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import Head from "next/head";
// Initialize Supabase client
const supabase = createClient(
@ -215,401 +216,413 @@ export default function Dashboard() {
const totalPeerPages = getPageCount(displayPeerIds.length);
return (
<div className="min-h-screen bg-gradient-to-bl from-black to-[#222222] text-white overflow-x-hidden">
{/* Header */}
<motion.header
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="sticky top-0 z-50 backdrop-blur-xl bg-black/50 border-b border-neutral-800"
>
<div className="max-w-[2000px] mx-auto px-4 sm:px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<img src="/logo.svg" alt="Codex" className="w-10 h-10" />
<h1 className="text-lg sm:text-xl font-bold">Testnet Metrics</h1>
</div>
<div className="flex items-center gap-3">
<select
value={timeframe}
onChange={(e) => setTimeframe(e.target.value)}
className="bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-sm font-medium
hover:border-neutral-700 focus:border-[#7afbaf] focus:ring-1 focus:ring-[#7afbaf]
transition-colors cursor-pointer outline-none"
>
<option value="7d">Last 7 Days</option>
<option value="30d">Last 30 Days</option>
<option value="1y">Last Year</option>
</select>
<button
onClick={fetchData}
disabled={loading}
className="p-2 text-neutral-400 hover:text-[#7afbaf] disabled:text-neutral-600
bg-neutral-900 border border-neutral-800 rounded-lg
hover:border-neutral-700 disabled:border-neutral-800 disabled:hover:border-neutral-800
focus:border-[#7afbaf] focus:ring-1 focus:ring-[#7afbaf]
transition-colors cursor-pointer disabled:cursor-not-allowed outline-none"
>
<RotateCw className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} />
<span className="sr-only">Refresh data</span>
</button>
<Dialog>
<DialogTrigger asChild>
<button
className="p-2 text-neutral-400 hover:text-[#7afbaf]
bg-neutral-900 border border-neutral-800 rounded-lg
hover:border-neutral-700 focus:border-[#7afbaf] focus:ring-1
focus:ring-[#7afbaf] transition-colors cursor-pointer outline-none"
>
<Info className="w-5 h-5" />
<span className="sr-only">Dashboard information</span>
</button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="mb-6">Testnet Metrics</DialogTitle>
<div className="w-full h-40 bg-neutral-800 rounded-lg mb-6 animate-pulse" />
<DialogDescription className="pt-3">
The data displayed in this dashboard is collected from Codex nodes that use the{' '}
<a
href="https://github.com/codex-storage/cli"
target="_blank"
rel="noopener noreferrer"
className="text-[#7afbaf] hover:underline"
>
Codex CLI
</a>
{' '}for running a Codex alturistic node in the testnet. Users agree to a privacy
disclaimer before using the Codex CLI and the data collected will be used to
understand the testnet statistics and help troubleshooting users who face
difficulty in getting onboarded to Codex.
</DialogDescription>
<div className="mt-8 space-y-4 border-neutral-800 pt-4">
<div>
<h4 className="text-sm font-semibold text-white mb-2">Don't wish to provide data?</h4>
<p className="text-sm text-neutral-400">
You can still run a Codex node without providing any data. To do this, please follow the steps mentioned in the <a
href="https://docs.codex.storage/"
target="_blank"
rel="noopener noreferrer"
className="text-[#7afbaf] hover:underline"
>
Codex documentation
</a> which does not use the Codex CLI.
</p>
</div>
<div>
<h4 className="text-sm font-semibold text-white mb-2">Is there an incentive to run a Codex node?</h4>
<p className="text-sm text-neutral-400">
Codex is currently in testnet and it is not incentivized. However, in the future, Codex may be incentivized as per the roadmap. But please bear in mind that no incentives are promised for testnet node operators.
</p>
</div>
<div>
<h4 className="text-sm font-semibold text-white mb-2">I have a question or suggestion</h4>
<p className="text-sm text-neutral-400">
The best way to get in touch with us is to join the
<a
href="https://discord.gg/codex-storage"
target="_blank"
rel="noopener noreferrer"
className="text-[#7afbaf] hover:underline"
>
{" "}Codex discord
</a> and ask your question in the #support channel.
</p>
</div>
</div>
</DialogHeader>
</DialogContent>
</Dialog>
</div>
</div>
</motion.header>
<>
<Head>
<title>Codex Metrics</title>
<meta name="description" content="Real-time metrics dashboard for Codex testnet nodes, displaying network statistics, version distribution, and geographic data." />
<meta property="og:title" content="Codex Metrics" />
<meta property="og:description" content="Real-time metrics dashboard for Codex testnet nodes, displaying network statistics, version distribution, and geographic data." />
<meta property="og:type" content="website" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/logo.svg" />
</Head>
<main className="max-w-[2000px] mx-auto px-4 sm:px-6 py-4 sm:py-6">
{error ? (
<ErrorState message={error} />
) : (
<div className="space-y-4 sm:space-y-6">
{/* Top Section: Stats + Graph */}
<div className="grid gap-4 sm:gap-6 lg:grid-cols-2">
{/* Left Column - Stats Cards */}
<div className="grid grid-cols-2 lg:grid-cols-1 lg:grid-rows-4 gap-4 lg:h-[450px]">
{[
{
title: "Active Nodes",
value: currentActiveNodes,
Icon: Users,
delay: 0,
isLoading: componentLoading.nodes,
},
{
title: "Average Peer Count",
value: averagePeerCount,
Icon: Network,
delay: 0.1,
isLoading: componentLoading.nodes,
},
{
title: "Total Nodes",
value: totalNodes,
Icon: Database,
delay: 0.2,
isLoading: componentLoading.metrics,
},
{
title: "Last Updated",
value: lastUpdated,
Icon: Clock,
delay: 0.3,
isLoading: componentLoading.nodes,
},
].map((stat) => (
<motion.div
key={stat.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: stat.delay }}
className="bg-neutral-900 p-4 sm:p-5 rounded-xl hover:bg-neutral-900/80
transition-colors border border-neutral-800 hover:border-neutral-700
flex flex-col justify-between h-full"
>
<h3 className="text-neutral-400 text-sm font-medium flex items-center gap-2 mb-3">
<stat.Icon className="w-4 h-4 opacity-60" />
{stat.title}
</h3>
<div className="mt-auto">
{stat.isLoading ? (
<div className="h-8 bg-neutral-800/50 rounded animate-pulse" />
) : stat.title === "Last Updated" ? (
<div className="flex items-baseline gap-2">
<span className="text-lg sm:text-xl lg:text-2xl font-bold text-[#7afbaf] tracking-tight">
{lastUpdated.time}
</span>
<span className="text-sm font-medium text-[#7afbaf] opacity-70">
{lastUpdated.dateText}
</span>
</div>
) : (
<p className="text-lg sm:text-xl lg:text-2xl font-bold text-[#7afbaf] tracking-tight">
{stat.value}
</p>
)}
</div>
</motion.div>
))}
</div>
{/* Right Column - Chart */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="bg-neutral-900 p-4 sm:p-6 rounded-xl h-[350px] lg:h-[450px] border border-neutral-800
hover:border-neutral-700 transition-colors"
>
<Tabs defaultValue="nodes" className="h-full flex flex-col">
<div className="flex items-center justify-center mb-6">
<TabsList className="bg-neutral-800 border border-neutral-700">
<TabsTrigger value="nodes" className="data-[state=active]:bg-neutral-900">
<Activity className="w-4 h-4 mr-2" />
Active Nodes
</TabsTrigger>
<TabsTrigger value="geo" className="data-[state=active]:bg-neutral-900">
<Globe className="w-4 h-4 mr-2" />
Geographic Distribution
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="nodes" className="flex-1 mt-0">
{componentLoading.metrics ? (
<div className="h-full flex items-center justify-center">
<ComponentSkeleton />
</div>
) : metrics.length === 0 ? (
<div className="h-full flex items-center justify-center">
<p className="text-neutral-400">No data available for the selected timeframe</p>
</div>
) : (
<div className="h-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={metrics}>
<CartesianGrid
strokeDasharray="3 3"
stroke="#333"
vertical={false}
/>
<XAxis
dataKey="date"
stroke="#666"
tickFormatter={(date) => format(new Date(date), "MMM d")}
fontSize={12}
tickMargin={10}
/>
<YAxis
stroke="#666"
fontSize={12}
tickMargin={10}
axisLine={false}
/>
<Tooltip
contentStyle={{
backgroundColor: "#1a1a1a",
border: "1px solid #333",
borderRadius: "8px",
fontFamily: "var(--font-inter)",
fontSize: "12px",
padding: "12px",
}}
cursor={{ stroke: "#666" }}
formatter={(value) => [`${value} nodes`, 'Active Nodes']}
labelFormatter={(label) => format(new Date(label), "MMM d, yyyy")}
/>
<Line
type="monotone"
dataKey="new_records_count"
stroke="#7afbaf"
strokeWidth={2}
dot={false}
activeDot={{ r: 6, fill: "#7afbaf" }}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
</TabsContent>
<TabsContent value="geo" className="flex-1 mt-0">
<div className="h-full flex items-center justify-center">
<p className="text-neutral-400">Geographic distribution view coming soon</p>
</div>
</TabsContent>
</Tabs>
</motion.div>
<div className="min-h-screen bg-gradient-to-bl from-black to-[#222222] text-white overflow-x-hidden">
{/* Header */}
<motion.header
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="sticky top-0 z-50 backdrop-blur-xl bg-black/50 border-b border-neutral-800"
>
<div className="max-w-[2000px] mx-auto px-4 sm:px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<img src="/logo.svg" alt="Codex" className="w-10 h-10" />
<h1 className="text-lg sm:text-xl font-bold">Testnet Metrics</h1>
</div>
{/* Bottom Section: Version Distribution + Active Peers */}
<div className="grid gap-4 sm:gap-6 lg:grid-cols-2">
{/* Version Distribution */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="bg-neutral-900 p-4 sm:p-6 rounded-xl border border-neutral-800
hover:border-neutral-700 transition-colors h-[300px] lg:h-[350px] flex flex-col"
<div className="flex items-center gap-3">
<select
value={timeframe}
onChange={(e) => setTimeframe(e.target.value)}
className="bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-sm font-medium
hover:border-neutral-700 focus:border-[#7afbaf] focus:ring-1 focus:ring-[#7afbaf]
transition-colors cursor-pointer outline-none"
>
<h3 className="text-neutral-400 mb-4 sm:mb-6 font-medium flex items-center gap-2">
<Database className="w-5 h-5 opacity-60" />
Version Distribution
</h3>
{componentLoading.versions ? (
<ComponentSkeleton />
) : Object.keys(versionDistribution).length === 0 ? (
<div className="flex items-center justify-center flex-1">
<p className="text-neutral-400">No version data available</p>
</div>
) : (
<>
<div className="space-y-4 overflow-y-auto flex-1 pr-2 scrollbar-thin
scrollbar-thumb-neutral-700 scrollbar-track-neutral-800">
{paginatedVersions.map(([version, count]) => (
<div key={version}>
<div className="flex justify-between mb-2">
<span className="font-medium text-sm sm:text-base">{version}</span>
<span className="font-medium text-sm sm:text-base text-[#7afbaf]">
{count}
<option value="7d">Last 7 Days</option>
<option value="30d">Last 30 Days</option>
<option value="1y">Last Year</option>
</select>
<button
onClick={fetchData}
disabled={loading}
className="p-2 text-neutral-400 hover:text-[#7afbaf] disabled:text-neutral-600
bg-neutral-900 border border-neutral-800 rounded-lg
hover:border-neutral-700 disabled:border-neutral-800 disabled:hover:border-neutral-800
focus:border-[#7afbaf] focus:ring-1 focus:ring-[#7afbaf]
transition-colors cursor-pointer disabled:cursor-not-allowed outline-none"
>
<RotateCw className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} />
<span className="sr-only">Refresh data</span>
</button>
<Dialog>
<DialogTrigger asChild>
<button
className="p-2 text-neutral-400 hover:text-[#7afbaf]
bg-neutral-900 border border-neutral-800 rounded-lg
hover:border-neutral-700 focus:border-[#7afbaf] focus:ring-1
focus:ring-[#7afbaf] transition-colors cursor-pointer outline-none"
>
<Info className="w-5 h-5" />
<span className="sr-only">Dashboard information</span>
</button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="mb-6">Testnet Metrics</DialogTitle>
<div className="w-full h-40 bg-neutral-800 rounded-lg mb-6 animate-pulse" />
<DialogDescription className="pt-3">
The data displayed in this dashboard is collected from Codex nodes that use the{' '}
<a
href="https://github.com/codex-storage/cli"
target="_blank"
rel="noopener noreferrer"
className="text-[#7afbaf] hover:underline"
>
Codex CLI
</a>
{' '}for running a Codex alturistic node in the testnet. Users agree to a privacy
disclaimer before using the Codex CLI and the data collected will be used to
understand the testnet statistics and help troubleshooting users who face
difficulty in getting onboarded to Codex.
</DialogDescription>
<div className="mt-8 space-y-4 border-neutral-800 pt-4">
<div>
<h4 className="text-sm font-semibold text-white mb-2">Don't wish to provide data?</h4>
<p className="text-sm text-neutral-400">
You can still run a Codex node without providing any data. To do this, please follow the steps mentioned in the <a
href="https://docs.codex.storage/"
target="_blank"
rel="noopener noreferrer"
className="text-[#7afbaf] hover:underline"
>
Codex documentation
</a> which does not use the Codex CLI.
</p>
</div>
<div>
<h4 className="text-sm font-semibold text-white mb-2">Is there an incentive to run a Codex node?</h4>
<p className="text-sm text-neutral-400">
Codex is currently in testnet and it is not incentivized. However, in the future, Codex may be incentivized as per the roadmap. But please bear in mind that no incentives are promised for testnet node operators.
</p>
</div>
<div>
<h4 className="text-sm font-semibold text-white mb-2">I have a question or suggestion</h4>
<p className="text-sm text-neutral-400">
The best way to get in touch with us is to join the
<a
href="https://discord.gg/codex-storage"
target="_blank"
rel="noopener noreferrer"
className="text-[#7afbaf] hover:underline"
>
{" "}Codex discord
</a> and ask your question in the #support channel.
</p>
</div>
</div>
</DialogHeader>
</DialogContent>
</Dialog>
</div>
</div>
</motion.header>
<main className="max-w-[2000px] mx-auto px-4 sm:px-6 py-4 sm:py-6">
{error ? (
<ErrorState message={error} />
) : (
<div className="space-y-4 sm:space-y-6">
{/* Top Section: Stats + Graph */}
<div className="grid gap-4 sm:gap-6 lg:grid-cols-2">
{/* Left Column - Stats Cards */}
<div className="grid grid-cols-2 lg:grid-cols-1 lg:grid-rows-4 gap-4 lg:h-[450px]">
{[
{
title: "Active Nodes",
value: currentActiveNodes,
Icon: Users,
delay: 0,
isLoading: componentLoading.nodes,
},
{
title: "Average Peer Count",
value: averagePeerCount,
Icon: Network,
delay: 0.1,
isLoading: componentLoading.nodes,
},
{
title: "Total Nodes",
value: totalNodes,
Icon: Database,
delay: 0.2,
isLoading: componentLoading.metrics,
},
{
title: "Last Updated",
value: lastUpdated,
Icon: Clock,
delay: 0.3,
isLoading: componentLoading.nodes,
},
].map((stat) => (
<motion.div
key={stat.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: stat.delay }}
className="bg-neutral-900 p-4 sm:p-5 rounded-xl hover:bg-neutral-900/80
transition-colors border border-neutral-800 hover:border-neutral-700
flex flex-col justify-between h-full"
>
<h3 className="text-neutral-400 text-sm font-medium flex items-center gap-2 mb-3">
<stat.Icon className="w-4 h-4 opacity-60" />
{stat.title}
</h3>
<div className="mt-auto">
{stat.isLoading ? (
<div className="h-8 bg-neutral-800/50 rounded animate-pulse" />
) : stat.title === "Last Updated" ? (
<div className="flex items-baseline gap-2">
<span className="text-lg sm:text-xl lg:text-2xl font-bold text-[#7afbaf] tracking-tight">
{lastUpdated.time}
</span>
<span className="text-sm font-medium text-[#7afbaf] opacity-70">
{lastUpdated.dateText}
</span>
</div>
<div className="w-full bg-neutral-800 rounded-full h-2 overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${(count / currentActiveNodes) * 100}%` }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="bg-[#7afbaf] h-2 rounded-full"
/>
</div>
</div>
))}
</div>
<PaginationControls
currentPage={versionPage}
totalPages={totalVersionPages}
onPageChange={setVersionPage}
className="mt-4 pt-4 border-t border-neutral-800"
/>
</>
)}
</motion.div>
{/* Active Peer IDs List */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="bg-neutral-900 p-4 sm:p-6 rounded-xl border border-neutral-800
hover:border-neutral-700 transition-colors h-[300px] lg:h-[350px] flex flex-col"
>
<div className="flex flex-col gap-4 sm:gap-6">
<h3 className="text-neutral-400 font-medium flex items-center gap-2">
<Network className="w-5 h-5 opacity-60" />
Active Peer IDs
</h3>
<div className="relative">
<input
type="text"
placeholder="Search peer IDs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-neutral-800 border border-neutral-700 rounded-lg px-4 py-2
text-sm placeholder-neutral-500 focus:border-[#7afbaf] focus:ring-1
focus:ring-[#7afbaf] transition-colors outline-none"
/>
<Search className="w-4 h-4 text-neutral-500 absolute right-3 top-1/2 -translate-y-1/2" />
</div>
) : (
<p className="text-lg sm:text-xl lg:text-2xl font-bold text-[#7afbaf] tracking-tight">
{stat.value}
</p>
)}
</div>
</motion.div>
))}
</div>
{componentLoading.peers ? (
<ComponentSkeleton className="mt-4" />
) : activePeerIds.length === 0 ? (
<div className="flex items-center justify-center flex-1">
<p className="text-neutral-400">No active peers available</p>
</div>
) : isSearching ? (
<div className="flex items-center justify-center flex-1">
<RotateCw className="w-6 h-6 text-neutral-400 animate-spin" />
</div>
) : searchQuery && searchResults.length === 0 ? (
<div className="flex items-center justify-center flex-1">
<p className="text-neutral-400">No matching peer IDs found</p>
</div>
) : (
<>
<div className="space-y-2 overflow-y-auto flex-1 pr-2 mt-4 scrollbar-thin
scrollbar-thumb-neutral-700 scrollbar-track-neutral-800">
{paginatedPeerIds.map((peerId, index) => (
<motion.div
key={peerId}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 * (index % 5) }}
className="bg-neutral-800 p-3 rounded-lg text-xs sm:text-sm font-medium
break-all hover:bg-neutral-700/50 transition-colors"
>
{peerId}
</motion.div>
))}
{/* Right Column - Chart */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="bg-neutral-900 p-4 sm:p-6 rounded-xl h-[350px] lg:h-[450px] border border-neutral-800
hover:border-neutral-700 transition-colors"
>
<Tabs defaultValue="nodes" className="h-full flex flex-col">
<div className="flex items-center justify-center mb-6">
<TabsList className="bg-neutral-800 border border-neutral-700">
<TabsTrigger value="nodes" className="data-[state=active]:bg-neutral-900">
<Activity className="w-4 h-4 mr-2" />
Active Nodes
</TabsTrigger>
<TabsTrigger value="geo" className="data-[state=active]:bg-neutral-900">
<Globe className="w-4 h-4 mr-2" />
Geographic Distribution
</TabsTrigger>
</TabsList>
</div>
<PaginationControls
currentPage={peerPage}
totalPages={totalPeerPages}
onPageChange={setPeerPage}
className="mt-4 pt-4 border-t border-neutral-800"
/>
</>
)}
</motion.div>
<TabsContent value="nodes" className="flex-1 mt-0">
{componentLoading.metrics ? (
<div className="h-full flex items-center justify-center">
<ComponentSkeleton />
</div>
) : metrics.length === 0 ? (
<div className="h-full flex items-center justify-center">
<p className="text-neutral-400">No data available for the selected timeframe</p>
</div>
) : (
<div className="h-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={metrics}>
<CartesianGrid
strokeDasharray="3 3"
stroke="#333"
vertical={false}
/>
<XAxis
dataKey="date"
stroke="#666"
tickFormatter={(date) => format(new Date(date), "MMM d")}
fontSize={12}
tickMargin={10}
/>
<YAxis
stroke="#666"
fontSize={12}
tickMargin={10}
axisLine={false}
/>
<Tooltip
contentStyle={{
backgroundColor: "#1a1a1a",
border: "1px solid #333",
borderRadius: "8px",
fontFamily: "var(--font-inter)",
fontSize: "12px",
padding: "12px",
}}
cursor={{ stroke: "#666" }}
formatter={(value) => [`${value} nodes`, 'Active Nodes']}
labelFormatter={(label) => format(new Date(label), "MMM d, yyyy")}
/>
<Line
type="monotone"
dataKey="new_records_count"
stroke="#7afbaf"
strokeWidth={2}
dot={false}
activeDot={{ r: 6, fill: "#7afbaf" }}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
</TabsContent>
<TabsContent value="geo" className="flex-1 mt-0">
<div className="h-full flex items-center justify-center">
<p className="text-neutral-400">Geographic distribution view coming soon</p>
</div>
</TabsContent>
</Tabs>
</motion.div>
</div>
{/* Bottom Section: Version Distribution + Active Peers */}
<div className="grid gap-4 sm:gap-6 lg:grid-cols-2">
{/* Version Distribution */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="bg-neutral-900 p-4 sm:p-6 rounded-xl border border-neutral-800
hover:border-neutral-700 transition-colors h-[300px] lg:h-[350px] flex flex-col"
>
<h3 className="text-neutral-400 mb-4 sm:mb-6 font-medium flex items-center gap-2">
<Database className="w-5 h-5 opacity-60" />
Version Distribution
</h3>
{componentLoading.versions ? (
<ComponentSkeleton />
) : Object.keys(versionDistribution).length === 0 ? (
<div className="flex items-center justify-center flex-1">
<p className="text-neutral-400">No version data available</p>
</div>
) : (
<>
<div className="space-y-4 overflow-y-auto flex-1 pr-2 scrollbar-thin
scrollbar-thumb-neutral-700 scrollbar-track-neutral-800">
{paginatedVersions.map(([version, count]) => (
<div key={version}>
<div className="flex justify-between mb-2">
<span className="font-medium text-sm sm:text-base">{version}</span>
<span className="font-medium text-sm sm:text-base text-[#7afbaf]">
{count}
</span>
</div>
<div className="w-full bg-neutral-800 rounded-full h-2 overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${(count / currentActiveNodes) * 100}%` }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="bg-[#7afbaf] h-2 rounded-full"
/>
</div>
</div>
))}
</div>
<PaginationControls
currentPage={versionPage}
totalPages={totalVersionPages}
onPageChange={setVersionPage}
className="mt-4 pt-4 border-t border-neutral-800"
/>
</>
)}
</motion.div>
{/* Active Peer IDs List */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="bg-neutral-900 p-4 sm:p-6 rounded-xl border border-neutral-800
hover:border-neutral-700 transition-colors h-[300px] lg:h-[350px] flex flex-col"
>
<div className="flex flex-col gap-4 sm:gap-6">
<h3 className="text-neutral-400 font-medium flex items-center gap-2">
<Network className="w-5 h-5 opacity-60" />
Active Peer IDs
</h3>
<div className="relative">
<input
type="text"
placeholder="Search peer IDs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-neutral-800 border border-neutral-700 rounded-lg px-4 py-2
text-sm placeholder-neutral-500 focus:border-[#7afbaf] focus:ring-1
focus:ring-[#7afbaf] transition-colors outline-none"
/>
<Search className="w-4 h-4 text-neutral-500 absolute right-3 top-1/2 -translate-y-1/2" />
</div>
</div>
{componentLoading.peers ? (
<ComponentSkeleton className="mt-4" />
) : activePeerIds.length === 0 ? (
<div className="flex items-center justify-center flex-1">
<p className="text-neutral-400">No active peers available</p>
</div>
) : isSearching ? (
<div className="flex items-center justify-center flex-1">
<RotateCw className="w-6 h-6 text-neutral-400 animate-spin" />
</div>
) : searchQuery && searchResults.length === 0 ? (
<div className="flex items-center justify-center flex-1">
<p className="text-neutral-400">No matching peer IDs found</p>
</div>
) : (
<>
<div className="space-y-2 overflow-y-auto flex-1 pr-2 mt-4 scrollbar-thin
scrollbar-thumb-neutral-700 scrollbar-track-neutral-800">
{paginatedPeerIds.map((peerId, index) => (
<motion.div
key={peerId}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 * (index % 5) }}
className="bg-neutral-800 p-3 rounded-lg text-xs sm:text-sm font-medium
break-all hover:bg-neutral-700/50 transition-colors"
>
{peerId}
</motion.div>
))}
</div>
<PaginationControls
currentPage={peerPage}
totalPages={totalPeerPages}
onPageChange={setPeerPage}
className="mt-4 pt-4 border-t border-neutral-800"
/>
</>
)}
</motion.div>
</div>
</div>
</div>
)}
</main>
</div>
)}
</main>
</div>
</>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB