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"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
type SVGProps = React.ComponentPropsWithoutRef<"svg">
|
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", {
|
const simpleIconModules = import.meta.glob("/node_modules/simple-icons/icons/*.svg", {
|
||||||
query: "?raw",
|
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) {
|
function toProviderSlug(provider: string) {
|
||||||
return provider.trim().replace(/[^a-z0-9]+/gi, "").toLowerCase()
|
return provider.trim().replace(/[^a-z0-9]+/gi, "").toLowerCase()
|
||||||
@@ -25,15 +34,30 @@ function loadProviderIconMarkup(provider: string) {
|
|||||||
return loader ? loader() : null
|
return loader ? loader() : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSvgViewBox(markup: string) {
|
function parseIconMarkup(markup: string): ParsedIconMarkup | null {
|
||||||
return markup.match(/viewBox="([^"]+)"/i)?.[1] ?? "0 0 24 24"
|
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 { title, viewBox, path }
|
||||||
return markup
|
}
|
||||||
.replace(/^<svg[^>]*>/i, "")
|
|
||||||
.replace(/<\/svg>\s*$/i, "")
|
async function loadProviderIcon(provider: string): Promise<IconMarkupState> {
|
||||||
.trim()
|
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({
|
export function OAuthProviderIcon({
|
||||||
@@ -42,35 +66,28 @@ export function OAuthProviderIcon({
|
|||||||
...props
|
...props
|
||||||
}: SVGProps & { provider: string; title?: string }) {
|
}: SVGProps & { provider: string; title?: string }) {
|
||||||
const providerSlug = toProviderSlug(provider)
|
const providerSlug = toProviderSlug(provider)
|
||||||
const [iconMarkup, setIconMarkup] = useState<string | null>(() => {
|
const [iconState, setIconState] = useState<IconMarkupState | null>(() => {
|
||||||
return iconMarkupCache.get(providerSlug) ?? null
|
return iconMarkupCache.get(providerSlug) ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cached = iconMarkupCache.get(providerSlug)
|
if (iconMarkupCache.has(providerSlug)) {
|
||||||
if (cached !== undefined) {
|
setIconState(iconMarkupCache.get(providerSlug) ?? null)
|
||||||
setIconMarkup(cached)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadPromise = loadProviderIconMarkup(provider)
|
|
||||||
if (!loadPromise) {
|
|
||||||
iconMarkupCache.set(providerSlug, null)
|
|
||||||
setIconMarkup(null)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
loadPromise
|
loadProviderIcon(provider)
|
||||||
.then((markup) => {
|
.then((result) => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
iconMarkupCache.set(providerSlug, markup)
|
iconMarkupCache.set(providerSlug, result)
|
||||||
setIconMarkup(markup)
|
setIconState(result)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
iconMarkupCache.set(providerSlug, null)
|
const missingState = { status: "missing" } as const
|
||||||
setIconMarkup(null)
|
iconMarkupCache.set(providerSlug, missingState)
|
||||||
|
setIconState(missingState)
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -78,20 +95,22 @@ export function OAuthProviderIcon({
|
|||||||
}
|
}
|
||||||
}, [provider, providerSlug])
|
}, [provider, providerSlug])
|
||||||
|
|
||||||
if (!iconMarkup) {
|
if (!iconState || iconState.status !== "ready") {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const iconTitle = title ?? iconState.icon.title
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
viewBox={getSvgViewBox(iconMarkup)}
|
viewBox={iconState.icon.viewBox}
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
aria-hidden={title ? undefined : "true"}
|
aria-hidden={iconTitle ? undefined : "true"}
|
||||||
role={title ? "img" : undefined}
|
role={iconTitle ? "img" : undefined}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{title ? <title>{title}</title> : null}
|
{iconTitle ? <title>{iconTitle}</title> : null}
|
||||||
<g dangerouslySetInnerHTML={{ __html: getSvgInnerMarkup(iconMarkup) }} />
|
<path d={iconState.icon.path} />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user