โ† Back
โ˜†
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...