← Back
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...