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
968 changes: 907 additions & 61 deletions src/components/ContributorList/contributors.css

Large diffs are not rendered by default.

343 changes: 276 additions & 67 deletions src/components/ContributorList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,116 +1,325 @@
import React, { useEffect, useState } from "react";
import { Octokit } from "@octokit/rest";
import "./contributors.css";
import React, { useEffect, useMemo, useState } from "react";
import Translate from "@docusaurus/Translate";
import {
ArrowUpRight,
GitBranch,
Github,
Heart,
Users,
} from "lucide-react";

import "./contributors.css";

type Contributor = {
login: string;
contributions: number;
html_url: string;
avatar_url: string;
type?: string;
};

// per page max 99 items
type ContibutorProps = {
contributorsPerPage: number;
contributorsIgnore: Array<string>;
type ContributorProps = {
contributorsPerPage?: number;
contributorsIgnore?: string[];
};

const defaultIgnoreContributors: string[] = [
const DEFAULT_IGNORE_CONTRIBUTORS = [
"restyled-commits",
"dependabot",
"renovate",
];
const ContributorList: React.FC<ContibutorProps> = ({

const GITHUB_OWNER = "CodeHarborHub";
const GITHUB_REPO = "codeharborhub.github.io";

const ContributorList: React.FC<ContributorProps> = ({
contributorsPerPage = 99,
contributorsIgnore = defaultIgnoreContributors,
}: ContibutorProps) => {
const [contributors, setContributors] = useState<Contributor>([]);
contributorsIgnore = DEFAULT_IGNORE_CONTRIBUTORS,
}) => {
const [contributors, setContributors] = useState<Contributor[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);

useEffect(() => {
const controller = new AbortController();

const fetchContributors = async () => {
const octokit = new Octokit();
try {
const response = await octokit.request(
`GET /repos/{owner}/{repo}/contributors?per_page=${contributorsPerPage + contributorsIgnore.length}`,
setLoading(true);
setError(false);

const perPage = Math.min(
Math.max(contributorsPerPage + contributorsIgnore.length, 1),
100,
);

const response = await fetch(
`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/contributors?per_page=${perPage}`,
{
owner: "codeharborhub",
repo: "codeharborhub.github.io",
headers: {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
signal: controller.signal,
},
);
const filteredContributors = response.data.filter(
(contribuidor: Contributor) =>
!contributorsIgnore.some((element) =>
contribuidor.login.toLowerCase().includes(element.toLowerCase()),
),

if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}

const data: Contributor[] = await response.json();

const ignored = contributorsIgnore.map((login) =>
login.toLowerCase(),
);

setContributors(filteredContributors);
} catch (error) {
console.error("Error fetching contributors:", error);
const filtered = data
.filter((contributor) => {
const login = contributor.login.toLowerCase();

return (
!ignored.includes(login) &&
!ignored.some((ignoredLogin) =>
login.includes(ignoredLogin),
)
);
})
.slice(0, contributorsPerPage);

setContributors(filtered);
} catch (fetchError) {
if (
fetchError instanceof DOMException &&
fetchError.name === "AbortError"
) {
return;
}

console.error("Failed to fetch CodeHarborHub contributors:", fetchError);
setError(true);
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
};

fetchContributors();
}, []);

return () => {
controller.abort();
};
}, [contributorsPerPage, contributorsIgnore]);

const contributorRows = useMemo(() => {
const rows: Contributor[][] = [[], [], []];

contributors.forEach((contributor, index) => {
rows[index % 3].push(contributor);
});

return rows;
}, [contributors]);

const githubContributorsUrl = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/graphs/contributors`;

return (
<>
<div className="father-flex-div mb-6">
<p className="intro">
<Translate
id="contributors.thankYouMessage"
description="Thank you message for contributors"
values={{
projectName: (
<a href="https://codeharborhub.github.io">
<i>CodeHarborHub</i>
</a>
),
}}
<section
className="contributors-section"
aria-labelledby="contributors-title"
>
<div className="contributors-section__container">
{/* Decorative background */}
<div
className="contributors-section__glow contributors-section__glow--one"
aria-hidden="true"
/>

<div
className="contributors-section__glow contributors-section__glow--two"
aria-hidden="true"
/>

{/* Header */}
<header className="contributors-section__header">
<div className="contributors-section__eyebrow">
<GitBranch size={15} aria-hidden="true" />
<span>OPEN SOURCE COMMUNITY</span>
</div>

<h2
id="contributors-title"
className="contributors-section__title"
>
{`Our heartfelt thanks to the incredible contributors making {projectName} a premier destination for developers. From code to documentation, your expertise
shapes this harbor. Together, we're building a faster, better, and more
inclusive open-source ecosystem.`}
</Translate>
</p>
<div className="grid">
{[0, 1, 2].map((value) => {
const middle = Math.ceil(contributors.length / 3);
const start = value === 0 ? 0 : middle * value;
const end =
value === 0
? middle
: value === 1
? middle * 2
: contributors.length;
Built by{" "}
<span>contributors</span>,
powered by community.
</h2>

return (
<div className="grid-item fade-horizontal" key={value}>
{contributors.slice(start, end).map((contributor) => (
<div className="circle">
<p className="contributors-section__description">
CodeHarborHub grows because developers, writers, designers,
and open-source contributors come together to build something
useful for everyone.
</p>
</header>

{/* Main contributor panel */}
<div className="contributors-section__panel">
<div className="contributors-section__panel-header">
<div className="contributors-section__panel-title">
<div className="contributors-section__icon">
<Users size={20} aria-hidden="true" />
</div>

<div>
<h3>Our contributors</h3>

<p>
Every contribution helps make the harbor stronger.
</p>
</div>
</div>

{!loading && !error && (
<div className="contributors-section__count">
<strong>{contributors.length}</strong>
<span>featured contributors</span>
</div>
)}
</div>

{/* Loading */}
{loading && (
<div
className="contributors-section__loading"
aria-label="Loading contributors"
>
{[...Array(42)].map((_, index) => (
<span
key={index}
className="contributors-section__skeleton"
/>
))}
</div>
)}

{/* Error */}
{!loading && error && (
<div className="contributors-section__error">
<div className="contributors-section__error-icon">
<Github size={22} aria-hidden="true" />
</div>

<div>
<strong>Contributors couldn't be loaded.</strong>
<p>
Please visit our GitHub repository to see the latest
contributor list.
</p>
</div>

<a
href={githubContributorsUrl}
target="_blank"
rel="noopener noreferrer"
className="contributors-section__error-link"
>
View on GitHub
<ArrowUpRight size={16} aria-hidden="true" />
</a>
</div>
)}

{/* Contributors */}
{!loading && !error && contributors.length > 0 && (
<div className="contributors-section__avatars">
{contributorRows.map((row, rowIndex) => (
<div
className="contributors-section__avatar-row"
key={`row-${rowIndex}`}
>
{row.map((contributor) => (
<a
href={contributor.html_url}
target="_blank"
rel="noopener noreferrer"
className="contributor-avatar"
key={contributor.login}
title={`${contributor.login} · ${contributor.contributions} contributions`}
aria-label={`View ${contributor.login} on GitHub`}
>
<img
className="circle"
src={contributor.avatar_url}
alt={`${contributor.login}'s avatar`}
alt=""
loading="lazy"
width="48"
height="48"
/>

<span className="contributor-avatar__tooltip">
<strong>{contributor.login}</strong>
<small>
{contributor.contributions} contributions
</small>
</span>
</a>
</div>
))}
</div>
);
})}
))}
</div>
))}
</div>
)}

{/* Empty */}
{!loading && !error && contributors.length === 0 && (
<div className="contributors-section__empty">
No contributors are currently available.
</div>
)}

{/* Footer */}
<div className="contributors-section__panel-footer">
<div className="contributors-section__thanks">
<Heart size={16} aria-hidden="true" />

<span>
<Translate
id="contributors.thankYouMessage"
description="Thank you message for CodeHarborHub contributors"
values={{
projectName: (
<a
href="https://codeharborhub.github.io"
target="_blank"
rel="noopener noreferrer"
>
CodeHarborHub
</a>
),
}}
>
{
"Thank you to every contributor helping {projectName} become a better place to learn, build, and collaborate."
}
</Translate>
</span>
</div>

<a
href={githubContributorsUrl}
target="_blank"
rel="noopener noreferrer"
className="contributors-section__github-button"
>
<Github size={17} aria-hidden="true" />
<span>View all contributors</span>
<ArrowUpRight size={16} aria-hidden="true" />
</a>
</div>
</div>
</div>
</>
</section>
);
};

export default ContributorList;
export default ContributorList;
Loading
Loading