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:
@@ -1,12 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Search, Filter, Calendar } from 'lucide-svelte';
|
import { Search, Filter, Calendar, Loader2 } from 'lucide-svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import { api } from '$lib/api';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
import Card from '$lib/components/ui/Card.svelte';
|
import Card from '$lib/components/ui/Card.svelte';
|
||||||
import Badge from '$lib/components/ui/Badge.svelte';
|
import Badge from '$lib/components/ui/Badge.svelte';
|
||||||
import MatchCard from '$lib/components/match/MatchCard.svelte';
|
import MatchCard from '$lib/components/match/MatchCard.svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import type { MatchListItem } from '$lib/types';
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
@@ -18,6 +20,19 @@
|
|||||||
let searchQuery = $state(currentSearch);
|
let searchQuery = $state(currentSearch);
|
||||||
let showFilters = $state(false);
|
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 handleSearch = () => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (searchQuery) params.set('search', searchQuery);
|
if (searchQuery) params.set('search', searchQuery);
|
||||||
@@ -27,7 +42,39 @@
|
|||||||
goto(`/matches?${params.toString()}`);
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -42,7 +89,13 @@
|
|||||||
|
|
||||||
<!-- Search & Filters -->
|
<!-- Search & Filters -->
|
||||||
<Card padding="lg" class="mb-8">
|
<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 flex-col gap-4 md:flex-row">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@@ -73,7 +126,7 @@
|
|||||||
{#each commonMaps as mapName}
|
{#each commonMaps as mapName}
|
||||||
<a
|
<a
|
||||||
href={`/matches?map=${mapName}`}
|
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}
|
class:badge-primary={currentMap === mapName}
|
||||||
>
|
>
|
||||||
{mapName}
|
{mapName}
|
||||||
@@ -103,16 +156,31 @@
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Matches Grid -->
|
<!-- Matches Grid -->
|
||||||
{#if data.matches.length > 0}
|
{#if matches.length > 0}
|
||||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{#each data.matches as match}
|
{#each matches as match}
|
||||||
<MatchCard {match} />
|
<MatchCard {match} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if data.hasMore}
|
<!-- Load More Button -->
|
||||||
|
{#if hasMore}
|
||||||
<div class="mt-8 text-center">
|
<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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
@@ -120,9 +188,7 @@
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<Calendar class="mx-auto mb-4 h-16 w-16 text-base-content/40" />
|
<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>
|
<h2 class="mb-2 text-xl font-semibold text-base-content">No Matches Found</h2>
|
||||||
<p class="text-base-content/60">
|
<p class="text-base-content/60">Try adjusting your filters or search query.</p>
|
||||||
Try adjusting your filters or search query.
|
|
||||||
</p>
|
|
||||||
{#if currentMap || currentPlayerId || currentSearch}
|
{#if currentMap || currentPlayerId || currentSearch}
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<Button variant="primary" href="/matches">View All Matches</Button>
|
<Button variant="primary" href="/matches">View All Matches</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user