Files
LaunchPad/packages/ui/src/Favicon.tsx

69 lines
2.2 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.
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>
);
}