Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

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) <noreply@anthropic.com>

+123 -30
+123 -30
system/public/kidlisp.com/buy.html
··· 443 443 background: rgba(0, 255, 136, 0.1); 444 444 text-shadow: 0 2px 6px rgba(0, 255, 136, 0.5), 0 1px 2px rgba(0, 0, 0, 0.4); 445 445 } 446 - .tab[data-tab="sold"] { 447 - color: var(--red); 448 - background: rgba(255, 107, 107, 0.1); 449 - text-shadow: 0 2px 6px rgba(255, 107, 107, 0.5), 0 1px 2px rgba(0, 0, 0, 0.4); 446 + .tab[data-tab="albums"] { 447 + color: var(--blue); 448 + background: rgba(112, 214, 255, 0.1); 449 + text-shadow: 0 2px 6px rgba(112, 214, 255, 0.5), 0 1px 2px rgba(0, 0, 0, 0.4); 450 450 } 451 451 .tab.active[data-tab="fresh"] { 452 452 background: rgba(0, 255, 136, 0.2); 453 453 border-bottom-color: var(--green); 454 454 box-shadow: inset 0 -1px 0 rgba(0, 255, 136, 0.5); 455 455 } 456 - .tab.active[data-tab="sold"] { 457 - background: rgba(255, 107, 107, 0.2); 458 - border-bottom-color: var(--red); 459 - box-shadow: inset 0 -1px 0 rgba(255, 107, 107, 0.5); 456 + .tab.active[data-tab="albums"] { 457 + background: rgba(112, 214, 255, 0.2); 458 + border-bottom-color: var(--blue); 459 + box-shadow: inset 0 -1px 0 rgba(112, 214, 255, 0.5); 460 + } 461 + /* ── Albums ── */ 462 + .albums-grid { 463 + display: flex; 464 + flex-direction: column; 465 + gap: 0; 466 + } 467 + .album-row { 468 + display: flex; 469 + align-items: center; 470 + gap: 16px; 471 + padding: 16px 20px; 472 + border-bottom: 1px solid var(--border); 473 + cursor: pointer; 474 + transition: background 0.15s; 475 + } 476 + .album-row:hover { 477 + background: var(--bg3); 478 + } 479 + .album-rank { 480 + font-family: var(--font-mono); 481 + font-size: 14px; 482 + color: var(--text2); 483 + min-width: 28px; 484 + text-align: right; 485 + } 486 + .album-name { 487 + font-family: var(--font-fun); 488 + font-size: 18px; 489 + font-weight: 700; 490 + color: var(--text); 491 + flex: 1; 492 + } 493 + .album-count { 494 + font-family: var(--font-mono); 495 + font-size: 16px; 496 + color: var(--blue); 497 + font-weight: 700; 498 + } 499 + .album-tokens { 500 + display: none; 501 + flex-wrap: wrap; 502 + gap: 6px; 503 + padding: 8px 20px 16px 64px; 504 + } 505 + .album-tokens.open { 506 + display: flex; 507 + } 508 + .album-token-chip { 509 + font-family: var(--font-mono); 510 + font-size: 13px; 511 + padding: 4px 10px; 512 + background: var(--bg3); 513 + border: 1px solid var(--border); 514 + border-radius: 20px; 515 + color: var(--text); 516 + cursor: pointer; 517 + transition: all 0.15s; 518 + } 519 + .album-token-chip:hover { 520 + background: var(--accent-bg); 521 + border-color: var(--accent); 522 + color: var(--accent); 460 523 } 461 524 462 525 .card-buy-btn { ··· 540 603 </header> 541 604 542 605 <div class="tabs"> 543 - <button class="tab active" data-tab="fresh">Fresh</button> 544 - <button class="tab" data-tab="sold">Sold</button> 606 + <button class="tab active" data-tab="fresh">Shop</button> 607 + <button class="tab" data-tab="albums">Albums</button> 545 608 </div> 546 609 547 610 <div class="grid" id="grid"> ··· 589 652 // ── State ── 590 653 let listings = []; 591 654 let soldEvents = []; 655 + let albums = []; 592 656 let walletAddress = null; 593 657 let beaconClient = null; 594 658 let currentTab = 'fresh'; ··· 746 810 return data?.data?.event || []; 747 811 } 748 812 813 + async function fetchAlbums() { 814 + 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`); 815 + const data = await res.json(); 816 + // Group by holder 817 + const grouped = {}; 818 + for (const b of data) { 819 + const addr = b['account.address']; 820 + if (!grouped[addr]) grouped[addr] = { address: addr, alias: b['account.alias'], tokens: [] }; 821 + grouped[addr].tokens.push({ name: b['token.metadata.name'], tokenId: b['token.tokenId'] }); 822 + } 823 + // Sort by count desc 824 + return Object.values(grouped).sort((a, b) => b.tokens.length - a.tokens.length); 825 + } 826 + 749 827 function formatXTZ(mutez) { 750 828 return (Number(mutez) / 1_000_000).toFixed(mutez % 1_000_000 === 0 ? 0 : 2); 751 829 } ··· 782 860 783 861 // ── Render ── 784 862 function renderGrid() { 785 - if (currentTab === 'sold') return renderSoldGrid(); 863 + if (currentTab === 'albums') return renderAlbumsGrid(); 786 864 787 - grid.classList.remove('sold-grid'); 865 + grid.classList.remove('sold-grid', 'albums-grid'); 788 866 // Sort by price ascending by default 789 867 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)); 790 868 if (!sorted.length) { ··· 834 912 }); 835 913 } 836 914 837 - function renderSoldGrid() { 838 - grid.classList.add('sold-grid'); 839 - if (!soldEvents.length) { 840 - grid.innerHTML = '<div class="loading">No sales yet</div>'; 915 + function renderAlbumsGrid() { 916 + grid.classList.remove('sold-grid'); 917 + grid.classList.add('albums-grid'); 918 + if (!albums.length) { 919 + grid.innerHTML = '<div class="loading">Loading albums…</div>'; 920 + fetchAlbums().then(data => { albums = data; renderAlbumsGrid(); }); 841 921 return; 842 922 } 843 923 844 - grid.innerHTML = soldEvents.map((ev, i) => { 845 - ev._isSold = true; 846 - const name = ev.token?.name || `#${ev.token?.token_id}`; 847 - const thumb = thumbUrl(ev.token?.thumbnail_uri, ev.token?.name); 848 - const price = formatXTZ(ev.price_xtz); 849 - return `<div class="sold-mini" data-idx="${i}"> 850 - ${thumb ? `<img src="${thumb}" alt="${name}" loading="lazy" />` : ''} 851 - <div class="sold-mini-overlay">${name}<br>${price} XTZ</div> 924 + grid.innerHTML = albums.map((album, i) => { 925 + const name = album.alias || sellerName(album.address); 926 + const tokens = album.tokens.sort((a, b) => Number(a.tokenId) - Number(b.tokenId)); 927 + return `<div class="album-entry"> 928 + <div class="album-row" data-idx="${i}"> 929 + <div class="album-rank">${i + 1}</div> 930 + <div class="album-name">${name}</div> 931 + <div class="album-count">${album.tokens.length}</div> 932 + </div> 933 + <div class="album-tokens" data-idx="${i}"> 934 + ${tokens.map(t => `<span class="album-token-chip" data-code="${t.name}">${t.name}</span>`).join('')} 935 + </div> 852 936 </div>`; 853 937 }).join(''); 854 938 855 - grid.querySelectorAll('.sold-mini').forEach(card => { 856 - card.addEventListener('click', () => { 857 - const idx = Number(card.dataset.idx); 858 - const ev = soldEvents[idx]; 859 - if (ev) openSoldModal(ev); 939 + grid.querySelectorAll('.album-row').forEach(row => { 940 + row.addEventListener('click', () => { 941 + const tokens = row.nextElementSibling; 942 + tokens.classList.toggle('open'); 943 + }); 944 + }); 945 + 946 + grid.querySelectorAll('.album-token-chip').forEach(chip => { 947 + chip.addEventListener('click', (e) => { 948 + e.stopPropagation(); 949 + const code = chip.dataset.code; 950 + window.open(`https://aesthetic.computer/${code}`, '_blank'); 860 951 }); 861 952 }); 862 953 } ··· 1096 1187 // ── Init ── 1097 1188 async function loadMarket() { 1098 1189 try { 1099 - const [freshData, soldData] = await Promise.all([fetchListings(), fetchSoldEvents()]); 1190 + const [freshData, soldData, albumsData] = await Promise.all([fetchListings(), fetchSoldEvents(), fetchAlbums()]); 1100 1191 listings = freshData; 1101 1192 soldEvents = soldData; 1193 + albums = albumsData; 1102 1194 const allAddrs = [ 1103 1195 ...freshData.map(l => l.seller_address), 1104 1196 ...freshData.flatMap(l => (l.token?.creators || []).map(c => c.creator_address)), 1197 + ...albumsData.map(a => a.address), 1105 1198 ]; 1106 1199 await resolveSellerNames(allAddrs); 1107 1200 renderGrid();