From 6bd7866688daf743ee21597d97ac7a6248189e20 Mon Sep 17 00:00:00 2001 From: dysf888 <47450409+dysf888@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:10:19 +0800 Subject: [PATCH] Add simple-icons support for OAuth providers --- package-lock.json | 20 ++++++++ package.json | 1 + src/components/ui/icon.tsx | 96 ++++++++++++++++++++++++++++++++++++-- src/routes/login.tsx | 5 +- src/routes/profile.tsx | 10 +++- 5 files changed, 126 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2a9c7b0..d81cf71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "react-i18next": "^17.0.8", "react-router-dom": "^7.18.0", "react-virtuoso": "^4.18.7", + "simple-icons": "^16.24.1", "sonner": "^2.0.7", "swr": "^2.4.1", "tailwind-merge": "^3.6.0", @@ -5940,6 +5941,25 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-icons": { + "version": "16.24.1", + "resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-16.24.1.tgz", + "integrity": "sha512-AnDQPrZAVzYSym7cBVIrnbhLk9auWGgkl+9hKvkbTqGEfH6TU7WKgumEXYYaJuM1Ib87+cnzr881UZniCM7t+A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/simple-icons" + }, + { + "type": "github", + "url": "https://github.com/sponsors/simple-icons" + } + ], + "license": "CC0-1.0", + "engines": { + "node": ">=0.12.18" + } + }, "node_modules/sonner": { "version": "2.0.7", "license": "MIT", diff --git a/package.json b/package.json index 52270f4..7b02dbf 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "react-i18next": "^17.0.8", "react-router-dom": "^7.18.0", "react-virtuoso": "^4.18.7", + "simple-icons": "^16.24.1", "sonner": "^2.0.7", "swr": "^2.4.1", "tailwind-merge": "^3.6.0", diff --git a/src/components/ui/icon.tsx b/src/components/ui/icon.tsx index 12445aa..4b299dd 100644 --- a/src/components/ui/icon.tsx +++ b/src/components/ui/icon.tsx @@ -1,7 +1,97 @@ -export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) { +import { useEffect, useState } from "react" + +type SVGProps = React.ComponentPropsWithoutRef<"svg"> + +const simpleIconModules = import.meta.glob("/node_modules/simple-icons/icons/*.svg", { + query: "?raw", + import: "default", +}) + +const iconLoadersBySlug = Object.fromEntries( + Object.entries(simpleIconModules).map(([path, loader]) => [ + path.slice(path.lastIndexOf("/") + 1, -".svg".length), + loader as () => Promise, + ]), +) + +const iconMarkupCache = new Map() + +function toProviderSlug(provider: string) { + return provider.trim().replace(/[^a-z0-9]+/gi, "").toLowerCase() +} + +function loadProviderIconMarkup(provider: string) { + const loader = iconLoadersBySlug[toProviderSlug(provider)] + return loader ? loader() : null +} + +function getSvgViewBox(markup: string) { + return markup.match(/viewBox="([^"]+)"/i)?.[1] ?? "0 0 24 24" +} + +function getSvgInnerMarkup(markup: string) { + return markup + .replace(/^]*>/i, "") + .replace(/<\/svg>\s*$/i, "") + .trim() +} + +export function OAuthProviderIcon({ + provider, + title, + ...props +}: SVGProps & { provider: string; title?: string }) { + const providerSlug = toProviderSlug(provider) + const [iconMarkup, setIconMarkup] = useState(() => { + 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) + return + } + + let cancelled = false + loadPromise + .then((markup) => { + if (cancelled) return + iconMarkupCache.set(providerSlug, markup) + setIconMarkup(markup) + }) + .catch(() => { + if (cancelled) return + iconMarkupCache.set(providerSlug, null) + setIconMarkup(null) + }) + + return () => { + cancelled = true + } + }, [provider, providerSlug]) + + if (!iconMarkup) { + return null + } + return ( - - + + {title ? {title} : null} + ) } diff --git a/src/routes/login.tsx b/src/routes/login.tsx index 6a48c01..b341429 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -8,7 +8,7 @@ import { FormLabel, FormMessage, } from "@/components/ui/form" -import { GitHubIcon } from "@/components/ui/icon" +import { OAuthProviderIcon } from "@/components/ui/icon" import { Input } from "@/components/ui/input" import { Separator } from "@/components/ui/separator" import { useAuth } from "@/hooks/useAuth" @@ -120,10 +120,11 @@ function Login() {
{settingData?.config?.oauth2_providers?.map((p: string) => ( ))} diff --git a/src/routes/profile.tsx b/src/routes/profile.tsx index 761f7af..b5e5506 100644 --- a/src/routes/profile.tsx +++ b/src/routes/profile.tsx @@ -4,6 +4,7 @@ import { ProfileCard } from "@/components/profile" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { OAuthProviderIcon } from "@/components/ui/icon" import { useMainStore } from "@/hooks/useMainStore" import { useMediaQuery } from "@/hooks/useMediaQuery" import { useServer } from "@/hooks/useServer" @@ -107,8 +108,15 @@ export default function ProfilePage() { {settingData?.config?.oauth2_providers?.map((provider) => ( -
+
+

{provider}:

{profile.oauth2_bind?.[provider.toLowerCase()] && (