// Navigation function router(pageName) { const viewContainer = document.getElementById('app-view'); const template = document.getElementById(`view-${pageName}`); // Update Nav State document.querySelectorAll('.nav-item').forEach(item => { if (item.dataset.page === pageName) item.classList.add('active'); else item.classList.remove('active'); }); // Swap View if (template) { viewContainer.innerHTML = ''; viewContainer.appendChild(template.content.cloneNode(true)); } // Init Page Logic if (pageName === 'dashboard') loadDashboard(); if (pageName === 'shop') loadShop(); if (pageName === 'subscription') loadSubscription(); if (pageName === 'profile') loadProfile(); // Lucide Icons if (window.lucide) lucide.createIcons(); // Smooth Scroll Top window.scrollTo({ top: 0, behavior: 'smooth' }); } // Global State const API_BASE = '/api'; let currentState = { user: null, subUrl: "" }; // Telegram Init const tg = window.Telegram?.WebApp; if (tg) { tg.ready(); tg.expand(); // Wrap in try-catch for header color as it might fail in some versions try { tg.setHeaderColor('#0f172a'); } catch (e) { } currentState.user = tg.initDataUnsafe?.user; } // Dev Mock if (!currentState.user) { currentState.user = { id: 123456789, first_name: 'Dev', username: 'developer' }; } // Initial UI Setup const headerAvatar = document.getElementById('header-avatar'); if (headerAvatar) { headerAvatar.textContent = (currentState.user.first_name || 'U')[0].toUpperCase(); } // ------ PAGE LOGIC ------ async function loadDashboard() { document.getElementById('user-name').textContent = currentState.user.first_name; try { const res = await fetch(`${API_BASE}/user/${currentState.user.id}`); const data = await res.json(); if (data.error) throw new Error(data.error); // Update Text document.getElementById('dash-status').textContent = data.status; document.getElementById('dash-limit').textContent = `${data.data_limit_gb} GB`; document.getElementById('dash-expire').textContent = data.expire_date; document.getElementById('dash-data-left').textContent = data.used_traffic_gb; // Progress Ring const circle = document.getElementById('data-ring'); if (circle) { const limit = data.data_limit_gb || 100; // avoid div by zero const used = data.used_traffic_gb || 0; const percent = Math.min((used / limit) * 100, 100); // stroke-dasharray="current, 100" (since pathLength=100 logic or simply percentage) // standard dasharray for 36 viewbox is approx 100. circle.setAttribute('stroke-dasharray', `${percent}, 100`); circle.style.stroke = percent > 90 ? '#f87171' : '#6366f1'; } // Save sub url globally currentState.subUrl = data.subscription_url; } catch (e) { console.error(e); document.getElementById('dash-status').textContent = 'Error'; } } async function loadShop() { const container = document.getElementById('plans-container'); container.innerHTML = '
'; try { const res = await fetch(`${API_BASE}/plans`); const plans = await res.json(); container.innerHTML = ''; plans.forEach(plan => { const card = document.createElement('div'); card.className = 'glass plan-card plan-item'; // plan-item for animation card.innerHTML = `
${plan.name} ${plan.price} ⭐️
${plan.data_limit} GB ${plan.days} Days
`; container.appendChild(card); }); lucide.createIcons(); } catch (e) { container.textContent = "Failed to load plans."; } } async function initPayment(planId) { if (!tg) return alert("Open in Telegram"); // Simple loader on button const btn = event.target; const oldText = btn.innerText; btn.innerText = 'Creating..'; btn.disabled = true; try { const body = { user_id: currentState.user.id, plan_id: planId }; if (currentState.promoCode) { body.promo_code = currentState.promoCode; // Reset after use or keep active? Bot usually resets. Let's keep it until refresh. } const res = await fetch(`${API_BASE}/create-invoice`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const data = await res.json(); if (data.invoice_link) { tg.openInvoice(data.invoice_link, (status) => { if (status === 'paid') { tg.showAlert('Successful Payment!'); router('dashboard'); } }); } else { tg.showAlert('Error: ' + (data.error || 'Unknown')); } } catch (e) { tg.showAlert('Network error'); } finally { btn.innerText = oldText; btn.disabled = false; } } async function loadSubscription() { const linkEl = document.getElementById('config-link'); const fetchBtn = document.querySelector('.btn-secondary'); // If we don't have URL, try to fetch it again via user stats if (!currentState.subUrl) { try { const res = await fetch(`${API_BASE}/user/${currentState.user.id}`); const data = await res.json(); currentState.subUrl = data.subscription_url; } catch (e) { } } const url = currentState.subUrl; if (url) { linkEl.textContent = url; // Gen QR const qrContainer = document.getElementById('qrcode-container'); qrContainer.innerHTML = ''; new QRCode(qrContainer, { text: url, width: 160, height: 160, colorDark: "#000000", colorLight: "#ffffff", correctLevel: QRCode.CorrectLevel.M }); } else { linkEl.textContent = "No active subscription found"; document.getElementById('qrcode-container').innerHTML = '
'; } } function copyConfig() { if (currentState.subUrl) { navigator.clipboard.writeText(currentState.subUrl); if (tg) tg.showAlert("Link copied to clipboard!"); else alert("Copied!"); } else { if (tg) tg.showAlert("No subscription to copy."); } } function toggleAcc(header) { const body = header.nextElementSibling; body.classList.toggle('open'); const icon = header.querySelector('svg'); // Simple rotation logic if needed, or just rely on CSS } async function loadProfile() { document.getElementById('profile-name').textContent = currentState.user.first_name; document.getElementById('profile-id').textContent = `ID: ${currentState.user.id}`; const avatar = document.getElementById('profile-avatar'); avatar.textContent = (currentState.user.first_name || 'U')[0].toUpperCase(); } async function checkPromo() { const input = document.getElementById('promo-input'); const resDiv = document.getElementById('promo-result'); const code = input.value.trim(); if (!code) return; try { const res = await fetch(`${API_BASE}/check-promo`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }) }); if (res.ok) { const data = await res.json(); resDiv.innerHTML = `
✅ ${data.description}. Apply at checkout!
`; currentState.promoCode = data.code; // Save for checkout tg.showAlert(`Promo valid! Discount: ${data.discount}%. Go to Shop to buy.`); } else { resDiv.innerHTML = `
❌ Invalid Code
`; } } catch (e) { resDiv.textContent = "Error checking promo"; } } function openHelp() { // Simple alert or modal. Using routing for now logic would be better if we had a help page. alert("Support: @hoshimach1"); } // Material Ripple Init document.addEventListener('click', function (e) { const target = e.target.closest('button, .action-card, .plan-card'); // Select ripple targets if (target) { const circle = document.createElement('span'); const diameter = Math.max(target.clientWidth, target.clientHeight); const radius = diameter / 2; const rect = target.getBoundingClientRect(); circle.style.width = circle.style.height = `${diameter}px`; circle.style.left = `${e.clientX - rect.left - radius}px`; circle.style.top = `${e.clientY - rect.top - radius}px`; circle.classList.add('ripple'); // Remove existing ripples to be clean or append? Append allows rapid clicks. const ripple = target.getElementsByClassName('ripple')[0]; if (ripple) { ripple.remove(); } target.appendChild(circle); } }); // Start document.addEventListener('DOMContentLoaded', () => { router('dashboard'); });