Skip to content

Commit 28ef991

Browse files
committed
feat: Add contributors section
1 parent f37c746 commit 28ef991

File tree

5 files changed

+324
-12
lines changed

5 files changed

+324
-12
lines changed

src/components/Contributors.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import React, { useState, useEffect } from 'react';
2+
import styles from './Contributors.module.css';
3+
4+
const REPOS = [
5+
'sparkison/m3u-editor',
6+
'sparkison/m3u-proxy',
7+
'sparkison/m3u-editor-docs-v2'
8+
];
9+
10+
export default function Contributors() {
11+
const [contributors, setContributors] = useState([]);
12+
const [loading, setLoading] = useState(true);
13+
const [error, setError] = useState(null);
14+
15+
useEffect(() => {
16+
fetchContributors();
17+
}, []);
18+
19+
const fetchContributors = async () => {
20+
try {
21+
const allContributors = new Map();
22+
23+
// Fetch contributors from all repos
24+
for (const repo of REPOS) {
25+
const response = await fetch(
26+
`https://api.github.com/repos/${repo}/contributors?per_page=100`,
27+
{
28+
headers: {
29+
'Accept': 'application/vnd.github.v3+json'
30+
}
31+
}
32+
);
33+
34+
if (!response.ok) {
35+
throw new Error(`Failed to fetch contributors for ${repo}`);
36+
}
37+
38+
const data = await response.json();
39+
40+
// Merge contributors, filtering out bots
41+
data.forEach(contributor => {
42+
// Skip bots (any login ending with [bot])
43+
if (contributor.login.endsWith('[bot]')) {
44+
return;
45+
}
46+
47+
if (allContributors.has(contributor.login)) {
48+
// Add contributions from this repo to existing contributor
49+
const existing = allContributors.get(contributor.login);
50+
existing.contributions += contributor.contributions;
51+
} else {
52+
// Add new contributor
53+
allContributors.set(contributor.login, {
54+
login: contributor.login,
55+
avatar_url: contributor.avatar_url,
56+
html_url: contributor.html_url,
57+
contributions: contributor.contributions
58+
});
59+
}
60+
});
61+
}
62+
63+
// Convert to array and sort by contributions
64+
const sortedContributors = Array.from(allContributors.values())
65+
.sort((a, b) => b.contributions - a.contributions);
66+
67+
setContributors(sortedContributors);
68+
setLoading(false);
69+
} catch (err) {
70+
console.error('Error fetching contributors:', err);
71+
setError(err.message);
72+
setLoading(false);
73+
}
74+
};
75+
76+
if (loading) {
77+
return (
78+
<section className={styles.contributorsSection}>
79+
<div className="container">
80+
<h2 className={styles.heading}>Contributors</h2>
81+
<p className={styles.loading}>Loading contributors...</p>
82+
</div>
83+
</section>
84+
);
85+
}
86+
87+
if (error) {
88+
return (
89+
<section className={styles.contributorsSection}>
90+
<div className="container">
91+
<h2 className={styles.heading}>Contributors</h2>
92+
<p className={styles.error}>Unable to load contributors. Please check back later.</p>
93+
</div>
94+
</section>
95+
);
96+
}
97+
98+
return (
99+
<section className={styles.contributorsSection}>
100+
<div className="container">
101+
<h2 className={styles.heading}>Contributors</h2>
102+
<p className={styles.subtitle}>
103+
Thank you to all the amazing people who have contributed to the M3U Editor project! 🙌
104+
</p>
105+
<div className={styles.contributorsGrid}>
106+
{contributors.map((contributor) => (
107+
<a
108+
key={contributor.login}
109+
href={contributor.html_url}
110+
target="_blank"
111+
rel="noopener noreferrer"
112+
className={styles.contributorCard}
113+
title={`${contributor.login} - ${contributor.contributions} contributions`}
114+
>
115+
<img
116+
src={contributor.avatar_url}
117+
alt={contributor.login}
118+
className={styles.avatar}
119+
loading="lazy"
120+
/>
121+
<div className={styles.contributorInfo}>
122+
<div className={styles.username}>{contributor.login}</div>
123+
<div className={styles.contributions}>
124+
{contributor.contributions} {contributor.contributions === 1 ? 'contribution' : 'contributions'}
125+
</div>
126+
</div>
127+
</a>
128+
))}
129+
</div>
130+
<p className={styles.footer}>
131+
Contributors are fetched from the{' '}
132+
<a href="https://github.com/sparkison/m3u-editor" target="_blank" rel="noopener noreferrer">
133+
m3u-editor
134+
</a>
135+
,{' '}
136+
<a href="https://github.com/sparkison/m3u-proxy" target="_blank" rel="noopener noreferrer">
137+
m3u-proxy
138+
</a>
139+
, and{' '}
140+
<a href="https://github.com/sparkison/m3u-editor-docs-v2" target="_blank" rel="noopener noreferrer">
141+
m3u-editor-docs-v2
142+
</a>
143+
{' '}repositories.
144+
</p>
145+
</div>
146+
</section>
147+
);
148+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
.contributorsSection {
2+
padding: 4rem 0;
3+
background: var(--ifm-background-surface-color);
4+
}
5+
6+
.heading {
7+
text-align: center;
8+
font-size: 2.5rem;
9+
font-weight: 700;
10+
margin-bottom: 1rem;
11+
background: linear-gradient(135deg, #f43f5e 0%, #b3509f 50%, #6366f1 100%);
12+
-webkit-background-clip: text;
13+
-webkit-text-fill-color: transparent;
14+
background-clip: text;
15+
}
16+
17+
.subtitle {
18+
text-align: center;
19+
font-size: 1.1rem;
20+
color: var(--ifm-color-emphasis-700);
21+
margin-bottom: 3rem;
22+
max-width: 600px;
23+
margin-left: auto;
24+
margin-right: auto;
25+
}
26+
27+
.loading,
28+
.error {
29+
text-align: center;
30+
padding: 2rem;
31+
color: var(--ifm-color-emphasis-600);
32+
}
33+
34+
.error {
35+
color: var(--ifm-color-danger);
36+
}
37+
38+
.contributorsGrid {
39+
display: grid;
40+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
41+
gap: 1.5rem;
42+
margin-bottom: 2rem;
43+
}
44+
45+
@media (max-width: 768px) {
46+
.contributorsGrid {
47+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
48+
gap: 1rem;
49+
}
50+
}
51+
52+
.contributorCard {
53+
display: flex;
54+
flex-direction: column;
55+
align-items: center;
56+
padding: 1.5rem 1rem;
57+
border-radius: 12px;
58+
background: var(--ifm-card-background-color);
59+
border: 1px solid var(--ifm-color-emphasis-200);
60+
text-decoration: none;
61+
color: inherit;
62+
transition: all 0.3s ease;
63+
position: relative;
64+
overflow: hidden;
65+
}
66+
67+
.contributorCard::before {
68+
content: '';
69+
position: absolute;
70+
top: 0;
71+
left: 0;
72+
right: 0;
73+
height: 3px;
74+
background: linear-gradient(90deg, #f43f5e 0%, #b3509f 50%, #6366f1 100%);
75+
opacity: 0;
76+
transition: opacity 0.3s ease;
77+
}
78+
79+
.contributorCard:hover {
80+
transform: translateY(-4px);
81+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
82+
border-color: var(--ifm-color-primary);
83+
}
84+
85+
.contributorCard:hover::before {
86+
opacity: 1;
87+
}
88+
89+
.avatar {
90+
width: 80px;
91+
height: 80px;
92+
border-radius: 50%;
93+
margin-bottom: 1rem;
94+
border: 3px solid var(--ifm-color-emphasis-200);
95+
transition: border-color 0.3s ease;
96+
}
97+
98+
.contributorCard:hover .avatar {
99+
border-color: var(--ifm-color-primary);
100+
}
101+
102+
.contributorInfo {
103+
text-align: center;
104+
width: 100%;
105+
}
106+
107+
.username {
108+
font-weight: 600;
109+
font-size: 1rem;
110+
margin-bottom: 0.25rem;
111+
color: var(--ifm-color-emphasis-900);
112+
word-break: break-word;
113+
}
114+
115+
.contributions {
116+
font-size: 0.875rem;
117+
color: var(--ifm-color-emphasis-600);
118+
}
119+
120+
.footer {
121+
text-align: center;
122+
font-size: 0.875rem;
123+
color: var(--ifm-color-emphasis-600);
124+
margin-top: 2rem;
125+
}
126+
127+
.footer a {
128+
color: var(--ifm-color-primary);
129+
text-decoration: none;
130+
font-weight: 500;
131+
}
132+
133+
.footer a:hover {
134+
text-decoration: underline;
135+
}
136+
137+
/* Dark mode adjustments */
138+
[data-theme='dark'] .contributorCard {
139+
background: var(--ifm-background-surface-color);
140+
border-color: var(--ifm-color-emphasis-300);
141+
}
142+
143+
[data-theme='dark'] .avatar {
144+
border-color: var(--ifm-color-emphasis-300);
145+
}
146+
147+
[data-theme='dark'] .contributorCard:hover .avatar {
148+
border-color: var(--ifm-color-primary);
149+
}

src/components/ReleaseVersions/styles.module.css

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44

55
.versionsContainer h2 {
66
text-align: center;
7-
color: var(--ifm-font-color-base);
7+
font-size: 2.5rem;
8+
font-weight: 700;
89
margin-bottom: 2rem;
9-
font-size: 2rem;
10+
background: linear-gradient(135deg, #f43f5e 0%, #b3509f 50%, #6366f1 100%);
11+
-webkit-background-clip: text;
12+
-webkit-text-fill-color: transparent;
13+
background-clip: text;
1014
}
1115

1216
.versionsGrid {

0 commit comments

Comments
 (0)