tournament outputs
This commit is contained in:
@@ -4,10 +4,12 @@ import PuzzleCard from "@/components/PuzzleCard.vue";
|
||||
import SubmissionForm from "@/components/SubmissionForm.vue";
|
||||
import AdminPanel from "@/components/AdminPanel.vue";
|
||||
import Results from "@/components/Results.vue";
|
||||
import Winners from "@/components/Winners.vue";
|
||||
import PuzzleResults from "@/components/PuzzleResults.vue";
|
||||
import { apiService, errorHelpers } from "@/services/apiService";
|
||||
import { usePuzzlesStore } from "@/stores/puzzles";
|
||||
import { useSubmissionsStore } from "@/stores/submissions";
|
||||
import type { PuzzleResponse, UserInfo } from "@/types";
|
||||
import type { PuzzleResponse, UserInfo, SteamCollection } from "@/types";
|
||||
import { useCountdown } from "@vueuse/core";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
@@ -26,6 +28,7 @@ const { openSubmissionModal, loadSubmissions, closeSubmissionModal } =
|
||||
|
||||
// Local state
|
||||
const userInfo = ref<UserInfo | null>(null);
|
||||
const collection = ref<SteamCollection | null>(null);
|
||||
const isLoading = ref(true);
|
||||
const error = ref<string>("");
|
||||
|
||||
@@ -34,6 +37,10 @@ const isSuperuser = computed(() => {
|
||||
return userInfo.value?.is_superuser || false;
|
||||
});
|
||||
|
||||
const isTournamentClosed = computed(() => {
|
||||
return !!(collection.value && !collection.value.accepting_submissions);
|
||||
});
|
||||
|
||||
// Computed property to get responses grouped by puzzle
|
||||
const responsesByPuzzle = computed(() => {
|
||||
const grouped: Record<number, PuzzleResponse[]> = {};
|
||||
@@ -66,6 +73,16 @@ async function initialize() {
|
||||
console.warn("User info error:", userResponse.error);
|
||||
}
|
||||
|
||||
// Load collection data
|
||||
console.log("Loading collection...");
|
||||
const collectionResponse = await apiService.getCollection();
|
||||
if (collectionResponse.data) {
|
||||
collection.value = collectionResponse.data;
|
||||
console.log("Collection loaded:", collectionResponse.data);
|
||||
} else if (collectionResponse.error) {
|
||||
console.warn("Collection error:", collectionResponse.error);
|
||||
}
|
||||
|
||||
// Load puzzles from API using store
|
||||
console.log("Loading puzzles...");
|
||||
await puzzlesStore.loadPuzzles();
|
||||
@@ -173,45 +190,62 @@ const goHome = () => {
|
||||
|
||||
<!-- Main Content -->
|
||||
<div v-else class="space-y-8">
|
||||
<!-- Collection Info -->
|
||||
<div class="mb-8">
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl">{{ props.collectionTitle }}</h2>
|
||||
<p class="text-base-content/70">
|
||||
{{ props.collectionDescription }}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-4 mt-4">
|
||||
<button @click="openSubmissionModal" class="btn btn-primary">
|
||||
<i class="mdi mdi-plus mr-2"></i>
|
||||
Submit Solution
|
||||
</button>
|
||||
|
||||
<!-- Winners Section (only when tournament is closed) -->
|
||||
<div v-if="isTournamentClosed" class="space-y-8">
|
||||
<Winners />
|
||||
<PuzzleResults />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
|
||||
<!-- Collection Info -->
|
||||
<div class="mb-8">
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl">{{ props.collectionTitle }}</h2>
|
||||
<p class="text-base-content/70">
|
||||
{{ props.collectionDescription }}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-4 mt-4">
|
||||
<button @click="openSubmissionModal" class="btn btn-primary" :disabled="isTournamentClosed">
|
||||
<i class="mdi mdi-plus mr-2"></i>
|
||||
Submit Solution
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="isTournamentClosed" class="alert alert-warning mt-4">
|
||||
<i class="mdi mdi-alert-circle text-xl"></i>
|
||||
<div>
|
||||
<h3 class="font-bold">Tournament Closed</h3>
|
||||
<div class="text-sm">This tournament is no longer accepting new submissions.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Results />
|
||||
<Results />
|
||||
|
||||
<!-- Admin Panel (only for superusers) -->
|
||||
<div v-if="isSuperuser">
|
||||
<AdminPanel />
|
||||
</div>
|
||||
<!-- Admin Panel (only for superusers) -->
|
||||
<div v-if="isSuperuser">
|
||||
<AdminPanel />
|
||||
</div>
|
||||
|
||||
<!-- Puzzles Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<PuzzleCard v-for="puzzle in puzzlesStore.puzzles" :key="puzzle.id" :puzzle="puzzle"
|
||||
:responses="responsesByPuzzle[puzzle.id] || []" />
|
||||
</div>
|
||||
<!-- Puzzles Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<PuzzleCard v-for="puzzle in puzzlesStore.puzzles" :key="puzzle.id" :puzzle="puzzle"
|
||||
:responses="responsesByPuzzle[puzzle.id] || []" />
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="puzzlesStore.puzzles.length === 0" class="text-center py-12">
|
||||
<div class="text-6xl mb-4">🧩</div>
|
||||
<h3 class="text-xl font-bold mb-2">No Puzzles Available</h3>
|
||||
<p class="text-base-content/70">
|
||||
Check back later for new puzzle collections!
|
||||
</p>
|
||||
</div>
|
||||
<!-- Empty State -->
|
||||
<div v-if="puzzlesStore.puzzles.length === 0" class="text-center py-12">
|
||||
<div class="text-6xl mb-4">🧩</div>
|
||||
<h3 class="text-xl font-bold mb-2">No Puzzles Available</h3>
|
||||
<p class="text-base-content/70">
|
||||
Check back later for new puzzle collections!
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -1,9 +1,13 @@
|
||||
import type {
|
||||
SteamCollection,
|
||||
SteamCollectionItem,
|
||||
Submission,
|
||||
PuzzleResponse,
|
||||
SubmissionFile,
|
||||
UserInfo
|
||||
UserInfo,
|
||||
TournamentWinners,
|
||||
TournamentSubmissions,
|
||||
TournamentPuzzleResults
|
||||
} from '../types'
|
||||
|
||||
// API Configuration
|
||||
@@ -101,6 +105,22 @@ export class ApiService {
|
||||
return this.request<SteamCollectionItem[]>('/submissions/puzzles')
|
||||
}
|
||||
|
||||
async getCollection(): Promise<ApiResponse<SteamCollection>> {
|
||||
return this.request<SteamCollection>('/submissions/collection')
|
||||
}
|
||||
|
||||
async getWinners(): Promise<ApiResponse<TournamentWinners>> {
|
||||
return this.request<TournamentWinners>('/results/winners')
|
||||
}
|
||||
|
||||
async getTopSubmissions(limit = 5): Promise<ApiResponse<TournamentSubmissions>> {
|
||||
return this.request<TournamentSubmissions>(`/results/top-submissions?limit=${limit}`)
|
||||
}
|
||||
|
||||
async getPuzzleResults(limit = 5): Promise<ApiResponse<TournamentPuzzleResults>> {
|
||||
return this.request<TournamentPuzzleResults>(`/results/puzzle-results?limit=${limit}`)
|
||||
}
|
||||
|
||||
// Submission endpoints
|
||||
async getSubmissions(limit = 20, offset = 0): Promise<ApiResponse<PaginatedResponse<Submission>>> {
|
||||
return this.request<PaginatedResponse<Submission>>(
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface SteamCollection {
|
||||
total_items: number
|
||||
unique_visitors: number
|
||||
current_favorites: number
|
||||
accepting_submissions: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
@@ -107,3 +108,68 @@ export interface UserInfo {
|
||||
is_superuser: boolean
|
||||
cas_groups?: string[]
|
||||
}
|
||||
|
||||
export interface WinnerFile {
|
||||
file_url: string
|
||||
original_filename: string
|
||||
}
|
||||
|
||||
export interface WinnerResponse {
|
||||
user_id: number
|
||||
username: string
|
||||
final_cost?: number
|
||||
final_cycles?: number
|
||||
final_area?: number
|
||||
rank_points?: number
|
||||
total_coef?: number
|
||||
files: WinnerFile[]
|
||||
}
|
||||
|
||||
export interface PuzzleWinner {
|
||||
puzzle_id: number
|
||||
puzzle_title: string
|
||||
winner?: WinnerResponse
|
||||
}
|
||||
|
||||
export interface TournamentWinners {
|
||||
winners: PuzzleWinner[]
|
||||
}
|
||||
|
||||
export interface PuzzleSubmissions {
|
||||
puzzle_id: number
|
||||
puzzle_title: string
|
||||
submissions: WinnerResponse[]
|
||||
}
|
||||
|
||||
export interface TournamentSubmissions {
|
||||
submissions: PuzzleSubmissions[]
|
||||
}
|
||||
|
||||
export interface PuzzlePointsFactor {
|
||||
cost: number
|
||||
cycles: number
|
||||
area: number
|
||||
}
|
||||
|
||||
export interface PuzzleSubmissionWithRank {
|
||||
rank: number
|
||||
user_id: number
|
||||
username: string
|
||||
final_cost?: number
|
||||
final_cycles?: number
|
||||
final_area?: number
|
||||
rank_points?: number
|
||||
total_coef?: number
|
||||
files: WinnerFile[]
|
||||
}
|
||||
|
||||
export interface PuzzleResults {
|
||||
puzzle_id: number
|
||||
puzzle_title: string
|
||||
points_factor?: PuzzlePointsFactor
|
||||
submissions: PuzzleSubmissionWithRank[]
|
||||
}
|
||||
|
||||
export interface TournamentPuzzleResults {
|
||||
results: PuzzleResults[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user