update
This commit is contained in:
parent
82a59d3da1
commit
7d204fe836
@ -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]">
|
||||||
10
src/app/[locale]/components/GameBannerSpacer.tsx
Normal file
10
src/app/[locale]/components/GameBannerSpacer.tsx
Normal 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 />
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 }
|
||||||
|
})
|
||||||
|
|||||||
@ -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)) }),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user