IPFS Portal · Verbeterplan

14 verbeteringen in 3 fasen — met flows, codevoorbeelden en architectuur

📦 IPFS Portal ⚡ Fase 1: ~2 dagen 🎯 14 items
📋 Scope

Het IPFS Portal heeft stevige basis: 11 pagina's, Kubo proxy, smart contract integratie, upload/history systeem. Maar mist gebruikersauth, rate limiting, notificaties, en veel UX polish. Dit plan pakt dat aan.

FaseFocusItemsDuur
Fase 1 Auth, limieten, notificaties 1–4 ~2 dagen
Fase 2 UX verbeteringen 5–9 ~1.5 dag
Fase 3 Technische diepgang 10–14 ~2 dagen
Fase 1

Auth · Limieten · Notificaties

~2 dagen
🔐

1. Login / Sessie Management

Inlogpagina + AuthContext + route bescherming
Critical
Huidig

Geen login. Iedereen met de URL heeft volledige toegang tot uploaden, admin, users beheren.

Doel

Alleen ingelogde gebruikers hebben toegang. Admin-rol voor user management.

Data flow

/login POST /api/auth/login
{username, password}
proxyUserAPI
/auth/login
JWT token
→ cookie
redirect /dashboard

Nieuwe bestanden

  • src/app/login/page.tsx — Login formulier met username/password
  • src/lib/auth.tsx — AuthContext + AuthProvider + useAuth() hook
  • src/app/register/page.tsx — Registratie formulier
  • src/app/layout.tsx — Wrap met AuthProvider
  • src/app/api/[...path]/route.ts — Voeg login/register/auth-check routes toe
  • src/components/AuthGuard.tsx — Redirect niet-ingelogde users naar /login

Code sketch — AuthContext

// src/lib/auth.tsx
import { createContext, useContext, useState, useEffect } from 'react';

interface AuthState {
  user: string | null;
  role: 'admin' | 'user' | null;
  login: (username: string, password: string) => Promise<void>;
  logout: () => void;
  loading: boolean;
}

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);

  // Check session on mount
  useEffect(() => {
    fetch('/api/auth/me')
      .then(r => r.ok ? r.json() : null)
      .then(d => setUser(d?.username ?? null))
      .finally(() => setLoading(false));
  }, []);

  async function login(username: string, password: string) {
    const res = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify({ username, password }),
    });
    if (!res.ok) throw new Error('Login failed');
    const data = await res.json();
    setUser(data.username);
  }

  return (
    <AuthContext.Provider value={{ user, loading, login, logout: () => { ... } }}>
      {children}
    </AuthContext.Provider>
  );
}
📊

2. Usage Limits per User

Echte opslaglimieten + free tier quota
Critical
Huidig

Settings heeft storageMax maar zegt "Applied server-side" — er is geen server-side check. 1 user kan onbeperkt uploaden.

Doel

Per user/wallet: max opslag (MB), max aantal uploads, free tier limiet. Check in API proxy voordat upload doorgaat.

Flow — upload met limietcheck

/api/files/upload checkLimit(user) over limit? → 429 Too Many OK → proxyIPFS Kubo add

Nieuwe bestanden

  • src/lib/limits.ts — Limiet configuratie + check functies
  • src/app/api/[...path]/route.ts — Injecteer limit check in proxyIPFS
  • src/app/dashboard/components/UsageCard.tsx — Visuele usage meter

Code sketch — limit checker

// src/lib/limits.ts
export interface UsageQuota {
  maxStorageMB: number;     // 30720 = 30 GB
  maxUploads: number;       // 1000
  freeUploadsPerDay: number; // 50
}

const DEFAULT_QUOTA: UsageQuota = {
  maxStorageMB: 30720,
  maxUploads: 1000,
  freeUploadsPerDay: 50,
};

export async function checkUploadAllowed(wallet?: string): Promise<{
  allowed: boolean;
  reason?: string;
  quota: UsageQuota;
  used: { storageMB: number; uploads: number };
}> {
  // Haal repo stats van Kubo
  // Check storage, upload count, free tier
  // Return: { allowed: false, reason: 'Storage limit reached (23/30 GB)' }
}
🔔

3. Notificatiesysteem

