diff --git a/polylan_submitter/src/Market.vue b/polylan_submitter/src/Market.vue
index 3597c28..8305304 100644
--- a/polylan_submitter/src/Market.vue
+++ b/polylan_submitter/src/Market.vue
@@ -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(() => {
-
-
-
-
-
-
{{ t("market.header") }}
-
-
-
-
-
+
diff --git a/polylan_submitter/src/Noita.vue b/polylan_submitter/src/Noita.vue
index 3d590a0..a77e550 100644
--- a/polylan_submitter/src/Noita.vue
+++ b/polylan_submitter/src/Noita.vue
@@ -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(() => {
-
-
-
-
+
diff --git a/polylan_submitter/src/OpusMagnum.vue b/polylan_submitter/src/OpusMagnum.vue
index 947d1de..e924273 100644
--- a/polylan_submitter/src/OpusMagnum.vue
+++ b/polylan_submitter/src/OpusMagnum.vue
@@ -139,37 +139,10 @@ const findPuzzleByName = (ocrPuzzleName: string) => {
const reloadPage = () => {
window.location.reload();
};
-
-const goHome = () => {
- window.location.href = `/${props.eventPath}/`;
-};
-
-
-
-
-
-
{{ t("opusMagnum.header") }}
-
-
-
-
-
+
diff --git a/polylan_submitter/src/Submissions.vue b/polylan_submitter/src/Submissions.vue
index 78bfca2..a339581 100644
--- a/polylan_submitter/src/Submissions.vue
+++ b/polylan_submitter/src/Submissions.vue
@@ -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;
@@ -520,32 +516,17 @@ onUnmounted(() => {
-
-
-
-
+
+
+
+
+
+ {{ wsConnected ? t("submissions.live") : t("submissions.offline") }}
+
+
+
diff --git a/polylan_submitter/src/SubmissionsSelect.vue b/polylan_submitter/src/SubmissionsSelect.vue
index 66ef0a3..8b27c72 100644
--- a/polylan_submitter/src/SubmissionsSelect.vue
+++ b/polylan_submitter/src/SubmissionsSelect.vue
@@ -26,14 +26,10 @@ useTitle(computed(() => t("submissionsSelect.title")));
const goToScope = (scopeId: string) => {
window.location.href = `/${props.eventPath}/submissions/${props.gamePath}/${scopeId}`;
};
-
-const goHome = () => {
- window.location.href = `/${props.eventPath}`;
-};
-
+
{{ t("submissionsSelect.title") }}
@@ -48,13 +44,6 @@ const goHome = () => {
{{ scope.name }}
-
-
-
-
diff --git a/polylan_submitter/src/bootstrap.ts b/polylan_submitter/src/bootstrap.ts
new file mode 100644
index 0000000..156b900
--- /dev/null
+++ b/polylan_submitter/src/bootstrap.ts
@@ -0,0 +1,11 @@
+// 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 = (() => {
+ const el = document.getElementById("bootstrap-data");
+ if (!el?.textContent) return {};
+ try {
+ return JSON.parse(el.textContent);
+ } catch {
+ return {};
+ }
+})();
diff --git a/polylan_submitter/src/home.ts b/polylan_submitter/src/home.ts
deleted file mode 100644
index 60a24c6..0000000
--- a/polylan_submitter/src/home.ts
+++ /dev/null
@@ -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(selector)
-const app = createApp(Home, { ...mountData?.dataset })
-app.use(i18n)
-app.mount(selector)
diff --git a/polylan_submitter/src/layout/AppFooter.vue b/polylan_submitter/src/layout/AppFooter.vue
new file mode 100644
index 0000000..a568156
--- /dev/null
+++ b/polylan_submitter/src/layout/AppFooter.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/polylan_submitter/src/layout/AppHeader.vue b/polylan_submitter/src/layout/AppHeader.vue
new file mode 100644
index 0000000..4490b13
--- /dev/null
+++ b/polylan_submitter/src/layout/AppHeader.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
diff --git a/polylan_submitter/src/main.ts b/polylan_submitter/src/main.ts
new file mode 100644
index 0000000..b1cf6b5
--- /dev/null
+++ b/polylan_submitter/src/main.ts
@@ -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");
diff --git a/polylan_submitter/src/market.ts b/polylan_submitter/src/market.ts
deleted file mode 100644
index 0b6005b..0000000
--- a/polylan_submitter/src/market.ts
+++ /dev/null
@@ -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(selector)
-const app = createApp(Market, { ...mountData?.dataset })
-app.use(pinia)
-app.use(i18n)
-app.mount(selector)
diff --git a/polylan_submitter/src/noita.ts b/polylan_submitter/src/noita.ts
deleted file mode 100644
index 34035b0..0000000
--- a/polylan_submitter/src/noita.ts
+++ /dev/null
@@ -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(selector)
-const app = createApp(Noita, { ...mountData?.dataset })
-app.use(pinia)
-app.use(i18n)
-app.mount(selector)
diff --git a/polylan_submitter/src/opus-magnum.ts b/polylan_submitter/src/opus-magnum.ts
deleted file mode 100644
index 86ffd67..0000000
--- a/polylan_submitter/src/opus-magnum.ts
+++ /dev/null
@@ -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(selector)
-const app = createApp(OpusMagnum, { ...mountData?.dataset })
-app.use(pinia)
-app.use(i18n)
-app.mount(selector)
diff --git a/polylan_submitter/src/router/index.ts b/polylan_submitter/src/router/index.ts
new file mode 100644
index 0000000..82c068c
--- /dev/null
+++ b/polylan_submitter/src/router/index.ts
@@ -0,0 +1,75 @@
+import { createRouter, createWebHistory, type RouteLocationNormalizedLoaded } 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 { bootstrap } from "@/bootstrap";
+
+declare module "vue-router" {
+ interface RouteMeta {
+ titleKey?: string;
+ getBackPath?: (route: RouteLocationNormalizedLoaded) => string;
+ }
+}
+
+// Route params (eventPath, gamePath, scopeId, ...) come from the URL;
+// page-specific extras Django can't express as URL segments (collection
+// title, scope name, ...) come from the bootstrap JSON island instead.
+const withBootstrap = (route: RouteLocationNormalizedLoaded) => ({
+ ...route.params,
+ ...bootstrap,
+});
+
+const eventHome = (route: RouteLocationNormalizedLoaded) => `/${route.params.eventPath}/`;
+
+export const router = createRouter({
+ history: createWebHistory("/"),
+ routes: [
+ {
+ path: "/:eventPath/",
+ name: "home",
+ component: Home,
+ props: withBootstrap,
+ },
+ {
+ path: "/:eventPath/market",
+ name: "market",
+ component: Market,
+ props: withBootstrap,
+ meta: { titleKey: "market.header", getBackPath: eventHome },
+ },
+ {
+ path: "/:eventPath/opus-magnum",
+ name: "opus-magnum",
+ component: OpusMagnum,
+ props: withBootstrap,
+ meta: { titleKey: "opusMagnum.header", getBackPath: eventHome },
+ },
+ {
+ path: "/:eventPath/noita",
+ name: "noita",
+ component: Noita,
+ props: withBootstrap,
+ meta: { titleKey: "noita.header", getBackPath: eventHome },
+ },
+ {
+ path: "/:eventPath/submissions/:gamePath",
+ name: "submissions-select",
+ component: SubmissionsSelect,
+ props: withBootstrap,
+ meta: { titleKey: "submissionsSelect.title", getBackPath: eventHome },
+ },
+ {
+ path: "/:eventPath/submissions/:gamePath/:scopeId",
+ name: "submissions",
+ component: Submissions,
+ props: withBootstrap,
+ meta: {
+ titleKey: "submissions.pageTitle",
+ getBackPath: (route) => `/${route.params.eventPath}/submissions/${route.params.gamePath}`,
+ },
+ },
+ ],
+});
diff --git a/polylan_submitter/src/submissions.ts b/polylan_submitter/src/submissions.ts
deleted file mode 100644
index a195362..0000000
--- a/polylan_submitter/src/submissions.ts
+++ /dev/null
@@ -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(selector)
-const app = createApp(Submissions, { ...mountData?.dataset })
-app.use(pinia)
-app.use(i18n)
-app.mount(selector)
diff --git a/polylan_submitter/src/submissions_select.ts b/polylan_submitter/src/submissions_select.ts
deleted file mode 100644
index 8b1e961..0000000
--- a/polylan_submitter/src/submissions_select.ts
+++ /dev/null
@@ -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(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)
diff --git a/polylan_submitter/submissions/views.py b/polylan_submitter/submissions/views.py
index 7d96328..71a2bd0 100644
--- a/polylan_submitter/submissions/views.py
+++ b/polylan_submitter/submissions/views.py
@@ -41,14 +41,15 @@ def submission_home(request: HttpRequest, event_path: str, game_path: str):
return render(
request,
- "submissions_select.html",
+ "app.html",
{
- "event": event,
- "game": game,
- "scopes_data": [
- {"id": str(scope.id), "name": scope.name}
- for scope in scopes.order_by("name")
- ],
+ "bootstrap": {
+ "gameName": game.name,
+ "scopes": [
+ {"id": str(scope.id), "name": scope.name}
+ for scope in scopes.order_by("name")
+ ],
+ },
},
)
@@ -73,12 +74,17 @@ def submission_view(
):
raise Http404
+ game = scope.event_game.game
return render(
request,
- "submissions.html",
+ "app.html",
{
- "event": event,
- "scope": scope,
- "game": scope.event_game.game,
+ "bootstrap": {
+ "scopeName": scope.name,
+ "gameName": game.name,
+ "gamePath": game.path,
+ "gameImage": game.image or "",
+ "gameSteamId": str(game.steam_app_id or ""),
+ },
},
)
diff --git a/polylan_submitter/templates/market.html b/polylan_submitter/templates/app.html
similarity index 52%
rename from polylan_submitter/templates/market.html
rename to polylan_submitter/templates/app.html
index 467bc50..4aa4517 100644
--- a/polylan_submitter/templates/market.html
+++ b/polylan_submitter/templates/app.html
@@ -3,9 +3,10 @@
{% load django_vite %}
{% block head %}
- {% vite_asset 'src/market.ts' %}
+ {% vite_asset 'src/main.ts' %}
{% endblock %}
{% block content %}
-
+ {{ bootstrap|json_script:"bootstrap-data" }}
+
{% endblock %}
diff --git a/polylan_submitter/templates/home.html b/polylan_submitter/templates/home.html
deleted file mode 100644
index bdf3c63..0000000
--- a/polylan_submitter/templates/home.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends "base.html" %}
-
-{% load django_vite %}
-
-{% block head %}
- {% vite_asset 'src/home.ts' %}
-{% endblock %}
-
-{% block content %}
-
-{% endblock %}
diff --git a/polylan_submitter/templates/noita.html b/polylan_submitter/templates/noita.html
deleted file mode 100644
index 761d607..0000000
--- a/polylan_submitter/templates/noita.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends "base.html" %}
-
-{% load django_vite %}
-
-{% block head %}
- {% vite_asset 'src/noita.ts' %}
-{% endblock %}
-
-{% block content %}
-
-{% endblock %}
diff --git a/polylan_submitter/templates/opus-magnum.html b/polylan_submitter/templates/opus-magnum.html
deleted file mode 100644
index 28735ee..0000000
--- a/polylan_submitter/templates/opus-magnum.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends "base.html" %}
-
-{% load django_vite %}
-
-{% block head %}
- {% vite_asset 'src/opus-magnum.ts' %}
-{% endblock %}
-
-{% block content %}
-
-{% endblock %}
diff --git a/polylan_submitter/templates/submissions.html b/polylan_submitter/templates/submissions.html
deleted file mode 100644
index c764543..0000000
--- a/polylan_submitter/templates/submissions.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends "base.html" %}
-
-{% load django_vite %}
-
-{% block head %}
- {% vite_asset 'src/submissions.ts' %}
-{% endblock %}
-
-{% block content %}
-
-{% endblock %}
diff --git a/polylan_submitter/templates/submissions_select.html b/polylan_submitter/templates/submissions_select.html
deleted file mode 100644
index 1dbb421..0000000
--- a/polylan_submitter/templates/submissions_select.html
+++ /dev/null
@@ -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" }}
-
-{% endblock %}
diff --git a/polylan_submitter/vite.config.ts b/polylan_submitter/vite.config.ts
index e1761e0..811bbcf 100644
--- a/polylan_submitter/vite.config.ts
+++ b/polylan_submitter/vite.config.ts
@@ -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'),
}
}
},