update
This commit is contained in:
parent
f27c6feedb
commit
530425a82c
@ -54,6 +54,9 @@
|
|||||||
pterodactylClientApiKey String?
|
pterodactylClientApiKey String?
|
||||||
|
|
||||||
timeZone String? // IANA-TZ, z.B. "Europe/Berlin"
|
timeZone String? // IANA-TZ, z.B. "Europe/Berlin"
|
||||||
|
|
||||||
|
// ✅ Datenschutz: darf eingeladen werden?
|
||||||
|
canBeInvited Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserStatus {
|
enum UserStatus {
|
||||||
@ -173,7 +176,7 @@
|
|||||||
teamId String?
|
teamId String?
|
||||||
team Team? @relation(fields: [teamId], references: [id])
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
|
|
||||||
match Match @relation(fields: [matchId], references: [id])
|
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||||||
user User @relation(fields: [steamId], references: [steamId])
|
user User @relation(fields: [steamId], references: [steamId])
|
||||||
|
|
||||||
stats PlayerStats?
|
stats PlayerStats?
|
||||||
@ -239,7 +242,7 @@
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
user User @relation("UserRankHistory", fields: [steamId], references: [steamId])
|
user User @relation("UserRankHistory", fields: [steamId], references: [steamId])
|
||||||
match Match? @relation("MatchRankHistory", fields: [matchId], references: [id])
|
match Match? @relation("MatchRankHistory", fields: [matchId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
model Schedule {
|
model Schedule {
|
||||||
@ -263,7 +266,7 @@
|
|||||||
confirmedBy User? @relation("ConfirmedSchedules", fields: [confirmedById], references: [steamId])
|
confirmedBy User? @relation("ConfirmedSchedules", fields: [confirmedById], references: [steamId])
|
||||||
|
|
||||||
linkedMatchId String? @unique
|
linkedMatchId String? @unique
|
||||||
linkedMatch Match? @relation(fields: [linkedMatchId], references: [id])
|
linkedMatch Match? @relation(fields: [linkedMatchId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@ -293,7 +296,7 @@
|
|||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
match Match @relation(fields: [matchId], references: [id])
|
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||||||
user User @relation(fields: [steamId], references: [steamId])
|
user User @relation(fields: [steamId], references: [steamId])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,7 +329,7 @@
|
|||||||
model MapVote {
|
model MapVote {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
matchId String @unique
|
matchId String @unique
|
||||||
match Match @relation(fields: [matchId], references: [id])
|
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
bestOf Int @default(3)
|
bestOf Int @default(3)
|
||||||
mapPool String[]
|
mapPool String[]
|
||||||
@ -359,7 +362,7 @@
|
|||||||
chosenBy String?
|
chosenBy String?
|
||||||
chooser User? @relation("VoteStepChooser", fields: [chosenBy], references: [steamId])
|
chooser User? @relation("VoteStepChooser", fields: [chosenBy], references: [steamId])
|
||||||
|
|
||||||
vote MapVote @relation(fields: [voteId], references: [id])
|
vote MapVote @relation(fields: [voteId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([voteId, order])
|
@@unique([voteId, order])
|
||||||
@@index([teamId])
|
@@index([teamId])
|
||||||
@ -371,7 +374,7 @@
|
|||||||
steamId String
|
steamId String
|
||||||
acceptedAt DateTime @default(now())
|
acceptedAt DateTime @default(now())
|
||||||
|
|
||||||
match Match @relation("MatchReadyMatch", fields: [matchId], references: [id])
|
match Match @relation("MatchReadyMatch", fields: [matchId], references: [id], onDelete: Cascade)
|
||||||
user User @relation("MatchReadyUser", fields: [steamId], references: [steamId])
|
user User @relation("MatchReadyUser", fields: [steamId], references: [steamId])
|
||||||
|
|
||||||
@@id([matchId, steamId])
|
@@id([matchId, steamId])
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
@ -60,7 +60,7 @@ export default function Card({
|
|||||||
<div
|
<div
|
||||||
style={style}
|
style={style}
|
||||||
className={[
|
className={[
|
||||||
'flex flex-col rounded-xl border border-gray-200 bg-white shadow-2xs',
|
'flex flex-col rounded-xl border border-gray-200 bg-white shadow-2xs p-3',
|
||||||
'dark:bg-neutral-800 dark:border-neutral-700 dark:shadow-neutral-700/70',
|
'dark:bg-neutral-800 dark:border-neutral-700 dark:shadow-neutral-700/70',
|
||||||
alignClasses,
|
alignClasses,
|
||||||
widthClasses[maxWidth],
|
widthClasses[maxWidth],
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback, useMemo } from 'react'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { useRouter, usePathname } from '@/i18n/navigation'
|
import { useRouter, usePathname } from '@/i18n/navigation'
|
||||||
import { useTranslations, useLocale } from 'next-intl'
|
import { useTranslations, useLocale } from 'next-intl'
|
||||||
@ -56,14 +56,82 @@ function getMapVoteState(m: Match, nowMs: number) {
|
|||||||
return { hasVote: true as const, isOpen, opensAt, opensInMs }
|
return { hasVote: true as const, isOpen, opensAt, opensInMs }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Timezone Utils ----
|
||||||
|
type ZonedParts = {
|
||||||
|
year: number; month: number; day: number; hour: number; minute: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getUserTimeZone(sessionTz?: string): string {
|
||||||
|
return (
|
||||||
|
sessionTz ||
|
||||||
|
Intl.DateTimeFormat().resolvedOptions().timeZone ||
|
||||||
|
'Europe/Berlin'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getZonedParts(date: Date | string, timeZone: string, locale = 'de-DE'): ZonedParts {
|
||||||
|
const d = typeof date === 'string' ? new Date(date) : date;
|
||||||
|
const parts = new Intl.DateTimeFormat(locale, {
|
||||||
|
timeZone,
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit', hour12: false,
|
||||||
|
}).formatToParts(d);
|
||||||
|
|
||||||
|
const g = (t: string) => Number(parts.find(p => p.type === t)?.value ?? '0');
|
||||||
|
return { year: g('year'), month: g('month'), day: g('day'), hour: g('hour'), minute: g('minute') };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateInTZ(date: Date | string, timeZone: string, locale = 'de-DE') {
|
||||||
|
const p = getZonedParts(date, timeZone, locale);
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
return `${pad(p.day)}.${pad(p.month)}.${p.year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimeInTZ(date: Date | string, timeZone: string, locale = 'de-DE') {
|
||||||
|
const p = getZonedParts(date, timeZone, locale);
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
return `${pad(p.hour)}:${pad(p.minute)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateKeyInTZ(date: Date | string, timeZone: string): string {
|
||||||
|
const p = getZonedParts(date, timeZone);
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
return `${p.year}-${pad(p.month)}-${pad(p.day)}`; // YYYY-MM-DD
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCookieClient(name: string): string | undefined {
|
||||||
|
if (typeof document === 'undefined') return undefined
|
||||||
|
const m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'))
|
||||||
|
return m ? decodeURIComponent(m[1]) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidIanaTzClient(tz?: string): tz is string {
|
||||||
|
if (!tz) return false
|
||||||
|
try { new Intl.DateTimeFormat('en-US', { timeZone: tz }).format(0); return true } catch { return false }
|
||||||
|
}
|
||||||
|
|
||||||
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 pathname = usePathname()
|
||||||
const locale = useLocale()
|
const locale = useLocale()
|
||||||
|
const [userTZ, setUserTZ] = useState<string>(() => {
|
||||||
|
const fromCookie = readCookieClient('tz')
|
||||||
|
if (isValidIanaTzClient(fromCookie)) return fromCookie!
|
||||||
|
if (session?.user?.timeZone && isValidIanaTzClient(session.user.timeZone)) return session.user.timeZone
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'Europe/Berlin'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
console.log(userTZ);
|
||||||
|
const weekdayFmt = useMemo(() =>
|
||||||
|
new Intl.DateTimeFormat(locale === 'de' ? 'de-DE' : 'en-GB', {
|
||||||
|
weekday: 'long',
|
||||||
|
timeZone: userTZ,
|
||||||
|
}),
|
||||||
|
[locale, userTZ])
|
||||||
|
|
||||||
const tMatches = useTranslations('matches')
|
const tMatches = useTranslations('matches')
|
||||||
|
const tMapvote = useTranslations('mapvote')
|
||||||
|
|
||||||
const { lastEvent } = useSSEStore()
|
const { lastEvent } = useSSEStore()
|
||||||
|
|
||||||
@ -94,6 +162,24 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
|
|
||||||
const [now, setNow] = useState(() => Date.now())
|
const [now, setNow] = useState(() => Date.now())
|
||||||
|
|
||||||
|
// Beim Mount & Tab-Fokus Cookie neu einlesen
|
||||||
|
useEffect(() => {
|
||||||
|
const apply = () => {
|
||||||
|
const fromCookie = readCookieClient('tz')
|
||||||
|
if (isValidIanaTzClient(fromCookie)) {
|
||||||
|
setUserTZ(prev => (prev === fromCookie ? prev : fromCookie!))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apply()
|
||||||
|
const onFocus = () => apply()
|
||||||
|
window.addEventListener('focus', onFocus)
|
||||||
|
document.addEventListener('visibilitychange', onFocus)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('focus', onFocus)
|
||||||
|
document.removeEventListener('visibilitychange', onFocus)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = setInterval(() => setNow(Date.now()), 1000)
|
const id = setInterval(() => setNow(Date.now()), 1000)
|
||||||
return () => clearInterval(id)
|
return () => clearInterval(id)
|
||||||
@ -265,14 +351,14 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
const grouped = (() => {
|
const grouped = (() => {
|
||||||
const sorted = [...matches].sort(
|
const sorted = [...matches].sort(
|
||||||
(a, b) => new Date(a.demoDate).getTime() - new Date(b.demoDate).getTime(),
|
(a, b) => new Date(a.demoDate).getTime() - new Date(b.demoDate).getTime(),
|
||||||
)
|
);
|
||||||
const map = new Map<string, Match[]>()
|
const map = new Map<string, Match[]>();
|
||||||
for (const m of sorted) {
|
for (const m of sorted) {
|
||||||
const key = toDateKey(new Date(m.demoDate))
|
const key = dateKeyInTZ(m.demoDate, userTZ);
|
||||||
map.set(key, [...(map.get(key) ?? []), m])
|
map.set(key, [...(map.get(key) ?? []), m]);
|
||||||
}
|
}
|
||||||
return Array.from(map.entries())
|
return Array.from(map.entries());
|
||||||
})()
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto py-8 px-4 space-y-6">
|
<div className="max-w-7xl mx-auto py-8 px-4 space-y-6">
|
||||||
@ -302,9 +388,9 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<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) => {
|
||||||
const dateObj = new Date(dateKey + 'T00:00:00')
|
const dateObj = new Date(dateKey + 'T00:00:00');
|
||||||
const weekdayFmt = locale === 'de' ? weekdayDE : weekdayEN
|
const weekday = weekdayFmt.format(dateObj);
|
||||||
const dayLabel = `${tMatches('day')} #${dayIdx + 1} – ${weekdayFmt.format(dateObj)}`
|
const dayLabel = `${tMatches('day')} #${dayIdx + 1} – ${weekday}`;
|
||||||
return (
|
return (
|
||||||
<div key={dateKey} className="flex flex-col gap-4">
|
<div key={dateKey} className="flex flex-col gap-4">
|
||||||
<div className="bg-yellow-300 dark:bg-yellow-500 text-center py-2 font-bold tracking-wider">
|
<div className="bg-yellow-300 dark:bg-yellow-500 text-center py-2 font-bold tracking-wider">
|
||||||
@ -322,7 +408,7 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
const mv = getMapVoteState(m, now)
|
const mv = getMapVoteState(m, now)
|
||||||
const opensText =
|
const opensText =
|
||||||
mv.hasVote && !mv.isOpen && mv.opensAt
|
mv.hasVote && !mv.isOpen && mv.opensAt
|
||||||
? `${tMatches("opens-in")} ${formatCountdown(mv.opensInMs)}`
|
? `${tMapvote("opens-in")} ${formatCountdown(mv.opensInMs)}`
|
||||||
: null
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -390,19 +476,20 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
|
|
||||||
{/* Zeile 3: Datum & Uhrzeit */}
|
{/* Zeile 3: Datum & Uhrzeit */}
|
||||||
<div className="flex flex-col items-center -mt-1 space-y-1">
|
<div className="flex flex-col items-center -mt-1 space-y-1">
|
||||||
|
{/* Datum-Badge */}
|
||||||
<span
|
<span
|
||||||
className={`px-3 py-1 rounded-full text-[13px] font-bold shadow ring-1 ring-black/10
|
className={`px-3 py-1 rounded-full text-[13px] font-bold shadow ring-1 ring-black/10
|
||||||
${isLive ? 'bg-red-500 text-white' : 'bg-yellow-400 text-gray-900 dark:bg-yellow-500 dark:text-black'}
|
${isLive ? 'bg-red-500 text-white' : 'bg-yellow-400 text-gray-900 dark:bg-yellow-500 dark:text-black'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{format(new Date(m.demoDate), 'dd.MM.yyyy', { locale: de })}
|
{formatDateInTZ(m.demoDate, userTZ, locale === 'de' ? 'de-DE' : 'en-GB')}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="flex items-center gap-1 text-xs font-semibold opacity-90">
|
<span className="flex items-center gap-1 text-xs font-semibold opacity-90">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 512 512">
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 512 512">
|
||||||
<path d="M256 48a208 208 0 1 0 208 208A208.24 208.24 0 0 0 256 48Zm0 384a176 176 0 1 1 176-176 176.2 176.2 0 0 1-176 176Zm80-176h-64V144a16 16 0 0 0-32 0v120a16 16 0 0 0 16 16h80a16 16 0 0 0 0-32Z" />
|
<path d="M256 48a208 208 0 1 0 208 208A208.24 208.24 0 0 0 256 48Zm0 384a176 176 0 1 1 176-176 176.2 176.2 0 0 1-176 176Zm80-176h-64V144a16 16 0 0 0-32 0v120a16 16 0 0 0 16 16h80a16 16 0 0 0 0-32Z" />
|
||||||
</svg>
|
</svg>
|
||||||
{format(new Date(m.demoDate), 'HH:mm', { locale: de })} Uhr
|
{formatTimeInTZ(m.demoDate, userTZ, locale === 'de' ? 'de-DE' : 'en-GB')} Uhr
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
// src/app/components/GameBanner.tsx
|
// src/app/components/GameBanner.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react'
|
import React, {useEffect, useRef, useState} from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import { useUiChromeStore } from '@/lib/useUiChromeStore'
|
import {useUiChromeStore} from '@/lib/useUiChromeStore'
|
||||||
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
import {MAP_OPTIONS} from '@/lib/mapOptions'
|
||||||
import { useRouter, usePathname } from '@/i18n/navigation'
|
import { useTranslations } from 'next-intl'
|
||||||
import { useTranslations, useLocale } from 'next-intl'
|
|
||||||
|
|
||||||
export type GameBannerVariant = 'connected' | 'disconnected'
|
export type GameBannerVariant = 'connected' | 'disconnected'
|
||||||
|
|
||||||
@ -15,31 +14,25 @@ type Props = {
|
|||||||
variant: GameBannerVariant
|
variant: GameBannerVariant
|
||||||
visible: boolean
|
visible: boolean
|
||||||
zIndex?: number
|
zIndex?: number
|
||||||
// gemeinsam
|
|
||||||
connectedCount: number
|
connectedCount: number
|
||||||
totalExpected: number
|
totalExpected: number
|
||||||
connectUri: string
|
|
||||||
onReconnect: () => void
|
onReconnect: () => void
|
||||||
// neu: zum harten Trennen (X-Button)
|
|
||||||
onDisconnect?: () => void
|
onDisconnect?: () => void
|
||||||
// nur für "connected"
|
|
||||||
serverLabel?: string
|
serverLabel?: string
|
||||||
mapKey?: string
|
mapKey?: string
|
||||||
mapLabel?: string
|
mapLabel?: string
|
||||||
phase?: string
|
phase?: string
|
||||||
score?: string
|
score?: string
|
||||||
// nur für "disconnected"
|
|
||||||
missingCount?: number
|
|
||||||
// optional: wenn im Dock unter Main gerendert
|
|
||||||
inline?: boolean
|
inline?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- helpers ---------- */
|
/* ---------- helpers ---------- */
|
||||||
function hashStr(s: string): number {
|
const hashStr = (s: string) => {
|
||||||
let h = 5381
|
let h = 5381
|
||||||
for (let i = 0; i < s.length; i++) h = ((h << 5) + h) + s.charCodeAt(i)
|
for (let i = 0; i < s.length; i++) h = ((h << 5) + h) + s.charCodeAt(i)
|
||||||
return h | 0
|
return h | 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickMapImageFromOptions(mapKey?: string): string | null {
|
function pickMapImageFromOptions(mapKey?: string): string | null {
|
||||||
if (!mapKey) return null
|
if (!mapKey) return null
|
||||||
const opt = MAP_OPTIONS.find(o => o.key.toLowerCase() === mapKey.toLowerCase())
|
const opt = MAP_OPTIONS.find(o => o.key.toLowerCase() === mapKey.toLowerCase())
|
||||||
@ -47,44 +40,48 @@ function pickMapImageFromOptions(mapKey?: string): string | null {
|
|||||||
const idx = Math.abs(hashStr(mapKey)) % opt.images.length
|
const idx = Math.abs(hashStr(mapKey)) % opt.images.length
|
||||||
return opt.images[idx] ?? null
|
return opt.images[idx] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickMapIcon(mapKey?: string): string | null {
|
function pickMapIcon(mapKey?: string): string | null {
|
||||||
if (!mapKey) return null
|
if (!mapKey) return null
|
||||||
const opt = MAP_OPTIONS.find(o => o.key.toLowerCase() === mapKey.toLowerCase())
|
const opt = MAP_OPTIONS.find(o => o.key.toLowerCase() === mapKey.toLowerCase())
|
||||||
return opt?.icon ?? null
|
return opt?.icon ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Banner auf mobilen Bildschirmen gar nicht rendern (unter sm)
|
||||||
|
function useIsSmDown() {
|
||||||
|
const [smDown, setSmDown] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
const mq = window.matchMedia('(max-width: 639.98px)')
|
||||||
|
setSmDown(mq.matches)
|
||||||
|
const onChange = (e: MediaQueryListEvent) => setSmDown(e.matches)
|
||||||
|
mq.addEventListener('change', onChange)
|
||||||
|
return () => mq.removeEventListener('change', onChange)
|
||||||
|
}, [])
|
||||||
|
return smDown
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- component ---------- */
|
/* ---------- component ---------- */
|
||||||
export default function GameBanner({
|
export default function GameBanner(props: Props) {
|
||||||
variant,
|
const {
|
||||||
visible,
|
variant, visible, zIndex = 9999, connectedCount, totalExpected,
|
||||||
zIndex = 9999,
|
onReconnect, onDisconnect, serverLabel, mapKey, mapLabel, phase, score, inline = false,
|
||||||
connectedCount,
|
} = props
|
||||||
totalExpected,
|
|
||||||
connectUri,
|
|
||||||
onReconnect,
|
|
||||||
onDisconnect,
|
|
||||||
serverLabel,
|
|
||||||
mapKey,
|
|
||||||
mapLabel,
|
|
||||||
phase,
|
|
||||||
score,
|
|
||||||
missingCount,
|
|
||||||
inline = false,
|
|
||||||
}: Props) {
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null)
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
const setBannerPx = useUiChromeStore(s => s.setGameBannerPx)
|
const setBannerPx = useUiChromeStore(s => s.setGameBannerPx)
|
||||||
|
const isSmDown = useIsSmDown()
|
||||||
|
const t = useTranslations('game-banner')
|
||||||
|
|
||||||
// ▼ 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 = !isSmDown && visible && phaseStr !== 'unknown'
|
||||||
|
|
||||||
// Übersetzungen
|
|
||||||
const tGameBanner = useTranslations('game-banner')
|
|
||||||
|
|
||||||
|
// <-- Hook wird IMMER aufgerufen, macht aber nichts wenn !show
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!show) { setBannerPx(0); return }
|
|
||||||
const el = ref.current
|
const el = ref.current
|
||||||
if (!el) return
|
if (!show || !el) { // guard
|
||||||
|
setBannerPx(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
const report = () => setBannerPx(el.getBoundingClientRect().height)
|
const report = () => setBannerPx(el.getBoundingClientRect().height)
|
||||||
report()
|
report()
|
||||||
const ro = new ResizeObserver(report)
|
const ro = new ResizeObserver(report)
|
||||||
@ -92,35 +89,43 @@ export default function GameBanner({
|
|||||||
return () => { ro.disconnect(); setBannerPx(0) }
|
return () => { ro.disconnect(); setBannerPx(0) }
|
||||||
}, [show, setBannerPx])
|
}, [show, setBannerPx])
|
||||||
|
|
||||||
// Ableitungen vor dem Guard
|
// ab hier darfst du bedingt rendern
|
||||||
const outerBase = inline ? '' : 'fixed right-0 bottom-0 left-0 sm:left-[16rem]'
|
if (!show) return null
|
||||||
const outerStyle = inline ? {} : { zIndex }
|
|
||||||
|
|
||||||
const wrapperClass =
|
const outerBase = inline ? '' : 'fixed right-0 bottom-0 left-0 sm:left-[16rem]'
|
||||||
variant === 'connected'
|
const outerStyle = inline ? undefined : ({ zIndex } as React.CSSProperties)
|
||||||
? 'bg-emerald-700/95 text-white ring-1 ring-black/10'
|
|
||||||
: 'bg-amber-700/95 text-white ring-1 ring-black/10'
|
const isConnected = variant === 'connected'
|
||||||
|
const wrapperClass = isConnected
|
||||||
|
? 'bg-emerald-700/95 text-white ring-1 ring-black/10'
|
||||||
|
: 'bg-amber-700/95 text-white ring-1 ring-black/10'
|
||||||
|
|
||||||
const bgUrl = pickMapImageFromOptions(mapKey)
|
const bgUrl = pickMapImageFromOptions(mapKey)
|
||||||
const mapIconConnected = pickMapIcon(mapKey)
|
const iconUrl = isConnected ? (pickMapIcon(mapKey) ?? '') : '/assets/img/icons/ui/disconnect.svg'
|
||||||
const iconUrl = variant === 'connected' ? (mapIconConnected ?? '') : '/assets/img/icons/ui/disconnect.svg'
|
|
||||||
|
|
||||||
const prettyMap = mapLabel ?? mapKey ?? '—'
|
const pretty = {
|
||||||
const prettyPhase = phaseStr || 'unknown' // nutzt die normalisierte Phase
|
map: mapLabel ?? mapKey ?? '—',
|
||||||
const prettyScore = score ?? '– : –'
|
phase: phaseStr || 'unknown',
|
||||||
|
score: score ?? '– : –',
|
||||||
|
}
|
||||||
|
|
||||||
const handleFocusGame = (e: React.MouseEvent) => {
|
const openGame = () => {
|
||||||
e.preventDefault()
|
|
||||||
try { window.location.href = 'steam://rungameid/730' } catch {}
|
try { window.location.href = 'steam://rungameid/730' } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ▼ nichts rendern, wenn Phase unbekannt oder sichtbar=false
|
const InfoRow = () => (
|
||||||
if (!show) return null
|
<div className="text-xs opacity-90 mt-0.5 flex flex-wrap gap-x-3 gap-y-1">
|
||||||
|
<span>Map: <span className="font-semibold">{pretty.map}</span></span>
|
||||||
|
<span>Phase: <span className="font-semibold">{pretty.phase}</span></span>
|
||||||
|
<span>Score: <span className="font-semibold">{pretty.score}</span></span>
|
||||||
|
<span>{t('player-connected')}: <span className="font-semibold">{connectedCount}</span> / {totalExpected}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={outerBase} style={outerStyle} ref={ref}>
|
<div className={outerBase} style={outerStyle} ref={ref}>
|
||||||
<div className={`relative overflow-hidden shadow-lg ${wrapperClass} transition duration-300 ease-in-out`}>
|
<div className={`relative overflow-hidden shadow-lg ${wrapperClass} transition duration-300 ease-in-out`}>
|
||||||
{/* Subtiler Map-Hintergrund */}
|
{/* Hintergrundbild (Map) */}
|
||||||
{bgUrl && (
|
{bgUrl && (
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
@ -140,64 +145,39 @@ export default function GameBanner({
|
|||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="pointer-events-none absolute inset-0"
|
className="pointer-events-none absolute inset-0"
|
||||||
style={{
|
style={{ background: 'linear-gradient(180deg, rgba(0,0,0,0.20) 0%, rgba(0,0,0,0.10) 40%, rgba(0,0,0,0.25) 100%)' }}
|
||||||
background: 'linear-gradient(180deg, rgba(0,0,0,0.20) 0%, rgba(0,0,0,0.10) 40%, rgba(0,0,0,0.25) 100%)',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Inhalt */}
|
{/* Inhalt */}
|
||||||
<div className="relative p-3 flex items-center gap-3">
|
<div className="relative p-3 flex items-center gap-3">
|
||||||
{/* Icon links */}
|
{/* Icon */}
|
||||||
{iconUrl ? (
|
{iconUrl && (
|
||||||
<div className="shrink-0 relative z-[1]">
|
<div className="shrink-0 relative z-[1]">
|
||||||
<div className="h-9 w-9 rounded-md bg-black/15 flex items-center justify-center ring-1 ring-black/20">
|
<div className="h-9 w-9 rounded-md bg-black/15 flex items-center justify-center ring-1 ring-black/20">
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
src={iconUrl}
|
src={iconUrl}
|
||||||
alt={variant === 'connected' ? (prettyMap || 'Map') : 'Disconnected'}
|
alt={isConnected ? (pretty.map || 'Map') : 'Disconnected'}
|
||||||
className="h-6 w-6 object-contain"
|
className="h-6 w-6 object-contain"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
|
|
||||||
|
{/* Text */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
{variant === 'connected' ? (
|
<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">
|
||||||
<div className="text-sm flex items-center gap-2">
|
{isConnected ? (serverLabel ?? 'CS2 Server') : t('disconnected')}
|
||||||
<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>
|
||||||
{serverLabel ?? 'CS2 Server'}
|
</div>
|
||||||
</span>
|
<InfoRow />
|
||||||
</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>{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">
|
|
||||||
{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>{tGameBanner("player-connected")}: <span className="font-semibold">{connectedCount}</span> / {totalExpected}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Buttons rechts */}
|
{/* Actions */}
|
||||||
<div className="relative z-[1] flex items-center gap-2">
|
<div className="relative z-[1] flex items-center gap-2">
|
||||||
{/* Radar */}
|
|
||||||
<Link href="/radar" className="inline-flex">
|
<Link href="/radar" className="inline-flex">
|
||||||
<Button
|
<Button
|
||||||
variant="white"
|
variant="white"
|
||||||
@ -207,15 +187,12 @@ export default function GameBanner({
|
|||||||
title={undefined}
|
title={undefined}
|
||||||
>
|
>
|
||||||
<span className="relative mr-2 h-4 w-4 rounded-full overflow-hidden ring-1 ring-black/20 bg-black/20">
|
<span className="relative mr-2 h-4 w-4 rounded-full overflow-hidden ring-1 ring-black/20 bg-black/20">
|
||||||
{/* zentraler Punkt */}
|
|
||||||
<span className="absolute inset-0 flex items-center justify-center">
|
<span className="absolute inset-0 flex items-center justify-center">
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-white/90 shadow-[0_0_6px_rgba(255,255,255,0.8)]" />
|
<span className="h-1.5 w-1.5 rounded-full bg-white/90 shadow-[0_0_6px_rgba(255,255,255,0.8)]" />
|
||||||
</span>
|
</span>
|
||||||
{/* expandierende Ringe */}
|
|
||||||
<span className="absolute inset-0 rounded-full border border-white/50 opacity-70 animate-ping-slow" />
|
<span className="absolute inset-0 rounded-full border border-white/50 opacity-70 animate-ping-slow" />
|
||||||
<span className="absolute inset-0 rounded-full border border-white/30 opacity-50 animate-ping-slower" />
|
<span className="absolute inset-0 rounded-full border border-white/30 opacity-50 animate-ping-slower" />
|
||||||
<span className="absolute inset-0 rounded-full border border-white/20 opacity-30 animate-ping-slowest" />
|
<span className="absolute inset-0 rounded-full border border-white/20 opacity-30 animate-ping-slowest" />
|
||||||
{/* rotierender Sweep */}
|
|
||||||
<span className="absolute inset-0 rounded-full overflow-hidden">
|
<span className="absolute inset-0 rounded-full overflow-hidden">
|
||||||
<span className="absolute left-1/2 top-1/2 origin-left -translate-y-1/2 h-[1.2px] w-full bg-gradient-to-r from-white/70 via-white/30 to-transparent animate-sweep" />
|
<span className="absolute left-1/2 top-1/2 origin-left -translate-y-1/2 h-[1.2px] w-full bg-gradient-to-r from-white/70 via-white/30 to-transparent animate-sweep" />
|
||||||
</span>
|
</span>
|
||||||
@ -224,53 +201,32 @@ export default function GameBanner({
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Spiel öffnen / Neu verbinden */}
|
{isConnected ? (
|
||||||
{variant === 'connected' ? (
|
<Button color="green" variant="solid" size="md" onClick={openGame} title="Spiel öffnen" />
|
||||||
<Button
|
|
||||||
color="green"
|
|
||||||
variant="solid"
|
|
||||||
size="md"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
try { window.location.href = 'steam://rungameid/730' } catch {}
|
|
||||||
}}
|
|
||||||
title="Spiel öffnen"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button color="green" variant="solid" size="md" onClick={onReconnect} title={t('reconnect')} />
|
||||||
color="green"
|
|
||||||
variant="solid"
|
|
||||||
size="md"
|
|
||||||
onClick={() => onReconnect()}
|
|
||||||
title={tGameBanner("reconnect")}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* X nur im verbundenen Zustand anzeigen */}
|
{isConnected && (
|
||||||
{variant === 'connected' && (
|
|
||||||
<Button
|
<Button
|
||||||
color="transparent"
|
color="transparent"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
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={tGameBanner("disconnected")}
|
aria-label={t('disconnected')}
|
||||||
title={undefined}
|
title={undefined}
|
||||||
>
|
>
|
||||||
<svg
|
<svg viewBox="0 0 24 24" className="h-5 w-5 block" aria-hidden="true">
|
||||||
viewBox="0 0 24 24"
|
|
||||||
className="h-5 w-5 block"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<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">{tGameBanner("quit")}</span>
|
<span className="mt-0.5 text-[11px] font-medium opacity-90">{t('quit')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CSS für Radar-Ping & Sweep */}
|
{/* Radar-Animationen */}
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
@keyframes ringPing {
|
@keyframes ringPing {
|
||||||
0% { transform: scale(0.4); opacity: 0.85; }
|
0% { transform: scale(0.4); opacity: 0.85; }
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
// src/app/components/GameBannerSpacer.tsx
|
// src/app/components/GameBannerSpacer.tsx
|
||||||
'use client'
|
'use client'
|
||||||
import {useEffect, useState} from 'react'
|
|
||||||
import {useUiChromeStore} from '@/lib/useUiChromeStore'
|
import {useUiChromeStore} from '@/lib/useUiChromeStore'
|
||||||
|
|
||||||
export default function GameBannerSpacer() {
|
export default function GameBannerSpacer({ className = '' }: { className?: string }) {
|
||||||
const bannerPx = useUiChromeStore(s => s.gameBannerPx) // oder passender Selector
|
const bannerPx = useUiChromeStore(s => s.gameBannerPx)
|
||||||
// Nur Höhe setzen, wenn inline-Banner genutzt wird – sonst 0
|
return <div className={className} style={{ height: bannerPx ?? 0 }} aria-hidden />
|
||||||
return <div style={{height: bannerPx ?? 0}} aria-hidden />
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,24 @@
|
|||||||
// /src/app/components/LoadingSpinner.tsx
|
// /src/app/components/LoadingSpinner.tsx
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useTranslations } from 'next-intl'
|
||||||
|
|
||||||
export default function LoadingSpinner() {
|
export default function LoadingSpinner() {
|
||||||
return (
|
|
||||||
<div className="flex flex-auto flex-col justify-center items-center p-2">
|
// Übersetzungen
|
||||||
<div className="flex justify-center">
|
const tCommon = useTranslations('common')
|
||||||
<div
|
|
||||||
className="animate-spin inline-block size-6 border-3 border-current border-t-transparent text-blue-600 rounded-full dark:text-blue-500"
|
return (
|
||||||
role="status"
|
<div className="flex flex-auto flex-col justify-center items-center p-2">
|
||||||
aria-label="loading"
|
<div className="flex justify-center">
|
||||||
>
|
<div
|
||||||
<span className="sr-only">Loading...</span>
|
className="animate-spin inline-block size-6 border-3 border-current border-t-transparent text-blue-600 rounded-full dark:text-blue-500"
|
||||||
|
role="status"
|
||||||
|
aria-label="loading"
|
||||||
|
>
|
||||||
|
<span className="sr-only">{tCommon("loading")}...</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation'
|
|||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { useSSEStore } from '@/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
import type { MapVoteState } from '../../../types/mapvote'
|
import type { MapVoteState } from '../../../types/mapvote'
|
||||||
|
import { useTranslations } from 'next-intl'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: any
|
match: any
|
||||||
@ -51,6 +52,9 @@ export default function MapVoteBanner({
|
|||||||
const [now, setNow] = useState(initialNow)
|
const [now, setNow] = useState(initialNow)
|
||||||
useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id) }, [])
|
useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id) }, [])
|
||||||
|
|
||||||
|
// Übersetzungen
|
||||||
|
const tCommon = useTranslations('common')
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|||||||
@ -70,15 +70,6 @@ export default function MapVoteProfileCard({
|
|||||||
<span className="truncate font-medium text-gray-900 dark:text-neutral-100">
|
<span className="truncate font-medium text-gray-900 dark:text-neutral-100">
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
{isActiveTurn ? (
|
|
||||||
<span className="mt-0.5 text-[11px] font-medium text-blue-700 dark:text-blue-300">
|
|
||||||
am Zug …
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="mt-0.5 text-[11px] text-gray-500 dark:text-neutral-400">
|
|
||||||
bereit
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* PremierRank ganz außen */}
|
{/* PremierRank ganz außen */}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import Link from 'next/link'
|
|||||||
|
|
||||||
type TeamWithPlayers = Team & { players?: MatchPlayer[] }
|
type TeamWithPlayers = Team & { players?: MatchPlayer[] }
|
||||||
|
|
||||||
/* ─────────────────── Hilfsfunktionen ────────────────────────── */
|
/* ─────────────────── Helpers ─────────────────── */
|
||||||
const kdr = (k?: number, d?: number) =>
|
const kdr = (k?: number, d?: number) =>
|
||||||
typeof k === 'number' && typeof d === 'number'
|
typeof k === 'number' && typeof d === 'number'
|
||||||
? d === 0
|
? d === 0
|
||||||
@ -46,10 +46,8 @@ type VoteAction = 'BAN' | 'PICK' | 'DECIDER'
|
|||||||
type VoteStep = { order: number; action: VoteAction; map?: string | null }
|
type VoteStep = { order: number; action: VoteAction; map?: string | null }
|
||||||
|
|
||||||
const mapLabelFromKey = (key?: string) => {
|
const mapLabelFromKey = (key?: string) => {
|
||||||
const k = (key ?? '').toLowerCase().replace(/\.bsp$/,'').replace(/^.*\//,'')
|
const k = (key ?? '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
|
||||||
return (
|
return MAP_OPTIONS.find(o => o.key === k)?.label ?? (k ? k : 'TBD')
|
||||||
MAP_OPTIONS.find(o => o.key === k)?.label ?? (k ? k : 'TBD')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maps aus dem MapVote (nur PICK/DECIDER, sortiert)
|
// Maps aus dem MapVote (nur PICK/DECIDER, sortiert)
|
||||||
@ -57,12 +55,25 @@ function extractSeriesMaps(match: Match): string[] {
|
|||||||
const steps = (match.mapVote?.steps ?? []) as unknown as VoteStep[]
|
const steps = (match.mapVote?.steps ?? []) as unknown as VoteStep[]
|
||||||
const picks = steps
|
const picks = steps
|
||||||
.filter(s => s && (s.action === 'PICK' || s.action === 'DECIDER'))
|
.filter(s => s && (s.action === 'PICK' || s.action === 'DECIDER'))
|
||||||
.sort((a,b) => (a.order ?? 0) - (b.order ?? 0))
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||||
.map(s => s.map ?? '')
|
.map(s => s.map ?? '')
|
||||||
const n = Math.max(1, match.bestOf ?? 1)
|
const n = Math.max(1, match.bestOf ?? 1)
|
||||||
return picks.slice(0, n)
|
return picks.slice(0, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wählt ein konsistentes Hintergrundbild pro Map
|
||||||
|
const pickMapImage = (key?: string) => {
|
||||||
|
const k = (key ?? '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
|
||||||
|
const opt = MAP_OPTIONS.find(o => o.key === k)
|
||||||
|
if (!opt?.images?.length) return null
|
||||||
|
// simple hash → gleiche Map → gleiches Bild
|
||||||
|
let h = 5381
|
||||||
|
for (let i = 0; i < k.length; i++) h = ((h << 5) + h) + k.charCodeAt(i)
|
||||||
|
const idx = Math.abs(h) % opt.images.length
|
||||||
|
return opt.images[idx] as string
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─────────────────── UI-Snippets ─────────────────── */
|
||||||
function SeriesStrip({
|
function SeriesStrip({
|
||||||
bestOf,
|
bestOf,
|
||||||
scoreA = 0,
|
scoreA = 0,
|
||||||
@ -84,20 +95,15 @@ function SeriesStrip({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<div className="text-sm text-gray-400">
|
<div className="text-sm text-gray-400">Best of {bestOf} • First to {needed}</div>
|
||||||
Best of {bestOf} • First to {needed}
|
<div className="text-sm font-semibold">{winsA}:{winsB}</div>
|
||||||
</div>
|
|
||||||
<div className="text-sm font-semibold">
|
|
||||||
{winsA}:{winsB}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2">
|
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
||||||
{Array.from({ length: total }).map((_, i) => {
|
{Array.from({length: total}).map((_, i) => {
|
||||||
const key = maps[i] ?? ''
|
const key = maps[i] ?? ''
|
||||||
const label = mapLabelFromKey(key)
|
const label = mapLabelFromKey(key)
|
||||||
|
|
||||||
const isDone = i < winsA + winsB
|
const isDone = i < winsA + winsB
|
||||||
const isCurrent = i === currentIdx
|
const isCurrent = i === currentIdx
|
||||||
const isFuture = i > winsA + winsB
|
const isFuture = i > winsA + winsB
|
||||||
@ -106,25 +112,22 @@ function SeriesStrip({
|
|||||||
<div
|
<div
|
||||||
key={`series-map-${i}`}
|
key={`series-map-${i}`}
|
||||||
className={[
|
className={[
|
||||||
'rounded-md px-3 py-2 border flex items-center justify-between',
|
'flex items-center justify-between rounded-md border px-3 py-2',
|
||||||
isCurrent
|
isCurrent
|
||||||
? 'border-blue-500 ring-2 ring-blue-300/50 bg-blue-500/10'
|
? 'border-blue-500 ring-2 ring-blue-300/50 bg-blue-500/10'
|
||||||
: isDone
|
: isDone
|
||||||
? 'border-emerald-500 bg-emerald-500/10'
|
? 'border-emerald-500 bg-emerald-500/10'
|
||||||
: 'border-gray-600 bg-neutral-800/40',
|
: 'border-gray-600 bg-neutral-800/40',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<span className="text-xs opacity-70 shrink-0">Map {i + 1}</span>
|
<span className="shrink-0 text-xs opacity-70">Map {i + 1}</span>
|
||||||
<span className="font-medium truncate">{label}</span>
|
<span className="truncate font-medium">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="shrink-0 flex items-center gap-1">
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
{isDone && <span className="text-xs font-semibold text-emerald-400">✔</span>}
|
||||||
{isDone && (
|
|
||||||
<span className="text-emerald-400 text-xs font-semibold">✔</span>
|
|
||||||
)}
|
|
||||||
{isCurrent && !isDone && !isFuture && (
|
{isCurrent && !isDone && !isFuture && (
|
||||||
<span className="text-blue-400 text-[11px] font-semibold">LIVE / als Nächstes</span>
|
<span className="text-[11px] font-semibold text-blue-400">LIVE / als Nächstes</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -135,24 +138,72 @@ function SeriesStrip({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─────────────────── Komponente ─────────────────────────────── */
|
function SeriesTabs({
|
||||||
export function MatchDetails({ match, initialNow }: { match: Match; initialNow: number }) {
|
maps,
|
||||||
const { data: session } = useSession()
|
winsA = 0,
|
||||||
const { lastEvent } = useSSEStore()
|
winsB = 0,
|
||||||
|
active,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
maps: string[]
|
||||||
|
winsA?: number | null
|
||||||
|
winsB?: number | null
|
||||||
|
active: number
|
||||||
|
onChange: (idx: number) => void
|
||||||
|
}) {
|
||||||
|
const done = (winsA ?? 0) + (winsB ?? 0)
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{maps.map((m, i) => {
|
||||||
|
const label = mapLabelFromKey(m)
|
||||||
|
const isActive = active === i
|
||||||
|
const isDone = i < done
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(i)}
|
||||||
|
className={[
|
||||||
|
'rounded-md px-3 py-1.5 text-sm transition',
|
||||||
|
isActive
|
||||||
|
? 'bg-blue-600 text-white ring-1 ring-blue-400 shadow'
|
||||||
|
: 'bg-neutral-800/50 text-neutral-200 ring-1 ring-neutral-600 hover:bg-neutral-700/60',
|
||||||
|
isDone && !isActive ? 'border-emerald-500/60' : '',
|
||||||
|
].join(' ')}
|
||||||
|
title={label || `Map ${i + 1}`}
|
||||||
|
>
|
||||||
|
<span className="mr-2 font-medium">Map {i + 1}</span>
|
||||||
|
<span className="opacity-80">{label || 'TBD'}</span>
|
||||||
|
{isDone && <span className="ml-2 text-emerald-400">✔</span>}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─────────────────── Komponente ─────────────────── */
|
||||||
|
export function MatchDetails({match, initialNow}: { match: Match; initialNow: number }) {
|
||||||
|
const {data: session} = useSession()
|
||||||
|
const {lastEvent} = useSSEStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const isAdmin = !!session?.user?.isAdmin
|
const isAdmin = !!session?.user?.isAdmin
|
||||||
|
|
||||||
// Hydration-sicher: keine sich ändernden Werte im SSR rendern
|
// Alle Maps der Serie (BO3/BO5)
|
||||||
// Wir brauchen "now" nur, um zu entscheiden, ob Mapvote schon gestartet ist
|
const allMaps = useMemo(() => extractSeriesMaps(match), [match.mapVote?.steps, match.bestOf])
|
||||||
const [now, setNow] = useState(initialNow)
|
const [activeMapIdx, setActiveMapIdx] = useState(0)
|
||||||
const [editMetaOpen, setEditMetaOpen] = useState(false)
|
|
||||||
|
|
||||||
// Lokale Overrides (analog MapVoteBanner), damit die Clients sofort reagieren
|
// Zeit / Modals
|
||||||
|
const [now, setNow] = useState(initialNow)
|
||||||
|
const [editMetaOpen, setEditMetaOpen] = useState(false)// Modal-State
|
||||||
|
const [editSide, setEditSide] = useState<EditSide | null>(null)
|
||||||
|
|
||||||
|
// Mapvote Overrides (SSE-reaktiv)
|
||||||
const [opensAtOverride, setOpensAtOverride] = useState<number | null>(null)
|
const [opensAtOverride, setOpensAtOverride] = useState<number | null>(null)
|
||||||
const [leadOverride, setLeadOverride] = useState<number | null>(null)
|
const [leadOverride, setLeadOverride] = useState<number | null>(null)
|
||||||
const lastHandledKeyRef = useRef<string>('')
|
const lastHandledKeyRef = useRef<string>('')
|
||||||
|
|
||||||
/* ─── Rollen & Rechte ─────────────────────────────────────── */
|
// Rollen & Rechte
|
||||||
const me = session?.user
|
const me = session?.user
|
||||||
const userId = me?.steamId
|
const userId = me?.steamId
|
||||||
const isLeaderA = !!userId && userId === match.teamA?.leader?.steamId
|
const isLeaderA = !!userId && userId === match.teamA?.leader?.steamId
|
||||||
@ -163,39 +214,56 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
const teamAPlayers = (match.teamA as TeamWithPlayers).players ?? []
|
const teamAPlayers = (match.teamA as TeamWithPlayers).players ?? []
|
||||||
const teamBPlayers = (match.teamB as TeamWithPlayers).players ?? []
|
const teamBPlayers = (match.teamB as TeamWithPlayers).players ?? []
|
||||||
|
|
||||||
/* ─── Map-Label ───────────────────────────────────────────── */
|
// Aktiv-Map aus Query (?m=2) initialisieren
|
||||||
const mapKey = normalizeMapKey(match.map)
|
useEffect(() => {
|
||||||
|
const sp = new URLSearchParams(window.location.search)
|
||||||
|
const m = Number(sp.get('m'))
|
||||||
|
if (Number.isFinite(m) && m >= 0 && m < allMaps.length) setActiveMapIdx(m)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [allMaps.length])
|
||||||
|
|
||||||
|
const setActive = (idx: number) => {
|
||||||
|
setActiveMapIdx(idx)
|
||||||
|
const sp = new URLSearchParams(window.location.search)
|
||||||
|
sp.set('m', String(idx))
|
||||||
|
const url = `${window.location.pathname}?${sp.toString()}${window.location.hash}`
|
||||||
|
window.history.replaceState(null, '', url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map-Kontext
|
||||||
|
const mapKey = normalizeMapKey(allMaps[activeMapIdx] ?? match.map)
|
||||||
const mapLabel =
|
const mapLabel =
|
||||||
MAP_OPTIONS.find(opt => opt.key === mapKey)?.label ??
|
MAP_OPTIONS.find(opt => opt.key === mapKey)?.label ??
|
||||||
MAP_OPTIONS.find(opt => opt.key === 'lobby_mapvote')?.label ??
|
MAP_OPTIONS.find(opt => opt.key === 'lobby_mapvote')?.label ??
|
||||||
'Unbekannte Map'
|
'Unbekannte Map'
|
||||||
|
|
||||||
/* ─── Match-Zeitpunkt (vom Server; ändert sich via router.refresh) ─── */
|
// Mapvote läuft / Map noch nicht final gewählt?
|
||||||
const dateString = match.matchDate ?? match.demoDate
|
const isPickBanPhase = Boolean(match.mapVote?.isOpen) || !mapKey || mapKey === 'lobby_mapvote'
|
||||||
const readableDate = dateString ? format(new Date(dateString), 'PPpp', { locale: de }) : 'Unbekannt'
|
|
||||||
|
|
||||||
|
// Datum
|
||||||
|
const dateString = match.matchDate ?? match.demoDate
|
||||||
|
const readableDate = dateString ? format(new Date(dateString), 'PPpp', {locale: de}) : 'Unbekannt'
|
||||||
|
|
||||||
|
// „Serie komplett“ (für SeriesStrip)
|
||||||
const seriesMaps = useMemo(() => {
|
const seriesMaps = useMemo(() => {
|
||||||
const fromVote = extractSeriesMaps(match)
|
const fromVote = extractSeriesMaps(match)
|
||||||
const n = Math.max(1, match.bestOf ?? 1)
|
const n = Math.max(1, match.bestOf ?? 1)
|
||||||
return fromVote.length >= n ? fromVote : [...fromVote, ...Array.from({ length: n - fromVote.length }, () => '')]
|
return fromVote.length >= n ? fromVote : [...fromVote, ...Array.from({length: n - fromVote.length}, () => '')]
|
||||||
}, [match.bestOf, match.mapVote?.steps?.length])
|
}, [match.bestOf, match.mapVote?.steps?.length])
|
||||||
|
|
||||||
/* ─── Modal-State ─────────────────────────────────────────── */
|
// Ticker für Mapvote-Zeitfenster
|
||||||
const [editSide, setEditSide] = useState<EditSide | null>(null)
|
|
||||||
|
|
||||||
/* ─── Live-Uhr für Mapvote-Startfenster ───────────────────── */
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = setInterval(() => setNow(Date.now()), 1000)
|
const id = setInterval(() => setNow(Date.now()), 1000)
|
||||||
return () => clearInterval(id)
|
return () => clearInterval(id)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Basiszeit des Matches (stabil; für Berechnung von opensAt-Fallback)
|
// Basiszeit des Matches (Fallback)
|
||||||
const matchBaseTs = useMemo(() => {
|
const matchBaseTs = useMemo(() => {
|
||||||
const raw = match.matchDate ?? match.demoDate ?? initialNow
|
const raw = match.matchDate ?? match.demoDate ?? initialNow
|
||||||
return new Date(raw).getTime()
|
return new Date(raw).getTime()
|
||||||
}, [match.matchDate, match.demoDate, initialNow])
|
}, [match.matchDate, match.demoDate, initialNow])
|
||||||
|
|
||||||
// Zeitpunkt, wann der Mapvote öffnet (Parent errechnet und an Banner gereicht)
|
// Öffnet der Mapvote wann?
|
||||||
const voteOpensAtTs = useMemo(() => {
|
const voteOpensAtTs = useMemo(() => {
|
||||||
if (opensAtOverride != null) return opensAtOverride
|
if (opensAtOverride != null) return opensAtOverride
|
||||||
if (match.mapVote?.opensAt) return new Date(match.mapVote.opensAt).getTime()
|
if (match.mapVote?.opensAt) return new Date(match.mapVote.opensAt).getTime()
|
||||||
@ -205,20 +273,17 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
return matchBaseTs - lead * 60_000
|
return matchBaseTs - lead * 60_000
|
||||||
}, [opensAtOverride, match.mapVote?.opensAt, match.mapVote?.leadMinutes, matchBaseTs, leadOverride])
|
}, [opensAtOverride, match.mapVote?.opensAt, match.mapVote?.leadMinutes, matchBaseTs, leadOverride])
|
||||||
|
|
||||||
const sseOpensAtTs = voteOpensAtTs
|
const sseOpensAtTs = voteOpensAtTs
|
||||||
const sseLeadMinutes = leadOverride
|
const sseLeadMinutes = leadOverride
|
||||||
|
|
||||||
const endDate = new Date(voteOpensAtTs)
|
const endDate = new Date(voteOpensAtTs)
|
||||||
const mapvoteStarted = (match.mapVote?.isOpen ?? false) || now >= voteOpensAtTs
|
const mapvoteStarted = (match.mapVote?.isOpen ?? false) || now >= voteOpensAtTs
|
||||||
|
|
||||||
const showEditA = canEditA && !mapvoteStarted
|
const showEditA = canEditA && !mapvoteStarted
|
||||||
const showEditB = canEditB && !mapvoteStarted
|
const showEditB = canEditB && !mapvoteStarted
|
||||||
|
|
||||||
/* ─── SSE-Listener (nur map-vote-updated & Co.) ───────────── */
|
// SSE-Listener (nur relevante Events)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastEvent) return
|
if (!lastEvent) return
|
||||||
|
|
||||||
// robustes Unwrap
|
|
||||||
const outer = lastEvent as any
|
const outer = lastEvent as any
|
||||||
const maybeInner = outer?.payload
|
const maybeInner = outer?.payload
|
||||||
const base = (maybeInner && typeof maybeInner === 'object' && 'type' in maybeInner && 'payload' in maybeInner)
|
const base = (maybeInner && typeof maybeInner === 'object' && 'type' in maybeInner && 'payload' in maybeInner)
|
||||||
@ -226,23 +291,15 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
: outer
|
: outer
|
||||||
|
|
||||||
const type = base?.type
|
const type = base?.type
|
||||||
const evt = base?.payload ?? base
|
const evt = base?.payload ?? base
|
||||||
if (!evt?.matchId || evt.matchId !== match.id) return
|
if (!evt?.matchId || evt.matchId !== match.id) return
|
||||||
|
|
||||||
// Dedupe-Key
|
|
||||||
const key = `${type}|${evt.matchId}|${evt.opensAt ?? ''}|${Number.isFinite(evt.leadMinutes) ? evt.leadMinutes : ''}`
|
const key = `${type}|${evt.matchId}|${evt.opensAt ?? ''}|${Number.isFinite(evt.leadMinutes) ? evt.leadMinutes : ''}`
|
||||||
if (key === lastHandledKeyRef.current) {
|
if (key === lastHandledKeyRef.current) return
|
||||||
// identisches Event bereits verarbeitet → ignorieren
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lastHandledKeyRef.current = key
|
lastHandledKeyRef.current = key
|
||||||
|
|
||||||
// eigentliche Verarbeitung
|
|
||||||
if (type === 'map-vote-updated') {
|
if (type === 'map-vote-updated') {
|
||||||
if (evt?.opensAt) {
|
if (evt?.opensAt) setOpensAtOverride(new Date(evt.opensAt).getTime())
|
||||||
const ts = new Date(evt.opensAt).getTime()
|
|
||||||
setOpensAtOverride(ts)
|
|
||||||
}
|
|
||||||
if (Number.isFinite(evt?.leadMinutes)) {
|
if (Number.isFinite(evt?.leadMinutes)) {
|
||||||
const lead = Number(evt.leadMinutes)
|
const lead = Number(evt.leadMinutes)
|
||||||
setLeadOverride(lead)
|
setLeadOverride(lead)
|
||||||
@ -251,33 +308,28 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
setOpensAtOverride(baseTs - lead * 60_000)
|
setOpensAtOverride(baseTs - lead * 60_000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// damit match.matchDate & Co. neu vom Server kommen
|
|
||||||
router.refresh()
|
router.refresh()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const REFRESH_TYPES = new Set(['map-vote-reset', 'map-vote-locked', 'map-vote-unlocked', 'match-lineup-updated'])
|
const REFRESH_TYPES = new Set(['map-vote-reset', 'map-vote-locked', 'map-vote-unlocked', 'match-lineup-updated'])
|
||||||
if (REFRESH_TYPES.has(type) && evt?.matchId === match.id) {
|
if (REFRESH_TYPES.has(type) && evt?.matchId === match.id) router.refresh()
|
||||||
router.refresh()
|
|
||||||
}
|
|
||||||
}, [lastEvent, match.id, router, match.matchDate, match.demoDate, initialNow])
|
}, [lastEvent, match.id, router, match.matchDate, match.demoDate, initialNow])
|
||||||
|
|
||||||
/* ─── Tabellen-Layout (fixe Breiten) ───────────────────────── */
|
// Tabellen-Layout
|
||||||
const ColGroup = () => (
|
const ColGroup = () => (
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '24%' }} />
|
<col style={{width: '24%'}} />
|
||||||
<col style={{ width: '8%' }} />
|
<col style={{width: '8%'}} />
|
||||||
{Array.from({ length: 13 }).map((_, i) => (
|
{Array.from({length: 13}).map((_, i) => <col key={i} style={{width: '5.666%'}} />)}
|
||||||
<col key={i} style={{ width: '5.666%' }} />
|
|
||||||
))}
|
|
||||||
</colgroup>
|
</colgroup>
|
||||||
)
|
)
|
||||||
|
|
||||||
/* ─── Match löschen ────────────────────────────────────────── */
|
// Löschen
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!confirm('Match wirklich löschen? Das kann nicht rückgängig gemacht werden.')) return
|
if (!confirm('Match wirklich löschen? Das kann nicht rückgängig gemacht werden.')) return
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/matches/${match.id}/delete`, { method: 'POST' })
|
const res = await fetch(`/api/matches/${match.id}/delete`, {method: 'POST'})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const j = await res.json().catch(() => ({}))
|
const j = await res.json().catch(() => ({}))
|
||||||
alert(j.message ?? 'Löschen fehlgeschlagen')
|
alert(j.message ?? 'Löschen fehlgeschlagen')
|
||||||
@ -290,7 +342,7 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Spieler-Tabelle (pure; keine Hooks hier drin!) ──────── */
|
// Spieler-Tabelle
|
||||||
const renderTable = (players: MatchPlayer[]) => {
|
const renderTable = (players: MatchPlayer[]) => {
|
||||||
const sorted = [...players].sort(
|
const sorted = [...players].sort(
|
||||||
(a, b) => (b.stats?.totalDamage ?? 0) - (a.stats?.totalDamage ?? 0),
|
(a, b) => (b.stats?.totalDamage ?? 0) - (a.stats?.totalDamage ?? 0),
|
||||||
@ -305,10 +357,8 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
'Spieler', 'Rank', 'Aim', 'K', 'A', 'D',
|
'Spieler', 'Rank', 'Aim', 'K', 'A', 'D',
|
||||||
'1K', '2K', '3K', '4K', '5K',
|
'1K', '2K', '3K', '4K', '5K',
|
||||||
'K/D', 'ADR', 'HS%', 'Damage',
|
'K/D', 'ADR', 'HS%', 'Damage',
|
||||||
].map((h) => (
|
].map(h => (
|
||||||
<Table.Cell key={h} as="th">
|
<Table.Cell key={h} as="th">{h}</Table.Cell>
|
||||||
{h}
|
|
||||||
</Table.Cell>
|
|
||||||
))}
|
))}
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Head>
|
</Table.Head>
|
||||||
@ -317,16 +367,16 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
{sorted.map((p) => (
|
{sorted.map((p) => (
|
||||||
<Table.Row key={p.user.steamId}>
|
<Table.Row key={p.user.steamId}>
|
||||||
<Table.Cell
|
<Table.Cell
|
||||||
className="py-1 flex items-center gap-2"
|
className="flex items-center gap-2 py-1"
|
||||||
hoverable
|
hoverable
|
||||||
onClick={() => router.push(`/profile/${p.user.steamId}`)}
|
onClick={() => router.push(`/profile/${p.user.steamId}`)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={p.user.avatar || '/assets/img/avatars/default_steam_avatar.jpg'}
|
src={p.user.avatar || '/assets/img/avatars/default_steam_avatar.jpg'}
|
||||||
alt={p.user.name}
|
alt={p.user.name}
|
||||||
className="w-8 h-8 rounded-full mr-3"
|
className="mr-3 h-8 w-8 rounded-full"
|
||||||
/>
|
/>
|
||||||
<div className="font-semibold text-base">{p.user.name ?? 'Unbekannt'}</div>
|
<div className="text-base font-semibold">{p.user.name ?? 'Unbekannt'}</div>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
@ -337,24 +387,18 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
{match.matchType === 'premier' && typeof p.stats?.rankChange === 'number' && (
|
{match.matchType === 'premier' && typeof p.stats?.rankChange === 'number' && (
|
||||||
<span
|
<span
|
||||||
className={`text-sm ${
|
className={`text-sm ${
|
||||||
p.stats.rankChange > 0
|
p.stats.rankChange > 0 ? 'text-green-500'
|
||||||
? 'text-green-500'
|
: p.stats.rankChange < 0 ? 'text-red-500' : ''
|
||||||
: p.stats.rankChange < 0
|
|
||||||
? 'text-red-500'
|
|
||||||
: ''
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{p.stats.rankChange > 0 ? '+' : ''}
|
{p.stats.rankChange > 0 ? '+' : ''}{p.stats.rankChange}
|
||||||
{p.stats.rankChange}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
{Number.isFinite(Number(p.stats?.aim))
|
{Number.isFinite(Number(p.stats?.aim)) ? `${Number(p.stats?.aim).toFixed(0)} %` : '-'}
|
||||||
? `${Number(p.stats?.aim).toFixed(0)} %`
|
|
||||||
: '-'}
|
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>{p.stats?.kills ?? '-'}</Table.Cell>
|
<Table.Cell>{p.stats?.kills ?? '-'}</Table.Cell>
|
||||||
<Table.Cell>{p.stats?.assists ?? '-'}</Table.Cell>
|
<Table.Cell>{p.stats?.assists ?? '-'}</Table.Cell>
|
||||||
@ -375,28 +419,26 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Render ─────────────────────────────────────────────── */
|
/* ─────────────────── Render ─────────────────── */
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Kopfzeile: Zurück + Admin-Buttons */}
|
{/* Topbar: Back + Admin */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link href="/schedule">
|
<Link href="/schedule">
|
||||||
<Button color="gray" variant="outline">
|
<Button color="gray" variant="outline">← Zurück</Button>
|
||||||
← Zurück
|
|
||||||
</Button>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setEditMetaOpen(true)}
|
onClick={() => setEditMetaOpen(true)}
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-md"
|
className="rounded-md bg-blue-600 px-3 py-1.5 text-white hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
Match bearbeiten
|
Match bearbeiten
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="bg-red-600 hover:bg-red-700 text-white px-3 py-1.5 rounded-md"
|
className="rounded-md bg-red-600 px-3 py-1.5 text-white hover:bg-red-700"
|
||||||
>
|
>
|
||||||
Match löschen
|
Match löschen
|
||||||
</Button>
|
</Button>
|
||||||
@ -404,34 +446,160 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-2xl font-bold">
|
{/* HEADER */}
|
||||||
Match auf {mapLabel} ({match.matchType})
|
<div id="match-header" className="relative overflow-hidden rounded-xl ring-1 ring-black/10">
|
||||||
</h1>
|
{/* Map-BG */}
|
||||||
|
{(() => {
|
||||||
|
// wenn Pick/Ban → immer Fallback anzeigen
|
||||||
|
const wanted = isPickBanPhase ? null : pickMapImage(mapKey)
|
||||||
|
const bg = wanted || '/assets/img/maps/lobby_mapveto_png.webp'
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${bg})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
filter: 'blur(2px)',
|
||||||
|
transform: 'scale(1.02)',
|
||||||
|
opacity: 0.55
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Hydration-sicher: Datum kommt vom Server und ändert sich nach SSE via router.refresh() */}
|
{/* Gradient + Shine */}
|
||||||
<p className="text-sm text-gray-500">Datum: {readableDate}</p>
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(180deg, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0.35) 40%, rgba(0,0,0,0.65) 100%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="pointer-events-none absolute inset-0 before:absolute before:-left-1/4 before:top-0 before:h-full before:w-1/2 before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent before:animate-[shine_4.5s_linear_infinite]" />
|
||||||
|
|
||||||
{(match.bestOf ?? 1) > 1 && (
|
{/* Content */}
|
||||||
<div className="mt-3">
|
<div className="relative p-4 sm:p-6">
|
||||||
<SeriesStrip
|
{/* Meta-Zeile */}
|
||||||
bestOf={match.bestOf ?? 3}
|
<div className="flex items-center justify-between gap-4">
|
||||||
scoreA={match.scoreA}
|
<div className="flex items-center gap-2 text-xs uppercase tracking-wide text-white/75">
|
||||||
scoreB={match.scoreB}
|
<span className="inline-flex items-center gap-1 rounded-md bg-black/25 px-2 py-1 ring-1 ring-white/10">
|
||||||
maps={extractSeriesMaps(match)}
|
{match.matchType || 'match'}
|
||||||
/>
|
</span>
|
||||||
|
<span className="opacity-60">•</span>
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-md bg-black/25 px-2 py-1 ring-1 ring-white/10">
|
||||||
|
Best of {match.bestOf ?? 1}
|
||||||
|
</span>
|
||||||
|
{dateString && (
|
||||||
|
<>
|
||||||
|
<span className="opacity-60">•</span>
|
||||||
|
<time className="inline-flex items-center gap-1 rounded-md bg-black/25 px-2 py-1 ring-1 ring-white/10">
|
||||||
|
{readableDate}
|
||||||
|
</time>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Teams + Score */}
|
||||||
|
<div className="mt-4 grid grid-cols-[1fr_auto_1fr] items-center gap-4 sm:gap-6">
|
||||||
|
{/* Team A */}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{match.teamA?.logo && (
|
||||||
|
<img
|
||||||
|
src={match.teamA.logo ? `/assets/img/logos/${match.teamA.logo}` : `/assets/img/logos/cs2.webp`}
|
||||||
|
alt={match.teamA.name ?? 'Team A'}
|
||||||
|
className="h-10 w-10 rounded-md object-cover ring-1 ring-white/20 bg-black/20 sm:h-12 sm:w-12"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-xs text-white/80">Team A</div>
|
||||||
|
<div className="truncate text-lg font-semibold text-white sm:text-xl">
|
||||||
|
{match.teamA?.name ?? 'Unbekannt'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Score */}
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-xs text-white/80">Score</div>
|
||||||
|
<div className="mx-auto mt-1 inline-flex items-center gap-3 rounded-lg bg-black/30 px-3 py-1.5 ring-1 ring-white/10">
|
||||||
|
<span className="animate-[pop_350ms_ease-out] text-2xl font-bold text-white drop-shadow-sm sm:text-3xl">
|
||||||
|
{match.scoreA ?? 0}
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold text-white/60">:</span>
|
||||||
|
<span className="animate-[pop_350ms_ease-out_120ms] text-2xl font-bold text-white drop-shadow-sm sm:text-3xl">
|
||||||
|
{match.scoreB ?? 0}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs text-white/75">{`on ${mapLabel}`}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Team B */}
|
||||||
|
<div className="min-w-0 justify-self-end">
|
||||||
|
<div className="flex items-center justify-end gap-3">
|
||||||
|
<div className="min-w-0 text-right">
|
||||||
|
<div className="text-xs text-white/80">Team B</div>
|
||||||
|
<div className="truncate text-lg font-semibold text-white sm:text-xl">
|
||||||
|
{match.teamB?.name ?? 'Unbekannt'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{match.teamB?.logo && (
|
||||||
|
<img
|
||||||
|
src={match.teamB.logo ? `/assets/img/logos/${match.teamB.logo}` : `/assets/img/logos/cs2.webp`}
|
||||||
|
alt={match.teamB.name ?? 'Team B'}
|
||||||
|
className="h-10 w-10 rounded-md object-cover ring-1 ring-white/20 bg-black/20 sm:h-12 sm:w-12"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Map-Tabs bei Serie */}
|
||||||
|
{allMaps.length > 1 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<SeriesTabs
|
||||||
|
maps={allMaps}
|
||||||
|
winsA={match.scoreA}
|
||||||
|
winsB={match.scoreB}
|
||||||
|
active={activeMapIdx}
|
||||||
|
onChange={setActive}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Series-Strip (Visuelle Übersicht) */}
|
||||||
|
{(match.bestOf ?? 1) > 1 && (
|
||||||
|
<div className="mt-5">
|
||||||
|
<SeriesStrip
|
||||||
|
bestOf={match.bestOf ?? 3}
|
||||||
|
scoreA={match.scoreA}
|
||||||
|
scoreB={match.scoreB}
|
||||||
|
maps={seriesMaps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="text-md mt-2">
|
{/* Header Animations */}
|
||||||
<strong>Teams:</strong> {match.teamA?.name ?? 'Unbekannt'} vs. {match.teamB?.name ?? 'Unbekannt'}
|
<style jsx>{`
|
||||||
|
@keyframes shine {
|
||||||
|
0% { transform: translateX(-60%); }
|
||||||
|
100% { transform: translateX(120%); }
|
||||||
|
}
|
||||||
|
@keyframes pop {
|
||||||
|
0% { transform: scale(0.8); opacity: .3; }
|
||||||
|
100% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-md">
|
{/* MapVote-Banner (mit berechneten Werten) */}
|
||||||
<strong>Score:</strong> {match.scoreA ?? 0}:{match.scoreB ?? 0}
|
{match.matchType === 'community' && (
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* MapVote-Banner erhält die aktuell berechneten (SSE-konformen) Werte */}
|
|
||||||
{(match.matchType === 'community' &&
|
|
||||||
<MapVoteBanner
|
<MapVoteBanner
|
||||||
match={match}
|
match={match}
|
||||||
initialNow={initialNow}
|
initialNow={initialNow}
|
||||||
@ -441,14 +609,14 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ───────── Team-Blöcke ───────── */}
|
{/* Teams / Tabellen */}
|
||||||
<div className="border-t pt-4 mt-4 space-y-10">
|
<div className="mt-4 space-y-10 border-t pt-4">
|
||||||
{/* Team A */}
|
{/* Team A */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<h2 className="text-xl font-semibold">
|
<h2 className="text-xl font-semibold">
|
||||||
{match.teamA?.logo && (
|
{match.teamA?.logo && (
|
||||||
<span className="relative inline-block w-8 h-8 mr-2 align-middle">
|
<span className="relative mr-2 inline-block h-8 w-8 align-middle">
|
||||||
<Image
|
<Image
|
||||||
src={match.teamA.logo ? `/assets/img/logos/${match.teamA.logo}` : `/assets/img/logos/cs2.webp`}
|
src={match.teamA.logo ? `/assets/img/logos/${match.teamA.logo}` : `/assets/img/logos/cs2.webp`}
|
||||||
alt="Teamlogo"
|
alt="Teamlogo"
|
||||||
@ -459,25 +627,24 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{match.teamB?.name ?? 'Team B'}
|
{match.teamA?.name ?? 'Team A'}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{canEditA && !mapvoteStarted && (
|
{showEditA && (
|
||||||
<Alert type="soft" color="dark" className="flex items-center justify-between gap-4">
|
<Alert type="soft" color="dark" className="flex items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="green" height="20" width="20" viewBox="0 0 640 640">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="green" height="20" width="20" viewBox="0 0 640 640">
|
||||||
<path d="M416 160C416 124.7 444.7 96 480 96C515.3 96 544 124.7 544 160L544 192C544 209.7 558.3 224 576 224C593.7 224 608 209.7 608 192L608 160C608 89.3 550.7 32 480 32C409.3 32 352 89.3 352 160L352 224L192 224C156.7 224 128 252.7 128 288L128 512C128 547.3 156.7 576 192 576L448 576C483.3 576 512 547.3 512 512L512 288C512 252.7 483.3 224 448 224L416 224L416 160z"/>
|
<path d="M416 160C416 124.7 444.7 96 480 96C515.3 96 544 124.7 544 160L544 192C544 209.7 558.3 224 576 224C593.7 224 608 209.7 608 192L608 160C608 89.3 550.7 32 480 32C409.3 32 352 89.3 352 160L352 224L192 224C156.7 224 128 252.7 128 288L128 512C128 547.3 156.7 576 192 576L448 576C483.3 576 512 547.3 512 512L512 288C512 252.7 483.3 224 448 224L416 224L416 160z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-gray-300">
|
<span className="text-gray-300">
|
||||||
Du kannst die Aufstellung noch bis{' '}
|
Du kannst die Aufstellung noch bis <strong>{format(endDate, 'dd.MM.yyyy HH:mm')}</strong> bearbeiten.
|
||||||
<strong>{format(endDate, 'dd.MM.yyyy HH:mm')}</strong> bearbeiten.
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setEditSide('A')}
|
onClick={() => setEditSide('A')}
|
||||||
className="px-3 py-1.5 text-sm rounded-lg bg-blue-600 hover:bg-blue-700 text-white"
|
className="rounded-lg bg-blue-600 px-3 py-1.5 text-sm text-white hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
Spieler bearbeiten
|
Spieler bearbeiten
|
||||||
</Button>
|
</Button>
|
||||||
@ -490,10 +657,10 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
|
|
||||||
{/* Team B */}
|
{/* Team B */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<h2 className="text-xl font-semibold">
|
<h2 className="text-xl font-semibold">
|
||||||
{match.teamB?.logo && (
|
{match.teamB?.logo && (
|
||||||
<span className="relative inline-block w-8 h-8 mr-2 align-middle">
|
<span className="relative mr-2 inline-block h-8 w-8 align-middle">
|
||||||
<Image
|
<Image
|
||||||
src={match.teamB.logo ? `/assets/img/logos/${match.teamB.logo}` : `/assets/img/logos/cs2.webp`}
|
src={match.teamB.logo ? `/assets/img/logos/${match.teamB.logo}` : `/assets/img/logos/cs2.webp`}
|
||||||
alt="Teamlogo"
|
alt="Teamlogo"
|
||||||
@ -507,22 +674,21 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
{match.teamB?.name ?? 'Team B'}
|
{match.teamB?.name ?? 'Team B'}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{canEditB && !mapvoteStarted && (
|
{showEditB && (
|
||||||
<Alert type="soft" color="dark" className="flex items-center justify-between gap-4">
|
<Alert type="soft" color="dark" className="flex items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="green" height="20" width="20" viewBox="0 0 640 640">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="green" height="20" width="20" viewBox="0 0 640 640">
|
||||||
<path d="M416 160C416 124.7 444.7 96 480 96C515.3 96 544 124.7 544 160L544 192C544 209.7 558.3 224 576 224C593.7 224 608 209.7 608 192L608 160C608 89.3 550.7 32 480 32C409.3 32 352 89.3 352 160L352 224L192 224C156.7 224 128 252.7 128 288L128 512C128 547.3 156.7 576 192 576L448 576C483.3 576 512 547.3 512 512L512 288C512 252.7 483.3 224 448 224L416 224L416 160z"/>
|
<path d="M416 160C416 124.7 444.7 96 480 96C515.3 96 544 124.7 544 160L544 192C544 209.7 558.3 224 576 224C593.7 224 608 209.7 608 192L608 160C608 89.3 550.7 32 480 32C409.3 32 352 89.3 352 160L352 224L192 224C156.7 224 128 252.7 128 288L128 512C128 547.3 156.7 576 192 576L448 576C483.3 576 512 547.3 512 512L512 288C512 252.7 483.3 224 448 224L416 224L416 160z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-gray-300">
|
<span className="text-gray-300">
|
||||||
Du kannst die Aufstellung noch bis{' '}
|
Du kannst die Aufstellung noch bis <strong>{format(endDate, 'dd.MM.yyyy HH:mm')}</strong> bearbeiten.
|
||||||
<strong>{format(endDate, 'dd.MM.yyyy HH:mm')}</strong> bearbeiten.
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setEditSide('B')}
|
onClick={() => setEditSide('B')}
|
||||||
className="px-3 py-1.5 text-sm rounded-lg bg-blue-600 hover:bg-blue-700 text-white"
|
className="rounded-lg bg-blue-600 px-3 py-1.5 text-sm text-white hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
Spieler bearbeiten
|
Spieler bearbeiten
|
||||||
</Button>
|
</Button>
|
||||||
@ -534,7 +700,8 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ───────── Modal ───────── */}
|
{/* Echte Modals (außerhalb IIFE, State oben): */}
|
||||||
|
{/* Spieler-Modal */}
|
||||||
{editSide && (
|
{editSide && (
|
||||||
<EditMatchPlayersModal
|
<EditMatchPlayersModal
|
||||||
show
|
show
|
||||||
@ -543,12 +710,13 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
teamA={match.teamA}
|
teamA={match.teamA}
|
||||||
teamB={match.teamB}
|
teamB={match.teamB}
|
||||||
side={editSide}
|
side={editSide}
|
||||||
initialA={teamAPlayers.map((mp) => mp.user.steamId)}
|
initialA={teamAPlayers.map(mp => mp.user.steamId)}
|
||||||
initialB={teamBPlayers.map((mp) => mp.user.steamId)}
|
initialB={teamBPlayers.map(mp => mp.user.steamId)}
|
||||||
onSaved={() => router.refresh()}
|
onSaved={() => router.refresh()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Meta-Modal */}
|
||||||
{editMetaOpen && (
|
{editMetaOpen && (
|
||||||
<EditMatchMetaModal
|
<EditMatchMetaModal
|
||||||
show
|
show
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
// /src/app/components/Modal.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
@ -70,9 +71,7 @@ export default function Modal({
|
|||||||
hs.close?.(modalEl)
|
hs.close?.(modalEl)
|
||||||
destroyIfExists()
|
destroyIfExists()
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {}
|
||||||
console.error('[Modal] HSOverlay Fehler:', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
modalEl.removeEventListener('hsOverlay:close', handleClose)
|
modalEl.removeEventListener('hsOverlay:close', handleClose)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// NotificationCenter.tsx
|
// /src/app/[locale]/components/NotificationCenter.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useRef, useMemo, useState } from 'react'
|
import { useEffect, useRef, useMemo, useState } from 'react'
|
||||||
@ -169,7 +169,10 @@ export default function NotificationCenter({
|
|||||||
) : (
|
) : (
|
||||||
!n.read && (
|
!n.read && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onSingleRead(n.id)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onSingleRead(n.id);
|
||||||
|
}}
|
||||||
title="Als gelesen markieren"
|
title="Als gelesen markieren"
|
||||||
className="p-1 text-gray-400 hover:text-gray-700 dark:hover:text-white"
|
className="p-1 text-gray-400 hover:text-gray-700 dark:hover:text-white"
|
||||||
color="gray"
|
color="gray"
|
||||||
|
|||||||
@ -1,31 +1,26 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState, useRef, useEffect } from 'react'
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
interface PopoverProps {
|
interface PopoverProps {
|
||||||
text: string
|
text: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||||
|
gap?: number // Abstand (px) zwischen Button und Popover
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Popover({ text, children, size = 'sm' }: PopoverProps) {
|
export default function Popover({
|
||||||
|
text,
|
||||||
|
children,
|
||||||
|
size = 'sm',
|
||||||
|
gap = 8,
|
||||||
|
}: PopoverProps) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
const [style, setStyle] = useState<React.CSSProperties>({ visibility: 'hidden' })
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||||
const popoverRef = useRef<HTMLDivElement>(null)
|
const popoverRef = useRef<HTMLDivElement>(null)
|
||||||
|
const sizeRef = useRef<{ w: number; h: number }>({ w: 0, h: 0 })
|
||||||
useEffect(() => {
|
const ticking = useRef(false)
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
popoverRef.current &&
|
|
||||||
!popoverRef.current.contains(event.target as Node) &&
|
|
||||||
!buttonRef.current?.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener('mousedown', handleClickOutside)
|
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const sizeClass = {
|
const sizeClass = {
|
||||||
sm: 'max-w-xs',
|
sm: 'max-w-xs',
|
||||||
@ -34,21 +29,138 @@ export default function Popover({ text, children, size = 'sm' }: PopoverProps) {
|
|||||||
xl: 'max-w-lg',
|
xl: 'max-w-lg',
|
||||||
}[size]
|
}[size]
|
||||||
|
|
||||||
|
// außerhalb klicken -> schließen
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
const onDown = (e: MouseEvent) => {
|
||||||
|
const t = e.target as Node
|
||||||
|
if (popoverRef.current?.contains(t)) return
|
||||||
|
if (buttonRef.current?.contains(t)) return
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', onDown)
|
||||||
|
return () => document.removeEventListener('mousedown', onDown)
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
// Größe 1x nach Öffnen messen (ohne Blinken)
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
const pop = popoverRef.current
|
||||||
|
if (!pop) return
|
||||||
|
|
||||||
|
// kurz offscreen, um zuverlässige Maße zu bekommen
|
||||||
|
const prev = pop.style.cssText
|
||||||
|
pop.style.position = 'fixed'
|
||||||
|
pop.style.left = '-9999px'
|
||||||
|
pop.style.top = '-9999px'
|
||||||
|
pop.style.visibility = 'hidden'
|
||||||
|
pop.style.maxWidth = '' // durch Tailwind-Klasse
|
||||||
|
sizeRef.current = { w: pop.offsetWidth, h: pop.offsetHeight }
|
||||||
|
pop.style.cssText = prev
|
||||||
|
|
||||||
|
// initial platzieren & sichtbar
|
||||||
|
place(true)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
// rAF-throttled positionierung
|
||||||
|
const place = (makeVisible = false) => {
|
||||||
|
if (!open) return
|
||||||
|
const btn = buttonRef.current
|
||||||
|
const pop = popoverRef.current
|
||||||
|
if (!btn || !pop) return
|
||||||
|
|
||||||
|
const rect = btn.getBoundingClientRect()
|
||||||
|
const vw = window.innerWidth
|
||||||
|
const vh = window.innerHeight
|
||||||
|
const mobile = vw < 640
|
||||||
|
const { w, h } = sizeRef.current
|
||||||
|
|
||||||
|
// Standard: unter dem Trigger
|
||||||
|
let top = rect.bottom + gap
|
||||||
|
// wenn unten zu wenig Platz: oberhalb anzeigen
|
||||||
|
if (top + h + 8 > vh) top = Math.max(8, rect.top - gap - h)
|
||||||
|
|
||||||
|
let left: number
|
||||||
|
let transform = ''
|
||||||
|
|
||||||
|
if (mobile) {
|
||||||
|
left = rect.left + rect.width / 2
|
||||||
|
transform = 'translateX(-50%)'
|
||||||
|
const half = w / 2
|
||||||
|
if (left - half < 8) left = 8 + half
|
||||||
|
if (left + half > vw - 8) left = vw - 8 - half
|
||||||
|
} else {
|
||||||
|
// rechts am Button ausrichten
|
||||||
|
left = rect.right - w
|
||||||
|
if (left < 8) left = 8
|
||||||
|
if (left + w > vw - 8) left = vw - 8 - w
|
||||||
|
}
|
||||||
|
|
||||||
|
setStyle((s) => ({
|
||||||
|
...s,
|
||||||
|
position: 'fixed',
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
transform,
|
||||||
|
zIndex: 50,
|
||||||
|
visibility: makeVisible ? 'visible' : s.visibility, // beim ersten Mal sichtbar schalten
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
const onScrollOrResize = () => {
|
||||||
|
if (ticking.current) return
|
||||||
|
ticking.current = true
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
place()
|
||||||
|
ticking.current = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', onScrollOrResize, true)
|
||||||
|
window.addEventListener('resize', onScrollOrResize)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', onScrollOrResize, true)
|
||||||
|
window.removeEventListener('resize', onScrollOrResize)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [open, gap])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
<button
|
<button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen((prev) => !prev)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
className="mt-1 text-xs text-gray-400 dark:text-neutral-500"
|
className="mt-1 text-xs text-gray-400 dark:text-neutral-500 text-left inline-flex"
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* mobiles Overlay – optional */}
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-40 sm:hidden"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{open && (
|
{open && (
|
||||||
<div
|
<div
|
||||||
ref={popoverRef}
|
ref={popoverRef}
|
||||||
className={`fixed z-10 mt-2 ${sizeClass} rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm text-gray-700 shadow-md dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300`}
|
style={style}
|
||||||
|
className={[
|
||||||
|
'w-[min(92vw,32rem)] sm:w-auto',
|
||||||
|
sizeClass,
|
||||||
|
'rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm text-gray-700 shadow-md',
|
||||||
|
'dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300',
|
||||||
|
'whitespace-normal break-words',
|
||||||
|
'will-change-transform', // smoother
|
||||||
|
].join(' ')}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
105
src/app/[locale]/components/ScrollSpyTabs.tsx
Normal file
105
src/app/[locale]/components/ScrollSpyTabs.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {useEffect, useMemo, useRef, useState} from 'react'
|
||||||
|
import type {RefObject} from 'react'
|
||||||
|
|
||||||
|
export type SpyItem = { id: string; label: string }
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items: SpyItem[]
|
||||||
|
/** Scroll-Container; wenn nicht gesetzt, wird `document` beobachtet */
|
||||||
|
containerRef?: RefObject<HTMLElement | null>
|
||||||
|
className?: string
|
||||||
|
activeClassName?: string
|
||||||
|
inactiveClassName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ScrollSpyTabs({
|
||||||
|
items,
|
||||||
|
containerRef,
|
||||||
|
className = 'flex flex-col gap-1',
|
||||||
|
activeClassName = 'bg-gray-100 text-gray-900 dark:bg-neutral-700 dark:text-white',
|
||||||
|
inactiveClassName = 'text-gray-500 hover:bg-gray-100 dark:text-neutral-400 dark:hover:bg-neutral-700'
|
||||||
|
}: Props) {
|
||||||
|
const [activeId, setActiveId] = useState<string>(items[0]?.id ?? '')
|
||||||
|
const observerRef = useRef<IntersectionObserver | null>(null)
|
||||||
|
|
||||||
|
// Sichtbarkeits-Beobachtung
|
||||||
|
useEffect(() => {
|
||||||
|
const rootEl = containerRef?.current ?? null
|
||||||
|
const sections = items
|
||||||
|
.map(i => (rootEl ?? document).querySelector<HTMLElement>(`#${CSS.escape(i.id)}`))
|
||||||
|
.filter(Boolean) as HTMLElement[]
|
||||||
|
|
||||||
|
if (sections.length === 0) return
|
||||||
|
|
||||||
|
// etwas Toleranz: Sektion gilt als aktiv, wenn ~40% sichtbar sind
|
||||||
|
observerRef.current?.disconnect()
|
||||||
|
observerRef.current = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const visible = entries
|
||||||
|
.filter(e => e.isIntersecting)
|
||||||
|
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)
|
||||||
|
|
||||||
|
if (visible[0]) {
|
||||||
|
const id = (visible[0].target as HTMLElement).id
|
||||||
|
setActiveId(id)
|
||||||
|
} else {
|
||||||
|
// Fallback: oberste Sektion, wenn nichts “offiziell” intersected
|
||||||
|
const first = sections.find(s => {
|
||||||
|
const rect = s.getBoundingClientRect()
|
||||||
|
const top = rect.top - (rootEl?.getBoundingClientRect().top ?? 0)
|
||||||
|
return top >= -10 // nahe am Anfang
|
||||||
|
})
|
||||||
|
if (first) setActiveId(first.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: rootEl ?? null,
|
||||||
|
// top padding, damit schon etwas früher aktiv markiert wird:
|
||||||
|
rootMargin: '-20% 0px -40% 0px',
|
||||||
|
threshold: [0.2, 0.4, 0.6, 0.8],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
sections.forEach((s) => observerRef.current?.observe(s))
|
||||||
|
return () => observerRef.current?.disconnect()
|
||||||
|
}, [items, containerRef])
|
||||||
|
|
||||||
|
const onJump = (id: string) => {
|
||||||
|
const rootEl = containerRef?.current ?? null
|
||||||
|
const el = (rootEl ?? document).querySelector<HTMLElement>(`#${CSS.escape(id)}`)
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
if (rootEl) {
|
||||||
|
// innerhalb eines Scroll-Containers:
|
||||||
|
const rootTop = rootEl.getBoundingClientRect().top
|
||||||
|
const targetTop = el.getBoundingClientRect().top
|
||||||
|
const delta = targetTop - rootTop + rootEl.scrollTop - 12 /* kleiner offset */
|
||||||
|
rootEl.scrollTo({ top: delta, behavior: 'smooth' })
|
||||||
|
} else {
|
||||||
|
// Fenster scrollen
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className={className} aria-label="Section navigation" role="tablist" aria-orientation="vertical">
|
||||||
|
{items.map((it) => {
|
||||||
|
const isActive = activeId === it.id
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={it.id}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={isActive}
|
||||||
|
onClick={() => onJump(it.id)}
|
||||||
|
className={`text-left py-2 px-3 rounded-lg transition-colors ${isActive ? activeClassName : inactiveClassName}`}
|
||||||
|
>
|
||||||
|
{it.label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -13,16 +13,38 @@ export type TabProps = {
|
|||||||
type TabsProps = {
|
type TabsProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
/** optional kontrollierter Modus */
|
/** optional kontrollierter Modus */
|
||||||
value?: string // aktiver Tab-Name
|
value?: string
|
||||||
onChange?: (name: string) => void
|
onChange?: (name: string) => void
|
||||||
|
/** neu: Ausrichtung */
|
||||||
|
orientation?: 'horizontal' | 'vertical'
|
||||||
|
/** optional: Styling */
|
||||||
|
className?: string
|
||||||
|
tabClassName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tabs({ children, value, onChange }: TabsProps) {
|
export function Tabs({
|
||||||
|
children,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
orientation = 'horizontal',
|
||||||
|
className = '',
|
||||||
|
tabClassName = ''
|
||||||
|
}: TabsProps) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const tabs = Array.isArray(children) ? children : [children]
|
const tabs = Array.isArray(children) ? children : [children]
|
||||||
|
const isVertical = orientation === 'vertical'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="flex gap-x-1" aria-label="Tabs" role="tablist" aria-orientation="horizontal">
|
<nav
|
||||||
|
className={[
|
||||||
|
'flex',
|
||||||
|
isVertical ? 'flex-col gap-y-1' : 'flex-row gap-x-1',
|
||||||
|
className
|
||||||
|
].join(' ')}
|
||||||
|
aria-label="Tabs"
|
||||||
|
role="tablist"
|
||||||
|
aria-orientation={isVertical ? 'vertical' : 'horizontal'}
|
||||||
|
>
|
||||||
{tabs
|
{tabs
|
||||||
.filter(
|
.filter(
|
||||||
(tab): tab is ReactElement<TabProps> =>
|
(tab): tab is ReactElement<TabProps> =>
|
||||||
@ -32,7 +54,10 @@ export function Tabs({ children, value, onChange }: TabsProps) {
|
|||||||
typeof tab.props.href === 'string'
|
typeof tab.props.href === 'string'
|
||||||
)
|
)
|
||||||
.map((tab, index) => {
|
.map((tab, index) => {
|
||||||
// Unkontrolliert (Routing) vs. kontrolliert (onChange)
|
const baseClasses =
|
||||||
|
'py-2 px-4 text-sm rounded-lg transition-colors ' + tabClassName
|
||||||
|
|
||||||
|
// kontrollierter Modus
|
||||||
if (onChange && value !== undefined) {
|
if (onChange && value !== undefined) {
|
||||||
const isActive = value === tab.props.name
|
const isActive = value === tab.props.name
|
||||||
return (
|
return (
|
||||||
@ -42,18 +67,20 @@ export function Tabs({ children, value, onChange }: TabsProps) {
|
|||||||
onClick={() => onChange(tab.props.name)}
|
onClick={() => onChange(tab.props.name)}
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={isActive}
|
aria-selected={isActive}
|
||||||
className={`py-2 px-4 text-sm rounded-lg transition-colors ${
|
className={
|
||||||
isActive
|
baseClasses +
|
||||||
|
' ' +
|
||||||
|
(isActive
|
||||||
? 'bg-gray-100 text-gray-900 dark:bg-neutral-700 dark:text-white'
|
? 'bg-gray-100 text-gray-900 dark:bg-neutral-700 dark:text-white'
|
||||||
: 'text-gray-500 hover:bg-gray-100 dark:text-neutral-400 dark:hover:bg-neutral-700'
|
: 'text-gray-500 hover:bg-gray-100 dark:text-neutral-400 dark:hover:bg-neutral-700')
|
||||||
}`}
|
}
|
||||||
>
|
>
|
||||||
{tab.props.name}
|
{tab.props.name}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standard: Link-basiert
|
// Link-basiert
|
||||||
const base = tab.props.href.replace(/\/$/, '')
|
const base = tab.props.href.replace(/\/$/, '')
|
||||||
const current = pathname.replace(/\/$/, '')
|
const current = pathname.replace(/\/$/, '')
|
||||||
const isActive = current === base || current.startsWith(base + '/')
|
const isActive = current === base || current.startsWith(base + '/')
|
||||||
@ -64,11 +91,13 @@ export function Tabs({ children, value, onChange }: TabsProps) {
|
|||||||
href={tab.props.href}
|
href={tab.props.href}
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={isActive}
|
aria-selected={isActive}
|
||||||
className={`py-2 px-4 text-sm rounded-lg transition-colors ${
|
className={
|
||||||
isActive
|
baseClasses +
|
||||||
|
' ' +
|
||||||
|
(isActive
|
||||||
? 'bg-gray-100 text-gray-900 dark:bg-neutral-700 dark:text-white'
|
? 'bg-gray-100 text-gray-900 dark:bg-neutral-700 dark:text-white'
|
||||||
: 'text-gray-500 hover:bg-gray-100 dark:text-neutral-400 dark:hover:bg-neutral-700'
|
: 'text-gray-500 hover:bg-gray-100 dark:text-neutral-400 dark:hover:bg-neutral-700')
|
||||||
}`}
|
}
|
||||||
>
|
>
|
||||||
{tab.props.name}
|
{tab.props.name}
|
||||||
</Link>
|
</Link>
|
||||||
@ -78,5 +107,4 @@ export function Tabs({ children, value, onChange }: TabsProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dummy-Komponente nur zur statischen Verwendung
|
|
||||||
Tabs.Tab = function Tab(_props: TabProps) { return null }
|
Tabs.Tab = function Tab(_props: TabProps) { return null }
|
||||||
|
|||||||
@ -78,6 +78,11 @@ function TeamCardComponent(
|
|||||||
|
|
||||||
const lastInviteCheck = useRef<number>(0)
|
const lastInviteCheck = useRef<number>(0)
|
||||||
|
|
||||||
|
// 🔒 Flood-Guards
|
||||||
|
const lastHandledRef = useRef<string>('') // Event-Dedupe
|
||||||
|
const softReloadInFlight = useRef(false) // keine Parallel-Reloads
|
||||||
|
const lastSoftReloadAt = useRef(0) // einfacher Throttle (ms)
|
||||||
|
|
||||||
/* ------- User+Teams laden (einmalig) ------- */
|
/* ------- User+Teams laden (einmalig) ------- */
|
||||||
const loadUserTeams = async () => {
|
const loadUserTeams = async () => {
|
||||||
try {
|
try {
|
||||||
@ -87,18 +92,13 @@ function TeamCardComponent(
|
|||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
||||||
const teams: Team[] = Array.isArray(data?.teams) ? data.teams : []
|
const teams: Team[] = Array.isArray(data?.teams) ? data.teams : []
|
||||||
setMyTeams(prev => {
|
setMyTeams(prev => (prev.length === teams.length && prev.every((t, i) => eqTeam(t, teams[i])) ? prev : teams))
|
||||||
if (prev.length === teams.length && prev.every((t, i) => eqTeam(t, teams[i]))) return prev
|
|
||||||
return teams
|
|
||||||
})
|
|
||||||
|
|
||||||
// Auto-Auswahl
|
// Auto-Auswahl
|
||||||
if (teams.length === 1) {
|
if (teams.length === 1) {
|
||||||
setSelectedTeam(teams[0])
|
setSelectedTeam(teams[0])
|
||||||
} else {
|
} else if (selectedTeam && !teams.some(t => t.id === selectedTeam.id)) {
|
||||||
if (selectedTeam && !teams.some(t => t.id === selectedTeam.id)) {
|
setSelectedTeam(null)
|
||||||
setSelectedTeam(null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Einladungen leeren, wenn ich mind. ein Team habe
|
// Einladungen leeren, wenn ich mind. ein Team habe
|
||||||
@ -112,8 +112,14 @@ function TeamCardComponent(
|
|||||||
|
|
||||||
useEffect(() => { loadUserTeams() }, []) // eslint-disable-line react-hooks/exhaustive-deps
|
useEffect(() => { loadUserTeams() }, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
/* ------- SSE-gestützte Soft-Reloads ------- */
|
/* ------- Gedrosseltes Soft-Reload ------- */
|
||||||
const softReload = async () => {
|
const softReload = async () => {
|
||||||
|
const now = Date.now()
|
||||||
|
if (softReloadInFlight.current) return
|
||||||
|
if (now - lastSoftReloadAt.current < 500) return // 500ms Cooldown
|
||||||
|
|
||||||
|
softReloadInFlight.current = true
|
||||||
|
lastSoftReloadAt.current = now
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/user', { cache: 'no-store' })
|
const res = await fetch('/api/user', { cache: 'no-store' })
|
||||||
if (!res.ok) return
|
if (!res.ok) return
|
||||||
@ -126,6 +132,7 @@ function TeamCardComponent(
|
|||||||
else if (selectedTeam && !teams.some(t => t.id === selectedTeam.id)) setSelectedTeam(null)
|
else if (selectedTeam && !teams.some(t => t.id === selectedTeam.id)) setSelectedTeam(null)
|
||||||
|
|
||||||
if (teams.length > 0 && pendingInvitations.length) setPendingInvitations([])
|
if (teams.length > 0 && pendingInvitations.length) setPendingInvitations([])
|
||||||
|
|
||||||
if (teams.length === 0 && Date.now() - lastInviteCheck.current > 1500) {
|
if (teams.length === 0 && Date.now() - lastInviteCheck.current > 1500) {
|
||||||
lastInviteCheck.current = Date.now()
|
lastInviteCheck.current = Date.now()
|
||||||
const inv = await fetch('/api/user/invitations', { cache: 'no-store' })
|
const inv = await fetch('/api/user/invitations', { cache: 'no-store' })
|
||||||
@ -137,17 +144,42 @@ function TeamCardComponent(
|
|||||||
setPendingInvitations(prev => (eqInviteList(prev, all) ? prev : all))
|
setPendingInvitations(prev => (eqInviteList(prev, all) ? prev : all))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch { /* noop */ }
|
} finally {
|
||||||
|
softReloadInFlight.current = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------- SSE-gestützte Updates (dedupliziert) ------- */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastEvent) return
|
if (!lastEvent) return
|
||||||
if (!isSseEventType(lastEvent.type)) return
|
if (!isSseEventType(lastEvent.type)) return
|
||||||
|
|
||||||
const { type, payload } = lastEvent
|
const { type, payload } = lastEvent as any
|
||||||
|
|
||||||
if (SELF_EVENTS.has(type)) { softReload(); return }
|
// Dedupe-Key (Type + teamId + version + invitationId)
|
||||||
|
const key = [
|
||||||
|
type,
|
||||||
|
payload?.teamId ?? '',
|
||||||
|
payload?.version ?? '',
|
||||||
|
payload?.invitationId ?? ''
|
||||||
|
].join('|')
|
||||||
|
if (key === lastHandledRef.current) return
|
||||||
|
lastHandledRef.current = key
|
||||||
|
|
||||||
|
// Logo-Event: nur lokal updaten, KEIN /api/user-Reload
|
||||||
|
if (type === 'team-logo-updated' && payload?.teamId && payload?.filename) {
|
||||||
|
setMyTeams(prev => prev.map(t => (t.id === payload.teamId ? { ...t, logo: payload.filename } as Team : t)))
|
||||||
|
if (selectedTeam?.id === payload.teamId) {
|
||||||
|
setSelectedTeam(prev =>
|
||||||
|
prev && prev.id === payload.teamId
|
||||||
|
? { ...prev, logo: payload.filename }
|
||||||
|
: prev
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invite revoked: Liste anpassen, dann gedrosseltes Reload
|
||||||
if (type === 'team-invite-revoked') {
|
if (type === 'team-invite-revoked') {
|
||||||
const revokedId = payload?.invitationId as string | undefined
|
const revokedId = payload?.invitationId as string | undefined
|
||||||
const revokedTeamId = payload?.teamId as string | undefined
|
const revokedTeamId = payload?.teamId as string | undefined
|
||||||
@ -163,10 +195,13 @@ function TeamCardComponent(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Relevante Gruppen → gedrosseltes Reload
|
||||||
|
if (SELF_EVENTS.has(type)) { softReload(); return }
|
||||||
if (TEAM_EVENTS.has(type)) { softReload(); return }
|
if (TEAM_EVENTS.has(type)) { softReload(); return }
|
||||||
if (INVITE_EVENTS.has(type) && myTeams.length === 0) { softReload(); return }
|
if (INVITE_EVENTS.has(type) && myTeams.length === 0) { softReload(); return }
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [lastEvent, myTeams.length, selectedTeam, pendingInvitations])
|
}, [lastEvent, myTeams.length]) // bewusst schlanke Dependencies
|
||||||
|
|
||||||
/* ------- Render-Zweige ------- */
|
/* ------- Render-Zweige ------- */
|
||||||
|
|
||||||
@ -177,7 +212,7 @@ function TeamCardComponent(
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{pendingInvitations.length > 0 && (
|
{pendingInvitations.length > 0 && (
|
||||||
<div className="space-y-4 mb-4">
|
<div className="mb-4 space-y-4">
|
||||||
{pendingInvitations.map(inv => (
|
{pendingInvitations.map(inv => (
|
||||||
<TeamInvitationBanner
|
<TeamInvitationBanner
|
||||||
key={inv.id}
|
key={inv.id}
|
||||||
@ -272,16 +307,16 @@ function TeamCardComponent(
|
|||||||
focus:outline-none focus:ring-2 focus:ring-blue-500
|
focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-3 mb-3">
|
<div className="mb-3 flex items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<img
|
<img
|
||||||
src={team.logo ? `/assets/img/logos/${team.logo}` : `/assets/img/logos/cs2.webp`}
|
src={team.logo ? `/assets/img/logos/${team.logo}` : `/assets/img/logos/cs2.webp`}
|
||||||
alt={team.name ?? 'Teamlogo'}
|
alt={team.name ?? 'Teamlogo'}
|
||||||
className="w-12 h-12 rounded-full object-cover border
|
className="h-12 w-12 rounded-full border object-cover
|
||||||
border-gray-200 dark:border-neutral-600"
|
border-gray-200 dark:border-neutral-600"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium truncate text-gray-800 dark:text-neutral-200">
|
<span className="truncate font-medium text-gray-800 dark:text-neutral-200">
|
||||||
{team.name ?? 'Team'}
|
{team.name ?? 'Team'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-500 dark:text-neutral-400">
|
<span className="text-xs text-gray-500 dark:text-neutral-400">
|
||||||
@ -289,8 +324,7 @@ function TeamCardComponent(
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Chevron */}
|
<svg className="h-4 w-4 text-gray-400" viewBox="0 0 24 24">
|
||||||
<svg className="w-4 h-4 text-gray-400" viewBox="0 0 24 24">
|
|
||||||
<path fill="currentColor" d="M9 6l6 6l-6 6V6z" />
|
<path fill="currentColor" d="M9 6l6 6l-6 6V6z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@ -302,7 +336,7 @@ function TeamCardComponent(
|
|||||||
src={p.avatar}
|
src={p.avatar}
|
||||||
alt={p.name}
|
alt={p.name}
|
||||||
title={p.name}
|
title={p.name}
|
||||||
className="w-8 h-8 rounded-full border-2 border-white
|
className="h-8 w-8 rounded-full border-2 border-white
|
||||||
dark:border-neutral-800 object-cover"
|
dark:border-neutral-800 object-cover"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -314,20 +348,21 @@ function TeamCardComponent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectedTeam gesetzt → Detailansicht mit "Zurück" oben links
|
// selectedTeam gesetzt → Detailansicht
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4 xl:mb-8">
|
<div className="mb-4 xl:mb-8">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="mb-2 flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedTeam(null)}
|
onClick={() => setSelectedTeam(null)}
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm border border-gray-300
|
className="inline-flex items-center gap-1.5 rounded-lg border border-gray-300
|
||||||
dark:border-neutral-600 bg-white dark:bg-neutral-800 hover:bg-gray-50 dark:hover:bg-neutral-700
|
bg-white px-3 py-1.5 text-sm hover:bg-gray-50 focus:outline-none
|
||||||
focus:outline-none focus:ring-2 focus:ring-blue-500"
|
focus:ring-2 focus:ring-blue-500 dark:border-neutral-600
|
||||||
|
dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
||||||
aria-label="Zurück zur Teamübersicht"
|
aria-label="Zurück zur Teamübersicht"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" className="w-4 h-4" aria-hidden>
|
<svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden>
|
||||||
<path fill="currentColor" d="M15 18l-6-6l6-6v12z" />
|
<path fill="currentColor" d="M15 18l-6-6l6-6v12z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Zurück</span>
|
<span>Zurück</span>
|
||||||
@ -337,7 +372,6 @@ function TeamCardComponent(
|
|||||||
<h1 className="text-lg font-semibold text-gray-800 dark:text-neutral-200">
|
<h1 className="text-lg font-semibold text-gray-800 dark:text-neutral-200">
|
||||||
Teamverwaltung
|
Teamverwaltung
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-sm text-gray-500 dark:text-neutral-500">
|
<p className="text-sm text-gray-500 dark:text-neutral-500">
|
||||||
Verwalte dein Team und lade Mitglieder ein
|
Verwalte dein Team und lade Mitglieder ein
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -527,7 +527,7 @@ export default function TeamMemberView({
|
|||||||
}}
|
}}
|
||||||
className="h-[34px] px-3 flex items-center justify-center"
|
className="h-[34px] px-3 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
✔
|
<span className="text-green-600">✓</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
title="Abbrechen"
|
title="Abbrechen"
|
||||||
@ -540,7 +540,7 @@ export default function TeamMemberView({
|
|||||||
}}
|
}}
|
||||||
className="h-[34px] px-3 flex items-center justify-center"
|
className="h-[34px] px-3 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
✖
|
<span className="text-red-600">✕</span>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -73,7 +73,7 @@ export default function ServerView({
|
|||||||
defaultValue=""
|
defaultValue=""
|
||||||
className="border py-2.5 sm:py-3 px-4 block w-full rounded-lg sm:text-sm
|
className="border py-2.5 sm:py-3 px-4 block w-full rounded-lg sm:text-sm
|
||||||
dark:bg-neutral-800 dark:text-neutral-200 dark:border-neutral-700"
|
dark:bg-neutral-800 dark:text-neutral-200 dark:border-neutral-700"
|
||||||
placeholder={cfg.pterodactylServerApiKey ? '•••••••• (gesetzt)' : 'noch nicht gesetzt'}
|
placeholder={cfg.pterodactylServerApiKey ? '•••••••• (gesetzt)' : 'ptla_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-neutral-500">
|
<p className="mt-1 text-xs text-neutral-500">
|
||||||
@ -118,7 +118,7 @@ export default function ServerView({
|
|||||||
defaultValue=""
|
defaultValue=""
|
||||||
className="border py-2.5 sm:py-3 px-4 block w-full rounded-lg sm:text-sm
|
className="border py-2.5 sm:py-3 px-4 block w-full rounded-lg sm:text-sm
|
||||||
dark:bg-neutral-800 dark:text-neutral-200 dark:border-neutral-700"
|
dark:bg-neutral-800 dark:text-neutral-200 dark:border-neutral-700"
|
||||||
placeholder={meUser?.pterodactylClientApiKey ? '•••••••• (gesetzt)' : 'noch nicht gesetzt'}
|
placeholder={meUser?.pterodactylClientApiKey ? '•••••••• (gesetzt)' : 'ptlc_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-neutral-500">
|
<p className="mt-1 text-xs text-neutral-500">
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import AppearanceSettings from "./account/AppearanceSettings"
|
|
||||||
import AuthCodeSettings from "./account/AuthCodeSettings"
|
|
||||||
import LatestKnownCodeSettings from "./account/ShareCodeSettings"
|
|
||||||
import { useTranslations, useLocale } from 'next-intl'
|
|
||||||
import UserSettings from "./account/UserSettings"
|
|
||||||
|
|
||||||
export default function AccountSettings() {
|
|
||||||
|
|
||||||
// Übersetzungen
|
|
||||||
const tSettings = useTranslations('settings')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>{/* Account Card */}
|
|
||||||
<div className="">
|
|
||||||
{/* Title */}
|
|
||||||
<div className="mb-4 xl:mb-8">
|
|
||||||
<h1 className="text-lg font-semibold text-gray-800 dark:text-neutral-200">
|
|
||||||
{tSettings("tabs.account.title")}
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-neutral-500">
|
|
||||||
{tSettings("tabs.account.description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/* End Title */}
|
|
||||||
|
|
||||||
{/* Form */}
|
|
||||||
<form>
|
|
||||||
|
|
||||||
{/* User Settings */}
|
|
||||||
<UserSettings />
|
|
||||||
{/* End User Settings */}
|
|
||||||
|
|
||||||
{/* Auth Code Settings */}
|
|
||||||
<AuthCodeSettings />
|
|
||||||
{/* End Auth Code Settings */}
|
|
||||||
|
|
||||||
{/* Auth Code Settings */}
|
|
||||||
<LatestKnownCodeSettings />
|
|
||||||
{/* End Auth Code Settings */}
|
|
||||||
|
|
||||||
{/* Appearance */}
|
|
||||||
<AppearanceSettings />
|
|
||||||
{/* End Appearance */}
|
|
||||||
</form>
|
|
||||||
{/* End Form */}
|
|
||||||
</div>
|
|
||||||
{/* End Account Card */}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -18,31 +18,31 @@ export default function AppearanceSettings() {
|
|||||||
if (!mounted) return null
|
if (!mounted) return null
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{ id: 'system', label: tSettings("tabs.account.page.AppearanceSettings.theme.system"), img: 'account-system-image.svg' },
|
{ id: 'system', label: tSettings("sections.account.page.AppearanceSettings.theme.system"), img: 'account-system-image.svg' },
|
||||||
{ id: 'light', label: tSettings("tabs.account.page.AppearanceSettings.theme.light"), img: 'account-light-image.svg' },
|
{ id: 'light', label: tSettings("sections.account.page.AppearanceSettings.theme.light"), img: 'account-light-image.svg' },
|
||||||
{ id: 'dark', label: tSettings("tabs.account.page.AppearanceSettings.theme.dark"), img: 'account-dark-image.svg' },
|
{ id: 'dark', label: tSettings("sections.account.page.AppearanceSettings.theme.dark"), img: 'account-dark-image.svg' },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-6 sm:py-8 space-y-5 border-t border-gray-200 first:border-t-0 dark:border-neutral-700">
|
<div className="py-3 sm:py-4 space-y-5">
|
||||||
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
||||||
<div className="sm:col-span-4 2xl:col-span-2">
|
<div className="sm:col-span-4 2xl:col-span-2">
|
||||||
<label className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
<label className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
||||||
{tSettings("tabs.account.page.AppearanceSettings.name")}
|
{tSettings("sections.account.page.AppearanceSettings.name")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:col-span-8 xl:col-span-6 2xl:col-span-5">
|
<div className="sm:col-span-8 xl:col-span-6 2xl:col-span-5">
|
||||||
<p className="text-sm text-gray-500 dark:text-neutral-500">
|
<p className="text-sm text-gray-500 dark:text-neutral-500">
|
||||||
{tSettings("tabs.account.page.AppearanceSettings.description")}
|
{tSettings("sections.account.page.AppearanceSettings.description")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 className="mt-3 text-sm font-semibold text-gray-800 dark:text-neutral-200">
|
<h3 className="mt-3 text-sm font-semibold text-gray-800 dark:text-neutral-200">
|
||||||
{tSettings("tabs.account.page.AppearanceSettings.theme-mode")}
|
{tSettings("sections.account.page.AppearanceSettings.theme-mode")}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p className="text-sm text-gray-500 dark:text-neutral-500">
|
<p className="text-sm text-gray-500 dark:text-neutral-500">
|
||||||
{tSettings("tabs.account.page.AppearanceSettings.theme-mode-description")}
|
{tSettings("sections.account.page.AppearanceSettings.theme-mode-description")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
// AuthCodeSettings.tsx
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
@ -101,20 +103,25 @@ export default function AuthCodeSettings() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-6 sm:py-8 space-y-5 border-t border-gray-200 dark:border-neutral-700">
|
<div className="py-3 sm:py-4 space-y-5">
|
||||||
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
||||||
<div className="sm:col-span-4 2xl:col-span-2">
|
<div className="sm:col-span-4 2xl:col-span-2">
|
||||||
<label htmlFor="auth-code" className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
<label htmlFor="auth-code" className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
||||||
{tSettings("tabs.account.page.AuthCodeSettings.name")}
|
{tSettings("sections.account.page.AuthCodeSettings.name")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<Popover text={tSettings("tabs.account.page.AuthCodeSettings.question")} size="xl">
|
<Popover text={tSettings("sections.account.page.AuthCodeSettings.question")} size="xl">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<i><q>{tSettings("tabs.account.page.AuthCodeSettings.description")}</q></i>
|
<i className="block w-full">
|
||||||
|
<q className="block w-full text-justify hyphens-auto">
|
||||||
|
{tSettings("sections.account.page.AuthCodeSettings.description")}
|
||||||
|
</q>
|
||||||
|
</i>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{tSettings("tabs.account.find-code")}
|
{tSettings("sections.account.find-code")}
|
||||||
<Link
|
<Link
|
||||||
href={tSettings("tabs.account.url")}
|
href={tSettings("sections.account.url")}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-600 underline hover:text-blue-800"
|
className="text-blue-600 underline hover:text-blue-800"
|
||||||
>
|
>
|
||||||
@ -158,7 +165,7 @@ export default function AuthCodeSettings() {
|
|||||||
|
|
||||||
{touched && (
|
{touched && (
|
||||||
<p className={`text-sm mt-2 ${authCodeValid ? 'text-teal-600' : 'text-red-600'}`}>
|
<p className={`text-sm mt-2 ${authCodeValid ? 'text-teal-600' : 'text-red-600'}`}>
|
||||||
{authCodeValid ? `✓ ${tCommon("saved")}!` : `${tSettings("tabs.account.page.AuthCodeSettings.invalid")}`}
|
{authCodeValid ? `✓ ${tCommon("saved")}!` : `${tSettings("sections.account.page.AuthCodeSettings.invalid")}`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -126,20 +126,20 @@ export default function LatestKnownCodeSettings() {
|
|||||||
const showError = touched && !isValid
|
const showError = touched && !isValid
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-6 sm:py-8 space-y-5 border-t border-gray-200 dark:border-neutral-700">
|
<div className="py-3 sm:py-4 space-y-5">
|
||||||
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
||||||
<div className="sm:col-span-4 2xl:col-span-2">
|
<div className="sm:col-span-4 2xl:col-span-2">
|
||||||
<label htmlFor="known-code" className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
<label htmlFor="known-code" className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
||||||
{tSettings("tabs.account.page.ShareCodeSettings.name")}
|
{tSettings("sections.account.page.ShareCodeSettings.name")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<Popover text={tSettings("tabs.account.page.ShareCodeSettings.question")} size="xl">
|
<Popover text={tSettings("sections.account.page.ShareCodeSettings.question")} size="xl">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<i><q>{tSettings("tabs.account.page.ShareCodeSettings.description")}</q></i>
|
<i><q>{tSettings("sections.account.page.ShareCodeSettings.description")}</q></i>
|
||||||
<p>
|
<p>
|
||||||
{tSettings("tabs.account.find-code")}
|
{tSettings("sections.account.find-code")}
|
||||||
<Link
|
<Link
|
||||||
href={tSettings("tabs.account.url")}
|
href={tSettings("sections.account.url")}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-600 underline hover:text-blue-800"
|
className="text-blue-600 underline hover:text-blue-800"
|
||||||
>
|
>
|
||||||
@ -175,7 +175,7 @@ export default function LatestKnownCodeSettings() {
|
|||||||
|
|
||||||
{showError && (
|
{showError && (
|
||||||
<p className="text-sm text-red-600 mt-2">
|
<p className="text-sm text-red-600 mt-2">
|
||||||
{tSettings("tabs.account.page.ShareCodeSettings.invalid-share-code")}
|
{tSettings("sections.account.page.ShareCodeSettings.invalid-share-code")}
|
||||||
<Link
|
<Link
|
||||||
href="https://help.steampowered.com/de/wizard/HelpWithGameIssue/?appid=730&issueid=128"
|
href="https://help.steampowered.com/de/wizard/HelpWithGameIssue/?appid=730&issueid=128"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import {useEffect, useMemo, useRef, useState} from 'react'
|
import {useEffect, useMemo, useRef, useState} from 'react'
|
||||||
import Popover from '../../Popover'
|
import Popover from '../../Popover'
|
||||||
import {useTranslations} from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
|
|
||||||
// Versuche, native Liste zu nehmen; fallback auf gängige Auswahl.
|
// Versuche, native Liste zu nehmen; fallback auf gängige Auswahl.
|
||||||
const FALLBACK_TIMEZONES = [
|
const FALLBACK_TIMEZONES = [
|
||||||
@ -38,6 +38,7 @@ export default function UserSettings() {
|
|||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [savedOk, setSavedOk] = useState<boolean | null>(null)
|
const [savedOk, setSavedOk] = useState<boolean | null>(null)
|
||||||
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
||||||
|
const [touched, setTouched] = useState(false)
|
||||||
|
|
||||||
const timeZones = useMemo(() => getTimeZones(), [])
|
const timeZones = useMemo(() => getTimeZones(), [])
|
||||||
const debounceTimer = useRef<number | null>(null)
|
const debounceTimer = useRef<number | null>(null)
|
||||||
@ -84,6 +85,7 @@ export default function UserSettings() {
|
|||||||
}
|
}
|
||||||
setInitialTz(tz)
|
setInitialTz(tz)
|
||||||
setSavedOk(true)
|
setSavedOk(true)
|
||||||
|
setTouched(false) // <- hinzu
|
||||||
// kleines Auto-Reset des „Gespeichert“-Hinweises
|
// kleines Auto-Reset des „Gespeichert“-Hinweises
|
||||||
window.setTimeout(() => setSavedOk(null), 2000)
|
window.setTimeout(() => setSavedOk(null), 2000)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -111,21 +113,13 @@ export default function UserSettings() {
|
|||||||
}
|
}
|
||||||
}, [timeZone, initialTz, loading])
|
}, [timeZone, initialTz, loading])
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="py-6 sm:py-8 border-t border-gray-200 dark:border-neutral-700">
|
|
||||||
<p className="text-sm text-gray-500 dark:text-neutral-500">Loading…</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-6 sm:py-8 space-y-5 border-t border-gray-200 dark:border-neutral-700">
|
<div className="py-3 sm:py-4 space-y-5">
|
||||||
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
||||||
{/* Label + Hilfe */}
|
{/* Label + Hilfe */}
|
||||||
<div className="sm:col-span-4 2xl:col-span-2">
|
<div className="sm:col-span-4 2xl:col-span-2">
|
||||||
<label htmlFor="user-timezone" className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
<label htmlFor="user-timezone" className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
||||||
{tSettings('tabs.account.page.UserSettings.timezone-label')}
|
{tSettings('sections.user.timezone-label')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -134,18 +128,30 @@ export default function UserSettings() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<select
|
<select
|
||||||
id="user-timezone"
|
id="user-timezone"
|
||||||
value={timeZone ?? ''}
|
value={timeZone ?? ''} // bleibt kontrolliert
|
||||||
onChange={(e) => setTimeZone(e.target.value || null)}
|
onChange={(e) => { setTimeZone(e.target.value || null); setTouched(true) }}
|
||||||
disabled={saving}
|
disabled={saving || loading} // während Lade-/Speicherphase sperren
|
||||||
className="w-full rounded-lg border border-gray-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 px-3 py-2 text-sm disabled:opacity-70"
|
aria-busy={loading ? 'true' : undefined} // a11y
|
||||||
|
className="max-w-md rounded-lg border border-gray-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 px-3 py-2 text-sm disabled:opacity-70"
|
||||||
>
|
>
|
||||||
<option value="">{tSettings('tabs.account.page.UserSettings.timezone-system')}</option>
|
{loading ? (
|
||||||
{timeZones.map((tz) => (
|
// Nur während des Ladens:
|
||||||
<option key={tz} value={tz}>{tz}</option>
|
<option value="" disabled>
|
||||||
))}
|
{tCommon("loading")}...
|
||||||
|
</option>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<option value="">
|
||||||
|
{tSettings('sections.user.timezone-system')}
|
||||||
|
</option>
|
||||||
|
{timeZones.map((tz) => (
|
||||||
|
<option key={tz} value={tz}>{tz}</option>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{/* Live-Status (optional dezent) */}
|
{/* Live-Status rechts (optional) */}
|
||||||
<span className="text-xs min-w-[80px] text-right">
|
<span className="text-xs min-w-[80px] text-right">
|
||||||
{saving && <span className="text-gray-500 dark:text-neutral-400">{tCommon('saving')}…</span>}
|
{saving && <span className="text-gray-500 dark:text-neutral-400">{tCommon('saving')}…</span>}
|
||||||
{savedOk === true && <span className="text-teal-600">✓ {tCommon('saved')}</span>}
|
{savedOk === true && <span className="text-teal-600">✓ {tCommon('saved')}</span>}
|
||||||
@ -153,13 +159,9 @@ export default function UserSettings() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Fehlertext detaillierter */}
|
{errorMsg && <p className="text-xs mt-1 text-red-600">{errorMsg}</p>}
|
||||||
{errorMsg && (
|
|
||||||
<p className="text-xs mt-1 text-red-600">{errorMsg}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Hinweis, wenn nichts geändert wurde */}
|
{touched && !saving && !loading && savedOk === null && timeZone === initialTz && (
|
||||||
{savedOk === null && timeZone === initialTz && (
|
|
||||||
<p className="text-xs mt-2 text-gray-500 dark:text-neutral-400">
|
<p className="text-xs mt-2 text-gray-500 dark:text-neutral-400">
|
||||||
{tCommon('no-changes')}
|
{tCommon('no-changes')}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
136
src/app/[locale]/components/settings/privacy/PrivacySettings.tsx
Normal file
136
src/app/[locale]/components/settings/privacy/PrivacySettings.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
// app/[locale]/settings/_components/PrivacySettings.tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import {useEffect, useRef, useState} from 'react'
|
||||||
|
import { useTranslations } from 'next-intl'
|
||||||
|
|
||||||
|
export default function PrivacySettings() {
|
||||||
|
const tSettings = useTranslations('settings')
|
||||||
|
const tCommon = useTranslations('common')
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [savedOk, setSavedOk] = useState<boolean | null>(null)
|
||||||
|
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Wert (default=true falls API nichts liefert)
|
||||||
|
const [canBeInvited, setCanBeInvited] = useState<boolean>(true)
|
||||||
|
const [initial, setInitial] = useState<boolean>(true)
|
||||||
|
|
||||||
|
const debounceTimer = useRef<number | null>(null)
|
||||||
|
const inFlight = useRef<AbortController | null>(null)
|
||||||
|
|
||||||
|
// Laden
|
||||||
|
useEffect(() => {
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/user/privacy', { cache: 'no-store' })
|
||||||
|
const data = await res.json().catch(() => ({}))
|
||||||
|
const value = typeof data?.canBeInvited === 'boolean' ? data.canBeInvited : true
|
||||||
|
setCanBeInvited(value)
|
||||||
|
setInitial(value)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[PrivacySettings] load failed:', e)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Speichern (debounced)
|
||||||
|
const persist = async (value: boolean) => {
|
||||||
|
inFlight.current?.abort()
|
||||||
|
const ctrl = new AbortController()
|
||||||
|
inFlight.current = ctrl
|
||||||
|
|
||||||
|
setSaving(true)
|
||||||
|
setSavedOk(null)
|
||||||
|
setErrorMsg(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/user/privacy', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ canBeInvited: value }),
|
||||||
|
signal: ctrl.signal
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const j = await res.json().catch(() => ({}))
|
||||||
|
throw new Error(j?.message || `HTTP ${res.status}`)
|
||||||
|
}
|
||||||
|
setInitial(value)
|
||||||
|
setSavedOk(true)
|
||||||
|
window.setTimeout(() => setSavedOk(null), 2000)
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name === 'AbortError') return
|
||||||
|
console.error('[PrivacySettings] save failed:', e)
|
||||||
|
setSavedOk(false)
|
||||||
|
setErrorMsg(e?.message ?? 'Save failed')
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) return
|
||||||
|
if (canBeInvited === initial) return
|
||||||
|
if (debounceTimer.current) window.clearTimeout(debounceTimer.current)
|
||||||
|
debounceTimer.current = window.setTimeout(() => {
|
||||||
|
persist(canBeInvited)
|
||||||
|
}, 400) as unknown as number
|
||||||
|
return () => {
|
||||||
|
if (debounceTimer.current) window.clearTimeout(debounceTimer.current)
|
||||||
|
}
|
||||||
|
}, [canBeInvited, initial, loading])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-6 sm:py-8 border-t border-gray-200 dark:border-neutral-700">
|
||||||
|
<div className="grid sm:grid-cols-12 gap-y-2 sm:gap-y-0 sm:gap-x-5 items-start">
|
||||||
|
{/* Label-Spalte */}
|
||||||
|
<div className="sm:col-span-4 2xl:col-span-2">
|
||||||
|
<span className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
||||||
|
{tSettings('sections.privacy.invites.label')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Inhalt-Spalte */}
|
||||||
|
<div className="sm:col-span-8 xl:col-span-6 2xl:col-span-5">
|
||||||
|
{/* Toggle */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={loading || saving}
|
||||||
|
onClick={() => setCanBeInvited(v => !v)}
|
||||||
|
className={[
|
||||||
|
'relative inline-flex h-6 w-11 items-center rounded-full transition',
|
||||||
|
canBeInvited ? 'bg-emerald-600' : 'bg-gray-300 dark:bg-neutral-700',
|
||||||
|
'disabled:opacity-60 disabled:cursor-not-allowed'
|
||||||
|
].join(' ')}
|
||||||
|
aria-pressed={canBeInvited}
|
||||||
|
aria-label={tSettings('sections.privacy.invites.label')}
|
||||||
|
title={undefined}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
'inline-block h-5 w-5 transform rounded-full bg-white transition',
|
||||||
|
canBeInvited ? 'translate-x-5' : 'translate-x-1',
|
||||||
|
].join(' ')}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p className="mt-2 text-sm text-gray-500 dark:text-neutral-400">
|
||||||
|
{tSettings('sections.privacy.invites.help')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Status/Fehler */}
|
||||||
|
<div className="mt-1 text-xs min-h-[1rem]">
|
||||||
|
{loading && <span className="text-gray-500 dark:text-neutral-400">{tCommon('loading') ?? 'Laden…'}</span>}
|
||||||
|
{saving && <span className="text-gray-500 dark:text-neutral-400">{tCommon('saving') ?? 'Speichern…'}</span>}
|
||||||
|
{savedOk === true && <span className="text-teal-600">✓ {tCommon('saved') ?? 'Gespeichert'}</span>}
|
||||||
|
{savedOk === false && <span className="text-red-600">{tCommon('save-failed') ?? 'Speichern fehlgeschlagen'}</span>}
|
||||||
|
{errorMsg && <p className="text-red-600">{errorMsg}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -59,7 +59,7 @@ export default async function RootLayout({children, params}: Props) {
|
|||||||
<main className="flex-1 min-w-0 overflow-hidden">
|
<main className="flex-1 min-w-0 overflow-hidden">
|
||||||
<div className="h-full box-border p-4 sm:p-6">{children}</div>
|
<div className="h-full box-border p-4 sm:p-6">{children}</div>
|
||||||
</main>
|
</main>
|
||||||
<GameBannerSpacer />
|
<GameBannerSpacer className="hidden sm:block" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// app/match-details/[matchId]/vote/VoteClient.tsx
|
// app/match-details/[matchId]/vote/VoteClient.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import MapVotePanel from '../components/MapVotePanel'
|
import MapVotePanel from '@/app/[locale]/components/MapVotePanel'
|
||||||
import { useMatch } from '../MatchContext' // aus dem Layout-Context
|
import { useMatch } from '../MatchContext' // aus dem Layout-Context
|
||||||
|
|
||||||
export default function VoteClient() {
|
export default function VoteClient() {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// app/match-details/[matchId]/vote/page.tsx
|
// app/match-details/[matchId]/vote/page.tsx
|
||||||
import Card from '../components/Card'
|
import Card from '@/app/[locale]/components/Card'
|
||||||
import VoteClient from './VoteClient' // Client-Komponente
|
import VoteClient from './VoteClient' // Client-Komponente
|
||||||
|
|
||||||
export default function VotePage() {
|
export default function VotePage() {
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { use, useState } from 'react'
|
|
||||||
import { notFound } from 'next/navigation'
|
|
||||||
import Card from '../../components/Card'
|
|
||||||
import CreateTeamButton from '../../components/CreateTeamButton'
|
|
||||||
import TeamCardComponent from '../../components/TeamCardComponent'
|
|
||||||
import AccountSettings from '../../components/settings/AccountSettings'
|
|
||||||
|
|
||||||
export default function SettingsTabPage({ params }: { params: Promise<{ tab: string }> }) {
|
|
||||||
const { tab } = use(params)
|
|
||||||
|
|
||||||
const renderTabContent = () => {
|
|
||||||
switch (tab) {
|
|
||||||
case 'account':
|
|
||||||
return (
|
|
||||||
<Card maxWidth='auto'>
|
|
||||||
<AccountSettings />
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
case 'privacy':
|
|
||||||
return (
|
|
||||||
<Card maxWidth='auto' />
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return notFound()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{renderTabContent()}</>
|
|
||||||
}
|
|
||||||
25
src/app/[locale]/settings/_sections/AccountSection.tsx
Normal file
25
src/app/[locale]/settings/_sections/AccountSection.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// app/[locale]/settings/_sections/AccountSection.tsx
|
||||||
|
|
||||||
|
import AuthCodeSettings from "../../components/settings/account/AuthCodeSettings";
|
||||||
|
import LatestKnownCodeSettings from "../../components/settings/account/ShareCodeSettings";
|
||||||
|
import { useTranslations } from 'next-intl'
|
||||||
|
|
||||||
|
export default function AccountSection() {
|
||||||
|
|
||||||
|
const tSettings = useTranslations('settings')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="account" className="scroll-mt-16 pb-10">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">{tSettings("sections.account.short")}</h2>
|
||||||
|
<form className="border-t border-gray-200 dark:border-neutral-700">
|
||||||
|
{/* Auth Code Settings */}
|
||||||
|
<AuthCodeSettings />
|
||||||
|
{/* End Auth Code Settings */}
|
||||||
|
|
||||||
|
{/* Auth Code Settings */}
|
||||||
|
<LatestKnownCodeSettings />
|
||||||
|
{/* End Auth Code Settings */}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
src/app/[locale]/settings/_sections/AppearanceSection.tsx
Normal file
18
src/app/[locale]/settings/_sections/AppearanceSection.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// app/[locale]/settings/_sections/AppearanceSection.tsx
|
||||||
|
|
||||||
|
import AppearanceSettings from "../../components/settings/account/AppearanceSettings";
|
||||||
|
import { useTranslations } from 'next-intl'
|
||||||
|
|
||||||
|
export default function AppearanceSection() {
|
||||||
|
|
||||||
|
const tSettings = useTranslations('settings')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="account" className="scroll-mt-16 pb-10">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">{tSettings("sections.appearance.short")}</h2>
|
||||||
|
<form className="border-t border-gray-200 dark:border-neutral-700">
|
||||||
|
<AppearanceSettings />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
src/app/[locale]/settings/_sections/PrivacySection.tsx
Normal file
18
src/app/[locale]/settings/_sections/PrivacySection.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// app/[locale]/settings/_sections/PrivacySection.tsx
|
||||||
|
|
||||||
|
import PrivacySettings from "../../components/settings/privacy/PrivacySettings";
|
||||||
|
import { useTranslations } from 'next-intl'
|
||||||
|
|
||||||
|
export default function PrivacySection() {
|
||||||
|
|
||||||
|
const tSettings = useTranslations('settings')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="privacy" className="scroll-mt-16 pb-10">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">{tSettings("sections.privacy.short")}</h2>
|
||||||
|
<form className="border-t border-gray-200 dark:border-neutral-700">
|
||||||
|
<PrivacySettings />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
src/app/[locale]/settings/_sections/UserSection.tsx
Normal file
18
src/app/[locale]/settings/_sections/UserSection.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// app/[locale]/settings/_sections/UserSection.tsx
|
||||||
|
|
||||||
|
import UserSettings from "../../components/settings/account/UserSettings";
|
||||||
|
import { useTranslations } from 'next-intl'
|
||||||
|
|
||||||
|
export default function UserSection() {
|
||||||
|
|
||||||
|
const tSettings = useTranslations('settings')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="account" className="scroll-mt-16 pb-10">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">{tSettings("sections.user.short")}</h2>
|
||||||
|
<form className="border-t border-gray-200 dark:border-neutral-700">
|
||||||
|
<UserSettings />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,21 +1,43 @@
|
|||||||
import { Tabs } from '../components/Tabs'
|
'use client'
|
||||||
import Tab from '../components/Tab'
|
|
||||||
import { useTranslations, useLocale } from 'next-intl'
|
|
||||||
|
|
||||||
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
|
import {useRef} from 'react'
|
||||||
|
import { useTranslations } from 'next-intl'
|
||||||
|
import ScrollSpyTabs from '../components/ScrollSpyTabs'
|
||||||
|
import Card from '../components/Card'
|
||||||
|
|
||||||
// Übersetzungen
|
export default function SettingsLayoutSettings({ children }: { children: React.ReactNode }) {
|
||||||
const tSettings = useTranslations('settings')
|
const tSettings = useTranslations('settings')
|
||||||
|
const mainRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ id: 'user', label: tSettings('sections.user.short') },
|
||||||
|
{ id: 'privacy', label: tSettings('sections.privacy.short') },
|
||||||
|
{ id: 'account', label: tSettings('sections.account.short') },
|
||||||
|
{ id: 'appearance', label: tSettings('sections.appearance.short') },
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<Card maxWidth='full'>
|
||||||
<Tabs>
|
<div className="container mx-auto h-[calc(100vh-4rem)] grid gap-6 md:grid-cols-[220px_1fr] min-h-0">
|
||||||
<Tab name={tSettings("tabs.account.short")} href="/settings/account" />
|
{/* Sidebar */}
|
||||||
<Tab name={tSettings("tabs.privacy.short")} href="/settings/privacy" />
|
<aside className="min-h-0 pr-2">
|
||||||
</Tabs>
|
<div className="sticky top-0 pt-2">
|
||||||
<div className="mt-6">
|
<ScrollSpyTabs
|
||||||
{children}
|
items={items}
|
||||||
|
containerRef={mainRef} // <- rechter Scroll-Container
|
||||||
|
className="flex flex-col gap-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* rechter, durchgehend scrollbarer Bereich */}
|
||||||
|
<main
|
||||||
|
ref={mainRef}
|
||||||
|
className="min-h-0 h-full overflow-y-auto pr-1 md:pr-2 overscroll-contain"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,17 @@
|
|||||||
import { redirect } from 'next/navigation'
|
// app/[locale]/settings/page.tsx
|
||||||
|
import Card from '../components/Card'
|
||||||
|
import AccountSection from './_sections/AccountSection'
|
||||||
|
import AppearanceSection from './_sections/AppearanceSection'
|
||||||
|
import PrivacySection from './_sections/PrivacySection'
|
||||||
|
import UserSection from './_sections/UserSection'
|
||||||
|
|
||||||
export default function SettingPage() {
|
export default function SettingsPage() {
|
||||||
redirect('/settings/account')
|
return (
|
||||||
|
<Card maxWidth='full'>
|
||||||
|
<UserSection />
|
||||||
|
<PrivacySection />
|
||||||
|
<AccountSection />
|
||||||
|
<AppearanceSection />
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,14 +26,33 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
}
|
}
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
|
// Tiefste Ebene in der MapVote-Kette
|
||||||
await tx.mapVoteStep.deleteMany({ where: { vote: { matchId } } })
|
await tx.mapVoteStep.deleteMany({ where: { vote: { matchId } } })
|
||||||
await tx.mapVote.deleteMany({ where: { matchId } })
|
await tx.mapVote.deleteMany({ where: { matchId } })
|
||||||
|
|
||||||
|
// Stats/Players (Stats zuerst)
|
||||||
await tx.playerStats.deleteMany({ where: { matchId } })
|
await tx.playerStats.deleteMany({ where: { matchId } })
|
||||||
await tx.matchPlayer.deleteMany({ where: { matchId } })
|
await tx.matchPlayer.deleteMany({ where: { matchId } })
|
||||||
|
|
||||||
|
// Sonstiges, das direkt/indirekt auf Match zeigt
|
||||||
await tx.rankHistory.deleteMany({ where: { matchId } })
|
await tx.rankHistory.deleteMany({ where: { matchId } })
|
||||||
await tx.demoFile.deleteMany({ where: { matchId } })
|
await tx.demoFile.deleteMany({ where: { matchId } })
|
||||||
await tx.serverRequest.deleteMany({ where: { matchId } })
|
await tx.serverRequest.deleteMany({ where: { matchId } }) // hat zwar keinen FK, aber aufräumen ist ok
|
||||||
await tx.schedule.deleteMany({ where: { linkedMatchId: matchId } })
|
await tx.schedule.deleteMany({ where: { linkedMatchId: matchId } })
|
||||||
|
|
||||||
|
// ✅ Neu: Ready-Entries
|
||||||
|
await tx.matchReady.deleteMany({ where: { matchId } })
|
||||||
|
|
||||||
|
// ✅ Optional: M-N Join-Table Einträge sicher entfernen
|
||||||
|
await tx.match.update({
|
||||||
|
where: { id: matchId },
|
||||||
|
data: {
|
||||||
|
teamAUsers: { set: [] },
|
||||||
|
teamBUsers: { set: [] },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Jetzt erst das Match selbst
|
||||||
await tx.match.delete({ where: { id: matchId } })
|
await tx.match.delete({ where: { id: matchId } })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
40
src/app/api/user/privacy/route.ts
Normal file
40
src/app/api/user/privacy/route.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// src/app/api/user/privacy/route.ts
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getServerSession } from 'next-auth'
|
||||||
|
import { authOptions } from '@/lib/auth'
|
||||||
|
import { prisma } from '@/lib/prisma'
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions(req))
|
||||||
|
if (!session?.user?.steamId) {
|
||||||
|
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { steamId: session.user.steamId },
|
||||||
|
select: { canBeInvited: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ canBeInvited: user?.canBeInvited ?? true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(req: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions(req))
|
||||||
|
if (!session?.user?.steamId) {
|
||||||
|
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json().catch(() => ({}))
|
||||||
|
const { canBeInvited } = body ?? {}
|
||||||
|
|
||||||
|
if (typeof canBeInvited !== 'boolean') {
|
||||||
|
return NextResponse.json({ message: 'Ungültiger Wert für canBeInvited' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { steamId: session.user.steamId },
|
||||||
|
data: { canBeInvited },
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true })
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@ -135,7 +135,8 @@ exports.Prisma.UserScalarFieldEnum = {
|
|||||||
status: 'status',
|
status: 'status',
|
||||||
lastActiveAt: 'lastActiveAt',
|
lastActiveAt: 'lastActiveAt',
|
||||||
pterodactylClientApiKey: 'pterodactylClientApiKey',
|
pterodactylClientApiKey: 'pterodactylClientApiKey',
|
||||||
timeZone: 'timeZone'
|
timeZone: 'timeZone',
|
||||||
|
canBeInvited: 'canBeInvited'
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.Prisma.TeamScalarFieldEnum = {
|
exports.Prisma.TeamScalarFieldEnum = {
|
||||||
|
|||||||
94
src/generated/prisma/index.d.ts
vendored
94
src/generated/prisma/index.d.ts
vendored
@ -2537,6 +2537,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt: Date | null
|
lastActiveAt: Date | null
|
||||||
pterodactylClientApiKey: string | null
|
pterodactylClientApiKey: string | null
|
||||||
timeZone: string | null
|
timeZone: string | null
|
||||||
|
canBeInvited: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserMaxAggregateOutputType = {
|
export type UserMaxAggregateOutputType = {
|
||||||
@ -2555,6 +2556,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt: Date | null
|
lastActiveAt: Date | null
|
||||||
pterodactylClientApiKey: string | null
|
pterodactylClientApiKey: string | null
|
||||||
timeZone: string | null
|
timeZone: string | null
|
||||||
|
canBeInvited: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCountAggregateOutputType = {
|
export type UserCountAggregateOutputType = {
|
||||||
@ -2573,6 +2575,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt: number
|
lastActiveAt: number
|
||||||
pterodactylClientApiKey: number
|
pterodactylClientApiKey: number
|
||||||
timeZone: number
|
timeZone: number
|
||||||
|
canBeInvited: number
|
||||||
_all: number
|
_all: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2601,6 +2604,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: true
|
lastActiveAt?: true
|
||||||
pterodactylClientApiKey?: true
|
pterodactylClientApiKey?: true
|
||||||
timeZone?: true
|
timeZone?: true
|
||||||
|
canBeInvited?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserMaxAggregateInputType = {
|
export type UserMaxAggregateInputType = {
|
||||||
@ -2619,6 +2623,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: true
|
lastActiveAt?: true
|
||||||
pterodactylClientApiKey?: true
|
pterodactylClientApiKey?: true
|
||||||
timeZone?: true
|
timeZone?: true
|
||||||
|
canBeInvited?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCountAggregateInputType = {
|
export type UserCountAggregateInputType = {
|
||||||
@ -2637,6 +2642,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: true
|
lastActiveAt?: true
|
||||||
pterodactylClientApiKey?: true
|
pterodactylClientApiKey?: true
|
||||||
timeZone?: true
|
timeZone?: true
|
||||||
|
canBeInvited?: true
|
||||||
_all?: true
|
_all?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2742,6 +2748,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt: Date | null
|
lastActiveAt: Date | null
|
||||||
pterodactylClientApiKey: string | null
|
pterodactylClientApiKey: string | null
|
||||||
timeZone: string | null
|
timeZone: string | null
|
||||||
|
canBeInvited: boolean
|
||||||
_count: UserCountAggregateOutputType | null
|
_count: UserCountAggregateOutputType | null
|
||||||
_avg: UserAvgAggregateOutputType | null
|
_avg: UserAvgAggregateOutputType | null
|
||||||
_sum: UserSumAggregateOutputType | null
|
_sum: UserSumAggregateOutputType | null
|
||||||
@ -2779,6 +2786,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: boolean
|
lastActiveAt?: boolean
|
||||||
pterodactylClientApiKey?: boolean
|
pterodactylClientApiKey?: boolean
|
||||||
timeZone?: boolean
|
timeZone?: boolean
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: boolean | User$teamArgs<ExtArgs>
|
team?: boolean | User$teamArgs<ExtArgs>
|
||||||
ledTeam?: boolean | User$ledTeamArgs<ExtArgs>
|
ledTeam?: boolean | User$ledTeamArgs<ExtArgs>
|
||||||
matchesAsTeamA?: boolean | User$matchesAsTeamAArgs<ExtArgs>
|
matchesAsTeamA?: boolean | User$matchesAsTeamAArgs<ExtArgs>
|
||||||
@ -2812,6 +2820,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: boolean
|
lastActiveAt?: boolean
|
||||||
pterodactylClientApiKey?: boolean
|
pterodactylClientApiKey?: boolean
|
||||||
timeZone?: boolean
|
timeZone?: boolean
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: boolean | User$teamArgs<ExtArgs>
|
team?: boolean | User$teamArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
|
|
||||||
@ -2831,6 +2840,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: boolean
|
lastActiveAt?: boolean
|
||||||
pterodactylClientApiKey?: boolean
|
pterodactylClientApiKey?: boolean
|
||||||
timeZone?: boolean
|
timeZone?: boolean
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: boolean | User$teamArgs<ExtArgs>
|
team?: boolean | User$teamArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
|
|
||||||
@ -2850,9 +2860,10 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: boolean
|
lastActiveAt?: boolean
|
||||||
pterodactylClientApiKey?: boolean
|
pterodactylClientApiKey?: boolean
|
||||||
timeZone?: boolean
|
timeZone?: boolean
|
||||||
|
canBeInvited?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"steamId" | "name" | "avatar" | "location" | "isAdmin" | "teamId" | "premierRank" | "authCode" | "lastKnownShareCode" | "lastKnownShareCodeDate" | "createdAt" | "status" | "lastActiveAt" | "pterodactylClientApiKey" | "timeZone", ExtArgs["result"]["user"]>
|
export type UserOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"steamId" | "name" | "avatar" | "location" | "isAdmin" | "teamId" | "premierRank" | "authCode" | "lastKnownShareCode" | "lastKnownShareCodeDate" | "createdAt" | "status" | "lastActiveAt" | "pterodactylClientApiKey" | "timeZone" | "canBeInvited", ExtArgs["result"]["user"]>
|
||||||
export type UserInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
|
export type UserInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
|
||||||
team?: boolean | User$teamArgs<ExtArgs>
|
team?: boolean | User$teamArgs<ExtArgs>
|
||||||
ledTeam?: boolean | User$ledTeamArgs<ExtArgs>
|
ledTeam?: boolean | User$ledTeamArgs<ExtArgs>
|
||||||
@ -2911,6 +2922,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt: Date | null
|
lastActiveAt: Date | null
|
||||||
pterodactylClientApiKey: string | null
|
pterodactylClientApiKey: string | null
|
||||||
timeZone: string | null
|
timeZone: string | null
|
||||||
|
canBeInvited: boolean
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
composites: {}
|
composites: {}
|
||||||
}
|
}
|
||||||
@ -3363,6 +3375,7 @@ export namespace Prisma {
|
|||||||
readonly lastActiveAt: FieldRef<"User", 'DateTime'>
|
readonly lastActiveAt: FieldRef<"User", 'DateTime'>
|
||||||
readonly pterodactylClientApiKey: FieldRef<"User", 'String'>
|
readonly pterodactylClientApiKey: FieldRef<"User", 'String'>
|
||||||
readonly timeZone: FieldRef<"User", 'String'>
|
readonly timeZone: FieldRef<"User", 'String'>
|
||||||
|
readonly canBeInvited: FieldRef<"User", 'Boolean'>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -20978,7 +20991,8 @@ export namespace Prisma {
|
|||||||
status: 'status',
|
status: 'status',
|
||||||
lastActiveAt: 'lastActiveAt',
|
lastActiveAt: 'lastActiveAt',
|
||||||
pterodactylClientApiKey: 'pterodactylClientApiKey',
|
pterodactylClientApiKey: 'pterodactylClientApiKey',
|
||||||
timeZone: 'timeZone'
|
timeZone: 'timeZone',
|
||||||
|
canBeInvited: 'canBeInvited'
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
||||||
@ -21413,6 +21427,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: DateTimeNullableFilter<"User"> | Date | string | null
|
lastActiveAt?: DateTimeNullableFilter<"User"> | Date | string | null
|
||||||
pterodactylClientApiKey?: StringNullableFilter<"User"> | string | null
|
pterodactylClientApiKey?: StringNullableFilter<"User"> | string | null
|
||||||
timeZone?: StringNullableFilter<"User"> | string | null
|
timeZone?: StringNullableFilter<"User"> | string | null
|
||||||
|
canBeInvited?: BoolFilter<"User"> | boolean
|
||||||
team?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null
|
team?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null
|
||||||
ledTeam?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null
|
ledTeam?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null
|
||||||
matchesAsTeamA?: MatchListRelationFilter
|
matchesAsTeamA?: MatchListRelationFilter
|
||||||
@ -21445,6 +21460,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: SortOrderInput | SortOrder
|
lastActiveAt?: SortOrderInput | SortOrder
|
||||||
pterodactylClientApiKey?: SortOrderInput | SortOrder
|
pterodactylClientApiKey?: SortOrderInput | SortOrder
|
||||||
timeZone?: SortOrderInput | SortOrder
|
timeZone?: SortOrderInput | SortOrder
|
||||||
|
canBeInvited?: SortOrder
|
||||||
team?: TeamOrderByWithRelationInput
|
team?: TeamOrderByWithRelationInput
|
||||||
ledTeam?: TeamOrderByWithRelationInput
|
ledTeam?: TeamOrderByWithRelationInput
|
||||||
matchesAsTeamA?: MatchOrderByRelationAggregateInput
|
matchesAsTeamA?: MatchOrderByRelationAggregateInput
|
||||||
@ -21480,6 +21496,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: DateTimeNullableFilter<"User"> | Date | string | null
|
lastActiveAt?: DateTimeNullableFilter<"User"> | Date | string | null
|
||||||
pterodactylClientApiKey?: StringNullableFilter<"User"> | string | null
|
pterodactylClientApiKey?: StringNullableFilter<"User"> | string | null
|
||||||
timeZone?: StringNullableFilter<"User"> | string | null
|
timeZone?: StringNullableFilter<"User"> | string | null
|
||||||
|
canBeInvited?: BoolFilter<"User"> | boolean
|
||||||
team?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null
|
team?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null
|
||||||
ledTeam?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null
|
ledTeam?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null
|
||||||
matchesAsTeamA?: MatchListRelationFilter
|
matchesAsTeamA?: MatchListRelationFilter
|
||||||
@ -21512,6 +21529,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: SortOrderInput | SortOrder
|
lastActiveAt?: SortOrderInput | SortOrder
|
||||||
pterodactylClientApiKey?: SortOrderInput | SortOrder
|
pterodactylClientApiKey?: SortOrderInput | SortOrder
|
||||||
timeZone?: SortOrderInput | SortOrder
|
timeZone?: SortOrderInput | SortOrder
|
||||||
|
canBeInvited?: SortOrder
|
||||||
_count?: UserCountOrderByAggregateInput
|
_count?: UserCountOrderByAggregateInput
|
||||||
_avg?: UserAvgOrderByAggregateInput
|
_avg?: UserAvgOrderByAggregateInput
|
||||||
_max?: UserMaxOrderByAggregateInput
|
_max?: UserMaxOrderByAggregateInput
|
||||||
@ -21538,6 +21556,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: DateTimeNullableWithAggregatesFilter<"User"> | Date | string | null
|
lastActiveAt?: DateTimeNullableWithAggregatesFilter<"User"> | Date | string | null
|
||||||
pterodactylClientApiKey?: StringNullableWithAggregatesFilter<"User"> | string | null
|
pterodactylClientApiKey?: StringNullableWithAggregatesFilter<"User"> | string | null
|
||||||
timeZone?: StringNullableWithAggregatesFilter<"User"> | string | null
|
timeZone?: StringNullableWithAggregatesFilter<"User"> | string | null
|
||||||
|
canBeInvited?: BoolWithAggregatesFilter<"User"> | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TeamWhereInput = {
|
export type TeamWhereInput = {
|
||||||
@ -22802,6 +22821,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
@ -22834,6 +22854,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -22864,6 +22885,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
@ -22896,6 +22918,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -22927,6 +22950,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUpdateManyMutationInput = {
|
export type UserUpdateManyMutationInput = {
|
||||||
@ -22944,6 +22968,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedUpdateManyInput = {
|
export type UserUncheckedUpdateManyInput = {
|
||||||
@ -22962,6 +22987,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TeamCreateInput = {
|
export type TeamCreateInput = {
|
||||||
@ -24510,6 +24536,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: SortOrder
|
lastActiveAt?: SortOrder
|
||||||
pterodactylClientApiKey?: SortOrder
|
pterodactylClientApiKey?: SortOrder
|
||||||
timeZone?: SortOrder
|
timeZone?: SortOrder
|
||||||
|
canBeInvited?: SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserAvgOrderByAggregateInput = {
|
export type UserAvgOrderByAggregateInput = {
|
||||||
@ -24532,6 +24559,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: SortOrder
|
lastActiveAt?: SortOrder
|
||||||
pterodactylClientApiKey?: SortOrder
|
pterodactylClientApiKey?: SortOrder
|
||||||
timeZone?: SortOrder
|
timeZone?: SortOrder
|
||||||
|
canBeInvited?: SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserMinOrderByAggregateInput = {
|
export type UserMinOrderByAggregateInput = {
|
||||||
@ -24550,6 +24578,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: SortOrder
|
lastActiveAt?: SortOrder
|
||||||
pterodactylClientApiKey?: SortOrder
|
pterodactylClientApiKey?: SortOrder
|
||||||
timeZone?: SortOrder
|
timeZone?: SortOrder
|
||||||
|
canBeInvited?: SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserSumOrderByAggregateInput = {
|
export type UserSumOrderByAggregateInput = {
|
||||||
@ -28589,6 +28618,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -28620,6 +28650,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
invites?: TeamInviteUncheckedCreateNestedManyWithoutUserInput
|
invites?: TeamInviteUncheckedCreateNestedManyWithoutUserInput
|
||||||
@ -28654,6 +28685,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -28684,6 +28716,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -29039,6 +29072,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -29070,6 +29104,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
invites?: TeamInviteUncheckedUpdateManyWithoutUserNestedInput
|
invites?: TeamInviteUncheckedUpdateManyWithoutUserNestedInput
|
||||||
@ -29119,6 +29154,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: DateTimeNullableFilter<"User"> | Date | string | null
|
lastActiveAt?: DateTimeNullableFilter<"User"> | Date | string | null
|
||||||
pterodactylClientApiKey?: StringNullableFilter<"User"> | string | null
|
pterodactylClientApiKey?: StringNullableFilter<"User"> | string | null
|
||||||
timeZone?: StringNullableFilter<"User"> | string | null
|
timeZone?: StringNullableFilter<"User"> | string | null
|
||||||
|
canBeInvited?: BoolFilter<"User"> | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TeamInviteUpsertWithWhereUniqueWithoutTeamInput = {
|
export type TeamInviteUpsertWithWhereUniqueWithoutTeamInput = {
|
||||||
@ -29248,6 +29284,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
@ -29279,6 +29316,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -29363,6 +29401,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
@ -29394,6 +29433,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -29468,6 +29508,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
@ -29499,6 +29540,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -29544,6 +29586,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
@ -29575,6 +29618,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -29682,6 +29726,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamB?: MatchCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -29713,6 +29758,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
invites?: TeamInviteUncheckedCreateNestedManyWithoutUserInput
|
invites?: TeamInviteUncheckedCreateNestedManyWithoutUserInput
|
||||||
@ -29747,6 +29793,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
@ -29778,6 +29825,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
invites?: TeamInviteUncheckedCreateNestedManyWithoutUserInput
|
invites?: TeamInviteUncheckedCreateNestedManyWithoutUserInput
|
||||||
@ -30368,6 +30416,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
@ -30399,6 +30448,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -30631,6 +30681,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
@ -30662,6 +30713,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -30814,6 +30866,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
@ -30845,6 +30898,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -30957,6 +31011,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
@ -30988,6 +31043,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -31168,6 +31224,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
@ -31199,6 +31256,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -31233,6 +31291,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
@ -31264,6 +31323,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -31466,6 +31526,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
@ -31497,6 +31558,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -31537,6 +31599,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
@ -31568,6 +31631,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -31737,6 +31801,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
@ -31768,6 +31833,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -31886,6 +31952,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
@ -31917,6 +31984,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -31946,6 +32014,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
@ -31977,6 +32046,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -32022,6 +32092,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
@ -32053,6 +32124,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -32307,6 +32379,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
@ -32338,6 +32411,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -32463,6 +32537,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
@ -32494,6 +32569,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -32631,6 +32707,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
team?: TeamCreateNestedOneWithoutMembersInput
|
team?: TeamCreateNestedOneWithoutMembersInput
|
||||||
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchCreateNestedManyWithoutTeamAUsersInput
|
||||||
@ -32662,6 +32739,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
ledTeam?: TeamUncheckedCreateNestedOneWithoutLeaderInput
|
||||||
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
matchesAsTeamA?: MatchUncheckedCreateNestedManyWithoutTeamAUsersInput
|
||||||
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
matchesAsTeamB?: MatchUncheckedCreateNestedManyWithoutTeamBUsersInput
|
||||||
@ -32780,6 +32858,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
@ -32811,6 +32890,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -33410,6 +33490,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: Date | string | null
|
lastActiveAt?: Date | string | null
|
||||||
pterodactylClientApiKey?: string | null
|
pterodactylClientApiKey?: string | null
|
||||||
timeZone?: string | null
|
timeZone?: string | null
|
||||||
|
canBeInvited?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TeamInviteCreateManyTeamInput = {
|
export type TeamInviteCreateManyTeamInput = {
|
||||||
@ -33527,6 +33608,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -33557,6 +33639,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -33587,6 +33670,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TeamInviteUpdateWithoutTeamInput = {
|
export type TeamInviteUpdateWithoutTeamInput = {
|
||||||
@ -33960,6 +34044,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamB?: MatchUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUpdateManyWithoutTeamBUsersNestedInput
|
||||||
@ -33991,6 +34076,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
matchesAsTeamB?: MatchUncheckedUpdateManyWithoutTeamBUsersNestedInput
|
||||||
invites?: TeamInviteUncheckedUpdateManyWithoutUserNestedInput
|
invites?: TeamInviteUncheckedUpdateManyWithoutUserNestedInput
|
||||||
@ -34021,6 +34107,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUpdateWithoutMatchesAsTeamBInput = {
|
export type UserUpdateWithoutMatchesAsTeamBInput = {
|
||||||
@ -34038,6 +34125,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
team?: TeamUpdateOneWithoutMembersNestedInput
|
team?: TeamUpdateOneWithoutMembersNestedInput
|
||||||
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUpdateManyWithoutTeamAUsersNestedInput
|
||||||
@ -34069,6 +34157,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
ledTeam?: TeamUncheckedUpdateOneWithoutLeaderNestedInput
|
||||||
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
matchesAsTeamA?: MatchUncheckedUpdateManyWithoutTeamAUsersNestedInput
|
||||||
invites?: TeamInviteUncheckedUpdateManyWithoutUserNestedInput
|
invites?: TeamInviteUncheckedUpdateManyWithoutUserNestedInput
|
||||||
@ -34099,6 +34188,7 @@ export namespace Prisma {
|
|||||||
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastActiveAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
pterodactylClientApiKey?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
timeZone?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
canBeInvited?: BoolFieldUpdateOperationsInput | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MatchPlayerUpdateWithoutMatchInput = {
|
export type MatchPlayerUpdateWithoutMatchInput = {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "prisma-client-8cab75712245f682edd2dce1ca5952c94f39f37ac5ea8dd517ddd6b716af1837",
|
"name": "prisma-client-fc3f88586732483ac25ea246b89655ccf69ac35e1c6d1e7c4e4311101fe5713a",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "default.js",
|
"browser": "default.js",
|
||||||
|
|||||||
BIN
src/generated/prisma/query_engine-windows.dll.node.tmp28872
Normal file
BIN
src/generated/prisma/query_engine-windows.dll.node.tmp28872
Normal file
Binary file not shown.
BIN
src/generated/prisma/query_engine-windows.dll.node.tmp3888
Normal file
BIN
src/generated/prisma/query_engine-windows.dll.node.tmp3888
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
117
src/lib/auth.ts
117
src/lib/auth.ts
@ -1,11 +1,47 @@
|
|||||||
// /src/lib/auth.ts
|
// /src/lib/auth.ts
|
||||||
|
|
||||||
import type { NextAuthOptions } from 'next-auth'
|
import type { NextAuthOptions } from 'next-auth'
|
||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
import Steam from 'next-auth-steam'
|
import Steam from 'next-auth-steam'
|
||||||
import { prisma } from './prisma'
|
import { prisma } from './prisma'
|
||||||
import type { SteamProfile } from '@/types/steam'
|
import type { SteamProfile } from '@/types/steam'
|
||||||
|
|
||||||
|
function readCookie(req: NextRequest, name: string): string | null {
|
||||||
|
try {
|
||||||
|
const raw = req.headers.get('cookie') || ''
|
||||||
|
const m = raw.match(new RegExp(`(?:^|; )${name}=([^;]*)`))
|
||||||
|
return m ? decodeURIComponent(m[1]) : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidIanaTz(tz: unknown): tz is string {
|
||||||
|
if (typeof tz !== 'string' || !tz) return false
|
||||||
|
try {
|
||||||
|
// Werfen lassen, wenn ungültig
|
||||||
|
new Intl.DateTimeFormat('en-US', { timeZone: tz }).format(0)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sehr kleiner Fallback-Guesser aus Ländercode -> TZ (optional)
|
||||||
|
function guessTzFromCountry(cc?: string | null): string | null {
|
||||||
|
if (!cc) return null
|
||||||
|
const C = cc.toUpperCase()
|
||||||
|
if (['DE','AT','CH'].includes(C)) return 'Europe/Berlin'
|
||||||
|
if (C === 'GB' || C === 'UK') return 'Europe/London'
|
||||||
|
if (C === 'FR') return 'Europe/Paris'
|
||||||
|
if (C === 'ES') return 'Europe/Madrid'
|
||||||
|
if (C === 'IT') return 'Europe/Rome'
|
||||||
|
if (C === 'NL') return 'Europe/Amsterdam'
|
||||||
|
if (C === 'PL') return 'Europe/Warsaw'
|
||||||
|
if (C === 'CZ') return 'Europe/Prague'
|
||||||
|
if (C === 'US') return null // zu viele Zonen – lieber Cookie abwarten
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
export const authOptions = (req: NextRequest): NextAuthOptions => ({
|
export const authOptions = (req: NextRequest): NextAuthOptions => ({
|
||||||
secret: process.env.NEXTAUTH_SECRET,
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
providers: [
|
providers: [
|
||||||
@ -19,42 +55,68 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
|
|||||||
const steamProfile = profile as SteamProfile
|
const steamProfile = profile as SteamProfile
|
||||||
const location = steamProfile.loccountrycode ?? null
|
const location = steamProfile.loccountrycode ?? null
|
||||||
|
|
||||||
await prisma.user.upsert({
|
const existing = await prisma.user.findUnique({
|
||||||
where: { steamId: steamProfile.steamid },
|
where: { steamId: steamProfile.steamid },
|
||||||
update: {
|
select: { timeZone: true },
|
||||||
name: steamProfile.personaname,
|
|
||||||
avatar: steamProfile.avatarfull,
|
|
||||||
...(location && { location }),
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
steamId: steamProfile.steamid,
|
|
||||||
name: steamProfile.personaname,
|
|
||||||
avatar: steamProfile.avatarfull,
|
|
||||||
location: steamProfile.loccountrycode,
|
|
||||||
isAdmin: false,
|
|
||||||
timeZone:
|
|
||||||
...(location && { location }),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Cookie lesen & validieren
|
||||||
|
const tzCookie = readCookie(req, 'tz')
|
||||||
|
const tzFromCookie = isValidIanaTz(tzCookie) ? tzCookie : null
|
||||||
|
|
||||||
|
// Fallback fürs erstmalige Anlegen
|
||||||
|
const guessedTz = tzFromCookie ?? guessTzFromCountry(location)
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
steamId: steamProfile.steamid,
|
||||||
|
name: steamProfile.personaname,
|
||||||
|
avatar: steamProfile.avatarfull,
|
||||||
|
location: location ?? undefined,
|
||||||
|
isAdmin: false,
|
||||||
|
timeZone: guessedTz ?? null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { steamId: steamProfile.steamid },
|
||||||
|
data: {
|
||||||
|
name: steamProfile.personaname,
|
||||||
|
avatar: steamProfile.avatarfull,
|
||||||
|
...(location && { location }),
|
||||||
|
// Wenn noch keine TZ in DB und Cookie vorhanden → einmalig setzen
|
||||||
|
...(existing.timeZone == null && tzFromCookie
|
||||||
|
? { timeZone: tzFromCookie }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
token.steamId = steamProfile.steamid
|
token.steamId = steamProfile.steamid
|
||||||
token.name = steamProfile.personaname
|
token.name = steamProfile.personaname
|
||||||
token.image = steamProfile.avatarfull
|
token.image = steamProfile.avatarfull
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DB laden (inkl. timeZone), Team & Admin setzen
|
||||||
const userInDb = await prisma.user.findUnique({
|
const userInDb = await prisma.user.findUnique({
|
||||||
where: { steamId: token.steamId || token.sub || '' },
|
where: { steamId: token.steamId || token.sub || '' },
|
||||||
|
select: { teamId: true, isAdmin: true, steamId: true, timeZone: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
if (userInDb) {
|
if (userInDb) {
|
||||||
token.team = userInDb.teamId ?? null
|
token.team = userInDb.teamId ?? null
|
||||||
if (userInDb.steamId === '76561198000414190') {
|
token.isAdmin =
|
||||||
token.isAdmin = true
|
userInDb.steamId === '76561198000414190'
|
||||||
} else {
|
? true
|
||||||
token.isAdmin = userInDb.isAdmin ?? false
|
: userInDb.isAdmin ?? false
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ➜ HIER: Cookie jedes Mal bevorzugen, sonst DB fallback
|
||||||
|
const currentCookie = readCookie(req, 'tz')
|
||||||
|
const cookieTz = isValidIanaTz(currentCookie) ? currentCookie : undefined
|
||||||
|
token.timeZone = cookieTz ?? userInDb?.timeZone ?? undefined
|
||||||
|
|
||||||
return token
|
return token
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -68,6 +130,8 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
|
|||||||
image: token.image,
|
image: token.image,
|
||||||
team: token.team ?? null,
|
team: token.team ?? null,
|
||||||
isAdmin: token.isAdmin ?? false,
|
isAdmin: token.isAdmin ?? false,
|
||||||
|
// ➜ TZ in die Session durchreichen
|
||||||
|
timeZone: token.timeZone,
|
||||||
}
|
}
|
||||||
return session
|
return session
|
||||||
},
|
},
|
||||||
@ -76,15 +140,8 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
|
|||||||
const isSignIn = url.startsWith(`${baseUrl}/api/auth/signin`);
|
const isSignIn = url.startsWith(`${baseUrl}/api/auth/signin`);
|
||||||
const isSignOut = url.startsWith(`${baseUrl}/api/auth/signout`);
|
const isSignOut = url.startsWith(`${baseUrl}/api/auth/signout`);
|
||||||
|
|
||||||
if (isSignOut) {
|
if (isSignOut) return `${baseUrl}/`;
|
||||||
return `${baseUrl}/`; // Nach Logout auf Startseite
|
if (isSignIn || url === baseUrl) return `${baseUrl}/dashboard`;
|
||||||
}
|
|
||||||
|
|
||||||
// Standard-Redirect nach Login
|
|
||||||
if (isSignIn || url === baseUrl) {
|
|
||||||
return `${baseUrl}/dashboard`; // z. B. Dashboard als Startpunkt
|
|
||||||
}
|
|
||||||
|
|
||||||
return url.startsWith(baseUrl) ? url : baseUrl;
|
return url.startsWith(baseUrl) ? url : baseUrl;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,9 +5,12 @@
|
|||||||
"saved": "Gespeichert",
|
"saved": "Gespeichert",
|
||||||
"save-failed": "Speichern fehlgeschlagen",
|
"save-failed": "Speichern fehlgeschlagen",
|
||||||
"no-changes": "Keine Änderungen",
|
"no-changes": "Keine Änderungen",
|
||||||
|
"loading": "Lädt",
|
||||||
"here": "hier",
|
"here": "hier",
|
||||||
"disconnect": "Trennen",
|
"disconnect": "Trennen",
|
||||||
"reset": "Zurücksetzen",
|
"reset": "Zurücksetzen",
|
||||||
|
"back": "Zurück",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
"monday": "Montag",
|
"monday": "Montag",
|
||||||
"tuesday": "Dienstag",
|
"tuesday": "Dienstag",
|
||||||
"wednesday": "Mittwoch",
|
"wednesday": "Mittwoch",
|
||||||
@ -28,7 +31,7 @@
|
|||||||
"overview": "Überblick",
|
"overview": "Überblick",
|
||||||
"stats": "Statistiken"
|
"stats": "Statistiken"
|
||||||
},
|
},
|
||||||
"schedule": "Spielpan"
|
"schedule": "Spielplan"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Willkommen!"
|
"title": "Willkommen!"
|
||||||
@ -49,7 +52,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"tabs": {
|
"sections": {
|
||||||
|
"user": {
|
||||||
|
"title": "Benutzereinstellungen",
|
||||||
|
"short": "Benutzer",
|
||||||
|
"description": "Verwalte deine Datenschutzeinstellungen.",
|
||||||
|
"timezone-label": "Zeitzone",
|
||||||
|
"timezone-help": "Wird für Datums-/Zeitangaben in der App genutzt.",
|
||||||
|
"timezone-hint": "Wähle deine IANA-Zeitzone (z. B. Europe/Berlin).",
|
||||||
|
"timezone-system": "Systemstandard verwenden"
|
||||||
|
},
|
||||||
|
"privacy": {
|
||||||
|
"title": "Datenschutzeinstellungen",
|
||||||
|
"short": "Datenschutz",
|
||||||
|
"description": "Verwalte deine Datenschutzeinstellungen.",
|
||||||
|
"invites": {
|
||||||
|
"label": "Einladungen erlauben",
|
||||||
|
"help": "Erlaube anderen, dich in ihre Teams einzuladen."
|
||||||
|
}
|
||||||
|
},
|
||||||
"account": {
|
"account": {
|
||||||
"title": "Accounteinstellungen",
|
"title": "Accounteinstellungen",
|
||||||
"short": "Account",
|
"short": "Account",
|
||||||
@ -79,18 +100,12 @@
|
|||||||
"light": "Hell",
|
"light": "Hell",
|
||||||
"dark": "Dunkel"
|
"dark": "Dunkel"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"UserSettings": {
|
|
||||||
"timezone-label": "Zeitzone",
|
|
||||||
"timezone-help": "Wird für Datums-/Zeitangaben in der App genutzt.",
|
|
||||||
"timezone-hint": "Wähle deine IANA-Zeitzone (z. B. Europe/Berlin).",
|
|
||||||
"timezone-system": "Systemstandard verwenden"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"privacy": {
|
"appearance": {
|
||||||
"title": "Datenschutzeinstellungen",
|
"title": "Darstellungseinstellungen",
|
||||||
"short": "Datenschutz",
|
"short": "Darstellung",
|
||||||
"description": "Verwalte deine Datenschutzeinstellungen."
|
"description": "Verwalte deine Datenschutzeinstellungen."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +122,10 @@
|
|||||||
"description": "Keine Matches geplant.",
|
"description": "Keine Matches geplant.",
|
||||||
"filter": "Nur meine Matches anzeigen",
|
"filter": "Nur meine Matches anzeigen",
|
||||||
"create-match": "Match erstellen",
|
"create-match": "Match erstellen",
|
||||||
"day": "Tag",
|
"day": "Tag"
|
||||||
|
},
|
||||||
|
"mapvote": {
|
||||||
|
"open": "offen",
|
||||||
"opens-in": "öffnet in"
|
"opens-in": "öffnet in"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|||||||
@ -5,9 +5,12 @@
|
|||||||
"saved": "Saved",
|
"saved": "Saved",
|
||||||
"save-failed": "Save failed",
|
"save-failed": "Save failed",
|
||||||
"no-changes": "No changes",
|
"no-changes": "No changes",
|
||||||
|
"loading": "Loading",
|
||||||
"here": "here",
|
"here": "here",
|
||||||
"disconnect": "Disconnect",
|
"disconnect": "Disconnect",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
|
"back": "Back",
|
||||||
|
"edit": "Edit",
|
||||||
"monday": "Monday",
|
"monday": "Monday",
|
||||||
"tuesday": "Tuesday",
|
"tuesday": "Tuesday",
|
||||||
"wednesday": "Wednesday",
|
"wednesday": "Wednesday",
|
||||||
@ -49,49 +52,61 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"tabs": {
|
"sections": {
|
||||||
|
"user": {
|
||||||
|
"title": "User Settings",
|
||||||
|
"short": "User",
|
||||||
|
"description": "Manage your privacy settings.",
|
||||||
|
"timezone-label": "Time Zone",
|
||||||
|
"timezone-help": "Used for date/time display in the app.",
|
||||||
|
"timezone-hint": "Choose your IANA time zone (e.g., Europe/Berlin).",
|
||||||
|
"timezone-system": "Use system default"
|
||||||
|
},
|
||||||
|
"privacy": {
|
||||||
|
"title": "Privacy Settings",
|
||||||
|
"short": "Privacy",
|
||||||
|
"description": "Manage your privacy settings.",
|
||||||
|
"invites": {
|
||||||
|
"label": "Allow invitations",
|
||||||
|
"help": "Let others invite you to their teams."
|
||||||
|
}
|
||||||
|
},
|
||||||
"account": {
|
"account": {
|
||||||
"title": "Account Settings",
|
"title": "Account Settings",
|
||||||
"short": "Account",
|
"short": "Account",
|
||||||
"description": "Manage your account settings and connected services.",
|
"description": "Manage your account settings and connected services.",
|
||||||
"find-code": "You can find your code",
|
"find-code": "You can find your code",
|
||||||
"url": "https://help.steampowered.com/en/wizard/HelpWithGameIssue/?appid=730&issueid=128",
|
"url": "https://help.steampowered.com/de/wizard/HelpWithGameIssue/?appid=730&issueid=128",
|
||||||
"page": {
|
"page": {
|
||||||
"AuthCodeSettings": {
|
"AuthCodeSettings": {
|
||||||
"name": "Authentication Code",
|
"name": "Authentication Code",
|
||||||
"question": "What is the Authentication Code?",
|
"question": "What is the authentication code?",
|
||||||
"description": "Third-party websites and applications can use this authentication code to access your match history, your overall performance in those matches, download replays of your matches, and analyze your gameplay.",
|
"description": "Third-party websites and applications can use this authentication code to access your match history, your overall performance in those matches, download replays, and analyze your gameplay.",
|
||||||
"invalid": "Invalid Authentication Code"
|
"invalid": "Invalid authentication code"
|
||||||
},
|
},
|
||||||
"ShareCodeSettings": {
|
"ShareCodeSettings": {
|
||||||
"name": "Match Share Code",
|
"name": "Share Code",
|
||||||
"question": "What is the Match Share Code?",
|
"question": "What is the share code?",
|
||||||
"description": "With the share code, applications can find and analyze your most recent official match.",
|
"description": "With the share code, applications can find and analyze your most recently played official match.",
|
||||||
"invalid-share-code": "Invalid Share Code! You can find your new Share Code"
|
"invalid-share-code": "Expired share code! You can find your new share code"
|
||||||
},
|
},
|
||||||
"AppearanceSettings": {
|
"AppearanceSettings": {
|
||||||
"name": "Appearance",
|
"name": "Appearance",
|
||||||
"description": "Choose your preferred theme. You can use a fixed style or follow your system setting.",
|
"description": "Choose your preferred theme. You can use a fixed style or follow the system setting.",
|
||||||
"theme-mode": "Theme Mode",
|
"theme-mode": "Theme Mode",
|
||||||
"theme-mode-description": "If you choose “System”, the appearance will automatically match your device.",
|
"theme-mode-description": "If “System” is selected, the appearance automatically follows your device.",
|
||||||
"theme": {
|
"theme": {
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"dark": "Dark"
|
"dark": "Dark"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"UserSettings": {
|
|
||||||
"timezone-label": "Time zone",
|
|
||||||
"timezone-help": "Used for date/time formatting in the app.",
|
|
||||||
"timezone-hint": "Choose your IANA time zone (e.g., Europe/Berlin).",
|
|
||||||
"timezone-system": "Use system default"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"privacy": {
|
"appearance": {
|
||||||
"title": "Privacy Settings",
|
"title": "Appearance Settings",
|
||||||
"short": "Privacy",
|
"short": "Appearance",
|
||||||
"description": "Manage your privacy preferences."
|
"description": "Manage your privacy settings."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -107,7 +122,10 @@
|
|||||||
"description": "No matches scheduled.",
|
"description": "No matches scheduled.",
|
||||||
"filter": "Show my matches only",
|
"filter": "Show my matches only",
|
||||||
"create-match": "Create Match",
|
"create-match": "Create Match",
|
||||||
"day": "Day",
|
"day": "Day"
|
||||||
|
},
|
||||||
|
"mapvote": {
|
||||||
|
"open": "open",
|
||||||
"opens-in": "opens in"
|
"opens-in": "opens in"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|||||||
3
src/types/next-auth.d.ts
vendored
3
src/types/next-auth.d.ts
vendored
@ -7,6 +7,7 @@ declare module 'next-auth' {
|
|||||||
steamId?: string
|
steamId?: string
|
||||||
isAdmin?: boolean
|
isAdmin?: boolean
|
||||||
team?: string | null
|
team?: string | null
|
||||||
|
timeZone?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ declare module 'next-auth' {
|
|||||||
steamId: string
|
steamId: string
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
team?: string | null
|
team?: string | null
|
||||||
|
timeZone?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,5 +26,6 @@ declare module 'next-auth/jwt' {
|
|||||||
team?: string | null
|
team?: string | null
|
||||||
name?: string
|
name?: string
|
||||||
image?: string
|
image?: string
|
||||||
|
timeZone?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user