Files
polylan-submitter/polylan_submitter/src/OpusMagnum.vue
T
2026-09-17 00:56:17 +02:00

253 lines
8.5 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { useI18n } from "vue-i18n";
import { useTitle } from "@vueuse/core";
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 TopUsersLeaderboard from "@/components/TopUsersLeaderboard.vue";
import { polylanSubmitterApiGetUserInfo, opusMagnumApiGetCollection } from "@/api";
import { apiService, errorHelpers } from "@/services/apiService";
import { usePuzzlesStore } from "@/stores/puzzles";
import { useSubmissionsStore } from "@/stores/submissions";
import { provideEventPath } from "@/composables/useEventPath";
import type { PuzzleResponse, UserInfo, SteamCollection } from "@/types";
import { useCountdown } from "@vueuse/core";
import { storeToRefs } from "pinia";
const props = defineProps<{
eventPath: string;
}>();
provideEventPath(props.eventPath);
apiService.setEventPath(props.eventPath);
const { t } = useI18n();
useTitle(computed(() => t("opusMagnum.pageTitle")));
const puzzlesStore = usePuzzlesStore();
const submissionsStore = useSubmissionsStore();
const { submissions, isSubmissionModalOpen } = storeToRefs(submissionsStore);
const { openSubmissionModal, loadSubmissions, closeSubmissionModal } =
submissionsStore;
// Local state
const userInfo = ref<UserInfo | null>(null);
const collection = ref<SteamCollection | null>(null);
const isLoading = ref(true);
const error = ref<string>("");
// Computed properties
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[]> = {};
submissions.value.forEach((submission) => {
submission.responses.forEach((response) => {
// Handle both number and object types for puzzle field
if (!grouped[response.puzzle_id]) {
grouped[response.puzzle_id] = [];
}
grouped[response.puzzle_id].push(response);
});
});
return grouped;
});
async function initialize() {
try {
isLoading.value = true;
error.value = "";
console.log("Starting data load...");
// Load user info
console.log("Loading user info...");
const userResponse = await polylanSubmitterApiGetUserInfo();
if (userResponse.data) {
userInfo.value = userResponse.data as UserInfo;
console.log("User info loaded:", userResponse.data);
} else if (userResponse.error) {
console.warn("User info error:", userResponse.error);
}
// Load collection data
console.log("Loading collection...");
const collectionResponse = await opusMagnumApiGetCollection({
path: { event_path: props.eventPath },
});
if (collectionResponse.data) {
collection.value = collectionResponse.data as SteamCollection;
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(props.eventPath);
console.log("Puzzles loaded:", puzzlesStore.puzzles.length);
// Load existing submissions using store
console.log("Loading submissions...");
await loadSubmissions();
console.log("Submissions loaded:", submissions.value.length);
console.log("Data load complete!");
} catch (err) {
error.value = errorHelpers.getErrorMessage(err);
console.error("Failed to load data:", err);
} finally {
isLoading.value = false;
console.log("Loading state set to false");
}
if (userInfo.value?.is_superuser) {
start();
}
}
const { remaining, start } = useCountdown(60, {
onComplete() {
initialize();
},
});
onMounted(async () => {
await initialize();
});
// Function to match puzzle name from OCR to actual puzzle
const findPuzzleByName = (ocrPuzzleName: string) => {
return puzzlesStore.findPuzzleByName(ocrPuzzleName);
};
const reloadPage = () => {
window.location.reload();
};
</script>
<template>
<div class="flex-1 bg-base-200">
<!-- Main Content -->
<div class="container min-w-3/4 mx-auto px-4 py-8">
<!-- Loading State -->
<div v-if="userInfo?.is_superuser" class="flex justify-center">
<div class="text-center">
<p class="mb-6 text-base-content/70">
<span class="loading loading-spinner loading-lg"></span>
{{ t("opusMagnum.autoReload", { seconds: remaining }) }}
</p>
</div>
</div>
<div v-if="isLoading" class="flex justify-center items-center min-h-[400px]">
<div class="text-center">
<span class="loading loading-spinner loading-lg"></span>
<p class="mt-4 text-base-content/70">{{ t("opusMagnum.loadingPuzzles") }}</p>
</div>
</div>
<!-- Error State -->
<div v-else-if="error" class="alert alert-error max-w-2xl mx-auto">
<i class="mdi mdi-alert-circle text-xl"></i>
<div>
<h3 class="font-bold">{{ t("opusMagnum.errorLoadingData") }}</h3>
<div class="text-sm">{{ error }}</div>
</div>
<button @click="reloadPage" class="btn btn-sm btn-outline">
<i class="mdi mdi-refresh mr-1"></i>
{{ t("common.retry") }}
</button>
</div>
<!-- Main Content -->
<div v-else class="space-y-8">
<!-- Results Section (only when tournament is closed) -->
<div v-if="isTournamentClosed" class="space-y-8">
<TopUsersLeaderboard />
<PuzzleResults />
<Winners />
</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">{{ collection?.title }}</h2>
<p class="text-base-content/70">
{{ collection?.description }}
</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>
{{ t("common.submitSolution") }}
</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">{{ t("opusMagnum.tournamentClosed") }}</h3>
<div class="text-sm">{{ t("opusMagnum.tournamentClosedMessage") }}</div>
</div>
</div>
</div>
</div>
</div>
<Results />
<!-- 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>
<!-- 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">{{ t("opusMagnum.noPuzzlesTitle") }}</h3>
<p class="text-base-content/70">
{{ t("opusMagnum.noPuzzlesMessage") }}
</p>
</div>
</template>
</div>
</div>
<!-- Submission Modal -->
<div v-if="isSubmissionModalOpen" class="modal modal-open">
<div class="modal-box max-w-6xl">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-lg">{{ t("common.submitSolution") }}</h3>
<button @click="closeSubmissionModal" class="btn btn-sm btn-circle btn-ghost">
<i class="mdi mdi-close"></i>
</button>
</div>
<SubmissionForm :puzzles="puzzlesStore.puzzles" :find-puzzle-by-name="findPuzzleByName" />
</div>
<div class="modal-backdrop" @click="closeSubmissionModal"></div>
</div>
</div>
</template>