168 lines
6.4 KiB
TypeScript
168 lines
6.4 KiB
TypeScript
// /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))
|
||
}
|