You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
50 lines
1.3 KiB
50 lines
1.3 KiB
import { isFaviconLoadFailed, markFaviconLoadFailed, normalizeFaviconDomain } from '@/lib/favicon-fail-cache' |
|
import { cn } from '@/lib/utils' |
|
import { useEffect, useRef, useState } from 'react' |
|
|
|
export function Favicon({ |
|
domain, |
|
className, |
|
fallback = null |
|
}: { |
|
domain: string |
|
className?: string |
|
fallback?: React.ReactNode |
|
}) { |
|
const host = normalizeFaviconDomain(domain) |
|
const [loading, setLoading] = useState(true) |
|
const [error, setError] = useState(false) |
|
const loadingRef = useRef(loading) |
|
loadingRef.current = loading |
|
|
|
useEffect(() => { |
|
const knownFailed = !host || isFaviconLoadFailed(host) |
|
setError(knownFailed) |
|
setLoading(!knownFailed) |
|
if (!host || knownFailed) return |
|
return () => { |
|
if (loadingRef.current) markFaviconLoadFailed(host) |
|
} |
|
}, [host]) |
|
|
|
if (error || !host) return fallback |
|
|
|
return ( |
|
<div className={cn('relative', className)}> |
|
{loading && <div className={cn('absolute inset-0', className)}>{fallback}</div>} |
|
<img |
|
src={`https://${host}/favicon.ico`} |
|
alt={host} |
|
className={cn('absolute inset-0', loading && 'opacity-0', className)} |
|
onError={() => { |
|
markFaviconLoadFailed(host) |
|
setError(true) |
|
}} |
|
onLoad={() => { |
|
loadingRef.current = false |
|
setLoading(false) |
|
}} |
|
/> |
|
</div> |
|
) |
|
}
|
|
|