Skip to content

Commit eb53da8

Browse files
authored
chore: implement backers component
1 parent 2be37a4 commit eb53da8

File tree

7 files changed

+279
-0
lines changed

7 files changed

+279
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Avatar from '@node-core/ui-components/Common/AvatarGroup/Avatar';
2+
import type { FC, PropsWithChildren } from 'react';
3+
4+
import provideBackers from '#site/next-data/providers/backersData';
5+
6+
const WithBackers: FC<PropsWithChildren> = () => {
7+
const backers = provideBackers();
8+
9+
return (
10+
<div className="flex max-w-full flex-wrap items-center justify-center gap-1">
11+
{backers.map(({ name, image, url }, i) => (
12+
<Avatar nickname={name} image={image} url={url} key={`${name}-${i}`} />
13+
))}
14+
</div>
15+
);
16+
};
17+
18+
export default WithBackers;
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { graphql as githubGraphQL } from '@octokit/graphql';
2+
3+
import { GITHUB_API_KEY } from '#site/next.constants.mjs';
4+
5+
// This is used to ensure that URLs are always in the correct format
6+
function fixUrl(url) {
7+
if (!url) {
8+
return null;
9+
}
10+
11+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
12+
return `https://${url}`;
13+
}
14+
15+
return url;
16+
}
17+
18+
async function fetchOpenCollectiveData() {
19+
const endpoint = 'https://api.opencollective.com/graphql/v2';
20+
21+
const query = `{
22+
account(slug: "nodejs") {
23+
orders(status: ACTIVE, filter: INCOMING) {
24+
totalCount
25+
nodes {
26+
fromAccount {
27+
name
28+
website
29+
imageUrl
30+
}
31+
amount {
32+
value
33+
}
34+
tier {
35+
slug
36+
}
37+
frequency
38+
totalDonations {
39+
value
40+
}
41+
}
42+
}
43+
}
44+
donations: orders(
45+
account: { slug: "nodejs" }
46+
frequency: ONETIME
47+
status: PAID
48+
filter: INCOMING
49+
) {
50+
totalCount
51+
nodes {
52+
id
53+
updatedAt
54+
frequency
55+
status
56+
amount {
57+
value
58+
currency
59+
}
60+
fromAccount {
61+
name
62+
website
63+
imageUrl
64+
}
65+
}
66+
}
67+
}`;
68+
69+
const response = await fetch(endpoint, {
70+
method: 'POST',
71+
headers: { 'Content-Type': 'application/json' },
72+
body: JSON.stringify({ query }),
73+
});
74+
75+
const payload = await response.json();
76+
77+
const sponsors = payload.data.account.orders.nodes.map(order => ({
78+
name: order.fromAccount.name,
79+
url: fixUrl(order.fromAccount.website),
80+
image: order.fromAccount.imageUrl,
81+
source: 'opencollective',
82+
}));
83+
84+
const donations = payload.data.donations.nodes.map(transaction => ({
85+
name: transaction.fromAccount.name,
86+
url: fixUrl(transaction.fromAccount.website),
87+
image: transaction.fromAccount.imageUrl,
88+
source: 'opencollective',
89+
}));
90+
91+
sponsors.push(...donations);
92+
93+
return sponsors;
94+
}
95+
96+
async function fetchGitHubSponsors() {
97+
if (GITHUB_API_KEY === null) {
98+
return [];
99+
}
100+
101+
function sponsorshipsQuery(cursor = null) {
102+
return `
103+
query {
104+
organization(login: "nodejs") {
105+
sponsorshipsAsMaintainer (first:100, includePrivate: false, after: "${cursor}") {
106+
nodes {
107+
sponsor: sponsorEntity {
108+
...on User {
109+
id: databaseId,
110+
name,
111+
login,
112+
avatarUrl,
113+
url,
114+
websiteUrl
115+
}
116+
...on Organization {
117+
id: databaseId,
118+
name,
119+
login,
120+
avatarUrl,
121+
url,
122+
websiteUrl
123+
}
124+
},
125+
}
126+
pageInfo {
127+
endCursor
128+
startCursor
129+
hasNextPage
130+
hasPreviousPage
131+
}
132+
}
133+
}
134+
}
135+
`;
136+
}
137+
138+
const sponsorshipsResponse = await githubGraphQL(sponsorshipsQuery(), {
139+
headers: {
140+
authorization: `token ${GITHUB_API_KEY}`,
141+
},
142+
});
143+
144+
let pageInfo =
145+
sponsorshipsResponse.organization.sponsorshipsAsMaintainer.pageInfo;
146+
const sponsorships =
147+
sponsorshipsResponse.organization.sponsorshipsAsMaintainer.nodes;
148+
149+
while (pageInfo.hasNextPage) {
150+
const cursor = pageInfo.endCursor;
151+
152+
const pagedResponse = await githubGraphQL(sponsorshipsQuery(cursor), {
153+
headers: {
154+
authorization: `token ${GITHUB_API_KEY}`,
155+
},
156+
});
157+
158+
pageInfo = pagedResponse.organization.sponsorshipsAsMaintainer.pageInfo;
159+
sponsorships.push(
160+
...pagedResponse.organization.sponsorshipsAsMaintainer.nodes
161+
);
162+
}
163+
164+
const sponsors = sponsorships
165+
// return an array in the same format as Open Collective
166+
.map(({ sponsor }) => ({
167+
name: sponsor.name || sponsor.login,
168+
image: `https://avatars.githubusercontent.com/u/${sponsor.id}`,
169+
url: fixUrl(sponsor.websiteUrl || sponsor.url),
170+
source: 'github',
171+
}));
172+
173+
return sponsors;
174+
}
175+
176+
export { fetchOpenCollectiveData, fetchGitHubSponsors };
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { cache } from 'react';
2+
3+
import {
4+
fetchGitHubSponsors,
5+
fetchOpenCollectiveData,
6+
} from '#site/next-data/generators/backersData.mjs';
7+
8+
const [github, openCollective] = await Promise.all([
9+
fetchGitHubSponsors(),
10+
fetchOpenCollectiveData(),
11+
]);
12+
13+
const provideBackers = cache(() => [...github, ...openCollective]);
14+
15+
export default provideBackers;

