feat: Implement Load More pagination for matches page

- Add pagination state management (matches, hasMore, nextPageTime)
- Create loadMore() function to fetch and append next page of results
- Replace placeholder "pagination coming soon" with functional Load More button
- Add loading spinner during pagination requests
- Show total matches count and "all loaded" message when complete
- Use $effect to reset pagination state when filters change

Completes Phase 5.2 pagination requirement from TODO.md.
Users can now browse through large match lists efficiently.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-04 23:17:33 +01:00
parent ea61061530
commit 43c50084c6

View File

@@ -1,12 +1,14 @@
<script lang="ts">
import { Search, Filter, Calendar } from 'lucide-svelte';
import { Search, Filter, Calendar, Loader2 } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { api } from '$lib/api';
import Button from '$lib/components/ui/Button.svelte';
import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte';
import MatchCard from '$lib/components/match/MatchCard.svelte';
import type { PageData } from './$types';
import type { MatchListItem } from '$lib/types';
let { data }: { data: PageData } = $props();
@@ -18,6 +20,19 @@
let searchQuery = $state(currentSearch);
let showFilters = $state(false);
// Pagination state
let matches = $state<MatchListItem[]>(data.matches);
let hasMore = $state(data.hasMore);
let nextPageTime = $state(data.nextPageTime);
let isLoadingMore = $state(false);
// Reset pagination when data changes (new filters applied)
$effect(() => {
matches = data.matches;
hasMore = data.hasMore;
nextPageTime = data.nextPageTime;
});
const handleSearch = () => {
const params = new URLSearchParams();
if (searchQuery) params.set('search', searchQuery);
@@ -27,7 +42,39 @@
goto(`/matches?${params.toString()}`);
};
const commonMaps = ['de_dust2', 'de_mirage', 'de_inferno', 'de_nuke', 'de_overpass', 'de_ancient', 'de_anubis'];
const loadMore = async () => {
if (!hasMore || isLoadingMore || !nextPageTime) return;
isLoadingMore = true;
try {
const matchesData = await api.matches.getMatches({
limit: 50,
map: data.filters.map,
player_id: data.filters.playerId,
before_time: nextPageTime
});
// Append new matches to existing list
matches = [...matches, ...matchesData.matches];
hasMore = matchesData.has_more;
nextPageTime = matchesData.next_page_time;
} catch (error) {
console.error('Failed to load more matches:', error);
// Show error toast or message here
} finally {
isLoadingMore = false;
}
};
const commonMaps = [
'de_dust2',
'de_mirage',
'de_inferno',
'de_nuke',
'de_overpass',
'de_ancient',
'de_anubis'
];
</script>
<svelte:head>
@@ -42,7 +89,13 @@
<!-- Search & Filters -->
<Card padding="lg" class="mb-8">
<form onsubmit={(e) => { e.preventDefault(); handleSearch(); }} class="flex flex-col gap-4">
<form
onsubmit={(e) => {
e.preventDefault();
handleSearch();
}}
class="flex flex-col gap-4"
>
<div class="flex flex-col gap-4 md:flex-row">
<div class="flex-1">
<div class="relative">
@@ -73,7 +126,7 @@
{#each commonMaps as mapName}
<a
href={`/matches?map=${mapName}`}
class="badge badge-lg badge-outline hover:badge-primary"
class="badge badge-outline badge-lg hover:badge-primary"
class:badge-primary={currentMap === mapName}
>
{mapName}
@@ -103,16 +156,31 @@
</Card>
<!-- Matches Grid -->
{#if data.matches.length > 0}
{#if matches.length > 0}
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{#each data.matches as match}
{#each matches as match}
<MatchCard {match} />
{/each}
</div>
{#if data.hasMore}
<!-- Load More Button -->
{#if hasMore}
<div class="mt-8 text-center">
<Badge variant="info">More matches available - pagination coming soon</Badge>
<Button variant="primary" size="lg" onclick={loadMore} disabled={isLoadingMore}>
{#if isLoadingMore}
<Loader2 class="mr-2 h-5 w-5 animate-spin" />
Loading...
{:else}
Load More Matches
{/if}
</Button>
<p class="mt-2 text-sm text-base-content/60">
Showing {matches.length} matches
</p>
</div>
{:else if matches.length > 0}
<div class="mt-8 text-center">
<Badge variant="default">All matches loaded ({matches.length} total)</Badge>
</div>
{/if}
{:else}
@@ -120,9 +188,7 @@
<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">
Try adjusting your filters or search query.
</p>
<p class="text-base-content/60">Try adjusting your filters or search query.</p>
{#if currentMap || currentPlayerId || currentSearch}
<div class="mt-4">
<Button variant="primary" href="/matches">View All Matches</Button>