Initial commit: SREBOT website (Express/EJS + i18n) - extracted from SREBOT monorepo

This commit is contained in:
clxud
2026-07-02 02:35:56 +00:00
commit 7f2ab08adc
145 changed files with 148257 additions and 0 deletions
+483
View File
@@ -0,0 +1,483 @@
// 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');
}
});