Offline banner: say how old the data is instead of failing silently

When the live connection drops, a slim notice under the header shows
the timestamp of the last snapshot — the page keeps working from it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 01:20:18 +02:00
parent 17ec56bceb
commit 7882c9af01
3 changed files with 18 additions and 1 deletions
+16 -1
View File
@@ -6,7 +6,7 @@ import type { LucideIcon } from 'lucide-react';
import { useUiStore } from '@/stores/uiStore'; import { useUiStore } from '@/stores/uiStore';
import { useLocaleStore } from '@/stores/localeStore'; import { useLocaleStore } from '@/stores/localeStore';
import { useTournamentStore } from '@/stores/tournamentStore'; import { useTournamentStore } from '@/stores/tournamentStore';
import { useT, type Dict } from '@/lib/i18n'; import { fmt, useFormat, useT, type Dict } from '@/lib/i18n';
import { cn } from '@/lib/cn'; import { cn } from '@/lib/cn';
interface NavItem { to: string; label: (t: Dict) => string; exact: boolean; icon: LucideIcon } interface NavItem { to: string; label: (t: Dict) => string; exact: boolean; icon: LucideIcon }
@@ -88,6 +88,19 @@ export function LanguageToggle() {
); );
} }
/** Slim stale-data notice while the live connection is down. */
function OfflineBanner() {
const { t, kickoffDay, kickoffTime } = useFormat();
const status = useTournamentStore((s) => s.status);
const generatedAt = useTournamentStore((s) => s.snapshot?.generatedAt);
if (status !== 'offline' || !generatedAt) return null;
return (
<div className="border-b border-warning/30 bg-warning/10 px-4 py-1.5 text-center text-xs text-muted" role="status">
{fmt(t.common.offlineBanner, { time: `${kickoffDay(generatedAt)} ${kickoffTime(generatedAt)}` })}
</div>
);
}
function MobileNav() { function MobileNav() {
const t = useT(); const t = useT();
const [moreOpen, setMoreOpen] = useState(false); const [moreOpen, setMoreOpen] = useState(false);
@@ -221,6 +234,8 @@ export function RootLayout() {
</div> </div>
</header> </header>
<OfflineBanner />
<main className="mx-auto w-full max-w-6xl flex-1 px-4 py-6 pb-24 sm:pb-6"> <main className="mx-auto w-full max-w-6xl flex-1 px-4 py-6 pb-24 sm:pb-6">
<Outlet /> <Outlet />
</main> </main>
+1
View File
@@ -22,6 +22,7 @@ export const de: Dict = {
connected: "Verbunden", connected: "Verbunden",
liveVia: "Live · {source}", liveVia: "Live · {source}",
offlineShowingSchedule: "Offline — Spielplan wird angezeigt", offlineShowingSchedule: "Offline — Spielplan wird angezeigt",
offlineBanner: "Offline — letzte Daten von {time}. Verbinde neu…",
switchToLightMode: "Zum hellen Modus wechseln", switchToLightMode: "Zum hellen Modus wechseln",
switchToDarkMode: "Zum dunklen Modus wechseln", switchToDarkMode: "Zum dunklen Modus wechseln",
draw: "Unentschieden", draw: "Unentschieden",
+1
View File
@@ -21,6 +21,7 @@ export const en = {
connected: "Connected", connected: "Connected",
liveVia: "Live · {source}", liveVia: "Live · {source}",
offlineShowingSchedule: "Offline — showing the schedule", offlineShowingSchedule: "Offline — showing the schedule",
offlineBanner: "Offline — showing the last data from {time}. Reconnecting…",
switchToLightMode: "Switch to light mode", switchToLightMode: "Switch to light mode",
switchToDarkMode: "Switch to dark mode", switchToDarkMode: "Switch to dark mode",
draw: "Draw", draw: "Draw",