feat: add admin panel with Discord OAuth and dashboard

Adds a React admin panel (panel/) with Discord OAuth2 login,
live dashboard via WebSocket, and settings/management pages.
Includes Docker build support, Vite proxy config for dev,
game_settings migration, and open-redirect protection on auth callback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
syntaxbullet
2026-02-13 20:27:14 +01:00
parent 121c242168
commit 2381f073ba
30 changed files with 3626 additions and 11 deletions

View File

@@ -0,0 +1,73 @@
import type { ReactNode } from "react";
export interface Column<T> {
key: string;
header: string;
render?: (row: T) => ReactNode;
className?: string;
}
interface DataTableProps<T> {
columns: Column<T>[];
data: T[];
keyField: string;
loading?: boolean;
onRowClick?: (row: T) => void;
emptyMessage?: string;
}
export default function DataTable<T extends Record<string, unknown>>({
columns,
data,
keyField,
loading,
onRowClick,
emptyMessage = "No data found",
}: DataTableProps<T>) {
if (loading) {
return (
<div className="flex justify-center p-8">
<span className="loading loading-spinner loading-lg" />
</div>
);
}
return (
<div className="overflow-x-auto">
<table className="table table-zebra w-full">
<thead>
<tr>
{columns.map((col) => (
<th key={col.key} className={col.className}>
{col.header}
</th>
))}
</tr>
</thead>
<tbody>
{data.length === 0 ? (
<tr>
<td colSpan={columns.length} className="text-center py-8 text-base-content/50">
{emptyMessage}
</td>
</tr>
) : (
data.map((row) => (
<tr
key={String(row[keyField])}
className={onRowClick ? "cursor-pointer hover" : ""}
onClick={() => onRowClick?.(row)}
>
{columns.map((col) => (
<td key={col.key} className={col.className}>
{col.render ? col.render(row) : String(row[col.key] ?? "")}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
);
}