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'
import React, { useEffect, useRef } from 'react'
import Link from 'next/link'
import Button from '../components/Button'
import Button from './Button'
import { useUiChromeStore } from '@/lib/useUiChromeStore'
import { MAP_OPTIONS } from '@/lib/mapOptions'
import { useRouter, usePathname } from '@/i18n/navigation'
import { useTranslations, useLocale } from 'next-intl'
export type TelemetryBannerVariant = 'connected' | 'disconnected'
export type GameBannerVariant = 'connected' | 'disconnected'
type Props = {
variant: TelemetryBannerVariant
variant: GameBannerVariant
visible: boolean
zIndex?: number
// gemeinsam
@ -54,7 +54,7 @@ function pickMapIcon(mapKey?: string): string | null {
}
/* ---------- component ---------- */
export default function TelemetryBanner({
export default function GameBanner({
variant,
visible,
zIndex = 9999,
@ -72,15 +72,12 @@ export default function TelemetryBanner({
inline = false,
}: Props) {
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"
const phaseStr = String(phase ?? 'unknown').toLowerCase()
const show = visible && phaseStr !== 'unknown'
const router = useRouter()
const pathname = usePathname()
// Übersetzungen
const tGameBanner = useTranslations('game-banner')
@ -96,7 +93,7 @@ export default function TelemetryBanner({
}, [show, setBannerPx])
// 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 wrapperClass =
@ -121,8 +118,8 @@ export default function TelemetryBanner({
if (!show) return null
return (
<div className={`${outerBase} h-full w-full`} style={outerStyle} ref={ref}>
<div className={`relative overflow-hidden h-full shadow-lg ${wrapperClass} transition duration-300 ease-in-out`}>
<div className={outerBase} style={outerStyle} ref={ref}>
<div className={`relative overflow-hidden shadow-lg ${wrapperClass} transition duration-300 ease-in-out`}>
{/* Subtiler Map-Hintergrund */}
{bgUrl && (
<div
@ -149,7 +146,7 @@ export default function TelemetryBanner({
/>
{/* 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 */}
{iconUrl ? (
<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 { useTelemetryStore } from '@/lib/useTelemetryStore'
import { useMatchRosterStore } from '@/lib/useMatchRosterStore'
import TelemetryBanner from './TelemetryBanner'
import TelemetryBanner from './GameBanner'
import { MAP_OPTIONS } from '@/lib/mapOptions'
import { useSSEStore } from '@/lib/useSSEStore'
@ -107,7 +107,7 @@ export default function TelemetrySocket() {
const [dockEl, setDockEl] = useState<HTMLElement | null>(null)
useEffect(() => {
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

View File

@ -34,7 +34,7 @@ export default function AppearanceSettings() {
<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">
Wähle dein bevorzugtes Design. Du kannst einen festen Stil verwenden oder das Systemverhalten übernehmen.
{tSettings("tabs.account.page.AppearanceSettings.description")}
</p>
<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 ReadyOverlayHost from './components/ReadyOverlayHost';
import TelemetrySocket from './components/TelemetrySocket';
import GameBannerSpacer from './components/GameBannerSpacer';
const geistSans = Geist({variable: '--font-geist-sans', 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;
if (!hasLocale(routing.locales, locale)) notFound();
// ⬇️ holt die von request.ts gemergten Namespaces
const messages = await getMessages();
const lang = await getLocale();
@ -44,6 +44,7 @@ export default async function RootLayout({children, params}: Props) {
<html lang={lang} suppressHydrationWarning>
<body className={`antialiased bg-white dark:bg-black min-h-dvh ${geistSans.variable} ${geistMono.variable}`}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{/* ⬇️ EIN globaler i18n-Provider um ALLES */}
<NextIntlClientProvider locale={lang} messages={messages}>
<Providers>
<SSEHandler />
@ -51,15 +52,17 @@ export default async function RootLayout({children, params}: Props) {
<AudioPrimer />
<ReadyOverlayHost />
<TelemetrySocket />
<div className="min-h-dvh grid grid-cols-1 sm:grid-cols-[16rem_1fr]">
<Sidebar />
<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>
</main>
<div id="telemetry-banner-dock" className="h-full max-h-[65px]" />
<GameBannerSpacer />
</div>
</div>
<NotificationBell />
</Providers>
</NextIntlClientProvider>

View File

@ -1,42 +1,14 @@
// /src/i18n/request.ts
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
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;
}
import {getRequestConfig} from 'next-intl/server'
import {hasLocale} from 'next-intl'
import {routing} from './routing'
export default getRequestConfig(async ({requestLocale}) => {
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested) ? (requested as string) : routing.defaultLocale;
return { locale, messages: await loadMessages(locale) };
});
const requested = await requestLocale
const locale = hasLocale(routing.locales, requested) ? (requested as string) : routing.defaultLocale
// ⬇️ 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'
type UiChromeState = {
telemetryBannerPx: number // aktuelle Bannerhöhe in Pixel (0 wenn unsichtbar)
setTelemetryBannerPx: (px: number) => void
gameBannerPx: number // aktuelle Bannerhöhe in Pixel (0 wenn unsichtbar)
setGameBannerPx: (px: number) => void
}
export const useUiChromeStore = create<UiChromeState>((set) => ({
telemetryBannerPx: 0,
setTelemetryBannerPx: (px) => set({ telemetryBannerPx: Math.max(0, Math.floor(px)) }),
gameBannerPx: 0,
setGameBannerPx: (px) => set({ gameBannerPx: Math.max(0, Math.floor(px)) }),
}))

View File

@ -1,4 +1,5 @@
{
"disconnect": "Verbindung trennen",
"nav": {
"dashboard": "Dashboard",
"teams": {
@ -8,13 +9,13 @@
},
"players": {
"label": "Spieler",
"overview": "Übersicht",
"overview": "Überblick",
"stats": "Statistiken"
},
"schedule": "Spielplan"
"schedule": "Spielpan"
},
"dashboard": {
"title": "Willkommen im Dashboard!"
"title": "Willkommen!"
},
"sidebar": {
"brand": "Iron:e",
@ -31,17 +32,51 @@
"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": {
"disconnected": "Verbindung getrennt",
"disconnected": "Nicht verbunden",
"player-connected": "Spieler verbunden",
"open-game": "Spiel öffnen",
"quit": "Verlassen",
"reconnect": "Neu verbinden"
"open-game": "Spiel starten",
"quit": "Beenden",
"reconnect": "Erneut verbinden"
},
"matches": {
"title": "Geplante Matches",
"description": "Keine Matches geplant.",
"filter": "Nur mein Team anzeigen",
"create-match": "Neues Match erstellen"
}
}
"create-match": "Match erstellen"
}
}

View File

@ -1,4 +1,5 @@
{
"disconnect": "Disconnect",
"nav": {
"dashboard": "Dashboard",
"teams": {
@ -23,7 +24,7 @@
"en": "English"
},
"footer": {
"login": "Login with Steam",
"login": "Sign in with Steam",
"profile": "Profile",
"team": "Team",
"settings": "Settings",
@ -31,17 +32,51 @@
"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": {
"disconnected": "Disconnected",
"player-connected": "Players connected",
"open-game": "Open game",
"open-game": "Open Game",
"quit": "Quit",
"reconnect": "Reconnect"
},
"matches": {
"title": "Scheduled matches",
"title": "Scheduled Matches",
"description": "No matches scheduled.",
"filter": "Show my team only",
"create-match": "Create new match"
}
"create-match": "Create Match"
}
}