Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> | |
| <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"> | |
| <meta http-equiv="Pragma" content="no-cache"> | |
| <meta http-equiv="Expires" content="0"> | |
| <!-- build-tag: v-codex-live-headers-1 --> | |
| <title>BloxID - Telegram WebApp Clicker Game</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <!-- Cache busting timestamp: 2025-09-01 03:17:00 --> | |
| <script src="https://telegram.org/js/telegram-web-app.js"></script> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| colors: { | |
| bg: '#000000', | |
| card: '#111111', | |
| muted: '#1a1a1a', | |
| text: '#ffffff', | |
| accent: 'rgb(228, 24, 95)', | |
| accent2: '#ff2a6d', | |
| secondary: '#00f0ff', | |
| tertiary: '#8a8a8a' | |
| }, | |
| fontFamily: { | |
| sans: ['Inter', 'sans-serif'] | |
| } | |
| } | |
| } | |
| } | |
| // Telegram Login (desktop link) with direct OAuth fallback | |
| function openTelegramLogin(){ | |
| // Direct OAuth URL (no widget). Bot ID taken from bot token. | |
| const BOT_ID = '8487518701'; | |
| const origin = encodeURIComponent(window.location.origin); | |
| const returnTo = encodeURIComponent('/api/telegram/auth_redirect.php'); | |
| const url = `https://oauth.telegram.org/auth?bot_id=${BOT_ID}&origin=${origin}&embed=0&request_access=write&return_to=${returnTo}`; | |
| // Always navigate in the same tab to avoid popup blockers | |
| window.location.href = url; | |
| return; | |
| // Start polling for cookie set by auth_redirect.php (last-resort fallback) | |
| try { | |
| let attempts = 0; | |
| const maxAttempts = 40; // ~20s | |
| const poll = setInterval(() => { | |
| attempts++; | |
| try { | |
| const m = document.cookie.match(/(?:^|; )tg_user=([^;]+)/); | |
| if (m) { | |
| const raw = decodeURIComponent(m[1]); | |
| const user = JSON.parse(atob(raw)); | |
| if (user && user.id) { | |
| console.log('🕑 Cookie polling captured Telegram user'); | |
| clearInterval(poll); | |
| document.cookie = 'tg_user=; Max-Age=0; path=/'; | |
| onTelegramAuth(user); | |
| } | |
| } | |
| } catch(e) {} | |
| if (attempts >= maxAttempts) clearInterval(poll); | |
| }, 500); | |
| } catch(e) {} | |
| } | |
| // Listen for fallback auth messages from auth_redirect.php | |
| window.addEventListener('message', function(event){ | |
| try { | |
| if (!event || !event.data) return; | |
| if (event.data.type === 'telegram-auth' && event.data.user) { | |
| console.log('📨 Received Telegram auth via postMessage'); | |
| onTelegramAuth(event.data.user); | |
| } | |
| } catch(e) { } | |
| }); | |
| // Storage event fallback (fires when auth_redirect writes localStorage) | |
| window.addEventListener('storage', function(e){ | |
| try { | |
| if (e.key === 'tg_user' && e.newValue) { | |
| const user = JSON.parse(atob(e.newValue)); | |
| if (user && user.id) { | |
| console.log('🧰 Received Telegram auth via storage event'); | |
| onTelegramAuth(user); | |
| } | |
| } | |
| } catch(err) {} | |
| }); | |
| async function onTelegramAuth(user){ | |
| try { | |
| const res = await fetch('/api/telegram/login.php', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ user }) | |
| }); | |
| const data = await res.json(); | |
| if (data && data.ok) { | |
| currentUser = { id: data.tid, first_name: user.first_name, username: data.username, photo_url: user.photo_url }; | |
| isLoggedIn = true; | |
| try { localStorage.setItem('bloxid_current_user', JSON.stringify(currentUser)); } catch(e) {} | |
| // Provide an app token for downstream API calls (desktop web flow) | |
| try { sessionStorage.setItem('app_token', data.hash || 'telegram'); } catch(e) {} | |
| updateUserProfile(currentUser); | |
| // Trigger data loads now that we are logged in | |
| try { loadUserData(); } catch(e) {} | |
| try { loadQuests(); } catch(e) {} | |
| // Persist to Admin users with session type | |
| try { | |
| const sessionType = (window.Telegram && window.Telegram.WebApp) ? 'mobile' : 'web_desktop'; | |
| fetch('/api/track_user.php', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ telegram_user_id: Number(currentUser.id), username: currentUser.username||currentUser.first_name||null, session_type: sessionType }) | |
| }).catch(()=>{}); | |
| } catch(e) {} | |
| showNotification('✅ Telegram linked successfully!', 'success'); | |
| // Update connect button UI | |
| try { | |
| const btn = document.getElementById('tgConnectBtn'); | |
| const txt = document.getElementById('tgBtnText'); | |
| if (btn) btn.classList.add('bg-blue-600','border-blue-500'); | |
| if (txt) txt.textContent = 'Connected'; | |
| } catch(e) {} | |
| const m = document.getElementById('tgLoginModal'); if (m) m.remove(); | |
| } else { | |
| showNotification('⚠️ Telegram link failed', 'error'); | |
| } | |
| } catch (e) { | |
| showNotification('⚠️ Telegram link error', 'error'); | |
| } | |
| } | |
| // Ensure Telegram widget can reach the handler in global scope | |
| window.onTelegramAuth = onTelegramAuth; | |
| </script> | |
| <script> | |
| // TEMP: Minimal Thirdweb login shim (no SDK) to unblock unified login | |
| async function thirdwebLoginShim(){ | |
| try { | |
| const email = prompt('Enter email to link Thirdweb'); if (!email) return; | |
| let wallet = prompt('Paste wallet (0x...) or leave blank'); | |
| if (!wallet || wallet.length < 6) { | |
| wallet = 'dev_' + (localStorage.getItem('tw_dev_wallet') || (function(){ | |
| const w = '0x' + Math.random().toString(16).slice(2) + Date.now().toString(16); | |
| localStorage.setItem('tw_dev_wallet', w); return w; | |
| })()); | |
| } | |
| const thirdweb_user_id = 'tw_' + btoa(email).replace(/[^a-zA-Z0-9]/g,'').slice(0,24); | |
| const session_type = 'web_desktop'; | |
| // Try server, but don’t block UX if it fails | |
| let serverOk = false, appToken = null; | |
| try { | |
| const res = await fetch('/api/auth/thirdweb_login.php', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ thirdweb_user_id, email, wallet_address: wallet, session_type }) | |
| }); | |
| const j = await res.json().catch(()=>null); | |
| if (j && j.ok) { serverOk = true; appToken = j.app_token || null; } | |
| } catch (e) {} | |
| // Always mark connected locally (sync later if server failed) | |
| try { | |
| if (appToken) sessionStorage.setItem('app_token', appToken); | |
| localStorage.setItem('thirdweb_email', email); | |
| localStorage.setItem('thirdweb_wallet', wallet); | |
| sessionStorage.setItem('thirdweb_connected', '1'); | |
| } catch (e) {} | |
| if (typeof showNotification === 'function') { | |
| showNotification(serverOk ? '✅ Thirdweb connected' : '✅ Connected (server sync pending)', serverOk ? 'success' : 'warning'); | |
| } else { | |
| alert(serverOk ? '✅ Thirdweb connected' : '✅ Connected (server sync pending)'); | |
| } | |
| // Optional: update button UI | |
| try { | |
| const btn = document.getElementById('twConnectBtn') || document.getElementById('twConnectBtnStatic'); | |
| if (btn) btn.textContent = 'Connected'; | |
| } catch (e) {} | |
| } catch (e) { | |
| if (typeof showNotification === 'function') showNotification('⚠️ Thirdweb error', 'error'); | |
| else alert('⚠️ Thirdweb error'); | |
| } | |
| } | |
| </script> | |
| <script> document.addEventListener('DOMContentLoaded', function(){ try { if (sessionStorage.getItem('thirdweb_connected') === '1') { const btn = document.getElementById('twConnectBtn') || document.getElementById('twConnectBtnStatic'); if (btn) btn.textContent = 'Connected'; } } catch(e) {} }); </script> | |
| <script> | |
| // Inject a desktop-only Thirdweb connect button for MVP | |
| document.addEventListener('DOMContentLoaded', function(){ | |
| try { | |
| // Detect real Telegram WebApp via user agent (shim should not block desktop) | |
| const isRealTG = /Telegram/i.test(navigator.userAgent || ''); | |
| if (isRealTG) return; | |
| // Try to place below the Telegram connect button if it exists | |
| const tgBtn = document.getElementById('tgConnectBtn'); | |
| const twBtn = document.createElement('button'); | |
| twBtn.id = 'twConnectBtn'; | |
| twBtn.textContent = 'Connect with Thirdweb'; | |
| twBtn.onclick = thirdwebLoginShim; | |
| if (tgBtn) { | |
| // Match size/styles by cloning classes; add margin-top | |
| try { twBtn.className = tgBtn.className; } catch(e) {} | |
| try { twBtn.style.marginTop = '8px'; } catch(e) {} | |
| tgBtn.parentNode.insertBefore(twBtn, tgBtn.nextSibling); | |
| } else { | |
| // Fallback fixed button if we can't find tg button | |
| twBtn.style.position = 'fixed'; | |
| twBtn.style.zIndex = '1000'; | |
| twBtn.style.right = '16px'; | |
| twBtn.style.top = '70px'; | |
| twBtn.style.padding = '10px 14px'; | |
| twBtn.style.borderRadius = '10px'; | |
| twBtn.style.border = '1px solid rgba(255,255,255,0.2)'; | |
| twBtn.style.background = 'linear-gradient(135deg, #6366f1, #06b6d4)'; | |
| twBtn.style.color = '#fff'; | |
| twBtn.style.cursor = 'pointer'; | |
| document.body.appendChild(twBtn); | |
| } | |
| } catch(e) {} | |
| }); | |
| </script> | |
| <style> | |
| :root { | |
| /* Cyberpunk Elegance Color Scheme */ | |
| --bg: #0f172a; | |
| --card: #334155; | |
| --muted: #475569; | |
| --text: #f8fafc; | |
| --accent: #6366f1; | |
| --accent-2: #06b6d4; | |
| --secondary: #f59e0b; | |
| --tertiary: #64748b; | |
| /* Additional colors */ | |
| --success: #10b981; | |
| --warning: #f97316; | |
| --error: #ef4444; | |
| --bg-secondary: #1e293b; | |
| } | |
| /* Google Fonts loaded via <link> tags in <head> */ | |
| body { | |
| background-color: var(--bg); | |
| color: var(--text); | |
| font-family: 'Inter', sans-serif; | |
| overflow-x: hidden; | |
| user-select: none; | |
| line-height: 1.6; | |
| /* Mobile optimizations */ | |
| -webkit-user-select: none; | |
| -webkit-touch-callout: none; | |
| -webkit-tap-highlight-color: transparent; | |
| touch-action: manipulation; | |
| /* Smooth scrolling */ | |
| scroll-behavior: smooth; | |
| -webkit-overflow-scrolling: touch; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| /* Prevent zoom on double tap */ | |
| * { | |
| touch-action: manipulation; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| /* Mobile-specific styles */ | |
| @media (max-width: 768px) { | |
| body { | |
| overflow-x: hidden; /* Only prevent horizontal scrolling */ | |
| overflow-y: auto; /* Allow vertical scrolling */ | |
| width: 100%; | |
| min-height: 100vh; | |
| min-height: 100dvh; | |
| } | |
| /* Ensure main container allows scrolling */ | |
| .min-h-screen { | |
| min-height: 100vh; | |
| min-height: 100dvh; | |
| } | |
| /* Show Aura Display Box on mobile as requested */ | |
| .absolute.top-4.right-4 { display: block ; } | |
| .goldenblox-responsive { | |
| display: none ; /* Hide GBLX Display Box */ | |
| } | |
| /* Fix mobile progress bar text alignment */ | |
| .absolute.bottom-0.left-0.right-0.p-4.md\\:hidden #passportTapLevelNextTextMobile { | |
| display: none ; /* Hide misaligned "Need X Aura" text */ | |
| } | |
| /* Hide Force Update button and any problematic white text on mobile */ | |
| button[onclick="forceLevelUpdate()"] { | |
| display: none ; | |
| } | |
| /* Hide any white text elements that might be misaligned in Level Progress box */ | |
| @media (max-width: 768px) { | |
| /* Only hide white text in specific problematic areas, not everywhere */ | |
| .card-glass .text-white:not(.my-bloxid-text):not(.wallet-button-text):not(.aura-progress-text):not(.level-info-text):not(.prevent-hide) { | |
| color: white ; /* Make white text visible on mobile */ | |
| } | |
| /* But keep the level number visible */ | |
| #passportLevelNumber { | |
| color: white ; | |
| } | |
| /* Ensure My BloxID text is always visible */ | |
| .my-bloxid-text { | |
| color: white ; | |
| } | |
| /* Ensure wallet button text is always visible */ | |
| .wallet-button-text { | |
| color: white ; | |
| fill: white ; | |
| opacity: 1 ; | |
| } | |
| /* Ensure wallet icon stays visible */ | |
| .wallet-icon { fill: #ffffff ; opacity: 1 ; } | |
| /* Make left info container full-width and allow button 100% width */ | |
| .left-info-container { width: calc(100% - 2rem); max-width: none; } | |
| #walletConnectBtn { width: 6rem ; } /* Match w-24 (96px) */ | |
| } | |
| /* Also hide desktop level progress bar on mobile */ | |
| .absolute.bottom-0.left-0.right-0.p-4.hidden.md\\:block { | |
| display: none ; /* Hide Desktop Level Progress Bar */ | |
| } | |
| /* Hide badges from user info on mobile */ | |
| .mt-2.flex.flex-col.space-y-1 { | |
| display: none ; /* Hide Badges Below User Info */ | |
| } | |
| /* Move user info down by 78px more */ | |
| .absolute.top-4.left-4 .mt-2.card-glass { | |
| position: fixed ; | |
| top: auto ; | |
| bottom: 17px ; /* 95px - 78px = 17px from bottom navigation */ | |
| left: 50% ; | |
| transform: translateX(-50%) ; | |
| z-index: 20 ; | |
| margin-top: 0 ; | |
| } | |
| /* Make user info more compact on mobile */ | |
| .absolute.top-4.left-4 .mt-2.card-glass .flex.items-center.space-x-2 { | |
| justify-content: center ; | |
| } | |
| /* Hide BloxID logo on mobile for cleaner tapping area */ | |
| .absolute.top-4.left-4 .card-glass.rounded-xl.overflow-hidden.bg-white\\/10:not(.mt-2) { | |
| display: none ; /* Hide BloxID Logo */ | |
| } | |
| /* Hide "My BloxID" text label on mobile */ | |
| .absolute.top-4.left-1\\/2 { | |
| display: none ; /* Hide My BloxID Text Label */ | |
| } | |
| /* Hide the level progress bar that appears in the mohawk head tapping zone */ | |
| .absolute.bottom-0.left-0.right-0.p-4 { | |
| display: none ; /* Hide all progress bars in tapping zone */ | |
| } | |
| } | |
| .card-glass { | |
| background: linear-gradient(145deg, rgba(51, 65, 85, 0.9), rgba(30, 41, 59, 0.8)); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid rgba(99, 102, 241, 0.2); | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| .card-glass:hover { | |
| border: 1px solid rgba(99, 102, 241, 0.4); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| transform: translateY(-2px); | |
| } | |
| /* Card hover animations for desktop */ | |
| .card-hover { | |
| transition: all 0.3s ease; | |
| transform: translateY(0); | |
| } | |
| .card-hover:hover { | |
| transform: translateY(-8px); | |
| box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2); | |
| border-color: rgba(99, 102, 241, 0.6); | |
| } | |
| .tap-button { | |
| background: linear-gradient(145deg, rgba(228, 24, 95, 0.8), rgba(0, 240, 255, 0.6)); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| transition: all 0.2s ease; | |
| } | |
| .tap-button:active { | |
| transform: scale(0.95); | |
| background: linear-gradient(145deg, rgba(228, 24, 95, 1), rgba(0, 240, 255, 0.8)); | |
| } | |
| .nav-btn { | |
| transition: all 0.3s ease; | |
| border-radius: 0.75rem; | |
| padding: 0.75rem 1.5rem; | |
| font-weight: 600; | |
| } | |
| .nav-btn.active { | |
| background: rgba(255, 255, 255, 0.1) ; | |
| backdrop-filter: blur(12px) ; | |
| border: 1px solid rgba(255, 255, 255, 0.2) ; | |
| color: white ; | |
| box-shadow: 0 4px 12px rgba(255, 255, 255, 0.1) ; | |
| transform: none ; | |
| } | |
| .nav-btn:hover:not(.active) { | |
| background: rgba(255, 255, 255, 0.03); | |
| backdrop-filter: blur(6px); | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| transform: translateY(-1px); | |
| } | |
| .notification { | |
| background: linear-gradient(145deg, rgba(99, 102, 241, 0.2), rgba(6, 182, 212, 0.1)); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid rgba(99, 102, 241, 0.3); | |
| } | |
| /* New button styles */ | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--accent), var(--accent-2)); | |
| color: white; | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 0.75rem; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| border: none; | |
| cursor: pointer; | |
| box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 20px rgba(99, 102, 241, 0.4); | |
| } | |
| .btn-secondary { | |
| background: rgba(99, 102, 241, 0.1); | |
| color: var(--accent); | |
| padding: 0.5rem 1rem; | |
| border-radius: 0.5rem; | |
| font-weight: 500; | |
| transition: all 0.3s ease; | |
| border: 1px solid rgba(99, 102, 241, 0.2); | |
| cursor: pointer; | |
| } | |
| .btn-secondary:hover { | |
| background: rgba(99, 102, 241, 0.2); | |
| border-color: rgba(99, 102, 241, 0.4); | |
| transform: translateY(-1px); | |
| } | |
| /* Typography improvements */ | |
| .text-display { | |
| font-size: 2.5rem; | |
| font-weight: 800; | |
| line-height: 1.2; | |
| } | |
| .text-heading { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| line-height: 1.3; | |
| } | |
| .text-subtitle { | |
| font-size: 1.125rem; | |
| font-weight: 600; | |
| line-height: 1.4; | |
| } | |
| .text-body { | |
| font-size: 1rem; | |
| font-weight: 400; | |
| line-height: 1.6; | |
| } | |
| .text-caption { | |
| font-size: 0.875rem; | |
| font-weight: 400; | |
| line-height: 1.5; | |
| } | |
| .tap-effect { | |
| animation: floatUp 0.6s ease-out forwards; | |
| } | |
| @keyframes floatUp { | |
| 0% { | |
| opacity: 1; | |
| transform: translateY(0) scale(1); | |
| } | |
| 100% { | |
| opacity: 0; | |
| transform: translateY(-40px) scale(1.1); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-slate-900 text-slate-100" data-build="v-raven-buddy-1"> | |
| <!-- Main App Container --> | |
| <div class="min-h-screen flex flex-col"> | |
| <!-- Main Content --> | |
| <main class="flex-1 px-4 sm:px-6 lg:px-8 py-6"> | |
| <div class="max-w-7xl mx-auto"> | |
| <!-- PASSPORT Tab --> | |
| <div id="passportTab" class="hidden space-y-6"> | |
| <!-- Hero Section for Quests with BloxID Features --> | |
| <section class="pt-2 pb-4 px-4 sm:px-6 lg:px-8"> | |
| <div class="max-w-7xl mx-auto"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-4"> | |
| <!-- Main Quest Hero Box with BloxID Features --> | |
| <div class="card-glass rounded-2xl overflow-hidden p-0 lg:col-span-2 relative hover:border-accent/50 transition-colors" style="min-height: 400px; background: linear-gradient(135deg, rgba(124, 76, 228, 0.15) 0%, rgba(0, 255, 255, 0.15) 100%);"> | |
| <!-- Mini Logo with My BloxID Logo --> | |
| <div class="absolute top-4 left-4 z-10 left-info-container"> | |
| <div class="card-glass rounded-xl overflow-hidden bg-white/10 border border-white/20 backdrop-blur-sm relative w-24 h-24 lg:w-32 lg:h-32 p-2"> | |
| <img src="bloxid_cube_NEW_PNG.png" alt="My BloxID Logo" class="w-full h-full object-contain"> | |
| </div> | |
| <!-- Desktop-only: User info card under logo --> | |
| <div class="hidden md:block mt-2 card-glass rounded-xl p-2 bg-white/10 border border-white/20 backdrop-blur-sm"> | |
| <div class="flex items-center space-x-2"> | |
| <div class="w-8 h-8 rounded-full bg-gradient-to-r from-accent to-secondary flex items-center justify-center font-bold text-xs">U</div> | |
| <div class="text-left"> | |
| <div class="font-semibold text-xs" id="usernameDesktop">User</div> | |
| <div class="text-xs text-gray-300">Level 1</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Connect buttons below logo (stack left on desktop) --> | |
| <div class="mt-2 flex md:flex-col items-center md:items-start space-x-2 md:space-x-0 md:space-y-2"> | |
| <button id="walletConnectBtn" onclick="connectTelegramWallet()" class="card-glass rounded-xl px-3 py-2 border text-white font-semibold text-xs flex items-center space-x-2 wallet-button-text prevent-hide"> | |
| <svg class="w-4 h-4 wallet-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10" fill="#0098EA"/><path d="M12 6l5.5 5.5L12 18 6.5 11.5 12 6z" fill="#fff"/></svg> | |
| <span id="walletBtnText" class="wallet-button-text prevent-hide">Connect</span> | |
| </button> | |
| <button id="tgConnectBtn" onclick="openTelegramLogin()" class="hidden md:flex card-glass rounded-xl px-3 py-2 border text-white font-semibold text-xs items-center space-x-2"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-4 h-4" fill="currentColor"><path d="M9.74 15.02l-.4 5.64c.57 0 .82-.24 1.12-.53l2.68-2.56 5.55 4.07c1.02.56 1.74.27 2.01-.95l3.63-17.03h.01c.32-1.48-.53-2.06-1.51-1.7L1.23 9.74c-1.46.57-1.44 1.39-.25 1.76l5.66 1.77L19.4 6.02c.63-.42 1.2-.19.73.23"/></svg> | |
| <span id="tgBtnText">Connect</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Aura Display Box (top right corner) --> | |
| <div class="absolute top-4 right-4 z-10"> | |
| <div class="card-glass rounded-xl overflow-hidden bg-white/10 border border-white/20 backdrop-blur-sm relative w-24 h-24 lg:w-32 lg:h-32 p-2"> | |
| <div class="text-center h-full flex flex-col justify-center"> | |
| <div class="flex flex-col items-center justify-center mb-1"> | |
| <img src="aura_new.png" alt="Aura" class="w-8 h-8 lg:w-10 lg:h-10 object-contain mb-1"> | |
| <h2 class="text-lg lg:text-2xl xl:text-3xl font-bold text-white" id="passportScoreDisplay">12,450</h2> | |
| </div> | |
| <p class="text-white text-xs">Total Aura</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- GBLX Display Box (mobile perfect, desktop perfect) --> | |
| <div class="absolute right-4 z-10 goldenblox-responsive"> | |
| <style> | |
| .goldenblox-responsive { | |
| top: 122px; /* Mobile default */ | |
| } | |
| @media (min-width: 768px) { | |
| .goldenblox-responsive { | |
| top: 152px ; /* Desktop override */ | |
| } | |
| } | |
| </style> | |
| <div class="card-glass rounded-xl overflow-hidden bg-white/10 border border-white/20 backdrop-blur-sm relative w-24 h-24 lg:w-32 lg:h-32 p-2"> | |
| <div class="text-center h-full flex flex-col justify-center"> | |
| <div class="flex flex-col items-center justify-center mb-1"> | |
| <img src="GoldenBlox_NEW.png" alt="GBLX" class="w-8 h-8 lg:w-10 lg:h-10 object-contain mb-1"> | |
| <h2 class="text-lg lg:text-2xl xl:text-3xl font-bold text-white" id="passportTapGBLXDisplay">1,250</h2> | |
| </div> | |
| <p class="text-gray-300 text-xs">GBLX</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- My BloxID Text Label at Top Center --> | |
| <div class="absolute top-4 left-1/2 transform -translate-x-1/2 z-10"> | |
| <span class="text-sm font-bold text-white bg-black/20 px-3 py-1 rounded-full my-bloxid-text">My BloxID</span> | |
| </div> | |
| <!-- Bottom-left user info (separate container from logo/buttons) --> | |
| <div class="absolute left-4 bottom-4 z-10 md:hidden"> | |
| <div class="card-glass rounded-lg p-2 bg-white/10 border border-white/20 backdrop-blur-sm"> | |
| <div class="flex items-center space-x-2"> | |
| <div class="w-7 h-7 rounded-full bg-gradient-to-r from-accent to-secondary flex items-center justify-center font-bold text-xs">U</div> | |
| <div class="font-semibold text-xs truncate max-w-[160px]" id="usernameBottom">User</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Tap Area Content --> | |
| <div class="absolute inset-0 flex flex-col justify-center items-center p-4 md:p-8 text-center"> | |
| <div class="space-y-4 md:space-y-6"> | |
| <!-- Tap Button with Mohawk Cube Head (30% bigger) --> | |
| <div> | |
| <button onclick="enhancedTap()" style="background: none; border: none; padding: 0;"> | |
| <img src="mohawk_cube_head.png" alt="Mohawk Cube Head" | |
| class="tap-image" | |
| style="max-width: 260px; max-height: 260px;"> | |
| </button> | |
| </div> | |
| <!-- Aura Progress Bar in Bottom Right Corner (Mobile) --> | |
| <div class="absolute bottom-4 right-4 md:hidden z-20"> | |
| <div class="card-glass rounded-lg p-2 bg-white/10 border border-white/20 backdrop-blur-sm w-28" onclick="showAuraDetails()"> | |
| <div class="text-center mb-1"> | |
| <span class="text-xs text-white font-semibold aura-progress-text">Aura Progress</span> | |
| <div class="text-xs text-gray-300 font-medium mt-0.5 level-info-text" id="mobileLevelInfo">Level 0 | Need 100 to level 2</div> | |
| </div> | |
| <div class="w-full bg-white/20 rounded-full h-2 backdrop-blur-sm relative overflow-hidden"> | |
| <div id="mobileAuraProgress" class="bg-gradient-to-r from-accent to-accent2 h-2 rounded-full" style="width: 0%"></div> | |
| </div> | |
| <div class="text-center"> | |
| <span class="text-xs text-white aura-progress-text" id="mobileAuraText">0 / 100</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Mobile Level Progress Bar at Bottom --> | |
| <div class="absolute bottom-0 left-0 right-0 p-4 md:hidden"> | |
| <div class="card-glass rounded-xl p-2 bg-white/5 border border-white/10 backdrop-blur-sm"> | |
| <div class="flex items-center justify-between text-xs mb-1"> | |
| <span id="passportTapLevelNumberMobile" class="font-semibold text-xs">Level 1</span> | |
| <span id="passportTapLevelNextTextMobile" class="text-gray-400 text-xs">Need 100 Aura</span> | |
| </div> | |
| <div class="w-full bg-white/10 rounded-full h-2 backdrop-blur-sm"> | |
| <div id="passportTapLevelProgressFillMobile" class="bg-gradient-to-r from-accent to-accent2 h-2 rounded-full transition-all duration-300" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Desktop Level Progress Bar at Bottom --> | |
| <div class="absolute bottom-0 left-0 right-0 p-4 hidden md:block"> | |
| <div class="card-glass rounded-xl p-2 bg-white/5 border border-white/10 backdrop-blur-sm"> | |
| <div class="flex items-center justify-between text-xs mb-1"> | |
| <span id="passportTapLevelNumberDesktop" class="font-semibold text-xs">Level 1</span> | |
| <span id="passportTapLevelNextTextDesktop" class="text-gray-400 text-xs">Need 100 Aura to reach Level 2</span> | |
| </div> | |
| <div class="w-full bg-white/10 rounded-full h-2 backdrop-blur-sm"> | |
| <div id="passportTapLevelProgressFillDesktop" class="bg-gradient-to-r from-accent to-accent2 h-2 rounded-full transition-all duration-300" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- GBLX Box --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-yellow-900/20 to-orange-900/20 p-4 relative"> | |
| <!-- Tag in top left corner --> | |
| <div class="absolute top-4 left-4 z-10"> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium"> | |
| GBLX | |
| </div> | |
| </div> | |
| <!-- Content centered --> | |
| <div class="h-full flex flex-col justify-center items-center"> | |
| <div class="flex items-center justify-center mb-2"> | |
| <img src="GoldenBlox_NEW.png" alt="GBLX" class="w-40 h-40"> | |
| </div> | |
| <h3 class="text-lg font-bold mt-1" id="passportBalanceDisplay">1,250</h3> | |
| <p class="text-gray-400 text-xs mt-1">Spendable GBLX</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs" onclick="tap()"> | |
| Tap to spend | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Additional BloxID Feature Boxes --> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mt-4"> | |
| <!-- Badge Vault Box --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-blue-900/20 to-indigo-900/20 p-4 relative"> | |
| <!-- Tag in top left corner --> | |
| <div class="absolute top-4 left-4 z-10"> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium"> | |
| 🏆 Badge Vault | |
| </div> | |
| </div> | |
| <!-- Content with 14 badge slots --> | |
| <div class="h-full flex flex-col justify-center pt-8"> | |
| <div class="grid grid-cols-7 gap-2"> | |
| <!-- Filled Badge Slots (3) --> | |
| <button onclick="showBadgeModal('blueblox')" class="badge-slot w-12 h-12 rounded-lg hover:bg-white/10 transition-all duration-200 flex items-center justify-center relative group"> | |
| <img src="BlueBlox_NEW.png" alt="BlueBlox Badge" class="w-8 h-8 object-contain"> | |
| </button> | |
| <button onclick="showBadgeModal('problox')" class="badge-slot w-12 h-12 rounded-lg hover:bg-white/10 transition-all duration-200 flex items-center justify-center relative group"> | |
| <img src="ProBlox_NEW.png" alt="ProBlox Badge" class="w-8 h-8 object-contain"> | |
| </button> | |
| <button onclick="showBadgeModal('mayorblox')" class="badge-slot w-12 h-12 rounded-lg hover:bg-white/10 transition-all duration-200 flex items-center justify-center relative group"> | |
| <img src="mayorblox_NEW.png" alt="MayorBlox Badge" class="w-8 h-8 object-contain"> | |
| </button> | |
| <!-- Empty Badge Slots (11) --> | |
| <div class="badge-slot w-12 h-12 rounded-lg bg-white/5 border border-white/10 backdrop-blur-sm flex items-center justify-center relative group"> | |
| <div class="w-8 h-8 rounded-lg bg-white/10 border border-white/20 flex items-center justify-center"> | |
| <span class="text-xs text-gray-400">?</span> | |
| </div> | |
| </div> | |
| <div class="badge-slot w-12 h-12 rounded-lg bg-white/5 border border-white/10 backdrop-blur-sm flex items-center justify-center relative group"> | |
| <div class="w-8 h-8 rounded-lg bg-white/10 border border-white/20 flex items-center justify-center"> | |
| <span class="text-xs text-gray-400">?</span> | |
| </div> | |
| </div> | |
| <div class="badge-slot w-12 h-12 rounded-lg bg-white/5 border border-white/10 backdrop-blur-sm flex items-center justify-center relative group"> | |
| <div class="w-8 h-8 rounded-lg bg-white/10 border border-white/20 flex items-center justify-center"> | |
| <span class="text-xs text-gray-400">?</span> | |
| </div> | |
| </div> | |
| <div class="badge-slot w-12 h-12 rounded-lg bg-white/5 border border-white/10 backdrop-blur-sm flex items-center justify-center relative group"> | |
| <div class="w-8 h-8 rounded-lg bg-white/10 border border-white/20 flex items-center justify-center"> | |
| <span class="text-xs text-gray-400">?</span> | |
| </div> | |
| </div> | |
| <div class="badge-slot w-12 h-12 rounded-lg bg-white/5 border border-white/10 backdrop-blur-sm flex items-center justify-center relative group"> | |
| <div class="w-8 h-8 rounded-lg bg-white/10 border border-white/20 flex items-center justify-center"> | |
| <span class="text-xs text-gray-400">?</span> | |
| </div> | |
| </div> | |
| <div class="badge-slot w-12 h-12 rounded-lg bg-white/5 border border-white/10 backdrop-blur-sm flex items-center justify-center relative group"> | |
| <div class="w-8 h-8 rounded-lg bg-white/10 border border-white/20 flex items-center justify-center"> | |
| <span class="text-xs text-gray-400">?</span> | |
| </div> | |
| </div> | |
| <div class="badge-slot w-12 h-12 rounded-lg bg-white/5 border border-white/10 backdrop-blur-sm flex items-center justify-center relative group"> | |
| <div class="w-8 h-8 rounded-lg bg-white/10 border border-white/20 flex items-center justify-center"> | |
| <span class="text-xs text-gray-400">?</span> | |
| </div> | |
| </div> | |
| <div class="badge-slot w-12 h-12 rounded-lg bg-white/5 border border-white/10 backdrop-blur-sm flex items-center justify-center relative group"> | |
| <div class="w-8 h-8 rounded-lg bg-white/10 border border-white/20 flex items-center justify-center"> | |
| <span class="text-xs text-gray-400">?</span> | |
| </div> | |
| </div> | |
| <div class="badge-slot w-12 h-12 rounded-lg bg-white/5 border border-white/10 backdrop-blur-sm flex items-center justify-center relative group"> | |
| <div class="w-8 h-8 rounded-lg bg-white/10 border border-white/20 flex items-center justify-center"> | |
| <span class="text-xs text-gray-400">?</span> | |
| </div> | |
| </div> | |
| <div class="badge-slot w-12 h-12 rounded-lg bg-white/5 border border-white/10 backdrop-blur-sm flex items-center justify-center relative group"> | |
| <div class="w-8 h-8 rounded-lg bg-white/10 border border-white/20 flex items-center justify-center"> | |
| <span class="text-xs text-gray-400">?</span> | |
| </div> | |
| </div> | |
| <div class="badge-slot w-12 h-12 rounded-lg bg-white/5 border border-white/10 backdrop-blur-sm flex items-center justify-center relative group"> | |
| <div class="w-8 h-8 rounded-lg bg-white/10 border border-white/20 flex items-center justify-center"> | |
| <span class="text-xs text-gray-400">?</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Monument Spotlight Box --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-purple-900/20 to-pink-900/20 p-4 relative" id="questMonumentSpotlightBox"> | |
| <!-- Tag in top left corner --> | |
| <div class="absolute top-4 left-4 z-10"> | |
| <div id="questMonumentSpotlightTag" class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium"> | |
| StreetBlox Nearby | |
| </div> | |
| </div> | |
| <!-- Content with single nearby monument --> | |
| <div class="h-full flex flex-col justify-start items-center pt-16 space-y-4"> | |
| <!-- Single Nearby Monument Card --> | |
| <div class="w-full"> | |
| <div class="card-glass rounded-lg p-3 bg-white/5 border border-white/10 backdrop-blur-sm"> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="w-8 h-8 rounded-full bg-white/10 backdrop-blur-sm border border-white/20 flex items-center justify-center"> | |
| <img src="Streetblox_NEW.png" alt="StreetBlox" class="w-5 h-5 object-contain"> | |
| </div> | |
| <div> | |
| <div class="text-sm font-semibold">Praça do Comércio</div> | |
| <div class="text-xs text-gray-400">420 GBLX • Nearby</div> | |
| </div> | |
| </div> | |
| <button class="text-xs text-accent hover:text-accent-2 transition-colors px-2 py-1 rounded bg-white/5"> | |
| Conquer | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- View All Button --> | |
| <button class="inline-flex items-center text-accent font-medium text-xs mt-2" onclick="showTab('monuments')"> | |
| View All StreetBlox | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Level Progress Box --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-emerald-900/20 to-teal-900/20 p-4 relative"> | |
| <!-- Tag in top left corner --> | |
| <div class="absolute top-2 left-2 z-10"> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium text-emerald-400"> | |
| Level Progress | |
| </div> | |
| </div> | |
| <!-- Content centered --> | |
| <div class="h-full flex flex-col justify-end pt-8"> | |
| <h3 class="text-lg font-bold mt-1" id="passportLevelNumber">Level 1</h3> | |
| <div class="mt-2 bg-muted rounded-full h-2 overflow-hidden"> | |
| <div id="passportLevelProgressFill" class="bg-gradient-to-r from-accent to-secondary h-2 rounded-full transition-all duration-300 ease-out" style="width: 0%"></div> | |
| </div> | |
| <p class="text-gray-400 text-xs mt-2" id="passportLevelNextText">Need 100 Aura to reach Level 2</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs" onclick="forceLevelUpdate()"> | |
| Force Update | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Featured Quests Section --> | |
| <section class="py-12 px-4 sm:px-6 lg:px-8"> | |
| <div class="max-w-7xl mx-auto"> | |
| <div class="flex items-center justify-between mb-8"> | |
| <h2 class="text-2xl font-bold">Featured Quests</h2> | |
| <a href="#" class="flex items-center text-accent hover:text-accent-2 transition-colors"> | |
| View All | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </a> | |
| </div> | |
| <div id="passportFeaturedGrid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> | |
| <!-- Connect TON Wallet Quest (Main Featured) --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-pink-600/30 to-purple-600/30 h-48 flex items-center justify-center"> | |
| <img src="ProBlox_NEW.png" alt="ProBlox" class="w-20 h-20 object-contain opacity-90"> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Connect TON Wallet</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Featured | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Connect and earn 150 GBLX + 3× multiplier while connected. | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]" onclick="startTonWalletQuest()"> | |
| Start Quest | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Add Friend Quest --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-emerald-600/30 to-teal-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/> | |
| <circle cx="9" cy="7" r="4"/> | |
| <path d="M19 8l2 2-2 2"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Add Friend Quest</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Active | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Add a friend by their Telegram ID and earn 25 GBLX! | |
| </p> | |
| <button onclick="startAddFriendQuest()" class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-accent hover:text-white"> | |
| Start Quest (25 GBLX) | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Quest Card 1 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-purple-600/30 to-cyan-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">NFT Collector Quest</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Active | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Collect 10 unique NFTs to unlock exclusive rewards | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Start Quest | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Friend Referral Test Quest --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-green-600/30 to-blue-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/> | |
| <circle cx="9" cy="7" r="4"/> | |
| <path d="m19 8 2 2-2 2"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Friend Referral Test</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Active | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Share this app with 1 friend and get 50 GBLX instantly! | |
| </p> | |
| <button onclick="startQuest(1, 'Friend Referral Test')" class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-accent hover:text-white"> | |
| Start Quest (50 GBLX) | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> <!-- Quest Card 2 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-indigo-600/30 to-emerald-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">DeFi Master Quest</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Active | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Complete 5 DeFi protocols to earn master badge | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Start Quest | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Quest Card 3 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-violet-600/30 to-blue-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">DAO Explorer Quest</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Active | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Participate in 3 DAO governance decisions | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Start Quest | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Quest Card 4 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-amber-600/30 to-yellow-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Metaverse Pioneer</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Active | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Explore 5 different metaverse worlds | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Start Quest | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| <!-- QUESTS Tab --> | |
| <div id="questsTab" class="hidden space-y-6"> | |
| <!-- Quest Hero Section --> | |
| <section class="pt-2 pb-4 px-4 sm:px-6 lg:px-8"> | |
| <div class="max-w-7xl mx-auto"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-4"> | |
| <!-- Main Quest Hero Box --> | |
| <div class="card-glass rounded-2xl overflow-hidden p-0 lg:col-span-2 relative hover:border-accent/50 transition-colors" style="min-height: 400px; background: linear-gradient(135deg, rgba(124, 76, 228, 0.15) 0%, rgba(0, 255, 255, 0.15) 100%);"> | |
| <!-- Mini Bento Box with QuestBlox Logo --> | |
| <div class="absolute top-4 left-4 w-24 h-24 lg:w-32 lg:h-32 z-10"> | |
| <div class="card-glass rounded-xl overflow-hidden h-full bg-white/10 border border-white/20 backdrop-blur-sm relative"> | |
| <div class="h-full flex items-center justify-center p-2"> | |
| <img src="QuestBlox_NEW.png" alt="QuestBlox" class="w-full h-full object-contain"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Quest Hero Content --> | |
| <div class="absolute bottom-0 left-0 w-full h-full flex flex-col justify-end items-start p-4 md:p-8 text-left pointer-events-none"> | |
| <div> | |
| <h2 class="text-xl md:text-3xl font-bold mb-2">Discover <span class="bg-gradient-to-r from-accent2 to-accent bg-clip-text text-transparent">Amazing Quests</span></h2> | |
| <p class="text-xs md:text-sm text-gray-300 mb-4 max-w-md"> | |
| Complete challenges, earn rewards, and unlock exclusive experiences in the Web3 ecosystem. | |
| </p> | |
| <div class="flex flex-col sm:flex-row gap-2 pointer-events-auto"> | |
| <button class="px-3 py-1.5 bg-accent rounded-full font-medium hover:bg-[#7c4ce4] transition-colors hover-gradient flex items-center justify-center text-xs"> | |
| Start Questing | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <polyline points="12 8 8 12 12 16"></polyline> | |
| <line x1="16" y1="12" x2="8" y2="12"></line> | |
| </svg> | |
| </button> | |
| <button class="px-3 py-1.5 bg-white/10 backdrop-blur-sm rounded-full font-medium hover:bg-white/20 transition-colors flex items-center justify-center text-xs border border-white/10"> | |
| View Leaderboard | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="11" cy="11" r="8"></circle> | |
| <line x1="21" y1="21" x2="16.65" y2="16.65"></line> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Quest Highlight Box 1 --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-purple-900/20 to-cyan-900/20 p-4 flex flex-col justify-end"> | |
| <div> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium"> | |
| Trending | |
| </div> | |
| <h3 class="text-lg font-bold mt-1">NFT Collection Quest</h3> | |
| <p class="text-gray-400 text-xs mt-1">Collect rare digital artifacts</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs"> | |
| Join Quest | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Additional Quest Highlight Boxes --> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mt-4"> | |
| <!-- Quest Highlight Box 2 --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-violet-900/20 to-blue-900/20 p-4 flex flex-col justify-end"> | |
| <div> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium text-accent2"> | |
| New | |
| </div> | |
| <h3 class="text-lg font-bold mt-1">DeFi Yield Quest</h3> | |
| <p class="text-gray-400 text-xs mt-1">Earn rewards through farming</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs"> | |
| Start Farming | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Quest Highlight Box 3 --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-amber-900/20 to-yellow-900/20 p-4 flex flex-col justify-end"> | |
| <div> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium text-amber-400"> | |
| Hot | |
| </div> | |
| <h3 class="text-lg font-bold mt-1">DAO Governance Quest</h3> | |
| <p class="text-gray-400 text-xs mt-1">Participate in decisions</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs"> | |
| Vote Now | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Quest Highlight Box 4 --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-emerald-900/20 to-teal-900/20 p-4 flex flex-col justify-end"> | |
| <div> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium text-emerald-400"> | |
| Popular | |
| </div> | |
| <h3 class="text-lg font-bold mt-1">Metaverse Quest</h3> | |
| <p class="text-gray-400 text-xs mt-1">Explore virtual worlds</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs"> | |
| Explore | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Featured Quests Section --> | |
| <section class="py-12 px-4 sm:px-6 lg:px-8"> | |
| <div class="max-w-7xl mx-auto"> | |
| <div class="flex items-center justify-between mb-8"> | |
| <h2 class="text-2xl font-bold">Featured Quests</h2> | |
| <a href="#" class="flex items-center text-accent hover:text-accent-2 transition-colors"> | |
| View All | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </a> | |
| </div> | |
| <div id="questsFeaturedGrid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> | |
| <!-- Connect TON Wallet Quest (QuestBlox Featured) --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-pink-600/30 to-purple-600/30 h-48 flex items-center justify-center"> | |
| <img src="ProBlox_NEW.png" alt="ProBlox" class="w-20 h-20 object-contain opacity-90"> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Connect TON Wallet</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Featured | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Connect and earn 150 GBLX + 3× multiplier while connected. | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]" onclick="startTonWalletQuest()"> | |
| Start Quest | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Quest Card 1 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-purple-600/30 to-cyan-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">NFT Collector Quest</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Active | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Collect 10 unique NFTs to unlock exclusive rewards | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Start Quest | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Quest Card 2 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-indigo-600/30 to-emerald-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">DeFi Master Quest</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Active | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Complete 5 DeFi protocols to earn master badge | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Start Quest | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Quest Card 3 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-violet-600/30 to-blue-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">DAO Explorer Quest</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Active | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Participate in 3 DAO governance decisions | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Start Quest | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Quest Card 4 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-amber-600/30 to-yellow-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Metaverse Pioneer</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Active | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Explore 5 different metaverse worlds | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Start Quest | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| <!-- BUDDYBLOX Tab --> | |
| <div id="friendsTab" class="hidden space-y-6"> | |
| <!-- BuddyBlox Hero Section --> | |
| <section class="pt-2 pb-4 px-4 sm:px-6 lg:px-8"> | |
| <div class="max-w-7xl mx-auto"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-4"> | |
| <!-- Main BuddyBlox Hero Box --> | |
| <div class="card-glass rounded-2xl overflow-hidden p-0 lg:col-span-2 relative hover:border-accent/50 transition-colors" style="min-height: 400px; background: linear-gradient(135deg, rgba(124, 76, 228, 0.15) 0%, rgba(0, 255, 255, 0.15) 100%);"> | |
| <!-- Mini Bento Box with BuddyBlox Logo --> | |
| <div class="absolute top-4 left-4 w-24 h-24 lg:w-32 lg:h-32 z-10"> | |
| <div class="card-glass rounded-xl overflow-hidden h-full bg-white/10 border border-white/20 backdrop-blur-sm relative"> | |
| <div class="h-full flex items-center justify-center p-2"> | |
| <img src="Buddyblox_NEW.png" alt="BuddyBlox" class="w-full h-full object-contain"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- BuddyBlox Hero Content --> | |
| <div class="absolute bottom-0 left-0 w-full h-full flex flex-col justify-end items-start p-4 md:p-8 text-left pointer-events-none"> | |
| <div> | |
| <h2 class="text-xl md:text-3xl font-bold mb-2">Connect with <span class="bg-gradient-to-r from-accent2 to-accent bg-clip-text text-transparent">Friends & Buddies</span></h2> | |
| <p class="text-xs md:text-sm text-gray-300 mb-4 max-w-md"> | |
| Invite friends, earn rewards together, and build your network in the Web3 community. | |
| </p> | |
| <div class="flex flex-col sm:flex-row gap-2 pointer-events-auto"> | |
| <button class="px-3 py-1.5 bg-accent rounded-full font-medium hover:bg-[#7c4ce4] transition-colors hover-gradient flex items-center justify-center text-xs"> | |
| Invite Friends | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path> | |
| <circle cx="9" cy="7" r="4"></circle> | |
| <path d="m22 21-2-2"></path> | |
| <path d="M16 16h6"></path> | |
| </svg> | |
| </button> | |
| <button class="px-3 py-1.5 bg-white/10 backdrop-blur-sm rounded-full font-medium hover:bg-white/20 transition-colors flex items-center justify-center text-xs border border-white/10"> | |
| View Network | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path> | |
| <circle cx="9" cy="7" r="4"></circle> | |
| <path d="m23 21-2-2"></path> | |
| <path d="M17 16h6"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- BuddyBlox Inline Controls (pointer events enabled) --> | |
| <div class="absolute top-4 left-4 z-20 pointer-events-auto hidden md:block"> | |
| <div class="mt-28 card-glass rounded-xl p-3 bg-white/10 border border-white/20"> | |
| <div class="text-xs mb-2">Add friend by Telegram ID</div> | |
| <div class="flex items-center gap-2"> | |
| <input id="buddyTidInput" placeholder="123456789" class="px-2 py-1 rounded bg-white/10 border border-white/20 text-xs" /> | |
| <button onclick="(function(){ const v=document.getElementById('buddyTidInput').value.trim(); if(v) addBuddy(v); })()" class="px-3 py-1 text-xs rounded bg-accent/60">Add</button> | |
| </div> | |
| <div class="mt-3 text-xs opacity-80">Your buddies</div> | |
| <div id="buddyList" class="mt-1 max-h-40 overflow-auto"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- BuddyBlox Highlight Box 1 --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-green-900/20 to-emerald-900/20 p-4 flex flex-col justify-end"> | |
| <div> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium"> | |
| Active | |
| </div> | |
| <h3 class="text-lg font-bold mt-1">Alice</h3> | |
| <p class="text-gray-400 text-xs mt-1">Level 3 • +50 coins today</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs"> | |
| View Profile | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Additional BuddyBlox Highlight Boxes --> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mt-4"> | |
| <!-- BuddyBlox Highlight Box 2 --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-blue-900/20 to-cyan-900/20 p-4 flex flex-col justify-end"> | |
| <div> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium text-blue-400"> | |
| Online | |
| </div> | |
| <h3 class="text-lg font-bold mt-1">Bob</h3> | |
| <p class="text-gray-400 text-xs mt-1">Level 2 • +30 coins yesterday</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs"> | |
| Send Message | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- BuddyBlox Highlight Box 3 --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-purple-900/20 to-pink-900/20 p-4 flex flex-col justify-end"> | |
| <div> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium text-purple-400"> | |
| New | |
| </div> | |
| <h3 class="text-lg font-bold mt-1">Charlie</h3> | |
| <p class="text-gray-400 text-xs mt-1">Level 1 • Just joined</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs"> | |
| Welcome | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- BuddyBlox Highlight Box 4 --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-orange-900/20 to-red-900/20 p-4 flex flex-col justify-end"> | |
| <div> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium text-orange-400"> | |
| Veteran | |
| </div> | |
| <h3 class="text-lg font-bold mt-1">Diana</h3> | |
| <p class="text-gray-400 text-xs mt-1">Level 5 • +100 coins today</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs"> | |
| Challenge | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Featured Buddies Section --> | |
| <section class="py-12 px-4 sm:px-6 lg:px-8"> | |
| <div class="max-w-7xl mx-auto"> | |
| <div class="flex items-center justify-between mb-8"> | |
| <h2 class="text-2xl font-bold">Featured Buddies</h2> | |
| <a href="#" class="flex items-center text-accent hover:text-accent-2 transition-colors"> | |
| View All | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </a> | |
| </div> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> | |
| <!-- Buddy Card 1 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-green-600/30 to-emerald-600/30 h-48 flex items-center justify-center"> | |
| <div class="w-16 h-16 rounded-full bg-gradient-to-r from-green-500 to-emerald-500 flex items-center justify-center text-white font-bold text-xl"> | |
| A | |
| </div> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Alice</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-green-400 mr-1"></div> | |
| Active | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Level 3 • +50 coins today | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Connect | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Buddy Card 2 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-blue-600/30 to-cyan-600/30 h-48 flex items-center justify-center"> | |
| <div class="w-16 h-16 rounded-full bg-gradient-to-r from-blue-500 to-cyan-500 flex items-center justify-center text-white font-bold text-xl"> | |
| B | |
| </div> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Bob</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-blue-400 mr-1"></div> | |
| Online | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Level 2 • +30 coins yesterday | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Message | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Buddy Card 3 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-purple-600/30 to-pink-600/30 h-48 flex items-center justify-center"> | |
| <div class="w-16 h-16 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center text-white font-bold text-xl"> | |
| C | |
| </div> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Charlie</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-purple-400 mr-1"></div> | |
| New | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Level 1 • Just joined | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Welcome | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Buddy Card 4 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-orange-600/30 to-red-600/30 h-48 flex items-center justify-center"> | |
| <div class="w-16 h-16 rounded-full bg-gradient-to-r from-orange-500 to-red-500 flex items-center justify-center text-white font-bold text-xl"> | |
| D | |
| </div> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Diana</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-orange-400 mr-1"></div> | |
| Veteran | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Level 5 • +100 coins today | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Challenge | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| <!-- STREETBLOX Tab --> | |
| <div id="monumentsTab" class="hidden space-y-6"> | |
| <!-- StreetBlox Hero Section --> | |
| <section class="pt-2 pb-4 px-4 sm:px-6 lg:px-8"> | |
| <div class="max-w-7xl mx-auto"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-4"> | |
| <!-- Main StreetBlox Hero Box --> | |
| <div class="card-glass rounded-2xl overflow-hidden p-0 lg:col-span-2 relative hover:border-accent/50 transition-colors" style="min-height: 400px; background: linear-gradient(135deg, rgba(124, 76, 228, 0.15) 0%, rgba(0, 255, 255, 0.15) 100%);"> | |
| <!-- Mini Bento Box with StreetBlox Logo --> | |
| <div class="absolute top-4 left-4 w-24 h-24 lg:w-32 lg:h-32 z-10"> | |
| <div class="card-glass rounded-xl overflow-hidden h-full bg-white/10 border border-white/20 backdrop-blur-sm relative"> | |
| <div class="h-full flex items-center justify-center p-2"> | |
| <img src="Streetblox_NEW.png" alt="StreetBlox" class="w-full h-full object-contain"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- StreetBlox Hero Content --> | |
| <div class="absolute bottom-0 left-0 w-full h-full flex flex-col justify-end items-start p-4 md:p-8 text-left pointer-events-none"> | |
| <div> | |
| <h2 class="text-xl md:text-3xl font-bold mb-2">Discover <span class="bg-gradient-to-r from-accent2 to-accent bg-clip-text text-transparent">Real-World Locations</span></h2> | |
| <p class="text-xs md:text-sm text-gray-300 mb-4 max-w-md"> | |
| Claim, conquer and defend monuments in your city. Build your empire one location at a time. | |
| </p> | |
| <div class="flex flex-col sm:flex-row gap-2 pointer-events-auto"> | |
| <button class="px-3 py-1.5 bg-accent rounded-full font-medium hover:bg-[#7c4ce4] transition-colors hover-gradient flex items-center justify-center text-xs"> | |
| Explore Monuments | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <polyline points="12 8 8 12 12 16"></polyline> | |
| <line x1="16" y1="12" x2="8" y2="12"></line> | |
| </svg> | |
| </button> | |
| <button class="px-3 py-1.5 bg-white/10 backdrop-blur-sm rounded-full font-medium hover:bg-white/20 transition-colors flex items-center justify-center text-xs border border-white/10"> | |
| View Map | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="11" cy="11" r="8"></circle> | |
| <line x1="21" y1="21" x2="16.65" y2="16.65"></line> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Monument Highlight Box 1 --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-purple-900/20 to-pink-900/20 p-4 flex flex-col justify-end"> | |
| <div> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium"> | |
| Nearby | |
| </div> | |
| <h3 class="text-lg font-bold mt-1">Praça do Comércio</h3> | |
| <p class="text-gray-400 text-xs mt-1">Claim cost: 420 GBLX</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs"> | |
| Conquer Monument | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Monument Highlight Box 2 --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-indigo-900/20 to-blue-900/20 p-4 flex flex-col justify-end"> | |
| <div> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium"> | |
| Nearby | |
| </div> | |
| <h3 class="text-lg font-bold mt-1">Torre de Belém</h3> | |
| <p class="text-gray-400 text-xs mt-1">Claim cost: 650 GBLX</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs"> | |
| Conquer Monument | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Monument Highlight Box 3 --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-violet-900/20 to-purple-900/20 p-4 flex flex-col justify-end"> | |
| <div> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium"> | |
| Nearby | |
| </div> | |
| <h3 class="text-lg font-bold mt-1">Mosteiro dos Jerónimos</h3> | |
| <p class="text-gray-400 text-xs mt-1">Claim cost: 850 GBLX</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs"> | |
| Conquer Monument | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Monument Highlight Box 4 --> | |
| <div class="card-glass rounded-2xl overflow-hidden relative hover:border-accent/50 transition-colors"> | |
| <div class="hero-slide relative h-full"> | |
| <div class="h-full bg-gradient-to-br from-amber-900/20 to-yellow-900/20 p-4 flex flex-col justify-end"> | |
| <div> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium"> | |
| Nearby | |
| </div> | |
| <h3 class="text-lg font-bold mt-1">Castelo de São Jorge</h3> | |
| <p class="text-gray-400 text-xs mt-1">Claim cost: 1200 GBLX</p> | |
| <button class="mt-2 inline-flex items-center text-accent font-medium text-xs"> | |
| Conquer Monument | |
| <svg class="ml-1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Featured Monuments Section --> | |
| <section class="py-12 px-4 sm:px-6 lg:px-8"> | |
| <div class="max-w-7xl mx-auto"> | |
| <div class="flex items-center justify-between mb-8"> | |
| <h2 class="text-2xl font-bold">Featured StreetBlox</h2> | |
| <a href="#" class="flex items-center text-accent hover:text-accent-2 transition-colors"> | |
| View All | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </a> | |
| </div> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> | |
| <!-- Monument Card 1 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-purple-600/30 to-cyan-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Praça do Comércio</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Available | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Historic square in Lisbon, Portugal | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Claim for 420 GBLX | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Monument Card 2 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-indigo-600/30 to-emerald-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Torre de Belém</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Available | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Iconic tower in Lisbon, Portugal | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Claim for 650 GBLX | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Monument Card 3 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-violet-600/30 to-blue-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Mosteiro dos Jerónimos</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Available | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Historic monastery in Lisbon | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Claim for 850 GBLX | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Monument Card 4 --> | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="bg-gradient-to-br from-amber-600/30 to-yellow-600/30 h-48 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="opacity-50"> | |
| <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/> | |
| </svg> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">Castelo de São Jorge</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Available | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| Medieval castle in Lisbon | |
| </p> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]"> | |
| Claim for 1200 GBLX | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Bottom Navigation --> | |
| <nav class="sticky bottom-0 z-30 py-2 px-4 sm:px-6 lg:px-8"> | |
| <div class="max-w-7xl mx-auto"> | |
| <div class="grid grid-cols-4 gap-3"> | |
| <!-- BloxID Logo Button --> | |
| <button class="nav-btn active py-3 px-2 rounded-xl font-medium text-sm transition-all flex flex-col items-center justify-center" onclick="showTab('passport')"> | |
| <img src="bloxid_cube_NEW_PNG.png" alt="BloxID" class="w-14 h-14 object-contain mb-1"> | |
| <span class="text-xs font-medium">BloxID</span> | |
| </button> | |
| <!-- QuestBlox Logo Button --> | |
| <button class="nav-btn py-3 px-2 rounded-xl font-medium text-sm transition-all flex flex-col items-center justify-center" onclick="showTab('quests')"> | |
| <img src="QuestBlox_NEW.png" alt="QuestBlox" class="w-14 h-14 object-contain mb-1"> | |
| <span class="text-xs font-medium">QuestBlox</span> | |
| </button> | |
| <!-- StreetBlox Logo Button --> | |
| <button id="streetbloxButton" class="nav-btn py-3 px-2 rounded-xl font-medium text-sm transition-all flex flex-col items-center justify-center" onclick="showTab('monuments')"> | |
| <img src="Streetblox_NEW.png" alt="StreetBlox" class="w-14 h-14 object-contain mb-1"> | |
| <span class="text-xs font-medium">StreetBlox</span> | |
| </button> | |
| <!-- BuddyBlox Logo Button --> | |
| <button class="nav-btn py-3 px-2 rounded-xl font-medium text-sm transition-all flex flex-col items-center justify-center" onclick="showTab('friends')"> | |
| <img src="Buddyblox_NEW.png" alt="BuddyBlox" class="w-14 h-14 object-contain mb-1"> | |
| <span class="text-xs font-medium">BuddyBlox</span> | |
| </button> | |
| </div> | |
| </div> | |
| </nav> | |
| </div> | |
| <!-- Notification --> | |
| <div class="notification fixed top-4 left-1/2 transform -translate-x-1/2 z-50 px-6 py-3 rounded-full text-sm font-medium opacity-0 transition-opacity" id="notification"></div> | |
| <script> | |
| // Telegram WebApp Integration | |
| // Get start parameter from URL | |
| const urlParams = new URLSearchParams(window.location.search); | |
| const startParam = urlParams.get('start'); | |
| // Telegram WebApp Shim for Browser Mode | |
| if (typeof Telegram === 'undefined' || !Telegram.WebApp) { | |
| window.Telegram = { | |
| WebApp: { | |
| initData: '', | |
| initDataUnsafe: { | |
| user: { | |
| id: startParam ? parseInt(startParam) : 123456789, | |
| first_name: 'Test', | |
| username: 'testuser' | |
| } | |
| }, | |
| expand: function() {}, | |
| setHeaderColor: function() {}, | |
| setBackgroundColor: function() {}, | |
| ready: function() {}, | |
| close: function() {}, | |
| showAlert: function() {}, | |
| showConfirm: function() {}, | |
| showPopup: function() {}, | |
| showScanQrPopup: function() {}, | |
| showMainButton: function() {}, | |
| hideMainButton: function() {}, | |
| showBackButton: function() {}, | |
| hideBackButton: function() {}, | |
| enableClosingConfirmation: function() {}, | |
| disableClosingConfirmation: function() {}, | |
| isClosingConfirmationEnabled: function() { return false; }, | |
| isExpanded: function() { return true; }, | |
| viewportHeight: window.innerHeight, | |
| viewportStableHeight: window.innerHeight, | |
| headerColor: '#2A2D36', | |
| backgroundColor: '#202229', | |
| themeParams: { | |
| bg_color: '#202229', | |
| text_color: '#ffffff', | |
| hint_color: '#999999', | |
| link_color: '#2481cc', | |
| button_color: '#2481cc', | |
| button_text_color: '#ffffff' | |
| } | |
| } | |
| }; | |
| } else { | |
| // If we have a start parameter and no user data, try to set it | |
| if (startParam && (!Telegram.WebApp.initDataUnsafe || !Telegram.WebApp.initDataUnsafe.user)) { | |
| console.log('Setting user data from start parameter:', startParam); | |
| if (!Telegram.WebApp.initDataUnsafe) { | |
| Telegram.WebApp.initDataUnsafe = {}; | |
| } | |
| Telegram.WebApp.initDataUnsafe.user = { | |
| id: parseInt(startParam), | |
| first_name: 'User', | |
| username: 'user' | |
| }; | |
| } | |
| } | |
| // Initialize Telegram WebApp | |
| try { | |
| Telegram.WebApp.expand(); | |
| Telegram.WebApp.setHeaderColor('#2A2D36'); | |
| Telegram.WebApp.setBackgroundColor('#202229'); | |
| Telegram.WebApp.ready(); | |
| // Log Telegram WebApp info | |
| console.log('Telegram WebApp initialized'); | |
| console.log('User:', Telegram.WebApp.initDataUnsafe?.user); | |
| console.log('Init Data:', Telegram.WebApp.initData); | |
| console.log('Start Param:', startParam); | |
| } catch (e) { | |
| console.log('Telegram WebApp not available, using browser mode'); | |
| } | |
| // Game state - Simple localStorage (no platform separation for now) | |
| let currentTab = 'passport'; | |
| let score = parseInt(localStorage.getItem('bloxid_unified_score')) || 0; | |
| let balance = parseInt(localStorage.getItem('bloxid_unified_balance')) || 1000; | |
| let multitap = 3; | |
| // Log loaded values for debugging | |
| console.log('🔄 Initial load - Score:', score, 'Balance:', balance); | |
| // Force save current values to ensure they persist | |
| localStorage.setItem('bloxid_unified_score', score); | |
| localStorage.setItem('bloxid_unified_balance', balance); | |
| // Global user variables for Telegram integration | |
| let currentUser = null; | |
| let userBadges = ['blueblox']; // Default BlueBlox badge | |
| let isLoggedIn = false; | |
| // Robust session restore (cookie or localStorage), plus badge persistence | |
| (function hardRestoreTelegramSession(){ | |
| try { | |
| // 1) Cookie from auth_redirect.php | |
| const cookieMatch = document.cookie.match(/(?:^|; )tg_user=([^;]+)/); | |
| if (!currentUser && cookieMatch) { | |
| try { | |
| const decoded = decodeURIComponent(cookieMatch[1]); | |
| const tg = JSON.parse(atob(decoded)); | |
| if (tg && tg.id) { | |
| currentUser = tg; | |
| localStorage.setItem('tg_user', JSON.stringify(tg)); | |
| localStorage.setItem('bloxid_current_user', JSON.stringify(tg)); // Also save for fallback | |
| // Clear one-time cookie | |
| document.cookie = 'tg_user=; Max-Age=0; path=/'; | |
| } | |
| } catch(_) {} | |
| } | |
| // 2) LocalStorage fallback | |
| if (!currentUser) { | |
| const ls = localStorage.getItem('tg_user'); | |
| if (ls) { | |
| try { | |
| const tg = JSON.parse(ls); | |
| if (tg && tg.id) { | |
| currentUser = tg; | |
| localStorage.setItem('bloxid_current_user', JSON.stringify(tg)); // Sync both keys | |
| } | |
| } catch(_) {} | |
| } | |
| } | |
| // 3) Apply to UI and badges | |
| if (currentUser) { | |
| isLoggedIn = true; | |
| // Don't call updateUserProfile if it might overwrite username | |
| const elU = document.getElementById('usernameDesktop'); | |
| if (elU) { | |
| const displayName = currentUser.username ? ('@' + currentUser.username) : | |
| (currentUser.first_name ? currentUser.first_name : ('ID ' + currentUser.id)); | |
| elU.textContent = displayName; | |
| } | |
| try { | |
| const verified = localStorage.getItem('telegram_wallet_verified') === 'true'; | |
| if (verified && Array.isArray(userBadges) && !userBadges.includes('problox')) { | |
| userBadges.push('problox'); | |
| updateBadgeDisplay(); | |
| } | |
| // Reflect wallet connected button | |
| const isConnected = localStorage.getItem('telegram_wallet_connected') === 'true'; | |
| const connectBtnInit = document.getElementById('walletConnectBtn'); | |
| const buttonTextInit = document.getElementById('walletBtnText'); | |
| if (isConnected && connectBtnInit) { | |
| connectBtnInit.disabled = false; | |
| connectBtnInit.className = 'card-glass rounded-xl px-4 py-2 w-full border text-white font-semibold text-xs flex items-center justify-center space-x-2 bg-green-600 hover:bg-green-700 border-green-400/50'; | |
| if (buttonTextInit) buttonTextInit.textContent = 'Connected!'; | |
| } | |
| } catch(_) {} | |
| // Refresh buddies on restore | |
| try { loadBuddies(); } catch(_) {} | |
| } | |
| // Cross-tab updates | |
| window.addEventListener('storage', (e) => { | |
| if (e.key === 'tg_user' && e.newValue) { | |
| try { const tg = JSON.parse(e.newValue); if (tg && tg.id) currentUser = tg; } catch(_) {} | |
| } | |
| }); | |
| console.log('<!-- session-restore-active -->'); | |
| } catch(_) {} | |
| })(); | |
| // Badge multiplier system | |
| const badgeMultipliers = { | |
| blueblox: { base: 1.5, premium: 2.0 }, // x1.5 base, x2.0 if Premium | |
| problox: { multiplier: 2.5 }, // x2.5 GBLX multiplier | |
| mayorblox: { multiplier: 1.8 } // x1.8 location multiplier | |
| }; | |
| // Get current user's total multiplier | |
| function getTotalMultiplier() { | |
| let multiplier = 1.0; | |
| if (userBadges.includes('blueblox')) { | |
| // Check if user has Telegram Premium | |
| const isPremium = currentUser?.is_premium || false; | |
| multiplier *= isPremium ? badgeMultipliers.blueblox.premium : badgeMultipliers.blueblox.base; | |
| } | |
| // Apply ProBlox multiplier only if badge present AND wallet is verified | |
| try { | |
| const wasVerified = localStorage.getItem('telegram_wallet_verified') === 'true'; | |
| if (wasVerified && userBadges.includes('problox')) { | |
| multiplier *= badgeMultipliers.problox.multiplier; | |
| } | |
| } catch (e) {} | |
| if (userBadges.includes('mayorblox')) { | |
| multiplier *= badgeMultipliers.mayorblox.multiplier; | |
| } | |
| return multiplier; | |
| } | |
| // Monument UI state (preview only) | |
| let monumentReady = false; | |
| let monumentUnderAttack = false; | |
| let nearestMonument = { name: 'Praça do Comércio', cost: 420 }; | |
| function tap() { | |
| console.log('🎯 TAP FUNCTION CALLED!'); | |
| // Haptic feedback for Telegram WebApp | |
| if (window.Telegram && window.Telegram.WebApp) { | |
| window.Telegram.WebApp.HapticFeedback.impactOccurred('light'); | |
| } | |
| // Enhanced tap logic - convert 1 GBLX to 1 Aura with badge multipliers | |
| if (balance > 0) { | |
| balance -= 1; | |
| // Apply badge multipliers to Aura earned | |
| const baseAura = 1; | |
| const multiplier = getTotalMultiplier(); | |
| const auraEarned = Math.floor(baseAura * multiplier); | |
| score += auraEarned; | |
| // Save to localStorage immediately | |
| localStorage.setItem('bloxid_unified_score', score); | |
| localStorage.setItem('bloxid_unified_balance', balance); | |
| console.log('💾 Saved - Score:', score, 'Balance:', balance); | |
| console.log('🏆 Badge multiplier:', multiplier.toFixed(1) + 'x', 'Aura earned:', auraEarned); | |
| console.log('✅ Tap successful - Balance:', balance, 'Score:', score); | |
| // Simple localStorage save (no server complexity) | |
| localStorage.setItem('bloxid_last_save', Date.now()); | |
| console.log('💾 Data saved to localStorage only'); | |
| // Update all displays | |
| const displays = [ | |
| 'questScoreDisplay', 'questBalanceDisplay', | |
| 'passportScoreDisplay', 'passportBalanceDisplay' | |
| ]; | |
| displays.forEach(id => { | |
| const element = document.getElementById(id); | |
| if (element) { | |
| if (id.includes('Score')) { | |
| element.textContent = score.toLocaleString(); | |
| } else { | |
| element.textContent = balance.toLocaleString(); | |
| } | |
| console.log('✅ Updated display:', id); | |
| } | |
| }); | |
| // Update level progress | |
| updateLevelProgress(); | |
| updateQuestLevelProgress(); | |
| // Show tap effect | |
| showTapEffect(`+${auraEarned || 1} Aura`); | |
| // Show notification | |
| showNotification('Converted 1 GB → 1 Aura', 'success'); | |
| } else { | |
| console.log('❌ No GBLX to convert!'); | |
| showNotification('No GBLX to convert!', 'warning'); | |
| } | |
| } | |
| function questTap() { | |
| // Same functionality as tap() but for QUESTS tab | |
| if (balance > 0) { | |
| balance -= 1; | |
| score += 1; | |
| // Update displays (only the elements that exist) | |
| document.getElementById('questScoreDisplay').textContent = score.toLocaleString(); | |
| document.getElementById('questBalanceDisplay').textContent = balance.toLocaleString(); | |
| document.getElementById('passportScoreDisplay').textContent = score.toLocaleString(); | |
| document.getElementById('passportBalanceDisplay').textContent = balance.toLocaleString(); | |
| updateLevelProgress(); | |
| updateQuestLevelProgress(); | |
| // Show enhanced tap effect | |
| showTapEffect(`+${auraEarned || 1} Aura`); | |
| // Show notification | |
| showNotification('Converted 1 GB → 1 Aura', 'success'); | |
| } else { | |
| showNotification('No GBLX to convert!', 'warning'); | |
| } | |
| } | |
| function tapToAscend() { | |
| if (balance >= 100) { | |
| const conversionRate = 1.5; // ProBlox multiplier | |
| const convertedAura = Math.floor(100 * conversionRate); | |
| balance -= 100; | |
| score += convertedAura; | |
| // Update display | |
| document.getElementById('questScoreDisplay').textContent = score.toLocaleString(); | |
| document.getElementById('questBalanceDisplay').textContent = balance.toLocaleString(); | |
| updateLevelProgress(); | |
| updateQuestLevelProgress(); | |
| // Show notification | |
| showNotification(`🚀 Ascended! +${convertedAura} Aura`, 'success'); | |
| // Add visual effect | |
| const button = event.target; | |
| button.style.transform = 'scale(0.95)'; | |
| setTimeout(() => { | |
| button.style.transform = 'scale(1)'; | |
| }, 150); | |
| } else { | |
| showNotification('Not enough GBLX!', 'warning'); | |
| } | |
| } | |
| function showTapEffect(text) { | |
| // Find the tap image | |
| let button = document.querySelector('img[src="mohawk_cube_head.png"]'); | |
| if (!button) { | |
| return; | |
| } | |
| const rect = button.getBoundingClientRect(); | |
| const effect = document.createElement('div'); | |
| effect.className = 'tap-effect absolute pointer-events-none'; | |
| effect.textContent = text || ''; | |
| effect.style.cssText = ` | |
| position: absolute; | |
| left: ${rect.left + rect.width / 2}px; | |
| top: ${rect.top + rect.height / 2}px; | |
| color: #00ff88; | |
| font-weight: bold; | |
| font-size: 20px; | |
| z-index: 1000; | |
| pointer-events: none; | |
| animation: floatUp 0.6s ease-out forwards; | |
| `; | |
| document.body.appendChild(effect); | |
| setTimeout(() => { | |
| if (effect.parentNode) { | |
| document.body.removeChild(effect); | |
| } | |
| }, 600); | |
| } | |
| function showNotification(message, type = 'info') { | |
| const notification = document.getElementById('notification'); | |
| notification.textContent = message; | |
| notification.style.opacity = '1'; | |
| setTimeout(() => { | |
| notification.style.opacity = '0'; | |
| }, 3000); | |
| } | |
| // Update tap context + spotlight based on monument state | |
| function updateMonumentUI() { | |
| const ctx = document.getElementById('tapContextLabel'); | |
| const spotBox = document.getElementById('monumentSpotlightBox'); | |
| const spotTag = document.getElementById('monumentSpotlightTag'); | |
| const spotTitle = document.getElementById('monumentSpotlightTitle'); | |
| const spotText = document.getElementById('monumentSpotlightText'); | |
| if (monumentUnderAttack) { | |
| if (ctx) ctx.textContent = 'RED ALERT: Defend your monument!'; | |
| if (spotTag) { spotTag.textContent = 'Red Alert'; spotTag.className = 'inline-block px-1.5 py-0.5 rounded-full text-xs font-medium bg-red-500/20 border border-red-500/30 text-red-600'; } | |
| if (spotTitle) spotTitle.textContent = 'Your monument is under attack'; | |
| if (spotText) spotText.textContent = 'Tap to defend and keep your Mayor status'; | |
| if (spotBox) spotBox.className = 'h-full p-4 flex flex-col justify-end bg-red-500/10'; | |
| return; | |
| } | |
| if (monumentReady) { | |
| if (ctx) ctx.textContent = `Tap to Conquer: ${nearestMonument.name} (Cost: ${nearestMonument.cost} GB)`; | |
| if (spotTag) { spotTag.textContent = 'Nearby Monument'; spotTag.className = 'inline-block px-1.5 py-0.5 rounded-full text-xs font-medium bg-black/10 border border-black/10'; } | |
| if (spotTitle) spotTitle.textContent = 'Nearby conquest available'; | |
| if (spotText) spotText.textContent = `${nearestMonument.name} • Cost: ${nearestMonument.cost} GB`; | |
| if (spotBox) spotBox.className = 'h-full p-4 flex flex-col justify-end'; | |
| } else { | |
| if (ctx) ctx.textContent = 'Tap to Convert: 1 GB → 1 Aura'; | |
| if (spotTag) { spotTag.textContent = 'Monument Spotlight'; spotTag.className = 'inline-block px-1.5 py-0.5 rounded-full text-xs font-medium bg-black/10 border border-black/10'; } | |
| if (spotTitle) spotTitle.textContent = 'No monument nearby'; | |
| if (spotText) spotText.textContent = 'Open Monuments to explore conquests'; | |
| if (spotBox) spotBox.className = 'h-full p-4 flex flex-col justify-end'; | |
| } | |
| } | |
| // Dev preview toggles (use in console) | |
| window.setMonumentReady = (v) => { monumentReady = !!v; monumentUnderAttack = false; updateMonumentUI(); }; | |
| window.setMonumentAlert = (v) => { monumentUnderAttack = !!v; updateMonumentUI(); }; | |
| // TEST FUNCTION - Run this in console to force level progress update | |
| window.testLevelProgress = () => { | |
| console.log('=== LEVEL PROGRESS TEST ==='); | |
| console.log('Current score:', score); | |
| console.log('Current level:', getLevel(score)); | |
| updateLevelProgress(); | |
| updateQuestLevelProgress(); | |
| console.log('Level progress forced update complete!'); | |
| }; | |
| // TEST FUNCTION - Test tap functionality | |
| window.testTap = () => { | |
| console.log('=== TAP TEST ==='); | |
| console.log('Current balance:', balance); | |
| console.log('Current score:', score); | |
| tap(); | |
| console.log('After tap - balance:', balance, 'score:', score); | |
| }; | |
| // FORCE LEVEL UPDATE FUNCTION - This will actually work! | |
| function forceLevelUpdate() { | |
| const aura = score; | |
| const level = getLevel(aura); | |
| // Find the correct next threshold | |
| let nextThreshold; | |
| if (level >= levelThresholds.length - 1) { | |
| // If at max level, create next threshold | |
| nextThreshold = levelThresholds[levelThresholds.length - 1] + 1000; | |
| } else { | |
| nextThreshold = levelThresholds[level + 1]; | |
| } | |
| const prevThreshold = levelThresholds[level] ?? 0; | |
| const progress = Math.max(0, Math.min(100, ((aura - prevThreshold) / (nextThreshold - prevThreshold)) * 100)); | |
| console.log('FORCE UPDATE:', {aura, level, progress, nextThreshold, prevThreshold}); | |
| // Force update the level progress bar | |
| const levelEl = document.getElementById('questLevelNumber'); | |
| const fillEl = document.getElementById('questLevelProgressFill'); | |
| const nextEl = document.getElementById('questLevelNextText'); | |
| if (levelEl) { | |
| levelEl.textContent = `Level ${level}`; | |
| console.log('Updated level text to:', `Level ${level}`); | |
| } | |
| if (fillEl) { | |
| fillEl.style.width = `${progress}%`; | |
| console.log('Updated progress bar to:', `${progress}%`); | |
| } | |
| if (nextEl) { | |
| nextEl.textContent = `Need ${Math.max(0, nextThreshold - aura)} Aura to reach Level ${level + 1}`; | |
| console.log('Updated next level text'); | |
| } | |
| // Force a visual update | |
| if (fillEl) { | |
| fillEl.style.transition = 'none'; | |
| fillEl.offsetHeight; // Force reflow | |
| fillEl.style.transition = 'all 0.3s ease-out'; | |
| } | |
| } | |
| // Level progression preview | |
| const levelThresholds = [0, 100, 250, 500, 1000, 1700, 2600, 3800, 5300, 7200, 9500, 12300, 15600, 19400, 23700, 28500, 33800, 39600, 45900, 52700, 60000]; | |
| function getLevel(aura) { | |
| let lvl = 0; // Start at level 0 | |
| for (let i = 1; i < levelThresholds.length; i++) { | |
| if (aura >= levelThresholds[i]) lvl = i; else break; | |
| } | |
| return lvl; | |
| } | |
| function updateLevelProgress() { | |
| const aura = score; // Aura equals total score in preview | |
| const level = getLevel(aura); | |
| const nextThreshold = levelThresholds[level + 1] ?? (levelThresholds[levelThresholds.length - 1] + 1000); | |
| const prevThreshold = levelThresholds[level] ?? 0; | |
| const progress = Math.max(0, Math.min(100, ((aura - prevThreshold) / (nextThreshold - prevThreshold)) * 100)); | |
| // Update PASSPORT tab level progress | |
| const passportLevelEl = document.getElementById('passportLevelNumber'); | |
| const passportFillEl = document.getElementById('passportLevelProgressFill'); | |
| const passportNextEl = document.getElementById('passportLevelNextText'); | |
| if (passportLevelEl) passportLevelEl.textContent = `Level ${level}`; | |
| if (passportFillEl) passportFillEl.style.width = `${progress}%`; | |
| if (passportNextEl) passportNextEl.textContent = `Need ${Math.max(0, nextThreshold - aura)} Aura to reach Level ${level + 1}`; | |
| // Update PASSPORT tap area level progress (Mobile) | |
| const passportTapLevelElMobile = document.getElementById('passportTapLevelNumberMobile'); | |
| const passportTapFillElMobile = document.getElementById('passportTapLevelProgressFillMobile'); | |
| const passportTapNextElMobile = document.getElementById('passportTapLevelNextTextMobile'); | |
| if (passportTapLevelElMobile) passportTapLevelElMobile.textContent = `Level ${level}`; | |
| if (passportTapFillElMobile) passportTapFillElMobile.style.width = `${progress}%`; | |
| if (passportTapNextElMobile) passportTapNextElMobile.textContent = `Need ${Math.max(0, nextThreshold - aura)} Aura`; | |
| // Update PASSPORT tap area level progress (Desktop) | |
| const passportTapLevelElDesktop = document.getElementById('passportTapLevelNumberDesktop'); | |
| const passportTapFillElDesktop = document.getElementById('passportTapLevelProgressFillDesktop'); | |
| const passportTapNextElDesktop = document.getElementById('passportTapLevelNextTextDesktop'); | |
| if (passportTapLevelElDesktop) passportTapLevelElDesktop.textContent = `Level ${level}`; | |
| if (passportTapFillElDesktop) passportTapFillElDesktop.style.width = `${progress}%`; | |
| if (passportTapNextElDesktop) passportTapNextElDesktop.textContent = `Need ${Math.max(0, nextThreshold - aura)} Aura to reach Level ${level + 1}`; | |
| // Update Mobile Aura Progress Bar (Bottom Right Corner) | |
| const mobileAuraProgressEl = document.getElementById('mobileAuraProgress'); | |
| const mobileAuraTextEl = document.getElementById('mobileAuraText'); | |
| const mobileLevelInfoEl = document.getElementById('mobileLevelInfo'); | |
| if (mobileAuraProgressEl) { | |
| mobileAuraProgressEl.style.width = `${progress}%`; | |
| } | |
| if (mobileAuraTextEl) { | |
| mobileAuraTextEl.textContent = `${aura} / ${nextThreshold}`; | |
| } | |
| if (mobileLevelInfoEl) { | |
| mobileLevelInfoEl.textContent = `Level ${level} | Need ${Math.max(0, nextThreshold - aura)} to level ${level + 1}`; | |
| } | |
| // Update legacy elements (if they exist) | |
| const levelEl = document.getElementById('levelNumber'); | |
| const fillEl = document.getElementById('levelProgressFill'); | |
| const nextEl = document.getElementById('levelNextText'); | |
| if (levelEl) levelEl.textContent = `Level ${level}`; | |
| if (fillEl) fillEl.style.width = `${progress}%`; | |
| if (nextEl) nextEl.textContent = `Need ${Math.max(0, nextThreshold - aura)} Aura to reach Level ${level + 1}`; | |
| } | |
| function updateQuestLevelProgress() { | |
| const aura = score; // Aura equals total score in preview | |
| const level = getLevel(aura); | |
| const nextThreshold = levelThresholds[level + 1] ?? (levelThresholds[levelThresholds.length - 1] + 1000); | |
| const prevThreshold = levelThresholds[level] ?? 0; | |
| const progress = Math.max(0, Math.min(100, ((aura - prevThreshold) / (nextThreshold - prevThreshold)) * 100)); | |
| const levelEl = document.getElementById('questLevelNumber'); | |
| const fillEl = document.getElementById('questLevelProgressFill'); | |
| const nextEl = document.getElementById('questLevelNextText'); | |
| if (levelEl) levelEl.textContent = `Level ${level}`; | |
| if (fillEl) fillEl.style.width = `${progress}%`; | |
| if (nextEl) nextEl.textContent = `Need ${Math.max(0, nextThreshold - aura)} Aura to reach Level ${level + 1}`; | |
| // Update QUEST tap area level progress | |
| const questTapLevelEl = document.getElementById('questTapLevelNumber'); | |
| const questTapFillEl = document.getElementById('questTapLevelProgressFill'); | |
| const questTapNextEl = document.getElementById('questTapLevelNextText'); | |
| if (questTapLevelEl) questTapLevelEl.textContent = `Level ${level}`; | |
| if (questTapFillEl) questTapFillEl.style.width = `${progress}%`; | |
| if (questTapNextEl) questTapNextEl.textContent = `Need ${Math.max(0, nextThreshold - aura)} Aura to reach Level ${level + 1}`; | |
| // Force immediate update and log for debugging | |
| console.log(`FORCED UPDATE: Aura=${aura}, Level=${level}, Progress=${progress}%, Next=${nextThreshold}, Prev=${prevThreshold}`); | |
| // Force the DOM update | |
| if (fillEl) { | |
| fillEl.style.width = `${progress}%`; | |
| fillEl.style.transition = 'none'; | |
| setTimeout(() => { | |
| fillEl.style.transition = 'all 0.3s ease-out'; | |
| }, 10); | |
| } | |
| // Force the QUEST tap area DOM update | |
| if (questTapFillEl) { | |
| questTapFillEl.style.width = `${progress}%`; | |
| questTapFillEl.style.transition = 'none'; | |
| setTimeout(() => { | |
| questTapFillEl.style.transition = 'all 0.3s ease-out'; | |
| }, 10); | |
| } | |
| } | |
| function showTab(tab) { | |
| currentTab = tab; | |
| console.log('🔄 Switching to tab:', tab); | |
| // Hide all tabs | |
| document.getElementById('passportTab').classList.add('hidden'); | |
| document.getElementById('questsTab').classList.add('hidden'); | |
| document.getElementById('friendsTab').classList.add('hidden'); | |
| document.getElementById('monumentsTab').classList.add('hidden'); | |
| // Show selected tab | |
| const targetTab = document.getElementById(tab + 'Tab'); | |
| if (targetTab) { | |
| targetTab.classList.remove('hidden'); | |
| console.log('✅ Tab shown:', tab + 'Tab'); | |
| } else { | |
| console.error('❌ Tab not found:', tab + 'Tab'); | |
| } | |
| // Update nav buttons - remove active from all | |
| document.querySelectorAll('.nav-btn').forEach(btn => { | |
| btn.classList.remove('active'); | |
| console.log('❌ Removed active from button'); | |
| }); | |
| // Add active to the correct button based on tab | |
| let activeButton = null; | |
| // Try different selectors for the active button | |
| if (tab === 'monuments') { | |
| // Special handling for StreetBlox | |
| activeButton = document.getElementById('streetbloxButton'); | |
| console.log('🔍 StreetBlox: Using ID selector, found:', activeButton); | |
| } else { | |
| activeButton = document.querySelector(`[onclick="showTab('${tab}')"]`); | |
| console.log('🔍 Other tabs: Using onclick selector, found:', activeButton); | |
| } | |
| if (activeButton) { | |
| // Remove active from all buttons first | |
| document.querySelectorAll('.nav-btn').forEach(btn => { | |
| btn.classList.remove('active'); | |
| btn.removeAttribute('data-active'); | |
| }); | |
| // Add active class AND data attribute | |
| activeButton.classList.add('active'); | |
| activeButton.setAttribute('data-active', 'true'); | |
| console.log('✅ Active button updated for tab:', tab); | |
| // Force the active state to persist with multiple attempts | |
| setTimeout(() => { | |
| activeButton.classList.add('active'); | |
| activeButton.setAttribute('data-active', 'true'); | |
| console.log('🔄 Re-applied active state for tab:', tab); | |
| }, 10); | |
| setTimeout(() => { | |
| activeButton.classList.add('active'); | |
| activeButton.setAttribute('data-active', 'true'); | |
| console.log('🔄 Re-applied active state again for tab:', tab); | |
| }, 100); | |
| setTimeout(() => { | |
| activeButton.classList.add('active'); | |
| activeButton.setAttribute('data-active', 'true'); | |
| console.log('🔄 Final re-application for tab:', tab); | |
| }, 500); | |
| } else { | |
| console.error('❌ Active button not found for tab:', tab); | |
| } | |
| } | |
| function buyUpgrade(type) { | |
| if (type === 'multitap' && balance >= 300) { | |
| balance -= 300; | |
| multitap += 1; | |
| document.getElementById('balanceDisplay').textContent = balance.toLocaleString(); | |
| showNotification('Multitap upgraded!', 'success'); | |
| } else { | |
| showNotification('Not enough GBLX!', 'warning'); | |
| } | |
| } | |
| // No energy system; timer reserved for future live updates | |
| // 3D tilt effect initialization | |
| document.addEventListener('DOMContentLoaded', function() { | |
| console.log('🚀 DOM Content Loaded - Starting initialization...'); | |
| // Handle Telegram OAuth return via URL hash: #tgAuthResult=BASE64_JSON | |
| try { | |
| const HASH_KEY = '#tgAuthResult='; | |
| if (location.hash && location.hash.startsWith(HASH_KEY)) { | |
| const enc = location.hash.substring(HASH_KEY.length); | |
| const json = atob(decodeURIComponent(enc)); | |
| const user = JSON.parse(json); | |
| if (user && user.id) { | |
| console.log('🔗 Parsed Telegram auth from URL hash'); | |
| onTelegramAuth(user); | |
| history.replaceState(null, '', location.pathname + location.search); | |
| } | |
| } | |
| } catch(e) { console.log('Hash auth parse skipped:', e.message); } | |
| // Restore Telegram login from localStorage (desktop web flow) | |
| try { | |
| const savedUserRaw = localStorage.getItem('bloxid_current_user'); | |
| if (savedUserRaw) { | |
| const savedUser = JSON.parse(savedUserRaw); | |
| if (savedUser && savedUser.id) { | |
| currentUser = savedUser; | |
| isLoggedIn = true; | |
| updateUserProfile(currentUser); | |
| giveBlueBloxBadge(); | |
| updateBadgeDisplay(); | |
| console.log('🔐 Restored Telegram session from localStorage for user', currentUser.id); | |
| // Reflect connected state on button | |
| try { | |
| const btn = document.getElementById('tgConnectBtn'); | |
| const txt = document.getElementById('tgBtnText'); | |
| if (btn) btn.classList.add('bg-blue-600','border-blue-500'); | |
| if (txt) txt.textContent = 'Connected'; | |
| } catch(e) {} | |
| } | |
| } | |
| } catch(e) { console.log('No saved Telegram session to restore'); } | |
| // Telegram WebApp initialization | |
| if (window.Telegram && window.Telegram.WebApp) { | |
| const tg = window.Telegram.WebApp; | |
| // Expand to full screen | |
| tg.expand(); | |
| // Enable closing confirmation | |
| tg.enableClosingConfirmation(); | |
| // Set header color to match theme | |
| tg.setHeaderColor('#0f172a'); | |
| // Set background color | |
| tg.setBackgroundColor('#0f172a'); | |
| // Ready to show | |
| tg.ready(); | |
| console.log('📱 Telegram WebApp initialized and expanded'); | |
| // Initialize Telegram user profile and login | |
| initializeTelegramUser(tg); | |
| } | |
| // Lightweight zoom prevention - only block multi-touch | |
| document.addEventListener('touchstart', function(e) { | |
| if (e.touches.length > 1) { | |
| e.preventDefault(); | |
| } | |
| }, { passive: false }); | |
| // Initialize displays with current values | |
| try { | |
| // Update all score displays immediately | |
| updateAllDisplays(); | |
| // Restore ProBlox badge if wallet was verified previously | |
| try { | |
| const wasVerified = localStorage.getItem('telegram_wallet_verified') === 'true'; | |
| if (wasVerified && !userBadges.includes('problox')) { | |
| userBadges.push('problox'); | |
| updateBadgeDisplay(); | |
| try { localStorage.setItem('bloxid_user_badges', JSON.stringify(userBadges)); } catch(e) {} | |
| } | |
| } catch(e) { } | |
| // Load quests immediately on app start | |
| setTimeout(() => { | |
| loadQuests(); | |
| // Sync with server for cross-platform compatibility | |
| syncWithServer(); | |
| }, 1000); | |
| // Legacy display updates | |
| document.getElementById('questScoreDisplay').textContent = score.toLocaleString(); | |
| document.getElementById('questBalanceDisplay').textContent = balance.toLocaleString(); | |
| } catch(e) { | |
| console.log('Display initialization skipped:', e.message); | |
| } | |
| // FORCE LEVEL PROGRESS UPDATE IMMEDIATELY | |
| setTimeout(() => { | |
| try { | |
| updateLevelProgress(); | |
| updateQuestLevelProgress(); | |
| forceLevelUpdate(); // ADD THIS TO FORCE IT TO WORK | |
| // Initialize mobile aura progress bar specifically | |
| const mobileAuraProgressEl = document.getElementById('mobileAuraProgress'); | |
| const mobileAuraTextEl = document.getElementById('mobileAuraText'); | |
| const mobileLevelInfoEl = document.getElementById('mobileLevelInfo'); | |
| if (mobileAuraProgressEl && mobileAuraTextEl && mobileLevelInfoEl) { | |
| const aura = score; | |
| const level = getLevel(aura); | |
| const nextThreshold = levelThresholds[level + 1] ?? (levelThresholds[levelThresholds.length - 1] + 1000); | |
| const prevThreshold = levelThresholds[level] ?? 0; | |
| const progress = Math.max(0, Math.min(100, ((aura - prevThreshold) / (nextThreshold - prevThreshold)) * 100)); | |
| mobileAuraProgressEl.style.width = `${progress}%`; | |
| mobileAuraTextEl.textContent = `${aura} / ${nextThreshold}`; | |
| mobileLevelInfoEl.textContent = `Level ${level} | Need ${Math.max(0, nextThreshold - aura)} to level ${level + 1}`; | |
| console.log('📱 Mobile aura progress bar initialized:', progress + '%'); | |
| } | |
| console.log('IMMEDIATE LEVEL UPDATE - Score:', score, 'Level:', getLevel(score)); | |
| } catch(e) { | |
| console.log('Level update skipped:', e.message); | |
| } | |
| }, 100); | |
| const tapImage = document.querySelector('.tap-image'); | |
| if (tapImage) { | |
| tapImage.addEventListener('mousemove', function(e) { | |
| const rect = this.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| const centerX = rect.width / 2; | |
| const centerY = rect.height / 2; | |
| const rotateX = (y - centerY) / centerY * -8; | |
| const rotateY = (x - centerX) / centerX * 8; | |
| this.style.transform = `rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.05) translateY(-4px)`; | |
| }); | |
| tapImage.addEventListener('mouseleave', function() { | |
| this.style.transform = 'rotateX(0deg) rotateY(0deg) scale(1) translateY(0px)'; | |
| }); | |
| tapImage.addEventListener('click', function() { | |
| // Add click animation | |
| this.style.transform = 'rotateX(-12deg) rotateY(0deg) scale(0.92) translateY(2px)'; | |
| setTimeout(() => { | |
| this.style.transform = 'rotateX(0deg) rotateY(0deg) scale(1) translateY(0px)'; | |
| }, 150); | |
| }); | |
| } | |
| // Initialize monument UI once DOM is ready | |
| try { updateMonumentUI(); } catch(e) {} | |
| try { updateLevelProgress(); } catch(e) {} | |
| try { updateQuestLevelProgress(); } catch(e) {} | |
| // CRITICAL: Ensure proper tab initialization | |
| console.log('🔧 Initializing tabs...'); | |
| // Get all tab elements | |
| const passportTab = document.getElementById('passportTab'); | |
| const questsTab = document.getElementById('questsTab'); | |
| const friendsTab = document.getElementById('friendsTab'); | |
| const monumentsTab = document.getElementById('monumentsTab'); | |
| console.log('📋 Tab elements found:', { | |
| passport: !!passportTab, | |
| quests: !!questsTab, | |
| friends: !!friendsTab, | |
| monuments: !!monumentsTab | |
| }); | |
| // Hide all tabs first | |
| if (passportTab) passportTab.classList.add('hidden'); | |
| if (questsTab) questsTab.classList.add('hidden'); | |
| if (friendsTab) friendsTab.classList.add('hidden'); | |
| if (monumentsTab) monumentsTab.classList.add('hidden'); | |
| // Set passport tab as default active | |
| if (passportTab) { | |
| passportTab.classList.remove('hidden'); | |
| console.log('✅ Passport tab shown successfully'); | |
| } else { | |
| console.error('❌ Passport tab not found'); | |
| } | |
| // Set active navigation button | |
| const navButtons = document.querySelectorAll('.nav-btn'); | |
| navButtons.forEach(btn => btn.classList.remove('active')); | |
| const passportNavBtn = document.querySelector('[onclick="showTab(\'passport\')"]'); | |
| if (passportNavBtn) { | |
| passportNavBtn.classList.add('active'); | |
| console.log('✅ Navigation button activated'); | |
| } | |
| // Initialize badge system | |
| try { | |
| showBadgeDetails('blueblox'); // Show default badge | |
| } catch(e) { | |
| console.log('Badge system initialization skipped:', e.message); | |
| } | |
| // Verify tab is visible after a short delay | |
| setTimeout(() => { | |
| const passportTab = document.getElementById('passportTab'); | |
| if (passportTab && !passportTab.classList.contains('hidden')) { | |
| console.log('✅ Page loaded successfully - Passport tab is visible'); | |
| } else { | |
| console.error('❌ Page load issue - Passport tab is not visible'); | |
| // Force show the tab | |
| if (passportTab) { | |
| passportTab.classList.remove('hidden'); | |
| console.log('🔄 Forced passport tab to be visible'); | |
| } | |
| } | |
| }, 100); | |
| console.log('🎉 Initialization complete!'); | |
| }); | |
| // FALLBACK: Immediate initialization in case DOMContentLoaded doesn't fire | |
| (function() { | |
| console.log('🔄 Fallback initialization running...'); | |
| // Check if DOM is already loaded | |
| if (document.readyState === 'loading') { | |
| console.log('📋 DOM still loading, waiting for DOMContentLoaded...'); | |
| return; | |
| } | |
| // Also restore Telegram session in fallback path | |
| try { | |
| const savedUserRaw = localStorage.getItem('bloxid_current_user'); | |
| if (savedUserRaw) { | |
| const savedUser = JSON.parse(savedUserRaw); | |
| if (savedUser && savedUser.id) { | |
| currentUser = savedUser; | |
| isLoggedIn = true; | |
| updateUserProfile(currentUser); | |
| giveBlueBloxBadge(); | |
| updateBadgeDisplay(); | |
| console.log('🔐 [fallback] Restored Telegram session from localStorage for user', currentUser.id); | |
| } | |
| } | |
| } catch(e) {} | |
| console.log('⚡ DOM already loaded, running immediate initialization...'); | |
| // Get all tab elements | |
| const passportTab = document.getElementById('passportTab'); | |
| const questsTab = document.getElementById('questsTab'); | |
| const friendsTab = document.getElementById('friendsTab'); | |
| const monumentsTab = document.getElementById('monumentsTab'); | |
| // Hide all tabs first | |
| if (passportTab) passportTab.classList.add('hidden'); | |
| if (questsTab) questsTab.classList.add('hidden'); | |
| if (friendsTab) friendsTab.classList.add('hidden'); | |
| if (monumentsTab) monumentsTab.classList.add('hidden'); | |
| // Show passport tab | |
| if (passportTab) { | |
| passportTab.classList.remove('hidden'); | |
| console.log('✅ Fallback: Passport tab shown'); | |
| } | |
| // Set active navigation button | |
| const navButtons = document.querySelectorAll('.nav-btn'); | |
| navButtons.forEach(btn => btn.classList.remove('active')); | |
| const passportNavBtn = document.querySelector('[onclick="showTab(\'passport\')"]'); | |
| if (passportNavBtn) { | |
| passportNavBtn.classList.add('active'); | |
| console.log('✅ Fallback: Navigation button activated'); | |
| } | |
| console.log('🎉 Fallback initialization complete!'); | |
| })(); | |
| // BuddyBlox API helpers | |
| async function addBuddy(buddyTid, buddyUsername = null) { | |
| try { | |
| const ownerTid = (window.currentUser && currentUser.id) ? String(currentUser.id) : ''; | |
| if (!ownerTid) { | |
| console.log('BUDDY_FAIL: No currentUser.id'); | |
| showNotification('Please connect Telegram first', 'error'); | |
| return; | |
| } | |
| console.log('BUDDY_TRY: Adding buddy', buddyTid, 'for owner', ownerTid); | |
| const res = await fetch('/api/buddies.php', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ owner_tid: ownerTid, buddy_tid: String(buddyTid), buddy_username: buddyUsername }) }); | |
| const j = await res.json(); | |
| console.log('BUDDY_RESPONSE:', j); | |
| if (j && j.ok) { | |
| console.log('BUDDY_OK: Added successfully'); | |
| showNotification('✅ Buddy added!', 'success'); | |
| loadBuddies(); | |
| } else { | |
| console.log('BUDDY_FAIL: Server rejected'); | |
| showNotification('⚠️ Could not add buddy', 'error'); | |
| } | |
| } catch(e) { | |
| console.log('BUDDY_ERROR:', e.message); | |
| showNotification('⚠️ Add buddy error', 'error'); | |
| } | |
| } | |
| async function loadBuddies() { | |
| try { | |
| const ownerTid = (window.currentUser && currentUser.id) ? String(currentUser.id) : ''; | |
| if (!ownerTid) return; | |
| const res = await fetch(`/api/buddies.php?owner_tid=${encodeURIComponent(ownerTid)}`); | |
| const j = await res.json(); | |
| if (j && j.ok && Array.isArray(j.buddies)) { | |
| const el = document.getElementById('buddyList'); | |
| if (el) el.innerHTML = j.buddies.map(b => `<div class=\"flex items-center gap-2 py-1\"><div class=\"w-6 h-6 rounded-full bg-white/10 flex items-center justify-center text-xs\">${(b.username||'U').slice(0,1)}</div><div class=\"text-xs\">${b.username||b.tid}</div></div>`).join(''); | |
| } | |
| } catch(e) {} | |
| } | |
| // Badge system functions | |
| function showBadgeDetails(badgeType) { | |
| // Find all badge detail elements (for both tabs) | |
| const badgeDetails = document.querySelectorAll('#badgeDetails, #passportBadgeDetails'); | |
| const badgeImages = document.querySelectorAll('#badgeDetailImage, #passportBadgeDetailImage'); | |
| const badgeTitles = document.querySelectorAll('#badgeDetailTitle, #passportBadgeDetailTitle'); | |
| const badgePerks = document.querySelectorAll('#badgeDetailPerk, #passportBadgeDetailPerk'); | |
| // Remove active class from all badge buttons | |
| document.querySelectorAll('.badge-btn').forEach(btn => { | |
| btn.classList.remove('bg-white/20', 'border-indigo-500/50'); | |
| btn.classList.add('hover:bg-white/10'); | |
| }); | |
| // Add active class to clicked badge button | |
| const activeBtns = document.querySelectorAll(`[onclick="showBadgeDetails('${badgeType}')"]`); | |
| activeBtns.forEach(btn => { | |
| btn.classList.add('bg-white/20', 'border-indigo-500/50'); | |
| btn.classList.remove('hover:bg-white/10'); | |
| }); | |
| // Update badge details based on type | |
| let badgeImageSrc, badgeTitleText, badgePerkText; | |
| switch(badgeType) { | |
| case 'blueblox': | |
| badgeImageSrc = 'BlueBlox_NEW.png'; | |
| badgeTitleText = 'BlueBlox'; | |
| badgePerkText = 'Multiplier: x1.2'; | |
| break; | |
| case 'problox': | |
| badgeImageSrc = 'ProBlox_NEW.png'; | |
| badgeTitleText = 'ProBlox'; | |
| badgePerkText = 'Multiplier: x1.5'; | |
| break; | |
| case 'mayorblox': | |
| badgeImageSrc = 'mayorblox_NEW.png'; | |
| badgeTitleText = 'MayorBlox'; | |
| badgePerkText = '+50 GBLX p/day'; | |
| break; | |
| } | |
| // Update all badge detail elements | |
| badgeImages.forEach(img => img.src = badgeImageSrc); | |
| badgeTitles.forEach(title => title.textContent = badgeTitleText); | |
| badgePerks.forEach(perk => perk.textContent = badgePerkText); | |
| } | |
| // New Badge Modal System | |
| function showBadgeModal(badgeType) { | |
| const modal = document.getElementById('badgeModal'); | |
| const modalImage = document.getElementById('modalBadgeImage'); | |
| const modalTitle = document.getElementById('modalBadgeTitle'); | |
| const modalPerk = document.getElementById('modalBadgePerk'); | |
| // Update modal content based on badge type | |
| let badgeImageSrc, badgeTitleText, badgePerkText; | |
| switch(badgeType) { | |
| case 'blueblox': | |
| badgeImageSrc = 'BlueBlox_NEW.png'; | |
| badgeTitleText = 'BlueBlox'; | |
| badgePerkText = 'Multiplier: x1.2'; | |
| break; | |
| case 'problox': | |
| badgeImageSrc = 'ProBlox_NEW.png'; | |
| badgeTitleText = 'ProBlox'; | |
| badgePerkText = 'Multiplier: x1.5'; | |
| break; | |
| case 'mayorblox': | |
| badgeImageSrc = 'mayorblox_NEW.png'; | |
| badgeTitleText = 'MayorBlox'; | |
| badgePerkText = '+50 GBLX p/day'; | |
| break; | |
| } | |
| // Update modal elements | |
| modalImage.src = badgeImageSrc; | |
| modalTitle.textContent = badgeTitleText; | |
| modalPerk.textContent = badgePerkText; | |
| // Show modal | |
| modal.classList.remove('hidden'); | |
| modal.classList.add('flex'); | |
| } | |
| function closeBadgeModal() { | |
| const modal = document.getElementById('badgeModal'); | |
| modal.classList.add('hidden'); | |
| modal.classList.remove('flex'); | |
| } | |
| // Close modal when clicking outside | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const modal = document.getElementById('badgeModal'); | |
| modal.addEventListener('click', function(e) { | |
| if (e.target === modal) { | |
| closeBadgeModal(); | |
| } | |
| }); | |
| // Check Telegram wallet connection status on page load | |
| const isConnected = localStorage.getItem('telegram_wallet_connected') === 'true'; | |
| const connectBtnInit = document.getElementById('walletConnectBtn'); | |
| const buttonTextInit = document.getElementById('walletBtnText'); | |
| // Restore badges from localStorage if present | |
| try { | |
| const storedBadges = localStorage.getItem('bloxid_user_badges'); | |
| if (storedBadges) { | |
| const parsed = JSON.parse(storedBadges); | |
| if (Array.isArray(parsed)) { | |
| userBadges = parsed; | |
| } | |
| } | |
| } catch(e) {} | |
| if (isConnected) { | |
| if (!userBadges.includes('problox')) { | |
| userBadges.push('problox'); | |
| try { localStorage.setItem('bloxid_user_badges', JSON.stringify(userBadges)); } catch(e) {} | |
| } | |
| updateBadgeDisplay(); | |
| enforceVerifiedProbloxBadge(); | |
| if (connectBtnInit) { | |
| connectBtnInit.disabled = false; | |
| connectBtnInit.className = 'card-glass rounded-xl px-4 py-2 w-full border text-white font-semibold text-xs flex items-center justify-center space-x-2 bg-green-600 hover:bg-green-700 border-green-400/50'; | |
| } | |
| if (buttonTextInit) buttonTextInit.textContent = 'Connected!'; | |
| } else { | |
| // Even if button shows Connect, if verified we still show the ProBlox badge | |
| enforceVerifiedProbloxBadge(); | |
| if (connectBtnInit) { | |
| connectBtnInit.disabled = false; | |
| connectBtnInit.className = 'card-glass rounded-xl px-4 py-2 w-full border text-white font-semibold text-xs flex items-center justify-center space-x-2 bg-gray-600 hover:bg-gray-700 border-white/20'; | |
| } | |
| if (buttonTextInit) buttonTextInit.textContent = 'Connect'; | |
| } | |
| }); | |
| </script> | |
| <style> | |
| @keyframes floatUp { | |
| 0% { | |
| opacity: 1; | |
| transform: translateY(0) scale(1); | |
| } | |
| 50% { | |
| opacity: 0.8; | |
| transform: translateY(-25px) scale(1.1); | |
| } | |
| 100% { | |
| opacity: 0; | |
| transform: translateY(-50px) scale(1.2); | |
| } | |
| } | |
| .tap-image { | |
| cursor: pointer; | |
| user-select: none; | |
| -webkit-user-select: none; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| /* No visual effects on mobile for maximum tap speed */ | |
| @media (max-width: 768px) { | |
| .tap-image { | |
| transition: none ; | |
| filter: none ; /* Remove all shadows/effects */ | |
| transform: none ; | |
| } | |
| .tap-image:active, | |
| .tap-image:hover { | |
| transform: none ; /* No effects at all */ | |
| filter: none ; | |
| } | |
| } | |
| /* Desktop effects only */ | |
| @media (min-width: 769px) { | |
| .tap-image { | |
| transition: transform 0.2s ease; | |
| filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2)); | |
| } | |
| .tap-image:hover { | |
| transform: scale(1.05); | |
| } | |
| } | |
| </style> | |
| <!-- Badge Modal --> | |
| <div id="badgeModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"> | |
| <div class="card-glass rounded-2xl p-6 max-w-sm w-full mx-4 relative"> | |
| <!-- Close Button --> | |
| <button onclick="closeBadgeModal()" class="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M18 6L6 18M6 6l12 12"/> | |
| </svg> | |
| </button> | |
| <!-- Badge Content --> | |
| <div class="text-center"> | |
| <div class="w-24 h-24 mb-4 mx-auto"> | |
| <img id="modalBadgeImage" src="BlueBlox_NEW.png" alt="Badge" class="w-full h-full object-contain"> | |
| </div> | |
| <h3 id="modalBadgeTitle" class="text-xl font-bold mb-2">BlueBlox</h3> | |
| <p id="modalBadgePerk" class="text-gray-300 mb-4">Multiplier: x1.2</p> | |
| <div class="text-sm text-gray-400"> | |
| <p>This badge provides a permanent multiplier bonus to your aura generation.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- FIX SCRIPT - Tab switching is now fixed in the main function above --> | |
| <script> | |
| console.log('🔧 Tab switching fix loaded! The main showTab function has been updated.'); | |
| // Add direct event listeners to all navigation buttons | |
| setTimeout(() => { | |
| console.log('🔧 Setting up direct event listeners for all nav buttons...'); | |
| // BloxID button | |
| const bloxidBtn = document.querySelector('[onclick="showTab(\'passport\')"]'); | |
| if (bloxidBtn) { | |
| bloxidBtn.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| showTab('passport'); | |
| }); | |
| } | |
| // QuestBlox button | |
| const questBtn = document.querySelector('[onclick="showTab(\'quests\')"]'); | |
| if (questBtn) { | |
| questBtn.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| showTab('quests'); | |
| }); | |
| } | |
| // StreetBlox button | |
| const streetbloxBtn = document.getElementById('streetbloxButton'); | |
| if (streetbloxBtn) { | |
| streetbloxBtn.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| showTab('monuments'); | |
| }); | |
| } | |
| // BuddyBlox button | |
| const buddyBtn = document.querySelector('[onclick="showTab(\'friends\')"]'); | |
| if (buddyBtn) { | |
| buddyBtn.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| showTab('friends'); | |
| }); | |
| } | |
| console.log('✅ All navigation buttons now have direct event listeners!'); | |
| }, 1000); | |
| // ===== COMPLETE TELEGRAM INTEGRATION ===== | |
| // Complete Telegram User Integration | |
| function initializeTelegramUser(tg) { | |
| console.log('👤 Initializing Telegram user...'); | |
| const user = tg.initDataUnsafe.user; | |
| if (user) { | |
| console.log('✅ Telegram user found:', user); | |
| // Send user data to login API | |
| fetch('/api/login/', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Telegram-Data': tg.initData | |
| }, | |
| body: JSON.stringify({ user: user }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.ok) { | |
| console.log('✅ User logged in successfully'); | |
| currentUser = user; | |
| isLoggedIn = true; | |
| // Update user profile display | |
| updateUserProfile(user); | |
| // Store user token for API calls | |
| sessionStorage.setItem('app_token', data.hash); | |
| // Load user badges from login response | |
| if (data.badges) { | |
| userBadges = data.badges; | |
| updateBadgeDisplay(); | |
| } | |
| // Load user's current score and badges | |
| loadUserData(); | |
| // Load available quests | |
| loadQuests(); | |
| } else { | |
| console.error('❌ Login failed:', data); | |
| useTestMode(); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('❌ Login error:', error); | |
| useTestMode(); | |
| }); | |
| } else { | |
| console.log('⚠️ No Telegram user data, using test mode'); | |
| useTestMode(); | |
| } | |
| } | |
| // TON Wallet Quest start handler (shows dialog on mobile) | |
| function startTonWalletQuest() { | |
| const isMobile = window.matchMedia('(max-width: 768px)').matches; | |
| if (isMobile) { | |
| // Mobile: connect directly | |
| connectTelegramWallet(); | |
| return; | |
| } | |
| // Desktop: show simple dialog similar to badge modal | |
| const existing = document.getElementById('tonQuestDialog'); | |
| if (existing) existing.parentNode.removeChild(existing); | |
| const dialog = document.createElement('div'); | |
| dialog.id = 'tonQuestDialog'; | |
| dialog.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm'; | |
| dialog.innerHTML = ` | |
| <div class="card-glass rounded-2xl p-6 max-w-sm w-full mx-4 relative"> | |
| <button id="tonDialogClose" class="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M18 6L6 18M6 6l12 12"/> | |
| </svg> | |
| </button> | |
| <div class="text-center"> | |
| <div class="w-20 h-20 mb-4 mx-auto"> | |
| <img src="ProBlox_NEW.png" alt="ProBlox" class="w-full h-full object-contain"> | |
| </div> | |
| <h3 class="text-xl font-bold mb-2">Connect TON Wallet</h3> | |
| <p class="text-gray-300 mb-4">Earn 150 GBLX + 3× multiplier while connected.</p> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <button id="tonChooseMobile" class="px-4 py-2 bg-muted rounded-lg text-sm">I'm on Mobile</button> | |
| <button id="tonChooseDesktop" class="px-4 py-2 bg-muted rounded-lg text-sm">I'm on Desktop</button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| document.body.appendChild(dialog); | |
| document.getElementById('tonDialogClose').onclick = () => dialog.remove(); | |
| document.getElementById('tonChooseMobile').onclick = () => { dialog.remove(); connectTelegramWallet(); }; | |
| document.getElementById('tonChooseDesktop').onclick = () => { dialog.remove(); connectTelegramWallet(); }; | |
| } | |
| // LAST-RESORT RESTORE: read tg_user cookie set by auth_redirect.php | |
| try { | |
| const m = document.cookie.match(/(?:^|; )tg_user=([^;]+)/); | |
| if (m) { | |
| const raw = decodeURIComponent(m[1]); | |
| const user = JSON.parse(atob(raw)); | |
| if (user && user.id) { | |
| console.log('🍪 Restored Telegram user from cookie'); | |
| onTelegramAuth(user); | |
| // Clear cookie | |
| document.cookie = 'tg_user=; Max-Age=0; path=/'; | |
| } | |
| } | |
| } catch(e) {} | |
| // Show detailed aura information when progress bar is clicked | |
| function showAuraDetails() { | |
| const currentLevel = getLevel(score); | |
| const nextThreshold = levelThresholds[currentLevel + 1] ?? (levelThresholds[levelThresholds.length - 1] + 1000); | |
| const prevThreshold = levelThresholds[currentLevel] ?? 0; | |
| const progress = Math.max(0, Math.min(100, ((score - prevThreshold) / (nextThreshold - prevThreshold)) * 100)); | |
| const message = `Level ${currentLevel}\n${score} / ${nextThreshold} Aura\n${progress.toFixed(1)}% Complete\n\nNext Level: ${nextThreshold - score} Aura needed`; | |
| if (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.showAlert) { | |
| window.Telegram.WebApp.showAlert(message); | |
| } else { | |
| alert(message); | |
| } | |
| } | |
| // Use test mode when Telegram data is not available | |
| function useTestMode() { | |
| currentUser = { | |
| id: 123456789, | |
| first_name: 'Test User', | |
| username: 'testuser', | |
| photo_url: null | |
| }; | |
| isLoggedIn = true; | |
| updateUserProfile(currentUser); | |
| giveBlueBloxBadge(); | |
| updateBadgeDisplay(); | |
| // Load existing values from localStorage or use defaults | |
| score = parseInt(localStorage.getItem('bloxid_unified_score')) || score; | |
| balance = parseInt(localStorage.getItem('bloxid_unified_balance')) || balance; | |
| // Save to localStorage | |
| localStorage.setItem('bloxid_unified_score', score); | |
| localStorage.setItem('bloxid_unified_balance', balance); | |
| // Update displays with loaded values | |
| updateAllDisplays(); | |
| // Load quests in test mode too | |
| loadQuests(); | |
| // Ensure displays are updated with loaded values | |
| setTimeout(() => { | |
| updateAllDisplays(); | |
| console.log('✅ Displays updated with persisted data - Score:', score, 'Balance:', balance); | |
| }, 500); | |
| } | |
| // Update user profile in UI | |
| function updateUserProfile(user) { | |
| console.log('🔄 Updating user profile display...'); | |
| // Update user avatar | |
| const avatarElement = document.querySelector('.w-8.h-8.rounded-full'); | |
| if (avatarElement) { | |
| if (user.photo_url) { | |
| avatarElement.innerHTML = `<img src="${user.photo_url}" class="w-8 h-8 rounded-full object-cover">`; | |
| } else { | |
| // Use first letter of name with gradient background | |
| const firstLetter = user.first_name ? user.first_name.charAt(0).toUpperCase() : 'U'; | |
| avatarElement.innerHTML = firstLetter; | |
| avatarElement.className = 'w-8 h-8 rounded-full bg-gradient-to-r from-accent to-secondary flex items-center justify-center font-bold text-xs'; | |
| } | |
| } | |
| // Update username display | |
| try { | |
| const bottom = document.getElementById('usernameBottom'); | |
| if (bottom) bottom.textContent = user.first_name || 'User'; | |
| const desktopName = document.getElementById('usernameDesktop'); | |
| if (desktopName) desktopName.textContent = user.first_name || 'User'; | |
| } catch(e) {} | |
| console.log('✅ User profile updated'); | |
| } | |
| // Give BlueBlox badge to user | |
| function giveBlueBloxBadge() { | |
| console.log('🏆 Giving BlueBlox badge to user...'); | |
| if (!userBadges.includes('blueblox')) { | |
| userBadges.push('blueblox'); | |
| updateBadgeDisplay(); | |
| showNotification('🏆 BlueBlox Badge Earned! Now you can do quests!', 'success'); | |
| } | |
| } | |
| // Update badge display with question marks for empty slots | |
| function updateBadgeDisplay() { | |
| // Ensure ProBlox is present if wallet is verified | |
| try { | |
| const wasVerified = localStorage.getItem('telegram_wallet_verified') === 'true'; | |
| if (wasVerified && !userBadges.includes('problox')) { | |
| userBadges.push('problox'); | |
| try { localStorage.setItem('bloxid_user_badges', JSON.stringify(userBadges)); } catch(e) {} | |
| } | |
| if (!wasVerified && userBadges.includes('problox')) { | |
| // If not verified anymore, ensure problox is removed from display list | |
| userBadges = userBadges.filter(b => b !== 'problox'); | |
| try { localStorage.setItem('bloxid_user_badges', JSON.stringify(userBadges)); } catch(e) {} | |
| } | |
| } catch (e) {} | |
| console.log('🏆 Updating badge display...'); | |
| const badgeSlots = document.querySelectorAll('.badge-slot'); | |
| badgeSlots.forEach((slot, index) => { | |
| if (index < userBadges.length) { | |
| const badgeType = userBadges[index]; | |
| let badgeImage = 'BlueBlox_NEW.png'; | |
| let badgeName = 'BlueBlox'; | |
| switch(badgeType) { | |
| case 'blueblox': | |
| badgeImage = 'BlueBlox_NEW.png'; | |
| badgeName = 'BlueBlox'; | |
| break; | |
| case 'problox': | |
| badgeImage = 'ProBlox_NEW.png'; | |
| badgeName = 'ProBlox'; | |
| break; | |
| case 'mayorblox': | |
| badgeImage = 'mayorblox_NEW.png'; | |
| badgeName = 'MayorBlox'; | |
| break; | |
| } | |
| slot.innerHTML = `<img src="${badgeImage}" alt="${badgeName} Badge" class="w-8 h-8 object-contain">`; | |
| slot.onclick = () => showBadgeModal(badgeType); | |
| slot.className = 'badge-slot w-12 h-12 rounded-lg hover:bg-white/10 transition-all duration-200 flex items-center justify-center relative group'; | |
| } else { | |
| // Empty slot with question mark | |
| slot.innerHTML = ` | |
| <div class="w-8 h-8 rounded-lg bg-white/10 border border-white/20 flex items-center justify-center"> | |
| <span class="text-xs text-gray-400">?</span> | |
| </div> | |
| `; | |
| slot.onclick = null; | |
| slot.className = 'badge-slot w-12 h-12 rounded-lg bg-white/5 border border-white/10 backdrop-blur-sm flex items-center justify-center relative group'; | |
| } | |
| }); | |
| } | |
| // Load user data from server | |
| function loadUserData() { | |
| if (!currentUser) return; | |
| fetch('/api/getUser/', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'App-Token': sessionStorage.getItem('app_token') || '' | |
| }, | |
| body: JSON.stringify({ user_id: currentUser.id }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.ok || data.score !== undefined) { | |
| console.log('✅ User data loaded:', data); | |
| // Prioritize server data for cross-platform sync | |
| score = parseInt(localStorage.getItem('bloxid_unified_score')) || 0 // DISABLED server override; | |
| balance = data.balance || parseInt(localStorage.getItem('bloxid_unified_balance')) || 1000; | |
| // Save server values to localStorage for offline use | |
| localStorage.setItem('bloxid_unified_score', score); | |
| localStorage.setItem('bloxid_unified_balance', balance); | |
| console.log('🔄 Cross-platform sync: Server score:', data.score, 'Server balance:', data.balance); | |
| // Load user badges (if stored in database) | |
| if (data.badges) { | |
| try { | |
| userBadges = JSON.parse(data.badges); | |
| } catch(e) { | |
| userBadges = ['blueblox']; // Default | |
| } | |
| } | |
| // Ensure user has BlueBlox badge | |
| if (!userBadges.includes('blueblox')) { | |
| userBadges.unshift('blueblox'); // Add at beginning | |
| } | |
| // Update all displays | |
| updateAllDisplays(); | |
| // Re-enforce verified ProBlox after any server badge load | |
| enforceVerifiedProbloxBadge(); | |
| updateBadgeDisplay(); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('❌ Error loading user data:', error); | |
| // Give default badge anyway | |
| giveBlueBloxBadge(); | |
| }); | |
| } | |
| // Enhanced tap function with server saving | |
| function enhancedTap() { | |
| if (!isLoggedIn) { | |
| console.log('⚠️ User not logged in, using local tap'); | |
| tap(); | |
| return; | |
| } | |
| // Call original tap function for immediate UI update | |
| tap(); | |
| // Save to server | |
| saveScoreToServer(); | |
| } | |
| // Save score to server in real-time with cross-platform sync | |
| function saveScoreToServer() { | |
| if (!currentUser || !isLoggedIn) return; | |
| fetch('/api/tap/', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'App-Token': sessionStorage.getItem('app_token') || '' | |
| }, | |
| body: JSON.stringify({ | |
| user_id: currentUser.id, | |
| score: score, | |
| balance: balance | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.ok) { | |
| console.log('💾 Score saved to server - synced across platforms'); | |
| // Update with server response for sync | |
| if (data.new_score !== undefined) { | |
| score = data.new_score; | |
| balance = data.new_balance || balance; | |
| // Update localStorage with server values | |
| localStorage.setItem('bloxid_unified_score', score); | |
| localStorage.setItem('bloxid_unified_balance', balance); | |
| updateAllDisplays(); | |
| } | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('❌ Error saving score:', error); | |
| }); | |
| } | |
| // Update all score displays | |
| function updateAllDisplays() { | |
| const displays = [ | |
| 'questScoreDisplay', 'questBalanceDisplay', | |
| 'passportScoreDisplay', 'passportBalanceDisplay', | |
| 'passportTapScoreDisplay', 'passportTapBalanceDisplay', | |
| 'passportTapGBLXDisplay' | |
| ]; | |
| displays.forEach(id => { | |
| const element = document.getElementById(id); | |
| if (element) { | |
| if (id.includes('Score') || id.includes('Aura')) { | |
| element.textContent = score.toLocaleString(); | |
| } else if (id.includes('Balance') || id.includes('GBLX')) { | |
| element.textContent = balance.toLocaleString(); | |
| } | |
| } | |
| }); | |
| // Update mobile aura progress bar when displays are updated | |
| updateLevelProgress(); | |
| } | |
| // Badge earning system | |
| function earnBadge(badgeType) { | |
| if (!userBadges.includes(badgeType)) { | |
| userBadges.push(badgeType); | |
| updateBadgeDisplay(); | |
| const badgeNames = { | |
| 'blueblox': 'BlueBlox', | |
| 'problox': 'ProBlox', | |
| 'mayorblox': 'MayorBlox' | |
| }; | |
| showNotification(`🏆 ${badgeNames[badgeType]} Badge Earned!`, 'success'); | |
| // Save badges to server | |
| saveBadgesToServer(); | |
| } | |
| } | |
| // Save badges to server | |
| function saveBadgesToServer() { | |
| if (!currentUser) return; | |
| fetch('/api/updateBadges/', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'App-Token': sessionStorage.getItem('app_token') || '' | |
| }, | |
| body: JSON.stringify({ | |
| user_id: currentUser.id, | |
| badges: JSON.stringify(userBadges) | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.ok) { | |
| console.log('🏆 Badges saved to server'); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('❌ Error saving badges:', error); | |
| }); | |
| } | |
| // Load and display quests from Admin CMS JSON API | |
| function loadQuests() { | |
| console.log('🎮 Loading quests from Admin API...'); | |
| fetch('/admin/quests_api.php') | |
| .then(r => r.json()) | |
| .then(data => { | |
| if (!data || !data.ok) throw new Error('Invalid response'); | |
| const quests = Array.isArray(data.quests) ? data.quests : []; | |
| console.log('📦 Quests from CMS:', quests); | |
| renderCmsFeaturedQuests(quests); | |
| displayQuestsListFallback(quests); | |
| // After quests, load content blocks | |
| loadContentBlocks(); | |
| }) | |
| .catch(err => { | |
| console.error('❌ Failed to load CMS quests:', err); | |
| // Even if quests fail, try to load content blocks | |
| loadContentBlocks(); | |
| }); | |
| } | |
| // Load content blocks (title/subtitle, wallet button text/link, images) | |
| function loadContentBlocks(){ | |
| fetch('/admin/content_api.php') | |
| .then(r=>r.json()) | |
| .then(data=>{ | |
| const c = (data && data.content) ? data.content : {}; | |
| // Title / subtitle | |
| try { | |
| const titleLabel = document.querySelector('.my-bloxid-text'); | |
| if (titleLabel && c.bloxid_title) titleLabel.textContent = c.bloxid_title; | |
| } catch(e) {} | |
| try { | |
| const subtitleEls = document.querySelectorAll('#passportTapLevelNumberMobile, #passportTapLevelNumberDesktop, #passportLevelNumber'); | |
| if (c.bloxid_subtitle) { | |
| // If subtitle provided, set it in the small profile area under user name | |
| const userInfo = document.querySelector('.font-semibold.text-xs + .text-xs.text-gray-300'); | |
| if (userInfo) userInfo.textContent = c.bloxid_subtitle; | |
| } | |
| } catch(e) {} | |
| // Wallet button | |
| try { | |
| const btnText = document.getElementById('walletBtnText'); | |
| if (btnText && c.wallet_button_text) btnText.textContent = c.wallet_button_text; | |
| const btn = document.getElementById('walletConnectBtn'); | |
| if (btn && c.wallet_button_link) { | |
| btn.onclick = function(){ window.open(c.wallet_button_link, '_blank'); }; | |
| } | |
| } catch(e) {} | |
| // Featured images | |
| try { | |
| const passportImg = document.querySelector('#passportFeaturedGrid img'); | |
| if (passportImg && c.featured_image_passport) passportImg.src = c.featured_image_passport; | |
| const questbloxImg = document.querySelector('#questsFeaturedGrid img'); | |
| if (questbloxImg && c.featured_image_questblox) questbloxImg.src = c.featured_image_questblox; | |
| } catch(e) {} | |
| // QuestBlox hero: title/subtitle and featured image in featured grid (not the small logo) | |
| try { | |
| const qbHeroTitle = document.querySelector('#questsTab .card-glass.lg\\:col-span-2 h2'); | |
| if (qbHeroTitle && c.questblox_title) qbHeroTitle.textContent = c.questblox_title; | |
| const qbHeroSub = document.querySelector('#questsTab .card-glass.lg\\:col-span-2 p'); | |
| if (qbHeroSub && c.questblox_subtitle) qbHeroSub.textContent = c.questblox_subtitle; | |
| // Override first featured card image (regardless of data source) | |
| const qbFeaturedImg = document.querySelector('#questsFeaturedGrid > div:first-child img'); | |
| if (qbFeaturedImg && c.questblox_featured_image) qbFeaturedImg.src = c.questblox_featured_image; | |
| // Set hero background image behind the text box | |
| const qbHeroBox = document.querySelector('#questsTab .card-glass.lg\\:col-span-2'); | |
| if (qbHeroBox && c.questblox_featured_image) { | |
| qbHeroBox.style.backgroundImage = `url('${c.questblox_featured_image}')`; | |
| qbHeroBox.style.backgroundSize = 'cover'; | |
| qbHeroBox.style.backgroundPosition = 'center'; | |
| } | |
| } catch(e) {} | |
| // StreetBlox titles/images | |
| try { | |
| const sbLogo = document.querySelector('img[alt="StreetBlox"]'); | |
| if (sbLogo && c.streetblox_featured_image) sbLogo.src = c.streetblox_featured_image; | |
| const sbTag = document.getElementById('questMonumentSpotlightTag'); | |
| if (sbTag && c.streetblox_title) sbTag.textContent = c.streetblox_title + ' Nearby'; | |
| } catch(e) {} | |
| // BuddyBlox titles/images | |
| try { | |
| const bbLogo = document.querySelector('img[alt="BuddyBlox"]'); | |
| if (bbLogo && c.buddyblox_featured_image) bbLogo.src = c.buddyblox_featured_image; | |
| const bbHeader = document.querySelector('#friendsTab h2, #friendsTab .text-2xl.font-bold'); | |
| if (bbHeader && c.buddyblox_title) bbHeader.textContent = c.buddyblox_title; | |
| } catch(e) {} | |
| }) | |
| .catch(()=>{}); | |
| } | |
| // Render featured quests into the two grids from CMS | |
| function renderCmsFeaturedQuests(quests) { | |
| const userId = (window.currentUser && currentUser.id) ? String(currentUser.id) : null; | |
| const userQuests = userId ? quests.filter(q => q.audience === 'user' && String(q.telegram_user_id||'') === userId) : []; | |
| const globalQuests = quests.filter(q => q.audience !== 'user'); | |
| const passportEl = document.getElementById('passportFeaturedGrid'); | |
| const questsEl = document.getElementById('questsFeaturedGrid'); | |
| if (!passportEl || !questsEl) return; | |
| function cardHtml(q){ | |
| const img = q.image_url || 'ProBlox_NEW.png'; | |
| const btnText = q.button_text || 'Start'; | |
| const reward = Number.isFinite(q.reward_gblx) ? q.reward_gblx : 0; | |
| const safeDesc = (q.description||'').replace(/</g,'<').replace(/>/g,'>'); | |
| return ` | |
| <div class="card-glass rounded-2xl overflow-hidden card-hover hover:border-accent/50 transition-colors"> | |
| <div class="relative h-48"> | |
| <div class="absolute inset-0" style="background-image:url('${img}');background-size:cover;background-position:center"></div> | |
| <div class="absolute inset-0 bg-gradient-to-br from-black/20 to-black/10"></div> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-bold text-lg">${q.title||'Quest'}</h3> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full bg-accent mr-1"></div> | |
| Active | |
| </div> | |
| </div> | |
| <p class="text-gray-300 text-sm mb-4">${safeDesc}</p> | |
| <div class="flex items-center justify-between gap-2"> | |
| <div class="text-xs text-secondary font-medium">Reward: ${reward} GBLX</div> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]" onclick="startQuestOrLink('${q.id||''}', '${(q.title||'Quest').replace(/'/g,"\\'")}', ${reward}, ${q.button_link ? `'${q.button_link.replace(/'/g,"\\'")}'` : 'null'})"> | |
| ${btnText} | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"><path d="M5 12h14M12 5l7 7-7 7"></path></svg> | |
| </button> | |
| <button class="text-xs font-medium px-3 py-1 bg-white/10 border border-white/20 rounded-lg" onclick="shareQuest('${(q.title||'Quest').replace(/'/g,"\\'")}', 'telegram', '${q.id||'generic'}')">Share</button> | |
| </div> | |
| </div> | |
| </div>`; | |
| } | |
| // Passport tab: show per-user first, then global (no cap) | |
| passportEl.innerHTML = ''; | |
| const passportList = [...userQuests, ...globalQuests]; | |
| passportList.forEach(q => { passportEl.insertAdjacentHTML('beforeend', cardHtml(q)); }); | |
| // QuestBlox tab: show all global quests (no cap) | |
| questsEl.innerHTML = ''; | |
| globalQuests.forEach(q => { questsEl.insertAdjacentHTML('beforeend', cardHtml(q)); }); | |
| // Also map the 4 QuestBlox highlight cards (top-right + three below) to the first 4 published global quests | |
| try { | |
| // Grab the first two grids inside QuestBlox container | |
| const grids = document.querySelectorAll('#questsTab .max-w-7xl .grid'); | |
| const topRow = grids && grids.length > 0 ? grids[0] : null; | |
| const lowerRow = grids && grids.length > 1 ? grids[1] : null; | |
| const topRightCard = topRow ? topRow.querySelectorAll(':scope > div')[1] : null; | |
| const lowerCards = lowerRow ? lowerRow.querySelectorAll(':scope > div') : []; | |
| function highlightCardHtml(q){ | |
| const img = q.image_url || 'ProBlox_NEW.png'; | |
| const btnText = q.button_text || 'Start'; | |
| const reward = Number.isFinite(q.reward_gblx) ? q.reward_gblx : 0; | |
| const safeDesc = (q.description||'').replace(/</g,'<').replace(/>/g,'>'); | |
| const onclick = q.button_link ? `window.open('${q.button_link}','_blank')` : `startQuest('${q.id||''}', '${(q.title||'Quest').replace(/'/g,"\\'")}', ${reward})`; | |
| return ` | |
| <div class="hero-slide relative h-full"> | |
| <div class="absolute inset-0" style="background-image:url('${img}');background-size:cover;background-position:center"></div> | |
| <div class="h-full relative bg-gradient-to-br from-black/30 to-black/10 p-4 flex flex-col justify-end"> | |
| <div> | |
| <div class="inline-block px-1.5 py-0.5 bg-muted rounded-full text-xs font-medium">Featured</div> | |
| <h3 class="text-lg font-bold mt-1">${q.title||'Quest'}</h3> | |
| <p class="text-gray-300 text-xs mt-1">${safeDesc}</p> | |
| <div class="flex items-center justify-between mt-2"> | |
| <div class="text-xs text-secondary font-medium">Reward: ${reward} GBLX</div> | |
| <button class="text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-[#22232e]" onclick="${onclick}"> | |
| ${btnText} | |
| <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" class=\"ml-1\"><path d=\"M5 12h14M12 5l7 7-7 7\"></path></svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div>`; | |
| } | |
| // Use the most recent 4 global quests (end of array) | |
| const featured = globalQuests.slice(-4); | |
| const lastIdx = featured.length - 1; | |
| if (topRightCard && lastIdx >= 0) { | |
| topRightCard.innerHTML = highlightCardHtml(featured[lastIdx]); | |
| } | |
| const rest = featured.slice(0, Math.max(0, lastIdx)); | |
| for (let i=0; i<lowerCards.length && i<rest.length; i++) { | |
| lowerCards[i].innerHTML = highlightCardHtml(rest[i]); | |
| } | |
| } catch(e) { | |
| console.warn('Highlight mapping skipped:', e); | |
| } | |
| } | |
| // Display quests list in QuestBlox section (fallback list renderer) | |
| function displayQuestsListFallback(quests) { | |
| console.log('🎮 Available quests:', quests); | |
| // Find the quest container | |
| const questContainer = document.querySelector('#questblox .space-y-4'); | |
| if (!questContainer) { | |
| console.log('❌ Quest container not found'); | |
| return; | |
| } | |
| // Clear existing quests (keep the first static quest for demo) | |
| const staticQuests = questContainer.children.length; | |
| console.log('📋 Found', staticQuests, 'static quests, adding', quests.length, 'dynamic quests'); | |
| // Add dynamic quests | |
| quests.forEach((quest, index) => { | |
| const questElement = document.createElement('div'); | |
| questElement.className = 'bg-card rounded-xl p-4 border border-muted'; | |
| questElement.innerHTML = ` | |
| <div class="flex items-start justify-between mb-3"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="w-10 h-10 rounded-full bg-gradient-to-r from-accent to-secondary flex items-center justify-center"> | |
| <span class="text-sm font-bold">${(quest.title||'Q').charAt(0)}</span> | |
| </div> | |
| <div> | |
| <h3 class="font-semibold text-sm">${quest.title||'Quest'}</h3> | |
| <div class="flex items-center space-x-2 mt-1"> | |
| <span class="text-secondary text-xs font-medium">${Number.isFinite(quest.reward_gblx)?quest.reward_gblx:(quest.reward_amount||0)} GBLX</span> | |
| <div class="px-2 py-1 bg-muted rounded-full text-xs flex items-center"> | |
| <div class="w-2 h-2 rounded-full ${quest.status === 'completed' ? 'bg-green-500' : 'bg-accent'} mr-1"></div> | |
| ${quest.status === 'completed' ? 'Completed' : 'Active'} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <p class="text-gray-400 text-sm mb-4"> | |
| ${(quest.description||'')} | |
| </p> | |
| <button onclick="startQuest('${quest.id||''}', '${(quest.title||'Quest').replace(/'/g,"\'")}')" | |
| class="quest-button text-xs font-medium px-3 py-1 bg-muted rounded-lg flex items-center transition-colors hover:bg-accent hover:text-white ${quest.status === 'completed' ? 'opacity-50 cursor-not-allowed' : ''}"> | |
| ${quest.status === 'completed' ? 'Completed' : 'Start Quest'} | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-1"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| </button> | |
| `; | |
| questContainer.appendChild(questElement); | |
| }); | |
| console.log('✅ Quests displayed successfully'); | |
| } | |
| // Quest interaction function | |
| function startQuest(questId, questTitle, rewardOverride) { | |
| console.log('🎯 Starting quest:', questId, questTitle, 'reward:', rewardOverride); | |
| // Show quest started notification with countdown | |
| showNotification(`🎮 Quest "${questTitle}" started! Auto-completing in 3 seconds...`, 'success'); | |
| // Disable the button temporarily | |
| const button = event.target; | |
| const originalText = button.innerHTML; | |
| button.innerHTML = 'Processing... 3'; | |
| button.disabled = true; | |
| let countdown = 3; | |
| const countdownInterval = setInterval(() => { | |
| countdown--; | |
| if (countdown > 0) { | |
| button.innerHTML = `Processing... ${countdown}`; | |
| } else { | |
| clearInterval(countdownInterval); | |
| button.innerHTML = 'Completing...'; | |
| completeQuest(questId, questTitle, rewardOverride); | |
| button.innerHTML = '✅ Completed'; | |
| button.style.backgroundColor = '#10b981'; | |
| } | |
| }, 1000); | |
| } | |
| // Start quest or open link, but still reward | |
| function startQuestOrLink(questId, questTitle, rewardOverride, link){ | |
| if (link) { | |
| try { window.open(link, '_blank'); } catch(e) {} | |
| // also award immediately to avoid losing credit | |
| completeQuest(questId, questTitle, rewardOverride); | |
| } else { | |
| startQuest(questId, questTitle, rewardOverride); | |
| } | |
| } | |
| // Complete quest function | |
| async function completeQuest(questId, questTitle, rewardOverride) { | |
| console.log('🏆 Completing quest:', questId, questTitle, 'reward:', rewardOverride); | |
| // Award GBLX | |
| const reward = Number.isFinite(rewardOverride) ? rewardOverride : 100; | |
| balance += reward; | |
| // Save to localStorage immediately | |
| localStorage.setItem('bloxid_unified_balance', balance); | |
| localStorage.setItem('bloxid_unified_score', score); | |
| console.log('💾 Quest reward saved - Score:', score, 'Balance:', balance); | |
| // Update displays | |
| updateAllDisplays(); | |
| // Show completion notification | |
| showNotification(`🏆 Quest "${questTitle}" completed! +${reward} GBLX earned!`, 'success'); | |
| console.log('✅ Quest completed, data saved to localStorage'); | |
| // Persist completion server-side (simple file-based progress) | |
| try { | |
| const tid = (window.currentUser && currentUser.id) ? String(currentUser.id) : 'guest'; | |
| await fetch('/api/quests_progress.php', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ action: 'complete', tid, quest_id: questId }) | |
| }); | |
| } catch (e) { | |
| console.warn('progress save failed', e); | |
| } | |
| } | |
| // Simple localStorage sync - No server complexity | |
| function syncWithServer() { | |
| console.log('🔄 Simple localStorage sync...'); | |
| // Save to localStorage | |
| localStorage.setItem('bloxid_unified_score', score); | |
| localStorage.setItem('bloxid_unified_balance', balance); | |
| localStorage.setItem('bloxid_last_sync', Date.now()); | |
| console.log('✅ Data synced to localStorage - Score:', score, 'Balance:', balance); | |
| } | |
| // Simple save function - localStorage only | |
| function saveToServer() { | |
| console.log('💾 Simple localStorage save...'); | |
| localStorage.setItem('bloxid_unified_score', score); | |
| localStorage.setItem('bloxid_unified_balance', balance); | |
| console.log('✅ Data saved to localStorage'); | |
| } | |
| // Ensure ProBlox badge if wallet is verified (helper) | |
| function enforceVerifiedProbloxBadge() { | |
| try { | |
| const wasVerified = localStorage.getItem('telegram_wallet_verified') === 'true'; | |
| if (wasVerified && !userBadges.includes('problox')) { | |
| userBadges.push('problox'); | |
| try { localStorage.setItem('bloxid_user_badges', JSON.stringify(userBadges)); } catch(e) {} | |
| updateBadgeDisplay(); | |
| } | |
| } catch (e) {} | |
| } | |
| // Quest sharing system for promotion (popup-based, non-blocking + resilient reward) | |
| function shareQuest(questTitle, platform = 'telegram', questId = 'generic') { | |
| const tid = (window.currentUser && currentUser.id) ? String(currentUser.id) : ''; | |
| const shareText = `🎮 Join me in "${questTitle}" on BloxID! Earn GBLX and complete amazing quests!`; | |
| const shareUrl = `https://t.me/bloxid_bot?start=quest_${tid || 'demo'}`; | |
| const tgShare = `https://t.me/share/url?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(shareText)}`; | |
| // Always open share in a separate window to avoid blocking this page | |
| const w = window.open(tgShare, '_blank', 'noopener,noreferrer,width=600,height=800'); | |
| let closedPoll = null; | |
| let rewarded = false; | |
| const sendReward = () => { | |
| if (rewarded) return; | |
| rewarded = true; | |
| if (tid) { | |
| fetch('/api/shares.php', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ telegram_user_id: tid, quest_id: String(questId || 'generic'), medium: platform }) | |
| }).then(r => r.json()).then(j => { | |
| if (j && j.ok && typeof j.reward_gblx === 'number') { | |
| balance += j.reward_gblx; | |
| updateAllDisplays(); | |
| console.log('SHARE_OK server', j.reward_gblx); | |
| showNotification(`🎉 +${j.reward_gblx} GBLX for sharing!`, 'success'); | |
| } else { | |
| balance += 27; | |
| updateAllDisplays(); | |
| console.log('SHARE_OK fallback', 27); | |
| showNotification('🎉 +27 GBLX for sharing!', 'success'); | |
| } | |
| }).catch(() => { | |
| balance += 27; | |
| updateAllDisplays(); | |
| console.log('SHARE_OK fallback', 27); | |
| showNotification('🎉 +27 GBLX for sharing!', 'success'); | |
| }); | |
| } else { | |
| balance += 27; | |
| updateAllDisplays(); | |
| console.log('SHARE_OK fallback-noTID', 27); | |
| showNotification('🎉 +27 GBLX for sharing!', 'success'); | |
| } | |
| }; | |
| // Reward after popup closes or after timeout | |
| const timeoutId = setTimeout(sendReward, 6000); | |
| try { | |
| if (w && typeof w.closed !== 'undefined') { | |
| closedPoll = setInterval(() => { | |
| if (w.closed) { | |
| clearInterval(closedPoll); | |
| clearTimeout(timeoutId); | |
| sendReward(); | |
| } | |
| }, 500); | |
| } else { | |
| console.log('Share popup blocked or unavailable; relying on timeout reward.'); | |
| } | |
| } catch (e) { | |
| console.log('Share popup check error:', e.message); | |
| } | |
| console.log('📤 Share started (popup), quest:', questTitle, 'id:', questId); | |
| } | |
| // Verify TonConnect session with server (issue + verify) | |
| async function verifyTonConnectionWithServer() { | |
| try { | |
| // Use explicit files to avoid PATH_INFO 404s on some hosts | |
| let issueRes = await fetch('/api/tonconnect/issue.php', { credentials: 'include' }); | |
| if (!issueRes.ok) throw new Error('Issue request failed'); | |
| const issueData = await issueRes.json(); | |
| if (!issueData || !issueData.ok || !issueData.challenge) throw new Error('Invalid issue response'); | |
| let verifyRes = await fetch('/api/tonconnect/verify.php', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| credentials: 'include', | |
| body: JSON.stringify({ challenge: issueData.challenge }) | |
| }); | |
| if (!verifyRes.ok) throw new Error('Verify request failed'); | |
| const verifyData = await verifyRes.json(); | |
| if (verifyData && verifyData.ok && verifyData.verified) { | |
| try { localStorage.setItem('telegram_wallet_verified', 'true'); } catch (e) {} | |
| return true; | |
| } | |
| return false; | |
| } catch (err) { | |
| console.log('TON server verify error:', err.message); | |
| return false; | |
| } | |
| } | |
| // Function to connect/disconnect Telegram wallet (toggle) | |
| function connectTelegramWallet() { | |
| const connectBtn = document.getElementById('walletConnectBtn'); | |
| const buttonText = document.getElementById('walletBtnText'); | |
| const isConnected = localStorage.getItem('telegram_wallet_connected') === 'true'; | |
| if (isConnected) { | |
| // DISCONNECT FLOW | |
| // Remove badge | |
| const idx = userBadges.indexOf('problox'); | |
| if (idx !== -1) { | |
| userBadges.splice(idx, 1); | |
| updateBadgeDisplay(); | |
| try { localStorage.setItem('bloxid_user_badges', JSON.stringify(userBadges)); } catch(e) {} | |
| } | |
| // Revert aura if previously rewarded | |
| if (localStorage.getItem('telegram_wallet_rewarded') === 'true') { | |
| balance = Math.max(0, balance - 150); | |
| updateAllDisplays(); | |
| localStorage.removeItem('telegram_wallet_rewarded'); | |
| } | |
| // Update state | |
| localStorage.removeItem('telegram_wallet_connected'); | |
| localStorage.removeItem('telegram_wallet_verified'); | |
| // Update button to grey "Connect" | |
| if (connectBtn) { | |
| connectBtn.disabled = false; | |
| connectBtn.className = 'card-glass rounded-xl px-4 py-2 w-full border text-white font-semibold text-xs flex items-center justify-center space-x-2 bg-gray-600 hover:bg-gray-700 border-white/20'; | |
| } | |
| if (buttonText) buttonText.textContent = 'Connect'; | |
| showNotification('🔌 Disconnected from wallet', 'success'); | |
| return; | |
| } | |
| // CONNECT FLOW via TonConnect | |
| if (connectBtn) { | |
| connectBtn.disabled = true; | |
| if (buttonText) buttonText.textContent = 'Connecting...'; | |
| } | |
| const doSuccess = async () => { | |
| // Verify with server before granting rewards/state | |
| const verified = await verifyTonConnectionWithServer(); | |
| if (!verified) { | |
| if (connectBtn) connectBtn.disabled = false; | |
| if (buttonText) buttonText.textContent = 'Connect'; | |
| showNotification('⚠️ Wallet connection could not be verified. Please try again.', 'error'); | |
| return; | |
| } | |
| // Grant badge | |
| giveProBloxBadge(); | |
| // Capture wallet address from TonConnect (if available) and link server-side | |
| try { | |
| if (typeof tonConnectUI !== 'undefined' && tonConnectUI?.connector?.wallet) { | |
| const account = tonConnectUI.connector.wallet?.account; | |
| const walletAddress = account?.address || account?.publicKey || null; | |
| if (walletAddress) { | |
| const telegramUserId = (window.currentUser && currentUser.id) || null; | |
| fetch('/api/tonconnect/link.php', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| credentials: 'include', | |
| body: JSON.stringify({ | |
| telegram_user_id: telegramUserId, | |
| wallet_address: walletAddress, | |
| chain: 'ton' | |
| }) | |
| }).catch(() => {}); | |
| } | |
| } | |
| } catch (e) { | |
| console.log('Wallet link skip:', e.message); | |
| } | |
| // Reward aura only once | |
| if (localStorage.getItem('telegram_wallet_rewarded') !== 'true') { | |
| balance += 150; // reward in GBLX | |
| updateAllDisplays(); | |
| localStorage.setItem('telegram_wallet_rewarded', 'true'); | |
| } | |
| // Persist connection | |
| localStorage.setItem('telegram_wallet_connected', 'true'); | |
| localStorage.setItem('telegram_wallet_verified', 'true'); | |
| // Update button to green "Connected!" | |
| if (connectBtn) { | |
| connectBtn.disabled = false; | |
| connectBtn.className = 'card-glass rounded-xl px-4 py-2 w-full border text-white font-semibold text-xs flex items-center justify-center space-x-2 bg-green-600 hover:bg-green-700 border-green-400/50'; | |
| } | |
| if (buttonText) buttonText.textContent = 'Connected!'; | |
| showNotification('✅ Telegram wallet connected successfully!', 'success'); | |
| }; | |
| try { | |
| if (tonConnectUI) { | |
| tonConnectUI.openModal(); | |
| tonConnectUI.onStatusChange((w) => { | |
| if (w) doSuccess(); | |
| }); | |
| return; | |
| } | |
| } catch (e) { | |
| console.log('TonConnect not available, fallback:', e.message); | |
| } | |
| // Fallback: simulate success | |
| setTimeout(() => { doSuccess(); }, 1200); | |
| } | |
| // Function to give ProBlox badge | |
| function giveProBloxBadge() { | |
| if (!userBadges.includes('problox')) { | |
| userBadges.push('problox'); | |
| updateBadgeDisplay(); | |
| showNotification('🏆 ProBlox Badge Earned!', 'success'); | |
| // Persist locally as fallback when server data is unavailable | |
| try { localStorage.setItem('bloxid_user_badges', JSON.stringify(userBadges)); } catch(e) {} | |
| // Save badges to server | |
| saveBadgesToServer(); | |
| } | |
| } | |
| // Add Friend Quest function | |
| function startAddFriendQuest() { | |
| const tid = prompt('Enter your friend\'s Telegram ID (numbers only):'); | |
| if (!tid || !/^\d{5,}$/.test(tid)) { | |
| showNotification('Please enter a valid Telegram ID (numbers only)', 'error'); | |
| return; | |
| } | |
| addBuddy(tid, null).then(() => { | |
| // Quest completion reward | |
| balance += 25; | |
| updateAllDisplays(); | |
| showNotification('🎉 Quest completed! +25 GBLX for adding a friend!', 'success'); | |
| console.log('FRIEND_QUEST_OK: +25 GBLX reward'); | |
| }).catch(() => { | |
| showNotification('Quest failed - could not add friend', 'error'); | |
| console.log('FRIEND_QUEST_FAIL: Could not add friend'); | |
| }); | |
| } | |
| // Function to show notifications | |
| function showNotification(message, type = 'info') { | |
| // Create notification element | |
| const notification = document.createElement('div'); | |
| notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg transition-all duration-300 transform translate-x-full`; | |
| // Set background color based on type | |
| if (type === 'success') { | |
| notification.classList.add('bg-green-500', 'text-white'); | |
| } else if (type === 'error') { | |
| notification.classList.add('bg-red-500', 'text-white'); | |
| } else { | |
| notification.classList.add('bg-blue-500', 'text-white'); | |
| } | |
| notification.textContent = message; | |
| document.body.appendChild(notification); | |
| // Animate in | |
| setTimeout(() => { | |
| notification.classList.remove('translate-x-full'); | |
| }, 100); | |
| // Remove after 3 seconds | |
| setTimeout(() => { | |
| notification.classList.add('translate-x-full'); | |
| setTimeout(() => { | |
| if (notification.parentNode) { | |
| notification.parentNode.removeChild(notification); | |
| } | |
| }, 300); | |
| }, 3000); | |
| } | |
| </script> | |
| <script src="https://unpkg.com/@tonconnect/ui@latest/dist/tonconnect-ui.min.js"></script> | |
| <script> | |
| // Initialize TonConnect UI (safe to load even if offline) | |
| let tonConnectUI; | |
| (function initTonConnect() { | |
| try { | |
| tonConnectUI = new TonConnectUI.TonConnectUI({ | |
| manifestUrl: 'https://bloxid.app/.well-known/tonconnect-manifest.json' | |
| }); | |
| } catch (e) { | |
| console.log('TonConnect UI not available yet:', e.message); | |
| } | |
| })(); | |
| </script> | |
| <!-- Static desktop-only Thirdweb connect (high z-index) --> | |
| <button id="twConnectBtnStatic" onclick="thirdwebLoginShim()" | |
| style="position:fixed;right:16px;bottom:80px;z-index:999999;padding:10px 14px;border-radius:10px;border:1px solid rgba(255,255,255,0.2);background:linear-gradient(135deg,#6366f1,#06b6d4);color:#fff;cursor:pointer;display:none;"> | |
| Connect with Thirdweb | |
| </button> | |
| <script> | |
| // Show static button on non-Telegram desktop only | |
| (function(){ | |
| try { | |
| const ua = navigator.userAgent || ''; | |
| const isRealTG = /Telegram/i.test(ua); | |
| const isDesktop = (typeof window !== 'undefined') && window.matchMedia && window.matchMedia('(min-width: 900px)').matches; | |
| const btn = document.getElementById('twConnectBtnStatic'); | |
| if (btn && !isRealTG && isDesktop) { btn.style.display = 'block'; } | |
| } catch(e) {} | |
| })(); | |
| </script> | |
| <script> | |
| // Fallback creator: ensure button exists and is visible on desktop non-Telegram | |
| (function(){ | |
| try { | |
| const ua = navigator.userAgent || ''; | |
| const isRealTG = /Telegram/i.test(ua); | |
| const isDesktop = (typeof window !== 'undefined') && window.matchMedia && window.matchMedia('(min-width: 900px)').matches; | |
| if (isRealTG || !isDesktop) return; | |
| let btn = document.getElementById('twConnectBtnStatic'); | |
| if (!btn) { | |
| btn = document.createElement('button'); | |
| btn.id = 'twConnectBtnStatic'; | |
| btn.textContent = 'Connect with Thirdweb'; | |
| btn.onclick = (typeof thirdwebLoginShim === 'function' ? thirdwebLoginShim : function(){ alert('Thirdweb not ready'); }); | |
| const s = btn.style; s.position='fixed'; s.right='16px'; s.bottom='80px'; s.zIndex='999999'; s.padding='10px 14px'; s.borderRadius='10px'; s.border='1px solid rgba(255,255,255,0.2)'; s.background='linear-gradient(135deg,#6366f1,#06b6d4)'; s.color='#fff'; s.cursor='pointer'; | |
| document.body.appendChild(btn); | |
| } | |
| btn.style.display = 'block'; | |
| } catch(e) {} | |
| })(); | |
| </script> | |
| <script src="/api/_inject_tw.js?v=1"></script> | |
| </body> | |
| <!-- build-tag: v-raven-buddy-3 --> | |
| </html> | |