fix: favicons
This commit is contained in:
@@ -47,7 +47,7 @@
|
||||
"sqlstring": "^2.3.3",
|
||||
"superjson": "^1.13.3",
|
||||
"svix": "^1.24.0",
|
||||
"url-metadata": "^4.1.1",
|
||||
"url-metadata": "^5.4.1",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
|
||||
@@ -71,7 +71,7 @@ async function fetchImage(
|
||||
url: URL,
|
||||
): Promise<{ buffer: Buffer; contentType: string; status: number }> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout
|
||||
const timeout = setTimeout(() => controller.abort(), 1000); // 10s timeout
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
@@ -175,20 +175,10 @@ async function processImage(
|
||||
bufferSize: buffer.length,
|
||||
});
|
||||
|
||||
// If Sharp fails, try to create a simple fallback image
|
||||
return createFallbackImage();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a simple transparent fallback image when Sharp can't process the original
|
||||
function createFallbackImage(): Buffer {
|
||||
// 1x1 transparent PNG
|
||||
return Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
||||
'base64',
|
||||
);
|
||||
}
|
||||
|
||||
// Process OG image with Sharp (resize to 300px width)
|
||||
async function processOgImage(
|
||||
buffer: Buffer,
|
||||
@@ -220,8 +210,7 @@ async function processOgImage(
|
||||
bufferSize: buffer.length,
|
||||
});
|
||||
|
||||
// If Sharp fails, try to create a simple fallback image
|
||||
return createFallbackImage();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,11 +230,15 @@ export async function getFavicon(
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
try {
|
||||
logger.info('getFavicon', {
|
||||
url: request.query.url,
|
||||
});
|
||||
const url = validateUrl(request.query.url);
|
||||
if (!url) {
|
||||
reply.header('Content-Type', 'image/png');
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return reply.send(createFallbackImage());
|
||||
return reply
|
||||
.status(404)
|
||||
.header('Content-Type', 'text/plain')
|
||||
.send('Not found');
|
||||
}
|
||||
|
||||
const cacheKey = createCacheKey(url.toString());
|
||||
@@ -259,11 +252,13 @@ export async function getFavicon(
|
||||
}
|
||||
|
||||
let imageUrl: URL;
|
||||
|
||||
// If it's a direct image URL, use it directly
|
||||
if (isDirectImage(url)) {
|
||||
imageUrl = url;
|
||||
} else {
|
||||
logger.info('before parseUrlMeta', {
|
||||
url: url.toString(),
|
||||
});
|
||||
// For website URLs, extract favicon from HTML
|
||||
const meta = await parseUrlMeta(url.toString());
|
||||
logger.info('parseUrlMeta result', {
|
||||
@@ -323,9 +318,10 @@ export async function getFavicon(
|
||||
|
||||
// Accept any response as long as we have valid image data
|
||||
if (buffer.length === 0) {
|
||||
reply.header('Content-Type', 'image/png');
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return reply.send(createFallbackImage());
|
||||
return reply
|
||||
.status(404)
|
||||
.header('Content-Type', 'text/plain')
|
||||
.send('Not found');
|
||||
}
|
||||
|
||||
// Process the image (resize to 30x30 PNG, or serve ICO as-is)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
process.env.TZ = 'UTC';
|
||||
|
||||
import compress from '@fastify/compress';
|
||||
import cookie from '@fastify/cookie';
|
||||
import cors, { type FastifyCorsOptions } from '@fastify/cors';
|
||||
@@ -12,7 +14,7 @@ import {
|
||||
type IServiceClientWithProject,
|
||||
runWithAlsSession,
|
||||
} from '@openpanel/db';
|
||||
import { getCache, getRedisPub } from '@openpanel/redis';
|
||||
import { getRedisPub } from '@openpanel/redis';
|
||||
import type { AppRouter } from '@openpanel/trpc';
|
||||
import { appRouter, createContext } from '@openpanel/trpc';
|
||||
|
||||
@@ -50,8 +52,6 @@ import { logger } from './utils/logger';
|
||||
|
||||
sourceMapSupport.install();
|
||||
|
||||
process.env.TZ = 'UTC';
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
client: IServiceClientWithProject | null;
|
||||
|
||||
@@ -72,7 +72,9 @@ interface UrlMetaData {
|
||||
|
||||
export async function parseUrlMeta(url: string) {
|
||||
try {
|
||||
const metadata = (await urlMetadata(url)) as UrlMetaData;
|
||||
const metadata = (await urlMetadata(url, {
|
||||
timeout: 500,
|
||||
})) as UrlMetaData;
|
||||
const data = transform(metadata, url);
|
||||
return data;
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import type { LucideIcon, LucideProps } from 'lucide-react';
|
||||
|
||||
const createFlagIcon = (url: string) => {
|
||||
return ((_props: LucideProps) => (
|
||||
<span
|
||||
className={`fi !block aspect-[1.33] overflow-hidden rounded-[2px] fi-${url}`}
|
||||
/>
|
||||
)) as LucideIcon;
|
||||
};
|
||||
|
||||
const data = {
|
||||
ie: createFlagIcon('ie'),
|
||||
tw: createFlagIcon('tw'),
|
||||
py: createFlagIcon('py'),
|
||||
kr: createFlagIcon('kr'),
|
||||
nz: createFlagIcon('nz'),
|
||||
do: createFlagIcon('do'),
|
||||
cl: createFlagIcon('cl'),
|
||||
dz: createFlagIcon('dz'),
|
||||
np: createFlagIcon('np'),
|
||||
ma: createFlagIcon('ma'),
|
||||
gh: createFlagIcon('gh'),
|
||||
zm: createFlagIcon('zm'),
|
||||
pa: createFlagIcon('pa'),
|
||||
tn: createFlagIcon('tn'),
|
||||
lk: createFlagIcon('lk'),
|
||||
sv: createFlagIcon('sv'),
|
||||
ve: createFlagIcon('ve'),
|
||||
sn: createFlagIcon('sn'),
|
||||
gt: createFlagIcon('gt'),
|
||||
xk: createFlagIcon('xk'),
|
||||
jm: createFlagIcon('jm'),
|
||||
cm: createFlagIcon('cm'),
|
||||
ni: createFlagIcon('ni'),
|
||||
uy: createFlagIcon('uy'),
|
||||
ss: createFlagIcon('ss'),
|
||||
cd: createFlagIcon('cd'),
|
||||
cu: createFlagIcon('cu'),
|
||||
kh: createFlagIcon('kh'),
|
||||
bb: createFlagIcon('bb'),
|
||||
gf: createFlagIcon('gf'),
|
||||
et: createFlagIcon('et'),
|
||||
pe: createFlagIcon('pe'),
|
||||
mo: createFlagIcon('mo'),
|
||||
mn: createFlagIcon('mn'),
|
||||
hn: createFlagIcon('hn'),
|
||||
cn: createFlagIcon('cn'),
|
||||
ng: createFlagIcon('ng'),
|
||||
se: createFlagIcon('se'),
|
||||
jp: createFlagIcon('jp'),
|
||||
hk: createFlagIcon('hk'),
|
||||
us: createFlagIcon('us'),
|
||||
gb: createFlagIcon('gb'),
|
||||
ua: createFlagIcon('ua'),
|
||||
ru: createFlagIcon('ru'),
|
||||
de: createFlagIcon('de'),
|
||||
fr: createFlagIcon('fr'),
|
||||
br: createFlagIcon('br'),
|
||||
in: createFlagIcon('in'),
|
||||
it: createFlagIcon('it'),
|
||||
es: createFlagIcon('es'),
|
||||
pl: createFlagIcon('pl'),
|
||||
nl: createFlagIcon('nl'),
|
||||
id: createFlagIcon('id'),
|
||||
tr: createFlagIcon('tr'),
|
||||
ph: createFlagIcon('ph'),
|
||||
ca: createFlagIcon('ca'),
|
||||
ar: createFlagIcon('ar'),
|
||||
mx: createFlagIcon('mx'),
|
||||
za: createFlagIcon('za'),
|
||||
au: createFlagIcon('au'),
|
||||
co: createFlagIcon('co'),
|
||||
ch: createFlagIcon('ch'),
|
||||
at: createFlagIcon('at'),
|
||||
be: createFlagIcon('be'),
|
||||
pt: createFlagIcon('pt'),
|
||||
my: createFlagIcon('my'),
|
||||
th: createFlagIcon('th'),
|
||||
vn: createFlagIcon('vn'),
|
||||
sg: createFlagIcon('sg'),
|
||||
eg: createFlagIcon('eg'),
|
||||
sa: createFlagIcon('sa'),
|
||||
pk: createFlagIcon('pk'),
|
||||
bd: createFlagIcon('bd'),
|
||||
ro: createFlagIcon('ro'),
|
||||
hu: createFlagIcon('hu'),
|
||||
cz: createFlagIcon('cz'),
|
||||
gr: createFlagIcon('gr'),
|
||||
il: createFlagIcon('il'),
|
||||
no: createFlagIcon('no'),
|
||||
fi: createFlagIcon('fi'),
|
||||
dk: createFlagIcon('dk'),
|
||||
sk: createFlagIcon('sk'),
|
||||
bg: createFlagIcon('bg'),
|
||||
hr: createFlagIcon('hr'),
|
||||
rs: createFlagIcon('rs'),
|
||||
ba: createFlagIcon('ba'),
|
||||
si: createFlagIcon('si'),
|
||||
lv: createFlagIcon('lv'),
|
||||
lt: createFlagIcon('lt'),
|
||||
ee: createFlagIcon('ee'),
|
||||
by: createFlagIcon('by'),
|
||||
md: createFlagIcon('md'),
|
||||
kz: createFlagIcon('kz'),
|
||||
uz: createFlagIcon('uz'),
|
||||
kg: createFlagIcon('kg'),
|
||||
tj: createFlagIcon('tj'),
|
||||
tm: createFlagIcon('tm'),
|
||||
az: createFlagIcon('az'),
|
||||
ge: createFlagIcon('ge'),
|
||||
am: createFlagIcon('am'),
|
||||
af: createFlagIcon('af'),
|
||||
ir: createFlagIcon('ir'),
|
||||
iq: createFlagIcon('iq'),
|
||||
sy: createFlagIcon('sy'),
|
||||
lb: createFlagIcon('lb'),
|
||||
jo: createFlagIcon('jo'),
|
||||
ps: createFlagIcon('ps'),
|
||||
kw: createFlagIcon('kw'),
|
||||
qa: createFlagIcon('qa'),
|
||||
om: createFlagIcon('om'),
|
||||
ye: createFlagIcon('ye'),
|
||||
ae: createFlagIcon('ae'),
|
||||
bh: createFlagIcon('bh'),
|
||||
cy: createFlagIcon('cy'),
|
||||
mt: createFlagIcon('mt'),
|
||||
sm: createFlagIcon('sm'),
|
||||
li: createFlagIcon('li'),
|
||||
is: createFlagIcon('is'),
|
||||
al: createFlagIcon('al'),
|
||||
mk: createFlagIcon('mk'),
|
||||
me: createFlagIcon('me'),
|
||||
ad: createFlagIcon('ad'),
|
||||
lu: createFlagIcon('lu'),
|
||||
mc: createFlagIcon('mc'),
|
||||
fo: createFlagIcon('fo'),
|
||||
gg: createFlagIcon('gg'),
|
||||
je: createFlagIcon('je'),
|
||||
im: createFlagIcon('im'),
|
||||
gi: createFlagIcon('gi'),
|
||||
va: createFlagIcon('va'),
|
||||
ax: createFlagIcon('ax'),
|
||||
bl: createFlagIcon('bl'),
|
||||
mf: createFlagIcon('mf'),
|
||||
pm: createFlagIcon('pm'),
|
||||
yt: createFlagIcon('yt'),
|
||||
wf: createFlagIcon('wf'),
|
||||
tf: createFlagIcon('tf'),
|
||||
re: createFlagIcon('re'),
|
||||
sc: createFlagIcon('sc'),
|
||||
mu: createFlagIcon('mu'),
|
||||
zw: createFlagIcon('zw'),
|
||||
mz: createFlagIcon('mz'),
|
||||
na: createFlagIcon('na'),
|
||||
bw: createFlagIcon('bw'),
|
||||
ls: createFlagIcon('ls'),
|
||||
sz: createFlagIcon('sz'),
|
||||
bi: createFlagIcon('bi'),
|
||||
rw: createFlagIcon('rw'),
|
||||
ug: createFlagIcon('ug'),
|
||||
ke: createFlagIcon('ke'),
|
||||
tz: createFlagIcon('tz'),
|
||||
mg: createFlagIcon('mg'),
|
||||
cr: createFlagIcon('cr'),
|
||||
ky: createFlagIcon('ky'),
|
||||
gy: createFlagIcon('gy'),
|
||||
mm: createFlagIcon('mm'),
|
||||
la: createFlagIcon('la'),
|
||||
gl: createFlagIcon('gl'),
|
||||
gp: createFlagIcon('gp'),
|
||||
fj: createFlagIcon('fj'),
|
||||
cv: createFlagIcon('cv'),
|
||||
gn: createFlagIcon('gn'),
|
||||
bj: createFlagIcon('bj'),
|
||||
bo: createFlagIcon('bo'),
|
||||
bq: createFlagIcon('bq'),
|
||||
bs: createFlagIcon('bs'),
|
||||
ly: createFlagIcon('ly'),
|
||||
bn: createFlagIcon('bn'),
|
||||
tt: createFlagIcon('tt'),
|
||||
sr: createFlagIcon('sr'),
|
||||
ec: createFlagIcon('ec'),
|
||||
mv: createFlagIcon('mv'),
|
||||
pr: createFlagIcon('pr'),
|
||||
ci: createFlagIcon('ci'),
|
||||
};
|
||||
|
||||
export default data;
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { LucideIcon, LucideProps } from 'lucide-react';
|
||||
import type { LucideProps } from 'lucide-react';
|
||||
import {
|
||||
ActivityIcon,
|
||||
BookIcon,
|
||||
CpuIcon,
|
||||
ExternalLinkIcon,
|
||||
HelpCircleIcon,
|
||||
MailIcon,
|
||||
@@ -14,96 +16,343 @@ import {
|
||||
TabletIcon,
|
||||
TvIcon,
|
||||
} from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
|
||||
import { useAppContext } from '@/hooks/use-app-context';
|
||||
import flags from './serie-icon.flags';
|
||||
import iconsWithUrls from './serie-icon.urls';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
type SerieIconProps = Omit<LucideProps, 'name'> & {
|
||||
name?: string | string[];
|
||||
};
|
||||
|
||||
function getProxyImage(url: string) {
|
||||
return `/misc/favicon?url=${encodeURIComponent(url)}`;
|
||||
}
|
||||
type IconType = 'lucide' | 'image' | 'flag';
|
||||
|
||||
const createImageIcon = (url: string) => {
|
||||
return ((_props: LucideProps) => {
|
||||
const context = useAppContext();
|
||||
return (
|
||||
<img
|
||||
alt="serie icon"
|
||||
className="w-full max-h-4 rounded-[2px] object-contain"
|
||||
src={context.apiUrl?.replace(/\/$/, '') + url}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
);
|
||||
}) as LucideIcon;
|
||||
};
|
||||
type ResolvedIcon =
|
||||
| { type: 'lucide'; Icon: React.ComponentType<LucideProps> }
|
||||
| { type: 'image'; url: string }
|
||||
| { type: 'flag'; code: string }
|
||||
| null;
|
||||
|
||||
const mapper: Record<string, LucideIcon> = {
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
const LUCIDE_ICONS: Record<string, React.ComponentType<LucideProps>> = {
|
||||
// Events
|
||||
screen_view: MonitorPlayIcon,
|
||||
session_start: ActivityIcon,
|
||||
session_end: ActivityIcon,
|
||||
link_out: ExternalLinkIcon,
|
||||
|
||||
// Misc
|
||||
// Devices
|
||||
smarttv: TvIcon,
|
||||
mobile: SmartphoneIcon,
|
||||
desktop: MonitorIcon,
|
||||
tablet: TabletIcon,
|
||||
|
||||
// Sources
|
||||
search: SearchIcon,
|
||||
social: PodcastIcon,
|
||||
email: MailIcon,
|
||||
podcast: PodcastIcon,
|
||||
comment: MessageCircleIcon,
|
||||
tech: CpuIcon,
|
||||
content: BookIcon,
|
||||
|
||||
// Misc
|
||||
unknown: HelpCircleIcon,
|
||||
[NOT_SET_VALUE]: ScanIcon,
|
||||
|
||||
...Object.entries(iconsWithUrls).reduce(
|
||||
(acc, [key, value]) => ({
|
||||
...acc,
|
||||
[key]: createImageIcon(getProxyImage(value)),
|
||||
}),
|
||||
{},
|
||||
),
|
||||
|
||||
...flags,
|
||||
};
|
||||
|
||||
const FLAG_CODES = new Set([
|
||||
'ie',
|
||||
'tw',
|
||||
'py',
|
||||
'kr',
|
||||
'nz',
|
||||
'do',
|
||||
'cl',
|
||||
'dz',
|
||||
'np',
|
||||
'ma',
|
||||
'gh',
|
||||
'zm',
|
||||
'pa',
|
||||
'tn',
|
||||
'lk',
|
||||
'sv',
|
||||
've',
|
||||
'sn',
|
||||
'gt',
|
||||
'xk',
|
||||
'jm',
|
||||
'cm',
|
||||
'ni',
|
||||
'uy',
|
||||
'ss',
|
||||
'cd',
|
||||
'cu',
|
||||
'kh',
|
||||
'bb',
|
||||
'gf',
|
||||
'et',
|
||||
'pe',
|
||||
'mo',
|
||||
'mn',
|
||||
'hn',
|
||||
'cn',
|
||||
'ng',
|
||||
'se',
|
||||
'jp',
|
||||
'hk',
|
||||
'us',
|
||||
'gb',
|
||||
'ua',
|
||||
'ru',
|
||||
'de',
|
||||
'fr',
|
||||
'br',
|
||||
'in',
|
||||
'it',
|
||||
'es',
|
||||
'pl',
|
||||
'nl',
|
||||
'id',
|
||||
'tr',
|
||||
'ph',
|
||||
'ca',
|
||||
'ar',
|
||||
'mx',
|
||||
'za',
|
||||
'au',
|
||||
'co',
|
||||
'ch',
|
||||
'at',
|
||||
'be',
|
||||
'pt',
|
||||
'my',
|
||||
'th',
|
||||
'vn',
|
||||
'sg',
|
||||
'eg',
|
||||
'sa',
|
||||
'pk',
|
||||
'bd',
|
||||
'ro',
|
||||
'hu',
|
||||
'cz',
|
||||
'gr',
|
||||
'il',
|
||||
'no',
|
||||
'fi',
|
||||
'dk',
|
||||
'sk',
|
||||
'bg',
|
||||
'hr',
|
||||
'rs',
|
||||
'ba',
|
||||
'si',
|
||||
'lv',
|
||||
'lt',
|
||||
'ee',
|
||||
'by',
|
||||
'md',
|
||||
'kz',
|
||||
'uz',
|
||||
'kg',
|
||||
'tj',
|
||||
'tm',
|
||||
'az',
|
||||
'ge',
|
||||
'am',
|
||||
'af',
|
||||
'ir',
|
||||
'iq',
|
||||
'sy',
|
||||
'lb',
|
||||
'jo',
|
||||
'ps',
|
||||
'kw',
|
||||
'qa',
|
||||
'om',
|
||||
'ye',
|
||||
'ae',
|
||||
'bh',
|
||||
'cy',
|
||||
'mt',
|
||||
'sm',
|
||||
'li',
|
||||
'is',
|
||||
'al',
|
||||
'mk',
|
||||
'me',
|
||||
'ad',
|
||||
'lu',
|
||||
'mc',
|
||||
'fo',
|
||||
'gg',
|
||||
'je',
|
||||
'im',
|
||||
'gi',
|
||||
'va',
|
||||
'ax',
|
||||
'bl',
|
||||
'mf',
|
||||
'pm',
|
||||
'yt',
|
||||
'wf',
|
||||
'tf',
|
||||
're',
|
||||
'sc',
|
||||
'mu',
|
||||
'zw',
|
||||
'mz',
|
||||
'na',
|
||||
'bw',
|
||||
'ls',
|
||||
'sz',
|
||||
'bi',
|
||||
'rw',
|
||||
'ug',
|
||||
'ke',
|
||||
'tz',
|
||||
'mg',
|
||||
'cr',
|
||||
'ky',
|
||||
'gy',
|
||||
'mm',
|
||||
'la',
|
||||
'gl',
|
||||
'gp',
|
||||
'fj',
|
||||
'cv',
|
||||
'gn',
|
||||
'bj',
|
||||
'bo',
|
||||
'bq',
|
||||
'bs',
|
||||
'ly',
|
||||
'bn',
|
||||
'tt',
|
||||
'sr',
|
||||
'ec',
|
||||
'mv',
|
||||
'pr',
|
||||
'ci',
|
||||
]);
|
||||
|
||||
function getProxyImageUrl(url: string): string {
|
||||
return `/misc/favicon?url=${encodeURIComponent(url)}`;
|
||||
}
|
||||
|
||||
function resolveIcon(name: string): ResolvedIcon {
|
||||
const key = name.toLowerCase();
|
||||
|
||||
const lucideIcon = LUCIDE_ICONS[key];
|
||||
if (lucideIcon) {
|
||||
return { type: 'lucide', Icon: lucideIcon };
|
||||
}
|
||||
|
||||
const imageUrl = iconsWithUrls[key as keyof typeof iconsWithUrls];
|
||||
if (imageUrl) {
|
||||
return { type: 'image', url: getProxyImageUrl(imageUrl) };
|
||||
}
|
||||
|
||||
if (FLAG_CODES.has(key)) {
|
||||
return { type: 'flag', code: key };
|
||||
}
|
||||
|
||||
if (name.includes('http')) {
|
||||
return { type: 'image', url: getProxyImageUrl(name) };
|
||||
}
|
||||
|
||||
if (name.match(/^.+\.\w{2,3}$/)) {
|
||||
return { type: 'image', url: getProxyImageUrl(`https://${name}`) };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function IconWrapper({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className={'relative max-h-4 flex-shrink-0 [&_svg]:!rounded-[2px]'}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ImageIcon({ url }: { url: string }) {
|
||||
const context = useAppContext();
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
if (hasError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fullUrl = context.apiUrl?.replace(/\/$/, '') + url;
|
||||
|
||||
return (
|
||||
<IconWrapper>
|
||||
<img
|
||||
src={fullUrl}
|
||||
alt=""
|
||||
className="w-full max-h-4 rounded-[2px] object-contain"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={() => setHasError(true)}
|
||||
/>
|
||||
</IconWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function FlagIcon({ code }: { code: string }) {
|
||||
return (
|
||||
<IconWrapper>
|
||||
<span
|
||||
className={`fi !block aspect-[1.33] overflow-hidden rounded-[2px] fi-${code}`}
|
||||
/>
|
||||
</IconWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function LucideIconWrapper({
|
||||
Icon,
|
||||
...props
|
||||
}: { Icon: React.ComponentType<LucideProps> } & LucideProps) {
|
||||
return (
|
||||
<IconWrapper>
|
||||
<Icon size={16} {...props} />
|
||||
</IconWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// Main component
|
||||
|
||||
export function SerieIcon({ name: names, ...props }: SerieIconProps) {
|
||||
const name = Array.isArray(names) ? names[0] : names;
|
||||
const Icon = useMemo(() => {
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mapped = mapper[name.toLowerCase()] ?? null;
|
||||
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
|
||||
if (name.includes('http')) {
|
||||
return createImageIcon(getProxyImage(name));
|
||||
}
|
||||
|
||||
// Matching image file name
|
||||
if (name.match(/(.+)\.\w{2,3}$/)) {
|
||||
return createImageIcon(getProxyImage(`https://${name}`));
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return null;
|
||||
}, [name]);
|
||||
}
|
||||
|
||||
return Icon ? (
|
||||
<div className="relative max-h-4 flex-shrink-0 [&_svg]:!rounded-[2px]">
|
||||
<Icon size={16} {...props} name={name} />
|
||||
</div>
|
||||
) : null;
|
||||
const resolved = resolveIcon(name);
|
||||
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (resolved.type) {
|
||||
case 'lucide':
|
||||
return <LucideIconWrapper Icon={resolved.Icon} {...props} />;
|
||||
case 'image':
|
||||
return <ImageIcon url={resolved.url} />;
|
||||
case 'flag':
|
||||
return <FlagIcon code={resolved.code} />;
|
||||
}
|
||||
}
|
||||
|
||||
1236
pnpm-lock.yaml
generated
1236
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user