diff --git a/scripts/gen-contributors.mjs b/scripts/gen-contributors.mjs new file mode 100644 index 0000000..199f745 --- /dev/null +++ b/scripts/gen-contributors.mjs @@ -0,0 +1,53 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const OPTOUT_FILE = path.join(__dirname, '../src/data/contributors-optout.json'); +const OUTPUT_FILE = path.join(__dirname, '../src/data/contributors.json'); + +async function run() { + try { + const optOutData = await fs.readFile(OPTOUT_FILE, 'utf-8'); + const optOutList = JSON.parse(optOutData); + + console.log('Fetching contributors from GitHub API...'); + + // In a real run without token, rate limits apply, so we handle fetch gracefully + const res = await fetch('https://api.github.com/repos/wraith-protocol/www/contributors', { + headers: { + 'User-Agent': 'Node-Fetch', + ...(process.env.GITHUB_TOKEN + ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } + : {}), + }, + }); + + if (!res.ok) { + throw new Error(`GitHub API returned ${res.status} ${res.statusText}`); + } + + const contributors = await res.json(); + + const leaderboard = contributors + .filter((c) => !optOutList.includes(c.login)) + .map((c) => ({ + username: c.login, + avatar: c.avatar_url, + profile: c.html_url, + prCount: c.contributions, + // Mock wave participation as requested, or derive if data existed. + waves: ['Wave 1'], + })); + + await fs.writeFile(OUTPUT_FILE, JSON.stringify(leaderboard, null, 2), 'utf-8'); + console.log( + `Successfully generated contributors.json with ${leaderboard.length} contributors.`, + ); + } catch (error) { + console.error('Error generating contributors:', error); + process.exit(1); + } +} + +run(); diff --git a/src/App.tsx b/src/App.tsx index f7cf24a..5ffe66b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,7 @@ const CaseStudies = lazy(() => import('./pages/CaseStudies')); const Careers = lazy(() => import('./pages/Careers')); const About = lazy(() => import('./pages/About')); const NotFound = lazy(() => import('./pages/NotFound')); +const Contributors = lazy(() => import('./pages/Contributors')); function Home() { return ( @@ -111,6 +112,14 @@ export default function App() { } /> + + + + } + /> } /> diff --git a/src/data/contributors-optout.json b/src/data/contributors-optout.json new file mode 100644 index 0000000..ae4062b --- /dev/null +++ b/src/data/contributors-optout.json @@ -0,0 +1 @@ +["dependabot[bot]", "github-actions[bot]"] diff --git a/src/data/contributors.json b/src/data/contributors.json new file mode 100644 index 0000000..bf92b79 --- /dev/null +++ b/src/data/contributors.json @@ -0,0 +1,9 @@ +[ + { + "username": "ghost", + "avatar": "https://github.com/ghost.png", + "profile": "https://github.com/ghost", + "prCount": 10, + "waves": ["Wave 1"] + } +] diff --git a/src/pages/Contributors.tsx b/src/pages/Contributors.tsx new file mode 100644 index 0000000..8c39f35 --- /dev/null +++ b/src/pages/Contributors.tsx @@ -0,0 +1,84 @@ +import { useEffect, useState } from 'react'; +import contributorsData from '../data/contributors.json'; + +interface Contributor { + username: string; + avatar: string; + profile: string; + prCount: number; + waves: string[]; +} + +export default function Contributors() { + const [contributors, setContributors] = useState([]); + + useEffect(() => { + // Data is static for the client and refreshed weekly by a GitHub Action/script. + // We import it directly here. + setContributors(contributorsData as Contributor[]); + }, []); + + return ( +
+
+

+ Contributor Leaderboard +

+

+ Celebrating the amazing people who build and improve Wraith Protocol. +

+
+

+ Note: Data is refreshed weekly via an automated script. If you wish to + opt-out of this leaderboard, please submit a PR to add your username to our opt-out list + at src/data/contributors-optout.json. +

+
+
+ + +
+ ); +}