← Back
β˜†
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...