diff --git a/apps/site/components/Common/Supporters/index.tsx b/apps/site/components/Common/Supporters/index.tsx index 06dddf7f23b44..fa894d2ac6c28 100644 --- a/apps/site/components/Common/Supporters/index.tsx +++ b/apps/site/components/Common/Supporters/index.tsx @@ -4,7 +4,7 @@ import type { Supporter } from '#site/types'; import type { FC } from 'react'; type SupportersListProps = { - supporters: Array>; + supporters: Array>; }; const SupportersList: FC = ({ supporters }) => ( diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index fc77c4b383e8b..cb465316f76d1 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -1,4 +1,8 @@ -import { OPENCOLLECTIVE_MEMBERS_URL } from '#site/next.constants.mjs'; +import { + OPENCOLLECTIVE_MEMBERS_URL, + GITHUB_GRAPHQL_URL, + GITHUB_API_KEY, +} from '#site/next.constants.mjs'; import { fetchWithRetry } from '#site/util/fetch'; /** @@ -26,4 +30,156 @@ 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 of supporters + */ +async function fetchGithubSponsorsData() { + if (!GITHUB_API_KEY) { + return []; + } + + const sponsors = []; + + // Fetch sponsorship pages + let cursor = null; + + while (true) { + const query = sponsorshipsQuery(cursor); + const data = await graphql(query); + + if (data.errors) { + throw new Error(JSON.stringify(data.errors)); + } + + const nodeRes = data.data.user?.sponsorshipsAsMaintainer; + if (!nodeRes) { + break; + } + + const { nodes, pageInfo } = nodeRes; + const mapped = nodes.map(n => { + const s = n.sponsor || n.sponsorEntity || n.sponsorEntity; // support different field names + return { + id: s?.id || null, + login: s?.login || null, + name: s?.name || s?.login || null, + avatar: s?.avatarUrl || null, + url: s?.websiteUrl || s?.url || null, + }; + }); + + sponsors.push(...mapped); + + if (!pageInfo.hasNextPage) { + break; + } + + cursor = pageInfo.endCursor; + } + return sponsors; +} + +function sponsorshipsQuery(cursor = null) { + return ` + query { + organization(login: "nodejs") { + sponsorshipsAsMaintainer (first: 100, includePrivate: false, after: "${cursor}") { + 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 + } + } + } + }`; +} + +// function donationsQuery(cursor = null) { +// return ` +// query { +// organization(login: "nodejs") { +// sponsorsActivities (first: 100, includePrivate: false, after: "${cursor}") { +// nodes { +// id +// sponsor { +// ...on User { +// id: databaseId, +// name, +// login, +// avatarUrl, +// url, +// websiteUrl +// } +// ...on Organization { +// id: databaseId, +// name, +// login, +// avatarUrl, +// url, +// websiteUrl +// } +// }, +// timestamp +// } +// } +// } +// }`; +// } + +const graphql = async (query, variables = {}) => { + const res = await fetch(GITHUB_GRAPHQL_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${GITHUB_API_KEY}`, + }, + body: JSON.stringify({ query, variables }), + }); + + 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 of supporters + */ +async function sponsorsData() { + const sponsors = await Promise.all([ + fetchGithubSponsorsData(), + fetchOpenCollectiveData(), + ]); + return sponsors.flat(); +} + +export default sponsorsData; diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index c90c61711b7c6..59e3c2807b30a 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -219,3 +219,8 @@ export const VULNERABILITIES_URL = */ export const OPENCOLLECTIVE_MEMBERS_URL = 'https://opencollective.com/nodejs/members/all.json'; + +/** + * The location of Github Graphql API + */ +export const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql'; diff --git a/apps/site/types/supporters.ts b/apps/site/types/supporters.ts index 5da04e07c50ca..b0f382b4da6a3 100644 --- a/apps/site/types/supporters.ts +++ b/apps/site/types/supporters.ts @@ -7,3 +7,4 @@ export type Supporter = { }; export type OpenCollectiveSupporter = Supporter<'opencollective'>; +export type GithubSponsorSupporter = Supporter<'github'>;