From f33497351773082d2569556dcbf55918a101cc3d Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 23 Apr 2026 13:28:07 -0700 Subject: [PATCH] keeps: permalinks for the mint modal + link Token #N to objkt /kept/\$code and /unkept/\$code now open the respective gallery tab and auto-open the mint modal for that piece. Uses the existing pending-deep-link pattern (pendingMintModalCode) and resolves against allCodes once the index renders. openMintModal pushes history entry, closeMintModal restores tab root, popstate keeps modal state in sync with nav. Distinct from the bare /\$code market-detail permalink so both behaviors coexist. Also lands the small pre-existing UX tweak in the kept index: the Token #N chip is now an anchor pointing at objkt.com/tokens/{contract}/{tokenId}, with an underline-on-hover style and event.stopPropagation so clicking it doesn't also open the mint modal. Co-Authored-By: Claude Opus 4.7 (1M context) --- system/public/kidlisp.com/keeps.html | 85 ++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 11 deletions(-) diff --git a/system/public/kidlisp.com/keeps.html b/system/public/kidlisp.com/keeps.html index 171a5f6eda..1cccfb2bd3 100644 --- a/system/public/kidlisp.com/keeps.html +++ b/system/public/kidlisp.com/keeps.html @@ -2310,7 +2310,9 @@ font-size: 12px; color: #8658e2; margin-left: auto; + text-decoration: none; } + a.keeps-row-token:hover { text-decoration: underline; } .keeps-row-hits { font-family: var(--font-mono); font-size: 12px; @@ -3962,13 +3964,18 @@ ? (knownOwner.alias || knownOwner.address.slice(0, 8) + '…' + knownOwner.address.slice(-4)) : '…'; const ownerHref = knownOwner?.address ? `https://objkt.com/profile/${knownOwner.address}` : null; + const objktTokenHref = (activeKeepInfo?.contractAddress && tokenId != null) + ? `${getObjktBase(activeKeepInfo.network)}/tokens/${activeKeepInfo.contractAddress}/${tokenId}` + : null; row.innerHTML = ` $${entry.code} $${entry.code} ${escapeHtml(handle)} ${hits.toLocaleString()} hit${hits !== 1 ? 's' : ''} - ${tokenId != null ? `Token #${tokenId}` : ''} + ${tokenId != null ? (objktTokenHref + ? `Token #${tokenId}` + : `Token #${tokenId}`) : ''} ${tokenId != null ? `${ownerHref ? `${escapeHtml(ownerLabel)}` : ownerLabel}` : ''} ${keptTimeStr ? `kept ${keptTimeStr}` : ''} ${createdTimeStr ? `made ${createdTimeStr}` : ''}`; @@ -4083,6 +4090,18 @@ } grid.appendChild(frag); + + // Deep-link: if the URL points at /kept/$code or /unkept/$code, open + // the mint modal for that piece once we have the entry in allCodes. + if (pendingMintModalCode) { + const match = allCodes.find(e => e.code === pendingMintModalCode); + if (match) { + const code = pendingMintModalCode; + pendingMintModalCode = null; + // Defer to next tick so the current render pass completes first. + setTimeout(() => openMintModal(match), 0); + } + } } function renderPaginationControls() { @@ -4315,7 +4334,8 @@ // KEEP DETAIL MODAL (permalink) // ========================================= let detailSortedEntries = []; // populated by renderMarket for index-based lookup - let pendingDeepLinkCode = null; // set on load if URL is /$code + let pendingDeepLinkCode = null; // set on load if URL is /$code (detail modal) + let pendingMintModalCode = null; // set on load if URL is /kept/$code or /unkept/$code const detailOverlay = document.getElementById('keep-detail-overlay'); const detailContent = document.getElementById('keep-detail-content'); const detailTitle = document.getElementById('keep-detail-title'); @@ -4867,16 +4887,23 @@ const pageParam = new URLSearchParams(location.search).get('page'); const page = pageParam ? Math.max(0, parseInt(pageParam, 10) - 1) : 0; if (isKeepsDomain) { - const seg = location.pathname.replace(/^\/+/, '').split('/')[0]; - // Redirect /buy, /market, and /$code permalinks to buy.kidlisp.com - if (seg === 'buy' || seg === 'market') { + const parts = location.pathname.replace(/^\/+/, '').split('/'); + const seg1 = parts[0] || ''; + const seg2 = parts[1] || ''; + // Redirect /buy and /market to buy.kidlisp.com + if (seg1 === 'buy' || seg1 === 'market') { window.location.href = 'https://buy.kidlisp.com'; return { tab: 'unkept', code: null, page: 0 }; } - if (seg.startsWith('$') && seg.length > 1) { - return { tab: 'unkept', code: seg.slice(1), page }; + // /kept/$code and /unkept/$code → gallery tab with mint modal open + // for that piece. Distinct from the bare /$code market-detail permalink. + if ((seg1 === 'kept' || seg1 === 'unkept') && seg2.startsWith('$') && seg2.length > 1) { + return { tab: seg1, code: seg2.slice(1), page, mintOpen: true }; + } + if (seg1.startsWith('$') && seg1.length > 1) { + return { tab: 'unkept', code: seg1.slice(1), page }; } - const tab = seg === 'kept' ? 'kept' : 'unkept'; + const tab = seg1 === 'kept' ? 'kept' : 'unkept'; return { tab, code: null, page }; } else { const hash = location.hash.replace(/^#/, ''); @@ -4889,7 +4916,11 @@ } } const initialRoute = tabFromLocation(); - if (initialRoute.code) pendingDeepLinkCode = initialRoute.code; + if (initialRoute.mintOpen && initialRoute.code) { + pendingMintModalCode = initialRoute.code; + } else if (initialRoute.code) { + pendingDeepLinkCode = initialRoute.code; + } switchTab(initialRoute.tab); // Show wallet button immediately; check if already connected @@ -4899,13 +4930,25 @@ // Handle browser back/forward window.addEventListener('popstate', () => { const route = tabFromLocation(); - // Close detail modal if navigating away from a $code permalink + const mintOverlayEl = document.getElementById('mint-overlay'); + const mintOpenNow = mintOverlayEl?.classList.contains('open'); + // Close modals whose permalink context no longer matches the route. if (!route.code && detailOverlay.classList.contains('open')) { detailOverlay.classList.remove('open'); document.body.style.overflow = ''; } + if (!route.mintOpen && mintOpenNow) { + closeMintModal(); + } switchTab(route.tab); - if (route.code) openKeepDetailByCode(route.code); + // Open the correct modal for the new URL. + if (route.mintOpen && route.code) { + const match = allCodes.find(e => e.code === route.code); + if (match) openMintModal(match); + else pendingMintModalCode = route.code; + } else if (route.code) { + openKeepDetailByCode(route.code); + } if (route.page > 0 && route.page < totalPages) { goToPage(route.page, { pushState: false }); } @@ -5987,6 +6030,18 @@ clearTrackLog(); document.getElementById('mint-overlay').classList.add('open'); document.body.style.overflow = 'hidden'; + + // Permalink: reflect the open mint modal in the URL so sharing /kept/$code + // or /unkept/$code reopens this exact view. Only push when the URL isn't + // already at this target (avoids duplicate history entries when the modal + // was opened via the initial-route deep link). + if (isKeepsDomain && entry?.code) { + const tabPath = activeTab === 'kept' ? '/kept' : activeTab === 'unkept' ? '/unkept' : ''; + const targetPath = `${tabPath}/$${entry.code}`; + if (location.pathname !== targetPath) { + history.pushState({ tab: activeTab, code: entry.code, mintOpen: true }, '', targetPath); + } + } } function beginMintFlow() { @@ -6002,6 +6057,14 @@ mintAutoSyncAfterRebake = false; document.getElementById('mint-overlay').classList.remove('open'); document.body.style.overflow = ''; + + // Permalink: restore URL to tab root when the modal closes. Matches + // /kept/$code or /unkept/$code (and bare /$code inside unkept context) + // so direct-link exit → back button feels right. + if (isKeepsDomain && /^\/(kept|unkept)?\/?\$[^/]+$/.test(location.pathname)) { + const tabPath = activeTab === 'kept' ? '/kept' : '/'; + history.pushState({ tab: activeTab }, '', tabPath); + } if (mintPollTimer) { clearInterval(mintPollTimer); mintPollTimer = null; } if (mintAbortController) { mintAbortController.abort(); -- 2.51.2