Aktueller Stand

This commit is contained in:
2026-01-15 16:24:09 +01:00
parent 5d2630a02f
commit 46eae2a2a9
70 changed files with 7866 additions and 447 deletions

View File

@@ -0,0 +1,202 @@
"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 [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 response = await fetch("/api/settings/google-places");
if (!response.ok) {
throw new Error("Einstellungen konnten nicht geladen werden.");
}
const payload = await response.json();
setApiKey(payload.apiKey || "");
setProvider(payload.provider || "osm");
setRegistrationEnabled(payload.registrationEnabled !== false);
} 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 response = await fetch("/api/settings/google-places", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ apiKey, provider, registrationEnabled })
});
if (!response.ok) {
const data = await response.json();
setError(data.error || "Speichern fehlgeschlagen.");
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">
<input
type="file"
accept="image/png,image/jpeg,image/webp,image/svg+xml"
onChange={(event) =>
setLogoFile(event.currentTarget.files?.[0] || null)
}
className="block text-sm text-slate-600"
/>
<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">
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>
)}
<label className="flex items-center gap-2 text-sm text-slate-700">
<input
type="checkbox"
checked={registrationEnabled}
onChange={(event) => setRegistrationEnabled(event.target.checked)}
/>
Registrierung erlauben
</label>
<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>
);
}