This commit is contained in:
Linrador 2025-09-23 15:27:42 +02:00
parent c692cefb22
commit bb7ac51509
229 changed files with 1011 additions and 773 deletions

47
messages/de.json Normal file
View File

@ -0,0 +1,47 @@
{
"nav": {
"dashboard": "Dashboard",
"teams": {
"label": "Teams",
"overview": "Übersicht",
"manage": "Teamverwaltung"
},
"players": {
"label": "Spieler",
"overview": "Übersicht",
"stats": "Statistiken"
},
"schedule": "Spielplan"
},
"dashboard": {
"title": "Willkommen im Dashboard!"
},
"sidebar": {
"brand": "Iron:e",
"language": {
"de": "Deutsch",
"en": "Englisch"
},
"footer": {
"login": "Mit Steam anmelden",
"profile": "Profil",
"team": "Team",
"settings": "Einstellungen",
"administration": "Administration",
"signout": "Abmelden"
}
},
"game-banner": {
"disconnected": "Verbindung getrennt",
"player-connected": "Spieler verbunden",
"open-game": "Spiel öffnen",
"quit": "Verlassen",
"reconnect": "Neu verbinden"
},
"matches": {
"title": "Geplante Matches",
"description": "Keine Matches geplant.",
"filter": "Nur mein Team anzeigen",
"create-match": "Neues Match erstellen"
}
}

47
messages/en.json Normal file
View File

@ -0,0 +1,47 @@
{
"nav": {
"dashboard": "Dashboard",
"teams": {
"label": "Teams",
"overview": "Overview",
"manage": "Team Management"
},
"players": {
"label": "Players",
"overview": "Overview",
"stats": "Statistics"
},
"schedule": "Schedule"
},
"dashboard": {
"title": "Welcome!"
},
"sidebar": {
"brand": "Iron:e",
"language": {
"de": "German",
"en": "English"
},
"footer": {
"login": "Login with Steam",
"profile": "Profile",
"team": "Team",
"settings": "Settings",
"administration": "Administration",
"signout": "Sign out"
}
},
"game-banner": {
"disconnected": "Disconnected",
"player-connected": "Players connected",
"open-game": "Open game",
"quit": "Quit",
"reconnect": "Reconnect"
},
"matches": {
"title": "Scheduled matches",
"description": "No matches scheduled.",
"filter": "Show my team only",
"create-match": "Create new match"
}
}

View File

@ -1,4 +1,5 @@
import type { NextConfig } from 'next'
import createNextIntlPlugin from 'next-intl/plugin';
const nextConfig: NextConfig = {
allowedDevOrigins: ['ironieopen.local', '*.ironieopen.local'],
@ -28,4 +29,5 @@ const nextConfig: NextConfig = {
},
}
export default nextConfig
const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);

16
package-lock.json generated
View File

@ -33,7 +33,7 @@
"nanoid": "^5.1.5",
"next": "15.3.0",
"next-auth-steam": "^0.4.0",
"next-intl": "^4.3.4",
"next-intl": "^4.3.9",
"next-themes": "^0.4.6",
"node-cron": "^3.0.3",
"node-fetch": "^3.3.2",
@ -6117,9 +6117,9 @@
}
},
"node_modules/next-intl": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.4.tgz",
"integrity": "sha512-VWLIDlGbnL/o4LnveJTJD1NOYN8lh3ZAGTWw2krhfgg53as3VsS4jzUVnArJdqvwtlpU/2BIDbWTZ7V4o1jFEw==",
"version": "4.3.9",
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.9.tgz",
"integrity": "sha512-4oSROHlgy8a5Qr2vH69wxo9F6K0uc6nZM2GNzqSe6ET79DEzOmBeSijCRzD5txcI4i+XTGytu4cxFsDXLKEDpQ==",
"funding": [
{
"type": "individual",
@ -6130,7 +6130,7 @@
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
"negotiator": "^1.0.0",
"use-intl": "^4.3.4"
"use-intl": "^4.3.9"
},
"peerDependencies": {
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
@ -8013,9 +8013,9 @@
}
},
"node_modules/use-intl": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.4.tgz",
"integrity": "sha512-sHfiU0QeJ1rirNWRxvCyvlSh9+NczcOzRnPyMeo2rtHXhVnBsvMRjE+UG4eh3lRhCxrvcqei/I0lBxsc59on1w==",
"version": "4.3.9",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.9.tgz",
"integrity": "sha512-bZu+h13HIgOvsoGleQtUe4E6gM49CRm+AH36KnJVB/qb1+Beo7jr7HNrR8YWH8oaOkQfGNm6vh0HTepxng8UTg==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "^2.2.0",

View File

@ -37,7 +37,7 @@
"nanoid": "^5.1.5",
"next": "15.3.0",
"next-auth-steam": "^0.4.0",
"next-intl": "^4.3.4",
"next-intl": "^4.3.9",
"next-themes": "^0.4.6",
"node-cron": "^3.0.3",
"node-fetch": "^3.3.2",

View File

