From a457cd7fe10c80df5d022b3625bfe86c78ba5dee Mon Sep 17 00:00:00 2001 From: Jeffrey Alan Scudder Date: Sun, 22 Mar 2026 21:04:18 -0700 Subject: [PATCH] buy.kidlisp.com: replace Fresh/Sold tabs with Shop/Albums Shop shows active listings. Albums shows collector leaderboard ranked by keeps count, with expandable token lists per collector. Co-Authored-By: Claude Opus 4.6 (1M context) --- system/public/kidlisp.com/buy.html | 153 +++++++++++++++++++++++------ 1 file changed, 123 insertions(+), 30 deletions(-) diff --git a/system/public/kidlisp.com/buy.html b/system/public/kidlisp.com/buy.html index db51ec2c2e..bcbe4c4e06 100644 --- a/system/public/kidlisp.com/buy.html +++ b/system/public/kidlisp.com/buy.html @@ -443,20 +443,83 @@ background: rgba(0, 255, 136, 0.1); text-shadow: 0 2px 6px rgba(0, 255, 136, 0.5), 0 1px 2px rgba(0, 0, 0, 0.4); } - .tab[data-tab="sold"] { - color: var(--red); - background: rgba(255, 107, 107, 0.1); - text-shadow: 0 2px 6px rgba(255, 107, 107, 0.5), 0 1px 2px rgba(0, 0, 0, 0.4); + .tab[data-tab="albums"] { + color: var(--blue); + background: rgba(112, 214, 255, 0.1); + text-shadow: 0 2px 6px rgba(112, 214, 255, 0.5), 0 1px 2px rgba(0, 0, 0, 0.4); } .tab.active[data-tab="fresh"] { background: rgba(0, 255, 136, 0.2); border-bottom-color: var(--green); box-shadow: inset 0 -1px 0 rgba(0, 255, 136, 0.5); } - .tab.active[data-tab="sold"] { - background: rgba(255, 107, 107, 0.2); - border-bottom-color: var(--red); - box-shadow: inset 0 -1px 0 rgba(255, 107, 107, 0.5); + .tab.active[data-tab="albums"] { + background: rgba(112, 214, 255, 0.2); + border-bottom-color: var(--blue); + box-shadow: inset 0 -1px 0 rgba(112, 214, 255, 0.5); + } + /* ── Albums ── */ + .albums-grid { + display: flex; + flex-direction: column; + gap: 0; + } + .album-row { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 20px; + border-bottom: 1px solid var(--border); + cursor: pointer; + transition: background 0.15s; + } + .album-row:hover { + background: var(--bg3); + } + .album-rank { + font-family: var(--font-mono); + font-size: 14px; + color: var(--text2); + min-width: 28px; + text-align: right; + } + .album-name { + font-family: var(--font-fun); + font-size: 18px; + font-weight: 700; + color: var(--text); + flex: 1; + } + .album-count { + font-family: var(--font-mono); + font-size: 16px; + color: var(--blue); + font-weight: 700; + } + .album-tokens { + display: none; + flex-wrap: wrap; + gap: 6px; + padding: 8px 20px 16px 64px; + } + .album-tokens.open { + display: flex; + } + .album-token-chip { + font-family: var(--font-mono); + font-size: 13px; + padding: 4px 10px; + background: var(--bg3); + border: 1px solid var(--border); + border-radius: 20px; + color: var(--text); + cursor: pointer; + transition: all 0.15s; + } + .album-token-chip:hover { + background: var(--accent-bg); + border-color: var(--accent); + color: var(--accent); } .card-buy-btn { @@ -540,8 +603,8 @@
- - + +
@@ -589,6 +652,7 @@ // ── State ── let listings = []; let soldEvents = []; + let albums = []; let walletAddress = null; let beaconClient = null; let currentTab = 'fresh'; @@ -746,6 +810,20 @@ return data?.data?.event || []; } + async function fetchAlbums() { + const res = await fetch(`https://api.tzkt.io/v1/tokens/balances?token.contract=${CONTRACT}&balance.gt=0&limit=200&select=account.address,account.alias,token.metadata.name,token.tokenId`); + const data = await res.json(); + // Group by holder + const grouped = {}; + for (const b of data) { + const addr = b['account.address']; + if (!grouped[addr]) grouped[addr] = { address: addr, alias: b['account.alias'], tokens: [] }; + grouped[addr].tokens.push({ name: b['token.metadata.name'], tokenId: b['token.tokenId'] }); + } + // Sort by count desc + return Object.values(grouped).sort((a, b) => b.tokens.length - a.tokens.length); + } + function formatXTZ(mutez) { return (Number(mutez) / 1_000_000).toFixed(mutez % 1_000_000 === 0 ? 0 : 2); } @@ -782,9 +860,9 @@ // ── Render ── function renderGrid() { - if (currentTab === 'sold') return renderSoldGrid(); + if (currentTab === 'albums') return renderAlbumsGrid(); - grid.classList.remove('sold-grid'); + grid.classList.remove('sold-grid', 'albums-grid'); // Sort by price ascending by default const sorted = [...listings].sort((a, b) => Number(a.price_xtz) - Number(b.price_xtz) || Number(a.token?.token_id || 0) - Number(b.token?.token_id || 0)); if (!sorted.length) { @@ -834,29 +912,42 @@ }); } - function renderSoldGrid() { - grid.classList.add('sold-grid'); - if (!soldEvents.length) { - grid.innerHTML = '
No sales yet
'; + function renderAlbumsGrid() { + grid.classList.remove('sold-grid'); + grid.classList.add('albums-grid'); + if (!albums.length) { + grid.innerHTML = '
Loading albums…
'; + fetchAlbums().then(data => { albums = data; renderAlbumsGrid(); }); return; } - grid.innerHTML = soldEvents.map((ev, i) => { - ev._isSold = true; - const name = ev.token?.name || `#${ev.token?.token_id}`; - const thumb = thumbUrl(ev.token?.thumbnail_uri, ev.token?.name); - const price = formatXTZ(ev.price_xtz); - return `
- ${thumb ? `${name}` : ''} -
${name}
${price} XTZ
+ grid.innerHTML = albums.map((album, i) => { + const name = album.alias || sellerName(album.address); + const tokens = album.tokens.sort((a, b) => Number(a.tokenId) - Number(b.tokenId)); + return `
+
+
${i + 1}
+
${name}
+
${album.tokens.length}
+
+
+ ${tokens.map(t => `${t.name}`).join('')} +
`; }).join(''); - grid.querySelectorAll('.sold-mini').forEach(card => { - card.addEventListener('click', () => { - const idx = Number(card.dataset.idx); - const ev = soldEvents[idx]; - if (ev) openSoldModal(ev); + grid.querySelectorAll('.album-row').forEach(row => { + row.addEventListener('click', () => { + const tokens = row.nextElementSibling; + tokens.classList.toggle('open'); + }); + }); + + grid.querySelectorAll('.album-token-chip').forEach(chip => { + chip.addEventListener('click', (e) => { + e.stopPropagation(); + const code = chip.dataset.code; + window.open(`https://aesthetic.computer/${code}`, '_blank'); }); }); } @@ -1096,12 +1187,14 @@ // ── Init ── async function loadMarket() { try { - const [freshData, soldData] = await Promise.all([fetchListings(), fetchSoldEvents()]); + const [freshData, soldData, albumsData] = await Promise.all([fetchListings(), fetchSoldEvents(), fetchAlbums()]); listings = freshData; soldEvents = soldData; + albums = albumsData; const allAddrs = [ ...freshData.map(l => l.seller_address), ...freshData.flatMap(l => (l.token?.creators || []).map(c => c.creator_address)), + ...albumsData.map(a => a.address), ]; await resolveSellerNames(allAddrs); renderGrid(); -- 2.51.2