Skip to content

Commit a5b235f

Browse files
sokramontogeekchenxsan
authored
sometimes show sponsors by monthly amount (#4058)
* sometimes show sponsors by monthly amount grab monthly sponsoring from transactions allow to choose between monthly and total display * Update src/components/Splash/Splash.jsx Co-authored-by: Sam Chen <chenxsan@gmail.com> Co-authored-by: Fernando Montoya <montogeek@gmail.com> Co-authored-by: Sam Chen <chenxsan@gmail.com>
1 parent 6c09479 commit a5b235f

File tree

4 files changed

+141
-71
lines changed

4 files changed

+141
-71
lines changed

src/components/Splash/Splash.jsx

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const SponsorsPlaceholder = () => (
2323
<h2>Latest Sponsors</h2>
2424
<PlaceholderComponent />
2525

26-
<h2 id="sponsors">Platinum Sponsors</h2>
26+
<h2>Platinum Sponsors</h2>
2727
<PlaceholderComponent />
2828

2929
<h2>Gold Sponsors</h2>
@@ -42,6 +42,7 @@ const SponsorsPlaceholder = () => (
4242

4343
const Splash = () => {
4444
const [showSponsors, setShowSponsors] = useState(false);
45+
const [supportType, setSupportType] = useState(() => Math.random() < 0.33 ? 'monthly' : 'total');
4546
useEffect(() => {
4647
if(isClient) setShowSponsors(true);
4748
}, []);
@@ -62,7 +63,7 @@ const Splash = () => {
6263
<div className="splash__section page__content">
6364
<Container>
6465
<Markdown>
65-
<h1>Support the Team</h1>
66+
<h1 id="sponsors">Support the Team</h1>
6667

6768
<p>
6869
Through contributions, donations, and sponsorship, you allow webpack to thrive. Your
@@ -72,23 +73,19 @@ const Splash = () => {
7273

7374
{ showSponsors ? (
7475
<React.Suspense fallback={<SponsorsPlaceholder />}>
75-
<h2>Latest Sponsors</h2>
76-
<Support rank="latest" />
76+
<p><label><input type="checkbox" checked={supportType === 'monthly'} onChange={e => setSupportType(e.target.checked ? 'monthly' : 'total')} /> Show sponsors by their average monthly amount of sponsoring in the last year.</label></p>
7777

78-
<h2 id="sponsors">Platinum Sponsors</h2>
79-
<Support rank="platinum" />
78+
<Support type={supportType} rank="latest" />
8079

81-
<h2>Gold Sponsors</h2>
82-
<Support rank="gold" />
80+
<Support type={supportType} rank="platinum" />
8381

84-
<h2>Silver Sponsors</h2>
85-
<Support rank="silver" />
82+
<Support type={supportType} rank="gold" />
8683

87-
<h2>Bronze Sponsors</h2>
88-
<Support rank="bronze" />
84+
<Support type={supportType} rank="silver" />
8985

90-
<h2>Backers</h2>
91-
<Support rank="backer" />
86+
<Support type={supportType} rank="bronze" />
87+
88+
<Support type={supportType} rank="backer" />
9289
</React.Suspense>
9390
) : (
9491
<SponsorsPlaceholder />

src/components/Support/AdditionalSupporters.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,23 @@ export default [
77
avatar: 'https://static.moonmail.io/moonmail-logo.svg',
88
website: 'https://moonmail.io/?utm_source=webpack.js.org',
99
totalDonations: 11000,
10+
monthlyDonations: 0,
1011
reason: 'Paypal'
1112
},
1213
{
1314
name: 'Google Angular',
1415
avatar: 'https://res.cloudinary.com/opencollective/image/upload/v1485288529/angular_uxllte.png',
1516
website: 'https://angular.io/?utm_source=webpack&utm_medium=documentation&utm_campaign=sponsorship',
1617
totalDonations: 250000,
18+
monthlyDonations: 0,
1719
reason: 'Paypal'
1820
},
1921
{
2022
name: 'Architects.io',
2123
avatar: null,
2224
website: 'http://architects.io/?utm_source=webpack&utm_medium=documentation&utm_campaign=sponsorship',
2325
totalDonations: 30000,
26+
monthlyDonations: 0,
2427
reason: 'Paypal'
2528
},
2629
{
@@ -29,13 +32,15 @@ export default [
2932
avatar: 'https://opencollective-production.s3-us-west-1.amazonaws.com/e8a1de10-99c8-11e6-8650-f92e594d5de8.png',
3033
website: 'https://peerigon.com/?utm_source=webpack&utm_medium=documentation&utm_campaign=sponsorship',
3134
totalDonations: 144139,
35+
monthlyDonations: 0,
3236
reason: 'webpack meetup 2017-07'
3337
},
3438
{
3539
name: 'Segment',
3640
avatar: SegmentLogo,
3741
website: 'https://segment.com/?utm_source=webpack&utm_medium=documentation&utm_campaign=sponsorship',
3842
totalDonations: 2400000,
43+
monthlyDonations: 0,
3944
reason: 'Sponsorship 2017-07 - 2017-09'
4045
}
4146
];

src/components/Support/Support.jsx

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ for(const additional of Additional) {
2727
SUPPORTERS.sort((a, b) => b.totalDonations - a.totalDonations);
2828

2929
// Define ranks
30-
const ranks = {
30+
const totalRanks = {
3131
backer: {
3232
maximum: 200,
3333
random: 100
@@ -52,6 +52,31 @@ const ranks = {
5252
minimum: 50000
5353
}
5454
};
55+
const monthlyRanks = {
56+
backer: {
57+
maximum: 10,
58+
random: 100
59+
},
60+
latest: {
61+
maxAge: 14 * 24 * 60 * 60 * 1000,
62+
limit: 10
63+
},
64+
bronze: {
65+
minimum: 10,
66+
maximum: 100
67+
},
68+
silver: {
69+
minimum: 100,
70+
maximum: 500
71+
},
72+
gold: {
73+
minimum: 500,
74+
maximum: 2500
75+
},
76+
platinum: {
77+
minimum: 2500
78+
}
79+
};
5580

5681
function formatMoney(number) {
5782
let str = Math.round(number) + '';
@@ -68,20 +93,23 @@ export default class Support extends React.Component {
6893
}
6994

7095
handleInView = (inView) => {
71-
if (!inView) {
96+
if (!inView || this.state.inView) {
7297
return;
7398
}
7499
this.setState({ inView });
75100
};
76101

77102
render() {
78-
let { rank } = this.props;
103+
let { rank, type } = this.props;
79104

80105
const { inView } = this.state;
81106

82107
let supporters = SUPPORTERS;
83108
let minimum, maximum, maxAge, limit, random;
84109

110+
const ranks = type === 'monthly' ? monthlyRanks : totalRanks;
111+
const getAmount = type === 'monthly' ? item => item.monthlyDonations : item => item.totalDonations;
112+
85113
if (rank && ranks[rank]) {
86114
minimum = ranks[rank].minimum;
87115
maximum = ranks[rank].maximum;
@@ -91,11 +119,11 @@ export default class Support extends React.Component {
91119
}
92120

93121
if (typeof minimum === 'number') {
94-
supporters = supporters.filter(item => item.totalDonations >= minimum * 100);
122+
supporters = supporters.filter(item => getAmount(item) >= minimum * 100);
95123
}
96124

97125
if (typeof maximum === 'number') {
98-
supporters = supporters.filter(item => item.totalDonations < maximum * 100);
126+
supporters = supporters.filter(item => getAmount(item) < maximum * 100);
99127
}
100128

101129
if (typeof maxAge === 'number') {
@@ -116,12 +144,13 @@ export default class Support extends React.Component {
116144
supporters[i] = temp;
117145
}
118146
supporters = supporters.slice(0, random);
119-
120-
// resort to keep order
121-
supporters.sort((a, b) => b.totalDonations - a.totalDonations);
122147
}
123148

124-
return (
149+
// resort to keep order
150+
supporters.sort((a, b) => getAmount(b) - getAmount(a));
151+
152+
return <>
153+
<h2>{ rank === 'backer' ? 'Backers' : rank === 'latest' ? 'Latest Sponsors' : `${rank[0].toUpperCase()}${rank.slice(1)} ${type === 'monthly' ? 'Monthly ' : ''}Sponsors`}</h2>
125154
<VisibilitySensor delayedCall
126155
partialVisibility
127156
intervalDelay={ 300 }
@@ -136,8 +165,12 @@ export default class Support extends React.Component {
136165
<p>The following persons/organizations made their first donation in the last {Math.round(maxAge / (1000 * 60 * 60 * 24))} days (limited to the top {limit}).</p>
137166
) : (
138167
<p>
139-
<b className="support__rank">{ rank } sponsors</b>
140-
<span>are those who have pledged { minimum ? `$${formatMoney(minimum)}` : 'up' } { maximum ? `to $${formatMoney(maximum)}` : 'or more' } to webpack.</span>
168+
<b className="support__rank">{ type === 'monthly' ? rank + ' monthly': rank } sponsors</b>
169+
{ type === 'monthly' ? (
170+
<span>are those who are currently pledging { minimum ? `$${formatMoney(minimum)}` : 'up' } { maximum ? `to $${formatMoney(maximum)}` : 'or more' } monthly to webpack.</span>
171+
) : (
172+
<span>are those who have pledged { minimum ? `$${formatMoney(minimum)}` : 'up' } { maximum ? `to $${formatMoney(maximum)}` : 'or more' } to webpack.</span>
173+
) }
141174
</p>
142175
)}
143176
</div>
@@ -146,7 +179,7 @@ export default class Support extends React.Component {
146179
supporters.map((supporter, index) => (
147180
<a key={ supporter.slug || index }
148181
className="support__item"
149-
title={ `$${formatMoney(supporter.totalDonations / 100)} by ${supporter.name || supporter.slug}` }
182+
title={ `$${formatMoney(supporter.totalDonations / 100)} by ${supporter.name || supporter.slug} ($${formatMoney(supporter.monthlyDonations / 100)} monthly)` }
150183
target="_blank"
151184
rel="noopener nofollow"
152185
href={ supporter.website || `https://opencollective.com/${supporter.slug}` }>
@@ -166,7 +199,7 @@ export default class Support extends React.Component {
166199
</div>
167200
</div>
168201
</VisibilitySensor>
169-
);
202+
</>;
170203
}
171204

172205
/**
@@ -176,6 +209,7 @@ export default class Support extends React.Component {
176209
*/
177210
_handleImgError(e) {
178211
const imgNode = e.target;
179-
imgNode.src = SmallIcon;
212+
if (imgNode.getAttribute('src') === SmallIcon) return;
213+
imgNode.setAttribute('src', SmallIcon);
180214
}
181215
}

src/utilities/fetch-supporters.js

Lines changed: 77 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,11 @@ const absoluteFilename = path.resolve(__dirname, '..', 'components', 'Support',
1313

1414
const graphqlEndpoint = 'https://api.opencollective.com/graphql/v2';
1515

16-
const graphqlQuery = `query account($limit: Int, $offset: Int) {
16+
const membersGraphqlQuery = `query account($limit: Int, $offset: Int) {
1717
account(slug: "webpack") {
18-
orders(limit: $limit, offset: $offset) {
19-
limit
20-
offset
21-
totalCount
18+
members(limit: $limit, offset: $offset) {
2219
nodes {
23-
fromAccount {
20+
account {
2421
name
2522
slug
2623
website
@@ -35,67 +32,104 @@ const graphqlQuery = `query account($limit: Int, $offset: Int) {
3532
}
3633
}`;
3734

38-
const graphqlPageSize = 1000;
35+
const transactionsGraphqlQuery = `query account($limit: Int, $offset: Int) {
36+
account(slug: "webpack") {
37+
transactions(limit: $limit, offset: $offset, includeIncognitoTransactions: false) {
38+
nodes {
39+
amountInHostCurrency {
40+
value
41+
}
42+
fromAccount {
43+
name
44+
slug
45+
website
46+
imageUrl
47+
}
48+
createdAt
49+
}
50+
}
51+
}
52+
}`;
53+
54+
const graphqlPageSize = 5000;
3955

4056
const nodeToSupporter = node => ({
41-
name: node.fromAccount.name,
42-
slug: node.fromAccount.slug,
43-
website: node.fromAccount.website,
44-
avatar: node.fromAccount.imageUrl,
57+
name: node.account.name,
58+
slug: node.account.slug,
59+
website: node.account.website,
60+
avatar: node.account.imageUrl,
4561
firstDonation: node.createdAt,
46-
totalDonations: node.totalDonations.value * 100
62+
totalDonations: node.totalDonations.value * 100,
63+
monthlyDonations: 0
4764
});
4865

49-
const getAllOrders = async () => {
66+
const getAllNodes = async (graphqlQuery, getNodes) => {
5067
const requestOptions = {
5168
method: 'POST',
5269
uri: graphqlEndpoint,
5370
body: { query: graphqlQuery, variables: { limit: graphqlPageSize, offset: 0 } },
5471
json: true
5572
};
5673

57-
let allOrders = [];
74+
let allNodes = [];
5875

59-
// Handling pagination if necessary (2 pages for ~1400 results in May 2019)
76+
// Handling pagination if necessary
6077
// eslint-disable-next-line
6178
while (true) {
6279
const result = await request(requestOptions);
63-
const orders = result.data.account.orders.nodes;
64-
allOrders = [...allOrders, ...orders];
80+
const nodes = getNodes(result.data);
81+
allNodes = [...allNodes, ...nodes];
6582
requestOptions.body.variables.offset += graphqlPageSize;
66-
if (orders.length < graphqlPageSize) {
67-
return allOrders;
83+
if (nodes.length < graphqlPageSize) {
84+
return allNodes;
6885
}
6986
}
7087
};
7188

72-
getAllOrders()
73-
.then(orders => {
74-
let supporters = orders.map(nodeToSupporter).sort((a, b) => b.totalDonations - a.totalDonations);
89+
const oneYearAgo = Date.now() - 365 * 24 * 60 * 60 * 1000;
7590

76-
// Deduplicating supporters with multiple orders
77-
supporters = uniqBy(supporters, 'slug');
91+
(async () => {
92+
const members = await getAllNodes(membersGraphqlQuery, data => data.account.members.nodes);
93+
let supporters = members.map(nodeToSupporter).sort((a, b) => b.totalDonations - a.totalDonations);
7894

79-
if (!Array.isArray(supporters)) {
80-
throw new Error('Supporters data is not an array.');
81-
}
95+
// Deduplicating supporters with multiple orders
96+
supporters = uniqBy(supporters, 'slug');
8297

83-
for (const item of supporters) {
84-
for (const key of REQUIRED_KEYS) {
85-
if (!item || typeof item !== 'object') {
86-
throw new Error(`Supporters: ${JSON.stringify(item)} is not an object.`);
87-
}
88-
if (!(key in item)) {
89-
throw new Error(`Supporters: ${JSON.stringify(item)} doesn't include ${key}.`);
90-
}
98+
const supportersBySlug = new Map();
99+
for (const supporter of supporters) {
100+
for (const key of REQUIRED_KEYS) {
101+
if (!supporter || typeof supporter !== 'object') {
102+
throw new Error(`Supporters: ${JSON.stringify(supporter)} is not an object.`);
103+
}
104+
if (!(key in supporter)) {
105+
throw new Error(`Supporters: ${JSON.stringify(supporter)} doesn't include ${key}.`);
91106
}
92107
}
108+
supportersBySlug.set(supporter.slug, supporter);
109+
}
110+
111+
// Calculate monthly amount from transactions
112+
const transactions = await getAllNodes(transactionsGraphqlQuery, data => data.account.transactions.nodes);
113+
for (const transaction of transactions) {
114+
if (!transaction.amountInHostCurrency) continue;
115+
const amount = transaction.amountInHostCurrency.value;
116+
if (!amount || amount <= 0) continue;
117+
const date = +new Date(transaction.createdAt);
118+
if (date < oneYearAgo) continue;
119+
const supporter = supportersBySlug.get(transaction.fromAccount.slug);
120+
if (!supporter) continue;
121+
supporter.monthlyDonations += amount * 100 / 12;
122+
}
93123

94-
// Write the file
95-
return asyncWriteFile(absoluteFilename, JSON.stringify(supporters, null, 2)).then(() =>
96-
console.log(`Fetched 1 file: ${filename}`)
97-
);
98-
})
99-
.catch(error => {
100-
console.error('utilities/fetch-supporters:', error);
101-
});
124+
for (const supporter of supporters) {
125+
supporter.monthlyDonations = Math.round(supporter.monthlyDonations);
126+
}
127+
128+
// Write the file
129+
return asyncWriteFile(absoluteFilename, JSON.stringify(supporters, null, 2)).then(() =>
130+
console.log(`Fetched 1 file: ${filename}`)
131+
);
132+
})().catch(error => {
133+
console.error('utilities/fetch-supporters:', error);
134+
process.exitCode = 1;
135+
});

0 commit comments

Comments
 (0)