diff --git a/src/equicordplugins/githubRepos/colors.ts b/src/equicordplugins/githubRepos/colors.ts index e674e1d79..a75505102 100644 --- a/src/equicordplugins/githubRepos/colors.ts +++ b/src/equicordplugins/githubRepos/colors.ts @@ -4,64 +4,64 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -export function getLanguageColor(language: string): string { - const colors: Record = { - "JavaScript": "#f1e05a", - "TypeScript": "#3178c6", - "Python": "#3572A5", - "Java": "#b07219", - "C#": "#178600", - "C++": "#f34b7d", - "C": "#555555", - "HTML": "#e34c26", - "CSS": "#563d7c", - "PHP": "#4F5D95", - "Ruby": "#701516", - "Go": "#00ADD8", - "Rust": "#dea584", - "Swift": "#ffac45", - "Kotlin": "#A97BFF", - "Dart": "#00B4AB", - "Shell": "#89e051", - "PowerShell": "#012456", - "Lua": "#000080", - "Perl": "#0298c3", - "R": "#198CE7", - "Scala": "#c22d40", - "Haskell": "#5e5086", - "Elixir": "#6e4a7e", - "Clojure": "#db5855", - "Vue": "#41b883", - "Svelte": "#ff3e00", - "Jupyter Notebook": "#DA5B0B", - "Assembly": "#6E4C13", - "COBOL": "#004B85", - "CoffeeScript": "#244776", - "Crystal": "#000100", - "D": "#BA595E", - "F#": "#B845FC", - "Fortran": "#4d41b1", - "GLSL": "#5686A5", - "Groovy": "#e69f56", - "Julia": "#a270ba", - "Markdown": "#083fa1", - "MATLAB": "#bb92ac", - "Objective-C": "#438eff", - "OCaml": "#3be133", - "Pascal": "#E3F171", - "Prolog": "#74283c", - "PureScript": "#1D222D", - "Racket": "#3c5caa", - "Raku": "#0000fb", - "Reason": "#ff5847", - "SCSS": "#c6538c", - "Solidity": "#AA6746", - "Tcl": "#e4cc98", - "Verilog": "#b2b7f8", - "VHDL": "#adb2cb", - "WebAssembly": "#04133b", - "Zig": "#ec915c" - }; +const colors: Record = { + "JavaScript": "#f1e05a", + "TypeScript": "#3178c6", + "Python": "#3572A5", + "Java": "#b07219", + "C#": "#178600", + "C++": "#f34b7d", + "C": "#555555", + "HTML": "#e34c26", + "CSS": "#563d7c", + "PHP": "#4F5D95", + "Ruby": "#701516", + "Go": "#00ADD8", + "Rust": "#dea584", + "Swift": "#ffac45", + "Kotlin": "#A97BFF", + "Dart": "#00B4AB", + "Shell": "#89e051", + "PowerShell": "#012456", + "Lua": "#000080", + "Perl": "#0298c3", + "R": "#198CE7", + "Scala": "#c22d40", + "Haskell": "#5e5086", + "Elixir": "#6e4a7e", + "Clojure": "#db5855", + "Vue": "#41b883", + "Svelte": "#ff3e00", + "Jupyter Notebook": "#DA5B0B", + "Assembly": "#6E4C13", + "COBOL": "#004B85", + "CoffeeScript": "#244776", + "Crystal": "#000100", + "D": "#BA595E", + "F#": "#B845FC", + "Fortran": "#4d41b1", + "GLSL": "#5686A5", + "Groovy": "#e69f56", + "Julia": "#a270ba", + "Markdown": "#083fa1", + "MATLAB": "#bb92ac", + "Objective-C": "#438eff", + "OCaml": "#3be133", + "Pascal": "#E3F171", + "Prolog": "#74283c", + "PureScript": "#1D222D", + "Racket": "#3c5caa", + "Raku": "#0000fb", + "Reason": "#ff5847", + "SCSS": "#c6538c", + "Solidity": "#AA6746", + "Tcl": "#e4cc98", + "Verilog": "#b2b7f8", + "VHDL": "#adb2cb", + "WebAssembly": "#04133b", + "Zig": "#ec915c" +}; +export function getLanguageColor(language: string): string { return colors[language] || "#858585"; } diff --git a/src/equicordplugins/githubRepos/components/ProfilePopoutComponent.tsx b/src/equicordplugins/githubRepos/components/ProfilePopoutComponent.tsx index 88591a36d..59f129686 100644 --- a/src/equicordplugins/githubRepos/components/ProfilePopoutComponent.tsx +++ b/src/equicordplugins/githubRepos/components/ProfilePopoutComponent.tsx @@ -6,11 +6,12 @@ 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 { fetchOrgRepos, fetchReposByUserId, fetchReposByUsername, fetchUserInfo, fetchUserOrgs, GitHubUserInfo } from "@equicordplugins/githubRepos/githubApi"; +import { GitHubRepo, RepoGroup } from "@equicordplugins/githubRepos/types"; +import { buildRepoGroups, getLanguageIconUrl, PERSONAL_GROUP_KEY } from "@equicordplugins/githubRepos/utils"; import { classes } from "@utils/misc"; import { findCssClassesLazy } from "@webpack"; -import { Clickable, openModal,React, useEffect, UserProfileStore, useState } from "@webpack/common"; +import { Clickable, openModal, React, useEffect, UserProfileStore, useState } from "@webpack/common"; import { ReposModal } from "./ReposModal"; @@ -19,39 +20,23 @@ const ProfileCardClasses = findCssClassesLazy("cardsList", "firstCardContainer", 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@develop/icons/github/github-original.svg"; - - const normalized = LANGUAGE_MAP[language.toLowerCase()] ?? language.toLowerCase().replace(/\s+/g, ""); - return `https://cdn.jsdelivr.net/gh/devicons/devicon@develop/icons/${normalized}/${normalized}-original.svg`; -} - export function ProfilePopoutComponent({ id, isSideBar = false }: { id: string, isSideBar?: boolean; }) { const [repos, setRepos] = useState([]); + const [groups, setGroups] = 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 => ( - + )); }; @@ -73,12 +58,17 @@ export function ProfilePopoutComponent({ id, isSideBar = false }: { id: string, const githubId = githubConnection.id; - const reposById = await fetchReposByUserId(githubId); - if (reposById) { setRepos(reposById); setLoading(false); return; } + let personalRepos = await fetchReposByUserId(githubId); + if (!personalRepos) personalRepos = await fetchReposByUsername(username); - const reposByUsername = await fetchReposByUsername(username); - setRepos(reposByUsername); + setRepos(personalRepos); setLoading(false); + + const orgs = await fetchUserOrgs(username); + const orgReposEntries = await Promise.all(orgs.map(async org => [org.login, await fetchOrgRepos(org.login)])); + const orgRepos = Object.fromEntries(orgReposEntries); + + setGroups(buildRepoGroups(userInfoData?.username ?? username, personalRepos, orgs, orgRepos, userInfoData?.avatarUrl)); } catch (error) { setError(error instanceof Error ? error.message : "Failed to fetch repositories"); setLoading(false); diff --git a/src/equicordplugins/githubRepos/components/ProfileTabComponent.tsx b/src/equicordplugins/githubRepos/components/ProfileTabComponent.tsx index 8b79bbcdd..a537d9b45 100644 --- a/src/equicordplugins/githubRepos/components/ProfileTabComponent.tsx +++ b/src/equicordplugins/githubRepos/components/ProfileTabComponent.tsx @@ -5,32 +5,24 @@ */ import { BaseText } from "@components/BaseText"; -import { fetchReposByUserId, fetchReposByUsername, fetchUserInfo, GitHubUserInfo } from "@equicordplugins/githubRepos/githubApi"; -import { GitHubRepo } from "@equicordplugins/githubRepos/types"; -import { openModal,React, useEffect, UserProfileStore, useState } from "@webpack/common"; +import { fetchOrgRepos, fetchReposByUserId, fetchReposByUsername, fetchUserInfo, fetchUserOrgs } from "@equicordplugins/githubRepos/githubApi"; +import { GitHubRepo, RepoGroup, RepoSortMode } from "@equicordplugins/githubRepos/types"; +import { buildRepoGroups, PERSONAL_GROUP_KEY, sortGroups } from "@equicordplugins/githubRepos/utils"; +import { React, useEffect, UserProfileStore, useState } from "@webpack/common"; import { cl, settings } from ".."; import { RepoCard } from "./RepoCard"; -import { ReposModal } from "./ReposModal"; +import { RepoSubTabs } from "./RepoSubTabs"; export function ProfileTabComponent({ id }: { id: string, theme: string; }) { - const [repos, setRepos] = useState([]); + const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [userInfo, setUserInfo] = useState(null); + const [activeKey, setActiveKey] = useState(PERSONAL_GROUP_KEY); + const [sortMode, setSortMode] = useState("count"); - const openReposModal = () => { - if (!userInfo) return; - - const sortedRepos = [...repos].sort((a, b) => b.stargazers_count - a.stargazers_count); - openModal(props => ( - - )); - }; + const sortedGroups = sortGroups(groups, sortMode); + const activeGroup = sortedGroups.find(g => g.key === activeKey) ?? sortedGroups[0]; useEffect(() => { const fetchData = async () => { @@ -55,22 +47,19 @@ export function ProfileTabComponent({ id }: { id: string, theme: string; }) { const username = githubConnection.name; const userInfoData = await fetchUserInfo(username); - if (userInfoData) { - setUserInfo(userInfoData); - } - const githubId = githubConnection.id; // Try to fetch by ID first, fall back to username - const reposById = await fetchReposByUserId(githubId); - if (reposById) { - setRepos(reposById); - setLoading(false); - return; - } + let personalRepos: GitHubRepo[] | null = await fetchReposByUserId(githubId); + if (!personalRepos) personalRepos = await fetchReposByUsername(username); - const reposByUsername = await fetchReposByUsername(username); - setRepos(reposByUsername); + const orgs = await fetchUserOrgs(username); + const orgReposEntries = await Promise.all(orgs.map(async org => [org.login, await fetchOrgRepos(org.login)])); + const orgRepos = Object.fromEntries(orgReposEntries); + + const builtGroups = buildRepoGroups(userInfoData?.username ?? username, personalRepos ?? [], orgs, orgRepos, userInfoData?.avatarUrl); + setGroups(builtGroups); + setActiveKey(builtGroups[0]?.key ?? PERSONAL_GROUP_KEY); setLoading(false); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to fetch repositories"; @@ -92,20 +81,20 @@ export function ProfileTabComponent({ id }: { id: string, theme: string; }) { Error: {error} ; - if (!repos.length) return null; + if (!groups.length) return null; return (
- - GitHub Repositories - {userInfo && ( - - {` (${repos.length})`} - - )} - + setSortMode(mode => mode === "count" ? "alpha" : "count")} + canSort={sortedGroups.length > 2} + />
- {repos.map(repo => ( + {activeGroup?.repos.map(repo => ( window.open(repo.html_url, "_blank"); - - const renderStars = () => { - if (!showStars) return null; - - return ( -
- - {repo.stargazers_count.toLocaleString()} -
- ); - }; - - const renderLink = () => { - return ( -
- -
- ); - }; - - const renderLanguage = () => { - if (!showLanguage || !repo.language) return null; - - return ( -
- - {repo.language} - {renderStars()} -
- ); - }; + const langColor = getLanguageColor(repo?.language); return ( - <> - {repo.description ? ( - - {({ onMouseLeave, onMouseEnter }) => ( -
-
- - {repo.name} - - {renderLink()} -
- {renderLanguage()} -
- )} -
- ) : ( -
-
- - {repo.name} - - {renderLink()} +
+
+ +
+
+ + {repo.name} + + {repo.description && ( + + {repo.description} + + )} + {(showLanguage || showStars) && ( +
+ {showLanguage && repo.language && ( + + + {repo.language} + + )} + {showStars && ( + + + {repo.stargazers_count.toLocaleString()} + + )}
- {renderLanguage()} -
- )} - + )} +
+ +
); } diff --git a/src/equicordplugins/githubRepos/components/RepoSubTabs.tsx b/src/equicordplugins/githubRepos/components/RepoSubTabs.tsx new file mode 100644 index 000000000..9b92831f8 --- /dev/null +++ b/src/equicordplugins/githubRepos/components/RepoSubTabs.tsx @@ -0,0 +1,60 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { RepoGroup, RepoSortMode } from "@equicordplugins/githubRepos/types"; +import { classes } from "@utils/misc"; +import { React } from "@webpack/common"; + +import { cl } from ".."; +import { SortIcon } from "./SortIcon"; + +interface RepoSubTabsProps { + groups: RepoGroup[]; + activeKey: string; + onSelect: (key: string) => void; + sortMode: RepoSortMode; + onToggleSort: () => void; + canSort: boolean; +} + +export function RepoSubTabs({ groups, activeKey, onSelect, sortMode, onToggleSort, canSort }: RepoSubTabsProps) { + if (groups.length <= 1 && !canSort) return null; + + const handleWheel = (e: React.WheelEvent) => { + if (e.deltaY === 0) return; + e.currentTarget.scrollLeft += e.deltaY; + }; + + return ( +
+
+ {groups.map(group => ( +
onSelect(group.key)} + > + {group.avatarUrl && ( + + )} + {group.label} + {group.repos.length} +
+ ))} +
+ {canSort && ( +
+ + {sortMode === "count" ? "Most repos" : "A-Z"} +
+ )} +
+ ); +} diff --git a/src/equicordplugins/githubRepos/components/ReposModal.tsx b/src/equicordplugins/githubRepos/components/ReposModal.tsx index 885c9a643..0e8fda0a3 100644 --- a/src/equicordplugins/githubRepos/components/ReposModal.tsx +++ b/src/equicordplugins/githubRepos/components/ReposModal.tsx @@ -4,61 +4,28 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { getLanguageColor } from "@equicordplugins/githubRepos/colors"; -import { GitHubRepo } from "@equicordplugins/githubRepos/types"; +import { RepoGroup, RepoSortMode } from "@equicordplugins/githubRepos/types"; +import { PERSONAL_GROUP_KEY, sortGroups } from "@equicordplugins/githubRepos/utils"; import { RenderModalProps } from "@vencord/discord-types"; -import { Modal, React } from "@webpack/common"; +import { Modal, React, useState } from "@webpack/common"; -import { cl } from ".."; -import { Star } from "./Star"; +import { cl, settings } from ".."; +import { RepoCard } from "./RepoCard"; +import { RepoSubTabs } from "./RepoSubTabs"; interface ReposModalProps { - repos: GitHubRepo[]; + groups: RepoGroup[]; + initialActiveKey: string; username: string; rootProps: any; } -export function ReposModal({ repos, username, rootProps }: ReposModalProps & { rootProps: RenderModalProps; }) { - const renderTableHeader = () => ( - - - Repository - Description - Language - Stars - - - ); +export function ReposModal({ groups, initialActiveKey, username, rootProps }: ReposModalProps & { rootProps: RenderModalProps; }) { + const [activeKey, setActiveKey] = useState(initialActiveKey); + const [sortMode, setSortMode] = useState("count"); - const renderTableRow = (repo: GitHubRepo) => ( - window.open(repo.html_url, "_blank")}> - -
{repo.name}
- - -
- {repo.description || ""} -
- - - {repo.language && ( -
- - {repo.language} -
- )} - - -
- - {repo.stargazers_count.toLocaleString()} -
- - - ); + const sortedGroups = sortGroups(groups, sortMode); + const activeGroup = sortedGroups.find(g => g.key === activeKey) ?? sortedGroups[0]; return ( -
- - - - - - - - {renderTableHeader()} - - {repos.map(renderTableRow)} - -
+
+ setSortMode(mode => mode === "count" ? "alpha" : "count")} + canSort={sortedGroups.length > 2} + /> +
+ {activeGroup?.repos.map(repo => ( + + ))} +
); diff --git a/src/equicordplugins/githubRepos/components/SortIcon.tsx b/src/equicordplugins/githubRepos/components/SortIcon.tsx new file mode 100644 index 000000000..125e84695 --- /dev/null +++ b/src/equicordplugins/githubRepos/components/SortIcon.tsx @@ -0,0 +1,26 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { IconProps } from "@equicordplugins/githubRepos/types"; +import { React } from "@webpack/common"; + +export function SortIcon({ className, width = 16, height = 16 }: IconProps) { + return ( + + + + ); +} diff --git a/src/equicordplugins/githubRepos/githubApi.ts b/src/equicordplugins/githubRepos/githubApi.ts index c589de712..949e24709 100644 --- a/src/equicordplugins/githubRepos/githubApi.ts +++ b/src/equicordplugins/githubRepos/githubApi.ts @@ -6,13 +6,14 @@ import { Logger } from "@utils/Logger"; -import { GitHubRepo } from "./types"; +import { GitHubOrg, GitHubRepo } from "./types"; const logger = new Logger("GitHubRepos"); export interface GitHubUserInfo { username: string; totalRepos: number; + avatarUrl: string; } export async function fetchUserInfo(username: string): Promise { @@ -25,7 +26,8 @@ export async function fetchUserInfo(username: string): Promise { + try { + const apiUrl = `https://api.github.com/users/${username}/orgs`; + const response = await fetch(apiUrl); + + if (!response.ok) return []; + + return await response.json(); + } catch (error) { + logger.error("Error fetching user orgs", error); + return []; + } +} + +export async function fetchOrgRepos(org: string, perPage: number = 30): Promise { + try { + const apiUrl = `https://api.github.com/orgs/${org}/repos?sort=stars&direction=desc&per_page=${perPage}`; + const response = await fetch(apiUrl); + + if (!response.ok) return []; + + const data = await response.json(); + return sortReposByStars(data); + } catch (error) { + logger.error("Error fetching org repos", error); + return []; + } +} + function sortReposByStars(repos: GitHubRepo[]): GitHubRepo[] { return repos.sort((a, b) => b.stargazers_count - a.stargazers_count); } diff --git a/src/equicordplugins/githubRepos/styles.css b/src/equicordplugins/githubRepos/styles.css index 4d1791d1a..bbd59053e 100644 --- a/src/equicordplugins/githubRepos/styles.css +++ b/src/equicordplugins/githubRepos/styles.css @@ -4,19 +4,20 @@ gap: 8px; } +.vc-github-repos-header { + color: var(--text-default); +} + .vc-github-repos-list { - display: grid; - grid-template-columns: 260px 260px; - gap: 16px; + display: flex; + flex-direction: column; + gap: 8px; } .vc-github-repos-star-icon { color: var(--text-status-idle); fill: var(--text-status-idle); -} - -.vc-github-repos-lang-name { - font-size: 12px; + flex-shrink: 0; } .vc-github-repos-footer { @@ -32,26 +33,169 @@ ); } -.vc-github-repos-button, .vc-github-repos-show-more { - color: var(--text-default); - width: 100%; + color: var(--text-muted); + font-size: 12px; + font-weight: 500; + cursor: pointer; } -.vc-github-repos-card { - padding: 8px; - border-radius: var(--radius-xs); - border: 1px solid +.vc-github-repos-show-more:hover { + color: var(--text-default); + text-decoration: underline; +} + +/* Sub-tabs */ + +.vc-github-repos-subtabs { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 8px; + min-width: 0; + border-bottom: 1px solid var( --profile-body-divider-color, var(--input-border-default, var(--input-border)) ); + padding-bottom: 6px; } -.vc-github-repos-header { - color: var(--text-default); +.vc-github-repos-subtabs-list { display: flex; - justify-content: space-between; + align-items: center; + gap: 4px; + overflow-x: auto; + flex: 0 1 auto; + min-width: 0; + scrollbar-width: none; +} + +.vc-github-repos-subtabs-list::-webkit-scrollbar { + display: none; +} + +.vc-github-repos-subtab { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: var(--radius-round); + color: var(--text-muted); + font-size: 12px; + font-weight: 500; + white-space: nowrap; + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; +} + +.vc-github-repos-subtab:hover { + background-color: var(--background-mod-subtle); + color: var(--text-default); +} + +.vc-github-repos-subtab-active, +.vc-github-repos-subtab-active:hover { + background-color: var(--background-mod-strong); + color: var(--text-default); +} + +.vc-github-repos-subtab-avatar { + width: 14px; + height: 14px; + border-radius: var(--radius-round); + flex-shrink: 0; +} + +.vc-github-repos-subtab-count { + color: var(--text-faint); +} + +.vc-github-repos-sort-toggle { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: var(--radius-xs); + color: var(--text-muted); + font-size: 12px; + font-weight: 500; + white-space: nowrap; + cursor: pointer; + flex-shrink: 0; + transition: background-color 0.15s ease, color 0.15s ease; +} + +.vc-github-repos-sort-toggle:hover { + background-color: var(--background-mod-subtle); + color: var(--text-default); +} + +.vc-github-repos-sort-icon { + flex-shrink: 0; +} + +/* Repo card (song-spotlight style) */ + +.vc-github-repos-card { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 8px; + border-radius: var(--radius-sm); + cursor: pointer; + position: relative; + transition: background-color 0.15s ease; +} + +.vc-github-repos-card:hover { + background-color: var(--background-mod-subtle); +} + +.vc-github-repos-art { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: var(--radius-sm); + flex-shrink: 0; +} + +.vc-github-repos-art-icon { + width: 20px; + height: 20px; + object-fit: contain; +} + +.vc-github-repos-info { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1; + gap: 1px; +} + +.vc-github-repos-name { + color: var(--text-strong); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.vc-github-repos-description { + color: var(--text-muted); + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.vc-github-repos-meta { + display: flex; + align-items: center; + gap: 10px; + margin-top: 2px; } .vc-github-repos-stars { @@ -59,14 +203,15 @@ align-items: center; gap: 4px; color: var(--text-muted); + font-size: 11px; } .vc-github-repos-language { display: flex; align-items: center; - gap: 6px; + gap: 5px; color: var(--text-muted); - font-size: 12px; + font-size: 11px; } .vc-github-repos-language-color { @@ -76,119 +221,42 @@ display: inline-block; } +.vc-github-repos-link { + cursor: pointer; + fill: var(--interactive-icon-default); + flex-shrink: 0; + opacity: 0; + transition: opacity 0.15s ease; +} + +.vc-github-repos-link:hover { + fill: var(--interactive-icon-hover); +} + +.vc-github-repos-card:hover .vc-github-repos-link { + opacity: 1; +} + +/* Modal */ + .vc-github-repos-modal { width: 100%; max-width: 900px; background-color: var(--background-base-lower); } -.vc-github-repos-modal-title { - color: var(--text-strong); - font-size: 20px; - font-weight: 600; -} - .vc-github-repos-modal-content { + display: flex; + flex-direction: column; + gap: 12px; padding: 16px 0; - overflow: hidden; } -.vc-github-repos-table-container { - width: 100%; - overflow-x: auto; -} - -.vc-github-repos-table { - width: 100%; - border-collapse: collapse; - color: var(--text-default); - font-size: 14px; - table-layout: fixed; -} - -.vc-github-repos-table colgroup { - display: table-column-group; -} - -.vc-github-repos-table col { - display: table-column; -} - -.vc-github-repos-table thead { - border-bottom: 2px solid var(--input-border-default, var(--input-border)); -} - -.vc-github-repos-table th { - text-align: left; - padding: 10px 16px; - font-weight: 600; - color: var(--text-strong); - border-bottom: 2px solid var(--input-border-default, var(--input-border)); -} - -.vc-github-repos-table th:last-child { - text-align: center; -} - -.vc-github-repos-table tbody tr { - border-bottom: 1px solid var(--input-border-default, var(--input-border)); - cursor: pointer; - transition: background-color 0.2s ease; -} - -.vc-github-repos-table tbody tr:hover { - background-color: var(--background-mod-subtle); -} - -.vc-github-repos-table td { - padding: 12px 16px; - vertical-align: middle; - border-bottom: 1px solid var(--input-border-default, var(--input-border)); - overflow: hidden; - text-overflow: ellipsis; -} - -.vc-github-repos-table-name { - font-weight: 500; - color: var(--text-strong); - display: flex; - align-items: center; - gap: 6px; -} - -.vc-github-repos-table-fork-icon { - color: var(--text-muted); -} - -.vc-github-repos-table-description { - color: var(--text-default); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.vc-github-repos-table-language { - display: flex; - align-items: center; - gap: 6px; - white-space: nowrap; -} - -.vc-github-repos-table-language-color { - width: 10px; - height: 10px; - border-radius: 50%; - display: inline-block; - flex-shrink: 0; -} - -.vc-github-repos-table-stars { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - white-space: nowrap; - text-align: center; +.vc-github-repos-modal-list { + max-height: 60vh; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thin-thumb) var(--scrollbar-thin-track); } .vc-github-repos-modal-footer-close { @@ -216,40 +284,11 @@ margin-top: 10px; } -.vc-github-repos-count { - color: var(--text-muted); -} - .vc-github-repos-error { color: var(--text-feedback-critical); margin-top: 10px; } -.vc-github-repos-link { - cursor: pointer; - fill: var(--interactive-icon-default); -} - -.vc-github-repos-name { - color: var(--interactive-icon-active); -} - -.vc-github-repos-header-repo { - width: 20%; -} - -.vc-github-repos-header-description { - width: 50%; -} - -.vc-github-repos-header-language { - width: 15%; -} - -.vc-github-repos-header-stars { - width: 10%; -} - /* Tab variant styles */ .vc-github-repos-container.vc-github-repos-tab { @@ -259,15 +298,13 @@ width: 100%; box-sizing: border-box; display: grid; - grid-template-rows: auto 1fr; - gap: 4px; + grid-template-rows: auto auto 1fr auto; + gap: 6px; overflow: hidden; position: relative; } .vc-github-repos-container.vc-github-repos-tab .vc-github-repos-list { - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 4px; overflow: hidden scroll; scrollbar-width: thin; scrollbar-color: var(--scrollbar-thin-thumb) var(--scrollbar-thin-track); @@ -276,27 +313,28 @@ .vc-github-repos-container.vc-github-repos-tab .vc-github-repos-card { padding: 4px 6px; - cursor: pointer; } -.vc-github-repos-container.vc-github-repos-tab .vc-github-repos-language-color { - width: 8px; - height: 8px; +.vc-github-repos-container.vc-github-repos-tab .vc-github-repos-art { + width: 32px; + height: 32px; +} + +.vc-github-repos-container.vc-github-repos-tab .vc-github-repos-art-icon { + width: 16px; + height: 16px; } .vc-github-repos-container.vc-github-repos-tab .vc-github-repos-name { - font-size: 14px; - line-height: 1.1; + font-size: 13px; + line-height: 1.2; +} + +.vc-github-repos-container.vc-github-repos-tab .vc-github-repos-description { + font-size: 11px; } .vc-github-repos-container.vc-github-repos-tab .vc-github-repos-language, .vc-github-repos-container.vc-github-repos-tab .vc-github-repos-stars { - font-size: 11px; - line-height: 1.1; - gap: 3px; -} - -.vc-github-repos-container.vc-github-repos-tab .vc-github-repos-star-icon, -.vc-github-repos-container.vc-github-repos-tab .vc-github-repos-stars { - margin-left: 2px; + font-size: 10px; } diff --git a/src/equicordplugins/githubRepos/types.ts b/src/equicordplugins/githubRepos/types.ts index 3cbdc4e6a..b059508f3 100644 --- a/src/equicordplugins/githubRepos/types.ts +++ b/src/equicordplugins/githubRepos/types.ts @@ -12,8 +12,26 @@ export interface GitHubRepo { stargazers_count: number; language: string; fork: boolean; + owner: { + login: string; + type: string; + }; } +export interface GitHubOrg { + login: string; + avatar_url: string; +} + +export interface RepoGroup { + key: string; + label: string; + avatarUrl?: string; + repos: GitHubRepo[]; +} + +export type RepoSortMode = "count" | "alpha"; + export interface IconProps { className?: string; width?: number; diff --git a/src/equicordplugins/githubRepos/utils.ts b/src/equicordplugins/githubRepos/utils.ts new file mode 100644 index 000000000..1d7377b9b --- /dev/null +++ b/src/equicordplugins/githubRepos/utils.ts @@ -0,0 +1,61 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { GitHubOrg, GitHubRepo, RepoGroup, RepoSortMode } from "./types"; + +const LANGUAGE_ICON_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", +}; + +export function getLanguageIconUrl(language: string | null): string { + if (!language) return "https://cdn.jsdelivr.net/gh/devicons/devicon@develop/icons/github/github-original.svg"; + + const normalized = LANGUAGE_ICON_MAP[language.toLowerCase()] ?? language.toLowerCase().replace(/\s+/g, ""); + return `https://cdn.jsdelivr.net/gh/devicons/devicon@develop/icons/${normalized}/${normalized}-original.svg`; +} + +export const PERSONAL_GROUP_KEY = "__personal__"; + +export function buildRepoGroups(username: string, personalRepos: GitHubRepo[], orgs: GitHubOrg[], orgRepos: Record, avatarUrl?: string): RepoGroup[] { + const groups: RepoGroup[] = []; + + if (personalRepos.length) { + groups.push({ key: PERSONAL_GROUP_KEY, label: username, avatarUrl, repos: personalRepos }); + } + + for (const org of orgs) { + const repos = orgRepos[org.login]; + if (repos?.length) { + groups.push({ key: org.login, label: org.login, avatarUrl: org.avatar_url, repos }); + } + } + + return groups; +} + +export function sortGroups(groups: RepoGroup[], mode: RepoSortMode): RepoGroup[] { + const personal = groups.find(g => g.key === PERSONAL_GROUP_KEY); + const rest = groups.filter(g => g.key !== PERSONAL_GROUP_KEY); + + const sorted = [...rest].sort((a, b) => { + return mode === "alpha" + ? a.label.localeCompare(b.label) + : b.repos.length - a.repos.length; + }); + + return personal ? [personal, ...sorted] : sorted; +}