opus-submitter/polylan_submitter/src/components/Results.vue

390 lines
14 KiB
Vue

<script setup lang="ts">
import { ref, onMounted } from "vue";
import RankBadge from "./RankBadge.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 PuzzleRanking {
id: number;
puzzle_id: number;
user_id: number;
final_cost?: number;
final_cycles?: number;
final_area?: number;
points?: number;
rank_points?: number;
}
interface ResultsData {
users: User[];
puzzles: Puzzle[];
responses_by_userid: Record<number, PuzzleResponse[]>;
ranking_by_puzzle: Record<number, PuzzleRanking[]>;
}
const isLoading = ref(true);
const resultsData = ref<ResultsData | null>(null);
const selectedTab = ref<"overall" | "byPuzzle">("overall");
const expandedPuzzleId = ref<number | null>(null);
const userInfo = ref({
username: "Player",
rank: null as number | null,
totalPoints: 0,
puzzlesSolved: 0,
isStaff: false,
});
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;
};
const clearCache = async () => {
try {
const response = await fetch("/api/cache/clear", {
method: "POST",
});
if (response.ok) {
alert("Cache cleared successfully!");
await fetchResults();
} else {
const error = await response.json();
alert(`Error clearing cache: ${error.detail || "Unknown error"}`);
}
} catch (error) {
console.error("Error clearing cache:", error);
alert("Error clearing cache. Please try again.");
}
};
const loadUserData = async () => {
try {
const response = await fetch("/api/user");
if (response.ok) {
const user = await response.json();
if (user.is_authenticated) {
userInfo.value.username = user.username;
userInfo.value.isStaff = user.is_staff || false;
await fetchResults();
// Calculate user's rank and stats
const ranking = getOverallRanking();
const userRankIndex = ranking.findIndex((u) => u.username === user.username);
if (userRankIndex !== -1) {
userInfo.value.rank = userRankIndex + 1;
userInfo.value.totalPoints = ranking[userRankIndex].totalPoints;
userInfo.value.puzzlesSolved = ranking[userRankIndex].puzzlesSolved;
}
}
}
} catch (error) {
console.error("Error loading user data:", error);
await fetchResults();
}
};
onMounted(() => {
loadUserData();
});
</script>
<template>
<div class="mb-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Left Column: Your Ranking -->
<div class="lg:col-span-1">
<div class="card bg-base-100 shadow-lg sticky top-8">
<div class="bg-gradient-to-br from-blue-600 to-blue-400 p-6 text-white rounded-t-2xl">
<i class="mdi mdi-trophy text-4xl"></i>
<h3 class="text-2xl font-bold mt-2">Your Ranking</h3>
</div>
<div class="card-body">
<div class="text-center mb-6">
<p class="text-sm text-base-content/70">Player</p>
<p class="text-3xl font-bold">{{ userInfo.username }}</p>
</div>
<div class="divider"></div>
<div v-if="isLoading" class="flex justify-center py-8">
<span class="loading loading-spinner loading-lg"></span>
</div>
<div v-else class="space-y-4">
<div class="text-center">
<p class="text-sm text-base-content/70 mb-1">Current Rank</p>
<p v-if="userInfo.rank !== null" class="text-4xl font-bold text-primary">
#{{ userInfo.rank }}
</p>
<p v-else class="text-2xl text-base-content/50">No rank yet</p>
</div>
<div class="text-center">
<p class="text-sm text-base-content/70 mb-1">Total Points</p>
<p class="text-2xl font-bold">{{ userInfo.totalPoints.toLocaleString() }}</p>
</div>
<div class="text-center">
<p class="text-sm text-base-content/70 mb-1">Puzzles Solved</p>
<p class="text-2xl font-bold">{{ userInfo.puzzlesSolved }}</p>
</div>
<button v-if="userInfo.isStaff" @click="clearCache" class="btn btn-error btn-sm w-full mt-6">
<i class="mdi mdi-cache-clear mr-1"></i>
Clear Cache
</button>
</div>
</div>
</div>
</div>
<!-- Right Column: Results -->
<div class="lg:col-span-2">
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-2xl mb-6">
<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">
<RankBadge :rank="index + 1" />
</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>
</template>