Replaced `.single()` with `.maybeSingle()` in multiple command files to handle cases where no record is found, and added a new /pricing route and navigation links to the UI. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: e91d020a-35a6-4add-9945-887dd3ecae9f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/tdDjujk Replit-Helium-Checkpoint-Created: true
176 lines
7 KiB
JavaScript
176 lines
7 KiB
JavaScript
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
|
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
|
const { getStandaloneXp, calculateLevel } = require('../utils/standaloneXp');
|
|
|
|
module.exports = {
|
|
data: new SlashCommandBuilder()
|
|
.setName('rank')
|
|
.setDescription('View your level and XP')
|
|
.addUserOption(option =>
|
|
option.setName('user')
|
|
.setDescription('User to check (defaults to yourself)')
|
|
.setRequired(false)
|
|
),
|
|
|
|
async execute(interaction, supabase, client) {
|
|
if (!supabase) {
|
|
return interaction.reply({ content: 'Database not configured.', ephemeral: true });
|
|
}
|
|
|
|
const target = interaction.options.getUser('user') || interaction.user;
|
|
await interaction.deferReply();
|
|
|
|
try {
|
|
const mode = await getServerMode(supabase, interaction.guildId);
|
|
|
|
if (mode === 'standalone') {
|
|
return handleStandaloneRank(interaction, supabase, target);
|
|
} else {
|
|
return handleFederatedRank(interaction, supabase, target);
|
|
}
|
|
} catch (error) {
|
|
console.error('Rank error:', error);
|
|
await interaction.editReply({ content: 'Failed to fetch rank data.' });
|
|
}
|
|
},
|
|
};
|
|
|
|
async function handleStandaloneRank(interaction, supabase, target) {
|
|
const data = await getStandaloneXp(supabase, target.id, interaction.guildId);
|
|
|
|
if (!data) {
|
|
return interaction.editReply({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setColor(EMBED_COLORS.standalone)
|
|
.setDescription(`${target.id === interaction.user.id ? 'You have' : `${target.tag} has`} no XP yet. Start chatting to earn XP!`)
|
|
]
|
|
});
|
|
}
|
|
|
|
const xp = data.xp || 0;
|
|
const prestige = data.prestige_level || 0;
|
|
const totalXpEarned = data.total_xp_earned || xp;
|
|
const level = calculateLevel(xp, 'normal');
|
|
const currentLevelXp = level * level * 100;
|
|
const nextLevelXp = (level + 1) * (level + 1) * 100;
|
|
const progress = xp - currentLevelXp;
|
|
const needed = nextLevelXp - currentLevelXp;
|
|
const progressPercent = Math.floor((progress / needed) * 100);
|
|
|
|
const progressBar = createProgressBar(progressPercent);
|
|
const prestigeInfo = getPrestigeInfo(prestige);
|
|
|
|
const { count: rankPosition } = await supabase
|
|
.from('guild_user_xp')
|
|
.select('*', { count: 'exact', head: true })
|
|
.eq('guild_id', interaction.guildId)
|
|
.gt('xp', xp);
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setColor(prestigeInfo.color)
|
|
.setTitle(`${prestigeInfo.icon} ${target.tag}'s Rank`)
|
|
.setThumbnail(target.displayAvatarURL())
|
|
.addFields(
|
|
{ name: 'Prestige', value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige})` : 'Not prestiged', inline: true },
|
|
{ name: 'Level', value: `**${level}**`, inline: true },
|
|
{ name: 'Server Rank', value: `#${(rankPosition || 0) + 1}`, inline: true },
|
|
{ name: 'Current XP', value: `**${xp.toLocaleString()}**`, inline: true },
|
|
{ name: 'XP Bonus', value: prestige > 0 ? `+${prestige * 5}%` : 'None', inline: true },
|
|
{ name: 'Total XP Earned', value: totalXpEarned.toLocaleString(), inline: true },
|
|
{ name: 'Progress to Next Level', value: `${progressBar}\n${progress.toLocaleString()} / ${needed.toLocaleString()} XP (${progressPercent}%)` }
|
|
)
|
|
.setFooter({ text: `🏠 Standalone Mode • ${interaction.guild.name}` })
|
|
.setTimestamp();
|
|
|
|
await interaction.editReply({ embeds: [embed] });
|
|
}
|
|
|
|
async function handleFederatedRank(interaction, supabase, target) {
|
|
const { data: link } = await supabase
|
|
.from('discord_links')
|
|
.select('user_id, primary_arm')
|
|
.eq('discord_id', target.id)
|
|
.maybeSingle();
|
|
|
|
if (!link) {
|
|
return interaction.editReply({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setColor(0xff6b6b)
|
|
.setDescription(`${target.id === interaction.user.id ? 'You are' : `${target.tag} is`} not linked to AeThex. Use \`/verify\` to link your account.`)
|
|
]
|
|
});
|
|
}
|
|
|
|
const { data: profile } = await supabase
|
|
.from('user_profiles')
|
|
.select('username, avatar_url, xp, bio, prestige_level, total_xp_earned')
|
|
.eq('id', link.user_id)
|
|
.maybeSingle();
|
|
|
|
const xp = profile?.xp || 0;
|
|
const prestige = profile?.prestige_level || 0;
|
|
const totalXpEarned = profile?.total_xp_earned || xp;
|
|
const level = Math.floor(Math.sqrt(xp / 100));
|
|
const currentLevelXp = level * level * 100;
|
|
const nextLevelXp = (level + 1) * (level + 1) * 100;
|
|
const progress = xp - currentLevelXp;
|
|
const needed = nextLevelXp - currentLevelXp;
|
|
const progressPercent = Math.floor((progress / needed) * 100);
|
|
|
|
const progressBar = createProgressBar(progressPercent);
|
|
const prestigeInfo = getPrestigeInfo(prestige);
|
|
|
|
const { count: rankPosition } = await supabase
|
|
.from('user_profiles')
|
|
.select('*', { count: 'exact', head: true })
|
|
.gt('xp', xp);
|
|
|
|
let avatarUrl = target.displayAvatarURL();
|
|
if (profile?.avatar_url && profile.avatar_url.startsWith('http')) {
|
|
avatarUrl = profile.avatar_url;
|
|
}
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setColor(prestigeInfo.color)
|
|
.setTitle(`${prestigeInfo.icon} ${profile?.username || target.tag}'s Rank`)
|
|
.setThumbnail(avatarUrl)
|
|
.addFields(
|
|
{ name: 'Prestige', value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige})` : 'Not prestiged', inline: true },
|
|
{ name: 'Level', value: `**${level}**`, inline: true },
|
|
{ name: 'Global Rank', value: `#${(rankPosition || 0) + 1}`, inline: true },
|
|
{ name: 'Current XP', value: `**${xp.toLocaleString()}**`, inline: true },
|
|
{ name: 'XP Bonus', value: prestige > 0 ? `+${prestige * 5}%` : 'None', inline: true },
|
|
{ name: 'Total XP Earned', value: totalXpEarned.toLocaleString(), inline: true },
|
|
{ name: 'Progress to Next Level', value: `${progressBar}\n${progress.toLocaleString()} / ${needed.toLocaleString()} XP (${progressPercent}%)` },
|
|
{ name: 'Primary Realm', value: link.primary_arm || 'None set', inline: true }
|
|
)
|
|
.setFooter({ text: prestige >= 1 ? `🌐 Federation • Prestige ${prestige} | XP earned across Discord & AeThex platforms` : '🌐 Federation • XP earned across Discord & AeThex platforms' })
|
|
.setTimestamp();
|
|
|
|
await interaction.editReply({ embeds: [embed] });
|
|
}
|
|
|
|
function createProgressBar(percent) {
|
|
const filled = Math.floor(percent / 10);
|
|
const empty = 10 - filled;
|
|
return '█'.repeat(filled) + '░'.repeat(empty);
|
|
}
|
|
|
|
function getPrestigeInfo(level) {
|
|
const prestiges = [
|
|
{ name: 'Unprestiged', icon: '⚪', color: 0x6b7280 },
|
|
{ name: 'Bronze', icon: '🥉', color: 0xcd7f32 },
|
|
{ name: 'Silver', icon: '🥈', color: 0xc0c0c0 },
|
|
{ name: 'Gold', icon: '🥇', color: 0xffd700 },
|
|
{ name: 'Platinum', icon: '💎', color: 0xe5e4e2 },
|
|
{ name: 'Diamond', icon: '💠', color: 0xb9f2ff },
|
|
{ name: 'Master', icon: '🔥', color: 0xff4500 },
|
|
{ name: 'Grandmaster', icon: '⚔️', color: 0x9400d3 },
|
|
{ name: 'Champion', icon: '👑', color: 0xffd700 },
|
|
{ name: 'Legend', icon: '🌟', color: 0xff69b4 },
|
|
{ name: 'Mythic', icon: '🌈', color: 0x7c3aed }
|
|
];
|
|
return prestiges[Math.min(level, 10)] || prestiges[0];
|
|
}
|