fix(submissions): top-header icon for live connection (ws)
This commit is contained in:
@@ -477,7 +477,7 @@ const loadUserSubmissions = async () => {
|
|||||||
|
|
||||||
// Match over (or user moved off it): back to the picker, which auto-enters
|
// Match over (or user moved off it): back to the picker, which auto-enters
|
||||||
// the next match when there's only one, so players follow along without a refresh.
|
// the next match when there's only one, so players follow along without a refresh.
|
||||||
const { isConnected: wsConnected } = useSubmissionsWebSocket({
|
useSubmissionsWebSocket({
|
||||||
userSubmissions,
|
userSubmissions,
|
||||||
onScopeUpdate: (scope) => {
|
onScopeUpdate: (scope) => {
|
||||||
if (scope.id !== props.scopeId || (scope.is_active && scope.assigned)) return;
|
if (scope.id !== props.scopeId || (scope.is_active && scope.assigned)) return;
|
||||||
@@ -557,14 +557,6 @@ onUnmounted(() => {
|
|||||||
<div class="flex-1 bg-base-200">
|
<div class="flex-1 bg-base-200">
|
||||||
<!-- 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">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useWebSocket } from '@vueuse/core'
|
import { useWebSocket } from '@vueuse/core'
|
||||||
|
import { usePageHeaderStore } from '@/stores/pageHeader'
|
||||||
|
|
||||||
interface SubmissionUpdate {
|
interface SubmissionUpdate {
|
||||||
id: string
|
id: string
|
||||||
@@ -107,6 +108,10 @@ export function useSubmissionsWebSocket({ userSubmissions, onScopeUpdate }: Opti
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Live badge in AppHeader (reset by the router on the next navigation)
|
||||||
|
const pageHeaderStore = usePageHeaderStore()
|
||||||
|
watch(isConnected, (connected) => pageHeaderStore.setLive(connected), { immediate: true })
|
||||||
|
|
||||||
const handleSubmissionUpdate = (submission: SubmissionUpdate) => {
|
const handleSubmissionUpdate = (submission: SubmissionUpdate) => {
|
||||||
const existingIndex = userSubmissions.value.findIndex((s: any) => s.id === submission.id)
|
const existingIndex = userSubmissions.value.findIndex((s: any) => s.id === submission.id)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const router = useRouter();
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const userInfo = useUser();
|
const userInfo = useUser();
|
||||||
const pageHeaderStore = usePageHeaderStore();
|
const pageHeaderStore = usePageHeaderStore();
|
||||||
const { title: dynamicTitle, backRoute: dynamicBackRoute } = storeToRefs(pageHeaderStore);
|
const { title: dynamicTitle, backRoute: dynamicBackRoute, live } = storeToRefs(pageHeaderStore);
|
||||||
|
|
||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
if (dynamicTitle.value) return dynamicTitle.value;
|
if (dynamicTitle.value) return dynamicTitle.value;
|
||||||
@@ -29,22 +29,61 @@ const goBack = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="navbar bg-base-100 shadow-lg">
|
<div class="navbar bg-base-100 shadow-lg">
|
||||||
<div class="container min-w-3/4 mx-auto w-full flex items-center gap-4">
|
<!-- 3 columns so the live badge stays centered whatever the sides' width -->
|
||||||
<button v-if="route.meta.getBackRoute" @click="goBack" class="btn btn-primary btn-sm">
|
<div class="container min-w-3/4 mx-auto w-full grid grid-cols-[1fr_auto_1fr] items-center gap-4">
|
||||||
<i class="mdi mdi-arrow-left"></i>
|
<div class="flex items-center gap-4 min-w-0">
|
||||||
{{ t("common.back") }}
|
<button v-if="route.meta.getBackRoute" @click="goBack" class="btn btn-primary btn-sm">
|
||||||
</button>
|
<i class="mdi mdi-arrow-left"></i>
|
||||||
<h1 class="text-xl font-bold">{{ title }}</h1>
|
{{ t("common.back") }}
|
||||||
<div class="flex-1"></div>
|
</button>
|
||||||
<div v-if="userInfo.isAuthenticated" class="flex items-center gap-2">
|
<h1 class="text-xl font-bold truncate">{{ title }}</h1>
|
||||||
<div class="text-sm">
|
</div>
|
||||||
<span class="font-medium">{{ userInfo.username }}</span>
|
<div>
|
||||||
<span v-if="userInfo.isStaff" class="badge badge-warning badge-xs ml-1">{{ t("common.admin") }}</span>
|
<!-- Hover (or tap, on mobile) to explain what live means for the player -->
|
||||||
|
<div v-if="live !== null" class="dropdown dropdown-hover dropdown-center">
|
||||||
|
<div tabindex="0" role="button" :class="['badge cursor-help', live ? 'badge-success' : 'badge-warning']">
|
||||||
|
<i :class="['mdi mr-1', live ? 'mdi-wifi' : 'mdi-wifi-off']"></i>
|
||||||
|
{{ live ? t("submissions.live") : t("submissions.offline") }}
|
||||||
|
<i class="mdi mdi-information-outline ml-1 opacity-70"></i>
|
||||||
|
</div>
|
||||||
|
<div tabindex="0" class="dropdown-content card card-sm bg-base-200 shadow-xl z-50 w-80 mt-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<template v-if="live">
|
||||||
|
<h3 class="card-title text-base">
|
||||||
|
<i class="mdi mdi-wifi text-success"></i>
|
||||||
|
{{ t("liveStatus.liveTitle") }}
|
||||||
|
</h3>
|
||||||
|
<p>{{ t("liveStatus.liveIntro") }}</p>
|
||||||
|
<ul class="list-disc pl-5 space-y-1">
|
||||||
|
<li>{{ t("liveStatus.liveSubmissions") }}</li>
|
||||||
|
<li>{{ t("liveStatus.liveMatches") }}</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-base-content/70">{{ t("liveStatus.liveNoRefresh") }}</p>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<h3 class="card-title text-base">
|
||||||
|
<i class="mdi mdi-wifi-off text-warning"></i>
|
||||||
|
{{ t("liveStatus.offlineTitle") }}
|
||||||
|
</h3>
|
||||||
|
<p>{{ t("liveStatus.offlineIntro") }}</p>
|
||||||
|
<p>{{ t("liveStatus.offlineReconnect") }}</p>
|
||||||
|
<p class="text-base-content/70">{{ t("liveStatus.offlineRefresh") }}</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-sm text-base-content/70">{{ t("common.notLoggedIn") }}</div>
|
<div class="flex items-center justify-end gap-4">
|
||||||
<a href="/api/docs" class="btn btn-xs">{{ t("common.apiDocs") }}</a>
|
<div v-if="userInfo.isAuthenticated" class="flex items-center gap-2">
|
||||||
<a v-if="userInfo.isStaff" href="/admin" class="btn btn-xs btn-warning">{{ t("common.adminPanel") }}</a>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -260,6 +260,17 @@
|
|||||||
"notRunning": "Sync wasn't running.",
|
"notRunning": "Sync wasn't running.",
|
||||||
"stopFailed": "Failed to stop the sync."
|
"stopFailed": "Failed to stop the sync."
|
||||||
},
|
},
|
||||||
|
"liveStatus": {
|
||||||
|
"liveTitle": "You're connected live",
|
||||||
|
"liveIntro": "This page updates on its own, you don't need to refresh it:",
|
||||||
|
"liveSubmissions": "Your submissions update as they're processed (uploaded files, text read from your screenshots).",
|
||||||
|
"liveMatches": "When your match ends, you're sent back to the match list, and straight into your next match if you have only one.",
|
||||||
|
"liveNoRefresh": "Just keep this page open between matches.",
|
||||||
|
"offlineTitle": "Live connection lost",
|
||||||
|
"offlineIntro": "The page can't get updates right now, so it won't move you to your next match or show processing results on its own.",
|
||||||
|
"offlineReconnect": "It reconnects automatically, usually within a few seconds.",
|
||||||
|
"offlineRefresh": "Still offline after a while? Refresh the page. Anything you already submitted is saved."
|
||||||
|
},
|
||||||
"errorPage": {
|
"errorPage": {
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"title": "Page not found",
|
"title": "Page not found",
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ export const usePageHeaderStore = defineStore("pageHeader", () => {
|
|||||||
// Overrides route.meta.getBackRoute when the right target depends on
|
// Overrides route.meta.getBackRoute when the right target depends on
|
||||||
// fetched data (e.g. skip the scope picker when it would auto-redirect).
|
// fetched data (e.g. skip the scope picker when it would auto-redirect).
|
||||||
const backRoute = ref<RouteLocationRaw | null>(null);
|
const backRoute = ref<RouteLocationRaw | null>(null);
|
||||||
|
// Live (websocket) connection state of the page, shown mid-header; null
|
||||||
|
// when the page has no live connection (no badge at all).
|
||||||
|
const live = ref<boolean | null>(null);
|
||||||
|
|
||||||
const setTitle = (value: string) => {
|
const setTitle = (value: string) => {
|
||||||
title.value = value;
|
title.value = value;
|
||||||
@@ -20,10 +23,15 @@ export const usePageHeaderStore = defineStore("pageHeader", () => {
|
|||||||
backRoute.value = value;
|
backRoute.value = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setLive = (value: boolean | null) => {
|
||||||
|
live.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
title.value = "";
|
title.value = "";
|
||||||
backRoute.value = null;
|
backRoute.value = null;
|
||||||
|
live.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return { title, backRoute, setTitle, setBackRoute, clear };
|
return { title, backRoute, live, setTitle, setBackRoute, setLive, clear };
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user