AeThex-Bot-Master/aethex-bot/public/dashboard.html
sirpiglr be74f5f0bf Update web server and dashboard styling for improved user experience
Refactor bot.js to separate HTTP server logic and introduce Express web portal. Update dashboard.html with new styling, color themes, and font imports.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 17beead7-b08f-41f9-a6ea-f8e2ff20a502
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/b2aEQYO
Replit-Helium-Checkpoint-Created: true
2025-12-08 23:59:05 +00:00

1115 lines
35 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - AeThex Bot</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--background: hsl(222, 84%, 4.9%);
--foreground: hsl(210, 40%, 98%);
--card: hsl(222, 84%, 6%);
--card-hover: hsl(222, 84%, 8%);
--primary: hsl(250, 100%, 60%);
--secondary: hsl(217.2, 32.6%, 17.5%);
--muted: hsl(217.2, 32.6%, 17.5%);
--muted-foreground: hsl(215, 20.2%, 65.1%);
--border: hsl(217.2, 32.6%, 17.5%);
--aethex-400: hsl(250, 100%, 70%);
--aethex-500: hsl(250, 100%, 60%);
--neon-purple: hsl(280, 100%, 70%);
--neon-blue: hsl(210, 100%, 70%);
--neon-green: hsl(120, 100%, 70%);
--success: hsl(142, 76%, 36%);
--warning: hsl(38, 92%, 50%);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--background);
color: var(--foreground);
min-height: 100vh;
}
.wallpaper {
position: fixed;
inset: 0;
background-image: linear-gradient(135deg, rgba(167, 139, 250, 0.05) 0%, rgba(96, 165, 250, 0.05) 100%);
pointer-events: none;
z-index: -1;
}
.app {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 260px;
background: rgba(10, 10, 20, 0.95);
border-right: 1px solid var(--border);
padding: 1.5rem;
display: flex;
flex-direction: column;
position: fixed;
height: 100vh;
overflow-y: auto;
}
.logo {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 2rem;
text-decoration: none;
color: var(--foreground);
}
.logo-icon {
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--aethex-500), var(--neon-blue));
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
}
.logo-text {
font-size: 1.25rem;
font-weight: 700;
background: linear-gradient(to right, var(--aethex-400), var(--neon-blue));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.guild-selector {
margin-bottom: 1.5rem;
}
.guild-selector select {
width: 100%;
padding: 0.75rem;
background: var(--secondary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--foreground);
font-size: 0.875rem;
cursor: pointer;
}
.nav-section {
margin-bottom: 1.5rem;
}
.nav-section-title {
font-size: 0.75rem;
text-transform: uppercase;
color: var(--muted-foreground);
margin-bottom: 0.75rem;
font-weight: 600;
letter-spacing: 0.05em;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 8px;
color: var(--muted-foreground);
text-decoration: none;
transition: all 0.2s;
cursor: pointer;
margin-bottom: 0.25rem;
}
.nav-item:hover, .nav-item.active {
background: var(--secondary);
color: var(--foreground);
}
.nav-item.active {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(59, 130, 246, 0.1));
border: 1px solid rgba(139, 92, 246, 0.3);
}
.nav-icon { width: 20px; text-align: center; }
.user-card {
margin-top: auto;
padding: 1rem;
background: var(--secondary);
border-radius: 12px;
display: flex;
align-items: center;
gap: 0.75rem;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--aethex-500);
}
.user-info {
flex: 1;
min-width: 0;
}
.user-name {
font-weight: 600;
font-size: 0.875rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-level {
font-size: 0.75rem;
color: var(--muted-foreground);
}
.main {
flex: 1;
margin-left: 260px;
padding: 2rem;
max-width: calc(100vw - 260px);
}
.page-header {
margin-bottom: 2rem;
}
.page-title {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.page-subtitle {
color: var(--muted-foreground);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.1), rgba(59, 130, 246, 0.05));
border: 1px solid rgba(139, 92, 246, 0.2);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s;
}
.stat-card:hover {
border-color: var(--aethex-500);
transform: translateY(-2px);
}
.stat-label {
font-size: 0.875rem;
color: var(--muted-foreground);
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--aethex-400);
}
.stat-sub {
font-size: 0.75rem;
color: var(--muted-foreground);
margin-top: 0.25rem;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
margin-bottom: 1.5rem;
}
.card-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 1.125rem;
font-weight: 600;
}
.card-body { padding: 1.5rem; }
.progress-bar {
height: 8px;
background: var(--secondary);
border-radius: 4px;
overflow: hidden;
margin-top: 0.5rem;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--aethex-500), var(--neon-blue));
border-radius: 4px;
transition: width 0.5s ease;
}
.achievement-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.achievement-item {
display: flex;
gap: 1rem;
padding: 1rem;
background: var(--secondary);
border-radius: 10px;
transition: all 0.2s;
}
.achievement-item:hover {
background: var(--card-hover);
}
.achievement-item.locked {
opacity: 0.5;
}
.achievement-icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, var(--aethex-500), var(--neon-blue));
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
flex-shrink: 0;
}
.achievement-info h4 {
font-weight: 600;
margin-bottom: 0.25rem;
}
.achievement-info p {
font-size: 0.875rem;
color: var(--muted-foreground);
}
.quest-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: var(--secondary);
border-radius: 10px;
margin-bottom: 0.75rem;
}
.quest-info { flex: 1; }
.quest-name {
font-weight: 600;
margin-bottom: 0.25rem;
}
.quest-desc {
font-size: 0.875rem;
color: var(--muted-foreground);
}
.quest-progress {
text-align: right;
min-width: 100px;
}
.quest-progress-text {
font-weight: 600;
color: var(--aethex-400);
}
.shop-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.shop-item {
background: var(--secondary);
border-radius: 12px;
padding: 1.25rem;
text-align: center;
transition: all 0.3s;
border: 1px solid transparent;
}
.shop-item:hover {
border-color: var(--aethex-500);
transform: translateY(-2px);
}
.shop-icon {
font-size: 2.5rem;
margin-bottom: 0.75rem;
}
.shop-name {
font-weight: 600;
margin-bottom: 0.5rem;
}
.shop-price {
color: var(--aethex-400);
font-weight: 600;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 600;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 0.875rem;
}
.btn-primary {
background: linear-gradient(135deg, var(--aethex-500), var(--neon-blue));
color: white;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
}
.btn-secondary {
background: var(--secondary);
color: var(--foreground);
border: 1px solid var(--border);
}
.leaderboard-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 0.5rem;
background: var(--secondary);
}
.leaderboard-item.top-3 {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(59, 130, 246, 0.1));
border: 1px solid rgba(139, 92, 246, 0.3);
}
.leaderboard-rank {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
border-radius: 8px;
background: var(--muted);
}
.leaderboard-item.top-3 .leaderboard-rank {
background: linear-gradient(135deg, var(--aethex-500), var(--neon-blue));
}
.leaderboard-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--aethex-500);
}
.leaderboard-name { flex: 1; font-weight: 500; }
.leaderboard-xp { color: var(--aethex-400); font-weight: 600; }
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--border);
padding-bottom: 0.5rem;
}
.tab {
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
color: var(--muted-foreground);
transition: all 0.2s;
}
.tab:hover { color: var(--foreground); }
.tab.active {
background: var(--aethex-500);
color: white;
}
.empty-state {
text-align: center;
padding: 3rem;
color: var(--muted-foreground);
}
.empty-state-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.login-prompt {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
text-align: center;
padding: 2rem;
}
.login-prompt h1 {
font-size: 2rem;
margin-bottom: 1rem;
}
.login-prompt p {
color: var(--muted-foreground);
margin-bottom: 2rem;
}
.hidden { display: none !important; }
.loading-spinner {
display: inline-block;
width: 24px;
height: 24px;
border: 3px solid var(--secondary);
border-top-color: var(--aethex-500);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.text-gradient {
background: linear-gradient(to right, var(--aethex-400), var(--neon-blue));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
z-index: 100;
transition: transform 0.3s;
}
.sidebar.open { transform: translateX(0); }
.main { margin-left: 0; max-width: 100%; }
}
</style>
</head>
<body>
<div class="wallpaper"></div>
<div id="loginPrompt" class="login-prompt">
<h1>Welcome to <span class="text-gradient">AeThex</span></h1>
<p>Login with Discord to view your profile, achievements, and more.</p>
<a href="/auth/discord" class="btn btn-primary">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/></svg>
Login with Discord
</a>
</div>
<div id="app" class="app hidden">
<aside class="sidebar">
<a href="/" class="logo">
<div class="logo-icon">A</div>
<span class="logo-text">AeThex</span>
</a>
<div class="guild-selector">
<select id="guildSelect">
<option value="">Select a server...</option>
</select>
</div>
<nav>
<div class="nav-section">
<div class="nav-section-title">Overview</div>
<div class="nav-item active" data-page="profile">
<span class="nav-icon">👤</span> Profile
</div>
<div class="nav-item" data-page="leaderboard">
<span class="nav-icon">🏆</span> Leaderboard
</div>
</div>
<div class="nav-section">
<div class="nav-section-title">Rewards</div>
<div class="nav-item" data-page="achievements">
<span class="nav-icon">🎖️</span> Achievements
</div>
<div class="nav-item" data-page="quests">
<span class="nav-icon">🎯</span> Quests
</div>
<div class="nav-item" data-page="shop">
<span class="nav-icon">🛒</span> Shop
</div>
<div class="nav-item" data-page="inventory">
<span class="nav-icon">🎒</span> Inventory
</div>
</div>
<div class="nav-section" id="adminSection" style="display:none">
<div class="nav-section-title">Admin</div>
<div class="nav-item" data-page="admin-xp">
<span class="nav-icon">⚙️</span> XP Settings
</div>
<div class="nav-item" data-page="admin-quests">
<span class="nav-icon">📝</span> Manage Quests
</div>
<div class="nav-item" data-page="admin-achievements">
<span class="nav-icon">🏅</span> Manage Achievements
</div>
<div class="nav-item" data-page="admin-shop">
<span class="nav-icon">🏪</span> Manage Shop
</div>
</div>
</nav>
<div class="user-card" id="userCard">
<img class="user-avatar" id="userAvatar" src="" alt="">
<div class="user-info">
<div class="user-name" id="userName">Loading...</div>
<div class="user-level" id="userLevel">Level 0</div>
</div>
<a href="/auth/logout" class="btn btn-secondary" style="padding:0.5rem">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg>
</a>
</div>
</aside>
<main class="main">
<div id="page-profile" class="page">
<div class="page-header">
<h1 class="page-title">Your <span class="text-gradient">Profile</span></h1>
<p class="page-subtitle">Track your progress and stats</p>
</div>
<div class="stats-grid" id="statsGrid">
<div class="stat-card">
<div class="stat-label">Total XP</div>
<div class="stat-value" id="statXp">0</div>
<div class="stat-sub" id="statXpProgress">0 XP to next level</div>
</div>
<div class="stat-card">
<div class="stat-label">Level</div>
<div class="stat-value" id="statLevel">0</div>
<div class="stat-sub" id="statPrestige">Prestige 0</div>
</div>
<div class="stat-card">
<div class="stat-label">Messages</div>
<div class="stat-value" id="statMessages">0</div>
<div class="stat-sub">All time</div>
</div>
<div class="stat-card">
<div class="stat-label">Voice Time</div>
<div class="stat-value" id="statVoice">0h</div>
<div class="stat-sub">All time</div>
</div>
<div class="stat-card">
<div class="stat-label">Daily Streak</div>
<div class="stat-value" id="statStreak">0</div>
<div class="stat-sub">days</div>
</div>
<div class="stat-card">
<div class="stat-label">Achievements</div>
<div class="stat-value" id="statAchievements">0</div>
<div class="stat-sub">earned</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Level Progress</h3>
</div>
<div class="card-body">
<div style="display:flex;justify-content:space-between;margin-bottom:0.5rem">
<span>Level <span id="currentLevel">0</span></span>
<span>Level <span id="nextLevel">1</span></span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="levelProgress" style="width:0%"></div>
</div>
<p style="margin-top:0.75rem;color:var(--muted-foreground);font-size:0.875rem">
<span id="xpCurrent">0</span> / <span id="xpRequired">100</span> XP
</p>
</div>
</div>
</div>
<div id="page-leaderboard" class="page hidden">
<div class="page-header">
<h1 class="page-title">Leaderboard</h1>
<p class="page-subtitle">See who's on top</p>
</div>
<div class="tabs">
<div class="tab active" data-lb-type="all">All Time</div>
<div class="tab" data-lb-type="monthly">Monthly</div>
<div class="tab" data-lb-type="weekly">Weekly</div>
</div>
<div class="card">
<div class="card-body" id="leaderboardList">
<div class="empty-state">
<div class="empty-state-icon">🏆</div>
<p>Select a server to view the leaderboard</p>
</div>
</div>
</div>
</div>
<div id="page-achievements" class="page hidden">
<div class="page-header">
<h1 class="page-title">Achievements</h1>
<p class="page-subtitle">Collect badges and show off your accomplishments</p>
</div>
<div class="achievement-grid" id="achievementGrid">
<div class="empty-state" style="grid-column:1/-1">
<div class="empty-state-icon">🎖️</div>
<p>Select a server to view achievements</p>
</div>
</div>
</div>
<div id="page-quests" class="page hidden">
<div class="page-header">
<h1 class="page-title">Quests</h1>
<p class="page-subtitle">Complete challenges to earn rewards</p>
</div>
<div class="card">
<div class="card-body" id="questList">
<div class="empty-state">
<div class="empty-state-icon">🎯</div>
<p>Select a server to view quests</p>
</div>
</div>
</div>
</div>
<div id="page-shop" class="page hidden">
<div class="page-header">
<h1 class="page-title">Shop</h1>
<p class="page-subtitle">Spend your XP on rewards</p>
</div>
<div class="shop-grid" id="shopGrid">
<div class="empty-state" style="grid-column:1/-1">
<div class="empty-state-icon">🛒</div>
<p>Select a server to view shop items</p>
</div>
</div>
</div>
<div id="page-inventory" class="page hidden">
<div class="page-header">
<h1 class="page-title">Inventory</h1>
<p class="page-subtitle">Your purchased items and perks</p>
</div>
<div class="shop-grid" id="inventoryGrid">
<div class="empty-state" style="grid-column:1/-1">
<div class="empty-state-icon">🎒</div>
<p>Your inventory is empty</p>
</div>
</div>
</div>
<div id="page-admin-xp" class="page hidden">
<div class="page-header">
<h1 class="page-title">XP Settings</h1>
<p class="page-subtitle">Configure XP earning for your server</p>
</div>
<div class="card">
<div class="card-body">
<p style="color:var(--muted-foreground)">Use <code>/xp-settings</code> in Discord to configure XP settings. Web-based configuration coming soon!</p>
</div>
</div>
</div>
<div id="page-admin-quests" class="page hidden">
<div class="page-header">
<h1 class="page-title">Manage Quests</h1>
<p class="page-subtitle">Create and edit server quests</p>
</div>
<div class="card">
<div class="card-body">
<p style="color:var(--muted-foreground)">Use <code>/quests-manage</code> in Discord to manage quests. Web-based management coming soon!</p>
</div>
</div>
</div>
<div id="page-admin-achievements" class="page hidden">
<div class="page-header">
<h1 class="page-title">Manage Achievements</h1>
<p class="page-subtitle">Create custom achievements</p>
</div>
<div class="card">
<div class="card-body">
<p style="color:var(--muted-foreground)">Use <code>/achievements-manage</code> in Discord to manage achievements. Web-based management coming soon!</p>
</div>
</div>
</div>
<div id="page-admin-shop" class="page hidden">
<div class="page-header">
<h1 class="page-title">Manage Shop</h1>
<p class="page-subtitle">Configure shop items and prices</p>
</div>
<div class="card">
<div class="card-body">
<p style="color:var(--muted-foreground)">Use <code>/shop-manage</code> in Discord to manage shop items. Web-based management coming soon!</p>
</div>
</div>
</div>
</main>
</div>
<script>
let currentUser = null;
let currentGuild = null;
let currentPage = 'profile';
async function init() {
try {
const res = await fetch('/api/me');
if (!res.ok) {
document.getElementById('loginPrompt').classList.remove('hidden');
return;
}
currentUser = await res.json();
document.getElementById('loginPrompt').classList.add('hidden');
document.getElementById('app').classList.remove('hidden');
document.getElementById('userAvatar').src = currentUser.avatarUrl;
document.getElementById('userName').textContent = currentUser.globalName || currentUser.username;
const select = document.getElementById('guildSelect');
currentUser.guilds.forEach(g => {
const opt = document.createElement('option');
opt.value = g.id;
opt.textContent = g.name;
opt.dataset.isAdmin = g.isAdmin;
select.appendChild(opt);
});
select.addEventListener('change', () => {
currentGuild = select.value;
const isAdmin = select.selectedOptions[0]?.dataset.isAdmin === 'true';
document.getElementById('adminSection').style.display = isAdmin ? 'block' : 'none';
loadPageData();
});
if (currentUser.guilds.length > 0) {
select.value = currentUser.guilds[0].id;
select.dispatchEvent(new Event('change'));
}
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
currentPage = item.dataset.page;
showPage(currentPage);
});
});
document.querySelectorAll('[data-lb-type]').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('[data-lb-type]').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
loadLeaderboard(tab.dataset.lbType);
});
});
await loadProfile();
} catch (e) {
console.error('Init error:', e);
document.getElementById('loginPrompt').classList.remove('hidden');
}
}
function showPage(page) {
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
document.getElementById('page-' + page)?.classList.remove('hidden');
loadPageData();
}
async function loadPageData() {
if (!currentGuild) return;
switch (currentPage) {
case 'profile': await loadProfile(); break;
case 'leaderboard': await loadLeaderboard('all'); break;
case 'achievements': await loadAchievements(); break;
case 'quests': await loadQuests(); break;
case 'shop': await loadShop(); break;
case 'inventory': await loadInventory(); break;
}
}
async function loadProfile() {
if (!currentUser) return;
try {
const [profileRes, statsRes] = await Promise.all([
fetch('/api/profile/' + currentUser.id),
currentGuild ? fetch('/api/stats/' + currentUser.id + '/' + currentGuild) : null
]);
const profileData = await profileRes.json();
const statsData = statsRes ? await statsRes.json() : { stats: null };
if (profileData.linked && profileData.profile) {
const p = profileData.profile;
const xp = p.xp || 0;
const level = Math.floor(Math.sqrt(xp / 100));
const nextLevel = level + 1;
const currentLevelXp = level * level * 100;
const nextLevelXp = nextLevel * nextLevel * 100;
const progress = ((xp - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100;
document.getElementById('statXp').textContent = xp.toLocaleString();
document.getElementById('statLevel').textContent = level;
document.getElementById('statPrestige').textContent = 'Prestige ' + (p.prestigeLevel || 0);
document.getElementById('statStreak').textContent = p.dailyStreak || 0;
document.getElementById('userLevel').textContent = 'Level ' + level;
document.getElementById('currentLevel').textContent = level;
document.getElementById('nextLevel').textContent = nextLevel;
document.getElementById('xpCurrent').textContent = (xp - currentLevelXp).toLocaleString();
document.getElementById('xpRequired').textContent = (nextLevelXp - currentLevelXp).toLocaleString();
document.getElementById('levelProgress').style.width = progress + '%';
}
if (statsData.stats) {
const s = statsData.stats;
document.getElementById('statMessages').textContent = (s.messages_sent || 0).toLocaleString();
const voiceMinutes = s.voice_minutes || 0;
document.getElementById('statVoice').textContent = Math.floor(voiceMinutes / 60) + 'h';
}
} catch (e) {
console.error('Profile load error:', e);
}
}
async function loadLeaderboard(type) {
if (!currentGuild) return;
const container = document.getElementById('leaderboardList');
container.innerHTML = '<div class="loading-spinner"></div>';
try {
const res = await fetch('/api/leaderboard/' + currentGuild + '?type=' + type + '&limit=25');
const data = await res.json();
if (!data.leaderboard || data.leaderboard.length === 0) {
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🏆</div><p>No leaderboard data yet</p></div>';
return;
}
container.innerHTML = data.leaderboard.map((entry, i) => `
<div class="leaderboard-item ${i < 3 ? 'top-3' : ''}">
<div class="leaderboard-rank">${i + 1}</div>
<img class="leaderboard-avatar" src="${entry.user_profiles?.avatar_url || ''}" alt="">
<div class="leaderboard-name">${entry.user_profiles?.username || 'Unknown'}</div>
<div class="leaderboard-xp">${(entry.xp || 0).toLocaleString()} XP</div>
</div>
`).join('');
} catch (e) {
container.innerHTML = '<div class="empty-state"><p>Failed to load leaderboard</p></div>';
}
}
async function loadAchievements() {
if (!currentGuild) return;
const container = document.getElementById('achievementGrid');
container.innerHTML = '<div class="loading-spinner"></div>';
try {
const res = await fetch('/api/achievements/' + currentGuild);
const data = await res.json();
if (!data.achievements || data.achievements.length === 0) {
container.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><div class="empty-state-icon">🎖️</div><p>No achievements configured for this server</p></div>';
return;
}
const earnedIds = new Set(data.userAchievements?.map(a => a.achievement_id) || []);
document.getElementById('statAchievements').textContent = earnedIds.size;
container.innerHTML = data.achievements.map(a => `
<div class="achievement-item ${earnedIds.has(a.id) ? '' : 'locked'}">
<div class="achievement-icon">${a.icon || '🏅'}</div>
<div class="achievement-info">
<h4>${a.name}</h4>
<p>${a.description || ''}</p>
</div>
</div>
`).join('');
} catch (e) {
container.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><p>Failed to load achievements</p></div>';
}
}
async function loadQuests() {
if (!currentGuild) return;
const container = document.getElementById('questList');
container.innerHTML = '<div class="loading-spinner"></div>';
try {
const res = await fetch('/api/quests/' + currentGuild);
const data = await res.json();
if (!data.quests || data.quests.length === 0) {
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🎯</div><p>No active quests</p></div>';
return;
}
const userQuestMap = {};
(data.userQuests || []).forEach(uq => userQuestMap[uq.quest_id] = uq);
container.innerHTML = data.quests.map(q => {
const uq = userQuestMap[q.id];
const progress = uq?.progress || 0;
const pct = Math.min(100, (progress / q.target_value) * 100);
return `
<div class="quest-item">
<div class="quest-info">
<div class="quest-name">${q.name}</div>
<div class="quest-desc">${q.description || ''}</div>
<div class="progress-bar" style="margin-top:0.5rem">
<div class="progress-fill" style="width:${pct}%"></div>
</div>
</div>
<div class="quest-progress">
<div class="quest-progress-text">${progress}/${q.target_value}</div>
<div style="font-size:0.75rem;color:var(--muted-foreground)">${q.xp_reward} XP</div>
</div>
</div>
`;
}).join('');
} catch (e) {
container.innerHTML = '<div class="empty-state"><p>Failed to load quests</p></div>';
}
}
async function loadShop() {
if (!currentGuild) return;
const container = document.getElementById('shopGrid');
container.innerHTML = '<div class="loading-spinner"></div>';
try {
const res = await fetch('/api/shop/' + currentGuild);
const data = await res.json();
if (!data.items || data.items.length === 0) {
container.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><div class="empty-state-icon">🛒</div><p>No shop items available</p></div>';
return;
}
const icons = { title: '📛', badge: '🏅', color: '🎨', role: '👑', xp_boost: '⚡', custom: '✨' };
container.innerHTML = data.items.map(item => `
<div class="shop-item">
<div class="shop-icon">${icons[item.item_type] || '🎁'}</div>
<div class="shop-name">${item.name}</div>
<div class="shop-price">${item.price.toLocaleString()} XP</div>
</div>
`).join('');
} catch (e) {
container.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><p>Failed to load shop</p></div>';
}
}
async function loadInventory() {
if (!currentGuild) return;
const container = document.getElementById('inventoryGrid');
container.innerHTML = '<div class="loading-spinner"></div>';
try {
const res = await fetch('/api/inventory/' + currentGuild);
const data = await res.json();
if (!data.inventory || data.inventory.length === 0) {
container.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><div class="empty-state-icon">🎒</div><p>Your inventory is empty</p></div>';
return;
}
const icons = { title: '📛', badge: '🏅', color: '🎨', role: '👑', xp_boost: '⚡', custom: '✨' };
container.innerHTML = data.inventory.map(item => `
<div class="shop-item ${item.equipped ? 'equipped' : ''}">
<div class="shop-icon">${icons[item.shop_items?.item_type] || '🎁'}</div>
<div class="shop-name">${item.shop_items?.name || 'Unknown Item'}</div>
<div style="font-size:0.75rem;color:var(--muted-foreground)">${item.equipped ? 'Equipped' : ''}</div>
</div>
`).join('');
} catch (e) {
container.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><p>Failed to load inventory</p></div>';
}
}
init();
</script>
</body>
</html>