Files
vereinskalender/components/AdminSystemSettings.tsx
2026-01-18 00:40:01 +01:00

279 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState } from "react";
export default function AdminSystemSettings() {
const [apiKey, setApiKey] = useState("");
const [provider, setProvider] = useState("osm");
const [registrationEnabled, setRegistrationEnabled] = useState(true);
const [publicAccessEnabled, setPublicAccessEnabled] = useState(true);
const [emailVerificationRequired, setEmailVerificationRequired] = useState(true);
const [appName, setAppName] = useState("Vereinskalender");
const [logoFile, setLogoFile] = useState<File | null>(null);
const [logoVersion, setLogoVersion] = useState(() => Date.now());
const [hasLogo, setHasLogo] = useState<boolean | null>(null);
const [status, setStatus] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const load = async () => {
try {
const [placesResponse, appNameResponse] = await Promise.all([
fetch("/api/settings/system"),
fetch("/api/settings/app-name")
]);
if (!placesResponse.ok) {
throw new Error("Einstellungen konnten nicht geladen werden.");
}
const payload = await placesResponse.json();
setApiKey(payload.apiKey || "");
setProvider(payload.provider || "osm");
setRegistrationEnabled(payload.registrationEnabled !== false);
setPublicAccessEnabled(payload.publicAccessEnabled !== false);
setEmailVerificationRequired(payload.emailVerificationRequired !== false);
if (appNameResponse.ok) {
const appPayload = await appNameResponse.json();
setAppName(appPayload.name || "Vereinskalender");
}
} catch (err) {
setError((err as Error).message);
}
};
const loadLogoStatus = async () => {
try {
const response = await fetch("/api/branding/logo", {
method: "HEAD",
cache: "no-store"
});
setHasLogo(response.ok);
} catch {
setHasLogo(false);
}
};
useEffect(() => {
load();
loadLogoStatus();
}, []);
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setStatus(null);
setError(null);
const [settingsResponse, appNameResponse] = await Promise.all([
fetch("/api/settings/system", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
apiKey,
provider,
registrationEnabled,
publicAccessEnabled,
emailVerificationRequired
})
}),
fetch("/api/settings/app-name", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: appName })
})
]);
if (!settingsResponse.ok) {
const data = await settingsResponse.json();
setError(data.error || "Speichern fehlgeschlagen.");
return;
}
if (!appNameResponse.ok) {
const data = await appNameResponse.json();
setError(data.error || "App-Name konnte nicht gespeichert werden.");
return;
}
setStatus("Gespeichert.");
};
const onLogoUpload = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setStatus(null);
setError(null);
if (!logoFile) {
setError("Bitte ein Logo auswählen.");
return;
}
const formData = new FormData();
formData.append("file", logoFile);
const response = await fetch("/api/settings/logo", {
method: "POST",
body: formData
});
if (!response.ok) {
const data = await response.json();
setError(data.error || "Logo-Upload fehlgeschlagen.");
return;
}
setLogoFile(null);
setLogoVersion(Date.now());
setHasLogo(true);
setStatus("Logo gespeichert.");
};
const onLogoRemove = async () => {
setStatus(null);
setError(null);
const response = await fetch("/api/settings/logo", { method: "DELETE" });
if (!response.ok) {
const data = await response.json();
setError(data.error || "Logo konnte nicht gelöscht werden.");
return;
}
setHasLogo(false);
setLogoVersion(Date.now());
setStatus("Logo entfernt.");
};
return (
<section className="card space-y-4">
<div>
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">System</p>
<h1 className="text-2xl font-semibold">API Einstellungen</h1>
</div>
<form onSubmit={onLogoUpload} className="space-y-3">
<div>
<p className="text-sm font-medium text-slate-700">App-Logo</p>
<p className="text-xs text-slate-500">
PNG, JPG, WEBP oder SVG. Empfohlen: 240×64 px.
</p>
</div>
{hasLogo ? (
<div className="flex items-center gap-3">
<img
src={`/api/branding/logo?ts=${logoVersion}`}
alt="Aktuelles Logo"
className="h-10 max-w-[180px] rounded-md border border-slate-200 bg-white object-contain px-2"
onError={() => setHasLogo(false)}
/>
<button
type="button"
onClick={onLogoRemove}
className="btn-ghost"
>
Logo entfernen
</button>
</div>
) : (
<p className="text-sm text-slate-500">Kein Logo hinterlegt.</p>
)}
<div className="flex flex-wrap items-center gap-3">
<label className="btn-ghost cursor-pointer">
Datei auswählen
<input
type="file"
accept="image/png,image/jpeg,image/webp,image/svg+xml"
onChange={(event) =>
setLogoFile(event.currentTarget.files?.[0] || null)
}
className="sr-only"
/>
</label>
<span className="text-sm text-slate-600">
{logoFile ? logoFile.name : "Keine Datei ausgewählt"}
</span>
<button type="submit" className="btn-accent">
Logo hochladen
</button>
</div>
</form>
<form onSubmit={onSubmit} className="space-y-3">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700">
App-Name
</label>
<input
type="text"
value={appName}
onChange={(event) => setAppName(event.target.value)}
className="w-full rounded-xl border border-slate-300 px-3 py-2"
placeholder="Vereinskalender"
required
maxLength={60}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700">
Ortsanbieter
</label>
<select
value={provider}
onChange={(event) => setProvider(event.target.value)}
className="w-full rounded-xl border border-slate-300 px-3 py-2"
>
<option value="osm">OpenStreetMap (Nominatim)</option>
<option value="google">Google Places</option>
</select>
</div>
{provider === "google" && (
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700">
Google Places API Key
</label>
<input
type="password"
value={apiKey}
onChange={(event) => setApiKey(event.target.value)}
className="w-full rounded-xl border border-slate-300 px-3 py-2"
placeholder="AIza..."
required
/>
</div>
)}
<div className="rounded-xl border border-slate-200 bg-slate-50/60 p-3 text-sm text-slate-700">
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">
Zugriff
</p>
<div className="mt-3 space-y-3">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={publicAccessEnabled}
onChange={(event) => setPublicAccessEnabled(event.target.checked)}
/>
Öffentlicher Zugriff erlauben
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={registrationEnabled}
onChange={(event) => setRegistrationEnabled(event.target.checked)}
/>
Registrierung erlauben
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={emailVerificationRequired}
onChange={(event) =>
setEmailVerificationRequired(event.target.checked)
}
/>
E-Mail-Verifizierung erforderlich
</label>
</div>
</div>
<button type="submit" className="btn-accent">
Speichern
</button>
</form>
{status && <p className="text-sm text-emerald-600">{status}</p>}
{error && <p className="text-sm text-red-600">{error}</p>}
</section>
);
}