@@ -27,7 +27,18 @@ export function renderUi(prefix, opts = {}) {
2727 const brand = typeof opts . brand === 'string' && opts . brand ? opts . brand : 'Markets' ;
2828 const tagline = typeof opts . tagline === 'string' && opts . tagline ? opts . tagline : 'prediction markets on your pod' ;
2929 // Social/OG affordances for standalone hosts; harmless defaults for JSS.
30- const ogTitle = `${ brand } — ${ tagline } ` ;
30+ const mk = opts . market && typeof opts . market === 'object' ? opts . market : null ;
31+ const esc0 = ( v ) => String ( v == null ? '' : v )
32+ . replace ( / & / g, '&' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) . replace ( / " / g, '"' ) ;
33+ // A shared market should unfurl as the QUESTION and its live odds — that
34+ // is the whole reason someone sends the link.
35+ const mkOdds = mk ? mk . outcomes
36+ . map ( ( o , i ) => `${ o } ${ ( mk . prices [ i ] * 100 ) . toFixed ( 0 ) } %` ) . slice ( 0 , 4 ) . join ( ' · ' ) : '' ;
37+ const mkWhen = mk ? ( ( ) => {
38+ const d = Math . round ( ( mk . closesAt - Date . now ( ) ) / 86400000 ) ;
39+ return mk . status === 'open' && d > 0 ? `closes in ${ d } d` : mk . status ;
40+ } ) ( ) : '' ;
41+ const ogTitle = mk ? esc0 ( mk . title ) : `${ brand } — ${ tagline } ` ;
3142 const ogImage = typeof opts . ogImage === 'string' ? opts . ogImage : '' ;
3243 const ogUrl = typeof opts . ogUrl === 'string' ? opts . ogUrl : '' ;
3344 const favicon = typeof opts . favicon === 'string' && opts . favicon ? opts . favicon
@@ -37,12 +48,15 @@ export function renderUi(prefix, opts = {}) {
3748 `<meta property="og:type" content="website">` ,
3849 `<meta property="og:site_name" content="${ brand } ">` ,
3950 `<meta property="og:title" content="${ ogTitle } ">` ,
40- `<meta property="og:description" content="Play-money prediction markets: bet paper credits, watch the odds move, climb the leaderboard, ask your own questions.">` ,
41- ogUrl ? `<meta property="og:url" content="${ ogUrl } ">` : '' ,
51+ `<meta property="og:description" content="${ mk
52+ ? esc0 ( `${ mkOdds } — ${ mkWhen } ${ mk . category ? ` · ${ mk . category } ` : '' } · play money on ${ brand } ` )
53+ : 'Play-money prediction markets: bet paper credits, watch the odds move, climb the leaderboard, ask your own questions.' } ">`,
54+ ogUrl ? `<meta property="og:url" content="${ ogUrl } ${ prefix } ${ mk ? `/m/${ esc0 ( mk . id ) } ` : '/' } ">` : '' ,
4255 ogImage ? `<meta property="og:image" content="${ ogImage } ">` : '' ,
4356 ogImage ? `<meta property="og:image:width" content="1200">\n<meta property="og:image:height" content="630">` : '' ,
4457 `<meta name="twitter:card" content="${ ogImage ? 'summary_large_image' : 'summary' } ">` ,
4558 `<meta name="twitter:title" content="${ ogTitle } ">` ,
59+ mk ? `<meta name="twitter:description" content="${ esc0 ( `${ mkOdds } — ${ mkWhen } ` ) } ">` : '' ,
4660 ogImage ? `<meta name="twitter:image" content="${ ogImage } ">` : '' ,
4761 ] . filter ( Boolean ) . join ( '\n' ) ;
4862 const P = JSON . stringify ( prefix ) ;
@@ -479,7 +493,8 @@ ${ogMeta}
479493
480494 <!-- ========================== detail view ========================== -->
481495 <div id="detail-view" class="hidden">
482- <p><a href="#" id="back">← All markets</a></p>
496+ <p style="display:flex;align-items:center;gap:var(--s3)"><a href="#" id="back">← All markets</a>
497+ <button type="button" id="share" class="ghost" style="font-size:var(--f-sm)">Copy link</button></p>
483498 <div class="cols">
484499 <aside class="rail">
485500 <div class="ticket" id="ticket">
@@ -1417,9 +1432,17 @@ ${ogMeta}
14171432 async function route() {
14181433 renderBoard();
14191434 const h = location.hash;
1420- if (h.startsWith('#m/')) return renderDetail(h.slice(3), true);
1435+ if (h.startsWith('#m/')) {
1436+ const id = h.slice(3);
1437+ // Make the ADDRESS BAR shareable: most people copy the URL rather
1438+ // than press a button, and the path form is the one that unfurls
1439+ // with this market's question and odds.
1440+ try { history.replaceState(null, '', PREFIX + '/m/' + id); } catch { /* file:// etc */ }
1441+ return renderDetail(id, true);
1442+ }
14211443 $('detail-view').classList.add('hidden');
14221444 $('list-view').classList.remove('hidden');
1445+ try { if (location.pathname !== (PREFIX || '/')) history.replaceState(null, '', (PREFIX || '/')); } catch { /* ignore */ }
14231446 document.title = ${ JSON . stringify ( brand + ' — ' + tagline ) } ;
14241447 current = null; cursor = null; paged = false;
14251448 $('d-position').dataset.shape = '';
@@ -1486,6 +1509,15 @@ ${ogMeta}
14861509 $('acct-pass').addEventListener('keydown', (e) => { if (e.key === 'Enter') acctAuth('/login'); });
14871510 }
14881511 $('back').onclick = (e) => { e.preventDefault(); location.hash = ''; };
1512+ // The share URL is the PATH form (/m/<id>), which the server renders with
1513+ // this market's question and odds in its meta — a hash link would unfurl
1514+ // as the generic site card.
1515+ $('share').onclick = async () => {
1516+ if (!current) return;
1517+ const url = location.origin + PREFIX + '/m/' + current.id;
1518+ try { await navigator.clipboard.writeText(url); toast('Link copied'); }
1519+ catch { window.prompt('Copy this link', url); }
1520+ };
14891521 $('t-buy').onclick = () => {
14901522 if (!me) {
14911523 $('auth-card').classList.remove('hidden');
@@ -1581,6 +1613,17 @@ ${ogMeta}
15811613 : 'Escrow: —';
15821614 };
15831615 $('c-outcomes').oninput = $('c-b').oninput = escrowPreview;
1616+ // Arrived via a share link? Hand off to the hash router.
1617+ (function shareEntry() {
1618+ // Both server-visible forms are honoured: the path (/m/<id>) and the
1619+ // query (?m=<id>). A fragment never reaches the server, which is why
1620+ // shared #m/ links unfurled as the generic card.
1621+ const byPath = /\/m\/([A-Za-z0-9_-]+)\/?$/.exec(location.pathname);
1622+ const byQuery = new URLSearchParams(location.search).get('m');
1623+ const id = (byPath && byPath[1]) || byQuery;
1624+ if (!id || location.hash) return;
1625+ history.replaceState(null, '', (PREFIX || '/') + '#m/' + id);
1626+ })();
15841627 window.addEventListener('hashchange', () => route());
15851628
15861629 // Live feed, with reconnect. Only re-render what is on screen.
0 commit comments