forked from CSGOWTF/csgowtf
fix: Fix Svelte 5 reactivity issues in matches page and update API handling
- Fix toast notification imports: change from showToast to toast.success/error - Remove hover preloading from app.html and Tabs component - Fix match rounds API handling with safe parsing for incomplete data - Fix pagination timestamp calculation (API returns Unix timestamp, not ISO string) - Refactor matches page state management to fix reactivity issues: - Replace separate state variables with single matchesState object - Completely replace state object on updates to trigger reactivity - Fix infinite loop in intersection observer effect - Add keyed each blocks for proper list rendering - Remove client-side filtering (temporarily) to isolate reactivity issues - Add error state handling with nextPageTime in matches loader 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { apiClient } from './client';
|
||||
import {
|
||||
parseMatchRounds,
|
||||
parseMatchRoundsSafe,
|
||||
parseMatchWeapons,
|
||||
parseMatchChat,
|
||||
parseMatchParseResponse
|
||||
@@ -69,13 +69,22 @@ export const matchesAPI = {
|
||||
* Get match round-by-round statistics
|
||||
* @param matchId - Match ID
|
||||
* @returns Round statistics and economy data
|
||||
* @throws Error if data is invalid or demo not parsed yet
|
||||
*/
|
||||
async getMatchRounds(matchId: string | number): Promise<MatchRoundsResponse> {
|
||||
const url = `/match/${matchId}/rounds`;
|
||||
const data = await apiClient.get<MatchRoundsResponse>(url);
|
||||
const data = await apiClient.get<unknown>(url);
|
||||
|
||||
// Validate with Zod schema
|
||||
return parseMatchRounds(data);
|
||||
// Validate with Zod schema using safe parse
|
||||
// This handles cases where the demo hasn't been parsed yet
|
||||
const result = parseMatchRoundsSafe(data);
|
||||
|
||||
if (!result.success) {
|
||||
// If validation fails, it's likely the demo hasn't been parsed yet
|
||||
throw new Error('Demo not parsed yet or invalid response format');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -117,30 +126,33 @@ export const matchesAPI = {
|
||||
const limit = params?.limit || 50;
|
||||
|
||||
// CRITICAL: API returns a plain array, not a wrapped object
|
||||
// We request limit + 1 to detect if there are more pages
|
||||
// NOTE: Backend has a hard limit of 20 matches per request
|
||||
// We assume hasMore = true if we get exactly the limit we requested
|
||||
const data = await apiClient.get<LegacyMatchListItem[]>(url, {
|
||||
params: {
|
||||
limit: limit + 1, // Request one extra to check if there are more
|
||||
limit: limit,
|
||||
map: params?.map,
|
||||
player_id: params?.player_id
|
||||
}
|
||||
});
|
||||
|
||||
// Check if there are more matches (if we got the extra one)
|
||||
const hasMore = data.length > limit;
|
||||
// Handle null or empty response
|
||||
if (!data || !Array.isArray(data)) {
|
||||
console.warn('[API] getMatches received null or invalid data');
|
||||
return transformMatchesListResponse([], false, undefined);
|
||||
}
|
||||
|
||||
// Remove the extra match if we have more
|
||||
const matchesToReturn = hasMore ? data.slice(0, limit) : data;
|
||||
// If we got exactly the limit, assume there might be more
|
||||
// If we got less, we've reached the end
|
||||
const hasMore = data.length === limit;
|
||||
|
||||
// If there are more matches, use the timestamp of the last match for pagination
|
||||
// This timestamp is used in the next request: /matches/next/{timestamp}
|
||||
const lastMatch =
|
||||
matchesToReturn.length > 0 ? matchesToReturn[matchesToReturn.length - 1] : undefined;
|
||||
const nextPageTime =
|
||||
hasMore && lastMatch ? Math.floor(new Date(lastMatch.date).getTime() / 1000) : undefined;
|
||||
// Get the timestamp from the LAST match BEFORE transformation
|
||||
// The legacy API format has `date` as a Unix timestamp (number)
|
||||
const lastLegacyMatch = data.length > 0 ? data[data.length - 1] : undefined;
|
||||
const nextPageTime = hasMore && lastLegacyMatch ? lastLegacyMatch.date : undefined;
|
||||
|
||||
// Transform legacy API response to new format
|
||||
return transformMatchesListResponse(matchesToReturn, hasMore, nextPageTime);
|
||||
return transformMatchesListResponse(data, hasMore, nextPageTime);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -153,28 +165,25 @@ export const matchesAPI = {
|
||||
const limit = params?.limit || 20;
|
||||
|
||||
// API returns a plain array, not a wrapped object
|
||||
// Backend has a hard limit of 20 matches per request
|
||||
const data = await apiClient.getCancelable<LegacyMatchListItem[]>(url, 'match-search', {
|
||||
params: {
|
||||
limit: limit + 1, // Request one extra to check if there are more
|
||||
limit: limit,
|
||||
map: params?.map,
|
||||
player_id: params?.player_id
|
||||
}
|
||||
});
|
||||
|
||||
// Check if there are more matches (if we got the extra one)
|
||||
const hasMore = data.length > limit;
|
||||
// If we got exactly the limit, assume there might be more
|
||||
const hasMore = data.length === limit;
|
||||
|
||||
// Remove the extra match if we have more
|
||||
const matchesToReturn = hasMore ? data.slice(0, limit) : data;
|
||||
|
||||
// If there are more matches, use the timestamp of the last match for pagination
|
||||
const lastMatch =
|
||||
matchesToReturn.length > 0 ? matchesToReturn[matchesToReturn.length - 1] : undefined;
|
||||
const nextPageTime =
|
||||
hasMore && lastMatch ? Math.floor(new Date(lastMatch.date).getTime() / 1000) : undefined;
|
||||
// Get the timestamp from the LAST match BEFORE transformation
|
||||
// The legacy API format has `date` as a Unix timestamp (number)
|
||||
const lastLegacyMatch = data.length > 0 ? data[data.length - 1] : undefined;
|
||||
const nextPageTime = hasMore && lastLegacyMatch ? lastLegacyMatch.date : undefined;
|
||||
|
||||
// Transform legacy API response to new format
|
||||
return transformMatchesListResponse(matchesToReturn, hasMore, nextPageTime);
|
||||
return transformMatchesListResponse(data, hasMore, nextPageTime);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Upload, Check, AlertCircle, Loader2 } from 'lucide-svelte';
|
||||
import { matchesAPI } from '$lib/api/matches';
|
||||
import { showToast } from '$lib/stores/toast';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let shareCode = $state('');
|
||||
@@ -21,12 +21,12 @@
|
||||
const trimmedCode = shareCode.trim().toUpperCase();
|
||||
|
||||
if (!trimmedCode) {
|
||||
showToast('Please enter a share code', 'error');
|
||||
toast.error('Please enter a share code');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidShareCode(trimmedCode)) {
|
||||
showToast('Invalid share code format', 'error');
|
||||
toast.error('Invalid share code format');
|
||||
parseStatus = 'error';
|
||||
statusMessage = 'Share code must be in format: CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX';
|
||||
return;
|
||||
@@ -45,7 +45,7 @@
|
||||
statusMessage =
|
||||
response.message ||
|
||||
'Match submitted successfully! Parsing may take a few minutes. You can view the match once parsing is complete.';
|
||||
showToast('Match submitted for parsing!', 'success');
|
||||
toast.success('Match submitted for parsing!');
|
||||
|
||||
// Wait a moment then redirect to the match page
|
||||
setTimeout(() => {
|
||||
@@ -54,12 +54,12 @@
|
||||
} else {
|
||||
parseStatus = 'error';
|
||||
statusMessage = response.message || 'Failed to parse share code';
|
||||
showToast(statusMessage, 'error');
|
||||
toast.error(statusMessage);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
parseStatus = 'error';
|
||||
statusMessage = error instanceof Error ? error.message : 'Failed to parse share code';
|
||||
showToast(statusMessage, 'error');
|
||||
toast.error(statusMessage);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Modal from '$lib/components/ui/Modal.svelte';
|
||||
import { playersAPI } from '$lib/api/players';
|
||||
import { showToast } from '$lib/stores/toast';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
|
||||
interface Props {
|
||||
playerId: string;
|
||||
@@ -31,12 +31,12 @@
|
||||
|
||||
try {
|
||||
await playersAPI.trackPlayer(playerId, authCode, shareCode || undefined);
|
||||
showToast('Player tracking activated successfully!', 'success');
|
||||
toast.success('Player tracking activated successfully!');
|
||||
isOpen = false;
|
||||
dispatch('tracked');
|
||||
} catch (err: unknown) {
|
||||
error = err instanceof Error ? err.message : 'Failed to track player';
|
||||
showToast(error, 'error');
|
||||
toast.error(error);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
@@ -53,12 +53,12 @@
|
||||
|
||||
try {
|
||||
await playersAPI.untrackPlayer(playerId, authCode);
|
||||
showToast('Player tracking removed successfully', 'success');
|
||||
toast.success('Player tracking removed successfully');
|
||||
isOpen = false;
|
||||
dispatch('untracked');
|
||||
} catch (err: unknown) {
|
||||
error = err instanceof Error ? err.message : 'Failed to untrack player';
|
||||
showToast(error, 'error');
|
||||
toast.error(error);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@
|
||||
class:tab-active={isActive(tab)}
|
||||
class:tab-disabled={tab.disabled}
|
||||
aria-disabled={tab.disabled}
|
||||
data-sveltekit-preload-data="hover"
|
||||
>
|
||||
{tab.label}
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user