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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user