opus-magnum results view
This commit is contained in:
parent
fa53d74295
commit
d2a9dbe4a4
@ -26,7 +26,9 @@ def results(request: HttpRequest) -> dict:
|
|||||||
ranking = {}
|
ranking = {}
|
||||||
|
|
||||||
for puzzle_id, responses in responses_by_puzzleid.items():
|
for puzzle_id, responses in responses_by_puzzleid.items():
|
||||||
ranking[puzzle_id] = sorted(responses, key=lambda x: x.rank_points)
|
ranking[puzzle_id] = sorted(
|
||||||
|
responses, key=lambda x: (x.rank_points is None, x.rank_points or 0)
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"users": CustomUser.objects.filter(pk__in=responses_by_userid.keys()),
|
"users": CustomUser.objects.filter(pk__in=responses_by_userid.keys()),
|
||||||
|
|||||||
@ -30,8 +30,13 @@ class PuzzleResponseRankingOut(ModelSchema):
|
|||||||
return obj.submission.user.id
|
return obj.submission.user.id
|
||||||
|
|
||||||
|
|
||||||
|
class UserDisplayOut(Schema):
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
|
||||||
|
|
||||||
class RankingSchema(Schema):
|
class RankingSchema(Schema):
|
||||||
users: list[UserInfoOut]
|
users: list[UserDisplayOut]
|
||||||
puzzles: list[SteamCollectionItemOut]
|
puzzles: list[SteamCollectionItemOut]
|
||||||
responses_by_userid: dict[int, list[PuzzleResponseRankingOut]]
|
responses_by_userid: dict[int, list[PuzzleResponseRankingOut]]
|
||||||
ranking_by_puzzle: dict[int, list[PuzzleResponseRankingOut]]
|
ranking_by_puzzle: dict[int, list[PuzzleResponseRankingOut]]
|
||||||
|
|||||||
@ -1,11 +1,279 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Puzzle {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PuzzleResponse {
|
||||||
|
id: number;
|
||||||
|
puzzle_id: number;
|
||||||
|
submission_id: string;
|
||||||
|
cost?: number;
|
||||||
|
cycles?: number;
|
||||||
|
area?: number;
|
||||||
|
rank_points?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResultsData {
|
||||||
|
users: User[];
|
||||||
|
puzzles: Puzzle[];
|
||||||
|
responses_by_userid: Record<number, PuzzleResponse[]>;
|
||||||
|
ranking_by_puzzle: Record<number, PuzzleResponse[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoading = ref(true);
|
||||||
|
const resultsData = ref<ResultsData | null>(null);
|
||||||
|
const selectedTab = ref<"overall" | "byPuzzle">("overall");
|
||||||
|
const expandedPuzzleId = ref<number | null>(null);
|
||||||
|
|
||||||
|
const fetchResults = async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/results/results");
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch results");
|
||||||
|
|
||||||
|
resultsData.value = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching results:", error);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOverallRanking = () => {
|
||||||
|
if (!resultsData.value) return [];
|
||||||
|
|
||||||
|
const userScores = resultsData.value.users.map((user) => {
|
||||||
|
const responses = resultsData.value!.responses_by_userid[user.id] || [];
|
||||||
|
const totalPoints = responses.reduce((sum, r) => sum + (r.rank_points || 0), 0);
|
||||||
|
const count = responses.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: user.username,
|
||||||
|
totalPoints,
|
||||||
|
puzzlesSolved: count,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return userScores.sort((a, b) => b.totalPoints - a.totalPoints);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPuzzleRanking = (puzzleId: number) => {
|
||||||
|
if (!resultsData.value) return [];
|
||||||
|
|
||||||
|
const ranking = resultsData.value.ranking_by_puzzle[puzzleId] || [];
|
||||||
|
return ranking.map((response) => {
|
||||||
|
console.log(response)
|
||||||
|
const user = resultsData.value!.users.find((u) => u.id === response.user_id);
|
||||||
|
return {
|
||||||
|
username: user?.username || "Unknown",
|
||||||
|
cost: response.final_cost,
|
||||||
|
cycles: response.final_cycles,
|
||||||
|
area: response.final_area,
|
||||||
|
points: response.points,
|
||||||
|
rank_points: response.rank_points || 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePuzzleExpanded = (puzzleId: number) => {
|
||||||
|
expandedPuzzleId.value = expandedPuzzleId.value === puzzleId ? null : puzzleId;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchResults();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="card bg-base-100 shadow-lg">
|
<div class="card bg-base-100 shadow-lg">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-2xl">General Results</h2>
|
<h2 class="card-title text-2xl mb-6">
|
||||||
<div class="flex flex-wrap gap-4 mt-4">TODO :)</div>
|
<i class="mdi mdi-trophy text-yellow-500 mr-2"></i>
|
||||||
|
General Results
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div v-if="isLoading" class="flex justify-center py-8">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!resultsData" class="text-center py-8">
|
||||||
|
<p class="text-base-content/70">No results available yet</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-6">
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="tabs tabs-boxed">
|
||||||
|
<button @click="selectedTab = 'overall'" :class="[
|
||||||
|
'tab',
|
||||||
|
selectedTab === 'overall' ? 'tab-active' : '',
|
||||||
|
]">
|
||||||
|
<i class="mdi mdi-chart-line mr-2"></i>
|
||||||
|
Overall Ranking
|
||||||
|
</button>
|
||||||
|
<button @click="selectedTab = 'byPuzzle'" :class="[
|
||||||
|
'tab',
|
||||||
|
selectedTab === 'byPuzzle' ? 'tab-active' : '',
|
||||||
|
]">
|
||||||
|
<i class="mdi mdi-puzzle mr-2"></i>
|
||||||
|
By Puzzle
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Overall Ranking -->
|
||||||
|
<div v-show="selectedTab === 'overall'" class="space-y-4">
|
||||||
|
<div v-if="getOverallRanking().length === 0" class="text-center py-8">
|
||||||
|
<p class="text-base-content/70">No submissions yet</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="overflow-x-auto">
|
||||||
|
<table class="table table-zebra w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Rank</th>
|
||||||
|
<th>Player</th>
|
||||||
|
<th class="text-right">Puzzles Solved</th>
|
||||||
|
<th class="text-right">Total Points</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(user, index) in getOverallRanking()" :key="user.username">
|
||||||
|
<td class="font-bold">
|
||||||
|
<span v-if="index === 0" class="badge badge-warning badge-lg">
|
||||||
|
🏆 #1
|
||||||
|
</span>
|
||||||
|
<span v-else-if="index === 1" class="badge badge-lg">
|
||||||
|
🥈 #2
|
||||||
|
</span>
|
||||||
|
<span v-else-if="index === 2" class="badge badge-lg">
|
||||||
|
🥉 #3
|
||||||
|
</span>
|
||||||
|
<span v-else>#{{ index + 1 }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="font-medium">{{ user.username }}</td>
|
||||||
|
<td class="text-right">{{ user.puzzlesSolved }}</td>
|
||||||
|
<td class="text-right font-bold">{{ user.totalPoints }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- By Puzzle Ranking -->
|
||||||
|
<div v-show="selectedTab === 'byPuzzle'" class="space-y-6">
|
||||||
|
<div v-for="puzzle in resultsData.puzzles" :key="puzzle.id" class="card bg-base-100 border border-base-300">
|
||||||
|
<button @click="togglePuzzleExpanded(puzzle.id)"
|
||||||
|
class="btn btn-ghost btn-lg w-full justify-start text-lg font-bold hover:bg-primary/20 rounded-b-none">
|
||||||
|
<i :class="['mdi mr-2', expandedPuzzleId === puzzle.id ? 'mdi-chevron-down' : 'mdi-chevron-right']"></i>
|
||||||
|
{{ puzzle.title }}
|
||||||
|
<span class="ml-auto badge badge-sm">
|
||||||
|
{{ getPuzzleRanking(puzzle.id).length }} submissions
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Expanded Details -->
|
||||||
|
<div v-if="expandedPuzzleId === puzzle.id" class="card-body">
|
||||||
|
<div v-if="getPuzzleRanking(puzzle.id).length === 0" class="text-center py-8">
|
||||||
|
<p class="text-base-content/70 text-lg">No submissions yet</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-6">
|
||||||
|
<!-- Top 3 Podium -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div v-for="(response, index) in getPuzzleRanking(puzzle.id).slice(0, 3)" :key="index"
|
||||||
|
class="card bg-base-200">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="text-xs text-base-content/70 font-bold">
|
||||||
|
{{ index === 0 ? '🏆 1st Place' : index === 1 ? '🥈 2nd Place' : '🥉 3rd Place' }}
|
||||||
|
</div>
|
||||||
|
<h4 class="font-bold text-lg">{{ response.username }}</h4>
|
||||||
|
<div class="divider my-2"></div>
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Cost</span>
|
||||||
|
<span class="badge badge-sm">{{ response.cost || 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Cycles</span>
|
||||||
|
<span class="badge badge-sm">{{ response.cycles || 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Area</span>
|
||||||
|
<span class="badge badge-sm">{{ response.area || 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between pt-2 border-t">
|
||||||
|
<span>Total (with coef.)</span>
|
||||||
|
<span class="badge badge-sm">{{ response.points || 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between pt-2 border-t">
|
||||||
|
<span class="font-bold">Points</span>
|
||||||
|
<span class="badge badge-primary">{{ response.rank_points }} pts</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Full Ranking Table -->
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table table-zebra w-full table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-12">Rank</th>
|
||||||
|
<th>Player</th>
|
||||||
|
<th class="text-center">Cost</th>
|
||||||
|
<th class="text-center">Cycles</th>
|
||||||
|
<th class="text-center">Area</th>
|
||||||
|
<th class="text-center">Total (with coef.)</th>
|
||||||
|
<th class="text-right">Points</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(response, index) in getPuzzleRanking(puzzle.id)" :key="index"
|
||||||
|
:class="{ 'bg-primary/10': index < 3 }">
|
||||||
|
<td class="font-bold">
|
||||||
|
<span v-if="index === 0" class="badge badge-warning">🏆</span>
|
||||||
|
<span v-else-if="index === 1" class="badge">🥈</span>
|
||||||
|
<span v-else-if="index === 2" class="badge">🥉</span>
|
||||||
|
<span v-else>#{{ index + 1 }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="font-medium">{{ response.username }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span v-if="response.cost" class="badge badge-sm">{{ response.cost }}</span>
|
||||||
|
<span v-else class="text-base-content/40">—</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span v-if="response.cycles" class="badge badge-sm">{{ response.cycles }}</span>
|
||||||
|
<span v-else class="text-base-content/40">—</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span v-if="response.area" class="badge badge-sm">{{ response.area }}</span>
|
||||||
|
<span v-else class="text-base-content/40">—</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span v-if="response.points" class="badge badge-sm">{{ response.points }}</span>
|
||||||
|
<span v-else class="text-base-content/40">—</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-right font-bold text-primary text-lg">{{ response.rank_points }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user