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: horizontal tiles with animated art, dense sold grid

Remove sort controls, stats bar, and counters. Fresh tab now shows
single-column horizontal tiles (image left, info right) using animated
IPFS thumbnails by default. Sold tab shows a high-density grid of
small still thumbnails with hover overlay.

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

+84 -174
+84 -174
system/public/kidlisp.com/buy.html
··· 153 153 .wallet-btn:hover { background: rgba(78, 205, 196, 0.25); box-shadow: 0 0 12px rgba(78, 205, 196, 0.3); } 154 154 .wallet-btn.connected { border-color: var(--green); color: var(--green); background: rgba(0, 255, 136, 0.1); } 155 155 156 - /* ── Stats bar ── */ 157 - .stats { 156 + /* ── Grid (Fresh = single column list, Sold = dense grid) ── */ 157 + .grid { 158 158 display: flex; 159 - gap: 20px; 160 - padding: 10px 20px; 161 - border-bottom: 1px solid var(--border); 162 - font-size: 12px; 163 - font-family: var(--font-mono); 164 - color: var(--text2); 165 - flex-wrap: wrap; 159 + flex-direction: column; 160 + gap: 10px; 161 + padding: 16px; 162 + max-width: 640px; 163 + margin: 0 auto; 166 164 } 167 - .stats strong { color: var(--gold); } 168 - 169 - /* ── Grid ── */ 170 - .grid { 165 + .grid.sold-grid { 171 166 display: grid; 172 - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 173 - gap: 14px; 174 - padding: 20px; 167 + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); 168 + gap: 4px; 175 169 max-width: 1400px; 176 - margin: 0 auto; 170 + padding: 10px; 177 171 } 178 172 173 + /* ── Fresh card (horizontal tile) ── */ 179 174 .card { 180 175 background: var(--bg2); 181 176 border: 1px solid var(--border); ··· 184 179 cursor: pointer; 185 180 transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s; 186 181 box-shadow: 0 2px 8px var(--card-glow); 182 + display: flex; 183 + flex-direction: row; 184 + align-items: stretch; 187 185 } 188 186 .card:hover { 189 187 border-color: var(--accent); 190 - transform: translateY(-3px) scale(1.01); 191 - box-shadow: 0 8px 24px var(--card-hover-glow); 188 + transform: translateY(-2px); 189 + box-shadow: 0 6px 20px var(--card-hover-glow); 192 190 } 193 191 194 192 .card-thumb { 195 - aspect-ratio: 1; 193 + width: 120px; 194 + min-height: 90px; 195 + flex-shrink: 0; 196 196 background: var(--bg3); 197 197 overflow: hidden; 198 198 position: relative; ··· 218 218 .card-thumb img { 219 219 width: 100%; height: 100%; 220 220 object-fit: cover; 221 - image-rendering: pixelated; 222 - transition: opacity 0.15s; 223 - } 224 - .card-thumb img.hover-webp { 225 - position: absolute; 226 - inset: 0; 227 - opacity: 0; 228 221 image-rendering: auto; 229 222 } 230 - .card:hover .card-thumb img.hover-webp { opacity: 1; } 231 - .card:hover .card-thumb img.static-png { opacity: 0; } 232 223 233 224 .card-body { 234 - padding: 8px 10px; 225 + padding: 10px 14px; 235 226 display: flex; 236 227 flex-direction: column; 237 - gap: 2px; 228 + justify-content: center; 229 + gap: 4px; 230 + flex: 1; 231 + min-width: 0; 238 232 } 239 233 240 234 .card-code { 241 - font-size: 13px; 235 + font-size: 14px; 242 236 font-weight: 700; 243 237 font-family: var(--font-mono); 244 238 color: var(--purple); 245 239 } 246 240 247 241 .card-price { 248 - font-size: 15px; 242 + font-size: 16px; 249 243 font-weight: 700; 250 244 color: var(--gold); 251 245 text-shadow: 0 0 8px rgba(255, 230, 109, 0.3); ··· 258 252 text-overflow: ellipsis; 259 253 white-space: nowrap; 260 254 } 255 + 256 + /* ── Sold mini-cards (dense grid of stills) ── */ 257 + .sold-mini { 258 + aspect-ratio: 1; 259 + background: var(--bg3); 260 + border-radius: 4px; 261 + overflow: hidden; 262 + cursor: pointer; 263 + position: relative; 264 + transition: transform 0.15s, box-shadow 0.15s; 265 + } 266 + .sold-mini:hover { 267 + transform: scale(1.05); 268 + box-shadow: 0 4px 12px var(--card-hover-glow); 269 + z-index: 1; 270 + } 271 + .sold-mini img { 272 + width: 100%; height: 100%; 273 + object-fit: cover; 274 + image-rendering: pixelated; 275 + } 276 + .sold-mini .sold-mini-overlay { 277 + position: absolute; 278 + inset: 0; 279 + background: rgba(0,0,0,0.5); 280 + display: flex; 281 + align-items: center; 282 + justify-content: center; 283 + opacity: 0; 284 + transition: opacity 0.15s; 285 + font-size: 9px; 286 + font-family: var(--font-mono); 287 + color: var(--gold); 288 + text-align: center; 289 + padding: 2px; 290 + } 291 + .sold-mini:hover .sold-mini-overlay { opacity: 1; } 261 292 262 293 /* ── Detail modal ── */ 263 294 .modal-overlay { ··· 432 463 background: linear-gradient(135deg, var(--accent), var(--neon-pink)); 433 464 color: #000; 434 465 border: none; 435 - padding: 7px 0; 466 + padding: 7px 14px; 436 467 font-family: var(--font-fun); 437 468 font-size: 12px; 438 469 font-weight: 700; 439 470 cursor: pointer; 440 - width: 100%; 441 471 transition: opacity 0.15s, transform 0.15s; 442 472 letter-spacing: 0.02em; 473 + border-radius: 6px; 474 + white-space: nowrap; 475 + align-self: center; 476 + flex-shrink: 0; 443 477 } 444 478 .card-buy-btn:hover { opacity: 0.9; transform: scale(1.02); } 445 479 .card-buy-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } 446 480 447 - .card.sold-card { opacity: 0.75; } 448 - .card.sold-card .card-price { color: var(--red); } 449 - .card-sold-label { 450 - font-size: 10px; 451 - font-weight: 700; 452 - color: var(--red); 453 - text-transform: uppercase; 454 - letter-spacing: 0.04em; 455 - } 456 - .card-date { 457 - font-size: 10px; 458 - color: var(--text2); 459 - } 460 - 461 - /* ── Sort controls ── */ 462 - .controls { 463 - display: flex; 464 - align-items: center; 465 - gap: 8px; 466 - padding: 12px 20px 0; 467 - max-width: 1400px; 468 - margin: 0 auto; 469 - } 470 - .sort-btn { 471 - background: none; 472 - border: 1px solid var(--border); 473 - color: var(--text2); 474 - padding: 4px 10px; 475 - font-family: var(--font-mono); 476 - font-size: 11px; 477 - cursor: pointer; 478 - } 479 - .sort-btn.active { 480 - border-color: var(--accent); 481 - color: var(--accent); 482 - background: var(--accent-bg); 483 - } 484 - 485 481 /* ── Footer ── */ 486 482 footer { 487 483 text-align: center; ··· 541 537 </div> 542 538 </header> 543 539 544 - <div class="stats" id="stats"> 545 - <span><strong id="stat-items">—</strong> items</span> 546 - <span><strong id="stat-listed">—</strong> listed</span> 547 - <span>floor <strong id="stat-floor">—</strong> XTZ</span> 548 - <span>owners <strong id="stat-owners">—</strong></span> 549 - </div> 550 - 551 540 <div class="tabs"> 552 541 <button class="tab active" data-tab="fresh">Fresh</button> 553 542 <button class="tab" data-tab="sold">Sold</button> 554 - </div> 555 - 556 - <div class="controls" id="controls"> 557 - <button class="sort-btn active" data-sort="price-asc">Price ↑</button> 558 - <button class="sort-btn" data-sort="price-desc">Price ↓</button> 559 - <button class="sort-btn" data-sort="newest">Newest</button> 560 543 </div> 561 544 562 545 <div class="grid" id="grid"> ··· 603 586 let soldEvents = []; 604 587 let walletAddress = null; 605 588 let beaconClient = null; 606 - let currentSort = 'price-asc'; 607 589 let currentTab = 'fresh'; 608 590 let selectedListing = null; 609 591 ··· 743 725 return data?.data?.event || []; 744 726 } 745 727 746 - async function fetchCollectionStats() { 747 - try { 748 - const res = await fetch(`https://api.tzkt.io/v1/tokens/balances?token.contract=${CONTRACT}&balance.gt=0&limit=10000&select=account.address`); 749 - const owners = await res.json(); 750 - const unique = new Set(owners); 751 - return { owners: unique.size }; 752 - } catch { return { owners: '—' }; } 753 - } 754 - 755 728 function formatXTZ(mutez) { 756 729 return (Number(mutez) / 1_000_000).toFixed(mutez % 1_000_000 === 0 ? 0 : 2); 757 730 } ··· 776 749 return uri; 777 750 } 778 751 779 - // ── Sorting ── 780 - function sortListings(list) { 781 - const sorted = [...list]; 782 - switch (currentSort) { 783 - case 'price-asc': 784 - sorted.sort((a, b) => Number(a.price_xtz) - Number(b.price_xtz)); 785 - break; 786 - case 'price-desc': 787 - sorted.sort((a, b) => Number(b.price_xtz) - Number(a.price_xtz)); 788 - break; 789 - case 'newest': 790 - sorted.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); 791 - break; 792 - } 793 - return sorted; 794 - } 795 - 796 - document.querySelectorAll('.sort-btn').forEach(btn => { 797 - btn.addEventListener('click', () => { 798 - document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active')); 799 - btn.classList.add('active'); 800 - currentSort = btn.dataset.sort; 801 - renderGrid(); 802 - }); 803 - }); 804 - 805 752 // ── Tabs ── 806 753 document.querySelectorAll('.tab').forEach(tab => { 807 754 tab.addEventListener('click', () => { 808 755 document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); 809 756 tab.classList.add('active'); 810 757 currentTab = tab.dataset.tab; 811 - document.getElementById('controls').style.display = currentTab === 'fresh' ? 'flex' : 'none'; 812 758 renderGrid(); 813 759 }); 814 760 }); ··· 817 763 function renderGrid() { 818 764 if (currentTab === 'sold') return renderSoldGrid(); 819 765 820 - const sorted = sortListings(listings); 766 + grid.classList.remove('sold-grid'); 767 + // Sort by price ascending by default 768 + const sorted = [...listings].sort((a, b) => Number(a.price_xtz) - Number(b.price_xtz)); 821 769 if (!sorted.length) { 822 770 grid.innerHTML = '<div class="loading">No active listings</div>'; 823 771 return; ··· 825 773 826 774 grid.innerHTML = sorted.map(l => { 827 775 const name = l.token?.name || `#${l.token?.token_id}`; 828 - const thumb = thumbUrl(l.token?.thumbnail_uri, l.token?.name); 829 - const webp = animatedWebpUrl(l.token?.thumbnail_uri); 776 + // Use animated IPFS thumbnail by default instead of static oven PNG 777 + const animated = animatedWebpUrl(l.token?.thumbnail_uri); 830 778 const price = formatXTZ(l.price_xtz); 831 779 const seller = l.seller_address?.slice(0, 8) + '…'; 832 780 const askId = l.id || l.bigmap_key; 833 - const hasThumb = thumb || webp; 834 781 return `<div class="card" data-id="${askId}"> 835 782 <div class="card-thumb"> 836 - ${hasThumb ? `<img class="static-png" src="${thumb}" alt="${name}" loading="lazy" />` : ''} 837 - ${webp ? `<img class="hover-webp" src="${webp}" alt="${name}" loading="lazy" />` : ''} 838 - ${!hasThumb ? `<div class="card-thumb-placeholder">✦</div>` : ''} 783 + ${animated ? `<img src="${animated}" alt="${name}" loading="lazy" />` : `<div class="card-thumb-placeholder">✦</div>`} 839 784 </div> 840 785 <div class="card-body"> 841 786 <div class="card-code">${name}</div> 842 787 <div class="card-price">${price} XTZ</div> 843 788 <div class="card-seller">${seller}</div> 844 789 </div> 845 - <button class="card-buy-btn" data-ask="${askId}">${walletAddress ? 'Buy Now — ' + price + ' XTZ' : 'Connect to Buy'}</button> 790 + <button class="card-buy-btn" data-ask="${askId}">${walletAddress ? 'Buy — ' + price + ' XTZ' : 'Buy'}</button> 846 791 </div>`; 847 792 }).join(''); 848 793 ··· 855 800 }); 856 801 }); 857 802 858 - // Buy Now button → direct purchase (no modal) 803 + // Buy button → direct purchase (no modal) 859 804 grid.querySelectorAll('.card-buy-btn').forEach(btn => { 860 805 btn.addEventListener('click', (e) => { 861 806 e.stopPropagation(); ··· 867 812 } 868 813 869 814 function renderSoldGrid() { 815 + grid.classList.add('sold-grid'); 870 816 if (!soldEvents.length) { 871 817 grid.innerHTML = '<div class="loading">No sales yet</div>'; 872 818 return; ··· 876 822 ev._isSold = true; 877 823 const name = ev.token?.name || `#${ev.token?.token_id}`; 878 824 const thumb = thumbUrl(ev.token?.thumbnail_uri, ev.token?.name); 879 - const webp = animatedWebpUrl(ev.token?.thumbnail_uri); 880 825 const price = formatXTZ(ev.price_xtz); 881 - const date = new Date(ev.timestamp).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); 882 - const buyer = ev.recipient_address?.slice(0, 8) + '…'; 883 - return `<div class="card sold-card" data-idx="${i}"> 884 - <div class="card-thumb"> 885 - ${thumb ? `<img class="static-png" src="${thumb}" alt="${name}" loading="lazy" />` : ''} 886 - ${webp ? `<img class="hover-webp" src="${webp}" alt="${name}" loading="lazy" />` : ''} 887 - </div> 888 - <div class="card-body"> 889 - <div class="card-code">${name}</div> 890 - <div class="card-price">${price} XTZ</div> 891 - <div class="card-sold-label">SOLD</div> 892 - <div class="card-date">${date} → ${buyer}</div> 893 - </div> 826 + return `<div class="sold-mini" data-idx="${i}"> 827 + ${thumb ? `<img src="${thumb}" alt="${name}" loading="lazy" />` : ''} 828 + <div class="sold-mini-overlay">${name}<br>${price} XTZ</div> 894 829 </div>`; 895 830 }).join(''); 896 831 897 - // Sold cards open modal too (view-only) 898 - grid.querySelectorAll('.sold-card').forEach(card => { 899 - card.style.cursor = 'pointer'; 832 + grid.querySelectorAll('.sold-mini').forEach(card => { 900 833 card.addEventListener('click', () => { 901 834 const idx = Number(card.dataset.idx); 902 835 const ev = soldEvents[idx]; ··· 940 873 overlay.classList.add('open'); 941 874 } 942 875 943 - function updateStats() { 944 - document.getElementById('stat-listed').textContent = listings.length; 945 - if (listings.length) { 946 - const prices = listings.map(l => Number(l.price_xtz)); 947 - const floor = Math.min(...prices) / 1_000_000; 948 - document.getElementById('stat-floor').textContent = floor.toFixed(floor % 1 === 0 ? 0 : 2); 949 - } 950 - } 951 876 952 877 // ── Modal (fullscreen) ── 953 878 function openModal(listing) { ··· 1070 995 // Remove from local listings 1071 996 listings = listings.filter(l => (l.id || l.bigmap_key) !== (listing.id || listing.bigmap_key)); 1072 997 renderGrid(); 1073 - updateStats(); 998 + 1074 999 1075 1000 return response.transactionHash; 1076 1001 } ··· 1139 1064 soldEvents = soldData; 1140 1065 liveDot.classList.remove('error'); 1141 1066 renderGrid(); 1142 - updateStats(); 1067 + 1143 1068 // Route from URL on first load (after listings are available) 1144 1069 if (!selectedListing) routeFromURL(); 1145 1070 } catch (err) { ··· 1148 1073 } 1149 1074 } 1150 1075 1151 - async function loadStats() { 1152 - try { 1153 - const res = await fetch(`https://api.tzkt.io/v1/tokens?contract=${CONTRACT}&limit=10000&select=id`); 1154 - const tokens = await res.json(); 1155 - document.getElementById('stat-items').textContent = tokens.length; 1156 - } catch {} 1157 - 1158 - try { 1159 - const res = await fetch(`https://api.tzkt.io/v1/tokens/balances?token.contract=${CONTRACT}&balance.gt=0&limit=10000&select=account.address`); 1160 - const owners = await res.json(); 1161 - document.getElementById('stat-owners').textContent = new Set(owners).size; 1162 - } catch {} 1163 - } 1164 - 1165 1076 // Boot 1166 1077 loadMarket(); 1167 - loadStats(); 1168 1078 initWallet().catch(() => {}); 1169 1079 setInterval(loadMarket, REFRESH_MS); 1170 1080 })();