generated from Dicken/dickendock
69 lines
2.2 KiB
TypeScript
69 lines
2.2 KiB
TypeScript
import { useState, useEffect } from "react";
|
||
|
||
export interface FaviconProps {
|
||
src?: string | null;
|
||
fallbackLetter: string;
|
||
size?: "sm" | "md";
|
||
}
|
||
|
||
const SIZE_CLASSES: Record<NonNullable<FaviconProps["size"]>, string> = {
|
||
sm: "h-4 w-4",
|
||
md: "h-5 w-5",
|
||
};
|
||
|
||
/**
|
||
* Zeigt ein Favicon mit einem immer hellen Hintergrund (unabhängig vom
|
||
* Dark/Light-Theme der App) – viele Favicons sind selbst dunkel/schwarz und
|
||
* wären auf dunklem Hintergrund sonst kaum zu erkennen. Ohne Favicon oder bei
|
||
* einem fehlgeschlagenen Ladeversuch (kaputte URL, 404, CORS) wird
|
||
* stattdessen der erste Buchstabe des Namens gezeigt.
|
||
*/
|
||
/**
|
||
* Externe/Geräte-Favicons laufen über den Backend-Proxy (/api/favicon-proxy):
|
||
* Ohne das würden auf der HTTPS-Seite viele http://-Favicons (typisch für
|
||
* Homelab-Geräte ohne eigenes TLS) vom Browser als "Mixed Content" blockiert
|
||
* und nie angezeigt - das Backend selbst fetcht sie ohne diese Einschränkung.
|
||
* Data-URLs (z. B. selbst hochgeladene Icons) werden direkt durchgereicht.
|
||
*/
|
||
function resolveFaviconSrc(src: string): string {
|
||
if (src.startsWith("data:")) return src;
|
||
return `/api/favicon-proxy?url=${encodeURIComponent(src)}`;
|
||
}
|
||
|
||
export function Favicon({ src, fallbackLetter, size = "md" }: FaviconProps) {
|
||
const dimension = SIZE_CLASSES[size];
|
||
const [failed, setFailed] = useState(false);
|
||
|
||
// Bei geändertem src (z. B. anderer Listeneintrag durch Virtualisierung/Key-Wiederverwendung)
|
||
// erneut versuchen statt dauerhaft im Fehlerzustand zu bleiben.
|
||
useEffect(() => {
|
||
setFailed(false);
|
||
}, [src]);
|
||
|
||
if (!src || failed) {
|
||
return (
|
||
<span
|
||
className={`flex ${dimension} shrink-0 items-center justify-center rounded
|
||
bg-black/10 text-[10px] font-medium text-black/50 dark:bg-white/10
|
||
dark:text-white/50`}
|
||
>
|
||
{fallbackLetter.charAt(0).toUpperCase()}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<span
|
||
className={`flex ${dimension} shrink-0 items-center justify-center rounded
|
||
bg-white p-0.5 ring-1 ring-black/5`}
|
||
>
|
||
<img
|
||
src={resolveFaviconSrc(src)}
|
||
alt=""
|
||
className="h-full w-full object-contain"
|
||
onError={() => setFailed(true)}
|
||
/>
|
||
</span>
|
||
);
|
||
}
|