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