import React, { useState } from 'react';
import type { Market } from '../../types/market';
import type { PriceUpdate } from '../../hooks/useWebSocket';
import { formatVolume, formatPrice, formatSpread, formatEndDate, timeUntil, getMarketTitle } from '../../utils/format';
import DetailsPanel from './DetailsPanel';
import { useT } from '../../i18n/LanguageContext';
interface Props {
markets: Market[];
loading: boolean;
sortCol: string;
sortDir: string;
onSort: (col: string) => void;
livePrices?: Map<string, PriceUpdate>;
changedIds?: Set<string>;
}
const columns = [
{ key: 'question', label: 'Market', sortable: false, className: 'col-market' },
{ key: 'category', label: 'Category', sortable: false, className: 'col-category' },
{ key: 'yes_price', label: 'YES', sortable: true, className: 'col-price' },
{ key: 'spread', label: 'Spread', sortable: true, className: 'col-spread' },
{ key: 'volume', label: 'Volume 24h', sortable: true, className: 'col-volume' },
{ key: 'liquidity', label: 'Liquidity', sortable: true, className: 'col-liquidity' },
{ key: 'end_date', label: 'Ends', sortable: true, className: 'col-ends' },
];
function PriceCell({ price, flash }: { price: number; flash?: 'up' | 'down' | null }) {
const pct = price * 100;
const color = pct >= 70 ? 'var(--profit)' : pct <= 30 ? 'var(--loss)' : 'var(--text-primary)';
const cls = flash === 'up' ? 'price-flash-up' : flash === 'down' ? 'price-flash-down' : '';
return <span className={cls} style={{ color, fontWeight: 600 }}>{formatPrice(price)}</span>;
}
export function CategoryBadge({ category }: { category: string }) {
const colors: Record<string, string> = {
Sports: '#f59e0b',
Politics: '#8b5cf6',
Crypto: '#f97316',
Weather: '#06b6d4',
Tech: '#3b82f6',
Finance: '#10b981',
Culture: '#ec4899',
Science: '#14b8a6',
Geopolitics: '#ef4444',
Elections: '#a855f7',
};
const bg = colors[category] || '#6b7280';
return (
<span className="badge" style={{ background: `${bg}20`, color: bg, borderColor: `${bg}40` }}>
{category}
</span>
);
}
function SkeletonRows({ count = 8 }: { count?: number }) {
return (
<>
{Array.from({ length: count }, (_, i) => (
<tr key={i} className="skeleton-row">
<td className="col-market">
<div className="market-cell">
<div className="skeleton-img" />
<div className="skeleton-text" style={{ width: `${55 + (i % 4) * 10}%` }} />
</div>
</td>
<td className="col-category"><div className="skeleton-badge" /></td>
<td className="col-price"><div className="skeleton-text skeleton-short" /></td>
<td className="col-spread"><div className="skeleton-text skeleton-short" /></td>
<td className="col-volume"><div className="skeleton-text skeleton-short" /></td>
<td className="col-liquidity"><div className="skeleton-text skeleton-short" /></td>
<td className="col-ends"><div className="skeleton-text skeleton-short" /></td>
</tr>
))}
</>
);
}
function SkeletonCards({ count = 6 }: { count?: number }) {
return (
<div className="mc-grid">
{Array.from({ length: count }, (_, i) => (
<div key={i} className="mc-card mc-skeleton">
<div className="mc-head">
<div className="skeleton-img" />
<div className="skeleton-text" style={{ flex: 1, height: 14 }} />
</div>
<div className="mc-stats">
<div className="skeleton-text" style={{ width: 40, height: 20 }} />
<div className="skeleton-text" style={{ width: 50, height: 12 }} />
</div>
</div>
))}
</div>
);
}
function MarketCard({ market, expanded, onToggle, livePrice, flash }: { market: Market; expanded: boolean; onToggle: () => void; livePrice?: number; flash?: 'up' | 'down' | null }) {
const { t } = useT();
const price = livePrice ?? market.yes_price;
const pct = price * 100;
const priceColor = pct >= 70 ? 'var(--profit)' : pct <= 30 ? 'var(--loss)' : 'var(--text-primary)';
const flashCls = flash === 'up' ? 'price-flash-up' : flash === 'down' ? 'price-flash-down' : '';
const pctRounded = Math.round(Math.max(0, Math.min(100, pct)));
const endsIn = timeUntil(market.end_date);
return (
<div className={`mc-card ${expanded ? 'mc-card-expanded' : ''}`}>
<div className="mc-body" onClick={onToggle}>
<div className="mc-head">
{market.image_url && (
<img src={market.image_url} alt="" className="mc-img" loading="lazy"
onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }} />
)}
<span className="mc-title">{getMarketTitle(market)}</span>
</div>
{/* Implied probability: a bar makes the bare ยข price legible at a glance */}
<div className="mc-prob">
<div className="mc-prob-bar">
<div className="mc-prob-fill" style={{ width: `${pctRounded}%`, background: priceColor }} />
</div>
<span className={`mc-prob-val ${flashCls}`} style={{ color: priceColor }}>
{formatPrice(price)} YES
</span>
</div>
<div className="mc-meta-row">
<CategoryBadge category={market.category} />
<span className="mc-stat">{t('mkt.volShort')} {formatVolume(market.volume_24h)}</span>
<span className="mc-stat">{t('mkt.liqShort')} {formatVolume(market.liquidity)}</span>
<span className="mc-stat">{t('mkt.spreadShort')} {formatSpread(market.spread)}</span>
{endsIn && <span className="mc-stat mc-stat-ends">โณ {endsIn}</span>}
</div>
</div>
{expanded && (
<div className="mc-details">
<DetailsPanel marketId={market.id} onClose={onToggle} />
</div>
)}
</div>
);
}
export default function MarketsTable({ markets, loading, sortCol, sortDir, onSort, livePrices, changedIds }: Props) {
const [expandedId, setExpandedId] = useState<string | null>(null);
// Helper: get live price or fallback to REST price
const getPrice = (m: Market) => {
const live = livePrices?.get(m.id);
return live ? live.yes : m.yes_price;
};
// Helper: determine flash direction for a market
const getFlash = (m: Market): 'up' | 'down' | null => {
if (!changedIds?.has(m.id) || !livePrices) return null;
const live = livePrices.get(m.id);
if (!live) return null;
return live.yes > m.yes_price ? 'up' : live.yes < m.yes_price ? 'down' : null;
};
const toggleExpand = (id: string) => {
setExpandedId(prev => prev === id ? null : id);
};
if (loading) {
return (
<>
{/* Mobile cards skeleton */}
<div className="mc-mobile-only">
<SkeletonCards />
</div>
{/* Desktop table skeleton */}
<div className="table-container mc-desktop-only">
<table className="markets-table">
<thead>
<tr>
{columns.map(col => (
<th key={col.key} className={col.className}>{col.label}</th>
))}
</tr>
</thead>
<tbody>
<SkeletonRows />
</tbody>
</table>
</div>
</>
);
}
if (markets.length === 0) {
return (
<div className="table-container">
<div className="table-empty">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" width="40" height="40">
<circle cx="11" cy="11" r="8" />
<path d="M21 21l-4.35-4.35" />
<path d="M8 11h6" />
</svg>
<p>No markets found</p>
<span>Try adjusting your filters or search query</span>
</div>
</div>
);
}
return (
<>
{/* Mobile card view */}
<div className="mc-mobile-only">
<div className="mc-grid">
{markets.map(m => (
<MarketCard
key={m.id}
market={m}
expanded={expandedId === m.id}
onToggle={() => toggleExpand(m.id)}
livePrice={livePrices?.get(m.id)?.yes}
flash={getFlash(m)}
/>
))}
</div>
</div>
{/* Desktop table view */}
<div className="table-container mc-desktop-only">
<table className="markets-table">
<thead>
<tr>
{columns.map(col => (
<th
key={col.key}
className={`${col.className} ${col.sortable ? 'sortable' : ''} ${sortCol === col.key ? 'sorted' : ''}`}
onClick={() => col.sortable && onSort(col.key)}
>
{col.label}
{col.sortable && sortCol === col.key && (
<span className="sort-arrow">{sortDir === 'desc' ? ' โ' : ' โ'}</span>
)}
</th>
))}
</tr>
</thead>
<tbody>
{markets.map(m => (
<React.Fragment key={m.id}>
<tr
className={`market-row ${expandedId === m.id ? 'market-row-expanded' : ''}`}
onClick={() => toggleExpand(m.id)}
>
<td className="col-market">
<div className="market-cell">
{m.image_url && (
<img
src={m.image_url}
alt=""
className="market-img"
loading="lazy"
onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
)}
<span className="market-question" title={m.question}>
{getMarketTitle(m)}
</span>
<svg className="expand-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
<path d="M6 9l6 6 6-6" />
</svg>
</div>
</td>
<td className="col-category">
<CategoryBadge category={m.category} />
</td>
<td className="col-price">
<PriceCell price={getPrice(m)} flash={getFlash(m)} />
</td>
<td className="col-spread">
{formatSpread(m.spread)}
</td>
<td className="col-volume">
{formatVolume(m.volume_24h)}
</td>
<td className="col-liquidity">
{formatVolume(m.liquidity)}
</td>
<td className="col-ends">
{formatEndDate(m.end_date)}
</td>
</tr>
{expandedId === m.id && (
<tr key={`${m.id}-details`} className="details-row">
<td colSpan={columns.length}>
<DetailsPanel marketId={m.id} onClose={() => setExpandedId(null)} />
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
</div>
</>
);
}
๐ Git History
6c47fa4chore: local Polikopi project home + Phase 1 redesign artifacts12 days ago
Show last diff
Loading...