updated
This commit is contained in:
parent
c692cefb22
commit
bb7ac51509
47
messages/de.json
Normal file
47
messages/de.json
Normal 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
47
messages/en.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import type { NextConfig } from 'next'
|
import type { NextConfig } from 'next'
|
||||||
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
allowedDevOrigins: ['ironieopen.local', '*.ironieopen.local'],
|
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
16
package-lock.json
generated
@ -33,7 +33,7 @@
|
|||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next": "15.3.0",
|
"next": "15.3.0",
|
||||||
"next-auth-steam": "^0.4.0",
|
"next-auth-steam": "^0.4.0",
|
||||||
"next-intl": "^4.3.4",
|
"next-intl": "^4.3.9",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
@ -6117,9 +6117,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next-intl": {
|
"node_modules/next-intl": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.9.tgz",
|
||||||
"integrity": "sha512-VWLIDlGbnL/o4LnveJTJD1NOYN8lh3ZAGTWw2krhfgg53as3VsS4jzUVnArJdqvwtlpU/2BIDbWTZ7V4o1jFEw==",
|
"integrity": "sha512-4oSROHlgy8a5Qr2vH69wxo9F6K0uc6nZM2GNzqSe6ET79DEzOmBeSijCRzD5txcI4i+XTGytu4cxFsDXLKEDpQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@ -6130,7 +6130,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "^0.5.4",
|
"@formatjs/intl-localematcher": "^0.5.4",
|
||||||
"negotiator": "^1.0.0",
|
"negotiator": "^1.0.0",
|
||||||
"use-intl": "^4.3.4"
|
"use-intl": "^4.3.9"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
|
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
|
||||||
@ -8013,9 +8013,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/use-intl": {
|
"node_modules/use-intl": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.9.tgz",
|
||||||
"integrity": "sha512-sHfiU0QeJ1rirNWRxvCyvlSh9+NczcOzRnPyMeo2rtHXhVnBsvMRjE+UG4eh3lRhCxrvcqei/I0lBxsc59on1w==",
|
"integrity": "sha512-bZu+h13HIgOvsoGleQtUe4E6gM49CRm+AH36KnJVB/qb1+Beo7jr7HNrR8YWH8oaOkQfGNm6vh0HTepxng8UTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/fast-memoize": "^2.2.0",
|
"@formatjs/fast-memoize": "^2.2.0",
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next": "15.3.0",
|
"next": "15.3.0",
|
||||||
"next-auth-steam": "^0.4.0",
|
"next-auth-steam": "^0.4.0",
|
||||||
"next-intl": "^4.3.4",
|
"next-intl": "^4.3.9",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { notFound, usePathname } from 'next/navigation'
|
import { notFound, usePathname } from 'next/navigation'
|
||||||
import Card from '@/app/components/Card'
|
import Card from '../components/Card'
|
||||||
import MatchesAdminManager from '@/app/components/admin/MatchesAdminManager'
|
import MatchesAdminManager from '../components/admin/MatchesAdminManager'
|
||||||
import AdminTeamsView from '@/app/components/admin/teams/AdminTeamsView'
|
import AdminTeamsView from '../components/admin/teams/AdminTeamsView'
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Tabs } from '@/app/components/Tabs'
|
import { Tabs } from '../components/Tabs'
|
||||||
import Tab from '@/app/components/Tab'
|
import Tab from '../components/Tab'
|
||||||
|
|
||||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@ -1,12 +1,12 @@
|
|||||||
// /src/app/admin/server/page.tsx
|
// /src/app/admin/server/page.tsx
|
||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth'
|
||||||
import { authOptions } from '@/app/lib/auth'
|
import { authOptions } from '@/lib/auth'
|
||||||
import { prisma } from '@/app/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
import Card from '@/app/components/Card'
|
import Card from '../components/Card'
|
||||||
import ServerView from '@/app/components/admin/server/ServerView'
|
import ServerView from '../components/admin/server/ServerView'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import LoadingSpinner from '@/app/components/LoadingSpinner'
|
import LoadingSpinner from '../components/LoadingSpinner'
|
||||||
import TeamMemberView from '@/app/components/TeamMemberView'
|
import TeamMemberView from '../components/TeamMemberView'
|
||||||
import { useTeamStore } from '@/app/lib/stores'
|
import { useTeamStore } from '@/lib/stores'
|
||||||
import { reloadTeam } from '@/app/lib/sse-actions'
|
import { reloadTeam } from '@/lib/sse-actions'
|
||||||
import type { Player } from '@/app/types/team'
|
import type { Player } from '../types/team'
|
||||||
|
|
||||||
type Props = { teamId: string }
|
type Props = { teamId: string }
|
||||||
|
|
||||||
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import Card from '@/app/components/Card'
|
import Card from '../components/Card'
|
||||||
import AdminTeamsView from '@/app/components/admin/teams/AdminTeamsView'
|
import AdminTeamsView from '../components/admin/teams/AdminTeamsView'
|
||||||
|
|
||||||
export default function AdminTeamsPage() {
|
export default function AdminTeamsPage() {
|
||||||
return (
|
return (
|
||||||
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { sound } from '@/app/lib/soundManager'
|
import { sound } from '@/lib/soundManager';
|
||||||
|
|
||||||
export default function AudioPrimer() {
|
export default function AudioPrimer() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -1,17 +1,18 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter, usePathname } from '@/i18n/navigation'
|
||||||
import Link from 'next/link'
|
import { useTranslations, useLocale } from 'next-intl'
|
||||||
import Image from 'next/image'
|
import Link from 'next/link'
|
||||||
import { format } from 'date-fns' // 👈 neu
|
import Image from 'next/image'
|
||||||
import { de } from 'date-fns/locale'
|
import { format } from 'date-fns'
|
||||||
import Switch from '@/app/components/Switch'
|
import { de } from 'date-fns/locale'
|
||||||
import Button from './Button'
|
import Switch from '../components/Switch'
|
||||||
import Modal from './Modal'
|
import Button from './Button'
|
||||||
import { Match } from '../types/match'
|
import Modal from './Modal'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { Match } from '../../../types/match'
|
||||||
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
|
|
||||||
type Props = { matchType?: string }
|
type Props = { matchType?: string }
|
||||||
|
|
||||||
@ -57,7 +58,12 @@ function getMapVoteState(m: Match, nowMs: number) {
|
|||||||
|
|
||||||
export default function CommunityMatchList({ matchType }: Props) {
|
export default function CommunityMatchList({ matchType }: Props) {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const locale = useLocale()
|
||||||
|
|
||||||
|
const tMatches = useTranslations('matches')
|
||||||
|
|
||||||
const { lastEvent } = useSSEStore()
|
const { lastEvent } = useSSEStore()
|
||||||
|
|
||||||
const [matches, setMatches] = useState<Match[]>([])
|
const [matches, setMatches] = useState<Match[]>([])
|
||||||
@ -272,18 +278,18 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
{/* Kopfzeile */}
|
{/* Kopfzeile */}
|
||||||
<div className="flex items-center justify-between flex-wrap gap-y-4">
|
<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">
|
<h1 className="text-2xl font-bold text-gray-700 dark:text-neutral-300">
|
||||||
Geplante Matches
|
{tMatches("title")}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Switch
|
<Switch
|
||||||
id="only-own-team"
|
id="only-own-team"
|
||||||
checked={onlyOwn}
|
checked={onlyOwn}
|
||||||
onChange={setOnlyOwn}
|
onChange={setOnlyOwn}
|
||||||
labelRight="Nur mein Team anzeigen"
|
labelRight={tMatches("filter")}
|
||||||
/>
|
/>
|
||||||
{session?.user?.isAdmin && (
|
{session?.user?.isAdmin && (
|
||||||
<Button color="blue" onClick={() => setShowCreate(true)}>
|
<Button color="blue" onClick={() => setShowCreate(true)}>
|
||||||
Match erstellen
|
{tMatches("create-match")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -291,7 +297,7 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
|
|
||||||
{/* Inhalt */}
|
{/* Inhalt */}
|
||||||
{grouped.length === 0 ? (
|
{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">
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||||
{grouped.map(([dateKey, dayMatches], dayIdx) => {
|
{grouped.map(([dateKey, dayMatches], dayIdx) => {
|
||||||
@ -1,5 +1,6 @@
|
|||||||
// CompRankBadge.tsx
|
// CompRankBadge.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip from './Tooltip';
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useDroppable, useDndContext } from '@dnd-kit/core'
|
import { useDroppable, useDndContext } from '@dnd-kit/core'
|
||||||
import { Player } from '../types/team'
|
import { Player } from '../../../types/team'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
type DroppableZoneProps = {
|
type DroppableZoneProps = {
|
||||||
@ -2,10 +2,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import Modal from '@/app/components/Modal'
|
import Modal from '../components/Modal'
|
||||||
import Alert from '@/app/components/Alert'
|
import Alert from '../components/Alert'
|
||||||
import Select from '@/app/components/Select'
|
import Select from '../components/Select'
|
||||||
import LoadingSpinner from '@/app/components/LoadingSpinner' // ⬅️ NEU
|
import LoadingSpinner from '../components/LoadingSpinner' // ⬅️ NEU
|
||||||
|
|
||||||
type TeamOption = { id: string; name: string; logo?: string | null }
|
type TeamOption = { id: string; name: string; logo?: string | null }
|
||||||
|
|
||||||
@ -14,12 +14,12 @@ import {
|
|||||||
SortableContext, verticalListSortingStrategy,
|
SortableContext, verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable'
|
} from '@dnd-kit/sortable'
|
||||||
|
|
||||||
import Modal from '@/app/components/Modal'
|
import Modal from '../components/Modal'
|
||||||
import SortableMiniCard from '@/app/components/SortableMiniCard'
|
import SortableMiniCard from '../components/SortableMiniCard'
|
||||||
import LoadingSpinner from '@/app/components/LoadingSpinner'
|
import LoadingSpinner from '../components/LoadingSpinner'
|
||||||
import { DroppableZone } from '@/app/components/DroppableZone'
|
import { DroppableZone } from '../components/DroppableZone'
|
||||||
|
|
||||||
import type { Player, Team } from '@/app/types/team'
|
import type { Player, Team } from '../../../types/team'
|
||||||
|
|
||||||
/* ───────────────────────── Typen ────────────────────────── */
|
/* ───────────────────────── Typen ────────────────────────── */
|
||||||
export type EditSide = 'A' | 'B'
|
export type EditSide = 'A' | 'B'
|
||||||
@ -5,7 +5,7 @@ import Modal from './Modal'
|
|||||||
import MiniCard from './MiniCard'
|
import MiniCard from './MiniCard'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import LoadingSpinner from './LoadingSpinner'
|
import LoadingSpinner from './LoadingSpinner'
|
||||||
import { Player, Team } from '../types/team'
|
import { Player, Team } from '../../../types/team'
|
||||||
import Pagination from './Pagination'
|
import Pagination from './Pagination'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
|
||||||
@ -4,8 +4,8 @@ import { useState, useEffect } from 'react'
|
|||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import MiniCard from './MiniCard'
|
import MiniCard from './MiniCard'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { Player, Team } from '../types/team'
|
import { Player, Team } from '../../../types/team'
|
||||||
import { leaveTeam } from '../lib/sse-actions'
|
import { leaveTeam } from '@/lib/sse-actions'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
show: boolean
|
show: boolean
|
||||||
@ -4,8 +4,8 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
import type { MapVoteState } from '../types/mapvote'
|
import type { MapVoteState } from '../../../types/mapvote'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: any
|
match: any
|
||||||
@ -7,15 +7,15 @@ import { useRouter } from 'next/navigation'
|
|||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
import { useReadyOverlayStore } from '@/app/lib/useReadyOverlayStore'
|
import { useReadyOverlayStore } from '@/lib/useReadyOverlayStore'
|
||||||
import MapVoteProfileCard from './MapVoteProfileCard'
|
import MapVoteProfileCard from './MapVoteProfileCard'
|
||||||
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import LoadingSpinner from './LoadingSpinner'
|
import LoadingSpinner from './LoadingSpinner'
|
||||||
import type { Match, MatchPlayer } from '../types/match'
|
import type { Match, MatchPlayer } from '../../../types/match'
|
||||||
import type { MapVoteState } from '../types/mapvote'
|
import type { MapVoteState } from '../../../types/mapvote'
|
||||||
import { MAP_OPTIONS } from '../lib/mapOptions'
|
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||||
|
|
||||||
/* =================== Utilities & constants =================== */
|
/* =================== Utilities & constants =================== */
|
||||||
|
|
||||||
@ -14,12 +14,12 @@ import EditMatchMetaModal from './EditMatchMetaModal'
|
|||||||
import EditMatchPlayersModal from './EditMatchPlayersModal'
|
import EditMatchPlayersModal from './EditMatchPlayersModal'
|
||||||
import type { EditSide } 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 Button from './Button'
|
||||||
import { MAP_OPTIONS } from '../lib/mapOptions'
|
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||||
import MapVoteBanner from './MapVoteBanner'
|
import MapVoteBanner from './MapVoteBanner'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
import { Team } from '../types/team'
|
import { Team } from '../../../types/team'
|
||||||
import Alert from './Alert'
|
import Alert from './Alert'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import Table from './Table'
|
import Table from './Table'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { MatchPlayer } from '@/app/types/match'
|
import { MatchPlayer } from '../../../types/match'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
player: MatchPlayer
|
player: MatchPlayer
|
||||||
@ -2,11 +2,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { sound } from '@/app/lib/soundManager'
|
import { sound } from '@/lib/soundManager'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import LoadingSpinner from './LoadingSpinner'
|
import LoadingSpinner from './LoadingSpinner'
|
||||||
import { MAP_OPTIONS } from '../lib/mapOptions'
|
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Team } from '@/app/types/team'
|
import { Team } from '../../../types/team'
|
||||||
import { MatchPlayer } from '../types/match'
|
import { MatchPlayer } from '../../../types/match'
|
||||||
import MatchPlayerCard from './MatchPlayerCard'
|
import MatchPlayerCard from './MatchPlayerCard'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
@ -3,9 +3,9 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import TeamCard from './TeamCard'
|
import TeamCard from './TeamCard'
|
||||||
import type { Team, Player } from '../types/team'
|
import type { Team, Player } from '../../../types/team'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
import { TEAM_EVENTS, INVITE_EVENTS } from '../lib/sseEvents'
|
import { TEAM_EVENTS, INVITE_EVENTS } from '@/lib/sseEvents'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialTeams: Team[]
|
initialTeams: Team[]
|
||||||
@ -4,9 +4,9 @@ import { useEffect, useState, useRef } from 'react'
|
|||||||
import NotificationCenter from './NotificationCenter'
|
import NotificationCenter from './NotificationCenter'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
import { NOTIFICATION_EVENTS, isSseEventType } from '../lib/sseEvents'
|
import { NOTIFICATION_EVENTS, isSseEventType } from '@/lib/sseEvents'
|
||||||
import { useUiChromeStore } from '@/app/lib/useUiChromeStore'
|
import { useUiChromeStore } from '@/lib/useUiChromeStore'
|
||||||
|
|
||||||
type Notification = {
|
type Notification = {
|
||||||
id: string
|
id: string
|
||||||
@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { Player, Team } from '@/app/types/team'
|
import { Player, Team } from '../../../types/team'
|
||||||
|
|
||||||
export type CardWidth =
|
export type CardWidth =
|
||||||
| 'sm' // max-w-sm (24rem)
|
| 'sm' // max-w-sm (24rem)
|
||||||
@ -3,10 +3,10 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
import MatchReadyOverlay from './MatchReadyOverlay'
|
import MatchReadyOverlay from './MatchReadyOverlay'
|
||||||
import { useReadyOverlayStore } from '@/app/lib/useReadyOverlayStore'
|
import { useReadyOverlayStore } from '@/lib/useReadyOverlayStore'
|
||||||
import { useMatchRosterStore } from '@/app/lib/useMatchRosterStore' // ⬅️ neu
|
import { useMatchRosterStore } from '@/lib/useMatchRosterStore'
|
||||||
|
|
||||||
// ---- kleiner In-Memory Cache für connectHref pro matchId
|
// ---- kleiner In-Memory Cache für connectHref pro matchId
|
||||||
const CONNECT_CACHE = new Map<string | undefined, string>()
|
const CONNECT_CACHE = new Map<string | undefined, string>()
|
||||||
@ -1,8 +1,9 @@
|
|||||||
// Select.tsx
|
// Select.tsx
|
||||||
import { useState, useRef, useEffect, useMemo } from "react";
|
import React, { useState, useRef, useEffect, useMemo } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
type Option = { value: string; label: string };
|
type Option = { value: string; label: React.ReactNode };
|
||||||
|
|
||||||
type SelectProps = {
|
type SelectProps = {
|
||||||
options: Option[];
|
options: Option[];
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@ -10,6 +11,8 @@ type SelectProps = {
|
|||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
dropDirection?: "up" | "down" | "auto";
|
dropDirection?: "up" | "down" | "auto";
|
||||||
className?: string;
|
className?: string;
|
||||||
|
showArrow?: boolean;
|
||||||
|
fullWidth?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Select({
|
export default function Select({
|
||||||
@ -18,7 +21,9 @@ export default function Select({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
dropDirection = "down",
|
dropDirection = "down",
|
||||||
className
|
className,
|
||||||
|
showArrow = true,
|
||||||
|
fullWidth = true,
|
||||||
}: SelectProps) {
|
}: SelectProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [direction, setDirection] = useState<"up" | "down">("down");
|
const [direction, setDirection] = useState<"up" | "down">("down");
|
||||||
@ -26,7 +31,7 @@ export default function Select({
|
|||||||
|
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
const buttonRef = useRef<HTMLButtonElement>(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]);
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [open, dropDirection]);
|
}, [open, dropDirection]);
|
||||||
|
|
||||||
// Click-outside: ignoriert Klicks im Portal-Menü
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handlePointerDown = (event: MouseEvent) => {
|
const handlePointerDown = (event: MouseEvent) => {
|
||||||
const t = event.target as Node;
|
const t = event.target as Node;
|
||||||
@ -85,7 +89,7 @@ export default function Select({
|
|||||||
const Menu = open
|
const Menu = open
|
||||||
? createPortal(
|
? createPortal(
|
||||||
<ul
|
<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"
|
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={{
|
style={{
|
||||||
left: coords.left,
|
left: coords.left,
|
||||||
@ -97,11 +101,8 @@ export default function Select({
|
|||||||
{options.map(option => (
|
{options.map(option => (
|
||||||
<li
|
<li
|
||||||
key={option.value}
|
key={option.value}
|
||||||
onClick={() => {
|
onClick={() => { onChange(option.value); setOpen(false); }}
|
||||||
onChange(option.value);
|
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 ${
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
className={`py-2 px-4 cursor-pointer 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" : ""
|
option.value === value ? "bg-gray-100 dark:bg-neutral-800 font-medium" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -119,14 +120,25 @@ export default function Select({
|
|||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen(prev => !prev)}
|
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="inline-flex items-center gap-2">
|
||||||
<span className="absolute top-1/2 right-3 -translate-y-1/2 pointer-events-none">
|
{selectedOption ? selectedOption.label : placeholder}
|
||||||
<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>
|
</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>
|
</button>
|
||||||
{Menu}
|
{Menu}
|
||||||
</div>
|
</div>
|
||||||
@ -1,19 +1,28 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useMemo } from 'react'
|
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 Button from './Button'
|
||||||
import SidebarFooter from './SidebarFooter'
|
import SidebarFooter from './SidebarFooter'
|
||||||
|
import Select from './Select';
|
||||||
|
import 'flag-icons/css/flag-icons.min.css';
|
||||||
|
|
||||||
type Submenu = 'teams' | 'players' | null
|
type Submenu = 'teams' | 'players' | null
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
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)
|
const [openSubmenu, setOpenSubmenu] = useState<Submenu>(null)
|
||||||
|
|
||||||
|
// Aktive Route prüfen (pathname kommt schon ohne Locale)
|
||||||
const isActive = (path: string) => pathname === path
|
const isActive = (path: string) => pathname === path
|
||||||
|
|
||||||
const navBtnBase =
|
const navBtnBase =
|
||||||
@ -28,12 +37,22 @@ export default function Sidebar() {
|
|||||||
const toggleSubmenu = (key: Exclude<Submenu, null>) =>
|
const toggleSubmenu = (key: Exclude<Submenu, null>) =>
|
||||||
setOpenSubmenu(prev => (prev === key ? null : key))
|
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)
|
// Gemeinsamer Inhalt (wird in Desktop-Aside und im Mobile-Drawer benutzt)
|
||||||
const SidebarInner = useMemo(
|
const SidebarInner = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<div className="flex flex-col h-full min-h-0">
|
<div className="flex flex-col h-full min-h-0">
|
||||||
<header className="p-4 flex items-center justify-between">
|
<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 */}
|
{/* Close-Button nur im mobilen Drawer sichtbar */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
@ -55,12 +74,13 @@ export default function Sidebar() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="link"
|
variant="link"
|
||||||
className={`${navBtnBase} ${isActive('/dashboard') ? activeClasses : idleClasses}`}
|
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">
|
<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="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
|
||||||
<path d="M9 22V12h6v10" />
|
<path d="M9 22V12h6v10" />
|
||||||
</svg>
|
</svg>
|
||||||
Dashboard
|
{tNav('dashboard')}
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@ -71,12 +91,14 @@ export default function Sidebar() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="link"
|
variant="link"
|
||||||
className={`${navBtnBase} ${idleClasses} justify-between`}
|
className={`${navBtnBase} ${idleClasses} justify-between`}
|
||||||
|
aria-expanded={openSubmenu === 'teams'}
|
||||||
|
aria-controls="submenu-teams"
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-x-3.5">
|
<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" />
|
<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>
|
</svg>
|
||||||
Teams
|
{tNav('teams.label')}
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<svg
|
||||||
className={`size-4 transition-transform ${openSubmenu === 'teams' ? 'rotate-180' : ''}`}
|
className={`size-4 transition-transform ${openSubmenu === 'teams' ? 'rotate-180' : ''}`}
|
||||||
@ -87,7 +109,7 @@ export default function Sidebar() {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{openSubmenu === 'teams' && (
|
{openSubmenu === 'teams' && (
|
||||||
<ul className="pl-6 space-y-1 mt-1">
|
<ul id="submenu-teams" className="pl-6 space-y-1 mt-1">
|
||||||
<li>
|
<li>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => { router.push('/teams'); setIsOpen(false) }}
|
onClick={() => { router.push('/teams'); setIsOpen(false) }}
|
||||||
@ -95,7 +117,7 @@ export default function Sidebar() {
|
|||||||
variant="link"
|
variant="link"
|
||||||
className="w-full text-start text-sm px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-neutral-700"
|
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>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@ -105,7 +127,7 @@ export default function Sidebar() {
|
|||||||
variant="link"
|
variant="link"
|
||||||
className="w-full text-start text-sm px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-neutral-700"
|
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>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -119,6 +141,8 @@ export default function Sidebar() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="link"
|
variant="link"
|
||||||
className={`${navBtnBase} ${idleClasses} justify-between`}
|
className={`${navBtnBase} ${idleClasses} justify-between`}
|
||||||
|
aria-expanded={openSubmenu === 'players'}
|
||||||
|
aria-controls="submenu-players"
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-x-3.5">
|
<span className="flex items-center gap-x-3.5">
|
||||||
<svg className="size-4" viewBox="0 0 24 24" fill="currentColor">
|
<svg className="size-4" viewBox="0 0 24 24" fill="currentColor">
|
||||||
@ -128,7 +152,7 @@ export default function Sidebar() {
|
|||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Spieler
|
{tNav('players.label')}
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<svg
|
||||||
className={`size-4 transition-transform ${openSubmenu === 'players' ? 'rotate-180' : ''}`}
|
className={`size-4 transition-transform ${openSubmenu === 'players' ? 'rotate-180' : ''}`}
|
||||||
@ -139,7 +163,7 @@ export default function Sidebar() {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{openSubmenu === 'players' && (
|
{openSubmenu === 'players' && (
|
||||||
<ul className="pl-6 space-y-1 mt-1">
|
<ul id="submenu-players" className="pl-6 space-y-1 mt-1">
|
||||||
<li>
|
<li>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => { router.push('/players'); setIsOpen(false) }}
|
onClick={() => { router.push('/players'); setIsOpen(false) }}
|
||||||
@ -147,7 +171,7 @@ export default function Sidebar() {
|
|||||||
variant="link"
|
variant="link"
|
||||||
className="w-full text-start text-sm px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-neutral-700"
|
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>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@ -157,7 +181,7 @@ export default function Sidebar() {
|
|||||||
variant="link"
|
variant="link"
|
||||||
className="w-full text-start text-sm px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-neutral-700"
|
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>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -179,19 +203,52 @@ export default function Sidebar() {
|
|||||||
<line x1="3" x2="21" y1="10" y2="10" />
|
<line x1="3" x2="21" y1="10" y2="10" />
|
||||||
<path d="M8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01M16 18h.01" />
|
<path d="M8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01M16 18h.01" />
|
||||||
</svg>
|
</svg>
|
||||||
Spielplan
|
{tNav('schedule')}
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</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">
|
<footer className="mt-auto border-t border-gray-200 dark:border-neutral-700">
|
||||||
<SidebarFooter />
|
<SidebarFooter />
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
[pathname, openSubmenu, locale, tNav, tSidebar]
|
||||||
[pathname, openSubmenu]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -232,7 +289,6 @@ export default function Sidebar() {
|
|||||||
className="absolute inset-0 bg-black/40"
|
className="absolute inset-0 bg-black/40"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Panel */}
|
{/* Panel */}
|
||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useSession, signIn, signOut } from 'next-auth/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 { useRouter, usePathname } from 'next/navigation'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import LoadingSpinner from '@/app/components/LoadingSpinner'
|
import LoadingSpinner from '../components/LoadingSpinner'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import UserAvatarWithStatus from './UserAvatarWithStatus'
|
import UserAvatarWithStatus from './UserAvatarWithStatus'
|
||||||
import PremierRankBadge from './PremierRankBadge'
|
import PremierRankBadge from './PremierRankBadge'
|
||||||
@ -14,6 +15,10 @@ import PremierRankBadge from './PremierRankBadge'
|
|||||||
export default function SidebarFooter() {
|
export default function SidebarFooter() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|
||||||
|
// Übersetzungen
|
||||||
|
const tSidebar = useTranslations('sidebar')
|
||||||
|
|
||||||
const { data: session, status } = useSession()
|
const { data: session, status } = useSession()
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
@ -47,11 +52,11 @@ export default function SidebarFooter() {
|
|||||||
if (status === 'unauthenticated') {
|
if (status === 'unauthenticated') {
|
||||||
return (
|
return (
|
||||||
<button
|
<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"
|
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" />
|
<i className="fab fa-steam" />
|
||||||
<span>Mit Steam anmelden</span>
|
<span>{tSidebar('footer.login')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -132,7 +137,7 @@ export default function SidebarFooter() {
|
|||||||
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
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"/>
|
<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>
|
</svg>
|
||||||
Profil
|
{tSidebar('footer.profile')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -145,7 +150,7 @@ export default function SidebarFooter() {
|
|||||||
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
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"/>
|
<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>
|
</svg>
|
||||||
Team
|
{tSidebar('footer.team')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -158,7 +163,7 @@ export default function SidebarFooter() {
|
|||||||
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
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"/>
|
<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>
|
</svg>
|
||||||
Einstellungen
|
{tSidebar('footer.settings')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{session?.user?.isAdmin && (
|
{session?.user?.isAdmin && (
|
||||||
@ -172,7 +177,7 @@ export default function SidebarFooter() {
|
|||||||
fill="currentColor">
|
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"/>
|
<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>
|
</svg>
|
||||||
Administration
|
{tSidebar('footer.administration')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -187,7 +192,7 @@ export default function SidebarFooter() {
|
|||||||
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
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"/>
|
<path d="M20 12H8m12 0-4 4m4-4-4-4M9 4H7a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h2"/>
|
||||||
</svg>
|
</svg>
|
||||||
Abmelden
|
{tSidebar('footer.signout')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -4,7 +4,7 @@
|
|||||||
import { useSortable, defaultAnimateLayoutChanges } from '@dnd-kit/sortable'
|
import { useSortable, defaultAnimateLayoutChanges } from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import MiniCard from './MiniCard'
|
import MiniCard from './MiniCard'
|
||||||
import { Player } from '../types/team'
|
import { Player } from '../../../types/team'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
player: Player
|
player: Player
|
||||||
@ -5,7 +5,7 @@ import { useState, useMemo } from 'react'
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
||||||
import type { Team } from '../types/team'
|
import type { Team } from '../../../types/team'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
team: Team
|
team: Team
|
||||||
@ -8,17 +8,17 @@ import { useSession } from 'next-auth/react'
|
|||||||
import TeamInvitationBanner from './TeamInvitationBanner'
|
import TeamInvitationBanner from './TeamInvitationBanner'
|
||||||
import TeamMemberView from './TeamMemberView'
|
import TeamMemberView from './TeamMemberView'
|
||||||
import NoTeamView from './NoTeamView'
|
import NoTeamView from './NoTeamView'
|
||||||
import LoadingSpinner from '@/app/components/LoadingSpinner'
|
import LoadingSpinner from '../components/LoadingSpinner'
|
||||||
import CreateTeamButton from './CreateTeamButton'
|
import CreateTeamButton from './CreateTeamButton'
|
||||||
import type { Player, Team } from '../types/team'
|
import type { Player, Team } from '../../../types/team'
|
||||||
import type { Invitation } from '../types/invitation'
|
import type { Invitation } from '../../../types/invitation'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
import {
|
import {
|
||||||
INVITE_EVENTS,
|
INVITE_EVENTS,
|
||||||
TEAM_EVENTS,
|
TEAM_EVENTS,
|
||||||
SELF_EVENTS,
|
SELF_EVENTS,
|
||||||
isSseEventType,
|
isSseEventType,
|
||||||
} from '@/app/lib/sseEvents'
|
} from '@/lib/sseEvents'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
refetchKey?: string
|
refetchKey?: string
|
||||||
@ -5,8 +5,8 @@ import { useState } from 'react'
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
||||||
import type { Invitation } from '../types/invitation'
|
import type { Invitation } from '../../../types/invitation'
|
||||||
import type { Team } from '../types/team'
|
import type { Team } from '../../../types/team'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
invitation: Invitation
|
invitation: Invitation
|
||||||
@ -10,22 +10,22 @@ import SortableMiniCard from './SortableMiniCard'
|
|||||||
import LeaveTeamModal from './LeaveTeamModal'
|
import LeaveTeamModal from './LeaveTeamModal'
|
||||||
import InvitePlayersModal from './InvitePlayersModal'
|
import InvitePlayersModal from './InvitePlayersModal'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import { Player } from '../types/team'
|
import { Player } from '../../../types/team'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
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 Button from './Button'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { Team } from '../types/team'
|
import { Team } from '../../../types/team'
|
||||||
import { useTeamStore } from '../lib/stores'
|
import { useTeamStore } from '@/lib/stores'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
import {
|
import {
|
||||||
TEAM_EVENTS,
|
TEAM_EVENTS,
|
||||||
SELF_EVENTS,
|
SELF_EVENTS,
|
||||||
isSseEventType,
|
isSseEventType,
|
||||||
type SSEEventType,
|
type SSEEventType,
|
||||||
} from '@/app/lib/sseEvents'
|
} from '@/lib/sseEvents'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
team?: Team
|
team?: Team
|
||||||
@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import PremierRankBadge from './PremierRankBadge'
|
import PremierRankBadge from './PremierRankBadge'
|
||||||
import { Player } from '../types/team'
|
import { Player } from '../../../types/team'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
players: Player[]
|
players: Player[]
|
||||||
@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import ComboBox from '@/app/components/ComboBox'
|
import ComboBox from '../components/ComboBox'
|
||||||
|
|
||||||
export default function TeamSelector() {
|
export default function TeamSelector() {
|
||||||
const [teams, setTeams] = useState<string[]>([])
|
const [teams, setTeams] = useState<string[]>([])
|
||||||
@ -3,9 +3,11 @@
|
|||||||
|
|
||||||
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 '@/app/components/Button'
|
import Button from '../components/Button'
|
||||||
import { useUiChromeStore } from '@/app/lib/useUiChromeStore'
|
import { useUiChromeStore } from '@/lib/useUiChromeStore'
|
||||||
import { MAP_OPTIONS } from '@/app/lib/mapOptions'
|
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 TelemetryBannerVariant = 'connected' | 'disconnected'
|
||||||
|
|
||||||
@ -75,6 +77,12 @@ export default function TelemetryBanner({
|
|||||||
// ▼ 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
|
||||||
|
const tGameBanner = useTranslations('game-banner')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!show) { setBannerPx(0); return }
|
if (!show) { setBannerPx(0); return }
|
||||||
@ -163,28 +171,28 @@ export default function TelemetryBanner({
|
|||||||
<>
|
<>
|
||||||
<div className="text-sm flex items-center gap-2">
|
<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">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs opacity-90 mt-0.5 flex flex-wrap gap-x-3 gap-y-1">
|
<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>Map: <span className="font-semibold">{prettyMap}</span></span>
|
||||||
<span>Phase: <span className="font-semibold">{prettyPhase}</span></span>
|
<span>Phase: <span className="font-semibold">{prettyPhase}</span></span>
|
||||||
<span>Score: <span className="font-semibold">{prettyScore}</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>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="text-sm flex items-center gap-2">
|
<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">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs opacity-90 mt-0.5 flex flex-wrap gap-x-3 gap-y-1">
|
<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>Map: <span className="font-semibold">{prettyMap}</span></span>
|
||||||
<span>Phase: <span className="font-semibold">{prettyPhase}</span></span>
|
<span>Phase: <span className="font-semibold">{prettyPhase}</span></span>
|
||||||
<span>Score: <span className="font-semibold">{prettyScore}</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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -237,7 +245,7 @@ export default function TelemetryBanner({
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
size="md"
|
size="md"
|
||||||
onClick={() => onReconnect()}
|
onClick={() => onReconnect()}
|
||||||
title="Neu verbinden"
|
title={tGameBanner("reconnect")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -249,7 +257,7 @@ export default function TelemetryBanner({
|
|||||||
size="md"
|
size="md"
|
||||||
className="w-12 h-12 !p-0 flex flex-col items-center justify-center leading-none"
|
className="w-12 h-12 !p-0 flex flex-col items-center justify-center leading-none"
|
||||||
onClick={() => onDisconnect?.()}
|
onClick={() => onDisconnect?.()}
|
||||||
aria-label="Verbindung trennen"
|
aria-label={tGameBanner("disconnected")}
|
||||||
title={undefined}
|
title={undefined}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@ -259,7 +267,7 @@ export default function TelemetryBanner({
|
|||||||
>
|
>
|
||||||
<path d="M6 6L18 18M18 6L6 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
<path d="M6 6L18 18M18 6L6 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||||
</svg>
|
</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>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -3,13 +3,13 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { useReadyOverlayStore } from '@/app/lib/useReadyOverlayStore'
|
import { useReadyOverlayStore } from '@/lib/useReadyOverlayStore'
|
||||||
import { usePresenceStore } from '@/app/lib/usePresenceStore'
|
import { usePresenceStore } from '@/lib/usePresenceStore'
|
||||||
import { useTelemetryStore } from '@/app/lib/useTelemetryStore'
|
import { useTelemetryStore } from '@/lib/useTelemetryStore'
|
||||||
import { useMatchRosterStore } from '@/app/lib/useMatchRosterStore'
|
import { useMatchRosterStore } from '@/lib/useMatchRosterStore'
|
||||||
import TelemetryBanner from './TelemetryBanner'
|
import TelemetryBanner from './TelemetryBanner'
|
||||||
import { MAP_OPTIONS } from '@/app/lib/mapOptions'
|
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
|
|
||||||
function makeWsUrl(host?: string, port?: string, path?: string, scheme?: string) {
|
function makeWsUrl(host?: string, port?: string, path?: string, scheme?: string) {
|
||||||
const h = (host ?? '').trim() || '127.0.0.1'
|
const h = (host ?? '').trim() || '127.0.0.1'
|
||||||
@ -4,7 +4,7 @@
|
|||||||
import { useEffect, useState, useMemo } from 'react'
|
import { useEffect, useState, useMemo } from 'react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
|
|
||||||
type Presence = 'online' | 'away' | 'offline'
|
type Presence = 'online' | 'away' | 'offline'
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
// /src/app/components/UserHeader.tsx
|
// /src/app/components/UserHeader.tsx
|
||||||
import { Tabs } from '@/app/components/Tabs'
|
import { Tabs } from '../components/Tabs'
|
||||||
import PremierRankBadge from './PremierRankBadge'
|
import PremierRankBadge from './PremierRankBadge'
|
||||||
|
|
||||||
type UserHeaderProps = {
|
type UserHeaderProps = {
|
||||||
@ -1,17 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
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 CommunityMatchList from '../CommunityMatchList'
|
||||||
import Card from '../Card'
|
|
||||||
|
|
||||||
function getRoundedDate() {
|
function getRoundedDate() {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@ -1,5 +1,5 @@
|
|||||||
// /src/app/admin/server/ServerView.tsx
|
// /src/app/admin/server/ServerView.tsx
|
||||||
import Button from '@/app/components/Button' // ⬅️ neu
|
import Button from '../../Button'
|
||||||
|
|
||||||
type ServerConfigShape = {
|
type ServerConfigShape = {
|
||||||
serverIp: string
|
serverIp: string
|
||||||
@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
import { useEffect, useState, useRef, useCallback } from 'react'
|
import { useEffect, useState, useRef, useCallback } from 'react'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import Button from '@/app/components/Button'
|
import Button from '../../Button'
|
||||||
import Modal from '@/app/components/Modal'
|
import Modal from '../../Modal'
|
||||||
import Input from '@/app/components/Input'
|
import Input from '../../Input'
|
||||||
import TeamCard from '@/app/components/TeamCard'
|
import TeamCard from '../../TeamCard'
|
||||||
import type { Team } from '@/app/types/team'
|
import type { Team } from '@/types/team'
|
||||||
import LoadingSpinner from '../../LoadingSpinner'
|
import LoadingSpinner from '../../LoadingSpinner'
|
||||||
|
|
||||||
export default function AdminTeamsView() {
|
export default function AdminTeamsView() {
|
||||||
@ -8,8 +8,8 @@ import { useRouter } from 'next/navigation'
|
|||||||
import Table from '../../../Table'
|
import Table from '../../../Table'
|
||||||
import PremierRankBadge from '../../../PremierRankBadge'
|
import PremierRankBadge from '../../../PremierRankBadge'
|
||||||
import CompRankBadge from '../../../CompRankBadge'
|
import CompRankBadge from '../../../CompRankBadge'
|
||||||
import { MAP_OPTIONS } from '@/app/lib/mapOptions'
|
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||||
import Button from '@/app/components/Button'
|
import Button from '../../../Button'
|
||||||
|
|
||||||
interface Match {
|
interface Match {
|
||||||
id: string
|
id: string
|
||||||
@ -1,10 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import Chart from '@/app/components/Chart'
|
import Chart from '../../../Chart'
|
||||||
import { MatchStats } from '@/app/types/match'
|
import { MatchStats } from '@/types/match'
|
||||||
import Card from '../../../Card'
|
import Card from '../../../Card'
|
||||||
// import UserClips from '../../../UserClips'
|
|
||||||
|
|
||||||
type MatchStatsProps = {
|
type MatchStatsProps = {
|
||||||
stats: { matches: MatchStats[] }
|
stats: { matches: MatchStats[] }
|
||||||
@ -12,7 +11,7 @@ type MatchStatsProps = {
|
|||||||
|
|
||||||
export default function UserProfile({ stats }: MatchStatsProps) {
|
export default function UserProfile({ stats }: MatchStatsProps) {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const steamId = session?.user?.steamId ?? '' // ← für UserClips
|
const steamId = session?.user?.steamId ?? ''
|
||||||
|
|
||||||
const { matches } = stats
|
const { matches } = stats
|
||||||
|
|
||||||
@ -8,8 +8,8 @@ import StaticEffects from './StaticEffects';
|
|||||||
import RadarHeader from './RadarHeader';
|
import RadarHeader from './RadarHeader';
|
||||||
import RadarCanvas from './RadarCanvas';
|
import RadarCanvas from './RadarCanvas';
|
||||||
|
|
||||||
import { useAvatarDirectoryStore } from '@/app/lib/useAvatarDirectoryStore';
|
import { useAvatarDirectoryStore } from '../../lib/useAvatarDirectoryStore';
|
||||||
import { useTelemetryStore } from '@/app/lib/useTelemetryStore';
|
import { useTelemetryStore } from '../../lib/useTelemetryStore';
|
||||||
|
|
||||||
import { useBombBeep } from './hooks/useBombBeep';
|
import { useBombBeep } from './hooks/useBombBeep';
|
||||||
import { useOverview } from './hooks/useOverview';
|
import { useOverview } from './hooks/useOverview';
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// /src/app/radar/TeamSidebar.tsx
|
// /src/app/radar/TeamSidebar.tsx
|
||||||
'use client'
|
'use client'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useAvatarDirectoryStore } from '@/app/lib/useAvatarDirectoryStore'
|
import { useAvatarDirectoryStore } from '../../lib/useAvatarDirectoryStore'
|
||||||
|
|
||||||
export type Team = 'T' | 'CT'
|
export type Team = 'T' | 'CT'
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { BombState } from '../lib/types';
|
import { BombState } from '@/lib/types';
|
||||||
|
|
||||||
const BOMB_FUSE_MS = 40_000;
|
const BOMB_FUSE_MS = 40_000;
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Mapper, Overview } from '../lib/types';
|
import { Mapper, Overview } from '@/lib/types';
|
||||||
import { defaultWorldToPx, parseOverviewJson, parseValveKvOverview } from '../lib/helpers';
|
import { defaultWorldToPx, parseOverviewJson, parseValveKvOverview } from '@/lib/helpers';
|
||||||
|
|
||||||
export function useOverview(activeMapKey: string | null, playersForAutoFit: {x:number;y:number}[]) {
|
export function useOverview(activeMapKey: string | null, playersForAutoFit: {x:number;y:number}[]) {
|
||||||
const [overview, setOverview] = useState<Overview | null>(null);
|
const [overview, setOverview] = useState<Overview | null>(null);
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { BombState, DeathMarker, Grenade, PlayerState, Score, Trail, WsStatus } from '../lib/types';
|
import { BombState, DeathMarker, Grenade, PlayerState, Score, Trail, WsStatus } from '@/lib/types';
|
||||||
import { UI } from '../lib/ui';
|
import { UI } from '@/lib/ui';
|
||||||
import { asNum, mapTeam, steamIdOf } from '../lib/helpers';
|
import { asNum, mapTeam, steamIdOf } from '@/lib/helpers';
|
||||||
import { normalizeGrenades } from '../lib/grenades';
|
import { normalizeGrenades } from '@/lib/grenades';
|
||||||
|
|
||||||
export function useRadarState(mySteamId: string | null) {
|
export function useRadarState(mySteamId: string | null) {
|
||||||
// WS / Map
|
// WS / Map
|
||||||
@ -4,11 +4,19 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
|
import { useRouter, usePathname } from '@/i18n/navigation'
|
||||||
|
import { useTranslations, useLocale } from 'next-intl'
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const { data: session, status } = useSession()
|
const { data: session, status } = useSession()
|
||||||
const [teams, setTeams] = useState<string[]>([])
|
const [teams, setTeams] = useState<string[]>([])
|
||||||
const [selectedTeam, setSelectedTeam] = useState('')
|
const [selectedTeam, setSelectedTeam] = useState('')
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const locale = useLocale()
|
||||||
|
|
||||||
|
const tDashboard = useTranslations('dashboard')
|
||||||
|
|
||||||
// Teams laden (robust)
|
// Teams laden (robust)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -44,7 +52,7 @@ export default function Dashboard() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="text-2xl font-bold mb-4 text-gray-800 dark:text-white">
|
<h1 className="text-2xl font-bold mb-4 text-gray-800 dark:text-white">
|
||||||
Willkommen im Dashboard!
|
{tDashboard('title')}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Beispiel: Teams anzeigen (optional) */}
|
{/* Beispiel: Teams anzeigen (optional) */}
|
||||||
|
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
Loading…
x
Reference in New Issue
Block a user