AeThex-Bot-Master/aethex-bot/public/dashboard.html
sirpiglr cbb7619552 Add new admin pages and update XP configuration endpoint
Introduces new API endpoints for XP configuration and quests, along with frontend updates in dashboard.html for styling and field name consistency.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 4b20cda7-6144-4154-8c0b-880fe406cb59
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/xfdSNeM
Replit-Helium-Checkpoint-Created: true
2025-12-09 03:04:32 +00:00

2209 lines
80 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=Fira+Code:wght@300;400;500;600;700&family=Source+Code+Pro:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--background: hsl(220, 50%, 4%);
--foreground: hsl(210, 20%, 98%);
--card: hsl(220, 50%, 6%);
--card-hover: hsl(220, 50%, 8%);
--primary: hsl(220, 100%, 60%);
--secondary: hsl(220, 30%, 15%);
--muted: hsl(220, 30%, 15%);
--muted-foreground: hsl(215, 15%, 65%);
--border: hsl(220, 30%, 20%);
--aethex-400: hsl(220, 100%, 75%);
--aethex-500: hsl(220, 100%, 65%);
--neon-blue: hsl(210, 100%, 70%);
--neon-cyan: hsl(190, 100%, 70%);
--neon-purple: hsl(270, 100%, 70%);
--success: hsl(142, 76%, 36%);
--warning: hsl(38, 92%, 50%);
--danger: hsl(0, 72%, 51%);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
.admin-form { margin-bottom: 2rem; }
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-size: 0.875rem;
font-weight: 500;
color: var(--foreground);
}
.form-input {
padding: 0.75rem 1rem;
background: var(--secondary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--foreground);
font-family: inherit;
font-size: 0.875rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-input:focus {
outline: none;
border-color: var(--aethex-500);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}
.form-input::placeholder {
color: var(--muted-foreground);
}
.form-hint {
font-size: 0.75rem;
color: var(--muted-foreground);
}
.form-toggle {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
user-select: none;
}
.form-toggle input {
display: none;
}
.toggle-slider {
width: 44px;
height: 24px;
background: var(--secondary);
border: 1px solid var(--border);
border-radius: 12px;
position: relative;
transition: all 0.2s;
}
.toggle-slider::before {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
background: var(--muted-foreground);
border-radius: 50%;
transition: all 0.2s;
}
.form-toggle input:checked + .toggle-slider {
background: var(--aethex-500);
border-color: var(--aethex-500);
}
.form-toggle input:checked + .toggle-slider::before {
left: 22px;
background: white;
}
.toggle-label {
font-size: 0.875rem;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
justify-content: flex-end;
}
.save-status {
margin-top: 1rem;
padding: 0.75rem 1rem;
border-radius: 8px;
font-size: 0.875rem;
}
.save-status.success {
background: rgba(34, 197, 94, 0.1);
border: 1px solid var(--success);
color: var(--success);
}
.save-status.error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid var(--danger);
color: var(--danger);
}
textarea.form-input {
min-height: 100px;
resize: vertical;
}
.item-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.item-row {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: var(--secondary);
border-radius: 10px;
transition: all 0.2s;
}
.item-row:hover {
background: var(--card-hover);
}
.item-info {
flex: 1;
min-width: 0;
}
.item-name {
font-weight: 600;
margin-bottom: 0.25rem;
}
.item-desc {
font-size: 0.875rem;
color: var(--muted-foreground);
}
.item-actions {
display: flex;
gap: 0.5rem;
}
.btn-icon {
padding: 0.5rem;
background: transparent;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--muted-foreground);
cursor: pointer;
transition: all 0.2s;
}
.btn-icon:hover {
background: var(--secondary);
color: var(--foreground);
}
.btn-icon.danger:hover {
background: rgba(239, 68, 68, 0.1);
border-color: var(--danger);
color: var(--danger);
}
.modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal-content {
background: var(--card);
border: 1px solid var(--border);
border-radius: 16px;
max-width: 600px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
body {
font-family: 'Fira Code', 'Source Code Pro', monospace;
background: var(--background);
color: var(--foreground);
min-height: 100vh;
}
.wallpaper {
position: fixed;
inset: 0;
background-image: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 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>
<form id="xpSettingsForm" class="admin-form">
<div class="card">
<div class="card-header">
<h3 class="card-title">Message XP</h3>
</div>
<div class="card-body">
<div class="form-grid">
<div class="form-group">
<label for="xpMsgMin">Minimum XP per Message</label>
<input type="number" id="xpMsgMin" name="message_xp_min" min="0" max="1000" value="15" class="form-input">
<span class="form-hint">Minimum XP earned per message</span>
</div>
<div class="form-group">
<label for="xpMsgMax">Maximum XP per Message</label>
<input type="number" id="xpMsgMax" name="message_xp_max" min="0" max="1000" value="25" class="form-input">
<span class="form-hint">Maximum XP earned per message</span>
</div>
<div class="form-group">
<label for="xpMsgCooldown">Message Cooldown (seconds)</label>
<input type="number" id="xpMsgCooldown" name="message_cooldown" min="0" max="3600" value="60" class="form-input">
<span class="form-hint">Time between earning XP from messages</span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Other XP Sources</h3>
</div>
<div class="card-body">
<div class="form-grid">
<div class="form-group">
<label for="xpReaction">XP per Reaction</label>
<input type="number" id="xpReaction" name="reaction_xp" min="0" max="100" value="5" class="form-input">
<span class="form-hint">XP earned when someone reacts to your message</span>
</div>
<div class="form-group">
<label for="xpVoice">XP per Voice Minute</label>
<input type="number" id="xpVoice" name="voice_xp_per_minute" min="0" max="100" value="2" class="form-input">
<span class="form-hint">XP earned per minute in voice channels</span>
</div>
<div class="form-group">
<label for="xpDaily">Daily Claim XP</label>
<input type="number" id="xpDaily" name="daily_xp" min="0" max="10000" value="100" class="form-input">
<span class="form-hint">XP from daily claim command</span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Level-Up Settings</h3>
</div>
<div class="card-body">
<div class="form-grid">
<div class="form-group">
<label for="xpBase">XP Base Difficulty</label>
<input type="number" id="xpBase" name="xp_base" min="50" max="500" value="100" class="form-input">
<span class="form-hint">Higher = slower leveling (formula: level = sqrt(xp/base))</span>
</div>
<div class="form-group">
<label for="levelUpChannel">Level-Up Announcement Channel</label>
<select id="levelUpChannel" name="levelup_channel_id" class="form-input">
<option value="">Current Channel</option>
</select>
<span class="form-hint">Where to announce level-ups</span>
</div>
<div class="form-group">
<label class="form-toggle">
<input type="checkbox" id="levelUpEnabled" name="levelup_enabled" checked>
<span class="toggle-slider"></span>
<span class="toggle-label">Enable Level-Up Messages</span>
</label>
</div>
<div class="form-group">
<label class="form-toggle">
<input type="checkbox" id="levelUpDm" name="levelup_dm">
<span class="toggle-slider"></span>
<span class="toggle-label">Send Level-Up via DM</span>
</label>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">XP Multipliers</h3>
</div>
<div class="card-body">
<div class="form-grid">
<div class="form-group">
<label for="xpMultiplier">Global XP Multiplier</label>
<input type="number" id="xpMultiplier" name="xp_multiplier" min="0.1" max="10" step="0.1" value="1.0" class="form-input">
<span class="form-hint">Multiplies all XP earned (1.0 = normal)</span>
</div>
<div class="form-group">
<label for="weekendMultiplier">Weekend Multiplier</label>
<input type="number" id="weekendMultiplier" name="weekend_multiplier" min="1" max="10" step="0.1" value="1.5" class="form-input">
<span class="form-hint">Extra multiplier on weekends</span>
</div>
</div>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="loadXpConfig()">Reset</button>
<button type="submit" class="btn btn-primary">Save Settings</button>
</div>
<div id="xpSaveStatus" class="save-status hidden"></div>
</form>
</div>
<div id="page-admin-quests" class="page hidden">
<div class="page-header" style="display:flex;justify-content:space-between;align-items:flex-start">
<div>
<h1 class="page-title">Manage Quests</h1>
<p class="page-subtitle">Create and edit server quests</p>
</div>
<button class="btn btn-primary" onclick="openQuestModal()">+ Create Quest</button>
</div>
<div class="card">
<div class="card-body">
<div id="adminQuestList" class="item-list">
<div class="empty-state">
<div class="empty-state-icon">🎯</div>
<p>No quests configured yet</p>
</div>
</div>
</div>
</div>
<div id="questSaveStatus" class="save-status hidden"></div>
</div>
<div id="questModal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3 id="questModalTitle">Create Quest</h3>
<button class="btn-icon" onclick="closeQuestModal()">&times;</button>
</div>
<form id="questForm" onsubmit="saveQuest(event)">
<div class="modal-body">
<div class="form-grid" style="grid-template-columns:1fr">
<div class="form-group">
<label for="questName">Quest Name</label>
<input type="text" id="questName" class="form-input" required placeholder="e.g., Message Master">
</div>
<div class="form-group">
<label for="questDescription">Description</label>
<textarea id="questDescription" class="form-input" rows="2" placeholder="e.g., Send 50 messages in the server"></textarea>
</div>
<div class="form-grid">
<div class="form-group">
<label for="questType">Quest Type</label>
<select id="questType" class="form-input" required>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="special">Special</option>
</select>
</div>
<div class="form-group">
<label for="questObjective">Objective</label>
<select id="questObjective" class="form-input" required>
<option value="messages">Send Messages</option>
<option value="reactions">Add Reactions</option>
<option value="voice_minutes">Voice Chat (minutes)</option>
<option value="commands">Use Commands</option>
<option value="daily_claims">Claim Daily Rewards</option>
<option value="level_ups">Level Up</option>
<option value="xp_earned">Earn XP</option>
</select>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="questTarget">Target Amount</label>
<input type="number" id="questTarget" class="form-input" min="1" value="10" required>
</div>
<div class="form-group">
<label for="questXpReward">XP Reward</label>
<input type="number" id="questXpReward" class="form-input" min="1" value="100" required>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="questDuration">Duration (hours)</label>
<input type="number" id="questDuration" class="form-input" min="0" value="24" placeholder="0 = no expiry">
<span class="form-hint">Leave 0 for no expiration</span>
</div>
<div class="form-group">
<label class="form-toggle" style="margin-top:1.5rem">
<input type="checkbox" id="questRepeatable">
<span class="toggle-slider"></span>
<span class="toggle-label">Repeatable</span>
</label>
</div>
</div>
<div class="form-group">
<label class="form-toggle">
<input type="checkbox" id="questActive" checked>
<span class="toggle-slider"></span>
<span class="toggle-label">Active</span>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeQuestModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save Quest</button>
</div>
<input type="hidden" id="questEditId" value="">
</form>
</div>
</div>
<div id="page-admin-achievements" class="page hidden">
<div class="page-header" style="display:flex;justify-content:space-between;align-items:flex-start">
<div>
<h1 class="page-title">Manage Achievements</h1>
<p class="page-subtitle">Create custom server achievements</p>
</div>
<button class="btn btn-primary" onclick="openAchievementModal()">+ Create Achievement</button>
</div>
<div class="card">
<div class="card-body">
<div id="adminAchievementList" class="item-list">
<div class="empty-state">
<div class="empty-state-icon">🏅</div>
<p>No achievements configured yet</p>
</div>
</div>
</div>
</div>
<div id="achievementSaveStatus" class="save-status hidden"></div>
</div>
<div id="achievementModal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3 id="achievementModalTitle">Create Achievement</h3>
<button class="btn-icon" onclick="closeAchievementModal()">&times;</button>
</div>
<form id="achievementForm" onsubmit="saveAchievement(event)">
<div class="modal-body">
<div class="form-grid" style="grid-template-columns:1fr">
<div class="form-grid">
<div class="form-group">
<label for="achievementName">Achievement Name</label>
<input type="text" id="achievementName" class="form-input" required placeholder="e.g., Message Champion">
</div>
<div class="form-group">
<label for="achievementIcon">Icon (emoji)</label>
<input type="text" id="achievementIcon" class="form-input" placeholder="e.g., 🏆" value="🏆">
</div>
</div>
<div class="form-group">
<label for="achievementDescription">Description</label>
<textarea id="achievementDescription" class="form-input" rows="2" placeholder="e.g., Send 1000 messages in the server"></textarea>
</div>
<div class="form-grid">
<div class="form-group">
<label for="achievementTrigger">Trigger Type</label>
<select id="achievementTrigger" class="form-input" required>
<option value="level">Reach Level</option>
<option value="prestige">Reach Prestige</option>
<option value="total_xp">Total XP Earned</option>
<option value="messages">Messages Sent</option>
<option value="reactions_given">Reactions Given</option>
<option value="reactions_received">Reactions Received</option>
<option value="voice_minutes">Voice Minutes</option>
<option value="daily_streak">Daily Streak</option>
<option value="commands_used">Commands Used</option>
</select>
</div>
<div class="form-group">
<label for="achievementValue">Trigger Value</label>
<input type="number" id="achievementValue" class="form-input" min="1" value="10" required>
<span class="form-hint">e.g., Level 10, 1000 XP, 50 messages</span>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="achievementXpReward">XP Reward</label>
<input type="number" id="achievementXpReward" class="form-input" min="0" value="100">
<span class="form-hint">XP granted when earned</span>
</div>
<div class="form-group">
<label for="achievementRoleReward">Role Reward ID</label>
<input type="text" id="achievementRoleReward" class="form-input" placeholder="Leave empty for none">
<span class="form-hint">Role ID to grant (optional)</span>
</div>
</div>
<div class="form-group">
<label class="form-toggle">
<input type="checkbox" id="achievementHidden">
<span class="toggle-slider"></span>
<span class="toggle-label">Hidden Achievement</span>
</label>
<span class="form-hint">Hidden achievements are not shown until earned</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeAchievementModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save Achievement</button>
</div>
<input type="hidden" id="achievementEditId" value="">
</form>
</div>
</div>
<div id="page-admin-shop" class="page hidden">
<div class="page-header" style="display:flex;justify-content:space-between;align-items:flex-start">
<div>
<h1 class="page-title">Manage Shop</h1>
<p class="page-subtitle">Configure shop items and prices</p>
</div>
<button class="btn btn-primary" onclick="openShopModal()">+ Add Item</button>
</div>
<div class="card">
<div class="card-body">
<div id="adminShopList" class="item-list">
<div class="empty-state">
<div class="empty-state-icon">🏪</div>
<p>No shop items configured yet</p>
</div>
</div>
</div>
</div>
<div id="shopSaveStatus" class="save-status hidden"></div>
</div>
<div id="shopModal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3 id="shopModalTitle">Add Shop Item</h3>
<button class="btn-icon" onclick="closeShopModal()">&times;</button>
</div>
<form id="shopForm" onsubmit="saveShopItem(event)">
<div class="modal-body">
<div class="form-grid" style="grid-template-columns:1fr">
<div class="form-grid">
<div class="form-group">
<label for="shopItemName">Item Name</label>
<input type="text" id="shopItemName" class="form-input" required placeholder="e.g., Gold Badge">
</div>
<div class="form-group">
<label for="shopItemType">Item Type</label>
<select id="shopItemType" class="form-input" required>
<option value="badge">Badge</option>
<option value="title">Title</option>
<option value="background">Background</option>
<option value="booster">XP Booster</option>
<option value="role">Role</option>
<option value="special">Special</option>
</select>
</div>
</div>
<div class="form-group">
<label for="shopItemDescription">Description</label>
<textarea id="shopItemDescription" class="form-input" rows="2" placeholder="Describe what this item does"></textarea>
</div>
<div class="form-grid">
<div class="form-group">
<label for="shopItemPrice">Price (XP)</label>
<input type="number" id="shopItemPrice" class="form-input" min="1" value="100" required>
</div>
<div class="form-group">
<label for="shopItemStock">Stock</label>
<input type="number" id="shopItemStock" class="form-input" min="-1" placeholder="-1 = unlimited">
<span class="form-hint">Leave empty or -1 for unlimited</span>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="shopItemLevelReq">Level Required</label>
<input type="number" id="shopItemLevelReq" class="form-input" min="0" value="0">
</div>
<div class="form-group">
<label for="shopItemPrestigeReq">Prestige Required</label>
<input type="number" id="shopItemPrestigeReq" class="form-input" min="0" value="0">
</div>
</div>
<div class="form-group">
<label class="form-toggle">
<input type="checkbox" id="shopItemEnabled" checked>
<span class="toggle-slider"></span>
<span class="toggle-label">Available in Shop</span>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeShopModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save Item</button>
</div>
<input type="hidden" id="shopEditId" value="">
</form>
</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();
// Handle URL query params for direct page navigation
const urlParams = new URLSearchParams(window.location.search);
const pageParam = urlParams.get('page');
if (pageParam) {
const navItem = document.querySelector(`.nav-item[data-page="${pageParam}"]`);
if (navItem) {
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
navItem.classList.add('active');
currentPage = pageParam;
showPage(pageParam);
}
}
} 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;
case 'admin-xp': await loadXpConfig(); break;
case 'admin-quests': await loadAdminQuests(); break;
case 'admin-achievements': await loadAdminAchievements(); break;
case 'admin-shop': await loadAdminShop(); 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>';
}
}
async function loadXpConfig() {
if (!currentGuild) return;
try {
const res = await fetch('/api/guild/' + currentGuild + '/config');
if (!res.ok) {
if (res.status === 403) {
showSaveStatus('xpSaveStatus', 'You do not have admin access to this server', 'error');
}
return;
}
const data = await res.json();
const config = data.xpConfig || {};
document.getElementById('xpMsgMin').value = config.message_xp_min ?? 15;
document.getElementById('xpMsgMax').value = config.message_xp_max ?? 25;
document.getElementById('xpMsgCooldown').value = config.message_cooldown ?? 60;
document.getElementById('xpReaction').value = config.reaction_xp ?? 5;
document.getElementById('xpVoice').value = config.voice_xp_per_minute ?? 2;
document.getElementById('xpDaily').value = config.daily_xp ?? 100;
document.getElementById('xpBase').value = config.xp_base ?? 100;
document.getElementById('levelUpChannel').value = config.levelup_channel_id || '';
document.getElementById('levelUpEnabled').checked = config.levelup_enabled !== false;
document.getElementById('levelUpDm').checked = config.levelup_dm === true;
document.getElementById('xpMultiplier').value = config.xp_multiplier ?? 1.0;
document.getElementById('weekendMultiplier').value = config.weekend_multiplier ?? 1.5;
} catch (e) {
console.error('Failed to load XP config:', e);
}
}
async function saveXpConfig(e) {
e.preventDefault();
if (!currentGuild) return;
const form = document.getElementById('xpSettingsForm');
const data = {
message_xp_min: parseInt(form.xpMsgMin.value) || 15,
message_xp_max: parseInt(form.xpMsgMax.value) || 25,
message_cooldown: parseInt(form.xpMsgCooldown.value) || 60,
reaction_xp: parseInt(form.xpReaction.value) || 5,
voice_xp_per_minute: parseInt(form.xpVoice.value) || 2,
daily_xp: parseInt(form.xpDaily.value) || 100,
xp_base: parseInt(form.xpBase.value) || 100,
levelup_channel_id: form.levelUpChannel.value || null,
levelup_enabled: form.levelUpEnabled.checked,
levelup_dm: form.levelUpDm.checked,
xp_multiplier: parseFloat(form.xpMultiplier.value) || 1.0,
weekend_multiplier: parseFloat(form.weekendMultiplier.value) || 1.5
};
try {
const res = await fetch('/api/guild/' + currentGuild + '/xp-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) {
showSaveStatus('xpSaveStatus', 'Settings saved successfully!', 'success');
} else {
const err = await res.json();
showSaveStatus('xpSaveStatus', err.error || 'Failed to save settings', 'error');
}
} catch (e) {
showSaveStatus('xpSaveStatus', 'Failed to save settings', 'error');
}
}
function showSaveStatus(elementId, message, type) {
const el = document.getElementById(elementId);
el.textContent = message;
el.className = 'save-status ' + type;
el.classList.remove('hidden');
setTimeout(() => {
el.classList.add('hidden');
}, 5000);
}
const QUEST_TYPES = { daily: { emoji: '☀️', name: 'Daily' }, weekly: { emoji: '📅', name: 'Weekly' }, special: { emoji: '⭐', name: 'Special' } };
const OBJECTIVES = { messages: '💬', reactions: '😄', voice_minutes: '🎙️', commands: '⚡', daily_claims: '🎁', level_ups: '📈', xp_earned: '✨' };
async function loadAdminQuests() {
if (!currentGuild) return;
const container = document.getElementById('adminQuestList');
container.innerHTML = '<div class="loading-spinner"></div>';
try {
const res = await fetch('/api/guild/' + currentGuild + '/quests');
if (!res.ok) {
container.innerHTML = '<div class="empty-state"><p>Failed to load quests</p></div>';
return;
}
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 quests configured yet</p></div>';
return;
}
container.innerHTML = data.quests.map(q => `
<div class="item-row">
<div style="font-size:1.5rem">${QUEST_TYPES[q.quest_type]?.emoji || '🎯'}</div>
<div class="item-info">
<div class="item-name">${q.name} ${!q.active ? '<span style="color:var(--warning);font-size:0.75rem">(Inactive)</span>' : ''}</div>
<div class="item-desc">${OBJECTIVES[q.objective] || ''} Target: ${q.target_value} | Reward: ${q.xp_reward} XP</div>
</div>
<div class="item-actions">
<button class="btn-icon" onclick="editQuest(${q.id})" title="Edit">✏️</button>
<button class="btn-icon danger" onclick="deleteQuest(${q.id})" title="Delete">🗑️</button>
</div>
</div>
`).join('');
window.adminQuests = data.quests;
} catch (e) {
container.innerHTML = '<div class="empty-state"><p>Failed to load quests</p></div>';
}
}
function openQuestModal(quest = null) {
document.getElementById('questModalTitle').textContent = quest ? 'Edit Quest' : 'Create Quest';
document.getElementById('questEditId').value = quest?.id || '';
document.getElementById('questName').value = quest?.name || '';
document.getElementById('questDescription').value = quest?.description || '';
document.getElementById('questType').value = quest?.quest_type || 'daily';
document.getElementById('questObjective').value = quest?.objective || 'messages';
document.getElementById('questTarget').value = quest?.target_value || 10;
document.getElementById('questXpReward').value = quest?.xp_reward || 100;
document.getElementById('questDuration').value = quest?.duration_hours || 24;
document.getElementById('questRepeatable').checked = quest?.repeatable || false;
document.getElementById('questActive').checked = quest?.active !== false;
document.getElementById('questModal').classList.remove('hidden');
}
function closeQuestModal() {
document.getElementById('questModal').classList.add('hidden');
}
function editQuest(questId) {
const quest = window.adminQuests?.find(q => q.id === questId);
if (quest) openQuestModal(quest);
}
async function saveQuest(e) {
e.preventDefault();
if (!currentGuild) return;
const questId = document.getElementById('questEditId').value;
const data = {
name: document.getElementById('questName').value,
description: document.getElementById('questDescription').value,
quest_type: document.getElementById('questType').value,
objective: document.getElementById('questObjective').value,
target_value: parseInt(document.getElementById('questTarget').value),
xp_reward: parseInt(document.getElementById('questXpReward').value),
duration_hours: parseInt(document.getElementById('questDuration').value) || 0,
repeatable: document.getElementById('questRepeatable').checked,
active: document.getElementById('questActive').checked
};
try {
const url = questId
? '/api/guild/' + currentGuild + '/quests/' + questId
: '/api/guild/' + currentGuild + '/quests';
const res = await fetch(url, {
method: questId ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) {
closeQuestModal();
showSaveStatus('questSaveStatus', questId ? 'Quest updated!' : 'Quest created!', 'success');
await loadAdminQuests();
} else {
const err = await res.json();
showSaveStatus('questSaveStatus', err.error || 'Failed to save quest', 'error');
}
} catch (e) {
showSaveStatus('questSaveStatus', 'Failed to save quest', 'error');
}
}
async function deleteQuest(questId) {
if (!confirm('Are you sure you want to delete this quest?')) return;
try {
const res = await fetch('/api/guild/' + currentGuild + '/quests/' + questId, {
method: 'DELETE'
});
if (res.ok) {
showSaveStatus('questSaveStatus', 'Quest deleted', 'success');
await loadAdminQuests();
} else {
showSaveStatus('questSaveStatus', 'Failed to delete quest', 'error');
}
} catch (e) {
showSaveStatus('questSaveStatus', 'Failed to delete quest', 'error');
}
}
const TRIGGER_TYPES = {
level: { emoji: '📈', name: 'Reach Level' },
prestige: { emoji: '👑', name: 'Reach Prestige' },
total_xp: { emoji: '✨', name: 'Total XP Earned' },
messages: { emoji: '💬', name: 'Messages Sent' },
reactions_given: { emoji: '😄', name: 'Reactions Given' },
reactions_received: { emoji: '❤️', name: 'Reactions Received' },
voice_minutes: { emoji: '🎙️', name: 'Voice Minutes' },
daily_streak: { emoji: '🔥', name: 'Daily Streak' },
commands_used: { emoji: '⚡', name: 'Commands Used' }
};
const ITEM_TYPES = {
badge: { emoji: '🏅', name: 'Badge' },
title: { emoji: '🏷️', name: 'Title' },
background: { emoji: '🎨', name: 'Background' },
booster: { emoji: '⚡', name: 'XP Booster' },
role: { emoji: '👑', name: 'Role' },
special: { emoji: '✨', name: 'Special' }
};
async function loadAdminAchievements() {
if (!currentGuild) return;
const container = document.getElementById('adminAchievementList');
container.innerHTML = '<div class="loading-spinner"></div>';
try {
const res = await fetch('/api/guild/' + currentGuild + '/achievements');
if (!res.ok) {
container.innerHTML = '<div class="empty-state"><p>Failed to load achievements</p></div>';
return;
}
const data = await res.json();
if (!data.achievements || data.achievements.length === 0) {
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🏅</div><p>No achievements configured yet</p></div>';
return;
}
container.innerHTML = data.achievements.map(a => `
<div class="item-row">
<div style="font-size:1.5rem">${a.icon || '🏆'}</div>
<div class="item-info">
<div class="item-name">${a.name} ${a.hidden ? '<span style="color:var(--muted-foreground);font-size:0.75rem">(Hidden)</span>' : ''}</div>
<div class="item-desc">${TRIGGER_TYPES[a.trigger_type]?.name || a.trigger_type}: ${a.trigger_value} | Reward: ${a.reward_xp || 0} XP${a.reward_role_id ? ' + Role' : ''}</div>
</div>
<div class="item-actions">
<button class="btn-icon" onclick="editAchievement(${a.id})" title="Edit">✏️</button>
<button class="btn-icon danger" onclick="deleteAchievement(${a.id})" title="Delete">🗑️</button>
</div>
</div>
`).join('');
window.adminAchievements = data.achievements;
} catch (e) {
container.innerHTML = '<div class="empty-state"><p>Failed to load achievements</p></div>';
}
}
function openAchievementModal(achievement = null) {
document.getElementById('achievementModalTitle').textContent = achievement ? 'Edit Achievement' : 'Create Achievement';
document.getElementById('achievementEditId').value = achievement?.id || '';
document.getElementById('achievementName').value = achievement?.name || '';
document.getElementById('achievementIcon').value = achievement?.icon || '🏆';
document.getElementById('achievementDescription').value = achievement?.description || '';
document.getElementById('achievementTrigger').value = achievement?.trigger_type || 'level';
document.getElementById('achievementValue').value = achievement?.trigger_value || 10;
document.getElementById('achievementXpReward').value = achievement?.reward_xp || 100;
document.getElementById('achievementRoleReward').value = achievement?.reward_role_id || '';
document.getElementById('achievementHidden').checked = achievement?.hidden || false;
document.getElementById('achievementModal').classList.remove('hidden');
}
function closeAchievementModal() {
document.getElementById('achievementModal').classList.add('hidden');
}
function editAchievement(achievementId) {
const achievement = window.adminAchievements?.find(a => a.id === achievementId);
if (achievement) openAchievementModal(achievement);
}
async function saveAchievement(e) {
e.preventDefault();
if (!currentGuild) return;
const achievementId = document.getElementById('achievementEditId').value;
const data = {
name: document.getElementById('achievementName').value,
icon: document.getElementById('achievementIcon').value || '🏆',
description: document.getElementById('achievementDescription').value,
trigger_type: document.getElementById('achievementTrigger').value,
trigger_value: parseInt(document.getElementById('achievementValue').value),
reward_xp: parseInt(document.getElementById('achievementXpReward').value) || 0,
reward_role_id: document.getElementById('achievementRoleReward').value || null,
hidden: document.getElementById('achievementHidden').checked
};
try {
const url = achievementId
? '/api/guild/' + currentGuild + '/achievements/' + achievementId
: '/api/guild/' + currentGuild + '/achievements';
const res = await fetch(url, {
method: achievementId ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) {
closeAchievementModal();
showSaveStatus('achievementSaveStatus', achievementId ? 'Achievement updated!' : 'Achievement created!', 'success');
await loadAdminAchievements();
} else {
const err = await res.json();
showSaveStatus('achievementSaveStatus', err.error || 'Failed to save achievement', 'error');
}
} catch (e) {
showSaveStatus('achievementSaveStatus', 'Failed to save achievement', 'error');
}
}
async function deleteAchievement(achievementId) {
if (!confirm('Are you sure you want to delete this achievement?')) return;
try {
const res = await fetch('/api/guild/' + currentGuild + '/achievements/' + achievementId, {
method: 'DELETE'
});
if (res.ok) {
showSaveStatus('achievementSaveStatus', 'Achievement deleted', 'success');
await loadAdminAchievements();
} else {
showSaveStatus('achievementSaveStatus', 'Failed to delete achievement', 'error');
}
} catch (e) {
showSaveStatus('achievementSaveStatus', 'Failed to delete achievement', 'error');
}
}
async function loadAdminShop() {
if (!currentGuild) return;
const container = document.getElementById('adminShopList');
container.innerHTML = '<div class="loading-spinner"></div>';
try {
const res = await fetch('/api/guild/' + currentGuild + '/shop/admin');
if (!res.ok) {
container.innerHTML = '<div class="empty-state"><p>Failed to load shop items</p></div>';
return;
}
const data = await res.json();
if (!data.items || data.items.length === 0) {
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🏪</div><p>No shop items configured yet</p></div>';
return;
}
container.innerHTML = data.items.map(item => `
<div class="item-row">
<div style="font-size:1.5rem">${ITEM_TYPES[item.item_type]?.emoji || '🎁'}</div>
<div class="item-info">
<div class="item-name">${item.name} ${!item.available ? '<span style="color:var(--warning);font-size:0.75rem">(Disabled)</span>' : ''}</div>
<div class="item-desc">${item.price.toLocaleString()} XP | Stock: ${item.stock ?? 'Unlimited'}${item.level_required > 0 ? ' | Level ' + item.level_required : ''}</div>
</div>
<div class="item-actions">
<button class="btn-icon" onclick="editShopItem(${item.id})" title="Edit">✏️</button>
<button class="btn-icon danger" onclick="deleteShopItem(${item.id})" title="Delete">🗑️</button>
</div>
</div>
`).join('');
window.adminShopItems = data.items;
} catch (e) {
container.innerHTML = '<div class="empty-state"><p>Failed to load shop items</p></div>';
}
}
function openShopModal(item = null) {
document.getElementById('shopModalTitle').textContent = item ? 'Edit Shop Item' : 'Add Shop Item';
document.getElementById('shopEditId').value = item?.id || '';
document.getElementById('shopItemName').value = item?.name || '';
document.getElementById('shopItemType').value = item?.item_type || 'badge';
document.getElementById('shopItemDescription').value = item?.description || '';
document.getElementById('shopItemPrice').value = item?.price || 100;
document.getElementById('shopItemStock').value = item?.stock ?? '';
document.getElementById('shopItemLevelReq').value = item?.level_required || 0;
document.getElementById('shopItemPrestigeReq').value = item?.prestige_required || 0;
document.getElementById('shopItemEnabled').checked = item?.available !== false;
document.getElementById('shopModal').classList.remove('hidden');
}
function closeShopModal() {
document.getElementById('shopModal').classList.add('hidden');
}
function editShopItem(itemId) {
const item = window.adminShopItems?.find(i => i.id === itemId);
if (item) openShopModal(item);
}
async function saveShopItem(e) {
e.preventDefault();
if (!currentGuild) return;
const itemId = document.getElementById('shopEditId').value;
const stockVal = document.getElementById('shopItemStock').value;
const data = {
name: document.getElementById('shopItemName').value,
item_type: document.getElementById('shopItemType').value,
description: document.getElementById('shopItemDescription').value,
price: parseInt(document.getElementById('shopItemPrice').value),
stock: stockVal === '' || stockVal === '-1' ? null : parseInt(stockVal),
level_required: parseInt(document.getElementById('shopItemLevelReq').value) || 0,
prestige_required: parseInt(document.getElementById('shopItemPrestigeReq').value) || 0,
available: document.getElementById('shopItemEnabled').checked
};
try {
const url = itemId
? '/api/guild/' + currentGuild + '/shop/' + itemId
: '/api/guild/' + currentGuild + '/shop';
const res = await fetch(url, {
method: itemId ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) {
closeShopModal();
showSaveStatus('shopSaveStatus', itemId ? 'Item updated!' : 'Item added!', 'success');
await loadAdminShop();
} else {
const err = await res.json();
showSaveStatus('shopSaveStatus', err.error || 'Failed to save item', 'error');
}
} catch (e) {
showSaveStatus('shopSaveStatus', 'Failed to save item', 'error');
}
}
async function deleteShopItem(itemId) {
if (!confirm('Are you sure you want to delete this shop item?')) return;
try {
const res = await fetch('/api/guild/' + currentGuild + '/shop/' + itemId, {
method: 'DELETE'
});
if (res.ok) {
showSaveStatus('shopSaveStatus', 'Item deleted', 'success');
await loadAdminShop();
} else {
showSaveStatus('shopSaveStatus', 'Failed to delete item', 'error');
}
} catch (e) {
showSaveStatus('shopSaveStatus', 'Failed to delete item', 'error');
}
}
document.getElementById('xpSettingsForm')?.addEventListener('submit', saveXpConfig);
init();
</script>
</body>
</html>