Update WebApp
This commit is contained in:
@@ -1,183 +1,294 @@
|
||||
// Navigation Router
|
||||
// Navigation
|
||||
function router(pageName) {
|
||||
const viewContainer = document.getElementById('app-view');
|
||||
const title = document.getElementById('page-title');
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
const template = document.getElementById(`view-${pageName}`);
|
||||
|
||||
// Update Nav
|
||||
navItems.forEach(item => {
|
||||
// Update Nav State
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
if (item.dataset.page === pageName) item.classList.add('active');
|
||||
else item.classList.remove('active');
|
||||
});
|
||||
|
||||
// Set Title
|
||||
title.textContent = pageName.charAt(0).toUpperCase() + pageName.slice(1);
|
||||
|
||||
// Load View
|
||||
const template = document.getElementById(`view-${pageName}`);
|
||||
// Swap View
|
||||
if (template) {
|
||||
viewContainer.innerHTML = '';
|
||||
viewContainer.appendChild(template.content.cloneNode(true));
|
||||
|
||||
// Initialize view specific logic
|
||||
if (pageName === 'dashboard') loadDashboard();
|
||||
if (pageName === 'shop') loadShop();
|
||||
if (pageName === 'profile') loadProfile();
|
||||
|
||||
// Re-init generic UI stuff like icons if new ones added
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// 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' });
|
||||
}
|
||||
|
||||
// Data Fetching
|
||||
// Global State
|
||||
const API_BASE = '/api';
|
||||
let currentState = {
|
||||
user: null,
|
||||
subUrl: ""
|
||||
};
|
||||
|
||||
// Telegram Integration
|
||||
let tgUser = null;
|
||||
if (window.Telegram && window.Telegram.WebApp) {
|
||||
const tg = window.Telegram.WebApp;
|
||||
// Telegram Init
|
||||
const tg = window.Telegram?.WebApp;
|
||||
if (tg) {
|
||||
tg.ready();
|
||||
tgUser = tg.initDataUnsafe?.user;
|
||||
|
||||
// Theme sync
|
||||
if (tg.colorScheme === 'dark') document.body.classList.add('dark');
|
||||
|
||||
// Expand
|
||||
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;
|
||||
}
|
||||
|
||||
// Fallback for browser testing
|
||||
if (!tgUser) {
|
||||
console.warn("No Telegram user detected, using mock user");
|
||||
tgUser = { id: 123456789, first_name: 'Test', username: 'testuser' };
|
||||
// Dev Mock
|
||||
if (!currentState.user) {
|
||||
currentState.user = { id: 123456789, first_name: 'Dev', username: 'developer' };
|
||||
}
|
||||
|
||||
// Update UI with User Info
|
||||
const sidebarName = document.getElementById('sidebar-name');
|
||||
const sidebarAvatar = document.getElementById('sidebar-avatar');
|
||||
if (sidebarName) sidebarName.textContent = tgUser.first_name || tgUser.username;
|
||||
if (sidebarAvatar) sidebarAvatar.textContent = (tgUser.first_name || 'U')[0].toUpperCase();
|
||||
// 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/${tgUser.id}`);
|
||||
if (!res.ok) throw new Error("Failed to fetch user");
|
||||
const res = await fetch(`${API_BASE}/user/${currentState.user.id}`);
|
||||
const data = await res.json();
|
||||
|
||||
const statusEl = document.getElementById('dash-status');
|
||||
const daysEl = document.getElementById('dash-days');
|
||||
const dataEl = document.getElementById('dash-data');
|
||||
const planEl = document.getElementById('sub-plan-name');
|
||||
const expireEl = document.getElementById('sub-expire-date');
|
||||
if (data.error) throw new Error(data.error);
|
||||
|
||||
if (statusEl) statusEl.textContent = data.status;
|
||||
if (daysEl) daysEl.textContent = data.days_left;
|
||||
if (dataEl) dataEl.textContent = `${data.data_usage || 0} GB`;
|
||||
if (planEl) planEl.textContent = data.plan;
|
||||
if (expireEl) expireEl.textContent = data.expire_date;
|
||||
// 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;
|
||||
|
||||
// Colorize status
|
||||
if (data.status === 'Active') {
|
||||
document.querySelector('.stat-info .value').style.color = '#4ade80';
|
||||
} else {
|
||||
document.querySelector('.stat-info .value').style.color = '#f87171';
|
||||
// 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);
|
||||
// Show error state?
|
||||
document.getElementById('dash-status').textContent = 'Error';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadShop() {
|
||||
const container = document.getElementById('plans-container');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="loading-spinner">Loading plans...</div>';
|
||||
container.innerHTML = '<div class="loading-spinner"></div>';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/plans`);
|
||||
if (!res.ok) throw new Error("Failed to fetch plans");
|
||||
const plans = await res.json();
|
||||
|
||||
container.innerHTML = '';
|
||||
plans.forEach(plan => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card glass plan-card';
|
||||
|
||||
// Features list generation
|
||||
const features = [
|
||||
`${plan.data_limit} GB Data`,
|
||||
`${plan.days} Days`,
|
||||
'High Speed'
|
||||
];
|
||||
|
||||
card.className = 'glass plan-card plan-item'; // plan-item for animation
|
||||
card.innerHTML = `
|
||||
<div class="plan-name">${plan.name}</div>
|
||||
<div class="plan-price">${plan.price} XTR</div>
|
||||
<ul class="plan-features">
|
||||
${features.map(f => `<li>${f}</li>`).join('')}
|
||||
</ul>
|
||||
<button class="btn-primary" onclick="buyPlan('${plan.id}')">Buy for ${plan.price}</button>
|
||||
<div class="plan-header">
|
||||
<span class="plan-title">${plan.name}</span>
|
||||
<span class="plan-price">${plan.price} ⭐️</span>
|
||||
</div>
|
||||
<div class="plan-specs">
|
||||
<span><i data-lucide="database"></i> ${plan.data_limit} GB</span>
|
||||
<span><i data-lucide="clock"></i> ${plan.days} Days</span>
|
||||
</div>
|
||||
<button class="btn-primary" onclick="initPayment('${plan.id}')">Purchase</button>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
});
|
||||
lucide.createIcons();
|
||||
|
||||
} catch (e) {
|
||||
container.innerHTML = 'Error loading plans.';
|
||||
container.textContent = "Failed to load plans.";
|
||||
}
|
||||
}
|
||||
|
||||
async function buyPlan(planId) {
|
||||
if (!window.Telegram || !window.Telegram.WebApp) {
|
||||
alert("Payment only works inside Telegram!");
|
||||
return;
|
||||
}
|
||||
async function initPayment(planId) {
|
||||
if (!tg) return alert("Open in Telegram");
|
||||
|
||||
const btn = document.activeElement;
|
||||
const originalText = btn.innerText;
|
||||
btn.innerText = 'Creating Invoice...';
|
||||
// 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({
|
||||
user_id: tgUser.id,
|
||||
plan_id: planId
|
||||
})
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.invoice_link) {
|
||||
window.Telegram.WebApp.openInvoice(data.invoice_link, (status) => {
|
||||
tg.openInvoice(data.invoice_link, (status) => {
|
||||
if (status === 'paid') {
|
||||
window.Telegram.WebApp.showAlert('Payment Successful! Subscription activated.');
|
||||
tg.showAlert('Successful Payment!');
|
||||
router('dashboard');
|
||||
} else if (status === 'cancelled') {
|
||||
// User cancelled
|
||||
} else {
|
||||
window.Telegram.WebApp.showAlert('Payment failed or pending.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
window.Telegram.WebApp.showAlert('Error creating invoice: ' + data.error);
|
||||
tg.showAlert('Error: ' + (data.error || 'Unknown'));
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
window.Telegram.WebApp.showAlert('Network error');
|
||||
console.error(e);
|
||||
tg.showAlert('Network error');
|
||||
} finally {
|
||||
btn.innerText = originalText;
|
||||
btn.innerText = oldText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
document.getElementById('profile-tg-id').textContent = tgUser.id;
|
||||
document.getElementById('profile-username').value = '@' + (tgUser.username || 'unknown');
|
||||
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 = '<div style="width:160px;height:160px;background:rgba(255,255,255,0.1);border-radius:12px"></div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Init
|
||||
router('dashboard');
|
||||
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 = `<div style="color:#4ade80; margin-top:8px">✅ ${data.description}. Apply at checkout!</div>`;
|
||||
currentState.promoCode = data.code; // Save for checkout
|
||||
tg.showAlert(`Promo valid! Discount: ${data.discount}%. Go to Shop to buy.`);
|
||||
} else {
|
||||
resDiv.innerHTML = `<div style="color:#f87171; margin-top:8px">❌ Invalid Code</div>`;
|
||||
}
|
||||
} 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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user