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