const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); const { getServerMode, EMBED_COLORS } = require("../utils/modeHelper"); const { getStandaloneXp, calculateLevel } = require("../utils/standaloneXp"); module.exports = { data: new SlashCommandBuilder() .setName("profile") .setDescription("View your profile") .addUserOption(option => option.setName('user') .setDescription('User to view profile of') .setRequired(false) ), async execute(interaction, supabase) { if (!supabase) { return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true }); } await interaction.deferReply(); const targetUser = interaction.options.getUser('user') || interaction.user; try { const mode = await getServerMode(supabase, interaction.guildId); if (mode === 'standalone') { return handleStandaloneProfile(interaction, supabase, targetUser); } else { return handleFederatedProfile(interaction, supabase, targetUser); } } catch (error) { console.error("Profile command error:", error); const embed = new EmbedBuilder() .setColor(0xff0000) .setTitle("Error") .setDescription("Failed to fetch profile. Please try again."); await interaction.editReply({ embeds: [embed] }); } }, }; async function handleStandaloneProfile(interaction, supabase, targetUser) { const data = await getStandaloneXp(supabase, targetUser.id, interaction.guildId); if (!data) { const embed = new EmbedBuilder() .setColor(EMBED_COLORS.standalone) .setTitle("No Profile Found") .setThumbnail(targetUser.displayAvatarURL({ size: 256 })) .setDescription( targetUser.id === interaction.user.id ? "You don't have any XP yet. Start chatting to build your profile!" : `${targetUser.tag} hasn't earned any XP yet in this server.` ); return await interaction.editReply({ embeds: [embed] }); } const xp = data.xp || 0; const prestige = data.prestige_level || 0; const totalXpEarned = data.total_xp_earned || xp; const level = calculateLevel(xp, 'normal'); const dailyStreak = data.daily_streak || 0; const currentLevelXp = level * level * 100; const nextLevelXp = (level + 1) * (level + 1) * 100; const progressXp = xp - currentLevelXp; const neededXp = nextLevelXp - currentLevelXp; const progressPercent = Math.min(100, Math.floor((progressXp / neededXp) * 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(EMBED_COLORS.standalone) .setAuthor({ name: targetUser.tag, iconURL: targetUser.displayAvatarURL({ size: 64 }) }) .setThumbnail(targetUser.displayAvatarURL({ size: 256 })) .addFields( { name: "Username", value: `\`${data.username || targetUser.username}\``, inline: true }, { name: "Server Rank", value: `#${(rankPosition || 0) + 1}`, inline: true }, { name: "Daily Streak", value: `${dailyStreak} days`, inline: true }, { name: `${prestigeInfo.icon || ''} Prestige`, value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige}) +${prestige * 5}% XP` : 'Not prestiged', inline: true }, { name: `Level ${level}`, value: `${progressBar}\n\`${xp.toLocaleString()}\` / \`${nextLevelXp.toLocaleString()}\` XP`, inline: false }, { name: "Total XP Earned", value: totalXpEarned.toLocaleString(), inline: true } ) .setFooter({ text: `🏠 Standalone Mode • ${interaction.guild.name}`, iconURL: interaction.guild.iconURL({ size: 32 }) }) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); } async function handleFederatedProfile(interaction, supabase, targetUser) { const { data: link } = await supabase .from("discord_links") .select("user_id, primary_arm") .eq("discord_id", targetUser.id) .maybeSingle(); if (!link) { const embed = new EmbedBuilder() .setColor(0xff6b6b) .setTitle("Not Linked") .setThumbnail(targetUser.displayAvatarURL({ size: 256 })) .setDescription( targetUser.id === interaction.user.id ? "You must link your Discord account to AeThex first.\nUse `/verify` to get started." : `${targetUser.tag} hasn't linked their Discord account to AeThex yet.` ); return await interaction.editReply({ embeds: [embed] }); } const { data: profile } = await supabase .from("user_profiles") .select("*") .eq("id", link.user_id) .maybeSingle(); if (!profile) { const embed = new EmbedBuilder() .setColor(0xff6b6b) .setTitle("Profile Not Found") .setDescription("The AeThex profile could not be found."); return await interaction.editReply({ embeds: [embed] }); } const armEmojis = { labs: "", gameforge: "", corp: "", foundation: "", devlink: "", }; const armColors = { labs: 0x22c55e, gameforge: 0xf97316, corp: 0x3b82f6, foundation: 0xec4899, devlink: 0x8b5cf6, }; const xp = profile.xp || 0; const prestige = profile.prestige_level || 0; const level = Math.floor(Math.sqrt(xp / 100)); const currentLevelXp = level * level * 100; const nextLevelXp = (level + 1) * (level + 1) * 100; const progressXp = xp - currentLevelXp; const neededXp = nextLevelXp - currentLevelXp; const progressPercent = Math.min(100, Math.floor((progressXp / neededXp) * 100)); const progressBar = createProgressBar(progressPercent); const prestigeInfo = getPrestigeInfo(prestige); const badges = profile.badges || []; const badgeDisplay = badges.length > 0 ? badges.map(b => getBadgeEmoji(b)).join(' ') : 'No badges yet'; let avatarUrl = targetUser.displayAvatarURL({ size: 256 }); if (profile.avatar_url && profile.avatar_url.startsWith('http')) { avatarUrl = profile.avatar_url; } const embed = new EmbedBuilder() .setColor(armColors[link.primary_arm] || 0x7c3aed) .setAuthor({ name: `${profile.full_name || profile.username || 'AeThex User'}`, iconURL: targetUser.displayAvatarURL({ size: 64 }) }) .setThumbnail(avatarUrl) .setDescription(profile.bio || '*No bio set*') .addFields( { name: "Username", value: `\`${profile.username || 'N/A'}\``, inline: true }, { name: `${armEmojis[link.primary_arm] || ""} Realm`, value: capitalizeFirst(link.primary_arm) || "Not set", inline: true }, { name: "Role", value: formatRole(profile.user_type), inline: true }, { name: `${prestigeInfo.icon || ''} Prestige`, value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige}) +${prestige * 5}% XP` : 'Not prestiged', inline: true }, { name: `Level ${level}`, value: `${progressBar}\n\`${xp.toLocaleString()}\` / \`${nextLevelXp.toLocaleString()}\` XP`, inline: false }, { name: "Badges", value: badgeDisplay, inline: false } ) .addFields({ name: "Links", value: `[View Full Profile](https://aethex.dev/creators/${profile.username}) • [AeThex Platform](https://aethex.dev)`, }) .setFooter({ text: `🌐 Federation • ${targetUser.tag}`, iconURL: 'https://aethex.dev/favicon.ico' }) .setTimestamp(); if (profile.banner_url) { embed.setImage(profile.banner_url); } await interaction.editReply({ embeds: [embed] }); } function createProgressBar(percent) { const filled = Math.floor(percent / 10); const empty = 10 - filled; return `${'▓'.repeat(filled)}${'░'.repeat(empty)} ${percent}%`; } function capitalizeFirst(str) { if (!str) return str; return str.charAt(0).toUpperCase() + str.slice(1); } function formatRole(role) { if (!role) return 'Member'; return role.split('_').map(capitalizeFirst).join(' '); } function getBadgeEmoji(badge) { const badgeMap = { 'verified': 'Verified', 'founder': 'Founder', 'early_adopter': 'Early Adopter', 'contributor': 'Contributor', 'creator': 'Creator', 'developer': 'Developer', 'moderator': 'Moderator', 'partner': 'Partner', 'premium': 'Premium', 'top_poster': 'Top Poster', 'helpful': 'Helpful', 'bug_hunter': 'Bug Hunter', 'event_winner': 'Event Winner', }; return badgeMap[badge] || `[${badge}]`; } 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]; }