mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-09-19 09:40:13 +00:00
Harden OAuth provider icon loading
This commit is contained in:
+50
-31
@@ -1,6 +1,15 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
type SVGProps = React.ComponentPropsWithoutRef<"svg">
|
||||
type IconMarkupState =
|
||||
| { status: "ready"; icon: ParsedIconMarkup }
|
||||
| { status: "missing" }
|
||||
|
||||
type ParsedIconMarkup = {
|
||||
title?: string
|
||||
viewBox: string
|
||||
path: string
|
||||
}
|
||||
|
||||
const simpleIconModules = import.meta.glob("/node_modules/simple-icons/icons/*.svg", {
|
||||
query: "?raw",
|
||||
@@ -14,7 +23,7 @@ const iconLoadersBySlug = Object.fromEntries(
|
||||
]),
|
||||
)
|
||||
|
||||
const iconMarkupCache = new Map<string, string | null>()
|
||||
const iconMarkupCache = new Map<string, IconMarkupState>()
|
||||
|
||||
function toProviderSlug(provider: string) {
|
||||
return provider.trim().replace(/[^a-z0-9]+/gi, "").toLowerCase()
|
||||
@@ -25,15 +34,30 @@ function loadProviderIconMarkup(provider: string) {
|
||||
return loader ? loader() : null
|
||||
}
|
||||
|
||||
function getSvgViewBox(markup: string) {
|
||||
return markup.match(/viewBox="([^"]+)"/i)?.[1] ?? "0 0 24 24"
|
||||
function parseIconMarkup(markup: string): ParsedIconMarkup | null {
|
||||
const viewBox = markup.match(/viewBox="([^"]+)"/i)?.[1] ?? "0 0 24 24"
|
||||
const title = markup.match(/<title>([^<]*)<\/title>/i)?.[1]
|
||||
const path = markup.match(/<path d="([^"]+)"/i)?.[1]
|
||||
|
||||
if (!path) {
|
||||
return null
|
||||
}
|
||||
|
||||
function getSvgInnerMarkup(markup: string) {
|
||||
return markup
|
||||
.replace(/^<svg[^>]*>/i, "")
|
||||
.replace(/<\/svg>\s*$/i, "")
|
||||
.trim()
|
||||
return { title, viewBox, path }
|
||||
}
|
||||
|
||||
async function loadProviderIcon(provider: string): Promise<IconMarkupState> {
|
||||
const markup = await loadProviderIconMarkup(provider)
|
||||
if (!markup) {
|
||||
return { status: "missing" }
|
||||
}
|
||||
|
||||
const parsed = parseIconMarkup(markup)
|
||||
if (!parsed) {
|
||||
return { status: "missing" }
|
||||
}
|
||||
|
||||
return { status: "ready", icon: parsed }
|
||||
}
|
||||
|
||||
export function OAuthProviderIcon({
|
||||
@@ -42,35 +66,28 @@ export function OAuthProviderIcon({
|
||||
...props
|
||||
}: SVGProps & { provider: string; title?: string }) {
|
||||
const providerSlug = toProviderSlug(provider)
|
||||
const [iconMarkup, setIconMarkup] = useState<string | null>(() => {
|
||||
const [iconState, setIconState] = useState<IconMarkupState | null>(() => {
|
||||
return iconMarkupCache.get(providerSlug) ?? null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const cached = iconMarkupCache.get(providerSlug)
|
||||
if (cached !== undefined) {
|
||||
setIconMarkup(cached)
|
||||
return
|
||||
}
|
||||
|
||||
const loadPromise = loadProviderIconMarkup(provider)
|
||||
if (!loadPromise) {
|
||||
iconMarkupCache.set(providerSlug, null)
|
||||
setIconMarkup(null)
|
||||
if (iconMarkupCache.has(providerSlug)) {
|
||||
setIconState(iconMarkupCache.get(providerSlug) ?? null)
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
loadPromise
|
||||
.then((markup) => {
|
||||
loadProviderIcon(provider)
|
||||
.then((result) => {
|
||||
if (cancelled) return
|
||||
iconMarkupCache.set(providerSlug, markup)
|
||||
setIconMarkup(markup)
|
||||
iconMarkupCache.set(providerSlug, result)
|
||||
setIconState(result)
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return
|
||||
iconMarkupCache.set(providerSlug, null)
|
||||
setIconMarkup(null)
|
||||
const missingState = { status: "missing" } as const
|
||||
iconMarkupCache.set(providerSlug, missingState)
|
||||
setIconState(missingState)
|
||||
})
|
||||
|
||||
return () => {
|
||||
@@ -78,20 +95,22 @@ export function OAuthProviderIcon({
|
||||
}
|
||||
}, [provider, providerSlug])
|
||||
|
||||
if (!iconMarkup) {
|
||||
if (!iconState || iconState.status !== "ready") {
|
||||
return null
|
||||
}
|
||||
|
||||
const iconTitle = title ?? iconState.icon.title
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={getSvgViewBox(iconMarkup)}
|
||||
viewBox={iconState.icon.viewBox}
|
||||
fill="currentColor"
|
||||
aria-hidden={title ? undefined : "true"}
|
||||
role={title ? "img" : undefined}
|
||||
aria-hidden={iconTitle ? undefined : "true"}
|
||||
role={iconTitle ? "img" : undefined}
|
||||
{...props}
|
||||
>
|
||||
{title ? <title>{title}</title> : null}
|
||||
<g dangerouslySetInnerHTML={{ __html: getSvgInnerMarkup(iconMarkup) }} />
|
||||
{iconTitle ? <title>{iconTitle}</title> : null}
|
||||
<path d={iconState.icon.path} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user