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
|
||||
// the next match when there's only one, so players follow along without a refresh.
|
||||
const { isConnected: wsConnected } = useSubmissionsWebSocket({
|
||||
useSubmissionsWebSocket({
|
||||
userSubmissions,
|
||||
onScopeUpdate: (scope) => {
|
||||
if (scope.id !== props.scopeId || (scope.is_active && scope.assigned)) return;
|
||||
@@ -557,14 +557,6 @@ onUnmounted(() => {
|
||||
<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 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">
|
||||
<!-- Right Column: Upload -->
|
||||
<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 { usePageHeaderStore } from '@/stores/pageHeader'
|
||||
|
||||
interface SubmissionUpdate {
|
||||
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 existingIndex = userSubmissions.value.findIndex((s: any) => s.id === submission.id)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const userInfo = useUser();
|
||||
const pageHeaderStore = usePageHeaderStore();
|
||||
const { title: dynamicTitle, backRoute: dynamicBackRoute } = storeToRefs(pageHeaderStore);
|
||||
const { title: dynamicTitle, backRoute: dynamicBackRoute, live } = storeToRefs(pageHeaderStore);
|
||||
|
||||
const title = computed(() => {
|
||||
if (dynamicTitle.value) return dynamicTitle.value;
|
||||
@@ -29,22 +29,61 @@ const goBack = () => {
|
||||
|
||||
<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>
|
||||
<!-- 3 columns so the live badge stays centered whatever the sides' width -->
|
||||
<div class="container min-w-3/4 mx-auto w-full grid grid-cols-[1fr_auto_1fr] items-center gap-4">
|
||||
<div class="flex items-center gap-4 min-w-0">
|
||||
<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 truncate">{{ title }}</h1>
|
||||
</div>
|
||||
<div>
|
||||
<!-- 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 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 class="flex items-center justify-end gap-4">
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -260,6 +260,17 @@
|
||||
"notRunning": "Sync wasn't running.",
|
||||
"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": {
|
||||
"notFound": {
|
||||
"title": "Page not found",
|
||||
|
||||
@@ -11,6 +11,9 @@ export const usePageHeaderStore = defineStore("pageHeader", () => {
|
||||
// Overrides route.meta.getBackRoute when the right target depends on
|
||||
// fetched data (e.g. skip the scope picker when it would auto-redirect).
|
||||
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) => {
|
||||
title.value = value;
|
||||
@@ -20,10 +23,15 @@ export const usePageHeaderStore = defineStore("pageHeader", () => {
|
||||
backRoute.value = value;
|
||||
};
|
||||
|
||||
const setLive = (value: boolean | null) => {
|
||||
live.value = value;
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
title.value = "";
|
||||
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