diff --git a/.gitignore b/.gitignore
index 911ff19cab..38a69c939b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,7 +30,9 @@ _output
/site/.sessions
/site/static/svelte-app.json
/site/static/contributors.jpg
+/site/static/donors.jpg
/site/static/workers
/site/scripts/svelte-app
/site/scripts/community
/site/src/routes/_contributors.js
+/site/src/routes/_donors.js
diff --git a/site/scripts/get_donors.js b/site/scripts/get_donors.js
new file mode 100644
index 0000000000..9e26391195
--- /dev/null
+++ b/site/scripts/get_donors.js
@@ -0,0 +1,63 @@
+import 'dotenv/config';
+import fs from 'fs';
+import fetch from 'node-fetch';
+import Jimp from 'jimp';
+import { dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+
+const force = process.env.FORCE_UPDATE === 'true';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+process.chdir(__dirname);
+
+const outputFile = `../src/routes/_donors.js`;
+if (!force && fs.existsSync(outputFile)) {
+ console.info(`[update/donors] ${outputFile} exists. Skipping`);
+ process.exit(0);
+}
+
+const SIZE = 64;
+
+async function main() {
+ const res = await fetch('https://opencollective.com/svelte/members/all.json');
+ const donors = await res.json();
+
+ const unique = new Map();
+ donors.forEach(d => unique.set(d.profile, d));
+
+ let backers = [...unique.values()]
+ .filter(({ role }) => role === 'BACKER')
+ .sort((a, b) => b.totalAmountDonated - a.totalAmountDonated);
+
+ const included = [];
+ for (let i = 0; i < backers.length; i += 1) {
+ const backer = backers[i];
+ console.log(`${i} / ${backers.length}: ${backer.name}`);
+
+ try {
+ const image_data = await fetch(backer.image);
+ const buffer = await image_data.arrayBuffer();
+ const image = await Jimp.read(buffer);
+ image.resize(SIZE, SIZE);
+ included.push({ backer, image });
+ } catch( err) {
+ console.log(`Skipping ${backer.name}: no image data`);
+ }
+ }
+
+ const sprite = new Jimp(SIZE * included.length, SIZE);
+ for (let i = 0; i < included.length; i += 1) {
+ sprite.composite(included[i].image, i * SIZE, 0);
+ }
+
+ await sprite.quality(80).write(`../static/donors.jpg`);
+ // TODO: Optimizing the static/donors.jpg image should probably get automated as well
+ console.log('remember to additionally optimize the resulting /static/donors.jpg image file via e.g. https://squoosh.app ');
+
+ const str = `[\n\t${included.map(a => `${JSON.stringify(a.backer.name)}`).join(',\n\t')}\n]`;
+
+ fs.writeFileSync(outputFile, `export default ${str};`);
+}
+
+main();
diff --git a/site/scripts/update.js b/site/scripts/update.js
index 1e8789e67c..724c0d1be8 100644
--- a/site/scripts/update.js
+++ b/site/scripts/update.js
@@ -4,5 +4,6 @@ sh.env['FORCE_UPDATE'] = process.argv.includes('--force=true');
Promise.all([
sh.exec('node ./scripts/get_contributors.js'),
+ sh.exec('node ./scripts/get_donors.js'),
sh.exec('node ./scripts/update_template.js')
]);
diff --git a/site/src/routes/_components/Donors.svelte b/site/src/routes/_components/Donors.svelte
new file mode 100644
index 0000000000..dc1f7ffcc8
--- /dev/null
+++ b/site/src/routes/_components/Donors.svelte
@@ -0,0 +1,27 @@
+
+
+
+
+{#each donors as donor, i}
+
+ {donor}
+
+{/each}
diff --git a/site/src/routes/index.svelte b/site/src/routes/index.svelte
index 3c84d6ea15..4290c99778 100644
--- a/site/src/routes/index.svelte
+++ b/site/src/routes/index.svelte
@@ -1,6 +1,7 @@