refactor: codebase opschoning — type safety, error handling, central config, page splits

- categorie A: alle catch(e: any) → catch(e: unknown) met instanceof check
- categorie B: stille .catch() logging toegevoegd (SWRegister, admin, upload, auth)
- categorie C: hardcoded 192.168.1.176 IPs vervangen door env var defaults
- categorie D: page files >250L gesplitst (history, settings, explorer, admin/payment,
  ipns, users, upload, dashboard) in helpers/components
- categorie E: eslint-disable vervangen in SearchInput.tsx
- src/lib/config.ts centrale config module (localhost defaults)
- CSP in next.config.ts dynamisch via env var
- deploy.sh: geen hardcoded IP meer (REQUIRED arg)
- lib/api/ lib/auth/ lib/wallet/ lib/search-index/ lib/storage/ gesplitst in modules
- ongebruikte bestanden verwijderd: api.ts, auth.tsx, wallet.ts, search-index.ts,
  PaymentPanel.tsx, SearchBar.tsx, proxy.ts, serve-static.js
This commit is contained in:
maikrolf
2026-07-19 15:35:18 +02:00
parent 88ce38a49e
commit f72b775379
144 changed files with 4644 additions and 10230 deletions
+50 -356
View File
@@ -2,28 +2,22 @@
import { useEffect, useState, useMemo } from 'react';
import { getHistory, clearHistory, removeMultipleFromHistory, type UploadRecord } from '@/lib/storage';
import { formatBytes, gatewayLink, truncateCid } from '@/lib/helpers';
import { downloadFile } from '@/lib/download';
import { useNotify } from '@/lib/notifications';
import { SkeletonTable } from '@/components/Skeleton';
import BatchBar from '@/components/BatchBar';
import ErrorBoundary from '@/components/ErrorBoundary';
import PortalLayout from '@/app/layout-portal';
import Link from 'next/link';
import BatchBar from '@/components/BatchBar';
import { Trash2 } from 'lucide-react';
import { recordKey, parseRecordKey } from './helpers';
import {
Clock, HardDrive, Copy, ExternalLink, Trash2, Search, Upload,
Database, Calendar, Filter, X, CheckCircle, Download,
Link as LinkIcon,
} from 'lucide-react';
function getMethodBadge(method: UploadRecord['method']): { bg: string; text: string; label: string } {
switch (method) {
case 'free': return { bg: 'bg-accent-green/10', text: 'text-accent-green', label: 'free' };
case 'eth': return { bg: 'bg-brand-500/10', text: 'text-brand-400', label: 'eth' };
case 'token': return { bg: 'bg-accent-purple/10', text: 'text-accent-purple', label: 'token' };
case 'quick': return { bg: 'bg-surface-700/50', text: 'text-surface-400', label: 'quick' };
}
}
HistoryHeader,
HistorySearch,
HistorySkeleton,
HistoryEmpty,
HistoryTable,
HistoryMobileCard,
HistoryNoResults,
HistoryResultsCount,
} from './components';
/* ════════════════════════════ Page ════════════════════════════ */
@@ -34,8 +28,6 @@ export default function HistoryPage() {
const [loading, setLoading] = useState(true);
const [selected, setSelected] = useState<Set<string>>(new Set());
const recordKey = (r: UploadRecord) => `${r.cid}||${r.date}`;
function load() {
setHistory(getHistory());
setLoading(false);
@@ -73,7 +65,7 @@ export default function HistoryPage() {
await navigator.clipboard.writeText(text);
setCopied(key);
setTimeout(() => setCopied(null), 2000);
} catch { /* ignore */ }
} catch (err) { console.error('[HistoryPage] Clipboard write failed:', err); }
}
/* ── Selection ── */
@@ -100,12 +92,7 @@ export default function HistoryPage() {
function handleBatchDelete() {
const count = selected.size;
if (!confirm(`Delete ${count} upload${count !== 1 ? 's' : ''} from history?`)) return;
const keys = Array.from(selected).map((key) => {
const idx = key.lastIndexOf('||');
return { cid: key.slice(0, idx), date: key.slice(idx + 2) };
});
const keys = Array.from(selected).map(parseRecordKey);
removeMultipleFromHistory(keys);
setHistory((prev) => prev.filter((r) => !selected.has(recordKey(r))));
setSelected(new Set());
@@ -117,353 +104,60 @@ export default function HistoryPage() {
<PortalLayout>
<ErrorBoundary label="HistoryPage">
<div className="animate-fade-in">
{/* ── Header ── */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl font-bold text-white">Upload History</h1>
<p className="text-sm text-surface-400 mt-1">
{stats.totalUploads > 0
? `${stats.totalUploads} upload${stats.totalUploads !== 1 ? 's' : ''} · ${formatBytes(stats.totalSize)} total · ${stats.uniqueCids} unique file${stats.uniqueCids !== 1 ? 's' : ''}`
: 'Track your past uploads'}
</p>
</div>
{history.length > 0 && (
<button
onClick={handleClear}
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-accent-rose/10 text-accent-rose text-sm font-medium hover:bg-accent-rose/20 transition-colors self-start"
>
<Trash2 className="w-4 h-4" />
Clear History
</button>
)}
</div>
<HistoryHeader
totalUploads={stats.totalUploads}
totalSize={stats.totalSize}
uniqueCids={stats.uniqueCids}
onClear={handleClear}
/>
{/* ── Search ── */}
{history.length > 0 && (
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-surface-500" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search by file name or CID…"
className="w-full pl-10 pr-10 py-2 rounded-lg bg-surface-900 border border-surface-700 text-sm text-surface-200 placeholder:text-surface-500 focus:outline-none focus:border-brand-500"
/>
{search && (
<button
onClick={() => setSearch('')}
className="absolute right-3 top-1/2 -translate-y-1/2 p-0.5 rounded hover:bg-surface-700 transition-colors"
>
<X className="w-4 h-4 text-surface-500" />
</button>
)}
</div>
<HistorySearch search={search} onChange={setSearch} />
)}
{/* ── Loading skeleton ── */}
{loading && (
<div className="glass rounded-xl overflow-hidden">
<div className="px-1">
<SkeletonTable rows={8} cols={3} />
</div>
</div>
)}
{loading && <HistorySkeleton />}
{/* ── Empty state ── */}
{!loading && history.length === 0 && (
<div className="glass rounded-xl p-12 text-center animate-fade-in">
<div className="flex justify-center mb-4">
<div className="p-3 rounded-xl bg-surface-800/50">
<Clock className="w-8 h-8 text-surface-500" />
</div>
</div>
<h3 className="text-lg font-semibold text-surface-300 mb-2">No upload history yet</h3>
<p className="text-sm text-surface-500 mb-6 max-w-md mx-auto">
Your uploaded files will appear here so you can easily copy CIDs, download files, or check upload details.
</p>
<Link
href="/upload"
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-brand-600 text-white text-sm font-medium hover:bg-brand-500 transition-colors"
>
<Upload className="w-4 h-4" />
Upload your first file
</Link>
</div>
)}
{!loading && history.length === 0 && <HistoryEmpty />}
{/* ── Desktop table ── */}
{filtered.length > 0 && (
<>
{/* Desktop table — hidden on small screens */}
<div className="hidden sm:block glass rounded-xl overflow-hidden">
<table className="w-full">
<thead>
<tr className="border-b border-surface-800 text-left text-xs font-medium text-surface-500 uppercase tracking-wider">
<th className="px-5 py-3 w-10">
<input
type="checkbox"
checked={selected.size === filtered.length && filtered.length > 0}
onChange={toggleAll}
className="rounded border-surface-600 bg-surface-800 text-brand-500 focus:ring-brand-500"
/>
</th>
<th className="px-5 py-3">File Name</th>
<th className="px-5 py-3">CID</th>
<th className="px-5 py-3">Size</th>
<th className="px-5 py-3">Date</th>
<th className="px-5 py-3">Method</th>
<th className="px-5 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-surface-800">
{filtered.map((rec) => {
const badge = getMethodBadge(rec.method);
const gwLink = gatewayLink(rec.cid);
return (
<tr key={rec.cid + rec.date} className="hover:bg-surface-800/30 transition-colors">
{/* Checkbox */}
<td className="px-5 py-3">
<input
type="checkbox"
checked={selected.has(recordKey(rec))}
onChange={() => toggleSelection(recordKey(rec))}
className="rounded border-surface-600 bg-surface-800 text-brand-500 focus:ring-brand-500"
/>
</td>
{/* File name */}
<td className="px-5 py-3">
<div className="flex items-center gap-2">
<HardDrive className="w-3.5 h-3.5 text-surface-500 shrink-0" />
<span className="text-sm text-surface-200 truncate max-w-[200px]" title={rec.name}>
{rec.name}
</span>
</div>
</td>
<HistoryTable
records={filtered}
selected={selected}
copied={copied}
onToggle={toggleSelection}
onToggleAll={toggleAll}
allSelected={selected.size === filtered.length}
onCopy={copyToClipboard}
/>
{/* CID */}
<td className="px-5 py-3">
<span
className="text-xs font-mono text-surface-400 cursor-default"
title={rec.cid}
>
{truncateCid(rec.cid)}
</span>
</td>
{/* Size */}
<td className="px-5 py-3">
<span className="text-xs text-surface-400">{formatBytes(rec.size)}</span>
</td>
{/* Date */}
<td className="px-5 py-3">
<div className="text-xs text-surface-400">
<div>{new Date(rec.date).toLocaleDateString()}</div>
<div className="text-surface-500">{new Date(rec.date).toLocaleTimeString()}</div>
</div>
</td>
{/* Method badge */}
<td className="px-5 py-3">
<span className={`inline-block px-1.5 py-0.5 rounded text-[10px] font-medium ${badge.bg} ${badge.text}`}>
{badge.label}
</span>
</td>
{/* Actions */}
<td className="px-5 py-3 text-right">
<div className="flex items-center justify-end gap-1">
{/* Copy CID */}
<button
onClick={() => copyToClipboard(rec.cid, `cid-${rec.cid}`)}
className="p-1.5 rounded-lg hover:bg-surface-700 transition-colors group"
title="Copy CID"
>
{copied === `cid-${rec.cid}` ? (
<CheckCircle className="w-3.5 h-3.5 text-accent-green" />
) : (
<Copy className="w-3.5 h-3.5 text-surface-500 group-hover:text-surface-300" />
)}
</button>
{/* Copy gateway link */}
<button
onClick={() => copyToClipboard(gwLink, `gw-${rec.cid}`)}
className="p-1.5 rounded-lg hover:bg-surface-700 transition-colors group"
title="Copy gateway link"
>
{copied === `gw-${rec.cid}` ? (
<CheckCircle className="w-3.5 h-3.5 text-accent-green" />
) : (
<LinkIcon className="w-3.5 h-3.5 text-surface-500 group-hover:text-surface-300" />
)}
</button>
{/* Download */}
<button
onClick={() => downloadFile(rec.cid, rec.name).catch(() => {})}
className="p-1.5 rounded-lg hover:bg-surface-700 transition-colors group"
title="Download file"
>
<Download className="w-3.5 h-3.5 text-surface-500 group-hover:text-surface-300" />
</button>
{/* Open in gateway */}
<a
href={gwLink}
target="_blank"
rel="noopener noreferrer"
className="p-1.5 rounded-lg hover:bg-surface-700 transition-colors group"
title="Open in gateway"
>
<ExternalLink className="w-3.5 h-3.5 text-surface-500 group-hover:text-surface-300" />
</a>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* ── Mobile cards ── */}
<div className="sm:hidden space-y-3">
{filtered.map((rec) => {
const badge = getMethodBadge(rec.method);
const gwLink = gatewayLink(rec.cid);
return (
<div key={rec.cid + rec.date} className="glass rounded-xl p-4 space-y-3">
{/* Checkbox + Name + method badge */}
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2 flex-1 min-w-0">
<input
type="checkbox"
checked={selected.has(recordKey(rec))}
onChange={() => toggleSelection(recordKey(rec))}
className="rounded border-surface-600 bg-surface-800 text-brand-500 focus:ring-brand-500 shrink-0 mt-0.5"
/>
<HardDrive className="w-4 h-4 text-surface-500 shrink-0" />
<span className="text-sm text-surface-200 truncate" title={rec.name}>
{rec.name}
</span>
</div>
<span className={`shrink-0 px-1.5 py-0.5 rounded text-[10px] font-medium ${badge.bg} ${badge.text}`}>
{badge.label}
</span>
</div>
{/* CID */}
<div className="flex items-center gap-2">
<span
className="text-xs font-mono text-surface-400 truncate"
title={rec.cid}
>
{truncateCid(rec.cid)}
</span>
<button
onClick={() => copyToClipboard(rec.cid, `cid-${rec.cid}`)}
className="p-1 rounded-lg hover:bg-surface-700 transition-colors shrink-0"
>
{copied === `cid-${rec.cid}` ? (
<CheckCircle className="w-3 h-3 text-accent-green" />
) : (
<Copy className="w-3 h-3 text-surface-500" />
)}
</button>
</div>
{/* Size + date */}
<div className="flex items-center gap-4 text-xs text-surface-500">
<span>{formatBytes(rec.size)}</span>
<span className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{new Date(rec.date).toLocaleDateString()}
</span>
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{new Date(rec.date).toLocaleTimeString()}
</span>
</div>
{/* Actions row */}
<div className="flex items-center gap-2 pt-1">
{/* Copy CID */}
<button
onClick={() => copyToClipboard(rec.cid, `cid-m-${rec.cid}`)}
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-surface-800/50 hover:bg-surface-700 text-xs text-surface-400 hover:text-surface-200 transition-colors"
>
<Copy className="w-3 h-3" />
Copy CID
</button>
{/* Copy link */}
<button
onClick={() => copyToClipboard(gwLink, `gw-m-${rec.cid}`)}
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-surface-800/50 hover:bg-surface-700 text-xs text-surface-400 hover:text-surface-200 transition-colors"
>
<LinkIcon className="w-3 h-3" />
Copy Link
</button>
{/* Download */}
<button
onClick={() => downloadFile(rec.cid, rec.name).catch(() => {})}
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-surface-800/50 hover:bg-surface-700 text-xs text-surface-400 hover:text-surface-200 transition-colors"
>
<Download className="w-3 h-3" />
Download
</button>
{/* Open */}
<a
href={gwLink}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-surface-800/50 hover:bg-surface-700 text-xs text-surface-400 hover:text-surface-200 transition-colors"
>
<ExternalLink className="w-3 h-3" />
Open
</a>
</div>
</div>
);
})}
{filtered.map((rec) => (
<HistoryMobileCard
key={rec.cid + rec.date}
rec={rec}
selected={selected}
copied={copied}
onToggle={toggleSelection}
onCopy={copyToClipboard}
/>
))}
</div>
{/* ── Results count ── */}
{search && filtered.length < history.length && (
<div className="mt-3 text-center text-xs text-surface-500">
Showing {filtered.length} of {history.length} uploads
<button
onClick={() => setSearch('')}
className="ml-2 text-brand-400 hover:text-brand-300 underline transition-colors"
>
Clear filter
</button>
</div>
<HistoryResultsCount
filtered={filtered.length}
total={history.length}
onClear={() => setSearch('')}
/>
)}
</>
)}
{/* ── No search results ── */}
{!loading && history.length > 0 && filtered.length === 0 && (
<div className="glass rounded-xl p-12 text-center">
<div className="flex justify-center mb-3">
<Search className="w-8 h-8 text-surface-500" />
</div>
<h3 className="text-base font-semibold text-surface-300 mb-1">No results found</h3>
<p className="text-sm text-surface-500">
No uploads match &ldquo;{search}&rdquo;
</p>
<button
onClick={() => setSearch('')}
className="mt-4 text-sm text-brand-400 hover:text-brand-300 underline transition-colors"
>
Clear search
</button>
</div>
<HistoryNoResults search={search} onClear={() => setSearch('')} />
)}
{/* ── Batch bar ── */}
<BatchBar
count={selected.size}
onClear={() => setSelected(new Set())}
@@ -472,7 +166,7 @@ export default function HistoryPage() {
key: 'delete',
label: 'Delete selected',
icon: <Trash2 className="w-3.5 h-3.5" />,
variant: 'danger',
variant: 'danger' as const,
onClick: handleBatchDelete,
},
]}