ironie-nextjs/src/app/radar/TeamSidebar.tsx
2025-09-13 15:49:05 +02:00

168 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /src/app/radar/TeamSidebar.tsx
'use client'
import React, { useEffect, useState } from 'react'
import { useAvatarDirectoryStore } from '@/app/lib/useAvatarDirectoryStore'
export type Team = 'T' | 'CT'
export type SidebarPlayer = {
id: string // <- SteamID
name?: string | null
hp?: number | null
armor?: number | null
helmet?: boolean | null
defuse?: boolean | null
hasBomb?: boolean | null
alive?: boolean | null
}
export default function TeamSidebar({
team,
teamId,
players,
align = 'left',
onHoverPlayer,
}: {
team: Team
teamId?: string
players: SidebarPlayer[]
align?: 'left' | 'right'
onHoverPlayer?: (id: string | null) => void
}) {
// ---- NEU: Team-Info (Logo) laden ----
const [teamLogo, setTeamLogo] = useState<string | null>(null)
const [teamApiName, setTeamApiName] = useState<string | null>(null)
const BOT_ICON = '/assets/img/icons/ui/bot.svg'
const isBotId = (id: string) => id?.toUpperCase().startsWith('BOT:')
useEffect(() => {
let abort = false
async function loadTeam() {
if (!teamId) { setTeamLogo(null); setTeamApiName(null); return }
try {
const res = await fetch(`/api/team/${teamId}`, { cache: 'no-store' })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
if (!abort) {
setTeamLogo(data?.logo || null)
setTeamApiName(data?.name || null)
}
} catch {
if (!abort) { setTeamLogo(null); setTeamApiName(null) }
}
}
loadTeam()
return () => { abort = true }
}, [teamId])
// ---- Rest wie gehabt ----
const ensureTeamsLoaded = useAvatarDirectoryStore(s => s.ensureTeamsLoaded)
const avatarById = useAvatarDirectoryStore(s => s.byId)
const avatarVer = useAvatarDirectoryStore(s => s.version)
useEffect(() => {
if (teamId) ensureTeamsLoaded([teamId])
}, [teamId, ensureTeamsLoaded])
const defaultTeamName = team === 'CT' ? 'Counter-Terrorists' : 'Terrorists'
const teamName = teamApiName || defaultTeamName
const teamColor = team === 'CT' ? 'text-blue-400' : 'text-amber-400'
const barArmor = team === 'CT' ? 'bg-blue-500' : 'bg-amber-500'
const ringColor = team === 'CT' ? 'ring-blue-500' : 'ring-amber-500'
const isRight = align === 'right'
// Fallback-Icon, falls API kein Logo liefert:
const fallbackLogo = '/assets/img/logos/cs2.webp';
const logoSrc = teamLogo || fallbackLogo
const aliveCount = players.filter(p => p.alive !== false && (p.hp ?? 1) > 0).length
const sorted = [...players].sort((a, b) => {
const al = (b.alive ? 1 : 0) - (a.alive ? 1 : 0)
if (al !== 0) return al
const hp = (b.hp ?? -1) - (a.hp ?? -1)
if (hp !== 0) return hp
return (a.name ?? '').localeCompare(b.name ?? '')
})
return (
<aside className="h-full min-h-0 flex flex-col rounded-md border border-neutral-700/60 bg-neutral-900/30 p-2 overflow-hidden">
{/* Header mit Logo + Name */}
<div className="flex items-center justify-between text-xs uppercase tracking-wide opacity-80">
<span className={`font-semibold flex items-center gap-2 ${teamColor}`}>
<img
src={logoSrc}
alt={teamName}
className="w-4 h-4 object-contain"
/>
{teamName}
</span>
<span className="tabular-nums">{aliveCount}/{players.length}</span>
</div>
{/* ... Rest der Komponente bleibt unverändert ... */}
<div className="mt-2 flex-1 overflow-auto space-y-3 pr-1">
{sorted.map(p => {
void avatarVer
const hp = clamp(p.alive === false ? 0 : p.hp ?? 100, 0, 100)
const armor = clamp(p.armor ?? 0, 0, 100)
const dead = p.alive === false || hp <= 0
const entry = avatarById[p.id] as any
const avatarUrl =
isBotId(p.id) // <- Bot? dann Bot-Icon
? BOT_ICON
: (entry && !entry?.notFound && entry?.avatar
? entry.avatar
: '/assets/img/avatars/default_steam_avatar.jpg')
const rowDir = isRight ? 'flex-row-reverse text-right' : 'flex-row'
const stackAlg = isRight ? 'items-end' : 'items-start'
return (
<div
key={`player-${p.id}`}
id={`player-${p.id}`}
onMouseEnter={() => onHoverPlayer?.(p.id)}
onMouseLeave={() => onHoverPlayer?.(null)}
className={`rounded-md px-2 py-2 transition cursor-pointer
bg-neutral-800/40 hover:bg-neutral-700/40
hover:ring-2 hover:ring-white/20
${dead ? 'opacity-60' : ''}`}
>
<div className={`flex ${rowDir} items-center gap-3`}>
<img
src={avatarUrl}
alt={p.name || p.id}
className={`w-12 h-12 rounded-full border border-white/10 ring-2 ${ringColor} bg-neutral-900 object-contain p-1`}
width={48}
height={48}
loading="lazy"
/>
<div className={`flex-1 min-w-0 flex flex-col ${stackAlg}`}>
<div className={`flex ${isRight ? 'flex-row-reverse' : ''} items-center gap-2 w-full`}>
<span className="truncate font-medium">{p.name || p.id}</span>
{p.hasBomb && team === 'T' && <span title="Bomb" className="text-red-400">💣</span>}
{p.helmet && <span title="Helmet" className="opacity-80">🪖</span>}
{p.defuse && team === 'CT' && <span title="Defuse Kit" className="opacity-80">🗝</span>}
<span className={`${isRight ? 'mr-auto' : 'ml-auto'} text-xs tabular-nums`}>{hp}</span>
</div>
<div className="mt-1 w-full">
<div className="h-2.5 rounded bg-neutral-700/60 overflow-hidden">
<div className="h-full bg-green-500" style={{ width: `${hp}%` }} />
</div>
<div className="mt-1 h-1.5 rounded bg-neutral-700/60 overflow-hidden">
<div className={`h-full ${barArmor}`} style={{ width: `${armor}%` }} />
</div>
</div>
</div>
</div>
</div>
)
})}
</div>
</aside>
)
}
function clamp(n: number, a: number, b: number) {
return Math.max(a, Math.min(b, n))
}