Улучшение взаимодействия и добавление веб-приложения

This commit is contained in:
2026-01-09 01:20:30 +03:00
parent eed252d52e
commit 2472947c1f
16 changed files with 2972 additions and 969 deletions

183
web_app/static/js/app.js Normal file
View File

@@ -0,0 +1,183 @@
// Navigation Router
function router(pageName) {
const viewContainer = document.getElementById('app-view');
const title = document.getElementById('page-title');
const navItems = document.querySelectorAll('.nav-item');
// Update Nav
navItems.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}`);
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();
}
}
// Data Fetching
const API_BASE = '/api';
// Telegram Integration
let tgUser = null;
if (window.Telegram && window.Telegram.WebApp) {
const tg = window.Telegram.WebApp;
tg.ready();
tgUser = tg.initDataUnsafe?.user;
// Theme sync
if (tg.colorScheme === 'dark') document.body.classList.add('dark');
// Expand
tg.expand();
}
// Fallback for browser testing
if (!tgUser) {
console.warn("No Telegram user detected, using mock user");
tgUser = { id: 123456789, first_name: 'Test', username: 'testuser' };
}
// 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();
async function loadDashboard() {
try {
const res = await fetch(`${API_BASE}/user/${tgUser.id}`);
if (!res.ok) throw new Error("Failed to fetch user");
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 (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;
// Colorize status
if (data.status === 'Active') {
document.querySelector('.stat-info .value').style.color = '#4ade80';
} else {
document.querySelector('.stat-info .value').style.color = '#f87171';
}
} catch (e) {
console.error(e);
// Show error state?
}
}
async function loadShop() {
const container = document.getElementById('plans-container');
if (!container) return;
container.innerHTML = '<div class="loading-spinner">Loading plans...</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.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>
`;
container.appendChild(card);
});
} catch (e) {
container.innerHTML = 'Error loading plans.';
}
}
async function buyPlan(planId) {
if (!window.Telegram || !window.Telegram.WebApp) {
alert("Payment only works inside Telegram!");
return;
}
const btn = document.activeElement;
const originalText = btn.innerText;
btn.innerText = 'Creating Invoice...';
btn.disabled = true;
try {
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
})
});
const data = await res.json();
if (data.invoice_link) {
window.Telegram.WebApp.openInvoice(data.invoice_link, (status) => {
if (status === 'paid') {
window.Telegram.WebApp.showAlert('Payment Successful! Subscription activated.');
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);
}
} catch (e) {
window.Telegram.WebApp.showAlert('Network error');
console.error(e);
} finally {
btn.innerText = originalText;
btn.disabled = false;
}
}
async function loadProfile() {
document.getElementById('profile-tg-id').textContent = tgUser.id;
document.getElementById('profile-username').value = '@' + (tgUser.username || 'unknown');
}
// Init
router('dashboard');

View File

@@ -0,0 +1,144 @@
const container = document.getElementById('stars-container');
// Create Canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
container.appendChild(canvas);
let width, height;
let stars = [];
let comets = [];
function resize() {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
initStars();
}
class Star {
constructor() {
this.reset();
this.y = Math.random() * height; // Initial random y
}
reset() {
this.x = Math.random() * width;
this.y = Math.random() * height;
this.z = Math.random() * 2 + 0.5; // Depth/Size/Speed
this.baseSize = Math.random() * 1.5;
this.alpha = Math.random() * 0.5 + 0.1;
this.twinkle = Math.random() * 0.05;
}
update() {
this.alpha += this.twinkle;
if (this.alpha > 0.8 || this.alpha < 0.1) this.twinkle = -this.twinkle;
}
draw() {
ctx.fillStyle = `rgba(255, 255, 255, ${this.alpha})`;
ctx.beginPath();
const size = this.baseSize * (this.z / 2);
ctx.arc(this.x, this.y, size, 0, Math.PI * 2);
ctx.fill();
}
}
class Comet {
constructor() {
this.reset();
}
reset() {
this.x = Math.random() * width;
this.y = Math.random() * height * 0.5;
this.len = Math.random() * 80 + 20;
this.speed = Math.random() * 5 + 2;
this.angle = Math.PI / 4 + (Math.random() - 0.5) * 0.2; // 45 degrees
this.active = false;
this.wait = Math.random() * 200 + 50;
}
update() {
if (!this.active) {
this.wait--;
if (this.wait <= 0) {
this.active = true;
this.x = Math.random() * width - 200; // Start off screen slightly
this.y = Math.random() * height * 0.5;
}
return;
}
this.x += Math.cos(this.angle) * this.speed;
this.y += Math.sin(this.angle) * this.speed;
if (this.x > width + 100 || this.y > height + 100) {
this.active = false;
this.reset();
this.wait = Math.random() * 500 + 100; // Wait longer before next
}
}
draw() {
if (!this.active) return;
// Gradient tail
const grad = ctx.createLinearGradient(
this.x, this.y,
this.x - Math.cos(this.angle) * this.len,
this.y - Math.sin(this.angle) * this.len
);
grad.addColorStop(0, 'rgba(255, 255, 255, 0.8)');
grad.addColorStop(1, 'rgba(99, 102, 241, 0)'); // Fade to purple blue
ctx.strokeStyle = grad;
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.lineTo(
this.x - Math.cos(this.angle) * this.len,
this.y - Math.sin(this.angle) * this.len
);
ctx.stroke();
// Head
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(this.x, this.y, 2, 0, Math.PI * 2);
ctx.fill();
// Glow
ctx.shadowBlur = 10;
ctx.shadowColor = '#6366f1';
ctx.fill();
ctx.shadowBlur = 0;
}
}
function initStars() {
stars = [];
comets = [];
for (let i = 0; i < 150; i++) stars.push(new Star());
for (let i = 0; i < 3; i++) comets.push(new Comet());
}
function animate() {
ctx.clearRect(0, 0, width, height);
// Background gradient for depth
// ctx.fillStyle = 'rgba(5, 5, 16, 0.2)';
// ctx.fillRect(0,0,width,height);
stars.forEach(s => { s.update(); s.draw(); });
comets.forEach(c => { c.update(); c.draw(); });
requestAnimationFrame(animate);
}
window.addEventListener('resize', resize);
resize();
animate();