Loading LunaLauncher...

If this persists, click here

" style="width: 100%; height: 100%; border: none; background: #000; display: block;" frameborder="0" scrolling="no" sandbox="allow-scripts allow-same-origin" allow="" title="${artwork.title} ${artwork.artist}"> ` : artwork.parent_inscription_id ? `
${artwork.title} ${artwork.artist}
` : `
NO PREVIEW AVAILABLE
` }

${artwork.title}

${artwork.artist}

${artwork.price_sats === 0 ? 'FREE' : `${artwork.price_sats.toLocaleString()} SATS`}

${artwork.minted} / ${artwork.max_mints === -1 ? '∞' : artwork.max_mints} MINTED

${artwork.soldOut ? '

VIEW

' : colUpcoming ? '

MINTING SOON

' : '

MINT NOW

' }
`; }).join(''); } } catch (error) { console.error('Error loading collection:', error); app.innerHTML = `

Error Loading Collection

Failed to load collection details.

← Back to Home
`; } } async function showMintPage(slug) { // Clean up viola classes if switching from viola to another mint if (currentArtworkSlug === 'viola-misscode-ordinals-central-park-sketched-pfp' && slug !== 'viola-misscode-ordinals-central-park-sketched-pfp') { document.body.classList.remove('viola-mint-page', 'viola-wallet-connected'); document.body.style.background = ''; const app = document.getElementById('app'); if (app) { app.classList.remove('viola-mint-page', 'viola-wallet-connected'); } } // Clean up NAWS classes if switching from NAWS to another mint if (currentArtworkSlug === 'nonnish-naws' && slug !== 'nonnish-naws') { document.body.classList.remove('naws-mint-page'); document.body.style.background = ''; const app = document.getElementById('app'); if (app) { app.classList.remove('naws-mint-page'); } // Remove NAWS style elements const nawsStyles = document.querySelectorAll('style[data-naws-style]'); nawsStyles.forEach(s => s.remove()); } // Clean up Hollowed Animals classes if switching to another mint if (currentArtworkSlug === 'savage-x-sxulr-hollowed-animals' && slug !== 'savage-x-sxulr-hollowed-animals') { document.body.classList.remove('hollowed-mint-page'); // Remove Hollowed style elements const hollowedStyles = document.querySelectorAll('style[data-hollowed-style]'); hollowedStyles.forEach(s => s.remove()); } if (currentArtworkSlug === EMPRESS_TRASH_LOTUS_MINT_SLUG && slug !== EMPRESS_TRASH_LOTUS_MINT_SLUG) { document.body.classList.remove('empress-trash-mint-gallery'); } // First validate that the artwork exists and is active // Use admin API if user is admin to bypass time restrictions const isAdmin = currentUser && currentUser.role === 'admin'; const apiEndpoint = isAdmin ? `/api/get-artwork-admin?slug=${slug}` : `/api/simple-artwork?slug=${slug}`; try { const response = await fetch(apiEndpoint, { credentials: 'include' // Include session cookie for admin API }); const data = await response.json(); if (!data.success || !data.artwork) { // Try admin API if regular API failed and user is admin if (!isAdmin) { // Check if user is admin and retry with admin API if (currentUser && currentUser.role === 'admin') { try { const adminResponse = await fetch(`/api/get-artwork-admin?slug=${slug}`, { credentials: 'include' }); const adminData = await adminResponse.json(); if (adminData.success && adminData.artwork) { // Show admin preview message const app = document.getElementById('app'); const template = document.getElementById('mint-template'); app.innerHTML = template.innerHTML; // Add admin preview banner const banner = document.createElement('div'); banner.style.cssText = ` background: #ff6600; color: #000000; padding: 10px; text-align: center; font-family: 'Press Start 2P', monospace; font-size: 10px; border-bottom: 2px solid #ffffff; margin-top: var(--ll-nav-h, 64px); margin-bottom: 20px; `; banner.innerHTML = '🔧 ADMIN PREVIEW - This mint is not yet live for public'; const mintNavForBanner = document.getElementById('mint-page-nav'); if (mintNavForBanner) mintNavForBanner.after(banner); else app.insertBefore(banner, app.firstChild); const mintMain = app.querySelector('.mint-redesign.main-layout'); if (mintMain) mintMain.style.paddingTop = '0'; // Initialize mint page functionality after DOM is updated setTimeout(() => { initializeMintPage(); loadArtworkForMinting(slug, adminData); }, 100); return; } } catch (adminError) { console.error('Admin API also failed:', adminError); } } } // Artwork not found or inactive const app = document.getElementById('app'); app.innerHTML = `

Artwork Not Available

The artwork you're looking for is either not found, inactive, or not yet approved for minting.

Slug: ${slug}

← Back to Live Mints
`; return; } // Artwork is valid and active, proceed to mint page currentPage = 'mint'; currentArtworkSlug = slug; const template = document.getElementById('mint-template'); const app = document.getElementById('app'); // Update physical print UI for environment-specific pricing setTimeout(() => updatePhysicalPrintUI(), 100); app.innerHTML = template.innerHTML; // Custom backgrounds for specific artworks const customBackgrounds = { 'mervin-monroe-simpsons-flash': './Simpsons.png' }; // Check for viola first (before generic customBackgrounds check) if (slug === 'viola-misscode-ordinals-central-park-sketched-pfp') { // Viola-specific styling - add class to body and app for scoping document.body.classList.add('viola-mint-page'); const app = document.getElementById('app'); if (app) app.classList.add('viola-mint-page'); setTimeout(() => { const style = document.createElement('style'); style.textContent = ` /* Viola mint page - scoped to body.viola-mint-page only */ body.viola-mint-page { background: #000000 url('https://pub-c4a124b338574b5dae1d2b555bfa25a6.r2.dev/Viola/violabg.png') top left/cover no-repeat fixed !important; } /* Hide header on viola mint page only */ body.viola-mint-page .header { display: none !important; } /* Hide network fee section */ #networkSection { display: none !important; } #networkSeparator { display: none !important; } /* Hide artwork container but keep the rest of left column */ #artwork-container { display: none !important; } /* Hide mint counter */ #mintCounter { display: none !important; } /* Mint redesign: left panel (replaces old .left-column > .container) */ .left-column .mint-art-panel { background-color: transparent !important; border: none !important; padding: 0 !important; min-height: 0 !important; height: auto !important; margin-top: 25vh !important; } /* Right column: first visible block after optional deth-reaper */ .right-column .mint-phase-bar { margin-top: 25vh !important; } /* Mobile-specific margins */ @media (max-width: 768px) { .left-column .mint-art-panel { margin-top: 25vh !important; } .right-column .mint-phase-bar { margin-top: 5px !important; } /* Mint button container and other sub-containers need small margins */ #feeSelectionStep, #feeSelectionStep .container, #walletInfo, .wallet-info, #quantity-selector, #physicalPrintStep, .right-column .container:not(:first-of-type) { margin: 5px !important; } } /* Title / artist block (mint redesign — was left-column artist panel) */ .mint-meta-header { margin-top: 0 !important; min-height: 0 !important; background: url('https://pub-c4a124b338574b5dae1d2b555bfa25a6.r2.dev/Viola/violabox.webp') center top/100% 100% no-repeat !important; background-color: transparent !important; border: none !important; padding: 15px !important; position: relative !important; box-sizing: border-box !important; } .mint-meta-header::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border: 20px solid transparent; border-image: url('https://pub-c4a124b338574b5dae1d2b555bfa25a6.r2.dev/Viola/violabox.png') 20 fill; border-image-slice: 20; pointer-events: none; z-index: 0; } .mint-meta-header > * { position: relative; z-index: 1; } /* Make right column container transparent with violabox border */ .right-column .container { background: url('https://pub-c4a124b338574b5dae1d2b555bfa25a6.r2.dev/Viola/violabox.webp') center/100% 100% no-repeat !important; background-color: transparent !important; border: none !important; position: relative !important; box-sizing: border-box !important; } .right-column .container::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border: 20px solid transparent; border-image: url('https://pub-c4a124b338574b5dae1d2b555bfa25a6.r2.dev/Viola/violabox.png') 20 fill; border-image-slice: 20; pointer-events: none; z-index: 0; } .right-column .container > * { position: relative; z-index: 1; } /* Make inner boxes with inline backgrounds transparent */ div[style*="background: #000"], div[style*="background:#000"], div[style*="background: #111"], div[style*="background:#111"] { background: transparent !important; } /* Make fee boxes transparent */ .fee-box { background: transparent !important; } /* Change all text to black for viola mint */ .left-column, .left-column *, .right-column, .right-column *, .viola-gallery-container, .viola-gallery-container * { color: #000000 !important; } /* Specific text elements */ .left-column p, .left-column span, .left-column div, .left-column strong, .right-column p, .right-column span, .right-column div, .right-column strong, .right-column label { color: #000000 !important; } /* Connect button should be white with padding */ /* Connect button with margin instead of padding */ #connectBtn { background: #ffffff !important; color: #000000 !important; border: 2px solid #000000 !important; margin-top: 25px !important; margin-bottom: 25px !important; } #connectBtn:hover:not(:disabled) { background: #f0f0f0 !important; color: #000000 !important; border: 2px solid #000000 !important; } /* All buttons in right column should be white (except connect which is handled separately) */ .right-column button:not(#connectBtn) { background: #ffffff !important; color: #000000 !important; border: 2px solid #000000 !important; } .right-column button:not(#connectBtn):hover:not(:disabled) { background: #f0f0f0 !important; color: #000000 !important; border: 2px solid #000000 !important; } /* Hide custom fee selector for viola mint */ #customFeeInput, label:has(input[value="custom"]), input[value="custom"], label:has(input[name="feeSpeed"][value="custom"]) { display: none !important; } /* Make selected fee highlight more transparent */ .fee-option input[type="radio"]:checked + .fee-box { background: rgba(255, 255, 255, 0.5) !important; border-color: rgba(255, 255, 255, 0.5) !important; } /* Remove white border around physical print option before connecting */ #physicalPrintStep div[style*="border: 2px solid #ffffff"], #physicalPrintStep div[style*="border:2px solid #ffffff"] { border: none !important; } #physicalPrintStep .container { border: none !important; } /* Also remove border from inner div with inline style */ #physicalPrintStep > div > div[style*="border"] { border: none !important; } #connectBtn:hover:not(:disabled) { background: #f0f0f0 !important; color: #000000 !important; border: 2px solid #000000 !important; } /* Fee selector region background */ body.viola-mint-page #feeSelectionStep, body.viola-mint-page #feeSelectionStep .container { background: url('https://pub-c4a124b338574b5dae1d2b555bfa25a6.r2.dev/Viola/violabox.webp') center top/100% 100% no-repeat !important; background-color: transparent !important; position: relative !important; } /* When wallet is connected, make sub-regions transparent but KEEP borders */ .viola-wallet-connected #walletInfo, .viola-wallet-connected #feeSelectionStep, .viola-wallet-connected #feeSelectionStep .container, .viola-wallet-connected #feeSelectionStep * .container, .viola-wallet-connected .wallet-info, .viola-wallet-connected .fee-box, .viola-wallet-connected div[style*="background: #000"], .viola-wallet-connected div[style*="background:#000"], .viola-wallet-connected div[style*="background: #111"], .viola-wallet-connected div[style*="background:#111"], .viola-wallet-connected #quantity-selector { background: transparent !important; border: none !important; } /* Override transparent background for fee selector to show violabox background */ .viola-wallet-connected #feeSelectionStep, .viola-wallet-connected #feeSelectionStep .container { background: url('https://pub-c4a124b338574b5dae1d2b555bfa25a6.r2.dev/Viola/violabox.webp') center top/100% 100% no-repeat !important; background-color: transparent !important; } /* Keep borders visible - don't hide them when wallet connected */ /* Borders stay visible via ::before pseudo-elements */ /* Other buttons outside right column keep black background */ .left-column button, .viola-gallery-container button { background: #000000 !important; color: #ffffff !important; border: 2px solid #000000 !important; } .left-column button:hover:not(:disabled), .viola-gallery-container button:hover:not(:disabled) { background: #ffffff !important; color: #000000 !important; border: 2px solid #000000 !important; } /* Make input fields have black text */ input, textarea, select { color: #000000 !important; background: #ffffff !important; } /* Links should be black */ a { color: #000000 !important; } /* Gallery item borders should be black */ .viola-gallery-item { border-color: #000000 !important; } /* Ensure all text in containers is black */ #artist-name, #artwork-title, #artwork-description, #artwork-price, .wallet-info, .wallet-info * { color: #000000 !important; } /* Hide art frame area (replaces old left-column flex art wrapper) */ .mint-art-frame { display: none !important; } /* Ensure main-layout wraps for gallery */ .main-layout { flex-wrap: wrap !important; } /* Gallery styles for Viola collection */ .viola-gallery-container { flex: 0 0 100% !important; width: calc(100% - 40px) !important; margin: 30px 20px !important; padding: 20px; background: url('https://pub-c4a124b338574b5dae1d2b555bfa25a6.r2.dev/Viola/violabox.webp') center top/100% 100% no-repeat !important; background-color: transparent !important; border: none !important; position: relative; box-sizing: border-box !important; order: 999; } .viola-gallery-container::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border: 20px solid transparent; border-image: url('https://pub-c4a124b338574b5dae1d2b555bfa25a6.r2.dev/Viola/violabox.png') 20 fill; border-image-slice: 20; pointer-events: none; z-index: 0; } .viola-gallery-container > * { position: relative; z-index: 1; } .viola-gallery-title { color: #000000; font-size: 11px; text-align: center; margin-bottom: 15px; font-family: 'Press Start 2P', monospace; } .viola-gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 10px; max-height: 400px; overflow-y: auto; } /* Fix mobile gallery overlap */ @media (max-width: 768px) { .viola-gallery-grid { grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)) !important; gap: 8px !important; } .viola-gallery-item { min-width: 60px !important; min-height: 60px !important; } } .viola-gallery-item { position: relative; aspect-ratio: 1; border: 2px solid #ffffff; overflow: hidden; cursor: pointer; } .viola-gallery-item img { width: 100%; height: 100%; object-fit: cover; } .viola-gallery-item.sold { opacity: 0.5; border-color: #ff0000; } .viola-gallery-item.sold::after { content: 'SOLD'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ff0000; color: #ffffff; padding: 2px 5px; font-size: 8px; font-family: 'Press Start 2P', monospace; } .viola-gallery-item.available { border-color: #00ff00; } .viola-gallery-stats { text-align: center; margin-bottom: 10px; color: #000000; font-size: 11px; } `; document.head.appendChild(style); // Add gallery container below both columns (full width) - only if not already added const mainLayout = app.querySelector('.main-layout'); if (mainLayout && !document.getElementById('viola-gallery-grid')) { const galleryContainer = document.createElement('div'); galleryContainer.className = 'viola-gallery-container'; galleryContainer.style.marginLeft = '20px'; galleryContainer.style.marginRight = '20px'; galleryContainer.style.marginBottom = '30px'; galleryContainer.innerHTML = ` `; mainLayout.appendChild(galleryContainer); // Fetch and display gallery loadViolaGallery(); } // Store flag to hide network section window.hideNetworkFeeForArtwork = true; // Watch for wallet connection and apply transparent styles function checkWalletConnection() { const walletInfo = document.getElementById('walletInfo'); const app = document.getElementById('app'); if (walletInfo && walletInfo.style.display !== 'none' && walletInfo.style.display !== '') { if (app) app.classList.add('viola-wallet-connected'); document.body.classList.add('viola-wallet-connected'); } else { if (app) app.classList.remove('viola-wallet-connected'); document.body.classList.remove('viola-wallet-connected'); } } // Check initially and set up observer setTimeout(() => { checkWalletConnection(); const observer = new MutationObserver(checkWalletConnection); const walletInfo = document.getElementById('walletInfo'); if (walletInfo) { observer.observe(walletInfo, { attributes: true, attributeFilter: ['style'] }); } // Also check feeSelectionStep const feeStep = document.getElementById('feeSelectionStep'); if (feeStep) { observer.observe(feeStep, { attributes: true, attributeFilter: ['style'] }); } }, 500); // Clean up when leaving the page window.addEventListener('beforeunload', () => { document.body.classList.remove('viola-mint-page', 'viola-wallet-connected'); const app = document.getElementById('app'); if (app) { app.classList.remove('viola-mint-page', 'viola-wallet-connected'); } }); }, 150); // Function to load Viola gallery - prevent duplicate calls async function loadViolaGallery() { // Prevent duplicate calls if (window.violaGalleryLoading) { return; } window.violaGalleryLoading = true; try { const response = await fetch('/api/get-pre-inscribed-gallery?slug=viola-misscode-ordinals-central-park-sketched-pfp'); const data = await response.json(); const grid = document.getElementById('viola-gallery-grid'); const stats = document.getElementById('viola-gallery-stats'); if (!grid || !stats) { window.violaGalleryLoading = false; return; } if (!data.success) { grid.innerHTML = '
Failed to load gallery
'; window.violaGalleryLoading = false; return; } stats.textContent = `${data.stats.sold} SOLD / ${data.stats.available} AVAILABLE / ${data.stats.total} TOTAL`; grid.innerHTML = data.inscriptions.map(insc => ` `).join(''); } catch (error) { console.error('Gallery load error:', error); const grid = document.getElementById('viola-gallery-grid'); if (grid) grid.innerHTML = '
Error loading gallery
'; } finally { window.violaGalleryLoading = false; } } } else if (slug === 'mervin-monroe-simpsons-flash') { // Simpsons-specific styling app.style.backgroundImage = `url('${customBackgrounds[slug]}')`; app.style.backgroundSize = 'cover'; app.style.backgroundPosition = 'center'; app.style.backgroundRepeat = 'no-repeat'; app.style.backgroundAttachment = 'fixed'; // Hide collection image and make boxes transparent setTimeout(() => { // Use CSS to hide artwork container and style boxes // This is more reliable than JS selectors const style = document.createElement('style'); style.textContent = ` /* Hide network fee section */ #networkSection { display: none !important; } #networkSeparator { display: none !important; } /* Hide artwork container but keep the rest of left column */ #artwork-container { display: none !important; } /* Hide mint counter */ #mintCounter { display: none !important; } .left-column .mint-art-panel { background: transparent !important; border: none !important; padding: 0 !important; min-height: 0 !important; height: auto !important; } /* Make right column container semi-transparent */ .right-column .container { background: rgba(0, 0, 0, 0.7) !important; } /* Make inner boxes with inline backgrounds semi-transparent */ div[style*="background: #000"], div[style*="background:#000"], div[style*="background: #111"], div[style*="background:#111"] { background: rgba(0, 0, 0, 0.7) !important; } /* Make fee boxes semi-transparent */ .fee-box { background: rgba(0, 0, 0, 0.7) !important; } .mint-art-frame { display: none !important; } /* Ensure main-layout wraps for gallery */ .main-layout { flex-wrap: wrap !important; } /* Gallery styles for Simpsons collection */ .simpsons-gallery-container { flex: 0 0 100% !important; width: calc(100% - 40px) !important; margin: 30px 20px !important; padding: 20px; background: rgba(0, 0, 0, 0.7); border: 2px solid #ffffff; box-sizing: border-box; order: 999; } .simpsons-gallery-title { color: #ffffff; font-size: 11px; text-align: center; margin-bottom: 15px; font-family: 'Press Start 2P', monospace; } .simpsons-gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 10px; max-height: 400px; overflow-y: auto; } .simpsons-gallery-item { position: relative; aspect-ratio: 1; border: 2px solid #ffffff; overflow: hidden; cursor: pointer; } .simpsons-gallery-item img { width: 100%; height: 100%; object-fit: cover; } .simpsons-gallery-item.sold { opacity: 0.5; border-color: #ff0000; } .simpsons-gallery-item.sold::after { content: 'SOLD'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ff0000; color: #ffffff; padding: 2px 5px; font-size: 8px; font-family: 'Press Start 2P', monospace; } .simpsons-gallery-item.available { border-color: #00ff00; } .simpsons-gallery-stats { text-align: center; margin-bottom: 10px; color: #cccccc; font-size: 11px; } `; document.head.appendChild(style); // Add gallery container below both columns (full width) const mainLayout = app.querySelector('.main-layout'); if (mainLayout) { const galleryContainer = document.createElement('div'); galleryContainer.className = 'simpsons-gallery-container'; galleryContainer.style.marginLeft = '20px'; galleryContainer.style.marginRight = '20px'; galleryContainer.style.marginBottom = '30px'; galleryContainer.innerHTML = ` `; mainLayout.appendChild(galleryContainer); // Fetch and display gallery loadSimpsonsGallery(); } // Store flag to hide network section window.hideNetworkFeeForArtwork = true; }, 150); // Function to load Simpsons gallery async function loadSimpsonsGallery() { try { const response = await fetch('/api/get-pre-inscribed-gallery?slug=mervin-monroe-simpsons-flash'); const data = await response.json(); const grid = document.getElementById('simpsons-gallery-grid'); const stats = document.getElementById('simpsons-gallery-stats'); if (!data.success) { grid.innerHTML = '
Failed to load gallery
'; return; } stats.textContent = `${data.stats.sold} SOLD / ${data.stats.available} AVAILABLE / ${data.stats.total} TOTAL`; grid.innerHTML = data.inscriptions.map(insc => ` `).join(''); } catch (error) { console.error('Gallery load error:', error); const grid = document.getElementById('simpsons-gallery-grid'); if (grid) grid.innerHTML = '
Error loading gallery
'; } } } else if (slug === 'theartist-socialage') { // SOCIALAGE collection - add gallery at the bottom setTimeout(() => { // Check if gallery already exists to prevent duplicates if (document.querySelector('.socialage-gallery-container')) { return; } const style = document.createElement('style'); style.textContent = ` /* Ensure main-layout wraps for gallery */ .main-layout { flex-wrap: wrap !important; } /* Gallery styles for SOCIALAGE collection */ .socialage-gallery-container { flex: 0 0 100% !important; width: calc(100% - 40px) !important; margin: 30px 20px !important; padding: 20px; background: rgba(0, 0, 0, 0.7); border: 2px solid #ffffff; box-sizing: border-box; order: 999; } .socialage-gallery-title { color: #ffffff; font-size: 11px; text-align: center; margin-bottom: 15px; font-family: 'Press Start 2P', monospace; } .socialage-gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 10px; max-height: 400px; overflow-y: auto; } .socialage-gallery-item { position: relative; aspect-ratio: 1; border: 2px solid #ffffff; overflow: hidden; cursor: pointer; } .socialage-gallery-item img { width: 100%; height: 100%; object-fit: cover; } .socialage-gallery-item.sold { opacity: 0.5; border-color: #ff0000; } .socialage-gallery-item.sold::after { content: 'SOLD'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ff0000; color: #ffffff; padding: 2px 5px; font-size: 8px; font-family: 'Press Start 2P', monospace; } .socialage-gallery-item.available { border-color: #00ff00; } .socialage-gallery-stats { text-align: center; margin-bottom: 10px; color: #cccccc; font-size: 11px; } `; document.head.appendChild(style); // Add gallery container below both columns (full width) const mainLayout = app.querySelector('.main-layout'); if (mainLayout) { const galleryContainer = document.createElement('div'); galleryContainer.className = 'socialage-gallery-container'; galleryContainer.style.marginLeft = '20px'; galleryContainer.style.marginRight = '20px'; galleryContainer.style.marginBottom = '30px'; galleryContainer.innerHTML = ` `; mainLayout.appendChild(galleryContainer); // Fetch and display gallery loadSocialageGallery(); } }, 150); // Function to load SOCIALAGE gallery - prevent duplicate calls async function loadSocialageGallery() { // Prevent duplicate calls if (window.socialageGalleryLoading) { return; } window.socialageGalleryLoading = true; try { const response = await fetch('/api/get-pre-inscribed-gallery?slug=theartist-socialage'); const data = await response.json(); const grid = document.getElementById('socialage-gallery-grid'); const stats = document.getElementById('socialage-gallery-stats'); if (!grid || !stats) { window.socialageGalleryLoading = false; return; } if (!data.success) { grid.innerHTML = '
Failed to load gallery
'; window.socialageGalleryLoading = false; return; } stats.textContent = `${data.stats.sold} SOLD / ${data.stats.available} AVAILABLE / ${data.stats.total} TOTAL`; if (data.inscriptions.length === 0) { grid.innerHTML = '
No inscriptions yet
'; return; } grid.innerHTML = data.inscriptions.map(insc => ` `).join(''); } catch (error) { console.error('SOCIALAGE gallery load error:', error); const grid = document.getElementById('socialage-gallery-grid'); if (grid) grid.innerHTML = '
Error loading gallery
'; } finally { window.socialageGalleryLoading = false; } } } else if (slug === 'viola-misscode-inside-us-gran-drama-dentro') { // Viola Inside US collection - add same gallery at the bottom as socialage setTimeout(() => { if (document.querySelector('.viola-inside-us-gallery-container')) { return; } const style = document.createElement('style'); style.textContent = ` .main-layout { flex-wrap: wrap !important; } .viola-inside-us-gallery-container { flex: 0 0 100% !important; width: calc(100% - 40px) !important; margin: 30px 20px !important; padding: 20px; background: rgba(0, 0, 0, 0.7); border: 2px solid #ffffff; box-sizing: border-box; order: 999; } .viola-inside-us-gallery-title { color: #ffffff; font-size: 11px; text-align: center; margin-bottom: 15px; font-family: 'Press Start 2P', monospace; } .viola-inside-us-gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 10px; max-height: 400px; overflow-y: auto; } .viola-inside-us-gallery-item { position: relative; aspect-ratio: 1; border: 2px solid #ffffff; overflow: hidden; cursor: pointer; } .viola-inside-us-gallery-item iframe { width: 100%; height: 100%; border: none; display: block; pointer-events: none; } .viola-inside-us-gallery-item.sold { opacity: 0.5; border-color: #ff0000; } .viola-inside-us-gallery-item.sold::after { content: 'SOLD'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ff0000; color: #ffffff; padding: 2px 5px; font-size: 8px; font-family: 'Press Start 2P', monospace; } .viola-inside-us-gallery-item.available { border-color: #00ff00; } .viola-inside-us-gallery-stats { text-align: center; margin-bottom: 10px; color: #cccccc; font-size: 11px; } `; document.head.appendChild(style); const mainLayout = app.querySelector('.main-layout'); if (mainLayout) { const galleryContainer = document.createElement('div'); galleryContainer.className = 'viola-inside-us-gallery-container'; galleryContainer.style.marginLeft = '20px'; galleryContainer.style.marginRight = '20px'; galleryContainer.style.marginBottom = '30px'; galleryContainer.innerHTML = ` `; mainLayout.appendChild(galleryContainer); loadViolaInsideUsGallery(); } }, 150); const VIOLA_GALLERY_PAGE_SIZE = 50; window.renderViolaInsideUsGalleryPage = function(page) { const grid = document.getElementById('viola-inside-us-gallery-grid'); const paginationEl = document.getElementById('viola-inside-us-gallery-pagination'); const list = window.violaInsideUsGalleryInscriptions || []; if (!grid || !list.length) return; const totalPages = Math.max(1, Math.ceil(list.length / VIOLA_GALLERY_PAGE_SIZE)); const p = Math.max(1, Math.min(page, totalPages)); window.violaInsideUsGalleryCurrentPage = p; const start = (p - 1) * VIOLA_GALLERY_PAGE_SIZE; const pageItems = list.slice(start, start + VIOLA_GALLERY_PAGE_SIZE); grid.innerHTML = pageItems.map(insc => ` `).join(''); if (paginationEl) { paginationEl.style.display = 'flex'; paginationEl.innerHTML = ` Page ${p} of ${totalPages} `; } } async function loadViolaInsideUsGallery() { if (window.violaInsideUsGalleryLoading) { return; } window.violaInsideUsGalleryLoading = true; try { const response = await fetch('/api/get-pre-inscribed-gallery?slug=viola-misscode-inside-us-gran-drama-dentro'); const data = await response.json(); const grid = document.getElementById('viola-inside-us-gallery-grid'); const stats = document.getElementById('viola-inside-us-gallery-stats'); if (!grid || !stats) { window.violaInsideUsGalleryLoading = false; return; } if (!data.success) { grid.innerHTML = '
Failed to load gallery
'; window.violaInsideUsGalleryLoading = false; return; } stats.textContent = `${data.stats.sold} SOLD / ${data.stats.available} AVAILABLE / ${data.stats.total} TOTAL`; if (data.inscriptions.length === 0) { grid.innerHTML = '
No inscriptions yet
'; window.violaInsideUsGalleryLoading = false; return; } window.violaInsideUsGalleryInscriptions = data.inscriptions; renderViolaInsideUsGalleryPage(1); } catch (error) { console.error('Viola Inside US gallery load error:', error); const grid = document.getElementById('viola-inside-us-gallery-grid'); if (grid) grid.innerHTML = '
Error loading gallery
'; } finally { window.violaInsideUsGalleryLoading = false; } } } else if (slug === 'dead-ordinals-x-bitscapes-bitcoin-is-dead') { // Dead Ordinals collection - add paginated gallery at the bottom setTimeout(() => { const style = document.createElement('style'); style.textContent = ` /* Ensure main-layout wraps for gallery */ .main-layout { flex-wrap: wrap !important; } /* Gallery styles for Dead Ordinals collection */ .dead-gallery-container { flex: 0 0 100% !important; width: calc(100% - 40px) !important; margin: 30px 20px !important; padding: 20px; background: rgba(0, 0, 0, 0.9); border: 2px solid #ffffff; box-sizing: border-box; order: 999; } .dead-gallery-title { color: #ffffff; font-size: 11px; text-align: center; margin-bottom: 15px; font-family: 'Press Start 2P', monospace; } .dead-gallery-stats { text-align: center; margin-bottom: 15px; color: #cccccc; font-size: 11px; } .dead-gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 12px; margin-bottom: 20px; } .dead-gallery-item { position: relative; aspect-ratio: 1; border: 2px solid #ffffff; overflow: hidden; cursor: pointer; transition: transform 0.2s, border-color 0.2s; } .dead-gallery-item:hover { transform: scale(1.05); } .dead-gallery-item img { width: 100%; height: 100%; object-fit: cover; } .dead-gallery-item.sold { opacity: 0.6; border-color: #ff0000; } .dead-gallery-item.sold::after { content: 'MINTED'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255, 0, 0, 0.9); color: #ffffff; padding: 3px 6px; font-size: 8px; font-family: 'Press Start 2P', monospace; } .dead-gallery-item.available { border-color: #00ff00; } .dead-gallery-item .item-number { position: absolute; bottom: 3px; left: 3px; } /* Modal styles for Dead Ordinals gallery */ .dead-gallery-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.95); z-index: 10000; justify-content: center; align-items: center; flex-direction: column; } .dead-gallery-modal.active { display: flex; } .dead-gallery-modal-close { position: absolute; top: 20px; right: 30px; color: #fff; font-size: 40px; cursor: pointer; z-index: 10001; font-family: Arial, sans-serif; } .dead-gallery-modal-close:hover { color: #ff0000; } .dead-gallery-modal-content { max-width: 90%; max-height: 80%; object-fit: contain; border: 3px solid #fff; } .dead-gallery-modal-info { margin-top: 15px; text-align: center; color: #fff; font-family: 'Press Start 2P', monospace; } .dead-gallery-modal-info .asset-number { font-size: 14px; margin-bottom: 10px; } .dead-gallery-modal-info .asset-status { font-size: 10px; padding: 5px 10px; display: inline-block; } .dead-gallery-modal-info .asset-status.minted { background: #ff0000; } .dead-gallery-modal-info .asset-status.available { background: #00aa00; background: rgba(0, 0, 0, 0.8); color: #fff; padding: 2px 4px; font-size: 7px; font-family: 'Press Start 2P', monospace; } .dead-gallery-pagination { display: flex; justify-content: center; align-items: center; gap: 15px; margin-top: 20px; } .dead-gallery-pagination button { background: #333; color: #fff; border: 2px solid #fff; padding: 8px 15px; font-size: 10px; font-family: 'Press Start 2P', monospace; cursor: pointer; } .dead-gallery-pagination button:hover:not(:disabled) { background: #555; } .dead-gallery-pagination button:disabled { opacity: 0.5; cursor: not-allowed; } .dead-gallery-pagination .page-info { color: #ccc; font-size: 10px; font-family: 'Press Start 2P', monospace; } `; document.head.appendChild(style); // Add gallery container below both columns (full width) - only if not already added const mainLayout = app.querySelector('.main-layout'); if (mainLayout && !document.getElementById('dead-gallery-grid')) { const galleryContainer = document.createElement('div'); galleryContainer.className = 'dead-gallery-container'; galleryContainer.innerHTML = ` `; mainLayout.appendChild(galleryContainer); // Add modal for full-size image viewing if (!document.getElementById('dead-gallery-modal')) { const modal = document.createElement('div'); modal.id = 'dead-gallery-modal'; modal.className = 'dead-gallery-modal'; modal.innerHTML = ` × Enlarged artwork `; modal.addEventListener('click', function(e) { if (e.target === modal) closeDeadGalleryModal(); }); document.body.appendChild(modal); } // Fetch and display gallery loadDeadGalleryPage(1); } }, 150); // Gallery pagination state window.deadGalleryCurrentPage = 1; const ITEMS_PER_PAGE = 50; // Modal functions for Dead Ordinals gallery window.openDeadGalleryModal = function(imgSrc, assetIndex, isMinted, inscriptionId) { const modal = document.getElementById('dead-gallery-modal'); const modalImg = document.getElementById('dead-gallery-modal-img'); const modalNumber = document.getElementById('dead-gallery-modal-number'); const modalStatus = document.getElementById('dead-gallery-modal-status'); const modalLink = document.getElementById('dead-gallery-modal-link'); if (modal && modalImg) { modalImg.src = imgSrc; modalNumber.textContent = '#' + assetIndex; modalStatus.textContent = isMinted ? 'MINTED' : 'AVAILABLE'; modalStatus.className = 'asset-status ' + (isMinted ? 'minted' : 'available'); if (inscriptionId) { modalLink.innerHTML = 'View on Ordinals.com'; } else { modalLink.innerHTML = ''; } modal.classList.add('active'); document.body.style.overflow = 'hidden'; } }; window.closeDeadGalleryModal = function() { const modal = document.getElementById('dead-gallery-modal'); if (modal) { modal.classList.remove('active'); document.body.style.overflow = ''; } }; // Close modal on escape key document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeDeadGalleryModal(); } }); // Function to load Dead Ordinals gallery with pagination (inscribe-on-demand) window.loadDeadGalleryPage = async function(page) { const grid = document.getElementById('dead-gallery-grid'); const stats = document.getElementById('dead-gallery-stats'); const pagination = document.getElementById('dead-gallery-pagination'); const pageInfo = document.getElementById('dead-page-info'); const prevBtn = document.getElementById('dead-prev-btn'); const nextBtn = document.getElementById('dead-next-btn'); if (!grid) return; try { grid.innerHTML = '
Loading assets...
'; // Fetch page from inscribe-on-demand gallery API const response = await fetch('/api/get-collection-gallery?slug=dead-ordinals-x-bitscapes-bitcoin-is-dead&page=' + page + '&limit=' + ITEMS_PER_PAGE); const data = await response.json(); if (!data.success) { grid.innerHTML = '
Failed to load gallery: ' + (data.error || 'Unknown error') + '
'; return; } window.deadGalleryCurrentPage = page; // Update stats stats.textContent = `${data.stats.minted} MINTED / ${data.stats.available} AVAILABLE / ${data.stats.total} TOTAL`; // Log debug info to console if (data.debug) { console.log('Gallery debug:', data.debug); } if (data.assets.length === 0) { let debugMsg = 'No assets found in this collection'; if (data.debug) { debugMsg += ` (artwork_id: ${data.debug.artworkId}, db count: ${data.debug.dbAssetCount})`; } grid.innerHTML = '
' + debugMsg + '
'; pagination.style.display = 'none'; return; } // Render grid with base64 images from R2 grid.innerHTML = data.assets.map(asset => { const hasImage = asset.imageDataBase64 && !asset.r2Error; const imgSrc = hasImage ? 'data:' + (asset.contentType || 'image/png') + ';base64,' + asset.imageDataBase64 : 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMzMzIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmb250LXNpemU9IjEwIj5ObyBJbWFnZTwvdGV4dD48L3N2Zz4='; const inscId = asset.inscriptionId || ''; return ''; }).join(''); // Update pagination const totalPages = data.pagination.totalPages; if (totalPages > 1) { pagination.style.display = 'flex'; pageInfo.textContent = `Page ${page} of ${totalPages}`; prevBtn.disabled = !data.pagination.hasPrev; nextBtn.disabled = !data.pagination.hasNext; } else { pagination.style.display = 'none'; } } catch (error) { console.error('Dead gallery load error:', error); grid.innerHTML = '
Error loading gallery
'; } }; } else if (slug === 'reece-swanepoel-5ha5ow5') { // Reece Swanepoel collection - same bottom gallery + expand-on-click as dead-ordinals setTimeout(() => { const style = document.createElement('style'); style.setAttribute('data-reece-gallery-style', 'true'); style.textContent = ` .main-layout { flex-wrap: wrap !important; } .reece-gallery-container { flex: 0 0 100% !important; width: calc(100% - 40px) !important; margin: 30px 20px !important; padding: 20px; background: rgba(0, 0, 0, 0.9); border: 2px solid #ffffff; box-sizing: border-box; order: 999; } .reece-gallery-title { color: #ffffff; font-size: 11px; text-align: center; margin-bottom: 15px; font-family: 'Press Start 2P', monospace; } .reece-gallery-stats { text-align: center; margin-bottom: 15px; color: #cccccc; font-size: 11px; } .reece-gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 12px; margin-bottom: 20px; } .reece-gallery-item { position: relative; aspect-ratio: 1; border: 2px solid #ffffff; overflow: hidden; cursor: pointer; transition: transform 0.2s, border-color 0.2s; } .reece-gallery-item:hover { transform: scale(1.05); } .reece-gallery-item img { width: 100%; height: 100%; object-fit: cover; } .reece-gallery-item.sold { opacity: 0.6; border-color: #ff0000; } .reece-gallery-item.sold::after { content: 'MINTED'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255, 0, 0, 0.9); color: #ffffff; padding: 3px 6px; font-size: 8px; font-family: 'Press Start 2P', monospace; } .reece-gallery-item.available { border-color: #00ff00; } .reece-gallery-item .item-number { position: absolute; bottom: 3px; left: 3px; } .reece-gallery-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.95); z-index: 10000; justify-content: center; align-items: center; flex-direction: column; } .reece-gallery-modal.active { display: flex; } .reece-gallery-modal-close { position: absolute; top: 20px; right: 30px; color: #fff; font-size: 40px; cursor: pointer; z-index: 10001; } .reece-gallery-modal-close:hover { color: #ff0000; } .reece-gallery-modal-content { max-width: 90%; max-height: 80%; object-fit: contain; border: 3px solid #fff; } .reece-gallery-modal-info { margin-top: 15px; text-align: center; color: #fff; font-family: 'Press Start 2P', monospace; } .reece-gallery-modal-info .asset-number { font-size: 14px; margin-bottom: 10px; } .reece-gallery-modal-info .asset-status { font-size: 10px; padding: 5px 10px; display: inline-block; } .reece-gallery-modal-info .asset-status.minted { background: #ff0000; } .reece-gallery-modal-info .asset-status.available { background: #00aa00; } .reece-gallery-pagination { display: flex; justify-content: center; align-items: center; gap: 15px; margin-top: 20px; } .reece-gallery-pagination button { background: #333; color: #fff; border: 2px solid #fff; padding: 8px 15px; font-size: 10px; font-family: 'Press Start 2P', monospace; cursor: pointer; } .reece-gallery-pagination button:hover:not(:disabled) { background: #555; } .reece-gallery-pagination button:disabled { opacity: 0.5; cursor: not-allowed; } .reece-gallery-pagination .page-info { color: #ccc; font-size: 10px; font-family: 'Press Start 2P', monospace; } `; document.head.appendChild(style); const mainLayout = app.querySelector('.main-layout'); if (mainLayout && !document.getElementById('reece-gallery-grid')) { const galleryContainer = document.createElement('div'); galleryContainer.className = 'reece-gallery-container'; galleryContainer.innerHTML = ` `; mainLayout.appendChild(galleryContainer); if (!document.getElementById('reece-gallery-modal')) { const modal = document.createElement('div'); modal.id = 'reece-gallery-modal'; modal.className = 'reece-gallery-modal'; modal.innerHTML = ` × Enlarged artwork `; modal.addEventListener('click', function(e) { if (e.target === modal) closeReeceGalleryModal(); }); document.body.appendChild(modal); } loadReeceGalleryPage(1); } }, 150); window.reeceGalleryCurrentPage = 1; const REECE_ITEMS_PER_PAGE = 50; window.openReeceGalleryModal = function(imgSrc, assetIndex, isMinted, inscriptionId) { const modal = document.getElementById('reece-gallery-modal'); const modalImg = document.getElementById('reece-gallery-modal-img'); const modalNumber = document.getElementById('reece-gallery-modal-number'); const modalStatus = document.getElementById('reece-gallery-modal-status'); const modalLink = document.getElementById('reece-gallery-modal-link'); if (modal && modalImg) { modalImg.src = imgSrc; modalNumber.textContent = '#' + assetIndex; modalStatus.textContent = isMinted ? 'MINTED' : 'AVAILABLE'; modalStatus.className = 'asset-status ' + (isMinted ? 'minted' : 'available'); if (inscriptionId) { modalLink.innerHTML = 'View on Ordinals.com'; } else { modalLink.innerHTML = ''; } modal.classList.add('active'); document.body.style.overflow = 'hidden'; } }; window.closeReeceGalleryModal = function() { const modal = document.getElementById('reece-gallery-modal'); if (modal) { modal.classList.remove('active'); document.body.style.overflow = ''; } }; document.addEventListener('keydown', function reeceGalleryEscape(e) { if (e.key === 'Escape') closeReeceGalleryModal(); }); window.loadReeceGalleryPage = async function(page) { const grid = document.getElementById('reece-gallery-grid'); const stats = document.getElementById('reece-gallery-stats'); const pagination = document.getElementById('reece-gallery-pagination'); const pageInfo = document.getElementById('reece-page-info'); const prevBtn = document.getElementById('reece-prev-btn'); const nextBtn = document.getElementById('reece-next-btn'); if (!grid) return; try { grid.innerHTML = '
Loading assets...
'; const response = await fetch('/api/get-collection-gallery?slug=reece-swanepoel-5ha5ow5&page=' + page + '&limit=' + REECE_ITEMS_PER_PAGE); const data = await response.json(); if (!data.success) { grid.innerHTML = '
Failed to load gallery: ' + (data.error || 'Unknown error') + '
'; return; } window.reeceGalleryCurrentPage = page; stats.textContent = `${data.stats.minted} MINTED / ${data.stats.available} AVAILABLE / ${data.stats.total} TOTAL`; if (data.assets.length === 0) { grid.innerHTML = '
No assets found in this collection.
'; pagination.style.display = 'none'; return; } grid.innerHTML = data.assets.map(asset => { const hasImage = asset.imageDataBase64 && !asset.r2Error; const imgSrc = hasImage ? 'data:' + (asset.contentType || 'image/png') + ';base64,' + asset.imageDataBase64 : 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMzMzIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmb250LXNpemU9IjEwIj5ObyBJbWFnZTwvdGV4dD48L3N2Zz4='; const inscId = asset.inscriptionId || ''; return ''; }).join(''); const totalPages = data.pagination.totalPages; if (totalPages > 1) { pagination.style.display = 'flex'; pageInfo.textContent = 'Page ' + page + ' of ' + totalPages; prevBtn.disabled = !data.pagination.hasPrev; nextBtn.disabled = !data.pagination.hasNext; } else { pagination.style.display = 'none'; } } catch (error) { console.error('Reece gallery load error:', error); grid.innerHTML = '
Error loading gallery
'; } }; } else if (slug === 'nonnish-naws') { // NAWS CEEFAX/Teletext styling - inspired by naws.fun document.body.classList.add('naws-mint-page'); const app = document.getElementById('app'); if (app) app.classList.add('naws-mint-page'); setTimeout(() => { const style = document.createElement('style'); style.setAttribute('data-naws-style', 'true'); style.textContent = ` /* NAWS CEEFAX/Teletext styling */ @import url('https://fonts.googleapis.com/css2?family=VT323&display=swap'); body.naws-mint-page { background: #000000 !important; font-family: 'VT323', 'Courier New', monospace !important; } /* Hide header on NAWS mint page */ body.naws-mint-page .header { display: none !important; } /* Rainbow stripe at top */ body.naws-mint-page #app::before { content: ''; display: block; width: 100%; height: 20px; background: repeating-linear-gradient( 90deg, #ff0000 0px, #ff0000 40px, #ffff00 40px, #ffff00 80px, #00ff00 80px, #00ff00 120px, #00ffff 120px, #00ffff 160px, #0000ff 160px, #0000ff 200px, #ff00ff 200px, #ff00ff 240px ); margin-bottom: 0; } /* Title banner - cyan with stars (grid row 1, full width — not order:-1, which breaks mint-redesign grid) */ body.naws-mint-page .main-layout::after { content: '★ THE REALITY REMIX COVEN ★'; display: block; width: calc(100% - 40px); margin: 10px 20px; padding: 12px 20px; background: #00ffff; color: #000000; font-family: 'VT323', monospace; font-size: 20px; text-align: center; letter-spacing: 3px; } /* Container styling - black with colored borders */ body.naws-mint-page .container { background: #000000 !important; border: 3px solid #00ffff !important; font-family: 'VT323', monospace !important; } /* Left column containers */ body.naws-mint-page .left-column .container { border-color: #00ff00 !important; position: relative; } /* Right column containers - magenta border */ body.naws-mint-page .right-column .container { border-color: #ff00ff !important; position: relative; } /* All text should use VT323 */ body.naws-mint-page * { font-family: 'VT323', monospace !important; } /* Text colors */ body.naws-mint-page .left-column, body.naws-mint-page .left-column * { color: #00ffff !important; } body.naws-mint-page .right-column, body.naws-mint-page .right-column * { color: #ffff00 !important; } /* Title styling */ body.naws-mint-page #artwork-title { color: #ffffff !important; font-size: 28px !important; text-transform: uppercase; letter-spacing: 2px; } /* Artist name */ body.naws-mint-page #artist-name { color: #00ff00 !important; font-size: 20px !important; } /* Description text */ body.naws-mint-page #artwork-description { color: #ffffff !important; font-size: 18px !important; line-height: 1.4 !important; } /* Price styling */ body.naws-mint-page #artwork-price { color: #ffff00 !important; font-size: 24px !important; } /* Buttons - Red with black text (CEEFAX style) */ body.naws-mint-page button, body.naws-mint-page #connectBtn { background: #ff0000 !important; color: #ffffff !important; border: 3px solid #ffffff !important; font-family: 'VT323', monospace !important; font-size: 20px !important; padding: 12px 24px !important; text-transform: uppercase; letter-spacing: 2px; cursor: pointer; transition: all 0.1s; } body.naws-mint-page button:hover:not(:disabled), body.naws-mint-page #connectBtn:hover:not(:disabled) { background: #ffff00 !important; color: #000000 !important; border-color: #000000 !important; } body.naws-mint-page button:disabled { background: #333333 !important; color: #666666 !important; border-color: #666666 !important; } /* Fee boxes */ body.naws-mint-page .fee-box { background: #000033 !important; border: 2px solid #0000ff !important; } body.naws-mint-page .fee-option input[type="radio"]:checked + .fee-box { background: #0000aa !important; border-color: #00ffff !important; } body.naws-mint-page .fee-box * { color: #00ffff !important; } /* Links */ body.naws-mint-page a { color: #00ffff !important; } body.naws-mint-page a:hover { color: #ffff00 !important; } /* Input fields */ body.naws-mint-page input, body.naws-mint-page textarea, body.naws-mint-page select { background: #000033 !important; color: #00ffff !important; border: 2px solid #0000ff !important; font-family: 'VT323', monospace !important; font-size: 18px !important; } /* Wallet info section */ body.naws-mint-page .wallet-info, body.naws-mint-page #walletInfo { background: #000033 !important; border: 2px solid #00ffff !important; } body.naws-mint-page .wallet-info *, body.naws-mint-page #walletInfo * { color: #00ffff !important; } /* Mint counter */ body.naws-mint-page #mintCounter { color: #00ff00 !important; font-size: 20px !important; } /* Artwork image container */ body.naws-mint-page #artwork-container { border: 4px solid #0000ff !important; background: #000033 !important; } /* Mint redesign uses CSS grid; keep NAWS banner + art left / connect & flow right */ body.naws-mint-page .main-layout.mint-redesign, body.naws-mint-page .mint-redesign.main-layout { display: grid !important; flex-wrap: unset !important; grid-template-columns: minmax(0, 1fr) minmax(280px, 500px) !important; gap: 22px !important; align-items: start !important; } body.naws-mint-page .main-layout.mint-redesign::after, body.naws-mint-page .mint-redesign.main-layout::after { grid-column: 1 / -1 !important; grid-row: 1 !important; width: 100% !important; max-width: none !important; margin-left: 0 !important; margin-right: 0 !important; box-sizing: border-box !important; } body.naws-mint-page .main-layout.mint-redesign .left-column, body.naws-mint-page .mint-redesign .left-column { grid-column: 1 !important; grid-row: 2 !important; min-width: 0 !important; } body.naws-mint-page .main-layout.mint-redesign .right-column, body.naws-mint-page .mint-redesign .right-column { grid-column: 2 !important; grid-row: 2 !important; min-width: 0 !important; } @media (max-width: 1024px) { body.naws-mint-page .main-layout.mint-redesign, body.naws-mint-page .mint-redesign.main-layout { grid-template-columns: 1fr !important; grid-template-rows: auto auto auto !important; } body.naws-mint-page .mint-redesign .left-column { grid-column: 1 !important; grid-row: 2 !important; } body.naws-mint-page .mint-redesign .right-column { grid-column: 1 !important; grid-row: 3 !important; } } /* Status messages */ body.naws-mint-page #statusMessage { background: #000033 !important; border: 2px solid #ffff00 !important; color: #ffff00 !important; } /* Physical print section */ body.naws-mint-page #physicalPrintStep { border-color: #00ff00 !important; } body.naws-mint-page #physicalPrintStep * { color: #00ff00 !important; } /* Quantity selector */ body.naws-mint-page #quantity-selector { border: 2px solid #ff00ff !important; } body.naws-mint-page #quantity-selector * { color: #ff00ff !important; } /* Override Press Start 2P with VT323 */ body.naws-mint-page [style*="Press Start 2P"] { font-family: 'VT323', monospace !important; } /* Scanline effect overlay */ body.naws-mint-page #app { position: relative; } body.naws-mint-page #app::after { content: ''; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: repeating-linear-gradient( 0deg, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 3px ); pointer-events: none; z-index: 9999; } /* Mobile adjustments */ @media (max-width: 768px) { body.naws-mint-page .main-layout::before, body.naws-mint-page .main-layout::after { width: calc(100% - 20px) !important; margin-left: 10px !important; margin-right: 10px !important; font-size: 14px !important; } body.naws-mint-page #artwork-title { font-size: 20px !important; } body.naws-mint-page button { font-size: 16px !important; padding: 10px 16px !important; } } `; document.head.appendChild(style); // Store flag to customize appearance window.nawsMintPage = true; // Clean up when leaving the page window.addEventListener('beforeunload', () => { document.body.classList.remove('naws-mint-page'); const app = document.getElementById('app'); if (app) { app.classList.remove('naws-mint-page'); } }); }, 150); } else if (slug === 'savage-x-sxulr-hollowed-animals') { // Hollowed Animals collection - fixed background at bottom right only setTimeout(() => { const style = document.createElement('style'); style.setAttribute('data-hollowed-style', 'true'); style.textContent = ` /* Fixed background image at bottom right - below header, behind UI */ body.hollowed-mint-page::after { content: ''; position: fixed; top: 60px; right: 0; bottom: 0; width: 100vw; background: url('./banners/hollowed_bg.png') no-repeat bottom right; background-size: auto calc(100vh - 60px); pointer-events: none; z-index: -1; opacity: 0.85; } @media (max-width: 768px) { body.hollowed-mint-page::after { top: 50px; background-size: auto 50vh; } } /* Transparent container in right column */ body.hollowed-mint-page .right-column .container { background: transparent; } `; document.head.appendChild(style); // Add class to body for the fixed background document.body.classList.add('hollowed-mint-page'); }, 150); } else if (slug === 'mchexley-pizza-comrades') { // Tokens on html so redesign `html { background: var(--ll-bg) }` picks them up; mint page only document.documentElement.classList.add('pizza-comrades-mint-page'); document.body.classList.add('pizza-comrades-mint-page'); const pizzaMintStyle = document.createElement('style'); pizzaMintStyle.setAttribute('data-pizza-comrades-mint-style', 'true'); pizzaMintStyle.textContent = ` html.pizza-comrades-mint-page { --ll-bg: #FE5403; --ll-surface: #fcd308; --ll-surface-2: #111111; --ll-border: #1a1a1a; --ll-border-hi: #1a1a1a; --ll-text: #1a1a1a; --ll-muted: #1a1a1a; --ll-mid: #111111; --ll-accent: #6395ee; --ll-accent-dim: #1a1a1a; --ll-danger: #e05252; --ll-success: #52c97e; --ll-serif: 'Cormorant Garamond', Georgia, serif; --ll-sans: 'Inter', system-ui, sans-serif; --ll-mono: 'JetBrains Mono', ui-monospace, monospace; --ll-nav-h: 64px; --neon-green: #06a18d; --neon-green-glow: rgb(5 161 141); --card-bg: #111111; } body.pizza-comrades-mint-page { background: #000000 !important; } body.pizza-comrades-mint-page #app { position: relative; z-index: 1; background: var(--ll-bg, #FE5403); min-height: calc(100vh - 60px); padding-top: 30px; padding-bottom: 50px; } body.pizza-comrades-mint-page #app .main-layout.mint-redesign { background: #FE5403; position: relative; z-index: 2; } body.pizza-comrades-mint-page .mint-redesign .mint-phase-upcoming-title { display: none !important; } body.pizza-comrades-mint-page .mint-redesign #connectBtn, body.pizza-comrades-mint-page .mint-redesign #inscribeBtn { background: #06a18d !important; color: #ffffff !important; border-color: #06a18d !important; } body.pizza-comrades-mint-page .mint-redesign #connectBtn:hover:not(:disabled), body.pizza-comrades-mint-page .mint-redesign #inscribeBtn:hover:not(:disabled) { background: #058a77 !important; border-color: #058a77 !important; color: #ffffff !important; } body.pizza-comrades-mint-page .mint-redesign #connectBtn:disabled, body.pizza-comrades-mint-page .mint-redesign #inscribeBtn:disabled { background: #3d3d3d !important; border-color: #3d3d3d !important; color: rgba(255,255,255,0.5) !important; } body.pizza-comrades-mint-page .mint-redesign #walletInfo, body.pizza-comrades-mint-page .mint-redesign #walletInfo p, body.pizza-comrades-mint-page .mint-redesign #walletAddress, body.pizza-comrades-mint-page .mint-redesign #walletAddress strong, body.pizza-comrades-mint-page .mint-redesign .address-display { color: #ffffff !important; } body.pizza-comrades-mint-page #walletChooserModal h3 { color: #fcd308 !important; } body.pizza-comrades-mint-page #walletChooserModal button[onclick*="chooseWalletAndConnect"] { background: #111111 !important; color: #fcd308 !important; border: 2px solid #fcd308 !important; } body.pizza-comrades-mint-page #walletChooserModal button[onclick*="chooseWalletAndConnect"]:hover { background: #1a1a1a !important; color: #fff6b0 !important; } body.pizza-comrades-mint-page #walletChooserModal button[onclick*="closeWalletChooser"] { background: #1a1a1a !important; color: #fcd308 !important; border: 1px solid #fcd308 !important; } body.pizza-comrades-mint-page .mint-enable-gallery-pag-btn { background: #111111 !important; color: #fcd308 !important; border: 1px solid #fcd308 !important; } body.pizza-comrades-mint-page .mint-enable-gallery-pag-btn:hover:not(:disabled) { background: #1a1a1a !important; color: #fff6b0 !important; border-color: #fff6b0 !important; } body.pizza-comrades-mint-page .mint-enable-gallery-pag-btn:disabled { background: #2a2a2a !important; color: rgba(252, 211, 8, 0.45) !important; border-color: rgba(252, 211, 8, 0.35) !important; } body.pizza-comrades-mint-page .mint-redesign #quantity-selector, body.pizza-comrades-mint-page .mint-redesign #quantity-selector label, body.pizza-comrades-mint-page .mint-redesign #quantity-selector p, body.pizza-comrades-mint-page .mint-redesign #quantity-selector #mint-quantity { color: #ffffff !important; } body.pizza-comrades-mint-page .mint-redesign #quantity-selector #mint-quantity { background: #111111 !important; } body.pizza-comrades-mint-page .mint-redesign #whitelist-info { background: #111111 !important; color: #fcd308 !important; border: 1px solid #fcd308 !important; } body.pizza-comrades-mint-page .mint-redesign #whitelist-info div { color: #fcd308 !important; } /* Global p-rule (ll-mid !important) kills inline white on Minting Not Available — scope to dark error card only */ body.pizza-comrades-mint-page .mint-redesign #walletStatus [style*="background: #000"] h3, body.pizza-comrades-mint-page .mint-redesign #walletStatus [style*="background: #000"] p { color: #ffffff !important; } body.pizza-comrades-mint-page .mint-redesign #walletStatus [style*="background: #000"] { background: #111111 !important; border: 1px solid #fcd308 !important; } body.pizza-comrades-mint-page .mint-redesign #walletStatus .error { color: #fecaca !important; } /* Fee speed: dark tiles, white copy; selected = same teal as Mint CTA (readable, not yellow-on-yellow) */ body.pizza-comrades-mint-page .mint-redesign .fee-box { background: #1a1a1a !important; border: 1px solid rgba(252, 211, 8, 0.4) !important; } body.pizza-comrades-mint-page .mint-redesign .fee-box div { color: #ffffff !important; } body.pizza-comrades-mint-page .mint-redesign .fee-box div:last-child { color: rgba(255, 255, 255, 0.75) !important; } body.pizza-comrades-mint-page .mint-redesign .fee-option input[type="radio"]:checked + .fee-box { background: #06a18d !important; border: 2px solid #ffffff !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); } body.pizza-comrades-mint-page .mint-redesign .fee-option input[type="radio"]:checked + .fee-box div, body.pizza-comrades-mint-page .mint-redesign .fee-option input[type="radio"]:checked + .fee-box div:last-child { color: #ffffff !important; } body.pizza-comrades-mint-page .mint-redesign #feeOptions label.fee-option span { color: #ffffff !important; } body.pizza-comrades-mint-page .mint-redesign #customFeeInput { background: #111111 !important; color: #ffffff !important; border: 1px solid rgba(252, 211, 8, 0.5) !important; } body.pizza-comrades-mint-page .mint-redesign #customFeeInput::placeholder { color: rgba(255, 255, 255, 0.45) !important; } /* Cost row: global rule uses yellow surface — force dark panel so +/= and labels stay legible */ body.pizza-comrades-mint-page .mint-redesign #costDisplayStep > div.mint-cost-inner { background: #1a1a1a !important; border: 1px solid rgba(252, 211, 8, 0.45) !important; } body.pizza-comrades-mint-page .mint-redesign .mint-cost-inner, body.pizza-comrades-mint-page .mint-redesign .mint-cost-inner p, body.pizza-comrades-mint-page .mint-redesign .mint-cost-inner strong { color: #ffffff !important; } body.pizza-comrades-mint-page .mint-redesign #networkSection p:first-child, body.pizza-comrades-mint-page .mint-redesign #networkBufferSection p:first-child, body.pizza-comrades-mint-page .mint-redesign .mint-cost-inner > div > p:first-child { color: rgba(255, 255, 255, 0.82) !important; } body.pizza-comrades-mint-page .mint-redesign #networkSeparator, body.pizza-comrades-mint-page .mint-redesign #networkBufferSeparator, body.pizza-comrades-mint-page .mint-redesign .mint-cost-inner > div[style*="font-size: 10px"] { color: #fcd308 !important; } body.pizza-comrades-mint-page .mint-redesign #physicalPrintCostDiv > div[style*="font-size: 10px"] { color: #fcd308 !important; } body.pizza-comrades-mint-page .mint-enable-gallery-modal-close { color: #fcd308 !important; border-color: rgba(252, 211, 8, 0.5) !important; } body.pizza-comrades-mint-page .mint-enable-gallery-modal-close:hover { color: #fff6b0 !important; border-color: #fcd308 !important; } #pizza-comrades-mint-bg { width: 25vw !important; height: 35vw !important; min-width: 150px; min-height: 200px; max-width: 400px; max-height: 550px; } @media (max-width: 768px) { #pizza-comrades-mint-bg { width: 30vw !important; height: 42vw !important; min-width: 100px; min-height: 140px; } } @media (max-width: 480px) { #pizza-comrades-mint-bg { width: 25vw !important; height: 35vw !important; min-width: 80px; min-height: 110px; } } `; document.head.appendChild(pizzaMintStyle); const bgImg = document.createElement('div'); bgImg.id = 'pizza-comrades-mint-bg'; bgImg.style.cssText = ` position: fixed; bottom: 0; left: 0; width: 300px; height: 400px; background-image: url('/pizza_comrades/pizza_comrade.png'); background-size: contain; background-repeat: no-repeat; background-position: bottom left; z-index: 1; pointer-events: none; opacity: 1; image-rendering: pixelated; `; app.appendChild(bgImg); } else { // Reset to default app.style.backgroundImage = ''; app.style.backgroundSize = ''; app.style.backgroundPosition = ''; app.style.backgroundRepeat = ''; app.style.backgroundAttachment = ''; } // Initialize mint page functionality after DOM is updated setTimeout(() => { initializeMintPage(); loadArtworkForMinting(slug, isAdmin ? data : null); if (isAdmin && data && data.artwork) { const lockedForPublic = data.artwork.isActive === false || isArtworkUpcoming(data.artwork); if (lockedForPublic) { mountAdminMintPreviewBanner('🔧 ADMIN PREVIEW — This mint is not yet live for the public'); } } }, 100); } catch (error) { // Network error or other issue const app = document.getElementById('app'); app.innerHTML = `

Error Loading Artwork

Unable to load artwork information. Please check your connection and try again.

← Back to Live Mints
`; } } function showLoginPage() { currentPage = 'login'; const template = document.getElementById('login-template'); const app = document.getElementById('app'); app.innerHTML = template.innerHTML; setupLoginPage(); } function showRegisterPage() { currentPage = 'register'; const template = document.getElementById('register-template'); const app = document.getElementById('app'); app.innerHTML = template.innerHTML; setupRegisterPage(); } function showSubmitPage() { currentPage = 'submit'; const template = document.getElementById('submit-template'); const app = document.getElementById('app'); app.innerHTML = template.innerHTML; setupSubmitPage(); } function showUserPortalPage() { currentPage = 'user-portal'; const template = document.getElementById('user-portal-template'); const app = document.getElementById('app'); app.innerHTML = template.innerHTML; setupUserPortalPage(); } function showAboutPage() { currentPage = 'about'; const template = document.getElementById('about-template'); const app = document.getElementById('app'); app.innerHTML = template.innerHTML; } let bitcoinosWalletList = null; function showBitcoinosCheckerPage() { currentPage = 'bitcoinos-checker'; const template = document.getElementById('bitcoinos-checker-template'); const app = document.getElementById('app'); if (!template || !app) return; app.innerHTML = template.innerHTML; const resultEl = document.getElementById('bitcoinos-result'); const inputEl = document.getElementById('bitcoinos-wallet-input'); if (resultEl) resultEl.style.display = 'none'; window.checkBitcoinosSpots = async function() { const raw = (inputEl && inputEl.value) ? inputEl.value.trim() : ''; const addr = raw.toLowerCase(); if (!addr) { if (resultEl) { resultEl.style.display = 'block'; resultEl.textContent = 'Paste your wallet address and click Check.'; } return; } if (!bitcoinosWalletList) { try { const res = await fetch('./banners/viola/bitcoinos-wallets.json'); if (!res.ok) throw new Error('Failed to load list'); bitcoinosWalletList = await res.json(); } catch (e) { if (resultEl) { resultEl.style.display = 'block'; resultEl.textContent = 'Could not load wallet list. Try again later.'; } return; } } const count = (typeof bitcoinosWalletList === 'object' && bitcoinosWalletList !== null && !Array.isArray(bitcoinosWalletList)) ? (bitcoinosWalletList[addr] || 0) : bitcoinosWalletList.filter(function(a) { return a === addr; }).length; if (resultEl) { resultEl.style.display = 'block'; resultEl.textContent = count === 0 ? 'You have 0 spots.' : 'You have ' + count + ' spot' + (count !== 1 ? 's' : '') + '.'; } }; if (inputEl) inputEl.addEventListener('keydown', function(e) { if (e.key === 'Enter') window.checkBitcoinosSpots(); }); } let pizzaComradesGtdWalletMap = null; let pizzaComradesFcfsWalletMap = null; function showPizzaComradesWalletCheckerPage() { currentPage = 'pizza-comrades-wallet-checker'; const template = document.getElementById('pizza-comrades-wallet-checker-template'); const app = document.getElementById('app'); if (!template || !app) return; app.innerHTML = template.innerHTML; const resultGtd = document.getElementById('pizza-comrades-gtd-result'); const resultFcfs = document.getElementById('pizza-comrades-fcfs-result'); const inputEl = document.getElementById('pizza-comrades-wallet-input'); function showBoth(gtdText, fcfsText) { if (resultGtd) { resultGtd.style.display = 'block'; resultGtd.textContent = gtdText; } if (resultFcfs) { resultFcfs.style.display = 'block'; resultFcfs.textContent = fcfsText; } } function hideResults() { if (resultGtd) { resultGtd.style.display = 'none'; resultGtd.textContent = ''; } if (resultFcfs) { resultFcfs.style.display = 'none'; resultFcfs.textContent = ''; } } hideResults(); function runPizzaComradesWalletCheck() { const raw = (inputEl && inputEl.value) ? inputEl.value.trim() : ''; const addr = raw.toLowerCase(); if (!addr) { showBoth('Enter your Taproot address and press Enter.', 'Enter your Taproot address and press Enter.'); return; } if (!/^bc1p[a-z0-9]{10,}$/i.test(addr)) { showBoth('That does not look like a Taproot address (expected bc1p…).', 'That does not look like a Taproot address (expected bc1p…).'); return; } if (pizzaComradesGtdWalletMap == null || pizzaComradesFcfsWalletMap == null) { showBoth('Loading…', 'Loading…'); Promise.all([ fetch('./pizza_comrades/pizza-comrades-gtd-wallets.json').then(function(res) { if (!res.ok) throw new Error('gtd'); return res.json(); }), fetch('./pizza_comrades/pizza-comrades-fcfs-wallets.json').then(function(res) { if (!res.ok) throw new Error('fcfs'); return res.json(); }) ]) .then(function(arr) { pizzaComradesGtdWalletMap = arr[0] && typeof arr[0] === 'object' ? arr[0] : {}; pizzaComradesFcfsWalletMap = arr[1] && typeof arr[1] === 'object' ? arr[1] : {}; runPizzaComradesWalletCheck(); }) .catch(function() { pizzaComradesGtdWalletMap = null; pizzaComradesFcfsWalletMap = null; showBoth('Could not load the GTD list. Try again later.', 'Could not load the FCFS list. Try again later.'); }); return; } const gtd = Number(pizzaComradesGtdWalletMap[addr]) || 0; const fcfs = Number(pizzaComradesFcfsWalletMap[addr]) || 0; const gtdWord = gtd === 1 ? 'spot' : 'spots'; const fcfsWord = fcfs === 1 ? 'spot' : 'spots'; showBoth( gtd === 0 ? 'You have 0 GTD spots for this wallet.' : 'You have ' + gtd + ' GTD ' + gtdWord + '.', fcfs === 0 ? 'You have 0 FCFS spots for this wallet.' : 'You have ' + fcfs + ' FCFS ' + fcfsWord + '.' ); } window.checkPizzaComradesWalletSpots = runPizzaComradesWalletCheck; if (inputEl) { inputEl.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); runPizzaComradesWalletCheck(); } }); inputEl.focus(); } } let fiveIl5nc5GtdWalletMap = null; let fiveIl5nc5LoyaltyWalletMap = null; function show5il5nc5WalletCheckerPage() { currentPage = '5il5nc5-wallet-checker'; const template = document.getElementById('5il5nc5-wallet-checker-template'); const app = document.getElementById('app'); if (!template || !app) return; app.innerHTML = template.innerHTML; const resultGtd = document.getElementById('5il5nc5-gtd-result'); const resultLoyalty = document.getElementById('5il5nc5-loyalty-result'); const inputEl = document.getElementById('5il5nc5-wallet-input'); function showBoth(gtdText, loyaltyText) { if (resultGtd) { resultGtd.style.display = 'block'; resultGtd.textContent = gtdText; } if (resultLoyalty) { resultLoyalty.style.display = 'block'; resultLoyalty.textContent = loyaltyText; } } function hideResults() { if (resultGtd) { resultGtd.style.display = 'none'; resultGtd.textContent = ''; } if (resultLoyalty) { resultLoyalty.style.display = 'none'; resultLoyalty.textContent = ''; } } hideResults(); function run5il5nc5WalletCheck() { const raw = (inputEl && inputEl.value) ? inputEl.value.trim() : ''; const addr = raw.toLowerCase(); if (!addr) { showBoth('Enter your Taproot address and press Enter.', 'Enter your Taproot address and press Enter.'); return; } if (!/^bc1p[a-z0-9]{10,}$/i.test(addr)) { showBoth('That does not look like a Taproot address (expected bc1p…).', 'That does not look like a Taproot address (expected bc1p…).'); return; } if (fiveIl5nc5GtdWalletMap == null || fiveIl5nc5LoyaltyWalletMap == null) { showBoth('Loading…', 'Loading…'); Promise.all([ fetch('./Reece/5il5nc5-gtd-wallets.json').then(function(res) { if (!res.ok) throw new Error('gtd'); return res.json(); }), fetch('./Reece/5il5nc5-loyalty-wallets.json').then(function(res) { if (!res.ok) throw new Error('loyalty'); return res.json(); }) ]) .then(function(arr) { fiveIl5nc5GtdWalletMap = arr[0] && typeof arr[0] === 'object' ? arr[0] : {}; fiveIl5nc5LoyaltyWalletMap = arr[1] && typeof arr[1] === 'object' ? arr[1] : {}; run5il5nc5WalletCheck(); }) .catch(function() { fiveIl5nc5GtdWalletMap = null; fiveIl5nc5LoyaltyWalletMap = null; showBoth('Could not load the GTD list. Try again later.', 'Could not load the Loyalty list. Try again later.'); }); return; } const gtd = Number(fiveIl5nc5GtdWalletMap[addr]) || 0; const loyalty = Number(fiveIl5nc5LoyaltyWalletMap[addr]) || 0; const gtdWord = gtd === 1 ? 'spot' : 'spots'; const loyaltyWord = loyalty === 1 ? 'spot' : 'spots'; showBoth( gtd === 0 ? 'You have 0 GTD spots for this wallet.' : 'You have ' + gtd + ' GTD ' + gtdWord + '.', loyalty === 0 ? 'You have 0 Loyalty spots for this wallet.' : 'You have ' + loyalty + ' Loyalty ' + loyaltyWord + '.' ); } window.check5il5nc5WalletSpots = run5il5nc5WalletCheck; if (inputEl) { inputEl.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); run5il5nc5WalletCheck(); } }); inputEl.focus(); } } function showSponsorshipPage() { currentPage = 'sponsorship'; const template = document.getElementById('sponsorship-template'); const app = document.getElementById('app'); app.innerHTML = template.innerHTML; // Load current sponsors after page renders setTimeout(() => loadCurrentSponsors(), 100); } // Load current sponsors for sponsorship page async function loadCurrentSponsors() { try { const response = await fetch('/api/get-active-sponsors'); const data = await response.json(); if (data.success && data.sponsors && data.sponsors.length > 0) { const sponsorsList = document.getElementById('current-sponsors-list'); if (sponsorsList) { // Check if we've already loaded sponsors (prevent duplicates) - exclude founders and hardcoded defaults const existingSponsors = sponsorsList.querySelectorAll('a:not(.founder-sponsor):not(.default-sponsor)'); if (existingSponsors.length > 0) { console.log('Current sponsors already loaded, skipping...'); return; } // Keep PQ default, add active sponsors const sponsorsHtml = data.sponsors.map(sponsor => { const tierColor = sponsor.tier === 'gold' ? '#FFD700' : sponsor.tier === 'silver' ? '#C0C0C0' : '#CD7F32'; const sponsorLink = getSponsorLink(sponsor); return `
${sponsor.sponsor_name}

${sponsor.sponsor_name}

${sponsor.tier}

`; }).join(''); sponsorsList.innerHTML += sponsorsHtml; } } } catch (error) { console.error('Failed to load current sponsors:', error); } } // Helper function to sanitize Twitter URLs (same as artwork display) function sanitizeTwitterUrl(twitterHandle) { if (!twitterHandle) return null; if (twitterHandle.startsWith('http')) return twitterHandle; return 'https://twitter.com/' + twitterHandle.replace('@', ''); } // Helper function to sanitize website URLs (same as artwork display) function sanitizeWebsiteUrl(websiteUrl) { if (!websiteUrl) return null; if (websiteUrl.startsWith('http://') || websiteUrl.startsWith('https://')) { return websiteUrl; } return 'https://' + websiteUrl; } // Helper function to get sponsor link (website or Twitter) function getSponsorLink(sponsor) { if (sponsor.website_url) { return sanitizeWebsiteUrl(sponsor.website_url); } if (sponsor.twitter_handle) { return sanitizeTwitterUrl(sponsor.twitter_handle); } return '#'; // Fallback if neither exists } // Sponsorship tier selection let selectedTierData = null; window.selectTier = function(tierName, amount) { selectedTierData = { tier: tierName, amount: amount }; // Show the payment form document.getElementById('tier-payment-form').style.display = 'block'; document.getElementById('selected-tier-name').textContent = tierName.toUpperCase(); document.getElementById('selected-tier-amount').textContent = amount.toLocaleString(); // Scroll to form document.getElementById('tier-payment-form').scrollIntoView({ behavior: 'smooth', block: 'start' }); }; window.cancelTierSelection = function() { selectedTierData = null; document.getElementById('tier-payment-form').style.display = 'none'; // Clear form document.getElementById('sponsor-name').value = ''; document.getElementById('sponsor-url').value = ''; document.getElementById('sponsor-twitter').value = ''; document.getElementById('sponsor-logo').value = ''; document.getElementById('sponsor-email').value = ''; document.getElementById('sponsor-description').value = ''; document.getElementById('tier-payment-result').innerHTML = ''; }; window.processTierSponsorship = async function() { if (!selectedTierData) return; const name = document.getElementById('sponsor-name').value.trim(); const url = document.getElementById('sponsor-url').value.trim(); const twitter = document.getElementById('sponsor-twitter').value.trim(); const logoFile = document.getElementById('sponsor-logo').files[0]; const email = document.getElementById('sponsor-email').value.trim(); const description = document.getElementById('sponsor-description').value.trim(); const resultDiv = document.getElementById('tier-payment-result'); // Validation if (!name || !url || !email || !logoFile) { resultDiv.innerHTML = '
Please fill in all required fields (*)
'; return; } // Validate logo file size if (logoFile.size > 500000) { resultDiv.innerHTML = '
Logo file must be under 500KB
'; return; } const btn = document.getElementById('tier-payment-btn'); btn.disabled = true; btn.textContent = 'PROCESSING...'; resultDiv.innerHTML = '
Connecting wallet...
'; try { // Connect Xverse wallet using sats-connect resultDiv.innerHTML = '
Opening wallet connection...
'; const walletResponse = await request('wallet_connect', null); console.log('Wallet connect response:', walletResponse); if (walletResponse.status !== 'success') { throw new Error('Wallet connection cancelled or failed'); } const paymentAddressInfo = walletResponse.result.addresses.find(a => a.purpose === AddressPurpose.Payment); if (!paymentAddressInfo) { throw new Error('Failed to get payment address from wallet'); } resultDiv.innerHTML = '
Wallet connected. Processing payment...
'; // Convert logo to base64 const logoBase64 = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result.split(',')[1]); reader.onerror = reject; reader.readAsDataURL(logoFile); }); // Platform wallet address const platformWallet = 'bc1qcefl3uc75wh39wcwqs4ca9xw8m7jhc35t3kv43'; // Send payment via Xverse resultDiv.innerHTML = '
Sending payment... Please confirm in your wallet.
'; const transferResponse = await request('sendTransfer', { recipients: [{ address: platformWallet, amount: selectedTierData.amount }] }); console.log('Transfer response:', transferResponse); if (transferResponse.status !== 'success') { throw new Error('Payment cancelled or failed'); } const txid = transferResponse.result?.txid; if (!txid) { throw new Error('No transaction ID received'); } resultDiv.innerHTML = '
Payment sent! Recording sponsorship...
'; // Record sponsorship in database const response = await fetch('/api/record-sponsorship', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tier: selectedTierData.tier, amount: selectedTierData.amount, sponsorName: name, websiteUrl: url, twitterHandle: twitter, logoBase64: logoBase64, logoFilename: logoFile.name, contactEmail: email, description: description, transactionId: txid, paymentWalletAddress: paymentAddressInfo.address }) }); const data = await response.json(); if (data.success) { console.log('Sponsorship successful! Showing thank you modal...'); console.log('Tier:', selectedTierData.tier, 'Name:', name, 'TxID:', txid); // Show personalized thank you modal showSponsorThankYouModal(selectedTierData.tier, name, txid); // Clear form cancelTierSelection(); // Reload sponsors in footer and page setTimeout(() => { loadActiveSponsors(); loadCurrentSponsors(); }, 2000); } else { const errorMsg = data.error || 'Failed to record sponsorship'; const errorDetails = data.details ? `\n\nDetails: ${data.details}` : ''; const errorStack = data.stack ? `\n\nStack: ${data.stack}` : ''; console.error('API Error:', errorMsg, errorDetails, errorStack); throw new Error(errorMsg); } } catch (error) { console.error('Sponsorship error:', error); resultDiv.innerHTML = `
Error: ${error.message || 'Failed to process sponsorship'}
`; } finally { btn.disabled = false; btn.textContent = 'PAY WITH BITCOIN'; } }; // Show personalized thank you modal for sponsors function showSponsorThankYouModal(tier, sponsorName, txid) { const tierColors = { 'bronze': '#CD7F32', 'silver': '#C0C0C0', 'gold': '#FFD700' }; const tierMessages = { 'bronze': 'Your support means the world to us! Your logo will proudly appear in our sponsors section and footer.', 'silver': 'We are incredibly grateful for your generous support! You\'ll receive featured placement and priority support.', 'gold': 'WOW! Your premium sponsorship is a game-changer for LunaLauncher! We can\'t wait to collaborate with you.' }; const tierColor = tierColors[tier] || '#00ff00'; const message = tierMessages[tier] || 'Thank you for your support!'; console.log('Creating modal with tier color:', tierColor); const modalHtml = ` `; console.log('Inserting modal HTML into body...'); document.body.insertAdjacentHTML('beforeend', modalHtml); console.log('Modal inserted successfully'); // Auto-close after 15 seconds setTimeout(() => { console.log('Auto-closing modal after 15 seconds'); closeSponsorThankYouModal(); }, 15000); } window.closeSponsorThankYouModal = function() { const modal = document.getElementById('sponsor-thank-you-modal'); if (modal) { modal.style.animation = 'fadeOut 0.3s'; setTimeout(() => modal.remove(), 300); } }; window.submitCustomSponsorInquiry = async function() { const name = document.getElementById('custom-sponsor-name').value.trim(); const email = document.getElementById('custom-sponsor-email').value.trim(); const company = document.getElementById('custom-sponsor-company').value.trim(); const budget = document.getElementById('custom-sponsor-budget').value; const message = document.getElementById('custom-sponsor-message').value.trim(); const resultDiv = document.getElementById('custom-sponsor-result'); if (!name || !email || !message) { resultDiv.innerHTML = '
Please fill in all required fields (*)
'; return; } const btn = document.getElementById('custom-sponsor-btn'); btn.disabled = true; btn.textContent = 'SENDING...'; resultDiv.innerHTML = '
Sending inquiry...
'; try { const response = await fetch('/api/submit-sponsor-inquiry', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, email: email, company: company, budgetRange: budget, message: message }) }); const data = await response.json(); if (data.success) { resultDiv.innerHTML = '
Thank you! We\'ll be in touch within 48 hours.
'; // Clear form document.getElementById('custom-sponsor-name').value = ''; document.getElementById('custom-sponsor-email').value = ''; document.getElementById('custom-sponsor-company').value = ''; document.getElementById('custom-sponsor-budget').value = ''; document.getElementById('custom-sponsor-message').value = ''; } else { resultDiv.innerHTML = `
Error: ${data.error || 'Failed to submit inquiry'}
`; } } catch (error) { console.error('Custom sponsor inquiry error:', error); resultDiv.innerHTML = '
Failed to send inquiry. Please try again later.
'; } finally { btn.disabled = false; btn.textContent = 'SUBMIT INQUIRY'; } }; window.processOneOffSupport = async function() { const amount = parseInt(document.getElementById('oneoff-amount').value); const name = document.getElementById('oneoff-name').value.trim() || 'Anonymous'; const resultDiv = document.getElementById('oneoff-result'); if (!amount || amount < 1000) { resultDiv.innerHTML = '
Please enter an amount of at least 1,000 sats
'; return; } const btn = document.getElementById('oneoff-btn'); btn.disabled = true; btn.textContent = 'PROCESSING...'; resultDiv.innerHTML = '
Connecting wallet...
'; try { // Connect Xverse wallet using sats-connect resultDiv.innerHTML = '
Opening wallet connection...
'; const walletResponse = await request('wallet_connect', null); console.log('Wallet connect response:', walletResponse); if (walletResponse.status !== 'success') { throw new Error('Wallet connection cancelled or failed'); } const paymentAddr = walletResponse.result.addresses.find(a => a.purpose === AddressPurpose.Payment); if (!paymentAddr) { throw new Error('Failed to get payment address from wallet'); } // Platform wallet address const platformWallet = 'bc1qcefl3uc75wh39wcwqs4ca9xw8m7jhc35t3kv43'; resultDiv.innerHTML = '
Sending payment... Please confirm in your wallet.
'; // Send payment via Xverse const transferResponse = await request('sendTransfer', { recipients: [{ address: platformWallet, amount: amount }] }); if (transferResponse.status !== 'success') { throw new Error('Payment cancelled or failed'); } const txid = transferResponse.result?.txid; if (!txid) { throw new Error('No transaction ID received'); } resultDiv.innerHTML = '
Payment sent! Recording...
'; // Record in database const response = await fetch('/api/record-oneoff-support', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: amount, supporterName: name, transactionId: txid, walletAddress: paymentAddr.address }) }); const data = await response.json(); if (data.success) { resultDiv.innerHTML = `

Thank you for your support! 🙏

Transaction: ${txid}

`; // Clear form document.getElementById('oneoff-amount').value = ''; document.getElementById('oneoff-name').value = ''; } else { throw new Error(data.error || 'Failed to record payment'); } } catch (error) { console.error('One-off support error:', error); resultDiv.innerHTML = `
Error: ${error.message || 'Failed to process payment'}
`; } finally { btn.disabled = false; btn.textContent = 'SEND SUPPORT'; } }; function showPointsPage() { currentPage = 'points'; const template = document.getElementById('points-template'); const app = document.getElementById('app'); app.innerHTML = template.innerHTML; // Check if wallet was previously connected const savedWallet = localStorage.getItem('pointsWalletAddress'); if (savedWallet) { document.getElementById('points-wallet-address').textContent = savedWallet; document.getElementById('points-wallet-disconnected').style.display = 'none'; document.getElementById('points-wallet-connected').style.display = 'block'; loadPointsForWallet(savedWallet); } } // Connect wallet for points window.connectWalletForPoints = async function() { try { const response = await request('getAccounts', { purposes: [AddressPurpose.Payment], message: 'Connect your wallet to view your creator points' }); if (response.status === 'success' && response.result && response.result.length > 0) { const paymentAddress = response.result.find(addr => addr.purpose === AddressPurpose.Payment); if (paymentAddress) { const address = paymentAddress.address; // Save wallet address localStorage.setItem('pointsWalletAddress', address); // Update UI document.getElementById('points-wallet-address').textContent = address; document.getElementById('points-wallet-disconnected').style.display = 'none'; document.getElementById('points-wallet-connected').style.display = 'block'; // Load points loadPointsForWallet(address); } } else { alert('Failed to connect wallet. Please make sure you have a Bitcoin wallet extension installed.'); } } catch (error) { console.error('Wallet connection error:', error); alert('Failed to connect wallet: ' + error.message); } }; // Connect wallet for points using Xverse window.connectWalletForPoints = async function() { try { const response = await request('getAccounts', { purposes: [AddressPurpose.Payment, AddressPurpose.Ordinals], message: 'Connect your wallet to view your creator points' }); if (response.status === 'success' && response.result && response.result.length > 0) { const paymentAccount = response.result.find(a => a.purpose === 'payment'); const ordinalsAccount = response.result.find(a => a.purpose === 'ordinals'); if (paymentAccount) { const paymentAddress = paymentAccount.address; const ordinalsAddress = ordinalsAccount ? ordinalsAccount.address : null; // Save wallet addresses localStorage.setItem('pointsWalletAddress', paymentAddress); if (ordinalsAddress) { localStorage.setItem('pointsOrdinalsAddress', ordinalsAddress); } // Update UI document.getElementById('points-wallet-address').textContent = paymentAddress; document.getElementById('points-ordinals-address').textContent = ordinalsAddress || 'Not available'; document.getElementById('points-wallet-disconnected').style.display = 'none'; document.getElementById('points-wallet-connected').style.display = 'block'; // Load points loadPointsForWallet(paymentAddress); } } else { alert('Failed to connect wallet. Please make sure you have a Bitcoin wallet extension installed.'); } } catch (error) { console.error('Wallet connection error:', error); alert('Failed to connect wallet: ' + error.message); } }; // Disconnect wallet window.disconnectWalletForPoints = function() { localStorage.removeItem('pointsWalletAddress'); localStorage.removeItem('pointsOrdinalsAddress'); document.getElementById('points-wallet-disconnected').style.display = 'block'; document.getElementById('points-wallet-connected').style.display = 'none'; document.getElementById('points-display-section').style.display = 'none'; }; // Update spend quantity options based on available points window.updateSpendQtyOptions = function(availablePoints) { const select = document.getElementById('spend-points-qty'); if (!select) return; const options = [ { value: 1, pts: 25 }, { value: 2, pts: 50 }, { value: 3, pts: 75 }, { value: 5, pts: 125 }, { value: 10, pts: 250 } ]; select.innerHTML = options .filter(opt => opt.pts <= availablePoints) .map(opt => ``) .join(''); }; // Spend creator points for NAWS window.spendCreatorPoints = async function() { if (!window.creatorPointsData) { alert('Please connect your wallet first'); return; } const qty = parseInt(document.getElementById('spend-points-qty').value) || 1; const pointsNeeded = qty * 25; if (window.creatorPointsData.available < pointsNeeded) { alert(`Not enough points. You have ${window.creatorPointsData.available}, need ${pointsNeeded}.`); return; } // Get ordinals address from connected wallet const ordinalsAddress = localStorage.getItem('pointsOrdinalsAddress'); if (!ordinalsAddress) { alert('No ordinals address connected. Please reconnect your wallet.'); return; } // Basic validation for taproot address if (!ordinalsAddress.startsWith('bc1p')) { alert('Connected ordinals address is not a taproot address (bc1p). Please use a wallet with a taproot ordinals address.'); return; } const statusEl = document.getElementById('spend-points-status'); const btn = document.getElementById('spend-points-btn'); statusEl.textContent = 'Processing...'; statusEl.style.color = '#ff9800'; btn.disabled = true; btn.style.opacity = '0.5'; try { const response = await fetch('/api/spend-creator-points', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ walletAddress: window.creatorPointsData.walletAddress, ordinalsAddress: ordinalsAddress, quantity: qty }) }); const data = await response.json(); if (data.success) { statusEl.innerHTML = `Success! ${qty} NAWS queued for inscription.
Remaining points: ${data.remainingPoints}`; // Keep button disabled and refresh points display btn.textContent = 'REFRESHING...'; setTimeout(async () => { await loadPointsForWallet(window.creatorPointsData.walletAddress); btn.disabled = false; btn.style.opacity = '1'; btn.textContent = 'REDEEM FOR NAWS'; }, 2000); } else { statusEl.innerHTML = `Error: ${data.error}`; btn.disabled = false; btn.style.opacity = '1'; } } catch (error) { console.error('Spend points error:', error); statusEl.innerHTML = `Error: ${error.message}`; btn.disabled = false; btn.style.opacity = '1'; } }; // Spend Luna points for NAWS window.spendLunaPoints = async function() { if (!window.lunaPointsData) { alert('Please connect your wallet first'); return; } const qty = parseInt(document.getElementById('spend-luna-points-qty').value) || 1; const pointsNeeded = qty * 25; if (window.lunaPointsData.available < pointsNeeded) { alert(`Not enough points. You have ${window.lunaPointsData.available}, need ${pointsNeeded}.`); return; } // Get ordinals address from connected wallet const ordinalsAddress = localStorage.getItem('pointsOrdinalsAddress'); if (!ordinalsAddress) { alert('No ordinals address connected. Please reconnect your wallet.'); return; } // Basic validation for taproot address if (!ordinalsAddress.startsWith('bc1p')) { alert('Connected ordinals address is not a taproot address (bc1p). Please use a wallet with a taproot ordinals address.'); return; } const statusEl = document.getElementById('spend-luna-points-status'); const btn = document.getElementById('spend-luna-points-btn'); statusEl.textContent = 'Processing...'; statusEl.style.color = '#ff9800'; btn.disabled = true; btn.style.opacity = '0.5'; try { const response = await fetch('/api/spend-luna-points', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ walletAddress: window.lunaPointsData.walletAddress, ordinalsAddress: ordinalsAddress, quantity: qty }) }); const data = await response.json(); if (data.success) { statusEl.innerHTML = `Success! ${qty} NAWS queued for inscription.
Remaining points: ${data.remainingPoints}`; // Keep button disabled and refresh points display btn.textContent = 'REFRESHING...'; setTimeout(async () => { await loadPointsForWallet(window.lunaPointsData.walletAddress); btn.disabled = false; btn.style.opacity = '1'; btn.textContent = 'REDEEM FOR NAWS'; }, 2000); } else { statusEl.innerHTML = `Error: ${data.error}`; btn.disabled = false; btn.style.opacity = '1'; } } catch (error) { console.error('Spend Luna points error:', error); statusEl.innerHTML = `Error: ${error.message}`; btn.disabled = false; btn.style.opacity = '1'; } }; // Load points for a wallet address async function loadPointsForWallet(address) { try { document.getElementById('points-loading').style.display = 'block'; document.getElementById('points-display-section').style.display = 'none'; document.getElementById('points-error').style.display = 'none'; // Fetch both creator and collector points const response = await fetch(`/api/get-all-points?address=${encodeURIComponent(address)}`); const data = await response.json(); if (data.success) { let hasAnyPoints = false; // Show Creator Points if user has any if (data.creatorPoints && data.creatorPoints.collections && data.creatorPoints.collections.length > 0) { hasAnyPoints = true; const availablePoints = data.creatorPoints.availablePoints || data.creatorPoints.totalPoints; const spentPoints = data.creatorPoints.spentPoints || 0; document.getElementById('creator-points-available').textContent = availablePoints.toLocaleString(); document.getElementById('creator-points-total').textContent = data.creatorPoints.totalPoints.toLocaleString(); document.getElementById('creator-points-spent').textContent = spentPoints.toLocaleString(); document.getElementById('creator-mint-count').textContent = data.creatorPoints.totalMints.toLocaleString(); // Store for spending window.creatorPointsData = { available: availablePoints, walletAddress: address }; // Show spend section if they have at least 25 points if (availablePoints >= 25) { document.getElementById('spend-points-section').style.display = 'block'; updateSpendQtyOptions(availablePoints); } else { document.getElementById('spend-points-section').style.display = 'none'; } // Show redemption history if any if (data.creatorPoints.redemptions && data.creatorPoints.redemptions.length > 0) { document.getElementById('redemption-history-section').style.display = 'block'; const historyList = document.getElementById('redemption-history-list'); historyList.innerHTML = data.creatorPoints.redemptions.map(r => `
-${r.points_spent} pts → ${r.original_name || r.asset_filename || 'NAWS'} ${new Date(r.created_at).toLocaleDateString()}
`).join(''); } else { document.getElementById('redemption-history-section').style.display = 'none'; } const collectionsList = document.getElementById('creator-collections-list'); collectionsList.innerHTML = data.creatorPoints.collections.map(collection => { // Determine image display based on mint type let imageHTML = ''; // For inscribe-on-demand, prioritize collection image if ((collection.mintType === 'inscribe_on_demand' || collection.mintType === 'parent_child_prints') && collection.imageFilename) { // Check if it's an HTML file if (collection.imageFilename.toLowerCase().endsWith('.html')) { imageHTML = ``; } else { imageHTML = `${collection.title}`; } } else if (collection.parentInscriptionId) { // For delegate/pre-inscribed, show parent inscription if (collection.parentType === 'recursive') { imageHTML = `
`; } else { imageHTML = `${collection.title}`; } } else { imageHTML = `
No Image
`; } return `
${imageHTML}

${collection.title}

by ${collection.artist}

${collection.minted} mints

${collection.points} points

`; }).join(''); document.getElementById('creator-points-section').style.display = 'block'; } // Show Luna Points if user has any if (data.lunaPoints && data.lunaPoints.mints && data.lunaPoints.mints.length > 0) { hasAnyPoints = true; const spentLunaPoints = data.lunaPoints.spentPoints || 0; const availableLunaPoints = data.lunaPoints.totalPoints - spentLunaPoints; // Store Luna points data globally window.lunaPointsData = { walletAddress: address, total: data.lunaPoints.totalPoints, spent: spentLunaPoints, available: availableLunaPoints }; document.getElementById('luna-points-total').textContent = data.lunaPoints.totalPoints.toLocaleString(); document.getElementById('luna-mint-count').textContent = data.lunaPoints.totalMints.toLocaleString(); // Show available/spent if any spent if (spentLunaPoints > 0) { document.getElementById('luna-points-available').textContent = availableLunaPoints.toLocaleString(); document.getElementById('luna-points-spent').textContent = spentLunaPoints.toLocaleString(); document.getElementById('luna-points-available-display').style.display = 'block'; } // Show spend section if user has 25+ available points if (availableLunaPoints >= 25) { document.getElementById('luna-spend-section').style.display = 'block'; // Update quantity options based on available points const maxQty = Math.floor(availableLunaPoints / 25); const qtySelect = document.getElementById('spend-luna-points-qty'); const options = [ { value: 1, pts: 25 }, { value: 2, pts: 50 }, { value: 3, pts: 75 }, { value: 5, pts: 125 }, { value: 10, pts: 250 } ].filter(opt => opt.value <= maxQty); qtySelect.innerHTML = options .map(opt => ``) .join(''); } const mintsList = document.getElementById('luna-mints-list'); mintsList.innerHTML = data.lunaPoints.mints.map(mint => { // Determine image display based on mint type let imageHTML = ''; // For inscribe-on-demand, prioritize collection image if ((mint.mintType === 'inscribe_on_demand' || mint.mintType === 'parent_child_prints') && mint.imageFilename) { // Check if it's an HTML file if (mint.imageFilename.toLowerCase().endsWith('.html')) { imageHTML = ``; } else { imageHTML = `${mint.title}`; } } else if (mint.parentInscriptionId) { // For delegate/pre-inscribed, show parent inscription if (mint.parentType === 'recursive') { imageHTML = `
`; } else { imageHTML = `${mint.title}`; } } else { imageHTML = `
No Image
`; } return `
${imageHTML}

${mint.title}

by ${mint.artist}

${mint.count} mint${mint.count > 1 ? 's' : ''}

${mint.points} points

`; }).join(''); document.getElementById('luna-points-section').style.display = 'block'; } if (hasAnyPoints) { document.getElementById('points-loading').style.display = 'none'; document.getElementById('points-display-section').style.display = 'block'; } else { document.getElementById('points-loading').style.display = 'none'; document.getElementById('points-error').style.display = 'block'; } } else { document.getElementById('points-loading').style.display = 'none'; document.getElementById('points-error').style.display = 'block'; } } catch (error) { console.error('Error loading points:', error); document.getElementById('points-loading').style.display = 'none'; document.getElementById('points-error').style.display = 'block'; } } function showAccountPage() { currentPage = 'account'; const template = document.getElementById('account-template'); const app = document.getElementById('app'); app.innerHTML = template.innerHTML; setupAccountPage(); } function setupAccountPage() { // Populate account information if (currentUser) { document.getElementById('account-username').textContent = currentUser.username; document.getElementById('account-email').textContent = currentUser.email; document.getElementById('account-role').textContent = currentUser.role; // Show admin section if user is admin const adminSection = document.getElementById('admin-account-section'); if (adminSection) { adminSection.style.display = currentUser.role === 'admin' ? 'block' : 'none'; } } } const ADMIN_PANEL_FRAGMENT_VERSION = '20260518-hide'; let adminPanelHtmlCache = null; async function loadAdminPanelHtml() { if (adminPanelHtmlCache) return adminPanelHtmlCache; try { const r = await fetch(`/admin-panel-fragment.html?v=${ADMIN_PANEL_FRAGMENT_VERSION}`, { credentials: 'include' }); if (!r.ok) throw new Error(String(r.status)); adminPanelHtmlCache = await r.text(); } catch (e) { console.error('Failed to load admin panel:', e); adminPanelHtmlCache = '

Could not load admin panel. Check your connection and refresh.

'; } return adminPanelHtmlCache; } async function showAdminPage() { currentPage = 'admin'; const app = document.getElementById('app'); if (!app) return; app.innerHTML = '

Loading admin…

'; const html = await loadAdminPanelHtml(); app.innerHTML = html; setupAdminPage(); try { await ensureAdminSensitiveModals(); } catch (e) { console.warn('Admin modals fragment failed to load', e); } } // Setup login page function setupLoginPage() { const loginBtn = document.getElementById('loginBtn'); if (loginBtn) { loginBtn.onclick = handleLogin; } // Add Enter key submission const inputs = ['loginEmail', 'loginPassword']; inputs.forEach(id => { const input = document.getElementById(id); if (input) { input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleLogin(); } }); } }); } // Setup register page function setupRegisterPage() { const registerBtn = document.getElementById('registerBtn'); if (registerBtn) { registerBtn.onclick = handleRegister; } // Add Enter key submission const inputs = ['registerEmail', 'registerUsername', 'registerPassword']; inputs.forEach(id => { const input = document.getElementById(id); if (input) { input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleRegister(); } }); } }); } // Handle login async function handleLogin() { const email = document.getElementById('loginEmail').value; const password = document.getElementById('loginPassword').value; if (!email || !password) { alert('Please enter email and password'); return; } try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, password }) }); const data = await response.json(); const resultDiv = document.getElementById('loginResult'); if (data.success) { currentUser = data.user; sessionId = data.sessionId; updateAuthUI(); resultDiv.innerHTML = '

Login successful!

'; setTimeout(() => { window.location.hash = '#home'; }, 1000); } else { resultDiv.innerHTML = `

${data.error}

`; } } catch (error) { document.getElementById('loginResult').innerHTML = '

Login failed

'; } } // Handle registration async function handleRegister() { const email = document.getElementById('registerEmail').value; const username = document.getElementById('registerUsername').value; const password = document.getElementById('registerPassword').value; if (!email || !username || !password) { alert('Please fill in all fields'); return; } try { const response = await fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, username, password }) }); const data = await response.json(); const resultDiv = document.getElementById('registerResult'); if (data.success) { currentUser = data.user; sessionId = data.sessionId; updateAuthUI(); resultDiv.innerHTML = '

Account created successfully!

'; setTimeout(() => { window.location.hash = '#home'; }, 1000); } else { resultDiv.innerHTML = `

${data.error}

`; } } catch (error) { document.getElementById('registerResult').innerHTML = '

Registration failed

'; } } // Batch upload function to prevent resource exhaustion async function uploadFilesBatch(files, uploadType, batchSize = 5) { const results = []; const total = files.length; let completed = 0; // Update progress const updateProgress = (current, total, status = '') => { const progressBar = document.getElementById('upload-progress-bar'); const progressText = document.getElementById('upload-progress-text'); if (progressBar) { progressBar.style.width = `${(current / total) * 100}%`; } if (progressText) { progressText.textContent = `${status} ${current}/${total} files uploaded`; } }; // Process files in batches for (let i = 0; i < files.length; i += batchSize) { const batch = files.slice(i, i + batchSize); // Upload batch in parallel const batchPromises = batch.map(async (file, batchIndex) => { const globalIndex = i + batchIndex; const formData = new FormData(); formData.append('image', file); formData.append('type', uploadType); formData.append('assetIndex', globalIndex.toString()); // Retry logic for failed uploads let attempts = 0; const maxRetries = 3; while (attempts < maxRetries) { try { const response = await fetch('/api/upload-collection-image', { method: 'POST', credentials: 'include', body: formData }); if (response.ok) { const result = await response.json(); return { ...result, originalIndex: globalIndex, originalFilename: file.name, attempts: attempts + 1 }; } else { // Server error - try to get error details let errorMessage = `HTTP ${response.status}`; try { const errorData = await response.json(); errorMessage = errorData.error || errorMessage; } catch (e) { // Couldn't parse error response } if (attempts < maxRetries - 1) { console.warn(`Upload attempt ${attempts + 1} failed for ${file.name}: ${errorMessage}. Retrying...`); attempts++; await new Promise(resolve => setTimeout(resolve, 1000 * attempts)); // Exponential backoff continue; } else { return { success: false, error: errorMessage, originalIndex: globalIndex, originalFilename: file.name, attempts: attempts + 1 }; } } } catch (networkError) { if (attempts < maxRetries - 1) { console.warn(`Network error attempt ${attempts + 1} for ${file.name}: ${networkError.message}. Retrying...`); attempts++; await new Promise(resolve => setTimeout(resolve, 1000 * attempts)); // Exponential backoff continue; } else { return { success: false, error: networkError.message, originalIndex: globalIndex, originalFilename: file.name, attempts: attempts + 1 }; } } } }); // Wait for batch to complete const batchResults = await Promise.all(batchPromises); results.push(...batchResults); // Update progress completed += batch.length; updateProgress(completed, total, 'Uploading...'); // Small delay between batches to prevent overwhelming the server if (i + batchSize < files.length) { await new Promise(resolve => setTimeout(resolve, 500)); } } updateProgress(total, total, 'Upload complete!'); return results; } // Batch upload function with custom asset indices async function uploadFilesBatchWithIndex(files, uploadType, batchSize = 5) { const results = []; const total = files.length; let completed = 0; // Update progress const updateProgress = (current, total, status = '') => { const progressBar = document.getElementById('upload-progress-bar'); const progressText = document.getElementById('upload-progress-text'); if (progressBar) { progressBar.style.width = `${(current / total) * 100}%`; } if (progressText) { progressText.textContent = `${status} ${current}/${total} files uploaded`; } }; // Process files in batches for (let i = 0; i < files.length; i += batchSize) { const batch = files.slice(i, i + batchSize); // Upload batch in parallel const batchPromises = batch.map(async (file) => { const formData = new FormData(); formData.append('image', file); formData.append('type', uploadType); formData.append('assetIndex', file.assetIndex.toString()); // Retry logic for failed uploads let attempts = 0; const maxRetries = 3; while (attempts < maxRetries) { try { const response = await fetch('/api/upload-collection-image', { method: 'POST', credentials: 'include', body: formData }); if (response.ok) { const result = await response.json(); return { ...result, originalIndex: file.assetIndex, originalFilename: file.name, attempts: attempts + 1 }; } else { // Server error - try to get error details let errorMessage = `HTTP ${response.status}`; try { const errorData = await response.json(); errorMessage = errorData.error || errorMessage; } catch (e) { // Couldn't parse error response } if (attempts < maxRetries - 1) { console.warn(`Upload attempt ${attempts + 1} failed for ${file.name}: ${errorMessage}. Retrying...`); attempts++; await new Promise(resolve => setTimeout(resolve, 1000 * attempts)); // Exponential backoff continue; } else { return { success: false, error: errorMessage, originalIndex: file.assetIndex, originalFilename: file.name, attempts: attempts + 1 }; } } } catch (networkError) { if (attempts < maxRetries - 1) { console.warn(`Network error attempt ${attempts + 1} for ${file.name}: ${networkError.message}. Retrying...`); attempts++; await new Promise(resolve => setTimeout(resolve, 1000 * attempts)); // Exponential backoff continue; } else { return { success: false, error: networkError.message, originalIndex: file.assetIndex, originalFilename: file.name, attempts: attempts + 1 }; } } } }); // Wait for batch to complete const batchResults = await Promise.all(batchPromises); results.push(...batchResults); // Update progress completed += batch.length; updateProgress(completed, total, 'Uploading...'); // Small delay between batches to prevent overwhelming the server if (i + batchSize < files.length) { await new Promise(resolve => setTimeout(resolve, 500)); } } updateProgress(total, total, 'Upload complete!'); return results; } // Batch save collection assets to avoid Cloudflare Workers subrequest limits async function saveCollectionAssetsBatch(artworkSlug, assetFilenames, batchSize = 20) { const total = assetFilenames.length; let completed = 0; const allResults = []; const allErrors = []; // Update progress const updateProgress = (current, total, status = '') => { const progressText = document.getElementById('upload-progress-text'); if (progressText) { progressText.textContent = `${status} ${current}/${total} assets saved to collection`; } }; // Process assets in batches for (let i = 0; i < assetFilenames.length; i += batchSize) { const batch = assetFilenames.slice(i, i + batchSize); try { const response = await fetch('/api/save-collection-assets', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ artworkSlug: artworkSlug, assetFilenames: batch }) }); const batchResult = await response.json(); if (batchResult.success) { allResults.push(...(batchResult.results || [])); completed += batch.length; updateProgress(completed, total, 'Saving...'); } else { console.error(`Batch save failed for batch ${Math.floor(i/batchSize) + 1}:`, batchResult); console.error('Detailed errors:', batchResult.errorDetails); allErrors.push({ batch: Math.floor(i/batchSize) + 1, filenames: batch, error: batchResult.error || batchResult.message || 'Unknown error', details: batchResult.errorDetails }); } } catch (error) { console.error(`Network error saving batch ${Math.floor(i/batchSize) + 1}:`, error); allErrors.push({ batch: Math.floor(i/batchSize) + 1, filenames: batch, error: error.message }); } // Small delay between batches if (i + batchSize < assetFilenames.length) { await new Promise(resolve => setTimeout(resolve, 500)); } } updateProgress(total, total, 'Save complete!'); return { success: allErrors.length === 0, results: allResults, errors: allErrors, totalSaved: allResults.length, totalFailed: total - allResults.length }; } // Setup submit page functionality function setupSubmitPage() { const submitBtn = document.getElementById('submitArtworkBtn'); if (submitBtn) { submitBtn.onclick = submitArtwork; } // Add Enter key submission for submit form const inputs = ['submitArtistInput', 'submitTitleInput', 'submitDescriptionInput', 'submitPriceInput', 'submitMaxMintsInput', 'submitParentIdInput', 'submitTwitterInput', 'submitDiscordInput', 'submitWebsiteInput', 'submitPaymentWalletInput', 'submitMintStartInput', 'submitMintEndInput']; inputs.forEach(id => { const input = document.getElementById(id); if (input) { input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); submitArtwork(); } }); } }); // Set up collection form const collectionBtn = document.getElementById('submitCollectionBtn'); if (collectionBtn) { collectionBtn.onclick = submitCollection; } // Set up pre-inscribed collection form const preInscribedBtn = document.getElementById('submitPreInscribedBtn'); if (preInscribedBtn) { preInscribedBtn.onclick = submitPreInscribedCollection; } // Set up pre-inscribed image preview const preInscribedImageInput = document.getElementById('preInscribedImageInput'); if (preInscribedImageInput) { preInscribedImageInput.addEventListener('change', handlePreInscribedImageUpload); } // Set up inscribe-on-demand collection form const onDemandBtn = document.getElementById('submitOnDemandBtn'); if (onDemandBtn) { onDemandBtn.onclick = submitOnDemandCollection; } // Set up on-demand image previews const onDemandCoverInput = document.getElementById('onDemandCoverImageInput'); if (onDemandCoverInput) { onDemandCoverInput.addEventListener('change', handleOnDemandCoverUpload); } const onDemandAssetsInput = document.getElementById('onDemandAssetsInput'); if (onDemandAssetsInput) { onDemandAssetsInput.addEventListener('change', handleOnDemandAssetsUpload); } // Set up add assets input const addAssetsInput = document.getElementById('addAssetsInput'); if (addAssetsInput) { addAssetsInput.addEventListener('change', handleAddAssetsUpload); } // Set up auction form const auctionBtn = document.getElementById('submitAuctionBtn'); if (auctionBtn) { auctionBtn.onclick = submitAuction; } // Initialize with art print tab active switchSubmitTab('art-print'); const submitPcpBtn = document.getElementById('submitPcpPrintBtn'); if (submitPcpBtn) { submitPcpBtn.onclick = function() { submitOnDemandCollection('parent_child_prints'); }; } } window.switchArtPrintSubtab = function(which) { const std = document.getElementById('art-print-standard-section'); const pcp = document.getElementById('art-print-parent-child-section'); const btnStd = document.getElementById('art-print-subtab-standard'); const btnPcp = document.getElementById('art-print-subtab-pcp'); if (!std || !pcp) return; if (which === 'parent-child') { std.style.display = 'none'; pcp.style.display = 'block'; if (btnStd) { btnStd.style.background = '#000'; btnStd.style.color = '#00ff88'; btnStd.style.borderColor = '#00aa55'; } if (btnPcp) { btnPcp.style.background = '#fff'; btnPcp.style.color = '#000'; btnPcp.style.borderColor = 'var(--neon-green)'; } } else { std.style.display = 'block'; pcp.style.display = 'none'; if (btnStd) { btnStd.style.background = '#fff'; btnStd.style.color = '#000'; btnStd.style.borderColor = 'var(--neon-green)'; } if (btnPcp) { btnPcp.style.background = '#000'; btnPcp.style.color = '#00ff88'; btnPcp.style.borderColor = '#00aa55'; } } }; // Switch between submit tabs window.switchSubmitTab = function(tabName) { const artPrintTab = document.getElementById('art-print-tab'); const inscribeOnDemandTab = document.getElementById('inscribe-on-demand-tab'); const preInscribedTab = document.getElementById('pre-inscribed-tab'); const collectionTab = document.getElementById('collection-tab'); const auctionTab = document.getElementById('auction-tab'); const artPrintForm = document.getElementById('art-print-form'); const inscribeOnDemandForm = document.getElementById('inscribe-on-demand-form'); const preInscribedForm = document.getElementById('pre-inscribed-form'); const collectionForm = document.getElementById('collection-form'); const auctionForm = document.getElementById('auction-form'); if (!artPrintTab || !inscribeOnDemandTab || !preInscribedTab || !collectionTab || !artPrintForm || !inscribeOnDemandForm || !preInscribedForm || !collectionForm) { return; } // Reset all tabs artPrintTab.style.background = '#000000'; artPrintTab.style.color = '#ffffff'; inscribeOnDemandTab.style.background = '#000000'; inscribeOnDemandTab.style.color = '#ffffff'; preInscribedTab.style.background = '#000000'; preInscribedTab.style.color = '#ffffff'; collectionTab.style.background = '#000000'; collectionTab.style.color = '#ffffff'; if (auctionTab) { auctionTab.style.background = '#000000'; auctionTab.style.color = '#ffffff'; } artPrintForm.style.display = 'none'; inscribeOnDemandForm.style.display = 'none'; preInscribedForm.style.display = 'none'; collectionForm.style.display = 'none'; if (auctionForm) auctionForm.style.display = 'none'; if (tabName === 'art-print') { artPrintTab.style.background = '#ffffff'; artPrintTab.style.color = '#000000'; artPrintForm.style.display = 'block'; if (typeof switchArtPrintSubtab === 'function') { switchArtPrintSubtab('standard'); } } else if (tabName === 'inscribe-on-demand') { inscribeOnDemandTab.style.background = '#ffffff'; inscribeOnDemandTab.style.color = '#000000'; inscribeOnDemandForm.style.display = 'block'; } else if (tabName === 'pre-inscribed') { preInscribedTab.style.background = '#ffffff'; preInscribedTab.style.color = '#000000'; preInscribedForm.style.display = 'block'; } else if (tabName === 'collection') { collectionTab.style.background = '#ffffff'; collectionTab.style.color = '#000000'; collectionForm.style.display = 'block'; } else if (tabName === 'auction') { if (auctionTab) { auctionTab.style.background = '#ff6b00'; auctionTab.style.color = '#000000'; } if (auctionForm) auctionForm.style.display = 'block'; } } // Toggle between static and recursive parent types window.toggleSubmitParentType = function() { const staticSection = document.getElementById('submitStaticParentSection'); const recursiveSection = document.getElementById('submitRecursiveParentSection'); const selectedType = document.querySelector('input[name="submitParentType"]:checked').value; if (selectedType === 'static') { staticSection.style.display = 'block'; recursiveSection.style.display = 'none'; } else { staticSection.style.display = 'none'; recursiveSection.style.display = 'block'; } } // Toggle between static and recursive parent types in admin modal window.toggleAdminParentType = function() { const staticSection = document.getElementById('adminStaticParentSection'); const recursiveSection = document.getElementById('adminRecursiveParentSection'); const selectedType = document.querySelector('input[name="adminParentType"]:checked').value; if (selectedType === 'static') { staticSection.style.display = 'block'; recursiveSection.style.display = 'none'; } else { staticSection.style.display = 'none'; recursiveSection.style.display = 'block'; } } // Toggle card pack section visibility window.toggleCardPackSection = function() { const cardPackSection = document.getElementById('cardPackSection'); const cardPackCheckbox = document.getElementById('adminCardPackInput'); if (cardPackCheckbox && cardPackCheckbox.checked) { if (cardPackSection) { cardPackSection.style.display = 'block'; } } else { if (cardPackSection) { cardPackSection.style.display = 'none'; } } } // Add a new pack card input row let packCardCounter = 0; window.addPackCard = function() { const container = document.getElementById('packCardsContainer'); if (!container) return; const cardDiv = document.createElement('div'); cardDiv.id = `packCard-${packCardCounter}`; cardDiv.style.cssText = 'margin-bottom: 15px; padding: 15px; background: #111; border: 1px solid #666;'; cardDiv.innerHTML = `
Card #${packCardCounter + 1}
`; container.appendChild(cardDiv); packCardCounter++; } // Remove a pack card window.removePackCard = function(index) { const cardDiv = document.getElementById(`packCard-${index}`); if (cardDiv) { cardDiv.remove(); } } // Get pack cards data (for admin panel) window.getPackCardsData = function() { const cards = []; let cardIndex = 0; while (true) { const cardDiv = document.getElementById(`packCard-${cardIndex}`); if (!cardDiv) break; const inscription = document.getElementById(`cardInscription-${cardIndex}`).value; const rarity = parseFloat(document.getElementById(`cardRarity-${cardIndex}`).value); if (inscription && !isNaN(rarity) && rarity >= 0 && rarity <= 100) { cards.push({ inscription_id: inscription, rarity_percentage: rarity }); } cardIndex++; } return cards; } // Helper function to read file as base64 async function readFileAsBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result.split(',')[1]); // Remove data:image/xxx;base64, prefix reader.onerror = reject; reader.readAsDataURL(file); }); } // Submit collection inquiry async function submitCollection() { const name = document.getElementById('collectionNameInput').value; const title = document.getElementById('collectionTitleInput').value; const email = document.getElementById('collectionEmailInput').value; const contactMethod = document.getElementById('collectionContactMethodInput').value; const contactHandle = document.getElementById('collectionContactInput').value; const details = document.getElementById('collectionDetailsInput').value; if (!name || !title || !email || !contactMethod || !details) { alert('Please fill in all required fields'); return; } // Basic email validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { alert('Please enter a valid email address'); return; } try { const response = await fetch('/api/submit-collection', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ name, title, email, contactMethod, contactHandle, details }) }); const data = await response.json(); const resultDiv = document.getElementById('collectionResult'); if (data.success) { resultDiv.innerHTML = `

Collection Inquiry Submitted!

Thank you for your interest in building a collection with LunaLauncher.

We'll review your submission and contact you within 24-48 hours via your preferred method.

`; // Clear form document.getElementById('collectionNameInput').value = ''; document.getElementById('collectionTitleInput').value = ''; document.getElementById('collectionEmailInput').value = ''; document.getElementById('collectionContactMethodInput').value = ''; document.getElementById('collectionContactInput').value = ''; document.getElementById('collectionDetailsInput').value = ''; } else { resultDiv.innerHTML = `

Submission Failed: ${data.error}

`; } } catch (error) { console.error('Collection submission error:', error); document.getElementById('collectionResult').innerHTML = `

Submission Failed: ${error.message || 'Network error'}

`; } } // Handle pre-inscribed collection cover upload and preview (images or HTML) function handlePreInscribedImageUpload(event) { const file = event.target.files[0]; const preview = document.getElementById('preInscribedImagePreview'); const previewImage = document.getElementById('preInscribedPreviewImage'); const previewIframe = document.getElementById('preInscribedPreviewIframe'); const fileSize = document.getElementById('preInscribedFileSize'); if (window.__preInscribedCoverBlobUrl) { try { URL.revokeObjectURL(window.__preInscribedCoverBlobUrl); } catch (e) {} window.__preInscribedCoverBlobUrl = null; } if (!file) { if (preview) preview.style.display = 'none'; if (previewImage) { previewImage.style.display = 'none'; previewImage.src = ''; } if (previewIframe) { previewIframe.style.display = 'none'; previewIframe.src = 'about:blank'; } return; } const lower = (file.name || '').toLowerCase(); const isHtml = file.type === 'text/html' || file.type === 'application/xhtml+xml' || lower.endsWith('.html') || lower.endsWith('.htm'); const validImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/avif']; const isImage = validImageTypes.includes(file.type); if (!isHtml && !isImage) { alert('Please select a valid image (JPG, PNG, GIF, WebP, AVIF) or an HTML cover file (.html / .htm).'); event.target.value = ''; preview.style.display = 'none'; return; } const maxSize = isHtml ? (1 * 1024 * 1024) : (5 * 1024 * 1024); if (file.size > maxSize) { alert(isHtml ? 'HTML cover must be smaller than 1MB. Reduce file size and try again.' : 'Image file must be smaller than 5MB. Please compress your image and try again.'); event.target.value = ''; preview.style.display = 'none'; return; } fileSize.textContent = (file.size / 1024).toFixed(1) + ' KB'; preview.style.display = 'block'; if (isHtml) { previewImage.style.display = 'none'; previewImage.src = ''; const blobUrl = URL.createObjectURL(file); window.__preInscribedCoverBlobUrl = blobUrl; previewIframe.style.display = 'block'; previewIframe.src = blobUrl; } else { previewIframe.style.display = 'none'; previewIframe.src = 'about:blank'; previewImage.style.display = 'block'; const reader = new FileReader(); reader.onload = function(e) { previewImage.src = e.target.result; }; reader.readAsDataURL(file); } } // Handle on-demand cover image upload and preview function handleOnDemandCoverUpload(event) { const file = event.target.files[0]; const preview = document.getElementById('onDemandCoverPreview'); const previewImage = document.getElementById('onDemandCoverPreviewImage'); const fileSize = document.getElementById('onDemandCoverFileSize'); if (!file) { preview.style.display = 'none'; return; } // Validate file type const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; if (!validTypes.includes(file.type)) { alert('Please select a valid image file (JPG, PNG, GIF, or WebP)'); event.target.value = ''; preview.style.display = 'none'; return; } // Validate file size (5MB limit for cover) const maxSize = 5 * 1024 * 1024; // 5MB if (file.size > maxSize) { alert('Cover image must be smaller than 5MB. Please compress your image and try again.'); event.target.value = ''; preview.style.display = 'none'; return; } // Show preview const reader = new FileReader(); reader.onload = function(e) { previewImage.src = e.target.result; fileSize.textContent = (file.size / 1024).toFixed(1) + ' KB'; preview.style.display = 'block'; }; reader.readAsDataURL(file); } // Handle on-demand collection assets upload and preview function handleOnDemandAssetsUpload(event) { const files = Array.from(event.target.files); const preview = document.getElementById('onDemandAssetsPreview'); const grid = document.getElementById('onDemandAssetsGrid'); const assetCount = document.getElementById('onDemandAssetCount'); const totalSize = document.getElementById('onDemandTotalSize'); const maxMintsInput = document.getElementById('onDemandMaxMintsInput'); if (!files || files.length === 0) { preview.style.display = 'none'; maxMintsInput.value = ''; return; } // Validate files (images and HTML) const validImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/avif']; const validHtmlTypes = ['text/html', 'application/octet-stream']; // Some browsers send HTML as octet-stream const validFiles = []; let totalSizeBytes = 0; for (const file of files) { const isImage = validImageTypes.includes(file.type); const isHtml = validHtmlTypes.includes(file.type) || file.name.toLowerCase().endsWith('.html'); if (!isImage && !isHtml) { alert(`File "${file.name}" is not a valid file type. Only JPG, PNG, GIF, WebP, AVIF images and HTML files are allowed.`); continue; } // Different size limits for images vs HTML const maxFileSize = isHtml ? 1 * 1024 * 1024 : 5 * 1024 * 1024; // 1MB for HTML, 5MB for images if (file.size > maxFileSize) { const sizeLimit = isHtml ? '1MB' : '5MB'; alert(`File "${file.name}" is too large (${(file.size / 1024 / 1024).toFixed(1)} MB). ${isHtml ? 'HTML files' : 'Images'} must be smaller than ${sizeLimit}.`); continue; } validFiles.push(file); totalSizeBytes += file.size; } if (validFiles.length === 0) { preview.style.display = 'none'; maxMintsInput.value = ''; return; } // Update counts assetCount.textContent = validFiles.length; totalSize.textContent = (totalSizeBytes / 1024).toFixed(1) + ' KB'; maxMintsInput.value = validFiles.length; // Show previews grid.innerHTML = ''; validFiles.forEach((file, index) => { const assetDiv = document.createElement('div'); assetDiv.style.cssText = 'text-align: center; padding: 5px;'; const isHtml = file.type === 'text/html' || file.name.toLowerCase().endsWith('.html'); if (isHtml) { // HTML file preview assetDiv.innerHTML = `
<HTML>

${file.name.length > 12 ? file.name.substring(0, 12) + '...' : file.name}

`; grid.appendChild(assetDiv); } else { // Image file preview const reader = new FileReader(); reader.onload = function(e) { assetDiv.innerHTML = ` Asset ${index + 1}

${file.name.length > 12 ? file.name.substring(0, 12) + '...' : file.name}

`; grid.appendChild(assetDiv); }; reader.readAsDataURL(file); } }); preview.style.display = 'block'; } // Submit inscribe-on-demand collection for review (IoD tab), or parent–child from Art Print sub-tab async function submitOnDemandCollection(mintTypeOverride) { const selectedMintType = mintTypeOverride === 'parent_child_prints' ? 'parent_child_prints' : 'inscribe_on_demand'; const isPcp = selectedMintType === 'parent_child_prints'; const resultDiv = document.getElementById(isPcp ? 'pcpPrintSubmitResult' : 'onDemandResult'); const artist = (document.getElementById(isPcp ? 'pcpSubmitArtistInput' : 'onDemandArtistInput') || {}).value?.trim() || ''; const title = (document.getElementById(isPcp ? 'pcpSubmitTitleInput' : 'onDemandTitleInput') || {}).value?.trim() || ''; const description = (document.getElementById(isPcp ? 'pcpSubmitDescriptionInput' : 'onDemandDescriptionInput') || {}).value?.trim() || ''; const priceEl = document.getElementById(isPcp ? 'pcpSubmitPriceInput' : 'onDemandPriceInput'); const price = parseInt(priceEl && priceEl.value, 10) || 0; const website = (document.getElementById(isPcp ? 'pcpSubmitWebsiteInput' : 'onDemandWebsiteInput') || {}).value || ''; const twitter = (document.getElementById(isPcp ? 'pcpSubmitTwitterInput' : 'onDemandTwitterInput') || {}).value || ''; const discord = (document.getElementById(isPcp ? 'pcpSubmitDiscordInput' : 'onDemandDiscordInput') || {}).value || ''; const paymentWallet = (document.getElementById(isPcp ? 'pcpSubmitPaymentWalletInput' : 'onDemandPaymentWalletInput') || {}).value || ''; const mintStart = (document.getElementById(isPcp ? 'pcpSubmitMintStartInput' : 'onDemandMintStartInput') || {}).value || ''; const mintEnd = (document.getElementById(isPcp ? 'pcpSubmitMintEndInput' : 'onDemandMintEndInput') || {}).value || ''; const physicalPrint = false; const enablePreview = isPcp ? false : document.getElementById('onDemandEnablePreviewInput').checked; const enableGallery = isPcp ? false : document.getElementById('onDemandEnableGalleryInput').checked; const isPixelArt = isPcp ? false : document.getElementById('onDemandPixelArtInput').checked; const coverInput = !isPcp ? document.getElementById('onDemandCoverImageInput') : null; const coverImageFile = coverInput && coverInput.files && coverInput.files[0]; const assetFiles = isPcp ? [] : Array.from(document.getElementById('onDemandAssetsInput').files || []); let pcpMaxMints = 0; if (isPcp) { const maxSupplyEl = document.getElementById('pcpSubmitMaxSupplyInput'); pcpMaxMints = parseInt(maxSupplyEl && maxSupplyEl.value, 10); if (!maxSupplyEl || !pcpMaxMints || pcpMaxMints < 1) { alert('Enter max supply (at least 1 edition).'); return; } } if (!artist || !title || !description) { alert('Please fill in all required fields (Artist, Title, and Description)'); return; } if (!isPcp && !coverImageFile) { alert('Please upload a collection cover image'); return; } if (!isPcp && (!assetFiles || assetFiles.length === 0)) { alert('Please upload at least one collection asset image'); return; } const mintStartDatetime = mintStart ? new Date(mintStart + 'Z').toISOString() : null; const mintEndDatetime = mintEnd ? new Date(mintEnd + 'Z').toISOString() : null; try { let parentChildPrintParentMode = null; let parentChildPrintParentImageFilename = null; let parentInscriptionForInsert = 'on-demand-placeholder'; let delegateInscriptionForInsert = null; let coverData; if (!isPcp) { const coverFormData = new FormData(); coverFormData.append('image', coverImageFile); coverFormData.append('type', 'collection'); const coverResponse = await fetch('/api/upload-collection-image', { method: 'POST', credentials: 'include', body: coverFormData }); coverData = await coverResponse.json(); if (!coverData.success) { alert('Failed to upload cover image: ' + coverData.error); return; } } else { const modeRadio = document.querySelector('input[name="pcpSubmitParentMode"]:checked'); parentChildPrintParentMode = modeRadio ? modeRadio.value : 'inscription'; if (parentChildPrintParentMode === 'inscription') { const pid = (document.getElementById('pcpSubmitParentInscriptionId') && document.getElementById('pcpSubmitParentInscriptionId').value.trim()) || ''; if (!pid) { alert('Enter the parent inscription ID (full inscription id ending with i0, etc.).'); return; } parentInscriptionForInsert = pid; const did = (document.getElementById('pcpSubmitDelegateInscriptionId') && document.getElementById('pcpSubmitDelegateInscriptionId').value.trim()) || ''; delegateInscriptionForInsert = did || pid; coverData = { success: true, filename: 'submitted.webp' }; } else { const pf = document.getElementById('pcpSubmitParentImageInput') && document.getElementById('pcpSubmitParentImageInput').files[0]; if (!pf) { alert('Please upload a parent image, or switch to inscription parent.'); return; } const pForm = new FormData(); pForm.append('image', pf); pForm.append('type', 'collection'); const pRes = await fetch('/api/upload-collection-image', { method: 'POST', credentials: 'include', body: pForm }); const pData = await pRes.json(); if (!pData.success) { alert('Failed to upload parent image: ' + (pData.error || 'unknown')); return; } parentChildPrintParentImageFilename = pData.filename; parentInscriptionForInsert = null; coverData = { success: true, filename: pData.filename }; } } let assetFilenames = []; if (!isPcp) { if (resultDiv) { resultDiv.innerHTML = `

Uploading ${assetFiles.length} collection assets...

Processing in batches to prevent errors.

Starting upload...

`; } const assetResults = await uploadFilesBatch(assetFiles, 'collection'); const failedUploads = assetResults.filter(result => !result.success); const successfulUploads = assetResults.filter(result => result.success); if (failedUploads.length > 0) { console.warn(`${failedUploads.length} uploads failed:`, failedUploads); if (successfulUploads.length === 0) { alert(`All ${failedUploads.length} asset uploads failed. Please check your connection and try again.`); return; } const proceed = confirm( `${failedUploads.length} out of ${assetFiles.length} assets failed to upload.\n\n` + `Successfully uploaded: ${successfulUploads.length} assets\n` + `Failed uploads: ${failedUploads.map(f => f.originalFilename || f.filename).join(', ')}\n\n` + `Do you want to proceed with the ${successfulUploads.length} successful uploads?\n\n` + `You can add the failed assets later using "Manage Assets".` ); if (!proceed) { return; } } assetFilenames = successfulUploads.map(result => result.filename); } const slug = `${artist}-${title}`.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); if (!slug || slug.length < 2) { alert('Artist and title must include enough letters or numbers to form a URL slug (try using Latin characters in the title).'); return; } const scriptInscriptionId = isPixelArt ? 'd686bf14aec7115b84cb9e85fe8d819d61217d115b8869c49694205ce3403a8ai0' : '5b0e2cf2d3d9a2aee10b5ae860487376badaa3fa4f2ac470fa25a4e3c1217c3ci0'; const maxMintsForApi = isPcp ? pcpMaxMints : assetFiles.length; const response = await fetch('/api/submit-artwork', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ slug, artist, title, description, price, maxMints: maxMintsForApi, parentType: 'static', scriptInscriptionId: scriptInscriptionId, twitter, discord, website, paymentWallet, physicalPrint, mintStartDatetime, mintEndDatetime, imageFilename: coverData.filename, mintType: selectedMintType, parentInscriptionId: parentInscriptionForInsert, delegateInscriptionId: delegateInscriptionForInsert, parentChildPrintParentMode: parentChildPrintParentMode, parentChildPrintParentImageFilename: parentChildPrintParentImageFilename, enablePreview: enablePreview, enableGallery: enableGallery, additionalDetails: JSON.stringify({ assetFilenames: assetFilenames, totalAssets: isPcp ? pcpMaxMints : assetFiles.length, coverImage: coverData.filename }) }) }); let data; const submitArtworkRaw = await response.text(); try { data = submitArtworkRaw ? JSON.parse(submitArtworkRaw) : {}; } catch (parseErr) { if (resultDiv) { resultDiv.innerHTML = `

Submission Failed (HTTP ${response.status})

The server did not return JSON. If this persists, try again or contact support.

`; } return; } if (data.success) { if (assetFilenames.length > 0) { if (resultDiv) { resultDiv.innerHTML = `

Saving ${assetFilenames.length} assets to collection...

Processing in batches to avoid API limits.

Starting save...

`; } await new Promise(resolve => setTimeout(resolve, 2000)); const assetsData = await saveCollectionAssetsBatch(data.slug, assetFilenames); if (assetsData.totalSaved > 0) { const successMessage = assetsData.success ? `Your collection "${title}" with ${assetsData.totalSaved} assets has been submitted for review.` : `Your collection "${title}" was created with ${assetsData.totalSaved} assets. ${assetsData.totalFailed} assets failed to save and can be added later.`; const isPcpSubmit = selectedMintType === 'parent_child_prints'; const successHeadline = isPcpSubmit ? 'Parent–Child Prints Collection Submitted!' : 'Inscribe on Demand Collection Created!'; const nextBullets = isPcpSubmit ? `
  • Admin review of your collection, parent source, and print slots
  • Once approved, paid mints queue delegate reveals in batches
  • Keep your parent inscription in the signer wallet if you used inscription parent
  • ` : `
  • Admin review of your collection and assets
  • Collection will be activated once approved
  • Users will mint random images from your collection
  • Each mint creates a unique inscription
  • `; if (resultDiv) { resultDiv.innerHTML = `

    ${successHeadline}

    ${successMessage}

    We'll review your submission and contact you within 24-48 hours.

    What happens next:

    `; } if (!isPcpSubmit) { document.getElementById('onDemandArtistInput').value = ''; document.getElementById('onDemandTitleInput').value = ''; document.getElementById('onDemandDescriptionInput').value = ''; document.getElementById('onDemandPriceInput').value = ''; document.getElementById('onDemandWebsiteInput').value = ''; document.getElementById('onDemandTwitterInput').value = ''; document.getElementById('onDemandDiscordInput').value = ''; document.getElementById('onDemandPaymentWalletInput').value = ''; document.getElementById('onDemandMintStartInput').value = ''; document.getElementById('onDemandMintEndInput').value = ''; document.getElementById('onDemandCoverImageInput').value = ''; document.getElementById('onDemandAssetsInput').value = ''; document.getElementById('onDemandMaxMintsInput').value = ''; document.getElementById('onDemandCoverPreview').style.display = 'none'; document.getElementById('onDemandAssetsPreview').style.display = 'none'; } else { const clearPcpAfterAssets = (id) => { const el = document.getElementById(id); if (el) el.value = ''; }; clearPcpAfterAssets('pcpSubmitArtistInput'); clearPcpAfterAssets('pcpSubmitTitleInput'); clearPcpAfterAssets('pcpSubmitDescriptionInput'); clearPcpAfterAssets('pcpSubmitPriceInput'); clearPcpAfterAssets('pcpSubmitMaxSupplyInput'); clearPcpAfterAssets('pcpSubmitWebsiteInput'); clearPcpAfterAssets('pcpSubmitTwitterInput'); clearPcpAfterAssets('pcpSubmitDiscordInput'); clearPcpAfterAssets('pcpSubmitPaymentWalletInput'); clearPcpAfterAssets('pcpSubmitMintStartInput'); clearPcpAfterAssets('pcpSubmitMintEndInput'); clearPcpAfterAssets('pcpSubmitParentInscriptionId'); clearPcpAfterAssets('pcpSubmitDelegateInscriptionId'); clearPcpAfterAssets('pcpSubmitParentImageInput'); const pcpModeInsc2 = document.querySelector('input[name="pcpSubmitParentMode"][value="inscription"]'); if (pcpModeInsc2) pcpModeInsc2.checked = true; } } else { const firstError = assetsData.errors[0]; const sampleErrors = firstError?.details?.slice(0, 3) || []; if (resultDiv) { resultDiv.innerHTML = `

    Collection Created but Assets Failed to Save

    Your collection "${title}" was created successfully, but none of the ${assetFilenames.length} assets could be saved to the collection.

    Error: ${firstError?.error || 'Unknown error'}

    ${sampleErrors.length > 0 ? `

    Sample asset errors:

    ` : ''}

    Please use the admin panel to add assets to your collection using "Manage Assets".

    `; } } } else if (isPcp) { const successMessage = `Your collection "${title}" is submitted with ${pcpMaxMints} edition slots. Add slot images later in Manage Assets if needed.`; if (resultDiv) { resultDiv.innerHTML = `

    Parent–Child Prints Collection Submitted!

    ${successMessage}

    We'll review your submission and contact you within 24-48 hours.

    What happens next:

    `; } const clearPcp = (id) => { const el = document.getElementById(id); if (el) el.value = ''; }; clearPcp('pcpSubmitArtistInput'); clearPcp('pcpSubmitTitleInput'); clearPcp('pcpSubmitDescriptionInput'); clearPcp('pcpSubmitPriceInput'); clearPcp('pcpSubmitMaxSupplyInput'); clearPcp('pcpSubmitWebsiteInput'); clearPcp('pcpSubmitTwitterInput'); clearPcp('pcpSubmitDiscordInput'); clearPcp('pcpSubmitPaymentWalletInput'); clearPcp('pcpSubmitMintStartInput'); clearPcp('pcpSubmitMintEndInput'); clearPcp('pcpSubmitParentInscriptionId'); clearPcp('pcpSubmitDelegateInscriptionId'); clearPcp('pcpSubmitParentImageInput'); const pcpModeInsc = document.querySelector('input[name="pcpSubmitParentMode"][value="inscription"]'); if (pcpModeInsc) pcpModeInsc.checked = true; } } else { if (resultDiv) { const detail = data.details ? `

    ${String(data.details).replace(/` : ''; resultDiv.innerHTML = `

    Submission Failed: ${data.error || 'Unknown error'}

    ${detail}
    `; } } } catch (error) { console.error('On-demand collection submission error:', error); const errDiv = document.getElementById(isPcp ? 'pcpPrintSubmitResult' : 'onDemandResult'); if (errDiv) { errDiv.innerHTML = `

    Submission Failed: ${error.message || 'Network error'}

    `; } } } // Submit pre-inscribed collection for review async function submitPreInscribedCollection() { const artist = document.getElementById('preInscribedSubmitArtistInput').value; const title = document.getElementById('preInscribedSubmitTitleInput').value; const description = document.getElementById('preInscribedSubmitDescriptionInput').value; const price = parseInt(document.getElementById('preInscribedSubmitPriceInput').value) || 0; const maxMints = parseInt(document.getElementById('preInscribedSubmitMaxMintsInput').value) || 100; const website = document.getElementById('preInscribedSubmitWebsiteInput').value; const twitter = document.getElementById('preInscribedSubmitTwitterInput').value; const discord = document.getElementById('preInscribedSubmitDiscordInput').value; const paymentWallet = document.getElementById('preInscribedSubmitPaymentWalletInput').value; const ordinalsWallet = document.getElementById('preInscribedOrdinalsWalletInput').value; const mintStart = document.getElementById('preInscribedSubmitMintStartInput').value; const mintEnd = document.getElementById('preInscribedSubmitMintEndInput').value; const physicalPrint = document.getElementById('preInscribedSubmitPhysicalPrintInput').checked; const isPixelArt = document.getElementById('preInscribedPixelArtInput').checked; const isNumerical = document.getElementById('preInscribedSubmitNumericalInput').checked; const details = document.getElementById('preInscribedSubmitDetailsInput').value; const imageFile = document.getElementById('preInscribedImageInput').files[0]; // Whitelist settings const whitelistEnabled = document.getElementById('preInscribedSubmitWhitelistEnabledInput').checked; const whitelistStart = document.getElementById('preInscribedSubmitWhitelistStartInput').value; const whitelistEnd = document.getElementById('preInscribedSubmitWhitelistEndInput').value; const whitelistCsvFile = document.getElementById('preInscribedSubmitWhitelistCsvInput').files[0]; // If using v2 with wallet, we don't need details or cover image (we'll fetch inscriptions) // If numerical type, we also don't need details or wallet const isV2 = !!ordinalsWallet; if (!artist || !title || !description) { alert('Please fill in Artist, Title, and Description'); return; } // For numerical collections, details and wallet are optional if (!isNumerical && !isV2 && !details) { alert('Please provide ordinal details or a wallet address with inscriptions'); return; } // For numerical collections, cover is still required (or provide wallet address for v2) if (!isNumerical && !isV2 && !imageFile) { alert('Please upload a collection cover (image or HTML) or provide a wallet address for v2'); return; } // For numerical collections, cover file is required if (isNumerical && !imageFile) { alert('Please upload a collection cover (image or HTML)'); return; } // Convert datetime-local values to UTC ISO strings const mintStartDatetime = mintStart ? new Date(mintStart + 'Z').toISOString() : null; const mintEndDatetime = mintEnd ? new Date(mintEnd + 'Z').toISOString() : null; // Generate slug from artist-title const slug = `${artist}-${title}`.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); try { // First, upload the image (required for numerical collections) let imageFilename = null; if (imageFile) { console.log('📤 Uploading pre-inscribed collection image:', imageFile.name); // Convert image to base64 const imageDataBase64 = await readFileAsBase64(imageFile); const imageResponse = await fetch('/api/upload-collection-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ imageData: imageDataBase64, filename: imageFile.name, uploadType: 'collection' }) }); const imageData = await imageResponse.json(); console.log('📤 Image upload response:', imageData); if (imageData.success) { imageFilename = imageData.filename; console.log('✅ Cover filename:', imageFilename); } else { alert('Failed to upload cover: ' + imageData.error); return; } } else if (isNumerical) { alert('Please upload a collection cover (image or HTML)'); return; } else { // For non-numerical, fallback to default imageFilename = 'pre-inscribed.webp'; } // Determine script ID based on pixel art setting const scriptInscriptionId = isPixelArt ? 'd686bf14aec7115b84cb9e85fe8d819d61217d115b8869c49694205ce3403a8ai0' // TODO: Replace with actual pixel art renderer inscription ID : '5b0e2cf2d3d9a2aee10b5ae860487376badaa3fa4f2ac470fa25a4e3c1217c3ci0'; // Determine mint type const mintType = isNumerical ? 'numerical' : 'pre_inscribed'; // Convert whitelist datetime const whitelistStartDatetime = whitelistStart ? new Date(whitelistStart + 'Z').toISOString() : null; const whitelistEndDatetime = whitelistEnd ? new Date(whitelistEnd + 'Z').toISOString() : null; // Prepare submission payload const submissionPayload = { slug, artist, title, description, price, maxMints, parentInscriptionId: 'pre-inscribed-placeholder', // Placeholder for pre-inscribed parentType: 'static', scriptInscriptionId: scriptInscriptionId, twitter, discord, website, paymentWallet, physicalPrint, mintStartDatetime, mintEndDatetime, imageFilename: imageFilename, mintType: mintType, whitelistEnabled: whitelistEnabled, whitelistStartDatetime: whitelistStartDatetime, whitelistEndDatetime: whitelistEndDatetime, additionalDetails: JSON.stringify({ ordinalsWallet: ordinalsWallet, ordinalDetails: details }) }; console.log('💾 Submitting pre-inscribed collection:', submissionPayload); // Then submit the collection const response = await fetch('/api/submit-artwork', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify(submissionPayload) }); const data = await response.json(); const resultDiv = document.getElementById('preInscribedResult'); if (data.success) { // If v2, auto-import inscriptions if (isV2) { resultDiv.innerHTML = '

    Importing inscriptions from wallet...

    '; const importResponse = await fetch('/api/import-and-setup-v2', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ artworkSlug: slug, walletAddress: ordinalsWallet }) }); const importData = await importResponse.json(); if (!importData.success) { resultDiv.innerHTML = `

    Collection Created but Import Failed:

    ${importData.error}

    You can manually import inscriptions from the admin panel.

    `; return; } // Handle whitelist upload if needed if (whitelistEnabled && whitelistCsvFile) { try { const reader = new FileReader(); reader.onload = async function(e) { const csvContent = e.target.result; const whitelistResponse = await fetch('/api/upload-whitelist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ artworkSlug: slug, csvData: csvContent, whitelistStartDatetime: whitelistStartDatetime, whitelistEndDatetime: whitelistEndDatetime }) }); const whitelistResult = await whitelistResponse.json(); if (whitelistResult.success) { resultDiv.innerHTML = `

    ✅ Collection Created & Inscriptions Imported!

    Collection "${title}" has been created successfully.

    Imported: ${importData.imported} inscription(s)

    Whitelist: ${whitelistResult.entriesProcessed} entries uploaded

    Your collection will be reviewed before going live.

    `; } else { resultDiv.innerHTML = `

    ✅ Collection Created & Inscriptions Imported!

    Collection "${title}" has been created successfully.

    Imported: ${importData.imported} inscription(s)

    Warning: Whitelist upload failed: ${whitelistResult.error}

    Your collection will be reviewed before going live.

    `; } }; reader.readAsText(whitelistCsvFile); return; // Don't show success message yet, wait for CSV processing } catch (whitelistError) { console.error('Whitelist upload error:', whitelistError); } } resultDiv.innerHTML = `

    ✅ Collection Created & Inscriptions Imported!

    Collection "${title}" has been created successfully.

    Imported: ${importData.imported} inscription(s)

    Your collection will be reviewed before going live.

    `; } else { // For non-v2 collections, handle whitelist upload if needed if (whitelistEnabled && whitelistCsvFile) { resultDiv.innerHTML = '

    Uploading whitelist...

    '; try { const reader = new FileReader(); reader.onload = async function(e) { const csvContent = e.target.result; const whitelistResponse = await fetch('/api/upload-whitelist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ artworkSlug: slug, csvData: csvContent, whitelistStartDatetime: whitelistStartDatetime, whitelistEndDatetime: whitelistEndDatetime }) }); const whitelistResult = await whitelistResponse.json(); if (whitelistResult.success) { resultDiv.innerHTML = `

    Pre-inscribed Collection Submitted!

    Your collection "${title}" has been submitted for review.

    Whitelist: ${whitelistResult.entriesProcessed} entries uploaded

    We'll contact you within 24-48 hours to coordinate the ordinal transfer and finalize the launch details.

    Next Steps:

    `; } else { resultDiv.innerHTML = `

    Pre-inscribed Collection Submitted!

    Your collection "${title}" has been submitted for review.

    Warning: Whitelist upload failed: ${whitelistResult.error}

    We'll contact you within 24-48 hours to coordinate the ordinal transfer and finalize the launch details.

    `; } }; reader.readAsText(whitelistCsvFile); return; // Don't show success message yet, wait for CSV processing } catch (whitelistError) { console.error('Whitelist upload error:', whitelistError); resultDiv.innerHTML = `

    Pre-inscribed Collection Submitted!

    Your collection "${title}" has been submitted for review.

    Warning: Whitelist upload failed

    We'll contact you within 24-48 hours to coordinate the ordinal transfer and finalize the launch details.

    `; } } else { resultDiv.innerHTML = `

    Pre-inscribed Collection Submitted!

    Your collection "${title}" has been submitted for review.

    We'll contact you within 24-48 hours to coordinate the ordinal transfer and finalize the launch details.

    Next Steps:

    `; } } // Clear form document.getElementById('preInscribedSubmitArtistInput').value = ''; document.getElementById('preInscribedSubmitTitleInput').value = ''; document.getElementById('preInscribedSubmitDescriptionInput').value = ''; document.getElementById('preInscribedSubmitPriceInput').value = ''; document.getElementById('preInscribedSubmitMaxMintsInput').value = ''; document.getElementById('preInscribedSubmitWebsiteInput').value = ''; document.getElementById('preInscribedSubmitTwitterInput').value = ''; document.getElementById('preInscribedSubmitDiscordInput').value = ''; document.getElementById('preInscribedSubmitPaymentWalletInput').value = ''; document.getElementById('preInscribedSubmitMintStartInput').value = ''; document.getElementById('preInscribedSubmitMintEndInput').value = ''; document.getElementById('preInscribedSubmitPhysicalPrintInput').checked = false; document.getElementById('preInscribedSubmitDetailsInput').value = ''; const preCoverInput = document.getElementById('preInscribedImageInput'); if (preCoverInput) { preCoverInput.value = ''; handlePreInscribedImageUpload({ target: preCoverInput }); } } else { resultDiv.innerHTML = `

    Submission Failed: ${data.error}

    `; } } catch (error) { console.error('Pre-inscribed collection submission error:', error); document.getElementById('preInscribedResult').innerHTML = `

    Submission Failed: ${error.message || 'Network error'}

    `; } } // Toggle whitelist settings for pre-inscribed submit form window.togglePreInscribedSubmitWhitelistSettings = function() { const checkbox = document.getElementById('preInscribedSubmitWhitelistEnabledInput'); const settings = document.getElementById('preInscribedSubmitWhitelistSettings'); if (checkbox && settings) { if (checkbox.checked) { settings.style.display = 'block'; } else { settings.style.display = 'none'; } } } // Handle unlimited supply checkbox function setupUnlimitedSupplyCheckbox() { const unlimitedSupplyInput = document.getElementById('submitUnlimitedSupplyInput'); if (unlimitedSupplyInput) { unlimitedSupplyInput.addEventListener('change', function() { const maxMintsInput = document.getElementById('submitMaxMintsInput'); if (this.checked) { maxMintsInput.disabled = true; maxMintsInput.style.opacity = '0.5'; maxMintsInput.placeholder = 'Unlimited'; } else { maxMintsInput.disabled = false; maxMintsInput.style.opacity = '1'; maxMintsInput.placeholder = '1000'; } }); } } // Set up the checkbox when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupUnlimitedSupplyCheckbox); } else { setupUnlimitedSupplyCheckbox(); } // Handle whitelist enabled checkbox function setupWhitelistCheckbox() { const whitelistEnabledInput = document.getElementById('submitWhitelistEnabledInput'); if (whitelistEnabledInput) { whitelistEnabledInput.addEventListener('change', function() { const whitelistSettings = document.getElementById('submitWhitelistSettings'); if (this.checked) { whitelistSettings.style.display = 'block'; } else { whitelistSettings.style.display = 'none'; } }); } } // Set up whitelist checkbox when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupWhitelistCheckbox); } else { setupWhitelistCheckbox(); } // Handle admin whitelist enabled checkbox function setupAdminWhitelistCheckbox() { const adminWhitelistEnabledInput = document.getElementById('adminWhitelistEnabledInput'); if (adminWhitelistEnabledInput) { adminWhitelistEnabledInput.addEventListener('change', function() { const adminWhitelistSettings = document.getElementById('adminWhitelistSettings'); if (this.checked) { adminWhitelistSettings.style.display = 'block'; } else { adminWhitelistSettings.style.display = 'none'; } }); } } // Set up admin whitelist checkbox when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupAdminWhitelistCheckbox); } else { setupAdminWhitelistCheckbox(); } // Upload whitelist for artwork (admin function) window.uploadWhitelistForArtwork = async function() { const csvFile = document.getElementById('adminWhitelistCsvInput').files[0]; const whitelistStart = document.getElementById('adminWhitelistStartInput').value; const whitelistEnd = document.getElementById('adminWhitelistEndInput').value; if (!csvFile) { alert('Please select a CSV file to upload'); return; } // Whitelist end date is now optional - allows permanent whitelists if (!window.editingSlug) { alert('No artwork selected for editing'); return; } try { const csvContent = await readFileAsText(csvFile); const response = await fetch('/api/upload-whitelist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ artworkSlug: window.editingSlug, csvData: csvContent, whitelistStartDatetime: whitelistStart ? new Date(whitelistStart + 'Z').toISOString() : null, whitelistEndDatetime: whitelistEnd ? new Date(whitelistEnd + 'Z').toISOString() : null }) }); const result = await response.json(); if (result.success) { alert(`Whitelist uploaded successfully! ${result.entriesProcessed} entries processed.`); document.getElementById('adminWhitelistEnabledInput').checked = true; } else { alert('Upload failed: ' + result.error); } } catch (error) { alert('Upload failed: ' + error.message); } }; // View whitelist entries for artwork window.viewWhitelistEntries = async function() { if (!window.editingSlug) { alert('No artwork selected for editing'); return; } try { const response = await fetch(`/api/manage-whitelist?artwork=${window.editingSlug}`); const result = await response.json(); if (result.success) { let message = `Whitelist for "${result.artwork.title}":\n\n`; message += `Total entries: ${result.totalEntries}\n`; message += `Whitelist enabled: ${result.artwork.whitelistEnabled ? 'Yes' : 'No'}\n`; if (result.artwork.whitelistStartDatetime) { message += `Start: ${new Date(result.artwork.whitelistStartDatetime).toLocaleString()}\n`; } if (result.artwork.whitelistEndDatetime) { message += `End: ${new Date(result.artwork.whitelistEndDatetime).toLocaleString()}\n`; } message += '\nEntries:\n'; result.entries.forEach(entry => { message += `${entry.walletAddress}: ${entry.mintsUsed}/${entry.maxMints} used\n`; }); alert(message); } else { alert('Failed to load whitelist: ' + result.error); } } catch (error) { alert('Failed to load whitelist: ' + error.message); } }; // Clear whitelist for artwork window.clearWhitelist = async function() { if (!window.editingSlug) { alert('No artwork selected for editing'); return; } if (!confirm('Are you sure you want to clear the entire whitelist? This cannot be undone.')) { return; } try { const response = await fetch('/api/upload-whitelist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ artworkSlug: window.editingSlug, csvData: '', // Empty CSV to clear whitelistStartDatetime: null, whitelistEndDatetime: null }) }); const result = await response.json(); if (result.success) { alert('Whitelist cleared successfully'); document.getElementById('adminWhitelistEnabledInput').checked = false; } else { alert('Failed to clear whitelist: ' + result.error); } } catch (error) { alert('Failed to clear whitelist: ' + error.message); } }; // === STANDALONE WHITELIST MANAGEMENT FUNCTIONS === // Global variable to track which artwork we're managing whitelist for let currentWhitelistArtworkSlug = null; // Open whitelist management modal window.manageWhitelist = async function(slug) { currentWhitelistArtworkSlug = slug; try { // Get artwork details const artworkResponse = await fetch(`/api/get-artwork-admin?slug=${slug}`); const artworkData = await artworkResponse.json(); if (!artworkData.success) { alert('Failed to load artwork details'); return; } const artwork = artworkData.artwork; // Populate artwork info document.getElementById('whitelist-artwork-title').textContent = artwork.title; document.getElementById('whitelist-artwork-artist').textContent = `${artwork.artist}`; document.getElementById('whitelist-artwork-slug').textContent = `Slug: ${artwork.slug}`; // Set whitelist settings const whitelistEnabledInput = document.getElementById('whitelistManagementEnabledInput'); whitelistEnabledInput.checked = !!artwork.whitelistEnabled; // Set dates if they exist if (artwork.whitelistStartDatetime) { const startDate = new Date(artwork.whitelistStartDatetime); document.getElementById('whitelistManagementStartInput').value = startDate.toISOString().slice(0, 16); } else { document.getElementById('whitelistManagementStartInput').value = ''; } if (artwork.whitelistEndDatetime) { const endDate = new Date(artwork.whitelistEndDatetime); document.getElementById('whitelistManagementEndInput').value = endDate.toISOString().slice(0, 16); } else { document.getElementById('whitelistManagementEndInput').value = ''; } // Setup checkbox toggle setupWhitelistManagementCheckbox(); // Show/hide settings based on enabled status const whitelistSettings = document.getElementById('whitelistManagementSettings'); if (whitelistSettings) { whitelistSettings.style.display = whitelistEnabledInput.checked ? 'block' : 'none'; } // Ensure the checkbox is properly initialized setTimeout(() => { const checkbox = document.getElementById('whitelistManagementEnabledInput'); if (checkbox) { checkbox.addEventListener('change', function() { const settings = document.getElementById('whitelistManagementSettings'); if (settings) { settings.style.display = this.checked ? 'block' : 'none'; } }); } }, 100); // Show modal document.getElementById('whitelist-management-modal').style.display = 'block'; } catch (error) { alert('Failed to load whitelist management: ' + error.message); } }; // Setup whitelist management checkbox function setupWhitelistManagementCheckbox() { const whitelistEnabledInput = document.getElementById('whitelistManagementEnabledInput'); if (whitelistEnabledInput) { // Remove existing listeners by cloning the element const newInput = whitelistEnabledInput.cloneNode(true); whitelistEnabledInput.parentNode.replaceChild(newInput, whitelistEnabledInput); // Add event listener to the new element newInput.addEventListener('change', function() { const whitelistSettings = document.getElementById('whitelistManagementSettings'); if (whitelistSettings) { if (this.checked) { whitelistSettings.style.display = 'block'; } else { whitelistSettings.style.display = 'none'; } } }); } } // Hide whitelist management modal window.hideWhitelistManagementModal = function() { document.getElementById('whitelist-management-modal').style.display = 'none'; currentWhitelistArtworkSlug = null; }; // Upload whitelist for management window.uploadWhitelistForManagement = async function() { const csvFile = document.getElementById('whitelistManagementCsvInput').files[0]; const whitelistStart = document.getElementById('whitelistManagementStartInput').value; const whitelistEnd = document.getElementById('whitelistManagementEndInput').value; if (!csvFile) { alert('Please select a CSV file to upload'); return; } // Whitelist end date is now optional - allows permanent whitelists if (!currentWhitelistArtworkSlug) { alert('No artwork selected for whitelist management'); return; } try { const csvContent = await readFileAsText(csvFile); const response = await fetch('/api/upload-whitelist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ artworkSlug: currentWhitelistArtworkSlug, csvData: csvContent, whitelistStartDatetime: whitelistStart ? new Date(whitelistStart + 'Z').toISOString() : null, whitelistEndDatetime: whitelistEnd ? new Date(whitelistEnd + 'Z').toISOString() : null }) }); const result = await response.json(); if (result.success) { alert(`Whitelist uploaded successfully! ${result.entriesProcessed} entries processed.`); document.getElementById('whitelistManagementEnabledInput').checked = true; } else { alert('Upload failed: ' + result.error); } } catch (error) { alert('Upload failed: ' + error.message); } }; // View whitelist entries for management window.viewWhitelistEntriesForManagement = async function() { if (!currentWhitelistArtworkSlug) { alert('No artwork selected for whitelist management'); return; } try { const response = await fetch(`/api/manage-whitelist?artwork=${currentWhitelistArtworkSlug}`); const result = await response.json(); if (result.success) { let message = `Whitelist for "${result.artwork.title}":\n\n`; message += `Total entries: ${result.totalEntries}\n`; message += `Whitelist enabled: ${result.artwork.whitelistEnabled ? 'Yes' : 'No'}\n\n`; if (result.entries.length > 0) { message += 'Entries:\n'; result.entries.forEach(entry => { message += `${entry.walletAddress} - Max: ${entry.maxMints}, Used: ${entry.mintsUsed}, Remaining: ${entry.remainingMints}\n`; }); } else { message += 'No whitelist entries found.'; } alert(message); } else { alert('Failed to load whitelist entries: ' + result.error); } } catch (error) { alert('Failed to load whitelist entries: ' + error.message); } }; // Clear whitelist for management window.clearWhitelistForManagement = async function() { if (!currentWhitelistArtworkSlug) { alert('No artwork selected for whitelist management'); return; } if (!confirm('Are you sure you want to clear the entire whitelist? This cannot be undone.')) { return; } try { const response = await fetch('/api/upload-whitelist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ artworkSlug: currentWhitelistArtworkSlug, csvData: '', // Empty CSV to clear whitelistStartDatetime: null, whitelistEndDatetime: null }) }); const result = await response.json(); if (result.success) { alert('Whitelist cleared successfully'); document.getElementById('whitelistManagementEnabledInput').checked = false; const whitelistSettings = document.getElementById('whitelistManagementSettings'); whitelistSettings.style.display = 'none'; } else { alert('Failed to clear whitelist: ' + result.error); } } catch (error) { alert('Failed to clear whitelist: ' + error.message); } }; // Save whitelist settings (just for enabling/disabling without CSV upload) window.saveWhitelistSettings = async function() { if (!currentWhitelistArtworkSlug) { alert('No artwork selected for whitelist management'); return; } const whitelistEnabled = document.getElementById('whitelistManagementEnabledInput').checked; const whitelistStart = document.getElementById('whitelistManagementStartInput').value; const whitelistEnd = document.getElementById('whitelistManagementEndInput').value; // Whitelist end date is now optional - allows permanent whitelists try { // First, get the current artwork data const artworkResponse = await fetch(`/api/get-artwork-admin?slug=${currentWhitelistArtworkSlug}`); const artworkData = await artworkResponse.json(); if (!artworkData.success) { alert('Failed to load artwork details'); return; } const artwork = artworkData.artwork; // Now save with complete artwork data plus whitelist settings const response = await fetch('/api/save-artwork', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ slug: artwork.slug, artist: artwork.artist, title: artwork.title, description: artwork.description, price: artwork.priceSats, maxMints: artwork.maxMints, parentInscriptionId: artwork.parentInscriptionId, parentType: artwork.parentType, scriptInscriptionId: artwork.scriptInscriptionId, imageFilename: 'unchanged', websiteUrl: artwork.websiteUrl, twitterUrl: artwork.twitterUrl, discordUrl: artwork.discordUrl, paymentWallet: artwork.artistPaymentWallet, mintStartDatetime: artwork.mintStartDatetime, mintEndDatetime: artwork.mintEndDatetime, physicalPrint: artwork.physicalPrintAvailable, isDelegate: artwork.isDelegate, mintType: artwork.mintType, additionalDetails: artwork.additionalDetails, enablePreview: artwork.enablePreview, whitelistEnabled: whitelistEnabled, whitelistStartDatetime: whitelistStart ? new Date(whitelistStart + 'Z').toISOString() : null, whitelistEndDatetime: whitelistEnd ? new Date(whitelistEnd + 'Z').toISOString() : null, isEdit: true }) }); const result = await response.json(); if (result.success) { alert('Whitelist settings saved successfully!'); } else { alert('Failed to save whitelist settings: ' + result.error); } } catch (error) { alert('Failed to save whitelist settings: ' + error.message); } }; // ============ ENHANCED WHITELIST FUNCTIONS ============ // Toggle whitelist settings visibility window.toggleWhitelistSettings = function() { const checkbox = document.getElementById('whitelistManagementEnabledInput'); const settings = document.getElementById('whitelistManagementSettings'); if (checkbox && settings) { if (checkbox.checked) { settings.style.display = 'block'; } else { settings.style.display = 'none'; } } }; // Toggle submit artwork whitelist settings visibility window.toggleSubmitWhitelistSettings = function() { const checkbox = document.getElementById('submitWhitelistEnabledInput'); const settings = document.getElementById('submitWhitelistSettings'); if (checkbox && settings) { if (checkbox.checked) { settings.style.display = 'block'; } else { settings.style.display = 'none'; } } }; // Toggle card pack fields visibility in submit form window.toggleSubmitCardPackFields = function() { const checkbox = document.getElementById('submitCardPackInput'); const fields = document.getElementById('submitCardPackFields'); if (checkbox && fields) { fields.style.display = checkbox.checked ? 'block' : 'none'; } }; // Switch between whitelist tabs window.switchWhitelistTab = function(tabName) { // Hide all tab contents document.querySelectorAll('.whitelist-tab-content').forEach(content => { content.style.display = 'none'; }); // Reset all tab buttons document.querySelectorAll('[id^="whitelist-"][id$="-tab"]').forEach(button => { button.style.background = '#333'; }); // Show selected tab content document.getElementById(`whitelist-${tabName}-content`).style.display = 'block'; // Highlight selected tab button document.getElementById(`whitelist-${tabName}-tab`).style.background = '#0066cc'; // Load data for the selected tab if (tabName === 'entries') { loadWhitelistEntries(); } else if (tabName === 'analytics') { loadWhitelistAnalytics(); } }; // Preview CSV before uploading window.previewWhitelistCSV = async function() { const csvFile = document.getElementById('whitelistManagementCsvInput').files[0]; if (!csvFile) { alert('Please select a CSV file to preview'); return; } try { const csvContent = await readFileAsText(csvFile); const response = await fetch('/api/upload-whitelist-enhanced', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ artworkSlug: currentWhitelistArtworkSlug, csvData: csvContent, validateOnly: true }) }); const result = await response.json(); if (result.success) { let message = `CSV Preview:\n\n`; message += `Total Entries: ${result.preview.totalEntries}\n`; message += `Unique Wallets: ${result.preview.uniqueWallets}\n`; message += `Total Allowed Mints: ${result.preview.totalAllowedMints}\n\n`; if (result.preview.warnings && result.preview.warnings.length > 0) { message += `Warnings:\n`; result.preview.warnings.forEach(warning => { message += `- ${warning.message}\n`; }); message += '\n'; } message += `Sample Entries:\n`; result.preview.sampleEntries.forEach(entry => { message += `${entry.walletAddress} - Max: ${entry.maxMints}\n`; }); alert(message); } else { alert('CSV validation failed:\n' + result.validationErrors.map(e => e.message).join('\n')); } } catch (error) { alert('Failed to preview CSV: ' + error.message); } }; // Load whitelist entries with search and filtering window.loadWhitelistEntries = async function(page = 1) { if (!currentWhitelistArtworkSlug) { alert('No artwork selected for whitelist management'); return; } const search = document.getElementById('whitelist-search').value; const filter = document.getElementById('whitelist-filter').value; const sortBy = document.getElementById('whitelist-sort').value; try { const params = new URLSearchParams({ artwork: currentWhitelistArtworkSlug, search: search, filter: filter, sortBy: sortBy, sortOrder: 'desc', page: page, limit: 20 }); const response = await fetch(`/api/manage-whitelist-enhanced?${params}`); const result = await response.json(); if (result.success) { displayWhitelistEntries(result); } else { alert('Failed to load whitelist entries: ' + result.error); } } catch (error) { alert('Failed to load whitelist entries: ' + error.message); } }; // Display whitelist entries in table format function displayWhitelistEntries(data) { const tableContainer = document.getElementById('whitelist-entries-table'); if (data.entries.length === 0) { tableContainer.innerHTML = '
    No whitelist entries found
    '; return; } let html = ` `; data.entries.forEach(entry => { const utilizationColor = entry.utilizationPercentage >= 100 ? '#ff6666' : entry.utilizationPercentage >= 50 ? '#ffaa66' : '#66ff66'; html += ` `; }); html += '
    Wallet Address Max Mints Used Remaining Utilization Actions
    ${entry.walletAddress} ${entry.maxMints} ${entry.mintsUsed} ${entry.remainingMints} ${entry.utilizationPercentage.toFixed(1)}%
    '; // Add statistics summary html += `
    Total Entries: ${data.statistics.totalEntries} Total Allowed: ${data.statistics.totalAllowedMints} Total Used: ${data.statistics.totalUsedMints} Active Wallets: ${data.statistics.activeWallets} Utilization: ${data.statistics.utilizationRate}%
    `; tableContainer.innerHTML = html; // Update pagination updateWhitelistPagination(data.pagination); }; // Update pagination controls function updateWhitelistPagination(pagination) { const paginationContainer = document.getElementById('whitelist-pagination'); let html = '
    '; if (pagination.hasPrev) { html += ``; } html += `Page ${pagination.page} of ${pagination.totalPages}`; if (pagination.hasNext) { html += ``; } html += '
    '; paginationContainer.innerHTML = html; }; // Add new whitelist entry window.addWhitelistEntry = async function() { const walletAddress = document.getElementById('new-wallet-address').value.trim(); const maxMints = parseInt(document.getElementById('new-max-mints').value); if (!walletAddress) { alert('Please enter a wallet address'); return; } if (isNaN(maxMints) || maxMints < 1 || maxMints > 100) { alert('Max mints must be between 1 and 100'); return; } try { const response = await fetch('/api/manage-whitelist-enhanced', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ artworkSlug: currentWhitelistArtworkSlug, walletAddress: walletAddress, maxMints: maxMints }) }); const result = await response.json(); if (result.success) { alert('Whitelist entry added successfully!'); document.getElementById('new-wallet-address').value = ''; document.getElementById('new-max-mints').value = '1'; loadWhitelistEntries(); } else { alert('Failed to add whitelist entry: ' + result.error); } } catch (error) { alert('Failed to add whitelist entry: ' + error.message); } }; // Edit whitelist entry window.editWhitelistEntry = async function(entryId) { const newWalletAddress = prompt('Enter new wallet address:'); const newMaxMints = prompt('Enter new max mints (1-100):'); if (!newWalletAddress || !newMaxMints) { return; } const maxMints = parseInt(newMaxMints); if (isNaN(maxMints) || maxMints < 1 || maxMints > 100) { alert('Max mints must be between 1 and 100'); return; } try { const response = await fetch('/api/manage-whitelist-enhanced', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ artworkSlug: currentWhitelistArtworkSlug, id: entryId, walletAddress: newWalletAddress, maxMints: maxMints }) }); const result = await response.json(); if (result.success) { alert('Whitelist entry updated successfully!'); loadWhitelistEntries(); } else { alert('Failed to update whitelist entry: ' + result.error); } } catch (error) { alert('Failed to update whitelist entry: ' + error.message); } }; // Delete whitelist entry window.deleteWhitelistEntry = async function(entryId) { if (!confirm('Are you sure you want to delete this whitelist entry?')) { return; } try { const response = await fetch('/api/manage-whitelist-enhanced', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ artworkSlug: currentWhitelistArtworkSlug, id: entryId }) }); const result = await response.json(); if (result.success) { alert('Whitelist entry deleted successfully!'); loadWhitelistEntries(); } else { alert('Failed to delete whitelist entry: ' + result.error); } } catch (error) { alert('Failed to delete whitelist entry: ' + error.message); } }; // Load whitelist analytics window.loadWhitelistAnalytics = async function() { if (!currentWhitelistArtworkSlug) { alert('No artwork selected for whitelist management'); return; } const timeframe = document.getElementById('analytics-timeframe').value; try { const response = await fetch(`/api/whitelist-analytics?artwork=${currentWhitelistArtworkSlug}&timeframe=${timeframe}&details=true`); const result = await response.json(); if (result.success) { displayWhitelistAnalytics(result); } else { alert('Failed to load whitelist analytics: ' + result.error); } } catch (error) { alert('Failed to load whitelist analytics: ' + error.message); } }; // Display whitelist analytics function displayWhitelistAnalytics(data) { const container = document.getElementById('whitelist-analytics-content-area'); let html = `
    Overview Statistics
    ${data.overview.totalWhitelistedWallets}
    Total Wallets
    ${data.overview.totalAllowedMints}
    Allowed Mints
    ${data.overview.totalUsedMints}
    Used Mints
    ${data.overview.conversionRate}%
    Conversion Rate
    ${data.overview.mintUtilizationRate}%
    Utilization Rate
    `; // Whitelist status if (data.whitelistStatus.active) { html += `
    Whitelist Active
    ${data.whitelistStatus.timeRemaining ? `Time Remaining: ${data.whitelistStatus.timeRemainingText}` : 'No end time set'}
    `; } else if (data.whitelistStatus.ended) { html += `
    Whitelist Ended
    Public minting is now open
    `; } else { html += `
    Whitelist Inactive
    Public minting is open
    `; } // Top performers if (data.topPerformers && data.topPerformers.length > 0) { html += `
    Top Performers
    `; data.topPerformers.forEach((wallet, index) => { const utilizationColor = wallet.utilizationPercentage >= 100 ? '#ff6666' : wallet.utilizationPercentage >= 50 ? '#ffaa66' : '#66ff66'; html += `
    ${wallet.walletAddress}
    ${wallet.mintsUsed}/${wallet.maxMints} (${wallet.utilizationPercentage.toFixed(1)}%)
    `; }); html += '
    '; } // Daily performance chart (simple text-based) if (data.dailyPerformance && data.dailyPerformance.length > 0) { html += `
    Daily Performance
    `; data.dailyPerformance.forEach(day => { html += `
    ${day.date}
    ${day.totalMints} mints (${day.uniqueMinters} wallets)
    `; }); html += '
    '; } container.innerHTML = html; }; // Add event listeners for search and filter document.addEventListener('DOMContentLoaded', function() { const searchInput = document.getElementById('whitelist-search'); const filterSelect = document.getElementById('whitelist-filter'); const sortSelect = document.getElementById('whitelist-sort'); if (searchInput) { let searchTimeout; searchInput.addEventListener('input', function() { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { loadWhitelistEntries(); }, 500); }); } if (filterSelect) { filterSelect.addEventListener('change', loadWhitelistEntries); } if (sortSelect) { sortSelect.addEventListener('change', loadWhitelistEntries); } }); // Prevent scroll wheel from changing number input values (especially price fields) document.addEventListener('DOMContentLoaded', function() { // Get all number inputs const numberInputs = document.querySelectorAll('input[type="number"]'); numberInputs.forEach(input => { // Prevent wheel events from changing the value input.addEventListener('wheel', function(e) { e.preventDefault(); // Blur the input to prevent accidental changes this.blur(); }, { passive: false }); // Also prevent focus on hover to reduce accidental changes input.addEventListener('focus', function() { this.addEventListener('wheel', function(e) { e.preventDefault(); }, { passive: false }); }); }); // Also listen for dynamically added number inputs const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(node) { if (node.nodeType === 1) { // Element node const newNumberInputs = node.querySelectorAll ? node.querySelectorAll('input[type="number"]') : []; newNumberInputs.forEach(input => { input.addEventListener('wheel', function(e) { e.preventDefault(); this.blur(); }, { passive: false }); }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); }); // ============ PAYMENT MANAGEMENT FUNCTIONS ============ let currentPaymentArtworkSlug = null; // Show payment management modal window.managePayments = async function(slug) { currentPaymentArtworkSlug = slug; try { await ensureAdminSensitiveModals(); // Get artwork details const artworkResponse = await fetch(`/api/get-artwork-admin?slug=${slug}`); const artworkData = await artworkResponse.json(); if (!artworkData.success) { alert('Failed to load artwork details'); return; } const artwork = artworkData.artwork; // Populate artwork info document.getElementById('payment-artwork-title').textContent = artwork.title; document.getElementById('payment-artwork-artist').textContent = `${artwork.artist}`; document.getElementById('payment-artwork-wallet').textContent = `Payment Wallet: ${artwork.artistPaymentWallet || 'Not set'}`; // Pre-fill payment wallet if available if (artwork.artistPaymentWallet) { document.getElementById('payment-wallet').value = artwork.artistPaymentWallet; } // Load payment summary await loadPaymentSummary(slug); // Set up form event listener setupPaymentFormListener(); // Show modal document.getElementById('payment-management-modal').style.display = 'block'; } catch (error) { console.error('Failed to load payment management:', error); alert('Failed to load payment management: ' + error.message); } }; // Hide payment management modal window.hidePaymentModal = function() { const modal = document.getElementById('payment-management-modal'); if (modal) modal.style.display = 'none'; currentPaymentArtworkSlug = null; const form = document.getElementById('add-payment-form'); if (form && typeof form.reset === 'function') form.reset(); }; // ============ SOLD ORDINALS MANAGEMENT ============ // View sold ordinals for pre-inscribed collections window.viewSoldOrdinals = async function(slug) { try { await ensureAdminSensitiveModals(); // Get artwork details const artworkResponse = await fetch(`/api/simple-artwork?slug=${slug}`); const artworkData = await artworkResponse.json(); if (!artworkData.success) { alert('Failed to load artwork details'); return; } const artwork = artworkData.artwork; // Populate artwork info document.getElementById('sold-ordinals-title').textContent = artwork.title; document.getElementById('sold-ordinals-artist').textContent = `${artwork.artist}`; // Fetch sold ordinals with destination wallets const listDiv = document.getElementById('sold-ordinals-list'); listDiv.innerHTML = '

    Loading sold ordinals...

    '; const response = await fetch(`/api/get-sold-ordinals?slug=${slug}`); const data = await response.json(); if (data.success && data.soldOrdinals) { if (data.soldOrdinals.length === 0) { listDiv.innerHTML = '

    No ordinals sold yet.

    '; } else { listDiv.innerHTML = data.soldOrdinals.map((ordinal, idx) => `

    SOLD - #${idx + 1}

    Inscription ID:
    ${ordinal.inscription_id} View ↗

    Transfer To:
    ${ordinal.ordinals_wallet_address || ordinal.wallet_address}

    Minted: ${new Date(ordinal.created_at).toLocaleString()} ${ordinal.transaction_id ? `
    Payment TX: ${ordinal.transaction_id.substring(0, 16)}... ↗` : ''}

    `).join(''); } } else { listDiv.innerHTML = `

    Error: ${data.error || 'Failed to load sold ordinals'}

    `; } // Show modal document.getElementById('sold-ordinals-modal').style.display = 'block'; } catch (error) { console.error('Failed to load sold ordinals:', error); alert('Failed to load sold ordinals: ' + error.message); } }; // Hide sold ordinals modal window.hideSoldOrdinalsModal = function() { document.getElementById('sold-ordinals-modal').style.display = 'none'; }; // ============ END SOLD ORDINALS MANAGEMENT ============ // Load payment summary and history async function loadPaymentSummary(slug) { try { const response = await fetch(`/api/get-payment-summary?slug=${slug}&includeDetails=true`); const data = await response.json(); if (data.success && data.earnings) { // Update summary (guard against missing view / nulls) const e = data.earnings; document.getElementById('summary-earned').textContent = (e.totalEarningsSats ?? 0).toLocaleString(); document.getElementById('summary-paid').textContent = (e.totalPaidSats ?? 0).toLocaleString(); document.getElementById('summary-outstanding').textContent = (e.outstandingBalanceSats ?? 0).toLocaleString(); document.getElementById('summary-count').textContent = e.paymentCount ?? 0; // Update outstanding color const outstandingSats = e.outstandingBalanceSats ?? 0; const outstandingElement = document.getElementById('summary-outstanding').parentElement; outstandingElement.style.color = outstandingSats > 0 ? '#ff6b35' : '#4CAF50'; // Auto-fill the payment amount field with outstanding balance if (outstandingSats > 0) { document.getElementById('payment-amount').value = outstandingSats; } // Load payment history const historyContainer = document.getElementById('payment-history'); if (data.paymentDetails && data.paymentDetails.length > 0) { historyContainer.innerHTML = data.paymentDetails.map(payment => `
    ${payment.amountSats.toLocaleString()} sats
    ${payment.amountSats > (data.artwork && data.artwork.priceSats || 0) ? '

    Recorded amount may include fees; artist share = price × mints.

    ' : ''}

    Date: ${new Date(payment.paymentDate).toLocaleDateString()}

    TX: ${payment.mempoolTxId.substring(0, 16)}...

    Wallet: ${payment.paymentWalletAddress.substring(0, 20)}...

    ${payment.notes ? `

    Notes: ${payment.notes}

    ` : ''}

    Recorded by ${payment.recordedBy} on ${new Date(payment.recordedAt).toLocaleDateString()}

    `).join(''); } else { historyContainer.innerHTML = '

    No payments recorded yet.

    '; } } else { console.error('Failed to load payment summary:', data.error); document.getElementById('payment-history').innerHTML = '

    Failed to load payment data.

    '; } } catch (error) { console.error('Failed to load payment summary:', error); document.getElementById('payment-history').innerHTML = '

    Error loading payment data.

    '; } } // Set up payment form event listener function setupPaymentFormListener() { const form = document.getElementById('add-payment-form'); if (form) { // Remove any existing listeners const newForm = form.cloneNode(true); form.parentNode.replaceChild(newForm, form); // Add fresh event listener newForm.addEventListener('submit', async function(e) { e.preventDefault(); if (!currentPaymentArtworkSlug) { alert('No artwork selected'); return; } const amountSats = parseInt(document.getElementById('payment-amount').value); const mempoolTxId = document.getElementById('payment-tx-id').value.trim(); const paymentWalletAddress = document.getElementById('payment-wallet').value.trim(); const notes = document.getElementById('payment-notes').value.trim(); if (!amountSats || amountSats <= 0) { alert('Please enter a valid payment amount'); return; } if (!mempoolTxId) { alert('Please enter the mempool transaction ID'); return; } if (!paymentWalletAddress) { alert('Please enter the payment wallet address'); return; } try { const response = await fetch('/api/add-payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ artworkSlug: currentPaymentArtworkSlug, amountSats: amountSats, mempoolTxId: mempoolTxId, paymentWalletAddress: paymentWalletAddress, notes: notes || null }) }); const result = await response.json(); if (result.success) { alert('Payment recorded successfully!'); newForm.reset(); // Pre-fill wallet again document.getElementById('payment-wallet').value = paymentWalletAddress; // Reload payment data await loadPaymentSummary(currentPaymentArtworkSlug); // Refresh admin list if (typeof loadArtworksForAdmin === 'function') { loadArtworksForAdmin(); } } else { alert('Failed to record payment: ' + result.error); } } catch (error) { console.error('Failed to add payment:', error); alert('Failed to record payment: ' + error.message); } }); } } // Delete payment window.deletePayment = async function(paymentId) { if (!confirm('Are you sure you want to delete this payment record? This cannot be undone.')) { return; } try { const response = await fetch('/api/delete-payment', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ paymentId: paymentId }) }); const result = await response.json(); if (result.success) { alert('Payment record deleted successfully'); // Reload payment data if (currentPaymentArtworkSlug) { await loadPaymentSummary(currentPaymentArtworkSlug); } // Refresh admin list if (typeof loadArtworksForAdmin === 'function') { loadArtworksForAdmin(); } } else { alert('Failed to delete payment: ' + result.error); } } catch (error) { console.error('Failed to delete payment:', error); alert('Failed to delete payment: ' + error.message); } }; // ============ END PAYMENT MANAGEMENT FUNCTIONS ============ // Submit artwork for review async function submitArtwork() { const artist = document.getElementById('submitArtistInput').value; const title = document.getElementById('submitTitleInput').value; const description = document.getElementById('submitDescriptionInput').value; const collectionName = document.getElementById('submitCollectionNameInput')?.value || null; const price = parseInt(document.getElementById('submitPriceInput').value) || 0; const isUnlimitedSupply = document.getElementById('submitUnlimitedSupplyInput').checked; const maxMints = isUnlimitedSupply ? -1 : (parseInt(document.getElementById('submitMaxMintsInput').value) || 1000); // Get parent type and corresponding parent ID const parentType = document.querySelector('input[name="submitParentType"]:checked').value; const parentId = parentType === 'static' ? document.getElementById('submitParentIdInput').value : document.getElementById('submitRecursiveParentIdInput').value; const isPixelArt = document.getElementById('submitPixelArtInput').checked; // Use pixel art renderer if checked, otherwise use default const scriptId = isPixelArt ? 'd686bf14aec7115b84cb9e85fe8d819d61217d115b8869c49694205ce3403a8ai0' // Pixel art renderer (renderer-pixel.js) : '5b0e2cf2d3d9a2aee10b5ae860487376badaa3fa4f2ac470fa25a4e3c1217c3ci0'; // Default renderer (renderer.js) // Card pack fields const isCardPack = document.getElementById('submitCardPackInput')?.checked || false; const delegateInscriptionId = document.getElementById('submitDelegateInscriptionInput')?.value?.trim() || null; const packOpeningVideo = document.getElementById('submitPackOpeningVideoInput')?.value?.trim() || null; const stampSeries = document.getElementById('submitStampSeriesInput')?.value?.trim() || null; const stampSeriesName = document.getElementById('submitStampSeriesNameInput')?.value?.trim() || null; const stampsPerPack = parseInt(document.getElementById('submitStampsPerPackInput')?.value) || 5; const twitter = document.getElementById('submitTwitterInput').value; const discord = document.getElementById('submitDiscordInput').value; const website = document.getElementById('submitWebsiteInput').value; const paymentWallet = document.getElementById('submitPaymentWalletInput').value; const physicalPrint = document.getElementById('submitPhysicalPrintInput').checked; const mintStart = document.getElementById('submitMintStartInput').value; const mintEnd = document.getElementById('submitMintEndInput').value; // Convert datetime-local values to UTC ISO strings const mintStartDatetime = mintStart ? new Date(mintStart + 'Z').toISOString() : null; const mintEndDatetime = mintEnd ? new Date(mintEnd + 'Z').toISOString() : null; // Handle whitelist settings const whitelistEnabled = document.getElementById('submitWhitelistEnabledInput').checked; let whitelistData = null; if (whitelistEnabled) { const whitelistStart = document.getElementById('submitWhitelistStartInput').value; const whitelistEnd = document.getElementById('submitWhitelistEndInput').value; const whitelistCsvFile = document.getElementById('submitWhitelistCsvInput').files[0]; // Whitelist end date is now optional - allows permanent whitelists // Create whitelist data even without CSV file (for settings only) whitelistData = { enabled: true, startDatetime: whitelistStart ? new Date(whitelistStart + 'Z').toISOString() : null, endDatetime: whitelistEnd ? new Date(whitelistEnd + 'Z').toISOString() : null, csvData: null }; // Only require CSV file if one is provided if (whitelistCsvFile) { try { const csvContent = await readFileAsText(whitelistCsvFile); whitelistData.csvData = csvContent; } catch (error) { alert('Failed to read CSV file: ' + error.message); return; } } } if (!artist || !title || !parentId) { alert('Artist name, title, and parent inscription ID are required'); return; } // Validate card pack fields if card pack is checked if (isCardPack) { if (!delegateInscriptionId) { alert('Delegate Inscription ID is required for card packs'); return; } if (!stampSeries || !stampSeriesName) { alert('Series ID and Series Display Name are required for card packs'); return; } } // Generate slug from artist-title to avoid conflicts const slug = `${artist}-${title}`.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); // Debug: Log whitelist data being sent console.log('Whitelist data being sent:', { whitelistEnabled, whitelistData, startDatetime: whitelistData ? whitelistData.startDatetime : null, endDatetime: whitelistData ? whitelistData.endDatetime : null }); try { const response = await fetch('/api/submit-artwork', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ slug, artist, title, description, collectionName, price, maxMints, parentInscriptionId: parentId, parentType: parentType, scriptInscriptionId: scriptId, twitter, discord, website, paymentWallet, physicalPrint, mintStartDatetime, mintEndDatetime, imageFilename: 'submitted.webp', // Default for submissions whitelistEnabled: whitelistEnabled, whitelistStartDatetime: whitelistData ? whitelistData.startDatetime : null, whitelistEndDatetime: whitelistData ? whitelistData.endDatetime : null, // Card pack fields isCardPack: isCardPack, delegateInscriptionId: delegateInscriptionId, packOpeningVideo: packOpeningVideo, stampSeries: stampSeries, stampSeriesName: stampSeriesName, stampsPerPack: stampsPerPack }) }); const data = await response.json(); const resultDiv = document.getElementById('submitResult'); if (data.success) { // If whitelist was enabled and has CSV data, upload the whitelist data if (whitelistData && whitelistData.csvData) { // Add a small delay to ensure artwork is fully created in database await new Promise(resolve => setTimeout(resolve, 1000)); try { const whitelistResponse = await fetch('/api/upload-whitelist', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ artworkSlug: data.slug, csvData: whitelistData.csvData, whitelistStartDatetime: whitelistData.startDatetime, whitelistEndDatetime: whitelistData.endDatetime }) }); const whitelistResult = await whitelistResponse.json(); if (whitelistResult.success) { resultDiv.innerHTML = `

    Submission Successful!

    Your artwork "${title}" has been submitted for review.

    Whitelist uploaded successfully with ${whitelistResult.entriesProcessed} entries.

    You'll be notified when it's approved and goes live.

    `; } else { resultDiv.innerHTML = `

    Artwork submitted but whitelist upload failed:

    ${whitelistResult.error}

    You can upload the whitelist later in the admin panel.

    `; } } catch (error) { resultDiv.innerHTML = `

    Artwork submitted but whitelist upload failed:

    ${error.message}

    You can upload the whitelist later in the admin panel.

    `; } } else if (whitelistData && !whitelistData.csvData) { resultDiv.innerHTML = `

    Submission Successful!

    Your artwork "${title}" has been submitted for review.

    Whitelist is enabled but no CSV file was uploaded. You can upload the whitelist CSV later in the admin panel.

    You'll be notified when it's approved and goes live.

    `; } else { resultDiv.innerHTML = `

    Submission Successful!

    Your artwork "${title}" has been submitted for review.

    You'll be notified when it's approved and goes live.

    `; } // Clear form document.getElementById('submitArtistInput').value = ''; document.getElementById('submitTitleInput').value = ''; document.getElementById('submitDescriptionInput').value = ''; document.getElementById('submitPriceInput').value = 0; document.getElementById('submitMaxMintsInput').value = 1000; document.getElementById('submitParentIdInput').value = ''; document.getElementById('submitTwitterInput').value = ''; document.getElementById('submitDiscordInput').value = ''; document.getElementById('submitWebsiteInput').value = ''; document.getElementById('submitMintStartInput').value = ''; document.getElementById('submitMintEndInput').value = ''; } else { resultDiv.innerHTML = `

    Submission Failed: ${data.error}

    `; } } catch (error) { console.error('Submission error:', error); document.getElementById('submitResult').innerHTML = `

    Submission Failed: ${error.message || 'Network error'}

    `; } } // Submit auction for review async function submitAuction() { const artist = document.getElementById('auctionArtistInput').value.trim(); const title = document.getElementById('auctionTitleInput').value.trim(); const description = document.getElementById('auctionDescriptionInput').value.trim(); const inscriptionId = document.getElementById('auctionInscriptionIdInput').value.trim(); const startingPrice = parseInt(document.getElementById('auctionStartingPriceInput').value) || 0; const reservePrice = parseInt(document.getElementById('auctionReservePriceInput').value) || null; const artistPaymentWallet = document.getElementById('auctionPaymentWalletInput').value.trim(); const auctionStart = document.getElementById('auctionStartDatetimeInput').value; const auctionEnd = document.getElementById('auctionEndDatetimeInput').value; const twitter = document.getElementById('auctionTwitterInput').value.trim(); const discord = document.getElementById('auctionDiscordInput').value.trim(); const website = document.getElementById('auctionWebsiteInput').value.trim(); // Platform wallet for receiving bids (custody) const paymentWallet = 'bc1qcefl3uc75wh39wcwqs4ca9xw8m7jhc35t3kv43'; // Validate required fields if (!artist || !title || !inscriptionId || !auctionEnd || !artistPaymentWallet) { alert('Artist name, title, inscription ID, payment wallet, and auction end time are required'); return; } // Validate Bitcoin wallet address format if (!artistPaymentWallet.startsWith('bc1q') && !artistPaymentWallet.startsWith('bc1p') && !artistPaymentWallet.startsWith('1') && !artistPaymentWallet.startsWith('3')) { alert('Please enter a valid Bitcoin wallet address'); return; } // Convert datetime-local values to ISO strings // If no start time provided, use current time (will start when approved) const now = new Date(); const auctionStartDatetime = auctionStart ? new Date(auctionStart).toISOString() : now.toISOString(); const auctionEndDatetime = auctionEnd ? new Date(auctionEnd).toISOString() : null; // Validate end date is in the future const endDate = new Date(auctionEndDatetime); const startDate = new Date(auctionStartDatetime); if (endDate <= now) { alert('Auction end time must be in the future'); return; } // Only validate start vs end if a specific start time was provided if (auctionStart && endDate <= startDate) { alert('Auction end time must be after start time'); return; } try { const response = await fetch('/api/submit-auction', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ artist, title, description, inscriptionId, startingPrice, reservePrice, auctionStartDatetime, auctionEndDatetime, paymentWallet, artistPaymentWallet, twitter, discord, website }) }); const data = await response.json(); const resultDiv = document.getElementById('auctionResult'); if (data.success) { resultDiv.innerHTML = `

    Auction Submitted!

    Your auction "${title}" has been submitted for review.

    You'll be notified when it's approved and goes live.

    `; // Clear form document.getElementById('auctionArtistInput').value = ''; document.getElementById('auctionTitleInput').value = ''; document.getElementById('auctionDescriptionInput').value = ''; document.getElementById('auctionInscriptionIdInput').value = ''; document.getElementById('auctionStartingPriceInput').value = '10000'; document.getElementById('auctionReservePriceInput').value = ''; document.getElementById('auctionStartDatetimeInput').value = ''; document.getElementById('auctionEndDatetimeInput').value = ''; document.getElementById('auctionTwitterInput').value = ''; document.getElementById('auctionDiscordInput').value = ''; document.getElementById('auctionWebsiteInput').value = ''; } else { resultDiv.innerHTML = `

    Submission Failed: ${data.error}

    `; } } catch (error) { console.error('Auction submission error:', error); document.getElementById('auctionResult').innerHTML = `

    Submission Failed: ${error.message || 'Network error'}

    `; } } // ============ AUCTION PAGE FUNCTIONS ============ let currentAuction = null; let auctionConnectedWallet = null; let auctionUserBid = null; let auctionCountdownInterval = null; function sortAuctionsForListing(auctions) { return [...auctions].sort((a, b) => { const statusOrder = { upcoming: 0, active: 1, ended: 2 }; if (statusOrder[a.status] !== statusOrder[b.status]) { return statusOrder[a.status] - statusOrder[b.status]; } if (a.status === 'upcoming') { return new Date(a.auctionStartDatetime) - new Date(b.auctionStartDatetime); } return new Date(b.auctionEndDatetime) - new Date(a.auctionEndDatetime); }); } function getStaticAuctionCardImageSrc(slug) { switch (slug) { case 'lady-redhorns-hodl-psychological-endurance': return '/RedHorns/physical.jpeg'; case 'lady-redhorns-proof-of-chaos-1': return '/lady-redhorns/poc-1/thumb-01.jpg'; case 'lady-redhorns-proof-of-chaos-2': return '/lady-redhorns/poc-2/thumb-01.jpg'; case 'lady-redhorns-proof-of-chaos-3': return '/lady-redhorns/poc-3/thumb-01.jpg'; case 'pizza-comrades-lady-lord-valentine': return '/pizza_comrades/auction.png'; case 'pizza-comrades-lil-pat': return '/pizza_comrades/Lil_Pat_BIG.png'; default: return null; } } function buildAuctionListingCardHtml(auction) { const startDate = new Date(auction.auctionStartDatetime); const endDate = new Date(auction.auctionEndDatetime); const now = new Date(); const isUpcoming = auction.status === 'upcoming'; const isEnded = auction.status === 'ended'; const isActive = auction.status === 'active'; let timeDisplay = ''; let timeColor = '#888'; if (isUpcoming) { const timeToStart = startDate - now; const days = Math.floor(timeToStart / (1000 * 60 * 60 * 24)); const hours = Math.floor((timeToStart % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((timeToStart % (1000 * 60 * 60)) / (1000 * 60)); if (days > 0) { timeDisplay = 'Starts ' + days + 'd ' + hours + 'h'; } else if (hours > 0) { timeDisplay = 'Starts ' + hours + 'h ' + minutes + 'm'; } else { timeDisplay = 'Starts ' + minutes + 'm'; } timeColor = '#ffaa00'; } else if (isActive) { const timeLeft = endDate - now; const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24)); const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60)); if (days > 0) { timeDisplay = days + 'd ' + hours + 'h left'; } else if (hours > 0) { timeDisplay = hours + 'h ' + minutes + 'm left'; } else { timeDisplay = minutes + 'm left'; } timeColor = timeLeft < 3600000 ? '#ff0000' : '#888'; } else { timeDisplay = 'Ended'; } const currentBid = auction.currentHighBidSats || auction.startingPriceSats; const borderColor = isEnded ? '#666' : (isUpcoming ? '#ffaa00' : '#ffffff'); let labelStyle; let statusLabel; if (isUpcoming) { labelStyle = 'background: #ff8800; color: #000;'; statusLabel = 'SOON'; } else if (isActive) { labelStyle = 'background: #00aa00; color: #fff;'; statusLabel = 'LIVE'; } else { labelStyle = 'background: #333; color: #999;'; statusLabel = 'SOLD'; } const opacity = isEnded ? '0.8' : '1'; const staticAuctionCardSrc = getStaticAuctionCardImageSrc(auction.slug); const cardImageSrc = staticAuctionCardSrc || `https://ordinals.com/content/${auction.inscriptionId}`; const cardImageFallback = staticAuctionCardSrc ? '' : `this.style.display='none'; this.parentElement.innerHTML='';`; return `
    ${statusLabel}

    ${auction.title}

    by ${auction.artist}

    ${isUpcoming ? 'Starting: ' : ''}${currentBid.toLocaleString()} sats ${timeDisplay}
    `; } function formatHomeAuctionSatsToBtc(sats) { const n = Number(sats) || 0; const btc = n / 1e8; let d = 6; if (btc >= 1) d = 4; else if (btc >= 0.1) d = 5; return '≈ ' + btc.toFixed(d) + ' ₿'; } function homeAuctionInscriptionLabel(auction) { const id = String(auction.inscriptionId || '').trim(); if (!id) return 'Timed auction'; const short = id.length > 16 ? id.slice(0, 10) + '…' + id.slice(-4) : id; return 'Timed auction · Inscription ' + short; } function startHomeAuctionCountdownTicker() { if (window.__homeAuctionCountdownInterval) { clearInterval(window.__homeAuctionCountdownInterval); } const tick = () => { document.querySelectorAll('[data-home-countdown]').forEach((root) => { const target = parseInt(root.getAttribute('data-target-ms'), 10); const hEl = root.querySelector('[data-cd-h]'); const mEl = root.querySelector('[data-cd-m]'); const sEl = root.querySelector('[data-cd-s]'); if (!hEl || !mEl || !sEl || !target) return; const rem = Math.max(0, target - Date.now()); if (rem <= 0) { hEl.textContent = '00'; mEl.textContent = '00'; sEl.textContent = '00'; return; } const secs = Math.floor(rem / 1000); const h = Math.floor(secs / 3600); const m = Math.floor((secs % 3600) / 60); const s = secs % 60; hEl.textContent = String(h).padStart(2, '0'); mEl.textContent = String(m).padStart(2, '0'); sEl.textContent = String(s).padStart(2, '0'); }); }; tick(); window.__homeAuctionCountdownInterval = setInterval(tick, 1000); } function buildHomeAuctionCardHtml(auction) { const isUpcoming = auction.status === 'upcoming'; const isActive = auction.status === 'active'; const endMs = new Date(auction.auctionEndDatetime).getTime(); const startMs = new Date(auction.auctionStartDatetime).getTime(); const targetMs = isUpcoming ? startMs : endMs; const slug = String(auction.slug || '').replace(/"/g, ''); const href = getAuctionUrl(slug); const staticAuctionCardSrc = getStaticAuctionCardImageSrc(auction.slug); const cardImageSrc = staticAuctionCardSrc || ('https://ordinals.com/content/' + auction.inscriptionId); const cardImageFallback = staticAuctionCardSrc ? '' : "this.style.display='none'; this.parentElement.innerHTML='';"; const title = escapeHtml(auction.title || 'Untitled'); const artistRaw = String(auction.artist || 'Unknown').trim(); const artistLine = escapeHtml(artistRaw.startsWith('@') ? artistRaw : '@' + artistRaw); const descRaw = (auction.description || '').trim(); const descShort = descRaw.length > 240 ? descRaw.slice(0, 237) + '…' : descRaw; const descHtml = descShort ? escapeHtml(descShort) : ''; const eyebrow = escapeHtml(homeAuctionInscriptionLabel(auction)); const bidCount = Number(auction.bidCount || 0); const currentHigh = Number(auction.currentHighBidSats || 0); const starting = Number(auction.startingPriceSats || 0); const displayBid = bidCount > 0 ? Math.max(currentHigh, starting) : starting; const bidLabel = bidCount > 0 ? 'Current high bid' : 'Starting at'; const minBid = Math.max(starting, currentHigh + 1000); const minBidStr = minBid.toLocaleString() + ' SATS'; const btcLine = formatHomeAuctionSatsToBtc(displayBid); const bidMeta = isUpcoming ? 'Bidding opens when the auction goes live' : (bidCount === 1 ? '1 bid placed' : bidCount + ' bids placed'); const hasReserve = !!(auction.reservePriceSats && Number(auction.reservePriceSats) > 0); let reserveHtml = ''; if (hasReserve && isActive) { const met = auction.reserveMet; const cls = met ? 'home-auction-reserve' : 'home-auction-reserve is-unmet'; const txt = met ? '✓ Reserve met' : 'Reserve not met'; reserveHtml = '

    ' + txt + '

    '; } else if (hasReserve && isUpcoming) { reserveHtml = '

    Reserve applies once live

    '; } const countdownHead = isUpcoming ? '
    Auction starts in
    ' : '
    Auction ends in
    '; const ctaText = isActive ? ('Place bid — Minimum ' + minBidStr) : 'View auction'; const foot = (isActive && bidCount > 0) ? '

    Highest offers update on the auction page after each bid.

    ' : ''; const imgAlt = collectionOrdinalsAlt(auction.title, auction.artist); return ( '
    ' + '' + '
    ' + '' + imgAlt + '' + '
    ' + '
    ' + '
    ' + '

    ' + eyebrow + '

    ' + '

    ' + title + '

    ' + '

    ' + artistLine + '

    ' + (descHtml ? '

    ' + descHtml + '

    ' : '') + '
    ' + '
    ' + countdownHead + '
    ' + '
    00Hours
    ' + '
    00Mins
    ' + '
    00Secs
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '

    ' + bidLabel + '

    ' + '

    ' + displayBid.toLocaleString() + ' SATS

    ' + '

    ' + btcLine + '

    ' + '

    ' + bidMeta + '

    ' + '
    ' + reserveHtml + '
    ' + '' + escapeHtml(ctaText) + '' + foot + '
    ' + '
    ' ); } async function loadAllAuctionsGroupedPage() { const gridUp = document.getElementById('all-auctions-grid-upcoming'); const gridOpen = document.getElementById('all-auctions-grid-open'); const gridEnded = document.getElementById('all-auctions-grid-ended'); if (!gridUp || !gridOpen || !gridEnded) return; const loading = '

    Loading auctions…

    '; gridUp.innerHTML = loading; gridOpen.innerHTML = loading; gridEnded.innerHTML = loading; const response = await fetch(getReadApiUrl(`/api/list-auctions?t=${Date.now()}`), { cache: 'no-store' }); const data = await response.json(); if (!data.success || !data.auctions) { const err = '

    No auctions found.

    '; gridUp.innerHTML = err; gridOpen.innerHTML = err; gridEnded.innerHTML = err; return; } const upcoming = []; const active = []; const ended = []; data.auctions.forEach((a) => { if (a.status === 'upcoming') upcoming.push(a); else if (a.status === 'active') active.push(a); else if (a.status === 'ended') ended.push(a); }); upcoming.sort((a, b) => new Date(a.auctionStartDatetime) - new Date(b.auctionStartDatetime)); active.sort((a, b) => new Date(b.auctionEndDatetime) - new Date(a.auctionEndDatetime)); ended.sort((a, b) => new Date(b.auctionEndDatetime) - new Date(a.auctionEndDatetime)); const empty = (msg) => `

    ${msg}

    `; gridUp.innerHTML = upcoming.length ? upcoming.map((a) => buildAuctionListingCardHtml(a)).join('') : empty('No upcoming auctions.'); gridOpen.innerHTML = active.length ? active.map((a) => buildAuctionListingCardHtml(a)).join('') : empty('No open auctions.'); gridEnded.innerHTML = ended.length ? ended.map((a) => buildAuctionListingCardHtml(a)).join('') : empty('No ended auctions yet.'); } // Load auctions for home page (upcoming + active + recently ended) async function loadActiveAuctions() { const section = document.getElementById('active-auctions-section'); const grid = document.getElementById('active-auctions-grid'); if (!section || !grid) return; if (window.__homeAuctionCountdownInterval) { clearInterval(window.__homeAuctionCountdownInterval); window.__homeAuctionCountdownInterval = null; } try { const response = await fetch(getReadApiUrl('/api/list-auctions?status=active_and_upcoming')); const data = await response.json(); if (!data.success || !data.auctions || data.auctions.length === 0) { if (window.__homeAuctionCountdownInterval) { clearInterval(window.__homeAuctionCountdownInterval); window.__homeAuctionCountdownInterval = null; } section.style.display = 'none'; return; } section.style.display = 'block'; const sortedAuctions = sortAuctionsForListing(data.auctions); grid.innerHTML = sortedAuctions.map((auction) => buildHomeAuctionCardHtml(auction)).join(''); startHomeAuctionCountdownTicker(); } catch (error) { console.error('Error loading auctions:', error); if (window.__homeAuctionCountdownInterval) { clearInterval(window.__homeAuctionCountdownInterval); window.__homeAuctionCountdownInterval = null; } section.style.display = 'none'; } } // Load all auctions for Auctions tab (upcoming, live, ended) async function loadAllAuctionsTab(gridId) { const id = gridId || 'all-auctions-grid'; const grid = document.getElementById(id); if (!grid) return; grid.innerHTML = '

    Loading auctions...

    '; try { const response = await fetch(getReadApiUrl('/api/list-auctions')); const data = await response.json(); if (!data.success || !data.auctions) { grid.innerHTML = '

    No auctions found.

    '; return; } if (data.auctions.length === 0) { grid.innerHTML = '

    No auctions yet.

    '; return; } const sortedAuctions = sortAuctionsForListing(data.auctions); grid.innerHTML = sortedAuctions.map((auction) => buildAuctionListingCardHtml(auction)).join(''); } catch (error) { console.error('Error loading all auctions:', error); grid.innerHTML = '

    Failed to load auctions.

    '; } } const EMPRESS_TRASH_LOTUS_MINT_SLUG = 'empress-trash-lotus-blooms'; const FIVE_IL5NC5_MINT_SLUG = 'reece-swanepoel-5il5nc5'; let fiveIl5nc5PhasePollTimer = null; /** Mint page: body scanner artwork + installation photo (Lady RedHorns–style gallery). */ function applyEmpressTrashLotusMintGallery(artwork) { document.body.classList.add('empress-trash-mint-gallery'); const frame = document.querySelector('#app .mint-redesign .mint-art-frame'); if (!frame || !artwork) return; const bodyScannerSrc = '/empress-trash/body-scanner.jpg'; const installSrc = '/empress-trash/lotus-blooms-installation.jpg'; const views = [ { src: bodyScannerSrc, thumb: bodyScannerSrc, label: 'Body scanner' }, { src: installSrc, thumb: installSrc, label: 'Installation' }, ]; let idx = 0; const diamondSvg = ''; const expandSvg = ''; const titleEsc = String(artwork.title || 'Artwork').replace(/"/g, '"'); const main0 = `${titleEsc}`; frame.innerHTML = ` `; const thumbsRoot = document.getElementById('et-mint-gallery-thumbs'); const mainHost = document.getElementById('et-mint-gallery-main'); const renderMain = () => { if (!mainHost) return; const v = views[idx]; mainHost.innerHTML = `${titleEsc}`; }; const renderThumbs = () => { if (!thumbsRoot) return; thumbsRoot.innerHTML = views .map((v, i) => { const thumbSrc = v.thumb || v.src; const lab = String(v.label || '').replace(/"/g, '"'); return `
    ${v.label}
    `; }) .join(''); thumbsRoot.querySelectorAll('.rh-thumb').forEach((btn) => { btn.addEventListener('click', () => { idx = parseInt(btn.getAttribute('data-index'), 10) || 0; renderMain(); renderThumbs(); }); }); }; renderThumbs(); const expandBtn = document.getElementById('et-mint-gallery-expand'); if (expandBtn) { expandBtn.addEventListener('click', () => { const v = views[idx]; window.open(v.src, '_blank', 'noopener,noreferrer'); }); } const prevBtn = document.querySelector('#et-mint-rh-gallery .rh-thumb-prev'); const nextBtn = document.querySelector('#et-mint-rh-gallery .rh-thumb-next'); const step = (delta) => { idx = (idx + delta + views.length) % views.length; renderMain(); renderThumbs(); }; if (prevBtn) prevBtn.addEventListener('click', () => step(-1)); if (nextBtn) nextBtn.addEventListener('click', () => step(1)); } function ladyRedhornsPocGalleryViews(subdir, count) { const views = []; for (let i = 1; i <= count; i++) { const n = String(i).padStart(2, '0'); views.push({ src: '/lady-redhorns/' + subdir + '/' + n + '.jpg', thumb: '/lady-redhorns/' + subdir + '/thumb-' + n + '.jpg', label: 'View ' + i, }); } return views; } const LADY_REDHORNS_GALLERY_BY_SLUG = { 'lady-redhorns-hodl-psychological-endurance': { defaultIndex: 1, pillText: 'Physical + digital artwork', footNote: 'Three views of the artwork', tagline: 'Original acrylic painting + Bitcoin inscription', includedItems: [ 'Original 50 × 50cm acrylic painting', 'Digital artwork inscribed on Bitcoin', 'Physical and digital ownership transferred together', ], views: [ { src: '/RedHorns/digital.jpeg', label: 'Digital inscription' }, { src: '/RedHorns/physical.jpeg', label: 'Original painting' }, { src: '/RedHorns/easel.jpeg', label: 'On easel' }, ], }, 'lady-redhorns-proof-of-chaos-1': { defaultIndex: 0, pillText: 'Proof of Chaos #1 · Acrylic on canvas', footNote: 'Photos of the physical painting (40 × 40 cm)', tagline: 'Acrylic on canvas · inscribed on Bitcoin', includedItems: [ 'Original 40 × 40 cm acrylic on canvas', 'Digital artwork inscribed on Bitcoin', 'Physical and digital ownership transferred together', ], views: ladyRedhornsPocGalleryViews('poc-1', 8), }, 'lady-redhorns-proof-of-chaos-2': { defaultIndex: 0, pillText: 'Proof of Chaos #2 · Acrylic on canvas', footNote: 'Photos of the physical painting (40 × 40 cm)', tagline: 'Acrylic on canvas · inscribed on Bitcoin', includedItems: [ 'Original 40 × 40 cm acrylic on canvas', 'Digital artwork inscribed on Bitcoin', 'Physical and digital ownership transferred together', ], views: ladyRedhornsPocGalleryViews('poc-2', 10), }, 'lady-redhorns-proof-of-chaos-3': { defaultIndex: 0, pillText: 'Proof of Chaos #3 · Acrylic on canvas', footNote: 'Photos of the physical painting (40 × 40 cm)', tagline: 'Acrylic on canvas · inscribed on Bitcoin', includedItems: [ 'Original 40 × 40 cm acrylic on canvas', 'Digital artwork inscribed on Bitcoin', 'Physical and digital ownership transferred together', ], views: ladyRedhornsPocGalleryViews('poc-3', 7), }, }; function ladyRedhornsGalleryAuctionConfig(slug) { return LADY_REDHORNS_GALLERY_BY_SLUG[slug] || null; } function setupLadyRedhornsAuctionUI(auctionSlug) { const cfg = ladyRedhornsGalleryAuctionConfig(auctionSlug); if (!cfg) return; document.body.classList.add('lady-redhorns-auction'); const frame = document.querySelector('#app .auction-redesign .mint-art-frame'); if (!frame) return; const views = cfg.views; let idx = typeof cfg.defaultIndex === 'number' ? cfg.defaultIndex : 0; if (idx < 0 || idx >= views.length) idx = 0; const diamondSvg = ''; const expandSvg = ''; frame.innerHTML = ` `; const thumbsRoot = document.getElementById('rh-gallery-thumbs'); const mainImg = document.getElementById('rh-auction-main-view'); const renderThumbs = () => { if (!thumbsRoot || !mainImg) return; thumbsRoot.innerHTML = views.map((v, i) => { const thumbSrc = v.thumb || v.src; return `
    ${v.label}
    `; }).join(''); thumbsRoot.querySelectorAll('.rh-thumb').forEach((btn) => { btn.addEventListener('click', () => { idx = parseInt(btn.getAttribute('data-index'), 10) || 0; mainImg.src = views[idx].src; renderThumbs(); }); }); }; renderThumbs(); const expandBtn = document.getElementById('rh-gallery-expand'); if (expandBtn && mainImg) { expandBtn.addEventListener('click', () => { window.open(mainImg.src, '_blank', 'noopener,noreferrer'); }); } const prevBtn = document.querySelector('#rh-auction-gallery .rh-thumb-prev'); const nextBtn = document.querySelector('#rh-auction-gallery .rh-thumb-next'); if (prevBtn && mainImg) { prevBtn.addEventListener('click', () => { idx = (idx + views.length - 1) % views.length; mainImg.src = views[idx].src; renderThumbs(); }); } if (nextBtn && mainImg) { nextBtn.addEventListener('click', () => { idx = (idx + 1) % views.length; mainImg.src = views[idx].src; renderThumbs(); }); } const stages = document.querySelector('#auction-phase-bar .mint-phase-stages'); if (stages && !stages.querySelector('.rh-auction-phase-eyebrow')) { const eyebrow = document.createElement('span'); eyebrow.className = 'rh-auction-phase-eyebrow'; eyebrow.textContent = 'LunaLauncher · Timed auction'; stages.appendChild(eyebrow); } const artistRow = document.querySelector('#app .mint-artist-row'); if (cfg.tagline && artistRow && !document.getElementById('rh-auction-tagline')) { const tag = document.createElement('p'); tag.id = 'rh-auction-tagline'; tag.className = 'rh-auction-tagline'; tag.textContent = cfg.tagline; artistRow.insertAdjacentElement('afterend', tag); } const statusBox = document.getElementById('auction-status-box'); if (statusBox && cfg.includedItems && cfg.includedItems.length && !document.getElementById('rh-included-section')) { const section = document.createElement('div'); section.className = 'mint-section rh-included-section'; section.id = 'rh-included-section'; const listHtml = cfg.includedItems.map((line) => '
  • ' + String(line).replace(/').join(''); section.innerHTML = `

    Included with winning bid

    `; const statusSection = statusBox.closest('.mint-section'); if (statusSection && statusSection.parentNode) { statusSection.parentNode.insertBefore(section, statusSection); } } } // Show auction page async function showAuctionPage(auctionSlug) { currentPage = 'auction'; const template = document.getElementById('auction-template'); const app = document.getElementById('app'); if (!template || !app) return; app.innerHTML = template.innerHTML; setupLadyRedhornsAuctionUI(auctionSlug); const auctionNav = document.getElementById('auction-page-nav'); if (auctionNav) { if (auctionNavScrollHandler) { window.removeEventListener('scroll', auctionNavScrollHandler); auctionNavScrollHandler = null; } auctionNavScrollHandler = () => { const n = document.getElementById('auction-page-nav'); if (!n || currentPage !== 'auction') return; n.classList.toggle('scrolled', window.scrollY > 24); }; window.addEventListener('scroll', auctionNavScrollHandler, { passive: true }); auctionNavScrollHandler(); } // Apply custom styling for specific auctions if (auctionSlug === 'pizza-comrades-lady-lord-valentine' || auctionSlug === 'pizza-comrades-lil-pat') { document.body.classList.add('pizza-comrades-auction'); // Add custom styles const pizzaStyle = document.createElement('style'); pizzaStyle.setAttribute('data-pizza-comrades-style', 'true'); pizzaStyle.textContent = ` body.pizza-comrades-auction { background: #000000 !important; } body.pizza-comrades-auction #app { position: relative; z-index: 1; background: #FE5403; min-height: calc(100vh - 60px); padding-top: 30px; padding-bottom: 50px; } body.pizza-comrades-auction #app .main-layout.mint-redesign { background: #000000; position: relative; z-index: 2; } #pizza-comrades-bg { width: 25vw !important; height: 35vw !important; min-width: 150px; min-height: 200px; max-width: 400px; max-height: 550px; } @media (max-width: 768px) { #pizza-comrades-bg { width: 30vw !important; height: 42vw !important; min-width: 100px; min-height: 140px; } } @media (max-width: 480px) { #pizza-comrades-bg { width: 25vw !important; height: 35vw !important; min-width: 80px; min-height: 110px; } } `; document.head.appendChild(pizzaStyle); // Add background image element to the app container const bgImg = document.createElement('div'); bgImg.id = 'pizza-comrades-bg'; bgImg.style.cssText = ` position: fixed; bottom: 0; left: 0; width: 300px; height: 400px; background-image: url('/pizza_comrades/pizza_comrade.png'); background-size: contain; background-repeat: no-repeat; background-position: bottom left; z-index: 1; pointer-events: none; opacity: 1; image-rendering: pixelated; `; app.appendChild(bgImg); } // Clear any existing countdown if (auctionCountdownInterval) { clearInterval(auctionCountdownInterval); } try { const response = await fetch(`/api/get-auction?slug=${encodeURIComponent(auctionSlug)}`); const data = await response.json(); if (!data.success || !data.auction) { app.innerHTML = `

    Auction Not Found

    This auction does not exist or is no longer active.

    ← Back to Home
    `; return; } currentAuction = data.auction; // Update artwork display const artworkContainer = document.getElementById('auction-artwork-container'); if (ladyRedhornsGalleryAuctionConfig(auctionSlug)) { const mainView = document.getElementById('rh-auction-main-view'); if (mainView) { mainView.alt = currentAuction.title || 'Auction artwork'; } } else if (artworkContainer) { const imageSrc = auctionSlug === 'pizza-comrades-lady-lord-valentine' ? '/pizza_comrades/auction.png' : auctionSlug === 'pizza-comrades-lil-pat' ? '/pizza_comrades/Lil_Pat_BIG.png' : (currentAuction.inscriptionId ? `https://ordinals.com/preview/${currentAuction.inscriptionId}` : null); if (imageSrc) { if (auctionSlug === 'pizza-comrades-lady-lord-valentine' || auctionSlug === 'pizza-comrades-lil-pat') { artworkContainer.innerHTML = `${currentAuction.title}`; } else { artworkContainer.innerHTML = ` `; } } } // Update auction details document.getElementById('auction-title').textContent = currentAuction.title; document.getElementById('auction-artist').textContent = `by ${currentAuction.artist}`; const auctionDescEl = document.getElementById('auction-description'); auctionDescEl.textContent = currentAuction.description || ''; auctionDescEl.style.display = currentAuction.description ? 'block' : 'none'; // Update bid info document.getElementById('auction-high-bid').textContent = `${currentAuction.currentHighBidSats.toLocaleString()} sats`; document.getElementById('auction-starting-price').textContent = `${currentAuction.startingPriceSats.toLocaleString()} sats`; // Reserve status const reserveRow = document.getElementById('auction-reserve-row'); if (currentAuction.reservePriceSats) { reserveRow.style.display = 'flex'; document.getElementById('auction-reserve-status').textContent = currentAuction.reserveMet ? 'Met' : 'Not met'; document.getElementById('auction-reserve-status').style.color = currentAuction.reserveMet ? '#00ff00' : '#888'; } // Status badge (mint-style phase pill) const statusBadge = document.getElementById('auction-status-badge'); statusBadge.className = 'mint-phase-stage'; if (currentAuction.status === 'active') { statusBadge.textContent = 'Live'; statusBadge.classList.add('mint-phase-active', 'auction-badge-live'); } else if (currentAuction.status === 'upcoming') { statusBadge.textContent = 'Upcoming'; statusBadge.classList.add('auction-badge-upcoming'); } else { statusBadge.textContent = 'Ended'; statusBadge.classList.add('auction-badge-ended'); } // End time const endDate = new Date(currentAuction.auctionEndDatetime); document.getElementById('auction-end-time').textContent = endDate.toLocaleString(); // Set up countdown updateAuctionCountdown(); auctionCountdownInterval = setInterval(updateAuctionCountdown, 1000); // Set minimum bid const minBid = Math.max(currentAuction.startingPriceSats, currentAuction.currentHighBidSats + 1000); document.getElementById('auction-bid-amount').value = minBid; document.getElementById('auction-bid-amount').min = minBid; updatePaymentAmount(); // Set up bid amount change listener document.getElementById('auction-bid-amount').addEventListener('input', updatePaymentAmount); // Update leaderboard updateAuctionLeaderboard(data.bids); // Handle different auction states if (currentAuction.status === 'ended' || currentAuction.status === 'settled') { // Hide bid section and show ended section document.getElementById('auction-bid-section').style.display = 'none'; document.getElementById('auction-ended-section').style.display = 'block'; // Show winning bid info if (data.bids && data.bids.length > 0) { const winningBid = data.bids[0]; // Bids are sorted highest first document.getElementById('auction-winning-amount').textContent = `${winningBid.bidAmountSats.toLocaleString()} sats`; document.getElementById('auction-winning-wallet').textContent = winningBid.walletAddress; document.getElementById('auction-winning-taproot').textContent = winningBid.taprootAddress || 'Not provided'; } else { document.getElementById('auction-winning-amount').textContent = 'No bids'; document.getElementById('auction-winning-wallet').textContent = ''; document.getElementById('auction-winning-taproot').textContent = ''; } } else if (currentAuction.status === 'upcoming') { // Disable bidding for upcoming auctions document.getElementById('auction-bid-section').style.opacity = '0.5'; document.getElementById('auction-connect-btn').disabled = true; document.getElementById('auction-connect-btn').textContent = 'Auction Not Started'; } } catch (error) { console.error('Error loading auction:', error); app.innerHTML = `

    Error Loading Auction

    ${error.message}

    ← Back to Home
    `; } } function updateAuctionCountdown() { if (!currentAuction) return; const now = new Date(); const endDate = new Date(currentAuction.auctionEndDatetime); const startDate = new Date(currentAuction.auctionStartDatetime); let targetDate = currentAuction.status === 'upcoming' ? startDate : endDate; let remaining = targetDate.getTime() - now.getTime(); if (remaining <= 0) { document.getElementById('auction-time-remaining').textContent = currentAuction.status === 'upcoming' ? 'Starting soon...' : 'Ended'; if (auctionCountdownInterval) { clearInterval(auctionCountdownInterval); } // Refresh page to update status if (currentAuction.status === 'active' || currentAuction.status === 'upcoming') { setTimeout(() => location.reload(), 2000); } return; } const days = Math.floor(remaining / (1000 * 60 * 60 * 24)); const hours = Math.floor((remaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((remaining % (1000 * 60)) / 1000); let timeStr = ''; if (days > 0) timeStr += `${days}d `; if (hours > 0 || days > 0) timeStr += `${hours}h `; timeStr += `${minutes}m ${seconds}s`; document.getElementById('auction-time-remaining').textContent = timeStr; } function updatePaymentAmount() { const bidAmount = parseInt(document.getElementById('auction-bid-amount').value) || 0; let paymentAmount = bidAmount; // If user has existing bid, they only pay the difference if (auctionUserBid) { paymentAmount = Math.max(0, bidAmount - auctionUserBid.bidAmountSats); document.getElementById('auction-your-current-bid').style.display = 'block'; document.getElementById('your-bid-amount').textContent = `${auctionUserBid.bidAmountSats.toLocaleString()} sats`; document.getElementById('auction-bid-topup-note').style.display = 'block'; } document.getElementById('auction-payment-amount').textContent = `${paymentAmount.toLocaleString()} sats`; } function updateAuctionLeaderboard(bids) { const leaderboard = document.getElementById('auction-leaderboard'); if (!bids || bids.length === 0) { leaderboard.innerHTML = '

    No bids yet. Be the first!

    '; return; } leaderboard.innerHTML = bids.map((bid, index) => { const isCurrentUser = auctionConnectedWallet && bid.walletAddress === auctionConnectedWallet; const rankIcon = `#${index + 1}`; const escWallet = (bid.walletAddress || '').replace(/"/g, '"'); const escTaproot = (bid.taprootAddress || '').replace(/"/g, '"'); const youClass = isCurrentUser ? ' auction-lb-you' : ''; return `
    ${rankIcon} ${escWallet}
    ${escTaproot ? `ord: ${escTaproot}` : ''}
    ${bid.bidAmountSats.toLocaleString()} sats
    `; }).join(''); } let auctionConnectedTaproot = null; // Store taproot/ordinals address window.connectWalletForAuction = async function() { try { const connectBtn = document.getElementById('auction-connect-btn'); connectBtn.innerHTML = ' Connecting...'; connectBtn.disabled = true; window.walletProvider = 'xverse'; // Use sats-connect library (same as mint pages) const response = await request('wallet_connect', null); if (response.status !== 'success') { throw new Error('Wallet connection failed.'); } // Find payment address const paymentAddressInfo = response.result.addresses.find(a => a.purpose === AddressPurpose.Payment); if (!paymentAddressInfo) { throw new Error('No payment address found in wallet.'); } auctionConnectedWallet = paymentAddressInfo.address; // Find ordinals/taproot address const ordinalsAddressInfo = response.result.addresses.find(a => a.purpose === AddressPurpose.Ordinals); auctionConnectedTaproot = ordinalsAddressInfo?.address || null; // Check if user has existing bid if (currentAuction) { const auctionResponse = await fetch(`/api/get-auction?slug=${encodeURIComponent(currentAuction.slug)}`); const data = await auctionResponse.json(); if (data.success && data.bids) { auctionUserBid = data.bids.find(b => b.walletAddress === auctionConnectedWallet); updatePaymentAmount(); updateAuctionLeaderboard(data.bids); } } // Show payment section document.getElementById('auction-connect-section').style.display = 'none'; document.getElementById('auction-payment-section').style.display = 'block'; document.getElementById('auction-connected-wallet').textContent = `${auctionConnectedWallet.substring(0, 8)}...${auctionConnectedWallet.slice(-6)}`; } catch (error) { console.error('Wallet connection error:', error); alert('Failed to connect wallet: ' + error.message); const connectBtn = document.getElementById('auction-connect-btn'); connectBtn.textContent = 'Connect wallet'; connectBtn.disabled = false; } }; window.placeBid = async function() { if (!currentAuction || !auctionConnectedWallet) { alert('Please connect your wallet first'); return; } const bidAmount = parseInt(document.getElementById('auction-bid-amount').value) || 0; const minBid = Math.max(currentAuction.startingPriceSats, currentAuction.currentHighBidSats + 1000); if (bidAmount < minBid) { alert(`Minimum bid is ${minBid.toLocaleString()} sats`); return; } // Calculate payment amount let paymentAmount = bidAmount; if (auctionUserBid) { paymentAmount = bidAmount - auctionUserBid.bidAmountSats; if (paymentAmount <= 0) { alert('New bid must be higher than your current bid'); return; } } // Create payment transaction const placeBidBtn = document.getElementById('auction-place-bid-btn'); placeBidBtn.disabled = true; placeBidBtn.textContent = 'Sending payment...'; try { const transferResponse = await sendPaymentFromWallet({ address: currentAuction.paymentWallet, amount: paymentAmount }); console.log('Auction bid payment response:', transferResponse); if (transferResponse.status !== 'success') { throw new Error(transferResponse.status === 'canceled' ? 'Payment cancelled' : (transferResponse.error || 'Payment failed')); } const txid = transferResponse.result?.txid; if (!txid) { throw new Error('No transaction ID received from wallet'); } placeBidBtn.textContent = 'Recording bid...'; // Record bid on server const response = await fetch('/api/place-bid', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ auctionSlug: currentAuction.slug, walletAddress: auctionConnectedWallet, taprootAddress: auctionConnectedTaproot, bidAmountSats: bidAmount, paymentTxid: txid }) }); const result = await response.json(); const resultDiv = document.getElementById('auction-bid-result'); if (result.success) { resultDiv.innerHTML = `

    Bid Placed Successfully!

    Your bid: ${bidAmount.toLocaleString()} sats

    Rank: #${result.rank}

    `; // Refresh auction data setTimeout(() => showAuctionPage(currentAuction.slug), 2000); } else { resultDiv.innerHTML = `

    Failed: ${result.error}

    `; } } catch (error) { console.error('Bid error:', error); document.getElementById('auction-bid-result').innerHTML = `

    Error: ${error.message}

    `; } finally { placeBidBtn.disabled = false; placeBidBtn.textContent = 'Place Bid'; } }; // ============ END AUCTION PAGE FUNCTIONS ============ // Setup user portal page function setupUserPortalPage() { loadUserArtworks(); loadUserCollections(); loadUserAuctions(); } // Load user's collections async function loadUserCollections() { try { const response = await fetch('/api/get-my-collections', { credentials: 'include' }); const data = await response.json(); const grid = document.getElementById('user-collections-grid'); if (!grid) return; if (data.success && data.collections && data.collections.length > 0) { grid.innerHTML = data.collections.map(collection => { const liveCount = collection.live_count || 0; const completedCount = collection.artwork_count - liveCount; return `

    ${collection.name}

    by ${collection.artist_name || 'Unknown'}

    ${collection.artwork_count} artwork${collection.artwork_count !== 1 ? 's' : ''}

    ${liveCount} currently live

    ${completedCount} completed

    ${collection.description ? `

    ${collection.description}

    ` : ''}
    `}).join(''); } else { grid.innerHTML = '

    No collections yet. Create one to group your artworks!

    '; } } catch (error) { console.error('Error loading collections:', error); const grid = document.getElementById('user-collections-grid'); if (grid) { grid.innerHTML = '

    Failed to load collections

    '; } } } window.deleteCollection = async function(collectionId, collectionName) { if (!confirm(`Are you sure you want to delete "${collectionName}"? Artworks in this collection will become standalone.`)) { return; } try { const response = await fetch('/api/manage-collection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ action: 'delete', collectionId: collectionId }) }); const data = await response.json(); if (data.success) { alert(data.message); if (currentPage === 'user-portal') { loadUserCollections(); loadUserArtworks(); loadUserAuctions(); } else if (currentUser?.role === 'admin') { loadCollectionsForAdmin(); } } else { alert('Error: ' + data.error); } } catch (error) { console.error('Error deleting collection:', error); alert('Failed to delete collection'); } }; // Load collections for admin async function loadCollectionsForAdmin() { try { const response = await fetch('/api/get-my-collections', { credentials: 'include' }); const data = await response.json(); const list = document.getElementById('collections-admin-list'); if (!list) return; if (data.success && data.collections && data.collections.length > 0) { list.innerHTML = data.collections.map(collection => { const liveCount = collection.live_count || 0; const completedCount = collection.artwork_count - liveCount; return `

    ${collection.name}

    by ${collection.artist_name || 'Unknown'}

    ${collection.artwork_count} artwork${collection.artwork_count !== 1 ? 's' : ''}

    ${liveCount} currently live

    ${completedCount} completed

    ${collection.description ? `

    ${collection.description}

    ` : ''}

    Slug: ${collection.slug}

    ${collection.is_active ? '● Active' : '○ Inactive'}

    `}).join(''); } else { list.innerHTML = '

    No collections yet. Create one to group artworks!

    '; } } catch (error) { console.error('Error loading collections:', error); const list = document.getElementById('collections-admin-list'); if (list) { list.innerHTML = '

    Failed to load collections

    '; } } } // Global variable to store payment details let userPaymentDetails = {}; // Load user's own artworks and collections async function loadUserArtworks() { try { // Fetch both artworks and collections const [artworksResponse, collectionsResponse] = await Promise.all([ fetch('/api/my-artworks', { credentials: 'include' }), fetch('/api/get-my-collections', { credentials: 'include' }) ]); const artworksData = await artworksResponse.json(); const collectionsData = await collectionsResponse.json(); const artworks = (artworksData.success && artworksData.artworks) ? artworksData.artworks : []; const collections = (collectionsData.success && collectionsData.collections) ? collectionsData.collections : []; userPaymentDetails = artworksData.paymentDetails || {}; displayUserArtworks(artworks, collections); } catch (error) { const userList = document.getElementById('user-artworks-list'); if (userList) { userList.innerHTML = '

    Failed to load your submissions

    '; } } } // Display user's artworks and collections function displayUserArtworks(artworks, collections = []) { const userList = document.getElementById('user-artworks-list'); if (!userList) return; if (artworks.length === 0 && collections.length === 0) { userList.innerHTML = '

    You haven\'t submitted any artworks yet. Submit your first artwork

    '; return; } let html = ''; // Display collections first if (collections.length > 0) { html += `

    Your Collections

    `; html += collections.map(collection => { const liveCount = collection.live_count || 0; const completedCount = collection.artwork_count - liveCount; const allCompleted = liveCount === 0 && collection.artwork_count > 0; return `
    ${collection.collection_image ? `${collection.name}` : `
    📂
    ` }

    ${collection.name}

    COLLECTION
    ${collection.artist_name ? `

    Artist: ${collection.artist_name}

    ` : ''}

    Artworks: ${collection.artwork_count}

    Currently Live: ${liveCount}

    Completed: ${completedCount}

    ${allCompleted ? `

    ✅ Collection Complete

    ` : collection.is_active ? `

    ✅ Live on marketplace

    ` : `

    ⏳ Pending admin approval

    ` } ${collection.description ? `

    ${collection.description}

    ` : ''}
    `; }).join(''); html += `

    Your Individual Artworks

    `; } // Display individual artworks if (artworks.length > 0) { html += artworks.map(artwork => `
    ${artwork.mintType === 'pre_inscribed' || artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints' || artwork.mintType === 'numerical' ? `${artwork.title} ${artwork.artist}` : artwork.parentType === 'recursive' ? `
    ` : artwork.parentInscriptionId ? `${artwork.title || 'Unknown'} by ${artwork.artist || 'Unknown'}` : `
    NO PREVIEW
    ` }

    ${artwork.title}

    ${artwork.status}

    Artist: ${artwork.artist}

    Price: ${artwork.price === 0 ? 'FREE' : artwork.price + ' sats'}

    Minted: ${artwork.minted} / ${artwork.total === -1 ? '∞' : artwork.total}

    ${artwork.earnings && artwork.earnings.totalEarningsSats > 0 ? `

    💰 YOUR EARNINGS

    ${artwork.earnings.paymentCount > 0 ? `` : ''}
    Total Earned: ${artwork.earnings.totalEarningsSats.toLocaleString()} sats
    Total Paid: ${artwork.earnings.totalPaidSats.toLocaleString()} sats
    Outstanding: ${artwork.earnings.outstandingBalanceSats.toLocaleString()} sats
    ${artwork.earnings.paymentCount} payment${artwork.earnings.paymentCount !== 1 ? 's' : ''} ${artwork.earnings.paymentPercentage}% paid
    ${artwork.artistPaymentWallet ? `

    Payment Wallet: ${artwork.artistPaymentWallet.substring(0, 20)}...

    ` : ''}
    ` : ''} ${artwork.isActive ? `

    ✅ Live on marketplace

    ` : `

    ⏳ Pending admin approval

    ` } ${artwork.description ? `

    ${formatDescription(artwork.description)}

    ` : ''}
    ${artwork.twitterUrl ? ` ` : ''} ${artwork.discordUrl ? ` ` : ''} ${artwork.websiteUrl ? ` ` : ''} ${(artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints') ? `` : ''} ${artwork.isActive ? `` : ''} ${artwork.isActive ? `` : ''} ${(!artwork.isActive || artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints') ? `` : ''} ${artwork.canDelete ? `` : ''}
    `).join(''); } userList.innerHTML = html; } // Toggle payment history visibility for artists window.togglePaymentHistory = function(artworkId) { const historyDiv = document.getElementById(`payment-history-${artworkId}`); if (!historyDiv) return; if (historyDiv.style.display === 'none') { // Show payment history const payments = userPaymentDetails[artworkId] || []; if (payments.length > 0) { historyDiv.innerHTML = `

    PAYMENT HISTORY

    ${payments.map(payment => `
    ${payment.amountSats.toLocaleString()} sats ${new Date(payment.paymentDate).toLocaleDateString()}

    Transaction: ${payment.mempoolTxId.substring(0, 16)}...

    To Wallet: ${payment.paymentWalletAddress.substring(0, 20)}...

    ${payment.notes ? `

    Notes: ${payment.notes}

    ` : ''}

    Recorded by ${payment.recordedBy} on ${new Date(payment.recordedAt).toLocaleDateString()}

    `).join('')} `; } else { historyDiv.innerHTML = '

    No payment records found

    '; } historyDiv.style.display = 'block'; } else { // Hide payment history historyDiv.style.display = 'none'; } }; // Load user's own auctions async function loadUserAuctions() { try { const response = await fetch('/api/get-my-auctions', { credentials: 'include' }); const data = await response.json(); const grid = document.getElementById('user-auctions-grid'); if (!grid) return; if (data.success && data.auctions && data.auctions.length > 0) { grid.innerHTML = data.auctions.map(auction => { const statusColors = { 'pending_approval': '#ffcc02', 'upcoming': '#00ccff', 'live': '#00ff00', 'ended': '#ff6600', 'settled': '#999999' }; const statusLabels = { 'pending_approval': 'Pending Approval', 'upcoming': 'Upcoming', 'live': 'LIVE', 'ended': 'Ended', 'settled': 'Settled' }; return `
    ${auction.inscriptionId ? `${auction.title}` : `
    NO INSCRIPTION
    ` }

    ${auction.title}

    ${statusLabels[auction.status] || auction.status}

    Artist: ${auction.artist}

    Starting Price: ${auction.startingPriceSats ? auction.startingPriceSats.toLocaleString() + ' sats' : 'Not set'}

    ${auction.reservePriceSats ? `

    Reserve: ${auction.reservePriceSats.toLocaleString()} sats

    ` : ''}

    Current High Bid: ${auction.currentHighBidSats ? auction.currentHighBidSats.toLocaleString() + ' sats' : 'No bids'}

    Start: ${new Date(auction.auctionStartDatetime).toLocaleString()}

    End: ${new Date(auction.auctionEndDatetime).toLocaleString()}

    ${auction.canEdit ? `` : ''} ${auction.canDelete ? `` : ''}
    `; }).join(''); } else { grid.innerHTML = '

    No auctions yet. Submit an auction

    '; } } catch (error) { console.error('Error loading user auctions:', error); const grid = document.getElementById('user-auctions-grid'); if (grid) { grid.innerHTML = '

    Failed to load auctions

    '; } } } // Show edit auction modal window.showEditAuctionModal = function(auctionId, inscriptionId, startingPrice, reservePrice, startDatetime, endDatetime) { document.getElementById('editAuctionId').value = auctionId; document.getElementById('editAuctionInscriptionId').value = inscriptionId || ''; document.getElementById('editAuctionStartingPrice').value = startingPrice || 0; document.getElementById('editAuctionReservePrice').value = reservePrice || ''; // Convert ISO datetime to datetime-local format if (startDatetime) { const startDate = new Date(startDatetime); document.getElementById('editAuctionStartDatetime').value = startDate.toISOString().slice(0, 16); } if (endDatetime) { const endDate = new Date(endDatetime); document.getElementById('editAuctionEndDatetime').value = endDate.toISOString().slice(0, 16); } document.getElementById('edit-auction-modal').style.display = 'block'; }; // Hide edit auction modal window.hideEditAuctionModal = function() { document.getElementById('edit-auction-modal').style.display = 'none'; }; // Save auction edit window.saveAuctionEdit = async function() { const auctionId = document.getElementById('editAuctionId').value; const inscriptionId = document.getElementById('editAuctionInscriptionId').value; const startingPrice = parseInt(document.getElementById('editAuctionStartingPrice').value) || 0; const reservePriceVal = document.getElementById('editAuctionReservePrice').value; const reservePrice = reservePriceVal ? parseInt(reservePriceVal) : null; const startDatetimeVal = document.getElementById('editAuctionStartDatetime').value; const endDatetimeVal = document.getElementById('editAuctionEndDatetime').value; if (!auctionId) { alert('No auction selected'); return; } if (!endDatetimeVal) { alert('End date/time is required'); return; } try { const response = await fetch('/api/update-my-auction', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ auctionId: parseInt(auctionId), inscriptionId: inscriptionId || null, startingPrice, reservePrice, auctionStartDatetime: startDatetimeVal ? new Date(startDatetimeVal).toISOString() : null, auctionEndDatetime: new Date(endDatetimeVal).toISOString() }) }); const data = await response.json(); if (data.success) { alert('Auction updated successfully!'); hideEditAuctionModal(); loadUserAuctions(); } else { alert('Error: ' + (data.error || 'Failed to update auction')); } } catch (error) { console.error('Error updating auction:', error); alert('Failed to update auction'); } }; // Delete user auction window.deleteUserAuction = async function(auctionId, title) { if (!confirm(`Are you sure you want to delete the auction "${title}"? This cannot be undone.`)) { return; } try { const response = await fetch('/api/delete-my-auction', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ auctionId }) }); const data = await response.json(); if (data.success) { alert('Auction deleted successfully!'); loadUserAuctions(); } else { alert('Error: ' + (data.error || 'Failed to delete auction')); } } catch (error) { console.error('Error deleting auction:', error); alert('Failed to delete auction'); } }; // Setup admin page functionality function setupAdminPage() { // Set up create new artwork button const createBtn = document.getElementById('createArtworkBtn'); if (createBtn) { createBtn.onclick = () => showArtworkModal(); } // Set up modal form buttons const saveBtn = document.getElementById('saveArtworkBtn'); if (saveBtn) saveBtn.onclick = saveArtwork; // Initialize with artworks tab active switchAdminTab('artworks'); } // Switch between payment sub-tabs window.switchPaymentSubTab = function(subTabName) { const listContent = document.getElementById('payments-list-content'); const incomeContent = document.getElementById('income-content'); const reconciliationContent = document.getElementById('reconciliation-content'); const listBtn = document.getElementById('payments-list-subtab'); const incomeBtn = document.getElementById('income-subtab'); const reconciliationBtn = document.getElementById('reconciliation-subtab'); if (!listContent || !incomeContent || !reconciliationContent || !listBtn || !incomeBtn || !reconciliationBtn) return; // Hide all content listContent.style.display = 'none'; incomeContent.style.display = 'none'; reconciliationContent.style.display = 'none'; // Reset button styles listBtn.style.background = '#000000'; listBtn.style.color = '#ffffff'; listBtn.style.border = '2px solid #ffffff'; incomeBtn.style.background = '#000000'; incomeBtn.style.color = '#ffffff'; incomeBtn.style.border = '2px solid #ffffff'; reconciliationBtn.style.background = '#000000'; reconciliationBtn.style.color = '#ffffff'; reconciliationBtn.style.border = '2px solid #ffffff'; // Show selected content and highlight button if (subTabName === 'list') { listContent.style.display = 'block'; listBtn.style.background = '#ffffff'; listBtn.style.color = '#000000'; listBtn.style.border = 'none'; loadAllPayments(); } else if (subTabName === 'income') { incomeContent.style.display = 'block'; incomeBtn.style.background = '#ffffff'; incomeBtn.style.color = '#000000'; incomeBtn.style.border = 'none'; loadIncomeOverview(); } else if (subTabName === 'reconciliation') { reconciliationContent.style.display = 'block'; reconciliationBtn.style.background = '#ffffff'; reconciliationBtn.style.color = '#000000'; reconciliationBtn.style.border = 'none'; loadWalletReconciliation(1); } }; // Load all payments for admin window.loadAllPayments = async function() { const container = document.getElementById('all-payments-list'); if (!container) return; container.innerHTML = '

    Loading payments...

    '; try { const searchTerm = document.getElementById('payments-search')?.value || ''; const response = await fetch(`/api/admin-payments?search=${encodeURIComponent(searchTerm)}&limit=50&offset=0`, { credentials: 'include' }); const data = await response.json(); if (data.success && data.payments) { displayAllPayments(data.payments, data.total); } else { container.innerHTML = '

    Failed to load payments: ' + (data.error || 'Unknown error') + '

    '; } } catch (error) { console.error('Failed to load payments:', error); container.innerHTML = '

    Failed to load payments

    '; } }; // Display all payments function displayAllPayments(payments, total) { const container = document.getElementById('all-payments-list'); if (!container) return; if (payments.length === 0) { container.innerHTML = '

    No payments found

    '; return; } container.innerHTML = `

    Total Payments: ${total}

    ${payments.map(payment => `
    ${payment.amountSats.toLocaleString()} sats ${new Date(payment.paymentDate).toLocaleDateString()}
    ${payment.artworkTitle} by ${payment.artworkArtist}
    Artist: ${payment.artistUsername || 'Unknown'} (${payment.artistEmail || 'No email'})
    Transaction: ${payment.mempoolTxId.substring(0, 20)}...
    To Wallet: ${payment.paymentWalletAddress.substring(0, 30)}...
    ${payment.amountSats > (payment.artworkPrice || 0) ? `
    Recorded amount may include fees; for artist share use price × mints.
    ` : ''} ${payment.notes ? `
    Notes: ${payment.notes}
    ` : ''}
    Recorded by ${payment.recordedBy} on ${new Date(payment.recordedAt).toLocaleDateString()}
    `).join('')} `; } // Load income overview window.loadIncomeOverview = async function() { const container = document.getElementById('income-overview'); if (!container) return; container.innerHTML = '

    Loading income data...

    '; try { const response = await fetch('/api/admin-income', { credentials: 'include' }); const data = await response.json(); if (data.success && data.income) { // Store income data globally for wallet verification window.currentIncomeData = data.income; displayIncomeOverview(data.income); // Automatically check wallet balance after loading income data setTimeout(() => { verifyMarketplaceWallet(); }, 500); } else { container.innerHTML = '

    Failed to load income data: ' + (data.error || 'Unknown error') + '

    '; } } catch (error) { console.error('Failed to load income:', error); container.innerHTML = '

    Failed to load income data

    '; } }; // Display income overview function displayIncomeOverview(income) { const container = document.getElementById('income-overview'); if (!container) return; container.innerHTML = `

    💰 MARKETPLACE WALLET OVERVIEW

    ${income.totalPlatformIncome.toLocaleString()}

    TOTAL INCOME

    ${income.totalPlatformExpenses.toLocaleString()}

    TOTAL EXPENSES

    ${income.netPlatformBalance.toLocaleString()}

    NET BALANCE

    MARKETPLACE FEES

    ${income.totalMarketplaceIncome.toLocaleString()} sats

    ${income.totalMints} mints × 2,000 sats

    PHYSICAL PRINT FEES

    ${income.totalPhysicalPrintFees.toLocaleString()} sats

    Estimated from print orders

    TOTAL EXPENSES (BLOCKCHAIN)

    ${income.totalPlatformExpenses.toLocaleString()} sats

    All outgoing transactions

    TRACKED EXPENSES

    ${income.totalTrackedExpenses.toLocaleString()} sats

    Artist + custom payments

    OWED TO ARTISTS

    ${income.totalOwedToArtists.toLocaleString()} sats

    Outstanding balance

    ${income.totalCustomPayments > 0 ? `

    CUSTOM PAYMENTS

    ${income.totalCustomPayments.toLocaleString()} sats

    ${income.customPaymentCount} wages/expenses

    ` : ''}

    📊 INCOME BREAKDOWN

    ARTIST EARNINGS

    Total Earned: ${income.totalArtistEarnings.toLocaleString()} sats

    Paid Out: ${income.totalPaidToArtists.toLocaleString()} sats

    Outstanding: ${income.totalOwedToArtists.toLocaleString()} sats

    PLATFORM WALLET

    Income: ${income.totalPlatformIncome.toLocaleString()} sats

    Total Expenses: ${income.totalPlatformExpenses.toLocaleString()} sats

    ├─ Tracked: ${income.totalTrackedExpenses.toLocaleString()} sats

    ${income.totalUntrackedExpenses > 0 ? `

    ├─ Untracked: ${income.totalUntrackedExpenses.toLocaleString()} sats

    ` : ''}

    Net Balance: ${income.netPlatformBalance.toLocaleString()} sats

    TOP EARNING ARTWORKS

    ${income.topArtworks.slice(0, 5).map(artwork => `
    ${artwork.title} ${artwork.artist}
    ${artwork.mints} mints × ${artwork.price} sats
    ${artwork.totalEarned.toLocaleString()} sats
    ${artwork.paymentPercentage}% paid
    `).join('')}

    RECENT PAYMENTS

    ${income.recentPayments.slice(0, 5).map(payment => `
    ${payment.amountSats.toLocaleString()} sats → ${payment.artworkTitle} by ${payment.artworkArtist}
    ${new Date(payment.paymentDate).toLocaleDateString()}
    by ${payment.recordedBy}
    `).join('')}
    `; } // Verify marketplace wallet balance against blockchain (using hardcoded address) window.verifyMarketplaceWallet = async function() { const MARKETPLACE_WALLET = 'bc1qcefl3uc75wh39wcwqs4ca9xw8m7jhc35t3kv43'; const resultsDiv = document.getElementById('wallet-balance-results'); if (!resultsDiv) return; resultsDiv.innerHTML = '

    Checking blockchain...

    '; try { const response = await fetch('/api/check-wallet-balance', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ walletAddress: MARKETPLACE_WALLET }) }); const data = await response.json(); if (data.success && data.wallet) { const wallet = data.wallet; // Get current calculated balance from income overview const calculatedBalance = getCurrentCalculatedBalance(); const difference = wallet.balance - calculatedBalance; const isAccurate = Math.abs(difference) < 1000; // Within 1000 sats tolerance resultsDiv.innerHTML = `

    ${wallet.balance.toLocaleString()}

    ACTUAL BALANCE

    ${calculatedBalance.toLocaleString()}

    CALCULATED

    ${difference >= 0 ? '+' : ''}${difference.toLocaleString()}

    DIFFERENCE

    ${isAccurate ? '✅ Balance matches calculations' : '⚠️ Balance discrepancy detected'}

    ${!isAccurate ? `

    Check for missing payments or untracked transactions

    ` : ''}

    Total Received: ${wallet.totalReceived.toLocaleString()} sats

    Total Sent: ${wallet.totalSent.toLocaleString()} sats

    Transactions: ${wallet.transactionCount}

    API Source: ${wallet.apiSource}

    Last Checked: ${new Date(data.timestamp).toLocaleString()}

    `; } else { resultsDiv.innerHTML = `

    Failed to fetch wallet data: ${data.error || 'Unknown error'}

    `; } } catch (error) { console.error('Failed to verify wallet balance:', error); resultsDiv.innerHTML = `

    Error checking wallet: ${error.message}

    `; } }; // Get current calculated balance from the income overview function getCurrentCalculatedBalance() { // This would be the net platform balance from the income data // For now, return 0 as placeholder - this will be updated when income data is loaded return window.currentIncomeData?.netPlatformBalance || 0; } // Analyze wallet transactions to find untracked payments window.analyzeWalletTransactions = async function() { const section = document.getElementById('transaction-analysis-section'); const resultsDiv = document.getElementById('transaction-analysis-results'); if (!section || !resultsDiv) return; // Show section and loading state section.style.display = 'block'; resultsDiv.innerHTML = '

    Analyzing blockchain transactions...

    '; try { const response = await fetch('/api/analyze-wallet-transactions', { credentials: 'include' }); const data = await response.json(); if (data.success && data.analysis) { const analysis = data.analysis; const transactions = data.transactions; resultsDiv.innerHTML = `
    TRANSACTION ANALYSIS SUMMARY

    ${analysis.incomingTransactionCount}

    INCOMING TXs

    ${analysis.outgoingTransactionCount}

    OUTGOING TXs

    ${analysis.trackedPaymentCount}

    TRACKED

    ${analysis.untrackedOutgoingCount}

    UNTRACKED

    ${analysis.untrackedOutgoingCount > 0 ? `
    ⚠️ UNTRACKED OUTGOING PAYMENTS (${analysis.untrackedOutgoingTotal.toLocaleString()} sats)
    ${transactions.untrackedOutgoing.map(tx => `
    ${tx.amount.toLocaleString()} sats
    ${tx.date ? new Date(tx.date).toLocaleDateString() : 'Pending'} | Block: ${tx.blockHeight || 'Unconfirmed'}
    ${tx.txid.substring(0, 12)}...
    `).join('')}
    ` : `

    ✅ All outgoing transactions are tracked!

    `} ${transactions.unmatchedRecords.length > 0 ? `
    ❌ UNMATCHED DATABASE RECORDS
    ${transactions.unmatchedRecords.map(txId => `
    Transaction not found on blockchain: ${txId}
    `).join('')}
    ` : ''}
    💰 RECENT INCOMING TRANSACTIONS
    ${transactions.recentIncoming.map(tx => `
    +${tx.amount.toLocaleString()} sats ${tx.date ? new Date(tx.date).toLocaleDateString() : 'Pending'}
    ${tx.txid.substring(0, 12)}...
    `).join('')}
    `; } else { resultsDiv.innerHTML = `

    Failed to analyze transactions: ${data.error || 'Unknown error'}

    `; } } catch (error) { console.error('Failed to analyze transactions:', error); resultsDiv.innerHTML = `

    Error analyzing transactions: ${error.message}

    `; } }; // Cleanup expired reservations window.cleanupReservations = async function() { if (!confirm('Clean up expired reservations? This will free up assets that have been reserved but not minted.')) { return; } try { const response = await fetch('/api/cleanup-expired-reservations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include' }); const data = await response.json(); if (data.success) { let message = `Cleanup completed!\n\n`; message += `Rows affected: ${data.cleanup.rowsAffected}\n`; message += `Before: ${data.cleanup.beforeStats.total_reserved} reserved (${data.cleanup.beforeStats.expired_reserved} expired)\n`; message += `After: ${data.cleanup.afterStats.total_reserved} reserved (${data.cleanup.afterStats.expired_reserved} expired)\n`; if (data.remainingReservations.length > 0) { message += `\nRemaining reservations:\n`; data.remainingReservations.forEach(res => { message += `- ${res.artwork_title}: ${res.asset_filename} (expires: ${res.reservation_expires_at}, expired: ${res.is_expired})\n`; }); } alert(message); } else { alert('Failed to cleanup reservations: ' + (data.error || 'Unknown error')); } } catch (error) { alert('Failed to cleanup reservations: ' + error.message); } }; // Debug collection assets window.debugCollectionAssets = async function() { const slug = prompt('Enter collection slug to debug:'); if (!slug) return; try { const response = await fetch(`/api/debug-collection-assets?slug=${slug}`, { credentials: 'include' }); const data = await response.json(); if (data.success) { let message = `DEBUG RESULTS for ${slug}:\n\n`; message += `Artwork: ${data.debug.artwork.title} by ${data.debug.artwork.artist}\n`; message += `Type: ${data.debug.artwork.mint_type}\n`; message += `Active: ${data.debug.artwork.is_active}\n\n`; message += `Total Assets: ${data.debug.totalAssets}\n`; message += `Total Images: ${data.debug.totalImages}\n`; message += `Available Assets: ${data.debug.availableAssets}\n`; message += `Expired Reservations: ${data.debug.expiredReservations}\n\n`; if (data.debug.assetsWithImageInfo.length > 0) { message += `ASSETS:\n`; data.debug.assetsWithImageInfo.forEach((item, i) => { const asset = item.asset; message += `${i+1}. ${asset.original_name} (${asset.asset_filename})\n`; message += ` Index: ${asset.asset_index}, Minted: ${asset.is_minted}, Reserved: ${asset.is_reserved}\n`; message += ` Has Image Match: ${item.hasMatch}\n`; if (item.matchingImage) { message += ` Image: ${item.matchingImage.filename} (R2: ${item.matchingImage.r2_key})\n`; } message += `\n`; }); } alert(message); } else { alert('Debug failed: ' + (data.error || 'Unknown error')); } } catch (error) { alert('Debug failed: ' + error.message); } }; // Add missing payment from untracked transaction window.addMissingPayment = async function(txId, amount) { try { await ensureAdminSensitiveModals(); } catch (e) { console.error(e); alert('Could not load admin UI. Refresh and try again.'); return; } document.getElementById('custom-payment-modal').style.display = 'block'; // Pre-fill form fields document.getElementById('custom-payment-amount').value = amount; document.getElementById('custom-payment-type').value = 'other'; document.getElementById('custom-payment-description').value = `Untracked payment - Transaction ${txId.substring(0, 12)}...`; document.getElementById('custom-payment-tx-id').value = txId; document.getElementById('custom-payment-notes').value = 'Found via transaction analysis - please update description and recipient info'; setupCustomPaymentForm(); alert('Pre-filled custom payment form with transaction data. Please review and update the details before saving.'); }; // Show custom payment modal window.addCustomPayment = async function() { try { await ensureAdminSensitiveModals(); } catch (e) { console.error(e); alert('Could not load admin UI. Refresh and try again.'); return; } document.getElementById('custom-payment-modal').style.display = 'block'; setupCustomPaymentForm(); }; // Hide custom payment modal window.hideCustomPaymentModal = function() { const modal = document.getElementById('custom-payment-modal'); if (modal) modal.style.display = 'none'; const form = document.getElementById('custom-payment-form'); if (form && typeof form.reset === 'function') form.reset(); }; // Set up custom payment form function setupCustomPaymentForm() { const form = document.getElementById('custom-payment-form'); if (form) { // Remove any existing listeners const newForm = form.cloneNode(true); form.parentNode.replaceChild(newForm, form); // Add fresh event listener newForm.addEventListener('submit', async function(e) { e.preventDefault(); const amountSats = parseInt(document.getElementById('custom-payment-amount').value); const paymentType = document.getElementById('custom-payment-type').value; const description = document.getElementById('custom-payment-description').value.trim(); const recipientInfo = document.getElementById('custom-payment-recipient').value.trim(); const mempoolTxId = document.getElementById('custom-payment-tx-id').value.trim(); const paymentWalletAddress = document.getElementById('custom-payment-wallet').value.trim(); const notes = document.getElementById('custom-payment-notes').value.trim(); // Validation if (!amountSats || amountSats <= 0) { alert('Please enter a valid payment amount'); return; } if (!description) { alert('Please enter a description for this payment'); return; } if (!mempoolTxId) { alert('Please enter the mempool transaction ID'); return; } try { const response = await fetch('/api/add-custom-payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ amountSats: amountSats, paymentType: paymentType, description: description, recipientInfo: recipientInfo || null, paymentWalletAddress: paymentWalletAddress || null, mempoolTxId: mempoolTxId, notes: notes || null }) }); const result = await response.json(); if (result.success) { alert('Custom payment recorded successfully!'); hideCustomPaymentModal(); // Refresh income overview if we're on that tab if (typeof loadIncomeOverview === 'function') { loadIncomeOverview(); } // Refresh wallet reconciliation if we're on that tab if (typeof loadWalletReconciliation === 'function') { const reconciliationContent = document.getElementById('reconciliation-content'); if (reconciliationContent && reconciliationContent.style.display === 'block') { // Get current page from pagination to maintain position const paginationContainer = document.getElementById('reconciliation-pagination'); let currentPage = 1; if (paginationContainer && paginationContainer.innerHTML.includes('Page ')) { const pageMatch = paginationContainer.innerHTML.match(/Page (\d+)/); if (pageMatch) { currentPage = parseInt(pageMatch[1]); } } loadWalletReconciliation(currentPage); } } } else { if (result.migrationNeeded) { alert('Custom payments feature not available yet. Database migration needed.'); } else { alert('Failed to record custom payment: ' + result.error); } } } catch (error) { console.error('Failed to add custom payment:', error); alert('Failed to record custom payment: ' + error.message); } }); } } // Load and display custom payments (for future expansion) window.loadCustomPayments = async function() { try { const response = await fetch('/api/admin-custom-payments?limit=20&offset=0', { credentials: 'include' }); const data = await response.json(); if (data.success && data.payments) { // This could be used to show custom payments in a dedicated section } else if (data.migrationNeeded) { // Custom payments table not available yet } else { console.error('Failed to load custom payments:', data.error); } } catch (error) { console.error('Failed to load custom payments:', error); } }; // Switch between admin tabs window.switchAdminTab = function(tabName) { const homeTabContent = document.getElementById('admin-home-tab'); const artworksTabContent = document.getElementById('admin-artworks-tab'); const collectionsTabContent = document.getElementById('admin-collections-tab'); const completedTabContent = document.getElementById('admin-completed-tab'); const onDemandTabContent = document.getElementById('admin-on-demand-tab'); const preInscribedTabContent = document.getElementById('admin-pre-inscribed-tab'); const inquiriesTabContent = document.getElementById('admin-inquiries-tab'); const printsTabContent = document.getElementById('admin-prints-tab'); const paymentsTabContent = document.getElementById('admin-payments-tab'); const supportersTabContent = document.getElementById('admin-supporters-tab'); const statsTabContent = document.getElementById('admin-stats-tab'); const autoSendTabContent = document.getElementById('admin-auto-send-tab'); const autoInscribeTabContent = document.getElementById('admin-auto-inscribe-tab'); const stampsTabContent = document.getElementById('admin-stamps-tab'); const auctionsTabContent = document.getElementById('admin-auctions-tab'); const homeTabBtn = document.getElementById('home-admin-tab'); const artworksTabBtn = document.getElementById('artworks-admin-tab'); const collectionsTabBtn = document.getElementById('collections-admin-tab'); const completedTabBtn = document.getElementById('completed-admin-tab'); const onDemandTabBtn = document.getElementById('on-demand-admin-tab'); const preInscribedTabBtn = document.getElementById('pre-inscribed-admin-tab'); const inquiriesTabBtn = document.getElementById('inquiries-admin-tab'); const printsTabBtn = document.getElementById('prints-admin-tab'); const paymentsTabBtn = document.getElementById('payments-admin-tab'); const supportersTabBtn = document.getElementById('supporters-admin-tab'); const statsTabBtn = document.getElementById('stats-admin-tab'); const autoSendTabBtn = document.getElementById('auto-send-admin-tab'); const autoInscribeTabBtn = document.getElementById('auto-inscribe-admin-tab'); const stampsTabBtn = document.getElementById('stamps-admin-tab'); const auctionsTabBtn = document.getElementById('auctions-admin-tab'); if (!homeTabContent || !artworksTabContent || !collectionsTabContent || !completedTabContent || !onDemandTabContent || !preInscribedTabContent || !inquiriesTabContent || !printsTabContent || !paymentsTabContent || !supportersTabContent || !statsTabContent || !autoSendTabContent || !autoInscribeTabContent || !stampsTabContent || !auctionsTabContent || !homeTabBtn || !artworksTabBtn || !collectionsTabBtn || !completedTabBtn || !onDemandTabBtn || !preInscribedTabBtn || !inquiriesTabBtn || !printsTabBtn || !paymentsTabBtn || !supportersTabBtn || !statsTabBtn || !autoSendTabBtn || !autoInscribeTabBtn || !stampsTabBtn || !auctionsTabBtn) { return; } // Reset all tab buttons homeTabBtn.style.background = '#000000'; homeTabBtn.style.color = '#ffffff'; homeTabBtn.style.border = '2px solid #ffffff'; artworksTabBtn.style.background = '#000000'; artworksTabBtn.style.color = '#ffffff'; artworksTabBtn.style.border = '2px solid #ffffff'; completedTabBtn.style.background = '#000000'; completedTabBtn.style.color = '#ffffff'; completedTabBtn.style.border = '2px solid #ffffff'; onDemandTabBtn.style.background = '#000000'; onDemandTabBtn.style.color = '#ffffff'; onDemandTabBtn.style.border = '2px solid #ffffff'; preInscribedTabBtn.style.background = '#000000'; preInscribedTabBtn.style.color = '#ffffff'; preInscribedTabBtn.style.border = '2px solid #ffffff'; inquiriesTabBtn.style.background = '#000000'; inquiriesTabBtn.style.color = '#ffffff'; inquiriesTabBtn.style.border = '2px solid #ffffff'; printsTabBtn.style.background = '#000000'; printsTabBtn.style.color = '#ffffff'; printsTabBtn.style.border = '2px solid #ffffff'; paymentsTabBtn.style.background = '#000000'; paymentsTabBtn.style.color = '#ffffff'; paymentsTabBtn.style.border = '2px solid #ffffff'; supportersTabBtn.style.background = '#000000'; supportersTabBtn.style.color = '#ffffff'; supportersTabBtn.style.border = '2px solid #ffffff'; collectionsTabBtn.style.background = '#000000'; collectionsTabBtn.style.color = '#ffffff'; collectionsTabBtn.style.border = '2px solid #ffffff'; statsTabBtn.style.background = '#000000'; statsTabBtn.style.color = '#ffffff'; statsTabBtn.style.border = '2px solid #ffffff'; autoSendTabBtn.style.background = '#000000'; autoSendTabBtn.style.color = '#ffffff'; autoSendTabBtn.style.border = '2px solid #ffffff'; autoInscribeTabBtn.style.background = '#000000'; autoInscribeTabBtn.style.color = '#ffffff'; autoInscribeTabBtn.style.border = '2px solid #ffffff'; stampsTabBtn.style.background = '#000000'; stampsTabBtn.style.color = '#ffffff'; stampsTabBtn.style.border = '2px solid #ffffff'; auctionsTabBtn.style.background = '#000000'; auctionsTabBtn.style.color = '#ffffff'; auctionsTabBtn.style.border = '2px solid #ffffff'; // Hide all tab content homeTabContent.style.display = 'none'; artworksTabContent.style.display = 'none'; collectionsTabContent.style.display = 'none'; completedTabContent.style.display = 'none'; onDemandTabContent.style.display = 'none'; preInscribedTabContent.style.display = 'none'; inquiriesTabContent.style.display = 'none'; printsTabContent.style.display = 'none'; paymentsTabContent.style.display = 'none'; supportersTabContent.style.display = 'none'; statsTabContent.style.display = 'none'; autoSendTabContent.style.display = 'none'; autoInscribeTabContent.style.display = 'none'; stampsTabContent.style.display = 'none'; auctionsTabContent.style.display = 'none'; if (tabName === 'home') { homeTabBtn.style.background = '#ffffff'; homeTabBtn.style.color = '#000000'; homeTabBtn.style.border = 'none'; homeTabContent.style.display = 'block'; loadAdminHomePageLayout(); } else if (tabName === 'artworks') { artworksTabBtn.style.background = '#ffffff'; artworksTabBtn.style.color = '#000000'; artworksTabBtn.style.border = 'none'; artworksTabContent.style.display = 'block'; loadArtworksForAdmin(); } else if (tabName === 'collections') { collectionsTabBtn.style.background = '#ffffff'; collectionsTabBtn.style.color = '#000000'; collectionsTabBtn.style.border = 'none'; collectionsTabContent.style.display = 'block'; loadCollectionsForAdmin(); } else if (tabName === 'completed') { completedTabBtn.style.background = '#ffffff'; completedTabBtn.style.color = '#000000'; completedTabBtn.style.border = 'none'; completedTabContent.style.display = 'block'; loadCompletedArtworks(); } else if (tabName === 'on-demand') { onDemandTabBtn.style.background = '#ffffff'; onDemandTabBtn.style.color = '#000000'; onDemandTabBtn.style.border = 'none'; onDemandTabContent.style.display = 'block'; loadOnDemandCollections(); } else if (tabName === 'pre-inscribed') { preInscribedTabBtn.style.background = '#ffffff'; preInscribedTabBtn.style.color = '#000000'; preInscribedTabBtn.style.border = 'none'; preInscribedTabContent.style.display = 'block'; loadPreInscribedCollections(); } else if (tabName === 'inquiries') { inquiriesTabBtn.style.background = '#ffffff'; inquiriesTabBtn.style.color = '#000000'; inquiriesTabBtn.style.border = 'none'; inquiriesTabContent.style.display = 'block'; loadCollectionInquiries(); } else if (tabName === 'prints') { printsTabBtn.style.background = '#ffffff'; printsTabBtn.style.color = '#000000'; printsTabBtn.style.border = 'none'; printsTabContent.style.display = 'block'; loadPhysicalPrintOrders(); } else if (tabName === 'payments') { paymentsTabBtn.style.background = '#ffffff'; paymentsTabBtn.style.color = '#000000'; paymentsTabBtn.style.border = 'none'; paymentsTabContent.style.display = 'block'; // Initialize with payments list sub-tab switchPaymentSubTab('list'); } else if (tabName === 'supporters') { supportersTabBtn.style.background = '#ffffff'; supportersTabBtn.style.color = '#000000'; supportersTabBtn.style.border = 'none'; supportersTabContent.style.display = 'block'; loadSupporters(); } else if (tabName === 'stats') { statsTabBtn.style.background = '#ffffff'; statsTabBtn.style.color = '#000000'; statsTabBtn.style.border = 'none'; statsTabContent.style.display = 'block'; loadAdminStats(); } else if (tabName === 'auto-send') { autoSendTabBtn.style.background = '#ffffff'; autoSendTabBtn.style.color = '#000000'; autoSendTabBtn.style.border = 'none'; autoSendTabContent.style.display = 'block'; loadAutoSendPanel(); } else if (tabName === 'auto-inscribe') { autoInscribeTabBtn.style.background = '#ffffff'; autoInscribeTabBtn.style.color = '#000000'; autoInscribeTabBtn.style.border = 'none'; autoInscribeTabContent.style.display = 'block'; loadAutoInscribeQueue(); } else if (tabName === 'stamps') { stampsTabBtn.style.background = '#ffffff'; stampsTabBtn.style.color = '#000000'; stampsTabBtn.style.border = 'none'; stampsTabContent.style.display = 'block'; startStampsAdminRefresh(); } else if (tabName === 'auctions') { auctionsTabBtn.style.background = '#ffffff'; auctionsTabBtn.style.color = '#000000'; auctionsTabBtn.style.border = 'none'; auctionsTabContent.style.display = 'block'; loadAuctionsForAdmin(); } // Stop auto-monitors when switching away (keep auto-send running so queue processes in background) if (tabName !== 'stamps') { stopStampsAdminRefresh(); } }; // Stamps Queue Functions let stampsAdminRefreshInterval = null; let adminStampsPage = 0; const adminStampsPerPage = 50; window.loadStampsQueue = async function() { try { const response = await fetch('/api/admin-stamp-queue'); const data = await response.json(); // Update last refresh time const now = new Date(); document.getElementById('stamps-last-refresh').textContent = now.toLocaleTimeString(); if (data.success) { // Update stats document.getElementById('stamps-queue-pending').textContent = data.stats.pending; document.getElementById('stamps-queue-processing').textContent = data.stats.processing; document.getElementById('stamps-queue-complete').textContent = data.stats.complete; document.getElementById('stamps-queue-failed').textContent = data.stats.failed; document.getElementById('stamps-queue-total').textContent = data.stats.total; // Update wallet info if (data.wallet) { document.getElementById('stamps-wallet-address').textContent = data.wallet.address; document.getElementById('stamps-wallet-balance').textContent = data.wallet.balance ? `${(data.wallet.balance / 100000000).toFixed(8)} BTC` : 'Unknown'; } // Update queue list const queueList = document.getElementById('stamps-queue-list'); if (data.items.length === 0) { queueList.innerHTML = '

    No items in queue

    '; } else { queueList.innerHTML = data.items.map(item => { const statusColors = { 'pending': '#4CAF50', 'processing': '#ff9800', 'complete': '#2196F3', 'failed': '#f44336' }; const statusColor = statusColors[item.status] || '#888'; return `
    #${item.id}
    ${item.recipientAddress.slice(0, 8)}...${item.recipientAddress.slice(-6)}
    ${item.delegateId.slice(0, 12)}...
    ${item.stampIndex + 1}/5
    ${item.status}
    ${item.status === 'failed' ? ` ` : ''} ${item.status === 'complete' && item.revealTxId ? ` VIEW TX ` : ''}
    ${item.error ? `
    Error: ${item.error}
    ` : ''} `; }).join(''); } } else { console.error('Failed to load stamps queue:', data.error); } } catch (error) { console.error('Error loading stamps queue:', error); } }; // Start auto-refresh when stamps admin tab is shown window.startStampsAdminRefresh = function() { if (stampsAdminRefreshInterval) clearInterval(stampsAdminRefreshInterval); loadStampsQueue(); loadAdminStampsPage(0); stampsAdminRefreshInterval = setInterval(() => { loadStampsQueue(); }, 15000); // Refresh every 15 seconds }; window.stopStampsAdminRefresh = function() { if (stampsAdminRefreshInterval) { clearInterval(stampsAdminRefreshInterval); stampsAdminRefreshInterval = null; } }; // Load paginated stamps for admin view window.loadAdminStampsPage = async function(page) { adminStampsPage = page; const offset = page * adminStampsPerPage; const grid = document.getElementById('admin-stamps-grid'); const countEl = document.getElementById('stamps-minted-count'); const pagination = document.getElementById('admin-stamps-pagination'); try { const response = await fetch(`/api/get-latest-stamps?limit=${adminStampsPerPage}&offset=${offset}`); const data = await response.json(); if (data.success && data.stamps && data.stamps.length > 0) { const totalPages = Math.ceil(data.total / adminStampsPerPage); countEl.textContent = `Showing ${offset + 1}-${Math.min(offset + data.stamps.length, data.total)} of ${data.total} minted stamps`; grid.innerHTML = data.stamps.map(stamp => `
    `).join(''); // Build pagination let paginationHtml = ''; if (page > 0) { paginationHtml += ``; } paginationHtml += `Page ${page + 1} of ${totalPages}`; if (page < totalPages - 1) { paginationHtml += ``; } pagination.innerHTML = paginationHtml; } else { countEl.textContent = '0 minted stamps'; grid.innerHTML = '

    No stamps minted yet

    '; pagination.innerHTML = ''; } } catch (error) { console.error('Error loading admin stamps:', error); countEl.textContent = 'Error loading stamps'; grid.innerHTML = '

    Error loading stamps

    '; } }; window.retryStampQueueItem = async function(queueId) { try { const response = await fetch('/api/admin-stamp-queue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'retry', queueId: queueId }) }); const result = await response.json(); if (result.success) { alert('Queue item reset to pending'); } else { alert('Error: ' + (result.error || 'Failed to retry')); } loadStampsQueue(); } catch (error) { console.error('Error retrying stamp:', error); alert('Error: ' + error.message); } }; window.retryAllFailedStamps = async function() { if (!confirm('Reset all failed items to pending?')) return; try { const response = await fetch('/api/admin-stamp-queue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'retry-all-failed' }) }); const result = await response.json(); if (result.success) { alert(result.message); } else { alert('Error: ' + (result.error || 'Failed to retry')); } loadStampsQueue(); } catch (error) { console.error('Error retrying all stamps:', error); alert('Error: ' + error.message); } }; // Auto-Send Panel Functions let autoSendMonitorInterval = null; window.loadAutoSendPanel = async function() { // Load all sections await loadBackendWallets(); await checkAllWalletBalances(); // Auto-check balances on load await loadAutoSendQueue(); await loadPreInscribedArtworks(); await loadBackendWalletsForImport(); await loadPreInscribedCollectionsForImport(); // Start auto-monitor every 30 seconds startAutoMonitor(); }; function startAutoMonitor() { // Clear any existing interval if (autoSendMonitorInterval) { clearInterval(autoSendMonitorInterval); } // Update status indicator updateAutoMonitorStatus('running', 'Auto-monitor running (every 10s)'); // Run monitor every 10 seconds autoSendMonitorInterval = setInterval(async () => { const now = new Date().toLocaleTimeString(); updateAutoMonitorStatus('running', `Running monitor... (${now})`); await runAutoSendMonitor(true); // Pass true for auto mode updateAutoMonitorStatus('running', `Last run: ${now} - Next in 10s`); }, 10000); console.log('✅ Auto-monitor started (runs every 10 seconds)'); } function stopAutoMonitor() { if (autoSendMonitorInterval) { clearInterval(autoSendMonitorInterval); autoSendMonitorInterval = null; updateAutoMonitorStatus('stopped', 'Auto-monitor stopped'); console.log('⏸️ Auto-monitor stopped'); } } function updateAutoMonitorStatus(status, message) { const statusEl = document.getElementById('auto-monitor-status'); if (statusEl) { const color = status === 'running' ? '#4CAF50' : '#999'; const icon = status === 'running' ? '🔄' : '⏸️'; statusEl.style.color = color; statusEl.textContent = `${icon} ${message}`; } } // Fix wallet private key by re-deriving from mnemonic window.fixWalletPrivateKey = async function(walletId) { if (!confirm('This will update the wallet\'s private key by re-deriving it from the mnemonic. Continue?')) { return; } try { const response = await fetch('/api/fix-wallet-private-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ walletId }) }); const data = await response.json(); if (data.success) { alert(`✅ Wallet keys updated successfully!\n\nWallet: ${data.wallet.name}\nTaproot: ${data.wallet.taproot_address}\n\nThe private key now matches the address.`); // Reload wallets await loadBackendWallets(); } else { alert('Failed to fix wallet: ' + (data.error || 'Unknown error')); } } catch (error) { console.error('Error fixing wallet:', error); alert('Failed to fix wallet: ' + error.message); } }; // Diagnostic function to check wallet keys window.runWalletDiagnostics = async function() { // Use the latest error message values - these are from the signing service // Expected pubkey is what the wallet's key derives to // Actual pubkey is what the ordinal's script contains // Expected address is where the ordinal actually is (from blockchain) const expectedPubkey = '2a27e0ef4090ea9c4316b3bdb3a9c0eb62b046a692b2799b22b6fe5fd7e6ca03'; const actualPubkey = '5d88ead3b065f16c2ad281c292d3a8f4cd5d9afe7b9fa8dc2ddfa72f1f25d12f'; const expectedAddress = 'bc1pjx27q9f80uzapc8rzrsasrt62xsq7x3mnq44hzrdxsq5hpjwe4cqkjac3c'; // This is where the ordinal ACTUALLY is try { const url = `/api/diagnose-wallet-keys?expected=${encodeURIComponent(expectedPubkey)}&actual=${encodeURIComponent(actualPubkey)}&address=${encodeURIComponent(expectedAddress)}`; const response = await fetch(url, { credentials: 'include' }); const data = await response.json(); if (data.success) { const diag = data.diagnostic; let message = '=== WALLET KEY DIAGNOSTICS ===\n\n'; message += `Expected Pubkey: ${diag.expectedPubkey}\n`; message += `Actual Pubkey: ${diag.actualPubkey}\n`; message += `Expected Address: ${diag.expectedAddress}\n\n`; if (diag.matchingWallet) { message += `✅ FOUND MATCHING WALLET:\n`; message += ` Name: ${diag.matchingWallet.name}\n`; message += ` ID: ${diag.matchingWallet.id}\n`; message += ` Taproot: ${diag.matchingWallet.taproot_address}\n`; message += ` Has Private Key: ${diag.matchingWallet.hasPrivateKey}\n`; message += ` Key Length: ${diag.matchingWallet.privateKeyLength}\n\n`; } else { message += `❌ NO WALLET FOUND WITH ADDRESS: ${diag.expectedAddress}\n\n`; } if (diag.queueItem) { message += `QUEUE ITEM:\n`; message += ` Status: ${diag.queueItem.status}\n`; message += ` Wallet ID: ${diag.queueItem.backend_wallet_id}\n`; message += ` Wallet Name: ${diag.queueItem.wallet_name}\n`; message += ` Wallet Taproot: ${diag.queueItem.wallet_taproot_address}\n`; message += ` Error: ${diag.queueItem.error_message || 'None'}\n\n`; } message += `ALL WALLETS:\n`; diag.allWallets.forEach(w => { message += ` ${w.name} (ID: ${w.id}): ${w.taproot_address || 'NULL'}`; if (w.matchesExpectedAddress) { message += ' ✅ MATCHES'; } message += `\n`; }); // Show in modal instead of alert showDiagnosticModal(message, data); console.log('Diagnostic data:', data); } else { alert('Diagnostic failed: ' + (data.error || 'Unknown error')); } } catch (error) { console.error('Diagnostic error:', error); alert('Failed to run diagnostics: ' + error.message); } }; // Show diagnostic results in a modal function showDiagnosticModal(message, data) { // Create modal overlay const overlay = document.createElement('div'); overlay.id = 'diagnostic-modal-overlay'; overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 10000; display: flex; align-items: center; justify-content: center;'; // Create modal content const modal = document.createElement('div'); modal.style.cssText = 'background: #111; border: 3px solid #fff; padding: 30px; max-width: 800px; max-height: 80vh; overflow-y: auto; position: relative; font-family: monospace;'; // Title const title = document.createElement('h3'); title.textContent = 'WALLET KEY DIAGNOSTICS'; title.style.cssText = 'color: #fff; margin: 0 0 20px 0; font-size: 16px; font-family: "Press Start 2P", monospace;'; // Textarea for copyable content const textarea = document.createElement('textarea'); textarea.value = message; textarea.style.cssText = 'width: 100%; height: 400px; background: #000; color: #0f0; border: 2px solid #0f0; padding: 15px; font-family: monospace; font-size: 11px; resize: vertical; white-space: pre; overflow-wrap: normal;'; textarea.readOnly = true; // Copy button const copyBtn = document.createElement('button'); copyBtn.textContent = 'COPY TO CLIPBOARD'; copyBtn.style.cssText = 'padding: 10px 20px; background: #4CAF50; color: #fff; border: 2px solid #fff; font-family: "Press Start 2P", monospace; font-size: 10px; cursor: pointer; margin: 15px 10px 0 0;'; copyBtn.onclick = () => { textarea.select(); document.execCommand('copy'); copyBtn.textContent = 'COPIED!'; setTimeout(() => { copyBtn.textContent = 'COPY TO CLIPBOARD'; }, 2000); }; // Close button const closeBtn = document.createElement('button'); closeBtn.textContent = 'CLOSE'; closeBtn.style.cssText = 'padding: 10px 20px; background: #f44336; color: #fff; border: 2px solid #fff; font-family: "Press Start 2P", monospace; font-size: 10px; cursor: pointer; margin: 15px 0 0 0;'; closeBtn.onclick = () => { document.body.removeChild(overlay); }; // JSON data section (collapsible) const jsonSection = document.createElement('details'); jsonSection.style.cssText = 'margin-top: 20px;'; const jsonSummary = document.createElement('summary'); jsonSummary.textContent = 'View Full JSON Data'; jsonSummary.style.cssText = 'color: #fff; cursor: pointer; margin-bottom: 10px; font-family: "Press Start 2P", monospace; font-size: 9px;'; const jsonPre = document.createElement('pre'); jsonPre.textContent = JSON.stringify(data, null, 2); jsonPre.style.cssText = 'background: #000; color: #0f0; padding: 15px; border: 1px solid #0f0; overflow-x: auto; font-size: 10px;'; jsonSection.appendChild(jsonSummary); jsonSection.appendChild(jsonPre); // Assemble modal modal.appendChild(title); modal.appendChild(textarea); modal.appendChild(copyBtn); modal.appendChild(closeBtn); modal.appendChild(jsonSection); overlay.appendChild(modal); // Close on overlay click overlay.onclick = (e) => { if (e.target === overlay) { document.body.removeChild(overlay); } }; // Add to page document.body.appendChild(overlay); // Focus textarea setTimeout(() => textarea.focus(), 100); } window.loadBackendWallets = async function() { const container = document.getElementById('backend-wallets-list'); if (!container) return; container.innerHTML = '

    Loading wallets...

    '; try { const response = await fetch('/api/manage-auto-send', { credentials: 'include' }); const data = await response.json(); if (data.success && data.wallets && data.wallets.length > 0) { container.innerHTML = data.wallets.map(wallet => { const walletId = 'wallet-' + wallet.id; const hasNewAddresses = wallet.segwit_address && wallet.taproot_address; // Show upgrade notice for old wallets if (!hasNewAddresses) { return `
    ${wallet.wallet_name} (Legacy) NEEDS UPGRADE
    ⚠️ This wallet was created with the old system
    Old address: ${wallet.wallet_address}
    Action Required:
    1. Export funds from this wallet if any
    2. Generate a new wallet with dual addresses
    3. Fund the new wallet instead
    `; } return `
    ${wallet.wallet_name} ${wallet.is_active ? 'ACTIVE' : 'INACTIVE'}
    ${wallet.wallet_type} WALLET
    ${(wallet.segwit_address || wallet.wallet_address).startsWith('3') ? '3...' : 'bc1q...'} address - Send Bitcoin here to pay for transaction fees (matches Xverse)
    bc1p... address - Send your pre-inscribed ordinals here
    ⚠️ Never share your recovery phrase with anyone
    ${wallet.balance_sats ? (wallet.balance_sats / 100000000).toFixed(8) + ' BTC' : '0.00000000 BTC'}
    ${wallet.balance_sats || 0} sats
    ${wallet.last_balance_check ? `
    Updated: ${new Date(wallet.last_balance_check).toLocaleString()}
    ` : '
    Checking...
    '}
    ${wallet.network || 'MAINNET'}
    ${wallet.ordinals_count || 0}
    Imported
    Import to see
    Available
    ${wallet.auto_send_enabled_count || 0}
    Auto-Send
    ${wallet.sold_count || 0}
    Sold
    ⚠️ To use this wallet:
    1. Send BTC for fees (estimate: 2,000-5,000 sats per send)
    2. Send your pre-inscribed ordinals to this address
    3. Import ordinals using the import tools
    4. Enable auto-send for your artwork
    `; }).join(''); } else { container.innerHTML = '

    No backend wallets found. Generate one to get started!

    '; } } catch (error) { console.error('Failed to load backend wallets:', error); container.innerHTML = '

    Error loading wallets

    '; } }; window.copyToClipboard = function(elementId) { const input = document.getElementById(elementId); input.select(); document.execCommand('copy'); // Visual feedback const button = event.target; const originalText = button.textContent; button.textContent = 'COPIED!'; button.style.background = '#2196F3'; setTimeout(() => { button.textContent = originalText; // Restore original color based on button type if (originalText === 'COPY' && elementId.includes('taproot')) { button.style.background = '#ff9800'; } else { button.style.background = '#4CAF50'; } }, 1500); }; window.exportWalletMnemonic = async function(walletId, walletName) { const password = prompt(`Enter password to export mnemonic for wallet "${walletName}":`); if (!password) { return; } try { const response = await fetch('/api/export-wallet-mnemonic', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ walletId, password }) }); const data = await response.json(); if (data.success) { // Show mnemonic in a modal showMnemonicModal(data); } else { alert('Failed to export mnemonic: ' + data.error); } } catch (error) { console.error('Failed to export mnemonic:', error); alert('Error exporting mnemonic: ' + error.message); } }; window.showMnemonicModal = function(data) { const modal = document.createElement('div'); modal.id = 'mnemonic-modal'; modal.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 20px;'; modal.innerHTML = `

    🔑 RECOVERY PHRASE

    ⚠️ CRITICAL: WRITE THIS DOWN AND STORE SECURELY OFFLINE
    ${data.mnemonic}
    Wallet: ${data.walletName}
    SegWit: ${data.segwitAddress}
    Taproot: ${data.taprootAddress}
    SECURITY WARNING:
    • Write this phrase on paper and store in a safe place
    • Never store it digitally or take a screenshot
    • Anyone with this phrase can steal your funds
    • You need this phrase to recover your wallet
    `; document.body.appendChild(modal); }; window.closeMnemonicModal = function() { const modal = document.getElementById('mnemonic-modal'); if (modal) { modal.remove(); } }; window.checkAllWalletBalances = async function() { try { const response = await fetch('/api/manage-auto-send', { credentials: 'include' }); const data = await response.json(); if (data.success && data.wallets) { // Check balance for each active wallet for (const wallet of data.wallets) { if (wallet.is_active) { await checkWalletBalance(wallet.id, true); // Silent mode } } // Reload wallets to show updated balances await loadBackendWallets(); } } catch (error) { console.error('Failed to check all balances:', error); } }; window.checkOnChainOrdinals = async function(walletId, taprootAddress) { const countElement = document.getElementById(`wallet-${walletId}-onchain`); try { // Use Best in Slot API (same as import function) const apiKey = '95bd3666-917f-4305-9d35-caefa0a70d07'; // BIS_API_KEY const response = await fetch( `https://api.bestinslot.xyz/v3/wallet/inscriptions?address=${encodeURIComponent(taprootAddress)}&offset=0&count=100`, { headers: { 'Accept': 'application/json', 'x-api-key': apiKey } } ); if (!response.ok) { console.log('Best in Slot API failed for wallet', walletId); if (countElement) { countElement.textContent = '?'; countElement.style.color = '#666'; } return; } const data = await response.json(); const count = data.data?.length || 0; console.log(`Wallet ${walletId} has ${count} ordinals on-chain at ${taprootAddress}`); // Update the UI element if (countElement) { countElement.textContent = count; if (count > 0) { countElement.style.color = '#9c27b0'; } else { countElement.style.color = '#666'; } } } catch (error) { console.error('Failed to check on-chain ordinals:', error); if (countElement) { countElement.textContent = '?'; countElement.style.color = '#666'; } } }; window.checkWalletBalance = async function(walletId, silent = false) { try { const response = await fetch('/api/check-wallet-balance', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ walletId }) }); const data = await response.json(); if (data.success) { if (!silent) { alert(`Balance Updated!\\n\\n${data.walletName}\\nAddress: ${data.address}\\n\\nBalance: ${data.balance.btc} BTC (${data.balance.sats} sats)\\n\\nTransactions: ${data.stats.transactions}\\nReceived: ${data.stats.received} sats\\nSpent: ${data.stats.spent} sats`); await loadBackendWallets(); } return data; } else { if (!silent) { alert('Failed to check balance: ' + data.error); } return null; } } catch (error) { console.error('Failed to check balance:', error); if (!silent) { alert('Error checking balance: ' + error.message); } return null; } }; window.deleteBackendWallet = async function(walletId, walletName) { if (!confirm(`⚠️ DELETE WALLET: ${walletName}?\\n\\nThis will mark the wallet as inactive. Make sure you've exported the recovery phrase and moved any funds first!\\n\\nType DELETE to confirm:`)) { return; } const confirmText = prompt('Type DELETE to confirm deletion:'); if (confirmText !== 'DELETE') { return; } try { const response = await fetch('/api/delete-backend-wallet', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ walletId }) }); const data = await response.json(); if (data.success) { alert('Wallet marked as inactive'); await loadBackendWallets(); } else { alert('Failed to delete wallet: ' + data.error); } } catch (error) { console.error('Failed to delete wallet:', error); alert('Error deleting wallet: ' + error.message); } }; // Test Inscribe Modal Functions let currentTestInscribeWallet = null; window.openTestInscribeModal = async function(walletId, walletName) { try { await ensureAdminSensitiveModals(); } catch (e) { console.error(e); alert('Could not load admin UI. Refresh and try again.'); return; } // Get wallet details const response = await fetch('/api/manage-auto-send', { credentials: 'include' }); const data = await response.json(); if (!data.success || !data.wallets) { alert('Failed to load wallet details'); return; } const wallet = data.wallets.find(w => w.id === walletId); if (!wallet) { alert('Wallet not found'); return; } currentTestInscribeWallet = wallet; // Populate modal document.getElementById('test-inscribe-wallet-name').textContent = walletName; document.getElementById('test-inscribe-segwit').textContent = wallet.segwit_address || wallet.wallet_address; document.getElementById('test-inscribe-taproot').textContent = wallet.taproot_address || 'N/A'; document.getElementById('test-inscribe-html').value = ''; document.getElementById('test-inscribe-recipient').value = ''; document.getElementById('test-inscribe-feerate').value = '1'; document.getElementById('test-inscribe-result').style.display = 'none'; // Show modal document.getElementById('test-inscribe-modal').style.display = 'block'; }; window.closeTestInscribeModal = function() { const el = document.getElementById('test-inscribe-modal'); if (el) el.style.display = 'none'; currentTestInscribeWallet = null; }; window.executeTestInscribe = async function() { if (!currentTestInscribeWallet) { alert('No wallet selected'); return; } const htmlContent = document.getElementById('test-inscribe-html').value.trim(); const recipientAddress = document.getElementById('test-inscribe-recipient').value.trim(); const feeRate = parseInt(document.getElementById('test-inscribe-feerate').value) || 1; if (!htmlContent) { alert('Please enter HTML content to inscribe'); return; } const confirmMsg = `Create Inscription?\\n\\n` + `Wallet: ${currentTestInscribeWallet.wallet_name}\\n` + `Size: ${htmlContent.length} bytes\\n` + `Fee Rate: ${feeRate} sat/vB\\n` + `Est. Cost: ~${200 + Math.ceil(htmlContent.length / 4) * feeRate + MINT_NETWORK_BUFFER_SATS_PER_MINT} sats\\n\\n` + `This will create a permanent inscription on Bitcoin!`; if (!confirm(confirmMsg)) { return; } const btn = document.getElementById('test-inscribe-btn'); const resultDiv = document.getElementById('test-inscribe-result'); btn.disabled = true; btn.textContent = '⏳ INSCRIBING...'; resultDiv.style.display = 'none'; try { const response = await fetch('/api/test-inscribe', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ walletId: currentTestInscribeWallet.id, htmlContent, recipientAddress: recipientAddress || null, feeRate }) }); const data = await response.json(); if (data.success) { resultDiv.innerHTML = `
    ✅ Inscription Created Successfully!
    Inscription ID:
    ${data.inscriptionId}
    VIEW ON ORDINALS.COM
    Fees: ${data.fees.total} sats (Commit: ${data.fees.commit}, Reveal: ${data.fees.reveal})
    `; resultDiv.style.display = 'block'; btn.textContent = '✅ INSCRIPTION COMPLETE'; // Refresh wallet balance await checkWalletBalance(currentTestInscribeWallet.id, true); await loadBackendWallets(); } else { resultDiv.innerHTML = `
    ❌ Inscription Failed
    ${data.error || 'Unknown error'}
    `; resultDiv.style.display = 'block'; btn.textContent = '🎨 CREATE INSCRIPTION'; btn.disabled = false; } } catch (error) { console.error('Test inscribe error:', error); resultDiv.innerHTML = `
    ❌ Request Failed
    ${error.message}
    `; resultDiv.style.display = 'block'; btn.textContent = '🎨 CREATE INSCRIPTION'; btn.disabled = false; } }; // Update file size counter when HTML changes document.addEventListener('DOMContentLoaded', function() { const htmlInput = document.getElementById('test-inscribe-html'); const sizeSpan = document.getElementById('test-inscribe-size'); const feeEstimate = document.getElementById('test-inscribe-fee-estimate'); if (htmlInput && sizeSpan) { htmlInput.addEventListener('input', function() { const size = htmlInput.value.length; sizeSpan.textContent = size.toLocaleString(); if (feeEstimate) { const feeRate = parseInt(document.getElementById('test-inscribe-feerate')?.value) || 1; const estimatedFee = 200 + Math.ceil(size / 4) * feeRate + MINT_NETWORK_BUFFER_SATS_PER_MINT; feeEstimate.textContent = estimatedFee.toLocaleString(); } }); } }); // Rotating banner: random from banners folder, crossfade every 10s. Call when home template is in DOM. (function() { var BANNERS = [ 'BANNER-INSIDE-US-2.png', 'FinalBanner_3.png', 'Pizza_Comrades_launchpad_banner_v2.png', 'glitch.png', 'socialagebanner.png' ]; var BANNER_BASE = './banners/'; var bannerIntervalId = null; function pickRandom(excludeFilename) { var options = excludeFilename ? BANNERS.filter(function(f) { return f !== excludeFilename; }) : BANNERS; if (options.length === 0) options = BANNERS; return options[Math.floor(Math.random() * options.length)]; } function setContainerHeight(container, img) { if (!container || !img.naturalWidth || !img.naturalHeight) return; var w = container.offsetWidth || 400; var ratio = img.naturalHeight / img.naturalWidth; var h = Math.max(120, Math.round(w * ratio)); container.style.height = h + 'px'; } function tryLoadSlide(slideEl, container, excludeFilename, onSuccess, maxRetries) { maxRetries = maxRetries != null ? maxRetries : 2; var attempt = 0; function load() { var filename = pickRandom(excludeFilename); var url = BANNER_BASE + filename; slideEl.onload = function() { setContainerHeight(container, slideEl); if (onSuccess) onSuccess(filename); }; slideEl.onerror = function() { attempt++; if (attempt <= maxRetries) load(); }; slideEl.src = url; } load(); } window.initRotatingBanner = function() { if (bannerIntervalId) clearInterval(bannerIntervalId); bannerIntervalId = null; var container = document.getElementById('rotating-banner'); var slide1 = document.getElementById('banner-slide-1'); var slide2 = document.getElementById('banner-slide-2'); if (!container || !slide1 || !slide2) return; var currentSlide = 1; var currentFilename = null; function showNext() { var nextSlideEl = currentSlide === 1 ? slide2 : slide1; var currentSlideEl = currentSlide === 1 ? slide1 : slide2; tryLoadSlide(nextSlideEl, container, currentFilename, function(filename) { currentFilename = filename; currentSlideEl.classList.remove('current'); currentSlideEl.classList.add('next'); nextSlideEl.classList.remove('next'); nextSlideEl.classList.add('current'); currentSlide = currentSlide === 1 ? 2 : 1; }, 3); } tryLoadSlide(slide1, container, null, function(filename) { currentFilename = filename; bannerIntervalId = setInterval(showNext, 10000); }, 3); // Fallback: if nothing loaded after 1s, retry init once (helps slow/timing issues) setTimeout(function() { if (slide1.naturalWidth === 0 && typeof window.initRotatingBanner === 'function') { window.initRotatingBanner(); } }, 1000); }; })(); // Track which queue tab is active window.autoSendQueueTab = window.autoSendQueueTab || 'active'; window.switchAutoSendTab = function(tab) { window.autoSendQueueTab = tab; loadAutoSendQueue(); }; window.loadAutoSendQueue = async function() { const container = document.getElementById('auto-send-queue-list'); if (!container) return; container.innerHTML = '

    Loading queue...

    '; try { const response = await fetch('/api/get-auto-send-queue', { credentials: 'include' }); const data = await response.json(); if (!data.success) { container.innerHTML = `
    Error: ${data.error || 'Failed to load queue'}
    `; return; } const allItems = data.items || []; // Group by status const byStatus = { pending: allItems.filter(i => i.status === 'pending'), checking_payment: allItems.filter(i => i.status === 'checking_payment'), payment_confirmed: allItems.filter(i => i.status === 'payment_confirmed'), sending: allItems.filter(i => i.status === 'sending'), sent: allItems.filter(i => i.status === 'sent'), failed: allItems.filter(i => i.status === 'failed') }; // Filter items based on current tab const activeTab = window.autoSendQueueTab || 'active'; const items = activeTab === 'sent' ? byStatus.sent : allItems.filter(i => i.status !== 'sent'); // Build tab header const activeCount = allItems.filter(i => i.status !== 'sent').length; const sentCount = byStatus.sent.length; const tabsHtml = `
    `; if (items.length === 0) { container.innerHTML = tabsHtml + `

    ${activeTab === 'sent' ? 'No sent items yet' : 'No active items in queue'}

    ${activeTab === 'sent' ? 'Completed sends will appear here' : 'Items will appear here when ordinals are purchased with auto-send enabled'}

    `; return; } const statusColors = { pending: '#ff9800', checking_payment: '#2196F3', payment_confirmed: '#4CAF50', sending: '#9C27B0', sent: '#4CAF50', failed: '#f44336' }; const statusLabels = { pending: 'Pending', checking_payment: 'Checking Payment', payment_confirmed: 'Payment Confirmed', sending: 'Sending', sent: 'Sent', failed: 'Failed' }; container.innerHTML = tabsHtml + `
    Pending: ${byStatus.pending.length}
    Checking: ${byStatus.checking_payment.length}
    Confirmed: ${byStatus.payment_confirmed.length}
    Sending: ${byStatus.sending.length}
    Failed: ${byStatus.failed.length}
    ${items.map(item => { const statusColor = statusColors[item.status] || '#999'; const statusLabel = statusLabels[item.status] || item.status; const createdDate = new Date(item.created_at).toLocaleString(); const lastCheck = item.last_check_at ? new Date(item.last_check_at).toLocaleString() : 'Never'; return `
    ${statusLabel}
    Artwork: ${item.artwork_title || item.artwork_slug || '(unlinked — bad artwork_id?)'}
    Inscription: ${item.inscription_id}
    Recipient: ${item.recipient_address}
    ${(!item.artwork_slug && !item.artwork_title) ? `
    ⚠️ No matching artwork row (check artwork_id=${item.artwork_id ?? 'null'}). This row was omitted from the admin list before.
    ` : ''}
    Created: ${createdDate}
    Payment TX: ${item.payment_txid ? `${String(item.payment_txid).substring(0, 16)}...` : '(none)'}
    Confirmations: ${item.payment_confirmations || 0} / ${item.required_confirmations || 1}
    Last Check: ${lastCheck}
    ${item.send_txid ? `Send TX: ${item.send_txid.substring(0, 16)}...
    ` : ''} ${item.send_fee_sats ? `Send Fee: ${item.send_fee_sats} sats
    ` : ''} ${item.sent_at ? `Sent At: ${new Date(item.sent_at).toLocaleString()}
    ` : ''} ${item.backend_wallet_name ? `Wallet: ${item.backend_wallet_name}
    ` : ''} ${item.retry_count > 0 ? `Retries: ${item.retry_count} / ${item.max_retries}
    ` : ''}
    ${item.error_message ? `
    Error: ${item.error_message}
    ` : ''} ${item.next_retry_at ? `
    Next retry: ${new Date(item.next_retry_at).toLocaleString()}
    ` : ''}
    `; }).join('')} `; } catch (error) { console.error('Failed to load auto-send queue:', error); container.innerHTML = `
    Error loading queue: ${error.message}
    `; } }; // Track current auto-inscribe tab and data window.autoInscribeCurrentTab = 'processing'; window.autoInscribeQueueData = null; window.switchAutoInscribeTab = function(tab) { window.autoInscribeCurrentTab = tab; // Update tab button styles const processingTab = document.getElementById('auto-inscribe-tab-processing'); const failedTab = document.getElementById('auto-inscribe-tab-failed'); const inscribedTab = document.getElementById('auto-inscribe-tab-inscribed'); [processingTab, failedTab, inscribedTab].forEach((el, i) => { if (!el) return; const active = (tab === 'processing' && i === 0) || (tab === 'failed' && i === 1) || (tab === 'inscribed' && i === 2); el.style.background = active ? (tab === 'failed' ? '#b71c1c' : tab === 'inscribed' ? '#4CAF50' : '#ff9800') : '#222'; el.style.color = active ? '#fff' : '#888'; el.style.borderColor = active ? '#fff' : '#333'; }); // Re-render with current data if (window.autoInscribeQueueData) { renderAutoInscribeQueue(window.autoInscribeQueueData); } }; window.renderAutoInscribeQueue = function(data) { const container = document.getElementById('auto-inscribe-queue-list'); if (!container) return; const allItems = data.queue || []; const stats = data.stats || {}; const currentTab = window.autoInscribeCurrentTab || 'processing'; // Filter items based on current tab const items = allItems.filter(item => { if (currentTab === 'processing') { return item.status === 'pending' || item.status === 'inscribing'; } if (currentTab === 'failed') { return item.status === 'failed'; } return item.status === 'inscribed'; }); // Update status bar const statusEl = document.getElementById('auto-inscribe-status'); if (statusEl) { statusEl.textContent = `Pending: ${stats.pending || 0} | Inscribing: ${stats.inscribing || 0} | Inscribed: ${stats.inscribed || 0} | Failed: ${stats.failed || 0}`; } // Update tab labels with counts const processingCount = (stats.pending || 0) + (stats.inscribing || 0); const failedCount = stats.failed || 0; const inscribedCount = stats.inscribed || 0; const processingTab = document.getElementById('auto-inscribe-tab-processing'); const failedTab = document.getElementById('auto-inscribe-tab-failed'); const inscribedTab = document.getElementById('auto-inscribe-tab-inscribed'); if (processingTab) processingTab.textContent = `PROCESSING (${processingCount})`; if (failedTab) failedTab.textContent = `FAILED (${failedCount})`; if (inscribedTab) inscribedTab.textContent = `INSCRIBED (${inscribedCount})`; if (items.length === 0) { const emptyMessage = currentTab === 'processing' ? 'No pending or inscribing items' : currentTab === 'failed' ? 'No failed items' : 'No inscribed items yet'; container.innerHTML = `

    ${emptyMessage}

    `; return; } const statusColors = { pending: '#ff9800', inscribing: '#2196F3', inscribed: '#4CAF50', failed: '#f44336' }; const statusLabels = { pending: 'Pending', inscribing: 'Inscribing', inscribed: 'Inscribed', failed: 'Failed' }; container.innerHTML = items.map(item => { const statusColor = statusColors[item.status] || '#999'; const statusLabel = statusLabels[item.status] || item.status; const queuedDate = item.queued_at ? new Date(item.queued_at).toLocaleString() : 'Unknown'; const inscribedDate = item.inscribed_at ? new Date(item.inscribed_at).toLocaleString() : null; return `
    ${statusLabel}
    Artwork: ${item.artwork_title || item.artwork_slug} by ${item.artwork_artist || 'Unknown'}
    Asset: ${item.original_name || item.asset_filename || 'Unknown'} (Index: ${item.asset_index || 'N/A'})
    ${item.inscription_id ? `Inscription: ${item.inscription_id}
    ` : ''} Recipient: ${item.ordinals_wallet_address || 'N/A'}
    Queued: ${queuedDate} ${inscribedDate ? `
    Inscribed: ${inscribedDate}` : ''}
    Payment TX: ${item.payment_txid ? item.payment_txid.substring(0, 16) + '...' : 'N/A'}
    Payment Wallet: ${item.payment_wallet_address ? item.payment_wallet_address.substring(0, 20) + '...' : 'N/A'}
    Backend Wallet: ${item.backend_wallet_name || 'N/A'}
    ${item.inscription_fees_sats ? `Inscription Fee: ${item.inscription_fees_sats} sats
    ` : ''}
    ${item.commit_txid ? `Commit TX: ${item.commit_txid.substring(0, 16)}...
    ` : ''} ${item.reveal_txid ? `Reveal TX: ${item.reveal_txid.substring(0, 16)}...
    ` : ''} ${item.inscription_address ? `Inscription Address: ${item.inscription_address.substring(0, 20)}...
    ` : ''} ${item.retry_count > 0 ? `Retries: ${item.retry_count} / ${item.max_retries || 3}
    ` : ''} ${item.next_retry_at ? `Next Retry: ${new Date(item.next_retry_at).toLocaleString()}
    ` : ''}
    ${item.error_message ? `
    Error: ${item.error_message}
    ` : ''}
    `; }).join(''); }; window.loadAutoInscribeQueue = async function() { const container = document.getElementById('auto-inscribe-queue-list'); if (!container) return; container.innerHTML = '

    Loading queue...

    '; try { const response = await fetch('/api/get-auto-inscribe-queue', { credentials: 'include' }); const data = await response.json(); if (!data.success) { container.innerHTML = `
    Error: ${data.error || 'Failed to load queue'}
    `; return; } // Store data globally for tab switching window.autoInscribeQueueData = data; // Render with current tab filter renderAutoInscribeQueue(data); } catch (error) { console.error('Failed to load auto-inscribe queue:', error); container.innerHTML = `
    Error loading queue: ${error.message}
    `; } }; window.runAutoInscribeQueue = async function() { const statusEl = document.getElementById('auto-inscribe-status'); if (statusEl) { statusEl.textContent = 'Running queue processor...'; statusEl.style.color = '#ff9800'; } try { const response = await fetch('/api/process-auto-inscribe-queue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include' }); const data = await response.json(); if (data.success) { if (statusEl) { statusEl.textContent = `Processed: ${data.processed || 0} items | Inscribed: ${data.inscribed || 0} | Failed: ${data.failed || 0}`; statusEl.style.color = '#4CAF50'; } // Reload queue to show updated status setTimeout(() => { loadAutoInscribeQueue(); }, 1000); } else { if (statusEl) { statusEl.textContent = `Error: ${data.error || 'Failed to process queue'}`; statusEl.style.color = '#f44336'; } } } catch (error) { console.error('Failed to run auto-inscribe queue:', error); if (statusEl) { statusEl.textContent = `Error: ${error.message}`; statusEl.style.color = '#f44336'; } } }; window.loadPreInscribedArtworks = async function() { const select = document.getElementById('auto-send-artwork-select'); if (!select) return; try { const response = await fetch('/api/list-artworks', { credentials: 'include' }); const data = await response.json(); console.log('Artworks loaded:', data.artworks?.length || 0); if (data.success && data.artworks) { const preInscribedArtworks = data.artworks.filter(a => { const mintType = a.mintType || a.mint_type; // Support both camelCase and snake_case return mintType === 'pre_inscribed_v2' || mintType === 'pre_inscribed'; }); console.log('Pre-inscribed artworks:', preInscribedArtworks.length); console.log('Mint types found:', [...new Set(data.artworks.map(a => a.mintType || a.mint_type))]); if (preInscribedArtworks.length > 0) { select.innerHTML = '' + preInscribedArtworks.map(artwork => `` ).join(''); } else { select.innerHTML = ''; } } else { select.innerHTML = ''; } } catch (error) { console.error('Failed to load artworks:', error); select.innerHTML = ''; } }; window.loadArtworkAutoSendSettings = async function() { const select = document.getElementById('auto-send-artwork-select'); const container = document.getElementById('artwork-auto-send-settings'); if (!select || !container) return; const artworkSlug = select.value; if (!artworkSlug) { container.innerHTML = '

    Select an artwork to manage auto-send settings

    '; return; } container.innerHTML = '

    Loading settings...

    '; try { const response = await fetch(`/api/manage-auto-send?artworkSlug=${artworkSlug}`, { credentials: 'include' }); const data = await response.json(); if (data.success && data.ordinals) { const totalOrdinals = data.ordinals.length; const autoSendEnabled = data.ordinals.filter(o => o.auto_send_enabled).length; const sold = data.ordinals.filter(o => o.is_sold).length; container.innerHTML = `
    ${data.artwork.title}
    Total: ${totalOrdinals}
    Auto-Send: ${autoSendEnabled}
    Sold: ${sold}
    ${data.ordinals.map((o, i) => `
    ${o.inscription_id.substring(0, 20)}... ${o.auto_send_enabled ? 'AUTO' : 'MANUAL'} ${o.is_sold ? 'SOLD' : ''}
    `).join('')}
    `; } else { container.innerHTML = '

    Failed to load settings

    '; } } catch (error) { console.error('Failed to load artwork auto-send settings:', error); container.innerHTML = '

    Error loading settings

    '; } }; window.showGenerateWalletModal = function() { const walletName = prompt('Enter a name for the new wallet (e.g., "Main Ordinals Wallet"):'); if (!walletName) return; generateBackendWallet(walletName); }; window.generateBackendWallet = async function(walletName) { try { const response = await fetch('/api/generate-backend-wallet', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ walletName, walletType: 'ordinals' }) }); const data = await response.json(); if (data.success) { // Success! Just reload the wallets to show the new one await loadBackendWallets(); // Scroll to the wallet setTimeout(() => { const walletsList = document.getElementById('backend-wallets-list'); if (walletsList) { walletsList.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }, 100); } else { console.error('Wallet generation error:', data); alert('Failed to generate wallet: ' + data.error + '\\n\\nDetails: ' + (data.details || 'No details') + '\\n\\nCheck browser console for more info'); } } catch (error) { console.error('Failed to generate wallet:', error); alert('Error generating wallet: ' + error.message); } }; window.runAutoSendMonitor = async function(autoMode = false) { // Skip confirmation if running automatically if (!autoMode && !confirm('Run the auto-send monitor to process pending ordinal sends?')) { return; } try { const response = await fetch('/api/auto-send-monitor', { method: 'POST', credentials: 'include' }); const data = await response.json(); if (data.success) { if (!autoMode) { // Manual run - show alert alert(`Auto-send monitor completed!\\n\\nProcessed: ${data.summary.processed}\\nSent: ${data.summary.sent}\\nFailed: ${data.summary.failed}\\nWaiting: ${data.summary.waiting}`); } else { // Auto run - just log console.log('✅ Auto-monitor run:', data.summary); } loadAutoSendQueue(); } else { if (!autoMode) { alert('Monitor failed: ' + data.error); } else { console.error('Auto-monitor failed:', data.error); } } } catch (error) { console.error('Failed to run monitor:', error); if (!autoMode) { alert('Error running monitor: ' + error.message); } } }; window.enableAutoSendForArtwork = async function(artworkSlug) { // Get first active backend wallet const response = await fetch('/api/manage-auto-send', { credentials: 'include' }); const data = await response.json(); if (!data.success || !data.wallets || data.wallets.length === 0) { alert('No backend wallet available. Generate one first!'); return; } const backendWalletId = data.wallets[0].id; try { const response = await fetch('/api/manage-auto-send', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ artworkSlug, backendWalletId, enableAutoSend: true }) }); const result = await response.json(); if (result.success) { alert(result.message); loadArtworkAutoSendSettings(); } else { alert('Failed: ' + result.error); } } catch (error) { console.error('Failed to enable auto-send:', error); alert('Error: ' + error.message); } }; window.loadBackendWalletsForImport = async function() { const select = document.getElementById('import-backend-wallet-select'); if (!select) return; try { const response = await fetch('/api/manage-auto-send', { credentials: 'include' }); const data = await response.json(); if (data.success && data.wallets) { select.innerHTML = '' + data.wallets.map(w => `` ).join(''); } } catch (error) { console.error('Failed to load wallets for import:', error); } }; window.loadPreInscribedCollectionsForImport = async function() { const select = document.getElementById('import-artwork-select'); if (!select) return; try { // Use admin-list to get ALL artworks (including inactive) const response = await fetch('/api/admin-list', { credentials: 'include' }); const data = await response.json(); console.log('Collections loaded for import:', data.artworks?.length || 0); if (data.success && data.artworks) { const preInscribed = data.artworks.filter(a => { const mintType = a.mintType || a.mint_type; // Support both camelCase and snake_case return mintType === 'pre_inscribed_v2' || mintType === 'pre_inscribed'; }); console.log('Pre-inscribed collections for import:', preInscribed.length); console.log('Mint types found:', data.artworks.map(a => a.mintType || a.mint_type).filter((v, i, a) => a.indexOf(v) === i)); if (preInscribed.length > 0) { select.innerHTML = '' + preInscribed.map(a => `` ).join(''); } else { select.innerHTML = ''; } } else { select.innerHTML = ''; } } catch (error) { console.error('Failed to load collections:', error); select.innerHTML = ''; } }; window.showWalletAddress = function() { const walletSelect = document.getElementById('import-backend-wallet-select'); const addressDiv = document.getElementById('selected-wallet-address'); const addressDisplay = document.getElementById('taproot-address-display'); const checkLink = document.getElementById('check-ordinals-link'); if (!walletSelect || !addressDiv) return; if (walletSelect.value) { const selectedOption = walletSelect.options[walletSelect.selectedIndex]; const taprootAddress = selectedOption.getAttribute('data-taproot'); if (taprootAddress) { addressDiv.style.display = 'block'; addressDisplay.textContent = taprootAddress; checkLink.href = `https://ordinals.com/address/${taprootAddress}`; } } else { addressDiv.style.display = 'none'; } }; window.importInscriptionIds = async function() { const walletSelect = document.getElementById('import-backend-wallet-select'); const artworkSelect = document.getElementById('import-artwork-select'); const inscriptionIdsInput = document.getElementById('inscription-ids-input'); const resultDiv = document.getElementById('import-result'); if (!walletSelect || !artworkSelect || !inscriptionIdsInput || !resultDiv) return; const backendWalletId = walletSelect.value; const artworkSlug = artworkSelect.value; const inscriptionIdsText = inscriptionIdsInput.value.trim(); if (!backendWalletId || !artworkSlug) { alert('Please select both a backend wallet and a collection'); return; } if (!inscriptionIdsText) { alert('Please enter at least one inscription ID'); return; } // Parse inscription IDs (one per line) const inscriptionIds = inscriptionIdsText .split('\\n') .map(id => id.trim()) .filter(id => id.length > 0); if (inscriptionIds.length === 0) { alert('No valid inscription IDs found'); return; } if (!confirm(`Link ${inscriptionIds.length} inscription(s) to collection with auto-send enabled?`)) { return; } resultDiv.style.display = 'block'; resultDiv.innerHTML = '

    Linking inscriptions... This may take a moment...

    '; try { const response = await fetch('/api/link-inscriptions-to-wallet', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backendWalletId: parseInt(backendWalletId), artworkSlug, inscriptionIds, enableAutoSend: true }) }); const data = await response.json(); if (data.success) { resultDiv.innerHTML = `
    ✅ Linked Successfully!
    Collection: ${data.artwork}
    Wallet: ${data.backendWallet}
    Linked: ${data.summary.imported}
    Skipped: ${data.summary.skipped}
    Failed: ${data.summary.failed}
    🚀 Auto-send ENABLED - Ordinals will automatically send when sold!
    `; // Clear input and refresh inscriptionIdsInput.value = ''; await loadBackendWallets(); await loadArtworkAutoSendSettings(); } else { resultDiv.innerHTML = `
    ❌ Import Failed
    ${data.error}
    `; } } catch (error) { console.error('Failed to link inscriptions:', error); resultDiv.innerHTML = `
    Error: ${error.message}
    `; } }; window.importBackendWalletOrdinals = async function() { const walletSelect = document.getElementById('import-backend-wallet-select'); const artworkSelect = document.getElementById('import-artwork-select'); const resultDiv = document.getElementById('import-result'); if (!walletSelect || !artworkSelect || !resultDiv) return; const backendWalletId = walletSelect.value; const artworkSlug = artworkSelect.value; if (!backendWalletId || !artworkSlug) { alert('Please select both a backend wallet and a collection'); return; } // Get taproot address from selected wallet const selectedOption = walletSelect.options[walletSelect.selectedIndex]; const taprootAddress = selectedOption.getAttribute('data-taproot'); if (!taprootAddress) { alert('Selected wallet does not have a Taproot address'); return; } if (!confirm(`Import all ordinals from backend wallet to collection and enable auto-send?\\n\\nThis will fetch inscriptions from: ${taprootAddress}`)) { return; } resultDiv.style.display = 'block'; resultDiv.innerHTML = '

    Importing ordinals... This may take a moment...

    '; try { const response = await fetch('/api/import-backend-wallet-ordinals', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backendWalletId: parseInt(backendWalletId), artworkSlug, taprootAddress, enableAutoSend: true }) }); const data = await response.json(); if (data.success) { resultDiv.innerHTML = `
    ✅ Import Successful!
    Collection: ${data.artwork}
    Wallet: ${data.backendWallet}
    Imported: ${data.summary.imported}
    Skipped: ${data.summary.skipped}
    Failed: ${data.summary.failed}
    ${data.autoSendEnabled ? '
    🚀 Auto-send ENABLED - Ordinals will automatically send when sold!
    ' : ''}
    `; // Refresh the wallets and artwork settings await loadBackendWallets(); await loadArtworkAutoSendSettings(); } else { resultDiv.innerHTML = `
    ❌ Import Failed
    ${data.error}
    `; } } catch (error) { console.error('Failed to import ordinals:', error); resultDiv.innerHTML = `
    Error: ${error.message}
    `; } }; window.disableAutoSendForArtwork = async function(artworkSlug) { const response = await fetch('/api/manage-auto-send', { credentials: 'include' }); const data = await response.json(); if (!data.success || !data.wallets || data.wallets.length === 0) { alert('No backend wallet available.'); return; } const backendWalletId = data.wallets[0].id; try { const response = await fetch('/api/manage-auto-send', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ artworkSlug, backendWalletId, enableAutoSend: false }) }); const result = await response.json(); if (result.success) { alert(result.message); loadArtworkAutoSendSettings(); } else { alert('Failed: ' + result.error); } } catch (error) { console.error('Failed to disable auto-send:', error); alert('Error: ' + error.message); } }; // Load wallet reconciliation data with pagination // Load wallet reconciliation data with pagination async function loadWalletReconciliation(page = 1) { const container = document.getElementById('wallet-transactions-list'); if (!container) return; container.innerHTML = '

    Loading wallet transactions...

    '; // Get selected limit const limitSelect = document.getElementById('reconciliation-limit'); const limit = limitSelect ? parseInt(limitSelect.value) : 50; try { const response = await fetch(`/api/wallet-reconciliation?page=${page}&limit=${limit}`, { credentials: 'include' }); const data = await response.json(); if (data.success) { displayWalletReconciliation(data, page); } else { container.innerHTML = '

    Failed to load reconciliation data

    '; } } catch (error) { console.error('Error loading wallet reconciliation:', error); container.innerHTML = '

    Error loading reconciliation data

    '; } } // Display wallet reconciliation data function displayWalletReconciliation(data, currentPage = 1) { // Update summary stats document.getElementById('total-outgoing').textContent = `${data.summary.totalOutgoingValue.toLocaleString()} sats`; document.getElementById('accounted-value').textContent = `${data.summary.accountedValue.toLocaleString()} sats`; document.getElementById('unaccounted-value').textContent = `${data.summary.unaccountedValue.toLocaleString()} sats`; document.getElementById('reconciliation-percentage').textContent = `${data.summary.reconciliationPercentage}%`; // Display transaction list const container = document.getElementById('wallet-transactions-list'); if (!container) return; if (data.transactions.length === 0) { container.innerHTML = '

    No outgoing transactions found

    '; return; } const transactionsList = data.transactions.map((tx, index) => { const date = tx.blockTime ? new Date(tx.blockTime * 1000).toLocaleString() : 'Unconfirmed'; const statusColor = tx.isAccounted ? '#4CAF50' : '#ff6b35'; const statusText = tx.isAccounted ? 'ACCOUNTED' : 'UNACCOUNTED'; return `
    Transaction ${index + 1} ${statusText}
    ${tx.totalSent.toLocaleString()} sats
    Fee: ${tx.fee.toLocaleString()} sats

    TX ID: ${tx.txid}

    Date: ${date} ${tx.blockHeight ? ` | Block: ${tx.blockHeight}` : ''}

    ${tx.outputs.length > 0 ? `

    Recipients:

    ${tx.outputs.map(output => `
    ${output.address} → ${output.value.toLocaleString()} sats
    `).join('')}
    ` : ''} ${!tx.isAccounted ? `
    ` : ''}
    `; }).join(''); container.innerHTML = transactionsList; // Add pagination controls const paginationContainer = document.getElementById('reconciliation-pagination'); if (paginationContainer && data.pagination) { let paginationHTML = ` Page ${data.pagination.currentPage} of ${data.pagination.totalPages} | ${data.pagination.totalOnPage} of ${data.pagination.totalTransactions} transactions `; // Previous button if (currentPage > 1) { paginationHTML += ` `; } // Next button if (data.pagination.hasMore) { paginationHTML += ` `; } paginationContainer.innerHTML = paginationHTML; } } // Add custom payment from unaccounted transaction window.addCustomPaymentFromTransaction = async function(txId, amount, recipientAddress = null) { try { await ensureAdminSensitiveModals(); } catch (e) { console.error(e); alert('Could not load admin UI. Refresh and try again.'); return; } document.getElementById('custom-payment-modal').style.display = 'block'; setupCustomPaymentForm(); // Pre-fill form fields document.getElementById('custom-payment-amount').value = amount; document.getElementById('custom-payment-type').value = 'other'; document.getElementById('custom-payment-description').value = `Unaccounted wallet transaction - ${txId}`; document.getElementById('custom-payment-tx-id').value = txId; document.getElementById('custom-payment-notes').value = 'Added from wallet reconciliation - please update description and recipient info'; // Pre-fill recipient address if provided if (recipientAddress) { document.getElementById('custom-payment-wallet').value = recipientAddress; } alert('Pre-filled custom payment form with transaction data. Please review and update the details before saving. The reconciliation screen will refresh after saving to show the transaction as accounted for.'); }; // Load admin statistics async function loadAdminStats() { try { const response = await fetch('/api/admin-stats', { credentials: 'include' }); const data = await response.json(); if (data.success) { displayAdminStats(data); } else { console.error('Failed to load admin stats:', data.error); document.getElementById('top-artists-earnings').innerHTML = '

    Failed to load earnings data

    '; document.getElementById('top-artists-fees').innerHTML = '

    Failed to load fees data

    '; } } catch (error) { console.error('Error loading admin stats:', error); document.getElementById('top-artists-earnings').innerHTML = '

    Error loading data

    '; document.getElementById('top-artists-fees').innerHTML = '

    Error loading data

    '; } } // Display admin statistics function displayAdminStats(data) { // Display top artists by earnings const earningsContainer = document.getElementById('top-artists-earnings'); if (data.topArtistsByEarnings && data.topArtistsByEarnings.length > 0) { earningsContainer.innerHTML = data.topArtistsByEarnings.map((artist, index) => `
    ${index + 1}. ${artist.artist}
    ${artist.totalEarnings.toLocaleString()} sats
    ${artist.artworkCount} artwork${artist.artworkCount !== 1 ? 's' : ''}
    `).join(''); } else { earningsContainer.innerHTML = '

    No earnings data available

    '; } // Display top artists by fees generated const feesContainer = document.getElementById('top-artists-fees'); if (data.topArtistsByFees && data.topArtistsByFees.length > 0) { feesContainer.innerHTML = data.topArtistsByFees.map((artist, index) => `
    ${index + 1}. ${artist.artist}
    ${artist.totalFees.toLocaleString()} sats
    ${artist.totalMints} mint${artist.totalMints !== 1 ? 's' : ''}
    `).join(''); } else { feesContainer.innerHTML = '

    No fees data available

    '; } // Display platform overview stats document.getElementById('total-artists').textContent = data.platformStats?.totalArtists || '0'; document.getElementById('total-artworks').textContent = data.platformStats?.totalArtworks || '0'; document.getElementById('total-mints').textContent = data.platformStats?.totalMints?.toLocaleString() || '0'; document.getElementById('total-volume').textContent = data.platformStats?.totalVolume ? `${data.platformStats.totalVolume.toLocaleString()} sats` : '0 sats'; } // Global variable to track connected wallet for stamps let stampsWalletAddress = null; // Load stamps for admin panel async function loadStamps() { try { const stampsList = document.getElementById('stamps-list'); if (!stampsList) return; stampsList.innerHTML = '
    Loading stamps...
    '; let url = '/api/get-stamps'; if (stampsWalletAddress) { url = `/api/check-stamp-holdings?wallet=${encodeURIComponent(stampsWalletAddress)}`; } const response = await fetch(url, { credentials: 'include' }); const data = await response.json(); if (data.success) { displayStamps(data.stamps); } else { console.error('Failed to load stamps:', data.error); stampsList.innerHTML = `
    Error loading stamps: ${data.error}
    `; } } catch (error) { console.error('Error loading stamps:', error); const stampsList = document.getElementById('stamps-list'); if (stampsList) { stampsList.innerHTML = '
    Failed to load stamps
    '; } } } // Track current series filter for my stamps let myStampsCurrentSeries = null; // Load card packs for stamps tab async function loadCardPacks(series = null) { if (!stampsWalletAddress) { document.getElementById('card-packs-section').style.display = 'none'; document.getElementById('stamps-season1-section').style.display = 'none'; return; } // Update current series filter if (series !== undefined) { myStampsCurrentSeries = series; } try { // Fetch card packs (for displaying packs to rip) const packsResponse = await fetch(`/api/get-user-card-packs?address=${encodeURIComponent(stampsWalletAddress)}`); const packsData = await packsResponse.json(); if (packsData.success && packsData.packs.length > 0) { displayCardPacks(packsData.packs); document.getElementById('card-packs-section').style.display = 'block'; } else { document.getElementById('card-packs-section').style.display = 'none'; } // Separately fetch stamps from card_pack_stamps by recipient address let stampsUrl = `/api/get-user-stamps?wallet=${encodeURIComponent(stampsWalletAddress)}`; if (myStampsCurrentSeries) { stampsUrl += `&series=${encodeURIComponent(myStampsCurrentSeries)}`; } const stampsResponse = await fetch(stampsUrl); const stampsData = await stampsResponse.json(); // Populate series dropdown const seriesSelect = document.getElementById('my-stamps-series-select'); if (seriesSelect && stampsData.availableSeries && stampsData.availableSeries.length > 0) { const currentValue = seriesSelect.value; seriesSelect.innerHTML = '' + stampsData.availableSeries.map(s => { const displayName = s.stamp_series_name || s.stamp_series; const artistSuffix = s.artist ? ` (by ${s.artist})` : ''; return ``; }).join(''); } if (stampsData.success && stampsData.stamps.length > 0) { displayStampsSeason1(stampsData.stamps); document.getElementById('stamps-season1-section').style.display = 'block'; } else { document.getElementById('stamps-season1-section').style.display = 'none'; } } catch (error) { console.error('Error loading card packs/stamps:', error); document.getElementById('card-packs-section').style.display = 'none'; document.getElementById('stamps-season1-section').style.display = 'none'; } } // Filter my stamps by series window.filterMyStampsBySeries = function(series) { myStampsCurrentSeries = series || null; loadCardPacks(myStampsCurrentSeries); }; // Display card packs function displayCardPacks(packs) { const packsList = document.getElementById('card-packs-list'); if (!packsList) return; packsList.innerHTML = packs.map(pack => { // Check if pack is opened OR in process (queued/processing/success) // Treat "failed" same as "queued" from user perspective - they just see "processing" const isOpened = pack.opened || pack.open_status === 'success'; const isQueued = pack.open_status === 'queued' || pack.open_status === 'processing' || pack.open_status === 'failed'; const isProcessed = isOpened || isQueued; const borderColor = isOpened ? '#666666' : (isQueued ? '#ff9800' : 'var(--neon-green)'); const grayFilter = isProcessed ? 'filter: grayscale(100%) brightness(0.4);' : ''; // Render image content based on parent_type (same as home page) let imageContent; if (pack.parent_inscription_id) { if (pack.parent_type === 'recursive') { // Recursive content - use nested iframe like home page imageContent = ` `; } else { // Static image imageContent = ` ${pack.artwork_title || 'Card Pack'}`; } } else { // Fallback placeholder imageContent = `
    CARD PACK
    `; } // Determine overlay and status text // Note: "failed" is treated as "queued" from user perspective - they just see "processing" let overlayContent = ''; let statusText = ''; if (isOpened) { overlayContent = `
    RIPPED!
    `; statusText = `
    Opened ${pack.opened_at ? new Date(pack.opened_at).toLocaleDateString() : 'recently'}
    ${pack.stamps?.length || 5} stamps sent to wallet
    `; } else if (isQueued) { // Show "processing" for queued, processing, AND failed statuses overlayContent = `
    PROCESSING
    `; statusText = `
    🔄 Minting in progress...
    Your stamps are being created
    `; } return `
    ${imageContent} ${overlayContent}

    ${pack.artwork_title || 'Card Pack'}

    ${isProcessed ? statusText : ` `}
    `; }).join(''); } // Display stamps from opened card packs (Season 1) function displayStampsSeason1(stamps) { const stampsList = document.getElementById('stamps-season1-list'); if (!stampsList) return; stampsList.innerHTML = stamps.map((stamp, index) => { const inscriptionId = stamp.stamp_inscription_id; const hasInscription = inscriptionId && inscriptionId.length > 10; const containerId = `stamp-${index}`; if (hasInscription) { return `
    ${inscriptionId.substring(0, 8)}...
    `; } else { // No inscription ID yet - show waiting state return `
    AWAITING
    CONFIRMATION
    Pending...
    `; } }).join(''); } // Check if stamp inscription has loaded (exists on chain) window.checkStampLoaded = async function(iframe, inscriptionId, containerId) { try { // Try to fetch the inscription to see if it exists const response = await fetch(`https://ordinals.com/content/${inscriptionId}`, { method: 'HEAD' }); if (!response.ok) { // Show waiting overlay const overlay = document.getElementById(`${containerId}-overlay`); if (overlay) overlay.style.display = 'flex'; } } catch (e) { // Show waiting overlay on error const overlay = document.getElementById(`${containerId}-overlay`); if (overlay) overlay.style.display = 'flex'; } }; // Show open pack modal with video function showOpenPackModal(packInscriptionId, packTitle, videoUrl, stampsPerPack = 5) { // Default video URL if none provided const defaultVideoUrl = 'https://pub-c4a124b338574b5dae1d2b555bfa25a6.r2.dev/cards/pack-rip.mov'; const packVideoUrl = videoUrl || defaultVideoUrl; // Create modal overlay const overlay = document.createElement('div'); overlay.id = 'pack-modal-overlay'; overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 10000; display: flex; align-items: center; justify-content: center;'; overlay.innerHTML = `

    ${packTitle}

    Ready to open your card pack?
    ${stampsPerPack} stamp${stampsPerPack !== 1 ? 's' : ''} will be minted to your ordinals wallet

    `; document.body.appendChild(overlay); // Close on backdrop click (only when not playing) overlay.addEventListener('click', (e) => { if (e.target === overlay && document.getElementById('pack-open-btn-container').style.display !== 'none') { closePackModal(); } }); } // Close pack modal function closePackModal() { const overlay = document.getElementById('pack-modal-overlay'); if (overlay) overlay.remove(); } // Stamp set rarity data const stampSetData = { 'season1': { name: 'On Chain Stamps', artist: 'SXULR', holoChance: '0.05% (1 in 2,000)', rarities: [ { tier: '★★★ Ultra Rare', probability: '0.32%', odds: '1 in 311', color: '#ffd700', bgGradient: 'linear-gradient(90deg, rgba(255,215,0,0.2) 0%, rgba(255,107,0,0.2) 100%)', stamps: 'OMB - Puff, OMB - War' }, { tier: '★★ Illustration Rare', probability: '2.89%', odds: '1 in 35', color: '#9370db', bgGradient: 'rgba(147,112,219,0.2)', stamps: 'Black Swan, Ord Family, World Peace, Shrooms' }, { tier: '★ Rare', probability: '14.47%', odds: '1 in 7', color: '#ffa500', bgGradient: 'rgba(255,165,0,0.15)', stamps: 'Lunatic, Fire Ninja, Billy, FTW, Aeon, Honeybadger' }, { tier: '★ Ace (Pink)', probability: '9.65%', odds: '1 in 10', color: '#ff69b4', bgGradient: 'rgba(255,105,180,0.15)', stamps: 'Pepe, Aces' }, { tier: '★ Uncommon (Silver)', probability: '29.90%', odds: '1 in 3', color: '#c0c0c0', bgGradient: 'rgba(192,192,192,0.15)', stamps: 'Banana, Altcoiner, Dickbutt' }, { tier: 'Common', probability: '42.77%', odds: '1 in 2', color: '#888888', bgGradient: 'rgba(100,100,100,0.15)', stamps: 'Goosinal, OCM, MIM, Bull, Bear, QUANTUM Cat, Bitamigos, Wizard of Ord' } ] }, 'stampsS1': { name: 'Stamps S1', artist: 'TheArtist', holoChance: '2.70% (1 in 37)', rarities: [ { tier: '★★★ Ultra Rare (3 Star)', probability: '0.32%', odds: '1 in 311', color: '#ffd700', bgGradient: 'linear-gradient(90deg, rgba(255,215,0,0.2) 0%, rgba(255,107,0,0.2) 100%)', stamps: 'Fiction' }, { tier: '★★ Illustration Rare (2 Star)', probability: '2.89%', odds: '1 in 35', color: '#9370db', bgGradient: 'rgba(147,112,219,0.2)', stamps: 'Fantasy, Poison, Soft Serve' }, { tier: '★ Rare (1 Star)', probability: '14.47%', odds: '1 in 7', color: '#ffa500', bgGradient: 'rgba(255,165,0,0.15)', stamps: 'FREEDOM, The Other Side, The Hideout, INK' }, { tier: '★ Ace (Pink Star)', probability: '9.65%', odds: '1 in 10', color: '#ff69b4', bgGradient: 'rgba(255,105,180,0.15)', stamps: 'Stock, New Felon' }, { tier: 'Uncommon', probability: '29.90%', odds: '1 in 3', color: '#c0c0c0', bgGradient: 'rgba(192,192,192,0.15)', stamps: 'Mask UP, SEXY' }, { tier: 'Common', probability: '42.77%', odds: '1 in 2', color: '#888888', bgGradient: 'rgba(100,100,100,0.15)', stamps: 'Short Memory, F THAT, Wake the Dead' } ] } }; let currentRaritySet = 'season1'; // Show rarity info modal function showRarityInfoModal() { const overlay = document.createElement('div'); overlay.id = 'rarity-modal-overlay'; overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 10000; display: flex; align-items: center; justify-content: center; overflow-y: auto; padding: 20px;'; overlay.innerHTML = `

    ⭐ RARITY TIERS & LAUNCH RATES ⭐

    ✨ HOLOGRAPHIC VARIANT ✨

    Any card has a chance to be a holographic variant, regardless of rarity tier.

    `; // Close on overlay click (outside modal) overlay.addEventListener('click', (e) => { if (e.target === overlay) closeRarityModal(); }); document.body.appendChild(overlay); // Generate set buttons dynamically const buttonsContainer = document.getElementById('rarity-set-buttons'); if (buttonsContainer) { buttonsContainer.innerHTML = Object.keys(stampSetData).map(key => { const set = stampSetData[key]; return ``; }).join(''); } // Initialize with current set switchRaritySet(currentRaritySet); } // Switch between rarity sets function switchRaritySet(setKey) { currentRaritySet = setKey; const setData = stampSetData[setKey]; if (!setData) return; // Update button styles Object.keys(stampSetData).forEach(key => { const btn = document.getElementById(`rarity-btn-${key}`); if (btn) { if (key === setKey) { btn.style.background = 'var(--neon-green)'; btn.style.color = '#000'; btn.style.border = '2px solid var(--neon-green)'; } else { btn.style.background = 'transparent'; btn.style.color = '#888'; btn.style.border = '2px solid #444'; } } }); // Update holo chance const holoSpan = document.getElementById('rarity-holo-chance'); if (holoSpan) holoSpan.textContent = setData.holoChance; // Generate table const container = document.getElementById('rarity-table-container'); if (!container) return; let tableHtml = ` `; setData.rarities.forEach(rarity => { tableHtml += ` `; }); tableHtml += '
    RARITY PROBABILITY ODDS STAMPS
    ${rarity.tier} ${rarity.probability} ${rarity.odds} ${rarity.stamps}
    '; container.innerHTML = tableHtml; } window.switchRaritySet = switchRaritySet; // Close rarity modal function closeRarityModal() { const overlay = document.getElementById('rarity-modal-overlay'); if (overlay) overlay.remove(); } window.closeRarityModal = closeRarityModal; window.showRarityInfoModal = showRarityInfoModal; // Show mint packs selection modal async function showMintPacksModal() { const overlay = document.createElement('div'); overlay.id = 'mint-packs-modal-overlay'; overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 10000; display: flex; align-items: center; justify-content: center; overflow-y: auto; padding: 20px;'; overlay.innerHTML = `

    SELECT A PACK

    Loading available packs...

    `; // Close on overlay click (outside modal) overlay.addEventListener('click', (e) => { if (e.target === overlay) closeMintPacksModal(); }); document.body.appendChild(overlay); // Fetch active card packs try { const response = await fetch('/api/list-artworks'); const data = await response.json(); if (data.success && data.artworks) { // Filter for active card packs (isCardPack from formatted response) const cardPacks = data.artworks.filter(a => a.isCardPack === true); const listContainer = document.getElementById('mint-packs-list'); if (!listContainer) return; if (cardPacks.length === 0) { listContainer.innerHTML = '

    No stamp packs available right now.

    '; return; } listContainer.innerHTML = cardPacks.map(pack => { // For recursive artworks, use iframe; for static, use background image const imageHtml = pack.parentType === 'recursive' ? `` : `
    `; return `
    ${imageHtml}

    ${pack.title}

    by ${pack.artist}

    ${pack.price > 0 ? pack.price.toLocaleString() + ' sats' : 'FREE'} ${pack.stampsPerPack ? ' • ' + pack.stampsPerPack + ' stamps' : ''}

    `}).join(''); } else { const listContainer = document.getElementById('mint-packs-list'); if (listContainer) { listContainer.innerHTML = '

    Failed to load packs.

    '; } } } catch (error) { console.error('Error loading packs:', error); const listContainer = document.getElementById('mint-packs-list'); if (listContainer) { listContainer.innerHTML = '

    Error loading packs.

    '; } } } function selectMintPack(slug) { closeMintPacksModal(); navigateToArtwork(slug); } window.selectMintPack = selectMintPack; function closeMintPacksModal() { const overlay = document.getElementById('mint-packs-modal-overlay'); if (overlay) overlay.remove(); } window.closeMintPacksModal = closeMintPacksModal; window.showMintPacksModal = showMintPacksModal; // Show all stamps modal let allStampsCurrentPage = 0; let allStampsCurrentSeries = null; // null means all series const allStampsPerPage = 50; async function showAllStampsModal() { allStampsCurrentPage = 0; allStampsCurrentSeries = null; const overlay = document.createElement('div'); overlay.id = 'all-stamps-modal-overlay'; overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 10000; display: flex; align-items: center; justify-content: center; overflow-y: auto; padding: 20px;'; overlay.innerHTML = `

    ALL COLLECTABLES

    Loading...

    Loading stamps...

    `; // Close on overlay click (outside modal) overlay.addEventListener('click', (e) => { if (e.target === overlay) closeAllStampsModal(); }); document.body.appendChild(overlay); // Load first page await loadAllStampsPage(0); } // Filter stamps by series window.filterAllStampsBySeries = function(series) { allStampsCurrentSeries = series || null; allStampsCurrentPage = 0; loadAllStampsPage(0); }; async function loadAllStampsPage(page) { allStampsCurrentPage = page; const offset = page * allStampsPerPage; const grid = document.getElementById('all-stamps-grid'); const countEl = document.getElementById('all-stamps-count'); const pagination = document.getElementById('all-stamps-pagination'); const seriesSelect = document.getElementById('all-stamps-series-select'); grid.innerHTML = '

    Loading stamps...

    '; try { // Build URL with optional series filter let url = `/api/get-latest-stamps?limit=${allStampsPerPage}&offset=${offset}`; if (allStampsCurrentSeries) { url += `&series=${encodeURIComponent(allStampsCurrentSeries)}`; } const response = await fetch(url); const data = await response.json(); // Populate series dropdown (only on first load or if dropdown is empty) if (seriesSelect && data.availableSeries && data.availableSeries.length > 0) { const currentValue = seriesSelect.value; seriesSelect.innerHTML = '' + data.availableSeries.map(s => { const displayName = s.stamp_series_name || s.stamp_series; const artistSuffix = s.artist ? ` (by ${s.artist})` : ''; return ``; }).join(''); } if (data.success && data.stamps && data.stamps.length > 0) { const totalPages = Math.ceil(data.total / allStampsPerPage); countEl.textContent = `Showing ${offset + 1}-${Math.min(offset + data.stamps.length, data.total)} of ${data.total} stamps`; grid.innerHTML = data.stamps.map(stamp => `
    `).join(''); // Build pagination let paginationHtml = ''; // Previous button if (page > 0) { paginationHtml += ``; } // Page numbers (show max 7 pages around current) const startPage = Math.max(0, page - 3); const endPage = Math.min(totalPages - 1, page + 3); if (startPage > 0) { paginationHtml += ``; if (startPage > 1) paginationHtml += `...`; } for (let i = startPage; i <= endPage; i++) { const isActive = i === page; paginationHtml += ``; } if (endPage < totalPages - 1) { if (endPage < totalPages - 2) paginationHtml += `...`; paginationHtml += ``; } // Next button if (page < totalPages - 1) { paginationHtml += ``; } pagination.innerHTML = paginationHtml; } else { countEl.textContent = '0 stamps'; grid.innerHTML = '

    No stamps found

    '; pagination.innerHTML = ''; } } catch (error) { console.error('Error loading all stamps:', error); countEl.textContent = 'Error'; grid.innerHTML = '

    Error loading stamps

    '; pagination.innerHTML = ''; } } window.loadAllStampsPage = loadAllStampsPage; // Close all stamps modal function closeAllStampsModal() { const overlay = document.getElementById('all-stamps-modal-overlay'); if (overlay) overlay.remove(); } window.closeAllStampsModal = closeAllStampsModal; window.showAllStampsModal = showAllStampsModal; // Open card pack async function openCardPack(packInscriptionId) { const btnContainer = document.getElementById('pack-open-btn-container'); const videoContainer = document.getElementById('pack-video-container'); const video = document.getElementById('pack-rip-video'); const resultDiv = document.getElementById('pack-result'); const resultMessage = document.getElementById('pack-result-message'); const modalTitle = document.getElementById('pack-modal-title'); // Hide button container and title btnContainer.style.display = 'none'; modalTitle.style.display = 'none'; // Show video container and fade in videoContainer.style.display = 'block'; setTimeout(() => { videoContainer.style.opacity = '1'; }, 50); // Play video try { await video.play(); } catch (e) { console.log('Video autoplay failed:', e); } // When video ends, fade out and show result video.onended = async () => { // Fade out video videoContainer.style.opacity = '0'; setTimeout(async () => { videoContainer.style.display = 'none'; // Call API to queue the stamps try { const response = await fetch('/api/open-card-pack', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ packInscriptionId: packInscriptionId, recipientAddress: stampsWalletAddress }) }); const result = await response.json(); if (result.success) { // Refresh card packs immediately so it shows as "processing" loadCardPacks(); resultMessage.innerHTML = `
    PACK OPENED!
    Your stamps have been entered into the queue
    and will be minted directly to your wallet.
    `; } else { resultMessage.innerHTML = `
    ${result.error || 'Failed to open pack'}
    `; } } catch (error) { resultMessage.innerHTML = `
    Error: ${error.message}
    `; } // Show result with fade in resultDiv.style.display = 'block'; setTimeout(() => { resultDiv.style.opacity = '1'; }, 50); }, 500); // Wait for fade out }; // Fallback if video doesn't trigger onended (e.g., format issues) setTimeout(() => { if (videoContainer.style.display === 'block' && videoContainer.style.opacity === '1') { video.onended(); } }, 15000); // 15 second max fallback } // Expose pack functions globally window.showOpenPackModal = showOpenPackModal; window.openCardPack = openCardPack; window.closePackModal = closePackModal; // Display stamps in admin panel function displayStamps(stamps) { const stampsList = document.getElementById('stamps-list'); if (!stampsList) return; if (stamps.length === 0) { stampsList.innerHTML = '

    No stamps available yet

    '; return; } stampsList.innerHTML = stamps.map(stamp => { const isOwned = stamp.owned || false; const imageFilter = stampsWalletAddress ? (isOwned ? 'none' : 'grayscale(100%) brightness(0.5)') : 'grayscale(100%) brightness(0.5)'; const borderColor = isOwned ? '#4CAF50' : '#ffffff'; const stampBadgeColor = isOwned ? '#4CAF50' : '#000000'; // Use parent inscription ID for image source let imageSrc; if (stamp.parent_inscription_id) { if (stamp.parent_type === 'recursive') { imageSrc = `
    `; } else { imageSrc = `${stamp.title}`; } } else { imageSrc = `
    NO IMAGE
    `; } return `
    ${imageSrc}

    ${stamp.title}

    by ${stamp.artist}

    ${stampsWalletAddress ? `
    ${isOwned ? '✓ YOU OWN THIS STAMP!' : '✗ You don\'t own this stamp'}
    ` : `
    Connect wallet to see if you own this stamp
    `}
    `; }).join(''); } // Connect wallet for stamps functionality window.connectWalletForStamps = async function() { try { const connectBtn = document.getElementById('connect-wallet-stamps-btn'); if (connectBtn) { connectBtn.innerHTML = ' Connecting...'; connectBtn.disabled = true; } // Use ordinals address for stamps const response = await request('getAccounts', { purposes: [AddressPurpose.Payment, AddressPurpose.Ordinals], message: 'Connect your wallet to check stamp ownership' }); if (response.status === 'success' && response.result && response.result.length > 0) { const ordinalsAddress = response.result.find(addr => addr.purpose === AddressPurpose.Ordinals); const paymentAddress = response.result.find(addr => addr.purpose === AddressPurpose.Payment); // Prefer ordinals address, fallback to payment address const addressToUse = ordinalsAddress || paymentAddress; if (addressToUse) { stampsWalletAddress = addressToUse.address; if (connectBtn) { connectBtn.innerHTML = 'Wallet Connected ✅'; connectBtn.disabled = true; } updateStampsWalletStatus(); loadStamps(); // Reload stamps to check ownership loadCardPacks(); // Load any card packs the user owns } else { throw new Error('No payment address found'); } } else { throw new Error('Wallet connection failed'); } } catch (error) { console.error('Error connecting wallet for stamps:', error); const connectBtn = document.getElementById('connect-wallet-stamps-btn'); if (connectBtn) { connectBtn.innerHTML = 'CONNECT WALLET'; connectBtn.disabled = false; } alert('Failed to connect wallet: ' + error.message); } } // Update wallet status display for stamps function updateStampsWalletStatus() { const walletStatus = document.getElementById('wallet-status-stamps'); const connectBtn = document.getElementById('connect-wallet-stamps-btn'); if (stampsWalletAddress) { walletStatus.textContent = `Connected: ${stampsWalletAddress.substring(0, 8)}...${stampsWalletAddress.substring(stampsWalletAddress.length - 8)}`; connectBtn.textContent = 'DISCONNECT'; connectBtn.onclick = disconnectWalletForStamps; } else { walletStatus.textContent = ''; connectBtn.textContent = 'CONNECT WALLET'; connectBtn.onclick = connectWalletForStamps; } } // Disconnect wallet for stamps function disconnectWalletForStamps() { stampsWalletAddress = null; updateStampsWalletStatus(); loadStamps(); // Reload stamps to show them greyed out document.getElementById('card-packs-section').style.display = 'none'; // Hide card packs } // Load collection inquiries for admin review async function loadCollectionInquiries() { try { const response = await fetch('/api/admin-collection-inquiries', { credentials: 'include' }); const data = await response.json(); if (data.success && data.inquiries) { displayCollectionInquiries(data.inquiries); } else { const inquiriesList = document.getElementById('inquiries-admin-list'); if (inquiriesList) { inquiriesList.innerHTML = '

    No collection inquiries found

    '; } } } catch (error) { const inquiriesList = document.getElementById('inquiries-admin-list'); if (inquiriesList) { inquiriesList.innerHTML = '

    Failed to load collection inquiries

    '; } } } // Display collection inquiries in admin interface function displayCollectionInquiries(inquiries) { const inquiriesList = document.getElementById('inquiries-admin-list'); if (!inquiriesList) return; if (inquiries.length === 0) { inquiriesList.innerHTML = '

    No collection inquiries submitted yet

    '; return; } inquiriesList.innerHTML = inquiries.map(inquiry => `

    ${inquiry.title}

    ${inquiry.status.toUpperCase()}

    Contact Name: ${inquiry.name}

    Email: ${inquiry.email}

    Preferred Contact: ${inquiry.contactMethod}

    ${inquiry.contactHandle ? `

    Contact Handle: ${inquiry.contactHandle}

    ` : ''}

    Submitted By: ${inquiry.submittedBy.username} (${inquiry.submittedBy.email})

    Submitted: ${new Date(inquiry.createdAt).toLocaleDateString()}

    ID: #${inquiry.id}

    Project Details:

    ${inquiry.details}
    `).join(''); } // Update inquiry status window.updateInquiryStatus = async function(inquiryId, newStatus) { try { const response = await fetch('/api/update-inquiry-status', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ inquiryId: inquiryId, status: newStatus }) }); const data = await response.json(); if (data.success) { // Reload the inquiries to show updated status loadCollectionInquiries(); } else { alert('Failed to update status: ' + (data.error || 'Unknown error')); } } catch (error) { alert('Failed to update status: Network error'); } }; // Load wolf tickets for admin window.loadWolfTickets = async function() { try { const container = document.getElementById('wolf-tickets-list'); if (!container) return; container.innerHTML = '

    Loading wolf tickets...

    '; const response = await fetch('/api/admin-wolf-tickets', { credentials: 'include' }); const data = await response.json(); if (data.success && data.tickets) { // Filter by status if selected const statusFilter = document.getElementById('wolf-tickets-status-filter'); let filteredTickets = data.tickets; if (statusFilter && statusFilter.value) { filteredTickets = data.tickets.filter(t => t.status === statusFilter.value); } displayWolfTickets(filteredTickets); } else { container.innerHTML = '

    No wolf tickets found

    '; } } catch (error) { console.error('Error loading wolf tickets:', error); const container = document.getElementById('wolf-tickets-list'); if (container) { container.innerHTML = '

    Failed to load wolf tickets

    '; } } }; // Display wolf tickets function displayWolfTickets(tickets) { const container = document.getElementById('wolf-tickets-list'); if (!container) return; if (tickets.length === 0) { container.innerHTML = '

    No tickets to display

    '; return; } container.innerHTML = tickets.map(ticket => { const statusColors = { 'pending': '#ffcc00', 'in_progress': '#00aaff', 'resolved': '#00ff00', 'rejected': '#ff6666' }; const statusColor = statusColors[ticket.status] || '#ffffff'; return `

    Ticket #${ticket.id}

    ${ticket.status.toUpperCase()}

    Wallet: ${ticket.wallet_address}

    Claimed Count: ${ticket.claimed_count || 'N/A'}

    Submitted: ${new Date(ticket.created_at).toLocaleString()}

    ${ticket.updated_at ? `

    Updated: ${new Date(ticket.updated_at).toLocaleString()}

    ` : ''} ${ticket.resolved_at ? `

    Resolved: ${new Date(ticket.resolved_at).toLocaleString()}

    ` : ''}

    Transaction IDs:

    ${ticket.transaction_ids}
    ${ticket.admin_notes ? `

    Admin Notes:

    ${ticket.admin_notes}
    ` : ''}
    `; }).join(''); } // Update wolf ticket status window.updateWolfTicket = async function(ticketId, newStatus) { try { const notesTextarea = document.getElementById(`notes-${ticketId}`); const adminNotes = notesTextarea ? notesTextarea.value.trim() : ''; const response = await fetch('/api/update-wolf-ticket', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ ticketId: ticketId, status: newStatus, adminNotes: adminNotes }) }); const data = await response.json(); if (data.success) { // Reload the tickets to show updated status loadWolfTickets(); } else { alert('Failed to update ticket: ' + (data.error || 'Unknown error')); } } catch (error) { console.error('Error updating wolf ticket:', error); alert('Failed to update ticket: Network error'); } }; // Load wolf claims for admin window.loadWolfClaims = async function() { try { const container = document.getElementById('wolf-claims-list'); const statsContainer = document.getElementById('wolf-claims-stats'); if (!container || !statsContainer) return; container.innerHTML = '

    Loading wolf claims...

    '; statsContainer.innerHTML = '

    Loading statistics...

    '; const response = await fetch('/api/admin-wolf-claims', { credentials: 'include' }); const data = await response.json(); if (data.success) { displayWolfClaimsStats(data.stats); displayWolfClaims(data.largeClaimers, data.regularBatches, data.existingBatches); } else { container.innerHTML = '

    Failed to load claims: ' + (data.error || 'Unknown error') + '

    '; } } catch (error) { console.error('Error loading wolf claims:', error); const container = document.getElementById('wolf-claims-list'); if (container) { container.innerHTML = '

    Failed to load wolf claims

    '; } } }; // Display wolf claims statistics function displayWolfClaimsStats(stats) { const container = document.getElementById('wolf-claims-stats'); if (!container) return; container.innerHTML = `

    TOTAL CLAIMERS

    ${stats.totalClaimers || 0}

    TOTAL CLAIMS

    ${stats.totalClaims || 0}

    LARGE CLAIMERS (>100)

    ${stats.largeClaimersCount || 0}

    REGULAR BATCHES

    ${stats.regularBatchesCount || 0}

    `; } // Display wolf claims (large claimers and batches) function displayWolfClaims(largeClaimers, regularBatches, existingBatches) { const container = document.getElementById('wolf-claims-list'); if (!container) return; let html = ''; // Display large claimers (>100 claims) if (largeClaimers && largeClaimers.length > 0) { html += '

    LARGE CLAIMERS (>100 Claims)

    '; largeClaimers.forEach((claimer, index) => { const mintNumbers = claimer.mint_numbers ? claimer.mint_numbers.split(',') : []; const existingBatch = existingBatches.find(b => b.batch_type === 'large' && b.ordinals_wallets === claimer.ordinals_wallet); const isSent = existingBatch && existingBatch.is_sent; html += `

    Large Claimer #${index + 1}

    ${isSent ? 'SENT' : 'PENDING'}

    Wallet: ${claimer.ordinals_wallet}

    Total Claims: ${claimer.claim_count}

    Mint Numbers (${mintNumbers.length} total):

    ${mintNumbers.join(', ')}
    ${isSent ? `

    Transaction ID: ${existingBatch.transaction_id}

    Sent At: ${new Date(existingBatch.sent_at).toLocaleString()}

    ${existingBatch.notes ? `

    Notes: ${existingBatch.notes}

    ` : ''}
    ` : `
    `}
    `; }); } // Display regular batches (grouped by 200 IDs) if (regularBatches && regularBatches.length > 0) { html += '

    REGULAR BATCHES (Grouped by 200 IDs)

    '; regularBatches.forEach((batch, index) => { const walletsStr = batch.wallets.join(','); const existingBatch = existingBatches.find(b => b.batch_type === 'regular' && b.ordinals_wallets === walletsStr); const isSent = existingBatch && existingBatch.is_sent; html += `

    Batch #${index + 1}

    ${isSent ? 'SENT' : 'PENDING'}

    Wallets in Batch: ${batch.wallets.length}

    Total IDs: ${batch.total_count}

    Wallet Breakdown:

    ${batch.wallet_details ? batch.wallet_details.map(wd => `

    Wallet: ${wd.wallet}

    Claims: ${wd.count}

    IDs: ${wd.mint_numbers.join(', ')}

    `).join('') : `
    ${batch.wallets.join('\\n')}
    `}

    Mint Numbers (${batch.mint_numbers.length} total):

    ${batch.mint_numbers.join(', ')}
    ${isSent ? `

    Transaction ID: ${existingBatch.transaction_id}

    Sent At: ${new Date(existingBatch.sent_at).toLocaleString()}

    ${existingBatch.notes ? `

    Notes: ${existingBatch.notes}

    ` : ''}
    ` : `
    `}
    `; }); } if (html === '') { html = '

    No wolf claims found

    '; } container.innerHTML = html; } // Mark batch as sent window.markBatchSent = async function(batchType, wallets, mintNumbers, txidElementId, notesElementId) { const txidInput = document.getElementById(txidElementId); const notesInput = document.getElementById(notesElementId); if (!txidInput) { alert('Error: Transaction ID input not found'); return; } const transactionId = txidInput.value.trim(); const notes = notesInput ? notesInput.value.trim() : ''; if (!transactionId) { alert('Please enter a transaction ID'); return; } if (!confirm(`Mark this batch as sent with transaction ID: ${transactionId}?`)) { return; } try { const response = await fetch('/api/mark-wolf-batch-sent', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ batchType: batchType, wallets: wallets, mintNumbers: mintNumbers, transactionId: transactionId, notes: notes }) }); const data = await response.json(); if (data.success) { alert('Batch marked as sent successfully!'); // Reload the claims to show updated status loadWolfClaims(); } else { alert('Failed to mark batch as sent: ' + (data.error || 'Unknown error')); } } catch (error) { console.error('Error marking batch as sent:', error); alert('Failed to mark batch as sent: Network error'); } }; // Load physical print orders for admin async function loadPhysicalPrintOrders() { try { const response = await fetch('/api/physical-print-orders', { credentials: 'include' }); const data = await response.json(); if (data.success && data.orders) { displayPhysicalPrintOrders(data.orders); } else { const printsList = document.getElementById('prints-admin-list'); if (printsList) { printsList.innerHTML = '

    No physical print orders found

    '; } } } catch (error) { console.error('Error loading physical print orders:', error); const printsList = document.getElementById('prints-admin-list'); if (printsList) { printsList.innerHTML = '

    Failed to load physical print orders

    '; } } } // Display physical print orders in admin interface function displayPhysicalPrintOrders(orders) { const printsList = document.getElementById('prints-admin-list'); if (!printsList) return; if (orders.length === 0) { printsList.innerHTML = '

    No physical print orders submitted yet

    '; return; } printsList.innerHTML = orders.map(order => `

    ${order.artwork.artist} - ${order.artwork.title}

    ORDER #${order.id}

    Artwork Details

    Artist: ${order.artwork.artist}

    Title: ${order.artwork.title}

    Price: ${order.artwork.priceSats} sats

    Artist Payment Wallet: ${order.artwork.artistPaymentWallet || 'Not provided'}

    Transaction Details

    Transaction ID: ${order.transactionId}

    Inscription ID: ${order.inscriptionId || 'N/A'}

    Wallet Address: ${order.walletAddress}

    Order Date: ${new Date(order.mintTimestamp).toLocaleDateString()}

    Shipping Address

    Name: ${order.physicalPrint.name}

    Address: ${order.physicalPrint.address}

    City: ${order.physicalPrint.city}

    State: ${order.physicalPrint.state}

    ZIP: ${order.physicalPrint.zip}

    Country: ${order.physicalPrint.country}

    Email: ${order.physicalPrint.email || 'Not provided'}

    Phone: ${order.physicalPrint.phone || 'Not provided'}

    `).join(''); } // Refresh physical print orders window.refreshPhysicalPrints = function() { loadPhysicalPrintOrders(); } // Print shipping label (placeholder function) window.printShippingLabel = function(orderId) { alert('Print shipping label functionality would be implemented here for order #' + orderId); } // Mark order as shipped (placeholder function) window.markAsShipped = function(orderId) { alert('Mark as shipped functionality would be implemented here for order #' + orderId); } // View order details (placeholder function) window.viewOrderDetails = function(orderId) { alert('View order details functionality would be implemented here for order #' + orderId); } // Load supporters/sponsors async function loadSupporters() { try { const statusFilter = document.getElementById('supporters-status-filter')?.value || ''; const tierFilter = document.getElementById('supporters-tier-filter')?.value || ''; const response = await fetch('/api/get-all-sponsors', { credentials: 'include' }); const data = await response.json(); if (data.success && data.sponsors) { let filteredSponsors = data.sponsors; // Apply filters if (statusFilter) { filteredSponsors = filteredSponsors.filter(s => s.status === statusFilter); } if (tierFilter) { filteredSponsors = filteredSponsors.filter(s => s.tier === tierFilter); } displaySupporters(filteredSponsors); displaySupportersStats(data.sponsors); } else { const supportersList = document.getElementById('supporters-list'); if (supportersList) { supportersList.innerHTML = '

    No supporters found

    '; } } } catch (error) { console.error('Error loading supporters:', error); const supportersList = document.getElementById('supporters-list'); if (supportersList) { supportersList.innerHTML = '

    Failed to load supporters

    '; } } } // Display supporters stats function displaySupportersStats(sponsors) { const statsDiv = document.getElementById('supporters-stats'); if (!statsDiv) return; const totalSponsors = sponsors.length; const activeSponsors = sponsors.filter(s => s.status === 'active').length; const totalSatsRaised = sponsors.reduce((sum, s) => sum + (s.amount_sats || 0), 0); const goldCount = sponsors.filter(s => s.tier === 'gold').length; const silverCount = sponsors.filter(s => s.tier === 'silver').length; const bronzeCount = sponsors.filter(s => s.tier === 'bronze').length; statsDiv.innerHTML = `

    TOTAL SPONSORS

    ${totalSponsors}

    ACTIVE

    ${activeSponsors}

    TOTAL RAISED

    ${totalSatsRaised.toLocaleString()} sats

    GOLD / SILVER / BRONZE

    ${goldCount} / ${silverCount} / ${bronzeCount}

    `; } // Display supporters list function displaySupporters(sponsors) { const supportersList = document.getElementById('supporters-list'); if (!supportersList) return; if (sponsors.length === 0) { supportersList.innerHTML = '

    No supporters match the selected filters

    '; return; } supportersList.innerHTML = sponsors.map(sponsor => { const tierColor = sponsor.tier === 'gold' ? '#FFD700' : sponsor.tier === 'silver' ? '#C0C0C0' : '#CD7F32'; const statusColor = sponsor.status === 'active' ? '#00ff00' : sponsor.status === 'pending' ? '#ffcc00' : '#666'; return `

    ${sponsor.sponsor_name}

    Tier: ${sponsor.tier}

    Status: ${sponsor.status}

    Amount: ${sponsor.amount_sats.toLocaleString()} sats

    ${sponsor.sponsor_name}

    Contact Information

    Email: ${sponsor.contact_email}

    Website: ${sponsor.website_url}

    ${sponsor.twitter_handle ? `

    Twitter: ${sponsor.twitter_handle}

    ` : ''}

    Payment Information

    Payment Address: ${sponsor.payment_address}

    Transaction ID: ${sponsor.transaction_id}

    Confirmed: ${sponsor.payment_confirmed_at ? new Date(sponsor.payment_confirmed_at).toLocaleString() : 'N/A'}

    ${sponsor.description ? `

    Description

    ${sponsor.description}

    ` : ''}

    Created: ${new Date(sponsor.created_at).toLocaleString()}

    ${sponsor.start_date ? `

    Start Date: ${new Date(sponsor.start_date).toLocaleString()}

    ` : ''} ${sponsor.end_date ? `

    End Date: ${new Date(sponsor.end_date).toLocaleString()}

    ` : ''}
    `; }).join(''); } // ============ AUCTION ADMIN FUNCTIONS ============ // Load auctions for admin management window.loadAuctionsForAdmin = async function() { try { const statusFilter = document.getElementById('auctions-status-filter')?.value || ''; const url = statusFilter ? `/api/admin-list-auctions?status=${statusFilter}` : '/api/admin-list-auctions'; const response = await fetch(url); const data = await response.json(); if (data.success && data.auctions) { displayAuctionsAdmin(data.auctions); } else { const auctionsList = document.getElementById('admin-auctions-list'); if (auctionsList) { auctionsList.innerHTML = '

    No auctions found

    '; } } } catch (error) { console.error('Error loading auctions:', error); const auctionsList = document.getElementById('admin-auctions-list'); if (auctionsList) { auctionsList.innerHTML = '

    Failed to load auctions

    '; } } } // Display auctions in admin panel function displayAuctionsAdmin(auctions) { const auctionsList = document.getElementById('admin-auctions-list'); if (!auctionsList) return; if (auctions.length === 0) { auctionsList.innerHTML = '

    No auctions found

    '; return; } auctionsList.innerHTML = auctions.map(auction => { const now = new Date(); const startDate = new Date(auction.auction_start_datetime); const endDate = new Date(auction.auction_end_datetime); let status = 'Pending'; let statusColor = '#ff6600'; if (auction.is_settled) { status = 'Settled'; statusColor = '#666666'; } else if (!auction.is_active) { status = 'Pending Approval'; statusColor = '#ff6600'; } else if (now < startDate) { status = 'Upcoming'; statusColor = '#ffcc00'; } else if (now >= startDate && now < endDate) { status = 'Active'; statusColor = '#00ff00'; } else { status = 'Ended'; statusColor = '#cc0000'; } return `

    ${auction.artist} - ${auction.title}

    ${status}

    Starting Price: ${auction.starting_price_sats.toLocaleString()} sats

    Reserve: ${auction.reserve_price_sats ? auction.reserve_price_sats.toLocaleString() + ' sats' : 'None'}

    Current High Bid: ${auction.current_high_bid_sats.toLocaleString()} sats

    Total Bids: ${auction.bid_count || 0}

    Start: ${new Date(auction.auction_start_datetime).toLocaleString()}

    End: ${new Date(auction.auction_end_datetime).toLocaleString()}

    Created: ${new Date(auction.created_at).toLocaleString()}

    Inscription: ${auction.inscription_id}

    ${auction.description ? '

    ' + auction.description + '

    ' : ''}
    ${!auction.is_active && !auction.is_settled ? '' + '' : ''} ${auction.is_active && !auction.is_settled ? '' : ''} ${!auction.is_settled && (status === 'Ended' || now >= endDate) ? '' : ''} ${!auction.is_settled ? '' : ''} View Auction
    `; }).join(''); } // Handle auction admin actions window.adminAuctionAction = async function(auctionId, action) { const actionLabels = { 'approve': 'approve', 'reject': 'reject', 'settle': 'settle', 'deactivate': 'deactivate' }; if (!confirm('Are you sure you want to ' + actionLabels[action] + ' this auction?')) { return; } try { const response = await fetch('/api/admin-approve-auction', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ auctionId, action }) }); const data = await response.json(); if (data.success) { alert(data.message); loadAuctionsForAdmin(); } else { alert('Error: ' + (data.error || 'Failed to perform action')); } } catch (error) { console.error('Error performing auction action:', error); alert('Failed to perform action'); } }; // Show edit times form for auction window.showEditAuctionTimes = function(auctionId, startDatetime, endDatetime) { const editForm = document.getElementById('edit-times-' + auctionId); if (!editForm) return; // Convert ISO datetime to local datetime-local format const formatForInput = (isoDate) => { const date = new Date(isoDate); const offset = date.getTimezoneOffset(); const localDate = new Date(date.getTime() - offset * 60000); return localDate.toISOString().slice(0, 16); }; document.getElementById('edit-start-' + auctionId).value = formatForInput(startDatetime); document.getElementById('edit-end-' + auctionId).value = formatForInput(endDatetime); editForm.style.display = 'block'; }; // Save auction times window.saveAuctionTimes = async function(auctionId) { const startInput = document.getElementById('edit-start-' + auctionId); const endInput = document.getElementById('edit-end-' + auctionId); if (!startInput.value || !endInput.value) { alert('Please enter both start and end times'); return; } const startDatetime = new Date(startInput.value).toISOString(); const endDatetime = new Date(endInput.value).toISOString(); if (new Date(endDatetime) <= new Date(startDatetime)) { alert('End time must be after start time'); return; } try { const response = await fetch('/api/admin-update-auction', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ auctionId, auctionStartDatetime: startDatetime, auctionEndDatetime: endDatetime }) }); const data = await response.json(); if (data.success) { alert('Auction times updated successfully'); document.getElementById('edit-times-' + auctionId).style.display = 'none'; loadAuctionsForAdmin(); } else { alert('Error: ' + (data.error || 'Failed to update auction')); } } catch (error) { console.error('Error updating auction times:', error); alert('Failed to update auction times'); } }; // Toggle bids list visibility and load bids window.toggleAuctionBids = async function(auctionId) { const bidsContainer = document.getElementById('auction-bids-' + auctionId); if (!bidsContainer) return; if (bidsContainer.style.display === 'none') { bidsContainer.style.display = 'block'; await loadAuctionBids(auctionId); } else { bidsContainer.style.display = 'none'; } }; // Load bids for an auction async function loadAuctionBids(auctionId) { const bidsList = document.getElementById('auction-bids-list-' + auctionId); if (!bidsList) return; try { const response = await fetch('/api/admin-auction-bids?auctionId=' + auctionId); const data = await response.json(); if (!data.success || !data.bids || data.bids.length === 0) { bidsList.innerHTML = '

    No bids for this auction

    '; return; } bidsList.innerHTML = ` ${data.bids.map((bid, index) => { const isWinner = index === 0; const statusColor = bid.is_refunded ? '#00cc00' : (isWinner ? '#ffcc00' : '#ff6600'); const statusText = bid.is_refunded ? 'Refunded' : (isWinner ? 'Winner' : 'Pending'); return '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; }).join('')}
    Rank Payment Wallet Ordinals Wallet Bid Paid Status Action
    #' + (index + 1) + '' + '' + bid.wallet_address + '' + '' + '' + (bid.taproot_address || 'Not provided') + '' + '' + bid.bid_amount_sats.toLocaleString() + '' + bid.total_paid_sats.toLocaleString() + '' + '' + statusText + '' + (bid.refund_txid ? '
    TX: ' + bid.refund_txid.substring(0, 10) + '...' : '') + '
    ' + (!bid.is_refunded && !isWinner ? '' : '-') + '
    `; } catch (error) { console.error('Error loading bids:', error); bidsList.innerHTML = '

    Error loading bids

    '; } } // Show refund form for a bid window.showRefundForm = function(bidId, auctionId) { const refundForm = document.getElementById('refund-form-' + bidId); if (refundForm) { refundForm.style.display = 'table-row'; } }; // Submit refund for a bid window.submitRefund = async function(bidId, auctionId) { const txidInput = document.getElementById('refund-txid-' + bidId); const refundTxid = txidInput?.value.trim(); if (!refundTxid) { alert('Please enter the refund transaction ID'); return; } try { const response = await fetch('/api/admin-refund-bid', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ bidId, refundTxid }) }); const data = await response.json(); if (data.success) { alert('Bid marked as refunded'); await loadAuctionBids(auctionId); } else { alert('Error: ' + (data.error || 'Failed to mark refund')); } } catch (error) { console.error('Error submitting refund:', error); alert('Failed to submit refund'); } }; // ============ PRE-INSCRIBED COLLECTIONS FUNCTIONS ============ // Load pre-inscribed collections async function loadPreInscribedCollections() { try { const response = await fetch('/api/admin-list'); const data = await response.json(); if (data.success && data.artworks) { // Filter for pre-inscribed collections only const preInscribedCollections = data.artworks.filter(artwork => artwork.mintType === 'pre_inscribed' ); displayPreInscribedCollections(preInscribedCollections); } else { const preInscribedList = document.getElementById('pre-inscribed-admin-list'); if (preInscribedList) { preInscribedList.innerHTML = '

    No pre-inscribed collections found

    '; } } } catch (error) { console.error('Error loading pre-inscribed collections:', error); const preInscribedList = document.getElementById('pre-inscribed-admin-list'); if (preInscribedList) { preInscribedList.innerHTML = '

    Failed to load pre-inscribed collections

    '; } } } // Display pre-inscribed collections function displayPreInscribedCollections(collections) { const preInscribedList = document.getElementById('pre-inscribed-admin-list'); if (!preInscribedList) return; if (collections.length === 0) { preInscribedList.innerHTML = '

    No pre-inscribed collections found. Create your first collection to get started!

    '; return; } preInscribedList.innerHTML = collections.map(collection => { return `

    ${collection.artist} - ${collection.title}

    ${collection.isActive ? 'ACTIVE' : 'INACTIVE'} PRE-INSCRIBED

    Price: ${collection.price} sats

    Max Supply: ${collection.maxMints === -1 ? '∞' : (collection.maxMints || collection.total || 'N/A')}

    Minted: ${collection.minted || 0}

    Physical Print: ${collection.physicalPrintAvailable ? 'Yes' : 'No'}

    Created: ${new Date(collection.createdAt || Date.now()).toLocaleDateString()}

    ${collection.description ? `

    ${formatDescription(collection.description)}

    ` : ''}
    `; }).join(''); // Load ordinal pool status only for legacy pre-inscribed collections collections.forEach(collection => { // Only load pool for collections WITHOUT ordinalsWallet (legacy) let hasWallet = false; try { if (collection.additionalDetails) { const details = JSON.parse(collection.additionalDetails); hasWallet = !!details.ordinalsWallet; } } catch (e) {} if (!hasWallet) { loadOrdinalPoolStatus(collection.slug); } else { // For v2, show N/A const statusSpan = document.getElementById(`pool-status-${collection.slug}`); if (statusSpan) statusSpan.textContent = 'N/A (V2)'; } }); } // Manage inscriptions for a pre-inscribed collection window.manageInscriptions = async function(artworkSlug) { window.currentManagingSlug = artworkSlug; document.getElementById('manage-inscriptions-modal').style.display = 'block'; await loadInscriptionsList(artworkSlug); }; // Hide manage inscriptions modal window.hideManageInscriptionsModal = function() { document.getElementById('manage-inscriptions-modal').style.display = 'none'; window.currentManagingSlug = null; }; // Load inscriptions list async function loadInscriptionsList(artworkSlug) { try { const response = await fetch(`/api/manage-pre-inscribed-ordinals?slug=${artworkSlug}`); const data = await response.json(); if (data.success) { document.getElementById('inscriptions-count').textContent = data.total; const listDiv = document.getElementById('inscriptions-list'); if (data.ordinals.length === 0) { listDiv.innerHTML = '

    No inscriptions yet. Add inscription IDs manually or import from a wallet.

    '; } else { listDiv.innerHTML = data.ordinals.map(ordinal => `

    ${ordinal.inscription_id}

    ${ordinal.is_sold ? `✅ Sold on ${new Date(ordinal.sold_at).toLocaleDateString()}` : '🟢 Available'} ${ordinal.asset_index !== null ? ` | Index: #${ordinal.asset_index + 1}` : ''}

    ${ordinal.ordinal_address ? `

    Current: ${ordinal.ordinal_address}

    ` : ''} ${ordinal.is_sold && ordinal.sold_to_address ? `

    Sold to: ${ordinal.sold_to_address}

    ` : ''}
    View ${!ordinal.is_sold ? ` ` : ''}
    `).join(''); } } else { document.getElementById('inscriptions-list').innerHTML = `

    ${data.error}

    `; } } catch (error) { console.error('Error loading inscriptions:', error); document.getElementById('inscriptions-list').innerHTML = `

    Failed to load inscriptions

    `; } } // Add inscription window.addInscription = async function() { const inscriptionId = document.getElementById('newInscriptionId').value.trim(); if (!inscriptionId) { alert('Please enter an inscription ID'); return; } if (!window.currentManagingSlug) { alert('No collection selected'); return; } try { const response = await fetch(`/api/manage-pre-inscribed-ordinals?slug=${window.currentManagingSlug}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ inscriptionId: inscriptionId, ordinalsAddress: null }) }); const data = await response.json(); if (data.success) { document.getElementById('newInscriptionId').value = ''; await loadInscriptionsList(window.currentManagingSlug); alert('Inscription added successfully!'); } else { alert('Failed to add inscription: ' + data.error); } } catch (error) { console.error('Error adding inscription:', error); alert('Failed to add inscription'); } }; // Remove inscription window.removeInscription = async function(inscriptionId) { if (!confirm(`Remove inscription ${inscriptionId.substring(0, 16)}...?`)) { return; } if (!window.currentManagingSlug) { alert('No collection selected'); return; } try { const response = await fetch( `/api/manage-pre-inscribed-ordinals?slug=${window.currentManagingSlug}&inscriptionId=${encodeURIComponent(inscriptionId)}`, { method: 'DELETE' } ); const data = await response.json(); if (data.success) { await loadInscriptionsList(window.currentManagingSlug); alert('Inscription removed successfully!'); } else { alert('Failed to remove inscription: ' + data.error); } } catch (error) { console.error('Error removing inscription:', error); alert('Failed to remove inscription'); } }; // Load ordinal pool status for a collection async function loadOrdinalPoolStatus(artworkSlug) { try { const response = await fetch(`/api/manage-ordinal-pool?artwork=${artworkSlug}`, { credentials: 'include' }); const data = await response.json(); const statusElement = document.getElementById(`pool-status-${artworkSlug}`); if (statusElement && data.success) { statusElement.innerHTML = `${data.available || 0} available / ${data.total || 0} total`; statusElement.style.color = (data.available || 0) > 0 ? '#00ff00' : '#ff6600'; } else if (statusElement) { statusElement.innerHTML = 'Error loading'; statusElement.style.color = '#ff0000'; } } catch (error) { const statusElement = document.getElementById(`pool-status-${artworkSlug}`); if (statusElement) { statusElement.innerHTML = 'Error loading'; statusElement.style.color = '#ff0000'; } } } // Show create pre-inscribed collection modal window.showCreatePreInscribedModal = function() { const modal = document.getElementById('pre-inscribed-modal'); if (modal) { modal.style.display = 'block'; // Clear form document.getElementById('preInscribedArtistInput').value = ''; document.getElementById('preInscribedTitleInput').value = ''; document.getElementById('preInscribedDescriptionInput').value = ''; document.getElementById('preInscribedPriceInput').value = 0; document.getElementById('preInscribedMaxMintsInput').value = 100; document.getElementById('preInscribedWebsiteInput').value = ''; document.getElementById('preInscribedPaymentWalletInput').value = ''; document.getElementById('preInscribedTwitterInput').value = ''; document.getElementById('preInscribedDiscordInput').value = ''; document.getElementById('preInscribedMintStartInput').value = ''; document.getElementById('preInscribedMintEndInput').value = ''; document.getElementById('preInscribedPhysicalPrintInput').checked = false; document.getElementById('pre-inscribed-modal-title').textContent = 'Create Pre-inscribed Collection'; window.editingPreInscribedSlug = null; } } // Hide pre-inscribed collection modal window.hidePreInscribedModal = function() { const modal = document.getElementById('pre-inscribed-modal'); if (modal) modal.style.display = 'none'; } // Save pre-inscribed collection window.savePreInscribedCollection = async function() { const artist = document.getElementById('preInscribedArtistInput').value; const title = document.getElementById('preInscribedTitleInput').value; const description = document.getElementById('preInscribedDescriptionInput').value; const price = parseInt(document.getElementById('preInscribedPriceInput').value) || 0; const maxMints = parseInt(document.getElementById('preInscribedMaxMintsInput').value) || 100; const website = document.getElementById('preInscribedWebsiteInput').value; const paymentWallet = document.getElementById('preInscribedPaymentWalletInput').value; const twitter = document.getElementById('preInscribedTwitterInput').value; const discord = document.getElementById('preInscribedDiscordInput').value; const mintStart = document.getElementById('preInscribedMintStartInput').value; const mintEnd = document.getElementById('preInscribedMintEndInput').value; const physicalPrint = document.getElementById('preInscribedPhysicalPrintInput').checked; const isNumerical = document.getElementById('preInscribedNumericalInput').checked; // Whitelist settings const whitelistEnabled = document.getElementById('preInscribedWhitelistEnabledInput').checked; const whitelistStart = document.getElementById('preInscribedWhitelistStartInput').value; const whitelistEnd = document.getElementById('preInscribedWhitelistEndInput').value; const whitelistCsvFile = document.getElementById('preInscribedWhitelistCsvInput').files[0]; if (!artist || !title) { alert('Artist and title are required'); return; } // Convert datetime-local values to UTC ISO strings const mintStartDatetime = mintStart ? new Date(mintStart + 'Z').toISOString() : null; const mintEndDatetime = mintEnd ? new Date(mintEnd + 'Z').toISOString() : null; const whitelistStartDatetime = whitelistStart ? new Date(whitelistStart + 'Z').toISOString() : null; const whitelistEndDatetime = whitelistEnd ? new Date(whitelistEnd + 'Z').toISOString() : null; // Generate slug from artist-title const slug = window.editingPreInscribedSlug || `${artist}-${title}`.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); // Determine mint type const mintType = isNumerical ? 'numerical' : 'pre_inscribed'; try { const response = await fetch('/api/save-artwork', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ slug, artist, title, description, price, maxMints, parentInscriptionId: 'pre-inscribed-placeholder', // Placeholder since we don't need this for pre-inscribed parentType: 'static', scriptInscriptionId: 'd686bf14aec7115b84cb9e85fe8d819d61217d115b8869c49694205ce3403a8ai0', imageFilename: 'pre-inscribed.webp', websiteUrl: website, paymentWallet: paymentWallet, twitterUrl: twitter, discordUrl: discord, mintStartDatetime: mintStartDatetime, mintEndDatetime: mintEndDatetime, physicalPrint: physicalPrint, mintType: mintType, whitelistEnabled: whitelistEnabled, whitelistStartDatetime: whitelistStartDatetime, whitelistEndDatetime: whitelistEndDatetime, isEdit: window.editingPreInscribedSlug ? true : false }) }); const data = await response.json(); if (data.success) { // If whitelist was enabled and has CSV data, upload it if (whitelistEnabled && whitelistCsvFile) { try { const csvContent = await readFileAsText(whitelistCsvFile); const whitelistResponse = await fetch('/api/upload-whitelist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ artworkSlug: slug, csvData: csvContent, whitelistStartDatetime: whitelistStartDatetime, whitelistEndDatetime: whitelistEndDatetime }) }); const whitelistResult = await whitelistResponse.json(); if (whitelistResult.success) { alert(data.message + `\\n\\nWhitelist uploaded successfully with ${whitelistResult.entriesProcessed} entries.${!isNumerical ? '\\n\\nDon\'t forget to add ordinals to the pool using the "Manage Ordinals" button!' : ''}`); } else { alert(data.message + '\\n\\nWhitelist upload failed: ' + whitelistResult.error); } } catch (whitelistError) { alert(data.message + '\\n\\nFailed to upload whitelist: ' + whitelistError.message); } } else { alert(data.message + (!isNumerical ? '\\n\\nDon\'t forget to add ordinals to the pool using the "Manage Ordinals" button!' : '')); } hidePreInscribedModal(); loadPreInscribedCollections(); // Refresh the list } else { alert('Error: ' + data.error); } } catch (error) { alert('Failed to save collection'); } } // Toggle whitelist settings visibility window.togglePreInscribedWhitelistSettings = function() { const checkbox = document.getElementById('preInscribedWhitelistEnabledInput'); const settings = document.getElementById('preInscribedWhitelistSettings'); if (checkbox && settings) { if (checkbox.checked) { settings.style.display = 'block'; } else { settings.style.display = 'none'; } } } // Manage ordinal pool for a collection window.manageOrdinalPool = function(artworkSlug) { alert(`Ordinal pool management for "${artworkSlug}" would open here.\\n\\nThis would show:\\n- Current ordinals in pool\\n- Add new ordinals\\n- Remove ordinals\\n- View distribution history`); // TODO: Implement ordinal pool management interface } // Edit pre-inscribed collection window.editPreInscribedCollection = async function(slug) { try { const response = await fetch(`/api/get-artwork-admin?slug=${slug}`); const data = await response.json(); if (data.success && data.artwork) { // Populate the modal with existing data document.getElementById('preInscribedArtistInput').value = data.artwork.artist || ''; document.getElementById('preInscribedTitleInput').value = data.artwork.title || ''; document.getElementById('preInscribedDescriptionInput').value = data.artwork.description || ''; document.getElementById('preInscribedPriceInput').value = data.artwork.price || 0; document.getElementById('preInscribedMaxMintsInput').value = data.artwork.maxMints || 100; document.getElementById('preInscribedWebsiteInput').value = data.artwork.websiteUrl || ''; document.getElementById('preInscribedPaymentWalletInput').value = data.artwork.artistPaymentWallet || ''; document.getElementById('preInscribedTwitterInput').value = data.artwork.twitterUrl || ''; document.getElementById('preInscribedDiscordInput').value = data.artwork.discordUrl || ''; document.getElementById('preInscribedPhysicalPrintInput').checked = !!data.artwork.physicalPrintAvailable; // Handle datetime fields if (data.artwork.mintStartDatetime) { const startDate = new Date(data.artwork.mintStartDatetime); document.getElementById('preInscribedMintStartInput').value = startDate.toISOString().slice(0, 16); } else { document.getElementById('preInscribedMintStartInput').value = ''; } if (data.artwork.mintEndDatetime) { const endDate = new Date(data.artwork.mintEndDatetime); document.getElementById('preInscribedMintEndInput').value = endDate.toISOString().slice(0, 16); } else { document.getElementById('preInscribedMintEndInput').value = ''; } document.getElementById('pre-inscribed-modal-title').textContent = 'Edit Pre-inscribed Collection'; window.editingPreInscribedSlug = slug; const modal = document.getElementById('pre-inscribed-modal'); if (modal) modal.style.display = 'block'; } else { alert('Failed to load collection for editing: ' + (data.error || 'Unknown error')); } } catch (error) { alert('Failed to load collection for editing'); } } // Toggle collection status window.toggleCollectionStatus = async function(slug, currentStatus) { const action = currentStatus ? 'deactivate' : 'activate'; if (!confirm(`Are you sure you want to ${action} this collection?`)) { return; } try { const response = await fetch('/api/toggle-artwork', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ slug }) }); const data = await response.json(); if (data.success) { alert(`Collection ${action}d successfully`); loadPreInscribedCollections(); // Refresh the list } else { alert('Failed to ' + action + ' collection: ' + (data.error || 'Unknown error')); } } catch (error) { alert('Failed to ' + action + ' collection: Network error'); } } // Delete pre-inscribed collection window.deletePreInscribedCollection = async function(slug, title) { if (!confirm(`Are you sure you want to delete "${title}"?\\n\\nThis will also remove all ordinals from the pool. This action cannot be undone.`)) { return; } try { const response = await fetch('/api/delete-artwork', { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ slug }) }); const data = await response.json(); if (data.success) { alert('Collection deleted successfully'); loadPreInscribedCollections(); // Refresh the list } else { alert('Failed to delete collection: ' + (data.error || 'Unknown error')); } } catch (error) { alert('Failed to delete collection: Network error'); } } // ============ END PRE-INSCRIBED COLLECTIONS FUNCTIONS ============ // ============ ON-DEMAND COLLECTIONS FUNCTIONS ============ // Load on-demand collections async function loadOnDemandCollections() { try { const response = await fetch('/api/admin-list'); const data = await response.json(); if (data.success && data.artworks) { // Filter for inscribe-on-demand collections only const onDemandCollections = data.artworks.filter(artwork => artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints'); displayOnDemandCollections(onDemandCollections); } else { const onDemandList = document.getElementById('on-demand-admin-list'); if (onDemandList) { onDemandList.innerHTML = '

    No on-demand collections found

    '; } } } catch (error) { console.error('Error loading on-demand collections:', error); const onDemandList = document.getElementById('on-demand-admin-list'); if (onDemandList) { onDemandList.innerHTML = '

    Failed to load on-demand collections

    '; } } } // Display on-demand collections function displayOnDemandCollections(collections) { const onDemandList = document.getElementById('on-demand-admin-list'); if (!onDemandList) return; if (collections.length === 0) { onDemandList.innerHTML = '

    No inscribe-on-demand collections found.

    '; return; } onDemandList.innerHTML = collections.map(collection => `

    ${collection.artist} - ${collection.title}

    ${collection.isActive ? 'ACTIVE' : 'INACTIVE'} ON-DEMAND

    Price: ${collection.price} sats

    Max Supply: ${collection.maxMints === -1 ? '∞' : (collection.maxMints || collection.total || 'N/A')}

    Minted: ${collection.minted || 0}

    Physical Print: ${collection.physicalPrintAvailable ? 'Yes' : 'No'}

    Created: ${new Date(collection.createdAt || Date.now()).toLocaleDateString()}

    Assets: Loading...

    ${collection.description ? `

    ${formatDescription(collection.description)}

    ` : ''}
    `).join(''); // Load asset status for each collection collections.forEach(collection => { loadCollectionAssetStatus(collection.slug); }); } // Load collection asset status async function loadCollectionAssetStatus(artworkSlug) { try { // This would call an API to get asset counts // For now, just show placeholder const statusElement = document.getElementById(`assets-status-${artworkSlug}`); if (statusElement) { statusElement.innerHTML = 'Available'; statusElement.style.color = '#00ff00'; } } catch (error) { const statusElement = document.getElementById(`assets-status-${artworkSlug}`); if (statusElement) { statusElement.innerHTML = 'Error loading'; statusElement.style.color = '#ff0000'; } } } // View collection assets window.viewCollectionAssets = async function(artworkSlug) { try { const response = await fetch(`/api/manage-collection-assets?artwork=${artworkSlug}`, { credentials: 'include' }); const data = await response.json(); if (data.success) { showAssetsGallery(artworkSlug, data); } else { console.error('Asset management error:', data.error); alert('Failed to load assets: ' + data.error); } } catch (error) { console.error('Error loading collection assets:', error); alert('Failed to load assets: ' + error.message); } } // Show assets gallery modal window.showAssetsGallery = async function(artworkSlug, assetsData) { const modal = document.getElementById('assets-gallery-modal'); if (!modal) return; // Get artwork info for title try { const artworkResponse = await fetch(`/api/get-artwork-admin?slug=${artworkSlug}`); const artworkData = await artworkResponse.json(); if (artworkData.success) { document.getElementById('assets-collection-title').textContent = artworkData.artwork.title; document.getElementById('assets-collection-artist').textContent = artworkData.artwork.artist; } } catch (error) { console.error('Failed to load artwork info:', error); } // Update info document.getElementById('assets-total-count').textContent = assetsData.total; document.getElementById('assets-available-count').textContent = assetsData.available; document.getElementById('assets-minted-count').textContent = assetsData.minted; // Store current artwork slug for actions window.currentAssetsArtwork = artworkSlug; // Display assets displayAssetsGallery(assetsData.assets); modal.style.display = 'block'; } // Hide assets gallery modal window.hideAssetsGallery = function() { const modal = document.getElementById('assets-gallery-modal'); if (modal) modal.style.display = 'none'; } // Display assets in gallery function displayAssetsGallery(assets) { const grid = document.getElementById('assets-gallery-grid'); if (!grid) return; if (assets.length === 0) { grid.innerHTML = '

    No assets found

    '; return; } grid.innerHTML = assets.map(asset => `
    ${asset.original_name}

    ${asset.original_name.length > 20 ? asset.original_name.substring(0, 20) + '...' : asset.original_name}

    #${asset.asset_index + 1}

    ${asset.is_minted ? 'MINTED' : 'AVAILABLE'}

    ${asset.is_minted ? `

    Minted: ${new Date(asset.minted_at).toLocaleDateString()}

    ${asset.inscription_id ? `

    ID: ${asset.inscription_id.substring(0, 8)}...

    ` : ''}
    ` : `` }
    `).join(''); } // Remove asset window.removeAsset = async function(assetId) { if (!confirm('Are you sure you want to remove this asset? This action cannot be undone.')) { return; } try { const response = await fetch(`/api/manage-collection-assets?artwork=${window.currentAssetsArtwork}&assetId=${assetId}`, { method: 'DELETE', credentials: 'include' }); const data = await response.json(); if (data.success) { // Update the max_mints display in edit modal if it exists const maxMintsInput = document.getElementById('editOnDemandMaxMintsInput'); if (maxMintsInput && data.newMaxMints !== undefined) { maxMintsInput.value = data.newMaxMints; } // Refresh the gallery viewCollectionAssets(window.currentAssetsArtwork); } else { alert('Failed to remove asset: ' + data.error); } } catch (error) { alert('Failed to remove asset: ' + error.message); } } // Add more assets - open the add assets modal window.addMoreAssets = async function() { if (!window.currentAssetsArtwork) { alert('No collection selected'); return; } await showAddAssetsModal(window.currentAssetsArtwork); } // Show add assets modal window.showAddAssetsModal = async function(artworkSlug) { const modal = document.getElementById('add-assets-modal'); if (!modal) return; // Store current artwork window.addingAssetsToArtwork = artworkSlug; // Get artwork info and current assets try { const [artworkResponse, assetsResponse] = await Promise.all([ fetch(`/api/get-artwork-admin?slug=${artworkSlug}`), fetch(`/api/manage-collection-assets?artwork=${artworkSlug}`, { credentials: 'include' }) ]); const artworkData = await artworkResponse.json(); const assetsData = await assetsResponse.json(); if (artworkData.success && assetsData.success) { // Update collection info document.getElementById('add-assets-collection-name').textContent = `${artworkData.artwork.artist} - ${artworkData.artwork.title}`; document.getElementById('add-assets-current-count').textContent = assetsData.total; document.getElementById('add-assets-available-count').textContent = assetsData.available; document.getElementById('add-assets-minted-count').textContent = assetsData.minted; // Clear previous selections document.getElementById('addAssetsInput').value = ''; document.getElementById('addAssetsPreview').style.display = 'none'; document.getElementById('addAssetsResult').innerHTML = ''; modal.style.display = 'block'; } else { alert('Failed to load collection information'); } } catch (error) { alert('Failed to load collection information: ' + error.message); } } // Hide add assets modal window.hideAddAssetsModal = function() { const modal = document.getElementById('add-assets-modal'); if (modal) modal.style.display = 'none'; } // Handle add assets file selection function handleAddAssetsUpload(event) { const files = Array.from(event.target.files); const preview = document.getElementById('addAssetsPreview'); const grid = document.getElementById('addAssetsGrid'); const assetCount = document.getElementById('addAssetsCount'); const totalSize = document.getElementById('addAssetsTotalSize'); if (!files || files.length === 0) { preview.style.display = 'none'; return; } // Validate files (images and HTML) const validImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/avif']; const validHtmlTypes = ['text/html', 'application/octet-stream']; // Some browsers send HTML as octet-stream const validFiles = []; let totalSizeBytes = 0; for (const file of files) { const isImage = validImageTypes.includes(file.type); const isHtml = validHtmlTypes.includes(file.type) || file.name.toLowerCase().endsWith('.html'); if (!isImage && !isHtml) { alert(`File "${file.name}" is not a valid file type. Only JPG, PNG, GIF, WebP, AVIF images and HTML files are allowed.`); continue; } // Different size limits for images vs HTML const maxFileSize = isHtml ? 1 * 1024 * 1024 : 5 * 1024 * 1024; // 1MB for HTML, 5MB for images if (file.size > maxFileSize) { const sizeLimit = isHtml ? '1MB' : '5MB'; alert(`File "${file.name}" is too large (${(file.size / 1024 / 1024).toFixed(1)} MB). ${isHtml ? 'HTML files' : 'Images'} must be smaller than ${sizeLimit}.`); continue; } validFiles.push(file); totalSizeBytes += file.size; } if (validFiles.length === 0) { preview.style.display = 'none'; return; } // Update counts assetCount.textContent = validFiles.length; totalSize.textContent = (totalSizeBytes / 1024 / 1024).toFixed(1) + ' MB'; // Show previews grid.innerHTML = ''; validFiles.forEach((file, index) => { const assetDiv = document.createElement('div'); assetDiv.style.cssText = 'text-align: center; padding: 5px;'; const isHtml = file.type === 'text/html' || file.name.toLowerCase().endsWith('.html'); if (isHtml) { // HTML file preview assetDiv.innerHTML = `
    <HTML>

    ${file.name.length > 12 ? file.name.substring(0, 12) + '...' : file.name}

    `; grid.appendChild(assetDiv); } else { // Image file preview const reader = new FileReader(); reader.onload = function(e) { assetDiv.innerHTML = ` New Asset ${index + 1}

    ${file.name.length > 12 ? file.name.substring(0, 12) + '...' : file.name}

    `; grid.appendChild(assetDiv); }; reader.readAsDataURL(file); } }); preview.style.display = 'block'; } // Upload additional assets window.uploadAdditionalAssets = async function() { const files = Array.from(document.getElementById('addAssetsInput').files); if (!files || files.length === 0) { alert('Please select at least one image to upload'); return; } if (!window.addingAssetsToArtwork) { alert('No collection selected'); return; } try { document.getElementById('addAssetsResult').innerHTML = `

    Uploading ${files.length} assets...

    This may take a moment.

    `; // Get current asset count to determine starting index const assetsResponse = await fetch(`/api/manage-collection-assets?artwork=${window.addingAssetsToArtwork}`, { credentials: 'include' }); const currentAssetsData = await assetsResponse.json(); const startingIndex = currentAssetsData.total || 0; // Upload all files using batch upload document.getElementById('addAssetsResult').innerHTML = `

    Uploading ${files.length} additional assets...

    Processing in batches to prevent errors.

    Starting upload...

    `; // Create files with adjusted indices const filesWithIndex = files.map((file, index) => { const adjustedFile = new File([file], file.name, { type: file.type }); adjustedFile.assetIndex = startingIndex + index; return adjustedFile; }); const uploadResults = await uploadFilesBatchWithIndex(filesWithIndex, 'collection'); const failedUploads = uploadResults.filter(result => !result.success); const successfulUploads = uploadResults.filter(result => result.success); if (failedUploads.length > 0) { console.warn(`${failedUploads.length} uploads failed:`, failedUploads); if (successfulUploads.length === 0) { alert(`All ${failedUploads.length} asset uploads failed. Please check your connection and try again.`); return; } else { alert( `${failedUploads.length} out of ${files.length} assets failed to upload.\n\n` + `Successfully uploaded: ${successfulUploads.length} assets\n` + `Failed uploads: ${failedUploads.map(f => f.originalFilename || f.filename).join(', ')}\n\n` + `The successful uploads have been added to your collection.` ); } } const assetFilenames = successfulUploads.map(result => result.filename); // Save assets to collection const saveResponse = await fetch('/api/save-collection-assets', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ artworkSlug: window.addingAssetsToArtwork, assetFilenames: assetFilenames }) }); const saveData = await saveResponse.json(); if (saveData.success) { // Update the max_mints display in edit modal if it exists const maxMintsInput = document.getElementById('editOnDemandMaxMintsInput'); if (maxMintsInput && saveData.totalAssets !== undefined) { maxMintsInput.value = saveData.totalAssets; } document.getElementById('addAssetsResult').innerHTML = `

    Assets Added Successfully!

    ${files.length} new assets have been added to your collection.

    The collection max supply has been updated automatically.

    `; // Clear form document.getElementById('addAssetsInput').value = ''; document.getElementById('addAssetsPreview').style.display = 'none'; // Refresh any open galleries or edit modals setTimeout(() => { hideAddAssetsModal(); if (window.currentAssetsArtwork === window.addingAssetsToArtwork) { viewCollectionAssets(window.addingAssetsToArtwork); } }, 2000); } else { document.getElementById('addAssetsResult').innerHTML = `

    Failed to save assets: ${saveData.error || 'Unknown error'}

    Response status: ${saveResponse.status}

    `; } } catch (error) { document.getElementById('addAssetsResult').innerHTML = `

    Upload failed: ${error.message}

    `; } } // Handle edit cover image preview function handleEditCoverPreview(event) { const file = event.target.files[0]; const newPreview = document.getElementById('editNewCoverPreview'); const newPreviewImg = document.getElementById('editNewCoverImage'); if (!file) { newPreview.style.display = 'none'; return; } // Validate file const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; if (!validTypes.includes(file.type)) { alert('Please select a valid image file (JPG, PNG, GIF, or WebP)'); event.target.value = ''; newPreview.style.display = 'none'; return; } const maxSize = 5 * 1024 * 1024; // 5MB if (file.size > maxSize) { alert('Cover image must be smaller than 5MB. Please compress your image and try again.'); event.target.value = ''; newPreview.style.display = 'none'; return; } // Show preview const reader = new FileReader(); reader.onload = function(e) { newPreviewImg.src = e.target.result; newPreview.style.display = 'block'; }; reader.readAsDataURL(file); } // Update cover image window.updateCoverImage = async function() { const coverFile = document.getElementById('editOnDemandCoverInput').files[0]; if (!coverFile) { alert('Please select a new cover image first'); return; } if (!window.editingOnDemandSlug) { alert('No collection selected for editing'); return; } try { // Upload new cover image const formData = new FormData(); formData.append('image', coverFile); formData.append('type', 'collection'); const uploadResponse = await fetch('/api/upload-collection-image', { method: 'POST', credentials: 'include', body: formData }); const uploadData = await uploadResponse.json(); if (!uploadData.success) { alert('Failed to upload new cover image: ' + uploadData.error); return; } // Update artwork with new cover image const updateResponse = await fetch('/api/save-artwork', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ slug: window.editingOnDemandSlug, artist: document.getElementById('editOnDemandArtistInput').value, title: document.getElementById('editOnDemandTitleInput').value, description: document.getElementById('editOnDemandDescriptionInput').value, price: parseInt(document.getElementById('editOnDemandPriceInput').value) || 0, maxMints: parseInt(document.getElementById('editOnDemandMaxMintsInput').value) || 0, parentInscriptionId: 'on-demand-placeholder', parentType: 'static', scriptInscriptionId: 'd686bf14aec7115b84cb9e85fe8d819d61217d115b8869c49694205ce3403a8ai0', imageFilename: uploadData.filename, // Use new cover image websiteUrl: document.getElementById('editOnDemandWebsiteInput').value, paymentWallet: document.getElementById('editOnDemandPaymentWalletInput').value, twitterUrl: document.getElementById('editOnDemandTwitterInput').value, discordUrl: document.getElementById('editOnDemandDiscordInput').value, physicalPrint: false, // Physical prints not available for inscribe-on-demand enablePreview: document.getElementById('editOnDemandEnablePreviewInput').checked, enableGallery: document.getElementById('editOnDemandEnableGalleryInput').checked, mintStartDatetime: document.getElementById('editOnDemandStartTimeInput').value || null, mintEndDatetime: document.getElementById('editOnDemandEndTimeInput').value || null, mintType: 'inscribe_on_demand', isEdit: true }) }); const updateData = await updateResponse.json(); if (updateData.success) { alert('Cover image updated successfully!'); // Update the current cover preview const currentCoverImg = document.getElementById('editCurrentCoverImage'); if (currentCoverImg) { currentCoverImg.src = `/api/get-collection-image?filename=${uploadData.filename}`; } // Clear the file input and new preview document.getElementById('editOnDemandCoverInput').value = ''; document.getElementById('editNewCoverPreview').style.display = 'none'; } else { alert('Failed to update cover image: ' + updateData.error); } } catch (error) { alert('Failed to update cover image: ' + error.message); } } // Show on-demand edit modal window.showOnDemandEditModal = async function(artwork) { const modal = document.getElementById('on-demand-edit-modal'); if (!modal) { alert('Edit modal not found - page may not be fully loaded'); return; } // Populate form with artwork data try { const artistInput = document.getElementById('editOnDemandArtistInput'); const titleInput = document.getElementById('editOnDemandTitleInput'); const descInput = document.getElementById('editOnDemandDescriptionInput'); if (!artistInput || !titleInput || !descInput) { alert('Edit form elements not found - modal may not be properly loaded'); return; } artistInput.value = artwork.artist || ''; titleInput.value = artwork.title || ''; descInput.value = artwork.description || ''; document.getElementById('editOnDemandPriceInput').value = artwork.price || 0; document.getElementById('editOnDemandMaxMintsInput').value = artwork.maxMints || 0; document.getElementById('editOnDemandWebsiteInput').value = artwork.websiteUrl || ''; document.getElementById('editOnDemandTwitterInput').value = artwork.twitterUrl || ''; document.getElementById('editOnDemandDiscordInput').value = artwork.discordUrl || ''; document.getElementById('editOnDemandPaymentWalletInput').value = artwork.artistPaymentWallet || ''; // Format datetime values for datetime-local inputs const formatDateTimeLocal = (dateString) => { if (!dateString) return ''; const date = new Date(dateString); if (isNaN(date.getTime())) return ''; // Format as YYYY-MM-DDTHH:MM (required format for datetime-local) return date.toISOString().slice(0, 16); }; document.getElementById('editOnDemandStartTimeInput').value = formatDateTimeLocal(artwork.mintStartDatetime); document.getElementById('editOnDemandEndTimeInput').value = formatDateTimeLocal(artwork.mintEndDatetime); document.getElementById('editOnDemandEnablePreviewInput').checked = !!artwork.enablePreview; document.getElementById('editOnDemandEnableGalleryInput').checked = artwork.enableGallery !== false; // Default to true } catch (formError) { alert('Error populating edit form: ' + formError.message); return; } // Store editing slug and mint type (inscribe_on_demand vs parent_child_prints) window.editingOnDemandSlug = artwork.slug; window.editingOnDemandMintType = artwork.mintType || 'inscribe_on_demand'; const editTitle = document.getElementById('on-demand-edit-title'); if (editTitle) { editTitle.textContent = window.editingOnDemandMintType === 'parent_child_prints' ? 'Edit Parent–Child Prints Collection' : 'Edit Inscribe-on-Demand Collection'; } const pcpAdmin = document.getElementById('editPcpAdminBlock'); if (pcpAdmin) { const isPcp = window.editingOnDemandMintType === 'parent_child_prints'; pcpAdmin.style.display = isPcp ? 'block' : 'none'; if (isPcp) { const pi = document.getElementById('editPcpParentInscriptionId'); const di = document.getElementById('editPcpDelegateInscriptionId'); const pm = document.getElementById('editPcpParentModeSelect'); if (pi) pi.value = artwork.parentInscriptionId || ''; if (di) di.value = artwork.delegateInscriptionId || ''; if (pm) pm.value = artwork.parentChildPrintParentMode || 'inscription'; } } // Load current cover image if (artwork.imageFilename) { const currentCoverImg = document.getElementById('editCurrentCoverImage'); if (currentCoverImg) { currentCoverImg.src = `/api/get-collection-image?filename=${artwork.imageFilename}`; } } // Set up cover image preview const editCoverInput = document.getElementById('editOnDemandCoverInput'); if (editCoverInput) { editCoverInput.addEventListener('change', handleEditCoverPreview); } // Load asset counts only (not the full asset list) try { const assetsResponse = await fetch(`/api/manage-collection-assets?artwork=${artwork.slug}&countsOnly=true`, { credentials: 'include' }); const assetsData = await assetsResponse.json(); if (assetsData.success) { // Update asset counts document.getElementById('edit-assets-total').textContent = assetsData.total; document.getElementById('edit-assets-available').textContent = assetsData.available; document.getElementById('edit-assets-minted').textContent = assetsData.minted; // Show placeholder for assets (will load on demand) const preview = document.getElementById('edit-assets-preview'); if (preview) { preview.innerHTML = `

    ${assetsData.total} assets in collection

    `; } } } catch (error) { console.error('Failed to load asset counts:', error); } modal.style.display = 'block'; } // Hide on-demand edit modal window.hideOnDemandEditModal = function() { const modal = document.getElementById('on-demand-edit-modal'); if (modal) modal.style.display = 'none'; } // Load assets preview on demand (paginated) window.loadEditAssetsPreview = async function(slug, page = 1) { const preview = document.getElementById('edit-assets-preview'); if (!preview) return; preview.innerHTML = `
    Loading assets...
    `; try { const response = await fetch(`/api/manage-collection-assets?artwork=${slug}&page=${page}&limit=20`, { credentials: 'include' }); const assetsData = await response.json(); if (assetsData.success && assetsData.assets) { displayEditAssetsPreview(assetsData.assets, assetsData.total, page); } else { preview.innerHTML = `
    Error loading assets: ${assetsData.error || 'Unknown error'}
    `; } } catch (error) { console.error('Failed to load assets preview:', error); preview.innerHTML = `
    Failed to load assets: ${error.message}
    `; } } // Display asset previews in edit modal (paginated) function displayEditAssetsPreview(assets, total, currentPage) { const preview = document.getElementById('edit-assets-preview'); if (!preview) return; if (assets.length === 0) { preview.innerHTML = '

    No assets

    '; return; } const assetsPerPage = 20; const totalPages = Math.ceil(total / assetsPerPage); preview.innerHTML = assets.map(asset => `
    ${asset.original_name}

    ${asset.is_minted ? 'M' : 'A'}

    `).join(''); // Add pagination controls if needed if (totalPages > 1) { const paginationDiv = document.createElement('div'); paginationDiv.style.cssText = 'grid-column: 1 / -1; text-align: center; padding: 10px; border-top: 1px solid #666;'; let paginationHTML = `
    Page ${currentPage} of ${totalPages} (${total} total assets)
    `; paginationHTML += '
    '; if (currentPage > 1) { paginationHTML += ``; } if (currentPage < totalPages) { paginationHTML += ``; } paginationHTML += '
    '; paginationDiv.innerHTML = paginationHTML; preview.appendChild(paginationDiv); } } // Open assets gallery from edit modal window.openAssetsGalleryFromEdit = function() { if (window.editingOnDemandSlug) { viewCollectionAssets(window.editingOnDemandSlug); } } // Save on-demand collection edits window.saveOnDemandEdit = async function() { const artist = document.getElementById('editOnDemandArtistInput').value; const title = document.getElementById('editOnDemandTitleInput').value; const description = document.getElementById('editOnDemandDescriptionInput').value; const price = parseInt(document.getElementById('editOnDemandPriceInput').value) || 0; const maxMints = parseInt(document.getElementById('editOnDemandMaxMintsInput').value) || 0; const website = document.getElementById('editOnDemandWebsiteInput').value; const twitter = document.getElementById('editOnDemandTwitterInput').value; const discord = document.getElementById('editOnDemandDiscordInput').value; const paymentWallet = document.getElementById('editOnDemandPaymentWalletInput').value; const enablePreview = document.getElementById('editOnDemandEnablePreviewInput').checked; const enableGallery = document.getElementById('editOnDemandEnableGalleryInput').checked; const startTime = document.getElementById('editOnDemandStartTimeInput').value; const endTime = document.getElementById('editOnDemandEndTimeInput').value; if (!artist || !title) { alert('Artist and title are required'); return; } try { const isPcpEdit = window.editingOnDemandMintType === 'parent_child_prints'; const parentInscriptionField = document.getElementById('editPcpParentInscriptionId'); const delegateInscriptionField = document.getElementById('editPcpDelegateInscriptionId'); const parentModeField = document.getElementById('editPcpParentModeSelect'); const requestData = { slug: window.editingOnDemandSlug, artist, title, description, price, maxMints, parentInscriptionId: isPcpEdit ? ((parentInscriptionField && parentInscriptionField.value.trim()) || null) : 'on-demand-placeholder', parentType: 'static', scriptInscriptionId: 'd686bf14aec7115b84cb9e85fe8d819d61217d115b8869c49694205ce3403a8ai0', imageFilename: 'unchanged', // Keep existing cover image websiteUrl: website, paymentWallet: paymentWallet, twitterUrl: twitter, discordUrl: discord, physicalPrint: false, // Physical prints not available for inscribe-on-demand enablePreview: enablePreview, enableGallery: enableGallery, mintStartDatetime: startTime || null, mintEndDatetime: endTime || null, mintType: window.editingOnDemandMintType || 'inscribe_on_demand', delegateInscriptionId: isPcpEdit && delegateInscriptionField ? (delegateInscriptionField.value.trim() || null) : undefined, parentChildPrintParentMode: isPcpEdit && parentModeField ? parentModeField.value : undefined, isEdit: true }; const response = await fetch('/api/save-artwork', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify(requestData) }); const data = await response.json(); if (data.success) { alert('Collection updated successfully'); hideOnDemandEditModal(); // Refresh the artwork list if (typeof loadUserArtworks === 'function') { loadUserArtworks(); } if (typeof loadOnDemandCollections === 'function') { loadOnDemandCollections(); } // Also refresh admin tabs if they exist if (typeof refreshCurrentAdminTab === 'function') { refreshCurrentAdminTab(); } } else { alert('Error: ' + data.error); } } catch (error) { alert('Failed to save changes'); } } // Edit on-demand collection from admin interface window.editOnDemandCollection = async function(slug) { try { const response = await fetch(`/api/get-artwork-admin?slug=${slug}`, { credentials: 'include' // Include session cookies }); if (!response.ok) { const errorText = await response.text(); alert(`Failed to load collection: ${response.status} ${response.statusText}`); return; } const data = await response.json(); if (data.success && data.artwork) { showOnDemandEditModal(data.artwork); } else { alert('Failed to load collection for editing: ' + (data.error || 'Unknown error')); } } catch (error) { alert('Failed to load collection for editing: ' + error.message); } } // Delete on-demand collection window.deleteOnDemandCollection = async function(slug, title) { if (!confirm(`Are you sure you want to delete "${title}"?\\n\\nThis will also remove all collection assets. This action cannot be undone.`)) { return; } try { const response = await fetch('/api/delete-artwork', { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ slug }) }); const data = await response.json(); if (data.success) { alert('Collection deleted successfully'); loadOnDemandCollections(); // Refresh the list } else { alert('Failed to delete collection: ' + (data.error || 'Unknown error')); } } catch (error) { alert('Failed to delete collection: Network error'); } } // ============ END ON-DEMAND COLLECTIONS FUNCTIONS ============ // Load artworks for admin management (including pending submissions) async function loadArtworksForAdmin() { try { const showHidden = document.getElementById('admin-show-hidden-artworks')?.checked; const response = await fetch(`/api/admin-list?_=${Date.now()}`, { credentials: 'include', cache: 'no-store', }); const data = await response.json(); if (data.success && data.artworks) { displayArtworksAdmin(data.artworks); } const hiddenListEl = document.getElementById('artworks-admin-hidden-list'); if (hiddenListEl) { if (showHidden) { const hiddenRes = await fetch(`/api/admin-list?hiddenOnly=1&_=${Date.now()}`, { credentials: 'include', cache: 'no-store', }); const hiddenData = await hiddenRes.json(); hiddenListEl.style.display = 'block'; if (hiddenData.success && hiddenData.artworks?.length) { hiddenListEl.innerHTML = '

    Hidden from admin lists

    ' + displayArtworksAdminCards(hiddenData.artworks, { showUnhide: true }); } else { hiddenListEl.innerHTML = '

    No hidden artworks

    '; } } else { hiddenListEl.style.display = 'none'; hiddenListEl.innerHTML = ''; } } } catch (error) { const adminList = document.getElementById('artworks-admin-list'); if (adminList) { adminList.innerHTML = '

    Failed to load artworks

    '; } } } // Load completed artworks (sold-out and fully paid) async function loadCompletedArtworks() { try { const response = await fetch('/api/admin-list'); const data = await response.json(); if (data.success && data.artworks) { // Filter for completed artworks const completedArtworks = data.artworks.filter(artwork => { const isSoldOut = artwork.total !== -1 && artwork.minted >= artwork.total; if (!isSoldOut) return false; // Must be sold out // For free artworks (price = 0), just being sold out is enough if (artwork.price === 0) return true; // For paid artworks, must also be fully paid const isFullyPaid = artwork.earnings && artwork.earnings.totalEarningsSats > 0 && artwork.earnings.outstandingBalanceSats <= 0; return isFullyPaid; }); displayCompletedArtworks(completedArtworks); } } catch (error) { const completedList = document.getElementById('completed-admin-list'); if (completedList) { completedList.innerHTML = '

    Failed to load completed artworks

    '; } } } // Helper function to get artwork image HTML function getArtworkImageHTML(artwork, size = '120px') { if (artwork.mintType === 'pre_inscribed' || artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints' || artwork.mintType === 'numerical') { return `${artwork.title} ${artwork.artist}`; } else if (artwork.parentType === 'recursive') { return `
    `; } else if (artwork.parentInscriptionId) { return `${artwork.title || 'Unknown'} by ${artwork.artist || 'Unknown'}`; } else { return `
    ✅ COMPLETED
    `; } } // Toggle expanded view for an artist's collections window.toggleArtistCollections = function(artistName) { const collectionsList = document.getElementById(`artist-collections-${artistName.replace(/[^a-zA-Z0-9]/g, '_')}`); const toggleBtn = document.getElementById(`toggle-btn-${artistName.replace(/[^a-zA-Z0-9]/g, '_')}`); if (collectionsList && toggleBtn) { if (collectionsList.style.display === 'none') { collectionsList.style.display = 'block'; toggleBtn.textContent = '▼ Hide All Collections'; } else { collectionsList.style.display = 'none'; toggleBtn.textContent = '▶ Show All Collections'; } } }; // Display completed artworks grouped by artist in alphabetical order function displayCompletedArtworks(artworks) { const completedList = document.getElementById('completed-admin-list'); if (!completedList) return; if (artworks.length === 0) { completedList.innerHTML = '

    No completed artworks yet

    '; return; } // Group artworks by artist const artistGroups = {}; artworks.forEach(artwork => { const artist = artwork.artist || 'Unknown'; if (!artistGroups[artist]) { artistGroups[artist] = []; } artistGroups[artist].push(artwork); }); // Sort artists alphabetically const sortedArtists = Object.keys(artistGroups).sort((a, b) => a.localeCompare(b)); // Build HTML for each artist group completedList.innerHTML = sortedArtists.map(artist => { const collections = artistGroups[artist]; // Sort collections by created date (newest first) to get the latest const sortedCollections = [...collections].sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0) ); const latestCollection = sortedCollections[0]; const artistId = artist.replace(/[^a-zA-Z0-9]/g, '_'); const totalCollections = collections.length; return `
    ${getArtworkImageHTML(latestCollection, '150px')}

    ${artist}

    ${totalCollections} Completed Collection${totalCollections > 1 ? 's' : ''}

    LATEST COLLECTION:

    ${latestCollection.title}

    Price: ${latestCollection.price === 0 ? 'FREE' : latestCollection.price + ' sats'}

    Minted: ${latestCollection.minted} / ${latestCollection.total}

    ${(latestCollection.mintType === 'inscribe_on_demand' || latestCollection.mintType === 'parent_child_prints') ? `` : `` }
    ${totalCollections > 1 ? ` ` : ''}
    ${totalCollections > 1 ? ` ` : ''}
    `; }).join(''); } // Refresh current admin tab after changes function refreshCurrentAdminTab() { // Check which tab is currently active and refresh it const artworksTabContent = document.getElementById('admin-artworks-tab'); const completedTabContent = document.getElementById('admin-completed-tab'); const onDemandTabContent = document.getElementById('admin-on-demand-tab'); const preInscribedTabContent = document.getElementById('admin-pre-inscribed-tab'); const inquiriesTabContent = document.getElementById('admin-inquiries-tab'); const printsTabContent = document.getElementById('admin-prints-tab'); const paymentsTabContent = document.getElementById('admin-payments-tab'); const statsTabContent = document.getElementById('admin-stats-tab'); // Check which tab content is currently visible and refresh the appropriate data if (artworksTabContent && artworksTabContent.style.display !== 'none') { loadArtworksForAdmin(); } else if (completedTabContent && completedTabContent.style.display === 'block') { loadCompletedArtworks(); } else if (onDemandTabContent && onDemandTabContent.style.display === 'block') { loadOnDemandCollections(); } else if (preInscribedTabContent && preInscribedTabContent.style.display === 'block') { loadPreInscribedCollections(); } else if (inquiriesTabContent && inquiriesTabContent.style.display === 'block') { loadCollectionInquiries(); } else if (printsTabContent && printsTabContent.style.display === 'block') { loadPhysicalPrintOrders(); } else if (paymentsTabContent && paymentsTabContent.style.display === 'block') { loadPaymentHistory(); } else if (statsTabContent && statsTabContent.style.display === 'block') { loadAdminStats(); } } function displayArtworksAdminCards(artworks, options = {}) { const { showUnhide = false } = options; return artworks.map(artwork => `
    ${artwork.mintType === 'pre_inscribed' || artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints' || artwork.mintType === 'numerical' ? `${artwork.title} ${artwork.artist}` : artwork.parentType === 'recursive' ? `
    ` : artwork.parentInscriptionId ? `${artwork.title} ${artwork.artist}` : `
    NO PREVIEW
    ` }

    ${artwork.title}

    ${artwork.status}

    Artist: ${artwork.artist}

    Price: ${artwork.price === 0 ? 'FREE' : artwork.price + ' sats'}

    Minted: ${artwork.minted} / ${artwork.total === -1 ? '∞' : artwork.total}

    ${artwork.earnings && artwork.earnings.totalEarningsSats > 0 ? `

    💰 PAYMENT TRACKING

    Earned: ${artwork.earnings.totalEarningsSats.toLocaleString()} sats

    Paid: ${artwork.earnings.totalPaidSats.toLocaleString()} sats (${artwork.earnings.paymentPercentage}%)

    Outstanding: ${artwork.earnings.outstandingBalanceSats.toLocaleString()} sats

    ${artwork.earnings.paymentCount} payment${artwork.earnings.paymentCount !== 1 ? 's' : ''} recorded

    ` : ''}

    Slug: ${artwork.slug}

    ${(artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints') ? `` : ''} ${artwork.mintType === 'pre_inscribed' ? `` : ''} ${artwork.mintType === 'pre_inscribed' ? `` : ''} ${(artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints') ? `` : artwork.mintType === 'pre_inscribed' ? `` : ``} ${showUnhide ? `` : `` } ${artwork.canDelete ? `` : ''}
    `).join(''); } function displayArtworksAdmin(artworks) { const adminList = document.getElementById('artworks-admin-list'); if (!adminList) return; const activeArtworks = artworks.filter(artwork => { const isSoldOut = artwork.total !== -1 && artwork.minted >= artwork.total; if (!isSoldOut) return true; if (artwork.price === 0) return false; const isFullyPaid = artwork.earnings && artwork.earnings.totalEarningsSats > 0 && artwork.earnings.outstandingBalanceSats <= 0; return !isFullyPaid; }); activeArtworks.sort((a, b) => { const aInactive = !a.isActive; const bInactive = !b.isActive; if (aInactive && !bInactive) return -1; if (!aInactive && bInactive) return 1; if (a.isActive && b.isActive) { const aOutstanding = a.earnings && a.earnings.outstandingBalanceSats > 0; const bOutstanding = b.earnings && b.earnings.outstandingBalanceSats > 0; if (aOutstanding && !bOutstanding) return -1; if (!aOutstanding && bOutstanding) return 1; } return new Date(b.createdAt || 0) - new Date(a.createdAt || 0); }); if (activeArtworks.length === 0) { adminList.innerHTML = '

    No active artworks found (completed artworks moved to Completed tab)

    '; return; } adminList.innerHTML = displayArtworksAdminCards(activeArtworks); } window.setAdminArtworkHidden = async function(slug, hidden) { if (hidden && !confirm('Hide this artwork from admin lists? It will stay live on the site if active.')) { return; } try { const response = await fetch('/api/toggle-admin-hidden-artwork', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ slug, hidden }), }); const data = await response.json(); if (data.success) { if (typeof loadArtworksForAdmin === 'function') { await loadArtworksForAdmin(); } else { refreshCurrentAdminTab(); } } else { alert('Error: ' + (data.error || 'Failed to update')); } } catch (error) { alert('Failed to update artwork visibility'); } }; // Show artwork modal function showArtworkModal(artwork = null) { const modal = document.getElementById('artwork-modal'); const modalTitle = document.getElementById('modal-title'); if (!modal) return; modal.style.display = 'block'; // Update modal title if (modalTitle) { modalTitle.textContent = artwork ? 'Edit Artwork' : 'Create New Artwork'; } // If editing, populate form if (artwork) { document.getElementById('artistInput').value = artwork.artist || ''; document.getElementById('titleInput').value = artwork.title || ''; document.getElementById('descriptionInput').value = artwork.description || ''; // Populate collection name if it exists const collectionNameInput = document.getElementById('collectionNameInput'); if (collectionNameInput && artwork.collectionName) { collectionNameInput.value = artwork.collectionName; } else if (collectionNameInput) { collectionNameInput.value = ''; } document.getElementById('priceInput').value = artwork.price || 0; // Handle infinite mints (-1) by showing a special placeholder const maxMintsInput = document.getElementById('maxMintsInput'); if (artwork.maxMints === -1 || artwork.total === -1) { maxMintsInput.value = -1; maxMintsInput.placeholder = 'Infinite (use -1)'; } else { maxMintsInput.value = artwork.maxMints || artwork.total || 1000; maxMintsInput.placeholder = ''; } // Handle parent type selection const parentType = artwork.parentType || 'static'; const staticRadio = document.querySelector(`input[name="adminParentType"][value="static"]`); const recursiveRadio = document.querySelector(`input[name="adminParentType"][value="recursive"]`); if (staticRadio && recursiveRadio) { if (parentType === 'static') { staticRadio.checked = true; document.getElementById('parentIdInput').value = artwork.parentInscriptionId || ''; document.getElementById('recursiveParentIdInput').value = ''; } else { recursiveRadio.checked = true; document.getElementById('parentIdInput').value = ''; document.getElementById('recursiveParentIdInput').value = artwork.parentInscriptionId || ''; } // Update UI based on parent type toggleAdminParentType(); } document.getElementById('websiteInput').value = artwork.websiteUrl || ''; document.getElementById('paymentWalletInput').value = artwork.artistPaymentWallet || ''; document.getElementById('twitterInput').value = artwork.twitterUrl || ''; document.getElementById('discordInput').value = artwork.discordUrl || ''; // Handle physical print checkbox const physicalPrintCheckbox = document.getElementById('physicalPrintInput'); if (physicalPrintCheckbox) { physicalPrintCheckbox.checked = !!artwork.physicalPrintAvailable; } // Handle delegate checkbox const delegateCheckbox = document.getElementById('delegateInput'); if (delegateCheckbox) { delegateCheckbox.checked = !!artwork.isDelegate; } // Handle pixel art checkbox - check if using pixel art script const pixelArtCheckbox = document.getElementById('adminPixelArtInput'); if (pixelArtCheckbox) { // Check if it's using the pixel art script // Pixel art: d686bf14... | Default: 5b0e2cf2... pixelArtCheckbox.checked = artwork.scriptInscriptionId === 'd686bf14aec7115b84cb9e85fe8d819d61217d115b8869c49694205ce3403a8ai0'; } // Handle stamp checkbox const stampCheckbox = document.getElementById('adminStampInput'); if (stampCheckbox) { stampCheckbox.checked = !!artwork.stamp; } // Handle datetime fields - convert from UTC to local datetime-local format if (artwork.mintStartDatetime) { const startDate = new Date(artwork.mintStartDatetime); document.getElementById('mintStartInput').value = startDate.toISOString().slice(0, 16); } else { document.getElementById('mintStartInput').value = ''; } if (artwork.mintEndDatetime) { const endDate = new Date(artwork.mintEndDatetime); document.getElementById('mintEndInput').value = endDate.toISOString().slice(0, 16); } else { document.getElementById('mintEndInput').value = ''; } window.editingSlug = artwork.slug; window.editingMintType = artwork.mintType || 'delegate'; // Store original mint type } else { // Clear form for new artwork document.getElementById('artistInput').value = ''; document.getElementById('titleInput').value = ''; document.getElementById('descriptionInput').value = ''; document.getElementById('priceInput').value = 0; document.getElementById('maxMintsInput').value = 1000; // Reset parent type to static and clear inputs document.querySelector('input[name="adminParentType"][value="static"]').checked = true; document.getElementById('parentIdInput').value = ''; document.getElementById('recursiveParentIdInput').value = ''; toggleAdminParentType(); document.getElementById('websiteInput').value = ''; document.getElementById('paymentWalletInput').value = ''; document.getElementById('twitterInput').value = ''; document.getElementById('discordInput').value = ''; document.getElementById('mintStartInput').value = ''; document.getElementById('mintEndInput').value = ''; // Clear physical print checkbox const physicalPrintCheckbox = document.getElementById('physicalPrintInput'); if (physicalPrintCheckbox) { physicalPrintCheckbox.checked = false; } // Clear delegate checkbox const delegateCheckbox = document.getElementById('delegateInput'); if (delegateCheckbox) { delegateCheckbox.checked = false; } // Clear pixel art checkbox const pixelArtCheckbox = document.getElementById('adminPixelArtInput'); if (pixelArtCheckbox) { pixelArtCheckbox.checked = false; } // Clear stamp checkbox const stampCheckbox = document.getElementById('adminStampInput'); if (stampCheckbox) { stampCheckbox.checked = false; } window.editingSlug = null; window.editingMintType = null; // Clear mint type for new artwork } } // Hide artwork modal window.hideArtworkModal = function() { const modal = document.getElementById('artwork-modal'); if (modal) modal.style.display = 'none'; }; // Show image upload modal window.showImageUploadModal = function() { const modal = document.getElementById('image-upload-modal'); if (modal) { modal.style.display = 'block'; setupImageUpload(); } }; // Show standalone inscribe modal (from header link) window.showStandaloneInscribeModal = function() { const modal = document.getElementById('image-upload-modal'); if (modal) { modal.style.display = 'block'; setupImageUpload(); // Mark this as standalone mode (not for artwork submission) window.standaloneInscribeMode = true; // Update modal title const title = modal.querySelector('h3'); if (title) { title.textContent = 'Inscribe Image'; } } }; // Hide image upload modal window.hideImageUploadModal = function() { const modal = document.getElementById('image-upload-modal'); if (modal) { modal.style.display = 'none'; // Reset form document.getElementById('imageFileInput').value = ''; document.getElementById('image-preview').style.display = 'none'; document.getElementById('inscription-progress').style.display = 'none'; document.getElementById('inscription-result').innerHTML = ''; document.getElementById('inscribeImageBtn').disabled = true; document.getElementById('inscribeImageBtn').innerHTML = 'Inscribe Image'; document.getElementById('image-fee-selection').style.display = 'none'; // Reset standalone mode and title window.standaloneInscribeMode = false; const title = modal.querySelector('h3'); if (title) { title.textContent = 'Upload & Inscribe Image'; } } }; // Setup image upload functionality function setupImageUpload() { const fileInput = document.getElementById('imageFileInput'); const previewDiv = document.getElementById('image-preview'); const previewImage = document.getElementById('preview-image'); const fileSizeSpan = document.getElementById('file-size'); const costSpan = document.getElementById('inscription-cost'); const inscribeBtn = document.getElementById('inscribeImageBtn'); const walletSection = document.getElementById('wallet-connect-section'); // Show/hide wallet connection section based on wallet status if (!walletConnected) { walletSection.style.display = 'block'; inscribeBtn.style.display = 'none'; } else { walletSection.style.display = 'none'; inscribeBtn.style.display = 'inline-block'; } fileInput.onchange = function(event) { const file = event.target.files[0]; if (!file) { previewDiv.style.display = 'none'; inscribeBtn.disabled = true; return; } // Validate file type if (!file.type.startsWith('image/')) { alert('Please select an image file'); fileInput.value = ''; return; } // Check file size (warn if over 350KB) const fileSizeKB = Math.round(file.size / 1024); fileSizeSpan.textContent = fileSizeKB + ' KB'; if (file.size > 350 * 1024) { costSpan.textContent = 'File large - high cost!'; costSpan.style.color = '#ff6b35'; } else { // Use the dynamic cost calculation function updateImageCostCalculation(); } // Show preview const reader = new FileReader(); reader.onload = function(e) { previewImage.src = e.target.result; previewDiv.style.display = 'block'; inscribeBtn.disabled = false; }; reader.readAsDataURL(file); }; } // Get selected fee rate for image uploads window.getImageSelectedFeeRate = function() { const selectedOption = document.querySelector('input[name="imageFeeSpeed"]:checked'); if (!selectedOption) return feeEstimates.medium; const value = selectedOption.value; return feeEstimates[value] || feeEstimates.medium; }; // Update cost calculation for image uploads window.updateImageCostCalculation = function() { const fileInput = document.getElementById('imageFileInput'); const file = fileInput.files[0]; const costSpan = document.getElementById('inscription-cost'); if (!file || !costSpan) return; const feeRate = window.getImageSelectedFeeRate(); // Xverse heavily optimizes inscriptions, so use realistic estimation // Based on actual Xverse behavior: ~33,060 bytes total transaction for 95KB image // This suggests Xverse compresses images significantly let estimatedTxSize; if (file.size <= 50 * 1024) { // <= 50KB estimatedTxSize = Math.min(file.size * 0.4, 15000); // Small files: ~40% of original, max 15KB } else if (file.size <= 200 * 1024) { // 50KB - 200KB estimatedTxSize = Math.min(file.size * 0.35, 35000); // Medium files: ~35% of original, max 35KB } else { // > 200KB estimatedTxSize = Math.min(file.size * 0.3, 60000); // Large files: ~30% of original, max 50KB } const networkFee = Math.ceil(estimatedTxSize * feeRate) + MINT_NETWORK_BUFFER_SATS_PER_MINT; const marketplaceFee = MARKETPLACE_FEE_SATS_PER_MINT; // marketplace fee per image inscription const totalCost = networkFee + marketplaceFee; costSpan.textContent = totalCost.toLocaleString() + ' sats (incl. ' + MARKETPLACE_FEE_SATS_PER_MINT.toLocaleString() + ' sat marketplace fee)'; costSpan.style.color = '#cccccc'; console.log('Image cost estimation (Xverse-optimized):', { originalFileSize: file.size, estimatedTxSize: estimatedTxSize, feeRate: feeRate, networkFee: networkFee, totalCost: totalCost, compressionRatio: (estimatedTxSize / file.size * 100).toFixed(1) + '%' }); }; // Update fee display for image uploads function updateImageFeeDisplay() { const slowFeeEl = document.getElementById('imageSlowFee'); const mediumFeeEl = document.getElementById('imageMediumFee'); const fastFeeEl = document.getElementById('imageFastFee'); if (slowFeeEl) slowFeeEl.textContent = `${feeEstimates.slow} sat/vB`; if (mediumFeeEl) mediumFeeEl.textContent = `${feeEstimates.medium} sat/vB`; if (fastFeeEl) fastFeeEl.textContent = `${feeEstimates.fast} sat/vB`; } // Connect wallet from upload modal window.connectWalletForUpload = async function() { try { const connectButton = document.querySelector('#wallet-connect-section button'); if (connectButton) { connectButton.innerHTML = ' Connecting...'; connectButton.disabled = true; } // Use sats-connect library (same as working files) const response = await request('wallet_connect', null); if (response.status !== 'success') { throw new Error('Wallet connection failed.'); } // Find payment and ordinals addresses const paymentAddressInfo = response.result.addresses.find(a => a.purpose === AddressPurpose.Payment); const ordinalsAddressInfo = response.result.addresses.find(a => a.purpose === AddressPurpose.Ordinals); if (!paymentAddressInfo) { throw new Error('No Taproot payment address found in wallet.'); } // Store wallet data (remove 0x prefix from pubkey like working files) walletData = { paymentAddress: paymentAddressInfo.address, ordinalsAddress: ordinalsAddressInfo ? ordinalsAddressInfo.address : paymentAddressInfo.address, publicKey: paymentAddressInfo.publicKey.slice(2) // Remove 0x prefix }; walletConnected = true; // Update the modal UI after successful connection if (connectButton) { connectButton.innerHTML = 'Wallet Connected ✅'; } // Show fee selection after wallet connection document.getElementById('image-fee-selection').style.display = 'block'; // Update fee display with current estimates updateImageFeeDisplay(); // Refresh the modal setup after wallet connection setupImageUpload(); } catch (error) { console.error('Wallet connection error:', error); const connectButton = document.querySelector('#wallet-connect-section button'); if (connectButton) { connectButton.innerHTML = 'Connect Wallet'; connectButton.disabled = false; } alert('Failed to connect wallet: ' + error.message); } }; // Inscribe image function window.inscribeImage = async function() { const fileInput = document.getElementById('imageFileInput'); const file = fileInput.files[0]; if (!file) { alert('Please select an image file first'); return; } if (!walletConnected) { alert('Please connect your wallet first'); return; } try { const inscribeBtn = document.getElementById('inscribeImageBtn'); const statusElement = document.getElementById('inscription-status'); const progressDiv = document.getElementById('inscription-progress'); const resultDiv = document.getElementById('inscription-result'); // Show progress progressDiv.style.display = 'block'; inscribeBtn.disabled = true; inscribeBtn.innerHTML = ' Inscribing...'; statusElement.textContent = 'Preparing image for inscription...'; // Read file as buffer const arrayBuffer = await file.arrayBuffer(); const uint8Array = new Uint8Array(arrayBuffer); // Convert to base64 for Xverse const base64Content = Buffer.from(uint8Array).toString('base64'); // Calculate cost for image inscription using selected fee rate const feeRate = window.getImageSelectedFeeRate(); // Use actual base64 content size, not estimated const actualBase64Size = base64Content.length; const overhead = 150; // witness data, script, etc. const totalSize = actualBase64Size + overhead; const networkFee = Math.ceil(totalSize * feeRate) + MINT_NETWORK_BUFFER_SATS_PER_MINT; statusElement.textContent = 'Creating inscription transaction...'; // Create inscription using Xverse createInscription({ payload: { network: { type: 'Mainnet' }, contentType: file.type, content: base64Content, payloadType: 'BASE_64', suggestedMinerFeeRate: window.getImageSelectedFeeRate(), // Use selected fee rate for image inscriptions appFee: MARKETPLACE_FEE_SATS_PER_MINT, // marketplace fee for image inscriptions appFeeAddress: 'bc1qcefl3uc75wh39wcwqs4ca9xw8m7jhc35t3kv43' }, onFinish: (response) => { // Don't rely on this callback - monitoring already started }, onCancel: () => { statusElement.textContent = 'Inscription cancelled by user'; inscribeBtn.innerHTML = 'Inscribe Image'; inscribeBtn.disabled = false; resultDiv.innerHTML = `

    Inscription cancelled. You can try again or close this modal.

    `; } }); // Start monitoring immediately since Xverse callbacks are unreliable (like mint pages) statusElement.textContent = 'Monitoring mempool for inscription...'; resultDiv.innerHTML = `

    Inscription Transaction Created!

    Monitoring mempool for inscription ID...

    `; inscribeBtn.innerHTML = 'Monitoring...'; inscribeBtn.disabled = true; // Start monitoring immediately monitorInscriptionTransaction(); } catch (error) { console.error('Inscription error:', error); document.getElementById('inscription-result').innerHTML = `

    Inscription Failed: ${error.message || 'Unknown error'}

    `; document.getElementById('inscribeImageBtn').innerHTML = 'Inscribe Image'; document.getElementById('inscribeImageBtn').disabled = false; } }; // Use inscription ID in parent form window.useInscriptionId = function(inscriptionId) { if (window.standaloneInscribeMode) { // In standalone mode, just show the inscription details const resultDiv = document.getElementById('inscription-result'); if (resultDiv) { resultDiv.innerHTML = `

    ✅ Inscription Complete!

    Inscription ID:

    ${inscriptionId}

    View on Ordinals.com:

    https://ordinals.com/content/${inscriptionId}

    You can now use this inscription ID in artwork submissions or share it directly.

    `; } // Don't close the modal in standalone mode, let user see the results } else { // Original behavior for artwork submission document.getElementById('submitParentIdInput').value = inscriptionId; hideImageUploadModal(); alert('Inscription ID has been added to your submission form!'); } }; // Monitor inscription transaction to get final inscription ID (like mint pages) async function monitorInscriptionTransaction() { const statusElement = document.getElementById('inscription-status'); const resultDiv = document.getElementById('inscription-result'); // Check if wallet data is available if (!walletData || !walletData.paymentAddress) { statusElement.textContent = 'Error: No wallet data available for monitoring'; return; } statusElement.textContent = 'Monitoring wallet transactions for inscription...'; const inscriptionStartTime = Date.now(); const monitorTransactions = async () => { try { // Get recent transactions from user's payment wallet (exactly like mint pages) const response = await fetch(`https://mempool.space/api/address/${walletData.paymentAddress}/txs`); if (response.ok) { const transactions = await response.json(); // Look for recent transactions (within last 10 minutes) - include unconfirmed const recentTxs = transactions.filter(tx => { // For unconfirmed transactions, use current time if (!tx.status.confirmed) { return true; // Include all unconfirmed transactions } const txTime = tx.status.block_time * 1000; // Convert to milliseconds const timeDiff = Date.now() - txTime; return timeDiff < 600000; // Within 10 minutes for confirmed }); // Check if any recent transaction has our app fee output (marketplace fee sats) const expectedAppFee = MARKETPLACE_FEE_SATS_PER_MINT; for (const tx of recentTxs) { // First verify this transaction is FROM our payment wallet const isFromOurWallet = tx.vin.some(input => input.prevout && input.prevout.scriptpubkey_address === walletData.paymentAddress ); if (!isFromOurWallet) { continue; // Skip transactions not from our wallet } // Additional check: only process transactions that are very recent (like mint pages) if (tx.status.confirmed) { const txTime = tx.status.block_time * 1000; const timeSinceInscriptionStart = Date.now() - inscriptionStartTime; // Only process transactions that are either: // 1. Very recent (within last 2 minutes) // 2. After inscription start time (allowing some buffer) if (txTime < inscriptionStartTime - 120000 && timeSinceInscriptionStart > 120000) { continue; // Skip old transactions } } // Unconfirmed transactions are assumed to be new // Check if it has payment to our app address (exactly like mint pages) const appFeeOutput = tx.vout.find(output => output.scriptpubkey_address === 'bc1qcefl3uc75wh39wcwqs4ca9xw8m7jhc35t3kv43' ); if (appFeeOutput) { // Check for EXACT fee amount to distinguish between different inscriptions (like mint pages) const hasValidPayment = appFeeOutput.value === expectedAppFee; if (hasValidPayment) { // Try to find the inscription transaction by tracing outputs (exactly like mint pages) let inscriptionId = null; // Look for outputs that might be spent in inscription transactions for (let i = 0; i < tx.vout.length; i++) { const output = tx.vout[i]; // Skip our app fee output if (output.scriptpubkey_address === 'bc1qcefl3uc75wh39wcwqs4ca9xw8m7jhc35t3kv43') { continue; } // Check if this output is spent (indicating it was used for inscription) try { const outpointResponse = await fetch(`https://mempool.space/api/tx/${tx.txid}/outspend/${i}`); if (outpointResponse.ok) { const outspend = await outpointResponse.json(); if (outspend.spent) { // Get the spending transaction (potential inscription) const spendingTxResponse = await fetch(`https://mempool.space/api/tx/${outspend.txid}`); if (spendingTxResponse.ok) { const spendingTx = await spendingTxResponse.json(); // Check if this looks like an inscription transaction if (spendingTx.vout.length > 0) { const firstOutput = spendingTx.vout[0]; if (firstOutput.scriptpubkey_address) { // Generate inscription ID: txid + "i" + output_index inscriptionId = `${outspend.txid}i0`; break; } } } } } } catch (traceError) { // Continue checking other outputs } } if (inscriptionId) { // Update the inscription ID display const inscriptionIdP = resultDiv.querySelector('p:nth-of-type(2)'); if (inscriptionIdP) { inscriptionIdP.innerHTML = `Inscription ID: ${inscriptionId}`; } statusElement.textContent = 'Inscription ID found! Auto-filling form...'; // Auto-fill the parent inscription field document.getElementById('submitParentIdInput').value = inscriptionId; // Show success message and auto-close after delay setTimeout(() => { hideImageUploadModal(); alert('Inscription ID has been automatically added to your submission form!'); }, 2000); return true; // Transaction found and processed } } } } } return false; // No matching transaction found } catch (error) { return false; } }; // Start monitoring immediately let attempts = 0; const maxAttempts = 60; // Check for 10 minutes (10 second intervals) const checkInterval = setInterval(async () => { attempts++; const found = await monitorTransactions(); if (found) { clearInterval(checkInterval); return; } statusElement.textContent = `Monitoring for inscription... (${attempts}/${maxAttempts})`; if (attempts >= maxAttempts) { statusElement.textContent = 'Monitoring timeout - please check transaction manually'; clearInterval(checkInterval); } }, 10000); // Check every 10 seconds // Also do an immediate first check after 10 seconds setTimeout(async () => { await monitorTransactions(); }, 10000); } // Save artwork (real functionality) async function saveArtwork() { const artist = document.getElementById('artistInput').value; const title = document.getElementById('titleInput').value; const description = document.getElementById('descriptionInput').value; const collectionName = document.getElementById('collectionNameInput')?.value || null; const price = parseInt(document.getElementById('priceInput').value) || 0; const maxMints = parseInt(document.getElementById('maxMintsInput').value) || 1000; // Get parent type and corresponding parent ID const parentType = document.querySelector('input[name="adminParentType"]:checked').value; const parentId = parentType === 'static' ? document.getElementById('parentIdInput').value : document.getElementById('recursiveParentIdInput').value; const website = document.getElementById('websiteInput').value; const paymentWallet = document.getElementById('paymentWalletInput').value; const twitter = document.getElementById('twitterInput').value; const discord = document.getElementById('discordInput').value; const mintStart = document.getElementById('mintStartInput').value; const mintEnd = document.getElementById('mintEndInput').value; const physicalPrint = document.getElementById('physicalPrintInput').checked; const isDelegate = document.getElementById('delegateInput').checked; const isPixelArt = document.getElementById('adminPixelArtInput').checked; const isStamp = document.getElementById('adminStampInput').checked; const isCardPack = document.getElementById('adminCardPackInput').checked; // Get pack cards data if it's a card pack const packCards = isCardPack ? getPackCardsData() : []; // Use pixel art renderer if checked, otherwise use default const scriptId = isPixelArt ? 'd686bf14aec7115b84cb9e85fe8d819d61217d115b8869c49694205ce3403a8ai0' // Pixel art renderer (renderer-pixel.js) : '5b0e2cf2d3d9a2aee10b5ae860487376badaa3fa4f2ac470fa25a4e3c1217c3ci0'; // Default renderer (renderer.js) // Convert datetime-local values to UTC ISO strings const mintStartDatetime = mintStart ? new Date(mintStart + 'Z').toISOString() : null; const mintEndDatetime = mintEnd ? new Date(mintEnd + 'Z').toISOString() : null; if (!artist || !title) { alert('Artist and title are required'); return; } // Generate slug from artist-title to avoid conflicts (only for new artworks) const slug = window.editingSlug || `${artist}-${title}`.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); try { const response = await fetch('/api/save-artwork', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ slug, artist, title, description, collectionName, price, maxMints, parentInscriptionId: parentId, parentType: parentType, scriptInscriptionId: scriptId, imageFilename: 'mintonbtc.webp', // Default for now websiteUrl: website, paymentWallet: paymentWallet, twitterUrl: twitter, discordUrl: discord, mintStartDatetime: mintStartDatetime, mintEndDatetime: mintEndDatetime, physicalPrint: physicalPrint, isDelegate: isDelegate, isStamp: isStamp, isCardPack: isCardPack, packCards: packCards, mintType: window.editingMintType || 'delegate', // Preserve original mint type when editing additionalDetails: null, enablePreview: false, // Default for regular artworks enableGallery: true, // Default for regular artworks whitelistEnabled: false, // Default for regular artworks whitelistStartDatetime: null, whitelistEndDatetime: null, isEdit: window.editingSlug ? true : false }) }); const data = await response.json(); if (data.success) { alert(data.message); hideArtworkModal(); // Refresh the user's artwork list if they're on the user portal if (typeof loadUserArtworks === 'function') { loadUserArtworks(); } // Refresh admin tabs if they exist if (typeof refreshCurrentAdminTab === 'function') { refreshCurrentAdminTab(); } } else { alert('Error: ' + data.error); } } catch (error) { alert('Failed to save artwork'); } } // Collection Management Functions let editingCollectionId = null; window.showCreateCollectionModal = async function() { editingCollectionId = null; document.getElementById('collection-modal-title').textContent = 'Create Collection'; document.getElementById('collectionModalNameInput').value = ''; document.getElementById('collectionModalArtistInput').value = ''; document.getElementById('collectionModalDescriptionInput').value = ''; document.getElementById('collectionModalImageInput').value = ''; // Load user's artworks for checkbox selection await loadArtworksForCollectionAssignment(); document.getElementById('collection-modal').style.display = 'block'; }; window.showEditCollectionModal = async function(collectionId) { editingCollectionId = collectionId; document.getElementById('collection-modal-title').textContent = 'Edit Collection'; try { const response = await fetch(`/api/get-collection-details?id=${collectionId}`); const data = await response.json(); if (data.success && data.collection) { document.getElementById('collectionModalNameInput').value = data.collection.name || ''; document.getElementById('collectionModalArtistInput').value = data.collection.artist_name || ''; document.getElementById('collectionModalDescriptionInput').value = data.collection.description || ''; // Load artworks and pre-check those in this collection await loadArtworksForCollectionAssignment(data.artworks.map(a => a.id)); } } catch (error) { console.error('Error loading collection:', error); } document.getElementById('collection-modal').style.display = 'block'; }; window.hideCollectionModal = function() { document.getElementById('collection-modal').style.display = 'none'; editingCollectionId = null; }; // Store all artworks for filtering let allCollectionArtworks = []; let collectionSelectedArtworkIds = []; async function loadArtworksForCollectionAssignment(selectedArtworkIds = []) { try { collectionSelectedArtworkIds = selectedArtworkIds; // Get user's artworks or all artworks if admin const isAdmin = currentUser && currentUser.role === 'admin'; if (isAdmin) { // For admins, get all artworks (including inactive) const response = await fetch('/api/simple-list?include_inactive=true'); const data = await response.json(); if (data.success) { allCollectionArtworks = data.artworks || []; } } else { // For users, get only their artworks const response = await fetch('/api/get-user-artworks', { credentials: 'include' }); const data = await response.json(); if (data.success) { allCollectionArtworks = data.artworks || []; } } // Apply initial filter filterCollectionArtworks(); } catch (error) { console.error('Error loading artworks:', error); } } window.filterCollectionArtworks = function() { const showInactive = document.getElementById('collectionShowInactiveCheckbox').checked; const artistFilter = document.getElementById('collectionArtistFilterInput').value.toLowerCase().trim(); // Filter artworks let filteredArtworks = allCollectionArtworks.filter(artwork => { // Filter by active status (show only active unless checkbox is checked) if (!showInactive && !artwork.is_active) { return false; } // Filter by artist name if (artistFilter && !artwork.artist.toLowerCase().includes(artistFilter)) { return false; } return true; }); const container = document.getElementById('collection-artwork-checkboxes'); if (filteredArtworks.length === 0) { container.innerHTML = '

    No artworks match the filters

    '; } else { container.innerHTML = filteredArtworks.map(artwork => { const isSelected = collectionSelectedArtworkIds.includes(artwork.id); const statusBadge = artwork.is_active ? '' : 'INACTIVE'; return ` `; }).join(''); } } window.saveCollection = async function() { const name = document.getElementById('collectionModalNameInput').value; const artistName = document.getElementById('collectionModalArtistInput').value; const description = document.getElementById('collectionModalDescriptionInput').value; const imageFile = document.getElementById('collectionModalImageInput').files[0]; if (!name) { alert('Collection name is required'); return; } if (!artistName) { alert('Artist name is required'); return; } // Get selected artwork IDs const checkboxes = document.querySelectorAll('.collection-artwork-checkbox:checked'); const artworkIds = Array.from(checkboxes).map(cb => parseInt(cb.value)); try { let collectionImage = null; // Upload image if provided if (imageFile) { console.log('📤 Uploading collection image:', imageFile.name); const imageData = await readFileAsBase64(imageFile); const uploadResponse = await fetch('/api/upload-collection-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ imageData: imageData, filename: imageFile.name, uploadType: 'collection' }) }); const uploadData = await uploadResponse.json(); console.log('📤 Upload response:', uploadData); if (uploadData.success) { collectionImage = uploadData.filename; console.log('✅ Collection image uploaded:', collectionImage); } else { alert('Failed to upload image: ' + (uploadData.error || 'Unknown error')); return; } } else if (editingCollectionId) { // If editing and no new image provided, preserve the existing image console.log('🔄 Editing collection, fetching existing image...'); const currentCollectionResponse = await fetch(`/api/get-collection-details?id=${editingCollectionId}`); const currentCollectionData = await currentCollectionResponse.json(); if (currentCollectionData.success && currentCollectionData.collection) { collectionImage = currentCollectionData.collection.collection_image; console.log('✅ Preserved existing image:', collectionImage); } } else { console.log('⚠️ No image provided for new collection'); } // Save collection const action = editingCollectionId ? 'update' : 'create'; const requestPayload = { action: action, collectionId: editingCollectionId, name: name, artistName: artistName, description: description, collectionImage: collectionImage, artworkIds: artworkIds }; console.log('💾 Saving collection:', requestPayload); const response = await fetch('/api/manage-collection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(requestPayload) }); const data = await response.json(); console.log('💾 Save collection response:', data); if (data.success) { alert(data.message); hideCollectionModal(); // Reload the page to show updated collections if (currentPage === 'user-portal') { showUserPortalPage(); } else if (currentUser?.role === 'admin') { // Refresh admin panel if needed if (typeof loadArtworksForAdmin === 'function') { loadArtworksForAdmin(); } if (typeof loadCollectionsForAdmin === 'function') { loadCollectionsForAdmin(); } } } else { alert('Error: ' + data.error); } } catch (error) { console.error('Error saving collection:', error); alert('Failed to save collection'); } }; // Edit artwork (real functionality) - admin version window.editArtwork = async function(slug) { try { const response = await fetch(`/api/get-artwork-admin?slug=${slug}`); const data = await response.json(); if (data.success && data.artwork) { const mintType = data.artwork.mintType; // Route to appropriate edit modal based on mint type if (mintType === 'inscribe_on_demand' || mintType === 'parent_child_prints') { showOnDemandEditModal(data.artwork); } else if (mintType === 'pre_inscribed') { editPreInscribedCollection(slug); } else { // delegate, numerical, or other types use the regular modal showArtworkModal(data.artwork); } } else { alert('Failed to load artwork for editing: ' + (data.error || 'Unknown error')); } } catch (error) { alert('Failed to load artwork for editing'); } }; // Edit user's own artwork window.editUserArtwork = async function(slug) { try { const response = await fetch(`/api/get-artwork-admin?slug=${slug}`); const data = await response.json(); if (data.success && data.artwork) { const mintType = data.artwork.mintType; // Route to appropriate edit modal based on mint type if (mintType === 'inscribe_on_demand' || mintType === 'parent_child_prints') { showOnDemandEditModal(data.artwork); } else if (mintType === 'pre_inscribed') { editPreInscribedCollection(slug); } else { // delegate, numerical, or other types use the regular modal showArtworkModal(data.artwork); } } else { alert('Failed to load artwork for editing: ' + (data.error || 'Unknown error')); } } catch (error) { alert('Failed to load artwork for editing'); } }; // Delete user artwork window.deleteUserArtwork = async function(slug, title) { if (!confirm(`Are you sure you want to delete "${title}"? This action cannot be undone.`)) { return; } try { const response = await fetch('/api/delete-artwork', { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ slug }) }); const data = await response.json(); if (data.success) { alert('Artwork deleted successfully'); loadUserArtworks(); // Refresh the list } else { alert('Failed to delete artwork: ' + (data.error || 'Unknown error')); } } catch (error) { alert('Failed to delete artwork: Network error'); } }; // Delete admin artwork window.deleteAdminArtwork = async function(slug, title) { if (!confirm(`Are you sure you want to delete "${title}"? This action cannot be undone.`)) { return; } try { const response = await fetch('/api/delete-artwork', { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ slug }) }); const data = await response.json(); if (data.success) { alert('Artwork deleted successfully'); refreshCurrentAdminTab(); // Refresh the current tab } else { alert('Failed to delete artwork: ' + (data.error || 'Unknown error')); } } catch (error) { alert('Failed to delete artwork: Network error'); } }; // Toggle artwork active status (real functionality) window.toggleArtwork = async function(slug) { try { const response = await fetch('/api/toggle-artwork', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ slug }) }); const data = await response.json(); if (data.success) { alert(data.message); refreshCurrentAdminTab(); // Refresh the current tab loadArtworksList(); // Refresh home page if visible } else { alert('Error: ' + data.error); } } catch (error) { alert('Failed to toggle artwork status'); } }; // Fetch current Bitcoin fee estimates async function fetchFeeEstimates() { // Set a timeout for API calls const fetchWithTimeout = async (url, timeout = 5000) => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal, mode: 'cors' }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); throw error; } }; try { const response = await fetchWithTimeout('https://mempool.space/api/v1/fees/recommended'); if (response.ok) { const fees = await response.json(); feeEstimates = { slow: fees.economyFee || fees.hourFee || 1, medium: fees.halfHourFee || 2, fast: fees.fastestFee || 5 }; return feeEstimates; } } catch (error) { } try { const response = await fetchWithTimeout('https://blockstream.info/api/fee-estimates'); if (response.ok) { const fees = await response.json(); feeEstimates = { slow: Math.ceil(fees['144'] || 1), // ~1 day medium: Math.ceil(fees['6'] || 2), // ~1 hour fast: Math.ceil(fees['2'] || 5) // ~20 minutes }; return feeEstimates; } } catch (error) { } // Use reasonable defaults based on current market conditions feeEstimates = { slow: 1, // 1 sat/vB - economy rate medium: 3, // 3 sat/vB - standard rate fast: 6 // 6 sat/vB - priority rate }; return feeEstimates; } // Update fee display function updateFeeDisplay() { // Only update if elements exist (on mint page) const slowFeeEl = document.getElementById('slowFee'); const mediumFeeEl = document.getElementById('mediumFee'); const fastFeeEl = document.getElementById('fastFee'); if (slowFeeEl) slowFeeEl.textContent = `${feeEstimates.slow} sat/vB`; if (mediumFeeEl) mediumFeeEl.textContent = `${feeEstimates.medium} sat/vB`; if (fastFeeEl) fastFeeEl.textContent = `${feeEstimates.fast} sat/vB`; // Update cost calculation with new fee estimates (only on mint page) if (currentPage === 'mint') { updateCostCalculation(); } } // Get selected fee rate window.getSelectedFeeRate = function() { const selectedOption = document.querySelector('input[name="feeSpeed"]:checked'); if (!selectedOption) return feeEstimates.medium; const value = selectedOption.value; if (value === 'custom') { const customFee = parseInt(document.getElementById('customFeeInput').value); return customFee > 0 ? customFee : feeEstimates.medium; } return feeEstimates[value] || feeEstimates.medium; }; // Update cost calculation based on selected fee window.updateCostCalculation = async function() { // Only calculate costs on mint page if (currentPage !== 'mint') { return; } // Get current artwork to determine type const currentArtwork = window.currentArtwork; // Handle inscribe-on-demand: delegate to updateBulkMintPrice (estimate until Mint Now) if (currentArtwork && (currentArtwork.mintType === 'inscribe_on_demand' || currentArtwork.mintType === 'parent_child_prints')) { // For inscribe-on-demand, updateBulkMintPrice handles all display updates // Call it here so fee rate changes are reflected if (window.updateBulkMintPrice) { await window.updateBulkMintPrice(); } return; // Exit early - updateBulkMintPrice handles everything for inscribe-on-demand } const feeRate = window.getSelectedFeeRate(); let imageSize = 100; // Fallback default let cost; // Guard clause: if no artwork is set yet, skip pre-inscribed/numerical handling if (currentArtwork && currentArtwork.mintType && (currentArtwork.mintType === 'pre_inscribed' || currentArtwork.mintType === 'numerical')) { // Pre-inscribed or numerical specific handling will be done later in the function } if (window.selectedAssetForCostCalculation && window.selectedAssetForCostCalculation.imageDataBase64) { // Use actual selected asset size for inscribe-on-demand imageSize = Math.ceil(window.selectedAssetForCostCalculation.imageDataBase64.length * 0.75); cost = await calculateInscriptionCost(imageSize, feeRate); console.log('✅ DISPLAY: Using actual selected asset size:', { assetName: window.selectedAssetForCostCalculation.originalName, actualSizeBytes: imageSize, feeRate: feeRate }); } else if (currentArtwork) { // Calculate accurate size for different artwork types if (currentArtwork.mintType === 'inscribe_on_demand' || currentArtwork.mintType === 'parent_child_prints') { const quantityInput = document.getElementById('mint-quantity'); const quantity = quantityInput ? parseInt(quantityInput.value) || 1 : 1; let totalNetworkFee = 0; let networkFeePerItem = 0; if (window.reservedInscriptionsForCost && window.reservedInscriptionsForCost.length > 0) { // Use actual file sizes from reserved assets for (const inscription of window.reservedInscriptionsForCost) { const networkFee = computeInscribeOnDemandNetworkFeePerMint( inscription.fileSize || 1000, inscription.contentType || 'text/html' ); totalNetworkFee += networkFee; } networkFeePerItem = totalNetworkFee / window.reservedInscriptionsForCost.length; imageSize = window.reservedInscriptionsForCost[0].fileSize || 1000; console.log('✅ DISPLAY: Using actual file sizes from reserved assets:', { reservedCount: window.reservedInscriptionsForCost.length, totalNetworkFee: totalNetworkFee, networkFeePerItem: networkFeePerItem }); } else { // Fallback to estimate if no reserved assets yet imageSize = 1000; // Conservative estimate (HTML ~600 bytes, small images ~5KB) networkFeePerItem = computeInscribeOnDemandNetworkFeePerMint(imageSize); totalNetworkFee = networkFeePerItem * quantity; console.log('✅ DISPLAY: Using estimate for inscribe-on-demand (no reserved assets yet):', { estimatedSizeBytes: imageSize, quantity: quantity, networkFeePerItem: networkFeePerItem, totalNetworkFee: totalNetworkFee, note: 'Will be updated with actual sizes when quantity changes' }); } // Calculate costs const marketplaceFee = MARKETPLACE_FEE_SATS_PER_MINT; const totalMarketplaceFee = marketplaceFee * quantity; const artworkPrice = currentArtwork.price || 0; const totalArtworkPrice = artworkPrice * quantity; // Get physical print fee if selected const physicalPrintCheckbox = document.getElementById('physicalPrintCheckbox'); let physicalPrintFee = 0; if (physicalPrintCheckbox && physicalPrintCheckbox.checked) { physicalPrintFee = await getPhysicalPrintCostInSats(); if (!physicalPrintFee || physicalPrintFee <= 0) { physicalPrintFee = 60000; } } const totalPhysicalPrintFee = physicalPrintFee * quantity; cost = { networkFee: totalNetworkFee, marketplaceFee: totalMarketplaceFee, artworkPrice: totalArtworkPrice, physicalPrintFee: totalPhysicalPrintFee, totalCost: totalNetworkFee + totalMarketplaceFee + totalArtworkPrice + totalPhysicalPrintFee, sizeBytes: imageSize, estimatedTxSize: estimateInscriptionScriptBytes(imageSize), feeRate: INSCRIBE_ON_DEMAND_FEE_RATE_SAT_VB, quantity: quantity, networkFeePerItem: networkFeePerItem }; } else if (currentArtwork.mintType === 'delegate' || currentArtwork.parentType === 'recursive') { // Generate the actual HTML content that would be inscribed let htmlContent = ''; if (currentArtwork.isDelegate) { // Custom delegate template htmlContent = ` `; } else if (currentArtwork.parentType === 'recursive') { htmlContent = ` `; } else { // Traditional recursive inscription using script from database const scriptId = currentArtwork.scriptInscriptionId || 'd686bf14aec7115b84cb9e85fe8d819d61217d115b8869c49694205ce3403a8ai0'; htmlContent = `
    `; } imageSize = htmlContent.length; cost = await calculateInscriptionCost(imageSize, feeRate); console.log('✅ DISPLAY: Using actual HTML size for delegate/recursive:', { mintType: currentArtwork.mintType, parentType: currentArtwork.parentType, htmlSizeBytes: imageSize, feeRate: feeRate, isDelegate: currentArtwork.isDelegate }); } else { // Fallback for other types cost = await calculateInscriptionCost(imageSize, feeRate); console.log('✅ DISPLAY: Using fallback size for unknown type:', imageSize); } } else { // No artwork data available, use fallback cost = await calculateInscriptionCost(imageSize, feeRate); console.log('✅ DISPLAY: Using fallback size (no artwork data):', imageSize); } // Get dynamic physical print cost const physicalPrintCheckbox = document.getElementById('physicalPrintCheckbox'); let physicalPrintFee = 0; if (physicalPrintCheckbox && physicalPrintCheckbox.checked) { physicalPrintFee = await getPhysicalPrintCostInSats(); // Ensure we always have a valid fee (fallback to 60,000 sats) if (!physicalPrintFee || physicalPrintFee <= 0) { physicalPrintFee = 60000; } } // Update total cost with dynamic physical print fee cost.physicalPrintFee = physicalPrintFee; cost.totalCost = cost.networkFee + cost.marketplaceFee + cost.artworkPrice + physicalPrintFee; // Handle pre-inscribed / numerical cost display (network buffer per mint for transfer/settlement) if (currentArtwork && (currentArtwork.mintType === 'pre_inscribed' || currentArtwork.mintType === 'numerical' || currentArtwork.mintType === 'pre_inscribed_v2')) { const marketplaceFee = MARKETPLACE_FEE_SATS_PER_MINT; const networkPerMint = MINT_NETWORK_BUFFER_SATS_PER_MINT; // Get dynamic physical print cost let physicalPrintFeePerMint = 0; if (physicalPrintCheckbox && physicalPrintCheckbox.checked) { physicalPrintFeePerMint = await getPhysicalPrintCostInSats(); // Ensure we always have a valid fee (fallback to 60,000 sats) if (!physicalPrintFeePerMint || physicalPrintFeePerMint <= 0) { physicalPrintFeePerMint = 60000; } } let pricePerMint = currentArtwork.price + marketplaceFee + networkPerMint; if (physicalPrintCheckbox && physicalPrintCheckbox.checked) { pricePerMint += physicalPrintFeePerMint; } // Get quantity from UI const quantityInput = document.getElementById('mint-quantity'); const quantity = quantityInput ? parseInt(quantityInput.value) || 1 : 1; const totalAmount = pricePerMint * quantity; const totalNetworkFee = networkPerMint * quantity; const totalMarketplaceFee = marketplaceFee * quantity; // Update the cost display elements for pre-inscribed v2 const networkElement = document.getElementById('networkFeesDisplay'); const marketplaceElement = document.getElementById('marketplaceFeeDisplay'); const totalElement = document.getElementById('totalCostDisplay'); const usdElement = document.getElementById('usdCostDisplay'); const artworkPriceElement = document.getElementById('artworkPriceDisplay'); const physicalPrintElement = document.getElementById('physicalPrintDisplay'); if (networkElement) networkElement.textContent = `${totalNetworkFee.toLocaleString()} sats`; if (marketplaceElement) marketplaceElement.textContent = `${totalMarketplaceFee.toLocaleString()} sats`; const networkSection = document.getElementById('networkSection'); const networkSeparator = document.getElementById('networkSeparator'); if (networkSection) networkSection.style.setProperty('display', 'block', 'important'); if (networkSeparator) networkSeparator.style.setProperty('display', 'block', 'important'); const networkBufferSectionPre = document.getElementById('networkBufferSection'); const networkBufferSeparatorPre = document.getElementById('networkBufferSeparator'); if (networkBufferSectionPre) networkBufferSectionPre.style.display = 'none'; if (networkBufferSeparatorPre) networkBufferSeparatorPre.style.display = 'none'; if (totalElement) totalElement.textContent = `${totalAmount.toLocaleString()} sats`; if (usdElement) usdElement.textContent = `(~$${(totalAmount * 0.00005).toFixed(2)})`; if (artworkPriceElement) { artworkPriceElement.textContent = currentArtwork.price === 0 ? 'FREE' : `${currentArtwork.price.toLocaleString()} sats`; } if (physicalPrintElement) { const physicalPrintTotal = (physicalPrintCheckbox && physicalPrintCheckbox.checked) ? (physicalPrintFeePerMint * quantity) : 0; physicalPrintElement.textContent = `${physicalPrintTotal.toLocaleString()} sats`; } // Update physical print UI elements const physicalPrintCostDiv = document.getElementById('physicalPrintCostDiv'); const addressForm = document.getElementById('addressForm'); const physicalPrintLabel = document.getElementById('physicalPrintLabel'); // Define isStaging for this scope const isStaging = window.location.hostname.includes('staging') || window.location.hostname.includes('localhost') || window.location.hostname.includes('127.0.0.1'); if (physicalPrintCheckbox && physicalPrintCheckbox.checked) { if (physicalPrintCostDiv) physicalPrintCostDiv.style.display = 'block'; if (addressForm) addressForm.style.display = 'block'; if (physicalPrintLabel) { const feeText = isStaging ? '£1' : '£60'; physicalPrintLabel.textContent = `Add Physical Print (+${feeText})`; } } else { if (physicalPrintCostDiv) physicalPrintCostDiv.style.display = 'none'; if (addressForm) addressForm.style.display = 'none'; if (physicalPrintLabel) { const feeText = isStaging ? '£1' : '£60'; physicalPrintLabel.textContent = `Add Physical Print (+${feeText})`; } } // Also update the bulk mint total if (typeof window.updateBulkMintPrice === 'function') { await window.updateBulkMintPrice(); } return; // Exit early for pre-inscribed / numerical } // Regular minting flow cost display const networkBufferSectionReg = document.getElementById('networkBufferSection'); const networkBufferSeparatorReg = document.getElementById('networkBufferSeparator'); if (networkBufferSectionReg) networkBufferSectionReg.style.display = 'none'; if (networkBufferSeparatorReg) networkBufferSeparatorReg.style.display = 'none'; const networkElement = document.getElementById('networkFeesDisplay'); const totalElement = document.getElementById('totalCostDisplay'); const usdElement = document.getElementById('usdCostDisplay'); const artworkPriceElement = document.getElementById('artworkPriceDisplay'); if (networkElement) networkElement.textContent = `${cost.networkFee.toLocaleString()} sats`; if (totalElement) totalElement.textContent = `${cost.totalCost.toLocaleString()} sats`; if (usdElement) usdElement.textContent = `(~$${(cost.totalCost * 0.00005).toFixed(2)})`; // Updated conversion rate if (artworkPriceElement) { artworkPriceElement.textContent = cost.artworkPrice === 0 ? 'FREE' : `${cost.artworkPrice.toLocaleString()} sats`; } // Handle physical print cost display const physicalPrintCostDiv = document.getElementById('physicalPrintCostDiv'); const physicalPrintDisplay = document.getElementById('physicalPrintDisplay'); const addressForm = document.getElementById('addressForm'); const physicalPrintLabel = document.getElementById('physicalPrintLabel'); // Check if physical print checkbox is checked (regardless of fee calculation) const isPhysicalPrintChecked = physicalPrintCheckbox && physicalPrintCheckbox.checked; if (isPhysicalPrintChecked) { // Show address form when checkbox is checked if (addressForm) addressForm.style.display = 'block'; // Show cost display only if we have a valid fee if (physicalPrintFee > 0) { if (physicalPrintCostDiv) physicalPrintCostDiv.style.display = 'flex'; if (physicalPrintDisplay) physicalPrintDisplay.textContent = `${physicalPrintFee.toLocaleString()} sats`; } else { // Fallback display when API fails if (physicalPrintCostDiv) physicalPrintCostDiv.style.display = 'flex'; if (physicalPrintDisplay) physicalPrintDisplay.textContent = '60,000 sats (estimated)'; } if (physicalPrintLabel) physicalPrintLabel.textContent = `Add Physical Print (+${getPhysicalPrintPriceText()})`; } else { if (physicalPrintCostDiv) physicalPrintCostDiv.style.display = 'none'; if (addressForm) addressForm.style.display = 'none'; if (physicalPrintLabel) physicalPrintLabel.textContent = `Add Physical Print (+${getPhysicalPrintPriceText()})`; } }; // Add event listeners for fee selection function setupFeeEventListeners() { // Radio button change listeners document.querySelectorAll('input[name="feeSpeed"]').forEach((radio, index) => { radio.addEventListener('change', (e) => { updateCostCalculation(); }); }); // Custom fee input listener const customFeeInput = document.getElementById('customFeeInput'); if (customFeeInput) { customFeeInput.addEventListener('input', (e) => { // Auto-select custom radio when typing const customRadio = document.querySelector('input[name="feeSpeed"][value="custom"]'); if (customRadio) { customRadio.checked = true; } updateCostCalculation(); }); // Also trigger on blur to ensure final calculation customFeeInput.addEventListener('blur', updateCostCalculation); } } // Helper function to convert satoshis to BTC function satsToBTC(sats) { return (sats / 100000000).toFixed(8); } // Helper function to get physical print price text based on environment function getPhysicalPrintPriceText() { const isStaging = window.location.hostname.includes('staging') || window.location.hostname.includes('localhost') || window.location.hostname.includes('127.0.0.1'); return isStaging ? '£1' : '£60'; } // Helper function to format description with preserved line breaks and HTML escaping function formatDescription(description) { if (!description) return ''; // Escape HTML characters first, then convert line breaks const escapedDescription = description .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); return escapedDescription.replace(/\n/g, '
    '); } // Helper function to read file as text function readFileAsText(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.onerror = (e) => reject(new Error('Failed to read file')); reader.readAsText(file); }); } // Download example whitelist CSV window.downloadWhitelistExample = function() { const csvContent = `bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh,3 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4,1 bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3,5 bc1q9vza2e8x573nczrlzms0wvx8cjn40f6k2k3uuc,2 bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq,1`; const blob = new Blob([csvContent], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'whitelist-example.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); }; // Update physical print UI elements based on environment function updatePhysicalPrintUI() { const priceText = getPhysicalPrintPriceText(); const physicalPrintLabel = document.getElementById('physicalPrintLabel'); const physicalPrintDescription = document.getElementById('physicalPrintDescription'); if (physicalPrintLabel) { physicalPrintLabel.textContent = `Add Physical Print (+${priceText})`; } if (physicalPrintDescription) { physicalPrintDescription.textContent = `Receive a high-quality Giclee print on Hahnemühle rag stock. Shipping included worldwide. Price converted from ${priceText} to sats at current exchange rate.`; } // Set up shipping address logging setupShippingAddressLogging(); } // Set up immediate shipping address logging function setupShippingAddressLogging() { const shippingFields = [ 'shippingName', 'shippingAddress', 'shippingCity', 'shippingState', 'shippingZip', 'shippingCountry', 'shippingEmail', 'shippingPhone' ]; // Add event listeners to all shipping fields shippingFields.forEach(fieldId => { const field = document.getElementById(fieldId); if (field) { // Log on blur (when user leaves the field) field.addEventListener('blur', debounce(logShippingAddressIfComplete, 500)); // Also log on input with longer delay field.addEventListener('input', debounce(logShippingAddressIfComplete, 2000)); } }); } // Debounce function to prevent excessive API calls function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Log shipping address if all required fields are filled async function logShippingAddressIfComplete() { // Only log if physical print is checked const physicalPrintCheckbox = document.getElementById('physicalPrintCheckbox'); if (!physicalPrintCheckbox || !physicalPrintCheckbox.checked) { return; } // Check if we have wallet addresses (user must be connected) if (!window.walletData || !window.walletData.paymentAddress || !window.walletData.ordinalsAddress) { return; } // Collect shipping details const shippingDetails = { name: document.getElementById('shippingName')?.value?.trim(), address: document.getElementById('shippingAddress')?.value?.trim(), city: document.getElementById('shippingCity')?.value?.trim(), state: document.getElementById('shippingState')?.value?.trim(), zip: document.getElementById('shippingZip')?.value?.trim(), country: document.getElementById('shippingCountry')?.value?.trim(), email: document.getElementById('shippingEmail')?.value?.trim(), phone: document.getElementById('shippingPhone')?.value?.trim() }; // Check if required fields are filled (email and phone are required now) if (!shippingDetails.name || !shippingDetails.address || !shippingDetails.city || !shippingDetails.country || !shippingDetails.email || !shippingDetails.phone) { return; // Don't log incomplete addresses } try { console.log('Logging shipping address immediately:', shippingDetails); const response = await fetch( '/api/log-shipping-address', withMintQueueFetchInit(currentArtworkSlug, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ artworkSlug: currentArtworkSlug || 'failure-to-launch', paymentWalletAddress: window.walletData.paymentAddress, ordinalsWalletAddress: window.walletData.ordinalsAddress, shippingDetails: shippingDetails, sessionId: null, }), }) ); const result = await response.json(); if (result.success) { console.log('✅ Shipping address logged successfully:', result.logId); // Store the log ID for later completion window.shippingLogId = result.logId; } else { console.warn('Failed to log shipping address:', result.error); } } catch (error) { console.error('Error logging shipping address:', error); } } // Toggle featured status for artwork window.toggleFeatured = async function(slug) { try { // First check current featured status const response = await fetch(`/api/manage-featured`); const featuredData = await response.json(); if (!featuredData.success) { alert('Failed to check featured status'); return; } const currentlyFeatured = featuredData.featured.find(f => f.slug === slug); if (currentlyFeatured) { // Remove from featured if (confirm('Remove this artwork from the three homepage large Live Launches slots?')) { const removeResponse = await fetch('/api/manage-featured', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ artworkSlug: slug }) }); const result = await removeResponse.json(); if (result.success) { alert(result.message); loadArtworksForAdmin(); // Refresh the admin list loadArtworksList(); // Refresh public view and carousel } else { alert('Error: ' + result.error); } } } else { if ((featuredData.count || 0) >= 3) { alert('Already 3 homepage spotlight launches. Remove one before adding another.'); return; } if (confirm('Add this artwork to the homepage large Live Launches (max 3)?')) { const addResponse = await fetch('/api/manage-featured', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ artworkSlug: slug }) }); const result = await addResponse.json(); if (result.success) { alert(result.message); loadArtworksForAdmin(); // Refresh the admin list loadArtworksList(); // Refresh public view and carousel } else { alert('Error: ' + result.error); } } } } catch (error) { alert('Failed to toggle featured status: ' + error.message); } }; function adminHomeSetStatus(msg) { const el = document.getElementById('admin-home-layout-status'); if (el) el.textContent = msg || ''; } function adminHomeArtworkBySlug(slug) { return (window.__adminHomeLayoutArtworks || []).find((a) => a.slug === slug); } function adminHomeEligibleArtworks() { return getSortedLiveArtworksForHome(window.__adminHomeLayoutArtworks || []); } function renderAdminHomeSpotlightUi() { const listEl = document.getElementById('admin-home-spotlight-list'); const pickEl = document.getElementById('admin-home-spotlight-pick'); if (!listEl || !pickEl) return; const slugs = window.__adminHomeFeaturedSlugs || []; listEl.innerHTML = slugs.length ? slugs .map((slug, idx) => { const a = adminHomeArtworkBySlug(slug); const label = a ? `${a.title} — ${a.artist}` : slug; return `
    ${idx + 1} ${escapeHtml(label)} ${escapeHtml(slug)}
    `; }) .join('') : '

    No spotlights yet — add up to three below.

    '; const eligible = adminHomeEligibleArtworks(); const inSpot = new Set(slugs); const options = eligible.filter((a) => !inSpot.has(a.slug)); pickEl.innerHTML = '' + options .map( (a) => `` ) .join(''); pickEl.disabled = slugs.length >= 3 || options.length === 0; } function renderAdminHomeGridUi() { const listEl = document.getElementById('admin-home-grid-list'); const pickEl = document.getElementById('admin-home-grid-pick'); if (!listEl || !pickEl) return; const slugs = window.__adminHomeGridSlugs || []; listEl.innerHTML = slugs.length ? slugs .map((slug, idx) => { const a = adminHomeArtworkBySlug(slug); const label = a ? `${a.title} — ${a.artist}` : slug; return `
    ${idx + 1} ${escapeHtml(label)}
    `; }) .join('') : '

    No custom order — the home page uses automatic newest-first order. Add mints below, then Save.

    '; const eligible = adminHomeEligibleArtworks(); const inGrid = new Set(slugs); const options = eligible.filter((a) => !inGrid.has(a.slug)); pickEl.innerHTML = '' + options .map( (a) => `` ) .join(''); pickEl.disabled = slugs.length >= 12 || options.length === 0; } window.loadAdminHomePageLayout = async function () { adminHomeSetStatus('Loading…'); try { const [listRes, featRes] = await Promise.all([ fetch('/api/simple-list'), fetch('/api/manage-featured'), ]); const listData = await listRes.json(); const featData = await featRes.json(); if (!listData.success) { adminHomeSetStatus('Could not load artworks.'); return; } window.__adminHomeLayoutArtworks = listData.artworks || []; window.__adminHomeGridSlugs = Array.isArray(listData.homeHeroGridSlugs) ? [...listData.homeHeroGridSlugs] : []; const featured = Array.isArray(featData.featured) ? featData.featured : []; featured.sort((a, b) => { const oa = a.featuredOrder != null ? Number(a.featuredOrder) : 999; const ob = b.featuredOrder != null ? Number(b.featuredOrder) : 999; return oa - ob; }); window.__adminHomeFeaturedSlugs = featured.map((f) => f.slug); renderAdminHomeSpotlightUi(); renderAdminHomeGridUi(); adminHomeSetStatus(''); } catch (e) { adminHomeSetStatus('Error: ' + (e && e.message ? e.message : String(e))); } }; window.adminHomeAddSpotlight = async function () { const pickEl = document.getElementById('admin-home-spotlight-pick'); const slug = pickEl && pickEl.value; if (!slug) return; if ((window.__adminHomeFeaturedSlugs || []).length >= 3) { alert('Already three spotlight mints. Remove one first.'); return; } adminHomeSetStatus('Saving…'); try { const res = await fetch('/api/manage-featured', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ artworkSlug: slug }), }); const data = await res.json(); if (!data.success) { alert(data.error || 'Failed to add spotlight'); adminHomeSetStatus(''); return; } await loadAdminHomePageLayout(); if (typeof loadArtworksList === 'function') loadArtworksList(); if (typeof loadArtworksForAdmin === 'function') loadArtworksForAdmin(); } catch (e) { alert(e.message || String(e)); adminHomeSetStatus(''); } }; window.adminHomeRemoveSpotlightAt = async function (idx) { const slug = (window.__adminHomeFeaturedSlugs || [])[idx]; if (!slug) return; return adminHomeRemoveSpotlight(slug); }; window.adminHomeRemoveSpotlight = async function (slug) { if (!slug || !confirm('Remove this mint from the three large home cards?')) return; adminHomeSetStatus('Saving…'); try { const res = await fetch('/api/manage-featured', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ artworkSlug: slug }), }); const data = await res.json(); if (!data.success) { alert(data.error || 'Failed to remove'); adminHomeSetStatus(''); return; } await loadAdminHomePageLayout(); if (typeof loadArtworksList === 'function') loadArtworksList(); if (typeof loadArtworksForAdmin === 'function') loadArtworksForAdmin(); } catch (e) { alert(e.message || String(e)); adminHomeSetStatus(''); } }; window.adminHomeMoveSpotlight = async function (index, delta) { const arr = [...(window.__adminHomeFeaturedSlugs || [])]; const j = index + delta; if (j < 0 || j >= arr.length) return; [arr[index], arr[j]] = [arr[j], arr[index]]; adminHomeSetStatus('Saving order…'); try { const res = await fetch('/api/manage-featured', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderedSlugs: arr }), }); const data = await res.json(); if (!data.success) { alert(data.error || 'Could not reorder'); adminHomeSetStatus(''); return; } await loadAdminHomePageLayout(); if (typeof loadArtworksList === 'function') loadArtworksList(); } catch (e) { alert(e.message || String(e)); adminHomeSetStatus(''); } }; window.adminHomeAddGridRow = function () { const pickEl = document.getElementById('admin-home-grid-pick'); const slug = pickEl && pickEl.value; if (!slug) return; const slugs = [...(window.__adminHomeGridSlugs || [])]; if (slugs.length >= 12) { alert('Hero grid allows at most 12 pinned mints.'); return; } if (slugs.includes(slug)) return; slugs.push(slug); window.__adminHomeGridSlugs = slugs; if (pickEl) pickEl.value = ''; renderAdminHomeGridUi(); }; window.adminHomeMoveGridRow = function (index, delta) { const slugs = [...(window.__adminHomeGridSlugs || [])]; const j = index + delta; if (j < 0 || j >= slugs.length) return; [slugs[index], slugs[j]] = [slugs[j], slugs[index]]; window.__adminHomeGridSlugs = slugs; renderAdminHomeGridUi(); }; window.adminHomeRemoveGridRow = function (index) { const slugs = [...(window.__adminHomeGridSlugs || [])]; if (index < 0 || index >= slugs.length) return; slugs.splice(index, 1); window.__adminHomeGridSlugs = slugs; renderAdminHomeGridUi(); }; window.adminHomeSaveGrid = async function () { const slugs = window.__adminHomeGridSlugs || []; adminHomeSetStatus('Saving grid…'); try { const res = await fetch('/api/home-hero-grid', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slugs }), }); const data = await res.json(); if (!data.success) { alert(data.error || 'Save failed'); adminHomeSetStatus(''); return; } window.__adminHomeGridSlugs = data.slugs || slugs; adminHomeSetStatus('Saved.'); if (typeof loadArtworksList === 'function') await loadArtworksList(); setTimeout(() => adminHomeSetStatus(''), 2500); } catch (e) { alert(e.message || String(e)); adminHomeSetStatus(''); } }; window.adminHomeClearGrid = async function () { if (!confirm('Clear custom grid order? The home page will use automatic newest-first order.')) return; adminHomeSetStatus('Clearing…'); try { const res = await fetch('/api/home-hero-grid', { method: 'DELETE' }); const data = await res.json(); if (!data.success) { alert(data.error || 'Could not clear'); adminHomeSetStatus(''); return; } window.__adminHomeGridSlugs = []; renderAdminHomeGridUi(); adminHomeSetStatus('Using automatic order.'); if (typeof loadArtworksList === 'function') await loadArtworksList(); setTimeout(() => adminHomeSetStatus(''), 2500); } catch (e) { alert(e.message || String(e)); adminHomeSetStatus(''); } }; // Complete shipping log with transaction details async function completeShippingLog(transactionId, inscriptionId = null) { if (!window.walletData || !window.shippingLogId) { return; } try { console.log('Completing shipping log with transaction:', transactionId); const response = await fetch('/api/complete-shipping-log', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ artworkSlug: currentArtworkSlug || 'failure-to-launch', paymentWalletAddress: window.walletData.paymentAddress, ordinalsWalletAddress: window.walletData.ordinalsAddress, transactionId: transactionId, inscriptionId: inscriptionId }) }); const result = await response.json(); if (result.success) { console.log('✅ Shipping log completed successfully'); } else { console.warn('Failed to complete shipping log:', result.error); } } catch (error) { console.error('Error completing shipping log:', error); } } // Convert £60 to sats using current exchange rate (£1 for staging) async function getPhysicalPrintCostInSats() { try { // Get current BTC price in GBP from CoinGecko const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=gbp'); const data = await response.json(); const btcPriceInGBP = data.bitcoin.gbp; // Use £1 for staging, £60 for production const isStaging = window.location.hostname.includes('staging') || window.location.hostname.includes('localhost') || window.location.hostname.includes('127.0.0.1'); const gbpAmount = isStaging ? 1 : 60; const btcAmount = gbpAmount / btcPriceInGBP; const satsAmount = Math.round(btcAmount * 100000000); return satsAmount; } catch (error) { console.error('Failed to get BTC price, using fallback:', error); // Fallback: £1 in sats for staging, £60 for production const isStaging = window.location.hostname.includes('staging') || window.location.hostname.includes('localhost') || window.location.hostname.includes('127.0.0.1'); return isStaging ? 1000 : 60000; // Rough estimate fallback } } // Calculate inscription cost based on image size and fee rate async function calculateInscriptionCost(imageSizeBytes, feeRate = 2) { // Use the actual content size passed as parameter const contentSize = imageSizeBytes || 100; // Use passed size or fallback to 100 const overhead = 150; // witness data, script, etc. const totalSize = contentSize + overhead; const networkFee = Math.ceil(totalSize * feeRate) + MINT_NETWORK_BUFFER_SATS_PER_MINT; const marketplaceFee = MARKETPLACE_FEE_SATS_PER_MINT; // sats const artworkPrice = window.currentArtwork ? window.currentArtwork.price : 0; // Free artwork // Check if physical print is selected const physicalPrintCheckbox = document.getElementById('physicalPrintCheckbox'); const physicalPrintFee = (physicalPrintCheckbox && physicalPrintCheckbox.checked) ? await getPhysicalPrintCostInSats() : 0; return { networkFee, marketplaceFee, artworkPrice, physicalPrintFee, totalCost: networkFee + marketplaceFee + artworkPrice + physicalPrintFee, sizeBytes: contentSize, estimatedTxSize: totalSize, feeRate }; } // No WASM initialization needed // Check whitelist eligibility immediately after wallet connection function renderMintWalletStatusCard(whitelistData) { if (!whitelistData || !whitelistData.success) return ''; const is5il5nc5 = currentArtworkSlug === FIVE_IL5NC5_MINT_SLUG && !!whitelistData.phase; const isAdmin = adminMintBypassActive(); const details = whitelistData.whitelistDetails; if (is5il5nc5) { const phaseName = whitelistData.phaseLabel || whitelistData.phase || 'Phase'; const priceLabel = whitelistData.priceSats === 0 ? 'FREE' : (whitelistData.priceSats != null ? `${whitelistData.priceSats.toLocaleString()} sats` : ''); if (!whitelistData.onWhitelist) { if (!isAdmin) return ''; return `

    🔧 ADMIN PREVIEW

    Wallet not on the ${phaseName} list.

    Current phase: ${phaseName}${priceLabel ? ` · ${priceLabel}` : ''} — you can still mint for testing.

    `; } if (!details) return ''; const { maxMints, mintsUsed, remainingMints } = details; return `

    ✅ ${phaseName.toUpperCase()}

    Your allocation: ${maxMints} mint${maxMints === 1 ? '' : 's'}
    Already minted: ${mintsUsed}
    Remaining: ${remainingMints} mint${remainingMints === 1 ? '' : 's'}
    `; } if (!details) return ''; const { maxMints, mintsUsed, remainingMints, whitelistMinted } = details; const isPostWhitelist = whitelistData.postWhitelistLimit === true; if (!isPostWhitelist && (!whitelistData.onWhitelist || maxMints === 0)) return ''; if (!whitelistData.canMint && !isAdmin) return ''; return `

    ${isPostWhitelist ? '🔓 PUBLIC MINTING (+2 PER WALLET)' : '✅ WHITELISTED'}

    ${isPostWhitelist && whitelistMinted > 0 ? `
    Whitelist: ${whitelistMinted} minted ✅
    ` : ''}
    ${isPostWhitelist ? 'Additional Limit' : 'Your Allocation'}: ${maxMints} mints
    ${isPostWhitelist ? 'Public Minted' : 'Already Minted'}: ${mintsUsed}
    Remaining: ${remainingMints} mints
    ${isPostWhitelist ? `
    Whitelist ended - ${maxMints} additional mints available${whitelistMinted > 0 ? ' (on top of your ' + whitelistMinted + ' whitelist mints)' : ''}
    ` : ''}
    `; } async function checkWhitelistEligibility() { if (adminMintBypassActive()) { return true; } if (!walletData || !walletData.paymentAddress) { return; // No wallet connected } // Get artwork slug from URL or global variable let artworkSlug = currentArtworkSlug; if (!artworkSlug) { const hash = window.location.hash; if (hash && hash.startsWith('#mint/')) { artworkSlug = hash.replace('#mint/', '').split('?')[0]; } else { const pm = window.location.pathname.match(/^\/mint\/([^/]+)\/?$/); if (pm) artworkSlug = decodeURIComponent(pm[1]).split('?')[0]; } } if (!artworkSlug) { return; // No artwork selected } try { const whitelistResponse = await fetch(`/api/check-whitelist?artwork=${artworkSlug}&wallet=${walletData.ordinalsAddress}`); const whitelistData = await whitelistResponse.json(); if (whitelistData.success && !whitelistData.canMint) { // Show whitelist error immediately let errorMessage = whitelistData.message; if (artworkSlug === FIVE_IL5NC5_MINT_SLUG) { if (whitelistData.phase === 'loyalty' || whitelistData.phase === 'gtd') { errorMessage = whitelistData.message || 'Wallet not eligible for the current phase'; } else if (whitelistData.phase === 'upcoming') { errorMessage = 'Mint has not started yet'; } } else if (whitelistData.whitelistActive && !whitelistData.onWhitelist) { errorMessage += '\n\nWhitelist period is currently active. Public minting will open after the whitelist period ends.'; if (whitelistData.whitelistEndTime) { const endDate = new Date(whitelistData.whitelistEndTime); errorMessage += `\nPublic minting opens: ${endDate.toLocaleDateString()} at ${endDate.toLocaleTimeString()}`; } } // Show appropriate error message based on whitelist status const walletStatus = document.getElementById('walletStatus'); if (walletStatus) { let displayMessage; if (whitelistData.onWhitelist && whitelistData.whitelistDetails) { // User is on whitelist but has used all mints const { maxMints, mintsUsed } = whitelistData.whitelistDetails; displayMessage = `You have used ${mintsUsed}/${maxMints} whitelist mints, you are unable to mint any more`; } else { // User is not on whitelist displayMessage = artworkSlug === FIVE_IL5NC5_MINT_SLUG ? (whitelistData.message || 'Wallet not eligible for the current phase') : 'Wallet address not on whitelist'; } walletStatus.innerHTML = `

    Minting Not Available

    ${displayMessage}

    `; } // Keep wallet info visible but disable connect button const connectBtn = document.getElementById('connectBtn'); if (connectBtn) { connectBtn.innerHTML = 'Wallet Connected ❌'; connectBtn.disabled = true; } return false; // Not eligible } // Update quantity selector max and display whitelist info const quantityInput = document.getElementById('mint-quantity'); const quantitySelector = document.getElementById('quantity-selector'); if (quantityInput && quantitySelector) { if (whitelistData.success && whitelistData.phase && artworkSlug === FIVE_IL5NC5_MINT_SLUG && whitelistData.whitelistDetails) { const { remainingMints, mintsUsed, maxMints } = whitelistData.whitelistDetails; const cap = Math.max(0, remainingMints); quantityInput.setAttribute('max', Math.max(1, cap)); quantityInput.value = Math.min(parseInt(quantityInput.value, 10) || 1, Math.max(1, cap)); const existingInfo = document.getElementById('whitelist-info'); if (existingInfo) existingInfo.remove(); if (whitelistData.canMint) { const whitelistInfo = document.createElement('div'); whitelistInfo.id = 'whitelist-info'; whitelistInfo.style.cssText = 'margin-top: 10px; padding: 10px; background: #000; border: 1px solid #fff; font-size: 10px; color: #fff; text-align: left;'; whitelistInfo.innerHTML = `
    ${whitelistData.phaseLabel || 'Phase'} mint:
    Minted: ${mintsUsed} / ${maxMints}
    Remaining: ${remainingMints} mints
    `; quantityInput.parentNode.insertBefore(whitelistInfo, quantityInput.nextSibling); } } else if (whitelistData.success && whitelistData.phase === 'public' && artworkSlug === FIVE_IL5NC5_MINT_SLUG) { quantityInput.removeAttribute('max'); const existingInfo = document.getElementById('whitelist-info'); if (existingInfo) existingInfo.remove(); } else if (whitelistData.success && whitelistData.postWhitelistLimit && whitelistData.whitelistDetails) { // 2-per-wallet collection (e.g. J.O.E): cap quantity by remaining post-whitelist mints const { remainingMints, mintsUsed, maxMints, whitelistMinted } = whitelistData.whitelistDetails; const cap = Math.max(0, remainingMints); quantityInput.setAttribute('max', Math.min(2, cap)); quantityInput.value = Math.min(parseInt(quantityInput.value) || 1, 2, cap); console.log(`Set quantity max to ${Math.min(2, cap)} (2 per wallet, ${remainingMints} remaining)`); } else if (whitelistData.success && whitelistData.whitelistActive && whitelistData.onWhitelist) { console.log('Whitelist mint:', whitelistData.whitelistDetails); // Set quantity selector max to whitelist remaining mints if (whitelistData.whitelistDetails && whitelistData.whitelistDetails.remainingMints !== undefined) { const { maxMints, mintsUsed, remainingMints } = whitelistData.whitelistDetails; quantityInput.setAttribute('max', remainingMints); quantityInput.value = Math.min(quantityInput.value || 1, remainingMints); console.log(`Set quantity selector max to ${remainingMints} (whitelist remaining mints)`); // Display whitelist info const whitelistInfo = document.createElement('div'); whitelistInfo.id = 'whitelist-info'; whitelistInfo.style.cssText = 'margin-top: 10px; padding: 10px; background: #000; border: 1px solid #fff; font-size: 10px; color: #fff; text-align: left;'; whitelistInfo.innerHTML = `
    Your Whitelist Status:
    Minted: ${mintsUsed} / ${maxMints}
    Remaining: ${remainingMints} mints
    `; // Remove existing whitelist info if present const existingInfo = document.getElementById('whitelist-info'); if (existingInfo) { existingInfo.remove(); } // Add whitelist info after the quantity input quantityInput.parentNode.insertBefore(whitelistInfo, quantityInput.nextSibling); } } } return true; // Eligible to mint } catch (error) { console.warn('Failed to check whitelist status:', error); // Continue with normal minting if whitelist check fails return true; } } // Wallet chooser modal (Connect -> Xverse or Unisat). Unisat only for inscribe-on-demand and pre-inscribed; auctions: Xverse only. function unisatAllowedForMint() { const a = window.currentArtwork; if (!a || !a.mintType) return false; return a.mintType === 'inscribe_on_demand' || a.mintType === 'parent_child_prints' || a.mintType === 'pre_inscribed' || a.mintType === 'numerical' || a.mintType === 'pre_inscribed_v2'; } window.showWalletChooser = function(context) { window.walletConnectFor = context || 'mint'; const modal = document.getElementById('walletChooserModal'); const unisatBtn = document.getElementById('wallet-chooser-unisat-btn'); if (unisatBtn) { if (context === 'auction') unisatBtn.style.display = 'none'; else if (context === 'mint') unisatBtn.style.display = unisatAllowedForMint() ? 'block' : 'none'; else unisatBtn.style.display = 'block'; } if (modal) modal.style.display = 'flex'; }; window.closeWalletChooser = function() { const modal = document.getElementById('walletChooserModal'); if (modal) modal.style.display = 'none'; }; window.chooseWalletAndConnect = async function(provider) { closeWalletChooser(); const ctx = window.walletConnectFor || 'mint'; if (ctx === 'auction') { if (provider === 'xverse') await connectWalletForAuction(); else if (provider === 'unisat') return; // Unisat not supported for auctions } else if (ctx === 'mint') { if (provider === 'xverse') await connectXverseWallet(); else if (provider === 'unisat') { if (!unisatAllowedForMint()) return; // Unisat only for inscribe-on-demand and pre-inscribed await connectUnisatWallet(); } } else { if (provider === 'xverse') await connectXverseWallet(); else if (provider === 'unisat') await connectUnisatWallet(); } }; // Connect to Wallet (using same approach as working files) window.connectXverseWallet = async function() { try { document.getElementById('connectBtn').innerHTML = ' Connecting...'; document.getElementById('connectBtn').disabled = true; window.walletProvider = 'xverse'; // Use sats-connect library (same as working files) const response = await request('wallet_connect', null); if (response.status !== 'success') { throw new Error('Wallet connection failed.'); } // Find payment and ordinals addresses const paymentAddressInfo = response.result.addresses.find(a => a.purpose === AddressPurpose.Payment); const ordinalsAddressInfo = response.result.addresses.find(a => a.purpose === AddressPurpose.Ordinals); if (!paymentAddressInfo) { throw new Error('No Taproot payment address found in wallet.'); } // Store wallet data (remove 0x prefix from pubkey like working files) walletData = { paymentAddress: paymentAddressInfo.address, ordinalsAddress: ordinalsAddressInfo ? ordinalsAddressInfo.address : paymentAddressInfo.address, publicKey: paymentAddressInfo.publicKey.slice(2) // Remove 0x prefix }; window.walletData = walletData; // Store globally for handleNumericalMint walletConnected = true; // Fetch linked wallets from Lunatics API try { await fetch(`/api/get-linked-wallets?wallet=${walletData.paymentAddress}`); } catch (error) { console.warn('Failed to fetch linked wallets:', error); } // Check if this wallet qualifies for free mints window.walletHasFreeMint = false; window.walletMintCount = 0; window.walletMaxFreeMints = 0; if (currentArtworkSlug && window.currentArtwork && window.currentArtwork.firstMintFree) { try { const freeMintCheck = await fetch(`/api/check-wallet-mints?slug=${currentArtworkSlug}&wallet=${walletData.ordinalsAddress}`); const freeMintData = await freeMintCheck.json(); if (freeMintData.success) { window.walletHasFreeMint = freeMintData.hasFreeMint; window.walletMintCount = freeMintData.mintCount; window.walletMaxFreeMints = freeMintData.maxFreeMints || 1; console.log('Free mint check:', { hasFreeMint: window.walletHasFreeMint, mintCount: window.walletMintCount, maxFreeMints: window.walletMaxFreeMints }); } } catch (error) { console.warn('Failed to check free mint status:', error); } } document.getElementById('walletAddress').innerHTML = ` Payment: ${walletData.paymentAddress}
    ${ordinalsAddressInfo ? 'Ordinals: ' + walletData.ordinalsAddress : ''} `; document.getElementById('walletInfo').style.display = 'block'; document.getElementById('connectBtn').innerHTML = 'Wallet Connected ✅'; document.getElementById('connectBtn').disabled = true; // Check whitelist eligibility immediately after wallet connection const isWhitelisted = await checkWhitelistEligibility(); // Show whitelist status prominently in wallet status area const walletStatus = document.getElementById('walletStatus'); if (walletStatus && isWhitelisted !== false) { // Check whitelist again to get details for display try { const whitelistResponse = await fetch(`/api/check-whitelist?artwork=${currentArtworkSlug}&wallet=${walletData.ordinalsAddress}`); const whitelistData = await whitelistResponse.json(); if (whitelistData.success) { const statusCard = renderMintWalletStatusCard(whitelistData); if (statusCard) walletStatus.innerHTML = statusCard; } } catch (err) { console.error('Failed to fetch whitelist details:', err); } } // Show/hide mint details based on whitelist status const isOnDemand = window.currentArtwork && (window.currentArtwork.mintType === 'inscribe_on_demand' || window.currentArtwork.mintType === 'parent_child_prints'); const lowFeeDisclaimerX = document.getElementById('low-fee-disclaimer'); if (window.mintPageUpcoming) { document.getElementById('feeSelectionStep').style.display = 'none'; document.getElementById('costDisplayStep').style.display = 'none'; document.getElementById('mintStep').style.display = 'block'; setInscribeButtonMintingSoon(document.getElementById('inscribeBtn')); if (lowFeeDisclaimerX) lowFeeDisclaimerX.style.display = 'none'; } else if (isWhitelisted) { document.getElementById('feeSelectionStep').style.display = isOnDemand ? 'none' : 'block'; document.getElementById('costDisplayStep').style.display = 'block'; if (lowFeeDisclaimerX) { lowFeeDisclaimerX.style.display = isOnDemand ? 'block' : 'none'; } await updateCostCalculation(); await loadTestImageData(); document.getElementById('mintStep').style.display = 'block'; } else { document.getElementById('feeSelectionStep').style.display = 'none'; document.getElementById('costDisplayStep').style.display = 'none'; document.getElementById('mintStep').style.display = 'none'; } } catch (error) { document.getElementById('walletStatus').innerHTML += `
    Error connecting wallet: ${error.message}
    `; document.getElementById('connectBtn').innerHTML = 'Connect'; document.getElementById('connectBtn').disabled = false; } }; // Connect with Unisat wallet window.connectUnisatWallet = async function() { if (typeof window.unisat === 'undefined') { alert('UniSat wallet not found. Please install the UniSat browser extension.'); return; } try { document.getElementById('connectBtn').innerHTML = ' Connecting...'; document.getElementById('connectBtn').disabled = true; window.walletProvider = 'unisat'; const unisat = window.unisat; await unisat.requestAccounts(); const accounts = await unisat.getAccounts(); if (!accounts || accounts.length === 0) { throw new Error('No accounts returned from UniSat.'); } const address = accounts[0]; if (!address || typeof address !== 'string') { throw new Error('Invalid address from UniSat.'); } // Ensure mainnet try { const network = await unisat.getNetwork(); if (network && network !== 'livenet') { await unisat.switchNetwork('livenet'); } } catch (e) { console.warn('UniSat network check/switch:', e); } let pubKey = ''; try { pubKey = (await unisat.getPublicKey()) || ''; if (pubKey && pubKey.startsWith('0x')) pubKey = pubKey.slice(2); } catch (e) { console.warn('UniSat getPublicKey:', e); } walletData = { paymentAddress: address, ordinalsAddress: address, publicKey: pubKey }; window.walletData = walletData; walletConnected = true; try { await fetch(`/api/get-linked-wallets?wallet=${walletData.paymentAddress}`); } catch (error) { console.warn('Failed to fetch linked wallets:', error); } window.walletHasFreeMint = false; window.walletMintCount = 0; window.walletMaxFreeMints = 0; if (currentArtworkSlug && window.currentArtwork && window.currentArtwork.firstMintFree) { try { const freeMintCheck = await fetch(`/api/check-wallet-mints?slug=${currentArtworkSlug}&wallet=${walletData.ordinalsAddress}`); const freeMintData = await freeMintCheck.json(); if (freeMintData.success) { window.walletHasFreeMint = freeMintData.hasFreeMint; window.walletMintCount = freeMintData.mintCount; window.walletMaxFreeMints = freeMintData.maxFreeMints || 1; } } catch (error) { console.warn('Failed to check free mint status:', error); } } document.getElementById('walletAddress').innerHTML = `Payment: ${walletData.paymentAddress}
    Ordinals: ${walletData.ordinalsAddress}`; document.getElementById('walletInfo').style.display = 'block'; document.getElementById('connectBtn').innerHTML = 'Wallet Connected ✅'; document.getElementById('connectBtn').disabled = true; const isWhitelisted = await checkWhitelistEligibility(); const walletStatus = document.getElementById('walletStatus'); if (walletStatus && isWhitelisted !== false) { try { const whitelistResponse = await fetch(`/api/check-whitelist?artwork=${currentArtworkSlug}&wallet=${walletData.ordinalsAddress}`); const whitelistData = await whitelistResponse.json(); if (whitelistData.success) { const statusCard = renderMintWalletStatusCard(whitelistData); if (statusCard) walletStatus.innerHTML = statusCard; } } catch (err) { console.error('Failed to fetch whitelist details:', err); } } const isOnDemand = window.currentArtwork && (window.currentArtwork.mintType === 'inscribe_on_demand' || window.currentArtwork.mintType === 'parent_child_prints'); const lowFeeDisclaimerU = document.getElementById('low-fee-disclaimer'); if (window.mintPageUpcoming) { document.getElementById('feeSelectionStep').style.display = 'none'; document.getElementById('costDisplayStep').style.display = 'none'; document.getElementById('mintStep').style.display = 'block'; setInscribeButtonMintingSoon(document.getElementById('inscribeBtn')); if (lowFeeDisclaimerU) lowFeeDisclaimerU.style.display = 'none'; } else if (isWhitelisted) { document.getElementById('feeSelectionStep').style.display = isOnDemand ? 'none' : 'block'; document.getElementById('costDisplayStep').style.display = 'block'; if (lowFeeDisclaimerU) lowFeeDisclaimerU.style.display = isOnDemand ? 'block' : 'none'; await updateCostCalculation(); await loadTestImageData(); document.getElementById('mintStep').style.display = 'block'; } else { document.getElementById('feeSelectionStep').style.display = 'none'; document.getElementById('costDisplayStep').style.display = 'none'; document.getElementById('mintStep').style.display = 'none'; } } catch (error) { document.getElementById('walletStatus').innerHTML += `
    Error connecting wallet: ${error.message}
    `; document.getElementById('connectBtn').innerHTML = 'Connect'; document.getElementById('connectBtn').disabled = false; } }; // Send payment via connected wallet (Xverse or Unisat) window.sendPaymentFromWallet = async function(options) { const { address, amount } = options; const feeRate = options.feeRate != null ? options.feeRate : (typeof window.getSelectedFeeRate === 'function' ? window.getSelectedFeeRate() : 5); if (window.walletProvider === 'unisat' && typeof window.unisat !== 'undefined') { try { const txid = await window.unisat.sendBitcoin(address, amount, { feeRate: feeRate || 5 }); const id = typeof txid === 'string' ? txid : (txid && txid.txid) ? txid.txid : null; if (!id) throw new Error('No txid from UniSat'); return { status: 'success', result: { txid: id } }; } catch (e) { if (e && (e.message === 'User rejected the request' || e.code === 4001)) { return { status: 'canceled' }; } return { status: 'error', error: e && e.message ? e.message : String(e) }; } } return await request('sendTransfer', { recipients: [{ address, amount }] }); }; // Load Mint on BTC Image Data (automatically called when wallet connects) async function loadTestImageData() { try { // Load the mintonbtc.webp image const response = await fetch('./mintonbtc.webp'); if (!response.ok) { throw new Error(`Failed to load mintonbtc.webp: ${response.status}`); } const imageData = await response.arrayBuffer(); generatedImageData = new Uint8Array(imageData); // Update cost calculation with current fee selection updateCostCalculation(); } catch (error) { throw error; } } // Add global event listeners to catch any wallet events window.addEventListener('message', (event) => { }); // Listen for any focus/blur events that might indicate popup closing window.addEventListener('focus', () => { }); // Handle numerical minting window.handleNumericalMint = async function() { try { const artwork = window.currentArtwork; const walletData = window.walletData; // Get quantity from UI (default to 1) const quantityInput = document.getElementById('mint-quantity'); const quantity = quantityInput ? parseInt(quantityInput.value) || 1 : 1; // SECURITY: Validate quantity against whitelist/wallet limits before minting if (walletData && walletData.ordinalsAddress) { const whitelistCheckResponse = await fetch(`/api/check-whitelist?artwork=${currentArtworkSlug}&wallet=${walletData.ordinalsAddress}`); const whitelistCheck = await whitelistCheckResponse.json(); // Check both during whitelist and post-whitelist periods if (whitelistCheck.success && whitelistCheck.whitelistDetails) { const remainingMints = whitelistCheck.whitelistDetails?.remainingMints || 0; if (quantity > remainingMints) { if (whitelistCheck.postWhitelistLimit) { // Post-whitelist: 2 ADDITIONAL mints const wlMinted = whitelistCheck.whitelistDetails.whitelistMinted || 0; const publicMinted = whitelistCheck.whitelistDetails.mintsUsed || 0; throw new Error(`Maximum 2 additional mints after whitelist. You minted ${wlMinted} during whitelist + ${publicMinted} public = ${remainingMints} remaining.`); } else { // During whitelist throw new Error(`You can only mint ${remainingMints} more item(s) based on your whitelist allocation`); } } } } document.getElementById('inscribeBtn').innerHTML = ` Reserving ${quantity} Mint(s)...`; // Get available numerical mint numbers const availableResponse = await fetch(`/api/get-available-numerical?slug=${currentArtworkSlug}&quantity=${quantity}`, { headers: mintQueueFetchHeaders(currentArtworkSlug), }); const availableData = await availableResponse.json(); if (!availableData.success) { if (availableData.soldOut) { throw new Error('This collection has sold out!'); } if (availableData.available) { throw new Error(`Only ${availableData.available} mint(s) available`); } throw new Error(availableData.error || 'No available mints'); } const mintNumbers = availableData.mintNumbers; // Calculate total amount - fee charged PER MINT const marketplaceFee = MARKETPLACE_FEE_SATS_PER_MINT; // numerical mints // Check if physical print is selected const physicalPrintCheckbox = document.getElementById('physicalPrintCheckbox'); const isPhysicalPrintChecked = physicalPrintCheckbox && physicalPrintCheckbox.checked; // Validate shipping details if physical print is selected if (isPhysicalPrintChecked) { const name = document.getElementById('physicalPrintName')?.value?.trim(); const address = document.getElementById('physicalPrintAddress')?.value?.trim(); const city = document.getElementById('physicalPrintCity')?.value?.trim(); const state = document.getElementById('physicalPrintState')?.value?.trim(); const zip = document.getElementById('physicalPrintZip')?.value?.trim(); const country = document.getElementById('physicalPrintCountry')?.value?.trim(); const email = document.getElementById('physicalPrintEmail')?.value?.trim(); const phone = document.getElementById('physicalPrintPhone')?.value?.trim(); if (!name || !address || !city || !state || !zip || !country || !email || !phone) { throw new Error('Please fill in all shipping details for physical print'); } } // Get physical print fee (dynamic conversion) let physicalPrintFee = 0; if (isPhysicalPrintChecked) { physicalPrintFee = await getPhysicalPrintCostInSats(); if (!physicalPrintFee || physicalPrintFee <= 0) { physicalPrintFee = 60000; } } let pricePerMint = artwork.price + marketplaceFee + MINT_NETWORK_BUFFER_SATS_PER_MINT; if (isPhysicalPrintChecked) { pricePerMint += physicalPrintFee; } const totalAmount = pricePerMint * quantity; // Show payment prompt document.getElementById('inscribeBtn').innerHTML = ' Approve Payment...'; let costBreakdown = `${quantity} × ${artwork.price.toLocaleString()} sats (artwork)`; costBreakdown += ` + ${quantity} × ${marketplaceFee.toLocaleString()} sats (marketplace fee)`; costBreakdown += ` + ${quantity} × ${MINT_NETWORK_BUFFER_SATS_PER_MINT.toLocaleString()} sats (network)`; if (isPhysicalPrintChecked) { costBreakdown += ` + ${quantity} × ${physicalPrintFee.toLocaleString()} sats (physical print)`; } document.getElementById('inscriptionResult').innerHTML = `

    Approve Payment in Wallet

    Quantity: ${quantity} mint(s)
    Mint Numbers: #${mintNumbers.join(', #')}
    Amount: ${totalAmount.toLocaleString()} sats
    ${costBreakdown}

    Payment confirmation will record your mints.

    `; // Request payment: UniSat via sendPaymentFromWallet, Xverse via sats-connect console.log('Requesting sendTransfer for', totalAmount, 'sats'); let transferResponse; if (window.walletProvider === 'unisat' && typeof window.unisat !== 'undefined') { transferResponse = await sendPaymentFromWallet({ address: 'bc1qcefl3uc75wh39wcwqs4ca9xw8m7jhc35t3kv43', amount: totalAmount }); } else { transferResponse = await request('sendTransfer', { recipients: [{ address: 'bc1qcefl3uc75wh39wcwqs4ca9xw8m7jhc35t3kv43', // Marketplace wallet – all mint payments go here amount: totalAmount }] }); } console.log('Payment response:', transferResponse); if (transferResponse.status !== 'success') { throw new Error(transferResponse.status === 'canceled' ? 'Payment cancelled' : (transferResponse.error || 'Payment failed')); } const paymentTxId = transferResponse.result?.txid; if (!paymentTxId) { throw new Error('No transaction ID received from wallet'); } console.log('Numerical mint payment txid:', paymentTxId); // Record the mint document.getElementById('inscribeBtn').innerHTML = ' Recording Mint...'; const mintResponse = await fetch( '/api/mint-numerical', withMintQueueFetchInit(currentArtworkSlug, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ artworkSlug: currentArtworkSlug, selectedMintNumbers: mintNumbers, paymentTxId: paymentTxId, ordinalsWalletAddress: walletData.ordinalsAddress, paymentWalletAddress: walletData.paymentAddress, }), }) ); const mintData = await mintResponse.json(); if (!mintData.success) { throw new Error(mintData.error || 'Failed to record mint'); } // Success! document.getElementById('inscribeBtn').innerHTML = 'Mint Successful!'; document.getElementById('inscriptionResult').innerHTML = `

    Mint Successful

    You have successfully minted ${quantity} item(s).

    Transaction: ${paymentTxId}

    `; // Refresh mint stats fetchMintStats(); // Refresh whitelist status for numerical mints if (walletData && artwork.mintType === 'numerical') { checkWhitelistEligibility(currentArtworkSlug, walletData.paymentAddress); } } catch (error) { console.error('Numerical mint error:', error); document.getElementById('inscriptionResult').innerHTML = `

    Mint Failed: ${error.message}

    `; document.getElementById('inscribeBtn').innerHTML = 'Mint Failed'; document.getElementById('inscribeBtn').disabled = false; } }; /** Parse Pages Function JSON; avoid SyntaxError when /api/* returns HTML (404 SPA shell, wrong host, undeployed functions). */ async function parseApiJsonResponse(response, contextLabel) { const text = await response.text(); const trimmed = text.trim(); if (!trimmed || trimmed.startsWith('<')) { throw new Error( contextLabel + ': server returned HTML instead of JSON (HTTP ' + response.status + '). ' + 'The /api route may not be deployed (Cloudflare Pages needs the functions/ folder), or this preview domain does not run the same project as production.' ); } try { return JSON.parse(text); } catch (e) { throw new Error(contextLabel + ': response is not valid JSON (HTTP ' + response.status + ').'); } } // Handle pre-inscribed v2 minting (with sendTransfer) window.handlePreInscribedV2Mint = async function() { try { const artwork = window.currentArtwork; const walletData = window.walletData; // Get quantity from UI (default to 1) const quantityInput = document.getElementById('mint-quantity'); const quantity = quantityInput ? parseInt(quantityInput.value) || 1 : 1; // SECURITY: Validate quantity against whitelist/wallet limits before minting if (walletData && walletData.ordinalsAddress) { const whitelistCheckResponse = await fetch(`/api/check-whitelist?artwork=${currentArtworkSlug}&wallet=${walletData.ordinalsAddress}`); const whitelistCheck = await parseApiJsonResponse(whitelistCheckResponse, 'check-whitelist'); // Check both during whitelist and post-whitelist periods if (whitelistCheck.success && whitelistCheck.whitelistDetails) { const remainingMints = whitelistCheck.whitelistDetails?.remainingMints || 0; if (quantity > remainingMints) { if (whitelistCheck.postWhitelistLimit) { // Post-whitelist: 2 ADDITIONAL mints const wlMinted = whitelistCheck.whitelistDetails.whitelistMinted || 0; const publicMinted = whitelistCheck.whitelistDetails.mintsUsed || 0; throw new Error(`Maximum 2 additional mints after whitelist. You minted ${wlMinted} during whitelist + ${publicMinted} public = ${remainingMints} remaining.`); } else { // During whitelist (remaining may be 0 due to supply/tail rules, not mints_used — show API message) const hint = whitelistCheck.message ? ` ${whitelistCheck.message}` : ''; throw new Error(`You can only mint ${remainingMints} more item(s). You have ${whitelistCheck.whitelistDetails.mintsUsed} of ${whitelistCheck.whitelistDetails.maxMints} whitelist spot(s) used.${hint}`); } } } } document.getElementById('inscribeBtn').innerHTML = ` Reserving ${quantity} Inscription(s)...`; // Reserve only when user clicks Mint Now: clear any old cache and always fetch fresh if (window.cachedReservedInscriptions && window.cachedReservedInscriptions.slug === currentArtworkSlug) { window.cachedReservedInscriptions = null; } const availableResponse = await fetch(`/api/get-available-inscription?slug=${currentArtworkSlug}&quantity=${quantity}`, { headers: mintQueueFetchHeaders(currentArtworkSlug), }); const availableData = await parseApiJsonResponse(availableResponse, 'get-available-inscription'); if (!availableData.success) { if (availableData.soldOut) { throw new Error('This collection has sold out!'); } if (availableData.available) { throw new Error(`Only ${availableData.available} inscription(s) available`); } throw new Error(availableData.error || 'No available inscriptions'); } const reservedInscriptions = availableData.inscriptions; console.log('✅ Reserved', reservedInscriptions.length, 'inscription(s) on Mint Now'); const assetIds = reservedInscriptions.map(i => i.assetId); const isOnDemandCollection = artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints'; const isPreInscribedOrNumerical = artwork.mintType === 'pre_inscribed' || artwork.mintType === 'numerical' || artwork.mintType === 'pre_inscribed_v2'; // Pre-inscribed/numerical: per-mint network buffer (same constant as other mint flows). On-demand uses full inscription estimate. let totalNetworkFee = 0; const networkFeesPerItem = []; if (isOnDemandCollection) { const standardFileSize = availableData.collectionMaxFileSize || null; for (const inscription of reservedInscriptions) { const fileSizeBytes = standardFileSize ?? (inscription.fileSize || 1000); const networkFee = computeInscribeOnDemandNetworkFeePerMint( fileSizeBytes, inscription.contentType || 'text/html' ); networkFeesPerItem.push({ assetId: inscription.assetId, fileSize: fileSizeBytes, networkFee: networkFee }); totalNetworkFee += networkFee; } } else if (isPreInscribedOrNumerical) { totalNetworkFee = MINT_NETWORK_BUFFER_SATS_PER_MINT * quantity; } // Calculate total amount - fee charged PER MINT const marketplaceFee = MARKETPLACE_FEE_SATS_PER_MINT; // marketplace fee per mint const totalMarketplaceFee = marketplaceFee * quantity; // Check if physical print is selected const physicalPrintCheckbox = document.getElementById('physicalPrintCheckbox'); const isPhysicalPrintChecked = physicalPrintCheckbox && physicalPrintCheckbox.checked; // Validate shipping details if physical print is selected if (isPhysicalPrintChecked) { const name = document.getElementById('physicalPrintName')?.value?.trim(); const address = document.getElementById('physicalPrintAddress')?.value?.trim(); const city = document.getElementById('physicalPrintCity')?.value?.trim(); const state = document.getElementById('physicalPrintState')?.value?.trim(); const zip = document.getElementById('physicalPrintZip')?.value?.trim(); const country = document.getElementById('physicalPrintCountry')?.value?.trim(); const email = document.getElementById('physicalPrintEmail')?.value?.trim(); const phone = document.getElementById('physicalPrintPhone')?.value?.trim(); if (!name || !address || !city || !state || !zip || !country || !email || !phone) { throw new Error('Please fill in all shipping details for physical print'); } } // Get physical print fee (dynamic conversion) let physicalPrintFee = 0; if (isPhysicalPrintChecked) { physicalPrintFee = await getPhysicalPrintCostInSats(); // Ensure we always have a valid fee (fallback to 60,000 sats) if (!physicalPrintFee || physicalPrintFee <= 0) { physicalPrintFee = 60000; } } // Apply free mint discount if eligible let totalArtworkPrice = artwork.price * quantity; let freeMintsApplied = 0; if (window.walletHasFreeMint && artwork.price > 0 && quantity >= 1) { // Calculate how many free mints remaining const remainingFreeMints = Math.max(0, (window.walletMaxFreeMints || 1) - (window.walletMintCount || 0)); freeMintsApplied = Math.min(quantity, remainingFreeMints); const paidItems = quantity - freeMintsApplied; totalArtworkPrice = artwork.price * paidItems; console.log('🎁 Free mints applied:', { freeMintsApplied, paidItems, originalPrice: artwork.price * quantity, discountedPrice: totalArtworkPrice }); } const totalPhysicalPrintFee = isPhysicalPrintChecked ? physicalPrintFee * quantity : 0; const totalAmount = totalNetworkFee + totalMarketplaceFee + totalArtworkPrice + totalPhysicalPrintFee; // Show payment prompt document.getElementById('inscribeBtn').innerHTML = ' Approve Payment...'; // Build cost breakdown let costBreakdown = `Network: ${totalNetworkFee.toLocaleString()} sats`; costBreakdown += ` + Marketplace: ${totalMarketplaceFee.toLocaleString()} sats`; costBreakdown += ` + Artwork: ${totalArtworkPrice.toLocaleString()} sats${freeMintsApplied > 0 ? ` (${freeMintsApplied} free)` : ''}`; if (isPhysicalPrintChecked) { costBreakdown += ` + Physical Print: ${totalPhysicalPrintFee.toLocaleString()} sats`; } costBreakdown += ` = Total: ${totalAmount.toLocaleString()} sats`; document.getElementById('inscriptionResult').innerHTML = `

    Approve Payment in Wallet

    Quantity: ${quantity} inscription(s)
    Amount: ${totalAmount.toLocaleString()} sats
    ${costBreakdown}

    ${quantity > 1 ? `${quantity} inscriptions` : 'The inscription'} will be sent to your ordinals address after payment confirmation.

    `; const mintPaymentAddress = isOnDemandCollection ? INSCRIBE_ON_DEMAND_PAYMENT_ADDRESS : MARKETPLACE_PAYMENT_ADDRESS; // Request payment: UniSat via sendPaymentFromWallet, Xverse via sats-connect console.log('Requesting sendTransfer for', totalAmount, 'sats to', mintPaymentAddress); let transferResponse; if (window.walletProvider === 'unisat' && typeof window.unisat !== 'undefined') { transferResponse = await sendPaymentFromWallet({ address: mintPaymentAddress, amount: totalAmount }); } else { transferResponse = await request('sendTransfer', { recipients: [{ address: mintPaymentAddress, amount: totalAmount }] }); } console.log('Payment response:', transferResponse); if (transferResponse.status !== 'success') { throw new Error(transferResponse.status === 'canceled' ? 'Payment cancelled' : (transferResponse.error || 'Payment failed')); } const paymentTxId = transferResponse.result?.txid; if (!paymentTxId) { throw new Error('No transaction ID received from wallet'); } console.log('Payment txid:', paymentTxId); // Collect physical print details if selected let physicalPrintDetails = null; if (isPhysicalPrintChecked) { const name = document.getElementById('physicalPrintName')?.value || ''; const address = document.getElementById('physicalPrintAddress')?.value || ''; const city = document.getElementById('physicalPrintCity')?.value || ''; const state = document.getElementById('physicalPrintState')?.value || ''; const zip = document.getElementById('physicalPrintZip')?.value || ''; const country = document.getElementById('physicalPrintCountry')?.value || ''; const email = document.getElementById('physicalPrintEmail')?.value || ''; const phone = document.getElementById('physicalPrintPhone')?.value || ''; physicalPrintDetails = { name, address, city, state, zip, country, email, phone }; } // Record the purchase document.getElementById('inscribeBtn').innerHTML = ' Recording Purchase...'; // Use record-mint for inscribe-on-demand, mint-pre-inscribed-v2 for pre-inscribed const isInscribeOnDemand = artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints'; const endpoint = isInscribeOnDemand ? '/api/record-mint' : '/api/mint-pre-inscribed-v2'; const recordResponse = await fetch( endpoint, withMintQueueFetchInit(currentArtworkSlug, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ artworkSlug: currentArtworkSlug, selectedAssetIds: assetIds, paymentTxId: paymentTxId, transactionId: paymentTxId, ordinalsWalletAddress: walletData.ordinalsAddress, paymentWalletAddress: walletData.paymentAddress, physicalPrintDetails: physicalPrintDetails, }), }) ); const recordData = await parseApiJsonResponse(recordResponse, 'mint-pre-inscribed-v2'); if (!recordData.success) { throw new Error(recordData.error || 'Failed to record purchase'); } // Success! document.getElementById('inscriptionResult').innerHTML = `

    Purchase Successful!

    Quantity: ${recordData.quantity} inscription(s)
    Payment confirmed: ${paymentTxId.substring(0, 16)}...

    Inscriptions will be transferred to this ordinals address:
    ${walletData.ordinalsAddress}

    View Payment Transaction

    `; document.getElementById('inscribeBtn').innerHTML = 'Mint Successful!'; document.getElementById('inscribeBtn').disabled = true; // Update mint counter fetchMintStats(); } catch (error) { console.error('Pre-inscribed v2 mint error:', error); document.getElementById('inscriptionResult').innerHTML = `

    Mint Failed: ${error.message}

    `; document.getElementById('inscribeBtn').innerHTML = 'Mint Failed'; document.getElementById('inscribeBtn').disabled = false; } }; // Handle pre-inscribed collection minting window.handlePreInscribedMint = async function() { try { document.getElementById('inscribeBtn').innerHTML = ' Processing Payment...'; // Get artwork details const artwork = window.currentArtwork; // Check if this is a numerical mint type if (artwork.mintType === 'numerical') { // Handle numerical minting return await handleNumericalMint(); } // All pre-inscribed collections now use the V2 flow // This includes both new collections and legacy ones return await handlePreInscribedV2Mint(); } catch (error) { console.error('Pre-inscribed mint error:', error); document.getElementById('inscriptionResult').innerHTML = `

    Mint Failed: ${error.message}

    `; document.getElementById('inscribeBtn').innerHTML = 'Mint Failed'; document.getElementById('inscribeBtn').disabled = false; } }; // Create delegate inscription referencing the parent inscription window.inscribeOrdinal = async function() { try { document.getElementById('inscribeBtn').innerHTML = ' Checking Eligibility...'; document.getElementById('inscribeBtn').disabled = true; // Check if we have current artwork data if (!window.currentArtwork) { throw new Error('Artwork data not loaded. Please refresh the page.'); } // Check if this is a pre-inscribed or inscribe-on-demand collection const isPreInscribed = window.currentArtwork.mintType === 'pre_inscribed' || window.currentArtwork.mintType === 'numerical'; const isInscribeOnDemand = window.currentArtwork.mintType === 'inscribe_on_demand' || window.currentArtwork.mintType === 'parent_child_prints'; console.log('Mint type:', window.currentArtwork.mintType, 'Is pre-inscribed:', isPreInscribed, 'Is inscribe-on-demand:', isInscribeOnDemand); // Whitelist eligibility is now checked immediately after wallet connection // No need to check again here since we already validated it // Handle pre-inscribed and inscribe-on-demand collections with the same flow (both support multiple minting) if (isPreInscribed || isInscribeOnDemand) { if (isInscribeOnDemand) { // Use the same flow as pre-inscribed for inscribe-on-demand return await handlePreInscribedV2Mint(); } else { return await handlePreInscribedMint(); } } document.getElementById('inscribeBtn').innerHTML = ' Reserving Mint Slot...'; // First, reserve a mint slot const reservationResponse = await fetch( '/api/reserve-mint-slot', withMintQueueFetchInit(currentArtworkSlug, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ artworkSlug: currentArtworkSlug || 'failure-to-launch', walletAddress: walletData.ordinalsAddress, }), }) ); const reservationData = await reservationResponse.json(); if (!reservationData.success) { // Handle sold out or other reservation failures document.getElementById('inscriptionResult').innerHTML = `

    Mint Failed: ${reservationData.error}

    ${reservationData.soldOut ? '

    This artwork has completely sold out!

    ' : ''}
    `; document.getElementById('inscribeBtn').innerHTML = 'Mint Failed'; document.getElementById('inscribeBtn').disabled = false; return; } // Store reservation info for later use window.currentReservation = { id: reservationData.reservationId, expiresAt: reservationData.expiresAt }; document.getElementById('inscribeBtn').innerHTML = ' Creating Inscription...'; // Check if this is an inscribe-on-demand collection let selectedAsset = null; let isOnDemandCollection = false; // Check actual mint type - now we can use the direct mint_type field if (window.currentArtwork) { isOnDemandCollection = window.currentArtwork.mintType === 'inscribe_on_demand' || window.currentArtwork.mintType === 'parent_child_prints'; console.log('Is on-demand collection:', isOnDemandCollection); } if (isOnDemandCollection) { // Check if we have a reserved asset from the preview if (window.previewAssetToMint && window.previewReservationExpiry && window.previewReservationExpiry > Date.now()) { console.log('Using reserved preview asset:', window.previewAssetToMint.originalName); selectedAsset = window.previewAssetToMint; // Set for cost calculation and update display window.selectedAssetForCostCalculation = selectedAsset; await window.updateCostCalculation(); document.getElementById('inscribeBtn').innerHTML = ' Using Your Reserved Asset...'; } else { // No reserved asset - get a random one from the API document.getElementById('inscribeBtn').innerHTML = ' Getting Random Asset...'; try { const response = await fetch( `/api/mint-on-demand-asset`, withMintQueueFetchInit(currentArtworkSlug, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ artworkSlug: currentArtworkSlug, userId: 'mint-' + Date.now(), }), }) ); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const assetData = await response.json(); if (assetData.success && assetData.asset) { selectedAsset = assetData.asset; console.log('✅ MOBILE: Got random asset for minting:', selectedAsset.originalName, 'ID:', selectedAsset.id); } else { throw new Error(assetData.error || 'Failed to get random asset'); } } catch (error) { console.error('Error getting random asset:', error); document.getElementById('inscriptionResult').innerHTML = `

    Error Getting Asset

    ${error.message}

    Collection may be sold out.

    `; document.getElementById('inscribeBtn').innerHTML = 'Mint Now'; document.getElementById('inscribeBtn').disabled = false; return; } } console.log('✅ MOBILE: Final selected asset before inscription:', { id: selectedAsset?.id, originalName: selectedAsset?.originalName, contentType: selectedAsset?.contentType }); document.getElementById('inscribeBtn').innerHTML = ' Creating Inscription...'; } // Calculate total cost including artwork price and physical print (MOVED HERE AFTER ASSET SELECTION) let cost; if (isOnDemandCollection && selectedAsset) { // For on-demand, cost is based on actual selected asset size let actualSize; if (selectedAsset.imageDataBase64) { // Calculate actual size from base64 data // Base64 encoding increases size by ~33%, so decode size = base64.length * 0.75 actualSize = Math.ceil(selectedAsset.imageDataBase64.length * 0.75); } else if (selectedAsset.isHtml) { // For HTML files, generate the HTML content to get actual size let htmlContent; // First check if artwork has a custom inscription_html_template if (artwork.inscriptionHtmlTemplate) { htmlContent = artwork.inscriptionHtmlTemplate; } else if (artwork.parentType === 'collection') { // Generate HTML wrapper for collection parent htmlContent = ` `; } else if (artwork.parentType === 'recursive') { // Generate HTML wrapper for recursive parent htmlContent = ` `; } else { // Generate traditional recursive inscription for static images using script from database const scriptId = artwork.scriptInscriptionId || 'd686bf14aec7115b84cb9e85fe8d819d61217d115b8869c49694205ce3403a8ai0'; htmlContent = `
    `; } actualSize = htmlContent.length; console.log('✅ ACCURATE: Generated HTML for inscribe-on-demand cost calculation:', { htmlSizeBytes: actualSize, parentType: artwork.parentType }); } else { // Fallback to rough estimate for non-HTML files if no data available actualSize = 60000; } cost = await calculateInscriptionCost(actualSize, window.getSelectedFeeRate()); console.log('✅ ACCURATE: Cost calculated for selected asset:', { assetName: selectedAsset.originalName, isHtml: selectedAsset.isHtml, base64Length: selectedAsset.imageDataBase64?.length, actualSizeBytes: actualSize, feeRate: window.getSelectedFeeRate(), totalCostSats: cost.inscriptionFee }); // Update the cost display with accurate fees window.selectedAssetForCostCalculation = selectedAsset; await window.updateCostCalculation(); } else { // For delegate collections or fallback, use HTML size or estimate const htmlSize = 1000; // Will be calculated properly below for delegate cost = await calculateInscriptionCost(htmlSize, window.getSelectedFeeRate()); console.log('Cost calculated for delegate or fallback'); } // Get current mint count and existing transaction IDs before transaction const preTransactionStats = await fetch(`/api/simple-artwork?slug=${currentArtworkSlug || 'failure-to-launch'}`); const preData = await preTransactionStats.json(); const initialMintCount = preData.success ? preData.minted : 0; // Get list of existing transaction IDs to prevent duplicates // We'll populate this from the database check during monitoring const existingTransactionIds = new Set(); // Record the timestamp when mint was initiated const mintStartTime = Date.now(); // Create inscription content using database artwork details const artwork = window.currentArtwork; let htmlContent, base64Content; // Debug: Log artwork delegate status console.log('Artwork delegate status:', { slug: artwork?.slug, isDelegate: artwork?.isDelegate, parentInscriptionId: artwork?.parentInscriptionId }); // Only generate HTML content for delegate inscriptions, not for inscribe-on-demand if (!isOnDemandCollection) { // First check if artwork has a custom inscription_html_template if (artwork.inscriptionHtmlTemplate) { // Use the custom template from the artworks table htmlContent = artwork.inscriptionHtmlTemplate; console.log('🎯 Using custom inscription_html_template from artworks table'); } else if (artwork.isDelegate) { // Check if this artwork uses custom delegate template // Fetch custom delegate template from API try { const templateResponse = await fetch(`/api/get-delegate-template?parentId=${artwork.parentInscriptionId}`); const templateData = await templateResponse.json(); if (templateData.success) { htmlContent = templateData.template; console.log('🎯 Fetched CUSTOM DELEGATE template from API'); } else { throw new Error('Failed to fetch delegate template'); } } catch (error) { console.error('Failed to fetch delegate template, using standard:', error); // Fallback to standard template htmlContent = ` `; } } else if (artwork.parentType === 'recursive') { // Generate HTML wrapper for recursive parent htmlContent = ` `; } else { // Generate traditional recursive inscription for static images using script from database const scriptId = artwork.scriptInscriptionId || 'd686bf14aec7115b84cb9e85fe8d819d61217d115b8869c49694205ce3403a8ai0'; htmlContent = `
    `; } // Convert to base64 for Xverse base64Content = Buffer.from(htmlContent).toString('base64'); console.log('Generated HTML content for delegate inscription'); } else { console.log('Skipping HTML generation for inscribe-on-demand collection'); } // For delegate collections, recalculate cost based on actual HTML size if (!isOnDemandCollection && htmlContent) { const htmlSize = htmlContent.length; cost = await calculateInscriptionCost(htmlSize, window.getSelectedFeeRate()); console.log('✅ ACCURATE: Cost recalculated for delegate HTML:', { htmlSizeBytes: htmlSize, feeRate: window.getSelectedFeeRate(), totalCostSats: cost.inscriptionFee }); } // Collect physical print details if selected const physicalPrintCheckbox = document.getElementById('physicalPrintCheckbox'); const physicalPrintRequested = physicalPrintCheckbox && physicalPrintCheckbox.checked; let physicalPrintDetails = null; if (physicalPrintRequested) { const name = document.getElementById('shippingName').value; const address = document.getElementById('shippingAddress').value; const city = document.getElementById('shippingCity').value; const state = document.getElementById('shippingState').value; const zip = document.getElementById('shippingZip').value; const country = document.getElementById('shippingCountry').value; const email = document.getElementById('shippingEmail').value; const phone = document.getElementById('shippingPhone').value; console.log('Physical print requested with details:', { name, address, city, state, zip, country, email, phone }); if (!name || !address || !city || !country || !email || !phone) { alert('Please fill in all required shipping address fields for physical print (including email and phone)'); document.getElementById('inscribeBtn').innerHTML = 'Mint Now'; document.getElementById('inscribeBtn').disabled = false; return; } physicalPrintDetails = { name, address, city, state, zip, country, email, phone }; console.log('Physical print details prepared:', physicalPrintDetails); } // Determine content for inscription based on mint type let inscriptionContent, contentType, payloadType; if (isOnDemandCollection && selectedAsset) { // For inscribe-on-demand, inscribe the selected asset directly inscriptionContent = selectedAsset.imageDataBase64; contentType = selectedAsset.contentType; payloadType = 'BASE_64'; console.log('Inscribing on-demand asset:', { assetId: selectedAsset.id, originalName: selectedAsset.originalName, contentType: contentType, isHtml: selectedAsset.isHtml, dataLength: inscriptionContent.length }); } else { // For delegate inscriptions, use the generated content inscriptionContent = base64Content; // For delegate inscriptions, use the generated HTML content contentType = 'text/html'; console.log('Inscribing delegate HTML content'); payloadType = 'BASE_64'; } // Add flags to prevent duplicate processing between callback and monitoring let transactionProcessed = false; let assetConfirmed = false; // Create inscription using Xverse createInscription method createInscription({ payload: { network: { type: 'Mainnet' }, contentType: contentType, content: inscriptionContent, payloadType: payloadType, suggestedMinerFeeRate: window.getSelectedFeeRate(), // Include marketplace fee + artwork price + physical print fee appFee: cost.marketplaceFee + cost.artworkPrice + cost.physicalPrintFee, appFeeAddress: 'bc1qcefl3uc75wh39wcwqs4ca9xw8m7jhc35t3kv43' }, onFinish: (response) => { // IMMEDIATELY show that callback was triggered const resultDiv = document.getElementById('inscriptionResult'); if (resultDiv) { resultDiv.innerHTML = `

    📱 MOBILE: Transaction sent!
    TX: ${response.txId ? response.txId.substring(0, 12) + '...' : 'NO TX ID'}
    Has Inscription: ${!!response.inscriptionId}

    🔄 Checking mempool for inscription ID...

    `; } // For mobile, don't process in callback - let monitoring handle it // Monitoring has the proper inscription ID tracing logic if (!response.inscriptionId) { console.log('📱 MOBILE: No inscription ID in callback, relying on monitoring'); // Don't set transactionProcessed - let monitoring handle it return; } if (transactionProcessed) { console.log('Transaction already processed via monitoring, skipping callback'); if (resultDiv) { resultDiv.innerHTML += `

    ⚠️ Already processed by monitoring - skipping

    `; } return; } transactionProcessed = true; console.log('✅ Processing transaction via Xverse callback'); console.log('🔧 CALLBACK RESPONSE:', response); console.log('🔧 CALLBACK: Selected asset for recording:', selectedAsset); console.log('🔧 CALLBACK: Is on-demand collection:', isOnDemandCollection); console.log('🔧 CALLBACK: Recording transaction with params:', { txId: response.txId, inscriptionId: response.inscriptionId, hasInscriptionId: !!response.inscriptionId, paymentAddress: walletData.paymentAddress, ordinalsAddress: walletData.ordinalsAddress, reservationId: window.currentReservation?.id, selectedAssetId: selectedAsset?.id, hasPhysicalPrint: !!physicalPrintDetails, artworkSlug: currentArtworkSlug }); // Record the mint transaction with reservation ID and selected asset // The new atomic implementation handles both mint_transactions and collection_assets // For inscribe-on-demand with multiple assets, pass array const selectedAssetIds = window.reservedAssetIds || (selectedAsset?.id ? [selectedAsset.id] : null); recordMintTransaction( response.txId, response.inscriptionId, walletData.paymentAddress, walletData.ordinalsAddress, window.currentReservation?.id, physicalPrintDetails, selectedAsset?.id, // For backward compatibility selectedAssetIds // Array for bulk minting ).then((recordResult) => { console.log('✅ Atomic mint recording completed:', recordResult); // Update mobile status const mobileStatus = document.getElementById('mobile-debug-status'); // Show detailed error if recording failed if (!recordResult || !recordResult.success) { console.error('❌ RECORD MINT FAILED:', recordResult); const errorMsg = recordResult?.error || 'Unknown error'; const errorDetails = recordResult?.details || ''; const errorDetailsObj = recordResult?.errorDetails || {}; if (mobileStatus) { mobileStatus.innerHTML = ` ❌ Recording failed:
    ${errorMsg}
    ${errorDetails}
    ${errorDetailsObj.message || ''}
    `; mobileStatus.style.color = '#ff6666'; } // Don't throw - let it continue to try stats update console.log('⚠️ Continuing despite record failure to check if mint was actually recorded'); return recordResult; // Return the failed result but don't throw } if (mobileStatus) { // Show detailed info about what happened const debugInfo = recordResult.debug || {}; const insertChanges = debugInfo.insertChanges; const prevCount = debugInfo.previousMintCount; const assetMarked = recordResult.assetMarkedAsMinted; // Always show raw debug data for mobile mobileStatus.innerHTML = ` DEBUG INFO:
    Recorded: ${recordResult.recorded}
    Insert Changes: ${insertChanges}
    Asset Marked: ${assetMarked}
    Count: ${prevCount} → ${recordResult.totalMints}
    Selected Asset ID: ${debugInfo.selectedAssetId || 'none'}
    Inscription ID: ${debugInfo.inscriptionId || 'NULL'}
    TX: ${response.txId.substring(0, 12)}... `; if (recordResult.recorded === false || insertChanges === 0) { mobileStatus.style.color = '#ffcc00'; // Yellow - duplicate } else if (recordResult.totalMints > prevCount) { mobileStatus.style.color = '#00ff00'; // Green - count increased } else { mobileStatus.style.color = '#ff6666'; // Red - something's wrong } } // The atomic transaction already handled asset marking for inscribe-on-demand if (recordResult.assetMarkedAsMinted) { console.log('✅ Asset automatically marked as minted in atomic transaction'); } // Update display if inscription ID was traced on backend if (!response.inscriptionId && recordResult.inscriptionId) { console.log('✅ MOBILE: Inscription ID was traced during recording:', recordResult.inscriptionId); displayInscriptionId = recordResult.inscriptionId; // Update the display with the traced inscription ID const inscriptionDisplay = document.getElementById('inscription-id-display'); if (inscriptionDisplay) { inscriptionDisplay.innerHTML = `Inscription ID: ${recordResult.inscriptionId}`; } if (mobileStatus) { mobileStatus.innerHTML = `✅ Mint recorded! Inscription ID traced! Count: ${recordResult.totalMints || '?'}`; } } else if (!response.inscriptionId && !recordResult.inscriptionId) { console.log('⚠️ MOBILE: Inscription ID not traced yet - will be updated via monitoring'); if (mobileStatus) { mobileStatus.innerHTML = `⚠️ Mint recorded (count: ${recordResult.totalMints || '?'}), inscription ID will appear shortly...`; mobileStatus.style.color = '#ffcc00'; } } return recordResult; }).then((recordResult) => { // Clear ALL reservation data after successful minting if (window.previewReservationExpiry) { window.previewReservationExpiry = null; window.previewAssetToMint = null; } // CRITICAL: Clear the currentReservation so next mint gets a fresh reservation if (window.currentReservation) { console.log('🔄 Clearing used reservation:', window.currentReservation.id); window.currentReservation = null; } // Update mint counter after recording and confirmation console.log('🔄 MOBILE: Fetching updated mint stats...'); const mobileStatus = document.getElementById('mobile-debug-status'); const existingDebugInfo = mobileStatus ? mobileStatus.innerHTML : ''; if (mobileStatus) { mobileStatus.innerHTML = existingDebugInfo + `

    🔄 Fetching stats...`; } fetchMintStats().then(() => { if (mobileStatus) { const currentCount = document.getElementById('mintedCount')?.textContent; // Append to existing debug info mobileStatus.innerHTML = existingDebugInfo + `

    ✅ Stats Fetched!
    Display Count: ${currentCount}`; } }).catch(err => { if (mobileStatus) { mobileStatus.innerHTML = existingDebugInfo + `

    ⚠️ Couldn't fetch stats. Refresh page.`; } }); // For mobile callbacks without inscription ID, start monitoring immediately // Use the same monitoring that works on desktop if (!response.inscriptionId) { console.log('🔧 MOBILE: No inscription ID in callback, starting monitoring immediately'); if (mobileStatus) { mobileStatus.innerHTML = existingDebugInfo + `

    ⏱️ Monitoring for inscription ID...`; } // Start monitoring right away (same as desktop) setTimeout(() => { monitorForInscriptionId(response.txId, currentArtworkSlug); }, 5000); // Start after 5 seconds } }).catch(error => { console.error('❌ Error in mint confirmation process:', error); console.error('❌ Error details:', { message: error.message, stack: error.stack }); // Show error visibly const mobileStatus = document.getElementById('mobile-debug-status'); if (mobileStatus) { mobileStatus.innerHTML = `❌ Error: ${error.message}`; mobileStatus.style.color = '#ff6666'; } // Still update stats even if confirmation failed console.log('🔄 Attempting to fetch stats despite error...'); fetchMintStats().then(() => { console.log('✅ Stats fetched after error'); if (mobileStatus) { mobileStatus.innerHTML = `⚠️ Error occurred but stats updated. Check if mint count increased.`; mobileStatus.style.color = '#ffcc00'; } }).catch(statsError => { console.error('❌ Failed to fetch stats after error:', statsError); if (mobileStatus) { mobileStatus.innerHTML = `❌ Error: Could not fetch stats. Refresh page to see updated count.`; mobileStatus.style.color = '#ff6666'; } }); }); // Store the initial inscription ID from callback let displayInscriptionId = response.inscriptionId; // Set up initial display with mobile debug info document.getElementById('inscriptionResult').innerHTML = `

    Transaction ID: ${response.txId}

    Inscription ID: ${displayInscriptionId || 'Tracing...'}

    📱 Mobile: Recording mint...

    View Transaction ↗
    `; document.getElementById('inscribeBtn').innerHTML = 'Inscription Complete ✅'; document.getElementById('inscribeBtn').disabled = false; // DEBUG: Check all variables before immediate confirmation console.log('🔍 Pre-confirmation debug check:', { isOnDemandCollection: isOnDemandCollection, selectedAsset: selectedAsset, selectedAssetId: selectedAsset?.id, responseInscriptionId: response.inscriptionId, responseTxId: response.txId, allConditionsMet: !!(isOnDemandCollection && selectedAsset?.id && response.inscriptionId) }); // Asset confirmation is now handled atomically in recordMintTransaction console.log('✅ Asset confirmation handled atomically in mint recording'); }, onCancel: () => { // Show a neutral checking message instead of assuming cancellation document.getElementById('inscriptionResult').innerHTML = `

    Checking transaction status...

    Popup was closed. Verifying if your transaction completed successfully.

    `; document.getElementById('inscribeBtn').innerHTML = ' Verifying...'; document.getElementById('inscribeBtn').disabled = true; } }); // Start monitoring mempool for transactions since callbacks don't work const startTime = Date.now(); const processedTransactionIds = new Set(); // Track processed transactions to avoid duplicates const monitorTransactions = async () => { try { // Check if transaction was already processed via callback if (transactionProcessed) { console.log('Transaction already processed via callback, stopping monitoring'); return true; // Stop monitoring } // We rely on INSERT OR IGNORE in the API to prevent duplicates // Focus on timestamp filtering to avoid processing old transactions // Get recent transactions from user's payment wallet const response = await fetch(`https://mempool.space/api/address/${walletData.paymentAddress}/txs`); if (response.ok) { const transactions = await response.json(); // Look for recent transactions (within last 10 minutes) - include unconfirmed const recentTxs = transactions.filter(tx => { // For unconfirmed transactions, use current time if (!tx.status.confirmed) { return true; // Include all unconfirmed transactions } const txTime = tx.status.block_time * 1000; // Convert to milliseconds const timeDiff = Date.now() - txTime; return timeDiff < 600000; // Within 10 minutes for confirmed }); // Check if any recent transaction has our app fee output const expectedAppFee = cost.marketplaceFee + cost.artworkPrice + cost.physicalPrintFee; console.log('Mempool monitoring - Expected app fee calculation:', { marketplaceFee: cost.marketplaceFee, artworkPrice: cost.artworkPrice, physicalPrintFee: cost.physicalPrintFee, totalExpected: expectedAppFee }); for (const tx of recentTxs) { // Skip if already processed if (processedTransactionIds.has(tx.txid)) { continue; } // First verify this transaction is FROM our payment wallet const isFromOurWallet = tx.vin.some(input => input.prevout && input.prevout.scriptpubkey_address === walletData.paymentAddress ); if (!isFromOurWallet) { continue; // Skip transactions not from our wallet } // Additional check: only process transactions that are very recent // This helps prevent processing old transactions from rapid successive mints // For confirmed transactions, check timing more strictly if (tx.status.confirmed) { const txTime = tx.status.block_time * 1000; const timeSinceMintStart = Date.now() - mintStartTime; // Only process transactions that are either: // 1. Very recent (within last 2 minutes) // 2. After mint start time (allowing some buffer) if (txTime < mintStartTime - 120000 && timeSinceMintStart > 120000) { continue; // Skip old transactions } } // Unconfirmed transactions are assumed to be new // Check if it has payment to our app address const appFeeOutput = tx.vout.find(output => output.scriptpubkey_address === 'bc1qcefl3uc75wh39wcwqs4ca9xw8m7jhc35t3kv43' ); if (appFeeOutput) { console.log('Found app fee output:', { expectedAppFee, actualAppFee: appFeeOutput.value, txid: tx.txid, match: appFeeOutput.value === expectedAppFee }); // Check for EXACT fee amount to distinguish between different artworks const hasValidPayment = appFeeOutput.value === expectedAppFee; if (hasValidPayment) { // Try to find the inscription transaction by tracing outputs let inscriptionId = null; try { // Look for outputs that might be spent in inscription transactions for (let i = 0; i < tx.vout.length; i++) { const output = tx.vout[i]; // Skip our app fee output if (output.scriptpubkey_address === 'bc1qcefl3uc75wh39wcwqs4ca9xw8m7jhc35t3kv43') { continue; } // Check if this output is spent (indicating it was used for inscription) try { const outpointResponse = await fetch(`https://mempool.space/api/tx/${tx.txid}/outspend/${i}`); if (outpointResponse.ok) { const outspend = await outpointResponse.json(); if (outspend.spent) { // Get the spending transaction (potential inscription) const spendingTxResponse = await fetch(`https://mempool.space/api/tx/${outspend.txid}`); if (spendingTxResponse.ok) { const spendingTx = await spendingTxResponse.json(); // Check if this looks like an inscription transaction // Inscription transactions typically have OP_RETURN or witness data if (spendingTx.vout.length > 0) { // The first output of an inscription transaction is usually the inscribed sat const firstOutput = spendingTx.vout[0]; if (firstOutput.scriptpubkey_address) { // Generate inscription ID: txid + "i" + output_index inscriptionId = `${outspend.txid}i0`; break; } } } } } } catch (traceError) { } } } catch (traceError) { } // Record this transaction with both wallet addresses and physical print details console.log('Attempting to record mint transaction:', { txid: tx.txid, inscriptionId, paymentAddress: walletData.paymentAddress, ordinalsAddress: walletData.ordinalsAddress, reservationId: window.currentReservation?.id, hasPhysicalPrintDetails: !!physicalPrintDetails, selectedAssetId: selectedAsset?.id }); // Mark transaction as processed to prevent callback from running transactionProcessed = true; console.log('Processing transaction via mempool monitoring'); try { // For inscribe-on-demand with multiple assets, pass array if available const selectedAssetIds = window.reservedAssetIds || (selectedAsset?.id ? [selectedAsset.id] : null); const recordResult = await recordMintTransaction( tx.txid, inscriptionId, // Include inscription ID if found walletData.paymentAddress, walletData.ordinalsAddress, window.currentReservation?.id, // Include reservation ID physicalPrintDetails, // Include physical print details selectedAsset?.id, // For backward compatibility selectedAssetIds // Array for bulk minting ); console.log('✅ MONITORING: Atomic mint recording result:', recordResult); if (!recordResult || !recordResult.success) { console.error('❌ MONITORING: Failed to record mint transaction:', recordResult); // For atomic transactions, if it fails we should retry // Don't return false immediately, let the monitoring continue console.log('🔄 MONITORING: Will retry on next cycle'); return false; // Continue monitoring if recording failed } // Success - log atomic transaction details if (recordResult.assetMarkedAsMinted) { console.log('✅ MONITORING: Asset automatically marked as minted'); } if (recordResult.recorded) { console.log('✅ MONITORING: Transaction successfully recorded in database'); } } catch (recordError) { console.error('❌ MONITORING: Error in atomic mint recording:', recordError); // For network errors or other issues, continue monitoring console.log('🔄 MONITORING: Will retry on next cycle due to error'); return false; // Continue monitoring if recording failed } // Show success document.getElementById('inscriptionResult').innerHTML = `
    Inscription Complete!

    Transaction ID: ${tx.txid}

    ${inscriptionId ? `

    Inscription ID: ${inscriptionId}

    ` : ''}

    Status: ${tx.status.confirmed ? 'Confirmed' : 'Unconfirmed (in mempool)'}

    View Transaction ↗ ${inscriptionId ? `
    View Inscription ↗` : ''}
    `; document.getElementById('inscribeBtn').innerHTML = 'Inscription Complete ✅'; document.getElementById('inscribeBtn').disabled = false; // Asset confirmation is now handled atomically in recordMintTransaction console.log('✅ MONITORING: Asset confirmation handled atomically in mint recording:', { isOnDemandCollection: isOnDemandCollection, selectedAssetId: selectedAsset?.id, inscriptionId: inscriptionId, txId: tx.txid }); // Mark transaction as processed processedTransactionIds.add(tx.txid); // CRITICAL: Clear the reservation so next mint gets a fresh one if (window.currentReservation) { console.log('🔄 MONITORING: Clearing used reservation:', window.currentReservation.id); window.currentReservation = null; } // Update mint counter (but don't stop monitoring) fetchMintStats(); // Continue monitoring for more transactions (don't return true here) // This allows processing multiple mints in batch } } } } return false; // Continue monitoring even if transactions were found } catch (error) { return false; } }; // Start monitoring immediately since onFinish doesn't work let attempts = 0; const maxAttempts = 60; // Check for 10 minutes (10 second intervals) const checkInterval = setInterval(async () => { attempts++; const found = await monitorTransactions(); if (found) { clearInterval(checkInterval); return; } if (attempts >= maxAttempts) { clearInterval(checkInterval); document.getElementById('inscriptionResult').innerHTML = `

    Transaction Cancelled or Not Found

    No transaction was detected within the expected timeframe. If you completed the transaction in your wallet, please check your wallet for confirmation.

    `; document.getElementById('inscribeBtn').innerHTML = 'Mint Now'; document.getElementById('inscribeBtn').disabled = false; } }, 10000); // Check every 10 seconds // Also do an immediate first check after 10 seconds setTimeout(async () => { await monitorTransactions(); }, 10000); } catch (error) { document.getElementById('inscriptionResult').innerHTML = `
    Error creating inscription: ${error.message}
    `; document.getElementById('inscribeBtn').innerHTML = 'Mint Now'; document.getElementById('inscribeBtn').disabled = false; } }; async function recordMintTransaction(transactionId, inscriptionId, paymentWallet, ordinalsWallet, reservationId = null, physicalPrintDetails = null, selectedAssetId = null, selectedAssetIds = null) { try { const requestData = { transactionId, inscriptionId, walletAddress: paymentWallet, // Keep for backward compatibility paymentWalletAddress: paymentWallet, ordinalsWalletAddress: ordinalsWallet, artworkSlug: currentArtworkSlug || 'failure-to-launch', reservationId, physicalPrintDetails, selectedAssetId, // For backward compatibility selectedAssetIds // Array for bulk minting (preferred) }; console.log('🔧 RECORDING MINT:', { hasInscriptionId: !!inscriptionId, hasTransactionId: !!transactionId, hasSelectedAssetId: !!selectedAssetId, hasSelectedAssetIds: !!selectedAssetIds, assetCount: selectedAssetIds ? selectedAssetIds.length : (selectedAssetId ? 1 : 0), artworkSlug: requestData.artworkSlug }); const response = await fetch( '/api/record-mint', withMintQueueFetchInit(currentArtworkSlug, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestData), }) ); if (!response.ok) { const errorData = await response.json(); console.error('❌ RECORD MINT FAILED:', errorData); // Create detailed error message let errorMsg = `HTTP ${response.status}: ${errorData.error || response.statusText}`; if (errorData.details) { errorMsg += `\nDetails: ${errorData.details}`; } if (errorData.errorDetails?.message) { errorMsg += `\nError: ${errorData.errorDetails.message}`; } throw new Error(errorMsg); } const data = await response.json(); console.log('✅ RECORD MINT SUCCESS:', data); // Update counter after successful recording if (data.success) { console.log('🔄 Updating mint stats after successful recording...'); console.log('📊 Mint count from API:', data.totalMints); console.log('🎨 Asset marked as minted:', data.assetMarkedAsMinted); await fetchMintStats(); console.log('✅ fetchMintStats completed'); // Complete shipping log if this was a physical print order if (physicalPrintDetails && transactionId) { await completeShippingLog(transactionId, inscriptionId); } // Show subtle message if transaction was skipped if (data.skipped) { console.log('Transaction skipped:', data.message); console.log('Skipped transaction details:', { recorded: data.recorded, totalMints: data.totalMints, artworkTitle: data.artworkTitle, artist: data.artist }); // Optionally show a subtle notification to user // You could add a toast notification here if desired } } return data; } catch (error) { console.error('❌ RECORD MINT TRANSACTION ERROR:', error); return { success: false, error: error.message }; } } // Check transaction status function window.checkTransactionStatus = async function() { try { const statsResponse = await fetch(`/api/simple-artwork?slug=${currentArtworkSlug || 'failure-to-launch'}`); const statsData = await statsResponse.json(); if (statsData.success) { // Update the mint counter document.getElementById('mintedCount').textContent = statsData.minted; document.getElementById('totalCount').textContent = statsData.total; document.getElementById('inscriptionResult').innerHTML = `

    Transaction status updated!

    Current mint count: ${statsData.minted}/${statsData.total === -1 ? '∞' : statsData.total}

    Note: If you just completed a transaction and the count didn't increase, the transaction may not have been recorded automatically. Please contact support with your transaction ID.

    `; } } catch (error) { document.getElementById('inscriptionResult').innerHTML = `
    Failed to check transaction status. Please refresh the page.
    `; } }; // Manual transaction recording function window.recordManualTransaction = function() { const txId = prompt('Enter your transaction ID:'); if (txId && txId.length === 64) { recordMintTransaction(txId, null, walletData.paymentAddress || 'unknown', walletData.ordinalsAddress || 'unknown') .then(() => { alert('Transaction recorded! Refreshing mint count...'); checkTransactionStatus(); }) .catch(() => { alert('Failed to record transaction. Please try again or contact support.'); }); } else { alert('Please enter a valid 64-character transaction ID.'); } }; // Export metadata function window.exportMetadata = async function(slug) { try { const response = await fetch(`/api/export-metadata?slug=${slug}`); if (!response.ok) { const errorData = await response.json(); alert(`Export failed: ${errorData.error}`); return; } // Get the metadata JSON const metadataText = await response.text(); // Create download link const blob = new Blob([metadataText], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${slug}-metadata.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); alert('Metadata exported successfully!'); } catch (error) { alert('Failed to export metadata: ' + error.message); } }; // View collection gallery function window.viewCollectionGallery = async function(slug, context = 'admin') { try { // Show modal and loading state document.getElementById('collection-gallery-modal').style.display = 'block'; document.getElementById('collection-gallery-loading').style.display = 'block'; document.getElementById('collection-gallery-content').style.display = 'none'; document.getElementById('collection-gallery-error').style.display = 'none'; // Fetch minted pieces data const response = await fetch(`/api/get-collection-minted?slug=${slug}`); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to load gallery data'); } // Populate collection details document.getElementById('gallery-collection-title-inner').textContent = data.artwork.title; document.getElementById('gallery-collection-artist').textContent = `by ${data.artwork.artist}`; document.getElementById('gallery-collection-description').textContent = data.artwork.description || 'No description provided'; document.getElementById('gallery-minted-count').textContent = data.artwork.totalMinted; document.getElementById('gallery-collection-price').textContent = data.artwork.price === 0 ? 'FREE' : `${data.artwork.price.toLocaleString()} sats`; // Load cover image or fallback to original artwork const coverImageDiv = document.getElementById('collection-cover-image'); if (data.artwork.imageFilename) { // Use collection cover image with fallback to parent inscription on error coverImageDiv.innerHTML = `${data.artwork.title} cover`; } else if (data.artwork.parentInscriptionId) { // Fallback to parent inscription (original artwork) - use iframe with img wrapper for proper scaling coverImageDiv.style.position = 'relative'; coverImageDiv.innerHTML = ``; } else if (data.artwork.scriptInscriptionId) { // Fallback to script inscription - use iframe with img wrapper for proper scaling coverImageDiv.style.position = 'relative'; coverImageDiv.innerHTML = ``; } else { coverImageDiv.innerHTML = '
    No Preview
    '; } // Populate social links const socialLinksDiv = document.getElementById('gallery-social-links'); let socialLinks = []; if (data.artwork.websiteUrl) { const url = data.artwork.websiteUrl.startsWith('http') ? data.artwork.websiteUrl : 'https://' + data.artwork.websiteUrl; socialLinks.push(` `); } if (data.artwork.twitterUrl) { const url = data.artwork.twitterUrl.startsWith('http') ? data.artwork.twitterUrl : 'https://twitter.com/' + data.artwork.twitterUrl.replace('@', ''); socialLinks.push(` `); } if (data.artwork.discordUrl) { const url = data.artwork.discordUrl.startsWith('http') ? data.artwork.discordUrl : 'https://discord.gg/' + data.artwork.discordUrl; socialLinks.push(` `); } socialLinksDiv.innerHTML = socialLinks.join(''); // Clear and populate gallery grid const galleryGrid = document.getElementById('collection-gallery-grid'); galleryGrid.innerHTML = ''; if (data.mintedPieces.length === 0) { galleryGrid.innerHTML = `

    No pieces have been minted yet.

    `; } else { // Create gallery items data.mintedPieces.forEach(piece => { const galleryItem = document.createElement('div'); galleryItem.style.cssText = ` background: #000; border: 2px solid #333; padding: 15px; text-align: center; transition: border-color 0.3s; `; // Use srcdoc with img wrapper for all types to ensure proper scaling const iframeContent = ``; galleryItem.innerHTML = `
    ${iframeContent}
    ${piece.name}
    ${piece.originalName ? `
    ${piece.originalName.replace(/\.(jpg|jpeg|png|gif|webp)$/i, '').replace(/[_-]/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
    ` : ''}
    Minted: ${new Date(piece.mintedAt).toLocaleDateString()}
    ${context === 'public' ? ` View on Marketplace ` : ` View Explorer `}
    `; // Add hover effect galleryItem.addEventListener('mouseenter', () => { galleryItem.style.borderColor = '#6600cc'; }); galleryItem.addEventListener('mouseleave', () => { galleryItem.style.borderColor = '#333'; }); galleryGrid.appendChild(galleryItem); }); } // Show content, hide loading document.getElementById('collection-gallery-loading').style.display = 'none'; document.getElementById('collection-gallery-content').style.display = 'block'; } catch (error) { console.error('Gallery loading error:', error); document.getElementById('collection-gallery-loading').style.display = 'none'; document.getElementById('collection-gallery-error').style.display = 'block'; } }; // Hide collection gallery modal window.hideCollectionGallery = function() { document.getElementById('collection-gallery-modal').style.display = 'none'; }; // Update mint end time display and countdown function updateMintEndTime(mintEndDatetime) { const mintEndTimeDiv = document.getElementById('mintEndTime'); const mintEndTimeValue = document.getElementById('mintEndTimeValue'); const mintTimeRemaining = document.getElementById('mintTimeRemaining'); if (!mintEndDatetime || !mintEndTimeDiv || !mintEndTimeValue || !mintTimeRemaining) { if (mintEndTimeDiv) mintEndTimeDiv.style.display = 'none'; return; } const endDate = new Date(mintEndDatetime); const now = new Date(); // Check if mint has already ended if (endDate <= now) { mintEndTimeDiv.style.display = 'block'; mintEndTimeDiv.style.background = '#ff0000'; // Red for ended mintEndTimeValue.textContent = 'MINT ENDED'; mintTimeRemaining.textContent = `Ended ${formatTimeAgo(now - endDate)} ago`; return; } // Show the mint end time mintEndTimeDiv.style.display = 'block'; mintEndTimeDiv.style.background = '#000000'; // Black for active with end time // Format the end date for display const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }; mintEndTimeValue.textContent = endDate.toLocaleDateString('en-US', options); // Update countdown function updateCountdown() { const now = new Date(); const timeLeft = endDate - now; if (timeLeft <= 0) { mintEndTimeValue.textContent = 'MINT ENDED'; mintTimeRemaining.textContent = 'Minting has ended'; mintEndTimeDiv.style.background = '#ff0000'; // Red for ended return; } const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24)); const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000); let timeRemainingText = ''; if (days > 0) { timeRemainingText = `${days}d ${hours}h ${minutes}m remaining`; } else if (hours > 0) { timeRemainingText = `${hours}h ${minutes}m ${seconds}s remaining`; } else if (minutes > 0) { timeRemainingText = `${minutes}m ${seconds}s remaining`; } else { timeRemainingText = `${seconds}s remaining`; } mintTimeRemaining.textContent = timeRemainingText; } // Update immediately and then every second updateCountdown(); setInterval(updateCountdown, 1000); } // Helper function to format time ago function formatTimeAgo(milliseconds) { const seconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) return `${days} day${days > 1 ? 's' : ''}`; if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''}`; if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''}`; return `${seconds} second${seconds > 1 ? 's' : ''}`; } // Mint tracking functions async function fetchMintStats() { try { const slug = currentArtworkSlug || 'failure-to-launch'; console.log('📊 Fetching mint stats for:', slug); const data = await fetchMintArtworkData(slug); // Debug logging console.log('📊 API Response:', { success: data.success, minted: data.minted, total: data.total, mintType: data.mintType, soldOut: data.soldOut }); console.log('🔄 Updating mint count to:', data.minted, 'of', data.total); // Update mint counter const mintedElement = document.getElementById('mintedCount'); const totalElement = document.getElementById('totalCount'); if (mintedElement) { mintedElement.textContent = data.minted; console.log('Updated mintedCount element to:', data.minted); } else { console.warn('mintedCount element not found'); } if (totalElement) { totalElement.textContent = data.total === -1 ? '∞' : data.total; console.log('Updated totalCount element to:', data.total === -1 ? '∞' : data.total); } else { console.warn('totalCount element not found'); } const progressFill = document.getElementById('mintProgressFill'); const progressPct = document.getElementById('mintProgressPct'); const remainingLabel = document.getElementById('mintRemainingLabel'); const mintedN = Number(data.minted) || 0; const totalN = data.total === -1 ? null : Number(data.total); if (totalN !== null && totalN > 0 && progressFill && progressPct) { const pct = Math.min(100, Math.round((mintedN / totalN) * 100)); progressFill.style.width = pct + '%'; progressPct.textContent = pct + '%'; if (remainingLabel) { const rem = Math.max(0, totalN - mintedN); remainingLabel.textContent = rem.toLocaleString() + ' remaining'; } } else { if (progressFill) progressFill.style.width = '0%'; if (progressPct) progressPct.textContent = totalN === null ? '—' : '0%'; if (remainingLabel) { remainingLabel.textContent = data.total === -1 ? 'Open edition' : '—'; } } // Update artwork details from database (only if we have the correct artwork) if (data.artwork && data.artwork.slug === slug) { updateArtworkDetails(data.artwork); if (slug === FIVE_IL5NC5_MINT_SLUG && data.fiveIl5nc5Phase) { window.fiveIl5nc5Schedule = data.fiveIl5nc5Phase; sync5il5nc5PhaseBar(data.fiveIl5nc5Phase); apply5il5nc5PhasePrice(data.fiveIl5nc5Phase); } // Update mint end time when refreshing stats updateMintEndTime(data.artwork.mintEndDatetime); } else if (data.artwork) { } // Update button based on sold out status const mintBtn = document.getElementById('inscribeBtn'); console.log('fetchMintStats - soldOut:', data.soldOut, 'total:', data.total, 'current button text:', mintBtn?.innerHTML); // Infinite supply mints (total: -1) should never be sold out const isInfiniteSupply = data.total === -1; const effectiveSoldOut = data.soldOut && !isInfiniteSupply; if (effectiveSoldOut) { mintBtn.innerHTML = 'SOLD OUT'; mintBtn.disabled = true; console.log('Set button to SOLD OUT'); } else { // Reset button only if it was previously SOLD OUT but now shouldn't be if (mintBtn.innerHTML === 'SOLD OUT') { mintBtn.innerHTML = 'Mint Now'; mintBtn.disabled = false; console.log('Reset button to Mint Now (not sold out or infinite supply)'); } } if (currentPage === 'mint' && mintBtn && data.success && data.artwork && data.artwork.slug === slug) { window.mintPageSoldOut = effectiveSoldOut; if (effectiveSoldOut) { syncMintPageUpcomingUi(data.artwork, { soldOut: true }); } else if (artworkIsUpcomingFromMintResponse(data)) { setInscribeButtonMintingSoon(mintBtn); syncMintPageUpcomingUi(data.artwork, { soldOut: false }); } else { syncMintPageUpcomingUi(data.artwork, { soldOut: false }); if (mintBtn.innerHTML === 'MINTING SOON') { mintBtn.innerHTML = 'Mint Now'; mintBtn.disabled = false; mintBtn.onclick = function () { inscribeOrdinal(); }; } await restoreMintStepsAfterArtworkLoadIfWalletReady(); } } return data; } catch (error) { // Use fallback values const fallbackData = { minted: 0, total: 1000, remaining: 1000, soldOut: false, artwork: { artist: 'NeedThat', title: 'Failure to Launch', price: 0, parentInscriptionId: 'd37ab9c6c318e8e24f2a51cc181bc62292cf0f2edd07cb75147b62a1a1d00421i0', scriptInscriptionId: 'd686bf14aec7115b84cb9e85fe8d819d61217d115b8869c49694205ce3403a8ai0' } }; document.getElementById('mintedCount').textContent = fallbackData.minted; document.getElementById('totalCount').textContent = fallbackData.total; const pf = document.getElementById('mintProgressFill'); const pp = document.getElementById('mintProgressPct'); const rl = document.getElementById('mintRemainingLabel'); if (pf) pf.style.width = '0%'; if (pp) pp.textContent = '0%'; if (rl) rl.textContent = '—'; // Don't call updateArtworkDetails with fallback data to prevent overwriting correct data return fallbackData; } } // Update artwork details in UI function updateArtworkDetails(artwork) { // Update specific elements by ID const artistName = document.getElementById('artist-name'); const artworkTitle = document.getElementById('artwork-title'); const artworkDescription = document.getElementById('artwork-description'); const artworkPrice = document.getElementById('artwork-price'); const socialLinks = document.getElementById('social-links'); if (artistName) artistName.textContent = artwork.artist; if (artworkTitle) artworkTitle.textContent = artwork.title; if (artworkDescription) { const formattedDescription = formatDescription(artwork.description); artworkDescription.innerHTML = formattedDescription; artworkDescription.style.display = artwork.description ? 'block' : 'none'; } if (artworkPrice) { const priceText = artwork.price === 0 ? 'FREE' : `${artwork.price.toLocaleString()} SATS`; artworkPrice.textContent = priceText; } // Update social media links with proper icons if (socialLinks) { let socialHTML = ''; // Helper function to build proper social media URLs function buildTwitterUrl(input) { if (!input) return ''; if (input.startsWith('http://') || input.startsWith('https://')) { return input; // Already a full URL } // Remove @ if present and build Twitter URL const username = input.replace('@', ''); return `https://twitter.com/${username}`; } function buildDiscordUrl(input) { if (!input) return ''; if (input.startsWith('http://') || input.startsWith('https://')) { return input; // Already a full URL } // If it looks like a Discord invite code, build invite URL if (input.includes('discord.gg/') || input.length < 20) { const code = input.replace('discord.gg/', ''); return `https://discord.gg/${code}`; } // Otherwise assume it's a username or server name return `https://discord.gg/${input}`; } function buildWebsiteUrl(input) { if (!input) return ''; if (input.startsWith('http://') || input.startsWith('https://')) { return input; // Already a full URL } return `https://${input}`; } if (artwork.twitterUrl) { const twitterUrl = buildTwitterUrl(artwork.twitterUrl); socialHTML += ` `; } if (artwork.discordUrl) { const discordUrl = buildDiscordUrl(artwork.discordUrl); socialHTML += ` `; } if (artwork.websiteUrl) { const websiteUrl = buildWebsiteUrl(artwork.websiteUrl); socialHTML += ` `; } socialLinks.innerHTML = socialHTML; } // Store artwork data globally for minting window.currentArtwork = artwork; } // Confirm that a reserved asset has been successfully minted async function confirmAssetMint(assetId, inscriptionId, transactionId) { console.log('🔄 confirmAssetMint called with:', { assetId, inscriptionId, transactionId }); try { const response = await fetch( '/api/confirm-asset-mint', withMintQueueFetchInit(window.currentArtworkSlug, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ assetId, inscriptionId, transactionId, sessionId: null, }), }) ); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); console.log('✅ confirmAssetMint success:', data); // Update counter after successful recording if (data.success) { await fetchMintStats(); // Show subtle message if transaction was skipped if (data.skipped) { console.log('Transaction skipped:', data.message); console.log('Skipped transaction details:', { recorded: data.recorded, totalMints: data.totalMints, artworkTitle: data.artworkTitle, artist: data.artist }); // Optionally show a subtle notification to user // You could add a toast notification here if desired } } return data; } catch (error) { console.error('❌ confirmAssetMint error:', error); return { success: false, error: error.message }; } } // Initialize fee estimates (only on mint pages) async function initializeFeeEstimates() { if (currentPage !== 'mint') return; // Start with default estimates immediately feeEstimates = { slow: 1, // 1 sat/vB - economy rate medium: 3, // 3 sat/vB - standard rate fast: 6 // 6 sat/vB - priority rate }; // Show the fee options immediately with defaults updateFeeDisplay(); // Try to fetch real fees in background try { await fetchFeeEstimates(); // Update display with real fees if successful updateFeeDisplay(); } catch (error) { // Keep using defaults - already displayed } } // Initialize mint page functionality function initializeMintPage() { if (currentPage !== 'mint') return; const mintNav = document.getElementById('mint-page-nav'); if (mintNav) { if (mintNavScrollHandler) { window.removeEventListener('scroll', mintNavScrollHandler); mintNavScrollHandler = null; } mintNavScrollHandler = () => { const n = document.getElementById('mint-page-nav'); if (!n || currentPage !== 'mint') return; n.classList.toggle('scrolled', window.scrollY > 24); }; window.addEventListener('scroll', mintNavScrollHandler, { passive: true }); mintNavScrollHandler(); } // Set up fee event listeners setupFeeEventListeners(); // Initialize fee estimates initializeFeeEstimates(); // Do initial cost calculation updateCostCalculation(); } // Global variable to track current home tab let currentHomeTab = 'live'; let currentMainTab = 'live'; // NEW: Switch between main navigation tabs window.switchMainTab = function(tabName) { currentMainTab = tabName; // Update tab buttons const tabs = ['live', 'all', 'stamps', 'packs', 'collections', 'auctions', 'gallery']; tabs.forEach(tab => { const tabBtn = document.getElementById(`tab-${tab}`); const view = document.getElementById(`view-${tab}`); if (tabBtn) { if (tab === tabName) { tabBtn.classList.add('active'); } else { tabBtn.classList.remove('active'); } } if (view) { if (tab === tabName) { view.classList.add('active'); } else { view.classList.remove('active'); } } }); // Load content for the selected tab if (tabName === 'live') { loadLiveArtworks(); } else if (tabName === 'all') { loadAllArtworks(); } else if (tabName === 'stamps') { loadStamps(); } else if (tabName === 'collections') { loadCollectionsView(); } else if (tabName === 'auctions') { loadAllAuctionsTab(); } else if (tabName === 'gallery') { // Gallery is static until wallet connects } else if (tabName === 'packs') { // Update advent calendar day when tab is opened updateAdventTitle(); } }; // Switch home page tabs between live, completed, and stamps (LEGACY - for compatibility) window.switchHomeTab = function(tabName) { // Map old tab names to new ones const tabMap = { 'live': 'live', 'completed': 'all', 'stamps': 'stamps' }; const newTabName = tabMap[tabName] || tabName; switchMainTab(newTabName); }; // Handle window resize for responsive grid // Resize handler removed - new UI handles responsive layout with CSS // Handle artwork click - desktop navigates immediately, mobile clicks through via onclick window.handleArtworkClick = function(event, slug, type = 'mint') { const isMobile = window.innerWidth <= 1024; if (!isMobile) { // Desktop: navigate immediately if (type === 'collection') { window.location.hash = `#collection/${slug}`; } else { // Check for custom artwork URLs const url = getArtworkUrl(slug); if (url.startsWith('#')) { window.location.hash = url; } else { window.location.href = url; } } } // Mobile: onclick handler will handle navigation directly }; // Navigate to artwork (handles both hash and custom URLs) window.navigateToArtwork = function(slug) { const url = getArtworkUrl(slug); if (url.startsWith('#')) { window.location.hash = url; } else { window.location.href = url; } }; window.navigateToAuction = function(slug) { window.location.href = getAuctionUrl(slug); }; // NEW: Display combined artworks and collections function displayCombinedItems(items, gridId, gridOptions) { const opts = gridOptions || {}; const isHeroMosaic = !!opts.heroMosaic; const grid = document.getElementById(gridId); if (!grid || !items || items.length === 0) { if (grid) grid.innerHTML = '

    No items available

    '; return; } grid.innerHTML = items.map(item => { if (item.type === 'collection') { const collection = item.data; const stillLiveCount = collection.live_count || 0; const completedCount = collection.artwork_count - stillLiveCount; let imageHtml = ''; if (collection.collection_image) { imageHtml = `${collectionOrdinalsAlt(collection.name, collection.artist_name)}`; } else { imageHtml = `
    📂
    `; } return `
    ${imageHtml}
    ${collection.artist_name || 'Various'}
    ${collection.name}
    ${collection.artwork_count} artworks
    ${stillLiveCount} live / ${completedCount} completed
    `; } else { // It's an artwork const artwork = item.data; const progress = artwork.total > 0 ? (artwork.minted / artwork.total * 100).toFixed(0) : 0; const isSoldOut = artwork.soldOut; // Get preview image let imageHtml = ''; const mosaicMediaLabel = collectionOrdinalsAlt(artwork.title, artwork.artist); // Custom thumbnail override for artwork 513 if (artwork.id === 513) { imageHtml = `${mosaicMediaLabel}`; } else if (artwork.mintType === 'pre_inscribed' || artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints' || artwork.mintType === 'numerical') { const r2Key = artwork.imageFilename; if (r2Key && r2Key.toLowerCase().endsWith('.html')) { imageHtml = ``; } else { imageHtml = `${mosaicMediaLabel}`; } } else if (artwork.parentType === 'recursive') { imageHtml = ``; } else if (artwork.parentInscriptionId) { imageHtml = ``; } else { imageHtml = `
    No Preview
    `; } if (isHeroMosaic) { const href = escapeHtmlAttr(getArtworkUrl(artwork.slug)); const safeSlug = escapeHtmlAttr(artwork.slug); const isUpcomingHero = isArtworkUpcoming(artwork); const startHero = isUpcomingHero ? formatMintStartForCard(artwork.mintStartDatetime) : ''; const pillHtml = isUpcomingHero ? `
    Minting soon${startHero ? `Starts ${escapeHtml(startHero)}` : ''}
    ` : ''; if (isUpcomingHero) { return ` `; } return `
    ${imageHtml}
    ${pillHtml}
    `; } const isUpcomingTile = isArtworkUpcoming(artwork); const startTile = isUpcomingTile ? formatMintStartForCard(artwork.mintStartDatetime) : ''; const startTileHtml = isUpcomingTile ? `
    ${startTile ? 'Starts ' + escapeHtml(startTile) : 'Minting soon'}
    ` : ''; const mintBtnLabel = isSoldOut ? 'VIEW' : (isUpcomingTile ? 'MINTING SOON' : 'MINT ARTWORK'); const mintBtnAction = isSoldOut ? "viewCollectionGallery('" + artwork.slug + "')" : isUpcomingTile ? 'void(0)' : "navigateToArtwork('" + artwork.slug + "')"; const mintBtnDisabled = isUpcomingTile && !isSoldOut ? ' disabled style="opacity:0.55;cursor:not-allowed;"' : ''; const tileClick = isUpcomingTile ? '' : ` onclick="if(window.innerWidth <= 1024) { navigateToArtwork('${artwork.slug}'); } else { handleArtworkClick(event, '${artwork.slug}'); }"`; return `
    ${imageHtml}
    ${artwork.artist}
    ${artwork.title}
    ${artwork.minted} / ${artwork.total === -1 ? '∞' : artwork.total} MINTED
    ${artwork.price === 0 ? 'FREE' : artwork.price.toLocaleString() + ' SATS'}
    ${startTileHtml}
    `; } }).join(''); } // NEW: Display artworks in thumbnail grid with hover effects (legacy, use displayCombinedItems instead) function displayArtworksThumbnails(artworks, gridId) { const grid = document.getElementById(gridId); if (!grid || !artworks || artworks.length === 0) { if (grid) grid.innerHTML = '

    No artworks available

    '; return; } // Sort by creation date descending (newest first), not by featured status const sortedArtworks = [...artworks].sort((a, b) => { const dateA = new Date(a.createdAt || 0); const dateB = new Date(b.createdAt || 0); return dateB - dateA; }); grid.innerHTML = sortedArtworks.map(artwork => { const progress = artwork.total > 0 ? (artwork.minted / artwork.total * 100).toFixed(0) : 0; const isSoldOut = artwork.soldOut; const isUpcomingThumb = isArtworkUpcoming(artwork); const startThumb = isUpcomingThumb ? formatMintStartForCard(artwork.mintStartDatetime) : ''; const startThumbHtml = isUpcomingThumb ? `
    ${startThumb ? 'Starts ' + escapeHtml(startThumb) : 'Minting soon'}
    ` : ''; const thumbMintLabel = isSoldOut ? 'VIEW' : (isUpcomingThumb ? 'MINTING SOON' : 'MINT ARTWORK'); const thumbMintAction = isSoldOut ? "viewCollectionGallery('" + artwork.slug + "')" : isUpcomingThumb ? 'void(0)' : "navigateToArtwork('" + artwork.slug + "')"; const thumbMintDisabled = isUpcomingThumb && !isSoldOut ? ' disabled style="opacity:0.55;cursor:not-allowed;"' : ''; const thumbClick = isUpcomingThumb ? '' : ` onclick="if(window.innerWidth <= 1024) { navigateToArtwork('${artwork.slug}'); } else { handleArtworkClick(event, '${artwork.slug}'); }"`; // Get preview image let imageHtml = ''; const thumbMediaLabel = collectionOrdinalsAlt(artwork.title, artwork.artist); // Custom thumbnail override for artwork 513 if (artwork.id === 513) { imageHtml = `${thumbMediaLabel}`; } else if (artwork.mintType === 'pre_inscribed' || artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints' || artwork.mintType === 'numerical') { const r2Key = artwork.imageFilename; if (r2Key && r2Key.toLowerCase().endsWith('.html')) { imageHtml = ``; } else { imageHtml = `${thumbMediaLabel}`; } } else if (artwork.parentType === 'recursive') { imageHtml = ``; } else if (artwork.parentInscriptionId) { imageHtml = ``; } else { imageHtml = `
    No Preview
    `; } return `
    ${imageHtml}
    ${artwork.artist}
    ${artwork.title}
    ${artwork.minted} / ${artwork.total === -1 ? '∞' : artwork.total} MINTED
    ${artwork.price === 0 ? 'FREE' : artwork.price.toLocaleString() + ' SATS'}
    ${startThumbHtml}
    `; }).join(''); } // NEW: Load live/active artworks function getSortedLiveArtworksForHome(overrideArtworks) { const baseArtworks = Array.isArray(overrideArtworks) ? overrideArtworks : (Array.isArray(window.mainPageAllArtworks) ? window.mainPageAllArtworks : (Array.isArray(window.allArtworks) ? window.allArtworks : [])); if (!baseArtworks.length) return []; const liveArtworks = baseArtworks.filter((artwork) => !artwork.soldOut); const standaloneLive = liveArtworks.filter((artwork) => !artwork.collectionSlug); const source = standaloneLive.length > 0 ? standaloneLive : liveArtworks; const upcoming = source.filter((a) => isArtworkUpcoming(a)); const liveNow = source.filter((a) => !isArtworkUpcoming(a)); upcoming.sort((a, b) => { const ta = new Date(a.mintStartDatetime || 0).getTime(); const tb = new Date(b.mintStartDatetime || 0).getTime(); return ta - tb; }); liveNow.sort((a, b) => { const dateA = new Date(a.createdAt || 0); const dateB = new Date(b.createdAt || 0); return dateB - dateA; }); return [...upcoming, ...liveNow]; } function heroAsciiBar(pct) { const total = 10; const filled = Math.max(0, Math.min(total, Math.round((Number(pct) || 0) / 100 * total))); let s = ''; for (let i = 0; i < total; i++) { s += i < filled ? '█' : ''; } return s; } function formatHeroMintId(artwork) { const raw = artwork && (artwork.id != null ? artwork.id : artwork.parentInscriptionId); if (raw == null || raw === '') return '—'; const asNum = Number(raw); if (Number.isFinite(asNum) && String(raw).indexOf('i') === -1) { return String(Math.trunc(asNum)).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } const s = String(raw); return s.length > 10 ? s.slice(0, 4) + '…' + s.slice(-4) : s; } function getHeroMintPreviewHtml(artwork) { const label = collectionOrdinalsAlt(artwork.title, artwork.artist); if (artwork.id === 513) { return `${escapeHtmlAttr(label)}`; } if (artwork.mintType === 'pre_inscribed' || artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints' || artwork.mintType === 'numerical') { const r2Key = artwork.imageFilename; if (r2Key && r2Key.toLowerCase().endsWith('.html')) { return ``; } if (r2Key) { return `${escapeHtmlAttr(label)}`; } } else if (artwork.parentType === 'recursive' && artwork.parentInscriptionId) { return ``; } else if (artwork.parentInscriptionId) { return `${escapeHtmlAttr(label)}`; } return '
    '; } function renderHeroMintFeed(artworks, gridId) { const grid = document.getElementById(gridId || 'artworks-grid-live'); if (!grid) return; const items = Array.isArray(artworks) ? artworks.filter(Boolean) : []; if (!items.length) { grid.innerHTML = '

    No live mints right now

    '; return; } const rows = items.map((artwork) => { const isUpcoming = isArtworkUpcoming(artwork); const total = Number(artwork.total); const minted = Number(artwork.minted) || 0; const pct = total > 0 ? Math.min(100, Math.round((minted / total) * 100)) : 0; const href = escapeHtmlAttr(getArtworkUrl(artwork.slug)); const title = escapeHtml(artwork.title || 'Untitled'); const artist = escapeHtml(artwork.artist || 'Unknown'); const idLabel = escapeHtml(formatHeroMintId(artwork)); const barHtml = isUpcoming ? '
    LIVE SOON
    minting soon' : total <= 0 ? `
    OPEN
    ${minted} minted` : `
    ${heroAsciiBar(pct)}
    ${pct}% minted`; const tag = isUpcoming ? 'div' : 'a'; const hrefAttr = isUpcoming ? '' : ` href="${href}"`; const noNavClass = isUpcoming ? ' hero-mint-row-soon' : ''; return `<${tag} class="hero-mint-row${noNavClass}"${hrefAttr} data-slug="${escapeHtmlAttr(artwork.slug)}">
    ${getHeroMintPreviewHtml(artwork)}
    #${idLabel}
    ${title}by ${artist}
    ${barHtml}
    `; }).join(''); // Duplicate for seamless vertical marquee grid.innerHTML = rows + rows; grid.classList.add('hero-mint-feed'); grid.classList.add('hero-feed-track'); } /** Live launches for the three large home cards: featured (by featuredOrder), then default live sort. */ function getHomeSpotlightDropsArtworks(limit) { const lim = typeof limit === 'number' ? limit : 3; const live = getSortedLiveArtworksForHome(); if (!live.length) return []; const featuredFirst = [...live] .filter((a) => a.isFeatured) .sort((a, b) => { const oa = a.featuredOrder != null ? Number(a.featuredOrder) : 999; const ob = b.featuredOrder != null ? Number(b.featuredOrder) : 999; if (oa !== ob) return oa - ob; const dateA = new Date(a.createdAt || 0); const dateB = new Date(b.createdAt || 0); return dateB - dateA; }); const out = []; const used = new Set(); for (const a of featuredFirst) { if (out.length >= lim) break; if (!used.has(a.slug)) { out.push(a); used.add(a.slug); } } for (const a of live) { if (out.length >= lim) break; if (!used.has(a.slug)) { out.push(a); used.add(a.slug); } } return out; } function escapeHtml(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function isArtworkUpcoming(artwork) { if (!artwork) return false; if (artwork.isUpcoming === true) return true; if (artwork.isUpcoming === false) return false; const start = artwork.mintStartDatetime || artwork.mint_start_datetime; if (!start) return false; const t = new Date(start).getTime(); if (Number.isNaN(t)) return false; return t > Date.now(); } function formatMintStartForCard(iso) { if (!iso) return ''; try { const d = new Date(iso); if (Number.isNaN(d.getTime())) return ''; return d.toLocaleString(undefined, { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'short' }); } catch (e) { return ''; } } function artworkIsUpcomingFromMintResponse(data) { if (adminMintBypassActive()) return false; if (!data) return false; if (data.isUpcoming === true) return true; return isArtworkUpcoming(data.artwork); } function setInscribeButtonMintingSoon(mintBtn) { if (!mintBtn) return; mintBtn.innerHTML = 'MINTING SOON'; mintBtn.disabled = true; mintBtn.onclick = function (e) { if (e) e.preventDefault(); return false; }; } function stop5il5nc5PhasePoller() { if (fiveIl5nc5PhasePollTimer) { clearInterval(fiveIl5nc5PhasePollTimer); fiveIl5nc5PhasePollTimer = null; } } function format5il5nc5PhasePrice(priceSats) { if (priceSats === 0) return 'FREE'; if (priceSats == null) return '—'; return `${priceSats.toLocaleString()} sats`; } function format5il5nc5Countdown(targetIso) { if (!targetIso) return ''; const diff = new Date(targetIso).getTime() - Date.now(); if (diff <= 0) return 'Starting now'; const totalSec = Math.floor(diff / 1000); const hours = Math.floor(totalSec / 3600); const minutes = Math.floor((totalSec % 3600) / 60); const seconds = totalSec % 60; if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`; if (minutes > 0) return `${minutes}m ${seconds}s`; return `${seconds}s`; } function sync5il5nc5PhaseBar(schedule) { if (!schedule || currentArtworkSlug !== FIVE_IL5NC5_MINT_SLUG) return; const phaseBar = document.getElementById('mintPhaseBar'); const phaseLive = document.getElementById('mintPhaseBarLive'); const phaseUpcoming = document.getElementById('mintPhaseBarUpcoming'); const phaseStart = document.getElementById('mintPhaseStartLabel'); if (!phaseBar || !phaseLive) return; const current = schedule.currentPhase; const isUpcoming = current === 'upcoming'; phaseBar.setAttribute('aria-hidden', 'false'); phaseBar.style.display = ''; if (isUpcoming) { if (phaseLive) phaseLive.style.display = 'none'; if (phaseUpcoming) phaseUpcoming.style.display = 'flex'; if (phaseStart) { const loyaltyPhase = (schedule.phases || []).find(function (p) { return p.id === 'loyalty'; }); const line = loyaltyPhase ? formatMintStartForCard(loyaltyPhase.start) : ''; phaseStart.textContent = line ? `Loyalty phase starts ${line}` : 'Loyalty phase start time to be announced'; } return; } if (phaseUpcoming) phaseUpcoming.style.display = 'none'; if (phaseLive) phaseLive.style.display = 'flex'; const stageDefs = [ { id: 'loyalty', label: 'Loyalty', price: 0 }, { id: 'gtd', label: 'GTD', price: 11000 }, { id: 'public', label: 'Public', price: 15000 }, ]; phaseLive.innerHTML = stageDefs.map(function (stage) { const active = stage.id === current; const priceLabel = stage.price === 0 ? 'FREE' : `${(stage.price / 1000).toFixed(0)}k`; return `${stage.label} · ${priceLabel}`; }).join(''); let countdownEl = document.getElementById('mintPhaseNextCountdown'); if (!countdownEl) { countdownEl = document.createElement('div'); countdownEl.id = 'mintPhaseNextCountdown'; countdownEl.className = 'mint-phase-upcoming-start'; countdownEl.style.marginTop = '6px'; phaseBar.appendChild(countdownEl); } if (schedule.nextPhaseAt) { const nextLabel = schedule.nextPhase === 'gtd' ? 'GTD' : schedule.nextPhase === 'public' ? 'Public' : 'Next phase'; countdownEl.textContent = `${nextLabel} in ${format5il5nc5Countdown(schedule.nextPhaseAt)}`; countdownEl.style.display = ''; } else { countdownEl.textContent = ''; countdownEl.style.display = 'none'; } } function apply5il5nc5PhasePrice(schedule) { if (!schedule || currentArtworkSlug !== FIVE_IL5NC5_MINT_SLUG) return; if (!window.currentArtwork) return; if (schedule.priceSats == null) return; window.currentArtwork.price = schedule.priceSats; const artworkPrice = document.getElementById('artwork-price'); if (artworkPrice) { artworkPrice.textContent = schedule.priceSats === 0 ? 'FREE' : `${schedule.priceSats.toLocaleString()} SATS`; } if (typeof window.updateBulkMintPrice === 'function') { window.updateBulkMintPrice(); } else if (typeof window.updateCostCalculation === 'function') { window.updateCostCalculation(); } } async function refresh5il5nc5PhaseUi() { if (currentArtworkSlug !== FIVE_IL5NC5_MINT_SLUG || currentPage !== 'mint') return; try { let url = '/api/5il5nc5-mint-phase'; if (walletConnected && window.walletData && window.walletData.ordinalsAddress) { url += `?wallet=${encodeURIComponent(window.walletData.ordinalsAddress)}`; } const response = await fetch(url); if (!response.ok) return; const data = await response.json(); if (!data.success || !data.schedule) return; window.fiveIl5nc5Schedule = data.schedule; sync5il5nc5PhaseBar(data.schedule); apply5il5nc5PhasePrice(data.schedule); if (walletConnected && data.wallet && !adminMintBypassActive()) { const qtyInput = document.getElementById('mint-quantity'); if (qtyInput && data.wallet.whitelistDetails && data.wallet.whitelistDetails.remainingMints != null) { const cap = Math.max(1, data.wallet.whitelistDetails.remainingMints); qtyInput.setAttribute('max', cap); qtyInput.value = Math.min(parseInt(qtyInput.value, 10) || 1, cap); } else if (qtyInput && data.wallet.phase === 'public') { qtyInput.removeAttribute('max'); } } } catch (e) { console.warn('refresh5il5nc5PhaseUi:', e); } } function start5il5nc5PhasePoller() { stop5il5nc5PhasePoller(); if (currentArtworkSlug !== FIVE_IL5NC5_MINT_SLUG) return; refresh5il5nc5PhaseUi(); fiveIl5nc5PhasePollTimer = setInterval(refresh5il5nc5PhaseUi, 1000); } function syncMintPageUpcomingUi(artwork, options) { const opts = options || {}; const is5il5nc5 = currentArtworkSlug === FIVE_IL5NC5_MINT_SLUG; if (is5il5nc5 && window.fiveIl5nc5Schedule) { sync5il5nc5PhaseBar(window.fiveIl5nc5Schedule); } const soldOut = !!opts.soldOut; const isFiveIl5nc5Upcoming = is5il5nc5 && window.fiveIl5nc5Schedule && window.fiveIl5nc5Schedule.currentPhase === 'upcoming'; const upcoming = !soldOut && !adminMintBypassActive() && (isFiveIl5nc5Upcoming || isArtworkUpcoming(artwork)); window.mintPageUpcoming = upcoming; const phaseBar = document.getElementById('mintPhaseBar'); const phaseLive = document.getElementById('mintPhaseBarLive'); const phaseUpcoming = document.getElementById('mintPhaseBarUpcoming'); const phaseStart = document.getElementById('mintPhaseStartLabel'); const feeStep = document.getElementById('feeSelectionStep'); const costStep = document.getElementById('costDisplayStep'); const qtySel = document.getElementById('quantity-selector'); const physStep = document.getElementById('physicalPrintStep'); if (upcoming) { if (phaseBar && !is5il5nc5) { phaseBar.setAttribute('aria-hidden', 'false'); phaseBar.style.display = ''; } if (!is5il5nc5 && phaseLive) phaseLive.style.display = 'none'; if (!is5il5nc5 && phaseUpcoming) phaseUpcoming.style.display = 'flex'; if (!is5il5nc5 && phaseStart) { const line = formatMintStartForCard(artwork && artwork.mintStartDatetime); phaseStart.textContent = line ? `Starts ${line}` : 'Start time to be announced'; } if (feeStep) feeStep.style.display = 'none'; if (costStep) costStep.style.display = 'none'; if (qtySel) qtySel.style.display = 'none'; if (physStep) physStep.style.display = 'none'; const mintStepEl = document.getElementById('mintStep'); if (mintStepEl) mintStepEl.style.display = 'block'; } else { if (phaseBar && !is5il5nc5) { phaseBar.setAttribute('aria-hidden', 'true'); } if (!is5il5nc5 && phaseLive) phaseLive.style.display = ''; if (!is5il5nc5 && phaseUpcoming) phaseUpcoming.style.display = 'none'; if (!is5il5nc5 && phaseStart) phaseStart.textContent = ''; } } async function restoreMintStepsAfterArtworkLoadIfWalletReady() { if (window.mintPageUpcoming || window.mintPageSoldOut) return; const mintStepEl = document.getElementById('mintStep'); const feeStep = document.getElementById('feeSelectionStep'); const costStep = document.getElementById('costDisplayStep'); if (!mintStepEl || !feeStep || !costStep) return; if (!walletConnected || !window.walletData) { mintStepEl.style.display = 'none'; feeStep.style.display = 'none'; costStep.style.display = 'none'; return; } try { const isWhitelisted = await checkWhitelistEligibility(); if (isWhitelisted) { const isOnDemand = window.currentArtwork && (window.currentArtwork.mintType === 'inscribe_on_demand' || window.currentArtwork.mintType === 'parent_child_prints'); feeStep.style.display = isOnDemand ? 'none' : 'block'; costStep.style.display = 'block'; const lowFeeDisclaimer = document.getElementById('low-fee-disclaimer'); if (lowFeeDisclaimer) lowFeeDisclaimer.style.display = isOnDemand ? 'block' : 'none'; await updateCostCalculation(); await loadTestImageData(); mintStepEl.style.display = 'block'; } else { feeStep.style.display = 'none'; costStep.style.display = 'none'; mintStepEl.style.display = 'none'; } } catch (e) { console.warn('restoreMintStepsAfterArtworkLoadIfWalletReady:', e); } } function mintTypeSupportsEnableGalleryGrid(mt) { return mt === 'pre_inscribed' || mt === 'pre_inscribed_v2' || mt === 'inscribe_on_demand' || mt === 'numerical'; } function mintGalleryModalOnKeydown(e) { if (e.key === 'Escape') closeMintGalleryModal(); } function closeMintGalleryModal() { const m = document.getElementById('mint-enable-gallery-modal'); if (!m) return; m.hidden = true; m.setAttribute('aria-hidden', 'true'); document.body.classList.remove('mint-gallery-modal-open'); const b = m.querySelector('.mint-enable-gallery-modal-body'); if (b) b.innerHTML = ''; } function ensureMintGalleryModal() { if (document.getElementById('mint-enable-gallery-modal')) return; const wrap = document.createElement('div'); wrap.id = 'mint-enable-gallery-modal'; wrap.className = 'mint-enable-gallery-modal'; wrap.setAttribute('hidden', ''); wrap.setAttribute('aria-hidden', 'true'); wrap.innerHTML = '' + ''; document.body.appendChild(wrap); wrap.addEventListener('click', function (ev) { if (ev.target.closest('[data-mg-modal-dismiss]')) closeMintGalleryModal(); }); wrap.querySelector('.mint-enable-gallery-modal-close').addEventListener('click', function (ev) { ev.stopPropagation(); closeMintGalleryModal(); }); if (!window._mintGalleryModalKeydownBound) { document.addEventListener('keydown', mintGalleryModalOnKeydown); window._mintGalleryModalKeydownBound = true; } } function openMintGalleryModalFromItem(item) { const kind = item.getAttribute('data-mg-kind'); if (!kind) return; const src = item.getAttribute('data-mg-src'); const label = item.getAttribute('data-mg-label'); ensureMintGalleryModal(); const m = document.getElementById('mint-enable-gallery-modal'); const modalBody = m.querySelector('.mint-enable-gallery-modal-body'); modalBody.innerHTML = ''; if (kind === 'ord') { const im = document.createElement('img'); im.className = 'mint-enable-gallery-modal-img'; im.src = src || ''; im.alt = ''; im.onerror = function () { im.onerror = null; const ifr = document.createElement('iframe'); ifr.className = 'mint-enable-gallery-modal-frame'; ifr.setAttribute('sandbox', 'allow-scripts allow-same-origin'); ifr.src = src || ''; ifr.title = 'Gallery preview'; modalBody.innerHTML = ''; modalBody.appendChild(ifr); }; modalBody.appendChild(im); } else if (kind === 'html') { const ifr = document.createElement('iframe'); ifr.className = 'mint-enable-gallery-modal-frame'; ifr.setAttribute('sandbox', 'allow-scripts allow-same-origin'); ifr.src = src || ''; ifr.title = 'Gallery preview'; modalBody.appendChild(ifr); } else if (kind === 'img') { const im = document.createElement('img'); im.className = 'mint-enable-gallery-modal-img'; im.src = src || ''; im.alt = ''; im.onerror = function () { im.style.opacity = '0.4'; }; modalBody.appendChild(im); } else { const d = document.createElement('div'); d.className = 'mint-enable-gallery-modal-numerical'; d.textContent = label || ''; modalBody.appendChild(d); } m.hidden = false; m.setAttribute('aria-hidden', 'false'); document.body.classList.add('mint-gallery-modal-open'); } function teardownMintEnableGallery() { closeMintGalleryModal(); const modal = document.getElementById('mint-enable-gallery-modal'); if (modal && modal.parentNode) modal.parentNode.removeChild(modal); if (window._mintGalleryModalKeydownBound) { document.removeEventListener('keydown', mintGalleryModalOnKeydown); window._mintGalleryModalKeydownBound = false; } window.mintEnableGalleryState = null; const root = document.getElementById('mint-enable-gallery-root'); if (root) root.remove(); const main = document.querySelector('.main-layout.mint-redesign'); if (main) main.classList.remove('mint-has-enable-gallery'); } let _mintAccessQueuePoll = null; let _mintAccessQueueHeartbeat = null; window._mintAccessQueueRelease = null; function teardownMintAccessQueue() { if (_mintAccessQueuePoll) { clearInterval(_mintAccessQueuePoll); _mintAccessQueuePoll = null; } if (_mintAccessQueueHeartbeat) { clearInterval(_mintAccessQueueHeartbeat); _mintAccessQueueHeartbeat = null; } if (typeof window._mintAccessQueueRelease === 'function') { try { window._mintAccessQueueRelease(); } catch (e) { /* ignore */ } window._mintAccessQueueRelease = null; } const ov = document.getElementById('mint-access-queue-overlay'); if (ov) ov.remove(); try { delete window._lunalauncherMintQueueToken; } catch (e) { /* ignore */ } } const MINT_QUEUE_SESSION_HEADER = 'X-Mint-Queue-Session'; /** Match mchexley-pizza-comrades mint: solid #FE5403 + pizza_comrade.png (same as #app .main-layout). */ const MINT_QUEUE_OVERLAY_STYLE = 'position:fixed;inset:0;z-index:100040;background-color:#FE5403;color:#1a1a1a;display:flex;align-items:center;justify-content:flex-start;flex-direction:column;gap:0;padding:0;padding-bottom:max(24px,env(safe-area-inset-bottom));padding-top:max(12px,env(safe-area-inset-top));text-align:center;font-family:var(--ll-sans,Inter,system-ui,sans-serif);overflow:auto;box-sizing:border-box;'; const MINT_QUEUE_OVERLAY_HTML = '' + '
    ' + '' + '
    ' + '
    ' + '

    You’re in line

    ' + '

    Joining the queue…

    ' + '

    Stay on this page until it’s your turn. Do not refresh — if you refresh, you leave the line and join again at the back of the queue.

    ' + '
    '; function mintQueueStatusLine(j) { const ahead = Number(j.waitingAhead) || 0; const hb = Math.max(15, Number(j.heartbeatSeconds) || 45); const estMin = ahead > 0 ? Math.max(1, Math.ceil(ahead * 2)) : Math.max(2, Math.ceil(hb / 22)); const minWord = estMin === 1 ? 'minute' : 'minutes'; if (ahead === 1) { return `There is 1 person ahead of you. Estimated wait: about ${estMin} ${minWord}.`; } if (ahead > 1) { return `There are ${ahead} people ahead of you. Estimated wait: about ${estMin} ${minWord}.`; } return `You’re next in line. Estimated wait: about ${estMin} ${minWord}.`; } function mintQueueStorageKey(slug) { return 'mint_q_sess_' + (slug || ''); } /** localStorage is shared across tabs on this origin; sessionStorage is per-tab and caused duplicate queue sessions. */ function mintQueueReadPersistedToken(key) { try { return localStorage.getItem(key) || sessionStorage.getItem(key) || ''; } catch (e) { return ''; } } function mintQueueWritePersistedToken(key, t) { if (!t) return; try { localStorage.setItem(key, t); try { sessionStorage.removeItem(key); } catch (e2) { /* ignore */ } } catch (e) { try { sessionStorage.setItem(key, t); } catch (e2) { /* ignore */ } } } function mintQueueClearPersistedToken(key) { if (!key) return; try { localStorage.removeItem(key); } catch (e) { /* ignore */ } try { sessionStorage.removeItem(key); } catch (e2) { /* ignore */ } } /** True when this page load was triggered by a reload (F5 / refresh). */ function mintQueueNavigationWasReload() { try { const nav = performance.getEntriesByType && performance.getEntriesByType('navigation')[0]; if (nav && nav.type === 'reload') return true; } catch (e) { /* ignore */ } try { if (typeof performance.navigation !== 'undefined' && performance.navigation.type === 1) return true; } catch (e2) { /* ignore */ } return false; } function getMintQueueSessionTokenForSlug(slug) { return mintQueueReadPersistedToken(mintQueueStorageKey(slug || window.currentArtworkSlug)); } function mountMintQueueOverlayIfMissing() { if (document.getElementById('mint-access-queue-overlay')) return; const overlay = document.createElement('div'); overlay.id = 'mint-access-queue-overlay'; overlay.setAttribute('role', 'status'); overlay.setAttribute('aria-live', 'polite'); overlay.style.cssText = MINT_QUEUE_OVERLAY_STYLE; overlay.innerHTML = MINT_QUEUE_OVERLAY_HTML; document.body.appendChild(overlay); } /** In-memory copy so mint fetches always see the token even if slug/key differs from sessionStorage. */ function getMintQueueTokenForRequests(slug) { try { if (window._lunalauncherMintQueueToken) return window._lunalauncherMintQueueToken; } catch (e) { /* ignore */ } return getMintQueueSessionTokenForSlug(slug); } function mintQueueFetchHeaders(slug) { const t = getMintQueueTokenForRequests(slug || window.currentArtworkSlug); const h = {}; if (t) h[MINT_QUEUE_SESSION_HEADER] = t; return h; } /** Adds X-Mint-Queue-Session + sessionToken on JSON POST when mint queue session exists */ function withMintQueueFetchInit(slug, init) { const t = getMintQueueTokenForRequests(slug || window.currentArtworkSlug); if (!t) return init || {}; const out = Object.assign({}, init); out.headers = Object.assign({}, (init && init.headers) || {}); out.headers[MINT_QUEUE_SESSION_HEADER] = t; if (out.body != null && typeof out.body === 'string') { try { const o = JSON.parse(out.body); if (o && typeof o === 'object' && o.sessionToken == null) o.sessionToken = t; out.body = JSON.stringify(o); } catch (e) { /* not JSON */ } } return out; } /** * Blocks mint UI until this browser holds an active slot (max concurrent browsers per artwork). * Token is localStorage + X-Mint-Queue-Session (shared across tabs on this device; not cookies). * Full page reload leaves the server queue and clears the token so the user rejoins at the back. * Requires artwork.mintQueueEnabled; skipped for admin preloaded previews. */ async function mintAccessQueueGate(slug) { // Same origin as /api/record-mint and other mint handlers. Do not use getReadApiUrl here: // on localhost that points at production, so the queue session lives in prod D1 while mints hit local D1 and always fail admission. const url = '/api/mint-access-queue'; const storageKey = 'mint_q_sess_' + slug; if (mintQueueNavigationWasReload()) { const prevTok = mintQueueReadPersistedToken(storageKey); if (prevTok) { try { await fetch(url, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', [MINT_QUEUE_SESSION_HEADER]: prevTok, }, body: JSON.stringify({ slug, action: 'leave', sessionToken: prevTok, }), }); } catch (e) { /* still drop client token so the next sync joins at the back */ } mintQueueClearPersistedToken(storageKey); try { delete window._lunalauncherMintQueueToken; } catch (e2) { /* ignore */ } } } teardownMintAccessQueue(); mountMintQueueOverlayIfMissing(); const getTok = () => mintQueueReadPersistedToken(storageKey); /** Always persist server sessionToken so after a server-side token rotation (e.g. expired→new row) the client tracks it. */ const setTok = (t) => { if (!t) return; mintQueueWritePersistedToken(storageKey, t); try { window._lunalauncherMintQueueToken = t; } catch (e) { /* ignore */ } }; const post = async (extra) => { const t = getTok(); const headers = { 'Content-Type': 'application/json' }; if (t) headers['X-Mint-Queue-Session'] = t; const body = Object.assign({ slug }, extra || {}, t ? { sessionToken: t } : {}); const r = await fetch(url, { method: 'POST', credentials: 'include', headers, body: JSON.stringify(body), }); const j = await r.json(); if (j && j.sessionToken) setTok(j.sessionToken); return j; }; const msg = () => document.getElementById('mint-access-queue-msg'); const onAdmitted = (j) => { if (_mintAccessQueuePoll) { clearInterval(_mintAccessQueuePoll); _mintAccessQueuePoll = null; } const ov = document.getElementById('mint-access-queue-overlay'); if (ov) ov.remove(); const hbSec = j.heartbeatSeconds || 45; const intervalMs = Math.min(30000, Math.max(10000, Math.floor((hbSec / 2) * 1000))); const admissionHeartbeat = async () => { try { const hj = await post({}); if (!hj || !hj.success) return; if (hj.queueDisabled) return; if (hj.admitted) return; if (_mintAccessQueueHeartbeat) { clearInterval(_mintAccessQueueHeartbeat); _mintAccessQueueHeartbeat = null; } mountMintQueueOverlayIfMissing(); if (!_mintAccessQueuePoll) { _mintAccessQueuePoll = setInterval(tick, 3000); } await tick(); } catch (e) { /* ignore */ } }; _mintAccessQueueHeartbeat = setInterval(admissionHeartbeat, intervalMs); window._mintAccessQueueRelease = function () { if (_mintAccessQueueHeartbeat) { clearInterval(_mintAccessQueueHeartbeat); _mintAccessQueueHeartbeat = null; } const lt = getTok(); const leaveHeaders = { 'Content-Type': 'application/json' }; if (lt) leaveHeaders['X-Mint-Queue-Session'] = lt; fetch(url, { method: 'POST', credentials: 'include', headers: leaveHeaders, body: JSON.stringify( Object.assign({ slug, action: 'leave' }, lt ? { sessionToken: lt } : {}) ), keepalive: true, }).catch(() => {}); }; }; const tick = async () => { try { const j = await post({}); if (!j.success) { const el = msg(); if (el) el.textContent = j.error || 'Could not join queue. Retrying…'; return; } if (j.queueDisabled) { if (_mintAccessQueuePoll) { clearInterval(_mintAccessQueuePoll); _mintAccessQueuePoll = null; } const qov = document.getElementById('mint-access-queue-overlay'); if (qov) qov.remove(); return; } if (j.admitted) { onAdmitted(j); return; } const el = msg(); if (el) el.textContent = mintQueueStatusLine(j); } catch (e) { const el = msg(); if (el) el.textContent = 'Connection issue. Retrying…'; } }; await tick(); if (!document.getElementById('mint-access-queue-overlay')) return; _mintAccessQueuePoll = setInterval(tick, 3000); } /** Ordinal gallery thumb: use img (works for image inscriptions); on fail swap to iframe (HTML/recursive). */ function mintGalleryOrdThumbFallback(img) { if (!img || img.getAttribute('data-ord-upgraded') === '1') return; img.setAttribute('data-ord-upgraded', '1'); const src = img.getAttribute('src'); const title = img.getAttribute('title') || ''; const ifr = document.createElement('iframe'); ifr.className = 'mint-enable-gallery-frame'; ifr.setAttribute('sandbox', 'allow-scripts allow-same-origin'); ifr.src = src || ''; ifr.title = title; img.replaceWith(ifr); } // Inline onerror= runs in global scope; module scripts do not create globals. window.mintGalleryOrdThumbFallback = mintGalleryOrdThumbFallback; function renderMintEnableGalleryCell(insc) { const idx = insc.index != null ? insc.index : ''; let cls = 'available'; if (insc.isSold) cls = 'sold'; else if (insc.isReserved) cls = 'reserved'; const titleBit = insc.isSold ? 'Minted' : insc.isReserved ? 'Held' : 'Available'; const dataOrd = insc.inscriptionId ? ` data-ordinal="${escapeHtmlAttr(insc.inscriptionId)}"` : ''; let inner = ''; let mgAttrs = ''; if (insc.inscriptionId) { const srcOrd = 'https://ordinals.com/content/' + insc.inscriptionId; mgAttrs = ` data-mg-kind="ord" data-mg-src="${escapeHtmlAttr(srcOrd)}"`; inner = ''; } else if (insc.imageFilename) { const low = String(insc.imageFilename).toLowerCase(); let u = getReadApiAssetUrl(`/api/get-collection-image?filename=${encodeURIComponent(insc.imageFilename)}`); if (low.endsWith('.html') || low.endsWith('.htm')) { u += (u.includes('?') ? '&' : '?') + 'rewrite_content=1'; mgAttrs = ` data-mg-kind="html" data-mg-src="${escapeHtmlAttr(u)}"`; inner = ``; } else { mgAttrs = ` data-mg-kind="img" data-mg-src="${escapeHtmlAttr(u)}"`; inner = ``; } } else { const numLabel = '#' + String(idx); mgAttrs = ` data-mg-kind="num" data-mg-label="${escapeHtmlAttr(numLabel)}"`; inner = ``; } return ``; } function attachMintEnableGalleryGridClick(grid) { if (!grid) return; grid.onclick = function (e) { if (e.target.closest('.mint-enable-gallery-pag-btn')) return; const item = e.target.closest('.mint-enable-gallery-item'); if (!item) return; const id = item.getAttribute('data-ordinal'); if (e.metaKey || e.ctrlKey) { if (id) window.open('https://ordinals.com/inscription/' + id, '_blank'); return; } openMintGalleryModalFromItem(item); }; } async function loadMintEnableGalleryPage(page) { const st = window.mintEnableGalleryState; if (!st || !st.slug) return; const grid = document.getElementById('mint-enable-gallery-grid'); const statsEl = document.getElementById('mint-enable-gallery-stats'); const pagEl = document.getElementById('mint-enable-gallery-pagination'); if (!grid) return; const p = Math.max(1, parseInt(page, 10) || 1); grid.innerHTML = ''; if (pagEl) pagEl.innerHTML = ''; try { const url = getReadApiUrl(`/api/get-pre-inscribed-gallery?slug=${encodeURIComponent(st.slug)}&page=${p}`); const res = await fetch(url, { cache: 'no-store' }); const data = await res.json(); if (!data.success) { grid.innerHTML = ''; if (statsEl) statsEl.textContent = ''; return; } st.page = data.pagination && data.pagination.page != null ? data.pagination.page : p; if (statsEl && data.stats) { const s = data.stats; let line = `${s.sold != null ? s.sold : 0} minted / ${s.available != null ? s.available : 0} available / ${s.total != null ? s.total : 0} total`; if (s.reserved > 0) line += ` (${s.reserved} held)`; statsEl.textContent = line; } if (!data.inscriptions || data.inscriptions.length === 0) { grid.innerHTML = ''; } else { grid.innerHTML = data.inscriptions.map(renderMintEnableGalleryCell).join(''); attachMintEnableGalleryGridClick(grid); } const pg = data.pagination || {}; const cur = pg.page || p; const tp = pg.totalPages || 1; const hasPrev = !!pg.hasPrev; const hasNext = !!pg.hasNext; if (pagEl) { pagEl.innerHTML = '' + 'Page ' + cur + ' of ' + tp + '' + ''; pagEl.querySelectorAll('[data-mg-p]').forEach(function (btn) { btn.addEventListener('click', function () { const np = parseInt(btn.getAttribute('data-mg-p'), 10); if (!Number.isNaN(np)) loadMintEnableGalleryPage(np); }); }); } } catch (err) { console.error('mint enable gallery:', err); grid.innerHTML = ''; } } function setupMintEnableGalleryIfNeeded(slug, artwork) { teardownMintEnableGallery(); if (slug === EMPRESS_TRASH_LOTUS_MINT_SLUG) return; if (!slug || !artwork || !artwork.enableGallery) return; if (!mintTypeSupportsEnableGalleryGrid(artwork.mintType)) return; const main = document.querySelector('.main-layout.mint-redesign'); if (!main) return; main.classList.add('mint-has-enable-gallery'); const root = document.createElement('section'); root.id = 'mint-enable-gallery-root'; root.className = 'mint-enable-gallery-root'; root.setAttribute('aria-label', 'Collection gallery'); root.innerHTML = '' + '' + '' + ''; const leftCol = main.querySelector('.left-column'); const artPanel = leftCol && leftCol.querySelector('.mint-art-panel'); if (artPanel) { artPanel.insertAdjacentElement('afterend', root); } else if (leftCol) { leftCol.appendChild(root); } else { main.appendChild(root); } window.mintEnableGalleryState = { slug: slug }; ensureMintGalleryModal(); loadMintEnableGalleryPage(1); } function collectionOrdinalsAlt(titleOrName, artistLabel) { const artist = String(artistLabel || 'Unknown').replace(/^@+/, '').trim() || 'Unknown'; const name = String(titleOrName || 'Collection').trim() || 'Collection'; return escapeHtml(`${name} by ${artist} — Bitcoin Ordinals`); } function getArtworkPreviewMarkupForDrops(artwork) { const mediaLabel = collectionOrdinalsAlt(artwork.title, artwork.artist); // Custom thumbnail override for artwork 513 if (artwork.id === 513) { return `${mediaLabel}`; } if (artwork.mintType === 'pre_inscribed' || artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints' || artwork.mintType === 'numerical') { const r2Key = artwork.imageFilename; if (r2Key && r2Key.toLowerCase().endsWith('.html')) { return ``; } return `${mediaLabel}`; } if (artwork.parentType === 'recursive') { return ``; } if (artwork.parentInscriptionId) { return ``; } return '
    No Preview
    '; } function renderLiveDropsGrid(gridId, limit = null) { const grid = document.getElementById(gridId); if (!grid) return; const liveArtworks = getSortedLiveArtworksForHome(); if (!liveArtworks.length) { grid.innerHTML = '

    No live launches available

    '; return; } const artworkItems = gridId === 'live-drops-grid' && limit === 3 ? getHomeSpotlightDropsArtworks(3) : typeof limit === 'number' ? liveArtworks.slice(0, limit) : liveArtworks; grid.innerHTML = artworkItems.map((artwork) => { const total = artwork.total === -1 ? null : Number(artwork.total || 0); const minted = Number(artwork.minted || 0); const remaining = total === null ? '∞' : Math.max(total - minted, 0).toLocaleString(); const progress = total && total > 0 ? Math.min(100, (minted / total) * 100) : 0; const priceText = artwork.price === 0 ? 'FREE' : `${Number(artwork.price || 0).toLocaleString()} SATS`; const title = escapeHtml(artwork.title || 'Untitled'); const artistRaw = (artwork.artist || 'Unknown').replace(/^@+/, ''); const artist = escapeHtml(artistRaw); const imageMarkup = getArtworkPreviewMarkupForDrops(artwork); const isHot = !isArtworkUpcoming(artwork) && progress >= 90 && total && total > 0; const description = escapeHtml((artwork.description || 'Live launch on Bitcoin ordinals.').slice(0, 180)); const safeSlug = String(artwork.slug || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'"); const mintHref = escapeHtmlAttr(getArtworkUrl(artwork.slug)); const mintAria = escapeHtmlAttr(`Mint: ${artwork.title || 'Untitled'}`); const isUpcoming = isArtworkUpcoming(artwork); const startLine = isUpcoming ? formatMintStartForCard(artwork.mintStartDatetime) : ''; const badgeHtml = isUpcoming ? '
    Upcoming
    ' : '
    Live Now
    '; const startRowHtml = startLine ? `
    Starts ${escapeHtml(startLine)}
    ` : (isUpcoming ? '
    Minting soon
    ' : ''); const footCta = isUpcoming ? 'Minting soon' : `Mint Now`; const launchArtOpen = isUpcoming ? `' : ''; return `
    ${launchArtOpen} ${imageMarkup} ${badgeHtml} ${isHot ? '
    Hot
    ' : ''} ${launchArtClose}
    ${artist}
    ${title}
    ${description}
    ${remaining}Available
    ${minted.toLocaleString()}Minted
    ${priceText}Price
    ${startRowHtml}
    ${total === null ? `${minted.toLocaleString()} minted` : `${minted.toLocaleString()} / ${total.toLocaleString()} minted`}${remaining} remaining

    ${priceText}

    per inscription

    ${footCta}
    `; }).join(''); } function loadLiveArtworks() { if (!window.allArtworks) { loadArtworksList(); return; } const grid = document.getElementById('artworks-grid-live'); if (!grid) return; const liveArtworks = getSortedLiveArtworksForHome(); const collections = window.allCollections || []; const isHomeHeroGrid = !!grid.closest('.ll-home-v2'); console.log('Loading live artworks:', liveArtworks.length, 'collections:', collections.length, 'home hero grid:', isHomeHeroGrid); // Only refresh stamps carousel when the carousel exists in the current layout if (document.getElementById('carousel-inner')) { loadLatestStampsForCarousel(); } if (isHomeHeroGrid) { const customOrder = Array.isArray(window.homeHeroGridSlugs) ? window.homeHeroGridSlugs.filter((s) => typeof s === 'string' && s.trim()) : []; const bySlug = new Map(liveArtworks.map((a) => [a.slug, a])); let heroItems; if (customOrder.length > 0) { heroItems = []; const used = new Set(); for (const slug of customOrder) { if (heroItems.length >= 12) break; const a = bySlug.get(slug); if (a) { heroItems.push(a); used.add(slug); } } for (const a of liveArtworks) { if (heroItems.length >= 12) break; if (!used.has(a.slug)) { heroItems.push(a); used.add(a.slug); } } } else { heroItems = liveArtworks.slice(0, 12); } renderHeroMintFeed(heroItems, 'artworks-grid-live'); renderLiveDropsGrid('live-drops-grid', 3); return; } // Create combined items array const combinedItems = []; // Add standalone live artworks (not in collections) const standaloneArtworks = liveArtworks.filter(artwork => !artwork.collectionSlug); standaloneArtworks.forEach(artwork => { combinedItems.push({ type: 'artwork', data: artwork }); }); // Add collections collections.forEach(collection => { combinedItems.push({ type: 'collection', data: collection }); }); // Sort by creation date (use latest_artwork_date for collections) combinedItems.sort((a, b) => { const dateA = new Date(a.type === 'collection' ? (a.data.latest_artwork_date || a.data.created_at) : a.data.createdAt || 0); const dateB = new Date(b.type === 'collection' ? (b.data.latest_artwork_date || b.data.created_at) : b.data.createdAt || 0); return dateB - dateA; }); // Display combined items displayCombinedItems(combinedItems, 'artworks-grid-live'); // Populate artist list for filters populateArtistList(); // Initialize filters after artworks are loaded setTimeout(() => { initializeFilters(); }, 100); } // Load latest minted stamps for carousel async function loadLatestStampsForCarousel() { try { const response = await fetch('/api/get-latest-stamps?limit=50'); const data = await response.json(); if (data.success && data.stamps && data.stamps.length > 0) { console.log('Loaded', data.stamps.length, 'stamps for carousel'); updateStampsCarousel(data.stamps); } else { console.log('No stamps found for carousel'); } } catch (error) { console.error('Error loading stamps for carousel:', error); } } // Update carousel with minted stamps function updateStampsCarousel(stamps) { const carouselInner = document.getElementById('carousel-inner'); if (!carouselInner || !stamps || stamps.length === 0) { console.log('Stamps Carousel: No carousel or stamps'); return; } const stampsOrdered = [...stamps].sort((a, b) => { const ta = a.created_at ? new Date(a.created_at).getTime() : 0; const tb = b.created_at ? new Date(b.created_at).getTime() : 0; const sa = Number.isFinite(ta) ? ta : 0; const sb = Number.isFinite(tb) ? tb : 0; return sb - sa; }); console.log('Updating carousel with', stampsOrdered.length, 'stamps'); // Function to generate stamp HTML - use actual inscription ID const generateStampHtml = (stamp) => { const stampId = stamp.stamp_inscription_id; return ` `; }; // Create the carousel items - duplicate for seamless looping const carouselItems = stampsOrdered.map(generateStampHtml).join(''); // Duplicate the items for infinite scroll effect carouselInner.innerHTML = carouselItems + carouselItems; console.log('Stamps carousel created with', stampsOrdered.length * 2, 'items (duplicated for infinite scroll)'); } // NEW: Update featured carousel (continuous scroll) - for artworks function updateCarousel(artworks) { const carouselInner = document.getElementById('carousel-inner'); if (!carouselInner || !artworks || artworks.length === 0) { console.log('Carousel: No carousel or artworks'); return; } console.log('Updating carousel with', artworks.length, 'artworks'); // Function to generate artwork HTML const generateArtworkHtml = (artwork) => { const progress = artwork.total > 0 ? (artwork.minted / artwork.total * 100).toFixed(0) : 0; const isSoldOut = artwork.soldOut; const isUpcomingCar = isArtworkUpcoming(artwork); const carMintLabel = isSoldOut ? 'VIEW' : (isUpcomingCar ? 'MINTING SOON' : 'MINT ARTWORK'); const carMintAction = isSoldOut ? "viewCollectionGallery('" + artwork.slug + "')" : isUpcomingCar ? 'void(0)' : "navigateToArtwork('" + artwork.slug + "')"; const carMintDisabled = isUpcomingCar && !isSoldOut ? ' disabled style="opacity:0.55;cursor:not-allowed;"' : ''; const carNavClick = isUpcomingCar ? '' : `onclick="navigateToArtwork('${artwork.slug}')"`; // Get preview image let imageHtml = ''; const carMediaLabel = collectionOrdinalsAlt(artwork.title, artwork.artist); // Custom thumbnail override for artwork 513 if (artwork.id === 513) { imageHtml = `${carMediaLabel}`; } else if (artwork.mintType === 'pre_inscribed' || artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints' || artwork.mintType === 'numerical') { const r2Key = artwork.imageFilename; if (r2Key && r2Key.toLowerCase().endsWith('.html')) { imageHtml = ``; } else { imageHtml = `${carMediaLabel}`; } } else if (artwork.parentType === 'recursive') { imageHtml = ``; } else if (artwork.parentInscriptionId) { imageHtml = ``; } return ` `; }; // Create the carousel items - duplicate the array for seamless looping const carouselItems = artworks.map(generateArtworkHtml).join(''); // Duplicate the items for infinite scroll effect carouselInner.innerHTML = carouselItems + carouselItems; console.log('Carousel created with', artworks.length * 2, 'items (duplicated for infinite scroll)'); } // NEW: Load all artworks (grouped by artist) function loadAllArtworks() { const grid = document.getElementById('artworks-grid-all'); if (!grid || !window.allArtworks) { if (grid) grid.innerHTML = '

    No artworks available

    '; return; } const artworks = window.allArtworks; // Group artworks by artist const artistGroups = {}; artworks.forEach(artwork => { const artist = artwork.artist || 'Unknown'; if (!artistGroups[artist]) { artistGroups[artist] = []; } artistGroups[artist].push(artwork); }); // Calculate creator points for each artist and sort by highest first const artistsWithPoints = Object.keys(artistGroups).map(artist => { const collections = artistGroups[artist]; let totalMints = 0; collections.forEach(collection => { totalMints += collection.minted || 0; }); return { name: artist, points: totalMints }; }); // Sort by creator points (highest first) const sortedArtists = artistsWithPoints .sort((a, b) => b.points - a.points) .map(artist => artist.name); // Build HTML for each artist group grid.innerHTML = sortedArtists.map(artist => { const collections = artistGroups[artist]; const totalCollections = collections.length; // Calculate stats let totalSatsRaised = 0; let totalMints = 0; let completedCount = 0; let ongoingCount = 0; collections.forEach(collection => { const minted = collection.minted || 0; const price = collection.price || 0; totalSatsRaised += minted * price; totalMints += minted; if (collection.soldOut) { completedCount++; } else { ongoingCount++; } }); return `

    ${artist}

    ${totalCollections} collection${totalCollections !== 1 ? 's' : ''} ${totalMints.toLocaleString()} mints ${totalSatsRaised.toLocaleString()} sats raised
    ${collections.map(artwork => { const progress = artwork.total > 0 ? (artwork.minted / artwork.total * 100).toFixed(0) : 0; const isSoldOut = artwork.soldOut; const isUpcomingGrp = isArtworkUpcoming(artwork); const grpMintLabel = isSoldOut ? 'VIEW' : (isUpcomingGrp ? 'MINTING SOON' : 'MINT'); const grpMintAction = isSoldOut ? "viewCollectionGallery('" + artwork.slug + "')" : isUpcomingGrp ? 'void(0)' : "navigateToArtwork('" + artwork.slug + "')"; const grpMintDisabled = isUpcomingGrp && !isSoldOut ? ' disabled style="opacity:0.55;cursor:not-allowed;"' : ''; const grpTileClick = isUpcomingGrp ? '' : ` onclick="if(window.innerWidth <= 1024) { navigateToArtwork('${artwork.slug}'); } else { handleArtworkClick(event, '${artwork.slug}'); }"`; let imageHtml = ''; const grpMediaLabel = collectionOrdinalsAlt(artwork.title, artwork.artist); if (artwork.mintType === 'pre_inscribed' || artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints' || artwork.mintType === 'numerical') { const r2Key = artwork.imageFilename; if (r2Key && r2Key.toLowerCase().endsWith('.html')) { imageHtml = ``; } else { imageHtml = `${grpMediaLabel}`; } } else if (artwork.parentType === 'recursive') { imageHtml = ``; } else if (artwork.parentInscriptionId) { imageHtml = ``; } else { imageHtml = `
    No Preview
    `; } return `
    ${imageHtml}
    ${artwork.title}
    ${artwork.minted} / ${artwork.total === -1 ? '∞' : artwork.total}
    ${artwork.price === 0 ? 'FREE' : artwork.price.toLocaleString() + ' SATS'}
    `; }).join('')}
    `; }).join(''); grid.style.display = 'grid'; grid.style.gridTemplateColumns = '1fr'; grid.style.gap = '20px'; } // NEW: Load collections view function loadCollectionsView() { const grid = document.getElementById('collections-grid'); if (!grid) return; if (!window.allCollections || window.allCollections.length === 0) { grid.innerHTML = '

    No collections available

    '; return; } console.log('Loading collections:', window.allCollections.length); // Convert collections to combined items format const combinedItems = window.allCollections.map(collection => ({ type: 'collection', data: collection })); // Display using combined items function displayCombinedItems(combinedItems, 'collections-grid'); } // NEW: Connect wallet for gallery (similar to stamps and points) let galleryWalletAddress = null; window.connectWalletForGallery = async function() { try { const connectBtn = document.querySelector('#gallery-content button'); if (connectBtn) { connectBtn.innerHTML = ' Connecting...'; connectBtn.disabled = true; } // Use payment address for gallery (same as points) const response = await request('getAccounts', { purposes: [AddressPurpose.Payment, AddressPurpose.Ordinals], message: 'Connect your wallet to view your minted artworks' }); if (response.status === 'success' && response.result && response.result.length > 0) { const paymentAddress = response.result.find(addr => addr.purpose === AddressPurpose.Payment); const ordinalsAddress = response.result.find(addr => addr.purpose === AddressPurpose.Ordinals); // Prefer payment address (same as points) const addressToUse = paymentAddress || ordinalsAddress; if (addressToUse) { galleryWalletAddress = addressToUse.address; // Load gallery artworks await loadGalleryArtworks(galleryWalletAddress); } else { throw new Error('No address found in wallet'); } } else { throw new Error('Wallet connection failed'); } } catch (error) { console.error('Error connecting wallet for gallery:', error); const connectBtn = document.querySelector('#gallery-content button'); if (connectBtn) { connectBtn.innerHTML = 'CONNECT WALLET'; connectBtn.disabled = false; } alert('Failed to connect wallet: ' + error.message); } }; // Load gallery artworks (similar to points page) async function loadGalleryArtworks(address) { try { const galleryContent = document.getElementById('gallery-content'); const galleryGrid = document.getElementById('gallery-grid'); // Show loading state if (galleryContent) { galleryContent.innerHTML = `

    YOUR GALLERY

    Connected: ${address.substring(0, 8)}...${address.substring(address.length - 8)}

    Loading your artworks...

    `; } // Fetch points data (which includes minted artworks) const response = await fetch(`/api/get-all-points?address=${encodeURIComponent(address)}`); const data = await response.json(); if (data.success && data.lunaPoints && data.lunaPoints.mints && data.lunaPoints.mints.length > 0) { const mintsList = data.lunaPoints.mints.map(mint => { // Determine image display based on mint type let imageHTML = ''; if ((mint.mintType === 'inscribe_on_demand' || mint.mintType === 'parent_child_prints') && mint.imageFilename) { if (mint.imageFilename.toLowerCase().endsWith('.html')) { imageHTML = ``; } else { imageHTML = ``; } } else if (mint.parentInscriptionId) { if (mint.parentType === 'recursive') { imageHTML = `
    `; } else { imageHTML = ``; } } else { imageHTML = `
    No Image
    `; } return `
    ${imageHTML}

    ${mint.title}

    by ${mint.artist}

    ${mint.count} minted

    `; }).join(''); if (galleryContent) { galleryContent.innerHTML = `

    YOUR GALLERY

    ${address.substring(0, 8)}...${address.substring(address.length - 8)}

    ${mintsList}
    `; } } else { if (galleryContent) { galleryContent.innerHTML = `

    YOUR GALLERY

    ${address.substring(0, 8)}...${address.substring(address.length - 8)}

    No artworks minted yet. Start minting!

    `; } } } catch (error) { console.error('Error loading gallery artworks:', error); const galleryContent = document.getElementById('gallery-content'); if (galleryContent) { galleryContent.innerHTML = `

    YOUR GALLERY

    Failed to load artworks

    `; } } } // Disconnect wallet for gallery window.disconnectWalletForGallery = function() { galleryWalletAddress = null; const galleryContent = document.getElementById('gallery-content'); const galleryGrid = document.getElementById('gallery-grid'); if (galleryContent) { galleryContent.innerHTML = `

    YOUR GALLERY

    Connect your wallet to view your minted artworks

    `; galleryContent.style.display = 'block'; } if (galleryGrid) { galleryGrid.style.display = 'none'; } }; // Flag to prevent double initialization let filtersInitialized = false; let freeMintsOnly = false; // Toggle free mints filter // Toggle mobile filters sidebar window.toggleMobileFilters = function() { const sidebar = document.querySelector('.filters-sidebar'); const overlay = document.querySelector('.mobile-filter-overlay'); if (sidebar && overlay) { sidebar.classList.toggle('active'); overlay.classList.toggle('active'); } }; window.toggleFreeMints = function() { freeMintsOnly = !freeMintsOnly; const btn = document.getElementById('free-mints-btn'); if (btn) { if (freeMintsOnly) { btn.style.background = '#fff'; btn.style.color = '#000'; btn.style.borderColor = '#fff'; } else { btn.style.background = '#000'; btn.style.color = '#fff'; btn.style.borderColor = '#fff'; } } applyFilters(); }; // NEW: Initialize filters function initializeFilters() { if (filtersInitialized) { console.log('Filters already initialized, skipping...'); return; } console.log('Initializing filters...'); filtersInitialized = true; // Price filter const priceMin = document.getElementById('price-min'); const priceMax = document.getElementById('price-max'); if (priceMin) { console.log('Adding price-min listener'); priceMin.addEventListener('input', () => { console.log('Price min changed to:', priceMin.value); applyFilters(); }); priceMin.addEventListener('change', applyFilters); } if (priceMax) { console.log('Adding price-max listener'); priceMax.addEventListener('input', () => { console.log('Price max changed to:', priceMax.value); applyFilters(); }); priceMax.addEventListener('change', applyFilters); } // Artist/Artwork search const artistSearch = document.getElementById('artist-search'); if (artistSearch) { console.log('Adding artist search listener'); artistSearch.addEventListener('input', () => { console.log('Search changed to:', artistSearch.value); // Update the artist list to show only matching artists populateArtistList(); applyFilters(); }); } } // Populate artist dropdown list function populateArtistList() { if (!window.allArtworks) { console.log('No allArtworks available for artist list'); return; } const artistList = document.getElementById('artist-list'); if (!artistList) { console.log('Artist list element not found'); return; } // Save current checkbox states const currentStates = {}; artistList.querySelectorAll('.artist-checkbox').forEach(cb => { currentStates[cb.dataset.artist] = cb.checked; }); // Get search term to filter artists const searchTerm = document.getElementById('artist-search')?.value?.toLowerCase() || ''; // Get unique artists and sort alphabetically let artists = [...new Set(window.allArtworks.map(a => a.artist))].filter(a => a).sort(); // Filter artists by search term if (searchTerm) { artists = artists.filter(artist => artist.toLowerCase().includes(searchTerm) || // Also show if any artwork title matches window.allArtworks.some(a => a.artist === artist && a.title?.toLowerCase().includes(searchTerm) ) ); } console.log('Populating artist list with', artists.length, 'artists (filtered from search)'); // Show message if no artists match if (artists.length === 0) { artistList.innerHTML = '
    No artists match your search
    '; return; } artistList.innerHTML = artists.map(artist => { const safeId = artist.replace(/[^a-zA-Z0-9]/g, '_'); const isChecked = currentStates[artist] ? 'checked' : ''; return `
    `; }).join(''); // Add event listeners to artist checkboxes const checkboxes = artistList.querySelectorAll('.artist-checkbox'); console.log('Adding event listeners to', checkboxes.length, 'checkboxes'); checkboxes.forEach(cb => { cb.addEventListener('change', () => { console.log('Artist checkbox changed:', cb.dataset.artist, cb.checked); applyFilters(); }); }); } // NEW: Apply filters to current view function applyFilters() { console.log('Applying filters...'); if (!window.allArtworks) { console.log('No allArtworks available'); return; } let filteredArtworks = [...window.allArtworks]; console.log('Starting with', filteredArtworks.length, 'artworks'); // Apply free mints filter first if (freeMintsOnly) { filteredArtworks = filteredArtworks.filter(a => (a.price || 0) === 0); console.log('After free mints filter:', filteredArtworks.length); } else { // Apply price filter only if not in free mints mode const priceMin = document.getElementById('price-min')?.value; const priceMax = document.getElementById('price-max')?.value; console.log('Price filter:', priceMin, 'to', priceMax); if (priceMin !== '' && priceMin !== null && priceMin !== undefined && priceMin !== '0') { const minPrice = parseInt(priceMin); filteredArtworks = filteredArtworks.filter(a => (a.price || 0) >= minPrice); console.log('After min price filter:', filteredArtworks.length); } if (priceMax !== '' && priceMax !== null && priceMax !== undefined) { const maxPrice = parseInt(priceMax); filteredArtworks = filteredArtworks.filter(a => (a.price || 0) <= maxPrice); console.log('After max price filter:', filteredArtworks.length); } } // Apply artist/artwork search const artistSearch = document.getElementById('artist-search')?.value?.toLowerCase(); if (artistSearch) { console.log('Search filter:', artistSearch); filteredArtworks = filteredArtworks.filter(a => a.artist?.toLowerCase().includes(artistSearch) || a.title?.toLowerCase().includes(artistSearch) ); console.log('After search filter:', filteredArtworks.length); } // Apply selected artists from dropdown const selectedArtists = []; const artistCheckboxes = document.querySelectorAll('.artist-checkbox:checked'); artistCheckboxes.forEach(cb => { selectedArtists.push(cb.dataset.artist); }); if (selectedArtists.length > 0) { console.log('Selected artists:', selectedArtists); filteredArtworks = filteredArtworks.filter(a => selectedArtists.includes(a.artist)); console.log('After artist filter:', filteredArtworks.length); } // Update the current view if (currentMainTab === 'live') { // For live tab, only show non-sold-out items const liveFiltered = filteredArtworks.filter(a => !a.soldOut); console.log('Live filtered:', liveFiltered.length); // Filter collections based on same criteria let collections = window.allCollections || []; console.log('Starting with', collections.length, 'collections'); // Apply search filter to collections if (artistSearch) { collections = collections.filter(c => c.name?.toLowerCase().includes(artistSearch) || c.artist_name?.toLowerCase().includes(artistSearch) ); console.log('After search filter:', collections.length, 'collections'); } // Apply selected artists filter to collections if (selectedArtists.length > 0) { collections = collections.filter(c => selectedArtists.includes(c.artist_name)); console.log('After artist filter:', collections.length, 'collections'); } // Apply price filter to collections (if they have a price field) if (!freeMintsOnly) { const priceMin = document.getElementById('price-min')?.value; const priceMax = document.getElementById('price-max')?.value; if (priceMin !== '' && priceMin !== null && priceMin !== undefined && priceMin !== '0') { const minPrice = parseInt(priceMin); collections = collections.filter(c => (c.price || 0) >= minPrice); console.log('After min price filter:', collections.length, 'collections'); } if (priceMax !== '' && priceMax !== null && priceMax !== undefined) { const maxPrice = parseInt(priceMax); collections = collections.filter(c => (c.price || 0) <= maxPrice); console.log('After max price filter:', collections.length, 'collections'); } } else { // Free mints only - filter collections with price 0 collections = collections.filter(c => (c.price || 0) === 0); console.log('After free mints filter:', collections.length, 'collections'); } const combinedItems = []; // Add standalone artworks const standaloneArtworks = liveFiltered.filter(artwork => !artwork.collectionSlug); standaloneArtworks.forEach(artwork => { combinedItems.push({ type: 'artwork', data: artwork }); }); // Add filtered collections collections.forEach(collection => { combinedItems.push({ type: 'collection', data: collection }); }); console.log('Combined items:', combinedItems.length, '(artworks:', standaloneArtworks.length, 'collections:', collections.length, ')'); // Sort by creation date (use latest_artwork_date for collections) combinedItems.sort((a, b) => { const dateA = new Date(a.type === 'collection' ? (a.data.latest_artwork_date || a.data.created_at) : a.data.createdAt || 0); const dateB = new Date(b.type === 'collection' ? (b.data.latest_artwork_date || b.data.created_at) : b.data.createdAt || 0); return dateB - dateA; }); displayCombinedItems(combinedItems, 'artworks-grid-live'); // The stamps carousel is independent of filters - it shows minted stamps // It was already loaded at page load via loadLatestStampsForCarousel() } else if (currentMainTab === 'all') { // For all artworks tab, display all filtered artworks const grid = document.getElementById('artworks-grid-all'); if (!grid) return; if (filteredArtworks.length === 0) { grid.innerHTML = '

    No artworks match your filters

    '; return; } // Group artworks by artist const artistGroups = {}; filteredArtworks.forEach(artwork => { const artist = artwork.artist || 'Unknown'; if (!artistGroups[artist]) { artistGroups[artist] = []; } artistGroups[artist].push(artwork); }); // Calculate creator points for each artist and sort by highest first const artistsWithPoints = Object.keys(artistGroups).map(artist => { const collections = artistGroups[artist]; let totalMints = 0; collections.forEach(collection => { totalMints += collection.minted || 0; }); return { name: artist, points: totalMints }; }); // Sort by creator points (highest first) const sortedArtists = artistsWithPoints .sort((a, b) => b.points - a.points) .map(artist => artist.name); // Call the existing function to build the HTML (reuse existing logic) // For now, we'll just call loadAllArtworks which will use filteredArtworks from window // Store filtered artworks temporarily const originalArtworks = window.allArtworks; window.allArtworks = filteredArtworks; loadAllArtworks(); window.allArtworks = originalArtworks; } else if (currentMainTab === 'collections') { // For collections tab, filter and display collections const grid = document.getElementById('collections-grid'); if (!grid) return; // Filter collections based on same criteria let collections = window.allCollections || []; console.log('Starting with', collections.length, 'collections'); // Apply search filter to collections if (artistSearch) { collections = collections.filter(c => c.name?.toLowerCase().includes(artistSearch) || c.artist_name?.toLowerCase().includes(artistSearch) ); console.log('After search filter:', collections.length, 'collections'); } // Apply selected artists filter to collections if (selectedArtists.length > 0) { collections = collections.filter(c => selectedArtists.includes(c.artist_name)); console.log('After artist filter:', collections.length, 'collections'); } // Apply price filter to collections if (!freeMintsOnly) { const priceMin = document.getElementById('price-min')?.value; const priceMax = document.getElementById('price-max')?.value; if (priceMin !== '' && priceMin !== null && priceMin !== undefined && priceMin !== '0') { const minPrice = parseInt(priceMin); collections = collections.filter(c => (c.price || 0) >= minPrice); console.log('After min price filter:', collections.length, 'collections'); } if (priceMax !== '' && priceMax !== null && priceMax !== undefined) { const maxPrice = parseInt(priceMax); collections = collections.filter(c => (c.price || 0) <= maxPrice); console.log('After max price filter:', collections.length, 'collections'); } } else { // Free mints only - filter collections with price 0 collections = collections.filter(c => (c.price || 0) === 0); console.log('After free mints filter:', collections.length, 'collections'); } if (collections.length === 0) { grid.innerHTML = '

    No collections match your filters

    '; return; } // Convert collections to combined items format const combinedItems = collections.map(collection => ({ type: 'collection', data: collection })); // Display using combined items function displayCombinedItems(combinedItems, 'collections-grid'); } } // Initialize filters when page loads if (typeof window !== 'undefined') { window.addEventListener('load', () => { setTimeout(initializeFilters, 1000); }); } // Load artworks list for home page async function loadArtworksList() { try { // Fetch both artworks and collections (with cache busting) const timestamp = Date.now(); const [artworksResponse, collectionsResponse] = await Promise.all([ fetch(getReadApiUrl(`/api/simple-list?t=${timestamp}`), { cache: 'no-store' }), fetch(getReadApiUrl(`/api/get-collections?t=${timestamp}`), { cache: 'no-store' }) ]); const artworksData = await artworksResponse.json(); const collectionsData = await collectionsResponse.json(); if (artworksData.success && artworksData.artworks) { // Store globally for use by other functions window.allArtworks = artworksData.artworks; window.allCollections = collectionsData.collections || []; // Preserve canonical home feed exactly from lunalauncher.io endpoints window.mainPageAllArtworks = artworksData.artworks; window.mainPageAllCollections = collectionsData.collections || []; window.homeHeroGridSlugs = Array.isArray(artworksData.homeHeroGridSlugs) ? artworksData.homeHeroGridSlugs : []; // Populate artist list from artworks table data populateArtistList(); // Initialize the current view based on active tab if (currentMainTab === 'live') { loadLiveArtworks(); } else if (currentMainTab === 'all') { loadAllArtworks(); } else if (currentMainTab === 'collections') { loadCollectionsView(); } else { // Default to live artworks loadLiveArtworks(); } } else { const grid = document.getElementById('artworks-grid-live'); if (grid) { grid.innerHTML = '

    No artworks available

    '; } } } catch (error) { console.error('Failed to load artworks:', error); const grid = document.getElementById('artworks-grid-live'); if (grid) { grid.innerHTML = '

    Failed to load artworks

    '; } } } // Display artworks grid on home page function displayArtworksList(artworks, collections = []) { const grid = document.getElementById('artworks-grid'); if (!grid) { return; } if (artworks.length === 0 && collections.length === 0) { grid.innerHTML = '

    No active mints available

    '; return; } // Store original artworks for search functionality window.allArtworks = artworks; window.allCollections = collections; // Filter by tab (live vs all artworks) first let tabFilteredArtworks; if (currentHomeTab === 'completed') { // Show all artworks (both completed and ongoing) tabFilteredArtworks = artworks; } else { // Live tab shows only active (non-sold-out) artworks tabFilteredArtworks = artworks.filter(artwork => !artwork.soldOut); } // Apply search filter if search term exists const searchTerm = document.getElementById('artwork-search')?.value?.toLowerCase() || ''; let filteredArtworks = tabFilteredArtworks; let filteredCollections = collections; if (searchTerm) { filteredArtworks = tabFilteredArtworks.filter(artwork => artwork.artist.toLowerCase().includes(searchTerm) || artwork.title.toLowerCase().includes(searchTerm) ); filteredCollections = collections.filter(collection => { const nameMatch = collection.name && collection.name.toLowerCase().includes(searchTerm); const artistMatch = collection.artist_name && collection.artist_name.toLowerCase().includes(searchTerm); const shouldInclude = nameMatch || artistMatch; if (shouldInclude) { console.log('Collection included:', collection.name, 'Artist:', collection.artist_name, 'Search:', searchTerm); } return shouldInclude; }); console.log('Search term:', searchTerm, 'Filtered collections:', filteredCollections.length, 'Filtered artworks:', filteredArtworks.length); } // Sort artworks: featured first, then non-featured, both ordered by ID descending filteredArtworks.sort((a, b) => { // Featured items come first if (a.isFeatured && !b.isFeatured) return -1; if (!a.isFeatured && b.isFeatured) return 1; // Within each group (both featured or both not featured), sort by ID descending return (b.id || 0) - (a.id || 0); }); if (filteredArtworks.length === 0 && filteredCollections.length === 0) { const emptyMessage = searchTerm ? 'No artworks match your search' : (currentHomeTab === 'completed' ? 'No artworks yet' : 'No live mints available'); grid.innerHTML = `

    ${emptyMessage}

    `; return; } // If showing all artworks tab, group by artist if (currentHomeTab === 'completed') { // Group artworks by artist const artistGroups = {}; filteredArtworks.forEach(artwork => { const artist = artwork.artist || 'Unknown'; if (!artistGroups[artist]) { artistGroups[artist] = []; } artistGroups[artist].push(artwork); }); // Calculate creator points for each artist and sort by highest first const artistsWithPoints = Object.keys(artistGroups).map(artist => { const collections = artistGroups[artist]; let totalMints = 0; collections.forEach(collection => { totalMints += collection.minted || 0; }); return { name: artist, points: totalMints }; }); // Sort by creator points (highest first) const sortedArtists = artistsWithPoints .sort((a, b) => b.points - a.points) .map(artist => artist.name); // Helper function to get collection preview image const getCollectionPreviewImage = (artwork, size = '250px') => { const prevLabel = collectionOrdinalsAlt(artwork.title, artwork.artist); // Custom thumbnail override for artwork 513 if (artwork.id === 513) { return `${prevLabel}`; } else if (artwork.mintType === 'pre_inscribed' || artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints' || artwork.mintType === 'numerical') { const r2Key = artwork.imageFilename; // Check if it's an HTML file if (r2Key && r2Key.toLowerCase().endsWith('.html')) { return ``; } else { return `${prevLabel}`; } } else if (artwork.parentType === 'recursive') { return ``; } else if (artwork.parentInscriptionId) { return ``; } else { return `
    ✅ COMPLETED
    `; } }; // Build HTML for each artist group - always single column grid.style.display = 'grid'; grid.classList.add('completed-view'); grid.style.gridTemplateColumns = '1fr'; grid.style.gap = '20px'; grid.innerHTML = sortedArtists.map(artist => { const collections = artistGroups[artist]; // Sort collections by created date (newest first) to get the latest const sortedCollections = [...collections].sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0) ); const latestCollection = sortedCollections[0]; const artistId = artist.replace(/[^a-zA-Z0-9]/g, '_'); const totalCollections = collections.length; // Calculate stats (including both completed and ongoing) let totalSatsRaised = 0; let totalMints = 0; let completedCount = 0; let ongoingCount = 0; collections.forEach(collection => { const minted = collection.minted || 0; const price = collection.price || 0; totalSatsRaised += minted * price; totalMints += minted; if (collection.soldOut) { completedCount++; } else { ongoingCount++; } }); // Creator points: 1 point per mint const creatorPoints = Math.floor(totalMints); // Platform fees raised (flat marketplace fee per mint) const platformFeesRaised = totalMints * MARKETPLACE_FEE_SATS_PER_MINT; return `
    ${getCollectionPreviewImage(latestCollection, '150px')}

    ${artist}

    ${totalCollections} Collection${totalCollections > 1 ? 's' : ''} ${completedCount > 0 ? ` (${completedCount} completed` : ''} ${ongoingCount > 0 ? `${completedCount > 0 ? ', ' : ' ('}${ongoingCount} ongoing)` : completedCount > 0 ? ')' : ''}

    TOTAL SATS RAISED

    ${totalSatsRaised.toLocaleString()}

    CREATOR POINTS

    ${creatorPoints.toLocaleString()}

    ${sortedCollections.map(artwork => { const isSoldOut = artwork.soldOut; const isUpcomingDash = !isSoldOut && isArtworkUpcoming(artwork); const progress = artwork.total > 0 ? (artwork.minted / artwork.total * 100).toFixed(0) : 0; const clickAction = isSoldOut ? `viewCollectionGallery('${artwork.slug}', 'public')` : isUpcomingDash ? null : `navigateToArtwork('${artwork.slug}')`; const thumbClickAttr = clickAction != null ? ` onclick="${clickAction}"` : ''; const thumbCursor = clickAction != null ? 'cursor: pointer;' : 'cursor: default;'; const btnLabel = isSoldOut ? 'View' : isUpcomingDash ? 'Soon' : 'Mint'; const btnOnclick = clickAction != null ? ` onclick="${clickAction}"` : ''; const btnDisabled = isUpcomingDash ? ' disabled' : ''; const btnStyle = isUpcomingDash ? 'background: #444; color: #aaa; border: 1px solid #fff; padding: 4px 6px; font-size: 6px; cursor: not-allowed; width: 100%; font-family: \'Press Start 2P\', monospace; opacity: 0.75;' : `background: ${isSoldOut ? '#6600cc' : '#ff6600'}; color: #fff; border: 1px solid #fff; padding: 4px 6px; font-size: 6px; cursor: pointer; width: 100%; font-family: 'Press Start 2P', monospace;`; return `
    ${getCollectionPreviewImage(artwork, '100%')}

    ${artwork.title}

    ${artwork.minted}/${artwork.total === -1 ? '∞' : artwork.total}

    ${!isSoldOut && artwork.total > 0 ? `
    ` : ''} ${btnLabel}
    `; }).join('')}
    `; }).join(''); return; // Exit early for completed view } // Reset grid for live mints - use CSS media queries grid.style.display = 'grid'; grid.classList.remove('completed-view'); // CSS handles: 3 columns mobile, 4 columns desktop grid.style.gridTemplateColumns = ''; // Let CSS media queries handle it grid.style.gap = ''; // Combine collections and artworks for mixed sorting // Collections should be sorted by their latest artwork date, after featured items const combinedItems = []; console.log('Adding collections to combined items. Search term:', searchTerm, 'Filtered collections:', filteredCollections.length); if (searchTerm && filteredCollections.length > 0) { console.log('Collections that passed filter:', filteredCollections.map(c => ({ name: c.name, artist: c.artist_name }))); } // Add collections to the combined array with metadata for sorting filteredCollections.forEach(collection => { combinedItems.push({ type: 'collection', data: collection, isFeatured: false, // Collections are never featured sortDate: new Date(collection.latest_artwork_date || collection.created_at || 0) }); }); // Add standalone artworks to the combined array const standaloneArtworks = filteredArtworks.filter(artwork => !artwork.collectionSlug); standaloneArtworks.forEach(artwork => { combinedItems.push({ type: 'artwork', data: artwork, isFeatured: artwork.isFeatured || false, sortDate: new Date(artwork.createdAt || 0) // Use created date for sorting }); }); // Sort all items by date first to identify the newest 4 const allItemsSorted = [...combinedItems].sort((a, b) => { const aSort = a.sortDate instanceof Date ? a.sortDate.getTime() : a.sortDate; const bSort = b.sortDate instanceof Date ? b.sortDate.getTime() : b.sortDate; return bSort - aSort; }); // Get the 4 newest items const newestItems = allItemsSorted.slice(0, 4); const newestItemsSet = new Set(newestItems.map(item => item.type === 'collection' ? item.data.slug : item.data.slug )); // Mark items as new combinedItems.forEach(item => { const itemSlug = item.type === 'collection' ? item.data.slug : item.data.slug; item.isNew = newestItemsSet.has(itemSlug); }); // Sort combined items: newest 4 first, then featured, then the rest by date descending combinedItems.sort((a, b) => { // Newest items come first if (a.isNew && !b.isNew) return -1; if (!a.isNew && b.isNew) return 1; // Among non-newest items, featured items come next if (!a.isNew && !b.isNew) { if (a.isFeatured && !b.isFeatured) return -1; if (!a.isFeatured && b.isFeatured) return 1; } // Within each group, sort by date descending (newest first) const aSort = a.sortDate instanceof Date ? a.sortDate.getTime() : a.sortDate; const bSort = b.sortDate instanceof Date ? b.sortDate.getTime() : b.sortDate; return bSort - aSort; }); // Generate HTML for each item based on type const allCards = combinedItems.map(item => { if (item.type === 'collection') { const collection = item.data; const stillLiveCount = collection.live_count || 0; const completedCount = collection.artwork_count - stillLiveCount; return `
    COLLECTION
    ${item.isNew ? '
    NEW
    ' : ''}
    ${collection.collection_image ? `
    ${collectionOrdinalsAlt(collection.name, collection.artist_name)}
    ` : `
    📂
    ` }

    ${collection.name}

    ${collection.artist_name ? `

    by ${collection.artist_name}

    ` : ''}

    ${collection.artwork_count} artwork${collection.artwork_count !== 1 ? 's' : ''}

    ${stillLiveCount} currently live

    ${completedCount} completed

    VIEW COLLECTION

    `; } else { // It's an artwork const artwork = item.data; const isUpcomingHome = !artwork.soldOut && isArtworkUpcoming(artwork); const artworkClickAction = artwork.soldOut ? `viewCollectionGallery('${artwork.slug}', 'public')` : isUpcomingHome ? '' : `navigateToArtwork('${artwork.slug}')`; const cardClickAttr = isUpcomingHome ? '' : `onclick="${artworkClickAction}"`; const cardHoverAttr = isUpcomingHome ? '' : `onmouseover="this.style.background='#111111'" onmouseout="this.style.background='#000000'"`; const cardCursorStyle = isUpcomingHome ? 'cursor: default;' : 'cursor: pointer;'; const cardMediaLabel = collectionOrdinalsAlt(artwork.title, artwork.artist); return `
    ${artwork.isFeatured ? '' : ''} ${item.isNew ? '
    NEW
    ' : ''}
    ${artwork.mintType === 'pre_inscribed' || artwork.mintType === 'inscribe_on_demand' || artwork.mintType === 'parent_child_prints' || artwork.mintType === 'numerical' ? `
    ${cardMediaLabel}
    ` : artwork.parentType === 'recursive' ? `
    ` : artwork.parentInscriptionId ? `
    ` : `
    NO PREVIEW AVAILABLE
    ` }

    ${artwork.title}

    ${artwork.artist}

    ${artwork.price === 0 ? 'FREE' : `${artwork.price.toLocaleString()} SATS`}

    ${artwork.minted} / ${artwork.total === -1 ? '∞' : artwork.total} minted

    ${artwork.soldOut ? `
    ✅ COMPLETED - Click to View Gallery
    ` : ''} ${artwork.total === -1 || artwork.soldOut ? '' : '
    '} ${artwork.earnings && artwork.earnings.totalEarningsSats > 0 ? `

    EARNINGS

    ${artwork.earnings.totalEarningsSats.toLocaleString()} sats earned

    ${artwork.earnings.paymentPercentage}% paid out

    ` : ''}
    ${artwork.soldOut ? '

    SOLD OUT

    ' : isUpcomingHome ? '

    MINTING SOON

    ' : '

    CLICK TO MINT

    '}
    `; } }).join(''); // Display combined and sorted collections and artworks grid.innerHTML = allCards; } // Search functionality function setupSearchFunctionality() { const searchInput = document.getElementById('artwork-search'); if (searchInput) { // Aggressively clear any auto-filled content searchInput.value = ''; // Clear again after a short delay to catch late auto-fills setTimeout(() => { if (searchInput.value && searchInput.value.includes('@')) { searchInput.value = ''; } }, 100); // Clear on focus if it contains email-like content searchInput.addEventListener('focus', function() { if (this.value && this.value.includes('@')) { this.value = ''; } }); searchInput.addEventListener('input', function() { if (window.allArtworks) { displayArtworksList(window.allArtworks); } }); } } // Clear search function window.clearSearch = function() { const searchInput = document.getElementById('artwork-search'); if (searchInput) { searchInput.value = ''; if (window.allArtworks) { displayArtworksList(window.allArtworks); } } } // Global function to clear all search fields (prevents email auto-fill) function clearAllSearchFields() { const searchFields = [ 'artwork-search', 'on-demand-search', 'prints-search', 'pre-inscribed-search' ]; searchFields.forEach(fieldId => { const field = document.getElementById(fieldId); if (field && (field.value.includes('@') || field.value.includes('.'))) { field.value = ''; } }); } // 3D Card Pack Renderer Functions let cardPackRotation = { x: -10, y: 20 }; let cardPackMouseDown = false; let cardPackLastMousePos = { x: 0, y: 0 }; async function render3DCardPack(slug, packImageFilename, parentInscriptionId, parentType) { const container = document.getElementById('artwork-container'); if (!container) return; // Determine image content based on parent inscription let imageContent; if (parentInscriptionId) { if (parentType === 'recursive') { // Recursive content - use iframe imageContent = ``; } else { // Static image from ordinals imageContent = `Card Pack`; } } else if (packImageFilename) { // Fallback to collection image imageContent = `Card Pack`; } else { // Placeholder imageContent = `
    CARD PACK
    `; } container.innerHTML = `
    ${imageContent}
    ${imageContent}
    `; // Initialize 3D controls initCardPack3DControls(); // Set initial rotation updateCardPackRotation(); // Show the container container.style.display = 'block'; } function initCardPack3DControls() { const scene = document.getElementById('card-pack-scene'); if (!scene) return; // Mouse events scene.addEventListener('mousedown', (e) => { cardPackMouseDown = true; cardPackLastMousePos = { x: e.clientX, y: e.clientY }; scene.style.transition = 'none'; }); document.addEventListener('mousemove', (e) => { if (!cardPackMouseDown) return; const deltaX = e.clientX - cardPackLastMousePos.x; const deltaY = e.clientY - cardPackLastMousePos.y; cardPackRotation.y += deltaX * 0.5; cardPackRotation.x -= deltaY * 0.5; // Clamp X rotation to prevent extreme angles cardPackRotation.x = Math.max(-90, Math.min(90, cardPackRotation.x)); cardPackLastMousePos = { x: e.clientX, y: e.clientY }; updateCardPackRotation(); }); document.addEventListener('mouseup', () => { if (cardPackMouseDown) { cardPackMouseDown = false; const scene = document.getElementById('card-pack-scene'); if (scene) { scene.style.transition = 'transform 0.1s ease-out'; } } }); // Touch events for mobile scene.addEventListener('touchstart', (e) => { cardPackMouseDown = true; const touch = e.touches[0]; cardPackLastMousePos = { x: touch.clientX, y: touch.clientY }; scene.style.transition = 'none'; e.preventDefault(); }); document.addEventListener('touchmove', (e) => { if (!cardPackMouseDown) return; const touch = e.touches[0]; const deltaX = touch.clientX - cardPackLastMousePos.x; const deltaY = touch.clientY - cardPackLastMousePos.y; cardPackRotation.y += deltaX * 0.5; cardPackRotation.x -= deltaY * 0.5; // Clamp X rotation cardPackRotation.x = Math.max(-90, Math.min(90, cardPackRotation.x)); cardPackLastMousePos = { x: touch.clientX, y: touch.clientY }; updateCardPackRotation(); e.preventDefault(); }); document.addEventListener('touchend', () => { if (cardPackMouseDown) { cardPackMouseDown = false; const scene = document.getElementById('card-pack-scene'); if (scene) { scene.style.transition = 'transform 0.1s ease-out'; } } }); } function updateCardPackRotation() { const scene = document.getElementById('card-pack-scene'); if (!scene) return; scene.style.transform = `rotateX(${cardPackRotation.x}deg) rotateY(${cardPackRotation.y}deg)`; } function resetCardPackRotation() { cardPackRotation = { x: -10, y: 20 }; const scene = document.getElementById('card-pack-scene'); if (scene) { scene.style.transition = 'transform 0.5s ease-out'; updateCardPackRotation(); setTimeout(() => { scene.style.transition = 'transform 0.1s ease-out'; }, 500); } } // Load individual artwork for minting async function loadArtworkForMinting(slug, preloadedData = null) { try { teardownMintEnableGallery(); teardownMintAccessQueue(); document.body.classList.remove('empress-trash-mint-gallery'); // Use preloaded data if provided (for admin previews), otherwise fetch let data; if (preloadedData) { data = preloadedData; } else { data = await fetchMintArtworkData(slug); } if (adminMintBypassActive() && data.success && data.artwork) { const lockedForPublic = data.artwork.isActive === false || isArtworkUpcoming(data.artwork); if (lockedForPublic) { mountAdminMintPreviewBanner('🔧 ADMIN PREVIEW — This mint is not yet live for the public'); } } if (data.success && data.artwork) { // Set current artwork slug BEFORE calling updateArtworkDetails currentArtworkSlug = slug; if (!adminMintBypassActive() && data.artwork.mintQueueEnabled) { await mintAccessQueueGate(slug); } // Show custom section for Deth Reaper collection const dethReaperCustom = document.getElementById('deth-reaper-custom'); if (dethReaperCustom) { if (slug === 'deth-reaper-algorithmic-blooms') { dethReaperCustom.style.display = 'block'; } else { dethReaperCustom.style.display = 'none'; } } updateArtworkDetails(data.artwork); if (slug === FIVE_IL5NC5_MINT_SLUG) { if (data.fiveIl5nc5Phase) { window.fiveIl5nc5Schedule = data.fiveIl5nc5Phase; sync5il5nc5PhaseBar(data.fiveIl5nc5Phase); apply5il5nc5PhasePrice(data.fiveIl5nc5Phase); } start5il5nc5PhasePoller(); } else { stop5il5nc5PhasePoller(); } // Update mint counter document.getElementById('mintedCount').textContent = data.minted; document.getElementById('totalCount').textContent = data.total === -1 ? '∞' : data.total; // Update mint end time if set updateMintEndTime(data.artwork.mintEndDatetime); // Update artwork display const container = document.getElementById('artwork-container'); const containerParent = container?.parentElement; if (containerParent) { [...containerParent.children].forEach((child) => { if (child === container) return; if (child.classList && child.classList.contains('mint-art-frame-overlay')) return; child.remove(); }); } if (container) { container.innerHTML = ''; // Clear existing content // Check if this is a card pack - render parent inscription content directly if (data.artwork.cardPack === 1 || data.artwork.card_pack === 1) { console.log('🎴 Rendering card pack for:', slug); if (data.artwork.parentInscriptionId) { if (data.artwork.parentType === 'recursive') { // Recursive content - use iframe like home page container.innerHTML = ` `; } else { // Static image container.innerHTML = ` ${data.artwork.title}`; } } else { // Fallback to collection image container.innerHTML = ` ${data.artwork.title}`; } container.style.display = 'block'; // Make container visible for card packs } else if (data.artwork.mintType === 'pre_inscribed' || data.artwork.mintType === 'numerical') { // Show collection image for pre-inscribed collections const img = document.createElement('img'); img.src = `/api/get-collection-image?filename=${data.artwork.imageFilename}`; img.alt = `${data.artwork.title} by ${data.artwork.artist}`; img.className = 'collection-image'; img.style.cssText = 'max-width: 100%; max-height: 60vh; border: 2px solid #ffffff; object-fit: contain;'; img.onload = function() { this.style.display = 'block'; }; img.onerror = function() { console.error('Mint page - failed to load collection image:', data.artwork.imageFilename); this.src = `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAwIiBoZWlnaHQ9IjQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMDAwIiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMiIvPjx0ZXh0IHg9IjUwJSIgeT0iNDAlIiBmaWxsPSIjZmZmIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSIgZm9udC1mYW1pbHk9Im1vbm9zcGFjZSIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiPlBSRS1JTlNDUklCRUQgQ09MTEVDVE9JTjwvdGV4dD48dGV4dCB4PSI1MCUiIHk9IjYwJSIgZmlsbD0iI2NjYyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iIGZvbnQtZmFtaWx5PSJtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTQiPkF1dG9tYXRpYyBPcmRpbmFsIERpc3RyaWJ1dGlvbjwvdGV4dD48L3N2Zz4=`; this.onerror = null; }; container.appendChild(img); container.style.display = 'block'; // Make container visible // Show quantity selector for pre-inscribed v2 collections, numerical collections, and inscribe-on-demand collections const hasOrdinalsWallet = data.artwork.additionalDetails && JSON.parse(data.artwork.additionalDetails).ordinalsWallet; if (hasOrdinalsWallet || data.artwork.mintType === 'numerical' || data.artwork.mintType === 'inscribe_on_demand' || data.artwork.mintType === 'parent_child_prints' || data.artwork.mintType === 'pre_inscribed_v2') { const quantitySelector = document.getElementById('quantity-selector'); if (quantitySelector) { quantitySelector.style.display = 'block'; // For inscribe-on-demand: reserve and cache inscriptions on load if (data.artwork.mintType === 'inscribe_on_demand' || data.artwork.mintType === 'parent_child_prints') { // Reservations happen only when user clicks Mint Now (not on load or for cost) window.cachedReservedInscriptions = null; } // Set up bulk mint price updater window.updateBulkMintPrice = async function() { const qty = parseInt(document.getElementById('mint-quantity').value) || 1; const price = (window.currentArtwork && window.currentArtwork.price != null) ? window.currentArtwork.price : (data.artwork.price || 0); // Handle inscribe-on-demand (inscription fees) vs parent–child prints (marketplace + buffer only, like pre-inscribed) if (data.artwork.mintType === 'parent_child_prints') { const marketplacePerMint = MARKETPLACE_FEE_SATS_PER_MINT; const networkPerMint = MINT_NETWORK_BUFFER_SATS_PER_MINT; let pricePerMint = price + marketplacePerMint + networkPerMint; const physicalPrintCheckboxPcp = document.getElementById('physicalPrintCheckbox'); const isPhysicalPrintCheckedPcp = physicalPrintCheckboxPcp && physicalPrintCheckboxPcp.checked; if (isPhysicalPrintCheckedPcp) { const physicalPrintFeePerMintPcp = await getPhysicalPrintCostInSats() || 60000; pricePerMint += physicalPrintFeePerMintPcp; } const totalPcp = pricePerMint * qty; document.getElementById('bulk-mint-total').textContent = `${totalPcp.toLocaleString()} sats (${qty} × ${pricePerMint.toLocaleString()} sats)`; const networkBufferSectionPcp = document.getElementById('networkBufferSection'); const networkBufferSeparatorPcp = document.getElementById('networkBufferSeparator'); if (networkBufferSectionPcp) networkBufferSectionPcp.style.display = 'none'; if (networkBufferSeparatorPcp) networkBufferSeparatorPcp.style.display = 'none'; } else if (data.artwork.mintType === 'inscribe_on_demand') { const estimatedFileSize = window.collectionMaxFileSize || 1000; const networkFeePerItem = computeInscribeOnDemandNetworkFeePerMint(estimatedFileSize); const totalNetworkFee = networkFeePerItem * qty; const marketplaceFee = MARKETPLACE_FEE_SATS_PER_MINT; const totalMarketplaceFee = marketplaceFee * qty; // Apply free mint discount if eligible let totalArtworkPrice = price * qty; let freeMintsApplied = 0; if (window.walletHasFreeMint && price > 0 && qty >= 1) { const remainingFreeMints = Math.max(0, (window.walletMaxFreeMints || 1) - (window.walletMintCount || 0)); freeMintsApplied = Math.min(qty, remainingFreeMints); const paidItems = qty - freeMintsApplied; totalArtworkPrice = price * paidItems; } // Check for physical print fee const physicalPrintCheckbox = document.getElementById('physicalPrintCheckbox'); const isPhysicalPrintChecked = physicalPrintCheckbox && physicalPrintCheckbox.checked; let physicalPrintFeePerMint = 0; if (isPhysicalPrintChecked) { physicalPrintFeePerMint = await getPhysicalPrintCostInSats(); // Ensure we always have a valid fee (fallback to 60,000 sats) if (!physicalPrintFeePerMint || physicalPrintFeePerMint <= 0) { physicalPrintFeePerMint = 60000; } } const totalPhysicalPrintFee = physicalPrintFeePerMint * qty; const total = totalNetworkFee + totalMarketplaceFee + totalArtworkPrice + totalPhysicalPrintFee; // Update the top cost display breakdown FIRST (this is the main display) const networkElement = document.getElementById('networkFeesDisplay'); const marketplaceElement = document.getElementById('marketplaceFeeDisplay'); const artworkElement = document.getElementById('artworkPriceDisplay'); const totalElement = document.getElementById('totalCostDisplay'); const networkBufferSectionEl = document.getElementById('networkBufferSection'); const networkBufferSeparatorEl = document.getElementById('networkBufferSeparator'); if (networkElement) networkElement.textContent = `${totalNetworkFee.toLocaleString()} sats`; if (networkBufferSectionEl) networkBufferSectionEl.style.display = 'none'; if (networkBufferSeparatorEl) networkBufferSeparatorEl.style.display = 'none'; if (marketplaceElement) marketplaceElement.textContent = `${totalMarketplaceFee.toLocaleString()} sats`; if (artworkElement) { if (freeMintsApplied > 0) { artworkElement.textContent = totalArtworkPrice === 0 ? `FREE (${freeMintsApplied} free)` : `${totalArtworkPrice.toLocaleString()} sats (${freeMintsApplied} free)`; } else { artworkElement.textContent = price === 0 ? 'FREE' : `${totalArtworkPrice.toLocaleString()} sats`; } } if (totalElement) totalElement.textContent = `${total.toLocaleString()} sats`; // Update the bottom display with breakdown const artworkDisplayText = freeMintsApplied > 0 ? `${totalArtworkPrice.toLocaleString()} (${freeMintsApplied} free)` : totalArtworkPrice.toLocaleString(); document.getElementById('bulk-mint-total').textContent = `${total.toLocaleString()} sats (Network: ${totalNetworkFee.toLocaleString()} + Marketplace: ${totalMarketplaceFee.toLocaleString()} + Artwork: ${artworkDisplayText}${totalPhysicalPrintFee > 0 ? ` + Physical: ${totalPhysicalPrintFee.toLocaleString()}` : ''})`; console.log('✅ updateBulkMintPrice: Updated displays (estimate, reserve on Mint Now):', { inscriptionFee: totalNetworkFee, marketplaceFee: totalMarketplaceFee, artworkPrice: totalArtworkPrice, total: total, quantity: qty, feeRate: INSCRIBE_ON_DEMAND_FEE_RATE_SAT_VB, freeMintsApplied: freeMintsApplied }); // Also trigger updateCostCalculation to ensure consistency // But skip it if we just updated everything above // (This prevents double-updating and ensures other triggers work) } else { // Pre-inscribed / numerical / pre_inscribed_v2: marketplace + network buffer per mint const marketplacePerMint = MARKETPLACE_FEE_SATS_PER_MINT; const networkPerMint = MINT_NETWORK_BUFFER_SATS_PER_MINT; let pricePerMint = price + marketplacePerMint + networkPerMint; const physicalPrintCheckbox = document.getElementById('physicalPrintCheckbox'); const isPhysicalPrintChecked = physicalPrintCheckbox && physicalPrintCheckbox.checked; if (isPhysicalPrintChecked) { const physicalPrintFeePerMint = await getPhysicalPrintCostInSats() || 60000; pricePerMint += physicalPrintFeePerMint; } const total = pricePerMint * qty; document.getElementById('bulk-mint-total').textContent = `${total.toLocaleString()} sats (${qty} × ${pricePerMint.toLocaleString()} sats)`; const networkBufferSectionElse = document.getElementById('networkBufferSection'); const networkBufferSeparatorElse = document.getElementById('networkBufferSeparator'); if (networkBufferSectionElse) networkBufferSectionElse.style.display = 'none'; if (networkBufferSeparatorElse) networkBufferSeparatorElse.style.display = 'none'; } // Don't call updateCostCalculation() here to avoid infinite loop }; // Initial calculation await window.updateBulkMintPrice(); } } } else if (data.artwork.mintType === 'inscribe_on_demand' || data.artwork.mintType === 'parent_child_prints') { // Preview/gallery flags still used by switchOnDemandDisplay when preview/gallery return const previewEnabled = data.artwork.enablePreview; const galleryEnabled = data.artwork.enableGallery !== false; // Default to true for backward compatibility // Create image container (mint load always shows collection cover only) const imageContainer = document.createElement('div'); imageContainer.id = 'on-demand-image-container'; imageContainer.style.cssText = 'text-align: center; min-height: 400px; display: flex; align-items: center; justify-content: center;'; container.appendChild(imageContainer); window.currentOnDemandDisplay = 'collection'; // Function to display a reserved asset consistently window.displayReservedAsset = function(assetData, container) { const img = document.createElement('img'); img.src = `data:${assetData.contentType};base64,${assetData.imageDataBase64}`; img.alt = `Preview: ${assetData.originalName}`; img.style.cssText = 'max-width: 100%; max-height: 60vh; border: 2px solid #ffffff; object-fit: contain;'; container.innerHTML = ''; container.appendChild(img); // Show reservation timer in a subtle way (but no text) const timerDiv = document.createElement('div'); timerDiv.id = 'preview-timer'; timerDiv.style.cssText = 'text-align: center; margin-top: 10px; font-size: 10px; color: #ffcc02;'; const updateTimer = () => { const remaining = Math.max(0, Math.floor((window.previewReservationExpiry - Date.now()) / 1000)); const minutes = Math.floor(remaining / 60); const seconds = remaining % 60; if (remaining > 0) { // No timer text shown to user setTimeout(updateTimer, 1000); } else { // Reservation expired - automatically get a new asset timerDiv.innerHTML = 'Getting new asset...'; timerDiv.style.color = '#ffaa00'; window.previewAssetToMint = null; window.previewReservationExpiry = null; // Automatically reserve a new asset setTimeout(() => { window.previewOnDemandAsset(); }, 1000); } }; container.appendChild(timerDiv); updateTimer(); }; // Function to preview/reserve a new on-demand asset window.previewOnDemandAsset = function() { const imageContainer = document.getElementById('on-demand-image-container'); if (!imageContainer) { console.error('Image container not found for asset preview'); return; } // Show loading state imageContainer.innerHTML = `
    Getting new asset...
    `; // Get the current artwork slug const artworkSlug = window.currentArtworkSlug || 'unknown'; // Reserve a new asset fetch( `/api/reserve-on-demand-asset`, withMintQueueFetchInit(artworkSlug, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ artworkSlug: artworkSlug, sessionId: 'auto-reserve-' + Date.now(), }), }) ) .then(response => { if (!response.ok) { return response.json().then(errorData => { throw new Error(errorData.error || `HTTP ${response.status}`); }); } return response.json(); }) .then(previewData => { console.log('Auto-reserve asset response:', previewData); if (previewData.success && previewData.asset) { // Store this reserved asset as the one to mint window.previewAssetToMint = previewData.asset; // Start countdown timer for this reservation const reservationExpiry = Date.now() + 5 * 60 * 1000; // 5 minutes window.previewReservationExpiry = reservationExpiry; // Use the reusable display function window.displayReservedAsset(previewData.asset, imageContainer); // Re-enable the mint button const mintBtn = document.getElementById('inscribeBtn'); if (mintBtn) { mintBtn.innerHTML = 'Mint Now'; mintBtn.disabled = false; } // Clear any loading messages const resultDiv = document.getElementById('inscriptionResult'); if (resultDiv) { resultDiv.innerHTML = ''; } } else { console.error('Asset reservation failed:', previewData); imageContainer.innerHTML = `
    Unable to get new asset
    ${previewData.error || 'Collection may be sold out or no assets available'}
    `; } }) .catch(error => { console.error('Error getting new asset:', error); imageContainer.innerHTML = `
    Error getting new asset
    ${error.message}
    `; }); }; // Load a specific page of the gallery window.loadGalleryPage = async function(page) { const imageContainer = document.getElementById('on-demand-image-container'); if (!imageContainer) return; // Show loading imageContainer.innerHTML = `
    Loading gallery page ${page}...
    Fetching assets...
    `; try { const response = await fetch(`/api/get-collection-gallery?slug=${window.currentGallerySlug}&page=${page}&limit=50`); const galleryData = await response.json(); if (galleryData.success && galleryData.assets) { const assets = galleryData.assets; const pagination = galleryData.pagination; const stats = galleryData.stats; if (assets.length === 0) { imageContainer.innerHTML = `
    No assets on this page
    `; return; } // Create gallery container const galleryContainer = document.createElement('div'); // Add pagination info const paginationInfo = document.createElement('div'); paginationInfo.style.cssText = 'text-align: center; margin-bottom: 15px; padding: 10px; background: #222; border: 2px solid #fff;'; paginationInfo.innerHTML = `
    Page ${pagination.page} of ${pagination.totalPages} | Showing ${assets.length} of ${stats.total} assets
    Minted: ${stats.minted} | Available: ${stats.available} | Reserved: ${stats.reserved}
    `; galleryContainer.appendChild(paginationInfo); // Add pagination controls const paginationControls = document.createElement('div'); paginationControls.style.cssText = 'text-align: center; margin-bottom: 15px;'; paginationControls.innerHTML = ` ${pagination.page} / ${pagination.totalPages} `; galleryContainer.appendChild(paginationControls); // Create gallery grid const galleryGrid = document.createElement('div'); galleryGrid.style.cssText = ` display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; padding: 15px; background: #111; border: 2px solid #fff; `; galleryGrid.innerHTML = assets.map(asset => `
    ${asset.originalName}
    ${asset.isMinted ? 'MINTED' : 'AVAILABLE'}
    #${asset.assetIndex}
    `).join(''); galleryContainer.appendChild(galleryGrid); // Add bottom pagination controls const bottomControls = paginationControls.cloneNode(true); bottomControls.style.marginTop = '15px'; bottomControls.style.marginBottom = '0px'; galleryContainer.appendChild(bottomControls); imageContainer.innerHTML = ''; imageContainer.appendChild(galleryContainer); // Update current page window.currentGalleryPage = page; } else { imageContainer.innerHTML = `
    Unable to load gallery
    Error loading collection assets
    `; } } catch (error) { console.error('Error fetching gallery assets:', error); imageContainer.innerHTML = `
    Gallery unavailable
    Error loading collection assets
    `; } }; window.switchOnDemandDisplay = function(mode) { console.log('🔧 switchOnDemandDisplay called with mode:', mode); window.currentOnDemandDisplay = mode; const collectionBtn = document.getElementById('show-collection-btn'); const previewBtn = document.getElementById('show-preview-btn'); const galleryBtn = document.getElementById('show-gallery-btn'); const imageContainer = document.getElementById('on-demand-image-container'); console.log('🔧 Button elements found:', { collection: !!collectionBtn, preview: !!previewBtn, gallery: !!galleryBtn, imageContainer: !!imageContainer }); // Reset all button styles if (collectionBtn) { collectionBtn.style.background = '#111'; collectionBtn.style.color = '#fff'; } if (previewBtn) { previewBtn.style.background = '#111'; previewBtn.style.color = '#fff'; } if (galleryBtn) { galleryBtn.style.background = '#111'; galleryBtn.style.color = '#fff'; } if (mode === 'collection') { if (collectionBtn) { collectionBtn.style.background = '#fff'; collectionBtn.style.color = '#000'; } // Show collection cover const img = document.createElement('img'); img.src = `/api/get-collection-image?filename=${data.artwork.imageFilename}`; img.alt = `${data.artwork.title} by ${data.artwork.artist}`; img.style.cssText = 'max-width: 100%; max-height: 60vh; border: 2px solid #ffffff; object-fit: contain;'; img.onerror = function() { this.src = `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAwIiBoZWlnaHQ9IjQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMDAwIiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMiIvPjx0ZXh0IHg9IjUwJSIgeT0iNDAlIiBmaWxsPSIjZmZmIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSIgZm9udC1mYW1pbHk9Im1vbm9zcGFjZSIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiPk9OLURFTUFORCBDT0xMRUNUSU9OPC90ZXh0Pjx0ZXh0IHg9IjUwJSIgeT0iNjAlIiBmaWxsPSIjY2NjIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSIgZm9udC1mYW1pbHk9Im1vbm9zcGFjZSIgZm9udC1zaXplPSIxNCI+UmFuZG9tIEFzc2V0IEluc2NyaXB0aW9uPC90ZXh0Pjwvc3ZnPg==`; this.onerror = null; }; imageContainer.innerHTML = ''; imageContainer.appendChild(img); } else if (mode === 'preview') { // Check if preview is enabled if (!previewEnabled) { // If preview is disabled, show collection cover instead window.switchOnDemandDisplay('collection'); return; } if (previewBtn) { previewBtn.style.background = '#fff'; previewBtn.style.color = '#000'; } // Check if we already have a reserved asset if (window.previewAssetToMint && window.previewReservationExpiry && window.previewReservationExpiry > Date.now()) { // Use existing reserved asset console.log('Using existing reserved asset:', window.previewAssetToMint.originalName); // Set for cost calculation and update display window.selectedAssetForCostCalculation = window.previewAssetToMint; window.updateCostCalculation().then(() => { console.log('✅ PREVIEW: Cost display updated for existing reserved asset'); }).catch(error => { console.error('Error updating cost display:', error); }); displayReservedAsset(window.previewAssetToMint, imageContainer); return; } // Show loading message and fetch new preview imageContainer.innerHTML = `
    Loading preview asset...
    Fetching random asset from collection...
    `; // Reserve a specific asset for this user (3-minute reservation) fetch( `/api/reserve-on-demand-asset`, withMintQueueFetchInit(data.artwork.slug, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ artworkSlug: data.artwork.slug, sessionId: 'preview-' + Date.now(), }), }) ) .then(response => { if (!response.ok) { return response.json().then(errorData => { throw new Error(errorData.error || `HTTP ${response.status}`); }); } return response.json(); }) .then(previewData => { console.log('Reserve asset response:', previewData); if (previewData.success && previewData.asset) { // Store this reserved asset as the one to mint window.previewAssetToMint = previewData.asset; // Set for cost calculation and update display window.selectedAssetForCostCalculation = previewData.asset; window.updateCostCalculation().then(() => { console.log('✅ PREVIEW: Cost display updated for reserved asset'); }).catch(error => { console.error('Error updating cost display:', error); }); // Start countdown timer for this reservation const reservationExpiry = Date.now() + 5 * 60 * 1000; // 5 minutes window.previewReservationExpiry = reservationExpiry; // Use the reusable display function displayReservedAsset(previewData.asset, imageContainer); } else { console.error('Asset reservation failed:', previewData); imageContainer.innerHTML = `
    Unable to reserve asset
    ${previewData.error || 'Collection may be sold out or no assets available'}
    ${previewData.debug ? `
    Debug: ${JSON.stringify(previewData.debug)}
    ` : ''}
    `; } }) .catch(error => { console.error('Error reserving preview asset:', error); // Try to get error details from response error.response?.json().then(errorData => { console.error('API Error details:', errorData); }).catch(() => {}); imageContainer.innerHTML = `
    Preview unavailable
    Error reserving asset for preview: ${error.message}
    `; }); } else if (mode === 'gallery') { // Check if gallery is enabled if (!galleryEnabled) { // If gallery is disabled, show collection cover instead window.switchOnDemandDisplay('collection'); return; } if (galleryBtn) { galleryBtn.style.background = '#fff'; galleryBtn.style.color = '#000'; } // Show loading message and fetch all assets imageContainer.innerHTML = `
    Loading collection gallery...
    Fetching all assets in collection...
    `; // Initialize gallery pagination window.currentGalleryPage = 1; window.currentGallerySlug = data.artwork.slug; // Load first page of gallery loadGalleryPage(1); } }; // Always show collection cover on load (preview/gallery restored later) window.switchOnDemandDisplay('collection'); // Show quantity selector for inscribe-on-demand collections const quantitySelector = document.getElementById('quantity-selector'); if (quantitySelector) { quantitySelector.style.display = 'block'; // 2-per-wallet collection: max quantity 2 const POST_WHITELIST_2_PER_WALLET_SLUG = 'sxulr-j-o-e-the-missing-pieces-public-edition'; const mintQtyInput = document.getElementById('mint-quantity'); if (mintQtyInput && (data.artwork.slug || slug) === POST_WHITELIST_2_PER_WALLET_SLUG) { mintQtyInput.setAttribute('max', '2'); const v = parseInt(mintQtyInput.value) || 1; if (v > 2) mintQtyInput.value = 2; } // Reservations happen only when user clicks Mint Now (not on load or for cost) window.cachedReservedInscriptions = null; try { const fsRes = await fetch(`/api/get-collection-file-sizes?slug=${encodeURIComponent(data.artwork.slug || slug)}`); const fsData = await fsRes.json(); window.collectionMaxFileSize = (fsData.success && fsData.stats && fsData.stats.max != null) ? fsData.stats.max : null; } catch (e) { window.collectionMaxFileSize = null; } // Set up bulk mint price updater window.updateBulkMintPrice = async function() { const qty = parseInt(document.getElementById('mint-quantity').value) || 1; const price = (window.currentArtwork && window.currentArtwork.price != null) ? window.currentArtwork.price : (data.artwork.price || 0); // Handle inscribe-on-demand (inscription fees) vs parent–child prints (marketplace + buffer only) if (data.artwork.mintType === 'parent_child_prints') { const marketplacePerMint = MARKETPLACE_FEE_SATS_PER_MINT; const networkPerMint = MINT_NETWORK_BUFFER_SATS_PER_MINT; let pricePerMint = price + marketplacePerMint + networkPerMint; const physicalPrintCheckboxPcp = document.getElementById('physicalPrintCheckbox'); const isPhysicalPrintCheckedPcp = physicalPrintCheckboxPcp && physicalPrintCheckboxPcp.checked; if (isPhysicalPrintCheckedPcp) { const physicalPrintFeePerMintPcp = await getPhysicalPrintCostInSats() || 60000; pricePerMint += physicalPrintFeePerMintPcp; } const totalPcp = pricePerMint * qty; document.getElementById('bulk-mint-total').textContent = `${totalPcp.toLocaleString()} sats (${qty} × ${pricePerMint.toLocaleString()} sats)`; const networkBufferSectionPcp = document.getElementById('networkBufferSection'); const networkBufferSeparatorPcp = document.getElementById('networkBufferSeparator'); if (networkBufferSectionPcp) networkBufferSectionPcp.style.display = 'none'; if (networkBufferSeparatorPcp) networkBufferSeparatorPcp.style.display = 'none'; } else if (data.artwork.mintType === 'inscribe_on_demand') { const estimatedFileSize = window.collectionMaxFileSize || 1000; const networkFeePerItem = computeInscribeOnDemandNetworkFeePerMint(estimatedFileSize); const totalNetworkFee = networkFeePerItem * qty; const marketplaceFee = MARKETPLACE_FEE_SATS_PER_MINT; const totalMarketplaceFee = marketplaceFee * qty; // Apply free mint discount if eligible let totalArtworkPrice = price * qty; let freeMintsApplied = 0; if (window.walletHasFreeMint && price > 0 && qty >= 1) { const remainingFreeMints = Math.max(0, (window.walletMaxFreeMints || 1) - (window.walletMintCount || 0)); freeMintsApplied = Math.min(qty, remainingFreeMints); const paidItems = qty - freeMintsApplied; totalArtworkPrice = price * paidItems; } // Check for physical print fee const physicalPrintCheckbox = document.getElementById('physicalPrintCheckbox'); const isPhysicalPrintChecked = physicalPrintCheckbox && physicalPrintCheckbox.checked; let physicalPrintFeePerMint = 0; if (isPhysicalPrintChecked) { physicalPrintFeePerMint = await getPhysicalPrintCostInSats(); if (!physicalPrintFeePerMint || physicalPrintFeePerMint <= 0) { physicalPrintFeePerMint = 60000; } } const totalPhysicalPrintFee = physicalPrintFeePerMint * qty; const total = totalNetworkFee + totalMarketplaceFee + totalArtworkPrice + totalPhysicalPrintFee; // Update cost display const networkElement = document.getElementById('networkFeesDisplay'); const marketplaceElement = document.getElementById('marketplaceFeeDisplay'); const artworkElement = document.getElementById('artworkPriceDisplay'); const totalElement = document.getElementById('totalCostDisplay'); const usdElement = document.getElementById('usdCostDisplay'); const networkBufferSectionEl2 = document.getElementById('networkBufferSection'); const networkBufferSeparatorEl2 = document.getElementById('networkBufferSeparator'); if (networkElement) networkElement.textContent = `${totalNetworkFee.toLocaleString()} sats`; if (networkBufferSectionEl2) networkBufferSectionEl2.style.display = 'none'; if (networkBufferSeparatorEl2) networkBufferSeparatorEl2.style.display = 'none'; if (marketplaceElement) marketplaceElement.textContent = `${totalMarketplaceFee.toLocaleString()} sats`; if (artworkElement) { if (freeMintsApplied > 0) { artworkElement.textContent = totalArtworkPrice === 0 ? `FREE (${freeMintsApplied} free)` : `${totalArtworkPrice.toLocaleString()} sats (${freeMintsApplied} free)`; } else { artworkElement.textContent = price === 0 ? 'FREE' : `${totalArtworkPrice.toLocaleString()} sats`; } } if (totalElement) totalElement.textContent = `${total.toLocaleString()} sats`; if (usdElement) usdElement.textContent = `(~$${(total * 0.00005).toFixed(2)})`; // Update bulk mint total display const artworkDisplayText = freeMintsApplied > 0 ? `${totalArtworkPrice.toLocaleString()} (${freeMintsApplied} free)` : totalArtworkPrice.toLocaleString(); document.getElementById('bulk-mint-total').textContent = `${total.toLocaleString()} sats (Network: ${totalNetworkFee.toLocaleString()} + Marketplace: ${totalMarketplaceFee.toLocaleString()} + Artwork: ${artworkDisplayText}${totalPhysicalPrintFee > 0 ? ` + Physical: ${totalPhysicalPrintFee.toLocaleString()}` : ''})`; console.log('✅ updateBulkMintPrice: Updated displays (estimate, reserve on Mint Now):', { inscriptionFee: totalNetworkFee, marketplaceFee: totalMarketplaceFee, artworkPrice: totalArtworkPrice, total: total, quantity: qty, feeRate: INSCRIBE_ON_DEMAND_FEE_RATE_SAT_VB, freeMintsApplied: freeMintsApplied }); // Don't call updateCostCalculation here - it would create infinite loop } else { // Pre-inscribed and numerical: marketplace + network buffer per mint const marketplacePerMint = MARKETPLACE_FEE_SATS_PER_MINT; const networkPerMint = MINT_NETWORK_BUFFER_SATS_PER_MINT; // Check for physical print fee const physicalPrintCheckbox = document.getElementById('physicalPrintCheckbox'); const isPhysicalPrintChecked = physicalPrintCheckbox && physicalPrintCheckbox.checked; let physicalPrintFeePerMint = 0; if (isPhysicalPrintChecked) { physicalPrintFeePerMint = await getPhysicalPrintCostInSats(); if (!physicalPrintFeePerMint || physicalPrintFeePerMint <= 0) { physicalPrintFeePerMint = 60000; } } let pricePerMint = price + marketplacePerMint + networkPerMint; if (isPhysicalPrintChecked) { pricePerMint += physicalPrintFeePerMint; } const total = pricePerMint * qty; document.getElementById('bulk-mint-total').textContent = `${total.toLocaleString()} sats (${qty} × ${pricePerMint.toLocaleString()} sats)`; const networkBufferSectionElse2 = document.getElementById('networkBufferSection'); const networkBufferSeparatorElse2 = document.getElementById('networkBufferSeparator'); if (networkBufferSectionElse2) networkBufferSectionElse2.style.display = 'none'; if (networkBufferSeparatorElse2) networkBufferSeparatorElse2.style.display = 'none'; // Trigger updateCostCalculation to refresh display if (typeof window.updateCostCalculation === 'function') { await window.updateCostCalculation(); } } }; // Initial calculation await window.updateBulkMintPrice(); } } else if (data.artwork.parentInscriptionId && data.artwork.parentType === 'recursive') { // Create iframe for recursive HTML - load directly (.mint-parent-preview-wrap sizes in CSS) const wrapper = document.createElement('div'); wrapper.className = 'mint-parent-preview-wrap'; const iframe = document.createElement('iframe'); iframe.src = `https://ordinals.com/content/${data.artwork.parentInscriptionId}`; iframe.setAttribute('sandbox', 'allow-scripts'); iframe.setAttribute('tabindex', '-1'); iframe.title = `${data.artwork.title} by ${data.artwork.artist}`; iframe.frameBorder = '0'; iframe.scrolling = 'no'; iframe.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none; background: #000;'; wrapper.appendChild(iframe); container.appendChild(wrapper); } else { // Create iframe with img wrapper for static images (allows proper scaling and sandboxing) const wrapper = document.createElement('div'); wrapper.className = 'mint-parent-preview-wrap'; const iframe = document.createElement('iframe'); if (data.artwork.parentInscriptionId) { iframe.srcdoc = ``; } else { // For pre-inscribed collections without parent inscription, show placeholder iframe.srcdoc = `
    PRE-INSCRIBED COLLECTION
    Automatic Ordinal Distribution
    `; } iframe.setAttribute('sandbox', 'allow-scripts'); iframe.setAttribute('allow', ''); iframe.title = `${data.artwork.title} by ${data.artwork.artist}`; iframe.frameBorder = '0'; iframe.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none; background: #000;'; wrapper.appendChild(iframe); container.appendChild(wrapper); } // Add fullscreen button for specific artworks (underneath the image) const fullscreenArtworks = { 'richart-s3qu3nc3r': 'f90df6134b4d171c5b1f9c85884c3e1075ef7fb32fa404a58004e28a0db274d1i0', 'richart-tactical-game': '94c91f823f145daf0200394433c1116781a7a669ba0b24a0d232f46838b37351i0', 'richart-street-reign': '77dcfb3f30e52428a23d36f75c88d8be44d8bbe986105e95d52469b55e9c2d5ai0', 'pq-pure-evil-4': '666434122704e2aa9f6c4c4274bd1966a79f31120137fbae00a687a22646dac6i0' }; if (fullscreenArtworks[slug]) { const maximizeBtn = document.createElement('button'); maximizeBtn.innerHTML = 'Click to open full screen'; maximizeBtn.style.cssText = ` padding: 10px 20px; background: #ffffff; color: #000000; border: 2px solid #ffffff; font-family: 'Press Start 2P', monospace; font-size: 10px; cursor: pointer; margin-top: 15px; display: block; margin-left: auto; margin-right: auto; `; maximizeBtn.onclick = function() { window.open('https://ordinals.com/content/' + fullscreenArtworks[slug], '_blank'); }; container.appendChild(maximizeBtn); } // pre_inscribed_v2 preview uses parent iframe / recursive branches above, not the // pre_inscribed | numerical branch — still need bulk quantity + pricing + WL info host. if (data.artwork.mintType === 'pre_inscribed_v2') { const quantitySelectorV2 = document.getElementById('quantity-selector'); if (quantitySelectorV2) { quantitySelectorV2.style.display = 'block'; window.updateBulkMintPrice = async function () { const qty = parseInt(document.getElementById('mint-quantity').value, 10) || 1; const price = (window.currentArtwork && window.currentArtwork.price != null) ? window.currentArtwork.price : (data.artwork.price || 0); const marketplacePerMint = MARKETPLACE_FEE_SATS_PER_MINT; const networkPerMint = MINT_NETWORK_BUFFER_SATS_PER_MINT; let pricePerMint = price + marketplacePerMint + networkPerMint; const physicalPrintCheckbox = document.getElementById('physicalPrintCheckbox'); const isPhysicalPrintChecked = physicalPrintCheckbox && physicalPrintCheckbox.checked; if (isPhysicalPrintChecked) { const physicalPrintFeePerMint = (await getPhysicalPrintCostInSats()) || 60000; pricePerMint += physicalPrintFeePerMint; } const total = pricePerMint * qty; const bulkEl = document.getElementById('bulk-mint-total'); if (bulkEl) { bulkEl.textContent = `${total.toLocaleString()} sats (${qty} × ${pricePerMint.toLocaleString()} sats)`; } const networkBufferSectionElse = document.getElementById('networkBufferSection'); const networkBufferSeparatorElse = document.getElementById('networkBufferSeparator'); if (networkBufferSectionElse) networkBufferSectionElse.style.display = 'none'; if (networkBufferSeparatorElse) networkBufferSeparatorElse.style.display = 'none'; }; await window.updateBulkMintPrice(); } } if (slug === EMPRESS_TRASH_LOTUS_MINT_SLUG) { applyEmpressTrashLotusMintGallery(data.artwork); } else { container.style.display = 'block'; } } // Show physical print option if available (but not for inscribe-on-demand) const physicalPrintStep = document.getElementById('physicalPrintStep'); if (physicalPrintStep) { if (data.artwork.physicalPrintAvailable && data.artwork.mintType !== 'inscribe_on_demand') { physicalPrintStep.style.display = 'block'; } else { physicalPrintStep.style.display = 'none'; } } // Check if sold out const mintBtn = document.getElementById('inscribeBtn'); console.log('loadArtworkForMinting - soldOut:', data.soldOut, 'total:', data.total, 'current button text:', mintBtn?.innerHTML); // Infinite supply mints (total: -1) should never be sold out const isInfiniteSupply = data.total === -1; const effectiveSoldOut = data.soldOut && !isInfiniteSupply; window.mintPageSoldOut = effectiveSoldOut; if (effectiveSoldOut) { // Check if gallery is enabled for this collection const galleryEnabled = data.artwork?.enableGallery !== false; if (galleryEnabled && (data.artwork?.mintType === 'pre_inscribed' || data.artwork?.mintType === 'pre_inscribed_v2' || data.artwork?.mintType === 'inscribe_on_demand' || data.artwork?.mintType === 'parent_child_prints' || data.artwork?.mintType === 'numerical')) { mintBtn.innerHTML = 'VIEW GALLERY'; mintBtn.disabled = false; mintBtn.onclick = function() { viewCollectionGallery(slug); }; console.log('Set button to VIEW GALLERY'); } else { mintBtn.innerHTML = 'SOLD OUT'; mintBtn.disabled = true; console.log('Set button to SOLD OUT'); } } else if (artworkIsUpcomingFromMintResponse(data)) { setInscribeButtonMintingSoon(mintBtn); } else { // Reset button only if it was previously SOLD OUT/VIEW GALLERY but now shouldn't be if (mintBtn && (mintBtn.innerHTML === 'SOLD OUT' || mintBtn.innerHTML === 'VIEW GALLERY' || mintBtn.innerHTML === 'MINTING SOON')) { mintBtn.innerHTML = 'Mint Now'; mintBtn.disabled = false; mintBtn.onclick = function() { inscribeOrdinal(); }; console.log('Reset button to Mint Now (not sold out or infinite supply)'); } } syncMintPageUpcomingUi(data.artwork, { soldOut: effectiveSoldOut }); await restoreMintStepsAfterArtworkLoadIfWalletReady(); setupMintEnableGalleryIfNeeded(slug, data.artwork); } } catch (error) { console.error('🚨 Display logic error - this is why fallback triggered:', error); console.error('🚨 Error stack:', error.stack); // Use fallback data and load image const fallbackArtwork = { artist: 'NeedThat', title: 'Failure to Launch', price: 0, parentInscriptionId: 'd37ab9c6c318e8e24f2a51cc181bc62292cf0f2edd07cb75147b62a1a1d00421i0', scriptInscriptionId: 'd686bf14aec7115b84cb9e85fe8d819d61217d115b8869c49694205ce3403a8ai0' }; // Don't call updateArtworkDetails to prevent overwriting correct data // Load fallback artwork display const container = document.getElementById('artwork-container'); if (container) { container.innerHTML = ''; // Clear existing content // Fallback is always static (default artwork) const img = document.createElement('img'); if (fallbackArtwork.parentInscriptionId) { img.src = `https://ordinals.com/content/${fallbackArtwork.parentInscriptionId}`; } else { img.src = `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAwIiBoZWlnaHQ9IjQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMDAwIiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMiIvPjx0ZXh0IHg9IjUwJSIgeT0iNDAlIiBmaWxsPSIjZmZmIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSIgZm9udC1mYW1pbHk9Im1vbm9zcGFjZSIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiPkZBTExCQUNLIElNQUdFPC90ZXh0PjwvdGV4dD48L3N2Zz4=`; } img.alt = `${fallbackArtwork.title} by ${fallbackArtwork.artist}`; img.className = 'generated-image'; img.style.cssText = 'max-width: 100%; max-height: 60vh; border: 2px solid #ffffff; object-fit: contain; image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges;'; img.onload = function() { this.style.display = 'block'; container.style.display = 'block'; }; img.onerror = function() { this.style.display = 'none'; this.onerror = null; }; container.appendChild(img); } } } // Monitor for inscription ID after mobile callback (when callback doesn't include inscription ID) async function monitorForInscriptionId(transactionId, artworkSlug, maxAttempts = 10) { console.log('🔧 MOBILE: Monitoring for inscription ID:', transactionId); for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { // Check if we can find the inscription ID by looking up the transaction const response = await fetch(`https://mempool.space/api/tx/${transactionId}`); if (response.ok) { const txData = await response.json(); // Look for inscription in the transaction outputs if (txData.vout && txData.vout.length > 0) { // Try to generate inscription ID (try both i0 and i1) const inscriptionVariants = [`${transactionId}i0`, `${transactionId}i1`]; for (const potentialInscriptionId of inscriptionVariants) { try { const inscriptionCheck = await fetch(`https://ordinals.com/inscription/${potentialInscriptionId}`, { method: 'HEAD' // Use HEAD to avoid downloading content }); if (inscriptionCheck.ok) { console.log('✅ MOBILE: Found inscription ID:', potentialInscriptionId); // Update the database with the inscription ID const updateResponse = await fetch('/api/update-inscription-id', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ transactionId: transactionId, inscriptionId: potentialInscriptionId, artworkSlug: artworkSlug }) }); if (updateResponse.ok) { const updateResult = await updateResponse.json(); console.log('✅ MOBILE: Updated inscription ID in database:', updateResult); // Update the UI display const inscriptionDisplay = document.getElementById('inscription-id-display'); if (inscriptionDisplay) { inscriptionDisplay.innerHTML = `Inscription ID: ${potentialInscriptionId}`; console.log('✅ MOBILE: Updated UI with traced inscription ID'); } // Update mobile status const mobileStatus = document.getElementById('mobile-debug-status'); if (mobileStatus) { mobileStatus.innerHTML = `✅ Inscription ID found and updated: ${potentialInscriptionId}`; mobileStatus.style.color = '#00ff00'; } return; // Success, stop monitoring } } } catch (inscriptionError) { // Try next variant console.log(`🔧 MOBILE: Inscription ${potentialInscriptionId} not found yet, trying next...`); } } } } } catch (error) { console.error(`🔧 MOBILE: Inscription monitoring attempt ${attempt} failed:`, error); } // Wait before next attempt (exponential backoff) const delay = Math.min(30000, 5000 * attempt); // 5s, 10s, 15s, 20s, 25s, 30s... console.log(`🔧 MOBILE: Waiting ${delay/1000}s before next inscription check...`); await new Promise(resolve => setTimeout(resolve, delay)); } console.log('🔧 MOBILE: Inscription ID monitoring completed without finding inscription'); } // Authentication functions async function checkAuthStatus() { if (shouldUseLiveApiForRead()) { currentUser = null; updateAuthUI(); return; } try { console.log('Checking auth status...'); // Add cache-busting timestamp to prevent browser caching const response = await fetch(getReadApiUrl('/api/auth/me?t=' + Date.now()), { credentials: 'include', cache: 'no-store' }); const data = await response.json(); console.log('Auth response:', data); if (data.authenticated && data.user) { console.log('User authenticated:', data.user); currentUser = data.user; updateAuthUI(); if (currentUser.role === 'admin') { void ensureAdminSensitiveModals().catch((e) => console.warn('Admin modals preload failed', e)); } } else { console.log('No authenticated user'); currentUser = null; updateAuthUI(); } } catch (error) { console.error('Auth check failed:', error); currentUser = null; updateAuthUI(); } } function updateAuthUI() { const authStatus = document.getElementById('auth-status'); const homeAuth = document.getElementById('home-redesign-auth'); const globalAuth = document.getElementById('global-redesign-auth'); const overlayAuth = document.getElementById('home-nav-overlay-auth'); const globalOverlayAuth = document.getElementById('global-nav-overlay-auth'); const uname = escapeHtml(currentUser ? currentUser.username : ''); if (authStatus) { if (currentUser) { authStatus.innerHTML = ` Hello, ${uname} ${currentUser.role === 'admin' ? 'Admin' : ''} Logout `; } else { authStatus.innerHTML = ` Login Register `; } } const closeHomeMenu = "if(typeof window.toggleHomeMenu==='function')window.toggleHomeMenu(false);"; const setRedesignAuth = (el) => { if (!el) return; if (currentUser) { el.innerHTML = ` Hello, ${uname} ${currentUser.role === 'admin' ? `Admin` : ''} Logout `; } else { el.innerHTML = `Log In`; } }; setRedesignAuth(homeAuth); setRedesignAuth(globalAuth); const setOverlayAuth = (el) => { if (!el) return; if (currentUser) { el.innerHTML = ` Hello, ${uname} ${currentUser.role === 'admin' ? `Admin` : ''} Logout `; } else { el.innerHTML = ` Log In Register `; } }; setOverlayAuth(overlayAuth); setOverlayAuth(globalOverlayAuth); updateSubmitButtonVisibility(); } function updateSubmitButtonVisibility() { const submitLink = document.getElementById('submit-art-link'); if (submitLink) { submitLink.style.display = currentUser ? 'inline-block' : 'none'; } } // Logout function window.logout = async function() { try { const response = await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); const data = await response.json(); console.log('Logout response:', data); } catch (error) { console.error('Logout error:', error); } // Always clear local state regardless of API response currentUser = null; sessionId = null; updateAuthUI(); const amHost = document.getElementById('admin-sensitive-modals-host'); if (amHost) { amHost.innerHTML = ''; delete amHost.dataset.loaded; } adminSensitiveModalsPromise = null; // Redirect to home window.location.hash = '#home'; }; // Forgot Password Functions window.showForgotPasswordModal = function() { const modal = document.getElementById('forgot-password-modal'); modal.style.display = 'block'; // Clear form document.getElementById('forgotPasswordEmail').value = ''; document.getElementById('forgotPasswordResult').innerHTML = ''; }; window.hideForgotPasswordModal = function() { document.getElementById('forgot-password-modal').style.display = 'none'; }; window.handleForgotPassword = async function() { const email = document.getElementById('forgotPasswordEmail').value; const resultDiv = document.getElementById('forgotPasswordResult'); const btn = document.getElementById('forgotPasswordBtn'); if (!email) { resultDiv.innerHTML = '
    Please enter your email address
    '; return; } btn.innerHTML = 'Sending...'; btn.disabled = true; try { const response = await fetch('/api/auth/forgot-password', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email }) }); const data = await response.json(); if (data.success) { resultDiv.innerHTML = `
    ✓ Email sent!
    ${data.message}
    `; } else { // Show debugging information for development let debugInfo = ''; if (data.debug) { debugInfo = `
    API Key: ${data.debug.hasApiKey ? 'Configured' : 'Missing'}
    Error: ${data.debug.emailError || 'Unknown'}
    ${data.debug.tempPassword ? `
    Temp Password: ${data.debug.tempPassword}
    ` : ''}
    `; } resultDiv.innerHTML = `
    ${data.error}
    ${debugInfo} `; } } catch (error) { resultDiv.innerHTML = '
    Failed to send reset request
    '; } finally { btn.innerHTML = 'Send Reset'; btn.disabled = false; } }; // Change Password Functions window.showChangePasswordModal = function() { const modal = document.getElementById('change-password-modal'); modal.style.display = 'block'; // Clear form document.getElementById('currentPassword').value = ''; document.getElementById('newPassword').value = ''; document.getElementById('confirmPassword').value = ''; document.getElementById('changePasswordResult').innerHTML = ''; }; window.hideChangePasswordModal = function() { document.getElementById('change-password-modal').style.display = 'none'; }; window.handleChangePassword = async function() { const currentPassword = document.getElementById('currentPassword').value; const newPassword = document.getElementById('newPassword').value; const confirmPassword = document.getElementById('confirmPassword').value; const resultDiv = document.getElementById('changePasswordResult'); const btn = document.getElementById('changePasswordBtn'); if (!currentPassword || !newPassword || !confirmPassword) { resultDiv.innerHTML = '
    Please fill in all fields
    '; return; } if (newPassword.length < 6) { resultDiv.innerHTML = '
    New password must be at least 6 characters
    '; return; } if (newPassword !== confirmPassword) { resultDiv.innerHTML = '
    New passwords do not match
    '; return; } btn.innerHTML = 'Changing...'; btn.disabled = true; try { const response = await fetch('/api/auth/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ currentPassword, newPassword }) }); const data = await response.json(); if (data.success) { resultDiv.innerHTML = '
    Password changed successfully!
    '; setTimeout(() => { hideChangePasswordModal(); }, 2000); } else { resultDiv.innerHTML = `
    ${data.error}
    `; } } catch (error) { resultDiv.innerHTML = '
    Failed to change password
    '; } finally { btn.innerHTML = 'Change Password'; btn.disabled = false; } }; // Load active sponsors for footer async function loadActiveSponsors() { try { console.log('🎯 Loading active sponsors...'); const response = await fetch(getReadApiUrl('/api/get-active-sponsors')); const data = await response.json(); console.log('📊 Sponsors API response:', data); if (data.success && data.sponsors && data.sponsors.length > 0) { console.log('✅ Found', data.sponsors.length, 'active sponsors'); const sponsorContainer = document.getElementById('sponsor-icons'); if (sponsorContainer) { console.log('✅ Sponsor container found'); // Check if we've already loaded sponsors using a data attribute if (sponsorContainer.dataset.sponsorsLoaded === 'true') { console.log('⚠️ Sponsors already loaded (data attribute), skipping...'); return; } // Also check for existing sponsor elements (backup check) - exclude founders and hardcoded defaults so API sponsors (e.g. bronze) still load const existingSponsors = sponsorContainer.querySelectorAll('a:not(.founder-sponsor):not(.default-sponsor)'); console.log('🔍 Existing non-founder sponsor elements found:', existingSponsors.length); if (existingSponsors.length > 0) { console.log('⚠️ Sponsors already loaded (DOM check), marking as loaded...'); sponsorContainer.dataset.sponsorsLoaded = 'true'; return; } console.log('🔨 Building sponsor HTML...'); // Keep the default PQ sponsor, add active sponsors after it const sponsorHtml = data.sponsors.map(sponsor => { // Use inline helper functions to avoid dependency issues let sponsorLink = '#'; if (sponsor.website_url) { sponsorLink = sponsor.website_url.startsWith('http://') || sponsor.website_url.startsWith('https://') ? sponsor.website_url : 'https://' + sponsor.website_url; } else if (sponsor.twitter_handle) { sponsorLink = sponsor.twitter_handle.startsWith('http') ? sponsor.twitter_handle : 'https://twitter.com/' + sponsor.twitter_handle.replace('@', ''); } const tierColor = sponsor.tier === 'gold' ? '#FFD700' : sponsor.tier === 'silver' ? '#C0C0C0' : '#CD7F32'; return `
    ${sponsor.sponsor_name}

    ${sponsor.sponsor_name}

    ${sponsor.tier}

    `; }).join(''); console.log('🔨 Generated sponsor HTML:', sponsorHtml); console.log('✅ Appending sponsor HTML to container'); // Append after the default PQ sponsor sponsorContainer.innerHTML += sponsorHtml; // Mark as loaded sponsorContainer.dataset.sponsorsLoaded = 'true'; console.log('✅ Sponsors loaded successfully! Total elements now:', sponsorContainer.querySelectorAll('a').length); // Also populate gold sponsors in the filters sidebar (gold only: hardcoded NoMoreLabs is already there) const goldSidebar = document.getElementById('gold-sponsors-sidebar'); if (goldSidebar) { const goldSponsors = data.sponsors.filter(s => s.tier === 'gold'); goldSponsors.forEach(sponsor => { let sponsorLink = '#'; if (sponsor.website_url) { sponsorLink = sponsor.website_url.startsWith('http') ? sponsor.website_url : 'https://' + sponsor.website_url; } else if (sponsor.twitter_handle) { sponsorLink = sponsor.twitter_handle.startsWith('http') ? sponsor.twitter_handle : 'https://twitter.com/' + sponsor.twitter_handle.replace('@', ''); } const el = document.createElement('a'); el.href = sponsorLink; el.target = '_blank'; el.rel = 'noopener noreferrer'; el.style.cssText = 'text-decoration: none; text-align: center;'; el.innerHTML = `
    ${sponsor.sponsor_name}

    ${sponsor.sponsor_name}

    `; goldSidebar.appendChild(el); }); } } else { console.error('❌ Sponsor container not found'); } } else { console.log('ℹ️ No active sponsors found or API returned error'); } } catch (error) { console.error('❌ Failed to load sponsors:', error); // Fail silently - footer still shows default sponsor } } // Initialize app async function initializeApp() { updateDesignPageState(); // Set up global modal first setupGlobalModal(); // NOTE: Sponsors are loaded in showHomePage() after template renders // Clear search field to prevent autofill setTimeout(() => { const searchField = document.getElementById('artwork-search'); if (searchField) { searchField.value = ''; } }, 100); // Check authentication status first await checkAuthStatus(); const currentHash = window.location.hash; const onMintPath = /^\/mint\/[^/]+\/?$/.test(window.location.pathname); const onAuctionPath = /^\/auction\/[^/]+\/?$/.test(window.location.pathname); if (!currentHash || currentHash === '#' || currentHash === '') { scrollRouteToTop(); if (onMintPath || onAuctionPath) { initRouter(); } else { showHomePage(); window.history.replaceState(null, '', '/#home'); updateDesignPageState(); if (typeof window.initializeHomeRedesignNav === 'function') { window.initializeHomeRedesignNav(); } } } else { initRouter(); } // Final check setTimeout(() => { const appContent = document.getElementById('app').innerHTML; }, 500); } // Immediate initialization - don't wait for events // Initialize as soon as script runs if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { initializeApp(); }); } else { initializeApp(); } // Also listen for load event as backup // Set up global modal functionality function setupGlobalModal() { const saveBtn = document.getElementById('saveArtworkBtn'); if (saveBtn) { saveBtn.onclick = saveArtwork; } } // Calculate Advent day (1-31 for December) window.getAdventDay = function() { const now = new Date(); const month = now.getMonth(); // 0-indexed, so December is 11 const day = now.getDate(); // Only show day number in December if (month === 11) { // December return day; } return 1; // Default to Day 1 if not December }; // Update Advent title with current day window.updateAdventTitle = function() { const titleElement = document.getElementById('advent-title'); if (titleElement) { const day = getAdventDay(); const artists = { 1: 'DuKe Höek', 2: 'Toph', 3: 'Sepim', 4: 'SxulR', 5: 'NEEDTHAT', 6: 'TheArtist', 7: 'Clare Von Savage', 8: 'Lady Redhorns', 9: 'TheArtist', 10: 'DethReaper', 11: 'Boozy', 12: 'ViolaMissCode', 13: 'Fuzey', 14: 'ArtSerge', 15: 'Sportoons', 16: 'Michelle Thompson', 17: 'Ronin', 18: 'TheArtist', 19: 'SxulR', 20: 'Reset', 22: 'Milla Si', 23: 'NEEDTHAT', 24: 'Unknown' }; titleElement.textContent = `DAY ${day} - ${artists[day] || 'Unknown'}`; } // Update video source based on current day const video = document.getElementById('advent-video'); if (video) { const day = getAdventDay(); const source = video.querySelector('source'); if (source) { source.src = `https://pub-c4a124b338574b5dae1d2b555bfa25a6.r2.dev/advent/day${day}.mp4?v=1`; video.load(); } } }; // Advent Calendar - Play video on image click with fade effect window.playAdventVideo = function() { console.log('playAdventVideo called'); const imageContainer = document.getElementById('advent-image-container'); const videoContainer = document.getElementById('advent-video-container'); const video = document.getElementById('advent-video'); console.log('Elements found:', { imageContainer: !!imageContainer, videoContainer: !!videoContainer, video: !!video }); if (imageContainer && videoContainer && video) { console.log('Starting fade transition...'); // Fade out image imageContainer.style.opacity = '0'; // After fade out, hide image and show video setTimeout(() => { imageContainer.style.display = 'none'; videoContainer.style.display = 'block'; console.log('Video container visible, attempting to play...'); // Fade in video and play immediately setTimeout(() => { videoContainer.style.opacity = '1'; // Add error listener video.onerror = function(e) { console.error('Video load error:', e); console.error('Video error code:', video.error); if (video.error) { alert('Video failed to load: ' + video.error.message + ' (Code: ' + video.error.code + ')'); } }; // Add loadstart listener video.onloadstart = function() { console.log('Video started loading...'); }; // Add canplay listener video.oncanplay = function() { console.log('Video can play now!'); }; console.log('Video src:', video.src || video.querySelector('source').src); console.log('Video readyState:', video.readyState); // Load video if not loaded if (video.readyState < 2) { console.log('Loading video...'); video.load(); } // Wait a moment for load, then play setTimeout(() => { // Play video with proper promise handling console.log('Calling video.play()...'); console.log('Video readyState after load:', video.readyState); const playPromise = video.play(); if (playPromise !== undefined) { playPromise.then(() => { console.log('Video playing successfully!'); }).catch(error => { console.error('Video play error:', error); alert('Could not play video: ' + error.message); }); } }, 500); }, 50); }, 300); } else { console.error('Missing elements!'); } }; // Advent Calendar - Close video window.closeAdventVideo = function() { const imageContainer = document.getElementById('advent-image-container'); const videoContainer = document.getElementById('advent-video-container'); const video = document.getElementById('advent-video'); if (imageContainer && videoContainer && video) { // Pause video safely try { video.pause(); } catch (e) { console.log('Video pause error:', e); } video.currentTime = 0; // Fade out video videoContainer.style.opacity = '0'; // After fade out, hide video and show image setTimeout(() => { videoContainer.style.display = 'none'; imageContainer.style.display = 'block'; // Fade in image setTimeout(() => { imageContainer.style.opacity = '1'; }, 50); }, 300); } }; // Initialize advent calendar when view is shown document.addEventListener('DOMContentLoaded', function() { updateAdventTitle(); }); window.addEventListener('load', () => { // Set up global modal setupGlobalModal(); // Only init if no content loaded yet const appContainer = document.getElementById('app'); const hasLoadingFallback = document.getElementById('loading-fallback'); if (hasLoadingFallback) { initializeApp(); } }); // Handle hash changes for navigation window.addEventListener('hashchange', () => { clearAllSearchFields(); // Clear search fields on navigation updateDesignPageState(); initRouter(); }); window.addEventListener('popstate', () => { clearAllSearchFields(); updateDesignPageState(); initRouter(); }); // No other initialization needed