Compare commits

...
2 Commits
Author SHA1 Message Date
Legrems 99a868a6be feat: switch to spa 2026-09-17 00:37:28 +02:00
Legrems 15590e5162 feat: "spa", but more like single entry-point 2026-09-16 23:25:16 +02:00
40 changed files with 491 additions and 369 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ dist-ssr
# Vite build output (generated locally via `pnpm build`, served by whitenoise)
static_source/vite/assets
static_source/vite/manifest.json
static_source/vite/vite.svg
static_source/vite/icon.png
tsconfig.tsbuildinfo
# vue-tsc emits a compiled copy of vite.config.ts here (tsconfig.node.json,
+2 -2
View File
@@ -7,5 +7,5 @@ from events.models import Event
@login_required
def market_home(request: HttpRequest, event_path: str):
event = get_object_or_404(Event, path=event_path)
return render(request, "market.html", {"event": event})
get_object_or_404(Event, path=event_path)
return render(request, "app.html")
+2 -1
View File
@@ -20,7 +20,8 @@
"tailwindcss": "^4.1.16",
"tesseract.js": "^5.1.1",
"vue": "^3.5.22",
"vue-i18n": "^11.4.12"
"vue-i18n": "^11.4.12",
"vue-router": "4"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.97.2",
+13
View File
@@ -38,6 +38,9 @@ importers:
vue-i18n:
specifier: ^11.4.12
version: 11.4.12(vue@3.5.22(typescript@5.9.3))
vue-router:
specifier: '4'
version: 4.6.4(vue@3.5.22(typescript@5.9.3))
devDependencies:
'@hey-api/openapi-ts':
specifier: ^0.97.2
@@ -1075,6 +1078,11 @@ packages:
peerDependencies:
vue: ^3.0.0
vue-router@4.6.4:
resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
peerDependencies:
vue: ^3.5.0
vue-tsc@3.1.2:
resolution: {integrity: sha512-3fd4DY0rFczs5f+VB3OhcLU83V6+3Puj2yLBe0Ak65k7ERk+STVNKaOAi0EBo6Lc15UiJB6LzU6Mxy4+h/pKew==}
hasBin: true
@@ -1962,6 +1970,11 @@ snapshots:
'@vue/devtools-api': 6.6.4
vue: 3.5.22(typescript@5.9.3)
vue-router@4.6.4(vue@3.5.22(typescript@5.9.3)):
dependencies:
'@vue/devtools-api': 6.6.4
vue: 3.5.22(typescript@5.9.3)
vue-tsc@3.1.2(typescript@5.9.3):
dependencies:
'@volar/typescript': 2.4.23
+12 -1
View File
@@ -1,4 +1,4 @@
from ninja import NinjaAPI
from ninja import NinjaAPI, Schema
from django.conf import settings
from django.core.cache import cache
from django.http import HttpRequest
@@ -62,6 +62,17 @@ def health_check(request):
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
@api.post("/cache/clear")
def clear_cache(request: HttpRequest, event_path: str | None = None):
+6 -26
View File
@@ -43,42 +43,22 @@ def home(request: HttpRequest):
@login_required
def event_home(request: HttpRequest, event_path: str):
event = get_object_or_404(Event, path=event_path)
return render(
request,
"home.html",
{
"event": event,
"market_enabled": settings.MARKET_ENABLED,
},
)
get_object_or_404(Event, path=event_path)
return render(request, "app.html")
@login_required
@require_game_enabled(OPUS_MAGNUM_APP_ID)
def opus_magnum_home(request: HttpRequest, event_path: str):
from opus_magnum.models import SteamCollection
event = get_object_or_404(Event, path=event_path)
return render(
request,
"opus-magnum.html",
{
"event": event,
"collection": SteamCollection.objects.filter(
is_active=True, event=event
).last(),
},
)
get_object_or_404(Event, path=event_path)
return render(request, "app.html")
@login_required
@require_game_enabled(NOITA_APP_ID)
def noita_home(request: HttpRequest, event_path: str):
event = get_object_or_404(Event, path=event_path)
return render(request, "noita.html", {"event": event})
get_object_or_404(Event, path=event_path)
return render(request, "app.html")
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

