Siteβ―Builder
Editing:
pixels.html
writable 0666
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>BestDealOn Pixel Exchange Prototype</title> <style> :root { --bg: #080b12; --panel: #111827; --panel-2: #0f172a; --ink: #e5edf8; --muted: #94a3b8; --line: rgba(148, 163, 184, 0.24); --accent: #60a5fa; --green: #22c55e; --yellow: #f59e0b; --red: #ef4444; --purple: #a78bfa; --cyan: #22d3ee; --tile: #1f2937; --shadow: 0 20px 70px rgba(0,0,0,.45); } * { box-sizing: border-box; } html, body { height: 100%; } body { margin: 0; background: radial-gradient(circle at 25% 12%, rgba(96,165,250,.18), transparent 35%), radial-gradient(circle at 80% 16%, rgba(167,139,250,.14), transparent 28%), linear-gradient(180deg, #080b12, #0a0f1d 55%, #070a12); color: var(--ink); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; overflow: hidden; } .app { height: 100vh; display: grid; grid-template-rows: 72px 1fr; } header { display: grid; grid-template-columns: 320px 1fr 420px; gap: 18px; align-items: center; padding: 14px 20px; border-bottom: 1px solid var(--line); backdrop-filter: blur(18px); background: rgba(8, 11, 18, .76); } .brand { display: flex; align-items: center; gap: 12px; min-width: 0; } .logo { width: 42px; height: 42px; border-radius: 12px; background: linear-gradient(90deg, rgba(255,255,255,.2) 1px, transparent 1px), linear-gradient(0deg, rgba(255,255,255,.2) 1px, transparent 1px), linear-gradient(135deg, #60a5fa, #a78bfa 45%, #22c55e); background-size: 7px 7px, 7px 7px, cover; box-shadow: 0 0 30px rgba(96,165,250,.35); } h1 { font-size: 18px; line-height: 1.1; margin: 0; letter-spacing: -.02em; } .subtitle { color: var(--muted); font-size: 12px; margin-top: 3px; } .searchbar { display: grid; grid-template-columns: 1fr auto auto auto; gap: 8px; } input, select, button, textarea { font-family: inherit; } .searchbar input, .searchbar select, .field input, .field select, .field textarea { width: 100%; border: 1px solid var(--line); background: rgba(15, 23, 42, .72); color: var(--ink); border-radius: 12px; padding: 11px 12px; outline: none; } .searchbar input:focus, .field input:focus, .field textarea:focus { border-color: rgba(96,165,250,.8); } button { border: 1px solid var(--line); color: var(--ink); background: rgba(30, 41, 59, .75); border-radius: 12px; padding: 10px 12px; cursor: pointer; font-weight: 650; letter-spacing: -.01em; } button:hover { background: rgba(51, 65, 85, .9); border-color: rgba(148, 163, 184, .4); } .primary { background: linear-gradient(135deg, #2563eb, #7c3aed); border-color: transparent; } .primary:hover { filter: brightness(1.08); } .danger { background: rgba(239,68,68,.14); border-color: rgba(239,68,68,.36); } .header-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; } .stat { background: rgba(15, 23, 42, .72); border: 1px solid var(--line); border-radius: 14px; padding: 8px 10px; min-width: 0; } .stat b { display: block; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .stat span { display: block; color: var(--muted); font-size: 11px; margin-top: 1px; } main { display: grid; grid-template-columns: minmax(720px, 1fr) 420px; min-height: 0; } .board-wrap { position: relative; min-width: 0; padding: 18px; overflow: hidden; } .board-shell { height: 100%; display: grid; grid-template-rows: auto 1fr auto; gap: 12px; border: 1px solid var(--line); border-radius: 24px; background: rgba(2, 6, 23, .54); box-shadow: var(--shadow); overflow: hidden; } .board-toolbar { display: grid; grid-template-columns: 1fr auto; gap: 10px; padding: 14px; border-bottom: 1px solid var(--line); background: rgba(15,23,42,.45); } .legend { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; color: var(--muted); font-size: 12px; } .dot { width: 9px; height: 9px; border-radius: 999px; display: inline-block; margin-right: 5px; vertical-align: -1px; } .dot.available { background: #334155; } .dot.sale { background: var(--green); } .dot.auction { background: var(--yellow); } .dot.owned { background: var(--accent); } .dot.bundle { background: var(--purple); } .toolbar-actions { display: flex; gap: 8px; } .canvas-stage { position: relative; overflow: hidden; cursor: crosshair; background: linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px), linear-gradient(0deg, rgba(255,255,255,.025) 1px, transparent 1px), radial-gradient(circle at center, rgba(59,130,246,.08), transparent 50%); background-size: 28px 28px, 28px 28px, cover; } canvas { display: block; width: 100%; height: 100%; image-rendering: pixelated; } .tooltip { position: absolute; pointer-events: none; z-index: 20; max-width: 270px; padding: 10px 12px; border: 1px solid rgba(148,163,184,.34); border-radius: 14px; background: rgba(15,23,42,.96); color: var(--ink); box-shadow: var(--shadow); transform: translate(12px, 12px); display: none; font-size: 12px; } .tooltip b { display: block; font-size: 13px; margin-bottom: 3px; } .selection-box { position: absolute; border: 2px solid #22d3ee; background: rgba(34,211,238,.12); display: none; pointer-events: none; z-index: 10; border-radius: 4px; } .board-footer { display: grid; grid-template-columns: 1fr auto; gap: 10px; padding: 12px 14px; border-top: 1px solid var(--line); background: rgba(15,23,42,.45); color: var(--muted); font-size: 12px; } .ticker { overflow: hidden; white-space: nowrap; } .ticker span { display: inline-block; padding-left: 100%; animation: crawl 34s linear infinite; } @keyframes crawl { from { transform: translateX(0); } to { transform: translateX(-100%); } } aside { min-height: 0; border-left: 1px solid var(--line); background: rgba(8, 11, 18, .82); display: grid; grid-template-rows: auto 1fr; } .tabs { display: grid; grid-template-columns: repeat(5, 1fr); padding: 12px 12px 0; gap: 5px; } .tab { font-size: 11px; padding: 9px 4px; border-radius: 11px 11px 0 0; border-bottom: 0; color: var(--muted); background: rgba(15,23,42,.6); } .tab.active { color: white; background: rgba(30,41,59,.92); border-color: rgba(96,165,250,.45); } .side-content { overflow: auto; padding: 14px; } .panel { border: 1px solid var(--line); background: rgba(15, 23, 42, .74); border-radius: 18px; padding: 14px; margin-bottom: 12px; } .panel h2, .panel h3 { margin: 0 0 8px; letter-spacing: -.02em; } .panel h2 { font-size: 18px; } .panel h3 { font-size: 14px; } .small { color: var(--muted); font-size: 12px; line-height: 1.5; } .row { display: flex; justify-content: space-between; gap: 12px; align-items: center; padding: 8px 0; border-top: 1px solid rgba(148,163,184,.14); } .row:first-child { border-top: none; } .pill { border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; font-size: 11px; color: var(--ink); background: rgba(30,41,59,.66); white-space: nowrap; } .pill.green { color: #bbf7d0; border-color: rgba(34,197,94,.4); background: rgba(34,197,94,.11); } .pill.yellow { color: #fde68a; border-color: rgba(245,158,11,.4); background: rgba(245,158,11,.12); } .pill.purple { color: #ddd6fe; border-color: rgba(167,139,250,.45); background: rgba(167,139,250,.12); } .pill.blue { color: #bfdbfe; border-color: rgba(96,165,250,.45); background: rgba(96,165,250,.12); } .btn-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; } .field { margin: 10px 0; } .field label { display: block; color: var(--muted); font-size: 12px; margin-bottom: 6px; } table { width: 100%; border-collapse: collapse; font-size: 12px; } th, td { padding: 8px 4px; border-bottom: 1px solid rgba(148,163,184,.14); text-align: left; } th { color: var(--muted); font-weight: 650; } tr.clickable { cursor: pointer; } tr.clickable:hover { background: rgba(96,165,250,.08); } .activity { display: grid; gap: 8px; } .event { display: grid; grid-template-columns: 36px 1fr; gap: 9px; align-items: start; border: 1px solid rgba(148,163,184,.14); border-radius: 14px; padding: 10px; background: rgba(2,6,23,.24); } .avatar { width: 36px; height: 36px; border-radius: 12px; display: grid; place-items: center; background: linear-gradient(135deg, rgba(96,165,250,.3), rgba(167,139,250,.2)); font-size: 17px; } .modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.62); display: none; z-index: 99; align-items: center; justify-content: center; padding: 20px; } .modal { width: min(620px, 96vw); max-height: 92vh; overflow: auto; background: rgba(15,23,42,.98); border: 1px solid rgba(148,163,184,.36); border-radius: 22px; box-shadow: var(--shadow); padding: 18px; } .modal-head { display: flex; justify-content: space-between; gap: 12px; align-items: start; margin-bottom: 8px; } .toast { position: fixed; right: 438px; bottom: 22px; background: rgba(15,23,42,.98); border: 1px solid rgba(34,197,94,.45); border-radius: 14px; padding: 12px 14px; box-shadow: var(--shadow); display: none; z-index: 110; max-width: 360px; font-size: 13px; } .mini-chart { height: 72px; display: flex; align-items: end; gap: 4px; padding-top: 6px; } .bar { flex: 1; border-radius: 5px 5px 0 0; background: linear-gradient(180deg, rgba(96,165,250,.85), rgba(96,165,250,.24)); min-height: 8px; } .progress { height: 8px; border-radius: 999px; background: rgba(148,163,184,.16); overflow: hidden; } .progress i { display:block; height:100%; background: linear-gradient(90deg, #22c55e, #60a5fa, #a78bfa); border-radius: inherit; } .kbd { border: 1px solid var(--line); background: rgba(2,6,23,.3); border-radius: 6px; padding: 1px 5px; color: var(--ink); } @media (max-width: 1120px) { body { overflow: auto; } .app { height: auto; min-height: 100vh; } header { grid-template-columns: 1fr; } main { grid-template-columns: 1fr; } aside { border-left: 0; border-top: 1px solid var(--line); min-height: 720px; } .board-wrap { height: 720px; } .toast { right: 20px; } } </style> </head> <body> <div class="app"> <header> <div class="brand"> <div class="logo" aria-hidden="true"></div> <div> <h1>BestDealOn Pixel Exchange</h1> <div class="subtitle">A live ad canvas where pixels become tradable creative territory</div> </div> </div> <div class="searchbar"> <input id="search" placeholder="Search brand, district, parcel ID, tagβ¦" /> <select id="statusFilter" aria-label="Filter by status"> <option value="all">All pixels</option> <option value="available">Available</option> <option value="sale">For sale</option> <option value="auction">Auction</option> <option value="owned">Owned</option> </select> <select id="viewMode" aria-label="View mode"> <option value="normal">Normal view</option> <option value="heat">Price heat map</option> <option value="liquidity">Bid pressure</option> <option value="renewal">Renewal clock</option> </select> <button id="randomHot" class="primary">Find hot deal</button> </div> <div class="header-stats"> <div class="stat"><b id="statFloor">$12.40</b><span>Floor / 10Γ10 parcel</span></div> <div class="stat"><b id="statVol">$38,920</b><span>24h matched value</span></div> <div class="stat"><b id="statListed">18.6%</b><span>Listed inventory</span></div> <div class="stat"><b id="statRenew">19 days</b><span>Median renewal window</span></div> </div> </header> <main> <section class="board-wrap"> <div class="board-shell"> <div class="board-toolbar"> <div class="legend"> <span><i class="dot available"></i>Available</span> <span><i class="dot sale"></i>For sale</span> <span><i class="dot auction"></i>Auction</span> <span><i class="dot owned"></i>Controlled</span> <span><i class="dot bundle"></i>Bundle</span> <span>Drag with <span class="kbd">Bundle mode</span> to group adjacent parcels.</span> </div> <div class="toolbar-actions"> <button id="zoomOut">β</button> <button id="zoomIn">+</button> <button id="bundleMode">Bundle mode</button> </div> </div> <div class="canvas-stage" id="stage"> <canvas id="board"></canvas> <div class="selection-box" id="selectionBox"></div> <div class="tooltip" id="tooltip"></div> </div> <div class="board-footer"> <div class="ticker"><span id="tickerText">Loading market tapeβ¦</span></div> <div id="cursorReadout">x: β y: β</div> </div> </div> </section> <aside> <div class="tabs"> <button class="tab active" data-tab="inspect">Inspect</button> <button class="tab" data-tab="exchange">Exchange</button> <button class="tab" data-tab="portfolio">Portfolio</button> <button class="tab" data-tab="bundle">Bundle</button> <button class="tab" data-tab="buzz">Buzz</button> </div> <div class="side-content"> <div id="tab-inspect" class="tabpane"></div> <div id="tab-exchange" class="tabpane" style="display:none"></div> <div id="tab-portfolio" class="tabpane" style="display:none"></div> <div id="tab-bundle" class="tabpane" style="display:none"></div> <div id="tab-buzz" class="tabpane" style="display:none"></div> </div> </aside> </main> </div> <div class="modal-backdrop" id="modalBackdrop"> <div class="modal" id="modal"></div> </div> <div class="toast" id="toast"></div> <script> (() => { const COLS = 72; const ROWS = 48; const CELL = 16; const districts = ["Prime Fold", "Coupon Alley", "Flash Plaza", "Brand Row", "Creator Corner", "Deal Dock", "Auction Arcade", "Renewal Ridge"]; const brands = ["Open Slot", "BentoBox", "Local Solar", "SneakerDrop", "PixelPup", "TravelMint", "MegaCoupon", "IndieGame", "CoffeeNow", "HomeGlow", "ClipCart", "NeonNoodle", "BestDealOn", "AdGarden", "DealForge"]; const tags = ["retail", "coupon", "launch", "local", "creator", "SaaS", "travel", "food", "finance", "gaming"]; const palette = { available: "#334155", sale: "#22c55e", auction: "#f59e0b", owned: "#60a5fa", bundle: "#a78bfa", reserved: "#64748b" }; const canvas = document.getElementById('board'); const ctx = canvas.getContext('2d'); const stage = document.getElementById('stage'); const tooltip = document.getElementById('tooltip'); const selectionBox = document.getElementById('selectionBox'); const statusFilter = document.getElementById('statusFilter'); const viewMode = document.getElementById('viewMode'); const search = document.getElementById('search'); const cursorReadout = document.getElementById('cursorReadout'); const tickerText = document.getElementById('tickerText'); const toast = document.getElementById('toast'); const modalBackdrop = document.getElementById('modalBackdrop'); const modal = document.getElementById('modal'); let zoom = 1; let offsetX = 0; let offsetY = 0; let selectedId = null; let bundleMode = false; let dragStart = null; let dragEnd = null; let hoverCell = null; function hash(n) { let x = Math.sin(n * 999.321) * 10000; return x - Math.floor(x); } function money(v) { return '$' + v.toLocaleString(undefined, {minimumFractionDigits: v < 100 ? 2 : 0, maximumFractionDigits: v < 100 ? 2 : 0}); } function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); } function days(n) { return `${n} day${n === 1 ? '' : 's'}`; } const parcels = []; for (let y = 0; y < ROWS; y++) { for (let x = 0; x < COLS; x++) { const idNum = y * COLS + x; const r = hash(idNum + 7); let status = 'available'; if (r > .82) status = 'sale'; if (r > .91) status = 'auction'; if (r > .955) status = 'owned'; const district = districts[Math.floor((x / COLS) * districts.length) % districts.length]; const centrality = 1 - Math.min(1, Math.hypot(x - COLS/2, y - ROWS/2) / Math.hypot(COLS/2, ROWS/2)); const price = Math.round((8 + centrality * 42 + hash(idNum + 11) * 18) * 100) / 100; const bidPressure = Math.round((centrality * .7 + hash(idNum + 12) * .6) * 100); const renewalDays = 2 + Math.floor(hash(idNum + 13) * 58); const brand = status === 'available' ? 'Open Slot' : brands[1 + Math.floor(hash(idNum + 14) * (brands.length - 1))]; parcels.push({ id: `BDO-${String(x).padStart(2,'0')}-${String(y).padStart(2,'0')}`, x, y, status, district, brand, tag: tags[Math.floor(hash(idNum + 15) * tags.length)], price, ask: Math.round(price * (1.08 + hash(idNum + 16) * .7) * 100) / 100, bid: Math.round(price * (.75 + hash(idNum + 17) * .36) * 100) / 100, bidPressure: clamp(bidPressure, 0, 100), renewalDays, owner: status === 'available' ? 'BestDealOn Treasury' : ['DealDAO', 'PixelWhale', 'LocalCrew', 'BrandDesk', 'CreatorPool'][Math.floor(hash(idNum+18)*5)], term: [30, 90, 180, 365][Math.floor(hash(idNum+19)*4)], ctr: Math.round((0.4 + centrality * 2.7 + hash(idNum+20)*1.2) * 100) / 100, impressions: Math.round(1000 + centrality * 15000 + hash(idNum+21) * 5000), watchlist: false, selected: false, bundle: null }); } } const creatives = [ {x: 3, y: 4, w: 10, h: 5, label: 'BEST DEAL TODAY', color: '#2563eb'}, {x: 18, y: 8, w: 8, h: 8, label: 'COUPON\nDROP', color: '#16a34a'}, {x: 34, y: 3, w: 16, h: 7, label: 'FOUNDERS WALL', color: '#7c3aed'}, {x: 55, y: 11, w: 9, h: 11, label: 'LIVE\nAUCTION', color: '#ea580c'}, {x: 7, y: 28, w: 12, h: 8, label: 'PIXEL\nPUP', color: '#0891b2'}, {x: 26, y: 31, w: 20, h: 9, label: 'BUNDLE BAY', color: '#be185d'}, {x: 51, y: 34, w: 15, h: 7, label: 'FLASH PLAZA', color: '#ca8a04'} ]; creatives.forEach((c, idx) => { for (let yy = c.y; yy < c.y + c.h; yy++) for (let xx = c.x; xx < c.x + c.w; xx++) { const p = getParcel(xx, yy); if (p) { p.status = idx % 2 ? 'auction' : 'owned'; p.brand = c.label.replace('\n',' '); p.owner = idx % 2 ? 'Auction House' : 'Anchor Sponsor'; p.bundle = `LOT-${idx+1}`; p.ask *= 2.2; p.bidPressure = clamp(p.bidPressure + 28, 0, 100); } } }); const activity = [ {icon:'β‘', title:'Coupon Alley lot BDO-11-22 received a new top bid', detail:'12 parcels, 14 minutes left, reserve met.'}, {icon:'π§©', title:'CreatorPool merged 42 adjacent parcels into a billboard bundle', detail:'Bundle score +18% because it touches Prime Fold.'}, {icon:'π', title:'Flash Plaza district challenge unlocked', detail:'First 20 brands to renew this week appear in the animated ticker.'}, {icon:'π', title:'Rare corner parcel listed below heat-map estimate', detail:'Scout bot flagged a possible underpriced placement.'}, {icon:'π°οΈ', title:'Renewal windows opening', detail:'148 parcels can renew at their original slot fee this week.'} ]; function getParcel(x, y) { if (x < 0 || y < 0 || x >= COLS || y >= ROWS) return null; return parcels[y * COLS + x]; } function fitCanvas() { const rect = stage.getBoundingClientRect(); const ratio = window.devicePixelRatio || 1; canvas.width = Math.floor(rect.width * ratio); canvas.height = Math.floor(rect.height * ratio); ctx.setTransform(ratio, 0, 0, ratio, 0, 0); if (!offsetX && !offsetY) { const boardW = COLS * CELL * zoom; const boardH = ROWS * CELL * zoom; offsetX = Math.max(16, (rect.width - boardW) / 2); offsetY = Math.max(16, (rect.height - boardH) / 2); } draw(); } function statusMatches(p) { const sf = statusFilter.value; const q = search.value.trim().toLowerCase(); const statusOk = sf === 'all' || p.status === sf; const queryOk = !q || [p.id, p.brand, p.owner, p.district, p.tag, p.bundle || ''].some(v => String(v).toLowerCase().includes(q)); return statusOk && queryOk; } function draw() { const rect = stage.getBoundingClientRect(); ctx.clearRect(0, 0, rect.width, rect.height); ctx.save(); ctx.translate(offsetX, offsetY); ctx.scale(zoom, zoom); ctx.fillStyle = '#020617'; ctx.fillRect(-1, -1, COLS * CELL + 2, ROWS * CELL + 2); for (const p of parcels) { const x = p.x * CELL, y = p.y * CELL; let fill = palette[p.status]; if (viewMode.value === 'heat') { const t = clamp((p.ask - 8) / 90, 0, 1); fill = `hsl(${210 - t * 170}, 82%, ${24 + t * 34}%)`; } else if (viewMode.value === 'liquidity') { const t = p.bidPressure / 100; fill = `hsl(${260 - t * 160}, 85%, ${24 + t * 34}%)`; } else if (viewMode.value === 'renewal') { const t = 1 - clamp(p.renewalDays / 60, 0, 1); fill = `hsl(${45 - t * 35}, 85%, ${28 + t * 34}%)`; } ctx.globalAlpha = statusMatches(p) ? 1 : .18; ctx.fillStyle = fill; ctx.fillRect(x + 1, y + 1, CELL - 2, CELL - 2); if (p.bundle) { ctx.strokeStyle = 'rgba(255,255,255,.22)'; ctx.strokeRect(x + 1.5, y + 1.5, CELL - 3, CELL - 3); } } ctx.globalAlpha = 1; for (const c of creatives) { const x = c.x * CELL, y = c.y * CELL, w = c.w * CELL, h = c.h * CELL; ctx.save(); ctx.globalAlpha = .9; const grd = ctx.createLinearGradient(x, y, x + w, y + h); grd.addColorStop(0, c.color); grd.addColorStop(1, '#020617'); ctx.fillStyle = grd; ctx.fillRect(x + 2, y + 2, w - 4, h - 4); ctx.strokeStyle = 'rgba(255,255,255,.38)'; ctx.lineWidth = 1; ctx.strokeRect(x + 2.5, y + 2.5, w - 5, h - 5); ctx.fillStyle = '#ffffff'; ctx.font = `bold ${Math.max(9, Math.min(18, h/3))}px ui-sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const lines = c.label.split('\n'); lines.forEach((line, i) => ctx.fillText(line, x + w / 2, y + h / 2 + (i - (lines.length-1)/2) * 18)); ctx.restore(); } if (selectedId) { const p = parcels.find(v => v.id === selectedId); if (p) { ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 3 / zoom; ctx.strokeRect(p.x * CELL + 2, p.y * CELL + 2, CELL - 4, CELL - 4); ctx.strokeStyle = '#22d3ee'; ctx.lineWidth = 1.5 / zoom; ctx.strokeRect(p.x * CELL - 2, p.y * CELL - 2, CELL + 4, CELL + 4); } } ctx.restore(); } function clientToCell(e) { const rect = stage.getBoundingClientRect(); const sx = e.clientX - rect.left; const sy = e.clientY - rect.top; const x = Math.floor((sx - offsetX) / (CELL * zoom)); const y = Math.floor((sy - offsetY) / (CELL * zoom)); return {x, y, sx, sy}; } function updateTooltip(e) { const {x, y} = clientToCell(e); const p = getParcel(x, y); hoverCell = p; if (!p) { tooltip.style.display = 'none'; cursorReadout.textContent = 'x: β y: β'; return; } cursorReadout.textContent = `x: ${x} y: ${y} β’ ${p.id}`; tooltip.innerHTML = `<b>${p.id} Β· ${p.district}</b> <div>${p.brand} <span style="color:#94a3b8">/ ${p.tag}</span></div> <div style="margin-top:5px">Status: <b style="display:inline;color:${palette[p.status]}">${p.status}</b> Β· Ask ${money(p.ask)} Β· Top bid ${money(p.bid)}</div> <div style="color:#94a3b8;margin-top:4px">Renewal window: ${days(p.renewalDays)} Β· CTR demo: ${p.ctr}%</div>`; tooltip.style.display = 'block'; tooltip.style.left = `${e.clientX - stage.getBoundingClientRect().left}px`; tooltip.style.top = `${e.clientY - stage.getBoundingClientRect().top}px`; } function selectParcel(p) { selectedId = p.id; draw(); renderInspect(); } stage.addEventListener('mousemove', e => { updateTooltip(e); if (bundleMode && dragStart) { const cell = clientToCell(e); dragEnd = {x: clamp(cell.x, 0, COLS-1), y: clamp(cell.y, 0, ROWS-1)}; updateSelectionBox(); } }); stage.addEventListener('mouseleave', () => { tooltip.style.display = 'none'; }); stage.addEventListener('mousedown', e => { const cell = clientToCell(e); const p = getParcel(cell.x, cell.y); if (bundleMode) { dragStart = {x: clamp(cell.x, 0, COLS-1), y: clamp(cell.y, 0, ROWS-1)}; dragEnd = {...dragStart}; updateSelectionBox(); } else if (p) { selectParcel(p); } }); stage.addEventListener('mouseup', e => { if (bundleMode && dragStart && dragEnd) { createBundleFromSelection(); dragStart = null; dragEnd = null; selectionBox.style.display = 'none'; } }); stage.addEventListener('wheel', e => { e.preventDefault(); const oldZoom = zoom; zoom = clamp(zoom + (e.deltaY < 0 ? .12 : -.12), .55, 2.4); const rect = stage.getBoundingClientRect(); const mx = e.clientX - rect.left, my = e.clientY - rect.top; offsetX = mx - ((mx - offsetX) / oldZoom) * zoom; offsetY = my - ((my - offsetY) / oldZoom) * zoom; draw(); }, {passive:false}); function updateSelectionBox() { if (!dragStart || !dragEnd) return; const x1 = Math.min(dragStart.x, dragEnd.x); const y1 = Math.min(dragStart.y, dragEnd.y); const x2 = Math.max(dragStart.x, dragEnd.x); const y2 = Math.max(dragStart.y, dragEnd.y); selectionBox.style.display = 'block'; selectionBox.style.left = `${offsetX + x1 * CELL * zoom}px`; selectionBox.style.top = `${offsetY + y1 * CELL * zoom}px`; selectionBox.style.width = `${(x2 - x1 + 1) * CELL * zoom}px`; selectionBox.style.height = `${(y2 - y1 + 1) * CELL * zoom}px`; } function createBundleFromSelection() { const x1 = Math.min(dragStart.x, dragEnd.x); const y1 = Math.min(dragStart.y, dragEnd.y); const x2 = Math.max(dragStart.x, dragEnd.x); const y2 = Math.max(dragStart.y, dragEnd.y); const width = x2 - x1 + 1, height = y2 - y1 + 1; const selected = []; for (let yy = y1; yy <= y2; yy++) for (let xx = x1; xx <= x2; xx++) selected.push(getParcel(xx, yy)); const totalAsk = selected.reduce((a,p)=>a+p.ask,0); const liquid = Math.round(selected.reduce((a,p)=>a+p.bidPressure,0)/selected.length); showBundleModal({x1,y1,x2,y2,width,height,count:selected.length,totalAsk,liquid,selected}); } function renderInspect() { const el = document.getElementById('tab-inspect'); const p = selectedId ? parcels.find(v => v.id === selectedId) : parcels.find(v => v.status === 'auction'); if (!selectedId && p) selectedId = p.id; if (!p) return; const bars = Array.from({length: 18}, (_, i) => `<i class="bar" style="height:${18 + hash(i + p.x*7 + p.y*13) * 54}px"></i>`).join(''); el.innerHTML = ` <div class="panel"> <div style="display:flex;justify-content:space-between;gap:12px;align-items:start"> <div><h2>${p.id}</h2><div class="small">${p.district} Β· ${p.x},${p.y} Β· ${p.tag}</div></div> <span class="pill ${p.status === 'sale' ? 'green' : p.status === 'auction' ? 'yellow' : p.status === 'owned' ? 'blue' : ''}">${p.status.toUpperCase()}</span> </div> <div style="height:82px;border-radius:15px;margin:12px 0;border:1px solid rgba(148,163,184,.22);background:linear-gradient(135deg,${palette[p.status]},#020617);display:grid;place-items:center;text-align:center;font-weight:850;letter-spacing:.04em">${p.brand}</div> <div class="row"><span>Asking price</span><b>${money(p.ask)}</b></div> <div class="row"><span>Top bid</span><b>${money(p.bid)}</b></div> <div class="row"><span>Renewal right</span><b>${money(Math.max(4, p.price * .32))} / term</b></div> <div class="row"><span>License term</span><b>${p.term} days</b></div> <div class="row"><span>Renewal window</span><b>${days(p.renewalDays)}</b></div> <div class="row"><span>Estimated daily views</span><b>${p.impressions.toLocaleString()}</b></div> <div class="btn-grid"> <button class="primary" onclick="window.pxAction('bid')">Place bid</button> <button onclick="window.pxAction('offer')">Make offer</button> <button onclick="window.pxAction('watch')">Watchlist</button> <button onclick="window.pxAction('ad')">Upload creative</button> </div> <p class="small" style="margin-bottom:0">Prototype terms: buyers control the ad slot during the license period and may transfer the license subject to content rules. This is not a real securities, derivatives, or crypto exchange.</p> </div> <div class="panel"> <h3>Market pulse</h3> <div class="mini-chart">${bars}</div> <div class="row"><span>Bid pressure</span><b>${p.bidPressure}%</b></div> <div class="progress"><i style="width:${p.bidPressure}%"></i></div> </div> <div class="panel"> <h3>Creative rules</h3> <div class="small">Every image/link goes through automated checks plus manual review for scams, malware, impersonation, adult content, and illegal offers. Ads are labeled and parcel ownership is public.</div> </div>`; } function getListings() { return parcels .filter(p => p.status === 'sale' || p.status === 'auction') .sort((a,b) => (b.bidPressure * b.ask) - (a.bidPressure * a.ask)) .slice(0, 15); } function renderExchange() { const listings = getListings(); const rows = listings.map(p => `<tr class="clickable" onclick="window.selectById('${p.id}')"><td>${p.id}</td><td>${p.district}</td><td>${p.status}</td><td>${money(p.ask)}</td><td>${money(p.bid)}</td></tr>`).join(''); document.getElementById('tab-exchange').innerHTML = ` <div class="panel"> <h2>Exchange floor</h2> <p class="small">The exchange is a marketplace for ad-slot licenses, not a financial derivatives venue. Users bid on placement rights, bundles, and renewal privileges.</p> <div class="btn-grid"> <button class="primary" onclick="window.openModal('auction')">Create auction</button> <button onclick="window.openModal('order')">Place limit order</button> </div> </div> <div class="panel"> <h3>Live order book</h3> <table><thead><tr><th>Parcel</th><th>District</th><th>Type</th><th>Ask</th><th>Bid</th></tr></thead><tbody>${rows}</tbody></table> </div> <div class="panel"> <h3>District indices</h3> ${districts.map((d,i) => `<div class="row"><span>${d}</span><b>${money(13 + i*3.4 + hash(i+55)*19)}</b></div>`).join('')} </div>`; } function renderPortfolio() { const mine = parcels.filter((p,i) => p.owner === 'DealDAO' || i % 103 === 0).slice(0,10); const value = mine.reduce((a,p)=>a+p.ask,0); document.getElementById('tab-portfolio').innerHTML = ` <div class="panel"> <h2>Your territory</h2> <div class="row"><span>Controlled parcels</span><b>${mine.length}</b></div> <div class="row"><span>Estimated placement value</span><b>${money(value)}</b></div> <div class="row"><span>Bundle bonus</span><b>+12%</b></div> <div class="btn-grid"><button class="primary" onclick="window.openModal('creative')">Launch campaign</button><button onclick="window.openModal('resale')">List territory</button></div> </div> <div class="panel"> <h3>Renewal calendar</h3> ${mine.slice(0,6).map(p=>`<div class="row clickable" onclick="window.selectById('${p.id}')"><span>${p.id}</span><b>${days(p.renewalDays)}</b></div>`).join('')} </div> <div class="panel"> <h3>Achievements</h3> <div class="row"><span>Contiguous Collector</span><span class="pill purple">Level 3</span></div> <div class="row"><span>Early Renewal Streak</span><span class="pill green">9Γ</span></div> <div class="row"><span>District Patron</span><span class="pill blue">Prime Fold</span></div> </div>`; } function renderBundle() { document.getElementById('tab-bundle').innerHTML = ` <div class="panel"> <h2>Bundle builder</h2> <p class="small">Turn adjacent pixels into larger creative territory. Bundles can carry one image, one link, one auction, and one renewal schedule.</p> <button id="sideBundleToggle" class="primary">${bundleMode ? 'Exit bundle mode' : 'Start selecting parcels'}</button> </div> <div class="panel"> <h3>Smart bundle ideas</h3> <div class="row"><span>Prime Fold 6Γ4 block</span><b>${money(1480)}</b></div> <div class="row"><span>Coupon Alley takeover</span><b>${money(2320)}</b></div> <div class="row"><span>Flash Plaza perimeter</span><b>${money(980)}</b></div> </div> <div class="panel"> <h3>Bundle score formula</h3> <div class="small">Score = contiguity + district traffic + creative size + renewal health + bid pressure. Higher scores get map glows, ticker mentions, and featured auction placement.</div> </div>`; document.getElementById('sideBundleToggle').addEventListener('click', toggleBundleMode); } function renderBuzz() { document.getElementById('tab-buzz').innerHTML = ` <div class="panel"> <h2>Game layer</h2> <p class="small">Daily drops, district competitions, bundle quests, and time-lapse history make the homepage entertaining even when users are not buying.</p> </div> <div class="activity">${activity.map(a=>`<div class="event"><div class="avatar">${a.icon}</div><div><b>${a.title}</b><div class="small">${a.detail}</div></div></div>`).join('')}</div> <div class="panel" style="margin-top:12px"> <h3>Featured quests</h3> <div class="row"><span>Build a 10Γ10 contiguous logo</span><span class="pill yellow">2.4Γ visibility</span></div> <div class="row"><span>Sponsor a local-business strip</span><span class="pill green">Badge drop</span></div> <div class="row"><span>Win a no-reserve midnight auction</span><span class="pill purple">Rare frame</span></div> </div>`; } function setTab(tab) { document.querySelectorAll('.tab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab)); document.querySelectorAll('.tabpane').forEach(p => p.style.display = 'none'); document.getElementById(`tab-${tab}`).style.display = 'block'; if (tab === 'exchange') renderExchange(); if (tab === 'portfolio') renderPortfolio(); if (tab === 'bundle') renderBundle(); if (tab === 'buzz') renderBuzz(); } document.querySelectorAll('.tab').forEach(b => b.addEventListener('click', () => setTab(b.dataset.tab))); function toggleBundleMode() { bundleMode = !bundleMode; document.getElementById('bundleMode').textContent = bundleMode ? 'Exit bundle mode' : 'Bundle mode'; document.getElementById('bundleMode').classList.toggle('primary', bundleMode); const s = document.getElementById('sideBundleToggle'); if (s) s.textContent = bundleMode ? 'Exit bundle mode' : 'Start selecting parcels'; showToast(bundleMode ? 'Bundle mode on. Drag over adjacent parcels to package them.' : 'Bundle mode off. Click any parcel to inspect it.'); } document.getElementById('bundleMode').addEventListener('click', toggleBundleMode); document.getElementById('zoomIn').addEventListener('click', () => { zoom = clamp(zoom + .2, .55, 2.4); draw(); }); document.getElementById('zoomOut').addEventListener('click', () => { zoom = clamp(zoom - .2, .55, 2.4); draw(); }); statusFilter.addEventListener('change', draw); viewMode.addEventListener('change', draw); search.addEventListener('input', draw); document.getElementById('randomHot').addEventListener('click', () => { const candidates = parcels.filter(p => (p.status === 'sale' || p.status === 'auction') && p.bidPressure > 72).sort((a,b) => a.ask - b.ask); const p = candidates[Math.floor(Math.random()*Math.min(20,candidates.length))] || candidates[0]; if (p) { selectParcel(p); offsetX = stage.getBoundingClientRect().width / 2 - (p.x * CELL + CELL / 2) * zoom; offsetY = stage.getBoundingClientRect().height / 2 - (p.y * CELL + CELL / 2) * zoom; draw(); showToast(`Scout found ${p.id}: ${p.district}, ${money(p.ask)} ask, ${p.bidPressure}% bid pressure.`); } }); function showToast(msg) { toast.textContent = msg; toast.style.display = 'block'; clearTimeout(showToast.t); showToast.t = setTimeout(()=>toast.style.display='none', 3800); } function closeModal() { modalBackdrop.style.display = 'none'; modal.innerHTML = ''; } modalBackdrop.addEventListener('click', e => { if (e.target === modalBackdrop) closeModal(); }); window.selectById = id => { const p = parcels.find(v => v.id === id); if (p) { selectParcel(p); setTab('inspect'); offsetX = stage.getBoundingClientRect().width / 2 - (p.x * CELL + CELL / 2) * zoom; offsetY = stage.getBoundingClientRect().height / 2 - (p.y * CELL + CELL / 2) * zoom; draw(); } }; window.pxAction = action => { const p = parcels.find(v => v.id === selectedId); if (!p) return; if (action === 'watch') { p.watchlist = !p.watchlist; showToast(`${p.id} ${p.watchlist ? 'added to' : 'removed from'} your watchlist.`); return; } if (action === 'bid') { openFormModal('Place bid', `Bid on ${p.id}`, [ {label:'Bid amount', value:(p.bid + 1.25).toFixed(2)}, {label:'Max renewal fee', value:Math.max(4, p.price*.32).toFixed(2)}, {label:'Bid expires in', value:'24 hours'} ], () => { p.bid = Math.max(p.bid, p.bid + 1.25); p.bidPressure = clamp(p.bidPressure + 4, 0, 100); p.status = 'auction'; showToast(`Demo bid placed on ${p.id}.`); renderInspect(); draw(); }); } else if (action === 'offer') { openFormModal('Make private offer', `Offer owner ${p.owner} a fixed price or a bundle swap.`, [ {label:'Offer amount', value:(p.ask*.92).toFixed(2)}, {label:'Message', value:'I want this for a launch campaign.'} ], () => showToast(`Demo offer sent for ${p.id}.`)); } else if (action === 'ad') { openFormModal('Upload creative', 'Mock ad upload flow', [ {label:'Destination URL', value:'https://bestdealon.com/deal'}, {label:'Ad text', value:'BestDealOn launch offer'}, {label:'Disclosure label', value:'Advertisement'} ], () => showToast('Creative submitted to review queue.')); } }; window.openModal = type => { if (type === 'auction') openFormModal('Create auction', 'Package selected pixels or list a parcel with reserve pricing.', [ {label:'Parcel or bundle ID', value:selectedId || 'LOT-NEW'}, {label:'Reserve price', value:'125.00'}, {label:'Auction length', value:'48 hours'} ], () => showToast('Auction created in demo mode.')); if (type === 'order') openFormModal('Place limit order', 'Bid automatically when parcels match your rules.', [ {label:'District', value:'Prime Fold'}, {label:'Max price per parcel', value:'42.00'}, {label:'Minimum contiguous size', value:'4 parcels'} ], () => showToast('Limit order saved in demo mode.')); if (type === 'creative') openFormModal('Launch campaign', 'Attach one creative to your controlled territory.', [ {label:'Campaign name', value:'BestDealOn homepage takeover'}, {label:'Destination URL', value:'https://bestdealon.com'}, {label:'Alt text', value:'BestDealOn featured discount'} ], () => showToast('Campaign launched in demo mode.')); if (type === 'resale') openFormModal('List territory', 'Create a fixed-price listing or start an auction.', [ {label:'Listing type', value:'Auction'}, {label:'Starting price', value:'750.00'}, {label:'Buy-now price', value:'1150.00'} ], () => showToast('Territory listed in demo mode.')); }; function openFormModal(title, intro, fields, cb) { modal.innerHTML = `<div class="modal-head"><div><h2 style="margin:0 0 4px">${title}</h2><div class="small">${intro}</div></div><button onclick="window.closePxModal()">Γ</button></div> ${fields.map(f=>`<div class="field"><label>${f.label}</label><input value="${f.value}"></div>`).join('')} <div class="panel" style="margin-top:12px"><h3>Contract summary</h3><div class="small">This demo models a transferable advertising license with renewal rights. Settlement, escrow, identity checks, ad review, refunds, and taxes would need production-grade workflows.</div></div> <div class="btn-grid"><button class="primary" id="modalSubmit">Confirm demo action</button><button onclick="window.closePxModal()">Cancel</button></div>`; modalBackdrop.style.display = 'flex'; document.getElementById('modalSubmit').onclick = () => { cb(); closeModal(); }; } function showBundleModal(bundle) { const premium = Math.round(bundle.totalAsk * (1 + Math.min(.3, bundle.count / 250)) * 100) / 100; modal.innerHTML = `<div class="modal-head"><div><h2 style="margin:0 0 4px">Create bundle</h2><div class="small">${bundle.width}Γ${bundle.height} contiguous parcels Β· ${bundle.count} total</div></div><button onclick="window.closePxModal()">Γ</button></div> <div class="row"><span>Area</span><b>${bundle.x1},${bundle.y1} β ${bundle.x2},${bundle.y2}</b></div> <div class="row"><span>Combined ask</span><b>${money(bundle.totalAsk)}</b></div> <div class="row"><span>Suggested bundle list</span><b>${money(premium)}</b></div> <div class="row"><span>Average bid pressure</span><b>${bundle.liquid}%</b></div> <div class="field"><label>Bundle name</label><input value="${bundle.width}Γ${bundle.height} Deal Wall"></div> <div class="field"><label>Creative concept</label><textarea rows="3">One image and one link across this contiguous territory.</textarea></div> <div class="btn-grid"><button class="primary" id="bundleConfirm">Package bundle</button><button onclick="window.closePxModal()">Cancel</button></div>`; modalBackdrop.style.display = 'flex'; document.getElementById('bundleConfirm').onclick = () => { const id = `LOT-${Math.floor(1000 + Math.random()*9000)}`; bundle.selected.forEach(p => { p.bundle = id; if (p.status === 'available') p.status = 'sale'; }); showToast(`${id} created with ${bundle.count} parcels. It can now be auctioned as one placement.`); closeModal(); draw(); renderBundle(); }; } window.closePxModal = closeModal; function updateTicker() { const listings = getListings().slice(0,8); tickerText.textContent = listings.map(p => `${p.id} ${p.status === 'auction' ? 'auction' : 'ask'} ${money(p.status === 'auction' ? p.bid : p.ask)} Β· ${p.district} Β· ${p.bidPressure}% pressure`).join(' β’ '); } function updateStats() { const listed = parcels.filter(p => p.status === 'sale' || p.status === 'auction').length / parcels.length * 100; const floor = Math.min(...parcels.filter(p => p.status === 'sale').map(p => p.ask)); const vol = Math.round(parcels.filter(p => p.status !== 'available').reduce((a,p)=>a+p.bid,0) * .18); const med = parcels.map(p=>p.renewalDays).sort((a,b)=>a-b)[Math.floor(parcels.length/2)]; document.getElementById('statFloor').textContent = money(floor); document.getElementById('statVol').textContent = money(vol); document.getElementById('statListed').textContent = `${listed.toFixed(1)}%`; document.getElementById('statRenew').textContent = days(med); } window.addEventListener('resize', fitCanvas); fitCanvas(); renderInspect(); renderExchange(); renderPortfolio(); renderBundle(); renderBuzz(); updateTicker(); updateStats(); })(); </script> </body> </html>
Save changes
Create folder
writable 0777
Create
Cancel