diff --git a/apps/nextjs/src/app/[locale]/manage/about/page.tsx b/apps/nextjs/src/app/[locale]/manage/about/page.tsx
index 9aa4aeb10..0be4d5687 100644
--- a/apps/nextjs/src/app/[locale]/manage/about/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/about/page.tsx
@@ -25,7 +25,14 @@ import {
Title,
UnstyledButton,
} from "@mantine/core";
-import { IconKeyboard, IconLanguage, IconLibrary, IconPackage, IconUsers } from "@tabler/icons-react";
+import {
+ IconKeyboard,
+ IconLanguage,
+ IconLibrary,
+ IconMoneybagHeart,
+ IconPackage,
+ IconUsers,
+} from "@tabler/icons-react";
import { capitalize, objectEntries } from "@homarr/common";
import { hotkeys } from "@homarr/definitions";
@@ -37,6 +44,7 @@ import { createMetaTitle } from "~/metadata";
import type { PackageJsonDependencies } from "~/versions/package-reader";
import { getPackageVersion } from "~/versions/package-reader";
import type githubContributorsJson from "../../../../../../../static-data/contributors.json";
+import type openCollectiveContributorsJson from "../../../../../../../static-data/opencollective-contributors.json";
import type crowdinContributorsJson from "../../../../../../../static-data/translators.json";
import classes from "./about.module.css";
@@ -71,6 +79,10 @@ export default async function AboutPage() {
res.json(),
)) as typeof crowdinContributorsJson;
+ const openCollectiveContributors = (await fetch(`${baseServerUrl}/api/about/contributors/opencollective`).then(
+ (res) => res.json(),
+ )) as typeof openCollectiveContributorsJson;
+
return (
@@ -136,6 +148,30 @@ export default async function AboutPage() {
+
+ }>
+
+ {t("accordion.sponsors.title")}
+
+ {t("accordion.sponsors.subtitle", {
+ count: String(openCollectiveContributors.length),
+ })}
+
+
+
+
+
+ {openCollectiveContributors.map((sponsor) => (
+
+ ))}
+
+
+
}>
diff --git a/apps/nextjs/src/app/api/about/contributors/opencollective/route.ts b/apps/nextjs/src/app/api/about/contributors/opencollective/route.ts
new file mode 100644
index 000000000..369f14a8c
--- /dev/null
+++ b/apps/nextjs/src/app/api/about/contributors/opencollective/route.ts
@@ -0,0 +1,9 @@
+import { NextResponse } from "next/server";
+
+import openCollectiveContributors from "../../../../../../../../static-data/opencollective-contributors.json";
+
+export const GET = () => {
+ return NextResponse.json(openCollectiveContributors);
+};
+
+export const dynamic = "force-static";
diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json
index aadcb0d96..817abb3f8 100644
--- a/packages/translation/src/lang/en.json
+++ b/packages/translation/src/lang/en.json
@@ -3790,6 +3790,10 @@
"title": "Translators",
"subtitle": "{count} contributing translations in many languages"
},
+ "sponsors": {
+ "title": "Sponsors",
+ "subtitle": "{count} supporting the project financially"
+ },
"libraries": {
"title": "Libraries",
"subtitle": "{count} used in the Code of Homarr"
diff --git a/scripts/update-contributors.mjs b/scripts/update-contributors.mjs
index 6bf17a250..2c7fb1054 100644
--- a/scripts/update-contributors.mjs
+++ b/scripts/update-contributors.mjs
@@ -1,14 +1,12 @@
import fs from "fs/promises";
const sources = {
- crowdin: [
- { projectId: 534422 },
- { projectId: 742587 },
- ],
+ crowdin: [{ projectId: 534422 }, { projectId: 742587 }],
github: [
{ slug: "ajnart", repository: "homarr" },
{ slug: "homarr-labs", repository: "homarr" },
],
+ opencollective: [{ slug: "homarr" }],
};
const env = {
@@ -47,11 +45,55 @@ const fetchCrowdinMembers = async (projectId) => {
};
const response = await fetch(url, options);
- const data = await response.json();
+ const result = await response.json();
- return data.data.flatMap((data) => data.data).map(contributor => ({
- username: contributor.username,
- avatarUrl: contributor.avatarUrl,
+ return result.data
+ .flatMap(({ data }) => data)
+ .map((contributor) => ({
+ username: contributor.username,
+ avatarUrl: contributor.avatarUrl,
+ }));
+};
+
+const fetchOpenCollectiveContributors = async (slug) => {
+ const query = `
+ query($slug: String!) {
+ account(slug: $slug) {
+ ... on Organization {
+ members(role: [BACKER]) {
+ nodes {
+ role
+ account {
+ id
+ name
+ slug
+ imageUrl
+ }
+ }
+ }
+ }
+ }
+ }
+ `;
+
+ const response = await fetch("https://api.opencollective.com/graphql/v2", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ query,
+ variables: { slug },
+ }),
+ });
+
+ const result = await response.json();
+ const members = result.data?.account?.members?.nodes || [];
+ return members.map((member) => ({
+ id: member.account.id,
+ name: member.account.name,
+ slug: member.account.slug,
+ imageUrl: member.account.imageUrl,
}));
};
@@ -61,6 +103,7 @@ const distinctBy = (callback) => (value, index, self) => {
const githubContributors = [];
const crowdinContributors = [];
+const opencollectiveContributors = [];
for (const { repository, slug } of sources.github) {
githubContributors.push(...(await fetchGithubContributors(slug, repository)));
@@ -77,3 +120,14 @@ for (const { projectId } of sources.crowdin) {
}
const distinctCrowdinContributors = crowdinContributors.filter(distinctBy((contributor) => contributor.username));
await fs.writeFile("./static-data/translators.json", JSON.stringify(distinctCrowdinContributors));
+
+for (const { slug } of sources.opencollective) {
+ opencollectiveContributors.push(...(await fetchOpenCollectiveContributors(slug)));
+}
+const distinctOpenCollectiveContributors = opencollectiveContributors.filter(
+ distinctBy((contributor) => contributor.slug),
+);
+await fs.writeFile(
+ "./static-data/opencollective-contributors.json",
+ JSON.stringify(distinctOpenCollectiveContributors),
+);
diff --git a/static-data/opencollective-contributors.json b/static-data/opencollective-contributors.json
new file mode 100644
index 000000000..fcdf9d4da
--- /dev/null
+++ b/static-data/opencollective-contributors.json
@@ -0,0 +1 @@
+[{"id":"zaxon793-jy8gpl0v-d89pbrkd-emwl5v04","name":"Mike","slug":"guest-5da646e8","imageUrl":"https://images.opencollective.com/guest-5da646e8/avatar.png"},{"id":"ywz9j4av-god8pgwg-db36mr35-nxklb0e7","name":"Guest","slug":"guest-554ec68f","imageUrl":"https://images.opencollective.com/guest-554ec68f/avatar.png"},{"id":"mlo94zn7-x08dpo09-xlxpewga-3vjbrky5","name":"Robert Hermann","slug":"robert-hermann","imageUrl":"https://images.opencollective.com/robert-hermann/avatar.png"},{"id":"zaxon793-jy8gpl0n-8r3pbrkd-emwl5v04","name":"Sebastian Raubuch","slug":"sebastian-raubuch","imageUrl":"https://images.opencollective.com/sebastian-raubuch/avatar.png"},{"id":"nmlo94zn-7x08dpod-ykm6ewga-3vjbrky5","name":"Didier LUTHI","slug":"didier-luthi","imageUrl":"https://images.opencollective.com/didier-luthi/avatar.png"},{"id":"vedj9wro-z3a56dgk-z3z67blg-8x4m0ykn","name":"Guest","slug":"guest-b1459a68","imageUrl":"https://images.opencollective.com/guest-b1459a68/avatar.png"},{"id":"gm9bnk80-437xqry4-8vepvzeo-ljdayw5r","name":"Mohister","slug":"mohister","imageUrl":"https://images.opencollective.com/mohister/1d8a592/avatar.png"},{"id":"vrgbk357-l4x96e70-m0apomew-a0jdyzn8","name":"sidens","slug":"sidens","imageUrl":"https://images.opencollective.com/sidens/avatar.png"},{"id":"mlo94zn7-x08dpo0d-l7epewga-3vjbrky5","name":"Chris","slug":"guest-937065bb","imageUrl":"https://images.opencollective.com/guest-937065bb/avatar.png"},{"id":"k9mbw7y4-8r3zq3ol-7zlq0ej5-lavnodgx","name":"Nick W","slug":"guest-7501840a","imageUrl":"https://images.opencollective.com/guest-7501840a/avatar.png"},{"id":"e0adkjrv-8xwm69o8-5al67z5n-4l93bgoy","name":"Jeppedy","slug":"guest-ab78e46a","imageUrl":"https://images.opencollective.com/guest-ab78e46a/avatar.png"},{"id":"a47byg9n-xozdp8xo-jgm6mjlv-03rek5w8","name":"naru","slug":"guest-02fd264a","imageUrl":"https://images.opencollective.com/guest-02fd264a/avatar.png"},{"id":"3k0exgzn-m8yj64g3-ll865wao-9r7b4dlv","name":"Jared Hocutt","slug":"jared-hocutt","imageUrl":"https://images.opencollective.com/jared-hocutt/avatar.png"},{"id":"eng0kzdy-vor4pz8l-exopbma8-37xlw95j","name":"Incognito","slug":"user-e63f4a5b","imageUrl":"https://images.opencollective.com/user-e63f4a5b/avatar.png"},{"id":"8k03reyd-5agmq50v-8vyplbwo-z7j4nxv9","name":"dazi","slug":"guest-11663a46","imageUrl":"https://images.opencollective.com/guest-11663a46/avatar.png"},{"id":"vedj9wro-z3a56dgd-zwl67blg-8x4m0ykn","name":"ZoopaMario","slug":"zoopamario","imageUrl":"https://images.opencollective.com/zoopamario/bea900d/avatar.png"},{"id":"a47byg9n-xozdp8xe-xmb6mjlv-03rek5w8","name":"FlamebergUA","slug":"guest-a59fd500","imageUrl":"https://images.opencollective.com/guest-a59fd500/avatar.png"},{"id":"8k03reyd-5agmq50y-kzoplbwo-z7j4nxv9","name":"StLCards","slug":"guest-e1de33dc","imageUrl":"https://images.opencollective.com/guest-e1de33dc/avatar.png"},{"id":"ejoxl3az-45w9pa54-dk46y870-mgkbrvdn","name":"Varadha","slug":"guest-6f68eed9","imageUrl":"https://images.opencollective.com/guest-6f68eed9/avatar.png"},{"id":"jrkx5lmn-v904qjl0-3oz68bwa-7zdygoe3","name":"Guest","slug":"guest-6da419b5","imageUrl":"https://images.opencollective.com/guest-6da419b5/avatar.png"},{"id":"mywxoz34-09rl6k3z-lmlqvenb-dj7gk85a","name":"Guest","slug":"guest-122d7e0a","imageUrl":"https://images.opencollective.com/guest-122d7e0a/avatar.png"},{"id":"3z8arxve-ymko60vm-3eaqgl5n-bj9w704d","name":"Ricardo","slug":"ricardo34","imageUrl":"https://images.opencollective.com/ricardo34/avatar.png"},{"id":"e0adkjrv-8xwm69or-reo67z5n-4l93bgoy","name":"GoM","slug":"guest-3944feab","imageUrl":"https://images.opencollective.com/guest-3944feab/avatar.png"},{"id":"gm9bnk80-437xqrnr-r5o6vzeo-ljdayw5r","name":"Brent Maxwell","slug":"brentmaxwell","imageUrl":"https://images.opencollective.com/brentmaxwell/avatar.png"},{"id":"gnxdwzj3-le5mpwzd-ll0py8rv-bko04a97","name":"Eric G","slug":"guest-478270f1","imageUrl":"https://images.opencollective.com/guest-478270f1/avatar.png"},{"id":"v349mrwg-z75lpyjo-ez8pa08d-jeybknox","name":"ninja","slug":"guest-0bb6ba36","imageUrl":"https://images.opencollective.com/guest-0bb6ba36/avatar.png"},{"id":"ywz9j4av-god8pgw0-53y6mr35-nxklb0e7","name":"s11","slug":"guest-6e9a2588","imageUrl":"https://images.opencollective.com/guest-6e9a2588/avatar.png"},{"id":"mlo94zn7-x08dpo30-v73pewga-3vjbrky5","name":"Incognito","slug":"user-cbad11af","imageUrl":"https://images.opencollective.com/user-cbad11af/avatar.png"},{"id":"v349mrwg-z75lpy7l-yzgpa08d-jeybknox","name":"Ronald Simmons","slug":"ronald-simmons","imageUrl":"https://images.opencollective.com/ronald-simmons/avatar.png"},{"id":"7ywz9j4a-vgod8pgl-me9qmr35-nxklb0e7","name":"Joel","slug":"guest-3376ab02","imageUrl":"https://images.opencollective.com/guest-3376ab02/avatar.png"},{"id":"v349mrwg-z75lpyjw-kxzpa08d-jeybknox","name":"Gokay Yamankurt","slug":"gokay-yamankurt","imageUrl":"https://images.opencollective.com/gokay-yamankurt/avatar.png"},{"id":"v349mrwg-z75lpyjv-59rpa08d-jeybknox","name":"Rhiz3K","slug":"rhiz3k","imageUrl":"https://images.opencollective.com/rhiz3k/7287acd/avatar.png"},{"id":"8k03reyd-5agmq5x7-77gplbwo-z7j4nxv9","name":"vukie","slug":"guest-40aabe13","imageUrl":"https://images.opencollective.com/guest-40aabe13/avatar.png"},{"id":"a47byg9n-xozdp84a-rmvqmjlv-03rek5w8","name":"Guest","slug":"guest-2fe979fa","imageUrl":"https://images.opencollective.com/guest-2fe979fa/avatar.png"},{"id":"rxg0j35l-zkwm6vgm-k3x6voe9-8n47daby","name":"AngryPig","slug":"guest-b9e8a062","imageUrl":"https://images.opencollective.com/guest-b9e8a062/avatar.png"}]
\ No newline at end of file