import { useMemo, useState } from 'react'; import type { CurrentUser, Role } from '@reforger-panel/shared'; import { ROLES, ROLE_LABELS } from '@reforger-panel/shared'; import { useActivity, useKillfeed, useKnownPlayers, useLogHealth, usePlayers, usePrimaryServer, useSetUserRole, useUsers, } from '../api/hooks.js'; import { formatDateTime, formatDuration, formatRelativeTime } from '../lib/format.js'; import { Badge, Card, EmptyState, PageHeader, RoleBadge, SearchInput, Spinner, } from '../components/ui.js'; import { ActivityList, CurrentPlayersCard } from '../components/widgets.js'; import { InvitesCard } from '../components/invites-card.js'; /* ---------------------------------------------------------------- players */ export function PlayersPage() { const server = usePrimaryServer(); if (!server) return ; return ; } type PlayerSort = 'online' | 'last_seen' | 'playtime' | 'sessions' | 'name'; function PlayersBody({ slug }: { slug: string }) { const { data: online } = usePlayers(slug); const { data: known } = useKnownPlayers(slug); const [sort, setSort] = useState('online'); const [query, setQuery] = useState(''); const sortedPlayers = useMemo(() => { const players = (known?.players ?? []).filter((player) => query ? player.displayName.toLowerCase().includes(query.toLowerCase()) : true, ); return [...players].sort((a, b) => { if (sort === 'online') { if (a.online !== b.online) return a.online ? -1 : 1; return b.lastSeenAt.localeCompare(a.lastSeenAt); } if (sort === 'last_seen') return b.lastSeenAt.localeCompare(a.lastSeenAt); if (sort === 'playtime') return b.totalPlaytimeSeconds - a.totalPlaytimeSeconds; if (sort === 'sessions') return b.totalSessions - a.totalSessions; return a.displayName.localeCompare(b.displayName); }); }, [known?.players, sort, query]); return (
} > {!known ? ( ) : sortedPlayers.length === 0 ? ( ) : (
{sortedPlayers.map((player) => ( ))}
Player Identity Last seen Sessions Playtime
{player.displayName} {player.online && online} {player.externalPlayerId ? ( `${player.externalPlayerId.slice(0, 12)}…` ) : ( name only )} {formatRelativeTime(player.lastSeenAt)} {player.totalSessions} {formatDuration(player.totalPlaytimeSeconds)}
)} ); } /* --------------------------------------------------------------- killfeed */ export function KillfeedPage() { const server = usePrimaryServer(); if (!server) return ; return ; } function teamClass(team: string | null): string { const normalized = team?.toLowerCase() ?? ''; if (normalized.includes('blue') || normalized.includes('blufor')) return 'bg-info-400'; if (normalized.includes('opfor') || normalized.includes('red')) return 'bg-danger-400'; if (normalized.includes('independent') || normalized.includes('green')) return 'bg-ok-400'; return 'bg-slate-faint'; } function positionLabel(position: { x: number; y: number; z?: number | null } | null): string { if (!position) return 'position unknown'; const z = typeof position.z === 'number' ? `, ${position.z.toFixed(0)}` : ''; return `${position.x.toFixed(0)}, ${position.y.toFixed(0)}${z}`; } function KillfeedBody({ slug }: { slug: string }) { const { data, isLoading } = useKillfeed(slug, 150); return (
{isLoading || !data ? ( ) : data.events.length === 0 ? ( ) : (
    {data.events.map((event) => (
  • {event.killerName} killed {event.victimName} {event.friendly && friendly}
    {formatDateTime(event.occurredAt)} attacker {positionLabel(event.killerPosition)} victim {positionLabel(event.victimPosition)} distance{' '} {event.distanceMeters !== null ? `${event.distanceMeters.toFixed(0)} m` : '—'} weapon {event.weapon ?? '—'}
  • ))}
)}
); } /* --------------------------------------------------------------- activity */ export function ActivityPage() { const server = usePrimaryServer(); if (!server) return ; return ; } function ActivityBody({ slug }: { slug: string }) { const { data } = useActivity(slug, 100); return (
{data ? : }
); } /* --------------------------------------------------------------- settings */ export function SettingsPage({ user }: { user: CurrentUser }) { const isOwner = user.role === 'owner'; const server = usePrimaryServer(); const { data: users } = useUsers(isOwner); const { data: logs } = useLogHealth(server?.slug ?? '', isOwner && server !== undefined); const setRole = useSetUserRole(); return (
{user.avatarUrl ? ( ) : ( {(user.displayName ?? user.username).slice(0, 1).toUpperCase()} )}

{user.displayName ?? user.username}{' '} ({user.username})

{isOwner && ( {!users ? ( ) : (
    {users.users.map((panelUser) => (
  • {panelUser.avatarUrl ? ( ) : ( )}

    {panelUser.displayName ?? panelUser.username}

    joined {formatDateTime(panelUser.createdAt)}

    {panelUser.id === user.id ? ( ) : ( )}
  • ))}
)}
)} {isOwner && } {isOwner && (
Pterodactyl
{logs?.configured ? ( configured ) : ( mock / not configured )}
Game log path
{logs?.logPath ?? '—'}

Connection settings are managed through environment variables. Workshop metadata is fetched on demand and cached in the API process — there is no background polling.

)}
); }