diff --git a/src/lib/components/dashboard/contests/PastContestCard.svelte b/src/lib/components/dashboard/contests/PastContestCard.svelte index 41deac2..e55631c 100644 --- a/src/lib/components/dashboard/contests/PastContestCard.svelte +++ b/src/lib/components/dashboard/contests/PastContestCard.svelte @@ -7,8 +7,10 @@ import TrendingUp from '@lucide/svelte/icons/trending-up'; import Eye from '@lucide/svelte/icons/eye'; import * as m from '$lib/paraglide/messages'; + import { AppRoutes } from '$lib/routes'; interface PastContestCardProps { + contestId: number; name: string; score: number; maxScore: number; @@ -17,8 +19,15 @@ participants: number; } - let { name, score, maxScore, completionPercentage, date, participants }: PastContestCardProps = - $props(); + let { + contestId, + name, + score, + maxScore, + completionPercentage, + date, + participants + }: PastContestCardProps = $props(); {m.past_contest_view_details()} diff --git a/src/routes/dashboard/user/contests/+page.svelte b/src/routes/dashboard/user/contests/+page.svelte index 2b5f862..5d1c21d 100644 --- a/src/routes/dashboard/user/contests/+page.svelte +++ b/src/routes/dashboard/user/contests/+page.svelte @@ -120,6 +120,7 @@
{#each contestsQuery.current.past as contest (contest.id)} import { page } from '$app/state'; - import { userStore } from '$lib/stores/user-store.svelte'; import { createParameterizedQuery } from '$lib/utils/query.svelte'; import { getContestInstance } from '$lib/services'; import { LoadingSpinner, ErrorCard, EmptyState } from '$lib/components/common'; import * as Table from '$lib/components/ui/table'; import * as Card from '$lib/components/ui/card'; import Trophy from '@lucide/svelte/icons/trophy'; - import Medal from '@lucide/svelte/icons/medal'; import Target from '@lucide/svelte/icons/target'; - import Users from '@lucide/svelte/icons/users'; import CheckCircle from '@lucide/svelte/icons/check-circle'; import { format } from 'date-fns'; import * as m from '$lib/paraglide/messages'; @@ -17,68 +14,36 @@ const contestService = getContestInstance(); const contestId = $derived(Number(page.params.contestId)); - const currentUserId = $derived(userStore.getUserUnsafe().id); const resultsQuery = createParameterizedQuery( () => contestId, async (id) => { if (!contestService) throw new Error('Service unavailable'); + if (!Number.isFinite(id)) throw new Error('Invalid contest ID'); const result = await contestService.getContestResults(id); if (!result.success) throw new Error(result.error || 'Failed to fetch results'); return result.data!; } ); - // Sort leaderboard by total score (sum of best scores across all tasks) - let sortedLeaderboard = $derived.by(() => { - if (!resultsQuery.current?.leaderboard) return []; + const taskResults = $derived(resultsQuery.current?.taskResults ?? []); - return [...resultsQuery.current.leaderboard] - .map((userStats) => { - const totalScore = userStats.taskBreakdown.reduce( - (sum: number, task: TaskResult) => sum + task.bestScore, - 0 - ); - return { ...userStats, totalScore }; - }) - .sort((a, b) => b.totalScore - a.totalScore); - }); - - // Find current user's position - let currentUserPosition = $derived.by(() => { - const position = sortedLeaderboard.findIndex((user) => user.user.id === currentUserId); - return position >= 0 ? position + 1 : null; - }); + const totalScore = $derived( + taskResults.reduce((sum: number, task: TaskResult) => sum + task.bestScore, 0) + ); - function getRankBadgeClass(rank: number): string { - if (rank === 1) return 'bg-primary/10 text-primary border-primary/20'; - if (rank === 2) return 'bg-secondary/10 text-secondary border-secondary/20'; - if (rank === 3) return 'bg-accent/10 text-accent-foreground border-accent/20'; - return 'bg-muted text-muted-foreground border-border'; - } + const tasksCompleted = $derived( + taskResults.filter((task: TaskResult) => task.bestScore === 100).length + ); - function getRankIcon(rank: number) { - // Returns Medal component for top 3 ranks, null for rank 4+ - if (rank <= 3) return Medal; - return null; - } + const totalSubmissions = $derived( + taskResults.reduce((sum: number, task: TaskResult) => sum + task.submissionCount, 0) + ); function formatContestDate(dateString: string): string { const date = new Date(dateString); return format(date, 'MMM dd, yyyy'); } - - // Pagination state - let currentPage = $state(1); - let pageSize = $state(10); - - let paginatedLeaderboard = $derived.by(() => { - const start = (currentPage - 1) * pageSize; - const end = start + pageSize; - return sortedLeaderboard.slice(start, end); - }); - - let totalPages = $derived(Math.ceil(sortedLeaderboard.length / pageSize));
@@ -104,344 +69,102 @@ /> {:else if resultsQuery.loading} - {:else if resultsQuery.current} + {:else if taskResults.length === 0} + + {:else} - {#if resultsQuery.current.myResults} -
- - - - - {m.contest_results_my_total_score()} - - - -

- {resultsQuery.current.myResults.taskResults - .reduce((sum: number, task: TaskResult) => sum + task.bestScore, 0) - .toFixed(1)} -

-
-
- - - - - - {m.contest_results_tasks_completed()} - - - -

- {resultsQuery.current.myResults.taskResults.filter( - (t: TaskResult) => t.bestScore === 100 - ).length} - / {resultsQuery.current.myResults.taskResults.length} -

-
-
- - - - - - {m.contest_results_total_submissions()} - - - -

- {resultsQuery.current.myResults.taskResults.reduce( - (sum: number, task: TaskResult) => sum + task.submissionCount, - 0 - )} -

-
-
- - {#if currentUserPosition} - - - - - {m.contest_results_my_rank()} - - - -

- #{currentUserPosition} -

-
-
- {/if} -
- - +
- - {m.contest_results_my_task_results()} + + + + {m.contest_results_my_total_score()} + -
- - - - {m.contest_results_task()} - - - {m.contest_results_score_submissions()} - - - - {#each resultsQuery.current.myResults.taskResults as taskResult (taskResult.task.id)} - - {taskResult.task.title} - - - - - {taskResult.bestScore.toFixed(1)}% - - / {taskResult.submissionCount} - - - {/each} - - -
+

{totalScore.toFixed(1)}

- {/if} - - {#if sortedLeaderboard.length >= 3} - - - - - {m.contest_results_top_champions()} + + + + + {m.contest_results_tasks_completed()} -
- - {#if sortedLeaderboard[1]} -
-
- -
-
-

- {sortedLeaderboard[1].user.name} - {sortedLeaderboard[1].user.surname} -

-

@{sortedLeaderboard[1].user.username}

-

- {sortedLeaderboard[1].totalScore.toFixed(1)} -

-

- {m.contest_results_solved_partial({ - solved: sortedLeaderboard[1].tasksSolved, - partial: sortedLeaderboard[1].tasksPartiallySolved - })} -

-
-
- {/if} - - - {#if sortedLeaderboard[0]} -
-
- -
-
-

- {sortedLeaderboard[0].user.name} - {sortedLeaderboard[0].user.surname} -

-

@{sortedLeaderboard[0].user.username}

-

- {sortedLeaderboard[0].totalScore.toFixed(1)} -

-

- {m.contest_results_solved_partial({ - solved: sortedLeaderboard[0].tasksSolved, - partial: sortedLeaderboard[0].tasksPartiallySolved - })} -

-
-
- {/if} - - - {#if sortedLeaderboard[2]} -
-
- -
-
-

- {sortedLeaderboard[2].user.name} - {sortedLeaderboard[2].user.surname} -

-

@{sortedLeaderboard[2].user.username}

-

- {sortedLeaderboard[2].totalScore.toFixed(1)} -

-

- {m.contest_results_solved_partial({ - solved: sortedLeaderboard[2].tasksSolved, - partial: sortedLeaderboard[2].tasksPartiallySolved - })} -

-
-
- {/if} -
+

+ {tasksCompleted} / {taskResults.length} +

- {/if} - - {#if sortedLeaderboard.length > 0} - -
- - - {m.contest_results_leaderboard()} - -

- {m.contest_results_participants({ count: sortedLeaderboard.length })} -

-
+ + + + {m.contest_results_total_submissions()} + -
- - - - {m.contest_results_rank()} - {m.contest_results_name()} - - {m.contest_results_total_score()} - - - - - - {#each paginatedLeaderboard as userStats, index (userStats.user.id)} - {@const rank = (currentPage - 1) * pageSize + index + 1} - {@const RankIcon = getRankIcon(rank)} - {@const isCurrentUser = userStats.user.id === currentUserId} - - -
- {#if RankIcon} -
- -
- {:else} - {rank} - {/if} -
-
- - {userStats.user.name} - {userStats.user.surname} - {#if isCurrentUser} - {m.contest_results_you()} - {/if} - - - - {userStats.totalScore.toFixed(1)} - - - -
- {/each} -
-
-
- - - {#if totalPages > 1} -
-

- {m.contest_results_showing({ - start: (currentPage - 1) * pageSize + 1, - end: Math.min(currentPage * pageSize, sortedLeaderboard.length), - total: sortedLeaderboard.length - })} -

-
- - - {m.contest_results_page_info({ current: currentPage, total: totalPages })} - - -
-
- {/if} +

{totalSubmissions}

- {:else} - - {/if} +
+ + + + + {m.contest_results_my_task_results()} + + +
+ + + + {m.contest_results_task()} + + + {m.contest_results_score_submissions()} + + + + {#each taskResults as taskResult (taskResult.task.id)} + + {taskResult.task.title} + + + + + {taskResult.bestScore.toFixed(1)}% + + + / {taskResult.submissionCount} + + + + {/each} + + +
+
+
{/if}