-
Notifications
You must be signed in to change notification settings - Fork 6.5k
chore: add github sponsors on supporters #8531
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
46d49e7
00e81c5
309a8bb
70c5076
21655fa
448e37e
0ad4e41
6da91ff
fccada1
29829ce
f419e15
c493a8d
6f5435b
536b553
7cb53c4
492c9ed
512ba91
dfdf2ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,85 @@ | ||
| import { OPENCOLLECTIVE_MEMBERS_URL } from '#site/next.constants.mjs'; | ||
| import { | ||
| OPENCOLLECTIVE_MEMBERS_URL, | ||
| GITHUB_GRAPHQL_URL, | ||
| GITHUB_READ_API_KEY, | ||
| } from '#site/next.constants.mjs'; | ||
| import { fetchWithRetry } from '#site/next.fetch.mjs'; | ||
| import { shuffle } from '#site/util/array'; | ||
|
|
||
| const SPONSORSHIPS_QUERY = ` | ||
| query ($cursor: String) { | ||
| organization(login: "nodejs") { | ||
| sponsorshipsAsMaintainer( | ||
| first: 100 | ||
| includePrivate: false | ||
| after: $cursor | ||
| activeOnly: false | ||
| ) { | ||
| nodes { | ||
| sponsor: sponsorEntity { | ||
| ...on User { | ||
| id: databaseId | ||
| name | ||
| login | ||
| avatarUrl | ||
| url | ||
| websiteUrl | ||
| } | ||
| ...on Organization { | ||
| id: databaseId | ||
| name | ||
| login | ||
| avatarUrl | ||
| url | ||
| websiteUrl | ||
| } | ||
| } | ||
| } | ||
| pageInfo { | ||
| endCursor | ||
| startCursor | ||
| hasNextPage | ||
| hasPreviousPage | ||
| } | ||
| } | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| const DONATIONS_QUERY = ` | ||
| query { | ||
| organization(login: "nodejs") { | ||
| sponsorsActivities(first: 100, includePrivate: false) { | ||
| nodes { | ||
| id | ||
| sponsor { | ||
| ...on User { | ||
| id: databaseId | ||
| name | ||
| login | ||
| avatarUrl | ||
| url | ||
| websiteUrl | ||
| } | ||
| ...on Organization { | ||
| id: databaseId | ||
| name | ||
| login | ||
| avatarUrl | ||
| url | ||
| websiteUrl | ||
| } | ||
| } | ||
| timestamp | ||
| tier: sponsorsTier { | ||
| monthlyPriceInDollars | ||
| isOneTime | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| /** | ||
| * Fetches supporters data from Open Collective API, filters active backers, | ||
|
|
@@ -15,12 +95,11 @@ async function fetchOpenCollectiveData() { | |
| const members = payload | ||
| .filter(({ role, isActive }) => role === 'BACKER' && isActive) | ||
| .sort((a, b) => b.totalAmountDonated - a.totalAmountDonated) | ||
| .map(({ name, website, image, profile }) => ({ | ||
| .map(({ name, image, profile }) => ({ | ||
| name, | ||
| image, | ||
| url: website, | ||
| // If profile starts with the guest- prefix, it's a non-existing account | ||
| profile: profile.startsWith('https://opencollective.com/guest-') | ||
| url: profile.startsWith('https://opencollective.com/guest-') | ||
| ? undefined | ||
| : profile, | ||
| source: 'opencollective', | ||
|
|
@@ -29,4 +108,136 @@ async function fetchOpenCollectiveData() { | |
| return members; | ||
| } | ||
|
|
||
| export default fetchOpenCollectiveData; | ||
| /** | ||
| * Fetches supporters data from Github API, filters active backers, | ||
| * and maps it to the Supporters type. | ||
| * | ||
| * @returns {Promise<Array<import('#site/types/supporters').GitHubSponsorSupporter>>} Array of supporters | ||
| */ | ||
| async function fetchGithubSponsorsData() { | ||
bjohansebas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (!GITHUB_READ_API_KEY) { | ||
| return []; | ||
| } | ||
|
|
||
| const [sponsorships, donations] = await Promise.all([ | ||
| fetchSponsorshipsQuery(), | ||
| fetchDonationsQuery(), | ||
| ]); | ||
|
|
||
| return [...sponsorships, ...donations]; | ||
| } | ||
|
|
||
| async function fetchSponsorshipsQuery() { | ||
| const sponsors = []; | ||
| let cursor = null; | ||
|
|
||
| while (true) { | ||
| const data = await graphql( | ||
| SPONSORSHIPS_QUERY, | ||
| cursor ? { cursor } : undefined | ||
| ); | ||
|
|
||
| if (data.errors) { | ||
| throw new Error(JSON.stringify(data.errors)); | ||
| } | ||
|
|
||
| const nodeRes = data.data.organization?.sponsorshipsAsMaintainer; | ||
| if (!nodeRes) { | ||
| break; | ||
| } | ||
|
|
||
| const { nodes, pageInfo } = nodeRes; | ||
| const mapped = nodes.map(n => { | ||
| const s = n.sponsor || n.sponsorEntity; // support different field names | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GraphQL alias makes
|
||
| return { | ||
| name: s?.name || s?.login || null, | ||
| image: s?.avatarUrl || null, | ||
| url: s?.url || null, | ||
| source: 'github', | ||
| }; | ||
bjohansebas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| sponsors.push(...mapped); | ||
|
|
||
| if (!pageInfo.hasNextPage) { | ||
| break; | ||
| } | ||
|
|
||
| cursor = pageInfo.endCursor; | ||
| } | ||
|
|
||
| return sponsors; | ||
| } | ||
|
|
||
| async function fetchDonationsQuery() { | ||
| const data = await graphql(DONATIONS_QUERY); | ||
|
|
||
| if (data.errors) { | ||
| throw new Error(JSON.stringify(data.errors)); | ||
| } | ||
|
|
||
| const nodeRes = data.data.organization?.sponsorsActivities; | ||
bjohansebas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (!nodeRes) { | ||
| return []; | ||
| } | ||
|
|
||
| const { nodes } = nodeRes; | ||
bjohansebas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return nodes.map(n => { | ||
| const s = n.sponsor || n.sponsorEntity; // support different field names | ||
| return { | ||
| name: s?.name || s?.login || null, | ||
| image: s?.avatarUrl || null, | ||
| url: s?.url || null, | ||
| source: 'github', | ||
| }; | ||
| }); | ||
bjohansebas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| const graphql = async (query, variables = {}) => { | ||
| const res = await fetchWithRetry(GITHUB_GRAPHQL_URL, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Bearer ${GITHUB_READ_API_KEY}`, | ||
| }, | ||
| body: JSON.stringify({ query, variables }), | ||
| }); | ||
bjohansebas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if (!res.ok) { | ||
| const text = await res.text(); | ||
| throw new Error(`GitHub API error: ${res.status} ${text}`); | ||
| } | ||
|
|
||
| return res.json(); | ||
| }; | ||
|
|
||
| /** | ||
| * Fetches supporters data from Open Collective API and GitHub Sponsors, filters active backers, | ||
| * and maps it to the Supporters type. | ||
| * | ||
| * @returns {Promise<Array<import('#site/types/supporters').OpenCollectiveSupporter | import('#site/types/supporters').GitHubSponsorSupporter>>} Array of supporters | ||
| */ | ||
| async function sponsorsData() { | ||
| const seconds = 300; // Change every 5 minutes | ||
| const seed = Math.floor(Date.now() / (seconds * 1000)); | ||
|
|
||
| const sponsorsResults = await Promise.allSettled([ | ||
| fetchGithubSponsorsData(), | ||
| fetchOpenCollectiveData(), | ||
| ]); | ||
|
|
||
| const sponsors = sponsorsResults.flatMap(result => { | ||
| if (result.status === 'fulfilled') { | ||
| return result.value; | ||
| } | ||
|
|
||
| console.error('Supporters data source failed:', result.reason); | ||
| return []; | ||
| }); | ||
|
|
||
| const shuffled = await shuffle(sponsors, seed); | ||
|
|
||
| return shuffled; | ||
| } | ||
|
Comment on lines
+220
to
+241
|
||
|
|
||
| export default sponsorsData; | ||


Uh oh!
There was an error while loading. Please reload this page.