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 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
16
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
@ -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 (
|
||||
@ -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'
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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 (
|
||||
@ -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(() => {
|
||||
@ -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) => {
|
||||
@ -1,5 +1,6 @@
|
||||
// CompRankBadge.tsx
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
@ -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 = {
|
||||
@ -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 }
|
||||
|
||||
@ -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'
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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 =================== */
|
||||
|
||||
@ -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'
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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'
|
||||
@ -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[]
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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>()
|
||||
@ -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>
|
||||
@ -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="
|
||||
@ -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>
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import PremierRankBadge from './PremierRankBadge'
|
||||
import { Player } from '../types/team'
|
||||
import { Player } from '../../../types/team'
|
||||
|
||||
type Props = {
|
||||
players: Player[]
|
||||
@ -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[]>([])
|
||||
@ -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>
|
||||
@ -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'
|
||||
@ -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'
|
||||
|
||||
@ -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 = {
|
||||
@ -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()
|
||||
@ -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
|
||||
@ -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() {
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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';
|
||||
@ -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'
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
@ -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
|
||||
@ -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) */}
|
||||
|
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