fix: favicons
This commit is contained in:
@@ -47,7 +47,7 @@
|
|||||||
"sqlstring": "^2.3.3",
|
"sqlstring": "^2.3.3",
|
||||||
"superjson": "^1.13.3",
|
"superjson": "^1.13.3",
|
||||||
"svix": "^1.24.0",
|
"svix": "^1.24.0",
|
||||||
"url-metadata": "^4.1.1",
|
"url-metadata": "^5.4.1",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ async function fetchImage(
|
|||||||
url: URL,
|
url: URL,
|
||||||
): Promise<{ buffer: Buffer; contentType: string; status: number }> {
|
): Promise<{ buffer: Buffer; contentType: string; status: number }> {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout
|
const timeout = setTimeout(() => controller.abort(), 1000); // 10s timeout
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
@@ -175,20 +175,10 @@ async function processImage(
|
|||||||
bufferSize: buffer.length,
|
bufferSize: buffer.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If Sharp fails, try to create a simple fallback image
|
throw error;
|
||||||
return createFallbackImage();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
// Process OG image with Sharp (resize to 300px width)
|
||||||
async function processOgImage(
|
async function processOgImage(
|
||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
@@ -220,8 +210,7 @@ async function processOgImage(
|
|||||||
bufferSize: buffer.length,
|
bufferSize: buffer.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If Sharp fails, try to create a simple fallback image
|
throw error;
|
||||||
return createFallbackImage();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,11 +230,15 @@ export async function getFavicon(
|
|||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
logger.info('getFavicon', {
|
||||||
|
url: request.query.url,
|
||||||
|
});
|
||||||
const url = validateUrl(request.query.url);
|
const url = validateUrl(request.query.url);
|
||||||
if (!url) {
|
if (!url) {
|
||||||
reply.header('Content-Type', 'image/png');
|
return reply
|
||||||
reply.header('Cache-Control', 'public, max-age=3600');
|
.status(404)
|
||||||
return reply.send(createFallbackImage());
|
.header('Content-Type', 'text/plain')
|
||||||
|
.send('Not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = createCacheKey(url.toString());
|
const cacheKey = createCacheKey(url.toString());
|
||||||
@@ -259,11 +252,13 @@ export async function getFavicon(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let imageUrl: URL;
|
let imageUrl: URL;
|
||||||
|
|
||||||
// If it's a direct image URL, use it directly
|
// If it's a direct image URL, use it directly
|
||||||
if (isDirectImage(url)) {
|
if (isDirectImage(url)) {
|
||||||
imageUrl = url;
|
imageUrl = url;
|
||||||
} else {
|
} else {
|
||||||
|
logger.info('before parseUrlMeta', {
|
||||||
|
url: url.toString(),
|
||||||
|
});
|
||||||
// For website URLs, extract favicon from HTML
|
// For website URLs, extract favicon from HTML
|
||||||
const meta = await parseUrlMeta(url.toString());
|
const meta = await parseUrlMeta(url.toString());
|
||||||
logger.info('parseUrlMeta result', {
|
logger.info('parseUrlMeta result', {
|
||||||
@@ -323,9 +318,10 @@ export async function getFavicon(
|
|||||||
|
|
||||||
// Accept any response as long as we have valid image data
|
// Accept any response as long as we have valid image data
|
||||||
if (buffer.length === 0) {
|
if (buffer.length === 0) {
|
||||||
reply.header('Content-Type', 'image/png');
|
return reply
|
||||||
reply.header('Cache-Control', 'public, max-age=3600');
|
.status(404)
|
||||||
return reply.send(createFallbackImage());
|
.header('Content-Type', 'text/plain')
|
||||||
|
.send('Not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the image (resize to 30x30 PNG, or serve ICO as-is)
|
// 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 compress from '@fastify/compress';
|
||||||
import cookie from '@fastify/cookie';
|
import cookie from '@fastify/cookie';
|
||||||
import cors, { type FastifyCorsOptions } from '@fastify/cors';
|
import cors, { type FastifyCorsOptions } from '@fastify/cors';
|
||||||
@@ -12,7 +14,7 @@ import {
|
|||||||
type IServiceClientWithProject,
|
type IServiceClientWithProject,
|
||||||
runWithAlsSession,
|
runWithAlsSession,
|
||||||
} from '@openpanel/db';
|
} from '@openpanel/db';
|
||||||
import { getCache, getRedisPub } from '@openpanel/redis';
|
import { getRedisPub } from '@openpanel/redis';
|
||||||
import type { AppRouter } from '@openpanel/trpc';
|
import type { AppRouter } from '@openpanel/trpc';
|
||||||
import { appRouter, createContext } from '@openpanel/trpc';
|
import { appRouter, createContext } from '@openpanel/trpc';
|
||||||
|
|
||||||
@@ -50,8 +52,6 @@ import { logger } from './utils/logger';
|
|||||||
|
|
||||||
sourceMapSupport.install();
|
sourceMapSupport.install();
|
||||||
|
|
||||||
process.env.TZ = 'UTC';
|
|
||||||
|
|
||||||
declare module 'fastify' {
|
declare module 'fastify' {
|
||||||
interface FastifyRequest {
|
interface FastifyRequest {
|
||||||
client: IServiceClientWithProject | null;
|
client: IServiceClientWithProject | null;
|
||||||
|
|||||||
@@ -72,7 +72,9 @@ interface UrlMetaData {
|
|||||||
|
|
||||||
export async function parseUrlMeta(url: string) {
|
export async function parseUrlMeta(url: string) {
|
||||||
try {
|
try {
|
||||||
const metadata = (await urlMetadata(url)) as UrlMetaData;
|
const metadata = (await urlMetadata(url, {
|
||||||
|
timeout: 500,
|
||||||
|
})) as UrlMetaData;
|
||||||
const data = transform(metadata, url);
|
const data = transform(metadata, url);
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
} 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 {
|
import {
|
||||||
ActivityIcon,
|
ActivityIcon,
|
||||||
|
BookIcon,
|
||||||
|
CpuIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
HelpCircleIcon,
|
HelpCircleIcon,
|
||||||
MailIcon,
|
MailIcon,
|
||||||
@@ -14,96 +16,343 @@ import {
|
|||||||
TabletIcon,
|
TabletIcon,
|
||||||
TvIcon,
|
TvIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useMemo } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||||
|
|
||||||
import { useAppContext } from '@/hooks/use-app-context';
|
import { useAppContext } from '@/hooks/use-app-context';
|
||||||
import flags from './serie-icon.flags';
|
|
||||||
import iconsWithUrls from './serie-icon.urls';
|
import iconsWithUrls from './serie-icon.urls';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
type SerieIconProps = Omit<LucideProps, 'name'> & {
|
type SerieIconProps = Omit<LucideProps, 'name'> & {
|
||||||
name?: string | string[];
|
name?: string | string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function getProxyImage(url: string) {
|
type IconType = 'lucide' | 'image' | 'flag';
|
||||||
return `/misc/favicon?url=${encodeURIComponent(url)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createImageIcon = (url: string) => {
|
type ResolvedIcon =
|
||||||
return ((_props: LucideProps) => {
|
| { type: 'lucide'; Icon: React.ComponentType<LucideProps> }
|
||||||
const context = useAppContext();
|
| { type: 'image'; url: string }
|
||||||
return (
|
| { type: 'flag'; code: string }
|
||||||
<img
|
| null;
|
||||||
alt="serie icon"
|
|
||||||
className="w-full max-h-4 rounded-[2px] object-contain"
|
|
||||||
src={context.apiUrl?.replace(/\/$/, '') + url}
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}) as LucideIcon;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapper: Record<string, LucideIcon> = {
|
// ============================================================================
|
||||||
|
// Constants
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const LUCIDE_ICONS: Record<string, React.ComponentType<LucideProps>> = {
|
||||||
// Events
|
// Events
|
||||||
screen_view: MonitorPlayIcon,
|
screen_view: MonitorPlayIcon,
|
||||||
session_start: ActivityIcon,
|
session_start: ActivityIcon,
|
||||||
session_end: ActivityIcon,
|
session_end: ActivityIcon,
|
||||||
link_out: ExternalLinkIcon,
|
link_out: ExternalLinkIcon,
|
||||||
|
|
||||||
// Misc
|
// Devices
|
||||||
smarttv: TvIcon,
|
smarttv: TvIcon,
|
||||||
mobile: SmartphoneIcon,
|
mobile: SmartphoneIcon,
|
||||||
desktop: MonitorIcon,
|
desktop: MonitorIcon,
|
||||||
tablet: TabletIcon,
|
tablet: TabletIcon,
|
||||||
|
|
||||||
|
// Sources
|
||||||
search: SearchIcon,
|
search: SearchIcon,
|
||||||
social: PodcastIcon,
|
social: PodcastIcon,
|
||||||
email: MailIcon,
|
email: MailIcon,
|
||||||
podcast: PodcastIcon,
|
podcast: PodcastIcon,
|
||||||
comment: MessageCircleIcon,
|
comment: MessageCircleIcon,
|
||||||
|
tech: CpuIcon,
|
||||||
|
content: BookIcon,
|
||||||
|
|
||||||
|
// Misc
|
||||||
unknown: HelpCircleIcon,
|
unknown: HelpCircleIcon,
|
||||||
[NOT_SET_VALUE]: ScanIcon,
|
[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) {
|
export function SerieIcon({ name: names, ...props }: SerieIconProps) {
|
||||||
const name = Array.isArray(names) ? names[0] : names;
|
const name = Array.isArray(names) ? names[0] : names;
|
||||||
const Icon = useMemo(() => {
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapped = mapper[name.toLowerCase()] ?? null;
|
const resolved = resolveIcon(name);
|
||||||
|
|
||||||
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 (!resolved) {
|
||||||
return null;
|
return null;
|
||||||
}, [name]);
|
}
|
||||||
|
|
||||||
return Icon ? (
|
switch (resolved.type) {
|
||||||
<div className="relative max-h-4 flex-shrink-0 [&_svg]:!rounded-[2px]">
|
case 'lucide':
|
||||||
<Icon size={16} {...props} name={name} />
|
return <LucideIconWrapper Icon={resolved.Icon} {...props} />;
|
||||||
</div>
|
case 'image':
|
||||||
) : null;
|
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