← Back
import { useState, useEffect, useCallback } from 'react';

// --- Types ---

export interface UserAnalyticsTotals {
  totalUsers: number;
  onboarded: number;        // users with a deposit wallet set up
  activeCopiers: number;    // distinct users with ≥1 active subscription
  activeSubs: number;
  pausedSubs: number;
  openPositions: number;
  closedPositions: number;
  totalNotional: number;    // sum of cost ($ entered) across copy positions
  realizedPnl: number;      // banked P&L of closed copy positions
  totalDeposits: number;    // sum of on-chain deposit-wallet balances (pUSD+USDC.e)
  totalPositionsValue: number; // sum of live market value of open copies
  totalBuilderFees: number; // real builder fees across ALL makers (CLOB /builder/trades)
}

export interface PopularLeader {
  address: string;
  subscribers: number;
  label: string | null;
  pseudonym: string | null;
  winRate: number | null;
  pnl: number | null;
  volume: number | null;
  trades: number | null;
  category: string | null;
  lastSeen: string | null;
}

export interface UserRow {
  address: string;
  createdAt: string | null;
  lastLogin: string | null;
  depositWallet: string | null;
  activeSubs: number;
  openPositions: number;
  closedPositions: number;
  notional: number;
  realizedPnl: number;
  positionsValue: number;        // live market value of this user's open copies
  unrealizedPnl: number;         // positionsValue − open-position cost basis
  depositBalance: number | null; // on-chain DW stablecoin balance (null until fetched)
  country: string | null;
  email: string | null;
  builderFees: number;           // REAL builder fees this user generated (CLOB /builder/trades, by maker=DW)
  builderTrades: number;         // count of fee-bearing builder trades for this user
}

const EMPTY_TOTALS: UserAnalyticsTotals = {
  totalUsers: 0, onboarded: 0, activeCopiers: 0, activeSubs: 0, pausedSubs: 0,
  openPositions: 0, closedPositions: 0, totalNotional: 0, realizedPnl: 0,
  totalDeposits: 0, totalPositionsValue: 0, totalBuilderFees: 0,
};

// --- Hook ---

export function useUserAnalytics() {
  const [totals, setTotals] = useState<UserAnalyticsTotals>(EMPTY_TOTALS);
  const [leaders, setLeaders] = useState<PopularLeader[]>([]);
  const [users, setUsers] = useState<UserRow[]>([]);
  const [loading, setLoading] = useState(true);

  const refresh = useCallback(async () => {
    setLoading(true);
    try {
      const res = await fetch('/api/analytics/users');
      const d = await res.json();
      if (d.success && d.data) {
        setTotals({ ...EMPTY_TOTALS, ...d.data.totals });
        setLeaders(d.data.leaders ?? []);
        setUsers(d.data.users ?? []);
      }
    } catch { /* ignore */ }
    setLoading(false);
  }, []);

  useEffect(() => {
    const t = setTimeout(refresh, 0);
    const interval = setInterval(refresh, 5 * 60_000); // 5 min auto-refresh
    return () => { clearTimeout(t); clearInterval(interval); };
  }, [refresh]);

  return { totals, leaders, users, loading, refresh };
}

📜 Git History

03a2d80chore(save): session 2026-06-22 — Phase2 redesign + 3mo history + realized-PnL fix; staged _impl edits11 days ago
Show last diff
Loading...