Implements new API endpoints and frontend components for fetching, displaying, and managing user-assigned titles within guilds. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 0a92717b-79d5-4240-a961-0b8ba7d9616c Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/bakeZwZ Replit-Helium-Checkpoint-Created: true
3482 lines
127 KiB
HTML
3482 lines
127 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 | Warden</title>
|
|
<style>
|
|
:root {
|
|
--background: #030712;
|
|
--foreground: #f8fafc;
|
|
--card: rgba(15, 23, 42, 0.6);
|
|
--card-border: rgba(99, 102, 241, 0.15);
|
|
--card-border-hover: rgba(99, 102, 241, 0.4);
|
|
--primary: #6366f1;
|
|
--primary-light: #818cf8;
|
|
--secondary: rgba(30, 41, 59, 0.5);
|
|
--muted: #64748b;
|
|
--muted-foreground: #94a3b8;
|
|
--border: rgba(51, 65, 85, 0.5);
|
|
--gradient-1: #6366f1;
|
|
--gradient-2: #3b82f6;
|
|
--gradient-3: #06b6d4;
|
|
--success: #10b981;
|
|
--warning: #f59e0b;
|
|
--danger: #ef4444;
|
|
}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: 'Courier New', Courier, monospace;
|
|
background: var(--background);
|
|
color: var(--foreground);
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.bg-grid {
|
|
position: fixed;
|
|
inset: 0;
|
|
background-image:
|
|
linear-gradient(rgba(99, 102, 241, 0.03) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(99, 102, 241, 0.03) 1px, transparent 1px);
|
|
background-size: 64px 64px;
|
|
pointer-events: none;
|
|
z-index: -2;
|
|
}
|
|
|
|
.bg-glow {
|
|
position: fixed;
|
|
top: -50%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 150%;
|
|
height: 100%;
|
|
background: radial-gradient(ellipse at center, rgba(99, 102, 241, 0.12) 0%, transparent 60%);
|
|
pointer-events: none;
|
|
z-index: -1;
|
|
}
|
|
|
|
.app {
|
|
display: flex;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.sidebar {
|
|
width: 280px;
|
|
background: rgba(3, 7, 18, 0.95);
|
|
backdrop-filter: blur(20px);
|
|
border-right: 1px solid var(--border);
|
|
padding: 1.5rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: fixed;
|
|
height: 100vh;
|
|
overflow-y: auto;
|
|
z-index: 50;
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
margin-bottom: 2rem;
|
|
text-decoration: none;
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.logo-icon {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 10px;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.logo-text {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, var(--foreground), var(--muted));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.guild-selector {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.guild-selector-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0.875rem 1rem;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.guild-selector-header:hover {
|
|
border-color: var(--card-border-hover);
|
|
}
|
|
|
|
.guild-selector-header.open {
|
|
border-radius: 12px 12px 0 0;
|
|
border-bottom-color: transparent;
|
|
}
|
|
|
|
.selected-guild {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.selected-guild-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 8px;
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.selected-guild-icon img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.selected-guild-name {
|
|
font-weight: 500;
|
|
font-size: 0.9rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.guild-selector-arrow {
|
|
color: var(--muted);
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.guild-selector-header.open .guild-selector-arrow {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.guild-dropdown {
|
|
display: none;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-top: none;
|
|
border-radius: 0 0 12px 12px;
|
|
max-height: 280px;
|
|
overflow-y: auto;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.guild-dropdown.open {
|
|
display: block;
|
|
}
|
|
|
|
.guild-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.875rem 1rem;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
border-left: 3px solid transparent;
|
|
}
|
|
|
|
.guild-card:hover {
|
|
background: rgba(99, 102, 241, 0.08);
|
|
}
|
|
|
|
.guild-card.active {
|
|
background: rgba(99, 102, 241, 0.12);
|
|
border-left-color: var(--primary);
|
|
}
|
|
|
|
.guild-card:last-child {
|
|
border-radius: 0 0 10px 10px;
|
|
}
|
|
|
|
.guild-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 10px;
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.guild-icon img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.guild-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.guild-name {
|
|
font-weight: 500;
|
|
font-size: 0.9rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.guild-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
margin-top: 0.125rem;
|
|
}
|
|
|
|
.guild-admin-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
padding: 0.125rem 0.5rem;
|
|
background: rgba(99, 102, 241, 0.15);
|
|
border: 1px solid rgba(99, 102, 241, 0.3);
|
|
border-radius: 6px;
|
|
color: var(--primary-light);
|
|
font-size: 0.65rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.guild-member-count {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.guild-check {
|
|
color: var(--primary);
|
|
opacity: 0;
|
|
transition: opacity 0.15s;
|
|
}
|
|
|
|
.guild-card.active .guild-check {
|
|
opacity: 1;
|
|
}
|
|
|
|
.nav-section {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.nav-section-title {
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
margin-bottom: 0.75rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.08em;
|
|
padding-left: 0.5rem;
|
|
}
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 10px;
|
|
color: var(--muted-foreground);
|
|
text-decoration: none;
|
|
transition: all 0.2s;
|
|
cursor: pointer;
|
|
margin-bottom: 0.25rem;
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.nav-item:hover {
|
|
background: rgba(99, 102, 241, 0.08);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.nav-item.active {
|
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(59, 130, 246, 0.1));
|
|
border: 1px solid rgba(99, 102, 241, 0.25);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.nav-icon { width: 20px; text-align: center; font-size: 1rem; }
|
|
|
|
.user-card {
|
|
margin-top: auto;
|
|
padding: 1rem;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.user-avatar {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
object-fit: cover;
|
|
}
|
|
|
|
.user-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.user-name {
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.user-level {
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.main {
|
|
flex: 1;
|
|
margin-left: 280px;
|
|
padding: 2.5rem;
|
|
max-width: calc(100vw - 280px);
|
|
}
|
|
|
|
.page-header {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
margin-bottom: 0.5rem;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.page-subtitle {
|
|
color: var(--muted);
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.text-gradient {
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2), var(--gradient-3));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-size: 200% 200%;
|
|
animation: gradient-shift 4s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes gradient-shift {
|
|
0%, 100% { background-position: 0% 50%; }
|
|
50% { background-position: 100% 50%; }
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 16px;
|
|
padding: 1.5rem;
|
|
backdrop-filter: blur(10px);
|
|
transition: all 0.3s;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.stat-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 1px;
|
|
background: linear-gradient(90deg, transparent, var(--primary), transparent);
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
.stat-card:hover {
|
|
border-color: var(--card-border-hover);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.stat-card:hover::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.8rem;
|
|
color: var(--muted);
|
|
margin-bottom: 0.5rem;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.75rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.stat-sub {
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.card {
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 16px;
|
|
margin-bottom: 1.5rem;
|
|
backdrop-filter: blur(10px);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.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: 1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.card-body { padding: 1.5rem; }
|
|
|
|
.progress-bar {
|
|
height: 8px;
|
|
background: rgba(51, 65, 85, 0.5);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, var(--gradient-1), var(--gradient-2));
|
|
border-radius: 4px;
|
|
transition: width 0.5s ease;
|
|
}
|
|
|
|
.achievement-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.achievement-item {
|
|
display: flex;
|
|
gap: 1rem;
|
|
padding: 1.25rem;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 14px;
|
|
transition: all 0.3s;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.achievement-item:hover {
|
|
border-color: var(--card-border-hover);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.achievement-item.locked {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.achievement-icon {
|
|
width: 52px;
|
|
height: 52px;
|
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(59, 130, 246, 0.1));
|
|
border: 1px solid rgba(99, 102, 241, 0.3);
|
|
border-radius: 12px;
|
|
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;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.achievement-info p {
|
|
font-size: 0.85rem;
|
|
color: var(--muted);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.quest-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1.25rem;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 14px;
|
|
margin-bottom: 0.75rem;
|
|
transition: all 0.3s;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.quest-item:hover {
|
|
border-color: var(--card-border-hover);
|
|
}
|
|
|
|
.quest-info { flex: 1; }
|
|
|
|
.quest-name {
|
|
font-weight: 600;
|
|
margin-bottom: 0.25rem;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.quest-desc {
|
|
font-size: 0.85rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.quest-progress {
|
|
text-align: right;
|
|
min-width: 100px;
|
|
}
|
|
|
|
.quest-progress-text {
|
|
font-weight: 600;
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.shop-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.shop-item {
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 16px;
|
|
padding: 1.5rem;
|
|
text-align: center;
|
|
transition: all 0.3s;
|
|
backdrop-filter: blur(10px);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.shop-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 1px;
|
|
background: linear-gradient(90deg, transparent, var(--primary), transparent);
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
.shop-item:hover {
|
|
border-color: var(--card-border-hover);
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.shop-item:hover::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.shop-icon {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.shop-name {
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.shop-price {
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.625rem 1.25rem;
|
|
border-radius: 10px;
|
|
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(--gradient-1), var(--gradient-2));
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--secondary);
|
|
border: 1px solid var(--border);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: rgba(51, 65, 85, 0.5);
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.btn-icon {
|
|
padding: 0.5rem;
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn-icon:hover {
|
|
background: rgba(99, 102, 241, 0.1);
|
|
border-color: var(--primary);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.btn-icon.danger:hover {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border-color: var(--danger);
|
|
color: var(--danger);
|
|
}
|
|
|
|
.leaderboard-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 1rem 1.25rem;
|
|
border-radius: 12px;
|
|
margin-bottom: 0.5rem;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.leaderboard-item:hover {
|
|
border-color: var(--card-border-hover);
|
|
}
|
|
|
|
.leaderboard-item.top-3 {
|
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.12), rgba(59, 130, 246, 0.08));
|
|
border: 1px solid rgba(99, 102, 241, 0.3);
|
|
}
|
|
|
|
.leaderboard-rank {
|
|
width: 36px;
|
|
height: 36px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 700;
|
|
border-radius: 10px;
|
|
background: rgba(51, 65, 85, 0.5);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.leaderboard-item.top-3 .leaderboard-rank {
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
}
|
|
|
|
.leaderboard-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
object-fit: cover;
|
|
}
|
|
|
|
.leaderboard-name { flex: 1; font-weight: 500; }
|
|
|
|
.leaderboard-xp {
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1.5rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.tab {
|
|
padding: 0.625rem 1.25rem;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
color: var(--muted);
|
|
transition: all 0.2s;
|
|
font-weight: 500;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.tab:hover {
|
|
color: var(--foreground);
|
|
background: rgba(99, 102, 241, 0.08);
|
|
}
|
|
|
|
.tab.active {
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
color: white;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 4rem 2rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.empty-state-icon {
|
|
font-size: 3.5rem;
|
|
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: 2.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 1rem;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.login-prompt p {
|
|
color: var(--muted);
|
|
margin-bottom: 2.5rem;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.hidden { display: none !important; }
|
|
|
|
.loading-spinner {
|
|
display: inline-block;
|
|
width: 28px;
|
|
height: 28px;
|
|
border: 3px solid rgba(51, 65, 85, 0.5);
|
|
border-top-color: var(--primary);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
.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.85rem;
|
|
font-weight: 500;
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.form-input {
|
|
padding: 0.75rem 1rem;
|
|
background: rgba(30, 41, 59, 0.5);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
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(--primary);
|
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
|
|
}
|
|
|
|
.form-input::placeholder {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.form-hint {
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.form-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.form-toggle input {
|
|
display: none;
|
|
}
|
|
|
|
.toggle-slider {
|
|
width: 48px;
|
|
height: 26px;
|
|
background: rgba(51, 65, 85, 0.5);
|
|
border: 1px solid var(--border);
|
|
border-radius: 13px;
|
|
position: relative;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.toggle-slider::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
width: 20px;
|
|
height: 20px;
|
|
background: var(--muted);
|
|
border-radius: 50%;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.form-toggle input:checked + .toggle-slider {
|
|
background: var(--primary);
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.form-toggle input:checked + .toggle-slider::before {
|
|
left: 24px;
|
|
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.875rem 1.25rem;
|
|
border-radius: 10px;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.save-status.success {
|
|
background: rgba(16, 185, 129, 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: 1.25rem;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 14px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.item-row:hover {
|
|
border-color: var(--card-border-hover);
|
|
}
|
|
|
|
.item-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.item-name {
|
|
font-weight: 600;
|
|
margin-bottom: 0.25rem;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.item-desc {
|
|
font-size: 0.85rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.item-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.item-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 0.25rem 0.625rem;
|
|
border-radius: 6px;
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
margin-left: 0.5rem;
|
|
}
|
|
|
|
.item-badge.active {
|
|
background: rgba(16, 185, 129, 0.15);
|
|
color: var(--success);
|
|
border: 1px solid rgba(16, 185, 129, 0.3);
|
|
}
|
|
|
|
.item-badge.inactive {
|
|
background: rgba(245, 158, 11, 0.15);
|
|
color: var(--warning);
|
|
border: 1px solid rgba(245, 158, 11, 0.3);
|
|
}
|
|
|
|
.item-badge.hidden {
|
|
background: rgba(100, 116, 139, 0.15);
|
|
color: var(--muted);
|
|
border: 1px solid rgba(100, 116, 139, 0.3);
|
|
}
|
|
|
|
.modal {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(3, 7, 18, 0.9);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
padding: 1rem;
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
|
|
.modal-content {
|
|
background: rgba(15, 23, 42, 0.95);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 20px;
|
|
max-width: 600px;
|
|
width: 100%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
backdrop-filter: blur(20px);
|
|
}
|
|
|
|
.modal-header {
|
|
padding: 1.5rem 1.75rem;
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.modal-header h3 {
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 1.75rem;
|
|
}
|
|
|
|
.modal-footer {
|
|
padding: 1.25rem 1.75rem;
|
|
border-top: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
@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%; padding: 1.5rem; }
|
|
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
.page-title { font-size: 1.5rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="bg-grid"></div>
|
|
<div class="bg-glow"></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">
|
|
<img src="/logo.png" alt="AeThex" class="logo-icon">
|
|
<span class="logo-text">AeThex | Warden</span>
|
|
</a>
|
|
|
|
<div class="guild-selector">
|
|
<div class="guild-selector-header" id="guildSelectorHeader" onclick="toggleGuildDropdown()">
|
|
<div class="selected-guild">
|
|
<div class="selected-guild-icon" id="selectedGuildIcon">?</div>
|
|
<span class="selected-guild-name" id="selectedGuildName">Select a server...</span>
|
|
</div>
|
|
<svg class="guild-selector-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M6 9l6 6 6-6"/>
|
|
</svg>
|
|
</div>
|
|
<div class="guild-dropdown" id="guildDropdown"></div>
|
|
</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 class="nav-item" data-page="titles">
|
|
<span class="nav-icon">🏷️</span> Titles
|
|
</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 class="nav-item" data-page="moderation">
|
|
<span class="nav-icon">🛡</span> Moderation
|
|
</div>
|
|
<div class="nav-item" data-page="analytics">
|
|
<span class="nav-icon">📊</span> Analytics
|
|
</div>
|
|
<div class="nav-item" data-page="federation">
|
|
<span class="nav-icon">🌐</span> Federation
|
|
</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);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"><span class="text-gradient">Leaderboard</span></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 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 id="page-achievements" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Achievements</span></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"><span class="text-gradient">Quests</span></h1>
|
|
<p class="page-subtitle">Complete challenges to earn rewards</p>
|
|
</div>
|
|
|
|
<div id="questList">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🎯</div>
|
|
<p>Select a server to view quests</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-shop" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Shop</span></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"><span class="text-gradient">Inventory</span></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-titles" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Titles</span></h1>
|
|
<p class="page-subtitle">Customize your display title that shows on your profile</p>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Current Title</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="currentTitleDisplay" style="display:flex;align-items:center;gap:1rem">
|
|
<div style="flex:1">
|
|
<div style="font-size:1.25rem;font-weight:600" id="activeTitleName">No title selected</div>
|
|
<div style="font-size:0.875rem;color:var(--muted)" id="activeTitleDesc">Select a title from your collection below</div>
|
|
</div>
|
|
<button class="btn btn-secondary" onclick="clearActiveTitle()" id="clearTitleBtn" style="display:none">Clear Title</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Your Titles</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="titlesList" class="item-list">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🏷️</div>
|
|
<p>You don't have any titles yet</p>
|
|
<p style="font-size:0.875rem;color:var(--muted);margin-top:0.5rem">Purchase titles from the shop or earn them through achievements</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-admin-xp" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">XP Settings</span></h1>
|
|
<p class="page-subtitle">Configure XP earning for your server</p>
|
|
</div>
|
|
|
|
<form id="xpSettingsForm" class="admin-form" onsubmit="saveXpConfig(event)">
|
|
<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">Min XP per Message</label>
|
|
<input type="number" id="xpMsgMin" name="message_xp_min" min="0" max="1000" value="15" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="xpMsgMax">Max XP per Message</label>
|
|
<input type="number" id="xpMsgMax" name="message_xp_max" min="0" max="1000" value="25" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="xpMsgCooldown">Cooldown (seconds)</label>
|
|
<input type="number" id="xpMsgCooldown" name="message_cooldown" min="0" max="3600" value="60" class="form-input">
|
|
</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">Reaction XP</label>
|
|
<input type="number" id="xpReaction" name="reaction_xp" min="0" max="100" value="5" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="xpVoice">Voice XP per Minute</label>
|
|
<input type="number" id="xpVoice" name="voice_xp_per_minute" min="0" max="100" value="2" class="form-input">
|
|
</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">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Leveling 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="10" max="1000" value="100" class="form-input">
|
|
<span class="form-hint">Lower = easier leveling</span>
|
|
</div>
|
|
<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">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="weekendMultiplier">Weekend Multiplier</label>
|
|
<input type="number" id="weekendMultiplier" name="weekend_multiplier" min="1" max="5" step="0.1" value="1.5" class="form-input">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Level-Up Announcements</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="levelUpChannel">Announcement Channel ID</label>
|
|
<input type="text" id="levelUpChannel" name="levelup_channel_id" placeholder="Leave blank for current channel" class="form-input">
|
|
</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="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"><span class="text-gradient">Manage Quests</span></h1>
|
|
<p class="page-subtitle">Create and edit server quests</p>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="openQuestModal()">+ Create Quest</button>
|
|
</div>
|
|
|
|
<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 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()">×</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"><span class="text-gradient">Manage Achievements</span></h1>
|
|
<p class="page-subtitle">Create custom server achievements</p>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="openAchievementModal()">+ Create Achievement</button>
|
|
</div>
|
|
|
|
<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 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()">×</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">
|
|
</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">
|
|
</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"><span class="text-gradient">Manage Shop</span></h1>
|
|
<p class="page-subtitle">Configure shop items and prices</p>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="openShopModal()">+ Add Item</button>
|
|
</div>
|
|
|
|
<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 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()">×</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>
|
|
|
|
<div id="page-moderation" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Moderation</span> Dashboard</h1>
|
|
<p class="page-subtitle">View and manage warnings, bans, and moderation actions</p>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
<div class="card-header">
|
|
<h3 class="card-title">User Search</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div style="display:flex;gap:1rem;flex-wrap:wrap">
|
|
<input type="text" id="modSearchInput" class="form-input" placeholder="Search by username or user ID..." style="flex:1;min-width:200px">
|
|
<button class="btn btn-primary" onclick="searchModUser()">Search</button>
|
|
</div>
|
|
<div id="modSearchResults" style="margin-top:1rem"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-grid" style="margin-bottom:1.5rem">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Warnings</div>
|
|
<div class="stat-value" id="modTotalWarnings">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Active Bans</div>
|
|
<div class="stat-value" id="modActiveBans">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Recent Actions</div>
|
|
<div class="stat-value" id="modRecentActions">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tabs" style="margin-bottom:1.5rem">
|
|
<div class="tab active" data-mod-tab="mod-warnings">Warnings</div>
|
|
<div class="tab" data-mod-tab="mod-bans">Bans</div>
|
|
<div class="tab" data-mod-tab="mod-activity">Activity Feed</div>
|
|
</div>
|
|
|
|
<div id="mod-warnings" class="mod-section">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Server Warnings</h3>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Reason</th>
|
|
<th>Moderator</th>
|
|
<th>Date</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="modWarningsList">
|
|
<tr><td colspan="5" class="empty-state">Loading warnings...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="mod-bans" class="mod-section hidden">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Server Bans</h3>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Reason</th>
|
|
<th>Moderator</th>
|
|
<th>Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="modBansList">
|
|
<tr><td colspan="4" class="empty-state">Loading bans...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="mod-activity" class="mod-section hidden">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Recent Activity Feed</h3>
|
|
</div>
|
|
<div class="card-body" id="modActivityFeed">
|
|
<div class="empty-state">Loading activity...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-analytics" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Analytics</span></h1>
|
|
<p class="page-subtitle">Server activity insights and statistics</p>
|
|
</div>
|
|
|
|
<div class="stats-grid" style="margin-bottom:1.5rem">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Messages Today</div>
|
|
<div class="stat-value" id="analyticsMessagesToday">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Active Users (7d)</div>
|
|
<div class="stat-value" id="analyticsActiveUsers">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">XP Earned Today</div>
|
|
<div class="stat-value" id="analyticsXpToday">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Commands Used</div>
|
|
<div class="stat-value" id="analyticsCommandsUsed">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Activity Over Time (Last 7 Days)</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="activityChart" style="height:200px;display:flex;align-items:flex-end;gap:0.5rem;padding:1rem 0">
|
|
<div class="empty-state" style="width:100%">Loading chart...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1.5rem">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Top XP Earners (Today)</h3>
|
|
</div>
|
|
<div class="card-body" id="analyticsTopEarners">
|
|
<div class="empty-state">Loading...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Most Active Channels</h3>
|
|
</div>
|
|
<div class="card-body" id="analyticsTopChannels">
|
|
<div class="empty-state">Loading...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Command Usage</h3>
|
|
</div>
|
|
<div class="card-body" id="analyticsCommandStats">
|
|
<div class="empty-state">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-federation" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Federation</span> Management</h1>
|
|
<p class="page-subtitle">Manage your federation membership, view bans, and applications</p>
|
|
</div>
|
|
|
|
<div class="stats-grid" id="fedStatsGrid">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Member Servers</div>
|
|
<div class="stat-value" id="fedTotalServers">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Active Bans</div>
|
|
<div class="stat-value" id="fedActiveBans">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Pending Applications</div>
|
|
<div class="stat-value" id="fedPendingApps">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tabs" style="margin-bottom:1.5rem">
|
|
<div class="tab active" data-fed-tab="fed-servers">Servers</div>
|
|
<div class="tab" data-fed-tab="fed-bans">Global Bans</div>
|
|
<div class="tab" data-fed-tab="fed-applications">Applications</div>
|
|
<div class="tab" data-fed-tab="fed-leaderboard">Leaderboard</div>
|
|
</div>
|
|
|
|
<div id="fed-servers" class="fed-section">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Federation Servers</h3>
|
|
</div>
|
|
<div class="card-body" id="fedServerGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem">
|
|
<div class="empty-state">Loading servers...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="fed-bans" class="fed-section hidden">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Global Ban List</h3>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Severity</th>
|
|
<th>Reason</th>
|
|
<th>Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="fedBanList">
|
|
<tr><td colspan="4" class="empty-state">Loading bans...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="fed-applications" class="fed-section hidden">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Pending Applications</h3>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Server</th>
|
|
<th>Category</th>
|
|
<th>Members</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="fedAppList">
|
|
<tr><td colspan="5" class="empty-state">Loading applications...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="fed-leaderboard" class="fed-section hidden">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Federation Reputation Leaders</h3>
|
|
</div>
|
|
<div class="card-body" id="fedLeaderboardList">
|
|
<div class="empty-state">Loading leaderboard...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:2rem">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Upgrade Protection</h3>
|
|
</div>
|
|
<div class="card-body" style="display:flex;gap:1rem;flex-wrap:wrap">
|
|
<a href="/pricing" class="btn btn-primary">View Pricing Plans</a>
|
|
<a href="/federation" class="btn btn-secondary">Learn About Federation</a>
|
|
</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;
|
|
|
|
populateGuildCards(currentUser.guilds);
|
|
|
|
if (currentUser.guilds.length > 0) {
|
|
selectGuild(currentUser.guilds[0]);
|
|
}
|
|
|
|
document.addEventListener('click', (e) => {
|
|
const selector = document.querySelector('.guild-selector');
|
|
if (!selector.contains(e.target)) {
|
|
closeGuildDropdown();
|
|
}
|
|
});
|
|
|
|
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();
|
|
|
|
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 populateGuildCards(guilds) {
|
|
const dropdown = document.getElementById('guildDropdown');
|
|
dropdown.innerHTML = '';
|
|
|
|
if (guilds.length === 0) {
|
|
dropdown.innerHTML = '<div style="padding: 1rem; text-align: center; color: var(--muted);">No servers found</div>';
|
|
return;
|
|
}
|
|
|
|
guilds.forEach(g => {
|
|
const card = document.createElement('div');
|
|
card.className = 'guild-card';
|
|
card.dataset.guildId = g.id;
|
|
card.dataset.isAdmin = g.isAdmin;
|
|
card.onclick = () => selectGuild(g);
|
|
|
|
const iconContent = g.icon
|
|
? `<img src="${g.icon}" alt="${g.name}">`
|
|
: g.name.charAt(0).toUpperCase();
|
|
|
|
let statusHtml = '';
|
|
if (g.isAdmin) {
|
|
statusHtml = '<span class="guild-admin-badge">Admin</span>';
|
|
}
|
|
if (g.memberCount) {
|
|
statusHtml += `<span class="guild-member-count">${g.memberCount.toLocaleString()} members</span>`;
|
|
}
|
|
|
|
card.innerHTML = `
|
|
<div class="guild-icon">${iconContent}</div>
|
|
<div class="guild-info">
|
|
<div class="guild-name">${escapeHtml(g.name)}</div>
|
|
<div class="guild-status">${statusHtml}</div>
|
|
</div>
|
|
<svg class="guild-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
|
<path d="M20 6L9 17l-5-5"/>
|
|
</svg>
|
|
`;
|
|
|
|
dropdown.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function selectGuild(guild) {
|
|
currentGuild = guild.id;
|
|
|
|
document.querySelectorAll('.guild-card').forEach(c => {
|
|
c.classList.toggle('active', c.dataset.guildId === guild.id);
|
|
});
|
|
|
|
const iconEl = document.getElementById('selectedGuildIcon');
|
|
if (guild.icon) {
|
|
iconEl.innerHTML = `<img src="${guild.icon}" alt="${guild.name}">`;
|
|
} else {
|
|
iconEl.innerHTML = guild.name.charAt(0).toUpperCase();
|
|
}
|
|
|
|
document.getElementById('selectedGuildName').textContent = guild.name;
|
|
document.getElementById('adminSection').style.display = guild.isAdmin ? 'block' : 'none';
|
|
|
|
closeGuildDropdown();
|
|
loadPageData();
|
|
}
|
|
|
|
function toggleGuildDropdown() {
|
|
const header = document.getElementById('guildSelectorHeader');
|
|
const dropdown = document.getElementById('guildDropdown');
|
|
const isOpen = dropdown.classList.contains('open');
|
|
|
|
header.classList.toggle('open', !isOpen);
|
|
dropdown.classList.toggle('open', !isOpen);
|
|
}
|
|
|
|
function closeGuildDropdown() {
|
|
document.getElementById('guildSelectorHeader').classList.remove('open');
|
|
document.getElementById('guildDropdown').classList.remove('open');
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
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 'titles': await loadTitles(); 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;
|
|
case 'federation': loadFederationData(); break;
|
|
case 'moderation': loadModerationData(); break;
|
|
case 'analytics': loadAnalyticsData(); 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 style="display:flex;justify-content:center;padding:3rem"><div class="loading-spinner"></div></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 style="display:flex;justify-content:center;padding:3rem;grid-column:1/-1"><div class="loading-spinner"></div></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 style="display:flex;justify-content:center;padding:3rem"><div class="loading-spinner"></div></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)">${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 style="display:flex;justify-content:center;padding:3rem;grid-column:1/-1"><div class="loading-spinner"></div></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: '⚡', booster: '⚡', background: '🎨', special: '✨', 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 style="display:flex;justify-content:center;padding:3rem;grid-column:1/-1"><div class="loading-spinner"></div></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: '⚡', booster: '⚡', background: '🎨', special: '✨', 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)">${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 style="display:flex;justify-content:center;padding:3rem"><div class="loading-spinner"></div></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}
|
|
<span class="item-badge ${q.active ? 'active' : 'inactive'}">${q.active ? 'Active' : '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 style="display:flex;justify-content:center;padding:3rem"><div class="loading-spinner"></div></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 class="item-badge hidden">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 style="display:flex;justify-content:center;padding:3rem"><div class="loading-spinner"></div></div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/shop');
|
|
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}
|
|
<span class="item-badge ${item.enabled ? 'active' : 'inactive'}">${item.enabled ? 'Active' : 'Disabled'}</span>
|
|
</div>
|
|
<div class="item-desc">${item.price.toLocaleString()} XP | ${ITEM_TYPES[item.item_type]?.name || item.item_type}</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 ?? -1;
|
|
document.getElementById('shopItemLevelReq').value = item?.level_required || 0;
|
|
document.getElementById('shopItemPrestigeReq').value = item?.prestige_required || 0;
|
|
document.getElementById('shopItemEnabled').checked = item?.enabled !== 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 data = {
|
|
name: document.getElementById('shopItemName').value,
|
|
item_type: document.getElementById('shopItemType').value,
|
|
description: document.getElementById('shopItemDescription').value,
|
|
price: parseInt(document.getElementById('shopItemPrice').value),
|
|
stock: parseInt(document.getElementById('shopItemStock').value) || -1,
|
|
level_required: parseInt(document.getElementById('shopItemLevelReq').value) || 0,
|
|
prestige_required: parseInt(document.getElementById('shopItemPrestigeReq').value) || 0,
|
|
enabled: 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 created!', '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 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.querySelectorAll('[data-fed-tab]').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('[data-fed-tab]').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.fed-section').forEach(s => s.classList.add('hidden'));
|
|
tab.classList.add('active');
|
|
document.getElementById(tab.dataset.fedTab).classList.remove('hidden');
|
|
});
|
|
});
|
|
|
|
async function loadFederationStats() {
|
|
try {
|
|
const res = await fetch('/api/federation/stats');
|
|
const data = await res.json();
|
|
document.getElementById('fedTotalServers').textContent = data.totalServers || 0;
|
|
document.getElementById('fedActiveBans').textContent = data.activeBans || 0;
|
|
document.getElementById('fedPendingApps').textContent = data.pendingApplications || 0;
|
|
} catch (e) {
|
|
console.error('Failed to load federation stats:', e);
|
|
}
|
|
}
|
|
|
|
async function loadFederationServers() {
|
|
try {
|
|
const res = await fetch('/api/federation/servers');
|
|
const data = await res.json();
|
|
const grid = document.getElementById('fedServerGrid');
|
|
|
|
if (!data.servers || data.servers.length === 0) {
|
|
grid.innerHTML = '<div class="empty-state">No servers in the federation yet. Use /federation apply to join!</div>';
|
|
return;
|
|
}
|
|
|
|
const categoryEmojis = { gaming: '🎮', creative: '🎨', development: '💻', education: '📚', community: '👥', business: '🏢' };
|
|
|
|
grid.innerHTML = data.servers.map(s => `
|
|
<div class="card" style="padding:1rem">
|
|
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.5rem">
|
|
<div style="width:40px;height:40px;background:var(--primary);border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1.25rem">${categoryEmojis[s.category] || '🌐'}</div>
|
|
<div>
|
|
<div style="font-weight:600">${s.guild_name}</div>
|
|
<div style="font-size:0.8rem;color:var(--muted)">${s.category || 'General'}</div>
|
|
</div>
|
|
</div>
|
|
<div style="font-size:0.85rem;color:var(--muted);margin-bottom:0.5rem">${s.description || 'No description'}</div>
|
|
<div style="font-size:0.8rem;color:var(--muted)">${(s.member_count || 0).toLocaleString()} members</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load federation servers:', e);
|
|
}
|
|
}
|
|
|
|
async function loadFederationBans() {
|
|
try {
|
|
const res = await fetch('/api/federation/bans?limit=50');
|
|
const data = await res.json();
|
|
const tbody = document.getElementById('fedBanList');
|
|
|
|
if (!data.bans || data.bans.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No active bans</td></tr>';
|
|
return;
|
|
}
|
|
|
|
const severityColors = { low: 'var(--muted)', medium: 'var(--warning)', high: '#f97316', critical: 'var(--danger)' };
|
|
|
|
tbody.innerHTML = data.bans.map(b => `
|
|
<tr>
|
|
<td>${b.username || b.user_id}</td>
|
|
<td><span style="padding:0.25rem 0.5rem;border-radius:4px;font-size:0.75rem;background:${severityColors[b.severity] || 'var(--muted)'};color:white">${(b.severity || 'unknown').toUpperCase()}</span></td>
|
|
<td>${(b.reason || '').substring(0, 50)}${(b.reason || '').length > 50 ? '...' : ''}</td>
|
|
<td>${new Date(b.created_at).toLocaleDateString()}</td>
|
|
</tr>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load federation bans:', e);
|
|
}
|
|
}
|
|
|
|
async function loadFederationApplications() {
|
|
try {
|
|
const res = await fetch('/api/federation/applications');
|
|
const data = await res.json();
|
|
const tbody = document.getElementById('fedAppList');
|
|
|
|
if (!data.applications || data.applications.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No applications</td></tr>';
|
|
return;
|
|
}
|
|
|
|
const statusColors = { pending: 'var(--warning)', approved: 'var(--success)', rejected: 'var(--danger)' };
|
|
|
|
tbody.innerHTML = data.applications.map(a => `
|
|
<tr>
|
|
<td>${a.guild_name}</td>
|
|
<td>${a.category || 'General'}</td>
|
|
<td>${(a.member_count || 0).toLocaleString()}</td>
|
|
<td><span style="padding:0.25rem 0.5rem;border-radius:4px;font-size:0.75rem;background:${statusColors[a.status] || 'var(--muted)'};color:white">${(a.status || 'pending').toUpperCase()}</span></td>
|
|
<td>
|
|
${a.status === 'pending' ? `
|
|
<button class="btn btn-primary" style="padding:0.25rem 0.5rem;font-size:0.75rem" onclick="approveFedApp(${a.id})">Approve</button>
|
|
<button class="btn btn-secondary" style="padding:0.25rem 0.5rem;font-size:0.75rem" onclick="rejectFedApp(${a.id})">Reject</button>
|
|
` : '-'}
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load federation applications:', e);
|
|
}
|
|
}
|
|
|
|
async function loadFederationLeaderboard() {
|
|
try {
|
|
const res = await fetch('/api/federation/leaderboard?limit=20');
|
|
const data = await res.json();
|
|
const container = document.getElementById('fedLeaderboardList');
|
|
|
|
if (!data.leaderboard || data.leaderboard.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">No reputation data yet. Be active across federation servers!</div>';
|
|
return;
|
|
}
|
|
|
|
const tierEmojis = { newcomer: '🌱', member: '⭐', veteran: '🏆', elite: '💎', legend: '👑' };
|
|
|
|
container.innerHTML = data.leaderboard.map((l, i) => `
|
|
<div style="display:flex;align-items:center;gap:1rem;padding:0.75rem;border-bottom:1px solid var(--border)">
|
|
<div style="width:32px;height:32px;border-radius:50%;background:${i < 3 ? 'linear-gradient(135deg,var(--primary),#3b82f6)' : 'var(--secondary)'};display:flex;align-items:center;justify-content:center;font-weight:700;font-size:0.875rem">${i + 1}</div>
|
|
<div style="flex:1">
|
|
<div style="font-weight:500">${l.discord_id}</div>
|
|
<div style="font-size:0.8rem;color:var(--muted)">${tierEmojis[l.rank_tier] || '🌱'} ${(l.rank_tier || 'newcomer').toUpperCase()}</div>
|
|
</div>
|
|
<div style="font-weight:600;color:var(--primary)">${(l.reputation_score || 0).toLocaleString()} rep</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load federation leaderboard:', e);
|
|
}
|
|
}
|
|
|
|
async function approveFedApp(id) {
|
|
if (!confirm('Approve this application?')) return;
|
|
try {
|
|
await fetch('/api/federation/applications/' + id + '/approve', { method: 'POST' });
|
|
loadFederationApplications();
|
|
loadFederationStats();
|
|
loadFederationServers();
|
|
} catch (e) {
|
|
alert('Failed to approve');
|
|
}
|
|
}
|
|
|
|
async function rejectFedApp(id) {
|
|
const reason = prompt('Rejection reason:');
|
|
if (!reason) return;
|
|
try {
|
|
await fetch('/api/federation/applications/' + id + '/reject', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ reason })
|
|
});
|
|
loadFederationApplications();
|
|
loadFederationStats();
|
|
} catch (e) {
|
|
alert('Failed to reject');
|
|
}
|
|
}
|
|
|
|
function loadFederationData() {
|
|
loadFederationStats();
|
|
loadFederationServers();
|
|
loadFederationBans();
|
|
loadFederationApplications();
|
|
loadFederationLeaderboard();
|
|
}
|
|
|
|
// Moderation Tab Event Listeners
|
|
document.querySelectorAll('[data-mod-tab]').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('[data-mod-tab]').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.mod-section').forEach(s => s.classList.add('hidden'));
|
|
tab.classList.add('active');
|
|
document.getElementById(tab.dataset.modTab).classList.remove('hidden');
|
|
});
|
|
});
|
|
|
|
function loadModerationData() {
|
|
loadModerationStats();
|
|
loadWarnings();
|
|
loadBans();
|
|
loadActivityFeed();
|
|
}
|
|
|
|
async function loadModerationStats() {
|
|
if (!currentGuild) return;
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/moderation/stats');
|
|
const data = await res.json();
|
|
document.getElementById('modTotalWarnings').textContent = data.totalWarnings || 0;
|
|
document.getElementById('modActiveBans').textContent = data.activeBans || 0;
|
|
document.getElementById('modRecentActions').textContent = data.recentActions || 0;
|
|
} catch (e) {
|
|
console.error('Failed to load moderation stats:', e);
|
|
}
|
|
}
|
|
|
|
async function loadWarnings() {
|
|
if (!currentGuild) return;
|
|
const tbody = document.getElementById('modWarningsList');
|
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Loading warnings...</td></tr>';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/moderation/warnings');
|
|
const data = await res.json();
|
|
|
|
if (!data.warnings || data.warnings.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No warnings found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.warnings.map(w => `
|
|
<tr>
|
|
<td>${escapeHtml(w.username || w.user_id)}</td>
|
|
<td>${escapeHtml((w.reason || 'No reason').substring(0, 50))}${(w.reason || '').length > 50 ? '...' : ''}</td>
|
|
<td>${escapeHtml(w.moderator_name || w.moderator_id || 'Unknown')}</td>
|
|
<td>${new Date(w.created_at).toLocaleDateString()}</td>
|
|
<td><button class="btn-icon danger" onclick="deleteWarning('${w.id}')" title="Delete Warning">✕</button></td>
|
|
</tr>
|
|
`).join('');
|
|
} catch (e) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Failed to load warnings</td></tr>';
|
|
}
|
|
}
|
|
|
|
async function loadBans() {
|
|
if (!currentGuild) return;
|
|
const tbody = document.getElementById('modBansList');
|
|
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">Loading bans...</td></tr>';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/moderation/bans');
|
|
const data = await res.json();
|
|
|
|
if (!data.bans || data.bans.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No bans found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.bans.map(b => `
|
|
<tr>
|
|
<td>${escapeHtml(b.username || b.user_id)}</td>
|
|
<td>${escapeHtml((b.reason || 'No reason').substring(0, 50))}${(b.reason || '').length > 50 ? '...' : ''}</td>
|
|
<td>${escapeHtml(b.moderator_name || b.moderator_id || 'Unknown')}</td>
|
|
<td>${new Date(b.created_at).toLocaleDateString()}</td>
|
|
</tr>
|
|
`).join('');
|
|
} catch (e) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">Failed to load bans</td></tr>';
|
|
}
|
|
}
|
|
|
|
async function loadActivityFeed() {
|
|
if (!currentGuild) return;
|
|
const container = document.getElementById('modActivityFeed');
|
|
container.innerHTML = '<div class="empty-state">Loading activity...</div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/moderation/activity');
|
|
const data = await res.json();
|
|
|
|
if (!data.activity || data.activity.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">No recent moderation activity</div>';
|
|
return;
|
|
}
|
|
|
|
const actionColors = { warn: 'var(--warning)', kick: '#f97316', ban: 'var(--danger)', timeout: 'var(--primary)', unban: 'var(--success)' };
|
|
|
|
container.innerHTML = data.activity.map(a => `
|
|
<div style="display:flex;align-items:center;gap:1rem;padding:0.75rem;border-bottom:1px solid var(--border)">
|
|
<div style="width:8px;height:8px;border-radius:50%;background:${actionColors[a.action_type] || 'var(--muted)'}"></div>
|
|
<div style="flex:1">
|
|
<div style="font-weight:500">${escapeHtml(a.target_name || a.target_id)} was ${a.action_type}${a.action_type.endsWith('n') ? 'ned' : 'ed'}</div>
|
|
<div style="font-size:0.8rem;color:var(--muted)">by ${escapeHtml(a.moderator_name || a.moderator_id)} - ${a.reason ? escapeHtml(a.reason.substring(0, 40)) : 'No reason'}</div>
|
|
</div>
|
|
<div style="font-size:0.75rem;color:var(--muted)">${new Date(a.created_at).toLocaleString()}</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state">Failed to load activity</div>';
|
|
}
|
|
}
|
|
|
|
async function searchModUser() {
|
|
if (!currentGuild) return;
|
|
const query = document.getElementById('modSearchInput').value.trim();
|
|
if (!query) return;
|
|
|
|
const container = document.getElementById('modSearchResults');
|
|
container.innerHTML = '<div style="display:flex;justify-content:center;padding:1rem"><div class="loading-spinner"></div></div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/moderation/search?q=' + encodeURIComponent(query));
|
|
const data = await res.json();
|
|
|
|
if (!data.results || data.results.length === 0) {
|
|
container.innerHTML = '<div style="padding:1rem;color:var(--muted)">No users found matching "' + escapeHtml(query) + '"</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.results.map(u => `
|
|
<div style="display:flex;align-items:center;gap:1rem;padding:0.75rem;background:var(--card);border:1px solid var(--card-border);border-radius:8px;margin-bottom:0.5rem">
|
|
<div style="width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--primary),#3b82f6);display:flex;align-items:center;justify-content:center;font-weight:600">${(u.username || 'U').charAt(0).toUpperCase()}</div>
|
|
<div style="flex:1">
|
|
<div style="font-weight:500">${escapeHtml(u.username || u.user_id)}</div>
|
|
<div style="font-size:0.8rem;color:var(--muted)">${u.warnings_count || 0} warnings</div>
|
|
</div>
|
|
<div style="font-size:0.8rem;color:var(--muted)">${u.is_banned ? '<span style="color:var(--danger)">BANNED</span>' : ''}</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div style="padding:1rem;color:var(--danger)">Search failed</div>';
|
|
}
|
|
}
|
|
|
|
async function deleteWarning(warningId) {
|
|
if (!confirm('Delete this warning?')) return;
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/moderation/warnings/' + warningId, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (res.ok) {
|
|
loadWarnings();
|
|
loadModerationStats();
|
|
} else {
|
|
alert('Failed to delete warning');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to delete warning');
|
|
}
|
|
}
|
|
|
|
// Analytics Functions
|
|
function loadAnalyticsData() {
|
|
loadAnalyticsStats();
|
|
loadActivityChart();
|
|
loadTopEarners();
|
|
loadTopChannels();
|
|
loadCommandStats();
|
|
}
|
|
|
|
async function loadAnalyticsStats() {
|
|
if (!currentGuild) return;
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/analytics/stats');
|
|
const data = await res.json();
|
|
document.getElementById('analyticsMessagesToday').textContent = (data.messagesToday || 0).toLocaleString();
|
|
document.getElementById('analyticsActiveUsers').textContent = (data.activeUsers || 0).toLocaleString();
|
|
document.getElementById('analyticsXpToday').textContent = (data.xpToday || 0).toLocaleString();
|
|
document.getElementById('analyticsCommandsUsed').textContent = (data.commandsUsed || 0).toLocaleString();
|
|
} catch (e) {
|
|
console.error('Failed to load analytics stats:', e);
|
|
}
|
|
}
|
|
|
|
async function loadActivityChart() {
|
|
if (!currentGuild) return;
|
|
const container = document.getElementById('activityChart');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/analytics/activity');
|
|
const data = await res.json();
|
|
|
|
if (!data.days || data.days.length === 0) {
|
|
container.innerHTML = '<div class="empty-state" style="width:100%">No activity data available</div>';
|
|
return;
|
|
}
|
|
|
|
const maxVal = Math.max(...data.days.map(d => d.messages || 0), 1);
|
|
|
|
container.innerHTML = data.days.map(d => {
|
|
const height = Math.max(10, ((d.messages || 0) / maxVal) * 150);
|
|
return `
|
|
<div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:0.5rem">
|
|
<div style="width:100%;max-width:40px;height:${height}px;background:linear-gradient(180deg,var(--primary),#3b82f6);border-radius:4px 4px 0 0;transition:height 0.3s"></div>
|
|
<div style="font-size:0.7rem;color:var(--muted)">${d.label || ''}</div>
|
|
<div style="font-size:0.65rem;color:var(--muted)">${(d.messages || 0).toLocaleString()}</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state" style="width:100%">Failed to load chart</div>';
|
|
}
|
|
}
|
|
|
|
async function loadTopEarners() {
|
|
if (!currentGuild) return;
|
|
const container = document.getElementById('analyticsTopEarners');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/analytics/top-earners');
|
|
const data = await res.json();
|
|
|
|
if (!data.earners || data.earners.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">No XP earned today</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.earners.map((e, i) => `
|
|
<div style="display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;${i < data.earners.length - 1 ? 'border-bottom:1px solid var(--border)' : ''}">
|
|
<div style="width:24px;font-weight:600;color:${i < 3 ? 'var(--primary)' : 'var(--muted)'}">#${i + 1}</div>
|
|
<div style="flex:1">${escapeHtml(e.username || e.user_id)}</div>
|
|
<div style="font-weight:600;color:var(--primary)">${(e.xp_earned || 0).toLocaleString()} XP</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state">Failed to load</div>';
|
|
}
|
|
}
|
|
|
|
async function loadTopChannels() {
|
|
if (!currentGuild) return;
|
|
const container = document.getElementById('analyticsTopChannels');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/analytics/top-channels');
|
|
const data = await res.json();
|
|
|
|
if (!data.channels || data.channels.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">No channel data</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.channels.map((c, i) => `
|
|
<div style="display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;${i < data.channels.length - 1 ? 'border-bottom:1px solid var(--border)' : ''}">
|
|
<div style="color:var(--muted)">#</div>
|
|
<div style="flex:1">${escapeHtml(c.channel_name || c.channel_id)}</div>
|
|
<div style="font-size:0.85rem;color:var(--muted)">${(c.message_count || 0).toLocaleString()} msgs</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state">Failed to load</div>';
|
|
}
|
|
}
|
|
|
|
async function loadCommandStats() {
|
|
if (!currentGuild) return;
|
|
const container = document.getElementById('analyticsCommandStats');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/analytics/commands');
|
|
const data = await res.json();
|
|
|
|
if (!data.commands || data.commands.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">No command data</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.commands.map((c, i) => `
|
|
<div style="display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;${i < data.commands.length - 1 ? 'border-bottom:1px solid var(--border)' : ''}">
|
|
<div style="color:var(--primary)">/${escapeHtml(c.command_name)}</div>
|
|
<div style="flex:1"></div>
|
|
<div style="font-size:0.85rem;color:var(--muted)">${(c.use_count || 0).toLocaleString()} uses</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state">Failed to load</div>';
|
|
}
|
|
}
|
|
|
|
// Titles Functions
|
|
async function loadTitles() {
|
|
if (!currentGuild) return;
|
|
const container = document.getElementById('titlesList');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/titles');
|
|
const data = await res.json();
|
|
|
|
// Update active title display
|
|
if (data.activeTitle) {
|
|
document.getElementById('activeTitleName').textContent = data.activeTitle.name;
|
|
document.getElementById('activeTitleDesc').textContent = data.activeTitle.description || 'Your current display title';
|
|
document.getElementById('clearTitleBtn').style.display = 'block';
|
|
} else {
|
|
document.getElementById('activeTitleName').textContent = 'No title selected';
|
|
document.getElementById('activeTitleDesc').textContent = 'Select a title from your collection below';
|
|
document.getElementById('clearTitleBtn').style.display = 'none';
|
|
}
|
|
|
|
if (!data.titles || data.titles.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🏷️</div>
|
|
<p>You don't have any titles yet</p>
|
|
<p style="font-size:0.875rem;color:var(--muted);margin-top:0.5rem">Purchase titles from the shop or earn them through achievements</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.titles.map(t => `
|
|
<div class="item-row">
|
|
<div style="width:40px;height:40px;background:linear-gradient(135deg,var(--primary),#3b82f6);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.25rem">🏷️</div>
|
|
<div class="item-info">
|
|
<div class="item-name">
|
|
${escapeHtml(t.name)}
|
|
${t.is_active ? '<span class="item-badge active">Active</span>' : ''}
|
|
</div>
|
|
<div class="item-desc">${escapeHtml(t.description || 'Custom title')}</div>
|
|
</div>
|
|
<div class="item-actions">
|
|
${!t.is_active ? `<button class="btn btn-primary" style="padding:0.5rem 1rem" onclick="setActiveTitle('${t.id}')">Use</button>` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state">Failed to load titles</div>';
|
|
}
|
|
}
|
|
|
|
async function setActiveTitle(titleId) {
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/titles/active', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ title_id: titleId })
|
|
});
|
|
|
|
if (res.ok) {
|
|
await loadTitles();
|
|
} else {
|
|
alert('Failed to set title');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to set title');
|
|
}
|
|
}
|
|
|
|
async function clearActiveTitle() {
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/titles/active', {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (res.ok) {
|
|
await loadTitles();
|
|
} else {
|
|
alert('Failed to clear title');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to clear title');
|
|
}
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|