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:
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
frontend/public/images/commanders.png
Normal file
BIN
frontend/public/images/commanders.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 269 KiB |
BIN
frontend/public/images/logs.png
Normal file
BIN
frontend/public/images/logs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 458 KiB |
BIN
frontend/public/images/round_timer.png
Normal file
BIN
frontend/public/images/round_timer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
BIN
frontend/public/images/stats.png
Normal file
BIN
frontend/public/images/stats.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 398 KiB |
@@ -44,9 +44,11 @@
|
||||
x-data="slideshow()"
|
||||
>
|
||||
<!-- Slides Container with Arrow Navigation -->
|
||||
<div class="relative bg-gray-100 pt-12 pb-12 overflow-hidden">
|
||||
<div class="relative bg-gray-100 pt-9 pb-9 overflow-hidden">
|
||||
<!-- Image Container -->
|
||||
<div class="relative aspect-video flex items-center justify-center">
|
||||
<div
|
||||
class="relative aspect-video flex items-center justify-center"
|
||||
>
|
||||
<template x-for="(slide, index) in slides" :key="index">
|
||||
<img
|
||||
:src="slide.src"
|
||||
@@ -71,22 +73,42 @@
|
||||
<!-- Left Arrow Button -->
|
||||
<button
|
||||
@click="previousSlide()"
|
||||
class="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-600 hover:text-gray-800 hover:bg-gray-200 rounded-full p-2 transition-colors"
|
||||
class="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-600 hover:text-gray-800 hover:bg-gray-200 rounded-full p-2 transition-colors"
|
||||
title="Previous slide"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Right Arrow Button -->
|
||||
<button
|
||||
@click="nextSlide()"
|
||||
class="absolute right-4 top-1/2 transform -translate-y-1/2 text-gray-600 hover:text-gray-800 hover:bg-gray-200 rounded-full p-2 transition-colors"
|
||||
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-600 hover:text-gray-800 hover:bg-gray-200 rounded-full p-2 transition-colors"
|
||||
title="Next slide"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.0.4
|
||||
2.0.5
|
||||
|
||||
Reference in New Issue
Block a user