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
+63 -89
View File
@@ -3,22 +3,20 @@
import { useState, useEffect, useCallback } from 'react';
import PortalLayout from '@/app/layout-portal';
import AuthGuard from '@/components/AuthGuard';
import { paymentService, type TokenInfo, type MAOSDiscountTier } from '@/lib/payment';
import { paymentService, PaymentProvider, type TokenInfo, type MAOSDiscountTier } from '@/lib/payment';
import {
connectWallet, switchToChain270, getInjectedProvider,
formatWeiToETH, formatBytes, type WalletProvider,
connectWallet, switchToChain270, getInjectedProvider, addWalletListener, disconnectWallet,
type WalletProvider,
} from '@/lib/wallet';
import {
Wallet, Loader2, CheckCircle, XCircle,
RefreshCw, AlertTriangle,
} from 'lucide-react';
import { SkeletonCard } from '@/components/Skeleton';
import { useNotify } from '@/lib/notifications';
import Overview from './components/Overview';
import PricingView from './components/PricingView';
import TokensView from './components/TokensView';
import TiersView from './components/TiersView';
type ViewMode = 'overview' | 'pricing' | 'tokens' | 'tiers';
type TxStatus = { ok: boolean; msg: string } | null;
import ErrorBoundary from '@/components/ErrorBoundary';
import type { ViewMode, TxStatus } from './helpers';
import { WalletConnect, WrongChainWarning, WalletInfo, NavTabs, TxStatusToast } from './components';
export default function AdminPaymentPage() {
const [address, setAddress] = useState<string | null>(null);
@@ -42,13 +40,14 @@ export default function AdminPaymentPage() {
const [contractBalance, setContractBalance] = useState<Record<string, string>>({});
const isOwner = !!(address && owner && address.toLowerCase() === owner.toLowerCase());
const { notify } = useNotify();
// ── Load all contract state ──
const refresh = useCallback(async () => {
setLoading(true);
setTxStatus(null);
try {
paymentService.isDeployed().then(setDeployed).catch(() => setDeployed(false));
paymentService.isDeployed().then(setDeployed).catch((err) => { console.error('[AdminPayment] Deploy check failed:', err); setDeployed(false); });
const [own, price, free, t, tiersData, uploads] = await Promise.all([
paymentService.getOwner(),
paymentService.getBasePricePerMB(),
@@ -62,7 +61,6 @@ export default function AdminPaymentPage() {
setFreeTier(free);
setTiers(tiersData);
// Deduplicate by symbol
const seen = new Set<string>();
const uniqueTokens = t.filter(tk => {
if (seen.has(tk.symbol)) return false;
@@ -72,32 +70,29 @@ export default function AdminPaymentPage() {
setTokens(uniqueTokens);
const symbols = uniqueTokens.map(tk => tk.symbol);
const revEntries = await Promise.all(
symbols.map(s => paymentService.getTotalRevenue(s))
);
const revEntries = await Promise.all(symbols.map(s => paymentService.getTotalRevenue(s)));
const revMap: Record<string, bigint> = {};
symbols.forEach((s, i) => { revMap[s] = revEntries[i]; });
setRevenues(revMap);
// Balances via RPC
const balMap: Record<string, string> = {};
for (const tk of t) {
try {
const client = paymentService['getPublicClient']();
const client = (paymentService as any)['getPublicClient']();
if (tk.symbol === 'ETH') {
const bal = await client.getBalance({ address: paymentService['contractAddress'] });
const bal = await client.getBalance({ address: (paymentService as any)['contractAddress'] });
balMap['ETH'] = bal.toString();
} else if (tk.address && tk.address !== '0x0000000000000000000000000000000000000000') {
const ERC20_BAL_ABI = [{ type: 'function', name: 'balanceOf', inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' }] as const;
const ERC20_BAL_ABI = [{ type: 'function' as const, name: 'balanceOf', inputs: [{ type: 'address' as const }], outputs: [{ type: 'uint256' as const }], stateMutability: 'view' as const }];
const bal = await client.readContract({
address: tk.address as `0x${string}`,
abi: ERC20_BAL_ABI,
functionName: 'balanceOf',
args: [paymentService['contractAddress']],
args: [(paymentService as any)['contractAddress']],
});
balMap[tk.symbol] = (bal as bigint).toString();
}
} catch { /* skip */ }
} catch (err) { console.error('[AdminPayment] Failed to fetch balance for token:', err); }
}
setContractBalance(balMap);
setTotalUploads(uploads);
@@ -111,6 +106,22 @@ export default function AdminPaymentPage() {
useEffect(() => { refresh(); }, [refresh]);
// ── Listen for external wallet disconnect ──
useEffect(() => {
if (!provider) return;
const cb = (accounts: string[]) => {
if (accounts.length === 0) {
setAddress(null);
setProvider(null);
setConnecting(false);
setWrongChain(false);
setWalletType(null);
notify({ type: 'info', title: 'Wallet disconnected' });
}
};
addWalletListener('accountsChanged', cb);
}, [provider, notify]);
// ── Connect wallet ──
async function handleConnect() {
setConnecting(true);
@@ -129,14 +140,20 @@ export default function AdminPaymentPage() {
else if (window.ethereum?.isRabby) setWalletType('Rabby');
else if (window.ethereum?.isMetaMask) setWalletType('MetaMask');
else setWalletType('Wallet');
} catch (e: any) {
setTxStatus({ ok: false, msg: e.message || 'Connect failed' });
} catch (e: unknown) {
setTxStatus({ ok: false, msg: e instanceof Error ? e.message : 'Connect failed' });
} finally {
setConnecting(false);
}
}
// ── Admin action helper ──
function handleDisconnect() {
disconnectWallet();
setAddress(null); setProvider(null); setConnecting(false);
setWrongChain(false); setWalletType(null);
notify({ type: 'success', title: 'Wallet disconnected' });
}
async function doTx(label: string, fn: () => Promise<string>) {
if (!provider) return;
setTxStatus(null);
@@ -144,8 +161,8 @@ export default function AdminPaymentPage() {
const hash = await fn();
setTxStatus({ ok: true, msg: `${label} success: ${hash.slice(0, 10)}...` });
await refresh();
} catch (e: any) {
setTxStatus({ ok: false, msg: `${label} failed: ${e.message || e}` });
} catch (e: unknown) {
setTxStatus({ ok: false, msg: `${label} failed: ${e instanceof Error ? e.message : String(e)}` });
}
}
@@ -164,64 +181,33 @@ export default function AdminPaymentPage() {
return (
<AuthGuard requireAdmin redirectTo="/dashboard">
<PortalLayout>
{/* ── Header ── */}
<ErrorBoundary label="Betaalbeheer">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-white">Payment Admin</h1>
<p className="text-sm text-surface-400 mt-1">Manage IPFS Portal payment contract</p>
</div>
<div className="flex items-center gap-3">
{address && (
<span className="text-xs text-surface-400 flex items-center gap-1.5">
<span className={`w-1.5 h-1.5 rounded-full ${isOwner ? 'bg-accent-green' : 'bg-surface-500'}`} />
{isOwner ? 'Owner' : 'Viewer'}
</span>
)}
<button onClick={refresh} disabled={loading} className="p-2 rounded-lg bg-surface-800 hover:bg-surface-700 text-surface-400 transition-colors">
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
{address && (
<WalletInfo
address={address}
isOwner={isOwner}
loading={loading}
onRefresh={refresh}
onDisconnect={handleDisconnect}
/>
)}
</div>
{/* ── Connect wallet ── */}
{!address && (
<div className="glass rounded-xl p-6 text-center space-y-4">
<Wallet className="w-8 h-8 text-surface-500 mx-auto" />
<p className="text-sm text-surface-400">Connect wallet to view admin panel</p>
<button onClick={handleConnect} disabled={connecting}
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-brand-500 hover:bg-brand-400 disabled:opacity-50 text-white text-sm font-medium transition-colors"
>
{connecting ? <><Loader2 className="w-4 h-4 animate-spin" /> Connecting...</> : <><Wallet className="w-4 h-4" /> Connect Wallet</>}
</button>
</div>
)}
{wrongChain && (
<div className="flex items-center gap-2 px-4 py-3 rounded-lg bg-amber-500/10 border border-amber-500/20 text-amber-400 text-sm mb-4">
<AlertTriangle className="w-4 h-4 shrink-0" />
Please switch to zkSync Local (chain 270)
</div>
)}
{!address && <WalletConnect connecting={connecting} onConnect={handleConnect} />}
{wrongChain && <WrongChainWarning />}
{address && (
<>
{/* ── Nav tabs ── */}
<div className="flex gap-1 mb-6 p-1 rounded-lg bg-surface-800/50 w-fit">
{(['overview', 'pricing', 'tokens', 'tiers'] as ViewMode[]).map(v => (
<button key={v} onClick={() => setView(v)}
className={`px-3 py-1.5 rounded-md text-xs font-medium capitalize transition-colors ${
view === v ? 'bg-surface-700 text-white' : 'text-surface-400 hover:text-surface-200'
}`}
>
{v === 'overview' ? 'Overview' : v === 'pricing' ? 'Pricing' : v === 'tokens' ? 'Tokens' : 'Discount Tiers'}
</button>
))}
</div>
<PaymentProvider>
<NavTabs view={view} onChange={setView} />
{/* ── Loading ── */}
{loading ? (
<div className="flex items-center gap-2 text-surface-400 text-sm py-12 justify-center">
<Loader2 className="w-4 h-4 animate-spin" /> Loading contract state...
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 animate-fade-in">
{Array.from({ length: 6 }).map((_, i) => <SkeletonCard key={i} />)}
</div>
) : (
<>
@@ -231,7 +217,7 @@ export default function AdminPaymentPage() {
tokens={tokens}
revenues={revenues}
contractBalance={contractBalance}
contractAddress={paymentService['contractAddress']}
contractAddress={(paymentService as any)['contractAddress']}
owner={owner}
isOwner={isOwner}
address={address}
@@ -239,7 +225,6 @@ export default function AdminPaymentPage() {
onNavigate={(v: string) => setView(v as ViewMode)}
/>
)}
{view === 'pricing' && (
<PricingView
basePrice={basePrice}
@@ -256,7 +241,6 @@ export default function AdminPaymentPage() {
address={address}
/>
)}
{view === 'tokens' && (
<TokensView
tokens={tokens}
@@ -277,7 +261,6 @@ export default function AdminPaymentPage() {
address={address}
/>
)}
{view === 'tiers' && (
<TiersView
tiers={tiers}
@@ -293,19 +276,10 @@ export default function AdminPaymentPage() {
</>
)}
{/* ── TX Status ── */}
{txStatus && (
<div className={`fixed bottom-6 right-6 flex items-center gap-2 px-4 py-3 rounded-xl shadow-lg text-sm animate-fade-in ${
txStatus.ok
? 'bg-accent-green/10 border border-accent-green/20 text-accent-green'
: 'bg-accent-rose/10 border border-accent-rose/20 text-accent-rose'
}`}>
{txStatus.ok ? <CheckCircle className="w-4 h-4 shrink-0" /> : <XCircle className="w-4 h-4 shrink-0" />}
{txStatus.msg}
</div>
)}
</>
<TxStatusToast status={txStatus} />
</PaymentProvider>
)}
</ErrorBoundary>
</PortalLayout>
</AuthGuard>
);