logo
TSZones个人导航站
搜索

一个获取 LOGO 位置的组件

一个获取 LOGO 位置的组件

来源:

1"use client";
2import { getDomain } from "@/lib/utils";
3import { useEffect, useState } from "react";
4
5interface IProps {
6 url: string;
7 size?: number;
8 className?: string;
9 timeout?: number;
10}
11
12const WebsiteLogo = ({
13 url,
14 size = 32,
15 className = "",
16 timeout = 1000, // 1 second
17}: IProps) => {
18 const domain = getDomain(url);
19 const [imgSrc, setImgSrc] = useState(`https://${domain}/logo.svg`);
20 const [fallbackIndex, setFallbackIndex] = useState(0);
21 const [isLoading, setIsLoading] = useState(true);
22 const [hasError, setHasError] = useState(false);
23
24 const fallbackSources = [
25 `https://${domain}/logo.svg`,
26 `https://${domain}/logo.png`,
27 `https://${domain}/apple-touch-icon.png`,
28 `https://${domain}/apple-touch-icon-precomposed.png`,
29 `https://www.google.com/s2/favicons?domain=${domain}&sz=64`,
30 `https://icons.duckduckgo.com/ip3/${domain}.ico`,
31 `https://${domain}/favicon.ico`,
32 ];
33
34 useEffect(() => {
35 let timeoutId: any;
36
37 if (isLoading) {
38 timeoutId = setTimeout(() => {
39 handleError();
40 }, timeout);
41 }
42
43 return () => {
44 if (timeoutId) {
45 clearTimeout(timeoutId);
46 }
47 };
48 }, [imgSrc, isLoading]);
49
50 const handleError = () => {
51 const nextIndex = fallbackIndex + 1;
52 if (nextIndex < fallbackSources.length) {
53 setFallbackIndex(nextIndex);
54 setImgSrc(fallbackSources[nextIndex]);
55 setIsLoading(true);
56 } else {
57 setHasError(true);
58 setIsLoading(false);
59 }
60 };
61
62 const handleLoad = () => {
63 setIsLoading(false);
64 setHasError(false);
65 };
66
67 return (
68 <div
69 className={`relative inline-block ${className}`}
70 style={{ width: size, height: size }}
71 >
72 {/* placeholder */}
73 {isLoading && (
74 <div className="absolute inset-0 animate-pulse">
75 <div className="w-full h-full rounded-md bg-gray-200/60" />
76 </div>
77 )}
78
79 <img
80 src={imgSrc}
81 alt={`${domain} logo`}
82 width={size}
83 height={size}
84 onError={handleError}
85 onLoad={handleLoad}
86 className={`inline-block transition-opacity duration-300 ${
87 isLoading ? "opacity-0" : "opacity-100"
88 }`}
89 style={{
90 objectFit: "contain",
91 display: hasError ? "none" : "inline-block",
92 }}
93 />
94
95 {/* Fallback: Display first letter of domain when all image sources fail */}
96 {hasError && (
97 <div
98 className="w-full h-full flex items-center justify-center bg-gray-100 rounded-md"
99 style={{ fontSize: `${size * 0.5}px` }}
100 >
101 {domain.charAt(0).toUpperCase()}
102 </div>
103 )}
104 </div>
105 );
106};
107
108export default WebsiteLogo;