import { useEffect, useState } from 'react';
import { Link, Navigate } from 'react-router-dom';
import { useAnalytics } from '../hooks/useAnalytics';
import type { BuilderTrade, VolumeEntry } from '../hooks/useAnalytics';
import { useIsAdmin } from '../hooks/useIsAdmin';
// Wallet linked to the SZHub builder profile β Polymarket batches builder-fee
// payouts here (weather-bot proxy). Distributor: 0xd7a0535cβ¦d4ba.
const PAYOUT_ADDR = '0x3643914646900cA7A5df15B8f5d1Cc5E32728c1a';
function fmtUsd(val: number): string {
if (val >= 1_000_000) return `$${(val / 1_000_000).toFixed(1)}M`;
if (val >= 1_000) return `$${(val / 1_000).toFixed(1)}K`;
return `$${val.toFixed(2)}`;
}
function fmtDate(iso: string): string {
try {
return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
} catch {
return iso;
}
}
function fmtTime(ts: string): string {
try {
// matchTime is unix seconds
const d = new Date(Number(ts) * 1000);
if (isNaN(d.getTime())) return ts;
return d.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
} catch {
return ts;
}
}
// Trades/volume/fees accrued since the last daily reset (00:00 UTC β the Polymarket
// volume day, same boundary the Daily Volume table uses) + a live countdown to the
// next reset. Helps gauge today's pace toward the ~$33k/day volume scale target.
function TodayActivity({ trades }: { trades: BuilderTrade[] }) {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, []);
// Start of the current UTC day, in unix seconds (matchTime is unix seconds).
const dayStartSec = Math.floor(now / 86_400_000) * 86_400;
const nextResetMs = (dayStartSec + 86_400) * 1000;
const today = trades.filter((t) => Number(t.matchTime) >= dayStartSec);
const count = today.length;
const volume = today.reduce((s, t) => s + (parseFloat(t.sizeUsdc) || 0), 0);
const fees = today.reduce((s, t) => s + (parseFloat(t.builderFee) || 0), 0);
const remMs = Math.max(0, nextResetMs - now);
const h = Math.floor(remMs / 3_600_000);
const m = Math.floor((remMs % 3_600_000) / 60_000);
const s = Math.floor((remMs % 60_000) / 1000);
const countdown = `${h}h ${String(m).padStart(2, '0')}m ${String(s).padStart(2, '0')}s`;
return (
<div className="analytics-payout" style={{ marginBottom: 12 }}>
<h3 className="analytics-section-title" style={{ marginTop: 0 }}>Today (since 00:00 UTC)</h3>
<div className="analytics-payout-row"><span>Trades</span><strong>{count}</strong></div>
<div className="analytics-payout-row"><span>Volume</span><strong>{fmtUsd(volume)}</strong></div>
<div className="analytics-payout-row"><span>Builder fee</span><strong className="text-profit">{fmtUsd(fees)}</strong></div>
<div className="analytics-payout-row"><span>Resets in</span><strong>{countdown}</strong></div>
<p className="analytics-payout-note">
Daily reset at 00:00 UTC (β17:00 Vancouver) Β· payout batch follows ~00:30 UTC.
</p>
</div>
);
}
export default function AnalyticsPage() {
const isAdmin = useIsAdmin();
const { volume, trades, totals, paidTotal, payouts, loading, refresh } = useAnalytics();
const pending = Math.max(0, totals.myBuilderFee - paidTotal);
const lastPayout = payouts[0];
// Block deep-links: only allowlisted emails may view builder revenue.
if (!isAdmin) return <Navigate to="/" replace />;
return (
<div className="portfolio-page">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<h2 className="portfolio-title">Builder Analytics</h2>
<button className="page-btn" onClick={refresh} disabled={loading} style={{ fontSize: 13 }}>
{loading ? 'Loadingβ¦' : 'Refresh'}
</button>
</div>
{/* Cross-link to the other admin analytics page */}
<div style={{ marginBottom: 12 }}>
<Link to="/analytics/users" className="page-btn" style={{ fontSize: 13, textDecoration: 'none' }}>
β User Analytics
</Link>
</div>
{/* KPI Cards */}
<div className="analytics-kpi-grid">
<KpiCard
label="My Builder Fee"
value={fmtUsd(totals.myBuilderFee)}
sub={`Maker ${fmtUsd(totals.makerFee)} Β· Taker ${fmtUsd(totals.takerFee)}`}
accent
/>
<KpiCard label="Platform Fee" value={fmtUsd(totals.totalFees)} />
<KpiCard label="Total Volume" value={fmtUsd(totals.totalVolume)} />
<KpiCard label="Trades" value={String(totals.totalTrades)} />
<KpiCard label="Active Users" value={String(totals.activeUsers)} />
<KpiCard label="Rank" value={`#${totals.rank}`} />
</div>
{/* Today's trade pace + countdown to the daily 00:00 UTC reset */}
<TodayActivity trades={trades} />
{/* Payout info β builder fees are paid in PUSD to the wallet linked to the
SZHub builder profile, batched daily ~00:30 UTC once accrued β₯ ~$1.
Live paid/pending amounts come in a follow-up (on-chain endpoint). */}
<div className="analytics-payout">
<h3 className="analytics-section-title" style={{ marginTop: 0 }}>Payouts</h3>
<div className="analytics-payout-row">
<span>Accrued (lifetime)</span>
<strong>{fmtUsd(totals.myBuilderFee)}</strong>
</div>
<div className="analytics-payout-row">
<span>Paid out</span>
<strong className="text-profit">{fmtUsd(paidTotal)}</strong>
</div>
<div className="analytics-payout-row">
<span>Pending</span>
<strong>{fmtUsd(pending)}</strong>
</div>
{lastPayout && (
<div className="analytics-payout-row">
<span>Last payout</span>
<a
href={`https://polygonscan.com/tx/${lastPayout.hash}`}
target="_blank"
rel="noreferrer"
>
{fmtDate(new Date(Number(lastPayout.timestamp) * 1000).toISOString())} Β· {fmtUsd(lastPayout.amount)} β
</a>
</div>
)}
<div className="analytics-payout-row">
<span>Destination</span>
<a
href={`https://polygonscan.com/address/${PAYOUT_ADDR}#tokentxns`}
target="_blank"
rel="noreferrer"
>
{PAYOUT_ADDR.slice(0, 6)}β¦{PAYOUT_ADDR.slice(-4)} β
</a>
</div>
<p className="analytics-payout-note">
Paid in <b>PUSD</b> Β· daily batch ~00:30 UTC (β17:30 Vancouver) Β· min β $1 to trigger a payout.
</p>
</div>
{/* Loading skeleton */}
{loading && trades.length === 0 && (
<div className="portfolio-loading">
{Array.from({ length: 3 }, (_, i) => (
<div key={i} className="position-card">
<div className="skeleton-line" style={{ width: '70%', height: 16 }} />
<div className="skeleton-line" style={{ width: '50%', height: 14, marginTop: 8 }} />
</div>
))}
</div>
)}
{/* Daily Volume Table */}
{volume.length > 0 && (
<>
<h3 className="analytics-section-title">Daily Volume</h3>
<div className="analytics-table-wrap">
<table className="analytics-table">
<thead>
<tr>
<th>Date</th>
<th style={{ textAlign: 'right' }}>Volume</th>
<th style={{ textAlign: 'right' }}>Users</th>
<th style={{ textAlign: 'right' }}>Rank</th>
</tr>
</thead>
<tbody>
{volume.slice().reverse().map((v: VolumeEntry, i: number) => (
<tr key={i}>
<td>{fmtDate(v.dt)}</td>
<td style={{ textAlign: 'right' }}>{fmtUsd(v.volume)}</td>
<td style={{ textAlign: 'right' }}>{v.activeUsers}</td>
<td style={{ textAlign: 'right' }}>#{v.rank}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
{/* Recent Trades */}
{trades.length > 0 && (
<>
<h3 className="analytics-section-title">Recent Trades</h3>
<div className="analytics-table-wrap">
<table className="analytics-table">
<thead>
<tr>
<th>Time</th>
<th>Side</th>
<th>Type</th>
<th style={{ textAlign: 'right' }}>Size</th>
<th style={{ textAlign: 'right' }}>Price</th>
<th style={{ textAlign: 'right' }}>My Fee</th>
</tr>
</thead>
<tbody>
{trades.slice(0, 50).map((t: BuilderTrade) => (
<tr key={t.id}>
<td>{fmtTime(t.matchTime)}</td>
<td>
<span className={t.side === 'BUY' ? 'text-profit' : 'text-loss'}>
{t.side}
</span>
</td>
<td>
<span className={`analytics-type-badge analytics-type-${(t.tradeType || '').toLowerCase()}`}>
{t.tradeType || 'β'}
</span>
</td>
<td style={{ textAlign: 'right' }}>{fmtUsd(parseFloat(t.sizeUsdc) || 0)}</td>
<td style={{ textAlign: 'right' }}>{(parseFloat(t.price) * 100).toFixed(0)}Β’</td>
<td style={{ textAlign: 'right' }}>${(parseFloat(t.builderFee) || 0).toFixed(4)}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
{/* Empty state */}
{!loading && trades.length === 0 && volume.length === 0 && (
<div className="portfolio-empty">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" width="36" height="36">
<path d="M3 3v18h18" />
<path d="M7 16l4-4 4 4 6-6" />
</svg>
<p>No builder activity yet</p>
<p style={{ fontSize: 13, opacity: 0.6 }}>Trades placed through SZHub will appear here</p>
</div>
)}
</div>
);
}
function KpiCard({ label, value, sub, accent }: { label: string; value: string; sub?: string; accent?: boolean }) {
return (
<div className={`analytics-kpi-card${accent ? ' analytics-kpi-accent' : ''}`}>
<span className="analytics-kpi-label">{label}</span>
<span className="analytics-kpi-value">{value}</span>
{sub && <span className="analytics-kpi-sub">{sub}</span>}
</div>
);
}
π Git History
6c47fa4chore: local Polikopi project home + Phase 1 redesign artifacts12 days ago
Show last diff
Loading...