apps/site/next.mdx.use.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import PartnersIconList from './components/Common/Partners/PartnersIconList';
66
import PartnersLogoList from './components/Common/Partners/PartnersLogoList';
77
import DownloadReleasesTable from './components/Downloads/DownloadReleasesTable';
88
import UpcomingMeetings from './components/MDX/Calendar/UpcomingMeetings';
9+
import WithBackers from './components/withBackers';
910
import WithBadgeGroup from './components/withBadgeGroup';
1011
import WithBanner from './components/withBanner';
1112
import WithNodeRelease from './components/withNodeRelease';
@@ -23,6 +24,8 @@ export const mdxComponents = {
2324
WithBanner,
2425
// HOC for providing Badge Data
2526
WithBadgeGroup,
27+
// HOC for providing Backers Data
28+
WithBackers,
2629
// Shows a list of Node.js Partners with Icons
2730
PartnersIconList,
2831
// Shows a list of Node.js Partners with Logos

apps/site/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@node-core/ui-components": "workspace:*",
3737
"@node-core/website-i18n": "workspace:*",
3838
"@nodevu/core": "0.3.0",
39+
"@octokit/graphql": "9.0.1",
3940
"@opentelemetry/api-logs": "~0.202.0",
4041
"@opentelemetry/instrumentation": "~0.202.0",
4142
"@opentelemetry/resources": "~1.30.1",

apps/site/pages/en/about/partners.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ Projects with their logo, name, tier, the description and a CTA button
4141

4242
Show a list of lists direct individual or organizational support that can be done through OpenCollective and GitHub Sponsors
4343

44+
<WithBackers />
45+
4446
## Become a Partner
4547

4648
this section isn't in the specification

pnpm-lock.yaml

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)