GithubRepos: Support User Orgs
This commit is contained in:
parent
88244a36cc
commit
faf2f66616
11 changed files with 593 additions and 437 deletions
|
|
@ -4,64 +4,64 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export function getLanguageColor(language: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
"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<string, string> = {
|
||||
"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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
"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<GitHubRepo[]>([]);
|
||||
const [groups, setGroups] = useState<RepoGroup[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [userInfo, setUserInfo] = useState<GitHubUserInfo | null>(null);
|
||||
|
||||
const openReposModal = () => {
|
||||
if (!userInfo) return;
|
||||
const sortedRepos = [...repos].sort((a, b) => b.stargazers_count - a.stargazers_count);
|
||||
|
||||
openModal(props => (
|
||||
<ReposModal repos={sortedRepos} username={userInfo.username} rootProps={props} />
|
||||
<ReposModal
|
||||
groups={groups.length ? groups : [{ key: PERSONAL_GROUP_KEY, label: userInfo?.username ?? "Personal", avatarUrl: userInfo?.avatarUrl, repos }]}
|
||||
initialActiveKey={PERSONAL_GROUP_KEY}
|
||||
username={userInfo.username}
|
||||
rootProps={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);
|
||||
|
|
|
|||
|
|
@ -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<GitHubRepo[]>([]);
|
||||
const [groups, setGroups] = useState<RepoGroup[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [userInfo, setUserInfo] = useState<GitHubUserInfo | null>(null);
|
||||
const [activeKey, setActiveKey] = useState<string>(PERSONAL_GROUP_KEY);
|
||||
const [sortMode, setSortMode] = useState<RepoSortMode>("count");
|
||||
|
||||
const openReposModal = () => {
|
||||
if (!userInfo) return;
|
||||
|
||||
const sortedRepos = [...repos].sort((a, b) => b.stargazers_count - a.stargazers_count);
|
||||
openModal(props => (
|
||||
<ReposModal
|
||||
repos={sortedRepos}
|
||||
username={userInfo.username}
|
||||
rootProps={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}
|
||||
</BaseText>;
|
||||
|
||||
if (!repos.length) return null;
|
||||
if (!groups.length) return null;
|
||||
|
||||
return (
|
||||
<div className={cl("container", "tab")}>
|
||||
<BaseText size="xs" weight="semibold" className={cl("header")}>
|
||||
GitHub Repositories
|
||||
{userInfo && (
|
||||
<span className={cl("count")}>
|
||||
{` (${repos.length})`}
|
||||
</span>
|
||||
)}
|
||||
</BaseText>
|
||||
<RepoSubTabs
|
||||
groups={sortedGroups}
|
||||
activeKey={activeGroup?.key ?? PERSONAL_GROUP_KEY}
|
||||
onSelect={setActiveKey}
|
||||
sortMode={sortMode}
|
||||
onToggleSort={() => setSortMode(mode => mode === "count" ? "alpha" : "count")}
|
||||
canSort={sortedGroups.length > 2}
|
||||
/>
|
||||
<div className={cl("list")}>
|
||||
{repos.map(repo => (
|
||||
{activeGroup?.repos.map(repo => (
|
||||
<RepoCard
|
||||
key={repo.id}
|
||||
repo={repo}
|
||||
|
|
|
|||
|
|
@ -7,92 +7,62 @@
|
|||
import { BaseText } from "@components/BaseText";
|
||||
import { getLanguageColor } from "@equicordplugins/githubRepos/colors";
|
||||
import { RepoCardProps } from "@equicordplugins/githubRepos/types";
|
||||
import { React, Tooltip } from "@webpack/common";
|
||||
import { getLanguageIconUrl } from "@equicordplugins/githubRepos/utils";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
import { cl } from "..";
|
||||
import { Star } from "./Star";
|
||||
|
||||
export function RepoCard({ repo, showStars, showLanguage }: RepoCardProps) {
|
||||
const handleClick = () => window.open(repo.html_url, "_blank");
|
||||
|
||||
const renderStars = () => {
|
||||
if (!showStars) return null;
|
||||
|
||||
return (
|
||||
<div className={cl("stars")}>
|
||||
<Star className={cl("star-icon")} />
|
||||
<BaseText size="sm">{repo.stargazers_count.toLocaleString()}</BaseText>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLink = () => {
|
||||
return (
|
||||
<div onClick={handleClick}>
|
||||
<svg
|
||||
className={cl("link")}
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8 5a1 1 0 0 0 0 2h7.59L5.29 17.3a1 1 0 1 0 1.42 1.4L17 8.42V16a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1H8Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLanguage = () => {
|
||||
if (!showLanguage || !repo.language) return null;
|
||||
|
||||
return (
|
||||
<div className={cl("language")}>
|
||||
<span
|
||||
className={cl("language-color")}
|
||||
style={{ backgroundColor: getLanguageColor(repo.language) }}
|
||||
/>
|
||||
<BaseText size="sm" className={cl("lang-name")}>{repo.language}</BaseText>
|
||||
{renderStars()}
|
||||
</div >
|
||||
);
|
||||
};
|
||||
const langColor = getLanguageColor(repo?.language);
|
||||
|
||||
return (
|
||||
<>
|
||||
{repo.description ? (
|
||||
<Tooltip text={repo.description} key={repo.id}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<div className={cl("card")}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className={cl("header")}>
|
||||
<BaseText size="sm" weight="medium" className={cl("name")}>
|
||||
{repo.name}
|
||||
</BaseText>
|
||||
{renderLink()}
|
||||
</div>
|
||||
{renderLanguage()}
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className={cl("card")} onClick={handleClick}>
|
||||
<div className={cl("header")}>
|
||||
<BaseText size="sm" weight="medium" className={cl("name")} >
|
||||
{repo.name}
|
||||
</BaseText>
|
||||
{renderLink()}
|
||||
<div className={cl("card")} onClick={handleClick}>
|
||||
<div className={cl("art")} style={{ backgroundColor: `${langColor}2b` }}>
|
||||
<img className={cl("art-icon")} src={getLanguageIconUrl(repo.language)} alt="" />
|
||||
</div>
|
||||
<div className={cl("info")}>
|
||||
<BaseText size="sm" weight="semibold" className={cl("name")}>
|
||||
{repo.name}
|
||||
</BaseText>
|
||||
{repo.description && (
|
||||
<BaseText size="sm" className={cl("description")}>
|
||||
{repo.description}
|
||||
</BaseText>
|
||||
)}
|
||||
{(showLanguage || showStars) && (
|
||||
<div className={cl("meta")}>
|
||||
{showLanguage && repo.language && (
|
||||
<span className={cl("language")}>
|
||||
<span className={cl("language-color")} style={{ backgroundColor: langColor }} />
|
||||
{repo.language}
|
||||
</span>
|
||||
)}
|
||||
{showStars && (
|
||||
<span className={cl("stars")}>
|
||||
<Star className={cl("star-icon")} width={12} height={12} />
|
||||
{repo.stargazers_count.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{renderLanguage()}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<svg
|
||||
className={cl("link")}
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8 5a1 1 0 0 0 0 2h7.59L5.29 17.3a1 1 0 1 0 1.42 1.4L17 8.42V16a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1H8Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
60
src/equicordplugins/githubRepos/components/RepoSubTabs.tsx
Normal file
60
src/equicordplugins/githubRepos/components/RepoSubTabs.tsx
Normal file
|
|
@ -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<HTMLDivElement>) => {
|
||||
if (e.deltaY === 0) return;
|
||||
e.currentTarget.scrollLeft += e.deltaY;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cl("subtabs")}>
|
||||
<div className={cl("subtabs-list")} onWheel={handleWheel}>
|
||||
{groups.map(group => (
|
||||
<div
|
||||
key={group.key}
|
||||
className={classes(cl("subtab"), activeKey === group.key && cl("subtab-active"))}
|
||||
onClick={() => onSelect(group.key)}
|
||||
>
|
||||
{group.avatarUrl && (
|
||||
<img className={cl("subtab-avatar")} src={group.avatarUrl} alt="" />
|
||||
)}
|
||||
<span className={cl("subtab-label")}>{group.label}</span>
|
||||
<span className={cl("subtab-count")}>{group.repos.length}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{canSort && (
|
||||
<div
|
||||
className={cl("sort-toggle")}
|
||||
onClick={onToggleSort}
|
||||
title={sortMode === "count" ? "Sorted by repo count. Click to sort A-Z" : "Sorted A-Z. Click to sort by repo count"}
|
||||
>
|
||||
<SortIcon className={cl("sort-icon")} width={14} height={14} />
|
||||
<span>{sortMode === "count" ? "Most repos" : "A-Z"}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 = () => (
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Repository</th>
|
||||
<th>Description</th>
|
||||
<th>Language</th>
|
||||
<th>Stars</th>
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
export function ReposModal({ groups, initialActiveKey, username, rootProps }: ReposModalProps & { rootProps: RenderModalProps; }) {
|
||||
const [activeKey, setActiveKey] = useState(initialActiveKey);
|
||||
const [sortMode, setSortMode] = useState<RepoSortMode>("count");
|
||||
|
||||
const renderTableRow = (repo: GitHubRepo) => (
|
||||
<tr key={repo.id} onClick={() => window.open(repo.html_url, "_blank")}>
|
||||
<td>
|
||||
<div className={cl("table-name")}>{repo.name}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className={cl("table-description")}>
|
||||
{repo.description || ""}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{repo.language && (
|
||||
<div className={cl("table-language")}>
|
||||
<span
|
||||
className={cl("table-language-color")}
|
||||
style={{ backgroundColor: getLanguageColor(repo.language) }}
|
||||
/>
|
||||
<span>{repo.language}</span>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className={cl("table-stars")}>
|
||||
<Star className={cl("star-icon")} />
|
||||
<span>{repo.stargazers_count.toLocaleString()}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
const sortedGroups = sortGroups(groups, sortMode);
|
||||
const activeGroup = sortedGroups.find(g => g.key === activeKey) ?? sortedGroups[0];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
@ -79,19 +46,25 @@ export function ReposModal({ repos, username, rootProps }: ReposModalProps & { r
|
|||
}
|
||||
]}
|
||||
>
|
||||
<div className={cl("table-container")}>
|
||||
<table className={cl("table")}>
|
||||
<colgroup>
|
||||
<col className={cl("header-repo")} />
|
||||
<col className={cl("header-description")} />
|
||||
<col className={cl("header-language")} />
|
||||
<col className={cl("header-stars")} />
|
||||
</colgroup>
|
||||
{renderTableHeader()}
|
||||
<tbody>
|
||||
{repos.map(renderTableRow)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={cl("modal-content")}>
|
||||
<RepoSubTabs
|
||||
groups={sortedGroups}
|
||||
activeKey={activeGroup?.key ?? PERSONAL_GROUP_KEY}
|
||||
onSelect={setActiveKey}
|
||||
sortMode={sortMode}
|
||||
onToggleSort={() => setSortMode(mode => mode === "count" ? "alpha" : "count")}
|
||||
canSort={sortedGroups.length > 2}
|
||||
/>
|
||||
<div className={cl("list", "modal-list")}>
|
||||
{activeGroup?.repos.map(repo => (
|
||||
<RepoCard
|
||||
key={repo.id}
|
||||
repo={repo}
|
||||
showStars={settings.store.showStars}
|
||||
showLanguage={settings.store.showLanguage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
26
src/equicordplugins/githubRepos/components/SortIcon.tsx
Normal file
26
src/equicordplugins/githubRepos/components/SortIcon.tsx
Normal file
|
|
@ -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 (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4.5 2a.75.75 0 0 1 .75.75v8.69l1.72-1.72a.75.75 0 1 1 1.06 1.06l-3 3a.75.75 0 0 1-1.06 0l-3-3a.75.75 0 1 1 1.06-1.06l1.72 1.72V2.75A.75.75 0 0 1 4.5 2ZM11.5 14a.75.75 0 0 1-.75-.75v-8.69l-1.72 1.72a.75.75 0 1 1-1.06-1.06l3-3a.75.75 0 0 1 1.06 0l3 3a.75.75 0 1 1-1.06 1.06l-1.72-1.72v8.69a.75.75 0 0 1-.75.75Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<GitHubUserInfo | null> {
|
||||
|
|
@ -25,7 +26,8 @@ export async function fetchUserInfo(username: string): Promise<GitHubUserInfo |
|
|||
const userData = await userInfoResponse.json();
|
||||
return {
|
||||
username: userData.login,
|
||||
totalRepos: userData.public_repos
|
||||
totalRepos: userData.public_repos,
|
||||
avatarUrl: userData.avatar_url
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Error fetching user info", error);
|
||||
|
|
@ -60,6 +62,35 @@ export async function fetchReposByUsername(username: string, perPage: number = 3
|
|||
return sortReposByStars(data);
|
||||
}
|
||||
|
||||
export async function fetchUserOrgs(username: string): Promise<GitHubOrg[]> {
|
||||
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<GitHubRepo[]> {
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
61
src/equicordplugins/githubRepos/utils.ts
Normal file
61
src/equicordplugins/githubRepos/utils.ts
Normal file
|
|
@ -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<string, string> = {
|
||||
"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<string, GitHubRepo[]>, 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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue