Merge branch 'main' of https://git.rother-woelki.de/chris/ironie-nextjs
# Conflicts: # src/app/[locale]/components/CommunityMatchList.tsx
This commit is contained in:
commit
bcdb2d41d7
@ -154,7 +154,6 @@
|
||||
roundHistory Json?
|
||||
winnerTeam String?
|
||||
|
||||
bestOf Int @default(3) // 1 | 3 | 5 – app-seitig validieren
|
||||
matchDate DateTime? // geplante Startzeit (separat von demoDate)
|
||||
mapVote MapVote?
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useRouter, usePathname } from '@/i18n/navigation'
|
||||
import { useTranslations, useLocale } from 'next-intl'
|
||||
@ -13,6 +13,7 @@ import Button from './Button'
|
||||
import Modal from './Modal'
|
||||
import { Match } from '../../../types/match'
|
||||
import { useSSEStore } from '@/lib/useSSEStore'
|
||||
import { useUserTimeZone } from '@/hooks/useUserTimeZone'
|
||||
|
||||
type Props = { matchType?: string }
|
||||
|
||||
@ -104,14 +105,13 @@ export default function CommunityMatchList({ matchType }: Props) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const locale = useLocale()
|
||||
const userTZ = getUserTimeZone(session?.user?.timeZone);
|
||||
const weekdayFmt = new Intl.DateTimeFormat(
|
||||
locale === 'de' ? 'de-DE' : 'en-GB',
|
||||
{
|
||||
const userTZ = useUserTimeZone([session?.user?.steamId])
|
||||
const weekdayFmt = useMemo(() =>
|
||||
new Intl.DateTimeFormat(locale === 'de' ? 'de-DE' : 'en-GB', {
|
||||
weekday: 'long',
|
||||
timeZone: userTZ,
|
||||
}
|
||||
);
|
||||
}),
|
||||
[locale, userTZ])
|
||||
|
||||
const tMatches = useTranslations('matches')
|
||||
const tMapvote = useTranslations('mapvote')
|
||||
@ -604,4 +604,4 @@ function formatCountdown(ms: number) {
|
||||
const s = totalSec % 60
|
||||
const pad = (n:number)=>String(n).padStart(2,'0')
|
||||
return `${h}:${pad(m)}:${pad(s)}`
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,40 @@ import Link from 'next/link'
|
||||
|
||||
type TeamWithPlayers = Team & { players?: MatchPlayer[] }
|
||||
|
||||
/* ─────────────────── Helpers ─────────────────── */
|
||||
/** Teile eines Datums in einer Ziel-TZ extrahieren */
|
||||
function getZonedParts(d: Date | string | number, timeZone: string, locale = 'de-DE') {
|
||||
const date = typeof d === 'number' ? new Date(d) : (typeof d === 'string' ? new Date(d) : d)
|
||||
const parts = new Intl.DateTimeFormat(locale, {
|
||||
timeZone,
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', hour12: false
|
||||
}).formatToParts(date)
|
||||
const pick = (t: Intl.DateTimeFormatPartTypes) => parts.find(p => p.type === t)?.value || ''
|
||||
return {
|
||||
y: pick('year'), m: pick('month'), d: pick('day'),
|
||||
hh: pick('hour'), mm: pick('minute')
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTimeInTZ(d: Date | string | number, tz: string, locale = 'de-DE') {
|
||||
const p = getZonedParts(d, tz, locale)
|
||||
return `${p.d}.${p.m}.${p.y} ${p.hh}:${p.mm}`
|
||||
}
|
||||
|
||||
/** user-TZ aus API lesen (fallbacks eingebaut) */
|
||||
async function fetchUserTZ(): Promise<string> {
|
||||
try {
|
||||
const res = await fetch('/api/user/timezone', { credentials: 'include', cache: 'no-store' })
|
||||
const j = res.ok ? await res.json() : null
|
||||
const fromDb = j && typeof j.timeZone === 'string' ? j.timeZone : null
|
||||
return fromDb
|
||||
?? Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
?? 'Europe/Berlin'
|
||||
} catch {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'Europe/Berlin'
|
||||
}
|
||||
}
|
||||
|
||||
const kdr = (k?: number, d?: number) =>
|
||||
typeof k === 'number' && typeof d === 'number'
|
||||
? d === 0
|
||||
@ -45,20 +78,63 @@ const normalizeMapKey = (raw?: string) =>
|
||||
type VoteAction = 'BAN' | 'PICK' | 'DECIDER'
|
||||
type VoteStep = { order: number; action: VoteAction; map?: string | null }
|
||||
|
||||
const mapLabelFromKey = (key?: string) => {
|
||||
const k = (key ?? '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
|
||||
return MAP_OPTIONS.find(o => o.key === k)?.label ?? (k ? k : 'TBD')
|
||||
// 1) Normalisieren: String → "de_mirage"
|
||||
const norm = (m?: unknown): string => {
|
||||
if (!m) return ''
|
||||
if (typeof m === 'string') return m.toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
|
||||
if (typeof m === 'object') {
|
||||
const o = m as Record<string, unknown>
|
||||
// häufige Felder, die den Mapkey tragen:
|
||||
const cand =
|
||||
o.key ?? o.map ?? o.name ?? o.id ?? (o as any)?.value ?? (o as any)?.slug
|
||||
return typeof cand === 'string'
|
||||
? cand.toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
|
||||
: ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// Maps aus dem MapVote (nur PICK/DECIDER, sortiert)
|
||||
function extractSeriesMaps(match: Match): string[] {
|
||||
const steps = (match.mapVote?.steps ?? []) as unknown as VoteStep[]
|
||||
const picks = steps
|
||||
.filter(s => s && (s.action === 'PICK' || s.action === 'DECIDER'))
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
.map(s => s.map ?? '')
|
||||
const n = Math.max(1, match.bestOf ?? 1)
|
||||
return picks.slice(0, n)
|
||||
// 2) Label: erst aus MAP_OPTIONS, sonst hübsch machen
|
||||
const humanize = (k: string) =>
|
||||
k.replace(/^de_/, '').replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
|
||||
const mapLabelFromKey = (key?: string) => {
|
||||
const k = norm(key)
|
||||
const fromList = MAP_OPTIONS.find(o => o.key === k)?.label
|
||||
return fromList ?? (k ? humanize(k) : 'TBD')
|
||||
}
|
||||
|
||||
// 3) Final-Maps extrahieren – deckt Strings/Objekte & verschiedene Felder ab
|
||||
function extractSeriesMaps(match: Match, bestOf: number): string[] {
|
||||
const n = Math.max(1, bestOf)
|
||||
|
||||
// a) klassische Steps (nur PICK/DECIDER)
|
||||
const fromSteps: unknown[] =
|
||||
(match.mapVote?.steps ?? [])
|
||||
.filter((s: any) => s && (s.action === 'PICK' || s.action === 'DECIDER'))
|
||||
.sort((a: any, b: any) => (a.order ?? 0) - (b.order ?? 0))
|
||||
.map((s: any) => s.map)
|
||||
|
||||
// b) häufige Ergebnis-Felder
|
||||
const mv: any = match.mapVote ?? {}
|
||||
const candidates: unknown[] = [
|
||||
mv.result?.maps,
|
||||
mv.result?.picks, // [{map: '...'}] / [{key:'...'}]
|
||||
mv.result?.series, // ['de_mirage', ...] oder [{key:...}]
|
||||
mv.final?.maps,
|
||||
(match as any)?.series?.maps,
|
||||
(match as any)?.maps,
|
||||
].flat().filter(Boolean)
|
||||
|
||||
// c) flach ziehen + normalisieren + entduplizieren (stabile Reihenfolge)
|
||||
const chain = [...fromSteps, ...candidates]
|
||||
.map(norm)
|
||||
.filter(Boolean)
|
||||
|
||||
const uniq: string[] = []
|
||||
for (const k of chain) if (!uniq.includes(k)) uniq.push(k)
|
||||
|
||||
return uniq.slice(0, n)
|
||||
}
|
||||
|
||||
// Wählt ein konsistentes Hintergrundbild pro Map
|
||||
@ -74,63 +150,87 @@ const pickMapImage = (key?: string) => {
|
||||
}
|
||||
|
||||
/* ─────────────────── UI-Snippets ─────────────────── */
|
||||
function SeriesStrip({
|
||||
function SeriesTabsStrip({
|
||||
bestOf,
|
||||
scoreA = 0,
|
||||
scoreB = 0,
|
||||
maps,
|
||||
active,
|
||||
onChange,
|
||||
}: {
|
||||
bestOf: number
|
||||
scoreA?: number | null
|
||||
scoreB?: number | null
|
||||
maps: string[]
|
||||
active: number
|
||||
onChange: (idx: number) => void
|
||||
}) {
|
||||
const winsA = Math.max(0, scoreA ?? 0)
|
||||
const winsB = Math.max(0, scoreB ?? 0)
|
||||
const needed = Math.ceil(bestOf / 2)
|
||||
const total = Math.max(bestOf, maps.length || 1)
|
||||
|
||||
const finished = winsA >= needed || winsB >= needed
|
||||
const currentIdx = finished ? -1 : Math.min(winsA + winsB, total - 1)
|
||||
|
||||
const onKey = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return
|
||||
e.preventDefault()
|
||||
const dir = e.key === 'ArrowRight' ? 1 : -1
|
||||
const next = (active + dir + total) % total
|
||||
onChange(next)
|
||||
// Fokus auf den neuen Tab schieben
|
||||
const el = document.getElementById(`tab-map-${next}`)
|
||||
el?.focus()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-400">Best of {bestOf} • First to {needed}</div>
|
||||
<div className="text-sm font-semibold">{winsA}:{winsB}</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
||||
{Array.from({length: total}).map((_, i) => {
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label="Maps der Serie"
|
||||
className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3"
|
||||
onKeyDown={onKey}
|
||||
>
|
||||
{Array.from({ length: total }).map((_, i) => {
|
||||
const key = maps[i] ?? ''
|
||||
const label = mapLabelFromKey(key)
|
||||
const isDone = i < winsA + winsB
|
||||
const isCurrent = i === currentIdx
|
||||
const isActive = i === active
|
||||
const isFuture = i > winsA + winsB
|
||||
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
key={`series-map-${i}`}
|
||||
id={`tab-map-${i}`}
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
aria-controls={`panel-map-${i}`}
|
||||
type="button"
|
||||
onClick={() => onChange(i)}
|
||||
className={[
|
||||
'flex items-center justify-between rounded-md border px-3 py-2',
|
||||
isCurrent
|
||||
? 'border-blue-500 ring-2 ring-blue-300/50 bg-blue-500/10'
|
||||
: isDone
|
||||
? 'border-emerald-500 bg-emerald-500/10'
|
||||
: 'border-gray-600 bg-neutral-800/40',
|
||||
'flex items-center justify-between rounded-md border px-3 py-2 text-left outline-none',
|
||||
isActive
|
||||
? 'shadow bg-blue-600/20 border-blue-500'
|
||||
: isCurrent
|
||||
? 'border-blue-500 bg-blue-500/10'
|
||||
: isDone
|
||||
? 'border-emerald-500 bg-emerald-500/10'
|
||||
: 'border-gray-600 bg-neutral-800/40',
|
||||
].join(' ')}
|
||||
title={label || `Map ${i + 1}`}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="shrink-0 text-xs opacity-70">Map {i + 1}</span>
|
||||
<span className="truncate font-medium">{label}</span>
|
||||
</div>
|
||||
<div className="shrink-0 flex items-center gap-1">
|
||||
</span>
|
||||
<span className="shrink-0 flex items-center gap-1">
|
||||
{isDone && <span className="text-xs font-semibold text-emerald-400">✔</span>}
|
||||
{isCurrent && !isDone && !isFuture && (
|
||||
<span className="text-[11px] font-semibold text-blue-400">LIVE / als Nächstes</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
@ -138,59 +238,23 @@ function SeriesStrip({
|
||||
)
|
||||
}
|
||||
|
||||
function SeriesTabs({
|
||||
maps,
|
||||
winsA = 0,
|
||||
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 isAdmin = !!session?.user?.isAdmin
|
||||
const [userTZ, setUserTZ] = useState<string>('Europe/Berlin')
|
||||
// ⬇️ bestOf nur im State halten: community → 3 (oder was du magst), sonst 1
|
||||
const [bestOf, setBestOf] = useState<1 | 3 | 5>(() =>
|
||||
match.matchType === 'community' ? 3 : 1
|
||||
)
|
||||
|
||||
// Alle Maps der Serie (BO3/BO5)
|
||||
const allMaps = useMemo(() => extractSeriesMaps(match), [match.mapVote?.steps, match.bestOf])
|
||||
// Alle Maps der Serie (BO3/BO5) – abhängig von bestOf-State
|
||||
const allMaps = useMemo(
|
||||
() => extractSeriesMaps(match, bestOf),
|
||||
[match.mapVote?.steps, (match as any)?.mapVote?.result?.maps, match.map, bestOf]
|
||||
)
|
||||
const [activeMapIdx, setActiveMapIdx] = useState(0)
|
||||
|
||||
// Zeit / Modals
|
||||
@ -213,6 +277,15 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
||||
|
||||
const teamAPlayers = (match.teamA as TeamWithPlayers).players ?? []
|
||||
const teamBPlayers = (match.teamB as TeamWithPlayers).players ?? []
|
||||
|
||||
const currentMapKey = normalizeMapKey(match.map)
|
||||
|
||||
// beim mount user-tz aus DB laden
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
fetchUserTZ().then(tz => { if (alive) setUserTZ(tz) })
|
||||
return () => { alive = false }
|
||||
}, [])
|
||||
|
||||
// Aktiv-Map aus Query (?m=2) initialisieren
|
||||
useEffect(() => {
|
||||
@ -242,14 +315,16 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
||||
|
||||
// Datum
|
||||
const dateString = match.matchDate ?? match.demoDate
|
||||
const readableDate = dateString ? format(new Date(dateString), 'PPpp', {locale: de}) : 'Unbekannt'
|
||||
const readableDate = dateString
|
||||
? formatDateTimeInTZ(dateString, userTZ, 'de-DE') // z.B. "31.08.2025 20:15"
|
||||
: 'Unbekannt'
|
||||
|
||||
// „Serie komplett“ (für SeriesStrip)
|
||||
// „Serie komplett“ (für SeriesStrip) – nutzt bestOf-State
|
||||
const seriesMaps = useMemo(() => {
|
||||
const fromVote = extractSeriesMaps(match)
|
||||
const n = Math.max(1, match.bestOf ?? 1)
|
||||
const fromVote = extractSeriesMaps(match, bestOf)
|
||||
const n = Math.max(1, bestOf)
|
||||
return fromVote.length >= n ? fromVote : [...fromVote, ...Array.from({length: n - fromVote.length}, () => '')]
|
||||
}, [match.bestOf, match.mapVote?.steps?.length])
|
||||
}, [bestOf, match.mapVote?.steps?.length])
|
||||
|
||||
// Ticker für Mapvote-Zeitfenster
|
||||
useEffect(() => {
|
||||
@ -316,6 +391,22 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
||||
if (REFRESH_TYPES.has(type) && evt?.matchId === match.id) router.refresh()
|
||||
}, [lastEvent, match.id, router, match.matchDate, match.demoDate, initialNow])
|
||||
|
||||
useEffect(() => {
|
||||
// Keine Tabs? oder noch Pick/Ban-Phase? → nichts tun
|
||||
if (bestOf <= 1 || !allMaps?.length || isPickBanPhase) return
|
||||
|
||||
const idx = allMaps.findIndex(m => normalizeMapKey(m) === currentMapKey)
|
||||
if (idx !== -1 && idx !== activeMapIdx) {
|
||||
setActiveMapIdx(idx)
|
||||
|
||||
// URL (m=) mitschieben, damit Refresh/Share konsistent bleibt
|
||||
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)
|
||||
}
|
||||
}, [currentMapKey, allMaps, bestOf, isPickBanPhase]) // ← Dependencies
|
||||
|
||||
// Tabellen-Layout
|
||||
const ColGroup = () => (
|
||||
<colgroup>
|
||||
@ -471,36 +562,50 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
filter: 'blur(2px)',
|
||||
transform: 'scale(1.02)',
|
||||
transform: 'scale(1.12)',
|
||||
transformOrigin: 'center',
|
||||
opacity: 0.55
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Gradient + Shine */}
|
||||
{/* Overlay: stärkere Vignette (Ränder kaschieren) */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0"
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
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%)',
|
||||
backgroundImage: `
|
||||
/* Radial: dunkelt Ecken/Kanten ab */
|
||||
radial-gradient(120% 120% at 50% 38%,
|
||||
rgba(0,0,0,0) 40%,
|
||||
rgba(0,0,0,.48) 72%,
|
||||
rgba(0,0,0,.86) 100%),
|
||||
/* Vertikal: oben/unten Verlauf */
|
||||
linear-gradient(180deg,
|
||||
rgba(0,0,0,.62) 0%,
|
||||
rgba(0,0,0,.35) 40%,
|
||||
rgba(0,0,0,.78) 100%),
|
||||
/* Horizontal: links/rechts Verlauf */
|
||||
linear-gradient(90deg,
|
||||
rgba(0,0,0,.72) 0%,
|
||||
rgba(0,0,0,0) 12%,
|
||||
rgba(0,0,0,0) 88%,
|
||||
rgba(0,0,0,.72) 100%)
|
||||
`,
|
||||
/* zusätzlicher weicher Rand */
|
||||
boxShadow: 'inset 0 0 140px rgba(0,0,0,.9), inset 0 0 40px rgba(0,0,0,.55)'
|
||||
}}
|
||||
/>
|
||||
<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]" />
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative p-4 sm:p-6">
|
||||
<div className="relative py-3">
|
||||
{/* Meta-Zeile */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<div className="flex items-center gap-2 text-xs uppercase tracking-wide text-white/75">
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-black/25 px-2 py-1 ring-1 ring-white/10">
|
||||
{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>
|
||||
@ -509,11 +614,15 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
||||
</time>
|
||||
</>
|
||||
)}
|
||||
<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 {bestOf}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Teams + Score */}
|
||||
<div className="mt-4 grid grid-cols-[1fr_auto_1fr] items-center gap-4 sm:gap-6">
|
||||
<div className="mt-4 grid grid-cols-[1fr_auto_1fr] items-center gap-4 sm:gap-6 px-1">
|
||||
{/* Team A */}
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-3">
|
||||
@ -569,26 +678,15 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
||||
</div>
|
||||
|
||||
{/* Map-Tabs bei Serie */}
|
||||
{allMaps.length > 1 && (
|
||||
{bestOf > 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}
|
||||
<SeriesTabsStrip
|
||||
bestOf={bestOf}
|
||||
scoreA={match.scoreA}
|
||||
scoreB={match.scoreB}
|
||||
maps={seriesMaps}
|
||||
active={activeMapIdx}
|
||||
onChange={setActive}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -596,10 +694,6 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
||||
|
||||
{/* Header Animations */}
|
||||
<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; }
|
||||
@ -646,7 +740,7 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
||||
<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>
|
||||
<span className="text-gray-300">
|
||||
Du kannst die Aufstellung noch bis <strong>{format(endDate, 'dd.MM.yyyy HH:mm')}</strong> bearbeiten.
|
||||
Du kannst die Aufstellung noch bis <strong>{formatDateTimeInTZ(endDate, userTZ)}</strong> bearbeiten.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -690,7 +784,7 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
||||
<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>
|
||||
<span className="text-gray-300">
|
||||
Du kannst die Aufstellung noch bis <strong>{format(endDate, 'dd.MM.yyyy HH:mm')}</strong> bearbeiten.
|
||||
Du kannst die Aufstellung noch bis <strong>{formatDateTimeInTZ(endDate, userTZ)}</strong> bearbeiten.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -739,7 +833,11 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
||||
defaultDateISO={match.matchDate ?? match.demoDate ?? null}
|
||||
defaultMap={match.map ?? null}
|
||||
defaultVoteLeadMinutes={match.mapVote?.leadMinutes ?? 60}
|
||||
defaultBestOf={Number(match.bestOf) === 5 ? 5 : 3}
|
||||
defaultBestOf={
|
||||
match.matchType === 'community'
|
||||
? (bestOf === 5 ? 5 : 3)
|
||||
: undefined
|
||||
}
|
||||
onSaved={() => { setTimeout(() => router.refresh(), 0) }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,383 +0,0 @@
|
||||
'use client'
|
||||
|
||||
export default function Profile() {
|
||||
return (
|
||||
<>
|
||||
<div className="p-3 md:p-5 bg-white border border-gray-200 shadow-2xs rounded-xl dark:bg-neutral-800 dark:border-neutral-700">
|
||||
<figure>
|
||||
<svg className="w-full" preserveAspectRatio="none" width="1113" height="161" viewBox="0 0 1113 161" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_697_201879)"><rect x="1" width="1112" height="348" fill="#B2E7FE"/><rect width="185.209" height="704.432" transform="matrix(0.50392 0.86375 -0.860909 0.508759 435.452 -177.87)" fill="#FF8F5D"/><rect width="184.653" height="378.667" transform="matrix(0.849839 -0.527043 0.522157 0.852849 -10.4556 -16.4521)" fill="#3ECEED"/><rect width="184.653" height="189.175" transform="matrix(0.849839 -0.527043 0.522157 0.852849 35.4456 58.5195)" fill="#4C48FF"/></g><defs><clipPath id="clip0_697_201879"><rect x="0.5" width="1112" height="161" rx="12" fill="white"/></clipPath></defs></svg>
|
||||
</figure>
|
||||
|
||||
<div className="-mt-24">
|
||||
<div className="relative flex size-30 mx-auto border-4 border-white rounded-full dark:border-neutral-800">
|
||||
<img className="object-cover size-full rounded-full" src="https://images.unsplash.com/photo-1659482633369-9fe69af50bfb?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=facearea&facepad=3&w=320&h=320&q=80" alt="Hero Image" />
|
||||
|
||||
<div className="absolute bottom-0 -end-2">
|
||||
<button type="button" className="group p-2 max-w-[125px] inline-flex justify-center items-center gap-x-2 text-start bg-red-600 border border-red-600 text-white text-xs font-medium rounded-full shadow-2xs align-middle focus:outline-hidden focus:bg-red-500" data-hs-overlay="#hs-pro-dsm">
|
||||
<svg className="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" x2="9.01" y1="9" y2="9"/><line x1="15" x2="15.01" y1="9" y2="9"/></svg>
|
||||
<span className="group-hover:block hidden">Offline</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-center">
|
||||
<h1 className="text-xl font-semibold text-gray-800 dark:text-neutral-200">
|
||||
James Collins
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-neutral-500">
|
||||
its_james
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 md:mt-7 -mb-0.5 flex flex-col md:flex-row md:justify-between md:items-center gap-3">
|
||||
<div className="md:order-2 flex justify-center md:justify-end">
|
||||
<label htmlFor="hs-pro-dupfub" className="relative py-1.5 px-3 inline-flex items-center justify-center sm:justify-start border border-gray-200 cursor-pointer font-medium text-sm rounded-lg peer-checked:bg-gray-100 hover:border-gray-300 focus:outline-none focus:border-gray-300 dark:border-neutral-700 dark:peer-checked:bg-neutral-800 dark:hover:border-neutral-600 dark:focus:border-neutral-600">
|
||||
<input type="checkbox" id="hs-pro-dupfub" className="peer hidden" checked />
|
||||
<span className="relative z-10 text-gray-800 dark:text-neutral-200 peer-checked:hidden">
|
||||
Follow
|
||||
</span>
|
||||
<span className="relative z-10 hidden peer-checked:flex text-gray-800 dark:text-neutral-200">
|
||||
Unfollow
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="relative flex justify-center md:justify-start" data-hs-scroll-nav='{
|
||||
"autoCentering": true
|
||||
}'>
|
||||
<nav className="hs-scroll-nav-body flex flex-nowrap gap-x-1 overflow-x-auto [&::-webkit-scrollbar]:h-0 snap-x snap-mandatory pb-1.5">
|
||||
<a className="snap-start relative inline-flex flex-nowrap items-center gap-x-2 px-2.5 py-1.5 hover:bg-gray-100 text-gray-500 hover:text-gray-800 text-sm whitespace-nowrap rounded-lg disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-gray-100 after:absolute after:-bottom-0.5 after:inset-x-0 after:z-10 after:w-1/4 after:h-0.5 after:rounded-full after:mx-auto after:pointer-events-none dark:text-neutral-500 dark:hover:text-neutral-300 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 after:bg-gray-600 text-gray-800 font-medium dark:bg-neutral-800 dark:text-white dark:after:bg-neutral-200 active" href="../../pro/dashboard/user-profile-my-profile.html">
|
||||
<svg className="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="18" cy="15" r="3"/><circle cx="9" cy="7" r="4"/><path d="M10 15H6a4 4 0 0 0-4 4v2"/><path d="m21.7 16.4-.9-.3"/><path d="m15.2 13.9-.9-.3"/><path d="m16.6 18.7.3-.9"/><path d="m19.1 12.2.3-.9"/><path d="m19.6 18.7-.4-1"/><path d="m16.8 12.3-.4-1"/><path d="m14.3 16.6 1-.4"/><path d="m20.7 13.8 1-.4"/></svg>
|
||||
My Profile
|
||||
</a>
|
||||
<a className="snap-start relative inline-flex flex-nowrap items-center gap-x-2 px-2.5 py-1.5 hover:bg-gray-100 text-gray-500 hover:text-gray-800 text-sm whitespace-nowrap rounded-lg disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-gray-100 after:absolute after:-bottom-0.5 after:inset-x-0 after:z-10 after:w-1/4 after:h-0.5 after:rounded-full after:mx-auto after:pointer-events-none dark:text-neutral-500 dark:hover:text-neutral-300 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 " href="../../pro/dashboard/user-profile-teams.html">
|
||||
<svg className="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
Teams
|
||||
</a>
|
||||
<a className="snap-start relative inline-flex flex-nowrap items-center gap-x-2 px-2.5 py-1.5 hover:bg-gray-100 text-gray-500 hover:text-gray-800 text-sm whitespace-nowrap rounded-lg disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-gray-100 after:absolute after:-bottom-0.5 after:inset-x-0 after:z-10 after:w-1/4 after:h-0.5 after:rounded-full after:mx-auto after:pointer-events-none dark:text-neutral-500 dark:hover:text-neutral-300 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 " href="../../pro/dashboard/user-profile-files.html">
|
||||
<svg className="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15.5 2H8.6c-.4 0-.8.2-1.1.5-.3.3-.5.7-.5 1.1v12.8c0 .4.2.8.5 1.1.3.3.7.5 1.1.5h9.8c.4 0 .8-.2 1.1-.5.3-.3.5-.7.5-1.1V6.5L15.5 2z"/><path d="M3 7.6v12.8c0 .4.2.8.5 1.1.3.3.7.5 1.1.5h9.8"/><path d="M15 2v5h5"/></svg>
|
||||
Files
|
||||
</a>
|
||||
<a className="snap-start relative inline-flex flex-nowrap items-center gap-x-2 px-2.5 py-1.5 hover:bg-gray-100 text-gray-500 hover:text-gray-800 text-sm whitespace-nowrap rounded-lg disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-gray-100 after:absolute after:-bottom-0.5 after:inset-x-0 after:z-10 after:w-1/4 after:h-0.5 after:rounded-full after:mx-auto after:pointer-events-none dark:text-neutral-500 dark:hover:text-neutral-300 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 " href="../../pro/dashboard/user-profile-connections.html">
|
||||
<svg className="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/></svg>
|
||||
Connections
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="hs-pro-dsm" className="hs-overlay hidden size-full fixed top-0 start-0 z-80 overflow-x-hidden overflow-y-auto [--close-when-click-inside:true] pointer-events-none" role="dialog" tabIndex={-1} aria-labelledby="hs-pro-dsm-label">
|
||||
<div className="hs-overlay-open:mt-7 hs-overlay-open:opacity-100 hs-overlay-open:duration-500 mt-0 opacity-0 ease-out transition-all sm:max-w-xl sm:w-full m-3 sm:mx-auto h-[calc(100%-56px)] min-h-[calc(100%-56px)] flex items-center">
|
||||
<div className="w-full flex flex-col bg-white rounded-xl pointer-events-auto shadow-xl dark:bg-neutral-800">
|
||||
<div className="py-2.5 px-4 flex justify-between items-center border-b border-gray-200 dark:border-neutral-700">
|
||||
<h3 id="hs-pro-dsm-label" className="font-medium text-gray-800 dark:text-neutral-200">
|
||||
Set status
|
||||
</h3>
|
||||
<button type="button" className="size-8 shrink-0 flex justify-center items-center gap-x-2 rounded-full border border-transparent bg-gray-100 text-gray-800 hover:bg-gray-200 disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-gray-200 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:text-neutral-400 dark:focus:bg-neutral-600" aria-label="Close" data-hs-overlay="#hs-pro-dsm">
|
||||
<span className="sr-only">Close</span>
|
||||
<svg className="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="hs-modal-status-body" className="p-4 space-y-6 max-h-[75dvh] overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500">
|
||||
<div className="flex items-center border border-gray-200 rounded-lg dark:border-neutral-700">
|
||||
<div className="p-3 border-e border-gray-200 dark:border-neutral-700">
|
||||
<svg className="shrink-0 size-4 text-gray-500 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" x2="9.01" y1="9" y2="9"/><line x1="15" x2="15.01" y1="9" y2="9"/></svg>
|
||||
</div>
|
||||
<input type="text" className="py-1.5 sm:py-2 px-3 block w-full border-transparent rounded-e-md sm:text-sm placeholder:text-gray-400 focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-transparent dark:border-transparent dark:text-neutral-300 dark:placeholder:text-white/60 dark:focus:ring-neutral-600" placeholder="What’s your status?" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm text-gray-500 dark:text-neutral-500">
|
||||
Suggestions
|
||||
</h4>
|
||||
|
||||
<div className="mt-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<label htmlFor="hs-pro-dupsms1" className="relative py-2 px-3 flex cursor-pointer bg-white text-sm rounded-lg focus:outline-hidden dark:bg-neutral-800">
|
||||
<input type="radio" id="hs-pro-dupsms1" name="hs-pro-dupsms" className="peer absolute top-0 start-0 size-full bg-transparent border border-gray-200 text-transparent rounded-lg cursor-pointer focus:ring-0 focus:ring-offset-0 after:relative after:-z-1 after:block after:size-full after:rounded-lg checked:after:bg-blue-50 checked:text-transparent checked:border-blue-600 checked:hover:border-blue-600 checked:focus:border-blue-600 checked:bg-none disabled:opacity-50 disabled:pointer-events-none focus:border-blue-600 dark:border-neutral-700 dark:checked:after:bg-blue-500/10 dark:checked:border-blue-500 dark:focus:border-neutral-600" />
|
||||
<span className="peer-checked:text-blue-600 dark:text-white dark:peer-checked:text-blue-500">
|
||||
🗓️ <span className="ms-2">In a meeting</span>
|
||||
</span>
|
||||
</label>
|
||||
<label htmlFor="hs-pro-dupsms3" className="relative py-2 px-3 flex cursor-pointer bg-white text-sm rounded-lg focus:outline-hidden dark:bg-neutral-800">
|
||||
<input type="radio" id="hs-pro-dupsms3" name="hs-pro-dupsms" className="peer absolute top-0 start-0 size-full bg-transparent border border-gray-200 text-transparent rounded-lg cursor-pointer focus:ring-0 focus:ring-offset-0 after:relative after:-z-1 after:block after:size-full after:rounded-lg checked:after:bg-blue-50 checked:text-transparent checked:border-blue-600 checked:hover:border-blue-600 checked:focus:border-blue-600 checked:bg-none disabled:opacity-50 disabled:pointer-events-none focus:border-blue-600 dark:border-neutral-700 dark:checked:after:bg-blue-500/10 dark:checked:border-blue-500 dark:focus:border-neutral-600" />
|
||||
<span className="peer-checked:text-blue-600 dark:text-white dark:peer-checked:text-blue-500">
|
||||
🚎 <span className="ms-2">Commuting</span>
|
||||
</span>
|
||||
</label>
|
||||
<label htmlFor="hs-pro-dupsms5" className="relative py-2 px-3 flex cursor-pointer bg-white text-sm rounded-lg focus:outline-hidden dark:bg-neutral-800">
|
||||
<input type="radio" id="hs-pro-dupsms5" name="hs-pro-dupsms" className="peer absolute top-0 start-0 size-full bg-transparent border border-gray-200 text-transparent rounded-lg cursor-pointer focus:ring-0 focus:ring-offset-0 after:relative after:-z-1 after:block after:size-full after:rounded-lg checked:after:bg-blue-50 checked:text-transparent checked:border-blue-600 checked:hover:border-blue-600 checked:focus:border-blue-600 checked:bg-none disabled:opacity-50 disabled:pointer-events-none focus:border-blue-600 dark:border-neutral-700 dark:checked:after:bg-blue-500/10 dark:checked:border-blue-500 dark:focus:border-neutral-600" />
|
||||
<span className="peer-checked:text-blue-600 dark:text-white dark:peer-checked:text-blue-500">
|
||||
🎯 <span className="ms-2">Focusing</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<label htmlFor="hs-pro-dupsms2" className="relative py-2 px-3 flex cursor-pointer bg-white text-sm rounded-lg focus:outline-hidden dark:bg-neutral-800">
|
||||
<input type="radio" id="hs-pro-dupsms2" name="hs-pro-dupsms" className="peer absolute top-0 start-0 size-full bg-transparent border border-gray-200 text-transparent rounded-lg cursor-pointer focus:ring-0 focus:ring-offset-0 after:relative after:-z-1 after:block after:size-full after:rounded-lg checked:after:bg-blue-50 checked:text-transparent checked:border-blue-600 checked:hover:border-blue-600 checked:focus:border-blue-600 checked:bg-none disabled:opacity-50 disabled:pointer-events-none focus:border-blue-600 dark:border-neutral-700 dark:checked:after:bg-blue-500/10 dark:checked:border-blue-500 dark:focus:border-neutral-600" />
|
||||
<span className="peer-checked:text-blue-600 dark:text-white dark:peer-checked:text-blue-500">
|
||||
🤒 <span className="ms-2">Out sick</span>
|
||||
</span>
|
||||
</label>
|
||||
<label htmlFor="hs-pro-dupsms7" className="relative py-2 px-3 flex cursor-pointer bg-white text-sm rounded-lg focus:outline-hidden dark:bg-neutral-800">
|
||||
<input type="radio" id="hs-pro-dupsms7" name="hs-pro-dupsms" className="peer absolute top-0 start-0 size-full bg-transparent border border-gray-200 text-transparent rounded-lg cursor-pointer focus:ring-0 focus:ring-offset-0 after:relative after:-z-1 after:block after:size-full after:rounded-lg checked:after:bg-blue-50 checked:text-transparent checked:border-blue-600 checked:hover:border-blue-600 checked:focus:border-blue-600 checked:bg-none disabled:opacity-50 disabled:pointer-events-none focus:border-blue-600 dark:border-neutral-700 dark:checked:after:bg-blue-500/10 dark:checked:border-blue-500 dark:focus:border-neutral-600" />
|
||||
<span className="peer-checked:text-blue-600 dark:text-white dark:peer-checked:text-blue-500">
|
||||
🌴 <span className="ms-2">On vacation</span>
|
||||
</span>
|
||||
</label>
|
||||
<label htmlFor="hs-pro-dupsms6" className="relative py-2 px-3 flex cursor-pointer bg-white text-sm rounded-lg focus:outline-hidden dark:bg-neutral-800">
|
||||
<input type="radio" id="hs-pro-dupsms6" name="hs-pro-dupsms" className="peer absolute top-0 start-0 size-full bg-transparent border border-gray-200 text-transparent rounded-lg cursor-pointer focus:ring-0 focus:ring-offset-0 after:relative after:-z-1 after:block after:size-full after:rounded-lg checked:after:bg-blue-50 checked:text-transparent checked:border-blue-600 checked:hover:border-blue-600 checked:focus:border-blue-600 checked:bg-none disabled:opacity-50 disabled:pointer-events-none focus:border-blue-600 dark:border-neutral-700 dark:checked:after:bg-blue-500/10 dark:checked:border-blue-500 dark:focus:border-neutral-600" />
|
||||
<span className="peer-checked:text-blue-600 dark:text-white dark:peer-checked:text-blue-500">
|
||||
🏡 <span className="ms-2">Working remotely</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="flex flex-col bg-white border border-gray-200 rounded-xl -space-y-px dark:bg-neutral-800 dark:border-neutral-700">
|
||||
<li className="p-3 border-t border-gray-200 first:border-t-0 dark:border-neutral-700">
|
||||
<div className="flex gap-x-3">
|
||||
<span className="mt-0.5 shrink-0 flex flex-col justify-center items-center size-6.5 bg-red-500 text-white rounded-full">
|
||||
<svg className="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M5.164 14H15c-1.5-1-2-5.902-2-7 0-.264-.02-.523-.06-.776L5.164 14zm6.288-10.617A4.988 4.988 0 0 0 8.995 2.1a1 1 0 1 0-1.99 0A5.002 5.002 0 0 0 3 7c0 .898-.335 4.342-1.278 6.113l9.73-9.73zM10 15a2 2 0 1 1-4 0h4zm-9.375.625a.53.53 0 0 0 .75.75l14.75-14.75a.53.53 0 0 0-.75-.75L.625 15.625z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<div className="grow">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<h4 className="text-sm font-semibold text-gray-800 dark:text-neutral-200">
|
||||
Offline
|
||||
</h4>
|
||||
<label htmlFor="hs-pro-dsmofs" className="relative inline-block w-11 h-6 cursor-pointer">
|
||||
<input type="checkbox" id="hs-pro-dsmofs" className="peer sr-only" />
|
||||
<span className="absolute inset-0 bg-gray-200 rounded-full transition-colors duration-200 ease-in-out peer-checked:bg-blue-600 dark:bg-neutral-700 dark:peer-checked:bg-blue-500 peer-disabled:opacity-50 peer-disabled:pointer-events-none"></span>
|
||||
<span className="absolute top-1/2 start-0.5 -translate-y-1/2 size-5 bg-white rounded-full shadow-sm transition-transform duration-200 ease-in-out peer-checked:translate-x-full dark:bg-neutral-400 dark:peer-checked:bg-white"></span>
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-neutral-500">
|
||||
Mute notifications and unassign new messages
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li className="p-3 border-t border-gray-200 first:border-t-0 dark:border-neutral-700">
|
||||
<div className="flex gap-x-3">
|
||||
<span className="mt-0.5 shrink-0 flex flex-col justify-center items-center size-6.5 bg-yellow-500 text-white rounded-full">
|
||||
<svg className="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<div className="grow">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<h4 className="text-sm font-semibold text-gray-800 dark:text-neutral-200">
|
||||
Do not disturb
|
||||
</h4>
|
||||
<label htmlFor="hs-pro-dsmdnds" className="relative inline-block w-11 h-6 cursor-pointer">
|
||||
<input type="checkbox" id="hs-pro-dsmdnds" className="peer sr-only" />
|
||||
<span className="absolute inset-0 bg-gray-200 rounded-full transition-colors duration-200 ease-in-out peer-checked:bg-blue-600 dark:bg-neutral-700 dark:peer-checked:bg-blue-500 peer-disabled:opacity-50 peer-disabled:pointer-events-none"></span>
|
||||
<span className="absolute top-1/2 start-0.5 -translate-y-1/2 size-5 bg-white rounded-full shadow-sm transition-transform duration-200 ease-in-out peer-checked:translate-x-full dark:bg-neutral-400 dark:peer-checked:bg-white"></span>
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-neutral-500">
|
||||
Mute notifications
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li className="p-3 border-t border-gray-200 first:border-t-0 dark:border-neutral-700">
|
||||
<div className="flex gap-x-3">
|
||||
<span className="mt-0.5 shrink-0 flex flex-col justify-center items-center size-6.5 bg-yellow-500 text-white rounded-full">
|
||||
<svg className="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<div className="grow">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<h4 className="text-sm font-semibold text-gray-800 dark:text-neutral-200">
|
||||
Matches
|
||||
</h4>
|
||||
<label htmlFor="hs-pro-dsmschs" className="relative inline-block w-11 h-6 cursor-pointer">
|
||||
<input type="checkbox" id="hs-pro-dsmschs" className="peer sr-only" />
|
||||
<span className="absolute inset-0 bg-gray-200 rounded-full transition-colors duration-200 ease-in-out peer-checked:bg-blue-600 dark:bg-neutral-700 dark:peer-checked:bg-blue-500 peer-disabled:opacity-50 peer-disabled:pointer-events-none"></span>
|
||||
<span className="absolute top-1/2 start-0.5 -translate-y-1/2 size-5 bg-white rounded-full shadow-sm transition-transform duration-200 ease-in-out peer-checked:translate-x-full dark:bg-neutral-400 dark:peer-checked:bg-white"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 sm:mt-1 flex flex-wrap items-center gap-2">
|
||||
<div className="relative">
|
||||
<select id="hs-pro-select-time1" data-hs-select='{
|
||||
"placeholder": "Select option...",
|
||||
"toggleTag": "<button type=\"button\" aria-expanded=\"false\"></button>",
|
||||
"toggleClasses": "hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 relative py-2 px-4 pe-7 flex text-nowrap w-full cursor-pointer bg-gray-100 rounded-lg text-start text-sm text-gray-800 focus:outline-hidden focus:bg-gray-200 before:absolute before:inset-0 before:z-1 dark:bg-neutral-700 dark:text-neutral-200 dark:focus:bg-neutral-700",
|
||||
"dropdownClasses": "mt-2 z-50 w-full min-w-36 max-h-72 p-1 space-y-0.5 overflow-hidden overflow-y-auto bg-white rounded-xl shadow-xl [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:bg-neutral-900",
|
||||
"optionClasses": "hs-selected:bg-gray-100 dark:hs-selected:bg-neutral-800 py-2 px-4 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-hidden focus:bg-gray-100 dark:text-neutral-300 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800",
|
||||
"optionTemplate": "<div className=\"flex justify-between items-center w-full\"><span data-title></span><span className=\"hidden hs-selected:block\"><svg className=\"shrink-0 size-3.5 text-gray-800 dark:text-neutral-200\" xmlns=\"http:.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>",
|
||||
"viewport": "#hs-modal-status-body"
|
||||
}' className="hidden">
|
||||
<option value="">Choose</option>
|
||||
<option>12:01 AM</option>
|
||||
<option>1:00 AM</option>
|
||||
<option>2:00 AM</option>
|
||||
<option>3:00 AM</option>
|
||||
<option>4:00 AM</option>
|
||||
<option>5:00 AM</option>
|
||||
<option>6:00 AM</option>
|
||||
<option>7:00 AM</option>
|
||||
<option>8:00 AM</option>
|
||||
<option selected>9:00 AM</option>
|
||||
<option >10:00 AM</option>
|
||||
<option>11:00 AM</option>
|
||||
<option>12:01 PM</option>
|
||||
<option>1:00 PM</option>
|
||||
<option>2:00 PM</option>
|
||||
<option>3:00 PM</option>
|
||||
<option>4:00 PM</option>
|
||||
<option>5:00 PM</option>
|
||||
<option>6:00 PM</option>
|
||||
<option>7:00 PM</option>
|
||||
<option>8:00 PM</option>
|
||||
<option>9:00 PM</option>
|
||||
<option>10:00 PM</option>
|
||||
<option>11:00 PM</option>
|
||||
</select>
|
||||
|
||||
<div className="absolute top-1/2 end-2.5 -translate-y-1/2">
|
||||
<svg className="shrink-0 size-3.5 text-gray-500 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="text-sm text-gray-500 dark:text-neutral-500">to:</span>
|
||||
|
||||
<div className="relative">
|
||||
<select id="hs-pro-select-time2" data-hs-select='{
|
||||
"placeholder": "Select option...",
|
||||
"toggleTag": "<button type=\"button\" aria-expanded=\"false\"></button>",
|
||||
"toggleClasses": "hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 relative py-2 px-4 pe-7 flex text-nowrap w-full cursor-pointer bg-gray-100 rounded-lg text-start text-sm text-gray-800 focus:outline-hidden focus:bg-gray-200 before:absolute before:inset-0 before:z-1 dark:bg-neutral-700 dark:text-neutral-200 dark:focus:bg-neutral-700",
|
||||
"dropdownClasses": "mt-2 z-50 w-full min-w-36 max-h-72 p-1 space-y-0.5 overflow-hidden overflow-y-auto bg-white rounded-xl shadow-xl [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:bg-neutral-900",
|
||||
"optionClasses": "hs-selected:bg-gray-100 dark:hs-selected:bg-neutral-800 py-2 px-4 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-hidden focus:bg-gray-100 dark:text-neutral-300 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800",
|
||||
"optionTemplate": "<div className=\"flex justify-between items-center w-full\"><span data-title></span><span className=\"hidden hs-selected:block\"><svg className=\"shrink-0 size-3.5 text-gray-800 dark:text-neutral-200\" xmlns=\"http:.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>",
|
||||
"viewport": "#hs-modal-status-body"
|
||||
}' className="hidden">
|
||||
<option value="">Choose</option>
|
||||
<option>12:01 AM</option>
|
||||
<option>1:00 AM</option>
|
||||
<option>2:00 AM</option>
|
||||
<option>3:00 AM</option>
|
||||
<option>4:00 AM</option>
|
||||
<option>5:00 AM</option>
|
||||
<option>6:00 AM</option>
|
||||
<option>7:00 AM</option>
|
||||
<option>8:00 AM</option>
|
||||
<option >9:00 AM</option>
|
||||
<option selected>10:00 AM</option>
|
||||
<option>11:00 AM</option>
|
||||
<option>12:01 PM</option>
|
||||
<option>1:00 PM</option>
|
||||
<option>2:00 PM</option>
|
||||
<option>3:00 PM</option>
|
||||
<option>4:00 PM</option>
|
||||
<option>5:00 PM</option>
|
||||
<option>6:00 PM</option>
|
||||
<option>7:00 PM</option>
|
||||
<option>8:00 PM</option>
|
||||
<option>9:00 PM</option>
|
||||
<option>10:00 PM</option>
|
||||
<option>11:00 PM</option>
|
||||
</select>
|
||||
|
||||
<div className="absolute top-1/2 end-2.5 -translate-y-1/2">
|
||||
<svg className="shrink-0 size-3.5 text-gray-500 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 sm:gap-4">
|
||||
<div className="flex items-center gap-x-3">
|
||||
<label className="text-sm text-gray-500 dark:text-neutral-500">
|
||||
Clear status
|
||||
</label>
|
||||
|
||||
<div className="relative">
|
||||
<select data-hs-select='{
|
||||
"placeholder": "Status",
|
||||
"toggleTag": "<button type=\"button\" aria-expanded=\"false\"></button>",
|
||||
"toggleClasses": "hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 relative py-2 ps-3 pe-7 inline-flex justify-center items-center text-start bg-white border border-gray-200 text-gray-800 text-sm rounded-lg shadow-2xs align-middle focus:outline-hidden focus:ring-2 focus:ring-blue-500 before:absolute before:inset-0 before:z-1 hover:bg-gray-50 dark:bg-neutral-800 dark:border-neutral-600 dark:text-neutral-200 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700",
|
||||
"dropdownClasses": "mt-2 z-50 w-48 p-1 space-y-0.5 bg-white rounded-xl shadow-xl dark:bg-neutral-900",
|
||||
"optionClasses": "hs-selected:bg-gray-100 dark:hs-selected:bg-neutral-800 py-2 px-4 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800",
|
||||
"optionTemplate": "<div className=\"flex justify-between items-center w-full\"><span data-title></span><span className=\"hidden hs-selected:block\"><svg className=\"shrink-0 size-3.5 text-gray-800 dark:text-neutral-200\" xmlns=\"http:.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>",
|
||||
"viewport": "#hs-modal-status-body"
|
||||
}' className="hidden">
|
||||
<option value="">Choose</option>
|
||||
<option selected>Never</option>
|
||||
<option>In 30 minutes</option>
|
||||
<option>In 1 hour</option>
|
||||
<option>Today</option>
|
||||
<option>This week</option>
|
||||
</select>
|
||||
|
||||
<div className="absolute top-1/2 end-2.5 -translate-y-1/2">
|
||||
<svg className="shrink-0 size-3.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-x-3">
|
||||
<label className="text-sm text-gray-500 dark:text-neutral-500">
|
||||
Visible to
|
||||
</label>
|
||||
|
||||
<div className="relative inline-block">
|
||||
<select id="hs-pro-select-visibility" data-hs-select='{
|
||||
"placeholder": "Visibile to",
|
||||
"toggleTag": "<button type=\"button\" aria-expanded=\"false\"><span className=\"me-2\" data-icon></span><span className=\"text-gray-800 dark:text-neutral-200\" data-title></span></button>",
|
||||
"toggleClasses": "hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 relative py-2 ps-3 pe-7 inline-flex justify-center items-center text-start bg-white border border-gray-200 text-gray-500 text-sm rounded-lg shadow-2xs align-middle focus:outline-hidden focus:ring-2 focus:ring-blue-500 before:absolute before:inset-0 before:z-1 hover:bg-gray-50 dark:bg-neutral-800 dark:border-neutral-600 dark:text-neutral-500 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700",
|
||||
"dropdownClasses": "mt-2 z-50 w-48 p-1 space-y-0.5 bg-white rounded-xl shadow-xl dark:bg-neutral-900",
|
||||
"optionClasses": "hs-selected:bg-gray-100 dark:hs-selected:bg-neutral-800 py-2 px-4 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800",
|
||||
"optionTemplate": "<div><div className=\"flex items-center\"><div className=\"me-2\" data-icon></div><div className=\"font-semibold text-gray-800 dark:text-neutral-200\" data-title></div></div><div className=\"text-sm text-gray-500 dark:text-neutral-500\" data-description></div></div>",
|
||||
"viewport": "#hs-modal-status-body"
|
||||
}' className="hidden">
|
||||
<option value="">Choose</option>
|
||||
<option value="1" selected data-hs-select-option='{
|
||||
"description": "Your status will be visible to everyone",
|
||||
"icon": "<svg className=\"shrink-0 size-4\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"lucide lucide-globe-2\"><path d=\"M21.54 15H17a2 2 0 0 0-2 2v4.54\"/><path d=\"M7 3.34V5a3 3 0 0 0 3 3v0a2 2 0 0 1 2 2v0c0 1.1.9 2 2 2v0a2 2 0 0 0 2-2v0c0-1.1.9-2 2-2h3.17\"/><path d=\"M11 21.95V18a2 2 0 0 0-2-2v0a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05\"/><circle cx=\"12\" cy=\"12\" r=\"10\"/></svg>"
|
||||
}'>Anyone</option>
|
||||
<option value="2" data-hs-select-option='{
|
||||
"icon": "<svg className=\"inline-block size-4\" width=\"32\" height=\"32\" viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M16.0355 1.75926C10.6408 1.75926 5.30597 1.49951 0.0111241 1C-0.288584 7.23393 5.50578 13.1282 12.7987 14.5668L13.9975 14.7266C14.3372 12.4289 15.9956 3.7773 16.595 1.73928C16.4152 1.75926 16.2353 1.75926 16.0355 1.75926Z\" fill=\"#A49DFF\"/><path d=\"M16.615 1.75926C16.615 1.75926 25.2266 7.9932 28.5234 16.3451C30.0419 11.3499 31.1608 6.15498 32 1C26.8051 1.49951 21.71 1.75926 16.615 1.75926Z\" fill=\"#28289A\"/><path d=\"M13.9975 14.7466L13.8177 15.9455C13.8177 15.9455 12.2592 28.4133 23.1886 31.9699C25.2266 26.8748 27.0049 21.6599 28.5234 16.3251C21.9698 15.8456 13.9975 14.7466 13.9975 14.7466Z\" fill=\"#5ADCEE\"/><path d=\"M16.6149 1.75927C16.0155 3.79729 14.3571 12.4089 14.0175 14.7466C14.0175 14.7466 21.9897 15.8456 28.5233 16.3251C25.1866 7.9932 16.6149 1.75927 16.6149 1.75927Z\" fill=\"#7878FF\"/></svg>"
|
||||
}'>Guideline</option>
|
||||
</select>
|
||||
|
||||
<div className="absolute top-1/2 end-2.5 -translate-y-1/2">
|
||||
<svg className="shrink-0 size-3.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 flex justify-between gap-x-2">
|
||||
<div className="flex-1 flex justify-end items-center gap-2">
|
||||
<button type="button" className="py-2 px-3 text-nowrap inline-flex justify-center items-center text-start whitespace-nowrap bg-white border border-gray-200 text-gray-800 text-sm font-medium rounded-lg shadow-2xs align-middle hover:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700" data-hs-overlay="#hs-pro-dsm">
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button type="button" className="py-2 px-3 text-nowrap inline-flex justify-center items-center gap-x-2 text-start whitespace-nowrap bg-blue-600 border border-blue-600 text-white text-sm font-medium rounded-lg shadow-2xs align-middle hover:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:ring-1 focus:ring-blue-300 dark:focus:ring-blue-500" data-hs-overlay="#hs-pro-dsm">
|
||||
Save status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -13,6 +13,7 @@ type SelectProps = {
|
||||
className?: string;
|
||||
showArrow?: boolean;
|
||||
fullWidth?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export default function Select({
|
||||
@ -24,6 +25,7 @@ export default function Select({
|
||||
className,
|
||||
showArrow = true,
|
||||
fullWidth = true,
|
||||
disabled = false,
|
||||
}: SelectProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [direction, setDirection] = useState<"up" | "down">("down");
|
||||
@ -56,7 +58,7 @@ export default function Select({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (!open || disabled) return;
|
||||
computePosition();
|
||||
|
||||
const onScroll = () => computePosition();
|
||||
@ -73,7 +75,7 @@ export default function Select({
|
||||
document.removeEventListener("keydown", onKey);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, dropDirection]);
|
||||
}, [open, dropDirection, disabled]);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePointerDown = (event: MouseEvent) => {
|
||||
@ -119,10 +121,14 @@ export default function Select({
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={() => setOpen(prev => !prev)}
|
||||
onClick={() => { if (!disabled) setOpen(prev => !prev); }}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
className={`relative py-2 px-3 ${showArrow ? 'pe-9' : ''} ${fullWidth ? 'w-full' : 'w-auto inline-flex'} w-full cursor-pointer bg-white border border-gray-200 rounded-lg text-start text-sm text-gray-800 hover:border-gray-300 focus:border-blue-500 focus:ring focus:ring-blue-500/50 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-300 ${className || ''}`}
|
||||
aria-disabled={disabled}
|
||||
disabled={disabled}
|
||||
className={`relative py-2 px-3 ${showArrow ? 'pe-9' : ''} ${fullWidth ? 'w-full' : 'w-auto inline-flex'} w-full cursor-pointer bg-white border border-gray-200 rounded-lg text-start text-sm text-gray-800 hover:border-gray-300 focus:border-blue-500 focus:ring focus:ring-blue-500/50 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-300 ${className || ''} ${
|
||||
disabled ? 'opacity-60 cursor-not-allowed pointer-events-auto' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{selectedOption ? selectedOption.label : placeholder}
|
||||
|
||||
150
src/app/[locale]/components/profile/[steamId]/Profile.tsx
Normal file
150
src/app/[locale]/components/profile/[steamId]/Profile.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
// /src/app/profile/[steamId]/Profile.tsx
|
||||
import Link from 'next/link'
|
||||
import Card from '../../Card'
|
||||
|
||||
type Props = { steamId: string }
|
||||
|
||||
type ApiStats = {
|
||||
stats: Array<{
|
||||
date: string
|
||||
kills: number
|
||||
deaths: number
|
||||
assists?: number | null
|
||||
totalDamage?: number | null
|
||||
matchType?: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
async function getStats(steamId: string): Promise<ApiStats | null> {
|
||||
const base = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'
|
||||
const res = await fetch(`${base}/api/stats/${steamId}`, { cache: 'no-store' })
|
||||
if (!res.ok) return null
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export default async function Profile({ steamId }: Props) {
|
||||
const data = await getStats(steamId)
|
||||
const matches = data?.stats ?? []
|
||||
|
||||
const games = matches.length
|
||||
const kills = matches.reduce((s, m) => s + (m.kills ?? 0), 0)
|
||||
const deaths = matches.reduce((s, m) => s + (m.deaths ?? 0), 0)
|
||||
const assists = matches.reduce((s, m) => s + (m.assists ?? 0), 0)
|
||||
const dmg = matches.reduce((s, m) => s + (m.totalDamage ?? 0), 0)
|
||||
const kd = deaths === 0 ? '∞' : (kills / Math.max(1, deaths)).toFixed(2)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Quick KPIs */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs text-neutral-400">Matches</div>
|
||||
<div className="mt-1 text-2xl font-bold">{games}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-blue-500/15 p-2 ring-1 ring-blue-400/20">🎮</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs text-neutral-400">Kills</div>
|
||||
<div className="mt-1 text-2xl font-bold">{kills}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-emerald-500/15 p-2 ring-1 ring-emerald-400/20">🎯</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs text-neutral-400">K/D</div>
|
||||
<div className="mt-1 text-2xl font-bold">{kd}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-fuchsia-500/15 p-2 ring-1 ring-fuchsia-400/20">⚖️</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs text-neutral-400">Damage (sum)</div>
|
||||
<div className="mt-1 text-2xl font-bold">{Math.round(dmg)}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-amber-500/15 p-2 ring-1 ring-amber-400/20">🔥</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Callouts to subpages */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Statistiken</h3>
|
||||
<p className="mt-1 text-sm text-neutral-400">
|
||||
Charts, Verläufe und Map-Auswertungen.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={`/profile/${steamId}/stats`}
|
||||
className="rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
|
||||
>
|
||||
Öffnen
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Matches</h3>
|
||||
<p className="mt-1 text-sm text-neutral-400">
|
||||
Alle Spiele in einer übersichtlichen Liste.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={`/profile/${steamId}/matches`}
|
||||
className="rounded-lg bg-neutral-700 px-3 py-1.5 text-sm font-medium text-white hover:bg-neutral-600"
|
||||
>
|
||||
Öffnen
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Letzte 5 Matches */}
|
||||
<Card>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-base font-semibold">Letzte Matches</h3>
|
||||
<Link href={`/profile/${steamId}/matches`} className="text-sm text-blue-400 hover:underline">
|
||||
Alle ansehen
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{matches.slice(0, 5).length === 0 ? (
|
||||
<p className="text-sm text-neutral-400">Noch keine Daten.</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-neutral-800/70">
|
||||
{matches.slice(0, 5).map((m, i) => (
|
||||
<li key={i} className="py-2 flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs text-neutral-500">{new Date(m.date).toLocaleString()}</div>
|
||||
<div className="text-sm">
|
||||
{m.matchType ? <span className="mr-2 text-neutral-400">{m.matchType} •</span> : null}
|
||||
<span>K {m.kills} / D {m.deaths} {Number.isFinite(m.assists) ? `/ A ${m.assists}` : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="rounded-md bg-neutral-800 px-2 py-0.5 text-xs">
|
||||
K/D
|
||||
{m.deaths === 0 ? '∞' : ((m.kills ?? 0) / Math.max(1, m.deaths ?? 0)).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
// /src/app/profile/[steamId]/matches/MatchesList.tsx
|
||||
import Link from 'next/link'
|
||||
import Card from '../../../Card'
|
||||
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||
|
||||
type Props = { steamId: string }
|
||||
|
||||
type MatchRow = {
|
||||
matchId?: string | number | null
|
||||
id?: string | number | null
|
||||
date: string
|
||||
map: string
|
||||
kills: number
|
||||
deaths: number
|
||||
assists?: number | null
|
||||
totalDamage?: number | null
|
||||
roundCount?: number | null
|
||||
scoreA?: number | null
|
||||
scoreB?: number | null
|
||||
team?: 'A' | 'B' | null
|
||||
result?: 'win' | 'loss' | 'draw' | null
|
||||
matchType?: string | null
|
||||
}
|
||||
|
||||
function labelForMap(key: string) {
|
||||
const k = key.toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
|
||||
return MAP_OPTIONS.find(o => o.key === k)?.label ?? k.replace(/^de_/, '').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
||||
}
|
||||
|
||||
function kdr(k: number, d: number) {
|
||||
if (!Number.isFinite(k) || !Number.isFinite(d)) return '-'
|
||||
return d === 0 ? '∞' : (k / d).toFixed(2)
|
||||
}
|
||||
|
||||
async function getData(steamId: string) {
|
||||
// greift auf die gleiche API wie die Stats-Seite zu
|
||||
const base = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'
|
||||
const res = await fetch(`${base}/api/stats/${steamId}`, { cache: 'no-store' })
|
||||
if (!res.ok) return { matches: [] as MatchRow[] }
|
||||
const json = await res.json()
|
||||
return { matches: (json?.stats ?? []) as MatchRow[] }
|
||||
}
|
||||
|
||||
export default async function MatchesList({ steamId }: Props) {
|
||||
const { matches } = await getData(steamId)
|
||||
|
||||
if (!matches?.length) {
|
||||
return (
|
||||
<Card>
|
||||
<p className="text-sm text-neutral-400">Keine Matches gefunden.</p>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{matches.map((m, idx) => {
|
||||
const linkId = String(m.matchId ?? m.id ?? '')
|
||||
const href = linkId ? `/match/${linkId}` : undefined
|
||||
const ADR =
|
||||
Number.isFinite(m.totalDamage) && Number.isFinite(m.roundCount) && (m.roundCount ?? 0) > 0
|
||||
? ((m.totalDamage as number) / (m.roundCount as number)).toFixed(1)
|
||||
: '-'
|
||||
|
||||
const resultPill =
|
||||
m.result === 'win' ? 'bg-emerald-500/15 text-emerald-300 ring-1 ring-emerald-500/30' :
|
||||
m.result === 'loss' ? 'bg-red-500/15 text-red-300 ring-1 ring-red-500/30' :
|
||||
'bg-neutral-500/15 text-neutral-300 ring-1 ring-neutral-500/30'
|
||||
|
||||
const content = (
|
||||
<div className="flex items-center justify-between gap-4 rounded-lg border border-neutral-700/60 bg-neutral-900/40 p-3 hover:bg-neutral-900/70 transition">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs text-neutral-400">{new Date(m.date).toLocaleString()}</div>
|
||||
<div className="truncate text-sm font-medium">
|
||||
{labelForMap(m.map)}
|
||||
{m.matchType ? <span className="ml-2 text-xs text-neutral-400">• {m.matchType}</span> : null}
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap gap-2 text-xs text-neutral-300">
|
||||
<span className="rounded-md bg-neutral-800/80 px-1.5 py-0.5">K: {m.kills}</span>
|
||||
<span className="rounded-md bg-neutral-800/80 px-1.5 py-0.5">D: {m.deaths}</span>
|
||||
{Number.isFinite(m.assists) && <span className="rounded-md bg-neutral-800/80 px-1.5 py-0.5">A: {m.assists}</span>}
|
||||
<span className="rounded-md bg-neutral-800/80 px-1.5 py-0.5">K/D: {kdr(m.kills, m.deaths)}</span>
|
||||
<span className="rounded-md bg-neutral-800/80 px-1.5 py-0.5">ADR: {ADR}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
{(Number.isFinite(m.scoreA) || Number.isFinite(m.scoreB)) && (
|
||||
<div className="text-lg font-semibold">
|
||||
{m.scoreA ?? 0} : {m.scoreB ?? 0}
|
||||
</div>
|
||||
)}
|
||||
<div className={`mt-1 inline-flex items-center rounded-full px-2 py-0.5 text-[11px] ${resultPill}`}>
|
||||
{m.result === 'win' ? 'Win' : m.result === 'loss' ? 'Loss' : 'Match'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={`${linkId || 'row'}-${idx}`}>
|
||||
{href ? (
|
||||
<Link href={href} className="block">{content}</Link>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,340 +0,0 @@
|
||||
// /src/app/components/profile/[steamId]/matches/UserMatchesList.tsx
|
||||
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import Table from '../../../Table'
|
||||
import PremierRankBadge from '../../../PremierRankBadge'
|
||||
import CompRankBadge from '../../../CompRankBadge'
|
||||
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||
import Button from '../../../Button'
|
||||
|
||||
interface Match {
|
||||
id: string
|
||||
map: string
|
||||
date: string
|
||||
score: string | null
|
||||
winnerTeam?: 'CT' | 'T' | 'Draw' | null
|
||||
team?: 'CT' | 'T' | null
|
||||
matchType: 'premier' | 'competitive' | string
|
||||
rating: string
|
||||
kills: number
|
||||
deaths: number
|
||||
kdr: string
|
||||
rankNew: number
|
||||
rankOld: number
|
||||
rankChange: number | null
|
||||
oneK: number
|
||||
twoK: number
|
||||
threeK: number
|
||||
fourK: number
|
||||
fiveK: number
|
||||
aim: number | string
|
||||
}
|
||||
|
||||
const parseScore = (raw?: string | null): [number, number] => {
|
||||
if (!raw) return [0, 0]
|
||||
const [a, b] = raw.split(':').map(n => Number(n.trim()))
|
||||
return [Number.isNaN(a) ? 0 : a, Number.isNaN(b) ? 0 : b]
|
||||
}
|
||||
|
||||
// Scroll-Parent des Sentinels finden (falls eigenes overflow-Element)
|
||||
function getScrollParent(el: HTMLElement | null): HTMLElement | Window {
|
||||
if (!el) return window
|
||||
let p: HTMLElement | null = el.parentElement
|
||||
const re = /(auto|scroll)/i
|
||||
while (p && p !== document.body) {
|
||||
const cs = getComputedStyle(p)
|
||||
if (re.test(cs.overflowY) || re.test(cs.overflow)) return p
|
||||
p = p.parentElement
|
||||
}
|
||||
return window
|
||||
}
|
||||
|
||||
export default function UserMatchesList({ steamId }: { steamId: string }) {
|
||||
const [matches, setMatches] = useState<Match[]>([])
|
||||
const [cursor, setCursor] = useState<string | null>(null)
|
||||
const [hasMore, setHasMore] = useState<boolean>(true)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
// Refs halten stets den aktuellen Wert für Callbacks
|
||||
const cursorRef = useRef<string | null>(null)
|
||||
const loadingRef = useRef(false)
|
||||
const hasMoreRef = useRef(true)
|
||||
|
||||
const router = useRouter()
|
||||
const sentinelRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const setCursorBoth = (c: string | null) => { setCursor(c); cursorRef.current = c }
|
||||
const setLoadingBoth = (v: boolean) => { setLoading(v); loadingRef.current = v }
|
||||
const setHasMoreBoth = (v: boolean) => { setHasMore(v); hasMoreRef.current = v }
|
||||
|
||||
// Seite laden – initial: ersetzen, sonst: anhängen
|
||||
const fetchPage = useCallback(async (opts?: { initial?: boolean }) => {
|
||||
if (!steamId) return
|
||||
if (loadingRef.current || !hasMoreRef.current) return
|
||||
|
||||
setLoadingBoth(true)
|
||||
try {
|
||||
const params = new URLSearchParams({ types: 'premier,competitive', limit: '10' })
|
||||
const useCursor = opts?.initial ? null : cursorRef.current
|
||||
if (useCursor) params.set('cursor', useCursor)
|
||||
|
||||
const res = await fetch(`/api/user/${steamId}/matches?` + params.toString(), { cache: 'no-store' })
|
||||
if (!res.ok) throw new Error('Failed to fetch')
|
||||
|
||||
const data = await res.json()
|
||||
const pageItems: Match[] = Array.isArray(data) ? data : (data.items ?? [])
|
||||
const pageNextCursor: string | null = Array.isArray(data) ? null : (data.nextCursor ?? null)
|
||||
const pageHasMore: boolean = Array.isArray(data) ? false : !!data.hasMore
|
||||
|
||||
if (opts?.initial) {
|
||||
setMatches(pageItems) // → genau 10 nach (Re-)Öffnen des Tabs
|
||||
} else {
|
||||
setMatches(prev => {
|
||||
const seen = new Set(prev.map(m => m.id))
|
||||
const merged = [...prev]
|
||||
for (const it of pageItems) if (!seen.has(it.id)) merged.push(it)
|
||||
return merged
|
||||
})
|
||||
}
|
||||
|
||||
setCursorBoth(pageNextCursor)
|
||||
setHasMoreBoth(pageHasMore)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
setLoadingBoth(false)
|
||||
}
|
||||
}, [steamId])
|
||||
|
||||
const onLoadMoreClick = useCallback(() => {
|
||||
// Guards sind bereits in fetchPage drin (loadingRef / hasMoreRef)
|
||||
fetchPage()
|
||||
}, [fetchPage])
|
||||
|
||||
// Sichtbarkeits-Check des Sentinels im aktuellen Layout/Container
|
||||
const sentinelInView = useCallback(() => {
|
||||
const el = sentinelRef.current
|
||||
if (!el) return false
|
||||
const rect = el.getBoundingClientRect()
|
||||
const vh = window.innerHeight || document.documentElement.clientHeight
|
||||
const vw = window.innerWidth || document.documentElement.clientWidth
|
||||
return rect.top < vh && rect.bottom >= 0 && rect.left < vw && rect.right >= 0
|
||||
}, [])
|
||||
|
||||
const checkAndLoadMore = useCallback(() => {
|
||||
if (!hasMoreRef.current || loadingRef.current) return
|
||||
if (sentinelInView()) fetchPage()
|
||||
}, [fetchPage, sentinelInView])
|
||||
|
||||
// Reset + erste Seite laden (ersetzen)
|
||||
useEffect(() => {
|
||||
setMatches([])
|
||||
setCursorBoth(null)
|
||||
setHasMoreBoth(true)
|
||||
setLoadingBoth(false)
|
||||
if (steamId) fetchPage({ initial: true })
|
||||
}, [steamId, fetchPage])
|
||||
|
||||
// IntersectionObserver mit KORREKTEM root (Scroll-Parent) + Fallback-Events
|
||||
useEffect(() => {
|
||||
const target = sentinelRef.current
|
||||
if (!target) return
|
||||
|
||||
const rootEl = getScrollParent(target)
|
||||
const rootForIO = rootEl instanceof Window ? undefined : rootEl
|
||||
|
||||
const io = new IntersectionObserver(
|
||||
entries => {
|
||||
const first = entries[0]
|
||||
if (first.isIntersecting && hasMoreRef.current && !loadingRef.current) {
|
||||
fetchPage()
|
||||
}
|
||||
},
|
||||
{ root: rootForIO as Element | undefined, rootMargin: '400px 0px 400px 0px' }
|
||||
)
|
||||
|
||||
io.observe(target)
|
||||
|
||||
// Scroll/Resize-Fallback auf dem tatsächlichen Scroll-Container
|
||||
const scrollTarget: any = rootEl instanceof Window ? window : rootEl
|
||||
const onScroll = () => checkAndLoadMore()
|
||||
const onResize = () => checkAndLoadMore()
|
||||
scrollTarget.addEventListener('scroll', onScroll, { passive: true })
|
||||
window.addEventListener('resize', onResize)
|
||||
|
||||
// Kick direkt nach Mount (z. B. wenn Sentinel bereits sichtbar)
|
||||
const id = window.setTimeout(checkAndLoadMore, 60)
|
||||
|
||||
// Reaktion auf Tab-Show/Hide per CSS (class/hidden/style)
|
||||
const mo = new MutationObserver(() => {
|
||||
// Frame abwarten, dann prüfen
|
||||
requestAnimationFrame(checkAndLoadMore)
|
||||
})
|
||||
// bis zur nächsten Panel-Grenze hochhorchen
|
||||
let node: Element | null = target.parentElement
|
||||
while (node && node !== document.body) {
|
||||
mo.observe(node, { attributes: true, attributeFilter: ['class', 'style', 'hidden'] })
|
||||
node = node.parentElement
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(id)
|
||||
io.disconnect()
|
||||
mo.disconnect()
|
||||
scrollTarget.removeEventListener('scroll', onScroll)
|
||||
window.removeEventListener('resize', onResize)
|
||||
}
|
||||
}, [checkAndLoadMore, fetchPage])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
matches-card
|
||||
flex flex-col bg-white border border-gray-200 shadow-2xs rounded-xl
|
||||
dark:bg-neutral-800 dark:border-neutral-700 dark:shadow-neutral-700/70
|
||||
"
|
||||
>
|
||||
{/* Tabelle: Rundung/Clipping kommt rein per CSS (siehe globals.css) */}
|
||||
<Table>
|
||||
<Table.Head>
|
||||
<Table.Row>
|
||||
{['Map','Date','Score','Rank','Aim','Kills','Deaths','K/D'].map(h => (
|
||||
<Table.Cell key={h} as="th">{h}</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
</Table.Head>
|
||||
|
||||
<Table.Body>
|
||||
{matches.map(m => {
|
||||
const mapInfo =
|
||||
MAP_OPTIONS.find(opt => opt.key === m.map) ??
|
||||
MAP_OPTIONS.find(opt => opt.key === 'lobby_mapvote')
|
||||
const [scoreCT, scoreT] = parseScore(m.score)
|
||||
|
||||
const ownCTSide = m.team !== 'T'
|
||||
const left = ownCTSide ? scoreCT : scoreT
|
||||
const right = ownCTSide ? scoreT : scoreCT
|
||||
|
||||
const scoreColor =
|
||||
left > right ? 'text-green-600 dark:text-green-400'
|
||||
: left < right ? 'text-red-600 dark:text-red-400'
|
||||
: 'text-yellow-600 dark:text-yellow-400'
|
||||
|
||||
return (
|
||||
<Table.Row
|
||||
key={m.id}
|
||||
onClick={() => router.push(`/match-details/${m.id}`)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Table.Cell hoverable>
|
||||
<div className="flex items-center gap-2">
|
||||
{(() => {
|
||||
const raw = m.map || ''
|
||||
const normKey = raw.replace(/^de_/, '') // "de_ancient" -> "ancient"
|
||||
|
||||
// 1) Versuch: exact normKey
|
||||
// 2) Versuch: raw (mit "de_")
|
||||
// 3) Versuch: wieder mit "de_" (falls m.map ohne Prefix kam)
|
||||
const opt =
|
||||
MAP_OPTIONS.find(o => o.key === normKey) ||
|
||||
MAP_OPTIONS.find(o => o.key === raw) ||
|
||||
MAP_OPTIONS.find(o => o.key === `de_${normKey}`) ||
|
||||
null
|
||||
|
||||
const label =
|
||||
opt?.label ||
|
||||
normKey || // sinnvoller Text, falls alles fehlt
|
||||
'Map'
|
||||
|
||||
// Icon-Quelle: erst aus MAP_OPTIONS, sonst generischer Pfad auf Basis des normKey
|
||||
const iconSrc = opt?.icon || `/assets/img/mapicons/map_icon_${normKey}.svg`
|
||||
|
||||
// Debug (kannst du wieder entfernen)
|
||||
// console.log({ raw, normKey, optKey: opt?.key, label, iconSrc })
|
||||
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
src={iconSrc}
|
||||
alt={label}
|
||||
width={32}
|
||||
height={32}
|
||||
className="shrink-0"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).src =
|
||||
'/assets/img/mapicons/map_icon_lobby_mapveto.svg'
|
||||
}}
|
||||
/>
|
||||
{label}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
|
||||
<Table.Cell hoverable>{new Date(m.date).toLocaleString()}</Table.Cell>
|
||||
|
||||
<Table.Cell hoverable>
|
||||
<span className={`font-medium ${scoreColor}`}>
|
||||
{left} : {right}
|
||||
</span>
|
||||
</Table.Cell>
|
||||
|
||||
<Table.Cell hoverable className="whitespace-nowrap">
|
||||
<div className="flex items-center gap-[6px]">
|
||||
{m.matchType === 'premier'
|
||||
? <PremierRankBadge rank={m.rankNew} />
|
||||
: <CompRankBadge rank={m.rankNew} /> }
|
||||
{m.rankChange !== null && m.matchType === 'premier' && (
|
||||
<span className={m.rankChange > 0 ? 'text-green-500' : m.rankChange < 0 ? 'text-red-500' : ''}>
|
||||
{m.rankChange > 0 ? '+' : ''}{m.rankChange}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
|
||||
<Table.Cell hoverable>
|
||||
{Number.isFinite(Number(m.aim))
|
||||
? `${Number(m.aim).toFixed(0)} %`
|
||||
: '-' }
|
||||
</Table.Cell>
|
||||
<Table.Cell hoverable>{m.kills}</Table.Cell>
|
||||
<Table.Cell hoverable>{m.deaths}</Table.Cell>
|
||||
<Table.Cell hoverable>{m.kdr}</Table.Cell>
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
{/* Divider immer vorhanden – Sichtbarkeit per :has(.load-more) */}
|
||||
<div className="table-divider hidden border-t border-gray-200 dark:border-neutral-700" />
|
||||
|
||||
{hasMore && (
|
||||
<div ref={sentinelRef} className="load-more mt-2 flex justify-center">
|
||||
<Button
|
||||
size="sm"
|
||||
color="blue"
|
||||
onClick={onLoadMoreClick}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
className="
|
||||
mb-3 px-4 py-2 rounded-lg text-sm font-medium
|
||||
bg-blue-600 text-white hover:bg-blue-700 focus:bg-blue-700
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500/50
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700
|
||||
"
|
||||
aria-busy={loading ? 'true' : 'false'}
|
||||
>
|
||||
{loading ? 'Lädt …' : 'Mehr laden'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,193 @@
|
||||
// /src/app/profile/[steamId]/stats/StatsView.tsx
|
||||
'use client'
|
||||
|
||||
import { useSession } from 'next-auth/react'
|
||||
import Chart from '../../../Chart'
|
||||
import Card from '../../../Card'
|
||||
import { MatchStats } from '@/types/match'
|
||||
|
||||
type Props = {
|
||||
stats: { matches: MatchStats[] }
|
||||
}
|
||||
|
||||
const kdr = (k?: number, d?: number) =>
|
||||
typeof k === 'number' && typeof d === 'number'
|
||||
? d === 0 ? '∞' : (k / d).toFixed(2)
|
||||
: '-'
|
||||
|
||||
export default function StatsView({ stats }: Props) {
|
||||
const { data: session } = useSession()
|
||||
const steamId = session?.user?.steamId ?? ''
|
||||
|
||||
const { matches } = stats
|
||||
|
||||
const totalKills = matches.reduce((sum, m) => sum + (m.kills ?? 0), 0)
|
||||
const totalDeaths = matches.reduce((sum, m) => sum + (m.deaths ?? 0), 0)
|
||||
const totalAssists = matches.reduce((sum, m) => sum + (m.assists ?? 0), 0)
|
||||
const avgKDR = kdr(totalKills, totalDeaths)
|
||||
|
||||
const premierMatches = matches.filter((m) => m.rankNew !== null && m.matchType === 'premier')
|
||||
const compMatches = matches.filter((m) => m.rankNew !== null && m.matchType !== 'premier')
|
||||
|
||||
const killsPerMap = matches.reduce<Record<string, number>>((acc, m) => {
|
||||
const key = (m.map || '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
|
||||
acc[key] = (acc[key] || 0) + (m.kills ?? 0)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const matchesPerMap = matches.reduce<Record<string, number>>((acc, m) => {
|
||||
const key = (m.map || '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
|
||||
acc[key] = (acc[key] || 0) + 1
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{/* linke Spalte */}
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<div className="relative mx-auto">
|
||||
<Chart
|
||||
type="doughnut"
|
||||
title="Ø Gesamt-K/D"
|
||||
labels={['Kills', 'Deaths']}
|
||||
datasets={[{
|
||||
label: 'Anzahl',
|
||||
data: [totalKills, totalDeaths],
|
||||
backgroundColor: ['rgba(54, 162, 235, 0.6)','rgba(255, 99, 132, 0.6)'],
|
||||
}]}
|
||||
hideLabels
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold">{avgKDR}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Chart
|
||||
type="doughnut"
|
||||
title="Kills vs Assists vs Deaths"
|
||||
labels={['Kills', 'Assists', 'Deaths']}
|
||||
datasets={[{
|
||||
label: 'Anteile',
|
||||
data: [totalKills, totalAssists, totalDeaths],
|
||||
backgroundColor: [
|
||||
'rgba(54, 162, 235, 0.6)',
|
||||
'rgba(255, 206, 86, 0.6)',
|
||||
'rgba(255, 99, 132, 0.6)',
|
||||
],
|
||||
}]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Highlights – Beispiel (auskommentiert) */}
|
||||
{/* {steamId && (
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold mb-2">Highlights</h3>
|
||||
<UserClips steamId={steamId} />
|
||||
</Card>
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
{/* rechte breite Spalte */}
|
||||
<div className="col-span-3 space-y-6">
|
||||
<Chart
|
||||
type="bar"
|
||||
title="Kills pro Match"
|
||||
labels={matches.map((m) => m.date)}
|
||||
datasets={[{ label: 'Kills', data: matches.map((m) => m.kills), backgroundColor: 'rgba(54, 162, 235, 0.6)' }]}
|
||||
/>
|
||||
|
||||
<Chart
|
||||
type="line"
|
||||
title="K/D Ratio pro Match"
|
||||
labels={matches.map((m) => m.date)}
|
||||
datasets={[{
|
||||
label: 'K/D',
|
||||
data: matches.map((m) => (m.deaths === 0 ? m.kills : (m.kills ?? 0) / Math.max(1, m.deaths ?? 0))),
|
||||
borderColor: 'rgba(255, 99, 132, 0.6)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
borderWidth: 2,
|
||||
}]}
|
||||
/>
|
||||
|
||||
<Chart
|
||||
type="line"
|
||||
title="Headshot % pro Match"
|
||||
labels={matches.map((m) => m.date)}
|
||||
datasets={[{
|
||||
label: 'HS%',
|
||||
data: matches.map((m) => m.headshotPct ?? 0),
|
||||
borderColor: 'rgba(153, 102, 255, 0.6)',
|
||||
backgroundColor: 'rgba(153, 102, 255, 0.2)',
|
||||
borderWidth: 2,
|
||||
}]}
|
||||
/>
|
||||
|
||||
<Chart
|
||||
type="bar"
|
||||
title="Gesamtdamage pro Match"
|
||||
labels={matches.map((m) => m.date)}
|
||||
datasets={[{
|
||||
label: 'Damage',
|
||||
data: matches.map((m) => m.totalDamage ?? 0),
|
||||
backgroundColor: 'rgba(255, 206, 86, 0.6)',
|
||||
}]}
|
||||
/>
|
||||
|
||||
{premierMatches.length > 0 && (
|
||||
<Chart
|
||||
type="line"
|
||||
title="Premier Rank-Verlauf"
|
||||
labels={premierMatches.map((m) => m.date)}
|
||||
datasets={[{
|
||||
label: 'Premier Rank',
|
||||
data: premierMatches.map((m) => m.rankNew ?? 0),
|
||||
borderColor: 'rgba(75, 192, 192, 0.6)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
borderWidth: 2,
|
||||
}]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{compMatches.length > 0 && (
|
||||
<Chart
|
||||
type="line"
|
||||
title="Competitive Rank-Verlauf"
|
||||
labels={compMatches.map((m) => m.date)}
|
||||
datasets={[{
|
||||
label: 'Comp Rank',
|
||||
data: compMatches.map((m) => m.rankNew ?? 0),
|
||||
borderColor: 'rgba(255, 159, 64, 0.6)',
|
||||
backgroundColor: 'rgba(255, 159, 64, 0.2)',
|
||||
borderWidth: 2,
|
||||
}]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Chart
|
||||
type="bar"
|
||||
title="Kills pro Map"
|
||||
labels={Object.keys(killsPerMap)}
|
||||
datasets={[{ label: 'Kills', data: Object.values(killsPerMap), backgroundColor: 'rgba(255, 159, 64, 0.6)' }]}
|
||||
/>
|
||||
|
||||
<Chart
|
||||
type="radar"
|
||||
title="Matches pro Map"
|
||||
labels={Object.keys(matchesPerMap)}
|
||||
datasets={[{
|
||||
label: 'Matches',
|
||||
data: Object.values(matchesPerMap),
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 2,
|
||||
}]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,230 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useSession } from 'next-auth/react'
|
||||
import Chart from '../../../Chart'
|
||||
import { MatchStats } from '@/types/match'
|
||||
import Card from '../../../Card'
|
||||
|
||||
type MatchStatsProps = {
|
||||
stats: { matches: MatchStats[] }
|
||||
}
|
||||
|
||||
export default function UserProfile({ stats }: MatchStatsProps) {
|
||||
const { data: session } = useSession()
|
||||
const steamId = session?.user?.steamId ?? ''
|
||||
|
||||
const { matches } = stats
|
||||
|
||||
const totalKills = matches.reduce((sum, m) => sum + m.kills, 0)
|
||||
const totalDeaths = matches.reduce((sum, m) => sum + m.deaths, 0)
|
||||
const totalAssists = matches.reduce((sum, m) => sum + m.assists, 0)
|
||||
const avgKDR = totalDeaths > 0 ? (totalKills / totalDeaths).toFixed(2) : '∞'
|
||||
const premierMatches = matches.filter((m) => m.rankNew !== null && m.matchType === 'premier')
|
||||
const compMatches = matches.filter((m) => m.rankNew !== null && m.matchType !== 'premier')
|
||||
|
||||
const killsPerMap = matches.reduce((acc, match) => {
|
||||
acc[match.map] = (acc[match.map] || 0) + match.kills
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
|
||||
const matchesPerMap = matches.reduce((acc, match) => {
|
||||
acc[match.map] = (acc[match.map] || 0) + 1
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{/* K/D-Anteil + Zahl */}
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<div className="relative mx-auto">
|
||||
<Chart
|
||||
type="doughnut"
|
||||
title="Ø Gesamt-K/D"
|
||||
labels={['Kills', 'Deaths']}
|
||||
datasets={[
|
||||
{
|
||||
label: 'Anzahl',
|
||||
data: [totalKills, totalDeaths],
|
||||
backgroundColor: [
|
||||
'rgba(54, 162, 235, 0.6)',
|
||||
'rgba(255, 99, 132, 0.6)',
|
||||
],
|
||||
},
|
||||
]}
|
||||
hideLabels
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-gray-800 dark:text-white">{avgKDR}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Kills vs Assists vs Deaths */}
|
||||
<Card>
|
||||
<Chart
|
||||
type="doughnut"
|
||||
title="Kills vs Assists vs Deaths"
|
||||
labels={['Kills', 'Assists', 'Deaths']}
|
||||
datasets={[
|
||||
{
|
||||
label: 'Anteile',
|
||||
data: [totalKills, totalAssists, totalDeaths],
|
||||
backgroundColor: [
|
||||
'rgba(54, 162, 235, 0.6)',
|
||||
'rgba(255, 206, 86, 0.6)',
|
||||
'rgba(255, 99, 132, 0.6)',
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* ► Allstar-Clips des aktuellen Users -------------------------- */}
|
||||
{/*
|
||||
{steamId && (
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold mb-2">Highlights</h3>
|
||||
<UserClips steamId={steamId} />
|
||||
</Card>
|
||||
)}
|
||||
*/}
|
||||
</div>
|
||||
|
||||
{/* Breite Diagramme */}
|
||||
<div className="col-span-3 space-y-6">
|
||||
{/* Kills pro Match */}
|
||||
<Chart
|
||||
type="bar"
|
||||
title="Kills pro Match"
|
||||
labels={matches.map((m) => m.date)}
|
||||
datasets={[
|
||||
{
|
||||
label: 'Kills',
|
||||
data: matches.map((m) => m.kills),
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* K/D pro Match */}
|
||||
<Chart
|
||||
type="line"
|
||||
title="K/D Ratio pro Match"
|
||||
labels={matches.map((m) => m.date)}
|
||||
datasets={[
|
||||
{
|
||||
label: 'K/D',
|
||||
data: matches.map((m) =>
|
||||
m.deaths === 0 ? m.kills : m.kills / m.deaths
|
||||
),
|
||||
borderColor: 'rgba(255, 99, 132, 0.6)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
borderWidth: 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Headshot % */}
|
||||
<Chart
|
||||
type="line"
|
||||
title="Headshot % pro Match"
|
||||
labels={matches.map((m) => m.date)}
|
||||
datasets={[
|
||||
{
|
||||
label: 'HS%',
|
||||
data: matches.map((m) => m.headshotPct),
|
||||
borderColor: 'rgba(153, 102, 255, 0.6)',
|
||||
backgroundColor: 'rgba(153, 102, 255, 0.2)',
|
||||
borderWidth: 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Damage */}
|
||||
<Chart
|
||||
type="bar"
|
||||
title="Gesamtdamage pro Match"
|
||||
labels={matches.map((m) => m.date)}
|
||||
datasets={[
|
||||
{
|
||||
label: 'Damage',
|
||||
data: matches.map((m) => m.totalDamage),
|
||||
backgroundColor: 'rgba(255, 206, 86, 0.6)',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Premier Rank-Verlauf */}
|
||||
{premierMatches.length > 0 && (
|
||||
<Chart
|
||||
type="line"
|
||||
title="Premier Rank-Verlauf"
|
||||
labels={premierMatches.map((m) => m.date)}
|
||||
datasets={[
|
||||
{
|
||||
label: 'Premier Rank',
|
||||
data: premierMatches.map((m) => m.rankNew ?? 0),
|
||||
borderColor: 'rgba(75, 192, 192, 0.6)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
borderWidth: 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Competitive Rank-Verlauf */}
|
||||
{compMatches.length > 0 && (
|
||||
<Chart
|
||||
type="line"
|
||||
title="Competitive Rank-Verlauf"
|
||||
labels={compMatches.map((m) => m.date)}
|
||||
datasets={[
|
||||
{
|
||||
label: 'Comp Rank',
|
||||
data: compMatches.map((m) => m.rankNew ?? 0),
|
||||
borderColor: 'rgba(255, 159, 64, 0.6)',
|
||||
backgroundColor: 'rgba(255, 159, 64, 0.2)',
|
||||
borderWidth: 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* Kills pro Map */}
|
||||
<Chart
|
||||
type="bar"
|
||||
title="Kills pro Map"
|
||||
labels={Object.keys(killsPerMap)}
|
||||
datasets={[
|
||||
{
|
||||
label: 'Kills',
|
||||
data: Object.values(killsPerMap),
|
||||
backgroundColor: 'rgba(255, 159, 64, 0.6)',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Matches pro Map (Radar) */}
|
||||
<Chart
|
||||
type="radar"
|
||||
title="Matches pro Map"
|
||||
labels={Object.keys(matchesPerMap)}
|
||||
datasets={[
|
||||
{
|
||||
label: 'Matches',
|
||||
data: Object.values(matchesPerMap),
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
// /src/app/components/UserHeader.tsx
|
||||
import { Tabs } from '../components/Tabs'
|
||||
import PremierRankBadge from './PremierRankBadge'
|
||||
import { Tabs } from '../../components/Tabs'
|
||||
import PremierRankBadge from '../../components/PremierRankBadge'
|
||||
|
||||
type UserHeaderProps = {
|
||||
steamId: string
|
||||
@ -3,7 +3,7 @@ import type { ReactNode } from 'react'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import Card from '../../components/Card'
|
||||
import UserHeader from '../../components/UserHeader'
|
||||
import UserHeader from './UserHeader'
|
||||
|
||||
export default async function ProfileLayout({
|
||||
children,
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
// /src/app/profile/[steamId]/matches/page.tsx
|
||||
|
||||
import UserMatchesList from '@/app/[locale]/components/profile/[steamId]/matches/UserMatchesList'
|
||||
import MatchesList from '@/app/[locale]/components/profile/[steamId]/matches/MatchesList'
|
||||
|
||||
export default function MatchesPage({ params }: { params: { steamId: string } }) {
|
||||
return (
|
||||
<UserMatchesList steamId={params.steamId} />
|
||||
)
|
||||
return <MatchesList steamId={params.steamId} />
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// /src/app/profile/[steamId]/page.tsx
|
||||
import { redirect } from 'next/navigation'
|
||||
import Profile from '../../components/profile/[steamId]/Profile'
|
||||
|
||||
export default function ProfileRedirect({ params }: { params: { steamId: string } }) {
|
||||
redirect(`/profile/${params.steamId}/stats`)
|
||||
export default function ProfilePage({ params }: { params: { steamId: string } }) {
|
||||
return <Profile steamId={params.steamId} /> // ggf. Props geben
|
||||
}
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
// /src/app/profile/[steamId]/stats/page.tsx
|
||||
|
||||
import UserProfile from '@/app/[locale]/components/profile/[steamId]/stats/UserProfile'
|
||||
import StatsView from '@/app/[locale]/components/profile/[steamId]/stats/StatsView'
|
||||
import { MatchStats } from '@/types/match'
|
||||
|
||||
async function getStats(steamId: string) {
|
||||
const res = await fetch(`http://localhost:3000/api/stats/${steamId}`, {
|
||||
cache: 'no-store',
|
||||
})
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'}/api/stats/${steamId}`, { cache: 'no-store' })
|
||||
if (!res.ok) return null
|
||||
return res.json()
|
||||
}
|
||||
@ -14,6 +11,5 @@ async function getStats(steamId: string) {
|
||||
export default async function StatsPage({ params }: { params: { steamId: string } }) {
|
||||
const data = await getStats(params.steamId)
|
||||
if (!data) return <p>Keine Statistiken verfügbar.</p>
|
||||
|
||||
return <UserProfile stats={{ matches: data.stats as MatchStats[] }} />
|
||||
return <StatsView stats={{ matches: data.stats as MatchStats[] }} />
|
||||
}
|
||||
|
||||
@ -285,7 +285,7 @@ async function ensureVote(matchId: string) {
|
||||
if (match.mapVote) return { match, vote: match.mapVote }
|
||||
|
||||
// Neu anlegen
|
||||
const bestOf = match.bestOf ?? 3
|
||||
const bestOf = match.matchType === 'community' ? 3 : 1
|
||||
const mapPool = MAP_OPTIONS.filter(m => m.active).map(m => m.key)
|
||||
const opensAt = voteOpensAt({ matchDate: match.matchDate ?? null, demoDate: match.demoDate ?? null })
|
||||
|
||||
@ -456,7 +456,7 @@ async function exportMatchToSftpDirect(match: any, vote: any) {
|
||||
const SFTPClient = (await import('ssh2-sftp-client')).default
|
||||
const mLike: MatchLike = {
|
||||
id: match.id,
|
||||
bestOf: match.bestOf ?? vote.bestOf ?? 3,
|
||||
bestOf: (vote?.bestOf ?? (match.matchType === 'community' ? 3 : 1)),
|
||||
teamA: { name: match.teamA?.name ?? 'Team_1', players: match.teamAUsers ?? [] },
|
||||
teamB: { name: match.teamB?.name ?? 'Team_2', players: match.teamBUsers ?? [] },
|
||||
}
|
||||
@ -469,7 +469,7 @@ async function exportMatchToSftpDirect(match: any, vote: any) {
|
||||
}
|
||||
|
||||
if (!sLike.locked) return
|
||||
const bestOf = mLike.bestOf ?? sLike.bestOf ?? 3
|
||||
const bestOf = (mLike.bestOf ?? sLike.bestOf ?? (match.matchType === 'community' ? 3 : 1))
|
||||
const chosen = (sLike.steps ?? []).filter(s => (s.action === 'pick' || s.action === 'decider') && s.map)
|
||||
if (chosen.length < bestOf) return
|
||||
|
||||
|
||||
@ -42,21 +42,49 @@ export async function GET(_: Request, { params }: { params: { matchId: string }
|
||||
: buildDefaultPayload(m);
|
||||
|
||||
// ⬇️ Zusatz: opensAt (und leadMinutes) an die Antwort hängen
|
||||
const baseTs = (m.matchDate ?? m.demoDate)?.getTime?.() ?? null;
|
||||
const opensAt = m.mapVote?.opensAt ?? null;
|
||||
const baseTs = (m.matchDate ?? m.demoDate)?.getTime?.() ?? null
|
||||
const opensAt = m.mapVote?.opensAt ?? null
|
||||
const leadMinutes =
|
||||
opensAt && baseTs != null
|
||||
? Math.max(0, Math.round((baseTs - opensAt.getTime()) / 60000))
|
||||
: null;
|
||||
: null
|
||||
|
||||
// ⬇️ MapVote aus DB in ein Frontend-Shape bringen (mit echten map-Keys!)
|
||||
const mapVoteFromDb = m.mapVote
|
||||
? {
|
||||
locked : m.mapVote.locked,
|
||||
isOpen : !m.mapVote.locked && (opensAt ? Date.now() >= opensAt.getTime() : false),
|
||||
opensAt : opensAt,
|
||||
leadMinutes,
|
||||
// Großbuchstaben bleiben erhalten: 'BAN' | 'PICK' | 'DECIDER'
|
||||
steps : m.mapVote.steps
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map(s => ({
|
||||
order : s.order,
|
||||
action : s.action, // 'BAN'|'PICK'|'DECIDER'
|
||||
map : s.map ?? null, // <-- WICHTIG: Map-Key durchreichen!
|
||||
teamId : s.teamId,
|
||||
chosenAt: s.chosenAt ?? null,
|
||||
chosenBy: s.chosenBy ?? null,
|
||||
})),
|
||||
// optional: kompaktes Ergebnisfeld, falls dein FE das nutzt
|
||||
result: {
|
||||
maps: m.mapVote.steps
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.filter(s => (s.action === 'PICK' || s.action === 'DECIDER') && s.map)
|
||||
.map(s => s.map as string),
|
||||
},
|
||||
}
|
||||
: null
|
||||
|
||||
return NextResponse.json({
|
||||
...payload,
|
||||
// Payload-MapVote (vom Builder) mit DB-Werten überschreiben/mergen
|
||||
mapVote: {
|
||||
...(payload as any).mapVote,
|
||||
opensAt, // <- wichtig
|
||||
leadMinutes, // optional, aber nett zu haben
|
||||
...(mapVoteFromDb ?? {}),
|
||||
},
|
||||
}, { headers: { 'Cache-Control': 'no-store' } });
|
||||
}, { headers: { 'Cache-Control': 'no-store' } })
|
||||
} catch (err) {
|
||||
console.error(`GET /matches/${params.matchId} failed:`, err)
|
||||
return NextResponse.json({ error: 'Failed to load match' }, { status: 500 })
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -185,7 +185,6 @@ exports.Prisma.MatchScalarFieldEnum = {
|
||||
roundCount: 'roundCount',
|
||||
roundHistory: 'roundHistory',
|
||||
winnerTeam: 'winnerTeam',
|
||||
bestOf: 'bestOf',
|
||||
matchDate: 'matchDate',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
|
||||
176
src/generated/prisma/index.d.ts
vendored
176
src/generated/prisma/index.d.ts
vendored
@ -7629,7 +7629,6 @@ export namespace Prisma {
|
||||
scoreA: number | null
|
||||
scoreB: number | null
|
||||
roundCount: number | null
|
||||
bestOf: number | null
|
||||
cs2MatchId: number | null
|
||||
}
|
||||
|
||||
@ -7637,7 +7636,6 @@ export namespace Prisma {
|
||||
scoreA: number | null
|
||||
scoreB: number | null
|
||||
roundCount: number | null
|
||||
bestOf: number | null
|
||||
cs2MatchId: number | null
|
||||
}
|
||||
|
||||
@ -7655,7 +7653,6 @@ export namespace Prisma {
|
||||
demoDate: Date | null
|
||||
roundCount: number | null
|
||||
winnerTeam: string | null
|
||||
bestOf: number | null
|
||||
matchDate: Date | null
|
||||
createdAt: Date | null
|
||||
updatedAt: Date | null
|
||||
@ -7677,7 +7674,6 @@ export namespace Prisma {
|
||||
demoDate: Date | null
|
||||
roundCount: number | null
|
||||
winnerTeam: string | null
|
||||
bestOf: number | null
|
||||
matchDate: Date | null
|
||||
createdAt: Date | null
|
||||
updatedAt: Date | null
|
||||
@ -7701,7 +7697,6 @@ export namespace Prisma {
|
||||
roundCount: number
|
||||
roundHistory: number
|
||||
winnerTeam: number
|
||||
bestOf: number
|
||||
matchDate: number
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
@ -7715,7 +7710,6 @@ export namespace Prisma {
|
||||
scoreA?: true
|
||||
scoreB?: true
|
||||
roundCount?: true
|
||||
bestOf?: true
|
||||
cs2MatchId?: true
|
||||
}
|
||||
|
||||
@ -7723,7 +7717,6 @@ export namespace Prisma {
|
||||
scoreA?: true
|
||||
scoreB?: true
|
||||
roundCount?: true
|
||||
bestOf?: true
|
||||
cs2MatchId?: true
|
||||
}
|
||||
|
||||
@ -7741,7 +7734,6 @@ export namespace Prisma {
|
||||
demoDate?: true
|
||||
roundCount?: true
|
||||
winnerTeam?: true
|
||||
bestOf?: true
|
||||
matchDate?: true
|
||||
createdAt?: true
|
||||
updatedAt?: true
|
||||
@ -7763,7 +7755,6 @@ export namespace Prisma {
|
||||
demoDate?: true
|
||||
roundCount?: true
|
||||
winnerTeam?: true
|
||||
bestOf?: true
|
||||
matchDate?: true
|
||||
createdAt?: true
|
||||
updatedAt?: true
|
||||
@ -7787,7 +7778,6 @@ export namespace Prisma {
|
||||
roundCount?: true
|
||||
roundHistory?: true
|
||||
winnerTeam?: true
|
||||
bestOf?: true
|
||||
matchDate?: true
|
||||
createdAt?: true
|
||||
updatedAt?: true
|
||||
@ -7898,7 +7888,6 @@ export namespace Prisma {
|
||||
roundCount: number | null
|
||||
roundHistory: JsonValue | null
|
||||
winnerTeam: string | null
|
||||
bestOf: number
|
||||
matchDate: Date | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
@ -7941,7 +7930,6 @@ export namespace Prisma {
|
||||
roundCount?: boolean
|
||||
roundHistory?: boolean
|
||||
winnerTeam?: boolean
|
||||
bestOf?: boolean
|
||||
matchDate?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
@ -7976,7 +7964,6 @@ export namespace Prisma {
|
||||
roundCount?: boolean
|
||||
roundHistory?: boolean
|
||||
winnerTeam?: boolean
|
||||
bestOf?: boolean
|
||||
matchDate?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
@ -8002,7 +7989,6 @@ export namespace Prisma {
|
||||
roundCount?: boolean
|
||||
roundHistory?: boolean
|
||||
winnerTeam?: boolean
|
||||
bestOf?: boolean
|
||||
matchDate?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
@ -8028,7 +8014,6 @@ export namespace Prisma {
|
||||
roundCount?: boolean
|
||||
roundHistory?: boolean
|
||||
winnerTeam?: boolean
|
||||
bestOf?: boolean
|
||||
matchDate?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
@ -8036,7 +8021,7 @@ export namespace Prisma {
|
||||
exportedAt?: boolean
|
||||
}
|
||||
|
||||
export type MatchOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"id" | "title" | "matchType" | "map" | "description" | "scoreA" | "scoreB" | "teamAId" | "teamBId" | "filePath" | "demoDate" | "demoData" | "roundCount" | "roundHistory" | "winnerTeam" | "bestOf" | "matchDate" | "createdAt" | "updatedAt" | "cs2MatchId" | "exportedAt", ExtArgs["result"]["match"]>
|
||||
export type MatchOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"id" | "title" | "matchType" | "map" | "description" | "scoreA" | "scoreB" | "teamAId" | "teamBId" | "filePath" | "demoDate" | "demoData" | "roundCount" | "roundHistory" | "winnerTeam" | "matchDate" | "createdAt" | "updatedAt" | "cs2MatchId" | "exportedAt", ExtArgs["result"]["match"]>
|
||||
export type MatchInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
|
||||
teamA?: boolean | Match$teamAArgs<ExtArgs>
|
||||
teamB?: boolean | Match$teamBArgs<ExtArgs>
|
||||
@ -8089,7 +8074,6 @@ export namespace Prisma {
|
||||
roundCount: number | null
|
||||
roundHistory: Prisma.JsonValue | null
|
||||
winnerTeam: string | null
|
||||
bestOf: number
|
||||
matchDate: Date | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
@ -8543,7 +8527,6 @@ export namespace Prisma {
|
||||
readonly roundCount: FieldRef<"Match", 'Int'>
|
||||
readonly roundHistory: FieldRef<"Match", 'Json'>
|
||||
readonly winnerTeam: FieldRef<"Match", 'String'>
|
||||
readonly bestOf: FieldRef<"Match", 'Int'>
|
||||
readonly matchDate: FieldRef<"Match", 'DateTime'>
|
||||
readonly createdAt: FieldRef<"Match", 'DateTime'>
|
||||
readonly updatedAt: FieldRef<"Match", 'DateTime'>
|
||||
@ -21053,7 +21036,6 @@ export namespace Prisma {
|
||||
roundCount: 'roundCount',
|
||||
roundHistory: 'roundHistory',
|
||||
winnerTeam: 'winnerTeam',
|
||||
bestOf: 'bestOf',
|
||||
matchDate: 'matchDate',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
@ -21800,7 +21782,6 @@ export namespace Prisma {
|
||||
roundCount?: IntNullableFilter<"Match"> | number | null
|
||||
roundHistory?: JsonNullableFilter<"Match">
|
||||
winnerTeam?: StringNullableFilter<"Match"> | string | null
|
||||
bestOf?: IntFilter<"Match"> | number
|
||||
matchDate?: DateTimeNullableFilter<"Match"> | Date | string | null
|
||||
createdAt?: DateTimeFilter<"Match"> | Date | string
|
||||
updatedAt?: DateTimeFilter<"Match"> | Date | string
|
||||
@ -21834,7 +21815,6 @@ export namespace Prisma {
|
||||
roundCount?: SortOrderInput | SortOrder
|
||||
roundHistory?: SortOrderInput | SortOrder
|
||||
winnerTeam?: SortOrderInput | SortOrder
|
||||
bestOf?: SortOrder
|
||||
matchDate?: SortOrderInput | SortOrder
|
||||
createdAt?: SortOrder
|
||||
updatedAt?: SortOrder
|
||||
@ -21871,7 +21851,6 @@ export namespace Prisma {
|
||||
roundCount?: IntNullableFilter<"Match"> | number | null
|
||||
roundHistory?: JsonNullableFilter<"Match">
|
||||
winnerTeam?: StringNullableFilter<"Match"> | string | null
|
||||
bestOf?: IntFilter<"Match"> | number
|
||||
matchDate?: DateTimeNullableFilter<"Match"> | Date | string | null
|
||||
createdAt?: DateTimeFilter<"Match"> | Date | string
|
||||
updatedAt?: DateTimeFilter<"Match"> | Date | string
|
||||
@ -21905,7 +21884,6 @@ export namespace Prisma {
|
||||
roundCount?: SortOrderInput | SortOrder
|
||||
roundHistory?: SortOrderInput | SortOrder
|
||||
winnerTeam?: SortOrderInput | SortOrder
|
||||
bestOf?: SortOrder
|
||||
matchDate?: SortOrderInput | SortOrder
|
||||
createdAt?: SortOrder
|
||||
updatedAt?: SortOrder
|
||||
@ -21937,7 +21915,6 @@ export namespace Prisma {
|
||||
roundCount?: IntNullableWithAggregatesFilter<"Match"> | number | null
|
||||
roundHistory?: JsonNullableWithAggregatesFilter<"Match">
|
||||
winnerTeam?: StringNullableWithAggregatesFilter<"Match"> | string | null
|
||||
bestOf?: IntWithAggregatesFilter<"Match"> | number
|
||||
matchDate?: DateTimeNullableWithAggregatesFilter<"Match"> | Date | string | null
|
||||
createdAt?: DateTimeWithAggregatesFilter<"Match"> | Date | string
|
||||
updatedAt?: DateTimeWithAggregatesFilter<"Match"> | Date | string
|
||||
@ -23242,7 +23219,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -23276,7 +23252,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -23306,7 +23281,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -23340,7 +23314,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -23372,7 +23345,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -23394,7 +23366,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -23418,7 +23389,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -24825,17 +24795,6 @@ export namespace Prisma {
|
||||
not?: InputJsonValue | JsonFieldRefInput<$PrismaModel> | JsonNullValueFilter
|
||||
}
|
||||
|
||||
export type IntFilter<$PrismaModel = never> = {
|
||||
equals?: number | IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | IntFieldRefInput<$PrismaModel>
|
||||
not?: NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type DemoFileNullableScalarRelationFilter = {
|
||||
is?: DemoFileWhereInput | null
|
||||
isNot?: DemoFileWhereInput | null
|
||||
@ -24867,7 +24826,6 @@ export namespace Prisma {
|
||||
roundCount?: SortOrder
|
||||
roundHistory?: SortOrder
|
||||
winnerTeam?: SortOrder
|
||||
bestOf?: SortOrder
|
||||
matchDate?: SortOrder
|
||||
createdAt?: SortOrder
|
||||
updatedAt?: SortOrder
|
||||
@ -24879,7 +24837,6 @@ export namespace Prisma {
|
||||
scoreA?: SortOrder
|
||||
scoreB?: SortOrder
|
||||
roundCount?: SortOrder
|
||||
bestOf?: SortOrder
|
||||
cs2MatchId?: SortOrder
|
||||
}
|
||||
|
||||
@ -24897,7 +24854,6 @@ export namespace Prisma {
|
||||
demoDate?: SortOrder
|
||||
roundCount?: SortOrder
|
||||
winnerTeam?: SortOrder
|
||||
bestOf?: SortOrder
|
||||
matchDate?: SortOrder
|
||||
createdAt?: SortOrder
|
||||
updatedAt?: SortOrder
|
||||
@ -24919,7 +24875,6 @@ export namespace Prisma {
|
||||
demoDate?: SortOrder
|
||||
roundCount?: SortOrder
|
||||
winnerTeam?: SortOrder
|
||||
bestOf?: SortOrder
|
||||
matchDate?: SortOrder
|
||||
createdAt?: SortOrder
|
||||
updatedAt?: SortOrder
|
||||
@ -24931,7 +24886,6 @@ export namespace Prisma {
|
||||
scoreA?: SortOrder
|
||||
scoreB?: SortOrder
|
||||
roundCount?: SortOrder
|
||||
bestOf?: SortOrder
|
||||
cs2MatchId?: SortOrder
|
||||
}
|
||||
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
|
||||
@ -24961,22 +24915,6 @@ export namespace Prisma {
|
||||
_max?: NestedJsonNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | IntFieldRefInput<$PrismaModel>
|
||||
not?: NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: NestedIntFilter<$PrismaModel>
|
||||
_avg?: NestedFloatFilter<$PrismaModel>
|
||||
_sum?: NestedIntFilter<$PrismaModel>
|
||||
_min?: NestedIntFilter<$PrismaModel>
|
||||
_max?: NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type MatchScalarRelationFilter = {
|
||||
is?: MatchWhereInput
|
||||
isNot?: MatchWhereInput
|
||||
@ -25016,6 +24954,17 @@ export namespace Prisma {
|
||||
createdAt?: SortOrder
|
||||
}
|
||||
|
||||
export type IntFilter<$PrismaModel = never> = {
|
||||
equals?: number | IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | IntFieldRefInput<$PrismaModel>
|
||||
not?: NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type FloatFilter<$PrismaModel = never> = {
|
||||
equals?: number | FloatFieldRefInput<$PrismaModel>
|
||||
in?: number[] | ListFloatFieldRefInput<$PrismaModel>
|
||||
@ -25201,6 +25150,22 @@ export namespace Prisma {
|
||||
winCount?: SortOrder
|
||||
}
|
||||
|
||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | IntFieldRefInput<$PrismaModel>
|
||||
not?: NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: NestedIntFilter<$PrismaModel>
|
||||
_avg?: NestedFloatFilter<$PrismaModel>
|
||||
_sum?: NestedIntFilter<$PrismaModel>
|
||||
_min?: NestedIntFilter<$PrismaModel>
|
||||
_max?: NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type FloatWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | FloatFieldRefInput<$PrismaModel>
|
||||
in?: number[] | ListFloatFieldRefInput<$PrismaModel>
|
||||
@ -26717,14 +26682,6 @@ export namespace Prisma {
|
||||
connect?: MatchReadyWhereUniqueInput | MatchReadyWhereUniqueInput[]
|
||||
}
|
||||
|
||||
export type IntFieldUpdateOperationsInput = {
|
||||
set?: number
|
||||
increment?: number
|
||||
decrement?: number
|
||||
multiply?: number
|
||||
divide?: number
|
||||
}
|
||||
|
||||
export type TeamUpdateOneWithoutMatchesAsTeamANestedInput = {
|
||||
create?: XOR<TeamCreateWithoutMatchesAsTeamAInput, TeamUncheckedCreateWithoutMatchesAsTeamAInput>
|
||||
connectOrCreate?: TeamCreateOrConnectWithoutMatchesAsTeamAInput
|
||||
@ -27023,6 +26980,14 @@ export namespace Prisma {
|
||||
connect?: MatchPlayerWhereUniqueInput
|
||||
}
|
||||
|
||||
export type IntFieldUpdateOperationsInput = {
|
||||
set?: number
|
||||
increment?: number
|
||||
decrement?: number
|
||||
multiply?: number
|
||||
divide?: number
|
||||
}
|
||||
|
||||
export type FloatFieldUpdateOperationsInput = {
|
||||
set?: number
|
||||
increment?: number
|
||||
@ -27558,6 +27523,17 @@ export namespace Prisma {
|
||||
not?: InputJsonValue | JsonFieldRefInput<$PrismaModel> | JsonNullValueFilter
|
||||
}
|
||||
|
||||
export type NestedFloatFilter<$PrismaModel = never> = {
|
||||
equals?: number | FloatFieldRefInput<$PrismaModel>
|
||||
in?: number[] | ListFloatFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | ListFloatFieldRefInput<$PrismaModel>
|
||||
lt?: number | FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | FloatFieldRefInput<$PrismaModel>
|
||||
not?: NestedFloatFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | ListIntFieldRefInput<$PrismaModel>
|
||||
@ -27574,17 +27550,6 @@ export namespace Prisma {
|
||||
_max?: NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedFloatFilter<$PrismaModel = never> = {
|
||||
equals?: number | FloatFieldRefInput<$PrismaModel>
|
||||
in?: number[] | ListFloatFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | ListFloatFieldRefInput<$PrismaModel>
|
||||
lt?: number | FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | FloatFieldRefInput<$PrismaModel>
|
||||
not?: NestedFloatFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedFloatWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | FloatFieldRefInput<$PrismaModel>
|
||||
in?: number[] | ListFloatFieldRefInput<$PrismaModel>
|
||||
@ -27754,7 +27719,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -27787,7 +27751,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -27821,7 +27784,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -27854,7 +27816,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -28299,7 +28260,6 @@ export namespace Prisma {
|
||||
roundCount?: IntNullableFilter<"Match"> | number | null
|
||||
roundHistory?: JsonNullableFilter<"Match">
|
||||
winnerTeam?: StringNullableFilter<"Match"> | string | null
|
||||
bestOf?: IntFilter<"Match"> | number
|
||||
matchDate?: DateTimeNullableFilter<"Match"> | Date | string | null
|
||||
createdAt?: DateTimeFilter<"Match"> | Date | string
|
||||
updatedAt?: DateTimeFilter<"Match"> | Date | string
|
||||
@ -28806,7 +28766,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -28838,7 +28797,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -28878,7 +28836,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -28910,7 +28867,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -30348,7 +30304,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -30381,7 +30336,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -30607,7 +30561,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -30640,7 +30593,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -30932,7 +30884,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -30965,7 +30916,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -31083,7 +31033,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -31116,7 +31065,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -31357,7 +31305,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -31390,7 +31337,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -31671,7 +31617,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -31704,7 +31649,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -31733,7 +31677,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -31766,7 +31709,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -31878,7 +31820,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -31911,7 +31852,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -32153,7 +32093,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -32186,7 +32125,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -32261,7 +32199,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -32294,7 +32231,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -32639,7 +32575,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -32672,7 +32607,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -32784,7 +32718,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -32817,7 +32750,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -33018,7 +32950,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -33051,7 +32982,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -33082,7 +33012,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -33104,7 +33033,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -33137,7 +33065,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -33168,7 +33095,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -33522,7 +33448,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -33545,7 +33470,6 @@ export namespace Prisma {
|
||||
roundCount?: number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: string | null
|
||||
bestOf?: number
|
||||
matchDate?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
@ -33731,7 +33655,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -33763,7 +33686,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -33794,7 +33716,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -33816,7 +33737,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -33848,7 +33768,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -33879,7 +33798,6 @@ export namespace Prisma {
|
||||
roundCount?: NullableIntFieldUpdateOperationsInput | number | null
|
||||
roundHistory?: NullableJsonNullValueInput | InputJsonValue
|
||||
winnerTeam?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
bestOf?: IntFieldUpdateOperationsInput | number
|
||||
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "prisma-client-fc3f88586732483ac25ea246b89655ccf69ac35e1c6d1e7c4e4311101fe5713a",
|
||||
"name": "prisma-client-f83ffd6f15d5d09bef7e96b9dc3d4dfe3004d8583cd7a36804111c84705fa416",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "default.js",
|
||||
|
||||
@ -154,7 +154,6 @@ model Match {
|
||||
roundHistory Json?
|
||||
winnerTeam String?
|
||||
|
||||
bestOf Int @default(3) // 1 | 3 | 5 – app-seitig validieren
|
||||
matchDate DateTime? // geplante Startzeit (separat von demoDate)
|
||||
mapVote MapVote?
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
34
src/hooks/useUserTimeZone.ts
Normal file
34
src/hooks/useUserTimeZone.ts
Normal file
@ -0,0 +1,34 @@
|
||||
// /src/hooks/useUserTimeZone.ts
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function useUserTimeZone(deps: any[] = []) {
|
||||
const [tz, setTz] = useState<string>('Europe/Berlin')
|
||||
|
||||
useEffect(() => {
|
||||
const ctrl = new AbortController()
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/user/timezone', {
|
||||
credentials: 'include',
|
||||
cache: 'no-store',
|
||||
signal: ctrl.signal,
|
||||
})
|
||||
const json = res.ok ? await res.json().catch(() => ({})) : {}
|
||||
const fromDb = typeof json?.timeZone === 'string' ? json.timeZone : null
|
||||
const fallback = Intl.DateTimeFormat().resolvedOptions().timeZone || 'Europe/Berlin'
|
||||
setTz(fromDb ?? fallback)
|
||||
} catch {
|
||||
const fallback = Intl.DateTimeFormat().resolvedOptions().timeZone || 'Europe/Berlin'
|
||||
setTz(fallback)
|
||||
}
|
||||
})()
|
||||
|
||||
return () => ctrl.abort()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps)
|
||||
|
||||
return tz
|
||||
}
|
||||
@ -5,20 +5,9 @@ import Steam from 'next-auth-steam'
|
||||
import { prisma } from './prisma'
|
||||
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 {
|
||||
@ -38,7 +27,7 @@ function guessTzFromCountry(cc?: string | null): string | null {
|
||||
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
|
||||
if (C === 'US') return null
|
||||
return null
|
||||
}
|
||||
|
||||
@ -55,19 +44,15 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
|
||||
const steamProfile = profile as SteamProfile
|
||||
const location = steamProfile.loccountrycode ?? null
|
||||
|
||||
// Gibt es den User schon?
|
||||
const existing = await prisma.user.findUnique({
|
||||
where: { steamId: steamProfile.steamid },
|
||||
select: { timeZone: true },
|
||||
})
|
||||
|
||||
// Cookie lesen & validieren
|
||||
const tzCookie = readCookie(req, 'tz')
|
||||
const tzFromCookie = isValidIanaTz(tzCookie) ? tzCookie : null
|
||||
|
||||
// Fallback fürs erstmalige Anlegen
|
||||
const guessedTz = tzFromCookie ?? guessTzFromCountry(location)
|
||||
|
||||
// Falls neu: anlegen inkl. geschätzter TZ
|
||||
if (!existing) {
|
||||
const guessedTz = guessTzFromCountry(location)
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
steamId: steamProfile.steamid,
|
||||
@ -75,19 +60,21 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
|
||||
avatar: steamProfile.avatarfull,
|
||||
location: location ?? undefined,
|
||||
isAdmin: false,
|
||||
timeZone: guessedTz ?? null,
|
||||
timeZone: guessedTz, // kann null sein
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Beim Login Stammdaten aktualisieren,
|
||||
// und NUR falls timeZone noch NULL ist, ggf. schätzen.
|
||||
const guessedTz = existing.timeZone ?? guessTzFromCountry(location)
|
||||
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 }
|
||||
...(existing.timeZone == null && guessedTz
|
||||
? { timeZone: guessedTz }
|
||||
: {}),
|
||||
},
|
||||
})
|
||||
@ -110,12 +97,12 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
|
||||
userInDb.steamId === '76561198000414190'
|
||||
? true
|
||||
: 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
|
||||
// ➜ einzig maßgeblich: DB-TimeZone
|
||||
token.timeZone = userInDb.timeZone ?? undefined
|
||||
} else {
|
||||
token.timeZone = undefined
|
||||
}
|
||||
|
||||
return token
|
||||
},
|
||||
@ -130,9 +117,10 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
|
||||
image: token.image,
|
||||
team: token.team ?? null,
|
||||
isAdmin: token.isAdmin ?? false,
|
||||
// ➜ TZ in die Session durchreichen
|
||||
timeZone: token.timeZone,
|
||||
}
|
||||
// ➜ für UI verfügbar
|
||||
timeZone: (token as any).timeZone ?? null,
|
||||
} as typeof session.user & { steamId: string; team: string | null; isAdmin: boolean; timeZone: string | null }
|
||||
|
||||
return session
|
||||
},
|
||||
|
||||
|
||||
3
src/types/next-auth.d.ts
vendored
3
src/types/next-auth.d.ts
vendored
@ -7,7 +7,6 @@ declare module 'next-auth' {
|
||||
steamId?: string
|
||||
isAdmin?: boolean
|
||||
team?: string | null
|
||||
timeZone?: string
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +14,6 @@ declare module 'next-auth' {
|
||||
steamId: string
|
||||
isAdmin: boolean
|
||||
team?: string | null
|
||||
timeZone?: string
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +24,5 @@ declare module 'next-auth/jwt' {
|
||||
team?: string | null
|
||||
name?: string
|
||||
image?: string
|
||||
timeZone?: string
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user