feat: Add sorting and result filtering to matches page
Adds client-side sorting and filtering capabilities: Sorting options: - Date (newest/oldest) - Duration (shortest/longest) - Score difference (close games/blowouts) - Toggle ascending/descending order Result filters: - All matches - Wins only (Team A won) - Losses only (Team B won) - Ties only Features: - Reactive $derived computed matches list - Shows filtered count vs total matches - Empty state when no matches match filters - Clear filter button when results are empty - Works seamlessly with pagination (Load More) Completes Phase 5.2 advanced features from TODO.md. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,11 @@
|
||||
let nextPageTime = $state(data.nextPageTime);
|
||||
let isLoadingMore = $state(false);
|
||||
|
||||
// Sorting and filtering state
|
||||
let sortBy = $state<'date' | 'duration' | 'score'>('date');
|
||||
let sortOrder = $state<'desc' | 'asc'>('desc');
|
||||
let resultFilter = $state<'all' | 'win' | 'loss' | 'tie'>('all');
|
||||
|
||||
// Reset pagination when data changes (new filters applied)
|
||||
$effect(() => {
|
||||
matches = data.matches;
|
||||
@@ -33,6 +38,44 @@
|
||||
nextPageTime = data.nextPageTime;
|
||||
});
|
||||
|
||||
// Computed filtered and sorted matches
|
||||
const displayMatches = $derived(() => {
|
||||
let filtered = [...matches];
|
||||
|
||||
// Apply result filter
|
||||
if (resultFilter !== 'all') {
|
||||
filtered = filtered.filter((match) => {
|
||||
const teamAWon = match.score_team_a > match.score_team_b;
|
||||
const teamBWon = match.score_team_b > match.score_team_a;
|
||||
const tie = match.score_team_a === match.score_team_b;
|
||||
|
||||
if (resultFilter === 'win') return teamAWon;
|
||||
if (resultFilter === 'loss') return teamBWon;
|
||||
if (resultFilter === 'tie') return tie;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
filtered.sort((a, b) => {
|
||||
let comparison = 0;
|
||||
|
||||
if (sortBy === 'date') {
|
||||
comparison = new Date(a.date).getTime() - new Date(b.date).getTime();
|
||||
} else if (sortBy === 'duration') {
|
||||
comparison = a.duration - b.duration;
|
||||
} else if (sortBy === 'score') {
|
||||
const aScoreDiff = Math.abs(a.score_team_a - a.score_team_b);
|
||||
const bScoreDiff = Math.abs(b.score_team_a - b.score_team_b);
|
||||
comparison = aScoreDiff - bScoreDiff;
|
||||
}
|
||||
|
||||
return sortOrder === 'desc' ? -comparison : comparison;
|
||||
});
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
const handleSearch = () => {
|
||||
const params = new URLSearchParams();
|
||||
if (searchQuery) params.set('search', searchQuery);
|
||||
@@ -120,7 +163,9 @@
|
||||
|
||||
<!-- Filter Panel (Collapsible) -->
|
||||
{#if showFilters}
|
||||
<div class="border-t border-base-300 pt-4">
|
||||
<div class="space-y-4 border-t border-base-300 pt-4">
|
||||
<!-- Map Filter -->
|
||||
<div>
|
||||
<h3 class="mb-3 font-semibold text-base-content">Filter by Map</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each commonMaps as mapName}
|
||||
@@ -134,6 +179,69 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result Filter -->
|
||||
<div>
|
||||
<h3 class="mb-3 font-semibold text-base-content">Filter by Result</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="badge badge-lg"
|
||||
class:badge-primary={resultFilter === 'all'}
|
||||
class:badge-outline={resultFilter !== 'all'}
|
||||
onclick={() => (resultFilter = 'all')}
|
||||
>
|
||||
All Matches
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="badge badge-lg"
|
||||
class:badge-success={resultFilter === 'win'}
|
||||
class:badge-outline={resultFilter !== 'win'}
|
||||
onclick={() => (resultFilter = 'win')}
|
||||
>
|
||||
Wins
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="badge badge-lg"
|
||||
class:badge-error={resultFilter === 'loss'}
|
||||
class:badge-outline={resultFilter !== 'loss'}
|
||||
onclick={() => (resultFilter = 'loss')}
|
||||
>
|
||||
Losses
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="badge badge-lg"
|
||||
class:badge-warning={resultFilter === 'tie'}
|
||||
class:badge-outline={resultFilter !== 'tie'}
|
||||
onclick={() => (resultFilter = 'tie')}
|
||||
>
|
||||
Ties
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sort Controls -->
|
||||
<div>
|
||||
<h3 class="mb-3 font-semibold text-base-content">Sort By</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<select bind:value={sortBy} class="select select-bordered select-sm">
|
||||
<option value="date">Date</option>
|
||||
<option value="duration">Duration</option>
|
||||
<option value="score">Score Difference</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm"
|
||||
onclick={() => (sortOrder = sortOrder === 'desc' ? 'asc' : 'desc')}
|
||||
>
|
||||
{sortOrder === 'desc' ? '↓ Descending' : '↑ Ascending'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
@@ -155,10 +263,19 @@
|
||||
{/if}
|
||||
</Card>
|
||||
|
||||
<!-- Results Summary -->
|
||||
{#if matches.length > 0 && resultFilter !== 'all'}
|
||||
<div class="mb-4">
|
||||
<Badge variant="info">
|
||||
Showing {displayMatches().length} of {matches.length} matches
|
||||
</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Matches Grid -->
|
||||
{#if matches.length > 0}
|
||||
{#if displayMatches().length > 0}
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each matches as match}
|
||||
{#each displayMatches() as match}
|
||||
<MatchCard {match} />
|
||||
{/each}
|
||||
</div>
|
||||
@@ -180,9 +297,28 @@
|
||||
</div>
|
||||
{:else if matches.length > 0}
|
||||
<div class="mt-8 text-center">
|
||||
<Badge variant="default">All matches loaded ({matches.length} total)</Badge>
|
||||
<Badge variant="default">
|
||||
All matches loaded ({matches.length} total{resultFilter !== 'all'
|
||||
? `, ${displayMatches().length} shown`
|
||||
: ''})
|
||||
</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if matches.length > 0 && displayMatches().length === 0}
|
||||
<Card padding="lg">
|
||||
<div class="text-center">
|
||||
<Calendar class="mx-auto mb-4 h-16 w-16 text-base-content/40" />
|
||||
<h2 class="mb-2 text-xl font-semibold text-base-content">No Matches Found</h2>
|
||||
<p class="text-base-content/60">
|
||||
No matches match your current filters. Try adjusting your filter settings.
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<Button variant="primary" onclick={() => (resultFilter = 'all')}>
|
||||
Clear Result Filter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{:else}
|
||||
<Card padding="lg">
|
||||
<div class="text-center">
|
||||
|
||||
Reference in New Issue
Block a user