import type { EnvironmentId } from "@t3tools/contracts";import { getProjectFaviconCacheKey, isProjectFaviconFallbackUrl,} from "@t3tools/shared/projectFavicon";import { FolderIcon } from "lucide-react";import type { ComponentType } from "react";import { useState } from "react";import { useAssetUrl } from "../assets/assetUrls";import { cn } from "~/lib/utils"; const loadedProjectFaviconSrcs = new Map<string, string>(); export function ProjectFavicon(input: { environmentId: EnvironmentId; cwd: string; className?: string | undefined; fallbackIcon?: ComponentType<{ className?: string }>;}) { const src = useAssetUrl(input.environmentId, { _tag: "project-favicon", cwd: input.cwd, }); const FallbackIcon = input.fallbackIcon ?? FolderIcon; if (!src || isProjectFaviconFallbackUrl(src)) { return <ProjectFaviconFallback className={input.className} icon={FallbackIcon} />; } const cacheKey = getProjectFaviconCacheKey(input.environmentId, input.cwd, src); return ( <ProjectFaviconImage key={cacheKey} cacheKey={cacheKey} src={src} className={input.className} fallbackIcon={FallbackIcon} /> );} function ProjectFaviconFallback({ className, icon: Icon,}: { readonly className?: string | undefined; readonly icon: ComponentType<{ className?: string }>;}) { return <Icon className={cn("size-3.5 shrink-0 text-muted-foreground/50", className)} />;} function ProjectFaviconImage({ cacheKey, src, className, fallbackIcon: FallbackIcon,}: { readonly cacheKey: string; readonly src: string; readonly className?: string | undefined; readonly fallbackIcon: ComponentType<{ className?: string }>;}) { const [displayedSrc, setDisplayedSrc] = useState<string | null>( () => loadedProjectFaviconSrcs.get(cacheKey) ?? null, ); const isLoading = displayedSrc !== src; const handleLoadError = (failedSrc: string) => { if (loadedProjectFaviconSrcs.get(cacheKey) === failedSrc) { loadedProjectFaviconSrcs.delete(cacheKey); } setDisplayedSrc((currentSrc) => (currentSrc === failedSrc ? null : currentSrc)); }; return ( <> {displayedSrc === null ? ( <ProjectFaviconFallback className={className} icon={FallbackIcon} /> ) : null} {displayedSrc ? ( <img src={displayedSrc} alt="" className={cn("size-3.5 shrink-0 rounded-sm object-contain", className)} onError={() => handleLoadError(displayedSrc)} /> ) : null} {isLoading ? ( <img src={src} alt="" className="hidden" onLoad={() => { loadedProjectFaviconSrcs.set(cacheKey, src); setDisplayedSrc(src); }} onError={() => handleLoadError(src)} /> ) : null} </> );}
import type { EnvironmentId } from "@t3tools/contracts";
import {
getProjectFaviconCacheKey,
isProjectFaviconFallbackUrl,
} from "@t3tools/shared/projectFavicon";
import { FolderIcon } from "lucide-react";
import type { ComponentType } from "react";
import { useState } from "react";
import { useAssetUrl } from "../assets/assetUrls";
import { cn } from "~/lib/utils";
const loadedProjectFaviconSrcs = new Map<string, string>();
export function ProjectFavicon(input: {
environmentId: EnvironmentId;
cwd: string;
className?: string | undefined;
fallbackIcon?: ComponentType<{ className?: string }>;
}) {
const src = useAssetUrl(input.environmentId, {
_tag: "project-favicon",
cwd: input.cwd,
});
const FallbackIcon = input.fallbackIcon ?? FolderIcon;
if (!src || isProjectFaviconFallbackUrl(src)) {
return <ProjectFaviconFallback className={input.className} icon={FallbackIcon} />;
}
const cacheKey = getProjectFaviconCacheKey(input.environmentId, input.cwd, src);
return (
<ProjectFaviconImage
key={cacheKey}
cacheKey={cacheKey}
src={src}
className={input.className}
fallbackIcon={FallbackIcon}
/>
);
}
function ProjectFaviconFallback({
className,
icon: Icon,
}: {
readonly className?: string | undefined;
readonly icon: ComponentType<{ className?: string }>;
}) {
return <Icon className={cn("size-3.5 shrink-0 text-muted-foreground/50", className)} />;
}
function ProjectFaviconImage({
cacheKey,
src,
className,
fallbackIcon: FallbackIcon,
}: {
readonly cacheKey: string;
readonly src: string;
readonly className?: string | undefined;
readonly fallbackIcon: ComponentType<{ className?: string }>;
}) {
const [displayedSrc, setDisplayedSrc] = useState<string | null>(
() => loadedProjectFaviconSrcs.get(cacheKey) ?? null,
);
const isLoading = displayedSrc !== src;
const handleLoadError = (failedSrc: string) => {
if (loadedProjectFaviconSrcs.get(cacheKey) === failedSrc) {
loadedProjectFaviconSrcs.delete(cacheKey);
}
setDisplayedSrc((currentSrc) => (currentSrc === failedSrc ? null : currentSrc));
};
return (
<>
{displayedSrc === null ? (
<ProjectFaviconFallback className={className} icon={FallbackIcon} />
) : null}
{displayedSrc ? (
<img
src={displayedSrc}
alt=""
className={cn("size-3.5 shrink-0 rounded-sm object-contain", className)}
onError={() => handleLoadError(displayedSrc)}
/>
) : null}
{isLoading ? (
<img
src={src}
alt=""
className="hidden"
onLoad={() => {
loadedProjectFaviconSrcs.set(cacheKey, src);
setDisplayedSrc(src);
}}
onError={() => handleLoadError(src)}
/>
) : null}
</>
);
}