diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 04b21f65..c7cf2272 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -92,6 +92,7 @@ jobs:
-e THEME_IMPORTANT \
-e THEME_WARNING \
-e THEME_CAUTION \
+ -e CONTRIBUTORS_PAT \
ghcr.io/pmndrs/docs:2 npm run build
env:
BASE_PATH: ${{ steps.set-base-path.outputs.BASE_PATH }}
@@ -115,6 +116,7 @@ jobs:
THEME_IMPORTANT: '${{ inputs.theme_important }}'
THEME_WARNING: '${{ inputs.theme_warning }}'
THEME_CAUTION: '${{ inputs.theme_caution }}'
+ CONTRIBUTORS_PAT: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-pages-artifact@v3
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6085ec17..1f6754ce 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -70,6 +70,7 @@ jobs:
--build-env THEME_IMPORTANT="#8957e5" \
--build-env THEME_WARNING="#d29922" \
--build-env THEME_CAUTION="#da3633" \
+ --build-env CONTRIBUTORS_PAT="${{ secrets.GITHUB_TOKEN }}" \
> deployment-url.txt
echo "deployment_url=$(cat deployment-url.txt)" >> $GITHUB_OUTPUT
diff --git a/docs/getting-started/authoring.mdx b/docs/getting-started/authoring.mdx
index b93a7efb..f04598e0 100644
--- a/docs/getting-started/authoring.mdx
+++ b/docs/getting-started/authoring.mdx
@@ -414,3 +414,32 @@ or better:
I'm a deprecated hint. Use `Gha` instead.
+
+### `Contributors`
+
+```md
+
+```
+
+> [!WARNING]
+> [`CONTRIBUTORS_PAT`](introduction#configuration:~:text=CONTRIBUTORS_PAT) needs to be set. Otherwise, it will display John Doe.
+
+
+ Result
+
+
+
+
+
+### Backers
+
+```md
+
+```
+
+
+ Result
+
+
+
+
\ No newline at end of file
diff --git a/docs/getting-started/introduction.mdx b/docs/getting-started/introduction.mdx
index 2563f742..4e5241f8 100644
--- a/docs/getting-started/introduction.mdx
+++ b/docs/getting-started/introduction.mdx
@@ -47,6 +47,7 @@ $ npm ci
| `THEME_IMPORTANT` | "important" color | `#8957e5` |
| `THEME_WARNING` | "warning" color | `#d29922` |
| `THEME_CAUTION` | "caution" color | `#da3633` |
+| `CONTRIBUTORS_PAT` | GitHub token for contributors API (see: https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2022-11-28#list-repository-collaborators) | `ghp_1234567890` |
\* Required
@@ -116,6 +117,7 @@ $ (
export THEME_IMPORTANT="#8957e5"
export THEME_WARNING="#d29922"
export THEME_CAUTION="#da3633"
+ export CONTRIBUTORS_PAT=
kill $(lsof -ti:"$_PORT")
npx serve $MDX -p $_PORT --no-port-switching --no-clipboard &
@@ -163,6 +165,7 @@ $ (
export THEME_IMPORTANT="#8957e5"
export THEME_WARNING="#d29922"
export THEME_CAUTION="#da3633"
+ export CONTRIBUTORS_PAT=
npm run build
@@ -211,6 +214,7 @@ $ (
export THEME_IMPORTANT="#8957e5"
export THEME_WARNING="#d29922"
export THEME_CAUTION="#da3633"
+ export CONTRIBUTORS_PAT=
rm -rf "$MDX/out"
@@ -238,6 +242,7 @@ $ (
-e THEME_IMPORTANT \
-e THEME_WARNING \
-e THEME_CAUTION \
+ -e CONTRIBUTORS_PAT \
pmndrs-docs npm run build
kill $(lsof -ti:"$_PORT")
diff --git a/package-lock.json b/package-lock.json
index 4254bb13..59896686 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@codesandbox/sandpack-react": "^2.19.8",
+ "@octokit/core": "^6.1.2",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-visually-hidden": "^1.1.0",
@@ -891,7 +892,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz",
"integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 18"
@@ -901,7 +901,6 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz",
"integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^5.0.0",
@@ -920,7 +919,6 @@
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.0.0",
@@ -934,7 +932,6 @@
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz",
"integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@octokit/request": "^9.0.0",
@@ -949,7 +946,6 @@
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==",
- "dev": true,
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest": {
@@ -1007,7 +1003,6 @@
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
"integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^10.0.0",
@@ -1023,7 +1018,6 @@
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz",
"integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.0.0"
@@ -1036,7 +1030,6 @@
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz",
"integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
@@ -2732,7 +2725,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
"integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
- "dev": true,
"license": "Apache-2.0"
},
"node_modules/binary-extensions": {
@@ -15284,7 +15276,6 @@
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==",
- "dev": true,
"license": "ISC"
},
"node_modules/universalify": {
diff --git a/package.json b/package.json
index 3b338a6d..5e971d97 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
},
"dependencies": {
"@codesandbox/sandpack-react": "^2.19.8",
+ "@octokit/core": "^6.1.2",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-visually-hidden": "^1.1.0",
diff --git a/src/components/mdx/People/People.tsx b/src/components/mdx/People/People.tsx
new file mode 100644
index 00000000..d572333b
--- /dev/null
+++ b/src/components/mdx/People/People.tsx
@@ -0,0 +1,151 @@
+import cn from '@/lib/cn'
+import { initials } from '@/utils/text'
+import { Octokit } from '@octokit/core'
+import Image from 'next/image'
+import { cache, ComponentProps } from 'react'
+import backerBadge from './backer-badge.svg'
+
+// ██████ ██████ ███ ██ ████████ ██████ ██ ██████ ██ ██ ████████ ██████ ██████ ███████
+// ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
+// ██ ██ ██ ██ ██ ██ ██ ██████ ██ ██████ ██ ██ ██ ██ ██ ██████ ███████
+// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
+// ██████ ██████ ██ ████ ██ ██ ██ ██ ██████ ██████ ██ ██████ ██ ██ ███████
+
+const octokit = new Octokit({
+ auth: process.env.CONTRIBUTORS_PAT,
+})
+
+export async function Contributors({
+ owner,
+ repo,
+ limit = 50,
+ className,
+ ...props
+}: { owner: string; repo: string; limit: number } & ComponentProps<'ul'>) {
+ const contributors = (
+ await cachedFetchContributors(owner, repo).catch(
+ (err) =>
+ Array.from({ length: 100 }).map(() => ({
+ login: 'jdoe',
+ html_url: 'https://github.com/jdoe',
+ })) as Awaited>,
+ )
+ ).slice(0, limit)
+
+ return (
+
+
+ {contributors.map(({ html_url, avatar_url, login }) => (
+ -
+
+
+ ))}
+
+
+ )
+}
+
+async function fetchContributors(owner: string, repo: string) {
+ const res = await octokit.request(`GET /repos/{owner}/{repo}/collaborators`, {
+ owner,
+ repo,
+ headers: {
+ 'X-GitHub-Api-Version': '2022-11-28',
+ },
+ })
+ return res.data
+}
+const cachedFetchContributors = cache(fetchContributors)
+
+// ██████ █████ ██████ ██ ██ ███████ ██████ ███████
+// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
+// ██████ ███████ ██ █████ █████ ██████ ███████
+// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
+// ██████ ██ ██ ██████ ██ ██ ███████ ██ ██ ███████
+
+export async function Backers({
+ repo,
+ limit = 50,
+ className,
+ ...props
+}: ComponentProps<'ul'> & {
+ repo: string
+ limit: number
+}) {
+ const backers = (await fetchBackers(repo)).slice(0, limit)
+
+ return (
+
+
+ {backers.map((backer) => (
+ -
+
+
+ ))}
+
+
+
+
+
+
+
+ )
+}
+
+async function fetchBackers(repo: string) {
+ const res = await fetch(`https://opencollective.com/${repo}/members/users.json`)
+ const backers: {
+ profile: string
+ name: string
+ image: string
+ totalAmountDonated: number
+ }[] = await res.json()
+
+ const backersMap = new Map(backers.map((backer) => [backer.name, backer]))
+ backersMap.forEach((backer) => {
+ const existingBacker = backersMap.get(backer.name)
+ if (existingBacker && backer.totalAmountDonated >= existingBacker.totalAmountDonated) {
+ backersMap.set(backer.name, backer) // replace with the backer with the highest donation
+ }
+ })
+ const uniqueBackers = Array.from(backersMap.values())
+
+ return uniqueBackers.sort((a, b) => b.totalAmountDonated - a.totalAmountDonated)
+}
+
+const cachedFetchBackers = cache(fetchBackers)
+
+//
+// common
+//
+
+function Avatar({
+ profileUrl,
+ imageUrl,
+ name,
+ className,
+ ...props
+}: { profileUrl: string; imageUrl: string; name: string } & ComponentProps<'a'>) {
+ return (
+
+ {imageUrl ? (
+
+ ) : (
+ initials(name)
+ )}
+
+ )
+}
diff --git a/src/components/mdx/People/backer-badge.svg b/src/components/mdx/People/backer-badge.svg
new file mode 100644
index 00000000..8180f020
--- /dev/null
+++ b/src/components/mdx/People/backer-badge.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/mdx/People/index.ts b/src/components/mdx/People/index.ts
new file mode 100644
index 00000000..3c50cbb9
--- /dev/null
+++ b/src/components/mdx/People/index.ts
@@ -0,0 +1 @@
+export * from './People'
diff --git a/src/components/mdx/index.tsx b/src/components/mdx/index.tsx
index 89e53f16..4942fc28 100644
--- a/src/components/mdx/index.tsx
+++ b/src/components/mdx/index.tsx
@@ -7,6 +7,7 @@ export * from './Hint'
export * from './Img'
export * from './Intro'
export * from './Keypoints'
+export * from './People'
export * from './Summary'
export * from './Toc'
diff --git a/src/utils/text.ts b/src/utils/text.ts
index cfb5b46f..86b399ee 100644
--- a/src/utils/text.ts
+++ b/src/utils/text.ts
@@ -10,3 +10,17 @@ export const highlight = (text: string, target: string) =>
target.length > 0
? text.replace(new RegExp(escape(target), 'gi'), (match: string) => `${match}`)
: text
+
+export function initials(name: string) {
+ const parts = name.split(' ')
+
+ if (parts.length > 1) {
+ return parts
+ .slice(0, 2)
+ .map(([char]) => char)
+ .join('')
+ .toUpperCase()
+ }
+
+ return name.slice(0, 2).toUpperCase()
+}