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