import type { EnvironmentId, ProjectIconColor, ProjectIconOverride } from "@t3tools/contracts"; import { getProjectFaviconCacheKey, isProjectFaviconFallbackUrl, } from "@t3tools/shared/projectFavicon"; import { BotIcon, BookOpenIcon, BracesIcon, CircuitBoardIcon, CloudCogIcon, Code2Icon, DatabaseIcon, FlaskConicalIcon, FolderCodeIcon, Gamepad2Icon, Globe2Icon, ImageIcon, Layers3Icon, MonitorIcon, MusicIcon, PackageIcon, ServerIcon, ShieldCheckIcon, ShoppingBagIcon, SmartphoneIcon, TerminalIcon, VideoIcon, } from "lucide-react"; import type { IconName } from "lucide-react/dynamic"; import type { ComponentType } from "react"; import { lazy, Suspense, useState } from "react"; import { useAssetUrlState } from "../assets/assetUrls"; import { selectProjectIcon, type ProjectIconName } from "../projectIconModel"; import { projectIconColorClassName } from "../projectIconColors"; import { cn } from "~/lib/utils"; const loadedProjectFaviconSrcs = new Map(); const DynamicIcon = lazy(() => import("lucide-react/dynamic").then((module) => ({ default: module.DynamicIcon })), ); function DynamicProjectIconFallback() { return ; } const PROJECT_ICONS: Record> = { ai: BotIcon, book: BookOpenIcon, braces: BracesIcon, circuit: CircuitBoardIcon, cloud: CloudCogIcon, code: Code2Icon, database: DatabaseIcon, desktop: MonitorIcon, "folder-code": FolderCodeIcon, game: Gamepad2Icon, image: ImageIcon, layers: Layers3Icon, mobile: SmartphoneIcon, music: MusicIcon, package: PackageIcon, security: ShieldCheckIcon, server: ServerIcon, shopping: ShoppingBagIcon, terminal: TerminalIcon, test: FlaskConicalIcon, video: VideoIcon, web: Globe2Icon, }; const PROJECT_ICON_COLOR_BY_NAME: Record = { ai: "violet", book: "amber", braces: "purple", circuit: "teal", cloud: "sky", code: "blue", database: "cyan", desktop: "indigo", "folder-code": "orange", game: "emerald", image: "pink", layers: "fuchsia", mobile: "lime", music: "fuchsia", package: "orange", security: "teal", server: "blue", shopping: "rose", terminal: "green", test: "yellow", video: "red", web: "sky", }; export function ProjectFavicon(input: { environmentId: EnvironmentId; cwd: string; projectName: string; faviconPath?: string | null | undefined; projectIcon?: ProjectIconOverride | null | undefined; className?: string | undefined; fallbackIcon?: ComponentType<{ className?: string }>; }) { const state = useProjectFaviconAsset(input); const src = state._tag === "Success" ? state.url : null; if (input.projectIcon?.kind === "emoji") { return ; } if (input.projectIcon?.kind === "lucide") { const colorClassName = projectIconColorClassName(input.projectIcon.color); const iconClassName = cn( "inline-flex size-3.5 shrink-0 items-center justify-center", colorClassName, input.className, ); return ( ); } const automaticIconName = input.fallbackIcon ? null : selectProjectIcon(input.projectName, input.cwd); const FallbackIcon = input.fallbackIcon ?? (automaticIconName?.kind === "lucide" ? PROJECT_ICONS[automaticIconName.icon] : undefined); const fallbackEmoji = automaticIconName?.kind === "emoji" ? automaticIconName.emoji : undefined; const fallbackColorClassName = automaticIconName?.kind === "lucide" ? projectIconColorClassName(PROJECT_ICON_COLOR_BY_NAME[automaticIconName.icon]) : undefined; if (!src || isProjectFaviconFallbackUrl(src)) { return ( ); } const cacheKey = getProjectFaviconCacheKey(input.environmentId, input.cwd, src); return ( ); } export function useProjectFaviconAsset(input: { readonly environmentId: EnvironmentId; readonly cwd: string; readonly faviconPath?: string | null | undefined; }) { return useAssetUrlState(input.environmentId, { _tag: "project-favicon", cwd: input.cwd, ...(input.faviconPath ? { path: input.faviconPath } : {}), }); } function ProjectFaviconFallback({ className, colorClassName, icon: Icon, emoji, }: { readonly className?: string | undefined; readonly colorClassName?: string | undefined; readonly icon?: ComponentType<{ className?: string }> | undefined; readonly emoji?: string | undefined; }) { if (emoji) { return ( ); } if (!Icon) return null; return ; } function ProjectFaviconImage({ cacheKey, src, className, fallbackIcon: FallbackIcon, fallbackEmoji, fallbackColorClassName, }: { readonly cacheKey: string; readonly src: string; readonly className?: string | undefined; readonly fallbackIcon?: ComponentType<{ className?: string }> | undefined; readonly fallbackEmoji?: string | undefined; readonly fallbackColorClassName?: string | undefined; }) { const [displayedSrc, setDisplayedSrc] = useState( () => 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 ? ( ) : null} {displayedSrc ? ( handleLoadError(displayedSrc)} /> ) : null} {isLoading ? ( { loadedProjectFaviconSrcs.set(cacheKey, src); setDisplayedSrc(src); }} onError={() => handleLoadError(src)} /> ) : null} ); }