Toast notifications voor uploads, errors, en acties
Critical
Huidig

Upload starten = geen feedback tot het klaar is. Errors verdwijnen in console.

Doel

Toast rechtsboven: "✅ Upload voltooid", "❌ Pin mislukt: …", "⚠️ Storage 90% vol"

Architectuur

Component notify({ type, title }) NotificationProvider Toast stack
(rechtsboven)
Auto-dismiss
na 4s

Nieuwe bestanden

  • src/lib/notifications.tsx — NotificationContext + useNotify() hook
  • src/components/Toast.tsx — Toast component met progress-bar
  • src/components/ToastContainer.tsx — Gestapelde toasts, max 5
  • src/app/layout.tsx — Wrap met NotificationProvider

Code sketch

// Gebruik in elke pagina:
const { notify } = useNotify();

async function handleUpload(file: File) {
  notify({ type: 'info', title: 'Uploaden…', message: file.name });
  try {
    const result = await uploadFile(file);
    notify({ type: 'success', title: 'Upload voltooid', message: result.cid });
  } catch (e) {
    notify({ type: 'error', title: 'Upload mislukt', message: shortError(e) });
  }
}
📊

4. User Usage Overview

Persoonlijk dashboard met opslag, uploads, quota-status
Critical
Huidig

Geen eigen overzicht. Stats staan verspreid over Dashboard (node-level), Users pagina (alleen admin), en Settings (alleen limiet tonen, geen verbruik).

Doel

Elke ingelogde user ziet in één oogopslag: storage gebruikt/limiet, aantal uploads, free quota resterend, bandwidth, pinned CIDs.

Component structuur

UsageOverview ├─ StorageGauge ├─ UploadCountCard ├─ FreeTierProgress ├─ BandwidthMini └─ RecentUploads

Data sources

DataBronAPI call
Storage used / maxKubo repo stat/api/repo/stats
Uploads countContract + localStoragegetUserUploads() + getHistory()
Free quota remainingContractpaymentService.getUserUploads()
Bandwidth in/outKubo bw stat/api/bw/stats
Pinned CIDsKubo pin ls/api/pins
Peer countKubo swarm peers/api/peers

Nieuwe bestanden

  • src/app/usage/page.tsx — Volledige usage overview pagina
  • src/app/dashboard/components/StorageGauge.tsx — Ring chart met used/max
  • src/app/dashboard/components/FreeTierProgress.tsx — Progress bar free uploads
  • src/components/PortalSidebar.tsx — Voeg "Usage" link toe

Code sketch — StorageGauge

// src/app/dashboard/components/StorageGauge.tsx
export function StorageGauge({ used, max }: { used: number; max: number }) {
  const pct = max > 0 ? Math.min(used / max, 1) : 0;
  const color = pct > 0.9 ? 'text-accent-rose'
              : pct > 0.7 ? '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">Storage</span>
        <span className={"text-xs font-mono " + color}>{used.toFixed(1)} / {max.toFixed(1)} GB</span>
      </div>
      <div className="h-2 rounded-full bg-surface-700 overflow-hidden">
        <div className={"h-full rounded-full transition-all duration-500 " + (
          pct > 0.9 ? 'bg-accent-rose'
          : pct > 0.7 ? 'bg-accent-amber'
          : 'bg-accent-cyan'
        )} style={{ width: `${pct * 100}%` }} />
      </div>
    </div>
  );
}
👤

5. User Self-Service

Registratie, wachtwoord wijzigen, profiel
High
Huidig

Alleen admin kan users aanmaken. Geen registratie of wachtwoord resetten.

Doel

Zelf registreren, wachtwoord wijzigen, gebruikersprofiel met upload stats.

Nieuwe bestanden

  • src/app/profile/page.tsx — Wachtwoord wijzigen, API key overzicht
  • src/app/register/page.tsx — Registratie met username/password
Fase 2

UX Verbeteringen

~1.5 dag
📑

5. Batch Operaties

Meerdere pins/history items selecteren + bulk acties
High

Flow — batch unpin

Checkbox per pin "2 selected" bar Knop "Unpin selected" Confirm dialog DELETE /api/pins/:cid
(parallel)
Notify result

