← Back
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import type { Market, MarketDetail } from '../types/market';
import { formatVolume, formatPrice, formatSpread, formatEndDate, getMarketTitle, getPolymarketUrl } from '../utils/format';
import { useAlerts } from '../hooks/useAlerts';
import PriceChart from '../components/screener/PriceChart';
import OrderbookMini from '../components/screener/OrderbookMini';
import '../redesign/editorial.css';

export default function MarketDetailPage() {
  const { id } = useParams<{ id: string }>();
  const navigate = useNavigate();
  const [market, setMarket] = useState<MarketDetail | null>(null);
  const [related, setRelated] = useState<Market[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [copied, setCopied] = useState(false);
  const [alertOpen, setAlertOpen] = useState(false);
  const [alertSide, setAlertSide] = useState<'yes' | 'no'>('yes');
  const [alertDir, setAlertDir] = useState<'above' | 'below'>('above');
  const [alertPrice, setAlertPrice] = useState('');
  const [alertSaving, setAlertSaving] = useState(false);
  const { createAlert } = useAlerts();

  useEffect(() => {
    if (!id) return;
    const t = setTimeout(() => {
      setLoading(true);
      setError(null);
      fetch(`/api/screener/market/${encodeURIComponent(id)}`)
        .then(r => r.json())
        .then(d => {
          if (d.success) {
            setMarket(d.data);
            // Fetch related markets by same event
            if (d.data.event_slug) {
              fetch(`/api/screener/markets?search=${encodeURIComponent(d.data.event_title)}&limit=6`)
                .then(r => r.json())
                .then(rd => {
                  if (rd.success) {
                    setRelated(rd.data.markets.filter((m: Market) => m.id !== id));
                  }
                })
                .catch(() => {});
            }
          } else {
            setError(d.error || 'Market not found');
          }
        })
        .catch(() => setError('Network error'))
        .finally(() => setLoading(false));
    }, 0);
    return () => { clearTimeout(t); };
  }, [id]);

  const handleShare = () => {
    navigator.clipboard.writeText(window.location.href).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    }).catch(() => {});
  };

  if (loading) {
    return (
      <div className="market-detail-page pk-market">
        <div className="detail-back-row">
          <button className="detail-back" onClick={() => navigate(-1)}>
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="18" height="18">
              <path d="M19 12H5M12 19l-7-7 7-7" />
            </svg>
            Back
          </button>
        </div>
        <div className="detail-hero">
          <div className="skeleton-line" style={{ width: '70%', height: 24 }} />
          <div className="skeleton-line" style={{ width: '40%', height: 16, marginTop: 8 }} />
        </div>
        <div className="details-prices" style={{ marginTop: 16 }}>
          <div className="skeleton-line" style={{ width: '100%', height: 64 }} />
          <div className="skeleton-line" style={{ width: '100%', height: 64 }} />
        </div>
      </div>
    );
  }

  if (error || !market || !id) {
    return (
      <div className="market-detail-page pk-market">
        <div className="detail-back-row">
          <button className="detail-back" onClick={() => navigate('/screener')}>
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="18" height="18">
              <path d="M19 12H5M12 19l-7-7 7-7" />
            </svg>
            Back to Screener
          </button>
        </div>
        <div className="table-empty" style={{ marginTop: 40 }}>
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" width="40" height="40">
            <circle cx="12" cy="12" r="10" />
            <line x1="12" y1="8" x2="12" y2="12" />
            <line x1="12" y1="16" x2="12.01" y2="16" />
          </svg>
          <p>{error || 'Market not found'}</p>
          <button className="page-btn" onClick={() => navigate('/screener')}>Go to Screener</button>
        </div>
      </div>
    );
  }

  const title = getMarketTitle(market);
  const polymarketUrl = getPolymarketUrl(market);

  return (
    <div className="market-detail-page">
      {/* Back + Share */}
      <div className="detail-back-row">
        <button className="detail-back" onClick={() => navigate(-1)}>
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="18" height="18">
            <path d="M19 12H5M12 19l-7-7 7-7" />
          </svg>
          Back
        </button>
        <button className="detail-share" onClick={handleShare}>
          {copied ? 'Copied!' : 'Share'}
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
            <circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" />
            <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
            <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
          </svg>
        </button>
      </div>

      {/* Desktop two-column layout */}
      <div className="detail-layout">
        {/* Main column: hero, prices, stats, chart */}
        <div className="detail-main">
          {/* Hero */}
          <div className="detail-hero">
            <div className="detail-hero-top">
              {market.image_url && (
                <img src={market.image_url} alt="" className="detail-hero-img"
                  onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }}
                />
              )}
              <div>
                <h1 className="detail-title">{title}</h1>
                <div className="detail-meta">
                  <span className="detail-category-badge">{market.category}</span>
                  <span className="detail-sep">|</span>
                  <span>Ends {formatEndDate(market.end_date)}</span>
                </div>
              </div>
            </div>
          </div>

          {/* Prices */}
          <div className="details-prices">
            <div className="price-block price-yes">
              <div className="price-label">YES</div>
              <div className="price-value">{formatPrice(market.yes_price)}</div>
            </div>
            <div className="price-block price-no">
              <div className="price-label">NO</div>
              <div className="price-value">{formatPrice(market.no_price)}</div>
            </div>
          </div>

          {/* Stats */}
          <div className="details-stats">
            <div className="stat-item">
              <span className="stat-label">Volume 24h</span>
              <span className="stat-value">{formatVolume(market.volume_24h)}</span>
            </div>
            <div className="stat-item">
              <span className="stat-label">Total Volume</span>
              <span className="stat-value">{formatVolume(market.volume_total)}</span>
            </div>
            <div className="stat-item">
              <span className="stat-label">Liquidity</span>
              <span className="stat-value">{formatVolume(market.liquidity)}</span>
            </div>
            <div className="stat-item">
              <span className="stat-label">Spread</span>
              <span className="stat-value">{formatSpread(market.spread)}</span>
            </div>
          </div>

          {/* Chart + Orderbook */}
          <div className="details-chart-ob">
            <PriceChart marketId={id} />
            <OrderbookMini marketId={id} />
          </div>

          {/* Tags */}
          {market.tags && market.tags.length > 0 && (
            <div className="details-tags">
              {market.tags.map(tag => (
                <span key={tag} className="details-tag">{tag}</span>
              ))}
            </div>
          )}
        </div>

        {/* Sidebar: alert, order form, links */}
        <div className="detail-sidebar">
          {/* Set Alert */}
          <div className="alert-section">
            {!alertOpen ? (
              <button className="alert-toggle-btn" onClick={() => setAlertOpen(true)}>
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
                  <path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9" />
                  <path d="M13.73 21a2 2 0 01-3.46 0" />
                </svg>
                Set Alert
              </button>
            ) : (
              <div className="alert-form">
                <div className="alert-form-row">
                  <select value={alertSide} onChange={e => setAlertSide(e.target.value as 'yes' | 'no')} className="alert-select">
                    <option value="yes">YES price</option>
                    <option value="no">NO price</option>
                  </select>
                  <select value={alertDir} onChange={e => setAlertDir(e.target.value as 'above' | 'below')} className="alert-select">
                    <option value="above">goes above</option>
                    <option value="below">goes below</option>
                  </select>
                  <div className="order-input-wrap" style={{ flex: 1 }}>
                    <input
                      type="number"
                      className="order-input"
                      placeholder={`${Math.round((alertSide === 'yes' ? market.yes_price : market.no_price) * 100)}`}
                      min="1" max="99" step="1"
                      value={alertPrice}
                      onChange={e => setAlertPrice(e.target.value)}
                    />
                    <span className="order-input-suffix">c</span>
                  </div>
                </div>
                <div className="alert-form-actions">
                  <button
                    className="alert-save-btn"
                    disabled={alertSaving || !alertPrice || Number(alertPrice) < 1 || Number(alertPrice) > 99}
                    onClick={async () => {
                      if (!id) return;
                      setAlertSaving(true);
                      const ok = await createAlert(id, alertSide, alertDir, Number(alertPrice) / 100);
                      setAlertSaving(false);
                      if (ok) {
                        setAlertOpen(false);
                        setAlertPrice('');
                      }
                    }}
                  >
                    {alertSaving ? 'Saving...' : 'Save Alert'}
                  </button>
                  <button className="alert-cancel-btn" onClick={() => { setAlertOpen(false); setAlertPrice(''); }}>
                    Cancel
                  </button>
                </div>
              </div>
            )}
          </div>

          {/* External link */}
          <div className="details-actions">
            <a href={polymarketUrl} target="_blank" rel="noopener noreferrer" className="btn-polymarket">
              View on Polymarket
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="14" height="14">
                <path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3" />
              </svg>
            </a>
          </div>
        </div>
      </div>

      {/* Related Markets */}
      {related.length > 0 && (
        <section className="detail-related">
          <h3>Related Markets</h3>
          <div className="hot-markets-grid">
            {related.slice(0, 4).map(m => (
              <div key={m.id} className="hot-card" onClick={() => navigate(`/market/${m.id}`)}>
                <div className="hot-card-top">
                  {m.image_url && (
                    <img src={m.image_url} alt="" className="hot-img" loading="lazy"
                      onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }}
                    />
                  )}
                  <span className="hot-category">{m.category}</span>
                </div>
                <div className="hot-question">
                  {getMarketTitle(m)}
                </div>
                <div className="hot-bottom">
                  <span className="hot-price">{formatPrice(m.yes_price)} YES</span>
                  <span className="hot-volume">{formatVolume(m.volume_24h)}</span>
                </div>
              </div>
            ))}
          </div>
        </section>
      )}
    </div>
  );
}

📜 Git History

9dfe057feat(poli): editorial Wallet/MarketDetail/Copy-subs + serif-var fix (chunk 8)10 days ago
6c47fa4chore: local Polikopi project home + Phase 1 redesign artifacts12 days ago
Show last diff
Loading...