import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import SubTabs from '../components/shared/SubTabs';
import EnterTradeModal from '../components/trade/EnterTradeModal';
import EdgeScanner from '../components/signals/EdgeScanner';
import { useWhaleConsensus } from '../hooks/useWhaleConsensus';
import type { WhaleConsensus } from '../hooks/useWhaleConsensus';
import { useWhaleInsider } from '../hooks/useWhaleInsider';
import type { WhaleInsider } from '../hooks/useWhaleInsider';
import { formatVolume } from '../utils/format';
import { whaleCodename } from '../utils/whales';
import { useT } from '../i18n/LanguageContext';
type FlowTab = 'consensus' | 'insider' | 'fire';
const PERIODS = [1, 7, 30];
const MIN_WHALES = [2, 3, 5];
const MIN_AGREE = [0, 0.6, 0.75];
// Freshness of the latest whale trade. Server timestamps are UTC without a
// marker, so append 'Z' before parsing. Returns null when unparseable.
function freshness(iso: string | null, t: (k: string, p?: Record<string, number>) => string) {
if (!iso) return null;
const ts = new Date(iso.endsWith('Z') ? iso : iso + 'Z').getTime();
if (isNaN(ts)) return null;
const h = (Date.now() - ts) / 3.6e6;
if (h < 1) return { text: t('flow.fresh.now'), isNew: true };
if (h < 24) return { text: t('flow.fresh.hours', { h: Math.round(h) }), isNew: true };
return { text: t('flow.fresh.days', { d: Math.round(h / 24) }), isNew: false };
}
function ConsensusCard({ r }: { r: WhaleConsensus }) {
const navigate = useNavigate();
const { t } = useT();
const [enterOpen, setEnterOpen] = useState(false);
const yes = r.netSide === 'Yes';
const fresh = freshness(r.lastTradeAt, t);
// entry → now price delta on the dominant side. +% = more expensive than whales.
let deltaEl = null;
if (r.currentPrice != null && r.avgPrice > 0) {
const pct = ((r.currentPrice - r.avgPrice) / r.avgPrice) * 100;
const abs = Math.abs(Math.round(pct));
const cls = pct > 2 ? 'flow-delta-bad' : pct < -2 ? 'flow-delta-good' : 'flow-delta-neutral';
const label = pct > 2
? t('flow.delta.pricier', { p: abs })
: pct < -2
? t('flow.delta.cheaper', { p: abs })
: t('flow.delta.same');
deltaEl = (
<div className="flow-delta">
<span className="flow-delta-track">
{t('flow.entryNow', { a: Math.round(r.avgPrice * 100), c: Math.round(r.currentPrice * 100) })}
</span>
<span className={`flow-delta-tag ${cls}`}>{label}</span>
</div>
);
}
return (
<div
className={`wcs-card ${yes ? 'wf-dir-yes' : 'wf-dir-no'}`}
onClick={() => navigate(`/market/${r.marketId}`)}
>
<div className="wcs-head">
<span className={`wf-bet ${yes ? 'wf-bet-yes' : 'wf-bet-no'}`}>
🐋 {r.whales} {t(r.whales === 1 ? 'whales.whaleOne' : 'whales.whaleMany')} → {r.netSide}
</span>
<span className="wcs-flow">{formatVolume(r.netFlowUsd)}</span>
</div>
<div className="wcs-q">{r.question || 'Unknown market'}</div>
{deltaEl}
<div className="wcs-meta">
<span>{t('whales.agreement', { p: Math.round(r.agreement * 100) })}</span>
<span>·</span>
<span>{r.trades} {t(r.trades === 1 ? 'whales.tradeOne' : 'whales.tradeMany')}</span>
{fresh && (
<>
<span>·</span>
<span className={fresh.isNew ? 'flow-fresh-new' : ''}>
{fresh.isNew ? '🆕 ' : ''}{fresh.text}
</span>
</>
)}
</div>
<button
className="flow-enter-btn"
onClick={e => { e.stopPropagation(); setEnterOpen(true); }}
>
{t('flow.enter')}
</button>
{enterOpen && (
<EnterTradeModal
marketId={r.marketId}
question={r.question || 'Unknown market'}
side={yes ? 'yes' : 'no'}
onClose={() => setEnterOpen(false)}
/>
)}
</div>
);
}
function Consensus() {
const { t } = useT();
const [days, setDays] = useState(7);
const [minWhales, setMinWhales] = useState(2);
const [minAgree, setMinAgree] = useState(0);
const { rows, loading } = useWhaleConsensus({ days, minAmount: 5000, limit: 40 });
const filtered = rows
.filter(r => r.whales >= minWhales && r.agreement >= minAgree)
.sort((a, b) => b.whales * b.agreement - a.whales * a.agreement);
return (
<div className="flow-consensus">
<div className="flow-filters">
<div className="flow-filter-group">
{PERIODS.map(p => (
<button key={p} className={`pill pill-sm ${days === p ? 'pill-active' : ''}`}
onClick={() => setDays(p)}>{p}{t('flow.dayShort')}</button>
))}
</div>
<div className="flow-filter-group">
{MIN_WHALES.map(n => (
<button key={n} className={`pill pill-sm ${minWhales === n ? 'pill-active' : ''}`}
onClick={() => setMinWhales(n)}>🐋≥{n}</button>
))}
</div>
<div className="flow-filter-group">
{MIN_AGREE.map(a => (
<button key={a} className={`pill pill-sm ${minAgree === a ? 'pill-active' : ''}`}
onClick={() => setMinAgree(a)}>{a === 0 ? t('flow.agreeAll') : `≥${Math.round(a * 100)}%`}</button>
))}
</div>
</div>
{loading && rows.length === 0 && (
<div className="wf-skeleton">
{Array.from({ length: 5 }).map((_, i) => <div key={i} className="wf-skeleton-row" />)}
</div>
)}
{!loading && filtered.length === 0 && (
<div className="wf-empty">
<div className="wf-empty-icon">🤝</div>
<div className="wf-empty-title">{t('whales.noConsensus')}</div>
<div className="wf-empty-sub">{t('whales.noConsensusSub')}</div>
</div>
)}
{filtered.length > 0 && (
<div className="wcs-list">
{filtered.map(r => <ConsensusCard key={r.marketId} r={r} />)}
</div>
)}
</div>
);
}
function InsiderCard({ r }: { r: WhaleInsider }) {
const navigate = useNavigate();
const { t, lang } = useT();
const [enterOpen, setEnterOpen] = useState(false);
const yes = r.outcome === 'Yes';
const fresh = freshness(r.lastTradeAt, t);
let deltaEl = null;
if (r.currentPrice != null && r.avgPrice > 0) {
const pct = ((r.currentPrice - r.avgPrice) / r.avgPrice) * 100;
const abs = Math.abs(Math.round(pct));
const cls = pct > 2 ? 'flow-delta-bad' : pct < -2 ? 'flow-delta-good' : 'flow-delta-neutral';
const label = pct > 2 ? t('flow.delta.pricier', { p: abs }) : pct < -2 ? t('flow.delta.cheaper', { p: abs }) : t('flow.delta.same');
deltaEl = (
<div className="flow-delta">
<span className="flow-delta-track">{t('flow.entryNow', { a: Math.round(r.avgPrice * 100), c: Math.round(r.currentPrice * 100) })}</span>
<span className={`flow-delta-tag ${cls}`}>{label}</span>
</div>
);
}
return (
<div className={`wcs-card ${yes ? 'wf-dir-yes' : 'wf-dir-no'}`} onClick={() => navigate(`/market/${r.marketId}`)}>
<div className="wcs-head">
<span className={`wf-bet ${yes ? 'wf-bet-yes' : 'wf-bet-no'}`}>🐋 {whaleCodename(r.address, lang)} → {r.outcome}</span>
<span className="wcs-flow">{formatVolume(r.amountUsd)}</span>
</div>
<div className="wcs-q">{r.question || 'Unknown market'}</div>
{deltaEl}
<div className="wcs-meta">
{r.winRate != null && <span className="flow-fresh-new">🎯 WR {Math.round(r.winRate * 100)}%</span>}
<span>·</span>
<span>{t('whales.avgEntry', { c: Math.round(r.avgPrice * 100) })}</span>
{fresh && (<><span>·</span><span className={fresh.isNew ? 'flow-fresh-new' : ''}>{fresh.isNew ? '🆕 ' : ''}{fresh.text}</span></>)}
</div>
<button className="flow-enter-btn" onClick={e => { e.stopPropagation(); setEnterOpen(true); }}>{t('flow.enter')}</button>
{enterOpen && (
<EnterTradeModal marketId={r.marketId} question={r.question || 'Unknown market'} side={yes ? 'yes' : 'no'} onClose={() => setEnterOpen(false)} />
)}
</div>
);
}
function InsiderFeed() {
const { t } = useT();
const [hours, setHours] = useState(24);
const { rows, loading } = useWhaleInsider({ hours, minAmount: 5000, limit: 40 });
return (
<div className="flow-consensus">
<div className="flow-filters">
<div className="flow-filter-group">
{[24, 72, 168].map(h => (
<button key={h} className={`pill pill-sm ${hours === h ? 'pill-active' : ''}`} onClick={() => setHours(h)}>
{h === 168 ? `7${t('flow.dayShort')}` : `${h}${t('flow.hourShort')}`}
</button>
))}
</div>
</div>
{loading && rows.length === 0 && (
<div className="wf-skeleton">{Array.from({ length: 5 }).map((_, i) => <div key={i} className="wf-skeleton-row" />)}</div>
)}
{!loading && rows.length === 0 && (
<div className="wf-empty"><div className="wf-empty-icon">🆕</div><div className="wf-empty-title">{t('flow.insiderEmpty')}</div></div>
)}
{rows.length > 0 && (
<div className="wcs-list">{rows.map(r => <InsiderCard key={r.marketId + r.address + r.outcome} r={r} />)}</div>
)}
</div>
);
}
export default function FlowPage() {
const { t } = useT();
const [tab, setTab] = useState<FlowTab>('consensus');
const tabs = [
{ key: 'consensus', label: t('flow.tabConsensus') },
{ key: 'insider', label: t('flow.tabInsider') },
{ key: 'fire', label: t('flow.tabFire') },
];
return (
<div className="whales-page">
<h2 className="sig-title">{t('flow.title')}</h2>
<p className="sig-desc">{t('flow.desc')}</p>
<SubTabs tabs={tabs} active={tab} onChange={k => setTab(k as FlowTab)} />
{tab === 'consensus' && <Consensus />}
{tab === 'insider' && <InsiderFeed />}
{tab === 'fire' && <EdgeScanner />}
</div>
);
}
📜 Git History
b9c19bfchore(poli): reconcile local Flow/Insider/manual-trade work with deployed state3 days ago
eb6fff8feat(poli): Flow chunk 3 — green «Войти» button + trade modal4 days ago
f5cc0aefeat(poli): Flow consensus chunk 2 — entry→now delta, freshness, sort, filters4 days ago
c545cc4feat(poli): add Поток (Flow) section — 5th nav item, /flow route, Консенсус/Инсайдер/Жар sub-tabs4 days ago
Show last diff
Loading...