AeThex-Bot-Master/aethex-bot/public/dashboard.html
sirpiglr 78a2a29bfc Add federation management to the dashboard and update the federation page
Add new navigation item and page structure for federation management in dashboard.html, and update federation.html with new styling and content sections for explaining the federation concept and features.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: f96502b3-64e2-454a-a892-81ab2a2f62a3
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/MGpDued
Replit-Helium-Checkpoint-Created: true
2025-12-10 02:51:47 +00:00

2883 lines
102 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>
<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="federation">
<span class="nav-icon">&#128737;</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-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()">&times;</button>
</div>
<form id="questForm" onsubmit="saveQuest(event)">
<div class="modal-body">
<div class="form-grid" style="grid-template-columns:1fr">
<div class="form-group">
<label for="questName">Quest Name</label>
<input type="text" id="questName" class="form-input" required placeholder="e.g., Message Master">
</div>
<div class="form-group">
<label for="questDescription">Description</label>
<textarea id="questDescription" class="form-input" rows="2" placeholder="e.g., Send 50 messages in the server"></textarea>
</div>
<div class="form-grid">
<div class="form-group">
<label for="questType">Quest Type</label>
<select id="questType" class="form-input" required>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="special">Special</option>
</select>
</div>
<div class="form-group">
<label for="questObjective">Objective</label>
<select id="questObjective" class="form-input" required>
<option value="messages">Send Messages</option>
<option value="reactions">Add Reactions</option>
<option value="voice_minutes">Voice Chat (minutes)</option>
<option value="commands">Use Commands</option>
<option value="daily_claims">Claim Daily Rewards</option>
<option value="level_ups">Level Up</option>
<option value="xp_earned">Earn XP</option>
</select>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="questTarget">Target Amount</label>
<input type="number" id="questTarget" class="form-input" min="1" value="10" required>
</div>
<div class="form-group">
<label for="questXpReward">XP Reward</label>
<input type="number" id="questXpReward" class="form-input" min="1" value="100" required>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="questDuration">Duration (hours)</label>
<input type="number" id="questDuration" class="form-input" min="0" value="24" placeholder="0 = no expiry">
<span class="form-hint">Leave 0 for no expiration</span>
</div>
<div class="form-group">
<label class="form-toggle" style="margin-top:1.5rem">
<input type="checkbox" id="questRepeatable">
<span class="toggle-slider"></span>
<span class="toggle-label">Repeatable</span>
</label>
</div>
</div>
<div class="form-group">
<label class="form-toggle">
<input type="checkbox" id="questActive" checked>
<span class="toggle-slider"></span>
<span class="toggle-label">Active</span>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeQuestModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save Quest</button>
</div>
<input type="hidden" id="questEditId" value="">
</form>
</div>
</div>
<div id="page-admin-achievements" class="page hidden">
<div class="page-header" style="display:flex;justify-content:space-between;align-items:flex-start">
<div>
<h1 class="page-title"><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()">&times;</button>
</div>
<form id="achievementForm" onsubmit="saveAchievement(event)">
<div class="modal-body">
<div class="form-grid" style="grid-template-columns:1fr">
<div class="form-grid">
<div class="form-group">
<label for="achievementName">Achievement Name</label>
<input type="text" id="achievementName" class="form-input" required placeholder="e.g., Message Champion">
</div>
<div class="form-group">
<label for="achievementIcon">Icon (emoji)</label>
<input type="text" id="achievementIcon" class="form-input" placeholder="e.g., 🏆" value="🏆">
</div>
</div>
<div class="form-group">
<label for="achievementDescription">Description</label>
<textarea id="achievementDescription" class="form-input" rows="2" placeholder="e.g., Send 1000 messages in the server"></textarea>
</div>
<div class="form-grid">
<div class="form-group">
<label for="achievementTrigger">Trigger Type</label>
<select id="achievementTrigger" class="form-input" required>
<option value="level">Reach Level</option>
<option value="prestige">Reach Prestige</option>
<option value="total_xp">Total XP Earned</option>
<option value="messages">Messages Sent</option>
<option value="reactions_given">Reactions Given</option>
<option value="reactions_received">Reactions Received</option>
<option value="voice_minutes">Voice Minutes</option>
<option value="daily_streak">Daily Streak</option>
<option value="commands_used">Commands Used</option>
</select>
</div>
<div class="form-group">
<label for="achievementValue">Trigger Value</label>
<input type="number" id="achievementValue" class="form-input" min="1" value="10" required>
<span class="form-hint">e.g., Level 10, 1000 XP, 50 messages</span>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="achievementXpReward">XP Reward</label>
<input type="number" id="achievementXpReward" class="form-input" min="0" value="100">
</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()">&times;</button>
</div>
<form id="shopForm" onsubmit="saveShopItem(event)">
<div class="modal-body">
<div class="form-grid" style="grid-template-columns:1fr">
<div class="form-grid">
<div class="form-group">
<label for="shopItemName">Item Name</label>
<input type="text" id="shopItemName" class="form-input" required placeholder="e.g., Gold Badge">
</div>
<div class="form-group">
<label for="shopItemType">Item Type</label>
<select id="shopItemType" class="form-input" required>
<option value="badge">Badge</option>
<option value="title">Title</option>
<option value="background">Background</option>
<option value="booster">XP Booster</option>
<option value="role">Role</option>
<option value="special">Special</option>
</select>
</div>
</div>
<div class="form-group">
<label for="shopItemDescription">Description</label>
<textarea id="shopItemDescription" class="form-input" rows="2" placeholder="Describe what this item does"></textarea>
</div>
<div class="form-grid">
<div class="form-group">
<label for="shopItemPrice">Price (XP)</label>
<input type="number" id="shopItemPrice" class="form-input" min="1" value="100" required>
</div>
<div class="form-group">
<label for="shopItemStock">Stock</label>
<input type="number" id="shopItemStock" class="form-input" min="-1" placeholder="-1 = unlimited">
<span class="form-hint">Leave empty or -1 for unlimited</span>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="shopItemLevelReq">Level Required</label>
<input type="number" id="shopItemLevelReq" class="form-input" min="0" value="0">
</div>
<div class="form-group">
<label for="shopItemPrestigeReq">Prestige Required</label>
<input type="number" id="shopItemPrestigeReq" class="form-input" min="0" value="0">
</div>
</div>
<div class="form-group">
<label class="form-toggle">
<input type="checkbox" id="shopItemEnabled" checked>
<span class="toggle-slider"></span>
<span class="toggle-label">Available in Shop</span>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeShopModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save Item</button>
</div>
<input type="hidden" id="shopEditId" value="">
</form>
</div>
</div>
<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 '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;
}
}
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();
}
init();
</script>
</body>
</html>