Some checks failed
Deploy to Production / test (push) Failing after 37s
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>
95 lines
2.7 KiB
TypeScript
95 lines
2.7 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { get, post } from "../lib/api";
|
|
|
|
export default function Settings() {
|
|
const [settings, setSettings] = useState<Record<string, unknown> | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [raw, setRaw] = useState("");
|
|
const [parseError, setParseError] = useState("");
|
|
|
|
useEffect(() => {
|
|
get<Record<string, unknown>>("/api/settings")
|
|
.then((data) => {
|
|
setSettings(data);
|
|
setRaw(JSON.stringify(data, null, 2));
|
|
})
|
|
.catch(() => {})
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
const handleRawChange = (value: string) => {
|
|
setRaw(value);
|
|
try {
|
|
JSON.parse(value);
|
|
setParseError("");
|
|
} catch (e) {
|
|
setParseError(e instanceof Error ? e.message : "Invalid JSON");
|
|
}
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (parseError) return;
|
|
setSaving(true);
|
|
try {
|
|
const parsed = JSON.parse(raw);
|
|
const updated = await post<Record<string, unknown>>("/api/settings", parsed);
|
|
setSettings(updated);
|
|
setRaw(JSON.stringify(updated, null, 2));
|
|
} catch (e) {
|
|
alert(typeof e === "object" && e && "error" in e ? (e as { error: string }).error : "Failed to save");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex justify-center p-12">
|
|
<span className="loading loading-spinner loading-lg" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h1 className="text-2xl font-bold">Settings</h1>
|
|
<button
|
|
className="btn btn-primary btn-sm"
|
|
onClick={handleSave}
|
|
disabled={saving || !!parseError}
|
|
>
|
|
{saving ? <span className="loading loading-spinner loading-sm" /> : "Save"}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="text-sm text-base-content/60 mb-2">
|
|
Edit game configuration directly. Changes are merged with existing settings.
|
|
</div>
|
|
|
|
{parseError && (
|
|
<div className="alert alert-error mb-3 py-2 text-sm">{parseError}</div>
|
|
)}
|
|
|
|
<textarea
|
|
className="textarea textarea-bordered w-full font-mono text-sm"
|
|
rows={30}
|
|
value={raw}
|
|
onChange={(e) => handleRawChange(e.target.value)}
|
|
/>
|
|
|
|
{settings && (
|
|
<div className="mt-4">
|
|
<h3 className="text-sm font-semibold mb-2">Quick Reference — Top-level keys:</h3>
|
|
<div className="flex flex-wrap gap-1">
|
|
{Object.keys(settings).map((key) => (
|
|
<span key={key} className="badge badge-sm badge-outline">{key}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|