Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/assets/example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 1 addition & 8 deletions .github/workflows/pr-version-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,4 @@ jobs:
echo "❌ Version was not bumped"
exit 1
fi

- name: Install dependencies
run: npm ci

- name: Ensure lockfile is up to date
run: |
npm install
git diff --exit-code package-lock.json
echo "✅ Version was bumped"
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

A lightweight React component for displaying GitHub repository cards.

![Example here](https://raw.githubusercontent.com/GalvinPython/github-cards-react/main/.github/assets/example.png)

*Screenshot showcasing an example of the card*

> NOTE: This package is currently only **client-side**. Server-side support is planned

Displays:
Expand Down Expand Up @@ -67,11 +71,13 @@ import { GithubCard } from "github-cards-react";

# Props

| Prop | Type | Required | Description |
| ---------- | ------------------- | -------- | ------------------------------------------------ |
| `username` | `string` | Yes | GitHub username or organization name |
| `repo` | `string` | Yes | Repository name |
| `theme` | `"light" \| "dark"` | No | Visual theme. Defaults to `"light"` |
| Prop | Type | Required | Description |
| ---------------- | ------------------- | -------- | -------------------------------------------------------- |
| `username` | `string` | Yes | GitHub username or organization name |
| `repo` | `string` | Yes | Repository name |
| `theme` | `"light" \| "dark"` | No | Visual theme. Defaults to `"light"` |
| `showLanguagesBar` | `boolean` | No | Whether to fetch and display the languages bar. Defaults to `false` |

# Testing

As of now, there is no official testing library implemented. There is however an Astro playground for you to test the library on. It's located in the `tests` folder
242 changes: 242 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "github-cards-react",
"version": "0.1.0",
"version": "0.2.0",
"description": "Lightweight React component for displaying GitHub repository cards.",
"license": "MIT",
"author": "GalvinPython",
Expand Down Expand Up @@ -51,6 +51,7 @@
},
"type": "module",
"dependencies": {
"@primer/octicons-react": "^19.22.0"
"@primer/octicons-react": "^19.22.0",
"linguist-colors-list": "^1.0.20260223"
}
}
}
250 changes: 207 additions & 43 deletions src/GithubCard.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,191 @@
import React, { useEffect, useState } from "react";
import { StarIcon, RepoForkedIcon } from "@primer/octicons-react";
import { linguistData } from "linguist-colors-list";

export interface GithubRepoData {
name: string;
description: string;
html_url: string;
stargazers_count: number;
forks_count: number;
language: string | null;
topics?: string[];
owner: { avatar_url: string };
languages?: GithubRepoLanguageData;
}

type GithubRepoLanguageData = Record<string, number>;

interface GithubCardProps {
username: string;
repo: string;
theme?: "light" | "dark";
showLanguagesBar?: boolean;
}

interface GithubRepoData {
name: string;
description: string;
html_url: string;
stargazers_count: number;
forks_count: number;
language: string;
interface GithubCardDummyProps {
username: string;
theme?: "light" | "dark";
data: GithubRepoData;
}

interface GithubCardBaseProps {
username: string;
theme: "light" | "dark";
data: GithubRepoData;
showLanguagesBar?: boolean;
}

const getContainerStyle = (theme: "light" | "dark"): React.CSSProperties => {
const isDark = theme === "dark";
return {
padding: "16px",
borderRadius: "8px",
border: `1px solid ${isDark ? "#30363d" : "#e1e4e8"}`,
backgroundColor: isDark ? "#0d1117" : "#ffffff",
color: isDark ? "#c9d1d9" : "#24292e",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
maxWidth: "400px",
};
};

const GithubCardBase: React.FC<GithubCardBaseProps> = ({
username,
theme,
data,
}) => {
const isDark = theme === "dark";

return (
<div style={getContainerStyle(theme)}>
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "12px" }}>
<img
src={data.owner?.avatar_url ?? "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"}
alt={username}
style={{
width: "32px",
height: "32px",
borderRadius: "50%",
}}
/>
<a
href={data.html_url}
style={{
color: isDark ? "#58a6ff" : "#0366d6",
textDecoration: "none",
}}
target="_blank"
rel="noopener noreferrer"
>
{username}/{data.name}
</a>
</div>
<p>{data.description || "No description provided."}</p>
<div style={{ display: "flex", flexWrap: "wrap", marginBottom: "8px" }}>
{data.topics && data.topics.length > 0 && (
<span>
{data.topics.map((topic) => (
<span
key={topic}
style={{
backgroundColor: isDark ? "#21262d" : "#f1f8ff",
color: isDark ? "#c9d1d9" : "#0366d6",
padding: "2px 6px",
borderRadius: "2em",
fontSize: "12px",
marginRight: "4px",
display: "inline-block",
}}
>
{topic}
</span>
))}
</span>
)}
</div>
<div
style={{
display: "flex",
flexDirection: "row",
gap: "12px",
alignItems: "center",
marginTop: "8px",
}}
>
{data.language && (
<span>
<span
style={{
backgroundColor:
linguistData[data.language]?.color || "#ededed",
width: "12px",
height: "12px",
borderRadius: "50%",
display: "inline-block",
marginRight: "4px",
}}
/>
{data.language}
</span>
)}

<span>
<StarIcon size="small" />{" "}
{data.stargazers_count.toLocaleString()}
</span>

<span>
<RepoForkedIcon size="small" />{" "}
{data.forks_count.toLocaleString()}
</span>
</div>

{data.languages && Object.keys(data.languages).length > 0 && (
<div
style={{
display: "flex",
width: "100%",
height: "8px",
borderRadius: "4px",
overflow: "hidden",
marginTop: "12px",
}}
>
{(() => {
const total = Object.values(data.languages!).reduce(
(a, b) => a + b,
0
);

return Object.entries(data.languages!).map(([lang, bytes]) => {
const percentage = (bytes / total) * 100;
const color =
linguistData[lang]?.color || "#ededed";

return (
<div
key={lang}
title={`${lang}: ${bytes.toLocaleString()} bytes`}
style={{
backgroundColor: color,
width: `${percentage}%`,
}}
/>
);
});
})()}
</div>
)}
Comment thread
GalvinPython marked this conversation as resolved.
</div>
);
};

