/* ─── Dynamic Navigation with Dropdown Submenus ─── */ /* Fetches nav items from /api/navigation and renders dropdowns */ /* Includes sitewide wallet connect button via window.solWallet */ (function() { 'use strict'; async function loadNavigation() { const navMenu = document.getElementById('navMenu'); if (!navMenu) return; try { const res = await fetch('/api/navigation'); if (!res.ok) throw new Error('Nav API ' + res.status); const items = await res.json(); // Sort by order items.sort((a, b) => (a.order || 0) - (b.order || 0)); // Determine current page for active state const path = window.location.pathname; const search = window.location.search; const fullPath = path + search; // Build nav HTML let html = ''; items.forEach(item => { const url = item.url || '/'; const label = (item.label || '').toUpperCase(); const children = item.children || []; const hasChildren = children.length > 0; // Determine if this link is active let isActive = false; if (url === '/' && (path === '/' || path === '/index.html')) { isActive = true; } else if (url !== '/' && path.startsWith(url.split('?')[0])) { isActive = true; } const activeClass = isActive ? ' active' : ''; const hasDropdownClass = hasChildren ? ' has-dropdown' : ''; // Check if it's an anchor link (homepage sections) const isAnchor = url.startsWith('#') || url.startsWith('/#'); const href = isAnchor && path !== '/' ? '/' + url.replace(/^\//, '') : url; html += `\n`; }); navMenu.innerHTML = html; // Bind mobile dropdown toggles after rendering initMobileDropdowns(); // Inject SOL price ticker and wallet button after nav loads injectSolPrice(); injectWalletButton(); } catch (err) { console.warn('Nav load failed, keeping existing:', err); } } // Mobile: tap parent to toggle dropdown function initMobileDropdowns() { const isMobile = () => window.innerWidth <= 768; document.querySelectorAll('.nav-item.has-dropdown > .nav-link').forEach(link => { link.addEventListener('click', function(e) { if (!isMobile()) return; // desktop uses hover const parent = this.parentElement; const dropdown = parent.querySelector('.dropdown'); if (!dropdown) return; // If dropdown is closed, prevent navigation and open it if (!parent.classList.contains('dropdown-open')) { e.preventDefault(); // Close all other dropdowns document.querySelectorAll('.nav-item.dropdown-open').forEach(item => { if (item !== parent) item.classList.remove('dropdown-open'); }); parent.classList.add('dropdown-open'); } else { // Dropdown already open — allow navigation or close e.preventDefault(); parent.classList.remove('dropdown-open'); } }); }); // Close dropdowns when tapping outside document.addEventListener('click', function(e) { if (!isMobile()) return; if (!e.target.closest('.nav-item.has-dropdown')) { document.querySelectorAll('.nav-item.dropdown-open').forEach(item => { item.classList.remove('dropdown-open'); }); } }); } // Nav toggle (hamburger menu) function initNavToggle() { const toggle = document.getElementById('navToggle'); const menu = document.getElementById('navMenu'); if (toggle && menu) { toggle.addEventListener('click', () => { menu.classList.toggle('open'); toggle.classList.toggle('active'); // Close any open dropdowns when closing menu if (!menu.classList.contains('open')) { document.querySelectorAll('.nav-item.dropdown-open').forEach(item => { item.classList.remove('dropdown-open'); }); } }); } } // ─── SOL Price Ticker ────────────────────────────────────── function injectSolPrice() { const navContainer = document.querySelector('.nav-container'); if (!navContainer || document.getElementById('navSolPrice')) return; const wrap = document.createElement('div'); wrap.id = 'navSolPrice'; wrap.className = 'nav-sol-price'; wrap.innerHTML = ` SOL -- `; // Insert before wallet wrap or nav-status const walletWrap = document.getElementById('navWalletWrap'); const navStatus = document.querySelector('.nav-status'); const insertBefore = walletWrap || navStatus; if (insertBefore) { navContainer.insertBefore(wrap, insertBefore); } else { navContainer.appendChild(wrap); } fetchSolPrice(); setInterval(fetchSolPrice, 30000); } async function fetchSolPrice() { const priceEl = document.getElementById('solPriceValue'); const changeEl = document.getElementById('solPriceChange'); if (!priceEl || !changeEl) return; try { // Try Binance first (reliable CORS) const res = await fetch('https://api.binance.com/api/v3/ticker/24hr?symbol=SOLUSDT'); if (!res.ok) throw new Error(res.status); const data = await res.json(); const price = parseFloat(data.lastPrice); const change = parseFloat(data.priceChangePercent); priceEl.textContent = '$' + price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const sign = change >= 0 ? '+' : ''; const arrow = change >= 0 ? '▲' : '▼'; changeEl.textContent = `${arrow} ${sign}${change.toFixed(1)}%`; changeEl.className = 'sol-price-change ' + (change >= 0 ? 'up' : 'down'); } catch (e) { // Fallback to CoinGecko try { const res2 = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd&include_24hr_change=true'); if (!res2.ok) throw new Error(res2.status); const data2 = await res2.json(); const price = data2.solana.usd; const change = data2.solana.usd_24h_change; priceEl.textContent = '$' + price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const sign = change >= 0 ? '+' : ''; const arrow = change >= 0 ? '▲' : '▼'; changeEl.textContent = `${arrow} ${sign}${change.toFixed(1)}%`; changeEl.className = 'sol-price-change ' + (change >= 0 ? 'up' : 'down'); } catch (e2) { console.warn('SOL price fetch failed:', e, e2); } } } // ─── Wallet Button Injection ──────────────────────────────── function injectWalletButton() { const navContainer = document.querySelector('.nav-container'); const navStatus = document.querySelector('.nav-status'); if (!navContainer) return; // Don't inject twice if (document.getElementById('navWalletWrap')) return; // Create wallet wrapper const wrap = document.createElement('div'); wrap.id = 'navWalletWrap'; wrap.className = 'nav-wallet-wrap'; wrap.innerHTML = ` `; // Insert before nav-status (or append to container) if (navStatus) { navContainer.insertBefore(wrap, navStatus); } else { navContainer.appendChild(wrap); } // Bind click const btn = document.getElementById('navWalletBtn'); btn.addEventListener('click', (e) => { e.stopPropagation(); toggleWalletDropdown(); }); // Close dropdown on outside click document.addEventListener('click', (e) => { if (!e.target.closest('#navWalletWrap')) { closeWalletDropdown(); } }); // Listen for wallet events window.addEventListener('wallet-connected', () => updateNavWalletUI()); window.addEventListener('wallet-disconnected', () => updateNavWalletUI()); // Set initial state updateNavWalletUI(); } function updateNavWalletUI() { const btn = document.getElementById('navWalletBtn'); if (!btn) return; const sw = window.solWallet; if (sw && sw.connected && sw.address) { btn.className = 'nav-wallet-btn connected'; btn.innerHTML = ` ${sw.truncAddr(sw.address)} `; } else { btn.className = 'nav-wallet-btn'; btn.innerHTML = ` CONNECT `; } // Close dropdown on state change closeWalletDropdown(); } function toggleWalletDropdown() { const dd = document.getElementById('navWalletDropdown'); if (!dd) return; if (dd.classList.contains('hidden')) { openWalletDropdown(); } else { closeWalletDropdown(); } } function openWalletDropdown() { const dd = document.getElementById('navWalletDropdown'); if (!dd) return; const sw = window.solWallet; if (!sw) { dd.innerHTML = '
WALLET MODULE NOT LOADED
'; dd.classList.remove('hidden'); return; } if (sw.connected && sw.address) { // ── Connected dropdown: address, copy, solscan, disconnect ── const addr = sw.address; const solscan = 'https://solscan.io/account/' + addr; dd.innerHTML = `
CONNECTED WALLET
${addr}
VIA ${sw.walletName || 'WALLET'}
SOLSCAN
`; // Copy address dd.querySelector('#nwCopyAddr').addEventListener('click', () => { navigator.clipboard.writeText(addr).then(() => { const el = dd.querySelector('#nwCopyAddr .nw-dd-copy-icon'); if (el) { el.textContent = '✓'; setTimeout(() => el.textContent = '⧉', 1500); } }); }); // Disconnect dd.querySelector('#nwDisconnect').addEventListener('click', () => { sw.disconnect(); }); } else { // ── Disconnected dropdown: wallet picker ── // Short delay for wallet detection setTimeout(() => { const available = sw.getAvailableWallets(); const detectedNames = new Set(available.map(w => w.name)); let html = ''; if (available.length > 0) { html += '
SELECT WALLET
'; html += available.map(w => ` `).join(''); } // Show install links for missing wallets const missing = sw.KNOWN_WALLETS.filter(w => !detectedNames.has(w.name)); if (missing.length > 0 && available.length < 3) { html += '
'; html += '
INSTALL
'; html += missing.slice(0, 3).map(w => ` ${w.icon} ${w.name} INSTALL ↗ `).join(''); } if (available.length === 0) { html = `
NO WALLETS DETECTED
Install a Solana wallet extension
`; // Still show install links html += sw.KNOWN_WALLETS.slice(0, 4).map(w => ` ${w.icon} ${w.name} INSTALL ↗ `).join(''); } dd.innerHTML = html; // Bind detected wallet clicks dd.querySelectorAll('.nw-dd-wallet[data-wallet]').forEach(el => { el.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); const name = el.dataset.wallet; const wallet = available.find(w => w.name === name); if (!wallet) return; // Show connecting state el.querySelector('.nw-dd-wallet-tag').textContent = 'CONNECTING...'; try { await sw.connect(wallet); } catch (err) { el.querySelector('.nw-dd-wallet-tag').textContent = 'REJECTED'; el.querySelector('.nw-dd-wallet-tag').classList.add('nw-dd-error'); setTimeout(() => { if (el.querySelector('.nw-dd-wallet-tag')) { el.querySelector('.nw-dd-wallet-tag').textContent = 'DETECTED'; el.querySelector('.nw-dd-wallet-tag').classList.remove('nw-dd-error'); } }, 2000); } }); }); }, 150); } dd.classList.remove('hidden'); } function closeWalletDropdown() { const dd = document.getElementById('navWalletDropdown'); if (dd) dd.classList.add('hidden'); } // Run on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { loadNavigation(); initNavToggle(); }); } else { loadNavigation(); initNavToggle(); } })();