Files
aurorabot/panel/src/pages/Settings.tsx
syntaxbullet 2381f073ba
Some checks failed
Deploy to Production / test (push) Failing after 37s
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>
2026-02-13 20:27:14 +01:00

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>
);
}