diff --git a/backend/src/repositories/GameRepository.js b/backend/src/repositories/GameRepository.js index 7b9d478..fb50d0c 100644 --- a/backend/src/repositories/GameRepository.js +++ b/backend/src/repositories/GameRepository.js @@ -97,16 +97,43 @@ export class GameRepository extends Repository { paramCount++ } - if (filters.won !== undefined) { - query += ` AND g.won = $${paramCount}` - params.push(filters.won) - paramCount++ - } + if (filters.won !== undefined) { + query += ` AND g.won = $${paramCount}` + params.push(filters.won) + paramCount++ + } - query += ` ORDER BY g.date DESC LIMIT $${paramCount} OFFSET $${paramCount + 1}` - params.push(limit, offset) + if (filters.roundsMin !== undefined) { + query += ` AND g.rounds >= $${paramCount}` + params.push(filters.roundsMin) + paramCount++ + } - return dbManager.all(query, params) + if (filters.roundsMax !== undefined) { + query += ` AND g.rounds <= $${paramCount}` + params.push(filters.roundsMax) + paramCount++ + } + + if (filters.colors && filters.colors.length > 0) { + // Filter by commander color identity - checks if colors array contains any of the specified colors + const colorConditions = filters.colors.map(() => { + const condition = `cmdr.colors @> $${paramCount}::jsonb` + paramCount++ + return condition + }) + query += ` AND (${colorConditions.join(' OR ')})` + filters.colors.forEach(color => { + params.push(JSON.stringify([color])) + }) + paramCount -= filters.colors.length + paramCount += filters.colors.length + } + + query += ` ORDER BY g.date DESC LIMIT $${paramCount} OFFSET $${paramCount + 1}` + params.push(limit, offset) + + return dbManager.all(query, params) } /** @@ -161,15 +188,48 @@ export class GameRepository extends Repository { paramCount++ } - if (filters.dateTo) { - query += ` AND g.date <= $${paramCount}` - params.push(filters.dateTo) - paramCount++ - } + if (filters.dateTo) { + query += ` AND g.date <= $${paramCount}` + params.push(filters.dateTo) + paramCount++ + } - query += ` ORDER BY g.date DESC` + if (filters.won !== undefined) { + query += ` AND g.won = $${paramCount}` + params.push(filters.won) + paramCount++ + } - return dbManager.all(query, params) + if (filters.roundsMin !== undefined) { + query += ` AND g.rounds >= $${paramCount}` + params.push(filters.roundsMin) + paramCount++ + } + + if (filters.roundsMax !== undefined) { + query += ` AND g.rounds <= $${paramCount}` + params.push(filters.roundsMax) + paramCount++ + } + + if (filters.colors && filters.colors.length > 0) { + // Filter by commander color identity + const colorConditions = filters.colors.map(() => { + const condition = `cmdr.colors @> $${paramCount}::jsonb` + paramCount++ + return condition + }) + query += ` AND (${colorConditions.join(' OR ')})` + filters.colors.forEach(color => { + params.push(JSON.stringify([color])) + }) + paramCount -= filters.colors.length + paramCount += filters.colors.length + } + + query += ` ORDER BY g.date DESC` + + return dbManager.all(query, params) } /** diff --git a/backend/src/routes/games.js b/backend/src/routes/games.js index a467ba2..fd5fded 100644 --- a/backend/src/routes/games.js +++ b/backend/src/routes/games.js @@ -100,25 +100,79 @@ const updateGameSchema = z.object({ }) const gameQuerySchema = z.object({ - q: z - .string('Search query must be a string') - .min(1, 'Search query cannot be empty') - .max(50, 'Search query limited to 50 characters') - .optional(), - limit: z - .coerce - .number('Limit must be a number') - .int('Limit must be a whole number') - .min(1, 'Minimum 1 game per page') - .max(100, 'Maximum 100 games per page') - .default(50), - offset: z - .coerce - .number('Offset must be a number') - .int('Offset must be a whole number') - .min(0, 'Offset cannot be negative') - .default(0) -}) + q: z + .string('Search query must be a string') + .min(1, 'Search query cannot be empty') + .max(50, 'Search query limited to 50 characters') + .optional(), + limit: z + .coerce + .number('Limit must be a number') + .int('Limit must be a whole number') + .min(1, 'Minimum 1 game per page') + .max(100, 'Maximum 100 games per page') + .default(50), + offset: z + .coerce + .number('Offset must be a number') + .int('Offset must be a whole number') + .min(0, 'Offset cannot be negative') + .default(0), + // Date range filters + dateFrom: z + .string('Date from must be a string') + .refine((date) => !isNaN(Date.parse(date)), { + message: 'Invalid date format (use YYYY-MM-DD)' + }) + .optional(), + dateTo: z + .string('Date to must be a string') + .refine((date) => !isNaN(Date.parse(date)), { + message: 'Invalid date format (use YYYY-MM-DD)' + }) + .optional(), + // Player count filter + playerCount: z + .coerce + .number('Player count must be a number') + .int('Player count must be a whole number') + .min(2, 'Minimum 2 players') + .max(8, 'Maximum 8 players') + .optional(), + // Commander ID filter + commanderId: z + .coerce + .number('Commander ID must be a number') + .int('Commander ID must be a whole number') + .positive('Commander ID must be positive') + .optional(), + // Win/Loss filter + won: z + .enum(['true', 'false']) + .transform(val => val === 'true') + .optional(), + // Rounds range filters + roundsMin: z + .coerce + .number('Min rounds must be a number') + .int('Min rounds must be a whole number') + .min(1, 'Minimum 1 round') + .optional(), + roundsMax: z + .coerce + .number('Max rounds must be a number') + .int('Max rounds must be a whole number') + .max(50, 'Maximum 50 rounds') + .optional(), + // Color filters (comma-separated: W,U,B,R,G) + colors: z + .string('Colors must be a string') + .transform(val => val.split(',').filter(c => c.trim())) + .refine(colors => colors.every(c => /^[WUBRG]$/.test(c)), { + message: 'Invalid color format (use W,U,B,R,G)' + }) + .optional() + }) export default async function gameRoutes(fastify, options) { // Initialize repositories diff --git a/frontend/public/images/commanders.png b/frontend/public/images/commanders.png new file mode 100644 index 0000000..c86db91 Binary files /dev/null and b/frontend/public/images/commanders.png differ diff --git a/frontend/public/images/logs.png b/frontend/public/images/logs.png new file mode 100644 index 0000000..82f0907 Binary files /dev/null and b/frontend/public/images/logs.png differ diff --git a/frontend/public/images/preview.png b/frontend/public/images/preview.png deleted file mode 100644 index cc3afc9..0000000 Binary files a/frontend/public/images/preview.png and /dev/null differ diff --git a/frontend/public/images/round_timer.png b/frontend/public/images/round_timer.png new file mode 100644 index 0000000..30187f3 Binary files /dev/null and b/frontend/public/images/round_timer.png differ diff --git a/frontend/public/images/stats.png b/frontend/public/images/stats.png new file mode 100644 index 0000000..d658d4b Binary files /dev/null and b/frontend/public/images/stats.png differ diff --git a/frontend/public/index.html b/frontend/public/index.html index 0d40255..132e7e0 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -44,9 +44,11 @@ x-data="slideshow()" > -