This commit is contained in:
Linrador 2025-09-24 11:14:51 +02:00
parent 82a59d3da1
commit 7d204fe836
9 changed files with 129 additions and 77 deletions

View File

@ -1,18 +1,18 @@
// src/app/components/TelemetryBanner.tsx // src/app/components/GameBanner.tsx
'use client' 'use client'
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
import Link from 'next/link' import Link from 'next/link'
import Button from '../components/Button' import Button from './Button'
import { useUiChromeStore } from '@/lib/useUiChromeStore' import { useUiChromeStore } from '@/lib/useUiChromeStore'
import { MAP_OPTIONS } from '@/lib/mapOptions' import { MAP_OPTIONS } from '@/lib/mapOptions'
import { useRouter, usePathname } from '@/i18n/navigation' import { useRouter, usePathname } from '@/i18n/navigation'
import { useTranslations, useLocale } from 'next-intl' import { useTranslations, useLocale } from 'next-intl'
export type TelemetryBannerVariant = 'connected' | 'disconnected' export type GameBannerVariant = 'connected' | 'disconnected'
type Props = { type Props = {
variant: TelemetryBannerVariant variant: GameBannerVariant
visible: boolean visible: boolean
zIndex?: number zIndex?: number
// gemeinsam // gemeinsam
@ -54,7 +54,7 @@ function pickMapIcon(mapKey?: string): string | null {
} }
/* ---------- component ---------- */ /* ---------- component ---------- */
export default function TelemetryBanner({ export default function GameBanner({
variant, variant,
visible, visible,
zIndex = 9999, zIndex = 9999,
@ -72,15 +72,12 @@ export default function TelemetryBanner({
inline = false, inline = false,
}: Props) { }: Props) {
const ref = useRef<HTMLDivElement | null>(null) const ref = useRef<HTMLDivElement | null>(null)
const setBannerPx = useUiChromeStore(s => s.setTelemetryBannerPx) const setBannerPx = useUiChromeStore(s => s.setGameBannerPx)
// ▼ Phase normalisieren und Sichtbarkeit nur erlauben, wenn nicht "unknown" // ▼ Phase normalisieren und Sichtbarkeit nur erlauben, wenn nicht "unknown"
const phaseStr = String(phase ?? 'unknown').toLowerCase() const phaseStr = String(phase ?? 'unknown').toLowerCase()
const show = visible && phaseStr !== 'unknown' const show = visible && phaseStr !== 'unknown'
const router = useRouter()
const pathname = usePathname()
// Übersetzungen // Übersetzungen
const tGameBanner = useTranslations('game-banner') const tGameBanner = useTranslations('game-banner')
@ -96,7 +93,7 @@ export default function TelemetryBanner({
}, [show, setBannerPx]) }, [show, setBannerPx])
// Ableitungen vor dem Guard // Ableitungen vor dem Guard
const outerBase = inline ? '' : 'fixed inset-x-0 bottom-0' const outerBase = inline ? '' : 'fixed right-0 bottom-0 left-0 sm:left-[16rem]'
const outerStyle = inline ? {} : { zIndex } const outerStyle = inline ? {} : { zIndex }
const wrapperClass = const wrapperClass =
@ -121,8 +118,8 @@ export default function TelemetryBanner({
if (!show) return null if (!show) return null
return ( return (
<div className={`${outerBase} h-full w-full`} style={outerStyle} ref={ref}> <div className={outerBase} style={outerStyle} ref={ref}>
<div className={`relative overflow-hidden h-full shadow-lg ${wrapperClass} transition duration-300 ease-in-out`}> <div className={`relative overflow-hidden shadow-lg ${wrapperClass} transition duration-300 ease-in-out`}>
{/* Subtiler Map-Hintergrund */} {/* Subtiler Map-Hintergrund */}
{bgUrl && ( {bgUrl && (
<div <div
@ -149,7 +146,7 @@ export default function TelemetryBanner({
/> />
{/* Inhalt */} {/* Inhalt */}
<div className="relative h-full p-3 flex items-center gap-3"> <div className="relative p-3 flex items-center gap-3">
{/* Icon links */} {/* Icon links */}
{iconUrl ? ( {iconUrl ? (
<div className="shrink-0 relative z-[1]"> <div className="shrink-0 relative z-[1]">

View File

@ -0,0 +1,10 @@
// src/app/components/GameBannerSpacer.tsx
'use client'
import {useEffect, useState} from 'react'
import {useUiChromeStore} from '@/lib/useUiChromeStore'
export default function GameBannerSpacer() {
const bannerPx = useUiChromeStore(s => s.gameBannerPx) // oder passender Selector
// Nur Höhe setzen, wenn inline-Banner genutzt wird sonst 0
return <div style={{height: bannerPx ?? 0}} aria-hidden />
}

View File

@ -7,7 +7,7 @@ import { useReadyOverlayStore } from '@/lib/useReadyOverlayStore'
import { usePresenceStore } from '@/lib/usePresenceStore' import { usePresenceStore } from '@/lib/usePresenceStore'
import { useTelemetryStore } from '@/lib/useTelemetryStore' import { useTelemetryStore } from '@/lib/useTelemetryStore'
import { useMatchRosterStore } from '@/lib/useMatchRosterStore' import { useMatchRosterStore } from '@/lib/useMatchRosterStore'
import TelemetryBanner from './TelemetryBanner' import TelemetryBanner from './GameBanner'
import { MAP_OPTIONS } from '@/lib/mapOptions' import { MAP_OPTIONS } from '@/lib/mapOptions'
import { useSSEStore } from '@/lib/useSSEStore' import { useSSEStore } from '@/lib/useSSEStore'
@ -107,7 +107,7 @@ export default function TelemetrySocket() {
const [dockEl, setDockEl] = useState<HTMLElement | null>(null) const [dockEl, setDockEl] = useState<HTMLElement | null>(null)
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
setDockEl(document.getElementById('telemetry-banner-dock') as HTMLElement | null) setDockEl(document.getElementById('game-banner-dock') as HTMLElement | null)
}, []) }, [])
// connect href from API // connect href from API

View File

@ -34,7 +34,7 @@ export default function AppearanceSettings() {
<div className="sm:col-span-8 xl:col-span-6 2xl:col-span-5"> <div className="sm:col-span-8 xl:col-span-6 2xl:col-span-5">
<p className="text-sm text-gray-500 dark:text-neutral-500"> <p className="text-sm text-gray-500 dark:text-neutral-500">
Wähle dein bevorzugtes Design. Du kannst einen festen Stil verwenden oder das Systemverhalten übernehmen. {tSettings("tabs.account.page.AppearanceSettings.description")}
</p> </p>
<h3 className="mt-3 text-sm font-semibold text-gray-800 dark:text-neutral-200"> <h3 className="mt-3 text-sm font-semibold text-gray-800 dark:text-neutral-200">

View File

@ -18,6 +18,7 @@ import UserActivityTracker from './components/UserActivityTracker';
import AudioPrimer from './components/AudioPrimer'; import AudioPrimer from './components/AudioPrimer';
import ReadyOverlayHost from './components/ReadyOverlayHost'; import ReadyOverlayHost from './components/ReadyOverlayHost';
import TelemetrySocket from './components/TelemetrySocket'; import TelemetrySocket from './components/TelemetrySocket';
import GameBannerSpacer from './components/GameBannerSpacer';
const geistSans = Geist({variable: '--font-geist-sans', subsets: ['latin']}); const geistSans = Geist({variable: '--font-geist-sans', subsets: ['latin']});
const geistMono = Geist_Mono({variable: '--font-geist-mono', subsets: ['latin']}); const geistMono = Geist_Mono({variable: '--font-geist-mono', subsets: ['latin']});
@ -36,7 +37,6 @@ export default async function RootLayout({children, params}: Props) {
const {locale} = await params; const {locale} = await params;
if (!hasLocale(routing.locales, locale)) notFound(); if (!hasLocale(routing.locales, locale)) notFound();
// ⬇️ holt die von request.ts gemergten Namespaces
const messages = await getMessages(); const messages = await getMessages();
const lang = await getLocale(); const lang = await getLocale();
@ -44,6 +44,7 @@ export default async function RootLayout({children, params}: Props) {
<html lang={lang} suppressHydrationWarning> <html lang={lang} suppressHydrationWarning>
<body className={`antialiased bg-white dark:bg-black min-h-dvh ${geistSans.variable} ${geistMono.variable}`}> <body className={`antialiased bg-white dark:bg-black min-h-dvh ${geistSans.variable} ${geistMono.variable}`}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{/* ⬇️ EIN globaler i18n-Provider um ALLES */}
<NextIntlClientProvider locale={lang} messages={messages}> <NextIntlClientProvider locale={lang} messages={messages}>
<Providers> <Providers>
<SSEHandler /> <SSEHandler />
@ -51,15 +52,17 @@ export default async function RootLayout({children, params}: Props) {
<AudioPrimer /> <AudioPrimer />
<ReadyOverlayHost /> <ReadyOverlayHost />
<TelemetrySocket /> <TelemetrySocket />
<div className="min-h-dvh grid grid-cols-1 sm:grid-cols-[16rem_1fr]"> <div className="min-h-dvh grid grid-cols-1 sm:grid-cols-[16rem_1fr]">
<Sidebar /> <Sidebar />
<div className="min-w-0 flex flex-col"> <div className="min-w-0 flex flex-col">
<main className="flex-1 in-w-0 overflow-hidden"> <main className="flex-1 min-w-0 overflow-hidden">
<div className="h-full box-border p-4 sm:p-6">{children}</div> <div className="h-full box-border p-4 sm:p-6">{children}</div>
</main> </main>
<div id="telemetry-banner-dock" className="h-full max-h-[65px]" /> <GameBannerSpacer />
</div> </div>
</div> </div>
<NotificationBell /> <NotificationBell />
</Providers> </Providers>
</NextIntlClientProvider> </NextIntlClientProvider>

View File

@ -1,42 +1,14 @@
// /src/i18n/request.ts // /src/i18n/request.ts
import {getRequestConfig} from 'next-intl/server'; import {getRequestConfig} from 'next-intl/server'
import {hasLocale} from 'next-intl'; import {hasLocale} from 'next-intl'
import {routing} from './routing'; import {routing} from './routing'
const namespaces = [
'common',
'nav',
'sidebar',
'settings',
'teams',
'matches',
'dashboard'
] as const;
type Namespace = (typeof namespaces)[number];
async function tryImport<T>(p: string): Promise<T | null> {
try { const mod = await import(p as any); return (mod as any).default as T; }
catch { return null; }
}
async function loadMessages(locale: string) {
const entries = await Promise.all(
namespaces.map(async (ns) => {
// ⬇️ neue Struktur: /messages/<namespace>/<locale>.json
const obj = await tryImport<Record<string, unknown>>(
`../messages/${ns}/${locale}.json`
);
return [ns, obj ?? {}] as const;
})
);
const merged = {} as Record<Namespace, Record<string, unknown>>;
for (const [ns, obj] of entries) merged[ns] = obj;
return merged;
}
export default getRequestConfig(async ({requestLocale}) => { export default getRequestConfig(async ({requestLocale}) => {
const requested = await requestLocale; const requested = await requestLocale
const locale = hasLocale(routing.locales, requested) ? (requested as string) : routing.defaultLocale; const locale = hasLocale(routing.locales, requested) ? (requested as string) : routing.defaultLocale
return { locale, messages: await loadMessages(locale) };
}); // ⬇️ Eine JSON pro Locale laden
const messages = (await import(`../messages/${locale}.json`)).default
return { locale, messages }
})

View File

@ -2,11 +2,11 @@
import { create } from 'zustand' import { create } from 'zustand'
type UiChromeState = { type UiChromeState = {
telemetryBannerPx: number // aktuelle Bannerhöhe in Pixel (0 wenn unsichtbar) gameBannerPx: number // aktuelle Bannerhöhe in Pixel (0 wenn unsichtbar)
setTelemetryBannerPx: (px: number) => void setGameBannerPx: (px: number) => void
} }
export const useUiChromeStore = create<UiChromeState>((set) => ({ export const useUiChromeStore = create<UiChromeState>((set) => ({
telemetryBannerPx: 0, gameBannerPx: 0,
setTelemetryBannerPx: (px) => set({ telemetryBannerPx: Math.max(0, Math.floor(px)) }), setGameBannerPx: (px) => set({ gameBannerPx: Math.max(0, Math.floor(px)) }),
})) }))

View File

@ -1,4 +1,5 @@
{ {
"disconnect": "Verbindung trennen",
"nav": { "nav": {
"dashboard": "Dashboard", "dashboard": "Dashboard",
"teams": { "teams": {
@ -8,13 +9,13 @@
}, },
"players": { "players": {
"label": "Spieler", "label": "Spieler",
"overview": "Übersicht", "overview": "Überblick",
"stats": "Statistiken" "stats": "Statistiken"
}, },
"schedule": "Spielplan" "schedule": "Spielpan"
}, },
"dashboard": { "dashboard": {
"title": "Willkommen im Dashboard!" "title": "Willkommen!"
}, },
"sidebar": { "sidebar": {
"brand": "Iron:e", "brand": "Iron:e",
@ -31,17 +32,51 @@
"signout": "Abmelden" "signout": "Abmelden"
} }
}, },
"settings": {
"tabs": {
"account": {
"title": "Accounteinstellungen",
"short": "Account",
"description": "Verwalte deine Kontoeinstellungen und verbundenen Dienste.",
"find-code": "Deinen Code findest du",
"here": "hier",
"url": "https://help.steampowered.com/de/wizard/HelpWithGameIssue/?appid=730&issueid=128",
"page": {
"AuthCodeSettings": {
"name": "Authentifizierungscode",
"question": "Was ist der Authentifizierungscode?",
"description": "Drittanbieter-Webseiten und -Anwendungen können diesen Authentifizierungscode nutzen, um auf deine Match-Historie, deine Gesamtleistung in diesen Matches zuzugreifen, Wiederholungen herunterzuladen und dein Gameplay zu analysieren.",
"button-disconnect": "Trennen"
},
"ShareCodeSettings": {
"name": "Austauschcode",
"question": "Was ist der Austauschcode?",
"description": "Mit dem Austauschcode können Anwendungen dein zuletzt ausgetragenes offizielles Match finden und analysieren."
},
"AppearanceSettings": {
"name": "Erscheinungsbild",
"description": "Wähle dein bevorzugtes Design. Du kannst einen festen Stil verwenden oder die Systemeinstellung übernehmen."
}
}
},
"privacy": {
"title": "Datenschutzeinstellungen",
"short": "Datenschutz",
"description": "Verwalte deine Datenschutzeinstellungen."
}
}
},
"game-banner": { "game-banner": {
"disconnected": "Verbindung getrennt", "disconnected": "Nicht verbunden",
"player-connected": "Spieler verbunden", "player-connected": "Spieler verbunden",
"open-game": "Spiel öffnen", "open-game": "Spiel starten",
"quit": "Verlassen", "quit": "Beenden",
"reconnect": "Neu verbinden" "reconnect": "Erneut verbinden"
}, },
"matches": { "matches": {
"title": "Geplante Matches", "title": "Geplante Matches",
"description": "Keine Matches geplant.", "description": "Keine Matches geplant.",
"filter": "Nur mein Team anzeigen", "filter": "Nur mein Team anzeigen",
"create-match": "Neues Match erstellen" "create-match": "Match erstellen"
} }
} }

View File

@ -1,4 +1,5 @@
{ {
"disconnect": "Disconnect",
"nav": { "nav": {
"dashboard": "Dashboard", "dashboard": "Dashboard",
"teams": { "teams": {
@ -23,7 +24,7 @@
"en": "English" "en": "English"
}, },
"footer": { "footer": {
"login": "Login with Steam", "login": "Sign in with Steam",
"profile": "Profile", "profile": "Profile",
"team": "Team", "team": "Team",
"settings": "Settings", "settings": "Settings",
@ -31,17 +32,51 @@
"signout": "Sign out" "signout": "Sign out"
} }
}, },
"settings": {
"tabs": {
"account": {
"title": "Account Settings",
"short": "Account",
"description": "Manage your account settings and connected services.",
"find-code": "You can find your code",
"here": "here",
"url": "https://help.steampowered.com/en/wizard/HelpWithGameIssue/?appid=730&issueid=128",
"page": {
"AuthCodeSettings": {
"name": "Authentication Code",
"question": "What is the Authentication Code?",
"description": "Third-party websites and applications can use this authentication code to access your match history, your overall performance in those matches, download replays of your matches, and analyze your gameplay.",
"button-disconnect": "Disconnect"
},
"ShareCodeSettings": {
"name": "Match Share Code",
"question": "What is the Match Share Code?",
"description": "With the share code, applications can find and analyze your most recent official match."
},
"AppearanceSettings": {
"name": "Appearance",
"description": "Choose your preferred theme. You can use a fixed style or follow your system setting."
}
}
},
"privacy": {
"title": "Privacy Settings",
"short": "Privacy",
"description": "Manage your privacy preferences."
}
}
},
"game-banner": { "game-banner": {
"disconnected": "Disconnected", "disconnected": "Disconnected",
"player-connected": "Players connected", "player-connected": "Players connected",
"open-game": "Open game", "open-game": "Open Game",
"quit": "Quit", "quit": "Quit",
"reconnect": "Reconnect" "reconnect": "Reconnect"
}, },
"matches": { "matches": {
"title": "Scheduled matches", "title": "Scheduled Matches",
"description": "No matches scheduled.", "description": "No matches scheduled.",
"filter": "Show my team only", "filter": "Show my team only",
"create-match": "Create new match" "create-match": "Create Match"
} }
} }