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
262 lines
9 KiB
JavaScript
262 lines
9 KiB
JavaScript
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];
|
|
}
|