Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | 8x 8x 668x 668x 668x 4x 4x 4x 668x 8x 567x 567x | // SPDX-License-Identifier: GPL-3.0-only
// Copyright (C) 2026 Max Boksem. See NOTICE for additional terms under GPLv3 section 7.
import { useState, type ReactNode } from "react";
import { cn } from "@/lib/utils";
/**
* Logo URLs that failed to load. It lives at module level because the virtual
* lists remount cards while scrolling, and each remount would retry a dead URL.
*/
const brokenImageUrls = new Set<string>();
/**
* Whether the image at url has failed before, and a callback for its onError.
* The answer follows url, so a card that is given a new logo tries it rather
* than keeping the old one's failure.
*/
export const useImageFailed = (url?: string): [boolean, () => void] => {
// Only re-renders after a failure. The set is the record of what failed.
const [, setFailedUrl] = useState<string>();
const failed = !!url && brokenImageUrls.has(url);
const markFailed = () => {
Iif (!url) return;
brokenImageUrls.add(url);
setFailedUrl(url);
};
return [failed, markFailed];
};
interface ChannelLogoProps {
url?: string;
/** Size, shape and background of the logo box. */
className?: string;
/** Shown when there is no URL or the image failed to load. */
fallback: ReactNode;
imgClassName?: string;
alt?: string;
}
export const ChannelLogo = ({
url,
className,
fallback,
imgClassName = "object-contain",
alt = "",
}: ChannelLogoProps) => {
const [failed, markFailed] = useImageFailed(url);
return (
<div
className={cn(
"relative bg-secondary flex items-center justify-center overflow-hidden shrink-0",
className
)}
>
{url && !failed ? (
<img
src={url}
alt={alt}
className={cn("h-full w-full", imgClassName)}
loading="lazy"
onError={markFailed}
/>
) : (
fallback
)}
</div>
);
};
|