+23
View File
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { onMounted } from "vue";
import { RouterView } from "vue-router";
import AppHeader from "@/layout/AppHeader.vue";
import AppFooter from "@/layout/AppFooter.vue";
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
onMounted(() => {
userStore.loadUserData();
});
</script>
<template>
<div class="min-h-screen flex flex-col">
<AppHeader />
<main class="flex-1 flex flex-col">
<RouterView />
</main>
<AppFooter />
</div>
</template>
+23 -22
View File
@@ -1,25 +1,25 @@
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { useTitle } from "@vueuse/core";
import { gamesApiListEventGames } from "./api";
import { gamesApiListEventGames, polylanSubmitterApiGetConfig } from "./api";
import type { GamesApiListEventGamesResponse } from "./api/types.gen";
interface Props {
eventPath: string;
marketEnabled?: string | boolean;
}
const props = withDefaults(defineProps<Props>(), {
marketEnabled: true,
});
const props = defineProps<Props>();
const router = useRouter();
const { t } = useI18n();
useTitle(computed(() => t("home.title")));
const games = ref<GamesApiListEventGamesResponse | undefined>();
const loading = ref(true);
const imageErrors = ref<Set<number>>(new Set());
const marketEnabled = ref(false);
const getHeaderImage = (game: GamesApiListEventGamesResponse[number]) => {
if (game.image) {
@@ -35,31 +35,32 @@ const onImageError = (gameId: number) => {
imageErrors.value.add(gameId);
};
const navigate = (path: string) => {
window.location.href = `/${props.eventPath}${path}`;
const goToMarket = () => {
router.push(`/${props.eventPath}/market`);
};
const isMarketEnabled = () => {
if (typeof props.marketEnabled === 'string') {
return props.marketEnabled === 'true';
}
return Boolean(props.marketEnabled);
const goToGame = (game: GamesApiListEventGamesResponse[number]) => {
const path = game.custom_path ? `/${game.path}` : `/submissions/${game.path}`;
router.push(`/${props.eventPath}${path}`);
};
onMounted(async () => {
// Fetch games enabled for this event
const response = await gamesApiListEventGames({
path: { event_path: props.eventPath },
});
if (response.data) {
games.value = response.data;
const [gamesResponse, configResponse] = await Promise.all([
gamesApiListEventGames({ path: { event_path: props.eventPath } }),
polylanSubmitterApiGetConfig(),
]);
if (gamesResponse.data) {
games.value = gamesResponse.data;
}
if (configResponse.data) {
marketEnabled.value = configResponse.data.market_enabled;
}
loading.value = false;
});
</script>
<template>
<div class="min-h-screen bg-base-300 flex items-center justify-center px-4">
<div class="flex-1 bg-base-300 flex items-center justify-center px-4">
<div class="w-full max-w-6xl">
<!-- Header -->
<div class="text-center mb-12">
@@ -77,7 +78,7 @@ onMounted(async () => {
<!-- Cards Grid -->
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- 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">
<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>
@@ -97,7 +98,7 @@ onMounted(async () => {
<!-- Game Cards -->
<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">
<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"
@@ -111,7 +112,7 @@ onMounted(async () => {
<div class="card-body">
<h2 class="card-title text-2xl">{{ game.name }}</h2>
<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>
{{ t("home.submitResults") }}
</button>
+1 -28
View File
@@ -21,10 +21,6 @@ useTitle(computed(() => t("market.pageTitle")));
const marketStore = useMarketStore();
const { markets, userInfo, isLoading } = storeToRefs(marketStore);
const goHome = () => {
window.location.href = `/${props.eventPath}/`;
};
const reloadPage = async () => {
await marketStore.refreshPage(props.eventPath);
};
@@ -35,30 +31,7 @@ onMounted(() => {
</script>
<template>
<div class="min-h-screen bg-base-200">
<!-- Header -->
<div class="navbar bg-base-100 shadow-lg">
<div class="container min-w-3/4 mx-auto w-full flex items-center gap-4">
<button @click="goHome" class="btn btn-primary btn-sm">
<i class="mdi mdi-arrow-left"></i>
{{ t("common.back") }}
</button>
<h1 class="text-xl font-bold">{{ t("market.header") }}</h1>
<div class="flex-1"></div>
<div class="flex items-center gap-4">
<div v-if="userInfo?.is_authenticated" class="flex items-center gap-2">
<div class="text-sm">
<span class="font-medium">{{ userInfo.username }}</span>
<span v-if="userInfo.is_superuser" class="badge badge-warning badge-xs ml-1">{{ t("common.admin") }}</span>
</div>
</div>
<div v-else class="text-sm text-base-content/70">{{ t("common.notLoggedIn") }}</div>
<a href="/api/docs" class="btn btn-xs">{{ t("common.apiDocs") }}</a>
<a href="/admin" class="btn btn-xs btn-warning">{{ t("common.adminPanel") }}</a>
</div>
</div>
</div>
<div class="flex-1 bg-base-200">
<!-- Main Content -->
<div class="container min-w-3/4 mx-auto px-4 py-8">
<!-- Header -->
+1 -19
View File
@@ -155,10 +155,6 @@ const submitRun = async () => {
}
};
const goHome = () => {
window.location.href = `/${props.eventPath}/`;
};
const clearCache = async () => {
try {
await noitaStore.clearCache(props.eventPath);
@@ -175,21 +171,7 @@ onMounted(() => {
</script>
<template>
<div class="min-h-screen bg-base-200">
<!-- Header -->
<div class="navbar bg-base-100 shadow-lg">
<div class="container min-w-3/4 mx-auto w-full flex items-center gap-4">
<button @click="goHome" class="btn btn-primary btn-sm">
<i class="mdi mdi-arrow-left"></i>
{{ t("common.back") }}
</button>
<h1 class="text-xl font-bold">{{ t("noita.header") }}</h1>
<div class="flex-1"></div>
<a href="/api/docs" class="btn btn-xs">{{ t("common.apiDocs") }}</a>
<a href="/admin" v-if="userInfo.isStaff" class="btn btn-xs btn-warning">{{ t("common.adminPanel") }}</a>
</div>
</div>
<div class="flex-1 bg-base-200">
<!-- Main Content -->
<div class="container min-w-3/4 mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
+3 -33
View File
@@ -20,9 +20,6 @@ import { storeToRefs } from "pinia";
const props = defineProps<{
eventPath: string;
collectionTitle: string;
collectionUrl: string;
collectionDescription: string;
}>();
provideEventPath(props.eventPath);
@@ -139,37 +136,10 @@ const findPuzzleByName = (ocrPuzzleName: string) => {
const reloadPage = () => {
window.location.reload();
};
const goHome = () => {
window.location.href = `/${props.eventPath}/`;
};
</script>
<template>
<div class="min-h-screen bg-base-200">
<!-- Header -->
<div class="navbar bg-base-100 shadow-lg">
<div class="container min-w-3/4 mx-auto w-full flex items-center gap-4">
<button @click="goHome" class="btn btn-primary btn-sm">
<i class="mdi mdi-arrow-left"></i>
{{ t("common.back") }}
</button>
<h1 class="text-xl font-bold">{{ t("opusMagnum.header") }}</h1>
<div class="flex-1"></div>
<div class="flex items-center gap-4">
<div v-if="userInfo?.is_authenticated" class="flex items-center gap-2">
<div class="text-sm">
<span class="font-medium">{{ userInfo.username }}</span>
<span v-if="userInfo.is_superuser" class="badge badge-warning badge-xs ml-1">{{ t("common.admin") }}</span>
</div>
</div>
<div v-else class="text-sm text-base-content/70">{{ t("common.notLoggedIn") }}</div>
<a href="/api/docs" class="btn btn-xs">{{ t("common.apiDocs") }}</a>
<a href="/admin" class="btn btn-xs btn-warning">{{ t("common.adminPanel") }}</a>
</div>
</div>
</div>
<div class="flex-1 bg-base-200">
<!-- Main Content -->
<div class="container min-w-3/4 mx-auto px-4 py-8">
<!-- Loading State -->
@@ -218,9 +188,9 @@ const goHome = () => {
<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>
<h2 class="card-title text-2xl">{{ collection?.title }}</h2>
<p class="text-base-content/70">
{{ props.collectionDescription }}
{{ collection?.description }}
</p>
<div class="flex flex-wrap gap-4 mt-4">
<button @click="openSubmissionModal" class="btn btn-primary" :disabled="isTournamentClosed">
+47 -42
View File
@@ -1,35 +1,35 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from "vue";
import { useRouter } from "vue-router";
import { storeToRefs } from "pinia";
import { useI18n } from "vue-i18n";
import { useTitle } from "@vueuse/core";
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 { provideEventPath } from "@/composables/useEventPath";
interface Props {
eventPath: string;
gamePath: string;
scopeId: string;
scopeName?: string;
gameName?: string;
gamePath?: string;
gameImage?: string;
gameSteamId?: string;
}
const props = withDefaults(defineProps<Props>(), {
scopeName: "Match",
gameName: "Game",
gamePath: "",
gameImage: "",
gameSteamId: "",
});
const props = defineProps<Props>();
provideEventPath(props.eventPath);
const router = useRouter();
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 { userInfo } = storeToRefs(userStore);
@@ -333,11 +333,11 @@ const processCropOcr = async (cropId: string) => {
};
const getGameHeaderImage = () => {
if (props.gameImage) {
return props.gameImage;
if (gameImage.value) {
return gameImage.value;
}
if (props.gameSteamId) {
return `https://cdn.akamai.steamstatic.com/steam/apps/${props.gameSteamId}/header.jpg`;
if (gameSteamId.value) {
return `https://cdn.akamai.steamstatic.com/steam/apps/${gameSteamId.value}/header.jpg`;
}
return null;
};
@@ -430,10 +430,6 @@ const submitSubmission = async () => {
}
};
const goHome = () => {
window.location.href = `/${props.eventPath}/submissions/${props.gamePath}`;
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
@@ -506,10 +502,34 @@ const handlePasteOnUploadArea = (event: ClipboardEvent) => {
}
};
onMounted(() => {
onMounted(async () => {
userStore.loadUserData();
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)
window.addEventListener('paste', handlePasteOnUploadArea);
});
@@ -520,32 +540,17 @@ onUnmounted(() => {
</script>
<template>
<div class="min-h-screen bg-base-200">
<!-- Header -->
<div class="navbar bg-base-100 shadow-lg">
<div class="container min-w-3/4 mx-auto w-full flex items-center gap-4">
<button @click="goHome" class="btn btn-primary btn-sm">
<i class="mdi mdi-arrow-left"></i>
{{ t("common.back") }}
</button>
<h1 class="text-xl font-bold">{{ t("submissions.pageTitle", { game: gameName, scope: scopeName }) }}</h1>
<div class="flex-1"></div>
<div class="flex-1 bg-base-200">
<!-- Main Content -->
<div class="container min-w-3/4 mx-auto px-4 py-8">
<!-- WebSocket Status Indicator -->
<div class="flex items-center gap-2">
<div class="flex justify-end mb-4">
<div :class="['badge', wsConnected ? 'badge-success' : 'badge-warning']">
<i :class="['mdi mr-1', wsConnected ? 'mdi-wifi' : 'mdi-wifi-off']"></i>
{{ wsConnected ? t("submissions.live") : t("submissions.offline") }}
</div>
</div>
<a href="/api/docs" class="btn btn-xs">{{ t("common.apiDocs") }}</a>
<a v-if="userInfo.isStaff" href="/admin" class="btn btn-xs btn-warning">{{ t("common.adminPanel") }}</a>
</div>
</div>
<!-- Main Content -->
<div class="container min-w-3/4 mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Right Column: Upload -->
<div class="lg:col-span-1 flex flex-col gap-8 order-1 lg:order-2">
+49 -27
View File
@@ -1,40 +1,69 @@
<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 { useTitle } from "@vueuse/core";
interface ScopeOption {
id: string;
name: string;
}
import { gamesApiListEventGames, submissionsApiListAvailableScopes } from "@/api";
import type { ScopeOut } from "@/api/types.gen";
interface Props {
eventPath: string;
gamePath: string;
gameName?: string;
scopes?: ScopeOption[];
}
const props = withDefaults(defineProps<Props>(), {
gameName: "Game",
scopes: () => [],
});
const props = defineProps<Props>();
const router = useRouter();
const { t } = useI18n();
useTitle(computed(() => t("submissionsSelect.title")));
const goToScope = (scopeId: string) => {
window.location.href = `/${props.eventPath}/submissions/${props.gamePath}/${scopeId}`;
const gameName = ref("");
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));
};
const goHome = () => {
window.location.href = `/${props.eventPath}`;
};
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>
<template>
<div class="min-h-screen bg-base-200 flex items-center justify-center p-4">
<div class="card bg-base-100 shadow-lg w-full max-w-md">
<div class="flex-1 bg-base-200 flex items-center justify-center p-4">
<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">
<h2 class="card-title">{{ t("submissionsSelect.title") }}</h2>
<p class="text-base-content/70 mb-2">
@@ -42,19 +71,12 @@ const goHome = () => {
</p>
<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">
<i class="mdi mdi-sword-cross mr-2"></i>
{{ scope.name }}
</button>
</div>
<div class="card-actions mt-4">
<button @click="goHome" class="btn btn-ghost btn-sm">
<i class="mdi mdi-arrow-left"></i>
{{ t("common.back") }}
</button>
</div>
</div>
</div>
</div>
+2 -2
View File
@@ -1,4 +1,4 @@
// 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 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 { 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, 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 { 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> & {
/**
@@ -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 });
/**
* 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
*
@@ -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 });
/**
* 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
*/
+66
View File
@@ -4,6 +4,16 @@ export type ClientOptions = {
baseUrl: string;
};
/**
* ConfigOut
*/
export type ConfigOut = {
/**
* Market Enabled
*/
market_enabled: boolean;
};
/**
* UserInfoOut
*
@@ -1125,6 +1135,22 @@ export type PolylanSubmitterApiHealthCheckResponses = {
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 = {
body?: never;
path?: never;
@@ -1668,6 +1694,46 @@ export type 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 = {
/**
* MultiPartBodyParams
-11
View File
@@ -1,11 +0,0 @@
import { createApp } from 'vue'
import Home from '@/Home.vue'
import { i18n } from '@/i18n'
import '@/style.css'
// const app = createApp(App)
const selector = "#app"
const mountData = document.querySelector<HTMLElement>(selector)
const app = createApp(Home, { ...mountData?.dataset })
app.use(i18n)
app.mount(selector)
@@ -0,0 +1,12 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const year = new Date().getFullYear();
</script>
<template>
<footer class="text-center text-sm text-base-content/50 py-6">
&copy; {{ year }} {{ t("common.appName") }}
</footer>
</template>
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { storeToRefs } from "pinia";
import { useUserStore } from "@/stores/user";
import { usePageHeaderStore } from "@/stores/pageHeader";
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const pageHeaderStore = usePageHeaderStore();
const { title: dynamicTitle } = storeToRefs(pageHeaderStore);
const title = computed(() => {
if (dynamicTitle.value) return dynamicTitle.value;
const key = route.meta.titleKey;
return key ? t(key) : t("common.appName");
});
const goBack = () => {
const backRoute = route.meta.getBackRoute?.(route);
if (backRoute) {
router.push(backRoute);
}
};
</script>
<template>
<div class="navbar bg-base-100 shadow-lg">
<div class="container min-w-3/4 mx-auto w-full flex items-center gap-4">
<button v-if="route.meta.getBackRoute" @click="goBack" class="btn btn-primary btn-sm">
<i class="mdi mdi-arrow-left"></i>
{{ t("common.back") }}
</button>
<h1 class="text-xl font-bold">{{ title }}</h1>
<div class="flex-1"></div>
<div v-if="userInfo.isAuthenticated" class="flex items-center gap-2">
<div class="text-sm">
<span class="font-medium">{{ userInfo.username }}</span>
<span v-if="userInfo.isStaff" class="badge badge-warning badge-xs ml-1">{{ t("common.admin") }}</span>
</div>
</div>
<div v-else class="text-sm text-base-content/70">{{ t("common.notLoggedIn") }}</div>
<a href="/api/docs" class="btn btn-xs">{{ t("common.apiDocs") }}</a>
<a v-if="userInfo.isStaff" href="/admin" class="btn btn-xs btn-warning">{{ t("common.adminPanel") }}</a>
</div>
</div>
</template>
+12
View File
@@ -0,0 +1,12 @@
import { createApp } from "vue";
import App from "@/App.vue";
import { router } from "@/router";
import { pinia } from "@/stores";
import { i18n } from "@/i18n";
import "@/style.css";
const app = createApp(App);
app.use(pinia);
app.use(router);
app.use(i18n);
app.mount("#app");
-12
View File
@@ -1,12 +0,0 @@
import { createApp } from 'vue'
import Market from '@/Market.vue'
import { pinia } from '@/stores'
import { i18n } from '@/i18n'
import '@/style.css'
const selector = "#app"
const mountData = document.querySelector<HTMLElement>(selector)
const app = createApp(Market, { ...mountData?.dataset })
app.use(pinia)
app.use(i18n)
app.mount(selector)
-12
View File
@@ -1,12 +0,0 @@
import { createApp } from 'vue'
import Noita from '@/Noita.vue'
import { pinia } from '@/stores'
import { i18n } from '@/i18n'
import '@/style.css'
const selector = "#app"
const mountData = document.querySelector<HTMLElement>(selector)
const app = createApp(Noita, { ...mountData?.dataset })
app.use(pinia)
app.use(i18n)
app.mount(selector)
-13
View File
@@ -1,13 +0,0 @@
import { createApp } from 'vue'
import OpusMagnum from '@/OpusMagnum.vue'
import { pinia } from '@/stores'
import { i18n } from '@/i18n'
import '@/style.css'
// const app = createApp(App)
const selector = "#app"
const mountData = document.querySelector<HTMLElement>(selector)
const app = createApp(OpusMagnum, { ...mountData?.dataset })
app.use(pinia)
app.use(i18n)
app.mount(selector)
+83
View File
@@ -0,0 +1,83 @@
import { createRouter, createWebHistory, type RouteLocationNormalizedLoaded, type RouteLocationRaw } from "vue-router";
import Home from "@/Home.vue";
import Market from "@/Market.vue";
import Noita from "@/Noita.vue";
import OpusMagnum from "@/OpusMagnum.vue";
import Submissions from "@/Submissions.vue";
import SubmissionsSelect from "@/SubmissionsSelect.vue";
import { usePageHeaderStore } from "@/stores/pageHeader";
declare module "vue-router" {
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;
getBackRoute?: (route: RouteLocationNormalizedLoaded) => RouteLocationRaw;
}
}
const eventHome = (route: RouteLocationNormalizedLoaded): RouteLocationRaw => ({
name: "home",
params: { eventPath: route.params.eventPath },
});
export const router = createRouter({
history: createWebHistory("/"),
routes: [
{
path: "/:eventPath/",
name: "home",
component: Home,
props: true,
},
{
path: "/:eventPath/market",
name: "market",
component: Market,
props: true,
meta: { titleKey: "market.header", getBackRoute: eventHome },
},
{
path: "/:eventPath/opus-magnum",
name: "opus-magnum",
component: OpusMagnum,
props: true,
meta: { titleKey: "opusMagnum.header", getBackRoute: eventHome },
},
{
path: "/:eventPath/noita",
name: "noita",
component: Noita,
props: true,
meta: { titleKey: "noita.header", getBackRoute: eventHome },
},
{
path: "/:eventPath/submissions/:gamePath",
name: "submissions-select",
component: SubmissionsSelect,
props: true,
meta: { titleKey: "submissionsSelect.title", getBackRoute: eventHome },
},
{
path: "/:eventPath/submissions/:gamePath/:scopeId",
name: "submissions",
component: Submissions,
props: true,
meta: {
// No static titleKey: Submissions.vue sets a dynamic
// "{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 };
});
-12
View File
@@ -1,12 +0,0 @@
import { createApp } from 'vue'
import Submissions from '@/Submissions.vue'
import { pinia } from '@/stores'
import { i18n } from '@/i18n'
import '@/style.css'
const selector = "#app"
const mountData = document.querySelector<HTMLElement>(selector)
const app = createApp(Submissions, { ...mountData?.dataset })
app.use(pinia)
app.use(i18n)
app.mount(selector)
@@ -1,12 +0,0 @@
import { createApp } from 'vue'
import SubmissionsSelect from '@/SubmissionsSelect.vue'
import { i18n } from '@/i18n'
import '@/style.css'
const selector = "#app"
const mountData = document.querySelector<HTMLElement>(selector)
const scopesEl = document.getElementById('scopes-data')
const scopes = scopesEl ? JSON.parse(scopesEl.textContent || '[]') : []
const app = createApp(SubmissionsSelect, { ...mountData?.dataset, scopes })
app.use(i18n)
app.mount(selector)
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})
def create_submission(
request: HttpRequest,
+2 -21
View File
@@ -39,18 +39,7 @@ def submission_home(request: HttpRequest, event_path: str, game_path: str):
if not scopes:
raise Http404
return render(
request,
"submissions_select.html",
{
"event": event,
"game": game,
"scopes_data": [
{"id": str(scope.id), "name": scope.name}
for scope in scopes.order_by("name")
],
},
)
return render(request, "app.html")
@login_required
@@ -73,12 +62,4 @@ def submission_view(
):
raise Http404
return render(
request,
"submissions.html",
{
"event": event,
"scope": scope,
"game": scope.event_game.game,
},
)
return render(request, "app.html")
@@ -3,9 +3,9 @@
{% load django_vite %}
{% block head %}
{% vite_asset 'src/noita.ts' %}
{% vite_asset 'src/main.ts' %}
{% endblock %}
{% block content %}
<div id="app" data-event-path="{{ event.path }}"></div>
<div id="app"></div>
{% endblock %}
+2 -1
View File
@@ -1,9 +1,10 @@
<!doctype html>
{% load django_vite %}
{% load static %}
<html lang="{{ LANGUAGE_CODE }}" data-theme="dim">
<head>
<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" />
<title>{% block title %}PolyLAN Submitter{% endblock %}</title>
{% if debug %}{% vite_hmr_client %}{% endif %}
-11
View File
@@ -1,11 +0,0 @@
{% extends "base.html" %}
{% load django_vite %}
{% block head %}
{% vite_asset 'src/home.ts' %}
{% endblock %}
{% block content %}
<div id="app" data-event-path="{{ event.path }}" data-market-enabled="{{ market_enabled|lower }}"></div>
{% endblock %}
-11
View File
@@ -1,11 +0,0 @@
{% extends "base.html" %}
{% load django_vite %}
{% block head %}
{% vite_asset 'src/market.ts' %}
{% endblock %}
{% block content %}
<div id="app" data-event-path="{{ event.path }}"></div>
{% endblock %}
@@ -1,11 +0,0 @@
{% extends "base.html" %}
{% load django_vite %}
{% block head %}
{% vite_asset 'src/opus-magnum.ts' %}
{% endblock %}
{% block content %}
<div id="app" data-event-path="{{ event.path }}" data-collection-title="{{ collection.title }}" data-collection-url="{{ collection.url }}" data-collection-description="{{ collection.description }}"></div>
{% endblock %}
@@ -1,11 +0,0 @@
{% extends "base.html" %}
{% load django_vite %}
{% block head %}
{% vite_asset 'src/submissions.ts' %}
{% endblock %}
{% block content %}
<div id="app" data-event-path="{{ event.path }}" data-scope-id="{{ scope.id }}" data-scope-name="{{ scope.name }}" data-game-name="{{ game.name }}" data-game-image="{{ game.image |default:'' }}" data-game-steam-id="{{ game.steam_app_id|default:'' }}" data-game-path="{{ game.path }}"></div>
{% endblock %}
@@ -1,12 +0,0 @@
{% extends "base.html" %}
{% load django_vite %}
{% block head %}
{% vite_asset 'src/submissions_select.ts' %}
{% endblock %}
{% block content %}
{{ scopes_data|json_script:"scopes-data" }}
<div id="app" data-event-path="{{ event.path }}" data-game-path="{{ game.path }}" data-game-name="{{ game.name }}"></div>
{% endblock %}
+2 -6
View File
@@ -29,12 +29,8 @@ export default defineConfig({
rollupOptions: {
input:
{
opus_magnum: resolve('./src/opus-magnum.ts'),
home: resolve('./src/home.ts'),
noita: resolve('./src/noita.ts'),
market: resolve('./src/market.ts'),
submissions: resolve('./src/submissions.ts'),
submissions_select: resolve('./src/submissions_select.ts'),
main: resolve('./src/main.ts'),
error: resolve('./src/error.ts'),
}
}
},