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: rename Albums to Keepers with leaderboard UI

- Gold/silver/bronze medals for top 3 collectors
- Bar graph showing distribution of keeps per collector
- Exclude tokens currently listed for sale
- Exclude tokens still held by original minter
- Gold accent color for Keepers tab

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+53 -19
+53 -19
system/public/kidlisp.com/buy.html
··· 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 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); 447 + color: var(--gold); 448 + background: rgba(255, 230, 109, 0.1); 449 + text-shadow: 0 2px 6px rgba(255, 230, 109, 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); ··· 454 454 box-shadow: inset 0 -1px 0 rgba(0, 255, 136, 0.5); 455 455 } 456 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); 457 + background: rgba(255, 230, 109, 0.2); 458 + border-bottom-color: var(--gold); 459 + box-shadow: inset 0 -1px 0 rgba(255, 230, 109, 0.5); 460 460 } 461 461 /* ── Albums ── */ 462 462 .albums-grid { ··· 477 477 background: var(--bg3); 478 478 } 479 479 .album-rank { 480 - font-family: var(--font-mono); 481 - font-size: 14px; 482 - color: var(--text2); 480 + font-size: 18px; 483 481 min-width: 28px; 484 - text-align: right; 482 + text-align: center; 485 483 } 486 484 .album-name { 487 485 font-family: var(--font-fun); 488 486 font-size: 18px; 489 487 font-weight: 700; 490 488 color: var(--text); 489 + flex-shrink: 0; 490 + } 491 + .album-bar-wrap { 491 492 flex: 1; 493 + display: flex; 494 + align-items: center; 495 + gap: 8px; 496 + min-width: 0; 497 + } 498 + .album-bar { 499 + height: 20px; 500 + border-radius: 4px; 501 + background: var(--blue); 502 + transition: width 0.3s ease; 503 + min-width: 4px; 504 + } 505 + .album-row:first-child .album-bar { 506 + background: var(--gold); 492 507 } 493 508 .album-count { 494 509 font-family: var(--font-mono); 495 - font-size: 16px; 496 - color: var(--blue); 510 + font-size: 14px; 511 + color: var(--text2); 497 512 font-weight: 700; 513 + flex-shrink: 0; 498 514 } 499 515 .album-tokens { 500 516 display: none; ··· 614 630 615 631 <div class="tabs"> 616 632 <button class="tab active" data-tab="fresh">Shop</button> 617 - <button class="tab" data-tab="albums">Albums</button> 633 + <button class="tab" data-tab="albums">Keepers</button> 618 634 </div> 619 635 620 636 <div class="grid" id="grid"> ··· 821 837 } 822 838 823 839 async function fetchAlbums() { 824 - 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,token.metadata.thumbnailUri`); 825 - const data = await res.json(); 826 - // Group by holder 840 + const [balRes, creatorsRes] = await Promise.all([ 841 + 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,token.metadata.thumbnailUri`), 842 + fetch(`https://api.tzkt.io/v1/tokens/transfers?token.contract=${CONTRACT}&from.null=true&limit=200&select=to.address,token.tokenId`), 843 + ]); 844 + const data = await balRes.json(); 845 + const creators = await creatorsRes.json(); 846 + // Build set of original minters (token creator = first recipient of mint) 847 + const minterByToken = {}; 848 + for (const c of creators) minterByToken[String(c['token.tokenId'])] = c['to.address']; 849 + // Build set of token IDs currently listed for sale 850 + const listedIds = new Set(listings.map(l => String(l.token?.token_id))); 851 + // Group by holder, excluding listed tokens and original minters 827 852 const grouped = {}; 828 853 for (const b of data) { 829 854 const addr = b['account.address']; 855 + const tokenId = String(b['token.tokenId']); 856 + if (listedIds.has(tokenId)) continue; 857 + if (addr === minterByToken[tokenId]) continue; 830 858 if (!grouped[addr]) grouped[addr] = { address: addr, alias: b['account.alias'], tokens: [] }; 831 859 grouped[addr].tokens.push({ name: b['token.metadata.name'], tokenId: b['token.tokenId'], thumbnailUri: b['token.metadata.thumbnailUri'] }); 832 860 } 833 - // Sort by count desc 834 - return Object.values(grouped).sort((a, b) => b.tokens.length - a.tokens.length); 861 + // Sort by count desc, filter out empty 862 + return Object.values(grouped).filter(a => a.tokens.length > 0).sort((a, b) => b.tokens.length - a.tokens.length); 835 863 } 836 864 837 865 function formatXTZ(mutez) { ··· 931 959 return; 932 960 } 933 961 962 + const maxCount = albums[0]?.tokens.length || 1; 934 963 grid.innerHTML = albums.map((album, i) => { 935 964 const name = album.alias || sellerName(album.address); 936 965 const tokens = album.tokens.sort((a, b) => Number(a.tokenId) - Number(b.tokenId)); 966 + const pct = Math.round((album.tokens.length / maxCount) * 100); 967 + const medal = i === 0 ? '\uD83E\uDD47' : i === 1 ? '\uD83E\uDD48' : i === 2 ? '\uD83E\uDD49' : `${i + 1}`; 937 968 return `<div class="album-entry"> 938 969 <div class="album-row" data-idx="${i}"> 939 - <div class="album-rank">${i + 1}</div> 970 + <div class="album-rank">${medal}</div> 940 971 <div class="album-name">${name}</div> 972 + <div class="album-bar-wrap"> 973 + <div class="album-bar" style="width:${pct}%"></div> 974 + </div> 941 975 <div class="album-count">${album.tokens.length}</div> 942 976 </div> 943 977 <div class="album-tokens" data-idx="${i}">