Bestanden

  • src/components/BatchBar.tsx — "N selected" floating bar
  • src/app/pins/page.tsx — Checkbox in elke rij + BatchBar
  • src/app/history/page.tsx — Idem voor history clear
🌓

6. Dark/Light Mode (live)

Theme switcher die direct werkt, niet pas bij nieuwe sessie
High
Huidig

"Theme preference is saved but only affects new sessions. Full theme switching coming soon."

Doel

Toggle in sidebar → direct alle CSS variablen naar light mode. CSS transition voor vloeiende overgang.

Bestanden

  • src/lib/theme.tsx — ThemeContext + useTheme() hook
  • src/app/layout.tsx — .dark / .light class op html element
  • src/app/globals.css — Voeg :root.light variablen toe
  • src/components/PortalSidebar.tsx — Theme toggle button

Code sketch — ThemeContext

// src/lib/theme.tsx
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState<'dark' | 'light'>(getSettings().theme);

  useEffect(() => {
    document.documentElement.classList.toggle('dark', theme === 'dark');
    document.documentElement.classList.toggle('light', theme === 'light');
  }, [theme]);

  const toggle = () => {
    const next = theme === 'dark' ? 'light' : 'dark';
    setTheme(next);
    saveSettings({ theme: next });
  };
}

CSS: :root.light { --bg: #fff; --surface-900: #f8fafc; ... } + * { transition: background-color 0.3s, border-color 0.3s; }

💀

7. Skeleton Loaders

Vervang "Loading…" text door skeletanimaties
Medium

Voorbeelden

PaginaHuidigSkeleton
Dashboard— (data komt leeg binnen)4 pulsende card outlines
Peers"Loading…"10 rij skeletten
Pins"Loading…"Lijst skeletten met icon + text
Users"Loading…"Tabel skeletten

Bestanden

  • src/components/Skeleton.tsx — <Skeleton className="h-4 w-full" />
  • src/app/dashboard/page.tsx — Skelet voor stat cards + peer lijst
  • src/app/peers/page.tsx — Skelet voor tabel
  • src/app/pins/page.tsx — Skelet voor pin lijst
  • src/app/users/page.tsx — Skelet voor user tabel

Code sketch

// src/components/Skeleton.tsx
export function Skeleton({ className }: { className?: string }) {
  return (
    <div
      className={`animate-pulse rounded-md bg-surface-800 ${className ?? ''}`}
      style={{ animationDuration: '1.5s' }}
    />
  );
}

// Gebruik in pins pagina:
{loading && (
  <div className="divide-y divide-surface-800">
    {Array.from({ length: 6 }).map((_, i) => (
      <div key={i} className="flex items-center gap-3 px-5 py-3">
        <Skeleton className="w-4 h-4 rounded" />
        <Skeleton className="h-4 w-48" />
        <Skeleton className="h-3 w-16 ml-auto" />
      </div>
    ))}
  </div>
)}
🛡️

8. Error Boundary

Voorkom lege pagina bij component crash
Medium

Bestanden

  • src/components/ErrorBoundary.tsx — Class component met componentDidCatch
  • src/app/error.tsx — Next.js error boundary (al ondersteund, alleen invullen)

Code sketch

// src/components/ErrorBoundary.tsx
export class ErrorBoundary extends React.Component<{ fallback?: ReactNode }, { error: Error | null }> {
  state = { error: null };

  static getDerivedStateFromError(error: Error) { return { error }; }

  render() {
    if (this.state.error) {
      return (
        <div className="glass rounded-xl p-8 text-center">
          <AlertCircle className="w-8 h-8 text-accent-rose mx-auto mb-3" />
          <h3 className="text-surface-200 font-semibold mb-1">Er ging iets mis</h3>
          <p className="text-xs text-surface-500 mb-4">{this.state.error.message}</p>
          <button onClick={() => this.setState({ error: null })}
            className="px-4 py-2 rounded-lg bg-brand-600 text-white text-sm">
            Probeer opnieuw
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}
⬇️

9. Bestanden Downloaden

Download knop in explorer + history i.p.v. alleen link kopiëren
Medium

Flow

Download knop fetch /api/explorer/cat/{cid} Blob → URL.createObjectURL <a download="..." href={url}> klik</a>

Bestanden

  • src/lib/download.ts — downloadFile(cid, filename) utility
  • src/app/explorer/components/DirectoryListing.tsx — Download knop per file
  • src/app/history/page.tsx — Download knop in acties
Fase 3

Technische Diepgang

~2 dagen
📱

10. PWA (Progressive Web App)

Installable als app, offline ondersteuning, manifest
Low

Bestanden

  • public/manifest.json — Naam, iconen, theme_color
  • public/sw.js — Service worker voor offline cache
  • public/icon-192.png, icon-512.png — App iconen
  • src/app/layout.tsx — Voeg manifest link + meta theme-color toe
🧪

11. Tests

Van 0 naar gedekt — unit + integratie
Low

Wat testen

LaagWatTool
Unithelpers.ts — formatBytes, gatewayLink, truncateCidVitest
Unitwallet.ts — formatWeiToETH, parseEtherVitest
Unitlimits.ts — checkUploadAllowedVitest
IntegratieCatch-all route — matchRoute, mapToKuboAPIVitest + MSW
E2EDashboard laadt, peers tonen, upload flowPlaywright

Bestanden

  • src/lib/__tests__/helpers.test.ts
  • src/lib/__tests__/wallet.test.ts
  • src/app/api/__tests__/route.test.ts
  • vitest.config.ts
  • e2e/dashboard.spec.ts

12. API Caching / SWR

Vervang eigen polling door SWR of React Query
Low

Huidig vs Nieuw

// HUIDIG — manual fetch + interval
useEffect(() => {
  load();                           // direct op mount
  const iv = setInterval(load, 15000); // elke 15s opnieuw
  return () => clearInterval(iv);
}, []);

// NIEUW — SWR (stale-while-revalidate)
const { data: peers } = useSWR('/api/peers', fetch, {
  refreshInterval: 15000,        // zelfde interval
  revalidateOnFocus: true,        // + op tab focus
  dedupingInterval: 5000,         // geen duplicaten
});

Voordelen

  • Automatische cache + deduplicatie
  • Optimistic UI mogelijk
  • Minder code per pagina
  • Global cache key management
🔌

13. WebSocket Live Updates

Bandwidth + peers live zonder te pollen
Low

Architectuur

Next.js server WS endpoint
/api/ws
Pushed updates
(peer join/leave, bw)
Dashboard
live update

Let op: Next.js edge runtime ondersteunt WebSocket niet native. Opties: aparte WS server (Node.js), of Server-Sent Events via route handler. SSE is eenvoudiger en werkt door bestaande proxy heen.

🔍

14. Full-text Search in IPFS

Doorzoek gepinde IPFS content op tekst
Low

Concept

  • Service die periodic alle gepinde files indexeert
  • Text extraction uit .txt, .md, .json, .html
  • Mini search index in IndexedDB of localStorage
  • Zoekveld in explorer + dashboard
  • Beperking: alleen tekstuele content, geen images/binary
Totaal

Overzicht alle bestanden

15 items
#ItemPriorityFaseNieuwe bestandenWijzigingen
1Login / SessieCriticalF142
2Usage LimitsCriticalF121
3NotificatiesCriticalF131
4User Usage OverviewCriticalF131
5Self-ServiceHighF120
6Batch OperatiesHighF212
7Dark/Light ModeHighF213
8Skeleton LoadersMediumF214
9Error BoundaryMediumF220
10DownloadsMediumF212
11PWALowF341
12TestsLowF340
13API CachingLowF305
14WebSocket/SSELowF321
15IPFS SearchLowF321
32
Nieuwe bestanden
24
Wijzigingen
4
Critical items
~6
Dagen totaal
💬 Mijn advies

Begin met Fase 1 — dat zijn de 5 items die het verschil maken tussen "demo" en "productie":

  1. 🔐 Login — zonder auth is alles open
  2. 📊 Usage limits — voorkom dat 1 user de hele schijf vult
  3. 🔔 Notificaties — gebruikers zien wat er gebeurt
  4. 📈 Usage Overview — inzicht in eigen verbruik
  5. 👤 Self-service — registratie + profiel

→ Zeg het woord en ik maak er een uitvoerbaar plan van met todo's.