export const GithubCard: React.FC<GithubCardProps> = ({
username,
repo,
theme = "light"
theme = "light",
showLanguagesBar = false,
}) => {
const [data, setData] = useState<GithubRepoData | null>(null);
const [error, setError] = useState<string | null>(null);
Expand All @@ -30,20 +196,37 @@ export const GithubCard: React.FC<GithubCardProps> = ({

const fetchRepo = async () => {
try {
setLoading(true);
setError(null);

const headers: HeadersInit = {
Accept: "application/vnd.github.v3+json"
Accept: "application/vnd.github.v3+json",
};

const res = await fetch(
`https://api.github.com/repos/${username}/${repo}`,
{ headers, signal: controller.signal }
{ headers, signal: controller.signal },
);

if (!res.ok) {
throw new Error(`GitHub API responded with ${res.status}`);
}

const json: GithubRepoData = await res.json();
let json: GithubRepoData = await res.json();
// Fetch languages data if showLanguagesBar is true
if (showLanguagesBar) {
const langRes = await fetch(
`https://api.github.com/repos/${username}/${repo}/languages`,
{ headers, signal: controller.signal },
);
if (langRes.ok) {
const langData = await langRes.json();
json.languages = langData;
} else {
console.warn(`Failed to fetch languages for ${username}/${repo}: ${langRes.status}`);
}
}

setData(json);
} catch (err) {
if (!controller.signal.aborted) {
Expand All @@ -59,46 +242,27 @@ export const GithubCard: React.FC<GithubCardProps> = ({
fetchRepo();

return () => controller.abort();
}, [username, repo]);

const isDark = theme === "dark";

const styles = {
container: {
padding: "16px",
borderRadius: "8px",
border: `1px solid ${isDark ? "#30363d" : "#e1e4e8"}`,
backgroundColor: isDark ? "#0d1117" : "#ffffff",
color: isDark ? "#c9d1d9" : "#24292e",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
maxWidth: "400px"
} as React.CSSProperties
};
}, [username, repo, showLanguagesBar]);

if (loading) {
return <div style={styles.container}>Loading...</div>;
return <div style={getContainerStyle(theme)}>Loading...</div>;
}

if (error || !data) {
return (
<div style={styles.container}>
<div style={getContainerStyle(theme)}>
Could not load {username}/{repo}
</div>
);
}

return (
<div style={styles.container}>
<a href={data.html_url} target="_blank" rel="noopener noreferrer">
{username}/{data.name}
</a>
<p>{data.description || "No description provided."}</p>
<div style={{ display: "flex", flexDirection: "row", gap: "8px", alignItems: "center" }}>
{data.language && <span>● {data.language}</span>}
<span><StarIcon size="small" className="mr-1" /> {data.stargazers_count.toLocaleString()}</span>
<span><RepoForkedIcon size="small" className="mr-1" /> {data.forks_count.toLocaleString()}</span>
</div>
</div>
);
};
return <GithubCardBase username={username} theme={theme} data={data} showLanguagesBar={showLanguagesBar} />;
};

export const GithubCardDummy: React.FC<GithubCardDummyProps> = ({
username,
theme = "light",
data,
}) => {
return <GithubCardBase username={username} theme={theme} data={data} />;
};
Loading