AeThex-Bot-Master/aethex-bot/public/dashboard.html
sirpiglr 5e4fc34d5d Update dashboard styling and font for a modern look
Replaced the 'Fira Code' and 'Source Code Pro' font imports with 'Inter' and updated CSS variables to reflect a new color scheme and card styling for the dashboard.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 2d7a4ca7-fe8d-440d-882b-b0359f0d60ef
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/Xy30ojl
Replit-Helium-Checkpoint-Created: true
2025-12-09 21:32:54 +00:00

2605 lines
90 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - AeThex Bot</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<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: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
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: 40px;
height: 40px;
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.125rem;
}
.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">
<div class="logo-icon">A</div>
<span class="logo-text">AeThex</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>
</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>
</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;
}
}
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');
}
}
init();
</script>
</body>
</html>