feat: "spa", but more like single entry-point
This commit is contained in:
@@ -7,5 +7,5 @@ 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):
|
||||||
event = get_object_or_404(Event, path=event_path)
|
get_object_or_404(Event, path=event_path)
|
||||||
return render(request, "market.html", {"event": event})
|
return render(request, "app.html", {"bootstrap": {}})
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
"tesseract.js": "^5.1.1",
|
"tesseract.js": "^5.1.1",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-i18n": "^11.4.12"
|
"vue-i18n": "^11.4.12",
|
||||||
|
"vue-router": "4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "^0.97.2",
|
"@hey-api/openapi-ts": "^0.97.2",
|
||||||
|
|||||||
Generated
+13
@@ -38,6 +38,9 @@ importers:
|
|||||||
vue-i18n:
|
vue-i18n:
|
||||||
specifier: ^11.4.12
|
specifier: ^11.4.12
|
||||||
version: 11.4.12(vue@3.5.22(typescript@5.9.3))
|
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:
|
devDependencies:
|
||||||
'@hey-api/openapi-ts':
|
'@hey-api/openapi-ts':
|
||||||
specifier: ^0.97.2
|
specifier: ^0.97.2
|
||||||
@@ -1075,6 +1078,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.0.0
|
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:
|
vue-tsc@3.1.2:
|
||||||
resolution: {integrity: sha512-3fd4DY0rFczs5f+VB3OhcLU83V6+3Puj2yLBe0Ak65k7ERk+STVNKaOAi0EBo6Lc15UiJB6LzU6Mxy4+h/pKew==}
|
resolution: {integrity: sha512-3fd4DY0rFczs5f+VB3OhcLU83V6+3Puj2yLBe0Ak65k7ERk+STVNKaOAi0EBo6Lc15UiJB6LzU6Mxy4+h/pKew==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -1962,6 +1970,11 @@ snapshots:
|
|||||||
'@vue/devtools-api': 6.6.4
|
'@vue/devtools-api': 6.6.4
|
||||||
vue: 3.5.22(typescript@5.9.3)
|
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):
|
vue-tsc@3.1.2(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@volar/typescript': 2.4.23
|
'@volar/typescript': 2.4.23
|
||||||
|
|||||||
@@ -43,14 +43,15 @@ def home(request: HttpRequest):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def event_home(request: HttpRequest, event_path: str):
|
def event_home(request: HttpRequest, event_path: str):
|
||||||
event = get_object_or_404(Event, path=event_path)
|
get_object_or_404(Event, path=event_path)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"home.html",
|
"app.html",
|
||||||
{
|
{
|
||||||
"event": event,
|
"bootstrap": {
|
||||||
"market_enabled": settings.MARKET_ENABLED,
|
"marketEnabled": settings.MARKET_ENABLED,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,15 +62,17 @@ def opus_magnum_home(request: HttpRequest, event_path: str):
|
|||||||
from opus_magnum.models import SteamCollection
|
from opus_magnum.models import SteamCollection
|
||||||
|
|
||||||
event = get_object_or_404(Event, path=event_path)
|
event = get_object_or_404(Event, path=event_path)
|
||||||
|
collection = SteamCollection.objects.filter(is_active=True, event=event).last()
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"opus-magnum.html",
|
"app.html",
|
||||||
{
|
{
|
||||||
"event": event,
|
"bootstrap": {
|
||||||
"collection": SteamCollection.objects.filter(
|
"collectionTitle": collection.title if collection else "",
|
||||||
is_active=True, event=event
|
"collectionUrl": collection.url if collection else "",
|
||||||
).last(),
|
"collectionDescription": collection.description if collection else "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,8 +80,8 @@ def opus_magnum_home(request: HttpRequest, event_path: str):
|
|||||||
@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):
|
||||||
event = get_object_or_404(Event, path=event_path)
|
get_object_or_404(Event, path=event_path)
|
||||||
return render(request, "noita.html", {"event": event})
|
return render(request, "app.html", {"bootstrap": {}})
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -59,7 +59,7 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<div class="w-full max-w-6xl">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="text-center mb-12">
|
<div class="text-center mb-12">
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ useTitle(computed(() => t("market.pageTitle")));
|
|||||||
const marketStore = useMarketStore();
|
const marketStore = useMarketStore();
|
||||||
const { markets, userInfo, isLoading } = storeToRefs(marketStore);
|
const { markets, userInfo, isLoading } = storeToRefs(marketStore);
|
||||||
|
|
||||||
const goHome = () => {
|
|
||||||
window.location.href = `/${props.eventPath}/`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const reloadPage = async () => {
|
const reloadPage = async () => {
|
||||||
await marketStore.refreshPage(props.eventPath);
|
await marketStore.refreshPage(props.eventPath);
|
||||||
};
|
};
|
||||||
@@ -35,30 +31,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-base-200">
|
<div class="flex-1 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>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="container min-w-3/4 mx-auto px-4 py-8">
|
<div class="container min-w-3/4 mx-auto px-4 py-8">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|||||||
@@ -155,10 +155,6 @@ const submitRun = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const goHome = () => {
|
|
||||||
window.location.href = `/${props.eventPath}/`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearCache = async () => {
|
const clearCache = async () => {
|
||||||
try {
|
try {
|
||||||
await noitaStore.clearCache(props.eventPath);
|
await noitaStore.clearCache(props.eventPath);
|
||||||
@@ -175,21 +171,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-base-200">
|
<div class="flex-1 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>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="container min-w-3/4 mx-auto px-4 py-8">
|
<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">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
|||||||
@@ -139,37 +139,10 @@ const findPuzzleByName = (ocrPuzzleName: string) => {
|
|||||||
const reloadPage = () => {
|
const reloadPage = () => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const goHome = () => {
|
|
||||||
window.location.href = `/${props.eventPath}/`;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-base-200">
|
<div class="flex-1 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>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="container min-w-3/4 mx-auto px-4 py-8">
|
<div class="container min-w-3/4 mx-auto px-4 py-8">
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
|
|||||||
@@ -430,10 +430,6 @@ const submitSubmission = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const goHome = () => {
|
|
||||||
window.location.href = `/${props.eventPath}/submissions/${props.gamePath}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatFileSize = (bytes: number) => {
|
const formatFileSize = (bytes: number) => {
|
||||||
if (bytes === 0) return "0 Bytes";
|
if (bytes === 0) return "0 Bytes";
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
@@ -520,32 +516,17 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-base-200">
|
<div class="flex-1 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>
|
|
||||||
|
|
||||||
<!-- WebSocket Status Indicator -->
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<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 -->
|
<!-- Main Content -->
|
||||||
<div class="container min-w-3/4 mx-auto px-4 py-8">
|
<div class="container min-w-3/4 mx-auto px-4 py-8">
|
||||||
|
<!-- WebSocket Status Indicator -->
|
||||||
|
<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>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
<!-- Right Column: Upload -->
|
<!-- Right Column: Upload -->
|
||||||
<div class="lg:col-span-1 flex flex-col gap-8 order-1 lg:order-2">
|
<div class="lg:col-span-1 flex flex-col gap-8 order-1 lg:order-2">
|
||||||
|
|||||||
@@ -26,14 +26,10 @@ useTitle(computed(() => t("submissionsSelect.title")));
|
|||||||
const goToScope = (scopeId: string) => {
|
const goToScope = (scopeId: string) => {
|
||||||
window.location.href = `/${props.eventPath}/submissions/${props.gamePath}/${scopeId}`;
|
window.location.href = `/${props.eventPath}/submissions/${props.gamePath}/${scopeId}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const goHome = () => {
|
|
||||||
window.location.href = `/${props.eventPath}`;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen 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 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>
|
||||||
@@ -48,13 +44,6 @@ const goHome = () => {
|
|||||||
{{ scope.name }}
|
{{ scope.name }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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<string, unknown> = (() => {
|
||||||
|
const el = document.getElementById("bootstrap-data");
|
||||||
|
if (!el?.textContent) return {};
|
||||||
|
try {
|
||||||
|
return JSON.parse(el.textContent);
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -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">
|
||||||
|
© {{ year }} {{ t("common.appName") }}
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
import { bootstrap } from "@/bootstrap";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { userInfo } = storeToRefs(userStore);
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
const key = route.meta.titleKey;
|
||||||
|
if (!key) return 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 = () => {
|
||||||
|
if (backPath.value) {
|
||||||
|
window.location.href = backPath.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</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="backPath" @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>
|
||||||
@@ -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");
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -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)
|
|
||||||
@@ -41,14 +41,15 @@ def submission_home(request: HttpRequest, event_path: str, game_path: str):
|
|||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"submissions_select.html",
|
"app.html",
|
||||||
{
|
{
|
||||||
"event": event,
|
"bootstrap": {
|
||||||
"game": game,
|
"gameName": game.name,
|
||||||
"scopes_data": [
|
"scopes": [
|
||||||
{"id": str(scope.id), "name": scope.name}
|
{"id": str(scope.id), "name": scope.name}
|
||||||
for scope in scopes.order_by("name")
|
for scope in scopes.order_by("name")
|
||||||
],
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,12 +74,17 @@ def submission_view(
|
|||||||
):
|
):
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
|
game = scope.event_game.game
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"submissions.html",
|
"app.html",
|
||||||
{
|
{
|
||||||
"event": event,
|
"bootstrap": {
|
||||||
"scope": scope,
|
"scopeName": scope.name,
|
||||||
"game": scope.event_game.game,
|
"gameName": game.name,
|
||||||
|
"gamePath": game.path,
|
||||||
|
"gameImage": game.image or "",
|
||||||
|
"gameSteamId": str(game.steam_app_id or ""),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
{% load django_vite %}
|
{% load django_vite %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% vite_asset 'src/market.ts' %}
|
{% vite_asset 'src/main.ts' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="app" data-event-path="{{ event.path }}"></div>
|
{{ bootstrap|json_script:"bootstrap-data" }}
|
||||||
|
<div id="app"></div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -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 %}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load django_vite %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
{% vite_asset 'src/noita.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 %}
|
|
||||||
@@ -29,12 +29,8 @@ export default defineConfig({
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input:
|
input:
|
||||||
{
|
{
|
||||||
opus_magnum: resolve('./src/opus-magnum.ts'),
|
main: resolve('./src/main.ts'),
|
||||||
home: resolve('./src/home.ts'),
|
error: resolve('./src/error.ts'),
|
||||||
noita: resolve('./src/noita.ts'),
|
|
||||||
market: resolve('./src/market.ts'),
|
|
||||||
submissions: resolve('./src/submissions.ts'),
|
|
||||||
submissions_select: resolve('./src/submissions_select.ts'),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user