tournament outputs
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user