diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 4897472..c332038 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,8 +1,30 @@ # šŸš€ VRBattles Discord Bot - Deployment Guide -## šŸ“¦ **Latest Version: v1.2.7** +## šŸ“¦ **Latest Version: v1.2.10** -### **What's New in v1.2.7:** +### **What's New in v1.2.10:** +- āœ… MAJOR FIX: Tournament status filter now works correctly and permanently +- āœ… Fixed sync-games script overwriting tournament status choices +- āœ… Tournament status uses actual API values (Published, In-progress, Ended) +- āœ… Excluded Unpublished status (admin-only tournaments) +- āœ… Protected manual choice customizations from future overwrites +- āœ… Added emojis to status filter choices for better UX + +### **Previous in v1.2.9:** +- āœ… FIXED: Tournament status filter now works correctly +- āœ… Tournament status filter uses proper values (Published, In-progress, Ended) +- āœ… Removed Unpublished status from filter options +- āœ… Added emojis to status filter choices for better UX + +### **Previous in v1.2.8:** +- āœ… NEW: `/tournaments` command with beautiful formatting +- āœ… Filter tournaments by status (Published, In-progress, Ended, Unpublished) +- āœ… Filter tournaments by game (VAIL, Echo Arena, Blacktop Hoops, etc.) +- āœ… Smart date formatting with relative times +- āœ… Rich embeds with prize pools, entry fees, and team sizes +- āœ… Enhanced PlayerService with tournaments API integration + +### **Previous in v1.2.7:** - āœ… Fixed Docker multi-platform build (x86_64 + ARM64) - āœ… Resolved "exec format error" on Coolify deployment - āœ… Enhanced deployment script with buildx support @@ -18,7 +40,7 @@ ### **Docker Images Available:** ``` far54/vrbattles-discord-bot:latest (always current) -far54/vrbattles-discord-bot:v1.2.7 (specific version) +far54/vrbattles-discord-bot:v1.2.10 (specific version) ``` ### **Multi-Platform Support:** @@ -63,6 +85,7 @@ NODE_ENV=production - `/finduser` - Search players (9 game dropdown) - `/findteam` - Search teams (9 game dropdown) - `/matchhistory` - View match history (optional game filter) +- `/tournaments` - **NEW!** View all tournaments with filters - `/subscribe` - Admin: Subscribe to game notifications - `/unsubscribe` - Admin: Remove game subscriptions - `/list_subscriptions` - Admin: View active subscriptions diff --git a/deploy-commands.js b/deploy-commands.js index 12e3af0..9ae24ec 100644 --- a/deploy-commands.js +++ b/deploy-commands.js @@ -159,6 +159,37 @@ const commands = [ .setDescription("The team name to search for") .setRequired(true) ), + new SlashCommandBuilder() + .setName("tournaments") + .setDescription("View all available tournaments") + .addStringOption((option) => + option + .setName("status") + .setDescription("Filter tournaments by status (optional)") + .setRequired(false) + .addChoices( + { name: "šŸ“¢ Published", value: "Published" }, + { name: "⚔ In-progress", value: "In-progress" }, + { name: "šŸ Ended", value: "Ended" } + ) + ) + .addStringOption((option) => + option + .setName("game") + .setDescription("Filter tournaments by game (optional)") + .setRequired(false) + .addChoices( + { name: "Big Ballers VR", value: "Big Ballers VR" }, + { name: "Blacktop Hoops", value: "Blacktop Hoops" }, + { name: "Breachers", value: "Breachers" }, + { name: "Echo Arena", value: "Echo Arena" }, + { name: "Echo Combat", value: "Echo Combat" }, + { name: "Gun Raiders", value: "Gun Raiders" }, + { name: "Nock", value: "Nock" }, + { name: "Orion Drift", value: "Orion Drift" }, + { name: "VAIL", value: "VAIL" } + ) + ), ]; const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); diff --git a/docs/issue-tracker.md b/docs/issue-tracker.md new file mode 100644 index 0000000..abfa05f --- /dev/null +++ b/docs/issue-tracker.md @@ -0,0 +1,49 @@ +# VRBattles Discord Bot – Issue Tracker + +Use this checklist to track code-quality fixes and consistency updates. Check items off as completed and commit this file with your changes. + +## How to use +- Edit this file, change "[ ]" to "[x]" for completed items, and include a short note or PR link if helpful. + +## Checklist + +- [ ] Replace admin permission check with PermissionFlagsBits.Administrator + - Location: `src/commands/CommandHandler.js` (in `handleRegisterServer`) + - Desired: `interaction.member.permissions.has(PermissionFlagsBits.Administrator)` + +- [ ] Replace legacy `flags` ephemeral usage with `ephemeral: true` + - Location: various replies in `src/commands/CommandHandler.js` and `src/commands/SubscriptionCommands.js` + - Notes: decide ephemerality at `reply`/`deferReply`; it cannot be changed via `editReply` + +- [ ] Switch `interaction.isCommand()` to `interaction.isChatInputCommand()` + - Location: `src/Bot.js` (in `handleInteraction`) + +- [ ] Use `PermissionFlagsBits` for channel permission checks + - Location: `src/services/NotificationService.js` + - Replace string permissions (e.g., 'SendMessages', 'ViewChannel') with `PermissionFlagsBits.*` + +- [ ] Clean up `SubscriptionCommands` duplicates and constructor mismatch + - Location: `src/commands/SubscriptionCommands.js` + - Remove duplicated methods (`getGame`, `createOrUpdateSubscription`, `safeReply`), fix constructor to accept `(supabase, logger)` and use the injected logger + +- [ ] Fix `PlayerService` logging + - Location: `src/services/PlayerService.js` + - Inject a logger or use a consistent logging strategy; remove `this.logger` references if not injected + +- [ ] Align `/help` category choices with handler expectations + - Locations: + - `deploy-commands.js` (slash command choices for `help`) + - `src/commands/HelpCommand.js` (expects categories like `getting_started`, `search`, etc.) + - Decide whether to offer topic categories or game names and make both sides consistent + +- [ ] Add guild-scoped slash command deployment for development + - Location: `deploy-commands.js` (or add a new `deploy-commands.dev.js`) + - Use `Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID)` for fast iteration in dev + +- [ ] Standardize interaction handlers and checks + - Ensure consistent use of `isChatInputCommand`, `isButton`, and ephemerality decisions across commands and buttons + +## Notes +- Keep PRs small and focused (one checklist item per PR when possible). +- Update this file with any extra context, follow-ups, or links. + diff --git a/package.json b/package.json index c0ab2fa..dd7c558 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vrbattles-discord-bot", - "version": "1.2.7", + "version": "1.2.11", "description": "VRBattles Discord Bot - Player search, team lookup, match notifications, and more for VR gaming communities", "main": "index.js", "scripts": { diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 270d314..15e8824 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -84,10 +84,11 @@ echo -e " 2. Update your application image to: ${FULL_IMAGE_NAME}:${VERSION}" echo -e " 3. Restart the application" echo "" echo -e "${BLUE}šŸŽÆ Features in this version:${NC}" +echo -e " āœ… NEW: /tournaments command with filtering" +echo -e " āœ… Tournament status & game filters" +echo -e " āœ… Beautiful tournament embeds with prizes" echo -e " āœ… All 9 games in dropdowns" echo -e " āœ… Working help button navigation" -echo -e " āœ… No debug message spam" echo -e " āœ… Automatic Supabase game sync" -echo -e " āœ… Optimized Docker image" echo "" echo -e "${GREEN}šŸŽ‰ Deployment complete!${NC}" \ No newline at end of file diff --git a/src/Bot.js b/src/Bot.js index 5a1e54b..c4aadcf 100644 --- a/src/Bot.js +++ b/src/Bot.js @@ -1,4 +1,4 @@ -const { Client, GatewayIntentBits } = require('discord.js'); +const { Client, GatewayIntentBits, MessageFlags } = require('discord.js'); const CommandHandler = require('./commands/CommandHandler'); const SubscriptionCommands = require('./commands/SubscriptionCommands'); const PlayerService = require('./services/PlayerService'); @@ -79,7 +79,7 @@ class Bot { } async handleInteraction(interaction) { - if (!interaction.isCommand() && !interaction.isButton()) { + if (!interaction.isChatInputCommand() && !interaction.isButton()) { this.logger.debug('Ignoring non-command/button interaction'); return; } @@ -93,31 +93,19 @@ class Bot { this.processingLock.set(lockKey, true); try { - if (interaction.isCommand()) { - // Quick defer with timeout handling + if (interaction.isChatInputCommand()) { + // Defer once to acknowledge within the 3s window const isPublicCommand = ['finduser', 'matchhistory', 'findteam'].includes(interaction.commandName); - try { - await Promise.race([ - interaction.deferReply({ ephemeral: !isPublicCommand }), - new Promise((_, reject) => setTimeout(() => reject(new Error('Defer timeout')), 2500)) - ]); + await interaction.deferReply(!isPublicCommand ? { flags: MessageFlags.Ephemeral } : {}); } catch (deferError) { this.logger.warn('Failed to defer interaction:', { error: deferError.message, command: interaction.commandName, guild: interaction.guild?.name }); - // Try to reply immediately if defer failed - try { - await interaction.reply({ - content: 'ā³ Processing your request...', - ephemeral: !isPublicCommand - }); - } catch (replyError) { - this.logger.error('Failed to reply after defer timeout:', replyError); - return; // Give up on this interaction - } + // If we failed to acknowledge, do not attempt another response + return; } this.logger.debug('Processing command', { diff --git a/src/commands/CommandHandler.js b/src/commands/CommandHandler.js index 3bcc032..2ab092e 100644 --- a/src/commands/CommandHandler.js +++ b/src/commands/CommandHandler.js @@ -1,4 +1,4 @@ -const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); +const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, PermissionFlagsBits, MessageFlags } = require('discord.js'); const EmbedBuilders = require('../utils/embedBuilders'); const HelpCommand = require('./HelpCommand'); const PaginationManager = require('../utils/PaginationManager'); @@ -22,9 +22,8 @@ class CommandHandler { // Check cooldown const cooldownTime = this.cooldownManager.checkCooldown(interaction); if (cooldownTime > 0) { - await interaction.reply({ - content: `ā° Please wait ${cooldownTime.toFixed(1)} seconds before using this command again.`, - flags: ['Ephemeral'] + await interaction.editReply({ + content: `ā° Please wait ${cooldownTime.toFixed(1)} seconds before using this command again.` }); return; } @@ -60,10 +59,12 @@ class CommandHandler { case 'version': await this.handleVersion(interaction); break; + case 'tournaments': + await this.handleTournaments(interaction); + break; default: await interaction.editReply({ - content: 'āŒ Unknown command.', - flags: ['Ephemeral'] + content: 'āŒ Unknown command.' }); } } catch (error) { @@ -83,7 +84,7 @@ class CommandHandler { } else { await interaction.reply({ content: 'āŒ An error occurred while processing your command.', - flags: ['Ephemeral'] + flags: MessageFlags.Ephemeral }); } } catch (replyError) { @@ -164,10 +165,9 @@ class CommandHandler { async handleRegisterServer(interaction) { try { // Check for admin permissions - if (!interaction.member.permissions.has('ADMINISTRATOR')) { + if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) { await interaction.editReply({ - content: 'āŒ You need administrator permissions to register this server.', - ephemeral: true + content: 'āŒ You need administrator permissions to register this server.' }); return; } @@ -197,8 +197,7 @@ class CommandHandler { if (!result || typeof result !== 'object') { this.logger.error('Invalid result from registerServer:', { result, guildId, serverName }); await interaction.editReply({ - content: 'āŒ Server registration failed due to invalid response. Please try again or contact support.', - ephemeral: true + content: 'āŒ Server registration failed due to invalid response. Please try again or contact support.' }); return; } @@ -211,8 +210,7 @@ class CommandHandler { timestamp: new Date().toISOString() }); await interaction.editReply({ - content: 'āŒ Server registration failed - no server data returned. This might be a database permissions issue.', - ephemeral: true + content: 'āŒ Server registration failed - no server data returned. This might be a database permissions issue.' }); return; } @@ -234,8 +232,7 @@ class CommandHandler { } await interaction.editReply({ - content: message, - ephemeral: true + content: message }); // Log the successful operation @@ -260,8 +257,7 @@ class CommandHandler { : 'āŒ An error occurred while registering the server. Please try again later.'; await interaction.editReply({ - content: errorMessage, - ephemeral: true + content: errorMessage }); throw error; @@ -446,8 +442,7 @@ class CommandHandler { // Input validation if (!teamName || typeof teamName !== 'string') { await interaction.editReply({ - content: 'āŒ Invalid team name provided.', - ephemeral: false + content: 'āŒ Invalid team name provided.' }); return; } @@ -460,8 +455,7 @@ class CommandHandler { if (!sanitizedTeamName) { await interaction.editReply({ - content: 'āŒ Team name must contain valid characters (letters, numbers, spaces, hyphens, underscores, or periods).', - ephemeral: false + content: 'āŒ Team name must contain valid characters (letters, numbers, spaces, hyphens, underscores, or periods).' }); return; } @@ -479,8 +473,7 @@ class CommandHandler { if (!teamData || !teamData.success || !teamData.teams || teamData.teams.length === 0) { await interaction.editReply({ - content: 'āŒ No teams found matching your search criteria.', - ephemeral: false + content: 'āŒ No teams found matching your search criteria.' }); return; } @@ -489,8 +482,7 @@ class CommandHandler { await interaction.editReply({ embeds: embeds, - components: [], - ephemeral: false + components: [] }); } catch (error) { this.logger.error('Error in handleFindTeam:', { @@ -564,7 +556,7 @@ class CommandHandler { await interaction.reply({ content: 'Select menu interactions are not yet implemented.', - flags: ['Ephemeral'] + flags: MessageFlags.Ephemeral }); } catch (error) { this.logger.error('Error in handleSelectMenu:', { @@ -640,8 +632,7 @@ class CommandHandler { } catch (error) { this.logger.error('Error in handleVersion:', error); await interaction.editReply({ - content: 'āŒ An error occurred while fetching version information.', - flags: ['Ephemeral'] + content: 'āŒ An error occurred while fetching version information.' }); } } @@ -654,6 +645,97 @@ class CommandHandler { .setURL(`https://www.vrbattles.gg/profile/${username}`) ); } + + async handleTournaments(interaction) { + try { + this.logger.info('Processing tournaments command', { + userId: interaction.user.id, + guildId: interaction.guildId + }); + + // Get filter options + const statusFilter = interaction.options.getString('status'); + const gameFilter = interaction.options.getString('game'); + + // Fetch tournaments data + const tournamentsData = await this.playerService.getTournamentsData(); + + if (!tournamentsData || tournamentsData.success === false) { + await interaction.editReply({ + content: 'āŒ Failed to fetch tournaments data. Please try again later.', + }); + return; + } + + let tournaments = tournamentsData.data || tournamentsData; + if (!Array.isArray(tournaments)) { + await interaction.editReply({ + content: 'āŒ Invalid tournaments data received from API.', + }); + return; + } + + // Apply filters + if (statusFilter) { + tournaments = tournaments.filter(t => t.tournament_status === statusFilter); + } + if (gameFilter) { + tournaments = tournaments.filter(t => t.name === gameFilter); + } + + if (tournaments.length === 0) { + const filterText = []; + if (statusFilter) filterText.push(`status: ${statusFilter}`); + if (gameFilter) filterText.push(`game: ${gameFilter}`); + const filterMsg = filterText.length > 0 ? ` matching ${filterText.join(', ')}` : ''; + + await interaction.editReply({ + content: `šŸ† No tournaments found${filterMsg}.`, + }); + return; + } + + // Sort tournaments by date (upcoming first, then recent) + tournaments.sort((a, b) => { + const dateA = new Date(a.date_start); + const dateB = new Date(b.date_start); + const now = new Date(); + + // Upcoming tournaments first + if (dateA > now && dateB > now) { + return dateA - dateB; // Soonest first + } + // Past tournaments last (most recent first) + if (dateA < now && dateB < now) { + return dateB - dateA; // Most recent first + } + // Mix: upcoming before past + if (dateA > now && dateB < now) return -1; + if (dateA < now && dateB > now) return 1; + + return dateA - dateB; + }); + + // Create tournament embed + const embed = EmbedBuilders.createTournamentsEmbed(tournaments, statusFilter, gameFilter); + + await interaction.editReply({ + embeds: [embed] + }); + + } catch (error) { + this.logger.error('Error in handleTournaments:', { + error: error.message, + stack: error.stack, + userId: interaction.user.id, + guildId: interaction.guildId + }); + + await interaction.editReply({ + content: 'āŒ An error occurred while fetching tournaments.', + }); + } + } } module.exports = CommandHandler; \ No newline at end of file diff --git a/src/scripts/syncGameChoices.js b/src/scripts/syncGameChoices.js index 4f5226c..76961b5 100644 --- a/src/scripts/syncGameChoices.js +++ b/src/scripts/syncGameChoices.js @@ -44,15 +44,23 @@ async function syncGameChoices() { const deployPath = path.join(__dirname, '../../deploy-commands.js'); let deployContent = fs.readFileSync(deployPath, 'utf8'); - // Pattern to match the game choices in addChoices() - const choicesPattern = /\.addChoices\(\s*([^)]+)\s*\)/g; + // Pattern to match ONLY game choice blocks (avoid tournament status choices) + // Look for game choice patterns in finduser, findteam, matchhistory, subscribe, unsubscribe commands + const gameChoiceCommands = ['finduser', 'findteam', 'matchhistory', 'subscribe', 'unsubscribe']; - // Replace all instances with updated choices - const newChoicesBlock = `.addChoices( + gameChoiceCommands.forEach(commandName => { + // Match the specific pattern for each game command + const commandPattern = new RegExp( + `(\\.setName\\("${commandName}"\\)[\\s\\S]*?\\.setName\\("game"\\)[\\s\\S]*?)\\.addChoices\\(\\s*([^)]+)\\s*\\)`, + 'g' + ); + + const newChoicesBlock = `.addChoices( ${choicesCode} )`; - - deployContent = deployContent.replace(choicesPattern, newChoicesBlock); + + deployContent = deployContent.replace(commandPattern, `$1${newChoicesBlock}`); + }); // Write updated file fs.writeFileSync(deployPath, deployContent); diff --git a/src/services/PlayerService.js b/src/services/PlayerService.js index 8e6d683..0c772af 100644 --- a/src/services/PlayerService.js +++ b/src/services/PlayerService.js @@ -76,6 +76,41 @@ class PlayerService { return `${this.baseUrl}/profile/${username}/stats`; } + async getTournamentsData() { + try { + console.log('Fetching tournaments data...'); + const url = `${this.baseUrl}/api/fetch/tournaments`; + console.log(`API URL: ${url}`); + + const response = await axios.get(url, { + timeout: 5000, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + validateStatus: function (status) { + return status >= 200 && status < 300; + } + }); + + console.log('Tournaments API Response:', JSON.stringify(response.data, null, 2)); + return response.data; + } catch (error) { + console.error('Error fetching tournaments data:', { + message: error.message, + response: error.response?.data, + status: error.response?.status + }); + return { success: false, error: 'Failed to fetch tournaments data' }; + } + } + + async getTournamentData(tournamentId) { + const url = `${this.baseUrl}/api/get_tournament_data/${tournamentId}`; + const response = await axios.get(url); + return response.data; + } + async findTeamByName(teamName, gameFilter) { try { // Double-check sanitization here as well for defense in depth diff --git a/src/tests/testTournaments.js b/src/tests/testTournaments.js new file mode 100644 index 0000000..226ad8a --- /dev/null +++ b/src/tests/testTournaments.js @@ -0,0 +1,90 @@ +// Test script for tournaments functionality +require('dotenv').config(); +const PlayerService = require('../services/PlayerService'); + +async function testTournamentsAPI() { + console.log('šŸ† Testing Tournaments API Functions...\n'); + + const playerService = new PlayerService(); + + try { + // Test 1: Get all tournaments + console.log('šŸ“‹ Test 1: Fetching all tournaments...'); + console.log('=' .repeat(50)); + + const tournamentsData = await playerService.getTournamentsData(); + + if (tournamentsData && tournamentsData.success !== false) { + console.log('āœ… Successfully fetched tournaments data!'); + console.log(`šŸ“Š Response structure:`, Object.keys(tournamentsData)); + + // Show some basic info about the tournaments + if (Array.isArray(tournamentsData.tournaments)) { + console.log(`šŸŽÆ Found ${tournamentsData.tournaments.length} tournaments`); + + // Show first few tournaments as examples + const sampleSize = Math.min(3, tournamentsData.tournaments.length); + console.log(`\nšŸ“– Sample tournaments (showing ${sampleSize}):`); + + for (let i = 0; i < sampleSize; i++) { + const tournament = tournamentsData.tournaments[i]; + console.log(` ${i + 1}. ${tournament.name || tournament.title || 'Unknown'} (ID: ${tournament.id || 'N/A'})`); + } + } else if (Array.isArray(tournamentsData)) { + console.log(`šŸŽÆ Found ${tournamentsData.length} tournaments (direct array)`); + + const sampleSize = Math.min(3, tournamentsData.length); + console.log(`\nšŸ“– Sample tournaments (showing ${sampleSize}):`); + + for (let i = 0; i < sampleSize; i++) { + const tournament = tournamentsData[i]; + console.log(` ${i + 1}. ${tournament.name || tournament.title || 'Unknown'} (ID: ${tournament.id || 'N/A'})`); + } + } else { + console.log('šŸ“Š Response data:', JSON.stringify(tournamentsData, null, 2)); + } + } else { + console.log('āŒ Failed to fetch tournaments data'); + console.log('Error:', tournamentsData?.error || 'Unknown error'); + } + + console.log('\n' + '=' .repeat(50)); + + // Test 2: Try to get specific tournament data (if we have any tournament IDs) + console.log('\nšŸŽÆ Test 2: Testing single tournament fetch...'); + console.log('=' .repeat(50)); + + // Try with a sample tournament ID (you might need to adjust this based on actual data) + const testTournamentId = '1'; // Start with ID 1 as a common starting point + + console.log(`šŸ“‹ Fetching tournament with ID: ${testTournamentId}`); + const singleTournament = await playerService.getTournamentData(testTournamentId); + + if (singleTournament && singleTournament.success !== false) { + console.log('āœ… Successfully fetched single tournament data!'); + console.log('šŸ“Š Tournament details:'); + console.log(` Name: ${singleTournament.name || singleTournament.title || 'Unknown'}`); + console.log(` Game: ${singleTournament.game || singleTournament.game_name || 'Unknown'}`); + console.log(` Status: ${singleTournament.status || 'Unknown'}`); + } else { + console.log(`āŒ Failed to fetch tournament with ID: ${testTournamentId}`); + console.log('Error:', singleTournament?.error || 'Unknown error'); + console.log('šŸ’” This might be normal if tournament ID 1 doesn\'t exist'); + } + + } catch (error) { + console.error('āŒ Test failed with error:', { + message: error.message, + stack: error.stack + }); + } + + console.log('\nšŸ Tournament API testing completed!'); +} + +// Run the test +if (require.main === module) { + testTournamentsAPI().catch(console.error); +} + +module.exports = { testTournamentsAPI }; \ No newline at end of file diff --git a/src/utils/embedBuilders.js b/src/utils/embedBuilders.js index 54b4078..588dab2 100644 --- a/src/utils/embedBuilders.js +++ b/src/utils/embedBuilders.js @@ -353,6 +353,141 @@ class EmbedBuilders { return embed; } + + static createTournamentsEmbed(tournaments, statusFilter, gameFilter) { + const embed = new EmbedBuilder() + .setTitle('šŸ† VRBattles Tournaments') + .setColor('#FFD700') // Gold color for tournaments + .setTimestamp(); + + // Add filter info in description if filters are applied + let description = ''; + const filterInfo = []; + if (statusFilter) filterInfo.push(`**Status:** ${statusFilter}`); + if (gameFilter) filterInfo.push(`**Game:** ${gameFilter}`); + + if (filterInfo.length > 0) { + description = `*Filtered by: ${filterInfo.join(' | ')}*\n\n`; + } + + description += `Found **${tournaments.length}** tournament${tournaments.length !== 1 ? 's' : ''}`; + embed.setDescription(description); + + // Group tournaments by status for better organization + const grouped = { + 'Published': [], + 'In-progress': [], + 'Ended': [], + 'Unpublished': [] + }; + + tournaments.forEach(tournament => { + const status = tournament.tournament_status || 'Unknown'; + if (!grouped[status]) grouped[status] = []; + grouped[status].push(tournament); + }); + + // Add tournaments by status (show active ones first) + const statusOrder = ['Published', 'In-progress', 'Ended', 'Unpublished']; + + statusOrder.forEach(status => { + if (grouped[status].length === 0) return; + + // Status emoji mapping + const statusEmoji = { + 'Published': 'šŸ“¢', + 'In-progress': '⚔', + 'Ended': 'šŸ', + 'Unpublished': 'šŸ“' + }; + + const emoji = statusEmoji[status] || 'šŸŽÆ'; + embed.addFields({ + name: `${emoji} ${status} Tournaments (${grouped[status].length})`, + value: '\u200B', + inline: false + }); + + // Add each tournament in this status + grouped[status].slice(0, 10).forEach(tournament => { + const tournamentName = tournament.tournament_name || 'Unknown Tournament'; + const gameName = tournament.name || 'Unknown Game'; + const prize = tournament.prize ? `$${tournament.prize}` : 'No prize'; + const entryFee = tournament.entry_cost === '0' ? 'Free' : `$${tournament.entry_cost}`; + const teamSize = tournament.team_size_local || tournament.team_size || 'Unknown'; + const maxTeams = tournament.max_teams || 'Unknown'; + const tournamentType = tournament.type || 'Unknown'; + + // Format date + let dateInfo = ''; + if (tournament.date_start) { + const startDate = new Date(tournament.date_start); + const now = new Date(); + + if (startDate > now) { + dateInfo = `šŸ—“ļø Starts: `; + } else { + dateInfo = `šŸ—“ļø Started: `; + } + } + + // Build tournament info + const tournamentInfo = [ + `šŸŽ® **${gameName}**`, + `šŸ’° Prize: **${prize}** | Entry: **${entryFee}**`, + `šŸ‘„ **${teamSize}** (Max: ${maxTeams}) | Type: **${tournamentType}**`, + dateInfo + ].filter(info => info).join('\n'); + + embed.addFields({ + name: `**${tournamentName}** (ID: ${tournament.id})`, + value: tournamentInfo, + inline: true + }); + }); + + // Add a separator if there are more statuses coming + const currentIndex = statusOrder.indexOf(status); + const hasMoreStatuses = statusOrder.slice(currentIndex + 1).some(s => grouped[s].length > 0); + if (hasMoreStatuses) { + embed.addFields({ name: '\u200B', value: '▬▬▬▬▬▬▬▬▬▬', inline: false }); + } + }); + + // Add footer with helpful info + const totalTournaments = tournaments.length; + if (totalTournaments > 30) { + embed.setFooter({ + text: `Showing first 30 tournaments. Total: ${totalTournaments} | Use filters to narrow results` + }); + } else { + embed.setFooter({ + text: 'Use /tournaments status: or /tournaments game: to filter results' + }); + } + + // Set thumbnail based on game filter or use default + if (gameFilter) { + // You could add game-specific thumbnails here + switch (gameFilter) { + case 'VAIL': + embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/--%20Logos/VAIL_Icon_Color.png'); + break; + case 'Echo Arena': + embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/echovr.png'); + break; + case 'Blacktop Hoops': + embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/Blacktop%20Hoops.png'); + break; + default: + embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/--%20Logos/VAIL_Icon_Color.png'); + } + } else { + embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/--%20Logos/VAIL_Icon_Color.png'); + } + + return embed; + } } module.exports = EmbedBuilders; \ No newline at end of file