import { useState } from 'react';
import { useNavigate, type NavigateFunction } from 'react-router-dom';
import { useWhales, type WhaleTrade } from '../../hooks/useWhales';
import { formatVolume, timeAgo } from '../../utils/format';
import { useT } from '../../i18n/LanguageContext';
interface Props {
limit?: number;
minAmount?: number;
days?: number;
marketId?: string;
}
export default function WhaleFeed({ limit = 50, minAmount = 5000, days = 7, marketId }: Props) {
const { t: tr } = useT();
const [includeResolved, setIncludeResolved] = useState(false);
const { trades, loading, meta } = useWhales({ limit, minAmount, days, marketId, includeResolved });
const navigate = useNavigate();
// The resolved-markets toggle only makes sense on the general feed; a single
// market view always shows every trade.
const showToggle = !marketId;
if (loading) {
return (
<div className="wf-skeleton">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="wf-skeleton-row" />
))}
</div>
);
}
if (trades.length === 0) {
return (
<div className="wf-empty">
<div className="wf-empty-icon">🐋</div>
<div className="wf-empty-title">No whale trades yet</div>
<div className="wf-empty-sub">
Tracking trades >{formatVolume(minAmount)} over the last {days} day{days > 1 ? 's' : ''}.
Data builds up over time as the scanner runs every 5 minutes.
</div>
</div>
);
}
return (
<div className="wf-list">
{showToggle && (
<div className="wf-toolbar">
<button
className={`pill ${includeResolved ? 'pill-active' : ''}`}
onClick={() => setIncludeResolved(v => !v)}
title={tr('wf.resolvedTitle')}
>
{includeResolved ? tr('wf.resolvedShown') : tr('wf.showResolved')}
</button>
</div>
)}
{trades.map((t) => (
<WhaleTradeRow key={t.id} trade={t} onNavigate={navigate} />
))}
{meta && meta.count >= limit && (
<div className="wf-more">Showing {meta.count} trades</div>
)}
</div>
);
}
function WhaleTradeRow({ trade: t, onNavigate }: { trade: WhaleTrade; onNavigate: NavigateFunction }) {
const { t: tr } = useT();
const isBuy = t.side === 'BUY';
const ago = timeAgo(t.timestamp);
const shortAddr = `${t.address.slice(0, 6)}...${t.address.slice(-4)}`;
const label = t.whaleLabel || shortAddr;
// Whale dossier: who is this trader? Lifetime size + accuracy qualify whether
// the trade is worth following. win_rate is 0..1 (null until resolved trades).
const wrPct = t.whaleWinRate != null ? Math.round(t.whaleWinRate * 100) : null;
const smart = t.whaleWinRate != null && t.whaleWinRate >= 0.6;
const dossier: string[] = [];
if (t.whaleTotalVolume) dossier.push(formatVolume(t.whaleTotalVolume));
if (t.whaleTotalTrades) dossier.push(`${t.whaleTotalTrades} ${tr('wf.tradesWord')}`);
if (wrPct != null) dossier.push(`WR ${wrPct}%`);
// Net directional stance of the trade: buying Yes OR selling No both lean
// bullish on Yes; buying No OR selling Yes lean bearish (= bet on No). The
// raw bought/sold + outcome is ambiguous on its own, so we collapse it.
const betYes = (isBuy && t.outcome === 'Yes') || (!isBuy && t.outcome === 'No');
const betSide = betYes ? 'Yes' : 'No';
return (
<div
className={`wf-row ${betYes ? 'wf-dir-yes' : 'wf-dir-no'}`}
onClick={() => onNavigate(`/market/${t.marketId}`)}
>
<div className="wf-row-left">
<div className="wf-head">
<span className={`wf-bet ${betYes ? 'wf-bet-yes' : 'wf-bet-no'}`}>
{tr('wf.betTo', { side: betSide })}
</span>
<span
className="wf-label-link"
onClick={(e) => { e.stopPropagation(); onNavigate('/whale', { state: { address: t.address } }); }}
>
{label}
</span>
{smart && <span className="wf-smart">๐ง Smart</span>}
</div>
<div className="wf-action">
<span className={isBuy ? 'wf-buy' : 'wf-sell'}>
{isBuy ? tr('wf.bought') : tr('wf.sold')}
</span>
{' '}
<span className="wf-amount">{formatVolume(t.amount)}</span>
{' '}
<span className={t.outcome === 'Yes' ? 'wf-yes' : 'wf-no'}>
{t.outcome}
</span>
</div>
{dossier.length > 0 && (
<div className="wf-dossier">๐ {dossier.join(' ยท ')}</div>
)}
<div className="wf-market">
{t.marketQuestion || 'Unknown market'}
</div>
</div>
<div className="wf-row-right">
<span className="wf-price">{Math.round(t.price * 100)}ยข</span>
<span className="wf-price-cap">{tr('wf.entry')}</span>
<span className="wf-time">{ago}</span>
</div>
</div>
);
}
๐ Git History
6c47fa4chore: local Polikopi project home + Phase 1 redesign artifacts12 days ago
Show last diff
Loading...