/* ─── 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 += `
`;
html += `${label}`;
if (hasChildren) {
html += `▾`;
}
html += ``;
// Render dropdown submenu
if (hasChildren) {
html += ``;
children.forEach(child => {
const childUrl = child.url || '#';
const childLabel = (child.label || '').toUpperCase();
// Check child active state
let childActive = false;
if (childUrl === fullPath) {
childActive = true;
} else if (childUrl.startsWith('/#') && path === '/') {
childActive = false; // anchor links - don't mark active
}
const childActiveClass = childActive ? ' active' : '';
// Handle anchor links from non-homepage
const childIsAnchor = childUrl.startsWith('#') || childUrl.startsWith('/#');
const childHref = childIsAnchor && path !== '/' ? '/' + childUrl.replace(/^\//, '') : childUrl;
const childDesc = child.description || '';
html += `- ${childLabel}${childDesc ? `${childDesc}` : ''}
`;
});
html += `
`;
}
html += `\n`;
});
navMenu.innerHTML = html;
// Bind mobile dropdown toggles after rendering
initMobileDropdowns();
// Inject shared wrapper for SOL price + wallet, then inject both
injectSolWalletGroup();
} 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 + Wallet Group Wrapper ────────────────────────────
function injectSolWalletGroup() {
const navContainer = document.querySelector('.nav-container');
const navStatus = document.querySelector('.nav-status');
if (!navContainer) return;
// Don't inject twice
if (document.getElementById('navSolWalletGroup')) return;
// Create shared wrapper
const group = document.createElement('div');
group.id = 'navSolWalletGroup';
group.className = 'nav-sol-wallet-group';
// Insert before nav-status
if (navStatus) {
navContainer.insertBefore(group, navStatus);
} else {
navContainer.appendChild(group);
}
// Now inject SOL price and wallet into the group
injectSolPrice(group);
injectWalletButton(group);
}
// ─── SOL Price Ticker ──────────────────────────────────────
function injectSolPrice(parent) {
if (!parent || document.getElementById('navSolPrice')) return;
const wrap = document.createElement('div');
wrap.id = 'navSolPrice';
wrap.className = 'nav-sol-price';
wrap.innerHTML = `
◎
SOL
--
`;
parent.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(parent) {
if (!parent) 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 = `
`;
parent.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'}
`;
// 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();
}
})();