diff --git a/src/equicordplugins/githubRepos/components/ProfilePopoutComponent.tsx b/src/equicordplugins/githubRepos/components/ProfilePopoutComponent.tsx new file mode 100644 index 000000000..c49535aab --- /dev/null +++ b/src/equicordplugins/githubRepos/components/ProfilePopoutComponent.tsx @@ -0,0 +1,138 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { Span } from "@components/Span"; +import { fetchReposByUserId, fetchReposByUsername, fetchUserInfo, GitHubUserInfo } from "@equicordplugins/githubRepos/githubApi"; +import { GitHubRepo } from "@equicordplugins/githubRepos/types"; +import { classes } from "@utils/misc"; +import { openModal } from "@utils/modal"; +import { findCssClassesLazy } from "@webpack"; +import { Clickable, React, useEffect, UserProfileStore, useState } from "@webpack/common"; + +import { ReposModal } from "./ReposModal"; + +const ProfileCardClasses = findCssClassesLazy("cardsList", "firstCardContainer", "card", "container"); +const ProfileCardContainerClasses = findCssClassesLazy("innerContainer", "icons", "icon", "displayCount", "displayCountText", "displayCountTextColor", "breadcrumb"); +const ProfileCardOverlayClasses = findCssClassesLazy("overlay", "isPrivate", "outer"); + +const LANGUAGE_MAP: Record = { + "c++": "cplusplus", + "c#": "csharp", + "f#": "fsharp", + "q#": "qsharp", + "objective-c": "objectivec", + "visual basic": "visualbasic", + "shell": "bash", + "batchfile": "bash", + "vim script": "vim", + "dockerfile": "docker", + "gdscript": "godot", + "html": "html5", +}; + +function getLanguageIconUrl(language: string | null): string { + if (!language) return "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/github/github-original.svg"; + + const normalized = LANGUAGE_MAP[language.toLowerCase()] ?? language.toLowerCase().replace(/\s+/g, ""); + return `https://cdn.jsdelivr.net/gh/devicons/devicon/icons/${normalized}/${normalized}-original.svg`; +} + +export function ProfilePopoutComponent({ id, theme }: { id: string, theme: string; }) { + const [repos, setRepos] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [userInfo, setUserInfo] = useState(null); + + const openReposModal = () => { + if (!userInfo) return; + const sortedRepos = [...repos].sort((a, b) => b.stargazers_count - a.stargazers_count); + openModal(props => ( + + )); + }; + + useEffect(() => { + const fetchData = async () => { + try { + const profile = UserProfileStore.getUserProfile(id); + if (!profile) { setLoading(false); return; } + + const connections = profile.connectedAccounts; + if (!connections?.length) { setLoading(false); return; } + + const githubConnection = connections.find(conn => conn.type === "github"); + if (!githubConnection) { setLoading(false); return; } + + const username = githubConnection.name; + const userInfoData = await fetchUserInfo(username); + if (userInfoData) setUserInfo(userInfoData); + + const githubId = githubConnection.id; + + const reposById = await fetchReposByUserId(githubId); + if (reposById) { setRepos(reposById); setLoading(false); return; } + + const reposByUsername = await fetchReposByUsername(username); + setRepos(reposByUsername); + setLoading(false); + } catch (error) { + setError(error instanceof Error ? error.message : "Failed to fetch repositories"); + setLoading(false); + } + }; + + fetchData(); + }, [id]); + + if (loading || error || !repos.length) return null; + + const topRepos = repos.slice(0, 4); + + return ( +
+
    +
  • + +
    + + GitHub Repositories + + {!!repos.length && ( +
    + {topRepos.slice(0, 4).map((repo, idx) => { + const showCount = idx === 3 && repos.length > 4; + + return ( +
    + {repo.language { + (e.target as HTMLImageElement).src = "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/github/github-original.svg"; + }} + /> + {showCount && ( +
    + + +{repos.length - 3} + +
    + )} +
    + ); + })} +
    + )} +
    +
    +
  • +
+
+ ); +} diff --git a/src/equicordplugins/githubRepos/components/GitHubReposComponent.tsx b/src/equicordplugins/githubRepos/components/ProfileTabComponent.tsx similarity index 71% rename from src/equicordplugins/githubRepos/components/GitHubReposComponent.tsx rename to src/equicordplugins/githubRepos/components/ProfileTabComponent.tsx index ea9e1e775..3008f94a0 100644 --- a/src/equicordplugins/githubRepos/components/GitHubReposComponent.tsx +++ b/src/equicordplugins/githubRepos/components/ProfileTabComponent.tsx @@ -5,7 +5,6 @@ */ import { BaseText } from "@components/BaseText"; -import { Button, TextButton } from "@components/Button"; import { fetchReposByUserId, fetchReposByUsername, fetchUserInfo, GitHubUserInfo } from "@equicordplugins/githubRepos/githubApi"; import { GitHubRepo } from "@equicordplugins/githubRepos/types"; import { openModal } from "@utils/modal"; @@ -15,12 +14,11 @@ import { cl, settings } from ".."; import { RepoCard } from "./RepoCard"; import { ReposModal } from "./ReposModal"; -export function GitHubReposComponent({ id, theme, variant = "popout" }: { id: string, theme: string; variant?: "popout" | "tab"; }) { +export function ProfileTabComponent({ id }: { id: string, theme: string; }) { const [repos, setRepos] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [userInfo, setUserInfo] = useState(null); - const [returnJustButton, setReturnJustButton] = useState(false); const openReposModal = () => { if (!userInfo) return; @@ -64,8 +62,6 @@ export function GitHubReposComponent({ id, theme, variant = "popout" }: { id: st const githubId = githubConnection.id; - if (!settings.store.showInMiniProfile && variant !== "tab") setReturnJustButton(true); - // Try to fetch by ID first, fall back to username const reposById = await fetchReposByUserId(githubId); if (reposById) { @@ -99,56 +95,26 @@ export function GitHubReposComponent({ id, theme, variant = "popout" }: { id: st if (!repos.length) return null; - if (returnJustButton) { - return ( - - ); - } - - const topRepos = variant === "tab" ? repos : repos.slice(0, 4); - return ( -
+
GitHub Repositories {userInfo && ( - {variant === "tab" - ? ` (${repos.length})` - : ` (Showing only top ${topRepos.length}/${userInfo.totalRepos})` - } + {` (${repos.length})`} )}
- {topRepos.map(repo => ( + {repos.map(repo => ( )) }
- {variant !== "tab" && ( -
- - Show More - -
- )}
); } diff --git a/src/equicordplugins/githubRepos/components/RepoCard.tsx b/src/equicordplugins/githubRepos/components/RepoCard.tsx index f4a4c0c50..3512b1516 100644 --- a/src/equicordplugins/githubRepos/components/RepoCard.tsx +++ b/src/equicordplugins/githubRepos/components/RepoCard.tsx @@ -12,7 +12,7 @@ import { React, Tooltip } from "@webpack/common"; import { cl } from ".."; import { Star } from "./Star"; -export function RepoCard({ repo, showStars, showLanguage, variant }: RepoCardProps & { variant?: "popout" | "tab"; }) { +export function RepoCard({ repo, showStars, showLanguage }: RepoCardProps) { const handleClick = () => window.open(repo.html_url, "_blank"); const renderStars = () => { @@ -62,8 +62,6 @@ export function RepoCard({ repo, showStars, showLanguage, variant }: RepoCardPro ); }; - const cardProps = variant === "tab" ? { onClick: handleClick } : {}; - return ( <> {repo.description ? ( @@ -72,7 +70,7 @@ export function RepoCard({ repo, showStars, showLanguage, variant }: RepoCardPro
@@ -85,7 +83,7 @@ export function RepoCard({ repo, showStars, showLanguage, variant }: RepoCardPro )} ) : ( -
+
{repo.name} diff --git a/src/equicordplugins/githubRepos/index.tsx b/src/equicordplugins/githubRepos/index.tsx index c621e4991..74282ba0f 100644 --- a/src/equicordplugins/githubRepos/index.tsx +++ b/src/equicordplugins/githubRepos/index.tsx @@ -7,7 +7,6 @@ import "./styles.css"; import { definePluginSettings } from "@api/Settings"; -import { BaseText } from "@components/BaseText"; import ErrorBoundary from "@components/ErrorBoundary"; import { EquicordDevs } from "@utils/constants"; import { classNameFactory } from "@utils/css"; @@ -16,7 +15,10 @@ import { User } from "@vencord/discord-types"; import { findByCodeLazy } from "@webpack"; import { React } from "@webpack/common"; -import { GitHubReposComponent } from "./components/GitHubReposComponent"; +import { ProfilePopoutComponent } from "./components/ProfilePopoutComponent"; +import { ProfileTabComponent } from "./components/ProfileTabComponent"; + +const getProfileThemeProps = findByCodeLazy(".getPreviewThemeColors", "primaryColor:"); export const cl = classNameFactory("vc-github-repos-"); @@ -31,11 +33,6 @@ export const settings = definePluginSettings({ description: "Show repository language", default: true }, - showInMiniProfile: { - type: OptionType.BOOLEAN, - description: "Show full ui in the mini profile instead of just a button", - default: true - }, showRepositoryTab: { type: OptionType.BOOLEAN, description: "Show repositories tab in profile modal (hides button in connections when enabled)", @@ -43,40 +40,6 @@ export const settings = definePluginSettings({ }, }); -const getProfileThemeProps = findByCodeLazy(".getPreviewThemeColors", "primaryColor:"); - -const ProfilePopoutComponent = ErrorBoundary.wrap( - (props: { user: User; displayProfile?: any; }) => { - return ( - - ); - }, - { - noop: true, - fallback: () => - Error, Failed to render GithubRepos - - } -); - -const ProfileRepositoriesTab = ErrorBoundary.wrap( - (props: { user: User; displayProfile?: any; }) => { - return ( - - ); - }, - { noop: true } -); - export default definePlugin({ name: "GitHubRepos", description: "Displays a user's public GitHub repositories in their profile", @@ -88,8 +51,8 @@ export default definePlugin({ { find: /onOpenUserProfileModal:\i\}\),\i/, replacement: { - match: /userId:\i\.id,guild:\i.{0,15}\}\).{0,100}(?=\])/, - replace: "$&,$self.ProfilePopoutComponent(arguments[0])" + match: /user:\i,widgets:.{0,100}?\}\),/, + replace: "$&$self.ProfileRepositoriesPopout(arguments[0])," } }, // User Profile Modal v2 @@ -97,7 +60,7 @@ export default definePlugin({ find: ".MODAL_V2,onClose:", replacement: { match: /displayProfile:(\i).*?connections:\i.{0,25}\i.\i\}\)\}\)/, - replace: "$&,$self.ProfilePopoutComponent({ user: arguments[0].user, displayProfile: $1 }),", + replace: "$&,$self.ProfileRepositoriesPopout({ user: arguments[0].user, displayProfile: $1 }),", predicate: () => !settings.store.showRepositoryTab, } }, @@ -119,6 +82,30 @@ export default definePlugin({ } } ], - ProfilePopoutComponent, - ProfileRepositoriesTab + ProfileRepositoriesPopout: ErrorBoundary.wrap((props: { user: User; displayProfile?: any; }) => { + return ( + + ); + }, + { + noop: true + } + ), + ProfileRepositoriesTab: ErrorBoundary.wrap((props: { user: User; displayProfile?: any; }) => { + return ( + + ); + }, + { + noop: true + } + ) });