Fehler: Ein hochgeladenes Logo blieb unsichtbar, auch nach dem Neuladen.
Die Oberflaeche prueft per Anfrage, ob ein Bild hinterlegt ist, und nutzte dafuer
HEAD. FastAPI registriert fuer eine GET-Route aber - anders als Starlette - kein
HEAD; die Anfrage lief in ein 405, und jedes hinterlegte Bild galt als "nicht
vorhanden". Der Upload selbst war die ganze Zeit in Ordnung.
- Neuer Endpunkt GET /branding liefert nur den Status ({"logo": true, ...}),
ohne Bilddaten zu uebertragen. Die Oberflaeche fragt jetzt diesen ab.
- Die Bild-Route beantwortet zusaetzlich HEAD, damit sie sich erwartungsgemaess
verhaelt.
Ausserdem:
- Die empfohlenen Abmessungen stehen jetzt sichtbar ueber der Vorschau
(Logo etwa 400x72 px, Favicon 64x64 px, jeweils hoechstens 512 KB).
- Nach dem Hochladen wird die tatsaechliche Bildgroesse angezeigt, damit man
sie mit der Empfehlung vergleichen kann.
Geprueft: "npm run build" laeuft durch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
2.9 KiB
JavaScript
91 lines
2.9 KiB
JavaScript
import { useEffect, useRef, useState } from "react";
|
||
import { api } from "../api";
|
||
import { brandingUrl, hasBranding } from "../branding";
|
||
import Icon from "./Icon";
|
||
|
||
/**
|
||
* Ein Bild der Installation setzen oder wieder entfernen (Logo bzw. Favicon).
|
||
* Ohne eigenes Bild bleibt das eingebaute Zeichen der Oberfläche stehen.
|
||
*/
|
||
export default function BrandingUpload({ kind, title, hint, empfehlung, onChanged }) {
|
||
const [vorhanden, setVorhanden] = useState(false);
|
||
const [version, setVersion] = useState(0);
|
||
const [busy, setBusy] = useState(false);
|
||
const [fehler, setFehler] = useState(null);
|
||
// Tatsaechliche Bildgroesse - damit man sieht, ob sie zur Empfehlung passt.
|
||
const [groesse, setGroesse] = useState(null);
|
||
const dateiFeld = useRef(null);
|
||
|
||
useEffect(() => {
|
||
hasBranding(kind).then(setVorhanden);
|
||
}, [kind]);
|
||
|
||
async function auswaehlen(event) {
|
||
const datei = event.target.files?.[0];
|
||
if (!datei) return;
|
||
setFehler(null);
|
||
setBusy(true);
|
||
try {
|
||
await api.uploadBranding(kind, datei);
|
||
setVorhanden(true);
|
||
setVersion(Date.now());
|
||
onChanged?.();
|
||
} catch (err) {
|
||
setFehler(err.message);
|
||
} finally {
|
||
setBusy(false);
|
||
// Zuruecksetzen, damit dieselbe Datei erneut gewaehlt werden kann.
|
||
if (dateiFeld.current) dateiFeld.current.value = "";
|
||
}
|
||
}
|
||
|
||
async function entfernen() {
|
||
setFehler(null);
|
||
setBusy(true);
|
||
try {
|
||
await api.deleteBranding(kind);
|
||
setVorhanden(false);
|
||
setVersion(Date.now());
|
||
onChanged?.();
|
||
} catch (err) {
|
||
setFehler(err.message);
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div style={{ marginBottom: "var(--sp-4)" }}>
|
||
<label style={{ marginBottom: "var(--sp-2)" }}>{title}</label>
|
||
{empfehlung && (
|
||
<p className="muted small mt-0"><strong>Empfohlen:</strong> {empfehlung}</p>
|
||
)}
|
||
<div className="brand-preview">
|
||
{vorhanden ? (
|
||
<img src={brandingUrl(kind, version)} alt={title}
|
||
onLoad={(e) => setGroesse({
|
||
w: e.currentTarget.naturalWidth,
|
||
h: e.currentTarget.naturalHeight,
|
||
})} />
|
||
) : (
|
||
<span className="muted small">Kein eigenes Bild – Standard wird verwendet</span>
|
||
)}
|
||
</div>
|
||
{vorhanden && groesse?.w > 0 && (
|
||
<p className="muted small mt-0">Hochgeladen: {groesse.w} × {groesse.h} px</p>
|
||
)}
|
||
<p className="muted small mt-0">{hint}</p>
|
||
{fehler && <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>}
|
||
<div className="field-inline">
|
||
<input ref={dateiFeld} type="file" onChange={auswaehlen} disabled={busy}
|
||
accept="image/png,image/jpeg,image/svg+xml,image/webp,image/gif,image/x-icon" />
|
||
{vorhanden && (
|
||
<button type="button" className="btn ghost" onClick={entfernen} disabled={busy}>
|
||
<Icon name="trash" size={14} />Zurücksetzen
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|