2b399fdb81
PR #1223 only staged the deletions of the old paths because the new top-level directories were still untracked when the commit was authored. This commit adds the actual restructured tree: SREBOT/ (existing bot), SHARED/ (vromfs, data_parser, ICONS/MAPS/FONTS, DAGOR_FILES, update_game_files), and TSSBOT/ (skeleton). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
483 lines
17 KiB
JavaScript
483 lines
17 KiB
JavaScript
// DOM Content Loaded
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Mobile Navigation
|
|
const hamburger = document.querySelector('.hamburger');
|
|
const navMenu = document.querySelector('.nav-menu');
|
|
const navLinks = document.querySelectorAll('.nav-link');
|
|
|
|
if (hamburger && navMenu) {
|
|
hamburger.addEventListener('click', function() {
|
|
hamburger.classList.toggle('active');
|
|
navMenu.classList.toggle('active');
|
|
|
|
// Prevent body scroll when menu is open
|
|
if (navMenu.classList.contains('active')) {
|
|
document.body.style.overflow = 'hidden';
|
|
} else {
|
|
document.body.style.overflow = '';
|
|
}
|
|
});
|
|
|
|
// Keyboard navigation support
|
|
hamburger.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
hamburger.click();
|
|
}
|
|
});
|
|
|
|
// Close mobile menu when clicking on nav links
|
|
navLinks.forEach(link => {
|
|
link.addEventListener('click', function() {
|
|
hamburger.classList.remove('active');
|
|
navMenu.classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
});
|
|
});
|
|
|
|
// Close mobile menu when clicking outside
|
|
document.addEventListener('click', function(e) {
|
|
if (hamburger && navMenu && !hamburger.contains(e.target) && !navMenu.contains(e.target)) {
|
|
hamburger.classList.remove('active');
|
|
navMenu.classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
}
|
|
});
|
|
|
|
// Close mobile menu on window resize if desktop size
|
|
window.addEventListener('resize', function() {
|
|
if (window.innerWidth > 768) {
|
|
hamburger.classList.remove('active');
|
|
navMenu.classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Smooth Scrolling for Navigation Links
|
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
anchor.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
const target = document.querySelector(this.getAttribute('href'));
|
|
if (target) {
|
|
target.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Button Click Handlers
|
|
const inviteButtons = [
|
|
'inviteBtn',
|
|
'inviteBtnMobile',
|
|
'heroInviteBtn',
|
|
'ctaInviteBtn',
|
|
'footerInviteBtn',
|
|
'freePlanInviteBtn'
|
|
];
|
|
|
|
const supportButtons = [
|
|
'supportBtn',
|
|
'heroSupportBtn',
|
|
'footerSupportBtn'
|
|
];
|
|
|
|
// Handle Invite Button Clicks
|
|
inviteButtons.forEach(buttonId => {
|
|
const button = document.getElementById(buttonId);
|
|
if (button) {
|
|
button.addEventListener('click', function() {
|
|
handleInviteClick();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Handle Support Button Clicks
|
|
supportButtons.forEach(buttonId => {
|
|
const button = document.getElementById(buttonId);
|
|
if (button) {
|
|
button.addEventListener('click', function() {
|
|
handleSupportClick();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Fetch and Update Stats
|
|
updateStats();
|
|
|
|
// Update stats every 30 seconds
|
|
setInterval(updateStats, 30000);
|
|
|
|
// New nav mobile menu toggle
|
|
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
|
|
const mobileMenu = document.getElementById('mobileMenu');
|
|
if (mobileMenuBtn && mobileMenu) {
|
|
mobileMenuBtn.addEventListener('click', () => {
|
|
mobileMenu.classList.toggle('hidden');
|
|
});
|
|
}
|
|
|
|
// Navbar Scroll Effect
|
|
const navbar = document.querySelector('.navbar');
|
|
if (navbar) {
|
|
window.addEventListener('scroll', function() {
|
|
if (window.scrollY > 100) {
|
|
navbar.style.background = 'rgba(13, 14, 15, 0.98)';
|
|
} else {
|
|
navbar.style.background = 'rgba(13, 14, 15, 1)';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Animate Numbers on Stats Section Intersection
|
|
const statsSection = document.querySelector('#stats');
|
|
if (statsSection) {
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
animateNumbers();
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.5 });
|
|
|
|
observer.observe(statsSection);
|
|
}
|
|
|
|
// Handle touch events for better mobile experience
|
|
let touchStartY = 0;
|
|
let touchEndY = 0;
|
|
|
|
document.addEventListener('touchstart', function(e) {
|
|
touchStartY = e.changedTouches[0].screenY;
|
|
}, { passive: true });
|
|
|
|
document.addEventListener('touchend', function(e) {
|
|
touchEndY = e.changedTouches[0].screenY;
|
|
handleSwipe();
|
|
}, { passive: true });
|
|
|
|
function handleSwipe() {
|
|
const swipeThreshold = 50;
|
|
const diff = touchStartY - touchEndY;
|
|
|
|
// Close mobile menu on upward swipe when menu is open
|
|
if (navMenu && hamburger && navMenu.classList.contains('active') && diff > swipeThreshold) {
|
|
hamburger.classList.remove('active');
|
|
navMenu.classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle Invite Button Click
|
|
async function handleInviteClick() {
|
|
try {
|
|
// Direct invite URL for Toothless SQB Bot
|
|
const inviteUrl = 'https://discord.com/oauth2/authorize?client_id=1254679514466877540&permissions=2048&scope=bot%20applications.commands';
|
|
|
|
window.open(inviteUrl, '_blank');
|
|
showNotification(window.__t ? __t('js.openingDiscordInvite') : 'Opening Discord invite!', 'success');
|
|
} catch (error) {
|
|
console.error('Error opening invite link:', error);
|
|
showNotification(window.__t ? __t('js.errorOpeningInvite') : 'Error opening invite link. Please try again later.', 'error');
|
|
|
|
// Fallback - same URL but ensure it opens
|
|
const fallbackUrl = 'https://discord.com/oauth2/authorize?client_id=1254679514466877540&permissions=2048&scope=bot%20applications.commands';
|
|
window.open(fallbackUrl, '_blank');
|
|
}
|
|
}
|
|
|
|
// Handle Support Button Click
|
|
async function handleSupportClick() {
|
|
try {
|
|
showNotification(window.__t ? __t('js.gettingSupportLink') : 'Getting support server link...', 'info');
|
|
|
|
const response = await fetch('/api/support');
|
|
const data = await response.json();
|
|
|
|
if (data.supportUrl) {
|
|
window.open(data.supportUrl, '_blank');
|
|
showNotification(window.__t ? __t('js.openingSupportServer') : 'Opening support server!', 'success');
|
|
} else {
|
|
throw new Error('No support URL received');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error getting support link:', error);
|
|
showNotification(window.__t ? __t('js.errorGettingSupport') : 'Error getting support link. Please try again later.', 'error');
|
|
|
|
// Fallback - Real support server invite
|
|
const fallbackUrl = 'https://discord.gg/BCvkK8JhPe';
|
|
window.open(fallbackUrl, '_blank');
|
|
}
|
|
}
|
|
|
|
// Update Stats from API
|
|
async function updateStats() {
|
|
try {
|
|
let stats;
|
|
|
|
// Check if API client is available, if not use fallback
|
|
if (window.apiClient && window.apiClient.getStats) {
|
|
stats = await window.apiClient.getStats();
|
|
} else {
|
|
// Fallback for pages without API client
|
|
const response = await fetch('/api/stats');
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
stats = await response.json();
|
|
}
|
|
|
|
// Update server count
|
|
const serverCountEl = document.getElementById('serverCount');
|
|
if (serverCountEl && stats.servers) {
|
|
serverCountEl.textContent = formatNumber(stats.servers);
|
|
}
|
|
|
|
// Update user count
|
|
const userCountEl = document.getElementById('userCount');
|
|
if (userCountEl && stats.users) {
|
|
userCountEl.textContent = formatNumber(stats.users) + '+';
|
|
}
|
|
|
|
// Update command count
|
|
const commandCountEl = document.getElementById('commandCount');
|
|
if (commandCountEl && stats.commands) {
|
|
commandCountEl.textContent = stats.commands + '+';
|
|
}
|
|
|
|
} catch (error) {
|
|
// Silently ignore — stat counters are non-critical and failures flash an annoying banner
|
|
}
|
|
}
|
|
|
|
// Format numbers with commas
|
|
function formatNumber(num) {
|
|
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
}
|
|
|
|
// Animate numbers when stats section comes into view
|
|
function animateNumbers() {
|
|
const numbers = document.querySelectorAll('.stat-number');
|
|
|
|
numbers.forEach(number => {
|
|
const target = parseInt(number.textContent.replace(/[^0-9]/g, ''));
|
|
const duration = 2000;
|
|
const start = performance.now();
|
|
|
|
function updateNumber(currentTime) {
|
|
const elapsed = currentTime - start;
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
|
|
// Easing function
|
|
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
|
const current = Math.floor(target * easeOutQuart);
|
|
|
|
if (number.textContent.includes('+')) {
|
|
number.textContent = formatNumber(current) + '+';
|
|
} else {
|
|
number.textContent = formatNumber(current);
|
|
}
|
|
|
|
if (progress < 1) {
|
|
requestAnimationFrame(updateNumber);
|
|
}
|
|
}
|
|
|
|
requestAnimationFrame(updateNumber);
|
|
});
|
|
}
|
|
|
|
// Show notification system
|
|
function showNotification(message, type = 'info') {
|
|
// Remove existing notifications
|
|
const existingNotification = document.querySelector('.notification');
|
|
if (existingNotification) {
|
|
existingNotification.remove();
|
|
}
|
|
|
|
// Create notification element
|
|
const notification = document.createElement('div');
|
|
notification.className = `notification notification-${type}`;
|
|
notification.textContent = message;
|
|
|
|
// Style the notification
|
|
notification.style.cssText = `
|
|
position: fixed;
|
|
top: 100px;
|
|
right: 20px;
|
|
background: ${type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3'};
|
|
color: white;
|
|
padding: 15px 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
z-index: 10000;
|
|
font-weight: 500;
|
|
opacity: 0;
|
|
transform: translateX(100%);
|
|
transition: all 0.3s ease;
|
|
`;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
// Animate in
|
|
setTimeout(() => {
|
|
notification.style.opacity = '1';
|
|
notification.style.transform = 'translateX(0)';
|
|
}, 100);
|
|
|
|
// Auto remove after 3 seconds
|
|
setTimeout(() => {
|
|
notification.style.opacity = '0';
|
|
notification.style.transform = 'translateX(100%)';
|
|
setTimeout(() => {
|
|
if (notification.parentNode) {
|
|
notification.remove();
|
|
}
|
|
}, 300);
|
|
}, 3000);
|
|
}
|
|
|
|
// Easter egg - Konami code
|
|
let konamiCode = [];
|
|
const konamiSequence = [
|
|
'ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown',
|
|
'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight',
|
|
'KeyB', 'KeyA'
|
|
];
|
|
|
|
function triggerKonamiEasterEgg() {
|
|
var t = typeof __t === 'function' ? __t : null;
|
|
|
|
// --- 1. Screen shake ---
|
|
document.body.style.transition = 'none';
|
|
var shakeFrames = [
|
|
'3px 0', '-3px 1px', '2px -1px', '-2px 2px',
|
|
'1px -2px', '-1px 1px', '2px 0', '0 0'
|
|
];
|
|
var si = 0;
|
|
var shakeInterval = setInterval(function () {
|
|
if (si >= shakeFrames.length) { clearInterval(shakeInterval); document.body.style.transform = ''; return; }
|
|
document.body.style.transform = 'translate(' + shakeFrames[si] + ')';
|
|
si++;
|
|
}, 40);
|
|
|
|
// --- 2. Confetti burst ---
|
|
var colors = ['#ff6b6b', '#ffd93d', '#6bcb77', '#4d96ff', '#ff922b', '#cc5de8', '#20c997'];
|
|
var confettiCount = 80;
|
|
var container = document.createElement('div');
|
|
container.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:99999;overflow:hidden;';
|
|
document.body.appendChild(container);
|
|
for (var i = 0; i < confettiCount; i++) {
|
|
var piece = document.createElement('div');
|
|
var size = Math.random() * 8 + 4;
|
|
var color = colors[Math.floor(Math.random() * colors.length)];
|
|
var startX = 50 + (Math.random() - 0.5) * 20;
|
|
var startY = 50 + (Math.random() - 0.5) * 10;
|
|
var dx = (Math.random() - 0.5) * 120;
|
|
var dy = -(Math.random() * 60 + 30);
|
|
var rot = Math.random() * 720 - 360;
|
|
var dur = Math.random() * 1.5 + 1.5;
|
|
piece.style.cssText =
|
|
'position:absolute;width:' + size + 'px;height:' + (size * 0.6) + 'px;' +
|
|
'background:' + color + ';border-radius:2px;' +
|
|
'left:' + startX + '%;top:' + startY + '%;' +
|
|
'opacity:1;pointer-events:none;';
|
|
piece.animate([
|
|
{ transform: 'translate(0,0) rotate(0deg)', opacity: 1 },
|
|
{ transform: 'translate(' + dx + 'vw,' + dy + 'vh) rotate(' + rot + 'deg)', opacity: 0 }
|
|
], { duration: dur * 1000, easing: 'cubic-bezier(.25,.8,.25,1)', fill: 'forwards' });
|
|
container.appendChild(piece);
|
|
}
|
|
setTimeout(function () { container.remove(); }, 4000);
|
|
|
|
// --- 3. Barrel roll ---
|
|
setTimeout(function () {
|
|
var style = document.createElement('style');
|
|
style.textContent = '@keyframes konamiBarrelRoll{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}';
|
|
document.head.appendChild(style);
|
|
document.body.style.transformOrigin = 'center center';
|
|
document.body.style.animation = 'konamiBarrelRoll 1s ease-in-out';
|
|
document.body.addEventListener('animationend', function handler() {
|
|
document.body.style.animation = '';
|
|
document.body.style.transformOrigin = '';
|
|
style.remove();
|
|
document.body.removeEventListener('animationend', handler);
|
|
});
|
|
}, 350);
|
|
|
|
// --- 4. Themed notification ---
|
|
var msg = t ? t('js.konamiActivated') : 'Achievement Unlocked: Secret Code!';
|
|
var notif = document.createElement('div');
|
|
notif.style.cssText =
|
|
'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(0);' +
|
|
'background:linear-gradient(135deg,rgba(30,30,30,0.95),rgba(50,50,50,0.95));' +
|
|
'border:2px solid #ffd93d;color:#ffd93d;padding:20px 40px;border-radius:12px;' +
|
|
'z-index:100000;font-size:1.4rem;font-weight:700;text-align:center;' +
|
|
'box-shadow:0 0 40px rgba(255,217,61,0.3);pointer-events:none;' +
|
|
'font-family:inherit;letter-spacing:1px;text-transform:uppercase;' +
|
|
'transition:transform 0.4s cubic-bezier(.34,1.56,.64,1),opacity 0.3s ease;opacity:0;';
|
|
notif.textContent = msg;
|
|
document.body.appendChild(notif);
|
|
setTimeout(function () {
|
|
notif.style.transform = 'translate(-50%,-50%) scale(1)';
|
|
notif.style.opacity = '1';
|
|
}, 50);
|
|
setTimeout(function () {
|
|
notif.style.transform = 'translate(-50%,-50%) scale(0.8)';
|
|
notif.style.opacity = '0';
|
|
setTimeout(function () { notif.remove(); }, 400);
|
|
}, 3000);
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
konamiCode.push(e.code);
|
|
|
|
if (konamiCode.length > konamiSequence.length) {
|
|
konamiCode.shift();
|
|
}
|
|
|
|
if (konamiCode.join(',') === konamiSequence.join(',')) {
|
|
triggerKonamiEasterEgg();
|
|
konamiCode = [];
|
|
}
|
|
});
|
|
|
|
// Mobile menu toggle
|
|
function toggleMobileMenu() {
|
|
const navMenu = document.querySelector('.nav-menu');
|
|
const hamburger = document.querySelector('.hamburger');
|
|
|
|
navMenu.classList.toggle('active');
|
|
hamburger.classList.toggle('active');
|
|
}
|
|
|
|
// Languages dropdown toggle
|
|
function toggleLanguagesList() {
|
|
const languagesList = document.getElementById('languagesList');
|
|
const dropdownToggle = document.querySelector('.dropdown-toggle');
|
|
|
|
languagesList.classList.toggle('show');
|
|
dropdownToggle.classList.toggle('active');
|
|
}
|
|
|
|
// Language switcher (ENG/RUS)
|
|
function switchLanguage(lang) {
|
|
const next = lang || (document.documentElement.lang === 'en' ? 'ru' : 'en');
|
|
if (next === document.documentElement.lang) return;
|
|
document.cookie = 'lang=' + next + ';path=/;max-age=31536000;SameSite=Lax';
|
|
window.location.reload();
|
|
}
|
|
|
|
// Language dropdown: close on outside click
|
|
document.addEventListener('click', function(e) {
|
|
var dd = document.getElementById('langDropdown');
|
|
if (dd && !dd.contains(e.target)) {
|
|
dd.classList.remove('open');
|
|
}
|
|
}); |