← Back
import { useNavigate } from 'react-router-dom';
import { useT } from '../../i18n/LanguageContext';
import type { SignalCard as Signal } from '../../hooks/useSignalsFeed';
import { formatVolume, formatPrice, timeAgo, timeUntil } from '../../utils/format';

interface Props {
  signal: Signal;
}

// Mini progress bar for a 0..1 score.
function ScaleBar({ label, value, cls }: { label: string; value: number; cls: string }) {
  const pct = Math.round(Math.max(0, Math.min(1, value)) * 100);
  return (
    <div className="sc-scale">
      <div className="sc-scale-head">
        <span className="sc-scale-label">{label}</span>
        <span className="sc-scale-val">{pct}%</span>
      </div>
      <div className="sc-scale-track">
        <div className={`sc-scale-fill ${cls}`} style={{ width: `${pct}%` }} />
      </div>
    </div>
  );
}

export default function SignalCard({ signal }: Props) {
  const navigate = useNavigate();
  const { t } = useT();

  const badge =
    signal.type === 'WHALE'
      ? { icon: '🐋', label: t('sig.badgeWhale'), cls: 'sc-badge-whale' }
      : signal.type === 'LONGSHOT'
        ? { icon: '🎯', label: t('sig.badgeFade'), cls: 'sc-badge-longshot' }
        : signal.type === 'MOMENTUM'
          ? { icon: '⚡', label: t('sig.badgeMomentum'), cls: 'sc-badge-momentum' }
          : { icon: '📈', label: t('sig.badgeSpike'), cls: 'sc-badge-spike' };

  let description: string;
  let metricLabel: string;
  let metricValue: string;
  let imageUrl: string | null = null;
  // Directional types carry a recommended side + its market-implied probability.
  let recSide: 'YES' | 'NO' | null = null;
  let impliedProb: number | null = null;
  let entryQuality: number | null = null;
  // WHALE-only: avg historical win-rate of the whales behind the signal, plus a
  // derived "smart money" tier chip when that WR is strong (>=60%).
  let wrPct: number | null = null;
  let smartMoney = false;

  if (signal.type === 'WHALE') {
    const side = signal.signal === 'BUY_YES' ? 'YES' : 'NO';
    const pct =
      signal.signal === 'BUY_YES'
        ? Math.round(signal.whaleYesPct * 100)
        : Math.round((1 - signal.whaleYesPct) * 100);
    // Show the real cluster size ($ volume + trade count) when available — more
    // honest than a bald "100%". Fall back to conviction phrasing for older rows
    // scored before whale_count/volume were populated (count === 0).
    if (signal.whaleCount > 0) {
      const n = signal.whaleCount;
      description = t('sig.smartTo', { side, vol: formatVolume(signal.whaleVolumeUsd), n, trades: t(n === 1 ? 'sig.tradeOne' : 'sig.tradeMany') });
    } else {
      const conviction = pct >= 100 ? t('sig.convAll') : t('sig.convPct', { pct });
      description = t('sig.smartConv', { side, conv: conviction });
    }
    metricLabel = t('sig.priceYes');
    metricValue = formatPrice(signal.marketPrice);
    imageUrl = signal.imageUrl;
    recSide = signal.recommendedSide;
    impliedProb = signal.impliedProb;
    entryQuality = signal.entryQuality;
    if (signal.whaleWr != null) {
      wrPct = Math.round(signal.whaleWr * 100);
      smartMoney = signal.whaleWr >= 0.6;
    }
  } else if (signal.type === 'LONGSHOT') {
    const side = signal.signal === 'BUY_YES' ? 'YES' : 'NO';
    description = t('sig.longshotDesc', { side });
    metricLabel = t('sig.priceYes');
    metricValue = formatPrice(signal.marketPrice);
    imageUrl = signal.imageUrl;
    recSide = signal.recommendedSide;
    impliedProb = signal.impliedProb;
    entryQuality = signal.entryQuality;
  } else if (signal.type === 'MOMENTUM') {
    description = t('sig.spikeDesc');
    metricLabel = t('sig.priceYes');
    metricValue = formatPrice(signal.marketPrice);
    imageUrl = signal.imageUrl;
    entryQuality = signal.entryQuality;
  } else {
    description = t('sig.volDesc', { score: (signal.score ?? 0).toFixed(1) });
    metricLabel = t('sig.vol24');
    metricValue = formatVolume(signal.volume24h);
  }

  const ago = timeAgo(signal.detectedAt);
  const endsIn = timeUntil(signal.endDate);

  // CTA: WHALE/LONGSHOT preselect the recommended side in the order form;
  // SPIKE/MOMENTUM have no direction, so they just open the market detail.
  const cta =
    (signal.type === 'WHALE' || signal.type === 'LONGSHOT')
      ? signal.signal === 'BUY_YES'
        ? { label: t('sig.buyYes'), cls: 'sc-cta-yes', to: `/market/${signal.marketId}?side=yes` }
        : { label: t('sig.buyNo'), cls: 'sc-cta-no', to: `/market/${signal.marketId}?side=no` }
      : { label: t('sig.more'), cls: 'sc-cta-neutral', to: `/market/${signal.marketId}` };

  const onCta = (e: React.MouseEvent) => {
    e.stopPropagation();
    navigate(cta.to);
  };

  return (
    <div className="sc-card" onClick={() => navigate(`/market/${signal.marketId}`)}>
      <div className="sc-top">
        {imageUrl && <img className="sc-img" src={imageUrl} alt="" loading="lazy" />}
        <div className="sc-top-text">
          <span className="sc-badges">
            <span className={`sc-badge ${badge.cls}`}>
              {badge.icon} {badge.label}
            </span>
            {smartMoney && <span className="sc-badge sc-badge-smart">{t('sig.smartBadge')}</span>}
          </span>
          <div className="sc-title">{signal.question ?? 'Unknown market'}</div>
        </div>
      </div>

      <div className="sc-desc">{description}</div>

      {recSide && (
        <div className="sc-bet">
          <span className={`sc-bet-side sc-bet-${recSide.toLowerCase()}`}>{t('sig.bet', { side: recSide })}</span>
          {impliedProb != null && (
            <span className="sc-bet-prob">{t('sig.prob', { p: Math.round(impliedProb * 100) })}</span>
          )}
        </div>
      )}

      <div className="sc-scales">
        <ScaleBar label={t('sig.strength')} value={signal.strength} cls="sc-fill-strength" />
        {entryQuality != null && (
          <ScaleBar label={t('sig.entryQuality')} value={entryQuality} cls="sc-fill-entry" />
        )}
      </div>

      <div className="sc-footer">
        <div className="sc-metric">
          <span className="sc-metric-label">{metricLabel}</span>
          <span className="sc-metric-value">{metricValue}</span>
        </div>
        <div className="sc-meta">
          {wrPct != null && <span className="sc-wr">{t('sig.whaleWr', { wr: wrPct })}</span>}
          {endsIn && <span className="sc-ends">⏳ {endsIn}</span>}
          {signal.category && <span className="sc-cat">{signal.category}</span>}
          {ago && <span className="sc-ago">{ago}</span>}
        </div>
      </div>

      <button className={`sc-cta ${cta.cls}`} onClick={onCta}>
        {cta.label}
      </button>
    </div>
  );
}

📜 Git History

6c47fa4chore: local Polikopi project home + Phase 1 redesign artifacts12 days ago
Show last diff
Loading...