feat: switch to spa

This commit is contained in:
2026-09-17 00:37:28 +02:00
parent 15590e5162
commit 99a868a6be
22 changed files with 317 additions and 170 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ dist-ssr
# Vite build output (generated locally via `pnpm build`, served by whitenoise) # Vite build output (generated locally via `pnpm build`, served by whitenoise)
static_source/vite/assets static_source/vite/assets
static_source/vite/manifest.json static_source/vite/manifest.json
static_source/vite/vite.svg static_source/vite/icon.png
tsconfig.tsbuildinfo tsconfig.tsbuildinfo
# vue-tsc emits a compiled copy of vite.config.ts here (tsconfig.node.json, # vue-tsc emits a compiled copy of vite.config.ts here (tsconfig.node.json,
+1 -1
View File
@@ -8,4 +8,4 @@ from events.models import Event
@login_required @login_required
def market_home(request: HttpRequest, event_path: str): def market_home(request: HttpRequest, event_path: str):
get_object_or_404(Event, path=event_path) get_object_or_404(Event, path=event_path)
return render(request, "app.html", {"bootstrap": {}}) return render(request, "app.html")
+12 -1
View File
@@ -1,4 +1,4 @@
from ninja import NinjaAPI from ninja import NinjaAPI, Schema
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.http import HttpRequest from django.http import HttpRequest
@@ -62,6 +62,17 @@ def health_check(request):
return {"status": "healthy", "service": "polylan-submitter-api"} return {"status": "healthy", "service": "polylan-submitter-api"}
class ConfigOut(Schema):
market_enabled: bool
# Site-wide config the SPA needs but that isn't tied to any one event
# (e.g. whether to render the market card at all on the home page).
@api.get("/config", response=ConfigOut)
def get_config(request):
return {"market_enabled": settings.MARKET_ENABLED}
# Cache management endpoint # Cache management endpoint
@api.post("/cache/clear") @api.post("/cache/clear")
def clear_cache(request: HttpRequest, event_path: str | None = None): def clear_cache(request: HttpRequest, event_path: str | None = None):
+4 -27
View File
@@ -44,44 +44,21 @@ def home(request: HttpRequest):
@login_required @login_required
def event_home(request: HttpRequest, event_path: str): def event_home(request: HttpRequest, event_path: str):
get_object_or_404(Event, path=event_path) get_object_or_404(Event, path=event_path)
return render(request, "app.html")
return render(
request,
"app.html",
{
"bootstrap": {
"marketEnabled": settings.MARKET_ENABLED,
},
},
)
@login_required @login_required
@require_game_enabled(OPUS_MAGNUM_APP_ID) @require_game_enabled(OPUS_MAGNUM_APP_ID)
def opus_magnum_home(request: HttpRequest, event_path: str): def opus_magnum_home(request: HttpRequest, event_path: str):
from opus_magnum.models import SteamCollection get_object_or_404(Event, path=event_path)
return render(request, "app.html")
event = get_object_or_404(Event, path=event_path)
collection = SteamCollection.objects.filter(is_active=True, event=event).last()
return render(
request,
"app.html",
{
"bootstrap": {
"collectionTitle": collection.title if collection else "",
"collectionUrl": collection.url if collection else "",
"collectionDescription": collection.description if collection else "",
},
},
)
@login_required @login_required
@require_game_enabled(NOITA_APP_ID) @require_game_enabled(NOITA_APP_ID)
def noita_home(request: HttpRequest, event_path: str): def noita_home(request: HttpRequest, event_path: str):
get_object_or_404(Event, path=event_path) get_object_or_404(Event, path=event_path)
return render(request, "app.html", {"bootstrap": {}}) return render(request, "app.html")
urlpatterns = [ urlpatterns = [
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+22 -21
View File
@@ -1,25 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from "vue"; import { ref, onMounted, computed } from "vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useTitle } from "@vueuse/core"; import { useTitle } from "@vueuse/core";
import { gamesApiListEventGames } from "./api"; import { gamesApiListEventGames, polylanSubmitterApiGetConfig } from "./api";
import type { GamesApiListEventGamesResponse } from "./api/types.gen"; import type { GamesApiListEventGamesResponse } from "./api/types.gen";
interface Props { interface Props {
eventPath: string; eventPath: string;
marketEnabled?: string | boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = defineProps<Props>();
marketEnabled: true,
});
const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
useTitle(computed(() => t("home.title"))); useTitle(computed(() => t("home.title")));
const games = ref<GamesApiListEventGamesResponse | undefined>(); const games = ref<GamesApiListEventGamesResponse | undefined>();
const loading = ref(true); const loading = ref(true);
const imageErrors = ref<Set<number>>(new Set()); const imageErrors = ref<Set<number>>(new Set());
const marketEnabled = ref(false);
const getHeaderImage = (game: GamesApiListEventGamesResponse[number]) => { const getHeaderImage = (game: GamesApiListEventGamesResponse[number]) => {
if (game.image) { if (game.image) {
@@ -35,24 +35,25 @@ const onImageError = (gameId: number) => {
imageErrors.value.add(gameId); imageErrors.value.add(gameId);
}; };
const navigate = (path: string) => { const goToMarket = () => {
window.location.href = `/${props.eventPath}${path}`; router.push(`/${props.eventPath}/market`);
}; };
const isMarketEnabled = () => { const goToGame = (game: GamesApiListEventGamesResponse[number]) => {
if (typeof props.marketEnabled === 'string') { const path = game.custom_path ? `/${game.path}` : `/submissions/${game.path}`;
return props.marketEnabled === 'true'; router.push(`/${props.eventPath}${path}`);
}
return Boolean(props.marketEnabled);
}; };
onMounted(async () => { onMounted(async () => {
// Fetch games enabled for this event const [gamesResponse, configResponse] = await Promise.all([
const response = await gamesApiListEventGames({ gamesApiListEventGames({ path: { event_path: props.eventPath } }),
path: { event_path: props.eventPath }, polylanSubmitterApiGetConfig(),
}); ]);
if (response.data) { if (gamesResponse.data) {
games.value = response.data; games.value = gamesResponse.data;
}
if (configResponse.data) {
marketEnabled.value = configResponse.data.market_enabled;
} }
loading.value = false; loading.value = false;
}); });
@@ -77,7 +78,7 @@ onMounted(async () => {
<!-- Cards Grid --> <!-- Cards Grid -->
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-8"> <div v-else class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Market Card --> <!-- Market Card -->
<div v-if="isMarketEnabled()" @click="navigate('/market')" <div v-if="marketEnabled" @click="goToMarket"
class="card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden"> class="card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden">
<figure class="relative h-60 bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center"> <figure class="relative h-60 bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center">
<i class="mdi mdi-chart-box text-6xl text-white opacity-80"></i> <i class="mdi mdi-chart-box text-6xl text-white opacity-80"></i>
@@ -97,7 +98,7 @@ onMounted(async () => {
<!-- Game Cards --> <!-- Game Cards -->
<div v-for="game in games" :key="game.id" <div v-for="game in games" :key="game.id"
@click="navigate(game.custom_path ? `/${game.path}` : `/submissions/${game.path}`)" @click="goToGame(game)"
class="card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden"> class="card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden">
<figure class="relative h-60 bg-base-300 overflow-hidden"> <figure class="relative h-60 bg-base-300 overflow-hidden">
<img v-if="getHeaderImage(game) && !imageErrors.has(game.id)" :src="getHeaderImage(game)!" :alt="game.name" <img v-if="getHeaderImage(game) && !imageErrors.has(game.id)" :src="getHeaderImage(game)!" :alt="game.name"
@@ -111,7 +112,7 @@ onMounted(async () => {
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-2xl">{{ game.name }}</h2> <h2 class="card-title text-2xl">{{ game.name }}</h2>
<div class="card-actions justify-end mt-4"> <div class="card-actions justify-end mt-4">
<button @click="navigate(game.path)" class="btn btn-primary"> <button class="btn btn-primary">
<i class="mdi mdi-arrow-right mr-2"></i> <i class="mdi mdi-arrow-right mr-2"></i>
{{ t("home.submitResults") }} {{ t("home.submitResults") }}
</button> </button>
+2 -5
View File
@@ -20,9 +20,6 @@ import { storeToRefs } from "pinia";
const props = defineProps<{ const props = defineProps<{
eventPath: string; eventPath: string;
collectionTitle: string;
collectionUrl: string;
collectionDescription: string;
}>(); }>();
provideEventPath(props.eventPath); provideEventPath(props.eventPath);
@@ -191,9 +188,9 @@ const reloadPage = () => {
<div class="mb-8"> <div class="mb-8">
<div class="card bg-base-100 shadow-lg"> <div class="card bg-base-100 shadow-lg">
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-2xl">{{ props.collectionTitle }}</h2> <h2 class="card-title text-2xl">{{ collection?.title }}</h2>
<p class="text-base-content/70"> <p class="text-base-content/70">
{{ props.collectionDescription }} {{ collection?.description }}
</p> </p>
<div class="flex flex-wrap gap-4 mt-4"> <div class="flex flex-wrap gap-4 mt-4">
<button @click="openSubmissionModal" class="btn btn-primary" :disabled="isTournamentClosed"> <button @click="openSubmissionModal" class="btn btn-primary" :disabled="isTournamentClosed">
+43 -19
View File
@@ -1,35 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from "vue"; import { ref, onMounted, onUnmounted, computed } from "vue";
import { useRouter } from "vue-router";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useTitle } from "@vueuse/core"; import { useTitle } from "@vueuse/core";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { submissionsApiCreateSubmission, submissionsApiListUserSubmissions, submissionsApiCreateImageCrop, submissionsApiDeleteImageCrop, submissionsApiProcessFileOcr, submissionsApiProcessCropOcr } from "@/api"; import { usePageHeaderStore } from "@/stores/pageHeader";
import { gamesApiListEventGames, submissionsApiListAvailableScopes, submissionsApiCreateSubmission, submissionsApiListUserSubmissions, submissionsApiCreateImageCrop, submissionsApiDeleteImageCrop, submissionsApiProcessFileOcr, submissionsApiProcessCropOcr } from "@/api";
import { useSubmissionsWebSocket } from "@/composables/useSubmissionsWebSocket"; import { useSubmissionsWebSocket } from "@/composables/useSubmissionsWebSocket";
import { provideEventPath } from "@/composables/useEventPath"; import { provideEventPath } from "@/composables/useEventPath";
interface Props { interface Props {
eventPath: string; eventPath: string;
gamePath: string;
scopeId: string; scopeId: string;
scopeName?: string;
gameName?: string;
gamePath?: string;
gameImage?: string;
gameSteamId?: string;
} }
const props = withDefaults(defineProps<Props>(), { const props = defineProps<Props>();
scopeName: "Match",
gameName: "Game",
gamePath: "",
gameImage: "",
gameSteamId: "",
});
provideEventPath(props.eventPath); provideEventPath(props.eventPath);
const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
useTitle(computed(() => t("submissions.pageTitle", { game: props.gameName, scope: props.scopeName }))); const pageHeaderStore = usePageHeaderStore();
const scopeName = ref("Match");
const gameName = ref("Game");
const gameImage = ref("");
const gameSteamId = ref("");
useTitle(computed(() => t("submissions.pageTitle", { game: gameName.value, scope: scopeName.value })));
const userStore = useUserStore(); const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore); const { userInfo } = storeToRefs(userStore);
@@ -333,11 +333,11 @@ const processCropOcr = async (cropId: string) => {
}; };
const getGameHeaderImage = () => { const getGameHeaderImage = () => {
if (props.gameImage) { if (gameImage.value) {
return props.gameImage; return gameImage.value;
} }
if (props.gameSteamId) { if (gameSteamId.value) {
return `https://cdn.akamai.steamstatic.com/steam/apps/${props.gameSteamId}/header.jpg`; return `https://cdn.akamai.steamstatic.com/steam/apps/${gameSteamId.value}/header.jpg`;
} }
return null; return null;
}; };
@@ -502,10 +502,34 @@ const handlePasteOnUploadArea = (event: ClipboardEvent) => {
} }
}; };
onMounted(() => { onMounted(async () => {
userStore.loadUserData(); userStore.loadUserData();
loadUserSubmissions(); loadUserSubmissions();
const [gamesResponse, scopesResponse] = await Promise.all([
gamesApiListEventGames({ path: { event_path: props.eventPath } }),
submissionsApiListAvailableScopes({
path: { event_path: props.eventPath, game_path: props.gamePath },
}),
]);
const game = gamesResponse.data?.find((g) => g.path === props.gamePath);
if (game) {
gameName.value = game.name;
gameImage.value = game.image ?? "";
gameSteamId.value = game.steam_app_id ? String(game.steam_app_id) : "";
}
const scope = scopesResponse.data?.find((s) => s.id === props.scopeId);
if (!scope) {
// Not in the list of scopes this user may access - bounce back to
// the picker rather than rendering a page for data we can't show.
router.replace({ name: "submissions-select", params: { eventPath: props.eventPath, gamePath: props.gamePath } });
return;
}
scopeName.value = scope.name;
pageHeaderStore.setTitle(t("submissions.pageTitle", { game: gameName.value, scope: scopeName.value }));
// Add paste listener (only processes if hovering upload area) // Add paste listener (only processes if hovering upload area)
window.addEventListener('paste', handlePasteOnUploadArea); window.addEventListener('paste', handlePasteOnUploadArea);
}); });
+49 -16
View File
@@ -1,36 +1,69 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useTitle } from "@vueuse/core"; import { useTitle } from "@vueuse/core";
import { gamesApiListEventGames, submissionsApiListAvailableScopes } from "@/api";
interface ScopeOption { import type { ScopeOut } from "@/api/types.gen";
id: string;
name: string;
}
interface Props { interface Props {
eventPath: string; eventPath: string;
gamePath: string; gamePath: string;
gameName?: string;
scopes?: ScopeOption[];
} }
const props = withDefaults(defineProps<Props>(), { const props = defineProps<Props>();
gameName: "Game",
scopes: () => [],
});
const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
useTitle(computed(() => t("submissionsSelect.title"))); useTitle(computed(() => t("submissionsSelect.title")));
const goToScope = (scopeId: string) => { const gameName = ref("");
window.location.href = `/${props.eventPath}/submissions/${props.gamePath}/${scopeId}`; const scopes = ref<ScopeOut[]>([]);
const loading = ref(true);
const scopeRoute = (scope: ScopeOut) => ({
name: "submissions",
params: { eventPath: props.eventPath, gamePath: props.gamePath, scopeId: scope.id },
});
const goToScope = (scope: ScopeOut) => {
router.push(scopeRoute(scope));
}; };
onMounted(async () => {
const [gamesResponse, scopesResponse] = await Promise.all([
gamesApiListEventGames({ path: { event_path: props.eventPath } }),
submissionsApiListAvailableScopes({
path: { event_path: props.eventPath, game_path: props.gamePath },
}),
]);
gameName.value = gamesResponse.data?.find((game) => game.path === props.gamePath)?.name ?? "";
const availableScopes = scopesResponse.data ?? [];
// Mirrors the server-side behavior this replaced: skip the picker
// straight to the only option, or bounce out if there's nothing to pick.
if (availableScopes.length === 1) {
router.replace(scopeRoute(availableScopes[0]));
return;
}
if (availableScopes.length === 0) {
router.replace({ name: "home", params: { eventPath: props.eventPath } });
return;
}
scopes.value = availableScopes;
loading.value = false;
});
</script> </script>
<template> <template>
<div class="flex-1 bg-base-200 flex items-center justify-center p-4"> <div class="flex-1 bg-base-200 flex items-center justify-center p-4">
<div class="card bg-base-100 shadow-lg w-full max-w-md"> <div v-if="loading" class="flex justify-center py-20">
<span class="loading loading-spinner loading-lg"></span>
</div>
<div v-else class="card bg-base-100 shadow-lg w-full max-w-md">
<div class="card-body"> <div class="card-body">
<h2 class="card-title">{{ t("submissionsSelect.title") }}</h2> <h2 class="card-title">{{ t("submissionsSelect.title") }}</h2>
<p class="text-base-content/70 mb-2"> <p class="text-base-content/70 mb-2">
@@ -38,7 +71,7 @@ const goToScope = (scopeId: string) => {
</p> </p>
<div class="flex flex-col gap-2 mt-2"> <div class="flex flex-col gap-2 mt-2">
<button v-for="scope in scopes" :key="scope.id" @click="goToScope(scope.id)" <button v-for="scope in scopes" :key="scope.id" @click="goToScope(scope)"
class="btn btn-outline justify-start"> class="btn btn-outline justify-start">
<i class="mdi mdi-sword-cross mr-2"></i> <i class="mdi mdi-sword-cross mr-2"></i>
{{ scope.name }} {{ scope.name }}
+2 -2
View File
@@ -1,4 +1,4 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
export { animationsApiPuzzleResults, animationsApiResults, animationsApiTopSubmissions, gamesApiListEventGames, gamesApiListGames, marketApiCloseMarket, marketApiCreateBet, marketApiListMarkets, marketApiListUserBets, marketApiResolveMarket, noitaApiGetLeaderboard, noitaApiGetResults, noitaApiSubmitLogFile, type Options, opusMagnumApiCreateSubmission, opusMagnumApiDeleteSubmission, opusMagnumApiGetCollection, opusMagnumApiGetStats, opusMagnumApiGetSubmission, opusMagnumApiListPuzzles, opusMagnumApiListResponsesNeedingValidation, opusMagnumApiListSubmissions, opusMagnumApiValidateAuto, opusMagnumApiValidateResponse, opusMagnumApiValidateSubmission, polylanSubmitterApiClearCache, polylanSubmitterApiGetUserInfo, polylanSubmitterApiHealthCheck, submissionsApiCreateImageCrop, submissionsApiCreateSubmission, submissionsApiDeleteImageCrop, submissionsApiGetSubmission, submissionsApiListMyScopes, submissionsApiListSubmissionsByGame, submissionsApiListUserSubmissions, submissionsApiProcessCropOcr, submissionsApiProcessFileOcr } from './sdk.gen'; export { animationsApiPuzzleResults, animationsApiResults, animationsApiTopSubmissions, gamesApiListEventGames, gamesApiListGames, marketApiCloseMarket, marketApiCreateBet, marketApiListMarkets, marketApiListUserBets, marketApiResolveMarket, noitaApiGetLeaderboard, noitaApiGetResults, noitaApiSubmitLogFile, type Options, opusMagnumApiCreateSubmission, opusMagnumApiDeleteSubmission, opusMagnumApiGetCollection, opusMagnumApiGetStats, opusMagnumApiGetSubmission, opusMagnumApiListPuzzles, opusMagnumApiListResponsesNeedingValidation, opusMagnumApiListSubmissions, opusMagnumApiValidateAuto, opusMagnumApiValidateResponse, opusMagnumApiValidateSubmission, polylanSubmitterApiClearCache, polylanSubmitterApiGetConfig, polylanSubmitterApiGetUserInfo, polylanSubmitterApiHealthCheck, submissionsApiCreateImageCrop, submissionsApiCreateSubmission, submissionsApiDeleteImageCrop, submissionsApiGetSubmission, submissionsApiListAvailableScopes, submissionsApiListMyScopes, submissionsApiListSubmissionsByGame, submissionsApiListUserSubmissions, submissionsApiProcessCropOcr, submissionsApiProcessFileOcr } from './sdk.gen';
export type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, AnimationsApiTopSubmissionsResponses, ClientOptions, GameOut, GamesApiListEventGamesData, GamesApiListEventGamesResponse, GamesApiListEventGamesResponses, GamesApiListGamesData, GamesApiListGamesResponse, GamesApiListGamesResponses, ImageCropIn, ImageCropOut, Input, LeaderboardEntryOut, LeaderboardOut, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponse, MarketApiCreateBetResponses, MarketApiListMarketsData, MarketApiListMarketsResponse, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponse, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponse, MarketApiResolveMarketResponses, MarketListSchema, MarketOptionSchema, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponse, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponse, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileError, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponse, NoitaApiSubmitLogFileResponses, NoitaSubmissionOut, ObjectivResultOut, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponse, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponse, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponse, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponse, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponse, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponse, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponse, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponse, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponse, OpusMagnumApiValidateSubmissionResponses, PagedSubmissionOut, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponse, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, PuzzlePointsFactorOut, PuzzleResponseIn, PuzzleResponseOut, PuzzleResponseRankingOut, PuzzleResultsOut, PuzzleSubmissionsOut, PuzzleSubmissionWithRankOut, RankingSchema, ResolveMarketSchema, ResultsOut, ScopeOut, SteamCollectionItemOut, SteamCollectionOut, SubmissionFileOut, SubmissionIn, SubmissionOut, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponse, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionError, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponse, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponse, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponse, SubmissionsApiGetSubmissionResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesError, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponse, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponse, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsError, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponse, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponse, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponse, SubmissionsApiProcessFileOcrResponses, TournamentPuzzleResultsOut, TournamentSubmissionsOut, UserBetCreateSchema, UserBetSchema, UserDisplayOut, UserInfoOut, ValidationIn, WinnerFileOut, WinnerResponseOut } from './types.gen'; export type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, AnimationsApiTopSubmissionsResponses, ClientOptions, ConfigOut, GameOut, GamesApiListEventGamesData, GamesApiListEventGamesResponse, GamesApiListEventGamesResponses, GamesApiListGamesData, GamesApiListGamesResponse, GamesApiListGamesResponses, ImageCropIn, ImageCropOut, Input, LeaderboardEntryOut, LeaderboardOut, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponse, MarketApiCreateBetResponses, MarketApiListMarketsData, MarketApiListMarketsResponse, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponse, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponse, MarketApiResolveMarketResponses, MarketListSchema, MarketOptionSchema, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponse, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponse, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileError, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponse, NoitaApiSubmitLogFileResponses, NoitaSubmissionOut, ObjectivResultOut, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponse, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponse, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponse, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponse, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponse, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponse, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponse, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponse, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponse, OpusMagnumApiValidateSubmissionResponses, PagedSubmissionOut, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetConfigData, PolylanSubmitterApiGetConfigResponse, PolylanSubmitterApiGetConfigResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponse, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, PuzzlePointsFactorOut, PuzzleResponseIn, PuzzleResponseOut, PuzzleResponseRankingOut, PuzzleResultsOut, PuzzleSubmissionsOut, PuzzleSubmissionWithRankOut, RankingSchema, ResolveMarketSchema, ResultsOut, ScopeOut, SteamCollectionItemOut, SteamCollectionOut, SubmissionFileOut, SubmissionIn, SubmissionOut, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponse, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionError, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponse, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponse, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponse, SubmissionsApiGetSubmissionResponses, SubmissionsApiListAvailableScopesData, SubmissionsApiListAvailableScopesError, SubmissionsApiListAvailableScopesErrors, SubmissionsApiListAvailableScopesResponse, SubmissionsApiListAvailableScopesResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesError, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponse, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponse, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsError, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponse, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponse, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponse, SubmissionsApiProcessFileOcrResponses, TournamentPuzzleResultsOut, TournamentSubmissionsOut, UserBetCreateSchema, UserBetSchema, UserDisplayOut, UserInfoOut, ValidationIn, WinnerFileOut, WinnerResponseOut } from './types.gen';
+17 -1
View File
@@ -2,7 +2,7 @@
import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client'; import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client';
import { client } from './client.gen'; import { client } from './client.gen';
import type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponses, GamesApiListEventGamesData, GamesApiListEventGamesResponses, GamesApiListGamesData, GamesApiListGamesResponses, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponses, MarketApiListMarketsData, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponses, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponses, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponses, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponses } from './types.gen'; import type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponses, GamesApiListEventGamesData, GamesApiListEventGamesResponses, GamesApiListGamesData, GamesApiListGamesResponses, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponses, MarketApiListMarketsData, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponses, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponses, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponses, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetConfigData, PolylanSubmitterApiGetConfigResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponses, SubmissionsApiListAvailableScopesData, SubmissionsApiListAvailableScopesErrors, SubmissionsApiListAvailableScopesResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & { export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
/** /**
@@ -25,6 +25,11 @@ export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends
*/ */
export const polylanSubmitterApiHealthCheck = <ThrowOnError extends boolean = false>(options?: Options<PolylanSubmitterApiHealthCheckData, ThrowOnError>) => (options?.client ?? client).get<PolylanSubmitterApiHealthCheckResponses, unknown, ThrowOnError>({ url: '/api/health', ...options }); export const polylanSubmitterApiHealthCheck = <ThrowOnError extends boolean = false>(options?: Options<PolylanSubmitterApiHealthCheckData, ThrowOnError>) => (options?.client ?? client).get<PolylanSubmitterApiHealthCheckResponses, unknown, ThrowOnError>({ url: '/api/health', ...options });
/**
* Get Config
*/
export const polylanSubmitterApiGetConfig = <ThrowOnError extends boolean = false>(options?: Options<PolylanSubmitterApiGetConfigData, ThrowOnError>) => (options?.client ?? client).get<PolylanSubmitterApiGetConfigResponses, unknown, ThrowOnError>({ url: '/api/config', ...options });
/** /**
* Clear Cache * Clear Cache
* *
@@ -201,6 +206,17 @@ export const gamesApiListEventGames = <ThrowOnError extends boolean = false>(opt
*/ */
export const submissionsApiListMyScopes = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiListMyScopesData, ThrowOnError>) => (options.client ?? client).get<SubmissionsApiListMyScopesResponses, SubmissionsApiListMyScopesErrors, ThrowOnError>({ url: '/api/events/{event_path}/submissions/scopes/mine', ...options }); export const submissionsApiListMyScopes = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiListMyScopesData, ThrowOnError>) => (options.client ?? client).get<SubmissionsApiListMyScopesResponses, SubmissionsApiListMyScopesErrors, ThrowOnError>({ url: '/api/events/{event_path}/submissions/scopes/mine', ...options });
/**
* List Available Scopes
*
* Scopes the current user may submit to for this game - superusers see
* every active scope, everyone else only the ones assigned to them. Used
* by the submissions-select and submissions-detail pages, both to list
* scopes and (for the detail page) to confirm the requested scope_id is
* one the current user is actually allowed to see.
*/
export const submissionsApiListAvailableScopes = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiListAvailableScopesData, ThrowOnError>) => (options.client ?? client).get<SubmissionsApiListAvailableScopesResponses, SubmissionsApiListAvailableScopesErrors, ThrowOnError>({ url: '/api/events/{event_path}/submissions/{game_path}/available-scopes', ...options });
/** /**
* Create Submission * Create Submission
*/ */
+66
View File
@@ -4,6 +4,16 @@ export type ClientOptions = {
baseUrl: string; baseUrl: string;
}; };
/**
* ConfigOut
*/
export type ConfigOut = {
/**
* Market Enabled
*/
market_enabled: boolean;
};
/** /**
* UserInfoOut * UserInfoOut
* *
@@ -1125,6 +1135,22 @@ export type PolylanSubmitterApiHealthCheckResponses = {
200: unknown; 200: unknown;
}; };
export type PolylanSubmitterApiGetConfigData = {
body?: never;
path?: never;
query?: never;
url: '/api/config';
};
export type PolylanSubmitterApiGetConfigResponses = {
/**
* OK
*/
200: ConfigOut;
};
export type PolylanSubmitterApiGetConfigResponse = PolylanSubmitterApiGetConfigResponses[keyof PolylanSubmitterApiGetConfigResponses];
export type PolylanSubmitterApiClearCacheData = { export type PolylanSubmitterApiClearCacheData = {
body?: never; body?: never;
path?: never; path?: never;
@@ -1668,6 +1694,46 @@ export type SubmissionsApiListMyScopesResponses = {
export type SubmissionsApiListMyScopesResponse = SubmissionsApiListMyScopesResponses[keyof SubmissionsApiListMyScopesResponses]; export type SubmissionsApiListMyScopesResponse = SubmissionsApiListMyScopesResponses[keyof SubmissionsApiListMyScopesResponses];
export type SubmissionsApiListAvailableScopesData = {
body?: never;
path: {
/**
* Event Path
*/
event_path: string;
/**
* Game Path
*/
game_path: string;
};
query?: never;
url: '/api/events/{event_path}/submissions/{game_path}/available-scopes';
};
export type SubmissionsApiListAvailableScopesErrors = {
/**
* Response
*
* Forbidden
*/
403: {
[key: string]: unknown;
};
};
export type SubmissionsApiListAvailableScopesError = SubmissionsApiListAvailableScopesErrors[keyof SubmissionsApiListAvailableScopesErrors];
export type SubmissionsApiListAvailableScopesResponses = {
/**
* Response
*
* OK
*/
200: Array<ScopeOut>;
};
export type SubmissionsApiListAvailableScopesResponse = SubmissionsApiListAvailableScopesResponses[keyof SubmissionsApiListAvailableScopesResponses];
export type SubmissionsApiCreateSubmissionData = { export type SubmissionsApiCreateSubmissionData = {
/** /**
* MultiPartBodyParams * MultiPartBodyParams
-11
View File
@@ -1,11 +0,0 @@
// Per-page data Django injects via `{{ bootstrap|json_script:"bootstrap-data" }}`
// in templates/app.html. Read once at boot, before the router/app are created.
export const bootstrap: Record<string, unknown> = (() => {
const el = document.getElementById("bootstrap-data");
if (!el?.textContent) return {};
try {
return JSON.parse(el.textContent);
} catch {
return {};
}
})();
+11 -12
View File
@@ -1,30 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed } from "vue";
import { useRoute } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { bootstrap } from "@/bootstrap"; import { usePageHeaderStore } from "@/stores/pageHeader";
const route = useRoute(); const route = useRoute();
const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const userStore = useUserStore(); const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore); const { userInfo } = storeToRefs(userStore);
const pageHeaderStore = usePageHeaderStore();
const { title: dynamicTitle } = storeToRefs(pageHeaderStore);
const title = computed(() => { const title = computed(() => {
if (dynamicTitle.value) return dynamicTitle.value;
const key = route.meta.titleKey; const key = route.meta.titleKey;
if (!key) return t("common.appName"); return key ? t(key) : t("common.appName");
return t(key, {
game: (bootstrap.gameName as string) ?? "",
scope: (bootstrap.scopeName as string) ?? "",
});
}); });
const backPath = computed(() => route.meta.getBackPath?.(route));
const goBack = () => { const goBack = () => {
if (backPath.value) { const backRoute = route.meta.getBackRoute?.(route);
window.location.href = backPath.value; if (backRoute) {
router.push(backRoute);
} }
}; };
</script> </script>
@@ -32,7 +31,7 @@ const goBack = () => {
<template> <template>
<div class="navbar bg-base-100 shadow-lg"> <div class="navbar bg-base-100 shadow-lg">
<div class="container min-w-3/4 mx-auto w-full flex items-center gap-4"> <div class="container min-w-3/4 mx-auto w-full flex items-center gap-4">
<button v-if="backPath" @click="goBack" class="btn btn-primary btn-sm"> <button v-if="route.meta.getBackRoute" @click="goBack" class="btn btn-primary btn-sm">
<i class="mdi mdi-arrow-left"></i> <i class="mdi mdi-arrow-left"></i>
{{ t("common.back") }} {{ t("common.back") }}
</button> </button>
+31 -23
View File
@@ -1,29 +1,27 @@
import { createRouter, createWebHistory, type RouteLocationNormalizedLoaded } from "vue-router"; import { createRouter, createWebHistory, type RouteLocationNormalizedLoaded, type RouteLocationRaw } from "vue-router";
import Home from "@/Home.vue"; import Home from "@/Home.vue";
import Market from "@/Market.vue"; import Market from "@/Market.vue";
import Noita from "@/Noita.vue"; import Noita from "@/Noita.vue";
import OpusMagnum from "@/OpusMagnum.vue"; import OpusMagnum from "@/OpusMagnum.vue";
import Submissions from "@/Submissions.vue"; import Submissions from "@/Submissions.vue";
import SubmissionsSelect from "@/SubmissionsSelect.vue"; import SubmissionsSelect from "@/SubmissionsSelect.vue";
import { bootstrap } from "@/bootstrap"; import { usePageHeaderStore } from "@/stores/pageHeader";
declare module "vue-router" { declare module "vue-router" {
interface RouteMeta { interface RouteMeta {
// Static label for AppHeader's title bar. Pages whose title depends on
// fetched data (e.g. submissions) set one at runtime via
// usePageHeaderStore() instead and leave this out.
titleKey?: string; titleKey?: string;
getBackPath?: (route: RouteLocationNormalizedLoaded) => string; getBackRoute?: (route: RouteLocationNormalizedLoaded) => RouteLocationRaw;
} }
} }
// Route params (eventPath, gamePath, scopeId, ...) come from the URL; const eventHome = (route: RouteLocationNormalizedLoaded): RouteLocationRaw => ({
// page-specific extras Django can't express as URL segments (collection name: "home",
// title, scope name, ...) come from the bootstrap JSON island instead. params: { eventPath: route.params.eventPath },
const withBootstrap = (route: RouteLocationNormalizedLoaded) => ({
...route.params,
...bootstrap,
}); });
const eventHome = (route: RouteLocationNormalizedLoaded) => `/${route.params.eventPath}/`;
export const router = createRouter({ export const router = createRouter({
history: createWebHistory("/"), history: createWebHistory("/"),
routes: [ routes: [
@@ -31,45 +29,55 @@ export const router = createRouter({
path: "/:eventPath/", path: "/:eventPath/",
name: "home", name: "home",
component: Home, component: Home,
props: withBootstrap, props: true,
}, },
{ {
path: "/:eventPath/market", path: "/:eventPath/market",
name: "market", name: "market",
component: Market, component: Market,
props: withBootstrap, props: true,
meta: { titleKey: "market.header", getBackPath: eventHome }, meta: { titleKey: "market.header", getBackRoute: eventHome },
}, },
{ {
path: "/:eventPath/opus-magnum", path: "/:eventPath/opus-magnum",
name: "opus-magnum", name: "opus-magnum",
component: OpusMagnum, component: OpusMagnum,
props: withBootstrap, props: true,
meta: { titleKey: "opusMagnum.header", getBackPath: eventHome }, meta: { titleKey: "opusMagnum.header", getBackRoute: eventHome },
}, },
{ {
path: "/:eventPath/noita", path: "/:eventPath/noita",
name: "noita", name: "noita",
component: Noita, component: Noita,
props: withBootstrap, props: true,
meta: { titleKey: "noita.header", getBackPath: eventHome }, meta: { titleKey: "noita.header", getBackRoute: eventHome },
}, },
{ {
path: "/:eventPath/submissions/:gamePath", path: "/:eventPath/submissions/:gamePath",
name: "submissions-select", name: "submissions-select",
component: SubmissionsSelect, component: SubmissionsSelect,
props: withBootstrap, props: true,
meta: { titleKey: "submissionsSelect.title", getBackPath: eventHome }, meta: { titleKey: "submissionsSelect.title", getBackRoute: eventHome },
}, },
{ {
path: "/:eventPath/submissions/:gamePath/:scopeId", path: "/:eventPath/submissions/:gamePath/:scopeId",
name: "submissions", name: "submissions",
component: Submissions, component: Submissions,
props: withBootstrap, props: true,
meta: { meta: {
titleKey: "submissions.pageTitle", // No static titleKey: Submissions.vue sets a dynamic
getBackPath: (route) => `/${route.params.eventPath}/submissions/${route.params.gamePath}`, // "{game} Submissions — {scope}" title once it has fetched both.
getBackRoute: (route) => ({
name: "submissions-select",
params: { eventPath: route.params.eventPath, gamePath: route.params.gamePath },
}),
}, },
}, },
], ],
}); });
// Every route change starts from a clean header title; pages that need a
// dynamic one (see submissions above) set it again once their data lands.
router.beforeEach(() => {
usePageHeaderStore().clear();
});
@@ -0,0 +1,20 @@
import { defineStore } from "pinia";
import { ref } from "vue";
// Lets a routed page hand AppHeader a title built from data only that page
// has fetched (e.g. "Noita Submissions — Semifinal", assembled from an API
// call keyed by the scopeId route param). Cleared on every navigation so a
// stale title never lingers while the next page is still loading.
export const usePageHeaderStore = defineStore("pageHeader", () => {
const title = ref("");
const setTitle = (value: string) => {
title.value = value;
};
const clear = () => {
title.value = "";
};
return { title, setTitle, clear };
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+32
View File
@@ -31,6 +31,38 @@ def list_my_scopes(request: HttpRequest, event_path: str, game_path: str):
) )
@router.get(f"{PREFIX}/{{game_path}}/available-scopes", response={200: list[ScopeOut], 403: dict})
def list_available_scopes(request: HttpRequest, event_path: str, game_path: str):
"""Scopes the current user may submit to for this game - superusers see
every active scope, everyone else only the ones assigned to them. Used
by the submissions-select and submissions-detail pages, both to list
scopes and (for the detail page) to confirm the requested scope_id is
one the current user is actually allowed to see."""
if not request.user.is_authenticated:
return 403, {"detail": "Authentication required"}
game = get_object_or_404(
Game.objects.enabled_for_event(request.event), path=game_path
)
scopes = (
Scope.objects.filter(
event_game__event=request.event,
event_game__game=game,
is_active=True,
)
if request.user.is_superuser
else Scope.objects.filter(
event_game__event=request.event,
event_game__game=game,
assigned_users=request.user,
is_active=True,
)
)
return list(scopes.order_by("name"))
@router.post(f"{PREFIX}/submission/create", response={200: SubmissionOut, 403: dict}) @router.post(f"{PREFIX}/submission/create", response={200: SubmissionOut, 403: dict})
def create_submission( def create_submission(
request: HttpRequest, request: HttpRequest,
+2 -27
View File
@@ -39,19 +39,7 @@ def submission_home(request: HttpRequest, event_path: str, game_path: str):
if not scopes: if not scopes:
raise Http404 raise Http404
return render( return render(request, "app.html")
request,
"app.html",
{
"bootstrap": {
"gameName": game.name,
"scopes": [
{"id": str(scope.id), "name": scope.name}
for scope in scopes.order_by("name")
],
},
},
)
@login_required @login_required
@@ -74,17 +62,4 @@ def submission_view(
): ):
raise Http404 raise Http404
game = scope.event_game.game return render(request, "app.html")
return render(
request,
"app.html",
{
"bootstrap": {
"scopeName": scope.name,
"gameName": game.name,
"gamePath": game.path,
"gameImage": game.image or "",
"gameSteamId": str(game.steam_app_id or ""),
},
},
)
-1
View File
@@ -7,6 +7,5 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{{ bootstrap|json_script:"bootstrap-data" }}
<div id="app"></div> <div id="app"></div>
{% endblock %} {% endblock %}
+2 -1
View File
@@ -1,9 +1,10 @@
<!doctype html> <!doctype html>
{% load django_vite %} {% load django_vite %}
{% load static %}
<html lang="{{ LANGUAGE_CODE }}" data-theme="dim"> <html lang="{{ LANGUAGE_CODE }}" data-theme="dim">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/png" href="{% static 'favicon.ico' %}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}PolyLAN Submitter{% endblock %}</title> <title>{% block title %}PolyLAN Submitter{% endblock %}</title>
{% if debug %}{% vite_hmr_client %}{% endif %} {% if debug %}{% vite_hmr_client %}{% endif %}