@ -1,9 +1,9 @@
'use client'
import { notFound, usePathname } from 'next/navigation'
import Card from '@/app/components/Card'
import MatchesAdminManager from '@/app/components/admin/MatchesAdminManager'
import AdminTeamsView from '@/app/components/admin/teams/AdminTeamsView'
import Card from '../components/Card'
import MatchesAdminManager from '../components/admin/MatchesAdminManager'
import AdminTeamsView from '../components/admin/teams/AdminTeamsView'
export default function AdminPage() {
const pathname = usePathname()

View File

@ -1,5 +1,5 @@
import { Tabs } from '@/app/components/Tabs'
import Tab from '@/app/components/Tab'
import { Tabs } from '../components/Tabs'
import Tab from '../components/Tab'
export default function AdminLayout({ children }: { children: React.ReactNode }) {
return (

View File

@ -1,12 +1,12 @@
// /src/app/admin/server/page.tsx
import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth'
import { prisma } from '@/app/lib/prisma'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'
import Card from '@/app/components/Card'
import ServerView from '@/app/components/admin/server/ServerView'
import Card from '../components/Card'
import ServerView from '../components/admin/server/ServerView'
export const dynamic = 'force-dynamic'

View File

@ -3,11 +3,11 @@
import { useCallback, useEffect, useState, useRef } from 'react'
import { useSession } from 'next-auth/react'
import LoadingSpinner from '@/app/components/LoadingSpinner'
import TeamMemberView from '@/app/components/TeamMemberView'
import { useTeamStore } from '@/app/lib/stores'
import { reloadTeam } from '@/app/lib/sse-actions'
import type { Player } from '@/app/types/team'
import LoadingSpinner from '../components/LoadingSpinner'
import TeamMemberView from '../components/TeamMemberView'
import { useTeamStore } from '@/lib/stores'
import { reloadTeam } from '@/lib/sse-actions'
import type { Player } from '../types/team'
type Props = { teamId: string }

View File

@ -2,8 +2,8 @@
'use client'
import Card from '@/app/components/Card'
import AdminTeamsView from '@/app/components/admin/teams/AdminTeamsView'
import Card from '../components/Card'
import AdminTeamsView from '../components/admin/teams/AdminTeamsView'
export default function AdminTeamsPage() {
return (

View File

@ -1,6 +1,6 @@
'use client'
import { useEffect } from 'react'
import { sound } from '@/app/lib/soundManager'
import { sound } from '@/lib/soundManager';
export default function AudioPrimer() {
useEffect(() => {

View File

@ -1,17 +1,18 @@
'use client'
import { useEffect, useState, useCallback } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import Image from 'next/image'
import { format } from 'date-fns' // 👈 neu
import { de } from 'date-fns/locale'
import Switch from '@/app/components/Switch'
import Button from './Button'
import Modal from './Modal'
import { Match } from '../types/match'
import { useSSEStore } from '@/app/lib/useSSEStore'
import { useSession } from 'next-auth/react'
import { useRouter, usePathname } from '@/i18n/navigation'
import { useTranslations, useLocale } from 'next-intl'
import Link from 'next/link'
import Image from 'next/image'
import { format } from 'date-fns'
import { de } from 'date-fns/locale'
import Switch from '../components/Switch'
import Button from './Button'
import Modal from './Modal'
import { Match } from '../../../types/match'
import { useSSEStore } from '@/lib/useSSEStore'
type Props = { matchType?: string }
@ -57,7 +58,12 @@ function getMapVoteState(m: Match, nowMs: number) {
export default function CommunityMatchList({ matchType }: Props) {
const { data: session } = useSession()
const router = useRouter()
const router = useRouter()
const pathname = usePathname()
const locale = useLocale()
const tMatches = useTranslations('matches')
const { lastEvent } = useSSEStore()
const [matches, setMatches] = useState<Match[]>([])
@ -272,18 +278,18 @@ export default function CommunityMatchList({ matchType }: Props) {
{/* Kopfzeile */}
<div className="flex items-center justify-between flex-wrap gap-y-4">
<h1 className="text-2xl font-bold text-gray-700 dark:text-neutral-300">
Geplante Matches
{tMatches("title")}
</h1>
<div className="flex items-center gap-4">
<Switch
id="only-own-team"
checked={onlyOwn}
onChange={setOnlyOwn}
labelRight="Nur mein Team anzeigen"
labelRight={tMatches("filter")}
/>
{session?.user?.isAdmin && (
<Button color="blue" onClick={() => setShowCreate(true)}>
Match erstellen
{tMatches("create-match")}
</Button>
)}
</div>
@ -291,7 +297,7 @@ export default function CommunityMatchList({ matchType }: Props) {
{/* Inhalt */}
{grouped.length === 0 ? (
<p className="text-gray-700 dark:text-neutral-300">Keine Matches geplant.</p>
<p className="text-gray-700 dark:text-neutral-300">{tMatches("description")}</p>
) : (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
{grouped.map(([dateKey, dayMatches], dayIdx) => {

View File

@ -1,5 +1,6 @@
// CompRankBadge.tsx
'use client';
import Image from 'next/image';
import Tooltip from './Tooltip';

View File

@ -1,7 +1,7 @@
'use client'
import { useDroppable, useDndContext } from '@dnd-kit/core'
import { Player } from '../types/team'
import { Player } from '../../../types/team'
import clsx from 'clsx'
type DroppableZoneProps = {

View File

@ -2,10 +2,10 @@
'use client'
import { useEffect, useMemo, useRef, useState } from 'react'
import Modal from '@/app/components/Modal'
import Alert from '@/app/components/Alert'
import Select from '@/app/components/Select'
import LoadingSpinner from '@/app/components/LoadingSpinner' // ⬅️ NEU
import Modal from '../components/Modal'
import Alert from '../components/Alert'
import Select from '../components/Select'
import LoadingSpinner from '../components/LoadingSpinner' // ⬅️ NEU
type TeamOption = { id: string; name: string; logo?: string | null }

View File

@ -14,12 +14,12 @@ import {
SortableContext, verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import Modal from '@/app/components/Modal'
import SortableMiniCard from '@/app/components/SortableMiniCard'
import LoadingSpinner from '@/app/components/LoadingSpinner'
import { DroppableZone } from '@/app/components/DroppableZone'
import Modal from '../components/Modal'
import SortableMiniCard from '../components/SortableMiniCard'
import LoadingSpinner from '../components/LoadingSpinner'
import { DroppableZone } from '../components/DroppableZone'
import type { Player, Team } from '@/app/types/team'
import type { Player, Team } from '../../../types/team'
/* ───────────────────────── Typen ────────────────────────── */
export type EditSide = 'A' | 'B'

View File

@ -5,7 +5,7 @@ import Modal from './Modal'
import MiniCard from './MiniCard'
import { useSession } from 'next-auth/react'
import LoadingSpinner from './LoadingSpinner'
import { Player, Team } from '../types/team'
import { Player, Team } from '../../../types/team'
import Pagination from './Pagination'
import { AnimatePresence, motion } from 'framer-motion'

View File

@ -4,8 +4,8 @@ import { useState, useEffect } from 'react'
import Modal from './Modal'
import MiniCard from './MiniCard'
import { useSession } from 'next-auth/react'
import { Player, Team } from '../types/team'
import { leaveTeam } from '../lib/sse-actions'
import { Player, Team } from '../../../types/team'
import { leaveTeam } from '@/lib/sse-actions'
type Props = {
show: boolean

View File

@ -4,8 +4,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useSession } from 'next-auth/react'
import { useSSEStore } from '@/app/lib/useSSEStore'
import type { MapVoteState } from '../types/mapvote'
import { useSSEStore } from '@/lib/useSSEStore'
import type { MapVoteState } from '../../../types/mapvote'
type Props = {
match: any

View File

@ -7,15 +7,15 @@ import { useRouter } from 'next/navigation'
import type React from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { useSession } from 'next-auth/react'
import { useSSEStore } from '@/app/lib/useSSEStore'
import { useReadyOverlayStore } from '@/app/lib/useReadyOverlayStore'
import { useSSEStore } from '@/lib/useSSEStore'
import { useReadyOverlayStore } from '@/lib/useReadyOverlayStore'
import MapVoteProfileCard from './MapVoteProfileCard'
import TeamPremierRankBadge from './TeamPremierRankBadge'
import Button from './Button'
import LoadingSpinner from './LoadingSpinner'
import type { Match, MatchPlayer } from '../types/match'
import type { MapVoteState } from '../types/mapvote'
import { MAP_OPTIONS } from '../lib/mapOptions'
import type { Match, MatchPlayer } from '../../../types/match'
import type { MapVoteState } from '../../../types/mapvote'
import { MAP_OPTIONS } from '@/lib/mapOptions'
/* =================== Utilities & constants =================== */

View File

@ -14,12 +14,12 @@ import EditMatchMetaModal from './EditMatchMetaModal'
import EditMatchPlayersModal from './EditMatchPlayersModal'
import type { EditSide } from './EditMatchPlayersModal'
import type { Match, MatchPlayer } from '../types/match'
import type { Match, MatchPlayer } from '../../../types/match'
import Button from './Button'
import { MAP_OPTIONS } from '../lib/mapOptions'
import { MAP_OPTIONS } from '@/lib/mapOptions'
import MapVoteBanner from './MapVoteBanner'
import { useSSEStore } from '@/app/lib/useSSEStore'
import { Team } from '../types/team'
import { useSSEStore } from '@/lib/useSSEStore'
import { Team } from '../../../types/team'
import Alert from './Alert'
import Image from 'next/image'
import Link from 'next/link'

View File

@ -1,6 +1,6 @@
import Table from './Table'
import Image from 'next/image'
import { MatchPlayer } from '@/app/types/match'
import { MatchPlayer } from '../../../types/match'
type Props = {
player: MatchPlayer

View File

@ -2,11 +2,11 @@
'use client'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { sound } from '@/app/lib/soundManager'
import { useSSEStore } from '@/app/lib/useSSEStore'
import { sound } from '@/lib/soundManager'
import { useSSEStore } from '@/lib/useSSEStore'
import { useSession } from 'next-auth/react'
import LoadingSpinner from './LoadingSpinner'
import { MAP_OPTIONS } from '../lib/mapOptions'
import { MAP_OPTIONS } from '@/lib/mapOptions'
type Props = {
open: boolean

View File

@ -1,7 +1,7 @@
'use client'
import { Team } from '@/app/types/team'
import { MatchPlayer } from '../types/match'
import { Team } from '../../../types/team'
import { MatchPlayer } from '../../../types/match'
import MatchPlayerCard from './MatchPlayerCard'
import Image from 'next/image'
import Button from './Button'

View File

@ -3,9 +3,9 @@
import { useEffect, useMemo, useState } from 'react'
import { useSession } from 'next-auth/react'
import TeamCard from './TeamCard'
import type { Team, Player } from '../types/team'
import { useSSEStore } from '@/app/lib/useSSEStore'
import { TEAM_EVENTS, INVITE_EVENTS } from '../lib/sseEvents'
import type { Team, Player } from '../../../types/team'
import { useSSEStore } from '@/lib/useSSEStore'
import { TEAM_EVENTS, INVITE_EVENTS } from '@/lib/sseEvents'
type Props = {
initialTeams: Team[]

View File

@ -4,9 +4,9 @@ import { useEffect, useState, useRef } from 'react'
import NotificationCenter from './NotificationCenter'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useSSEStore } from '@/app/lib/useSSEStore'
import { NOTIFICATION_EVENTS, isSseEventType } from '../lib/sseEvents'
import { useUiChromeStore } from '@/app/lib/useUiChromeStore'
import { useSSEStore } from '@/lib/useSSEStore'
import { NOTIFICATION_EVENTS, isSseEventType } from '@/lib/sseEvents'
import { useUiChromeStore } from '@/lib/useUiChromeStore'
type Notification = {
id: string

View File

@ -1,7 +1,7 @@
'use client'
import Image from 'next/image'
import { Player, Team } from '@/app/types/team'
import { Player, Team } from '../../../types/team'
export type CardWidth =
| 'sm' // max-w-sm (24rem)

View File

@ -3,10 +3,10 @@
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useSession } from 'next-auth/react'
import { useSSEStore } from '@/app/lib/useSSEStore'
import { useSSEStore } from '@/lib/useSSEStore'
import MatchReadyOverlay from './MatchReadyOverlay'
import { useReadyOverlayStore } from '@/app/lib/useReadyOverlayStore'
import { useMatchRosterStore } from '@/app/lib/useMatchRosterStore' // ⬅️ neu
import { useReadyOverlayStore } from '@/lib/useReadyOverlayStore'
import { useMatchRosterStore } from '@/lib/useMatchRosterStore'
// ---- kleiner In-Memory Cache für connectHref pro matchId
const CONNECT_CACHE = new Map<string | undefined, string>()

View File

@ -1,8 +1,9 @@
// Select.tsx
import { useState, useRef, useEffect, useMemo } from "react";
import React, { useState, useRef, useEffect, useMemo } from "react";
import { createPortal } from "react-dom";
type Option = { value: string; label: string };
type Option = { value: string; label: React.ReactNode };
type SelectProps = {
options: Option[];
placeholder?: string;
@ -10,6 +11,8 @@ type SelectProps = {
onChange: (value: string) => void;
dropDirection?: "up" | "down" | "auto";
className?: string;
showArrow?: boolean;
fullWidth?: boolean;
};
export default function Select({
@ -18,7 +21,9 @@ export default function Select({
value,
onChange,
dropDirection = "down",
className
className,
showArrow = true,
fullWidth = true,
}: SelectProps) {
const [open, setOpen] = useState(false);
const [direction, setDirection] = useState<"up" | "down">("down");
@ -26,7 +31,7 @@ export default function Select({
const rootRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<HTMLUListElement>(null); // 👈 NEU
const menuRef = useRef<HTMLUListElement>(null);
const selectedOption = useMemo(() => options.find(o => o.value === value), [options, value]);
@ -70,7 +75,6 @@ export default function Select({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, dropDirection]);
// Click-outside: ignoriert Klicks im Portal-Menü
useEffect(() => {
const handlePointerDown = (event: MouseEvent) => {
const t = event.target as Node;
@ -85,7 +89,7 @@ export default function Select({
const Menu = open
? createPortal(
<ul
ref={menuRef} // 👈 wichtig
ref={menuRef}
className="z-[9999] fixed bg-white border border-gray-200 rounded-lg shadow-lg max-h-60 overflow-y-auto text-sm dark:bg-neutral-900 dark:border-neutral-700"
style={{
left: coords.left,
@ -97,11 +101,8 @@ export default function Select({
{options.map(option => (
<li
key={option.value}
onClick={() => {
onChange(option.value);
setOpen(false);
}}
className={`py-2 px-4 cursor-pointer hover:bg-gray-100 dark:hover:bg-neutral-800 dark:text-neutral-200 ${
onClick={() => { onChange(option.value); setOpen(false); }}
className={`py-2 px-3 cursor-pointer flex items-center gap-2 hover:bg-gray-100 dark:hover:bg-neutral-800 dark:text-neutral-200 ${
option.value === value ? "bg-gray-100 dark:bg-neutral-800 font-medium" : ""
}`}
>
@ -119,14 +120,25 @@ export default function Select({
ref={buttonRef}
type="button"
onClick={() => setOpen(prev => !prev)}
className={`relative py-2 px-4 pe-10 w-full cursor-pointer bg-white border border-gray-200 rounded-lg text-start text-sm text-gray-800 hover:border-gray-300 focus:border-blue-500 focus:ring focus:ring-blue-500/50 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 ${className}`}
aria-haspopup="listbox"
aria-expanded={open}
className={`relative py-2 px-3 ${showArrow ? 'pe-9' : ''} ${fullWidth ? 'w-full' : 'w-auto inline-flex'} w-full cursor-pointer bg-white border border-gray-200 rounded-lg text-start text-sm text-gray-800 hover:border-gray-300 focus:border-blue-500 focus:ring focus:ring-blue-500/50 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-300 ${className || ''}`}
>
{selectedOption ? selectedOption.label : placeholder}
<span className="absolute top-1/2 right-3 -translate-y-1/2 pointer-events-none">
<svg className="w-4 h-4 text-gray-500 dark:text-neutral-500" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path d="M7 10l5 5 5-5" />
</svg>
<span className="inline-flex items-center gap-2">
{selectedOption ? selectedOption.label : placeholder}
</span>
{showArrow && (
// Pfeil: standardmäßig ↑; bei open gedreht ↓
<span
className={`absolute top-1/2 right-3 -translate-y-1/2 pointer-events-none transition-transform ${open ? 'rotate-180' : ''}`}
aria-hidden
>
<svg className="w-4 h-4 text-gray-500 dark:text-neutral-500" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path d="M7 14l5-5 5 5" />
</svg>
</span>
)}
</button>
{Menu}
</div>

View File

@ -1,19 +1,28 @@
'use client'
import { useState, useMemo } from 'react'
import { useRouter, usePathname } from 'next/navigation'
import { useRouter, usePathname } from '@/i18n/navigation'
import { useTranslations, useLocale } from 'next-intl'
import Button from './Button'
import SidebarFooter from './SidebarFooter'
import Select from './Select';
import 'flag-icons/css/flag-icons.min.css';
type Submenu = 'teams' | 'players' | null
export default function Sidebar() {
const router = useRouter()
const pathname = usePathname()
const locale = useLocale()
const [isOpen, setIsOpen] = useState(false) // mobile drawer
// Übersetzungen
const tNav = useTranslations('nav')
const tSidebar = useTranslations('sidebar')
const [isOpen, setIsOpen] = useState(false) // mobile drawer
const [openSubmenu, setOpenSubmenu] = useState<Submenu>(null)
// Aktive Route prüfen (pathname kommt schon ohne Locale)
const isActive = (path: string) => pathname === path
const navBtnBase =
@ -28,12 +37,22 @@ export default function Sidebar() {
const toggleSubmenu = (key: Exclude<Submenu, null>) =>
setOpenSubmenu(prev => (prev === key ? null : key))
// ✅ Locale-Wechsel: gleiche Route behalten, nur Locale ändern
const changeLocale = (nextLocale: 'en' | 'de') => {
if (nextLocale === locale) return
// pathname ist z.B. '/dashboard' next-intl setzt das Locale
router.replace(pathname, {locale: nextLocale})
setIsOpen(false)
}
// Gemeinsamer Inhalt (wird in Desktop-Aside und im Mobile-Drawer benutzt)
const SidebarInner = useMemo(
() => (
<div className="flex flex-col h-full min-h-0">
<header className="p-4 flex items-center justify-between">
<span className="font-semibold text-xl text-black dark:text-white">Iron:e</span>
<span className="font-semibold text-xl text-black dark:text-white">
{tSidebar('brand')}
</span>
{/* Close-Button nur im mobilen Drawer sichtbar */}
<button
onClick={() => setIsOpen(false)}
@ -55,12 +74,13 @@ export default function Sidebar() {
size="sm"
variant="link"
className={`${navBtnBase} ${isActive('/dashboard') ? activeClasses : idleClasses}`}
aria-label={tNav('dashboard')}
>
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
<path d="M9 22V12h6v10" />
</svg>
Dashboard
{tNav('dashboard')}
</Button>
</li>
@ -71,12 +91,14 @@ export default function Sidebar() {
size="sm"
variant="link"
className={`${navBtnBase} ${idleClasses} justify-between`}
aria-expanded={openSubmenu === 'teams'}
aria-controls="submenu-teams"
>
<span className="flex items-center gap-x-3.5">
<svg className="size-5" viewBox="0 0 640 640" fill="currentColor">
<svg className="size-4" viewBox="0 0 640 640" fill="currentColor">
<path d="M320 64c35.3 0 64 28.7 64 64s-28.7 64-64 64-64-28.7-64-64 28.7-64 64-64zm96 312c0 25-12.7 47-32 59.9V528c0 26.5-21.5 48-48 48h-32c-26.5 0-48-21.5-48-48v-92.1c-19.3-12.9-32-34.9-32-59.9v-40c0-53 43-96 96-96s96 43 96 96v40zM160 96c30.9 0 56 25.1 56 56s-25.1 56-56 56-56-25.1-56-56 25.1-56 56-56zm16 240v32c0 32.5 12.1 62.1 32 84.7V528c0 1.2 0 2.5.1 3.7-8.6 7.6-19.8 12.3-31.1 12.3H144c-26.5 0-48-21.5-48-48v-56.6C76.9 428.4 64 407.7 64 384v-32c0-53 43-96 96-96 12.7 0 24.8 2.5 35.9 6.9-12.6 21.4-19.9 46.8-19.9 73.1zM480 96c30.9 0 56 25.1 56 56s-25.1 56-56 56-56-25.1-56-56 25.1-56 56-56zm-48 432v-75.3c19.9-22.5 32-52.2 32-84.7v-32c0-26.7-7.3-52.1-19.9-73.1 11.1-4.4 23.2-6.9 35.9-6.9 53 0 96 43 96 96v32c0 23.7-12.9 44.4-32 55.4V496c0 26.5-21.5 48-48 48h-32c-10.8 0-21-3.6-29.1-9.7.1-1.2.1-2.5.1-3.7z" />
</svg>
Teams
{tNav('teams.label')}
</span>
<svg
className={`size-4 transition-transform ${openSubmenu === 'teams' ? 'rotate-180' : ''}`}
@ -87,7 +109,7 @@ export default function Sidebar() {
</Button>
{openSubmenu === 'teams' && (
<ul className="pl-6 space-y-1 mt-1">
<ul id="submenu-teams" className="pl-6 space-y-1 mt-1">
<li>
<Button
onClick={() => { router.push('/teams'); setIsOpen(false) }}
@ -95,7 +117,7 @@ export default function Sidebar() {
variant="link"
className="w-full text-start text-sm px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-neutral-700"
>
Übersicht
{tNav('teams.overview')}
</Button>
</li>
<li>
@ -105,7 +127,7 @@ export default function Sidebar() {
variant="link"
className="w-full text-start text-sm px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-neutral-700"
>
Teamverwaltung
{tNav('teams.manage')}
</Button>
</li>
</ul>
@ -119,6 +141,8 @@ export default function Sidebar() {
size="sm"
variant="link"
className={`${navBtnBase} ${idleClasses} justify-between`}
aria-expanded={openSubmenu === 'players'}
aria-controls="submenu-players"
>
<span className="flex items-center gap-x-3.5">
<svg className="size-4" viewBox="0 0 24 24" fill="currentColor">
@ -128,7 +152,7 @@ export default function Sidebar() {
clipRule="evenodd"
/>
</svg>
Spieler
{tNav('players.label')}
</span>
<svg
className={`size-4 transition-transform ${openSubmenu === 'players' ? 'rotate-180' : ''}`}
@ -139,7 +163,7 @@ export default function Sidebar() {
</Button>
{openSubmenu === 'players' && (
<ul className="pl-6 space-y-1 mt-1">
<ul id="submenu-players" className="pl-6 space-y-1 mt-1">
<li>
<Button
onClick={() => { router.push('/players'); setIsOpen(false) }}
@ -147,7 +171,7 @@ export default function Sidebar() {
variant="link"
className="w-full text-start text-sm px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-neutral-700"
>
Übersicht
{tNav('players.overview')}
</Button>
</li>
<li>
@ -157,7 +181,7 @@ export default function Sidebar() {
variant="link"
className="w-full text-start text-sm px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-neutral-700"
>
Statistiken
{tNav('players.stats')}
</Button>
</li>
</ul>
@ -179,19 +203,52 @@ export default function Sidebar() {
<line x1="3" x2="21" y1="10" y2="10" />
<path d="M8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01M16 18h.01" />
</svg>
Spielplan
{tNav('schedule')}
</Button>
</li>
</ul>
</nav>
{/* Language Switcher ganz unten, mittig (mit Flaggen) */}
<div className="mt-2 mb-2 px-4">
<div className="flex items-center justify-center">
<Select
value={locale}
onChange={(val) => changeLocale(val as 'en' | 'de')}
dropDirection="up"
showArrow={false}
fullWidth={false} // 👈 NEU
options={[
{
value: 'en',
label: (
<span className="inline-flex items-center gap-2">
<span className="fi fi-gb" aria-hidden />
<span>{tSidebar('language.en')}</span>
</span>
)
},
{
value: 'de',
label: (
<span className="inline-flex items-center gap-2">
<span className="fi fi-de" aria-hidden />
<span>{tSidebar('language.de')}</span>
</span>
)
}
]}
placeholder="Language"
/>
</div>
</div>
<footer className="mt-auto border-t border-gray-200 dark:border-neutral-700">
<SidebarFooter />
</footer>
</div>
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[pathname, openSubmenu]
[pathname, openSubmenu, locale, tNav, tSidebar]
)
return (
@ -232,7 +289,6 @@ export default function Sidebar() {
className="absolute inset-0 bg-black/40"
onClick={() => setIsOpen(false)}
/>
{/* Panel */}
<div
className="

View File

@ -2,11 +2,12 @@
import { useEffect, useState } from 'react'
import { useSession, signIn, signOut } from 'next-auth/react'
import { signOutWithStatus } from '@/app/lib/signOutWithStatus'
import { useTranslations, useLocale } from 'next-intl'
import { signOutWithStatus } from '@/lib/signOutWithStatus'
import { useRouter, usePathname } from 'next/navigation'
import { AnimatePresence, motion } from 'framer-motion'
import Image from 'next/image'
import LoadingSpinner from '@/app/components/LoadingSpinner'
import LoadingSpinner from '../components/LoadingSpinner'
import Button from './Button'
import UserAvatarWithStatus from './UserAvatarWithStatus'
import PremierRankBadge from './PremierRankBadge'
@ -14,6 +15,10 @@ import PremierRankBadge from './PremierRankBadge'
export default function SidebarFooter() {
const router = useRouter()
const pathname = usePathname()
// Übersetzungen
const tSidebar = useTranslations('sidebar')
const { data: session, status } = useSession()
const [isOpen, setIsOpen] = useState(false)
@ -47,11 +52,11 @@ export default function SidebarFooter() {
if (status === 'unauthenticated') {
return (
<button
onClick={() => signIn('steam')}
onClick={() => signIn('steam', { callbackUrl: `/dashboard` })}
className="flex items-center justify-center gap-2 w-full py-4 px-6 bg-green-800 text-white text-md font-medium hover:bg-green-900 transition"
>
<i className="fab fa-steam" />
<span>Mit Steam anmelden</span>
<span>{tSidebar('footer.login')}</span>
</button>
)
}
@ -132,7 +137,7 @@ export default function SidebarFooter() {
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 9h3m-3 3h3m-3 3h3m-6 1c-.306-.613-.933-1-1.618-1H7.618c-.685 0-1.312.387-1.618 1M4 5h16a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Zm7 5a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z"/>
</svg>
Profil
{tSidebar('footer.profile')}
</Button>
<Button
@ -145,7 +150,7 @@ export default function SidebarFooter() {
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 9h3m-3 3h3m-3 3h3m-6 1c-.306-.613-.933-1-1.618-1H7.618c-.685 0-1.312.387-1.618 1M4 5h16a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Zm7 5a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z"/>
</svg>
Team
{tSidebar('footer.team')}
</Button>
<Button
@ -158,7 +163,7 @@ export default function SidebarFooter() {
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M10 19H5a1 1 0 0 1-1-1v-1a3 3 0 0 1 3-3h2m10 1a3 3 0 0 1-3 3m3-3a3 3 0 0 0-3-3m3 3h1m-4 3a3 3 0 0 1-3-3m3 3v1m-3-4a3 3 0 0 1 3-3m-3 3h-1m4-3v-1m-2.121 1.879-.707-.707m5.656 5.656-.707-.707m-4.242 0-.707.707m5.656-5.656-.707.707M12 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/>
</svg>
Einstellungen
{tSidebar('footer.settings')}
</Button>
{session?.user?.isAdmin && (
@ -172,7 +177,7 @@ export default function SidebarFooter() {
fill="currentColor">
<path transform="scale(0.046875)" d="M78.6 5C69.1-2.4 55.6-1.5 47 7L7 47c-8.5 8.5-9.4 22-2.1 31.6l80 104c4.5 5.9 11.6 9.4 19 9.4l54.1 0 109 109c-14.7 29-10 65.4 14.3 89.6l112 112c12.5 12.5 32.8 12.5 45.3 0l64-64c12.5-12.5 12.5-32.8 0-45.3l-112-112c-24.2-24.2-60.6-29-89.6-14.3l-109-109 0-54.1c0-7.5-3.5-14.5-9.4-19L78.6 5zM19.9 396.1C7.2 408.8 0 426.1 0 444.1C0 481.6 30.4 512 67.9 512c18 0 35.3-7.2 48-19.9L233.7 374.3c-7.8-20.9-9-43.6-3.6-65.1l-61.7-61.7L19.9 396.1zM512 144c0-10.5-1.1-20.7-3.2-30.5c-2.4-11.2-16.1-14.1-24.2-6l-63.9 63.9c-3 3-7.1 4.7-11.3 4.7L352 176c-8.8 0-16-7.2-16-16l0-57.4c0-4.2 1.7-8.3 4.7-11.3l63.9-63.9c8.1-8.1 5.2-21.8-6-24.2C388.7 1.1 378.5 0 368 0C288.5 0 224 64.5 224 144l0 .8 85.3 85.3c36-9.1 75.8 .5 104 28.7L429 274.5c49-23 83-72.8 83-130.5zM56 432a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z"/>
</svg>
Administration
{tSidebar('footer.administration')}
</Button>
)}
@ -187,7 +192,7 @@ export default function SidebarFooter() {
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 12H8m12 0-4 4m4-4-4-4M9 4H7a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h2"/>
</svg>
Abmelden
{tSidebar('footer.signout')}
</Button>
</div>
</motion.div>

View File

@ -4,7 +4,7 @@
import { useSortable, defaultAnimateLayoutChanges } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import MiniCard from './MiniCard'
import { Player } from '../types/team'
import { Player } from '../../../types/team'
type Props = {
player: Player

View File

@ -5,7 +5,7 @@ import { useState, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import Button from './Button'
import TeamPremierRankBadge from './TeamPremierRankBadge'
import type { Team } from '../types/team'
import type { Team } from '../../../types/team'
type Props = {
team: Team

View File

@ -8,17 +8,17 @@ import { useSession } from 'next-auth/react'
import TeamInvitationBanner from './TeamInvitationBanner'
import TeamMemberView from './TeamMemberView'
import NoTeamView from './NoTeamView'
import LoadingSpinner from '@/app/components/LoadingSpinner'
import LoadingSpinner from '../components/LoadingSpinner'
import CreateTeamButton from './CreateTeamButton'
import type { Player, Team } from '../types/team'
import type { Invitation } from '../types/invitation'
import { useSSEStore } from '@/app/lib/useSSEStore'
import type { Player, Team } from '../../../types/team'
import type { Invitation } from '../../../types/invitation'
import { useSSEStore } from '@/lib/useSSEStore'
import {
INVITE_EVENTS,
TEAM_EVENTS,
SELF_EVENTS,
isSseEventType,
} from '@/app/lib/sseEvents'
} from '@/lib/sseEvents'
type Props = {
refetchKey?: string

View File

@ -5,8 +5,8 @@ import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Button from './Button'
import TeamPremierRankBadge from './TeamPremierRankBadge'
import type { Invitation } from '../types/invitation'
import type { Team } from '../types/team'
import type { Invitation } from '../../../types/invitation'
import type { Team } from '../../../types/team'
type Props = {
invitation: Invitation

View File

@ -10,22 +10,22 @@ import SortableMiniCard from './SortableMiniCard'
import LeaveTeamModal from './LeaveTeamModal'
import InvitePlayersModal from './InvitePlayersModal'
import Modal from './Modal'
import { Player } from '../types/team'
import { Player } from '../../../types/team'
import { AnimatePresence, motion } from 'framer-motion'
import { leaveTeam, reloadTeam, renameTeam } from '@/app/lib/sse-actions'
import { leaveTeam, reloadTeam, renameTeam } from '@/lib/sse-actions'
import Button from './Button'
import Image from 'next/image'
import TeamPremierRankBadge from './TeamPremierRankBadge'
import Link from 'next/link'
import { Team } from '../types/team'
import { useTeamStore } from '../lib/stores'
import { useSSEStore } from '@/app/lib/useSSEStore'
import { Team } from '../../../types/team'
import { useTeamStore } from '@/lib/stores'
import { useSSEStore } from '@/lib/useSSEStore'
import {
TEAM_EVENTS,
SELF_EVENTS,
isSseEventType,
type SSEEventType,
} from '@/app/lib/sseEvents'
} from '@/lib/sseEvents'
type Props = {
team?: Team

View File

@ -1,7 +1,7 @@
'use client'
import PremierRankBadge from './PremierRankBadge'
import { Player } from '../types/team'
import { Player } from '../../../types/team'
type Props = {
players: Player[]

View File

@ -1,7 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
import ComboBox from '@/app/components/ComboBox'
import ComboBox from '../components/ComboBox'
export default function TeamSelector() {
const [teams, setTeams] = useState<string[]>([])

View File

@ -3,9 +3,11 @@
import React, { useEffect, useRef } from 'react'
import Link from 'next/link'
import Button from '@/app/components/Button'
import { useUiChromeStore } from '@/app/lib/useUiChromeStore'
import { MAP_OPTIONS } from '@/app/lib/mapOptions'
import Button from '../components/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'
@ -75,6 +77,12 @@ export default function TelemetryBanner({
// ▼ 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')
useEffect(() => {
if (!show) { setBannerPx(0); return }
@ -163,28 +171,28 @@ export default function TelemetryBanner({
<>
<div className="text-sm flex items-center gap-2">
<span className="inline-flex items-center gap-1 font-semibold px-2 py-0.5 rounded-md bg-white/10 ring-1 ring-white/15">
{serverLabel ?? 'CS2-Server'}
{serverLabel ?? 'CS2 Server'}
</span>
</div>
<div className="text-xs opacity-90 mt-0.5 flex flex-wrap gap-x-3 gap-y-1">
<span>Map: <span className="font-semibold">{prettyMap}</span></span>
<span>Phase: <span className="font-semibold">{prettyPhase}</span></span>
<span>Score: <span className="font-semibold">{prettyScore}</span></span>
<span>Spieler verbunden: <span className="font-semibold">{connectedCount}</span> / {totalExpected}</span>
<span>{tGameBanner("player-connected")}: <span className="font-semibold">{connectedCount}</span> / {totalExpected}</span>
</div>
</>
) : (
<>
<div className="text-sm flex items-center gap-2">
<span className="inline-flex items-center gap-1 font-semibold px-2 py-0.5 rounded-md bg-white/10 ring-1 ring-white/15">
Verbindung getrennt
{tGameBanner("disconnected")}
</span>
</div>
<div className="text-xs opacity-90 mt-0.5 flex flex-wrap gap-x-3 gap-y-1">
<span>Map: <span className="font-semibold">{prettyMap}</span></span>
<span>Phase: <span className="font-semibold">{prettyPhase}</span></span>
<span>Score: <span className="font-semibold">{prettyScore}</span></span>
<span>Spieler verbunden: <span className="font-semibold">{connectedCount}</span> / {totalExpected}</span>
<span>{tGameBanner("player-connected")}: <span className="font-semibold">{connectedCount}</span> / {totalExpected}</span>
</div>
</>
)}
@ -237,7 +245,7 @@ export default function TelemetryBanner({
variant="solid"
size="md"
onClick={() => onReconnect()}
title="Neu verbinden"
title={tGameBanner("reconnect")}
/>
)}
@ -249,7 +257,7 @@ export default function TelemetryBanner({
size="md"
className="w-12 h-12 !p-0 flex flex-col items-center justify-center leading-none"
onClick={() => onDisconnect?.()}
aria-label="Verbindung trennen"
aria-label={tGameBanner("disconnected")}
title={undefined}
>
<svg
@ -259,7 +267,7 @@ export default function TelemetryBanner({
>
<path d="M6 6L18 18M18 6L6 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
<span className="mt-0.5 text-[11px] font-medium opacity-90">Verlassen</span>
<span className="mt-0.5 text-[11px] font-medium opacity-90">{tGameBanner("quit")}</span>
</Button>
)}
</div>

View File

@ -3,13 +3,13 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useSession } from 'next-auth/react'
import { useReadyOverlayStore } from '@/app/lib/useReadyOverlayStore'
import { usePresenceStore } from '@/app/lib/usePresenceStore'
import { useTelemetryStore } from '@/app/lib/useTelemetryStore'
import { useMatchRosterStore } from '@/app/lib/useMatchRosterStore'
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 { MAP_OPTIONS } from '@/app/lib/mapOptions'
import { useSSEStore } from '@/app/lib/useSSEStore'
import { MAP_OPTIONS } from '@/lib/mapOptions'
import { useSSEStore } from '@/lib/useSSEStore'
function makeWsUrl(host?: string, port?: string, path?: string, scheme?: string) {
const h = (host ?? '').trim() || '127.0.0.1'

View File

@ -4,7 +4,7 @@
import { useEffect, useState, useMemo } from 'react'
import Image from 'next/image'
import clsx from 'clsx'
import { useSSEStore } from '@/app/lib/useSSEStore'
import { useSSEStore } from '@/lib/useSSEStore'
type Presence = 'online' | 'away' | 'offline'

View File

@ -1,5 +1,5 @@
// /src/app/components/UserHeader.tsx
import { Tabs } from '@/app/components/Tabs'
import { Tabs } from '../components/Tabs'
import PremierRankBadge from './PremierRankBadge'
type UserHeaderProps = {

View File

@ -1,17 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
import { useSession } from 'next-auth/react'
import Modal from '@/app/components/Modal'
import Select from '@/app/components/Select'
import Input from '../Input'
import Button from '../Button'
import DatePickerWithTime from '../DatePickerWithTime'
import Link from 'next/link'
import Image from 'next/image'
import Switch from '../Switch'
import CommunityMatchList from '../CommunityMatchList'
import Card from '../Card'
function getRoundedDate() {
const now = new Date()

View File

@ -1,5 +1,5 @@
// /src/app/admin/server/ServerView.tsx
import Button from '@/app/components/Button' // ⬅️ neu
import Button from '../../Button'
type ServerConfigShape = {
serverIp: string

View File

@ -4,11 +4,11 @@
import { useEffect, useState, useRef, useCallback } from 'react'
import { useSession } from 'next-auth/react'
import Button from '@/app/components/Button'
import Modal from '@/app/components/Modal'
import Input from '@/app/components/Input'
import TeamCard from '@/app/components/TeamCard'
import type { Team } from '@/app/types/team'
import Button from '../../Button'
import Modal from '../../Modal'
import Input from '../../Input'
import TeamCard from '../../TeamCard'
import type { Team } from '@/types/team'
import LoadingSpinner from '../../LoadingSpinner'
export default function AdminTeamsView() {

View File

@ -8,8 +8,8 @@ import { useRouter } from 'next/navigation'
import Table from '../../../Table'
import PremierRankBadge from '../../../PremierRankBadge'
import CompRankBadge from '../../../CompRankBadge'
import { MAP_OPTIONS } from '@/app/lib/mapOptions'
import Button from '@/app/components/Button'
import { MAP_OPTIONS } from '@/lib/mapOptions'
import Button from '../../../Button'
interface Match {
id: string

View File

@ -1,10 +1,9 @@
'use client'
import { useSession } from 'next-auth/react'
import Chart from '@/app/components/Chart'
import { MatchStats } from '@/app/types/match'
import Chart from '../../../Chart'
import { MatchStats } from '@/types/match'
import Card from '../../../Card'
// import UserClips from '../../../UserClips'
type MatchStatsProps = {
stats: { matches: MatchStats[] }
@ -12,7 +11,7 @@ type MatchStatsProps = {
export default function UserProfile({ stats }: MatchStatsProps) {
const { data: session } = useSession()
const steamId = session?.user?.steamId ?? '' // ← für UserClips
const steamId = session?.user?.steamId ?? ''
const { matches } = stats

View File

@ -8,8 +8,8 @@ import StaticEffects from './StaticEffects';
import RadarHeader from './RadarHeader';
import RadarCanvas from './RadarCanvas';
import { useAvatarDirectoryStore } from '@/app/lib/useAvatarDirectoryStore';
import { useTelemetryStore } from '@/app/lib/useTelemetryStore';
import { useAvatarDirectoryStore } from '../../lib/useAvatarDirectoryStore';
import { useTelemetryStore } from '../../lib/useTelemetryStore';
import { useBombBeep } from './hooks/useBombBeep';
import { useOverview } from './hooks/useOverview';

View File

@ -1,7 +1,7 @@
// /src/app/radar/TeamSidebar.tsx
'use client'
import React, { useEffect, useState } from 'react'
import { useAvatarDirectoryStore } from '@/app/lib/useAvatarDirectoryStore'
import { useAvatarDirectoryStore } from '../../lib/useAvatarDirectoryStore'
export type Team = 'T' | 'CT'

View File

@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { BombState } from '../lib/types';
import { BombState } from '@/lib/types';
const BOMB_FUSE_MS = 40_000;

View File

@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { Mapper, Overview } from '../lib/types';
import { defaultWorldToPx, parseOverviewJson, parseValveKvOverview } from '../lib/helpers';
import { Mapper, Overview } from '@/lib/types';
import { defaultWorldToPx, parseOverviewJson, parseValveKvOverview } from '@/lib/helpers';
export function useOverview(activeMapKey: string | null, playersForAutoFit: {x:number;y:number}[]) {
const [overview, setOverview] = useState<Overview | null>(null);

View File

@ -1,8 +1,8 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { BombState, DeathMarker, Grenade, PlayerState, Score, Trail, WsStatus } from '../lib/types';
import { UI } from '../lib/ui';
import { asNum, mapTeam, steamIdOf } from '../lib/helpers';
import { normalizeGrenades } from '../lib/grenades';
import { BombState, DeathMarker, Grenade, PlayerState, Score, Trail, WsStatus } from '@/lib/types';
import { UI } from '@/lib/ui';
import { asNum, mapTeam, steamIdOf } from '@/lib/helpers';
import { normalizeGrenades } from '@/lib/grenades';
export function useRadarState(mySteamId: string | null) {
// WS / Map

View File

@ -4,11 +4,19 @@
import { useEffect, useState } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter, usePathname } from '@/i18n/navigation'
import { useTranslations, useLocale } from 'next-intl'
export default function Dashboard() {
const { data: session, status } = useSession()
const [teams, setTeams] = useState<string[]>([])
const [selectedTeam, setSelectedTeam] = useState('')
const router = useRouter()
const pathname = usePathname()
const locale = useLocale()
const tDashboard = useTranslations('dashboard')
// Teams laden (robust)
useEffect(() => {
@ -44,7 +52,7 @@ export default function Dashboard() {
return (
<>
<h1 className="text-2xl font-bold mb-4 text-gray-800 dark:text-white">
Willkommen im Dashboard!
{tDashboard('title')}
</h1>
{/* Beispiel: Teams anzeigen (optional) */}

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Some files were not shown because too many files have changed in this diff Show More