Add a new coins system for users to earn and spend currency
Implement new API endpoints for retrieving and configuring guild coins, and add a dedicated section in the dashboard for coin-related information. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 1a60159e-fbc2-40e0-8dc7-acade23267a8 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/bakeZwZ Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
dc7fb514b8
commit
ede721af0f
2 changed files with 358 additions and 0 deletions
|
|
@ -1158,6 +1158,9 @@
|
|||
<div class="nav-item" data-page="titles">
|
||||
<span class="nav-icon">🏷️</span> Titles
|
||||
</div>
|
||||
<div class="nav-item" data-page="coins">
|
||||
<span class="nav-icon">🪙</span> Coins
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-section" id="adminSection" style="display:none">
|
||||
|
|
@ -1236,6 +1239,11 @@
|
|||
<div class="stat-value" id="statAchievements">0</div>
|
||||
<div class="stat-sub">earned</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" id="statCoinLabel">Coins</div>
|
||||
<div class="stat-value" id="statCoins">0</div>
|
||||
<div class="stat-sub">server currency</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
|
@ -1370,6 +1378,66 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-coins" class="page hidden">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title"><span class="text-gradient" id="coinPageTitle">Coins</span></h1>
|
||||
<p class="page-subtitle">Server currency you can earn and spend</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" id="coinBalanceLabel">Your Balance</div>
|
||||
<div class="stat-value" id="coinBalance">0</div>
|
||||
<div class="stat-sub" id="coinCurrencyName">Coins</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:1.5rem">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title" id="coinLeaderboardTitle">Coin Leaderboard</h3>
|
||||
</div>
|
||||
<div class="card-body" id="coinLeaderboardBody">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🪙</div>
|
||||
<p>No coin leaderboard data yet</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:1.5rem" id="coinAdminCard" style="display:none">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Coin Settings (Admin)</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="coinSettingsForm" onsubmit="saveCoinConfig(event)">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="coinName">Currency Name</label>
|
||||
<input type="text" id="coinName" name="coin_name" value="Coins" class="form-input" placeholder="e.g., Coins, Gold, Credits">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="messageCoins">Coins per Message</label>
|
||||
<input type="number" id="messageCoins" name="message_coins" min="0" max="100" value="1" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dailyCoins">Daily Claim Coins</label>
|
||||
<input type="number" id="dailyCoins" name="daily_coins" min="0" max="1000" value="50" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="coinsEnabled" name="coins_enabled" checked>
|
||||
<span>Coins Enabled</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:1.5rem">
|
||||
<button type="submit" class="btn btn-primary">Save Coin Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
|
|
@ -2204,6 +2272,7 @@
|
|||
case 'shop': await loadShop(); break;
|
||||
case 'inventory': await loadInventory(); break;
|
||||
case 'titles': await loadTitles(); break;
|
||||
case 'coins': await loadCoins(); break;
|
||||
case 'admin-xp': await loadXpConfig(); break;
|
||||
case 'admin-quests': await loadAdminQuests(); break;
|
||||
case 'admin-achievements': await loadAdminAchievements(); break;
|
||||
|
|
@ -2255,6 +2324,22 @@
|
|||
document.getElementById('statVoice').textContent = Math.floor(voiceMinutes / 60) + 'h';
|
||||
}
|
||||
|
||||
// Load coins for profile display
|
||||
if (currentGuild) {
|
||||
try {
|
||||
const coinsRes = await fetch('/api/guild/' + currentGuild + '/coins');
|
||||
if (coinsRes.ok) {
|
||||
const coinsData = await coinsRes.json();
|
||||
const statCoinsEl = document.getElementById('statCoins');
|
||||
const statCoinLabelEl = document.getElementById('statCoinLabel');
|
||||
if (statCoinsEl) statCoinsEl.textContent = (coinsData.coins || 0).toLocaleString();
|
||||
if (statCoinLabelEl) statCoinLabelEl.textContent = coinsData.coinName || 'Coins';
|
||||
}
|
||||
} catch (ce) {
|
||||
// Silently skip on error
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('Profile load error:', e);
|
||||
}
|
||||
|
|
@ -3476,6 +3561,120 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Coins functions
|
||||
async function loadCoins() {
|
||||
if (!currentGuild) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/guild/' + currentGuild + '/coins');
|
||||
if (!res.ok) throw new Error('Failed to load');
|
||||
const data = await res.json();
|
||||
|
||||
const coinName = data.coinName || 'Coins';
|
||||
|
||||
document.getElementById('coinPageTitle').textContent = coinName;
|
||||
document.getElementById('coinBalance').textContent = (data.coins || 0).toLocaleString();
|
||||
document.getElementById('coinCurrencyName').textContent = coinName;
|
||||
document.getElementById('statCoins').textContent = (data.coins || 0).toLocaleString();
|
||||
document.getElementById('statCoinLabel').textContent = coinName;
|
||||
|
||||
// Load leaderboard
|
||||
loadCoinLeaderboard(coinName);
|
||||
|
||||
// Show admin card if user is admin
|
||||
const adminCard = document.getElementById('coinAdminCard');
|
||||
if (isCurrentGuildAdmin()) {
|
||||
adminCard.style.display = 'block';
|
||||
loadCoinConfig();
|
||||
} else {
|
||||
adminCard.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load coins:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCoinLeaderboard(coinName) {
|
||||
if (!currentGuild) return;
|
||||
|
||||
const container = document.getElementById('coinLeaderboardBody');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/guild/' + currentGuild + '/coins/leaderboard?limit=10');
|
||||
if (!res.ok) throw new Error('Failed to load');
|
||||
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 coin leaderboard data yet</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = data.leaderboard.map((entry, idx) => `
|
||||
<div class="leaderboard-item ${idx < 3 ? 'top-3' : ''}">
|
||||
<div class="leaderboard-rank">${idx + 1}</div>
|
||||
<div class="leaderboard-avatar" style="background:linear-gradient(135deg, var(--gradient-1), var(--gradient-2));display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600">${(entry.user_id || '?').substring(0, 2).toUpperCase()}</div>
|
||||
<span class="leaderboard-name">User ${entry.user_id?.substring(0, 8) || 'Unknown'}</span>
|
||||
<span class="leaderboard-xp">${(entry.coins || 0).toLocaleString()} ${coinName || 'Coins'}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
container.innerHTML = '<div class="empty-state">Failed to load leaderboard</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCoinConfig() {
|
||||
if (!currentGuild) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/guild/' + currentGuild + '/coins/config');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const config = data.config || {};
|
||||
|
||||
document.getElementById('coinName').value = config.coin_name || 'Coins';
|
||||
document.getElementById('messageCoins').value = config.message_coins ?? 1;
|
||||
document.getElementById('dailyCoins').value = config.daily_coins ?? 50;
|
||||
document.getElementById('coinsEnabled').checked = config.coins_enabled !== false;
|
||||
} catch (e) {
|
||||
console.error('Failed to load coin config:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCoinConfig(e) {
|
||||
e.preventDefault();
|
||||
if (!currentGuild) return;
|
||||
|
||||
const formData = {
|
||||
coin_name: document.getElementById('coinName').value || 'Coins',
|
||||
message_coins: parseInt(document.getElementById('messageCoins').value) || 1,
|
||||
daily_coins: parseInt(document.getElementById('dailyCoins').value) || 50,
|
||||
coins_enabled: document.getElementById('coinsEnabled').checked
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/guild/' + currentGuild + '/coins/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Coin settings saved!');
|
||||
loadCoins();
|
||||
} else {
|
||||
alert('Failed to save coin settings');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to save coin settings');
|
||||
}
|
||||
}
|
||||
|
||||
function isCurrentGuildAdmin() {
|
||||
if (!currentUser || !currentGuild) return false;
|
||||
const guild = currentUser.guilds?.find(g => g.id === currentGuild);
|
||||
return guild?.isAdmin === true;
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1931,6 +1931,165 @@ function createWebServer(discordClient, supabase, options = {}) {
|
|||
}
|
||||
});
|
||||
|
||||
// Coins API endpoints
|
||||
app.get('/api/guild/:guildId/coins', async (req, res) => {
|
||||
if (!supabase) return res.status(503).json({ error: 'Database not available' });
|
||||
|
||||
const userId = req.session.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
|
||||
|
||||
const { guildId } = req.params;
|
||||
|
||||
try {
|
||||
const { data: link } = await supabase
|
||||
.from('discord_links')
|
||||
.select('user_id')
|
||||
.eq('discord_id', userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (!link) {
|
||||
return res.json({ coins: 0, coinName: 'Coins' });
|
||||
}
|
||||
|
||||
const { data: stats } = await supabase
|
||||
.from('user_stats')
|
||||
.select('coins')
|
||||
.eq('user_id', link.user_id)
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
const { data: config } = await supabase
|
||||
.from('xp_config')
|
||||
.select('coin_name')
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
res.json({
|
||||
coins: stats?.coins || 0,
|
||||
coinName: config?.coin_name || 'Coins'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch coins:', error);
|
||||
res.status(500).json({ error: 'Failed to load coins' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/guild/:guildId/coins/config', async (req, res) => {
|
||||
if (!supabase) return res.status(503).json({ error: 'Database not available' });
|
||||
|
||||
const userId = req.session.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
|
||||
|
||||
const { guildId } = req.params;
|
||||
|
||||
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
||||
if (!userGuild || !userGuild.isAdmin) {
|
||||
return res.status(403).json({ error: 'No admin access' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: config } = await supabase
|
||||
.from('xp_config')
|
||||
.select('coins_enabled, message_coins, daily_coins, coin_name')
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
res.json({
|
||||
config: config || { coins_enabled: true, message_coins: 1, daily_coins: 50, coin_name: 'Coins' }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch coin config:', error);
|
||||
res.status(500).json({ error: 'Failed to load coin config' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/guild/:guildId/coins/config', async (req, res) => {
|
||||
if (!supabase) return res.status(503).json({ error: 'Database not available' });
|
||||
|
||||
const userId = req.session.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
|
||||
|
||||
const { guildId } = req.params;
|
||||
|
||||
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
||||
if (!userGuild || !userGuild.isAdmin) {
|
||||
return res.status(403).json({ error: 'No admin access' });
|
||||
}
|
||||
|
||||
const { coins_enabled, message_coins, daily_coins, coin_name } = req.body;
|
||||
|
||||
// Validate inputs
|
||||
const validMessageCoins = Math.max(0, Math.min(100, parseInt(message_coins) || 1));
|
||||
const validDailyCoins = Math.max(0, Math.min(1000, parseInt(daily_coins) || 50));
|
||||
const validCoinName = (coin_name || 'Coins').toString().substring(0, 32);
|
||||
const validCoinsEnabled = coins_enabled === true;
|
||||
|
||||
try {
|
||||
const { data: existing } = await supabase
|
||||
.from('xp_config')
|
||||
.select('guild_id')
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
const coinData = {
|
||||
coins_enabled: validCoinsEnabled,
|
||||
message_coins: validMessageCoins,
|
||||
daily_coins: validDailyCoins,
|
||||
coin_name: validCoinName,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
const { error } = await supabase
|
||||
.from('xp_config')
|
||||
.update(coinData)
|
||||
.eq('guild_id', guildId);
|
||||
if (error) throw error;
|
||||
} else {
|
||||
const { error } = await supabase
|
||||
.from('xp_config')
|
||||
.insert({ guild_id: guildId, ...coinData });
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to save coin config:', error);
|
||||
res.status(500).json({ error: 'Failed to save coin settings' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/guild/:guildId/coins/leaderboard', async (req, res) => {
|
||||
if (!supabase) return res.status(503).json({ error: 'Database not available' });
|
||||
|
||||
const { guildId } = req.params;
|
||||
const { limit = 50 } = req.query;
|
||||
|
||||
try {
|
||||
const { data: leaderboard } = await supabase
|
||||
.from('user_stats')
|
||||
.select('user_id, coins')
|
||||
.eq('guild_id', guildId)
|
||||
.gt('coins', 0)
|
||||
.order('coins', { ascending: false })
|
||||
.limit(parseInt(limit));
|
||||
|
||||
const { data: config } = await supabase
|
||||
.from('xp_config')
|
||||
.select('coin_name')
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
res.json({
|
||||
leaderboard: leaderboard || [],
|
||||
coinName: config?.coin_name || 'Coins'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch coin leaderboard:', error);
|
||||
res.status(500).json({ error: 'Failed to load leaderboard' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
|
|
|
|||
Loading…
Reference in a new issue