Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
47 kB · 1428 lines
HTML
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429<!DOCTYPE html><!-- buy.kidlisp.com — Direct purchase page for KidLisp Keeps --><html lang="en"><head> <meta charset="utf-8"> <script> // Deep-link no-flash guard: if the URL is a /$code permalink, mark the // document before first paint so the opaque modal covers the home grid // immediately (the listing data fills it in once it loads). if (/^\/\$[^\/]/.test(location.pathname)) document.documentElement.classList.add('deeplink'); </script> <title>Buy KidLisp Keeps</title> <meta name="description" content="Collect KidLisp Keeps — interactive generative art on Tezos. Connect your wallet and buy directly." /> <meta property="og:type" content="website" /> <meta property="og:url" content="https://buy.kidlisp.com" /> <meta property="og:title" content="Buy KidLisp Keeps" /> <meta property="og:description" content="Collect interactive generative art on Tezos. Connect your wallet and buy directly." /> <meta property="og:image" content="https://oven.aesthetic.computer/kidlisp-og/site/buy.png" /> <meta property="og:image:width" content="1200" /> <meta property="og:image:height" content="630" /> <meta name="twitter:card" content="summary_large_image" /> <link rel="icon" type="image/gif" href="https://assets.aesthetic.computer/kidlisp-favicon.gif" /> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Comic+Relief:ital,wght@0,400;0,700;1,400;1,700&family=Noto+Sans+Mono:wght@400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Mono&text=%EA%9C%A9&display=swap" rel="stylesheet"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> <style> @font-face { font-family: 'YWFTProcessing-Regular'; src: url('https://aesthetic.computer/type/webfonts/ywft-processing-regular.woff2') format('woff2'); font-weight: normal; font-style: normal; font-display: swap; }
:root { /* Dark (default) — vegas night */ --bg: #12101a; --bg2: #1a1726; --bg3: #221e30; --bg-grad-1: rgba(205, 92, 155, 0.16); --bg-grad-2: rgba(112, 214, 255, 0.12); --text: #e8e0f0; --text2: #9088a0; --border: #2e2840; --accent: #f59e0b; --accent-bg: rgba(245, 158, 11, 0.15); --green: #00ff88; --blue: #70D6FF; --red: #ff6b6b; --purple: rgb(205, 92, 155); --gold: #FFE66D; --neon-pink: #ff6eb4; --neon-cyan: #4ECDC4; --font-mono: 'Noto Sans Mono', 'SF Mono', 'Consolas', monospace; --font-display: 'YWFTProcessing-Regular', monospace; --font-fun: 'Comic Relief', 'Comic Sans MS', cursive; --radius: 10px; --card-glow: rgba(205, 92, 155, 0.15); --card-hover-glow: rgba(245, 158, 11, 0.25); }
@media (prefers-color-scheme: light) { :root { --bg: #faf8ff; --bg2: #f0ecf8; --bg3: white; --bg-grad-1: rgba(205, 92, 155, 0.12); --bg-grad-2: rgba(112, 214, 255, 0.1); --text: #1a1528; --text2: #6a5f80; --border: #ddd4ee; --card-glow: rgba(205, 92, 155, 0.08); --card-hover-glow: rgba(245, 158, 11, 0.15); } }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font-fun); background: radial-gradient(1100px 620px at 8% -12%, var(--bg-grad-1), transparent 70%), radial-gradient(900px 560px at 92% -18%, var(--bg-grad-2), transparent 72%), var(--bg); color: var(--text); min-height: 100vh; -webkit-font-smoothing: antialiased; background-attachment: fixed; }
/* ── Header ── */ header { display: flex; align-items: center; justify-content: space-between; padding: 12px 20px; border-bottom: 1px solid var(--border); background: var(--bg2); position: sticky; top: 0; z-index: 100; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15); backdrop-filter: blur(8px); }
.logo { font-family: var(--font-fun); font-weight: bold; font-size: 20px; text-decoration: none; display: flex; align-items: center; gap: 0; } .logo:hover { text-decoration: none; } .logo span { text-shadow: 1px 1px 0px rgba(0,0,0,0.3); } .logo .buy-b { color: #FF6B6B; } .logo .buy-u { color: #4ECDC4; } .logo .buy-y { color: #FFE66D; } .logo .dot { color: #70D6FF; text-shadow: none; } .logo .baby-k { color: #FF6B6B; } .logo .baby-i { color: #4ECDC4; } .logo .baby-d { color: #FFE66D; } .logo .baby-l { color: #A8E6CF; } .logo .baby-i2 { color: #FF8B94; } .logo .baby-s { color: #F7DC6F; } .logo .baby-p { color: #BB8FCE; } .logo .com-part { color: #70D6FF; text-shadow: none; }
.header-right { display: flex; align-items: center; gap: 10px; }
.wallet-btn { background: linear-gradient(135deg, rgba(78, 205, 196, 0.15), rgba(205, 92, 155, 0.15)); border: 1px solid var(--neon-cyan); color: var(--neon-cyan); padding: 8px 14px; border-radius: var(--radius); font-family: 'Comic Relief', 'Comic Sans MS', 'Noto Sans Mono', cursive; font-size: 12px; font-weight: 700; cursor: pointer; transition: all 0.2s; } .wallet-btn:hover { background: rgba(78, 205, 196, 0.25); box-shadow: 0 0 12px rgba(78, 205, 196, 0.3); } .wallet-btn.connected { border-color: var(--green); color: var(--green); background: rgba(0, 255, 136, 0.1); }
/* ── Grid (Fresh = single column list, Sold = dense grid) ── */ .grid { display: flex; flex-direction: column; gap: 12px; padding: 18px 16px 14px; max-width: 760px; margin: 0 auto; } .grid.sold-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 4px; max-width: 1400px; padding: 10px; }
/* ── Fresh card (full-bleed background strip) ── */ .card { position: relative; border: 1px solid var(--border); border-radius: 14px; overflow: hidden; cursor: pointer; transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s; box-shadow: 0 2px 8px var(--card-glow); display: flex; flex-direction: row; align-items: center; min-height: 112px; background-size: cover; background-position: center; background-color: var(--bg3); } .card::before { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0.45) 50%, rgba(0,0,0,0.15) 100%); z-index: 1; } .card:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 6px 20px var(--card-hover-glow); }
@keyframes shimmer { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
.card-body { position: relative; z-index: 2; padding: 15px 18px; display: flex; flex-direction: column; justify-content: center; gap: 4px; flex: 1; min-width: 0; }
.card-code { font-size: 20px; font-weight: 700; font-family: var(--font-display); letter-spacing: 0.02em; color: #fff; text-shadow: 0 1px 4px rgba(0,0,0,0.6); }
.card-price { font-size: 17px; font-weight: 700; color: var(--gold); text-shadow: 0 1px 4px rgba(0,0,0,0.5); }
.card-seller { font-size: 10px; color: rgba(255,255,255,0.6); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
/* ── Sold mini-cards (dense grid of stills) ── */ .sold-mini { aspect-ratio: 1; background: var(--bg3); border-radius: 4px; overflow: hidden; cursor: pointer; position: relative; transition: transform 0.15s, box-shadow 0.15s; } .sold-mini:hover { transform: scale(1.05); box-shadow: 0 4px 12px var(--card-hover-glow); z-index: 1; } .sold-mini img { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; } .sold-mini .sold-mini-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.15s; font-size: 9px; font-family: var(--font-mono); color: var(--gold); text-align: center; padding: 2px; } .sold-mini:hover .sold-mini-overlay { opacity: 1; }
/* ── Detail modal (responsive full-page) ── */ .modal-overlay { display: none; position: fixed; inset: 0; background: var(--bg); z-index: 200; flex-direction: column; } .modal-overlay.open { display: flex; } /* Deep-link: show the opaque modal from first paint so the home grid never flashes */ html.deeplink .modal-overlay { display: flex; }
.modal { flex: 1; display: flex; flex-direction: column; overflow: hidden; height: 100%; }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 14px; border-bottom: 1px solid var(--border); background: var(--bg2); flex-shrink: 0; }
.modal-title { font-size: 21px; font-weight: 700; color: var(--purple); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.modal-close { background: none; color: var(--text2); border: 1px solid var(--border); width: 36px; height: 36px; flex-shrink: 0; border-radius: var(--radius); cursor: pointer; font-size: 18px; transition: border-color 0.15s; } .modal-close:hover { border-color: var(--accent); color: var(--accent); }
.modal-preview { flex: 1; background: var(--bg3); position: relative; min-height: 0; } .modal-preview iframe { width: 100%; height: 100%; border: none; }
.modal-footer { padding: 10px 14px; display: flex; align-items: center; gap: 10px; border-top: 1px solid var(--border); background: var(--bg2); flex-shrink: 0; flex-wrap: wrap; }
.modal-price { font-size: 23px; font-weight: 700; color: var(--gold); }
.modal-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.modal-seller { font-size: 13px; color: var(--neon-pink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.modal-permalink { font-size: 14px; font-family: var(--font-mono); color: var(--neon-cyan); cursor: pointer; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .modal-permalink:hover { text-decoration: underline; }
.buy-btn { background: var(--gold); color: #1a1528; border: none; padding: 14px 32px; border-radius: var(--radius); font-family: var(--font-fun); font-size: 17px; font-weight: 700; cursor: pointer; transition: opacity 0.15s, transform 0.15s, box-shadow 0.15s; box-shadow: 0 2px 12px rgba(255, 230, 109, 0.25); flex-shrink: 0; } .buy-btn:hover { opacity: 0.9; transform: scale(1.03); box-shadow: 0 4px 20px rgba(255, 230, 109, 0.4); } .buy-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; }
.buy-status { font-size: 13px; color: var(--text2); min-height: 16px; width: 100%; } .buy-status.success { color: var(--green); } .buy-status.error { color: var(--red); }
.modal-links { display: flex; gap: 16px; font-size: 15px; font-weight: 600; } .modal-links a { color: var(--text2); text-decoration: none; } /* Color-coded explorer links */ .modal-links a:nth-child(1) { color: var(--purple); } /* objkt */ .modal-links a:nth-child(2) { color: var(--neon-cyan); } /* tzkt */ .modal-links a:hover { text-decoration: underline; filter: brightness(1.2); }
/* ── Empty / loading ── */ .loading { text-align: center; padding: 60px 20px; color: var(--text2); font-size: 14px; }
/* ── Tabs ── */ .tabs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: stretch; gap: 0; padding: 0; background: var(--bg2); border-bottom: 1px solid var(--border); overflow: hidden; width: 100%; } .tab { width: 100%; min-width: 0; padding: 16px 12px; font-family: var(--font-fun); font-size: 20px; font-weight: 700; text-align: center; background: var(--bg3); border: none; border-bottom: 4px solid transparent; cursor: pointer; transition: all 0.15s; user-select: none; } .tab:hover { filter: brightness(1.05); } .tab[data-tab="fresh"] { color: var(--green); 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="albums"] { color: var(--gold); background: rgba(255, 230, 109, 0.1); text-shadow: 0 2px 6px rgba(255, 230, 109, 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="albums"] { background: rgba(255, 230, 109, 0.2); border-bottom-color: var(--gold); box-shadow: inset 0 -1px 0 rgba(255, 230, 109, 0.5); }
.shop-intro { max-width: 760px; margin: 12px auto 0; padding: 10px 16px; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.03em; text-transform: uppercase; color: var(--text2); border-left: 2px solid var(--accent); background: rgba(255, 255, 255, 0.03); } @media (prefers-color-scheme: light) { .shop-intro { background: rgba(255, 255, 255, 0.6); } } /* ── 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; background-size: cover; background-position: center; background-repeat: no-repeat; position: relative; } .album-row::before { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, var(--bg) 0%, rgba(18, 16, 26, 0.85) 40%, rgba(18, 16, 26, 0.6) 100%); pointer-events: none; } .album-row > * { position: relative; z-index: 1; } .album-row > .album-bar { z-index: 0; } .album-row:hover::before { background: linear-gradient(90deg, var(--bg3) 0%, rgba(34, 30, 48, 0.85) 40%, rgba(34, 30, 48, 0.5) 100%); } .album-bar { position: absolute; top: 0; left: 0; bottom: 0; border-radius: 0; background: var(--blue); opacity: 0.35; transition: width 0.3s ease; z-index: 0; pointer-events: none; } .album-row:first-child .album-bar { background: var(--gold); } .album-rank { font-size: 18px; min-width: 28px; text-align: center; font-weight: 900; text-shadow: 0 1px 4px rgba(0,0,0,0.7); } .album-name { font-family: var(--font-fun); font-size: 18px; font-weight: 900; color: var(--text); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-shadow: 0 1px 4px rgba(0,0,0,0.7); } .album-count { font-family: var(--font-mono); font-size: 16px; color: var(--text); font-weight: 900; flex-shrink: 0; text-shadow: 0 1px 4px rgba(0,0,0,0.7); } .album-tokens { display: none; flex-wrap: wrap; gap: 10px; padding: 12px 20px 20px 64px; } .album-tokens.open { display: flex; } .album-thumb { width: 80px; cursor: pointer; transition: transform 0.15s; } .album-thumb:hover { transform: scale(1.08); } .album-thumb img { width: 80px; height: 80px; object-fit: cover; border-radius: 8px; border: 1px solid var(--border); display: block; } .album-thumb-label { font-family: var(--font-mono); font-size: 11px; color: var(--text2); text-align: center; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card-buy-btn { position: relative; z-index: 2; background: linear-gradient(180deg, #fff3ac 0%, var(--gold) 100%); color: #1a1528; border: none; padding: 12px 20px; margin-right: 14px; font-family: var(--font-fun); font-size: 14px; font-weight: 700; cursor: pointer; transition: opacity 0.15s, transform 0.15s; letter-spacing: 0.02em; border-radius: 8px; white-space: nowrap; align-self: center; flex-shrink: 0; box-shadow: 0 2px 8px rgba(255, 230, 109, 0.38); } .card-buy-btn:hover { opacity: 0.85; transform: scale(1.03); } .card-buy-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
/* ── Larger screens: scale typography up ── */ @media (min-width: 900px) { .logo { font-size: 24px; } .tab { font-size: 24px; padding: 20px 14px; } .shop-intro { font-size: 12px; padding: 12px 18px; } .grid { max-width: 820px; } .card { min-height: 124px; } .card-body { padding: 18px 22px; } .card-code { font-size: 24px; } .card-price { font-size: 19px; } .card-seller { font-size: 11px; } .card-buy-btn { font-size: 15px; padding: 14px 24px; } .loading { font-size: 16px; } .modal-title { font-size: 18px; } .modal-price { font-size: 21px; } .buy-btn { font-size: 18px; padding: 15px 34px; } }
@media (min-width: 1300px) { .logo { font-size: 27px; } .tab { font-size: 27px; padding: 22px 16px; } .shop-intro { font-size: 13px; } .grid { max-width: 940px; padding-top: 22px; } .card { min-height: 136px; } .card-code { font-size: 28px; } .card-price { font-size: 21px; } .card-seller { font-size: 12px; } .card-buy-btn { font-size: 16px; padding: 16px 26px; } }
/* ── Footer ── */ footer { text-align: center; padding: 40px 20px; border-top: 1px solid var(--border); margin-top: 40px; display: flex; align-items: center; justify-content: center; gap: 6px; } .footer-label { font-family: var(--font-fun); font-size: 20px; font-weight: bold; color: var(--text2); } .footer-keeps { font-family: var(--font-fun); font-weight: bold; font-size: 20px; text-decoration: none; display: inline-flex; gap: 0; } .footer-keeps:hover { text-decoration: none; } .footer-keeps span { text-shadow: 1px 1px 0px rgba(0,0,0,0.3); } @keyframes keeps-green { 0% { color: #00ff41; } 25% { color: #39ff7a; } 50% { color: #00c832; } 75% { color: #80ffb0; } 100% { color: #00ff41; } } .footer-keeps .kl0 { color: #00ff41; animation: keeps-green 3s ease-in-out infinite; } .footer-keeps .kl1 { color: #00ff41; animation: keeps-green 3s ease-in-out infinite -0.6s; } .footer-keeps .kl2 { color: #00ff41; animation: keeps-green 3s ease-in-out infinite -1.2s; } .footer-keeps .kl3 { color: #00ff41; animation: keeps-green 3s ease-in-out infinite -1.8s; } .footer-keeps .kl4 { color: #00ff41; animation: keeps-green 3s ease-in-out infinite -2.4s; } .footer-keeps .baby-k { color: #FF6B6B; } .footer-keeps .baby-i { color: #4ECDC4; } .footer-keeps .baby-d { color: #FFE66D; } .footer-keeps .baby-l { color: #A8E6CF; } .footer-keeps .baby-i2 { color: #FF8B94; } .footer-keeps .baby-s { color: #F7DC6F; } .footer-keeps .baby-p { color: #BB8FCE; } .footer-keeps .com-part { color: #70D6FF; text-shadow: none; } </style></head><body>
<header> <a href="/" class="logo"><span class="buy-b">b</span><span class="buy-u">u</span><span class="buy-y">y</span><span class="dot">.</span><span class="baby-k">K</span><span class="baby-i">i</span><span class="baby-d">d</span><span class="baby-l">L</span><span class="baby-i2">i</span><span class="baby-s">s</span><span class="baby-p">p</span><span class="com-part">.com</span></a> <div class="header-right"> <button class="wallet-btn" id="wallet-btn">ꜩ Connect Tezos</button> </div> </header>
<div class="tabs"> <button class="tab active" data-tab="fresh">Shop</button> <button class="tab" data-tab="albums">Buyers</button> </div>
<div class="grid" id="grid"> <div class="loading">Loading listings…</div> </div>
<div class="modal-overlay" id="modal-overlay"> <div class="modal" id="modal"> <div class="modal-header"> <div class="modal-title" id="modal-title"></div> <button class="modal-close" id="modal-close">✕</button> </div> <div class="modal-preview" id="modal-preview"><div class="loading">Loading keep…</div></div> <div class="modal-footer"> <div class="modal-price" id="modal-price"></div> <div class="modal-info"> <div class="modal-seller" id="modal-seller"></div> <a class="modal-permalink" id="modal-permalink" target="_blank"></a> </div> <button class="buy-btn" id="buy-btn" disabled>Connect Tezos to buy</button> <div class="buy-status" id="buy-status"></div> <div class="modal-links" id="modal-links"></div> </div> </div> </div>
<footer> <div class="footer-label">See also:</div> <a href="https://keep.kidlisp.com" class="footer-keeps"><span class="kl0">k</span><span class="kl1">e</span><span class="kl2">e</span><span class="kl3">p</span><span class="com-part">.</span><span class="baby-k">K</span><span class="baby-i">i</span><span class="baby-d">d</span><span class="baby-l">L</span><span class="baby-i2">i</span><span class="baby-s">s</span><span class="baby-p">p</span><span class="com-part">.com</span></a> </footer>
<script>(function() { 'use strict';
// ── Config ── const CONTRACT = 'KT1Q1irsjSZ7EfUN4qHzAB2t7xLBPsAWYwBB'; const MARKETPLACE = 'KT1SwbTqhSKF6Pdokiu1K4Fpi17ahPPzmt1X'; const OBJKT_GQL = 'https://data.objkt.com/v3/graphql'; const OBJKT_BASE = 'https://objkt.com'; const OVEN = 'https://oven.aesthetic.computer/grab/png/280/210/'; const REFRESH_MS = 15000; const KEEPS_URL = 'https://keep.kidlisp.com';
// ── State ── let listings = []; let soldEvents = []; let albums = []; let walletAddress = null; let beaconClient = null; let currentTab = 'fresh'; let selectedListing = null;
// ── Address → .tez domain resolver ── const domainCache = {}; async function resolveSellerNames(addresses) { const unknown = [...new Set(addresses)].filter(a => !(a in domainCache)); if (!unknown.length) return; await Promise.all(unknown.map(async (addr) => { try { const res = await fetch(`https://api.tzkt.io/v1/domains?owner=${addr}&limit=1`); const data = await res.json(); domainCache[addr] = data?.[0]?.name || null; } catch { domainCache[addr] = null; } })); } function sellerName(addr) { if (!addr) return '?'; const domain = domainCache[addr]; return domain || addr; }
// ── DOM ── const grid = document.getElementById('grid'); const walletBtn = document.getElementById('wallet-btn'); const overlay = document.getElementById('modal-overlay'); const buyBtn = document.getElementById('buy-btn'); const buyStatus = document.getElementById('buy-status');
// ── Beacon SDK ── async function loadBeaconSDK() { if (window.beacon) return; const origDefProp = Object.defineProperty; Object.defineProperty = function(obj, prop, desc) { if (obj === window && prop === 'ethereum') { try { return origDefProp.call(this, obj, prop, desc); } catch { return obj; } } return origDefProp.call(this, obj, prop, desc); }; await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = 'https://unpkg.com/@airgap/beacon-sdk@4.8.1/dist/walletbeacon.min.js'; s.onload = resolve; s.onerror = (e) => { Object.defineProperty = origDefProp; reject(e); }; document.head.appendChild(s); }); }
async function initWallet() { await loadBeaconSDK(); beaconClient = new window.beacon.DAppClient({ name: 'KidLisp Buy', network: { type: window.beacon.NetworkType.MAINNET }, walletConnectOptions: { projectId: '647c41004744244efab53b7c6ef80443' }, }); beaconClient.subscribeToEvent(window.beacon.BeaconEvent.ACTIVE_ACCOUNT_SET, (account) => { if (account) { walletAddress = account.address; updateWalletUI(); } }); const active = await beaconClient.getActiveAccount(); if (active) { walletAddress = active.address; updateWalletUI(); } }
async function connectWallet() { if (!beaconClient) await initWallet(); if (walletAddress) { // Disconnect await beaconClient.clearActiveAccount(); await beaconClient.destroy().catch(() => {}); beaconClient = null; walletAddress = null; updateWalletUI(); return; } const perms = await beaconClient.requestPermissions(); walletAddress = perms.address; updateWalletUI(); }
function updateWalletUI() { if (walletAddress) { walletBtn.textContent = 'ꜩ ' + walletAddress.slice(0, 5) + '…' + walletAddress.slice(-4); walletBtn.classList.add('connected'); if (selectedListing) { buyBtn.disabled = false; buyBtn.textContent = 'Buy for ' + formatXTZ(selectedListing.price_xtz) + ' XTZ'; } } else { walletBtn.textContent = 'ꜩ Connect Tezos'; walletBtn.classList.remove('connected'); buyBtn.disabled = true; buyBtn.textContent = 'Connect Tezos to buy'; } }
walletBtn.addEventListener('click', connectWallet);
// ── Data ── async function fetchListings() { const query = `query { listing_active( where: { fa_contract: { _eq: "${CONTRACT}" } } order_by: { timestamp: desc } limit: 200 ) { id bigmap_key price_xtz timestamp seller_address token { token_id name thumbnail_uri artifact_uri creators { creator_address } } } }`;
const res = await fetch(OBJKT_GQL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }), }); const data = await res.json(); return data?.data?.listing_active || []; }
async function fetchSoldEvents() { const query = `query { event( where: { fa_contract: { _eq: "${CONTRACT}" } marketplace_event_type: { _eq: "list_buy" } } order_by: { timestamp: desc } limit: 200 ) { timestamp price_xtz recipient_address creator_address token { token_id name thumbnail_uri artifact_uri } } }`; const res = await fetch(OBJKT_GQL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }), }); const data = await res.json(); return data?.data?.event || []; }
async function fetchAlbums() { const [balRes, creatorsRes] = await Promise.all([ 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`), fetch(`https://api.tzkt.io/v1/tokens/transfers?token.contract=${CONTRACT}&from.null=true&limit=200&select=to.address,token.tokenId`), ]); const data = await balRes.json(); const creators = await creatorsRes.json(); // Build set of original minters (token creator = first recipient of mint) const minterByToken = {}; for (const c of creators) minterByToken[String(c['token.tokenId'])] = c['to.address']; // Build set of token IDs currently listed for sale const listedIds = new Set(listings.map(l => String(l.token?.token_id))); // Group by holder, excluding listed tokens and original minters const grouped = {}; for (const b of data) { const addr = b['account.address']; const tokenId = String(b['token.tokenId']); if (listedIds.has(tokenId)) continue; if (addr === minterByToken[tokenId]) continue; 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'], thumbnailUri: b['token.metadata.thumbnailUri'] }); } // Sort by count desc, filter out empty return Object.values(grouped).filter(a => a.tokens.length > 0).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); }
function thumbUrl(uri, name) { if (name) { const code = name.startsWith('$') ? name : '$' + name; return `${OVEN}${code}?density=1&nowait=true&source=buy`; } if (!uri) return ''; if (uri.startsWith('ipfs://')) { return 'https://ipfs.aesthetic.computer/ipfs/' + uri.replace('ipfs://', ''); } return uri; }
function animatedWebpUrl(uri) { if (!uri) return ''; if (uri.startsWith('ipfs://')) { return 'https://ipfs.aesthetic.computer/ipfs/' + uri.replace('ipfs://', ''); } return uri; }
// ── Tabs ── document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); currentTab = tab.dataset.tab; renderGrid(); }); });
// ── Render ── function renderGrid() { if (currentTab === 'albums') return renderAlbumsGrid();
grid.classList.remove('sold-grid', 'albums-grid'); // Sort by most recently listed first const sorted = [...listings].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); if (!sorted.length) { grid.innerHTML = '<div class="loading">No active listings</div>'; return; }
grid.innerHTML = sorted.map(l => { const name = l.token?.name || `#${l.token?.token_id}`; const animated = animatedWebpUrl(l.token?.thumbnail_uri); const price = formatXTZ(l.price_xtz); const seller = sellerName(l.seller_address); const creator = l.token?.creators?.[0]?.creator_address; const creatorLabel = creator ? sellerName(creator) : ''; const listed = new Date(l.timestamp).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); const askId = l.id || l.bigmap_key; const bgStyle = animated ? `background-image:url('${animated}')` : ''; return `<div class="card" data-id="${askId}" style="${bgStyle}"> <div class="card-body"> <div class="card-code">${name}</div> <div class="card-price">${price} XTZ</div> <div class="card-seller">seller: ${seller}</div> ${creatorLabel ? `<div class="card-seller">author: ${creatorLabel}</div>` : ''} <div class="card-seller">listed: ${listed}</div> </div> <button class="card-buy-btn" data-ask="${askId}">${walletAddress ? 'Buy — ' + price + ' XTZ' : 'Buy'}</button> </div>`; }).join('');
// Card click → modal grid.querySelectorAll('.card-body').forEach(el => { el.addEventListener('click', () => { const id = el.closest('.card').dataset.id; const listing = listings.find(l => String(l.id || l.bigmap_key) === id); if (listing) openModal(listing); }); });
// Buy button → direct purchase (no modal) grid.querySelectorAll('.card-buy-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const askId = btn.dataset.ask; const listing = listings.find(l => String(l.id || l.bigmap_key) === askId); if (listing) purchaseFromCard(listing, btn); }); }); }
function renderAlbumsGrid() { grid.classList.remove('sold-grid'); grid.classList.add('albums-grid'); if (!albums.length) { grid.innerHTML = '<div class="loading">Loading albums…</div>'; fetchAlbums().then(data => { albums = data; renderAlbumsGrid(); }); return; }
const maxCount = albums[0]?.tokens.length || 1; 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)); const lastToken = tokens[tokens.length - 1]; const lastThumb = animatedWebpUrl(lastToken?.thumbnailUri); const rowBg = lastThumb ? `background-image:url('${lastThumb}')` : ''; const pct = Math.round((album.tokens.length / maxCount) * 100); const medal = i === 0 ? '\uD83E\uDD47' : i === 1 ? '\uD83E\uDD48' : i === 2 ? '\uD83E\uDD49' : `${i + 1}`; return `<div class="album-entry"> <div class="album-row" data-idx="${i}" style="${rowBg}"> <div class="album-bar" style="width:${pct}%"></div> <div class="album-rank">${medal}</div> <div class="album-name">${name}</div> <div class="album-count">${album.tokens.length}</div> </div> <div class="album-tokens" data-idx="${i}"> ${tokens.map(t => { const thumb = animatedWebpUrl(t.thumbnailUri); return `<div class="album-thumb" data-code="${t.name}"> ${thumb ? `<img src="${thumb}" alt="${t.name}" loading="lazy" />` : `<div style="width:80px;height:80px;background:var(--bg3);border-radius:8px;border:1px solid var(--border)"></div>`} <div class="album-thumb-label">${t.name}</div> </div>`; }).join('')} </div> </div>`; }).join('');
grid.querySelectorAll('.album-row').forEach(row => { row.addEventListener('click', () => { const tokens = row.nextElementSibling; tokens.classList.toggle('open'); }); });
grid.querySelectorAll('.album-thumb').forEach(thumb => { thumb.addEventListener('click', (e) => { e.stopPropagation(); const code = thumb.dataset.code; window.open(`https://aesthetic.computer/${code}`, '_blank'); }); }); }
function openSoldModal(ev) { const name = ev.token?.name || `#${ev.token?.token_id}`; const tokenId = ev.token?.token_id; const price = formatXTZ(ev.price_xtz); const code = name.startsWith('$') ? name.slice(1) : tokenId;
const permalink = location.origin + '/' + (name.startsWith('$') ? name : '$' + code);
selectedListing = null; // Not purchasable document.getElementById('modal-title').textContent = name + ' (sold)'; document.getElementById('modal-price').textContent = price + ' XTZ'; document.getElementById('modal-seller').textContent = 'Buyer: ' + ev.recipient_address;
const permalinkEl = document.getElementById('modal-permalink'); permalinkEl.textContent = permalink; permalinkEl.href = permalink;
const preview = document.getElementById('modal-preview'); preview.innerHTML = ''; const artUri = ev.token?.artifact_uri || ev.token?.thumbnail_uri; if (artUri) { const ipfsUrl = artUri.startsWith('ipfs://') ? 'https://ipfs.aesthetic.computer/ipfs/' + artUri.replace('ipfs://', '') : artUri; const iframe = document.createElement('iframe'); iframe.src = ipfsUrl; iframe.setAttribute('sandbox', 'allow-scripts'); preview.appendChild(iframe); }
document.getElementById('modal-links').innerHTML = ` <a href="${OBJKT_BASE}/tokens/${CONTRACT}/${tokenId}" target="_blank">objkt ↗</a> `;
buyBtn.disabled = true; buyBtn.textContent = 'Sold'; buyStatus.textContent = new Date(ev.timestamp).toLocaleString(); buyStatus.className = 'buy-status';
history.pushState({ code }, name, '/' + (name.startsWith('$') ? name : '$' + code)); overlay.classList.add('open'); }
// ── Modal (fullscreen) ── function openModal(listing) { selectedListing = listing; const name = listing.token?.name || `#${listing.token?.token_id}`; const price = formatXTZ(listing.price_xtz); const tokenId = listing.token?.token_id; const code = name.startsWith('$') ? name.slice(1) : tokenId;
const permalink = location.origin + '/' + (name.startsWith('$') ? name : '$' + code);
document.getElementById('modal-title').textContent = name; document.getElementById('modal-price').textContent = price + ' XTZ'; document.getElementById('modal-seller').textContent = listing.seller_address;
const permalinkEl = document.getElementById('modal-permalink'); permalinkEl.textContent = permalink; permalinkEl.href = permalink;
// Embed artifact directly from IPFS (self-contained HTML) const preview = document.getElementById('modal-preview'); preview.innerHTML = ''; const artUri = listing.token?.artifact_uri || listing.token?.thumbnail_uri; if (artUri) { const ipfsUrl = artUri.startsWith('ipfs://') ? 'https://ipfs.aesthetic.computer/ipfs/' + artUri.replace('ipfs://', '') : artUri; const iframe = document.createElement('iframe'); iframe.src = ipfsUrl; iframe.setAttribute('sandbox', 'allow-scripts'); preview.appendChild(iframe); }
// Links document.getElementById('modal-links').innerHTML = ` <a href="${OBJKT_BASE}/tokens/${CONTRACT}/${tokenId}" target="_blank">objkt ↗</a> <a href="https://tzkt.io/${CONTRACT}/tokens/${tokenId}" target="_blank">tzkt ↗</a> `;
buyStatus.textContent = ''; buyStatus.className = 'buy-status';
if (walletAddress) { if (walletAddress === listing.seller_address) { buyBtn.disabled = true; buyBtn.textContent = 'Your listing'; } else { buyBtn.disabled = false; buyBtn.textContent = 'Buy for ' + price + ' XTZ'; } } else { buyBtn.disabled = false; buyBtn.textContent = 'Connect Tezos to buy'; }
// Update URL to permalink history.pushState({ code }, name, '/' + (name.startsWith('$') ? name : '$' + code)); overlay.classList.add('open'); }
function closeModal(fromPopstate) { overlay.classList.remove('open'); selectedListing = null; document.getElementById('modal-preview').innerHTML = ''; if (!fromPopstate) history.pushState({}, '', '/'); }
document.getElementById('modal-close').addEventListener('click', () => closeModal(false)); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && overlay.classList.contains('open')) closeModal(false); });
// Handle back button window.addEventListener('popstate', () => { if (overlay.classList.contains('open')) { closeModal(true); } else { routeFromURL(); } });
// Permalink routing: /$code opens that listing's modal function routeFromURL() { const path = location.pathname.replace(/^\/+/, ''); if (path.startsWith('$') && path.length > 1) { const code = path; const listing = listings.find(l => l.token?.name === code); if (listing) openModal(listing); } }
// Deep-link no-flash: on a /$code URL, open the modal shell synchronously at // boot (before any fetch) so the home grid never shows. Real content fills in // once listings load; if the code matches nothing we fall back to home. let deepLinkPending = false; function preloadDeepLink() { const path = location.pathname.replace(/^\/+/, ''); if (!(path.startsWith('$') && path.length > 1)) return; deepLinkPending = true; document.getElementById('modal-title').textContent = path; document.getElementById('modal-preview').innerHTML = '<div class="loading">Loading keep…</div>'; document.getElementById('modal-price').textContent = ''; document.getElementById('modal-seller').textContent = ''; document.getElementById('modal-permalink').textContent = ''; document.getElementById('modal-links').innerHTML = ''; buyStatus.textContent = ''; buyStatus.className = 'buy-status'; buyBtn.disabled = true; buyBtn.textContent = 'Loading…'; overlay.classList.add('open'); // real open state… document.documentElement.classList.remove('deeplink'); // …so closeModal works }
// ── Purchase (shared logic) ── async function executePurchase(listing) { if (!walletAddress) { await connectWallet(); if (!walletAddress) throw new Error('Wallet not connected'); } if (!beaconClient) await initWallet();
const askId = Number(listing.bigmap_key || listing.id); const priceMutez = String(listing.price_xtz);
const response = await beaconClient.requestOperation({ sourceAddress: walletAddress, operationDetails: [{ kind: 'transaction', destination: MARKETPLACE, amount: priceMutez, parameters: { entrypoint: 'fulfill_ask', value: { prim: 'Pair', args: [ { int: String(askId) }, { prim: 'Pair', args: [ { int: '1' }, { prim: 'Pair', args: [ { prim: 'None' }, { prim: 'Pair', args: [{ prim: 'None' }, []] } ]} ]} ] } } }] });
// Remove from local listings listings = listings.filter(l => (l.id || l.bigmap_key) !== (listing.id || listing.bigmap_key)); renderGrid();
return response.transactionHash; }
// Purchase from card "Buy Now" button async function purchaseFromCard(listing, btn) { if (!walletAddress) { await connectWallet(); if (!walletAddress) return; renderGrid(); // re-render with updated wallet state return; }
const origText = btn.textContent; btn.disabled = true; btn.textContent = 'Confirm in wallet…';
try { const txHash = await executePurchase(listing); btn.textContent = 'Purchased! ' + txHash.slice(0, 8) + '…'; btn.style.background = 'var(--green)'; } catch (err) { const msg = err?.message || String(err); btn.textContent = msg.includes('aborted') || msg.includes('cancel') ? 'Cancelled' : 'Error'; btn.disabled = false; setTimeout(() => { btn.textContent = origText; }, 2000); } }
// Purchase from modal buyBtn.addEventListener('click', async () => { if (!selectedListing) return; if (!walletAddress) { await connectWallet(); if (!walletAddress) return; updateWalletUI(); return; }
const listing = selectedListing; buyBtn.disabled = true; buyBtn.textContent = 'Confirm in wallet…'; buyStatus.textContent = ''; buyStatus.className = 'buy-status';
try { const txHash = await executePurchase(listing); buyBtn.textContent = 'Purchased!'; buyStatus.textContent = 'tx: ' + txHash.slice(0, 12) + '…'; buyStatus.className = 'buy-status success'; setTimeout(() => closeModal(false), 3000); } catch (err) { const msg = err?.message || String(err); buyStatus.textContent = msg.includes('aborted') || msg.includes('cancel') ? 'Cancelled' : msg.slice(0, 80); buyStatus.className = 'buy-status error'; buyBtn.disabled = false; buyBtn.textContent = 'Buy for ' + formatXTZ(listing.price_xtz) + ' XTZ'; } });
// ── Init ── async function loadMarket() { try { 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();
// Route from URL on first load (after listings are available) if (!selectedListing) routeFromURL(); // Deep-link code matched nothing → drop the loading modal back to the shop if (deepLinkPending && !selectedListing) closeModal(true); } catch (err) { console.error('[buy] Failed to load:', err); if (deepLinkPending && !selectedListing) closeModal(true); } }
// Boot preloadDeepLink(); // open the modal shell before paint on /$code permalinks loadMarket(); initWallet().catch(() => {});})();</script>
<!-- Development live reload via session-server --><script>(function() { if (window.location.hostname !== 'localhost' && !window.location.hostname.includes('local.')) return;
let ws = null; let reconnect = null;
function connect() { if (ws && ws.readyState === WebSocket.OPEN) return; if (reconnect) { clearInterval(reconnect); reconnect = null; }
const url = window.location.hostname.includes('local.') ? 'wss://session.local.aesthetic.computer' : 'wss://localhost:8889';
try { ws = new WebSocket(url); } catch { scheduleReconnect(); return; }
ws.onmessage = (e) => { try { const msg = JSON.parse(e.data); if (msg.type === 'reload') { const piece = msg.content?.piece; if (piece === '*refresh*' || piece === 'buy' || piece?.includes('buy')) { setTimeout(() => window.location.reload(), 150); } } } catch {} };
ws.onclose = () => { ws = null; scheduleReconnect(); }; ws.onerror = () => {}; }
function scheduleReconnect() { if (!reconnect) reconnect = setInterval(connect, 3000); }
connect();})();</script></body></html>