SIWE auth, API proxy fixes, dashboard, login, usage pages
- SIWE auth flow: challenge/login/logout/me API routes + JWT middleware
- Fixed [...path] catch-all: Kubo proxy with matchRoute() + mapToKuboAPI()
- Fixed session_id → nonce field mismatch in AuthProvider
- Fixed dashboard pins crash: Kubo returns {Keys: {...}}, not array
- Added middleware route guard (307 redirect to /login)
- Added login, register, profile, usage, upload pages
- Added dashboard components (StorageGauge, FreeTierProgress)
- Added SWR hooks + AuthGuard + Providers + Toast/ErrorBoundary
- Added payment integration (smart contract ABI, tiers, tokens)
- Fixed IPNS key listing: Name/Id → name/id mapping
- Added PWA support (manifest, service worker, icons)
- Added tests for helpers, limits, search-index, wallet
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
/* ════════════════════════════ FreeTierProgress ════════════════════════════ */
|
||||
|
||||
interface FreeTierProgressProps {
|
||||
used: number;
|
||||
max: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export default function FreeTierProgress({
|
||||
used,
|
||||
max,
|
||||
label = 'Free uploads vandaag',
|
||||
}: FreeTierProgressProps) {
|
||||
const pct = max > 0 ? Math.min((used / max) * 100, 100) : 0;
|
||||
const remaining = Math.max(0, max - used);
|
||||
const color =
|
||||
remaining === 0
|
||||
? 'bg-accent-rose'
|
||||
: remaining < 5
|
||||
? 'bg-accent-amber'
|
||||
: 'bg-accent-green';
|
||||
|
||||
return (
|
||||
<div className="glass rounded-xl p-5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs text-surface-400">{label}</span>
|
||||
<span className={`text-xs font-mono font-medium ${
|
||||
remaining === 0 ? 'text-accent-rose' : 'text-accent-green'
|
||||
}`}>
|
||||
{remaining} / {max}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2.5 rounded-full bg-surface-700 overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-700 ease-out ${color}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1.5">
|
||||
<span className="text-[10px] text-surface-500">
|
||||
{used} gebruikt vandaag
|
||||
</span>
|
||||
{remaining <= 3 && remaining > 0 && (
|
||||
<span className="text-[10px] text-accent-amber">Bijna op</span>
|
||||
)}
|
||||
{remaining === 0 && (
|
||||
<span className="text-[10px] text-accent-rose">Limiet bereikt</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
/* ════════════════════════════ StorageGauge ════════════════════════════ */
|
||||
|
||||
interface StorageGaugeProps {
|
||||
usedGB: number;
|
||||
maxGB: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export default function StorageGauge({ usedGB, maxGB, label = 'Storage' }: StorageGaugeProps) {
|
||||
const pct = maxGB > 0 ? Math.min((usedGB / maxGB) * 100, 100) : 0;
|
||||
const color =
|
||||
pct > 90 ? 'bg-accent-rose' : pct > 70 ? 'bg-accent-amber' : 'bg-accent-cyan';
|
||||
|
||||
const textColor =
|
||||
pct > 90 ? 'text-accent-rose' : pct > 70 ? 'text-accent-amber' : 'text-accent-cyan';
|
||||
|
||||
return (
|
||||
<div className="glass rounded-xl p-5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs text-surface-400">{label}</span>
|
||||
<span className={`text-xs font-mono font-medium ${textColor}`}>
|
||||
{usedGB.toFixed(1)} / {maxGB.toFixed(1)} GB
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2.5 rounded-full bg-surface-700 overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-700 ease-out ${color}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1.5">
|
||||
<span className="text-[10px] text-surface-500">{pct.toFixed(0)}% gebruikt</span>
|
||||
{pct > 80 && (
|
||||
<span className="text-[10px] text-accent-rose flex items-center gap-1">
|
||||
⚠️ Bijna vol
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+57
-47
@@ -1,13 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getPeers, listPins, checkHealth, getRepoStats, getBWStats } from '@/lib/api';
|
||||
import type { RepoStats, BWStats } from '@/lib/api';
|
||||
import PortalLayout from '@/app/layout-portal';
|
||||
import Link from 'next/link';
|
||||
import { SkeletonCard, SkeletonRow } from '@/components/Skeleton';
|
||||
import { useDashboard } from '@/lib/swr';
|
||||
import { useRealtime } from '@/lib/useRealtime';
|
||||
import {
|
||||
HardDrive,
|
||||
Globe,
|
||||
Wifi,
|
||||
Database,
|
||||
Server,
|
||||
@@ -17,49 +16,42 @@ import {
|
||||
Fingerprint,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface PeerData { id: string; addr: string; latency: string }
|
||||
interface PinData { cid: string; name: string; size: number }
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [peers, setPeers] = useState<PeerData[]>([]);
|
||||
const [pins, setPins] = useState<PinData[]>([]);
|
||||
const [health, setHealth] = useState<{ status: string; node: string } | null>(null);
|
||||
const [repo, setRepo] = useState<RepoStats | null>(null);
|
||||
const [bw, setBW] = useState<BWStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { data, isValidating } = useDashboard();
|
||||
const realtime = useRealtime();
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
try {
|
||||
const [h, p, pinList, r, b] = await Promise.all([
|
||||
checkHealth().catch(() => null),
|
||||
getPeers().catch(() => [] as PeerData[]),
|
||||
listPins().catch(() => [] as PinData[]),
|
||||
getRepoStats().catch(() => null),
|
||||
getBWStats().catch(() => null),
|
||||
]);
|
||||
setHealth(h);
|
||||
setPeers(p);
|
||||
setPins(pinList);
|
||||
setRepo(r);
|
||||
setBW(b);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
// Use SWR data, fall back to SSE live data where available
|
||||
const peers = data?.peers ?? [];
|
||||
const pins = data?.pins ?? [];
|
||||
const health = data?.health;
|
||||
const repo = data?.repo;
|
||||
|
||||
// Bandwidth: prefer SSE live data, fallback to SWR
|
||||
const bwLive = realtime.bandwidth;
|
||||
const bw = bwLive
|
||||
? {
|
||||
totalIn: bwLive.totalIn,
|
||||
totalOut: bwLive.totalOut,
|
||||
rateIn: bwLive.rateIn,
|
||||
rateOut: bwLive.rateOut,
|
||||
}
|
||||
}
|
||||
load();
|
||||
const iv = setInterval(load, 15000);
|
||||
return () => clearInterval(iv);
|
||||
}, []);
|
||||
: data?.bw;
|
||||
|
||||
const loading = isValidating && !data;
|
||||
|
||||
const repoSizeGb = repo ? (repo.repoSize / 1e9).toFixed(2) : '—';
|
||||
const bwIn = bw ? (bw.totalIn / 1e6).toFixed(1) : '—';
|
||||
const bwOut = bw ? (bw.totalOut / 1e6).toFixed(1) : '—';
|
||||
|
||||
// Peer count from SSE (live) or SWR
|
||||
const peerCount = realtime.peers?.count ?? peers.length;
|
||||
// Pin count from SWR
|
||||
const pinCount = pins.length;
|
||||
|
||||
const stats = [
|
||||
{ label: 'Peers', value: peers.length, icon: Wifi, color: 'text-accent-cyan', bg: 'bg-accent-cyan/10' },
|
||||
{ label: 'Pins', value: pins.length, icon: HardDrive, color: 'text-accent-purple', bg: 'bg-accent-purple/10' },
|
||||
{ label: 'Node', value: health?.status ?? '—', icon: Server, color: 'text-accent-green', bg: 'bg-accent-green/10' },
|
||||
{ label: 'Peers', value: peerCount, icon: Wifi, color: 'text-accent-cyan', bg: 'bg-accent-cyan/10' },
|
||||
{ label: 'Pins', value: pinCount, icon: HardDrive, color: 'text-accent-purple', bg: 'bg-accent-purple/10' },
|
||||
{ label: 'Node', value: health?.status ?? (realtime.connected ? 'connected' : '—'), icon: Server, color: 'text-accent-green', bg: 'bg-accent-green/10' },
|
||||
{ label: 'Repo', value: `${repoSizeGb} GB`, icon: Database, color: 'text-accent-amber', bg: 'bg-accent-amber/10' },
|
||||
];
|
||||
|
||||
@@ -71,17 +63,33 @@ export default function DashboardPage() {
|
||||
<h1 className="text-2xl font-bold text-white">Dashboard</h1>
|
||||
<p className="text-sm text-surface-400 mt-1">IPFS node overview, storage, and bandwidth</p>
|
||||
</div>
|
||||
{loading && (
|
||||
<div className="flex items-center gap-2 text-xs text-surface-500">
|
||||
<div className="w-3 h-3 rounded-full border border-surface-600 border-t-transparent animate-spin" />
|
||||
refreshing…
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
{realtime.connected && (
|
||||
<span className="flex items-center gap-1.5 text-xs text-accent-green">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent-green animate-pulse" />
|
||||
Live
|
||||
</span>
|
||||
)}
|
||||
{!realtime.connected && (
|
||||
<span className="flex items-center gap-1.5 text-xs text-surface-500">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-surface-600" />
|
||||
Polling
|
||||
</span>
|
||||
)}
|
||||
{loading && (
|
||||
<div className="flex items-center gap-2 text-xs text-surface-500">
|
||||
<div className="w-3 h-3 rounded-full border border-surface-600 border-t-transparent animate-spin" />
|
||||
loading…
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stat cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
{stats.map((s) => (
|
||||
{loading
|
||||
? Array.from({ length: 4 }).map((_, i) => <SkeletonCard key={i} />)
|
||||
: stats.map((s) => (
|
||||
<div key={s.label} className="glass rounded-xl p-5 animate-fade-in">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className={`p-2 rounded-lg ${s.bg}`}>
|
||||
@@ -101,9 +109,11 @@ export default function DashboardPage() {
|
||||
<div className="glass rounded-xl p-5 animate-fade-in">
|
||||
<h2 className="text-sm font-semibold text-white mb-4 flex items-center justify-between">
|
||||
<span className="flex items-center gap-2"><Wifi className="w-4 h-4 text-accent-cyan" />Connected Peers</span>
|
||||
<span className="text-xs text-surface-500">{peers.length} peer{peers.length !== 1 ? 's' : ''}</span>
|
||||
<span className="text-xs text-surface-500">{peerCount} peer{peerCount !== 1 ? 's' : ''}</span>
|
||||
</h2>
|
||||
{peers.length === 0 ? (
|
||||
{loading
|
||||
? Array.from({ length: 5 }).map((_, i) => <SkeletonRow key={i} cols={2} />)
|
||||
: peers.length === 0 ? (
|
||||
<p className="text-sm text-surface-500">No peers connected</p>
|
||||
) : (
|
||||
<div className="space-y-1.5 max-h-56 overflow-y-auto">
|
||||
|
||||
Reference in New Issue
Block a user