tournament outputs

This commit is contained in:
2026-05-22 06:18:11 +02:00
parent bf21a5eae6
commit 92dddca964
21 changed files with 925 additions and 69 deletions
@@ -0,0 +1,184 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { apiService } from "@/services/apiService";
import type { TournamentPuzzleResults } from "@/types";
const isLoading = ref(true);
const resultsData = ref<TournamentPuzzleResults | null>(null);
const error = ref<string>("");
// Modal state
const showImageModal = ref(false);
const selectedImageUrl = ref<string>("");
const selectedImageName = ref<string>("");
const fetchResults = async () => {
isLoading.value = true;
error.value = "";
try {
const response = await apiService.getPuzzleResults(5);
if (response.data) {
resultsData.value = response.data;
} else if (response.error) {
error.value = response.error;
console.error("Error fetching results:", response.error);
}
} catch (err) {
error.value = err instanceof Error ? err.message : "Failed to fetch results";
console.error("Error fetching results:", err);
} finally {
isLoading.value = false;
}
};
const formatNumber = (num: number | undefined) => {
return num !== undefined ? num.toLocaleString() : "—";
};
const openImageModal = (fileUrl: string, fileName: string) => {
selectedImageUrl.value = fileUrl;
selectedImageName.value = fileName;
showImageModal.value = true;
};
const closeImageModal = () => {
showImageModal.value = false;
selectedImageUrl.value = "";
selectedImageName.value = "";
};
const getRankBadge = (rank: number) => {
const badges = ["🥇", "🥈", "🥉"];
return badges[rank - 1] || `#${rank}`;
};
onMounted(() => {
fetchResults();
});
</script>
<template>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-2xl flex items-center gap-2">
<i class="mdi mdi-table text-blue-500 text-3xl"></i>
Results by Puzzle
</h2>
<div v-if="isLoading" class="flex justify-center py-12">
<span class="loading loading-spinner loading-lg"></span>
</div>
<div v-else-if="error" class="alert alert-error">
<i class="mdi mdi-alert-circle text-xl"></i>
<div>{{ error }}</div>
</div>
<div v-else-if="!resultsData || resultsData.results.length === 0" class="text-center py-8">
<p class="text-base-content/70">No results available yet.</p>
</div>
<div v-else class="space-y-8">
<div v-for="puzzle in resultsData.results" :key="puzzle.puzzle_id" class="border-b pb-8 last:border-b-0">
<!-- Puzzle Header with Coefficients -->
<div class="mb-4">
<div class="bg-base-200 p-3 rounded-lg mb-4" v-if="puzzle.points_factor">
<p class="text-xs text-base-content/70 font-semibold mb-2">Points Coefficients</p>
<div class="grid grid-cols-3 gap-2">
<div class="text-center">
<span class="font-bold text-primary"><small>x</small>{{ puzzle.points_factor.cost }}</span>
<p class="text-xs text-base-content/70">Cost</p>
</div>
<div class="text-center">
<span class="font-bold text-primary"><small>x</small>{{ puzzle.points_factor.cycles }}</span>
<p class="text-xs text-base-content/70">Cycles</p>
</div>
<div class="text-center">
<span class="font-bold text-primary"><small>x</small>{{ puzzle.points_factor.area }}</span>
<p class="text-xs text-base-content/70">Area</p>
</div>
</div>
</div>
</div>
<!-- Results Table -->
<div v-if="puzzle.submissions.length > 0" class="overflow-x-auto">
<table class="table table-sm table-zebra">
<thead>
<tr>
<th class="w-12 text-center">Pos</th>
<th>User</th>
<th class="text-right">Cost</th>
<th class="text-right">Cycles</th>
<th class="text-right">Area</th>
<th class="text-right font-bold">Total Pts</th>
<th class="text-right font-bold">Total Coef</th>
<th class="text-center">GIF</th>
</tr>
</thead>
<tbody>
<tr v-for="submission in puzzle.submissions" :key="`${puzzle.puzzle_id}-${submission.user_id}`">
<td class="text-center text-lg font-bold">
{{ getRankBadge(submission.rank) }}
</td>
<td class="font-semibold">{{ submission.username }}</td>
<td class="text-right">{{ formatNumber(submission.final_cost) }}</td>
<td class="text-right">{{ formatNumber(submission.final_cycles) }}</td>
<td class="text-right">{{ formatNumber(submission.final_area) }}</td>
<td class="text-right font-bold" :class="{
'text-yellow-600': submission.rank === 1,
'text-gray-600': submission.rank === 2,
'text-orange-600': submission.rank === 3,
}">
{{ formatNumber(submission.rank_points) }}
</td>
<td class="text-right font-bold text-primary">
{{ formatNumber(submission.total_coef) }}
</td>
<td class="text-center">
<button v-if="submission.files.length > 0"
@click="openImageModal(submission.files[0].file_url, submission.files[0].original_filename)"
class="btn btn-xs btn-primary gap-1">
<i class="mdi mdi-image"></i>
View
</button>
<span v-else class="text-base-content/50"></span>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="p-4 bg-base-200 rounded-lg text-center text-base-content/70">
No submissions yet
</div>
</div>
</div>
</div>
</div>
<!-- Image Modal -->
<div v-if="showImageModal" class="modal modal-open">
<div class="modal-box max-w-7xl w-full">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-lg">{{ selectedImageName }}</h3>
<button @click="closeImageModal" class="btn btn-sm btn-circle btn-ghost">
<i class="mdi mdi-close"></i>
</button>
</div>
<div class="flex justify-center bg-base-200 rounded-lg p-4">
<img :src="selectedImageUrl" :alt="selectedImageName" class="object-contain" />
</div>
<div class="modal-action mt-4">
<a :href="selectedImageUrl" target="_blank" class="btn btn-primary btn-sm">
<i class="mdi mdi-download"></i>
Download
</a>
<button @click="closeImageModal" class="btn btn-sm">Close</button>
</div>
</div>
<div class="modal-backdrop" @click="closeImageModal"></div>
</div>
</template>
@@ -0,0 +1,171 @@
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { apiService } from "@/services/apiService";
import type { TournamentSubmissions, WinnerResponse, PuzzleSubmissions } from "@/types";
const isLoading = ref(true);
const submissionsData = ref<TournamentSubmissions | null>(null);
const error = ref<string>("");
// Modal state
const showImageModal = ref(false);
const selectedImageUrl = ref<string>("");
const selectedImageName = ref<string>("");
const fetchSubmissions = async () => {
isLoading.value = true;
error.value = "";
try {
const response = await apiService.getTopSubmissions(5);
if (response.data) {
submissionsData.value = response.data;
} else if (response.error) {
error.value = response.error;
console.error("Error fetching submissions:", response.error);
}
} catch (err) {
error.value = err instanceof Error ? err.message : "Failed to fetch submissions";
console.error("Error fetching submissions:", err);
} finally {
isLoading.value = false;
}
};
const formatNumber = (num: number | undefined) => {
return num !== undefined ? num.toLocaleString() : "—";
};
// Flatten all submissions into a single table
const flattenedRows = computed(() => {
if (!submissionsData.value) return [];
const rows: Array<{
puzzleName: string;
username: string;
cost: number | undefined;
cycles: number | undefined;
area: number | undefined;
total: number | undefined;
totalCoef: number | undefined;
files: Array<{ url: string; name: string }>;
}> = [];
submissionsData.value.submissions.forEach((puzzle: PuzzleSubmissions) => {
puzzle.submissions.forEach((submission: WinnerResponse) => {
rows.push({
puzzleName: puzzle.puzzle_title,
username: submission.username,
cost: submission.final_cost,
cycles: submission.final_cycles,
area: submission.final_area,
total: submission.rank_points,
totalCoef: submission.total_coef,
files: submission.files.map(f => ({ url: f.file_url, name: f.original_filename })),
});
});
});
return rows;
});
const openImageModal = (fileUrl: string, fileName: string) => {
selectedImageUrl.value = fileUrl;
selectedImageName.value = fileName;
showImageModal.value = true;
};
const closeImageModal = () => {
showImageModal.value = false;
selectedImageUrl.value = "";
selectedImageName.value = "";
};
onMounted(() => {
fetchSubmissions();
});
</script>
<template>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-2xl flex items-center gap-2">
<i class="mdi mdi-trophy text-yellow-500 text-3xl"></i>
Top Submissions
</h2>
<div v-if="isLoading" class="flex justify-center py-12">
<span class="loading loading-spinner loading-lg"></span>
</div>
<div v-else-if="error" class="alert alert-error">
<i class="mdi mdi-alert-circle text-xl"></i>
<div>{{ error }}</div>
</div>
<div v-else-if="flattenedRows.length === 0" class="text-center py-8">
<p class="text-base-content/70">No results available yet.</p>
</div>
<div v-else class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Puzzle</th>
<th>User</th>
<th class="text-right">Cost</th>
<th class="text-right">Cycles</th>
<th class="text-right">Area</th>
<th class="text-right">Total Pts</th>
<th class="text-right">Total Coef</th>
<th class="text-center">GIF</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in flattenedRows" :key="index">
<td class="font-semibold">{{ row.puzzleName }}</td>
<td>{{ row.username }}</td>
<td class="text-right">{{ formatNumber(row.cost) }}</td>
<td class="text-right">{{ formatNumber(row.cycles) }}</td>
<td class="text-right">{{ formatNumber(row.area) }}</td>
<td class="text-right font-bold">{{ formatNumber(row.total) }}</td>
<td class="text-right font-bold text-primary">{{ formatNumber(row.totalCoef) }}</td>
<td class="text-center">
<button v-if="row.files.length > 0" @click="openImageModal(row.files[0].url, row.files[0].name)"
class="btn btn-xs btn-primary gap-1">
<i class="mdi mdi-image"></i>
View
</button>
<span v-else class="text-base-content/50"></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Image Modal -->
<div v-if="showImageModal" class="modal modal-open">
<div class="modal-box max-w-7xl w-full">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-lg">{{ selectedImageName }}</h3>
<button @click="closeImageModal" class="btn btn-sm btn-circle btn-ghost">
<i class="mdi mdi-close"></i>
</button>
</div>
<div class="flex justify-center bg-base-200 rounded-lg p-4">
<img :src="selectedImageUrl" :alt="selectedImageName" class="object-contain" />
</div>
<div class="modal-action mt-4">
<a :href="selectedImageUrl" target="_blank" class="btn btn-primary btn-sm">
<i class="mdi mdi-download"></i>
Download
</a>
<button @click="closeImageModal" class="btn btn-sm">Close</button>
</div>
</div>
<div class="modal-backdrop" @click="closeImageModal"></div>
</div>
</template>