forked from zovos/bot_tg
554 lines
20 KiB
JavaScript
554 lines
20 KiB
JavaScript
/**
|
||
* FenyaBot Mini App — Main Application
|
||
* SPA with vanilla JS routing and page modules
|
||
*/
|
||
|
||
import { API } from './api.js';
|
||
|
||
// ── Telegram WebApp SDK ──
|
||
const tg = window.Telegram?.WebApp;
|
||
if (tg) {
|
||
tg.ready();
|
||
tg.expand();
|
||
tg.setHeaderColor('#060a14');
|
||
tg.setBackgroundColor('#060a14');
|
||
}
|
||
|
||
function haptic(type = 'impact') {
|
||
try {
|
||
if (type === 'impact') tg?.HapticFeedback?.impactOccurred('light');
|
||
else if (type === 'success') tg?.HapticFeedback?.notificationOccurred('success');
|
||
else if (type === 'error') tg?.HapticFeedback?.notificationOccurred('error');
|
||
} catch {}
|
||
}
|
||
|
||
// ── State ──
|
||
let currentPage = 'home';
|
||
let profileData = null;
|
||
let chatHistory = [];
|
||
let selectedBet = null; // { league, matchIndex, outcome, odds }
|
||
|
||
const container = document.getElementById('page-container');
|
||
const navBtns = document.querySelectorAll('.nav-btn');
|
||
|
||
// ── Router ──
|
||
function navigate(page, data = null) {
|
||
currentPage = page;
|
||
haptic();
|
||
navBtns.forEach(b => b.classList.toggle('active', b.dataset.page === page));
|
||
container.style.animation = 'none';
|
||
container.offsetHeight;
|
||
container.style.animation = 'fadeIn 0.25s ease';
|
||
|
||
switch (page) {
|
||
case 'home': renderHome(); break;
|
||
case 'betting': renderBetting(data); break;
|
||
case 'casino': renderCasino(); break;
|
||
case 'schedule': renderSchedule(); break;
|
||
case 'chat': renderChat(); break;
|
||
default: renderHome();
|
||
}
|
||
}
|
||
|
||
navBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => navigate(btn.dataset.page));
|
||
});
|
||
|
||
// ── Helpers ──
|
||
function $(html) {
|
||
const t = document.createElement('template');
|
||
t.innerHTML = html.trim();
|
||
return t.content.firstChild;
|
||
}
|
||
|
||
function showResult(icon, title, desc, btnText = 'OK') {
|
||
return new Promise(resolve => {
|
||
const overlay = $(`
|
||
<div class="result-overlay">
|
||
<div class="result-popup">
|
||
<div class="result-icon">${icon}</div>
|
||
<div class="result-title">${title}</div>
|
||
<div class="result-desc">${desc}</div>
|
||
<button class="btn btn-primary">${btnText}</button>
|
||
</div>
|
||
</div>
|
||
`);
|
||
overlay.querySelector('.btn').onclick = () => { overlay.remove(); resolve(); };
|
||
overlay.onclick = (e) => { if (e.target === overlay) { overlay.remove(); resolve(); } };
|
||
document.body.appendChild(overlay);
|
||
});
|
||
}
|
||
|
||
function skeleton(lines = 3) {
|
||
return `<div class="card">${'<div class="skeleton skeleton-line" style="width:' + (60 + Math.random()*35) + '%"></div>'.repeat(lines)}</div>`;
|
||
}
|
||
|
||
// ══════════════════════════════════════════
|
||
// PAGE: HOME
|
||
// ══════════════════════════════════════════
|
||
|
||
async function renderHome() {
|
||
container.innerHTML = `
|
||
<div class="profile-card card">
|
||
<div class="skeleton skeleton-circle"></div>
|
||
<div class="skeleton skeleton-line" style="width:40%;margin:0 auto"></div>
|
||
<div class="skeleton skeleton-line" style="width:60%;margin:8px auto"></div>
|
||
</div>
|
||
<div class="stat-grid">
|
||
<div class="stat-item"><div class="skeleton skeleton-line"></div></div>
|
||
<div class="stat-item"><div class="skeleton skeleton-line"></div></div>
|
||
</div>
|
||
`;
|
||
|
||
try {
|
||
profileData = await API.getProfile();
|
||
const p = profileData;
|
||
const length = p.length != null ? p.length.toFixed(1) : '—';
|
||
|
||
container.innerHTML = `
|
||
<div class="profile-card card">
|
||
<div class="profile-avatar">👤</div>
|
||
<div class="profile-name">${p.first_name || p.username || 'Кент'}</div>
|
||
<div class="profile-balance">${length} см</div>
|
||
<div class="profile-balance-label">текущий размер</div>
|
||
</div>
|
||
|
||
<div class="stat-grid">
|
||
<div class="stat-item">
|
||
<div class="stat-value">${p.active_bets}</div>
|
||
<div class="stat-label">Активных ставок</div>
|
||
</div>
|
||
<div class="stat-item" id="penis-btn" style="cursor:pointer">
|
||
<div class="stat-value">🎲</div>
|
||
<div class="stat-label">Крутить penis</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-header">Быстрые действия</div>
|
||
<div class="quick-actions">
|
||
<button class="quick-action" data-action="betting">
|
||
<div class="quick-action-icon">⚽</div>
|
||
<div class="quick-action-label">Ставки</div>
|
||
</button>
|
||
<button class="quick-action" data-action="casino">
|
||
<div class="quick-action-icon">🎰</div>
|
||
<div class="quick-action-label">Казино</div>
|
||
</button>
|
||
<button class="quick-action" data-action="top">
|
||
<div class="quick-action-icon">🏆</div>
|
||
<div class="quick-action-label">Топ</div>
|
||
</button>
|
||
<button class="quick-action" data-action="history">
|
||
<div class="quick-action-icon">📊</div>
|
||
<div class="quick-action-label">История</div>
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// Крутить penis
|
||
document.getElementById('penis-btn')?.addEventListener('click', async () => {
|
||
haptic();
|
||
try {
|
||
const res = await API.playPenis();
|
||
if (res.success) {
|
||
haptic('success');
|
||
await showResult('🎉', 'Успех!', res.message);
|
||
} else {
|
||
await showResult('⏳', 'Подожди', res.message);
|
||
}
|
||
renderHome();
|
||
} catch (e) {
|
||
haptic('error');
|
||
await showResult('❌', 'Ошибка', e.message);
|
||
}
|
||
});
|
||
|
||
// Quick actions
|
||
container.querySelectorAll('.quick-action').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const action = btn.dataset.action;
|
||
if (action === 'betting') navigate('betting');
|
||
else if (action === 'casino') navigate('casino');
|
||
else if (action === 'top') showTop();
|
||
else if (action === 'history') showBetHistory();
|
||
});
|
||
});
|
||
|
||
} catch (e) {
|
||
container.innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon">⚠️</div>
|
||
<div class="empty-state-text">Не удалось загрузить профиль<br>${e.message}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
async function showTop() {
|
||
try {
|
||
const data = await API.getTop();
|
||
await showResult('🏆', 'Лидерборд', data.text.replace(/\n/g, '<br>'));
|
||
} catch (e) {
|
||
await showResult('❌', 'Ошибка', e.message);
|
||
}
|
||
}
|
||
|
||
async function showBetHistory() {
|
||
try {
|
||
const data = await API.getBetHistory();
|
||
await showResult('📊', 'История ставок', data.text.replace(/\n/g, '<br>'));
|
||
} catch (e) {
|
||
await showResult('❌', 'Ошибка', e.message);
|
||
}
|
||
}
|
||
|
||
// ══════════════════════════════════════════
|
||
// PAGE: BETTING
|
||
// ══════════════════════════════════════════
|
||
|
||
async function renderBetting(data) {
|
||
if (data?.league) {
|
||
return renderMatches(data.league, data.leagueName);
|
||
}
|
||
|
||
container.innerHTML = `<div class="section-header">⚽ Ставки на матчи</div>${skeleton(2)}`;
|
||
|
||
try {
|
||
const leagues = await API.getLeagues();
|
||
container.innerHTML = `
|
||
<div class="section-header">⚽ Выбери лигу</div>
|
||
<div class="league-list" id="league-list"></div>
|
||
`;
|
||
const list = document.getElementById('league-list');
|
||
leagues.forEach(l => {
|
||
const item = $(`
|
||
<div class="league-item">
|
||
<span class="league-icon">⚽</span>
|
||
<span class="league-name">${l.name}</span>
|
||
<span class="league-arrow">›</span>
|
||
</div>
|
||
`);
|
||
item.addEventListener('click', () => {
|
||
haptic();
|
||
navigate('betting', { league: l.alias, leagueName: l.name });
|
||
});
|
||
list.appendChild(item);
|
||
});
|
||
} catch (e) {
|
||
container.innerHTML = `<div class="empty-state"><div class="empty-state-icon">❌</div><div class="empty-state-text">${e.message}</div></div>`;
|
||
}
|
||
}
|
||
|
||
async function renderMatches(league, leagueName) {
|
||
container.innerHTML = `
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px">
|
||
<button class="btn btn-secondary btn-sm" id="back-btn" style="width:auto;padding:8px 12px">← Назад</button>
|
||
<div class="section-header" style="margin:0">${leagueName}</div>
|
||
</div>
|
||
${skeleton(3)}
|
||
`;
|
||
document.getElementById('back-btn').onclick = () => navigate('betting');
|
||
|
||
try {
|
||
const matches = await API.getMatches(league);
|
||
if (!matches.length) {
|
||
container.innerHTML += `<div class="empty-state"><div class="empty-state-icon">📭</div><div class="empty-state-text">Нет матчей в этой лиге</div></div>`;
|
||
return;
|
||
}
|
||
|
||
// Remove skeleton
|
||
container.querySelectorAll('.card').forEach(c => c.remove());
|
||
|
||
matches.forEach((m, idx) => {
|
||
const date = m.commence ? new Date(m.commence).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : '';
|
||
const card = $(`
|
||
<div class="card match-card" style="animation-delay:${idx * 0.05}s">
|
||
<div class="match-time">${date}</div>
|
||
<div class="match-teams">
|
||
<div class="match-team">${m.home}</div>
|
||
<div class="match-vs">VS</div>
|
||
<div class="match-team">${m.away}</div>
|
||
</div>
|
||
<div class="match-odds">
|
||
<button class="odds-btn" data-outcome="1" data-odds="${m.odds_home || 0}">
|
||
<span class="odds-label">П1</span>
|
||
<span class="odds-value">${m.odds_home?.toFixed(2) || '—'}</span>
|
||
</button>
|
||
<button class="odds-btn" data-outcome="X" data-odds="${m.odds_draw || 0}">
|
||
<span class="odds-label">X</span>
|
||
<span class="odds-value">${m.odds_draw?.toFixed(2) || '—'}</span>
|
||
</button>
|
||
<button class="odds-btn" data-outcome="2" data-odds="${m.odds_away || 0}">
|
||
<span class="odds-label">П2</span>
|
||
<span class="odds-value">${m.odds_away?.toFixed(2) || '—'}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`);
|
||
|
||
card.querySelectorAll('.odds-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
haptic();
|
||
selectedBet = {
|
||
league,
|
||
matchIndex: m.index,
|
||
outcome: btn.dataset.outcome,
|
||
odds: parseFloat(btn.dataset.odds),
|
||
home: m.home,
|
||
away: m.away,
|
||
};
|
||
showBetSlip();
|
||
});
|
||
});
|
||
|
||
container.appendChild(card);
|
||
});
|
||
|
||
} catch (e) {
|
||
container.innerHTML += `<div class="empty-state"><div class="empty-state-icon">❌</div><div class="empty-state-text">${e.message}</div></div>`;
|
||
}
|
||
}
|
||
|
||
function showBetSlip() {
|
||
if (!selectedBet) return;
|
||
document.querySelector('.bet-slip')?.remove();
|
||
|
||
const outcomeLabel = { '1': `П1 (${selectedBet.home})`, 'X': 'Ничья', '2': `П2 (${selectedBet.away})` };
|
||
const slip = $(`
|
||
<div class="bet-slip visible">
|
||
<div class="bet-slip-header">
|
||
<span class="card-title">${outcomeLabel[selectedBet.outcome]}</span>
|
||
<span class="badge badge-blue">x${selectedBet.odds?.toFixed(2) || '?'}</span>
|
||
</div>
|
||
<input type="number" class="bet-input" placeholder="Ставка (см)" step="0.1" min="0.1" max="3" value="0.5">
|
||
<button class="btn btn-primary" id="place-bet-btn">Поставить</button>
|
||
</div>
|
||
`);
|
||
|
||
slip.querySelector('#place-bet-btn').addEventListener('click', async () => {
|
||
const amount = parseFloat(slip.querySelector('.bet-input').value);
|
||
if (!amount || amount <= 0) return;
|
||
try {
|
||
const res = await API.placeBet({
|
||
league: selectedBet.league,
|
||
match_index: selectedBet.matchIndex,
|
||
outcome: selectedBet.outcome,
|
||
amount,
|
||
});
|
||
haptic('success');
|
||
slip.remove();
|
||
await showResult('✅', 'Ставка принята!', res.message);
|
||
} catch (e) {
|
||
haptic('error');
|
||
await showResult('❌', 'Ошибка', e.message);
|
||
}
|
||
});
|
||
|
||
document.body.appendChild(slip);
|
||
setTimeout(() => slip.classList.add('visible'), 10);
|
||
}
|
||
|
||
// ══════════════════════════════════════════
|
||
// PAGE: CASINO
|
||
// ══════════════════════════════════════════
|
||
|
||
async function renderCasino() {
|
||
const balance = profileData?.length != null ? profileData.length.toFixed(1) : '?';
|
||
container.innerHTML = `
|
||
<div class="section-header">🎰 Казино</div>
|
||
<div class="card" style="text-align:center;padding:24px">
|
||
<div style="font-size:13px;color:var(--text-secondary);margin-bottom:4px">Баланс</div>
|
||
<div style="font-size:24px;font-weight:800;color:var(--blue-400)" id="casino-balance">${balance} см</div>
|
||
</div>
|
||
|
||
<div class="card" style="text-align:center;padding:24px">
|
||
<div class="slots-display">
|
||
<div class="slot-reel" id="reel1">🎰</div>
|
||
<div class="slot-reel" id="reel2">🎰</div>
|
||
<div class="slot-reel" id="reel3">🎰</div>
|
||
</div>
|
||
<div class="casino-bet-input">
|
||
<input type="number" id="casino-bet" value="0.5" step="0.1" min="0.1" max="1" placeholder="Ставка">
|
||
<span style="color:var(--text-secondary);font-size:13px">см</span>
|
||
</div>
|
||
<button class="btn btn-primary btn-lg" id="spin-btn">🎰 Крутить!</button>
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('spin-btn').addEventListener('click', async () => {
|
||
const bet = parseFloat(document.getElementById('casino-bet').value);
|
||
if (!bet || bet <= 0) return;
|
||
|
||
haptic();
|
||
const btn = document.getElementById('spin-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Крутим...';
|
||
|
||
// Анимация вращения
|
||
const symbols = ['🍒', '🍋', '🔔', '💎', '7️⃣', '🍀'];
|
||
const reels = [document.getElementById('reel1'), document.getElementById('reel2'), document.getElementById('reel3')];
|
||
reels.forEach(r => r.classList.add('spinning'));
|
||
|
||
const spinInterval = setInterval(() => {
|
||
reels.forEach(r => { r.textContent = symbols[Math.floor(Math.random() * symbols.length)]; });
|
||
}, 100);
|
||
|
||
try {
|
||
const res = await API.playCasino(bet);
|
||
|
||
clearInterval(spinInterval);
|
||
|
||
// Останавливаем слоты по одному
|
||
for (let i = 0; i < 3; i++) {
|
||
await new Promise(r => setTimeout(r, 300));
|
||
reels[i].classList.remove('spinning');
|
||
reels[i].textContent = res.reels[i];
|
||
if (res.won) reels[i].classList.add('won');
|
||
haptic();
|
||
}
|
||
|
||
// Обновляем баланс
|
||
document.getElementById('casino-balance').textContent = `${res.new_length.toFixed(1)} см`;
|
||
if (profileData) profileData.length = res.new_length;
|
||
|
||
await new Promise(r => setTimeout(r, 500));
|
||
|
||
if (res.won) {
|
||
haptic('success');
|
||
const jackpot = res.matches === 3;
|
||
await showResult(
|
||
jackpot ? '🎉' : '✅',
|
||
jackpot ? 'ДЖЕКПОТ!!!' : 'Выигрыш!',
|
||
`${jackpot ? 'Три одинаковых!' : 'Две совпали!'}\nx${res.multiplier}\n+${res.delta.toFixed(1)} см\nБаланс: ${res.new_length.toFixed(1)} см`
|
||
);
|
||
} else {
|
||
haptic('error');
|
||
await showResult('❌', 'Мимо!', `−${Math.abs(res.delta).toFixed(1)} см\nБаланс: ${res.new_length.toFixed(1)} см`);
|
||
}
|
||
|
||
} catch (e) {
|
||
clearInterval(spinInterval);
|
||
reels.forEach(r => r.classList.remove('spinning'));
|
||
haptic('error');
|
||
await showResult('❌', 'Ошибка', e.message);
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = '🎰 Крутить!';
|
||
reels.forEach(r => r.classList.remove('won'));
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════
|
||
// PAGE: SCHEDULE
|
||
// ══════════════════════════════════════════
|
||
|
||
async function renderSchedule() {
|
||
const days = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
|
||
const today = new Date().getDay();
|
||
const todayIdx = today === 0 ? 6 : today - 1;
|
||
|
||
container.innerHTML = `
|
||
<div class="section-header">📅 Расписание</div>
|
||
<div class="schedule-day-tabs" id="day-tabs">
|
||
${days.map((d, i) => `<button class="day-tab ${i === todayIdx ? 'active' : ''}" data-day="${i}">${d}</button>`).join('')}
|
||
</div>
|
||
<div id="lessons-container">${skeleton(4)}</div>
|
||
`;
|
||
|
||
async function loadDay(dayIdx) {
|
||
const lc = document.getElementById('lessons-container');
|
||
lc.innerHTML = skeleton(3);
|
||
try {
|
||
const lessons = await API.getSchedule(dayIdx);
|
||
if (!lessons.length) {
|
||
lc.innerHTML = `<div class="empty-state"><div class="empty-state-icon">🎉</div><div class="empty-state-text">Нет пар!</div></div>`;
|
||
return;
|
||
}
|
||
lc.innerHTML = '<div class="card">' + lessons.map(l => `
|
||
<div class="lesson-item">
|
||
<div class="lesson-number">${l.pair || '—'}</div>
|
||
<div class="lesson-info">
|
||
<div class="lesson-title">${l.title}</div>
|
||
<div class="lesson-time">${l.time_start && l.time_end ? `${l.time_start} — ${l.time_end}` : ''}</div>
|
||
</div>
|
||
</div>
|
||
`).join('') + '</div>';
|
||
} catch (e) {
|
||
lc.innerHTML = `<div class="empty-state"><div class="empty-state-icon">❌</div><div class="empty-state-text">${e.message}</div></div>`;
|
||
}
|
||
}
|
||
|
||
document.getElementById('day-tabs').addEventListener('click', (e) => {
|
||
const tab = e.target.closest('.day-tab');
|
||
if (!tab) return;
|
||
haptic();
|
||
document.querySelectorAll('.day-tab').forEach(t => t.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
loadDay(parseInt(tab.dataset.day));
|
||
});
|
||
|
||
loadDay(todayIdx);
|
||
}
|
||
|
||
// ══════════════════════════════════════════
|
||
// PAGE: CHAT
|
||
// ══════════════════════════════════════════
|
||
|
||
function renderChat() {
|
||
container.innerHTML = `
|
||
<div class="section-header">💬 ИИ на фене</div>
|
||
<div class="chat-messages" id="chat-messages">
|
||
${chatHistory.length === 0 ? `
|
||
<div class="chat-bubble bot">Йо, кент! Базарь, я тут. Пиши чё надо, братуха 🤙</div>
|
||
` : chatHistory.map(m => `<div class="chat-bubble ${m.role}">${m.text}</div>`).join('')}
|
||
</div>
|
||
<div class="chat-input-bar">
|
||
<input type="text" id="chat-input" placeholder="Напиши сообщение..." autocomplete="off">
|
||
<button class="chat-send-btn" id="chat-send">➤</button>
|
||
</div>
|
||
`;
|
||
|
||
const input = document.getElementById('chat-input');
|
||
const sendBtn = document.getElementById('chat-send');
|
||
const messages = document.getElementById('chat-messages');
|
||
|
||
async function send() {
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
|
||
input.value = '';
|
||
haptic();
|
||
|
||
// Add user bubble
|
||
chatHistory.push({ role: 'user', text });
|
||
const userBubble = $(`<div class="chat-bubble user">${text}</div>`);
|
||
messages.appendChild(userBubble);
|
||
messages.scrollTop = messages.scrollHeight;
|
||
|
||
// Loading
|
||
const loading = $(`<div class="chat-bubble bot" id="chat-loading">💭 печатает...</div>`);
|
||
messages.appendChild(loading);
|
||
messages.scrollTop = messages.scrollHeight;
|
||
|
||
try {
|
||
const res = await API.talk(text);
|
||
loading.remove();
|
||
chatHistory.push({ role: 'bot', text: res.response });
|
||
const botBubble = $(`<div class="chat-bubble bot">${res.response}</div>`);
|
||
messages.appendChild(botBubble);
|
||
messages.scrollTop = messages.scrollHeight;
|
||
} catch (e) {
|
||
loading.remove();
|
||
const errBubble = $(`<div class="chat-bubble bot">Ошибка: ${e.message}</div>`);
|
||
messages.appendChild(errBubble);
|
||
}
|
||
}
|
||
|
||
sendBtn.addEventListener('click', send);
|
||
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') send(); });
|
||
input.focus();
|
||
}
|
||
|
||
// ── Init ──
|
||
navigate('home');
|