Expand game query filters and update UI assets

- Backend: extend filters with dateFrom/dateTo, won, roundsMin/Max and
  colors; implement color identity using jsonb and order by date with
  limit/offset.
- Frontend: adjust slideshow spacing in index.html and include new
  images; bump version to 2.0.5.
This commit is contained in:
2026-01-18 14:07:44 +01:00
parent cc4ecd0ce1
commit 6d2345bee7
4 changed files with 37 additions and 151 deletions

View File

@@ -97,43 +97,16 @@ 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++
}
if (filters.roundsMin !== undefined) {
query += ` AND g.rounds >= $${paramCount}`
params.push(filters.roundsMin)
paramCount++
}
query += ` ORDER BY g.date DESC LIMIT $${paramCount} OFFSET $${paramCount + 1}`
params.push(limit, offset)
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)
return dbManager.all(query, params)
}
/**
@@ -188,48 +161,15 @@ 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++
}
if (filters.won !== undefined) {
query += ` AND g.won = $${paramCount}`
params.push(filters.won)
paramCount++
}
query += ` ORDER BY g.date DESC`
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)
return dbManager.all(query, params)
}
/**

View File

@@ -100,79 +100,25 @@ 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),
// 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()
})
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)
})
export default async function gameRoutes(fastify, options) {
// Initialize repositories

View File

@@ -37,8 +37,8 @@
</div>
</div>
<!-- Preview Slideshow Section -->
<div class="mt-12 max-w-6xl mx-auto">
<!-- Preview Slideshow Section -->
<div class="mt-12 max-w-5xl mx-auto">
<div
class="bg-white rounded-lg shadow-lg overflow-hidden"
x-data="slideshow()"

View File

@@ -1 +1 @@
2.0.5
2.0.6