diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml new file mode 100644 index 0000000000..0e73f4f970 --- /dev/null +++ b/.github/workflows/contributors.yml @@ -0,0 +1,38 @@ +name: Update Contributors + +on: + schedule: + - cron: "23 3 * * 1" + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: contributors + cancel-in-progress: true + +jobs: + update: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v6 + with: + node-version: 22 + - uses: pnpm/action-setup@v6 + - name: Refresh contributor activity + working-directory: docs + run: pnpm contributors:update + env: + GITHUB_TOKEN: ${{ github.token }} + - name: Commit refreshed snapshot + run: | + if git diff --quiet -- docs/data/contributors.json; then + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add docs/data/contributors.json + git commit -m "chore(docs): refresh contributor activity" + git push diff --git a/docs/CONTRIBUTORS.md b/docs/CONTRIBUTORS.md new file mode 100644 index 0000000000..45c0cb0682 --- /dev/null +++ b/docs/CONTRIBUTORS.md @@ -0,0 +1,20 @@ +# Contributor certificate OAuth setup + +The contributors page treats an account as eligible only when it has authored at least one pull request merged into `t8y2/dbx`. Commit counts are displayed as an additional metric but do not grant eligibility by themselves. + +Create a GitHub OAuth App with: + +- Homepage URL: `https://dbxio.com` +- Authorization callback URL: `https://dbxio.com/api/auth/github/callback` + +Configure the deployed Cloudflare Worker secrets from `docs/`: + +```bash +pnpm dlx wrangler secret put GITHUB_CLIENT_ID +pnpm dlx wrangler secret put GITHUB_CLIENT_SECRET +pnpm dlx wrangler secret put SESSION_SECRET +``` + +`SESSION_SECRET` should be a randomly generated value of at least 32 bytes. The OAuth flow requests no repository scopes, reads the authenticated user's public GitHub identity, and discards the access token immediately afterward. + +For a non-production callback origin, also configure `GITHUB_OAUTH_CALLBACK_URL` as a Worker secret matching the callback registered in the OAuth App. diff --git a/docs/app/[lang]/contributors/page.tsx b/docs/app/[lang]/contributors/page.tsx new file mode 100644 index 0000000000..b398aa038e --- /dev/null +++ b/docs/app/[lang]/contributors/page.tsx @@ -0,0 +1,55 @@ +import type { Metadata } from "next"; +import contributorSnapshot from "@/data/contributors.json"; +import { ContributorsExperience } from "@/components/contributors/ContributorsExperience"; +import { LandingFooter } from "@/components/landing/LandingFooter"; +import { LandingNav } from "@/components/landing/LandingNav"; +import type { ContributorActivityData } from "@/lib/contributorActivity"; +import { SITE_URL } from "@/lib/metadata"; + +const pageMetadata = { + en: { + title: "DBX Contributors", + description: "Explore the people building DBX and download a certificate generated from public GitHub activity.", + }, + cn: { + title: "DBX 贡献者", + description: "查看共同建设 DBX 的开源贡献者,并根据公开 GitHub 活动生成贡献证书。", + }, +}; + +export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise { + const { lang } = await params; + const locale = lang === "cn" ? "cn" : "en"; + const metadata = pageMetadata[locale]; + + return { + ...metadata, + alternates: { + canonical: `${SITE_URL}/${locale}/contributors`, + languages: { + en: `${SITE_URL}/en/contributors`, + zh: `${SITE_URL}/cn/contributors`, + "x-default": `${SITE_URL}/en/contributors`, + }, + }, + openGraph: { + title: metadata.title, + description: metadata.description, + url: `${SITE_URL}/${locale}/contributors`, + }, + }; +} + +export default async function ContributorsPage({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params; + const locale = lang === "cn" ? "cn" : "en"; + const data = contributorSnapshot as ContributorActivityData; + + return ( +
+ + + +
+ ); +} diff --git a/docs/app/[lang]/page.tsx b/docs/app/[lang]/page.tsx index 1da8213d68..b1281917c0 100644 --- a/docs/app/[lang]/page.tsx +++ b/docs/app/[lang]/page.tsx @@ -594,7 +594,7 @@ export default async function LandingPage({ params }: { params: Promise<{ lang: {/* Contributors */} - + {/* Updates */} diff --git a/docs/components/contributors/ContributorsExperience.module.css b/docs/components/contributors/ContributorsExperience.module.css new file mode 100644 index 0000000000..a519104019 --- /dev/null +++ b/docs/components/contributors/ContributorsExperience.module.css @@ -0,0 +1,253 @@ +.experience { + --violet: #6ea8ff; + --violet-deep: #348bcf; + --lavender: #52d5ff; + --gold: #d9ad3d; + --ink: #f7fbff; + --muted: #9aa9bc; + min-height: 100vh; + overflow: hidden; + background: + radial-gradient(circle at 82% 20%, rgba(82, 213, 255, 0.05), transparent 28rem), + #10151d; + color: var(--ink); + font-family: "Avenir Next", "PingFang SC", sans-serif; +} + +.hero { + position: relative; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(390px, 0.72fr); + align-items: center; + gap: 58px; + min-height: 590px; + padding: 108px max(28px, calc((100vw - 1180px) / 2)) 66px; + overflow: hidden; + background: + radial-gradient(circle at 14% 8%, rgba(82, 213, 255, 0.14), transparent 24rem), + radial-gradient(circle at 84% 64%, rgba(217, 173, 61, 0.08), transparent 26rem), + linear-gradient(138deg, #10151d 0%, #0c1528 58%, #111827 100%); + color: #fff; +} + +.hero::before, +.hero::after { + content: ""; + position: absolute; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 50%; +} + +.hero::before { width: 560px; height: 560px; right: -230px; top: 30px; } +.hero::after { width: 310px; height: 310px; right: -105px; top: 155px; } + +.sparkles { position: absolute; inset: 0; pointer-events: none; } +.sparkles i { + --x: calc((var(--i) * 47) % 96); + --y: calc((var(--i) * 71) % 88); + position: absolute; + left: calc(var(--x) * 1%); + top: calc(var(--y) * 1%); + width: 4px; + height: 4px; + border-radius: 50%; + background: rgba(139, 232, 247, 0.78); + box-shadow: 0 0 18px rgba(82, 213, 255, 0.72); + animation: twinkle 2.8s ease-in-out infinite; + animation-delay: calc(var(--i) * -110ms); +} + +.heroCopy, +.heroCertificate { position: relative; z-index: 2; } + +.hero h1 { + max-width: 680px; + margin: 28px 0 22px; + font-family: "Avenir Next Condensed", "DIN Alternate", "PingFang SC", sans-serif; + font-size: clamp(50px, 4.8vw, 74px); + font-weight: 800; + line-height: 1.02; + letter-spacing: -0.045em; + text-wrap: balance; +} + +.heroCopy > p { + max-width: 650px; + margin: 0; + color: rgba(255, 255, 255, 0.8); + font-size: 17px; + line-height: 1.72; +} + +.githubButton { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + min-height: 52px; + margin-top: 28px; + padding: 0 22px; + border: 0; + border-radius: 10px; + background: #f7fbff; + color: #111827; + box-shadow: 0 18px 38px rgba(0, 0, 0, 0.24); + font-size: 14px; + font-weight: 750; + cursor: pointer; + transition: transform 160ms ease, box-shadow 160ms ease; +} +.githubButton:hover { transform: translateY(-2px); box-shadow: 0 22px 46px rgba(0, 0, 0, 0.34); } +.authLoading { margin-top: 34px; color: rgba(255, 255, 255, 0.76); font: 600 13px ui-monospace, monospace; } + +.verifiedPanel { + display: grid; + grid-template-columns: 48px minmax(0, 1fr) auto auto; + align-items: center; + gap: 13px; + max-width: 610px; + margin-top: 34px; + padding: 10px; + border: 1px solid rgba(255, 255, 255, 0.26); + border-radius: 14px; + background: rgba(17, 21, 29, 0.7); + backdrop-filter: blur(18px); +} +.verifiedPanel img { width: 48px; height: 48px; border-radius: 10px; object-fit: cover; } +.verifiedPanel small { display: block; color: rgba(255, 255, 255, 0.66); font-size: 10px; } +.verifiedPanel strong { display: block; margin-top: 2px; font-size: 14px; } +.verifiedPanel > button:not(.logoutButton), +.verifiedPanel a { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + min-height: 40px; + padding: 0 14px; + border: 0; + border-radius: 8px; + background: #fff; + color: var(--violet-deep); + font-size: 12px; + font-weight: 760; + text-decoration: none; + cursor: pointer; +} +.logoutButton { display: grid; place-items: center; width: 38px; height: 38px; border: 0; border-radius: 8px; background: rgba(37, 33, 91, 0.22); color: #fff; cursor: pointer; } +.notEligible { margin-top: 12px !important; color: #fff4c7 !important; font-size: 12px !important; line-height: 1.55 !important; } + +.heroCertificate { perspective: 1000px; } +.certificatePreview { + position: relative; + min-height: 360px; + padding: 32px; + overflow: hidden; + border-radius: 24px; + background: rgba(255, 255, 255, 0.96); + color: #24233b; + box-shadow: 0 32px 76px rgba(0, 0, 0, 0.36); + transform: rotate(1.5deg); +} +.certificatePreview::before { content: ""; position: absolute; inset: 15px; border: 2px solid rgba(217, 173, 61, 0.72); border-radius: 16px; pointer-events: none; } +.certificatePreview::after { content: ""; position: absolute; width: 270px; height: 270px; right: -100px; bottom: -120px; border-radius: 50%; background: rgba(98, 95, 224, 0.07); } +.previewTop { display: flex; align-items: center; justify-content: space-between; color: var(--violet); font: 800 13px ui-monospace, monospace; letter-spacing: 0.12em; } +.previewTop > span { display: inline-flex; align-items: center; gap: 10px; } +.previewTop img { border-radius: 7px; } +.certificatePreview > small { display: block; margin-top: 48px; color: var(--gold); font-size: 13px; font-weight: 750; letter-spacing: 0.08em; } +.certificatePreview > strong { display: block; max-width: 76%; margin-top: 12px; overflow: hidden; color: var(--violet-deep); font-size: clamp(38px, 4vw, 58px); line-height: 1; text-overflow: ellipsis; white-space: nowrap; } +.certificatePreview > p { max-width: 75%; margin: 16px 0 0; color: #716f86; font-size: 12px; line-height: 1.55; } +.previewMetrics { display: grid; grid-template-columns: repeat(2, 1fr); gap: 9px; width: 72%; margin-top: 24px; } +.previewMetrics div { padding: 13px; border-radius: 10px; background: #f6f2e8; } +.previewMetrics b { display: block; font: 800 24px ui-monospace, monospace; } +.previewMetrics span { display: block; margin-top: 3px; color: #9c7e31; font: 700 8px ui-monospace, monospace; letter-spacing: 0.06em; } +.previewSeal { position: absolute; right: 40px; top: 120px; display: grid; place-items: center; width: 84px; height: 84px; overflow: hidden; border: 5px solid var(--gold); border-radius: 50%; background: var(--gold); color: #fff; box-shadow: 0 0 0 10px rgba(217, 173, 61, 0.12); font: 800 28px ui-monospace, monospace; } +.previewSeal img { width: 100%; height: 100%; object-fit: cover; } +.metrics { + position: relative; + z-index: 4; + display: grid; + grid-template-columns: repeat(4, 1fr); + max-width: 1180px; + margin: -22px auto 0; + overflow: hidden; + border: 1px solid rgba(155, 176, 205, 0.18); + border-radius: 18px; + background: rgba(17, 21, 29, 0.96); + box-shadow: 0 18px 54px rgba(0, 0, 0, 0.24); +} +.metrics div { padding: 22px 28px; border-right: 1px solid rgba(155, 176, 205, 0.16); } +.metrics div:last-child { border-right: 0; } +.metrics strong { display: block; color: #f7fbff; font-size: 28px; } +.metrics span { display: block; margin-top: 4px; color: var(--muted); font-size: 11px; } + +.directorySection { max-width: 1180px; margin: 0 auto; padding: 92px 28px 120px; } +.directoryHeader h2 { margin: 0; font-size: clamp(30px, 3vw, 42px); line-height: 1.12; letter-spacing: -0.03em; } +.directoryHeader { display: flex; align-items: end; justify-content: space-between; gap: 32px; margin-bottom: 28px; } +.directoryHeader label { display: flex; align-items: center; gap: 9px; width: min(360px, 100%); height: 46px; padding: 0 14px; border: 1px solid rgba(155, 176, 205, 0.2); border-radius: 10px; background: rgba(17, 21, 29, 0.88); color: var(--violet); } +.directoryHeader input { width: 100%; outline: 0; background: transparent; color: var(--ink); font-size: 13px; } +.directoryGrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; } +.directoryGrid a { display: grid; grid-template-columns: 46px minmax(0, 1fr) auto; align-items: center; gap: 12px; min-width: 0; padding: 12px; border: 1px solid rgba(155, 176, 205, 0.16); border-radius: 10px; background: rgba(17, 21, 29, 0.82); color: var(--ink); text-decoration: none; transition: transform 160ms ease, border-color 160ms ease, background 160ms ease; } +.directoryGrid a:hover { transform: translateY(-3px); border-color: rgba(110, 168, 255, 0.38); background: rgba(23, 28, 38, 0.96); } +.directoryGrid img { width: 46px; height: 46px; border-radius: 10px; object-fit: cover; } +.directoryGrid strong { display: block; overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; } +.directoryGrid small { display: block; margin-top: 4px; color: var(--muted); font-size: 10px; } +.directoryGrid svg { color: #718396; } +.empty { padding: 60px; border: 1px dashed rgba(155, 176, 205, 0.2); border-radius: 12px; color: var(--muted); text-align: center; } + +.modalBackdrop { position: fixed; z-index: 100; inset: 0; display: grid; place-items: center; padding: 24px; background: rgba(31, 27, 66, 0.74); backdrop-filter: blur(16px); } +.modal { width: min(980px, 100%); } +.fullCertificate { position: relative; min-height: 590px; padding: 54px; overflow: hidden; border-radius: 24px; background: #fff; color: #24233b; box-shadow: 0 45px 130px rgba(0, 0, 0, 0.42); } +.fullCertificate::before { content: ""; position: absolute; inset: 18px; border: 2px solid rgba(217, 173, 61, 0.74); border-radius: 16px; pointer-events: none; } +.fullCertificate::after { content: ""; position: absolute; right: -110px; bottom: -150px; width: 380px; height: 380px; border-radius: 50%; background: rgba(98, 95, 224, 0.06); } +.verifiedText { display: block; margin-top: 8px; color: var(--gold); font: 750 10px ui-monospace, monospace; letter-spacing: 0.12em; } +.fullCertificate > small { display: block; margin-top: 72px; color: var(--gold); font-size: 14px; font-weight: 760; } +.fullCertificate > em { display: block; margin-top: 25px; color: var(--muted); font-size: 13px; font-style: normal; } +.fullCertificate h2 { max-width: 72%; margin: 7px 0 16px; overflow: hidden; color: var(--violet-deep); font-size: clamp(48px, 7vw, 82px); line-height: 1; letter-spacing: -0.04em; text-overflow: ellipsis; white-space: nowrap; } +.fullCertificate > p { max-width: 690px; color: var(--muted); line-height: 1.65; } +.fullMetrics { display: grid; grid-template-columns: repeat(2, minmax(0, 230px)); gap: 12px; margin-top: 36px; } +.fullMetrics div { padding: 17px; border-radius: 12px; background: #f7f1df; } +.fullMetrics b { display: block; font: 800 30px ui-monospace, monospace; } +.fullMetrics span { display: block; margin-top: 4px; color: #9a7c2e; font: 700 9px ui-monospace, monospace; letter-spacing: 0.08em; } +.fullSeal { position: absolute; right: 72px; top: 180px; display: grid; place-items: center; width: 128px; height: 128px; overflow: hidden; border: 7px solid var(--gold); border-radius: 50%; background: var(--gold); color: #fff; box-shadow: 0 0 0 13px rgba(217, 173, 61, 0.12); font: 800 38px ui-monospace, monospace; } +.fullSeal img { width: 100%; height: 100%; object-fit: cover; } +.fullCertificate footer { position: absolute; right: 54px; bottom: 36px; left: 54px; display: flex; justify-content: space-between; color: #aaa7b8; font: 650 10px ui-monospace, monospace; letter-spacing: 0.08em; } +.modalActions { display: flex; justify-content: flex-end; gap: 9px; margin-top: 12px; } +.modalActions button { display: inline-flex; align-items: center; gap: 8px; min-height: 43px; padding: 0 16px; border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 9px; background: rgba(255, 255, 255, 0.12); color: #fff; font-size: 12px; font-weight: 740; cursor: pointer; } +.modalActions button:first-child { border-color: #fff; background: #fff; color: var(--violet-deep); } + +@keyframes twinkle { 50% { opacity: 0.3; transform: scale(0.65); } } + +@media (max-width: 1050px) { + .hero { grid-template-columns: 1fr; padding-top: 112px; } + .heroCopy { max-width: 760px; margin: 0 auto; text-align: center; } + .heroCopy > p { margin-right: auto; margin-left: auto; } + .heroCertificate { width: min(620px, 100%); margin: 0 auto; } + .verifiedPanel { margin-right: auto; margin-left: auto; text-align: left; } + .directoryGrid { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 760px) { + .hero { gap: 48px; min-height: 0; padding: 100px 18px 72px; } + .hero h1 { font-size: 51px; } + .verifiedPanel { grid-template-columns: 44px minmax(0, 1fr) auto; } + .verifiedPanel > button:not(.logoutButton), .verifiedPanel a { grid-column: 1 / -1; } + .metrics { grid-template-columns: repeat(2, 1fr); margin: -24px 18px 0; } + .metrics div:nth-child(2) { border-right: 0; } + .metrics div:nth-child(-n + 2) { border-bottom: 1px solid rgba(155, 176, 205, 0.16); } + .directorySection { padding-right: 18px; padding-left: 18px; } + .directoryHeader { align-items: stretch; flex-direction: column; } + .directoryGrid { grid-template-columns: 1fr; } + .certificatePreview { min-height: 390px; padding: 30px; } + .certificatePreview > p { max-width: 68%; } + .previewSeal { right: 34px; width: 74px; height: 74px; font-size: 22px; } + .fullCertificate { min-height: 650px; padding: 38px 32px; } + .fullCertificate h2 { max-width: 100%; font-size: 48px; } + .fullSeal { top: 120px; right: 42px; width: 82px; height: 82px; font-size: 26px; } + .fullMetrics { grid-template-columns: 1fr; max-width: 230px; } + .fullCertificate footer { right: 32px; left: 32px; } +} + +@media (prefers-reduced-motion: reduce) { + .sparkles i { animation: none; opacity: 1; transform: none; } + .githubButton, .directoryGrid a { transition: none; } +} diff --git a/docs/components/contributors/ContributorsExperience.tsx b/docs/components/contributors/ContributorsExperience.tsx new file mode 100644 index 0000000000..923aedcba7 --- /dev/null +++ b/docs/components/contributors/ContributorsExperience.tsx @@ -0,0 +1,312 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { Award, Download, ExternalLink, Github, LogOut, Search, ShieldCheck } from "lucide-react"; +import type { ContributorActivity, ContributorActivityData } from "@/lib/contributorActivity"; +import styles from "./ContributorsExperience.module.css"; + +type Language = "en" | "cn"; +type AuthUser = { login: string; avatarUrl: string; profileUrl: string }; +type AuthState = { status: "loading" } | { status: "signed-out" } | { status: "signed-in"; user: AuthUser }; + +const copy = { + en: { + title: "Your work is part of DBX.", + intro: "Sign in with GitHub to verify your identity and collect a shareable certificate for pull requests merged into DBX.", + signIn: "Verify with GitHub", + signingIn: "Checking GitHub session…", + contributors: "verified contributors", + merged: "merged pull requests", + commits: "repository commits", + stars: "GitHub stars", + directory: "Contributor directory", + search: "Search GitHub login", + noResults: "No matching contributor.", + signedInAs: "Verified as", + claim: "Open my certificate", + notEligible: "This GitHub account does not have a merged pull request in DBX yet.", + contributionCallout: "Ship a focused pull request and join the wall.", + viewGuide: "Contribution guide", + signOut: "Sign out", + certificate: "Open-source contributor", + awardedTo: "Presented to", + certificateBody: "For work accepted and merged into DBX, helping make database tools better for everyone.", + download: "Download certificate", + close: "Close", + mergedLabel: "MERGED PULL REQUESTS", + verified: "GITHUB IDENTITY VERIFIED", + }, + cn: { + title: "你的代码,已经成为 DBX 的一部分。", + intro: "使用 GitHub 验证身份,领取一张属于你的贡献者证书,记录被 DBX 接受并合并的 Pull Request。", + signIn: "使用 GitHub 验证身份", + signingIn: "正在检查 GitHub 登录状态…", + contributors: "位贡献者", + merged: "个已合并 PR", + commits: "次仓库 Commit", + stars: "个 GitHub Star", + directory: "完整贡献者名单", + search: "搜索 GitHub 用户名", + noResults: "没有匹配的贡献者。", + signedInAs: "已验证身份", + claim: "打开我的证书", + notEligible: "这个 GitHub 账号暂时还没有被 DBX 合并的 Pull Request。", + contributionCallout: "提交一个专注、可合并的 PR,加入这面贡献者墙。", + viewGuide: "查看贡献指南", + signOut: "退出验证", + certificate: "开源项目贡献者", + awardedTo: "授予", + certificateBody: "感谢你的代码被 DBX 接受并合并,让每个人都能使用更好的数据库工具。", + download: "下载贡献者证书", + close: "关闭", + mergedLabel: "已合并 PULL REQUEST", + verified: "GITHUB 身份已验证", + }, +} as const; + +function formatNumber(value: number, lang: Language) { + return new Intl.NumberFormat(lang === "cn" ? "zh-CN" : "en-US").format(value); +} + +function initials(login: string) { + return login.slice(0, 2).toUpperCase(); +} + +function loadImage(source: string): Promise { + return new Promise((resolve) => { + const image = new Image(); + image.onload = () => resolve(image); + image.onerror = () => resolve(null); + image.src = source; + }); +} + +async function drawCertificate(contributor: ContributorActivity, lang: Language) { + const canvas = document.createElement("canvas"); + canvas.width = 1800; + canvas.height = 1120; + const context = canvas.getContext("2d"); + if (!context) return; + const text = copy[lang]; + const [logo, avatar] = await Promise.all([loadImage("/logo.png"), loadImage(`/api/contributor-avatar?login=${encodeURIComponent(contributor.login)}`)]); + + // The downloaded artifact is the certificate itself, not a screenshot of the page backdrop. + context.fillStyle = "#fffdf8"; + context.fillRect(0, 0, 1800, 1120); + + context.fillStyle = "#fffdf8"; + context.beginPath(); + context.roundRect(92, 82, 1616, 956, 38); + context.fill(); + context.strokeStyle = "#e2b94f"; + context.lineWidth = 4; + context.strokeRect(126, 116, 1548, 888); + context.strokeStyle = "rgba(226,185,79,0.38)"; + context.lineWidth = 1; + context.strokeRect(144, 134, 1512, 852); + + if (logo) context.drawImage(logo, 196, 174, 58, 58); + context.fillStyle = "#5b67d9"; + context.font = "800 32px ui-monospace, monospace"; + context.fillText("DBX · OPEN SOURCE DATABASE TOOL", 276, 216); + context.fillStyle = "#a17c21"; + context.font = "700 22px ui-monospace, monospace"; + context.fillText(text.verified, 196, 260); + + context.fillStyle = "#242640"; + context.font = "800 70px sans-serif"; + context.fillText(text.certificate, 196, 410); + context.fillStyle = "#79788a"; + context.font = "500 28px sans-serif"; + context.fillText(text.awardedTo, 196, 474); + context.fillStyle = "#5b67d9"; + context.font = "800 104px sans-serif"; + context.fillText(`@${contributor.login}`, 196, 596); + + context.fillStyle = "#5e5e72"; + context.font = "400 29px sans-serif"; + context.fillText(text.certificateBody, 196, 672); + + const metrics = [ + [formatNumber(contributor.mergedPullRequests, lang), text.mergedLabel], + [formatNumber(contributor.commits, lang), "COMMITS"], + ]; + metrics.forEach(([value, label], index) => { + const x = 196 + index * 500; + context.fillStyle = "#f7f0d8"; + context.beginPath(); + context.roundRect(x, 746, 470, 142, 22); + context.fill(); + context.fillStyle = "#242640"; + context.font = "800 58px ui-monospace, monospace"; + context.fillText(value, x + 34, 818); + context.fillStyle = "#9a7a25"; + context.font = "700 18px ui-monospace, monospace"; + context.fillText(label, x + 34, 854); + }); + + context.fillStyle = "#e2b94f"; + context.beginPath(); + context.arc(1458, 540, 134, 0, Math.PI * 2); + context.fill(); + context.save(); + context.beginPath(); + context.arc(1458, 540, 118, 0, Math.PI * 2); + context.clip(); + if (avatar) { + context.drawImage(avatar, 1340, 422, 236, 236); + } else { + context.fillStyle = "#fffdf6"; + context.fillRect(1340, 422, 236, 236); + context.fillStyle = "#5b67d9"; + context.font = "800 60px ui-monospace, monospace"; + context.textAlign = "center"; + context.fillText(initials(contributor.login), 1458, 560); + context.textAlign = "left"; + } + context.restore(); + + context.fillStyle = "#8b8998"; + context.font = "500 19px ui-monospace, monospace"; + context.fillText(`t8y2/dbx · ${new Date().toISOString().slice(0, 10)}`, 196, 948); + + const link = document.createElement("a"); + link.download = `dbx-contributor-${contributor.login}.png`; + link.href = canvas.toDataURL("image/png"); + link.click(); +} + +function GithubMark() { + return ; +} + +export function ContributorsExperience({ data, lang }: { data: ContributorActivityData; lang: Language }) { + const text = copy[lang]; + const [auth, setAuth] = useState({ status: "loading" }); + const [query, setQuery] = useState(""); + const [certificateOpen, setCertificateOpen] = useState(false); + + const mergedPullRequests = useMemo(() => data.contributors.reduce((total, contributor) => total + contributor.mergedPullRequests, 0), [data.contributors]); + const commits = useMemo(() => data.contributors.reduce((total, contributor) => total + contributor.commits, 0), [data.contributors]); + const verifiedContributor = useMemo(() => { + if (auth.status !== "signed-in") return null; + return data.contributors.find((contributor) => contributor.login.toLowerCase() === auth.user.login.toLowerCase()) ?? null; + }, [auth, data.contributors]); + const filtered = useMemo(() => { + const normalized = query.trim().toLowerCase(); + if (!normalized) return data.contributors; + return data.contributors.filter((contributor) => contributor.login.toLowerCase().includes(normalized)); + }, [data.contributors, query]); + + useEffect(() => { + fetch("/api/auth/me", { credentials: "include" }) + .then(async (response) => (response.ok ? response.json() : null)) + .then((result: { authenticated?: boolean; user?: AuthUser } | null) => { + if (result?.authenticated && result.user) setAuth({ status: "signed-in", user: result.user }); + else setAuth({ status: "signed-out" }); + }) + .catch(() => setAuth({ status: "signed-out" })); + }, []); + + function signIn() { + const returnTo = `${window.location.pathname}${window.location.search}`; + window.location.assign(`/api/auth/github/start?returnTo=${encodeURIComponent(returnTo)}`); + } + + async function signOut() { + await fetch("/api/auth/logout", { method: "POST", credentials: "include" }).catch(() => undefined); + setAuth({ status: "signed-out" }); + setCertificateOpen(false); + } + + return ( +
+
+ +
+

{text.title}

+

{text.intro}

+ + {auth.status === "loading" ? ( +
{text.signingIn}
+ ) : auth.status === "signed-out" ? ( + + ) : ( +
+ +
{text.signedInAs}@{auth.user.login}
+ {verifiedContributor ? ( + + ) : ( + {text.viewGuide} + )} + +
+ )} + {auth.status === "signed-in" && !verifiedContributor ?

{text.notEligible}
{text.contributionCallout}

: null} +
+ +
+
+
DBXDBX
+ {text.certificate} + @{verifiedContributor?.login ?? "YOUR_NAME"} +

{text.certificateBody}

+
+
{verifiedContributor?.mergedPullRequests ?? "—"}{text.mergedLabel}
+
{verifiedContributor?.commits ?? "—"}COMMITS
+
+
{verifiedContributor ? {verifiedContributor.login} : }
+
+
+
+ +
+
{formatNumber(data.contributors.length, lang)}{text.contributors}
+
{formatNumber(mergedPullRequests, lang)}{text.merged}
+
{formatNumber(commits, lang)}{text.commits}
+
{formatNumber(data.stars, lang)}{text.stars}
+
+ +
+

{text.directory}

+ {filtered.length ? ( + + ) :
{text.noResults}
} +
+ + {certificateOpen && verifiedContributor ? ( +
{ if (event.target === event.currentTarget) setCertificateOpen(false); }}> +
+
+
DBXDBX · OPEN SOURCE DATABASE TOOL
+ {text.verified} + {text.certificate} + {text.awardedTo} +

@{verifiedContributor.login}

+

{text.certificateBody}

+
+
{verifiedContributor.mergedPullRequests}{text.mergedLabel}
+
{verifiedContributor.commits}COMMITS
+
+
{verifiedContributor.login}
+
t8y2/dbxdbxio.com
+
+
+ + +
+
+
+ ) : null} +
+ ); +} diff --git a/docs/components/landing/ContributorsWall.tsx b/docs/components/landing/ContributorsWall.tsx index c196a9d2d3..c8a3ff0a62 100644 --- a/docs/components/landing/ContributorsWall.tsx +++ b/docs/components/landing/ContributorsWall.tsx @@ -33,7 +33,7 @@ function ContributorAvatar({ c }: { c: Contributor }) { ); } -export function ContributorsWallContent({ contributors, title, desc }: { contributors: Contributor[]; title: string; desc: string }) { +export function ContributorsWallContent({ contributors, title, desc, lang }: { contributors: Contributor[]; title: string; desc: string; lang: "en" | "cn" }) { const uniqueContributors = dedupeContributors(contributors); if (uniqueContributors.length === 0) return null; @@ -43,8 +43,12 @@ export function ContributorsWallContent({ contributors, title, desc }: { contrib

{title}

{desc}{" "} - - {uniqueContributors.length}+ contributors on GitHub + + {lang === "cn" ? `查看 ${uniqueContributors.length}+ 位贡献者` : `Explore ${uniqueContributors.length}+ contributors`} + + + + {lang === "cn" ? "下载贡献者证书" : "Download contributor certificate"}

diff --git a/docs/components/landing/LandingNav.tsx b/docs/components/landing/LandingNav.tsx index 74c27557d6..4788b4bf39 100644 --- a/docs/components/landing/LandingNav.tsx +++ b/docs/components/landing/LandingNav.tsx @@ -10,13 +10,14 @@ const i18n = { databases: "Databases", changelog: "Changelog", community: "Community", + contributors: "Contributors", drivers: "Offline Drivers", lang: "中文", }, - cn: { home: "首页", docs: "文档", databases: "数据库", changelog: "更新日志", community: "交流群", drivers: "离线驱动", lang: "English" }, + cn: { home: "首页", docs: "文档", databases: "数据库", changelog: "更新日志", community: "交流群", contributors: "贡献者", drivers: "离线驱动", lang: "English" }, }; -export function LandingNav({ lang, active }: { lang: "en" | "cn"; active?: "home" | "databases" | "changelog" | "community" | "drivers" }) { +export function LandingNav({ lang, active }: { lang: "en" | "cn"; active?: "home" | "databases" | "changelog" | "community" | "contributors" | "drivers" }) { const ref = useRef(null); const t = i18n[lang]; const otherLang = lang === "cn" ? "en" : "cn"; @@ -24,6 +25,7 @@ export function LandingNav({ lang, active }: { lang: "en" | "cn"; active?: "home databases: `/${otherLang}/databases`, changelog: `/${otherLang}/changelog`, community: `/${otherLang}/community`, + contributors: `/${otherLang}/contributors`, drivers: `/${otherLang}/drivers`, }; const langHref = langHrefMap[active ?? ""] ?? `/${otherLang}`; @@ -64,6 +66,9 @@ export function LandingNav({ lang, active }: { lang: "en" | "cn"; active?: "home {t.community} + + {t.contributors} + {t.drivers} diff --git a/docs/data/contributors.json b/docs/data/contributors.json new file mode 100644 index 0000000000..4ec1666b6e --- /dev/null +++ b/docs/data/contributors.json @@ -0,0 +1,1105 @@ +{ + "repository": "t8y2/dbx", + "generatedAt": "2026-07-19T05:23:09.753Z", + "stars": 10252, + "contributors": [ + { + "login": "t8y2", + "avatarUrl": "https://avatars.githubusercontent.com/u/77960507?v=4", + "profileUrl": "https://github.com/t8y2", + "commits": 2442, + "mergedPullRequests": 13, + "firstContributionAt": "2026-04-30T08:14:51Z", + "latestContributionAt": "2026-05-30T11:15:38Z" + }, + { + "login": "zipg", + "avatarUrl": "https://avatars.githubusercontent.com/u/4047349?v=4", + "profileUrl": "https://github.com/zipg", + "commits": 283, + "mergedPullRequests": 283, + "firstContributionAt": "2026-06-22T06:10:59Z", + "latestContributionAt": "2026-07-19T00:53:20Z" + }, + { + "login": "eryajf", + "avatarUrl": "https://avatars.githubusercontent.com/u/33259379?v=4", + "profileUrl": "https://github.com/eryajf", + "commits": 81, + "mergedPullRequests": 80, + "firstContributionAt": "2026-06-08T11:45:45Z", + "latestContributionAt": "2026-07-18T14:11:35Z" + }, + { + "login": "Abeautifulsnow", + "avatarUrl": "https://avatars.githubusercontent.com/u/28704977?v=4", + "profileUrl": "https://github.com/Abeautifulsnow", + "commits": 58, + "mergedPullRequests": 46, + "firstContributionAt": "2026-05-06T03:16:57Z", + "latestContributionAt": "2026-07-17T13:15:00Z" + }, + { + "login": "serenez", + "avatarUrl": "https://avatars.githubusercontent.com/u/38424953?v=4", + "profileUrl": "https://github.com/serenez", + "commits": 55, + "mergedPullRequests": 52, + "firstContributionAt": "2026-04-30T04:27:54Z", + "latestContributionAt": "2026-07-19T01:58:04Z" + }, + { + "login": "CN-Scars", + "avatarUrl": "https://avatars.githubusercontent.com/u/57054419?v=4", + "profileUrl": "https://github.com/CN-Scars", + "commits": 42, + "mergedPullRequests": 42, + "firstContributionAt": "2026-05-30T10:02:05Z", + "latestContributionAt": "2026-07-19T00:52:55Z" + }, + { + "login": "Illuminated2020", + "avatarUrl": "https://avatars.githubusercontent.com/u/95458741?v=4", + "profileUrl": "https://github.com/Illuminated2020", + "commits": 40, + "mergedPullRequests": 1, + "firstContributionAt": "2026-05-12T09:13:51Z", + "latestContributionAt": "2026-05-12T09:22:36Z" + }, + { + "login": "onceMisery", + "avatarUrl": "https://avatars.githubusercontent.com/u/37446017?v=4", + "profileUrl": "https://github.com/onceMisery", + "commits": 30, + "mergedPullRequests": 28, + "firstContributionAt": "2026-06-10T14:19:43Z", + "latestContributionAt": "2026-07-14T15:16:10Z" + }, + { + "login": "lexmin0412", + "avatarUrl": "https://avatars.githubusercontent.com/u/24385370?v=4", + "profileUrl": "https://github.com/lexmin0412", + "commits": 28, + "mergedPullRequests": 28, + "firstContributionAt": "2026-06-09T07:13:22Z", + "latestContributionAt": "2026-07-17T09:12:32Z" + }, + { + "login": "onenewcode", + "avatarUrl": "https://avatars.githubusercontent.com/u/94808747?v=4", + "profileUrl": "https://github.com/onenewcode", + "commits": 27, + "mergedPullRequests": 27, + "firstContributionAt": "2026-06-30T01:53:31Z", + "latestContributionAt": "2026-07-17T08:57:38Z" + }, + { + "login": "SuLea-IT", + "avatarUrl": "https://avatars.githubusercontent.com/u/105108570?v=4", + "profileUrl": "https://github.com/SuLea-IT", + "commits": 25, + "mergedPullRequests": 30, + "firstContributionAt": "2026-04-30T05:56:34Z", + "latestContributionAt": "2026-07-10T10:15:25Z" + }, + { + "login": "rarnu", + "avatarUrl": "https://avatars.githubusercontent.com/u/209014?v=4", + "profileUrl": "https://github.com/rarnu", + "commits": 23, + "mergedPullRequests": 13, + "firstContributionAt": "2026-05-02T07:44:05Z", + "latestContributionAt": "2026-05-29T05:31:32Z" + }, + { + "login": "LwClick", + "avatarUrl": "https://avatars.githubusercontent.com/u/38545827?v=4", + "profileUrl": "https://github.com/LwClick", + "commits": 22, + "mergedPullRequests": 22, + "firstContributionAt": "2026-06-26T03:29:22Z", + "latestContributionAt": "2026-07-16T17:17:49Z" + }, + { + "login": "gggaiitx", + "avatarUrl": "https://avatars.githubusercontent.com/u/62124152?v=4", + "profileUrl": "https://github.com/gggaiitx", + "commits": 21, + "mergedPullRequests": 21, + "firstContributionAt": "2026-07-06T09:30:53Z", + "latestContributionAt": "2026-07-16T16:11:47Z" + }, + { + "login": "ptma", + "avatarUrl": "https://avatars.githubusercontent.com/u/1761883?v=4", + "profileUrl": "https://github.com/ptma", + "commits": 18, + "mergedPullRequests": 18, + "firstContributionAt": "2026-06-09T08:33:11Z", + "latestContributionAt": "2026-07-16T10:42:34Z" + }, + { + "login": "yavon007", + "avatarUrl": "https://avatars.githubusercontent.com/u/27227092?v=4", + "profileUrl": "https://github.com/yavon007", + "commits": 18, + "mergedPullRequests": 7, + "firstContributionAt": "2026-05-02T20:43:01Z", + "latestContributionAt": "2026-05-20T08:10:15Z" + }, + { + "login": "linjianlin", + "avatarUrl": "https://avatars.githubusercontent.com/u/46356519?v=4", + "profileUrl": "https://github.com/linjianlin", + "commits": 16, + "mergedPullRequests": 16, + "firstContributionAt": "2026-06-17T08:11:49Z", + "latestContributionAt": "2026-06-26T10:48:46Z" + }, + { + "login": "jischeng", + "avatarUrl": "https://avatars.githubusercontent.com/u/49861575?v=4", + "profileUrl": "https://github.com/jischeng", + "commits": 13, + "mergedPullRequests": 13, + "firstContributionAt": "2026-06-24T12:52:59Z", + "latestContributionAt": "2026-07-16T10:21:25Z" + }, + { + "login": "juvevood", + "avatarUrl": "https://avatars.githubusercontent.com/u/1421513?v=4", + "profileUrl": "https://github.com/juvevood", + "commits": 13, + "mergedPullRequests": 13, + "firstContributionAt": "2026-06-29T10:02:55Z", + "latestContributionAt": "2026-07-17T03:26:38Z" + }, + { + "login": "SpencerZhang", + "avatarUrl": "https://avatars.githubusercontent.com/u/1736590?v=4", + "profileUrl": "https://github.com/SpencerZhang", + "commits": 12, + "mergedPullRequests": 12, + "firstContributionAt": "2026-06-17T03:22:35Z", + "latestContributionAt": "2026-07-17T08:05:50Z" + }, + { + "login": "haipengno1", + "avatarUrl": "https://avatars.githubusercontent.com/u/3533065?v=4", + "profileUrl": "https://github.com/haipengno1", + "commits": 11, + "mergedPullRequests": 11, + "firstContributionAt": "2026-06-10T15:55:06Z", + "latestContributionAt": "2026-07-06T18:05:34Z" + }, + { + "login": "Bacon2994", + "avatarUrl": "https://avatars.githubusercontent.com/u/108093786?v=4", + "profileUrl": "https://github.com/Bacon2994", + "commits": 11, + "mergedPullRequests": 7, + "firstContributionAt": "2026-04-30T09:05:18Z", + "latestContributionAt": "2026-05-09T06:42:25Z" + }, + { + "login": "dal1wg", + "avatarUrl": "https://avatars.githubusercontent.com/u/91363952?v=4", + "profileUrl": "https://github.com/dal1wg", + "commits": 10, + "mergedPullRequests": 10, + "firstContributionAt": "2026-06-16T09:58:42Z", + "latestContributionAt": "2026-07-02T06:03:20Z" + }, + { + "login": "xddcode", + "avatarUrl": "https://avatars.githubusercontent.com/u/48708512?v=4", + "profileUrl": "https://github.com/xddcode", + "commits": 9, + "mergedPullRequests": 9, + "firstContributionAt": "2026-07-10T06:34:09Z", + "latestContributionAt": "2026-07-17T10:14:11Z" + }, + { + "login": "DengQingNian", + "avatarUrl": "https://avatars.githubusercontent.com/u/117324885?v=4", + "profileUrl": "https://github.com/DengQingNian", + "commits": 8, + "mergedPullRequests": 11, + "firstContributionAt": "2026-06-02T16:19:01Z", + "latestContributionAt": "2026-07-10T07:13:57Z" + }, + { + "login": "chenhbb", + "avatarUrl": "https://avatars.githubusercontent.com/u/34821912?v=4", + "profileUrl": "https://github.com/chenhbb", + "commits": 8, + "mergedPullRequests": 8, + "firstContributionAt": "2026-06-20T07:06:56Z", + "latestContributionAt": "2026-06-29T18:45:22Z" + }, + { + "login": "xKrah", + "avatarUrl": "https://avatars.githubusercontent.com/u/23560682?v=4", + "profileUrl": "https://github.com/xKrah", + "commits": 7, + "mergedPullRequests": 3, + "firstContributionAt": "2026-05-11T05:22:15Z", + "latestContributionAt": "2026-05-11T14:10:01Z" + }, + { + "login": "luoianun", + "avatarUrl": "https://avatars.githubusercontent.com/u/20828060?v=4", + "profileUrl": "https://github.com/luoianun", + "commits": 6, + "mergedPullRequests": 6, + "firstContributionAt": "2026-06-17T05:09:05Z", + "latestContributionAt": "2026-07-01T11:05:45Z" + }, + { + "login": "ZionLin2016", + "avatarUrl": "https://avatars.githubusercontent.com/u/24632648?v=4", + "profileUrl": "https://github.com/ZionLin2016", + "commits": 6, + "mergedPullRequests": 6, + "firstContributionAt": "2026-07-03T09:51:57Z", + "latestContributionAt": "2026-07-10T11:27:07Z" + }, + { + "login": "antwa", + "avatarUrl": "https://avatars.githubusercontent.com/u/5325867?v=4", + "profileUrl": "https://github.com/antwa", + "commits": 5, + "mergedPullRequests": 5, + "firstContributionAt": "2026-07-13T09:25:38Z", + "latestContributionAt": "2026-07-16T09:01:10Z" + }, + { + "login": "dbin0123", + "avatarUrl": "https://avatars.githubusercontent.com/u/18297300?v=4", + "profileUrl": "https://github.com/dbin0123", + "commits": 5, + "mergedPullRequests": 5, + "firstContributionAt": "2026-06-11T15:15:39Z", + "latestContributionAt": "2026-06-19T15:06:21Z" + }, + { + "login": "fraluc06", + "avatarUrl": "https://avatars.githubusercontent.com/u/67560467?v=4", + "profileUrl": "https://github.com/fraluc06", + "commits": 5, + "mergedPullRequests": 5, + "firstContributionAt": "2026-06-01T08:26:41Z", + "latestContributionAt": "2026-06-30T17:35:07Z" + }, + { + "login": "tianyifeng-druid", + "avatarUrl": "https://avatars.githubusercontent.com/u/67090734?v=4", + "profileUrl": "https://github.com/tianyifeng-druid", + "commits": 5, + "mergedPullRequests": 5, + "firstContributionAt": "2026-06-21T01:57:56Z", + "latestContributionAt": "2026-07-04T15:06:22Z" + }, + { + "login": "ZhonFortune", + "avatarUrl": "https://avatars.githubusercontent.com/u/160460068?v=4", + "profileUrl": "https://github.com/ZhonFortune", + "commits": 5, + "mergedPullRequests": 5, + "firstContributionAt": "2026-06-30T16:59:33Z", + "latestContributionAt": "2026-07-08T16:35:34Z" + }, + { + "login": "Caisin", + "avatarUrl": "https://avatars.githubusercontent.com/u/25792786?v=4", + "profileUrl": "https://github.com/Caisin", + "commits": 4, + "mergedPullRequests": 4, + "firstContributionAt": "2026-06-06T11:21:25Z", + "latestContributionAt": "2026-06-29T03:16:38Z" + }, + { + "login": "Dreamescaper", + "avatarUrl": "https://avatars.githubusercontent.com/u/17177729?v=4", + "profileUrl": "https://github.com/Dreamescaper", + "commits": 4, + "mergedPullRequests": 4, + "firstContributionAt": "2026-06-09T07:54:46Z", + "latestContributionAt": "2026-06-26T09:45:45Z" + }, + { + "login": "jeeinn", + "avatarUrl": "https://avatars.githubusercontent.com/u/13930054?v=4", + "profileUrl": "https://github.com/jeeinn", + "commits": 4, + "mergedPullRequests": 4, + "firstContributionAt": "2026-07-02T15:29:53Z", + "latestContributionAt": "2026-07-07T10:09:02Z" + }, + { + "login": "wuxiemian", + "avatarUrl": "https://avatars.githubusercontent.com/u/29685283?v=4", + "profileUrl": "https://github.com/wuxiemian", + "commits": 4, + "mergedPullRequests": 4, + "firstContributionAt": "2026-07-05T16:45:23Z", + "latestContributionAt": "2026-07-12T13:30:37Z" + }, + { + "login": "wu-clan", + "avatarUrl": "https://avatars.githubusercontent.com/u/52145145?v=4", + "profileUrl": "https://github.com/wu-clan", + "commits": 4, + "mergedPullRequests": 3, + "firstContributionAt": "2026-05-04T09:11:57Z", + "latestContributionAt": "2026-05-06T04:09:26Z" + }, + { + "login": "CSXFanMeng", + "avatarUrl": "https://avatars.githubusercontent.com/u/105690322?v=4", + "profileUrl": "https://github.com/CSXFanMeng", + "commits": 3, + "mergedPullRequests": 3, + "firstContributionAt": "2026-07-02T10:52:18Z", + "latestContributionAt": "2026-07-19T02:32:34Z" + }, + { + "login": "echocde", + "avatarUrl": "https://avatars.githubusercontent.com/u/70051971?v=4", + "profileUrl": "https://github.com/echocde", + "commits": 3, + "mergedPullRequests": 3, + "firstContributionAt": "2026-06-10T13:16:26Z", + "latestContributionAt": "2026-06-25T04:51:02Z" + }, + { + "login": "jthanh8144", + "avatarUrl": "https://avatars.githubusercontent.com/u/80233350?v=4", + "profileUrl": "https://github.com/jthanh8144", + "commits": 3, + "mergedPullRequests": 3, + "firstContributionAt": "2026-07-15T03:09:27Z", + "latestContributionAt": "2026-07-16T10:21:11Z" + }, + { + "login": "kkk000111999", + "avatarUrl": "https://avatars.githubusercontent.com/u/296277678?v=4", + "profileUrl": "https://github.com/kkk000111999", + "commits": 3, + "mergedPullRequests": 3, + "firstContributionAt": "2026-06-27T03:48:16Z", + "latestContributionAt": "2026-06-28T06:52:25Z" + }, + { + "login": "vergil-lai", + "avatarUrl": "https://avatars.githubusercontent.com/u/2268937?v=4", + "profileUrl": "https://github.com/vergil-lai", + "commits": 3, + "mergedPullRequests": 3, + "firstContributionAt": "2026-06-13T11:23:45Z", + "latestContributionAt": "2026-06-24T06:53:47Z" + }, + { + "login": "wds824", + "avatarUrl": "https://avatars.githubusercontent.com/u/93659702?v=4", + "profileUrl": "https://github.com/wds824", + "commits": 3, + "mergedPullRequests": 3, + "firstContributionAt": "2026-04-30T06:28:17Z", + "latestContributionAt": "2026-04-30T09:48:13Z" + }, + { + "login": "zhuhaoh", + "avatarUrl": "https://avatars.githubusercontent.com/u/88836213?v=4", + "profileUrl": "https://github.com/zhuhaoh", + "commits": 3, + "mergedPullRequests": 3, + "firstContributionAt": "2026-07-06T10:48:19Z", + "latestContributionAt": "2026-07-08T16:45:29Z" + }, + { + "login": "zorohu", + "avatarUrl": "https://avatars.githubusercontent.com/u/24402746?v=4", + "profileUrl": "https://github.com/zorohu", + "commits": 3, + "mergedPullRequests": 3, + "firstContributionAt": "2026-07-08T02:54:27Z", + "latestContributionAt": "2026-07-12T14:59:27Z" + }, + { + "login": "Michaelxwb", + "avatarUrl": "https://avatars.githubusercontent.com/u/34806714?v=4", + "profileUrl": "https://github.com/Michaelxwb", + "commits": 3, + "mergedPullRequests": 2, + "firstContributionAt": "2026-05-28T11:02:25Z", + "latestContributionAt": "2026-05-29T04:31:05Z" + }, + { + "login": "ahua8522", + "avatarUrl": "https://avatars.githubusercontent.com/u/50774091?v=4", + "profileUrl": "https://github.com/ahua8522", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-07-06T07:06:32Z", + "latestContributionAt": "2026-07-07T02:34:32Z" + }, + { + "login": "asdlem", + "avatarUrl": "https://avatars.githubusercontent.com/u/53173237?v=4", + "profileUrl": "https://github.com/asdlem", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-06-26T03:33:01Z", + "latestContributionAt": "2026-07-01T08:35:48Z" + }, + { + "login": "bigliangzao", + "avatarUrl": "https://avatars.githubusercontent.com/u/33975524?v=4", + "profileUrl": "https://github.com/bigliangzao", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-06-17T09:09:59Z", + "latestContributionAt": "2026-06-17T11:00:05Z" + }, + { + "login": "cherlinbest-pixel", + "avatarUrl": "https://avatars.githubusercontent.com/u/293644924?v=4", + "profileUrl": "https://github.com/cherlinbest-pixel", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-06-20T04:19:56Z", + "latestContributionAt": "2026-06-20T16:04:52Z" + }, + { + "login": "Doracoin", + "avatarUrl": "https://avatars.githubusercontent.com/u/9206272?v=4", + "profileUrl": "https://github.com/Doracoin", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-06-10T08:32:55Z", + "latestContributionAt": "2026-06-11T09:29:40Z" + }, + { + "login": "funnytime75", + "avatarUrl": "https://avatars.githubusercontent.com/u/162818986?v=4", + "profileUrl": "https://github.com/funnytime75", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-06-20T08:35:12Z", + "latestContributionAt": "2026-06-30T01:14:44Z" + }, + { + "login": "IT-SDJ", + "avatarUrl": "https://avatars.githubusercontent.com/u/18157979?v=4", + "profileUrl": "https://github.com/IT-SDJ", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-06-06T17:51:50Z", + "latestContributionAt": "2026-06-17T13:16:31Z" + }, + { + "login": "jing2uo", + "avatarUrl": "https://avatars.githubusercontent.com/u/3081432?v=4", + "profileUrl": "https://github.com/jing2uo", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-07-12T13:29:00Z", + "latestContributionAt": "2026-07-13T09:03:34Z" + }, + { + "login": "JinRudy", + "avatarUrl": "https://avatars.githubusercontent.com/u/28995387?v=4", + "profileUrl": "https://github.com/JinRudy", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-06-08T09:25:42Z", + "latestContributionAt": "2026-06-09T03:59:49Z" + }, + { + "login": "lc6464", + "avatarUrl": "https://avatars.githubusercontent.com/u/64722907?v=4", + "profileUrl": "https://github.com/lc6464", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-06-13T09:34:22Z", + "latestContributionAt": "2026-06-29T13:21:46Z" + }, + { + "login": "libondev", + "avatarUrl": "https://avatars.githubusercontent.com/u/47918504?v=4", + "profileUrl": "https://github.com/libondev", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-06-13T12:34:22Z", + "latestContributionAt": "2026-06-17T18:01:01Z" + }, + { + "login": "mapan0424", + "avatarUrl": "https://avatars.githubusercontent.com/u/45232878?v=4", + "profileUrl": "https://github.com/mapan0424", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-07-17T05:42:50Z", + "latestContributionAt": "2026-07-19T01:58:49Z" + }, + { + "login": "onlyc0302", + "avatarUrl": "https://avatars.githubusercontent.com/u/24369193?v=4", + "profileUrl": "https://github.com/onlyc0302", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-07-09T10:09:16Z", + "latestContributionAt": "2026-07-10T05:27:11Z" + }, + { + "login": "polemp", + "avatarUrl": "https://avatars.githubusercontent.com/u/48376445?v=4", + "profileUrl": "https://github.com/polemp", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-06-05T10:39:05Z", + "latestContributionAt": "2026-06-12T09:14:06Z" + }, + { + "login": "rainhan99", + "avatarUrl": "https://avatars.githubusercontent.com/u/16128160?v=4", + "profileUrl": "https://github.com/rainhan99", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-07-12T05:47:37Z", + "latestContributionAt": "2026-07-13T09:51:52Z" + }, + { + "login": "Sanjeever", + "avatarUrl": "https://avatars.githubusercontent.com/u/78210374?v=4", + "profileUrl": "https://github.com/Sanjeever", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-06-01T15:04:40Z", + "latestContributionAt": "2026-06-09T04:06:39Z" + }, + { + "login": "uicky", + "avatarUrl": "https://avatars.githubusercontent.com/u/48382673?v=4", + "profileUrl": "https://github.com/uicky", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-06-19T02:24:16Z", + "latestContributionAt": "2026-06-20T14:28:02Z" + }, + { + "login": "vvrfxyz", + "avatarUrl": "https://avatars.githubusercontent.com/u/108868578?v=4", + "profileUrl": "https://github.com/vvrfxyz", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-05-26T14:29:54Z", + "latestContributionAt": "2026-05-27T05:31:22Z" + }, + { + "login": "weihanhan", + "avatarUrl": "https://avatars.githubusercontent.com/u/25023984?v=4", + "profileUrl": "https://github.com/weihanhan", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-06-26T10:53:25Z", + "latestContributionAt": "2026-06-30T12:06:23Z" + }, + { + "login": "xiaoge19961220", + "avatarUrl": "https://avatars.githubusercontent.com/u/76191330?v=4", + "profileUrl": "https://github.com/xiaoge19961220", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-07-03T06:18:00Z", + "latestContributionAt": "2026-07-03T11:25:02Z" + }, + { + "login": "Mukesh-234234", + "avatarUrl": "https://avatars.githubusercontent.com/u/281438489?v=4", + "profileUrl": "https://github.com/Mukesh-234234", + "commits": 1, + "mergedPullRequests": 3, + "firstContributionAt": "2026-05-03T18:36:11Z", + "latestContributionAt": "2026-05-05T09:30:42Z" + }, + { + "login": "adntian", + "avatarUrl": "https://avatars.githubusercontent.com/u/2285733?v=4", + "profileUrl": "https://github.com/adntian", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-05-28T08:46:06Z", + "latestContributionAt": "2026-05-28T09:59:00Z" + }, + { + "login": "aiLi0617", + "avatarUrl": "https://avatars.githubusercontent.com/u/76681595?v=4", + "profileUrl": "https://github.com/aiLi0617", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-19T14:50:55Z", + "latestContributionAt": "2026-06-19T15:58:34Z" + }, + { + "login": "andremacola", + "avatarUrl": "https://avatars.githubusercontent.com/u/3408809?v=4", + "profileUrl": "https://github.com/andremacola", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-05T19:27:22Z", + "latestContributionAt": "2026-06-06T01:15:09Z" + }, + { + "login": "athoune", + "avatarUrl": "https://avatars.githubusercontent.com/u/74048?v=4", + "profileUrl": "https://github.com/athoune", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-14T12:51:26Z", + "latestContributionAt": "2026-06-14T14:53:10Z" + }, + { + "login": "B022MC", + "avatarUrl": "https://avatars.githubusercontent.com/u/61958617?v=4", + "profileUrl": "https://github.com/B022MC", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-30T04:00:21Z", + "latestContributionAt": "2026-06-30T09:34:14Z" + }, + { + "login": "BabyLy233", + "avatarUrl": "https://avatars.githubusercontent.com/u/79321988?v=4", + "profileUrl": "https://github.com/BabyLy233", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-14T07:55:34Z", + "latestContributionAt": "2026-07-14T11:03:42Z" + }, + { + "login": "baizhi958216", + "avatarUrl": "https://avatars.githubusercontent.com/u/46777503?v=4", + "profileUrl": "https://github.com/baizhi958216", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-05-21T01:48:37Z", + "latestContributionAt": "2026-05-21T02:24:47Z" + }, + { + "login": "BlueSkyXN", + "avatarUrl": "https://avatars.githubusercontent.com/u/63384277?v=4", + "profileUrl": "https://github.com/BlueSkyXN", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-05-10T12:02:47Z", + "latestContributionAt": "2026-05-10T12:53:40Z" + }, + { + "login": "cl1107", + "avatarUrl": "https://avatars.githubusercontent.com/u/21985353?v=4", + "profileUrl": "https://github.com/cl1107", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-09T08:33:26Z", + "latestContributionAt": "2026-06-09T08:46:00Z" + }, + { + "login": "clj1566317", + "avatarUrl": "https://avatars.githubusercontent.com/u/295271018?v=4", + "profileUrl": "https://github.com/clj1566317", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-02T05:59:24Z", + "latestContributionAt": "2026-07-02T06:21:53Z" + }, + { + "login": "ColdSand803", + "avatarUrl": "https://avatars.githubusercontent.com/u/74580802?v=4", + "profileUrl": "https://github.com/ColdSand803", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-13T09:15:44Z", + "latestContributionAt": "2026-07-17T10:51:34Z" + }, + { + "login": "cwatanab", + "avatarUrl": "https://avatars.githubusercontent.com/u/10162172?v=4", + "profileUrl": "https://github.com/cwatanab", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-15T09:19:19Z", + "latestContributionAt": "2026-06-15T09:27:06Z" + }, + { + "login": "d0zingcat", + "avatarUrl": "https://avatars.githubusercontent.com/u/8235790?v=4", + "profileUrl": "https://github.com/d0zingcat", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-02T07:45:55Z", + "latestContributionAt": "2026-07-02T08:37:21Z" + }, + { + "login": "Driftlux", + "avatarUrl": "https://avatars.githubusercontent.com/u/96872606?v=4", + "profileUrl": "https://github.com/Driftlux", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-29T09:41:33Z", + "latestContributionAt": "2026-06-30T13:56:47Z" + }, + { + "login": "ducheng1", + "avatarUrl": "https://avatars.githubusercontent.com/u/51262427?v=4", + "profileUrl": "https://github.com/ducheng1", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-08T09:14:47Z", + "latestContributionAt": "2026-07-08T10:53:49Z" + }, + { + "login": "eninem123", + "avatarUrl": "https://avatars.githubusercontent.com/u/35692409?v=4", + "profileUrl": "https://github.com/eninem123", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-09T07:48:04Z", + "latestContributionAt": "2026-07-09T08:39:06Z" + }, + { + "login": "faithererer", + "avatarUrl": "https://avatars.githubusercontent.com/u/104252444?v=4", + "profileUrl": "https://github.com/faithererer", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-05T11:00:04Z", + "latestContributionAt": "2026-07-05T11:38:51Z" + }, + { + "login": "gdzhu8023", + "avatarUrl": "https://avatars.githubusercontent.com/u/21006156?v=4", + "profileUrl": "https://github.com/gdzhu8023", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-03T16:19:58Z", + "latestContributionAt": "2026-07-03T16:58:52Z" + }, + { + "login": "HandSonic", + "avatarUrl": "https://avatars.githubusercontent.com/u/8078023?v=4", + "profileUrl": "https://github.com/HandSonic", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-13T23:36:43Z", + "latestContributionAt": "2026-07-14T07:31:04Z" + }, + { + "login": "Hank076", + "avatarUrl": "https://avatars.githubusercontent.com/u/3490170?v=4", + "profileUrl": "https://github.com/Hank076", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-01T08:54:31Z", + "latestContributionAt": "2026-06-01T10:04:58Z" + }, + { + "login": "hieptuanle", + "avatarUrl": "https://avatars.githubusercontent.com/u/3164648?v=4", + "profileUrl": "https://github.com/hieptuanle", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-06T09:27:49Z", + "latestContributionAt": "2026-07-06T10:21:15Z" + }, + { + "login": "huangyemin", + "avatarUrl": "https://avatars.githubusercontent.com/u/11872262?v=4", + "profileUrl": "https://github.com/huangyemin", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-26T23:23:38Z", + "latestContributionAt": "2026-06-27T02:02:15Z" + }, + { + "login": "iiiusky", + "avatarUrl": "https://avatars.githubusercontent.com/u/45157320?v=4", + "profileUrl": "https://github.com/iiiusky", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-16T10:33:05Z", + "latestContributionAt": "2026-06-16T10:52:02Z" + }, + { + "login": "James-Leong", + "avatarUrl": "https://avatars.githubusercontent.com/u/109658865?v=4", + "profileUrl": "https://github.com/James-Leong", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-08T17:32:08Z", + "latestContributionAt": "2026-06-09T02:27:06Z" + }, + { + "login": "JeaceLiu", + "avatarUrl": "https://avatars.githubusercontent.com/u/31269749?v=4", + "profileUrl": "https://github.com/JeaceLiu", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-17T07:01:40Z", + "latestContributionAt": "2026-07-17T08:32:50Z" + }, + { + "login": "kingcanfish", + "avatarUrl": "https://avatars.githubusercontent.com/u/43931057?v=4", + "profileUrl": "https://github.com/kingcanfish", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-05-02T02:18:47Z", + "latestContributionAt": "2026-05-02T02:36:28Z" + }, + { + "login": "lazyanubis", + "avatarUrl": "https://avatars.githubusercontent.com/u/46840787?v=4", + "profileUrl": "https://github.com/lazyanubis", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-12T06:57:24Z", + "latestContributionAt": "2026-07-12T14:03:57Z" + }, + { + "login": "leic4u", + "avatarUrl": "https://avatars.githubusercontent.com/u/32786903?v=4", + "profileUrl": "https://github.com/leic4u", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-17T14:08:31Z", + "latestContributionAt": "2026-06-17T14:12:59Z" + }, + { + "login": "LSvKing", + "avatarUrl": "https://avatars.githubusercontent.com/u/255360?v=4", + "profileUrl": "https://github.com/LSvKing", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-05-25T03:02:18Z", + "latestContributionAt": "2026-05-25T03:10:17Z" + }, + { + "login": "ManjusriBuddha", + "avatarUrl": "https://avatars.githubusercontent.com/u/55905747?v=4", + "profileUrl": "https://github.com/ManjusriBuddha", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-16T10:05:26Z", + "latestContributionAt": "2026-07-17T03:28:14Z" + }, + { + "login": "Max29xD", + "avatarUrl": "https://avatars.githubusercontent.com/u/85517789?v=4", + "profileUrl": "https://github.com/Max29xD", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-05-11T03:50:52Z", + "latestContributionAt": "2026-05-11T04:09:14Z" + }, + { + "login": "Odonno", + "avatarUrl": "https://avatars.githubusercontent.com/u/6053067?v=4", + "profileUrl": "https://github.com/Odonno", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-15T07:10:22Z", + "latestContributionAt": "2026-06-15T09:01:01Z" + }, + { + "login": "orwenxiang", + "avatarUrl": "https://avatars.githubusercontent.com/u/1335999?v=4", + "profileUrl": "https://github.com/orwenxiang", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-27T09:12:08Z", + "latestContributionAt": "2026-06-27T12:03:17Z" + }, + { + "login": "private20", + "avatarUrl": "https://avatars.githubusercontent.com/u/89687927?v=4", + "profileUrl": "https://github.com/private20", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-27T13:17:40Z", + "latestContributionAt": "2026-06-27T13:44:36Z" + }, + { + "login": "Quinlevi", + "avatarUrl": "https://avatars.githubusercontent.com/u/24589232?v=4", + "profileUrl": "https://github.com/Quinlevi", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-07T03:43:59Z", + "latestContributionAt": "2026-06-07T18:07:00Z" + }, + { + "login": "shenmo7192", + "avatarUrl": "https://avatars.githubusercontent.com/u/47873776?v=4", + "profileUrl": "https://github.com/shenmo7192", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-13T10:58:52Z", + "latestContributionAt": "2026-07-13T11:45:02Z" + }, + { + "login": "shnno13", + "avatarUrl": "https://avatars.githubusercontent.com/u/14847218?v=4", + "profileUrl": "https://github.com/shnno13", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-15T07:04:26Z", + "latestContributionAt": "2026-07-15T10:35:34Z" + }, + { + "login": "shoaky009", + "avatarUrl": "https://avatars.githubusercontent.com/u/33277733?v=4", + "profileUrl": "https://github.com/shoaky009", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-01T09:51:21Z", + "latestContributionAt": "2026-06-01T10:12:20Z" + }, + { + "login": "srako", + "avatarUrl": "https://avatars.githubusercontent.com/u/18043224?v=4", + "profileUrl": "https://github.com/srako", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-09T07:01:29Z", + "latestContributionAt": "2026-06-09T08:23:14Z" + }, + { + "login": "sunny0826", + "avatarUrl": "https://avatars.githubusercontent.com/u/24563928?v=4", + "profileUrl": "https://github.com/sunny0826", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-05T08:58:05Z", + "latestContributionAt": "2026-06-05T10:16:18Z" + }, + { + "login": "sweetwisdom", + "avatarUrl": "https://avatars.githubusercontent.com/u/57869305?v=4", + "profileUrl": "https://github.com/sweetwisdom", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-12T03:18:32Z", + "latestContributionAt": "2026-06-12T03:30:03Z" + }, + { + "login": "Tssshhhh", + "avatarUrl": "https://avatars.githubusercontent.com/u/109692520?v=4", + "profileUrl": "https://github.com/Tssshhhh", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-08T15:20:59Z", + "latestContributionAt": "2026-07-08T16:32:53Z" + }, + { + "login": "Wakanlolz", + "avatarUrl": "https://avatars.githubusercontent.com/u/42348395?v=4", + "profileUrl": "https://github.com/Wakanlolz", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-19T07:11:53Z", + "latestContributionAt": "2026-06-19T16:13:09Z" + }, + { + "login": "wnyr", + "avatarUrl": "https://avatars.githubusercontent.com/u/8221975?v=4", + "profileUrl": "https://github.com/wnyr", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-06T03:39:01Z", + "latestContributionAt": "2026-07-06T04:57:27Z" + }, + { + "login": "xcstatus", + "avatarUrl": "https://avatars.githubusercontent.com/u/40375067?v=4", + "profileUrl": "https://github.com/xcstatus", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-17T11:08:23Z", + "latestContributionAt": "2026-06-17T14:06:33Z" + }, + { + "login": "xiaoliangL", + "avatarUrl": "https://avatars.githubusercontent.com/u/29444559?v=4", + "profileUrl": "https://github.com/xiaoliangL", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-02T05:54:07Z", + "latestContributionAt": "2026-07-02T06:28:58Z" + }, + { + "login": "YangYuS8", + "avatarUrl": "https://avatars.githubusercontent.com/u/107938462?v=4", + "profileUrl": "https://github.com/YangYuS8", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-25T03:02:22Z", + "latestContributionAt": "2026-06-25T04:38:11Z" + }, + { + "login": "yehaonanxx", + "avatarUrl": "https://avatars.githubusercontent.com/u/81568420?v=4", + "profileUrl": "https://github.com/yehaonanxx", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-26T09:47:19Z", + "latestContributionAt": "2026-06-26T09:58:08Z" + }, + { + "login": "Yikuanzz", + "avatarUrl": "https://avatars.githubusercontent.com/u/90631539?v=4", + "profileUrl": "https://github.com/Yikuanzz", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-04-30T07:30:36Z", + "latestContributionAt": "2026-04-30T07:40:40Z" + }, + { + "login": "yuuuxt", + "avatarUrl": "https://avatars.githubusercontent.com/u/3153534?v=4", + "profileUrl": "https://github.com/yuuuxt", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-15T12:13:22Z", + "latestContributionAt": "2026-07-16T10:19:44Z" + }, + { + "login": "zergmk2", + "avatarUrl": "https://avatars.githubusercontent.com/u/698746?v=4", + "profileUrl": "https://github.com/zergmk2", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-05T00:04:15Z", + "latestContributionAt": "2026-07-05T12:29:50Z" + }, + { + "login": "zouchao", + "avatarUrl": "https://avatars.githubusercontent.com/u/1620223?v=4", + "profileUrl": "https://github.com/zouchao", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-05T15:39:07Z", + "latestContributionAt": "2026-06-05T16:15:53Z" + }, + { + "login": "zucchiniEvader", + "avatarUrl": "https://avatars.githubusercontent.com/u/26758630?v=4", + "profileUrl": "https://github.com/zucchiniEvader", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-06-23T05:56:48Z", + "latestContributionAt": "2026-06-23T17:44:46Z" + } + ] +} diff --git a/docs/lib/contributorActivity.test.ts b/docs/lib/contributorActivity.test.ts new file mode 100644 index 0000000000..c1500d928f --- /dev/null +++ b/docs/lib/contributorActivity.test.ts @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import { test } from "vitest"; +import { sortContributorActivity, type ContributorActivityData } from "./contributorActivity"; + +const data: ContributorActivityData = { + repository: "t8y2/dbx", + generatedAt: "2026-07-19T00:00:00.000Z", + stars: 100, + contributors: [ + { + login: "reviewer", + avatarUrl: "https://example.com/reviewer.png", + profileUrl: "https://github.com/reviewer", + commits: 2, + mergedPullRequests: 3, + firstContributionAt: null, + latestContributionAt: null, + }, + ], +}; + +test("sortContributorActivity ranks eligible contributors by commits", () => { + const sorted = sortContributorActivity([ + { ...data.contributors[0], login: "newcomer", commits: 99, mergedPullRequests: 1 }, + data.contributors[0], + ]); + + assert.equal(sorted[0].login, "newcomer"); +}); diff --git a/docs/lib/contributorActivity.ts b/docs/lib/contributorActivity.ts new file mode 100644 index 0000000000..36d10e58e1 --- /dev/null +++ b/docs/lib/contributorActivity.ts @@ -0,0 +1,28 @@ +export type ContributorActivity = { + login: string; + avatarUrl: string; + profileUrl: string; + commits: number; + mergedPullRequests: number; + firstContributionAt: string | null; + latestContributionAt: string | null; +}; + +export type ContributorActivityData = { + repository: string; + generatedAt: string; + stars: number; + contributors: ContributorActivity[]; +}; + +export function sortContributorActivity(contributors: ContributorActivity[]): ContributorActivity[] { + return [...contributors].sort((left, right) => { + const commitDifference = right.commits - left.commits; + if (commitDifference !== 0) return commitDifference; + + const pullRequestDifference = right.mergedPullRequests - left.mergedPullRequests; + if (pullRequestDifference !== 0) return pullRequestDifference; + + return left.login.localeCompare(right.login, "en", { sensitivity: "base" }); + }); +} diff --git a/docs/lib/worker.test.ts b/docs/lib/worker.test.ts new file mode 100644 index 0000000000..689f809e2d --- /dev/null +++ b/docs/lib/worker.test.ts @@ -0,0 +1,15 @@ +import assert from "node:assert/strict"; +import { test } from "vitest"; +import { sanitizeReturnTo, signPayload, verifySignedPayload } from "../worker"; + +test("signed OAuth payloads round-trip and reject tampering", async () => { + const signed = await signPayload({ login: "dbx-user" }, "test-secret"); + assert.deepEqual(await verifySignedPayload<{ login: string }>(signed, "test-secret"), { login: "dbx-user" }); + assert.equal(await verifySignedPayload(`${signed}x`, "test-secret"), null); +}); + +test("OAuth return paths stay on the DBX origin", () => { + assert.equal(sanitizeReturnTo("/cn/contributors"), "/cn/contributors"); + assert.equal(sanitizeReturnTo("//evil.example"), "/en/contributors"); + assert.equal(sanitizeReturnTo("https://evil.example"), "/en/contributors"); +}); diff --git a/docs/package.json b/docs/package.json index 1547e98bf5..aacc4541e5 100644 --- a/docs/package.json +++ b/docs/package.json @@ -6,6 +6,7 @@ "dev": "next dev", "build": "next build", "postbuild": "node scripts/generate-sitemap.mjs", + "contributors:update": "node scripts/update-contributors.mjs", "start": "next start", "postinstall": "fumadocs-mdx" }, diff --git a/docs/scripts/update-contributors.mjs b/docs/scripts/update-contributors.mjs new file mode 100644 index 0000000000..b12b30d6eb --- /dev/null +++ b/docs/scripts/update-contributors.mjs @@ -0,0 +1,113 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +const repository = process.env.DBX_GITHUB_REPOSITORY || "t8y2/dbx"; +const token = process.env.GITHUB_TOKEN; +const apiBase = "https://api.github.com"; +const outputPath = resolve(import.meta.dirname, "../data/contributors.json"); + +const headers = { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "dbx-contributor-sync", + ...(token ? { Authorization: `Bearer ${token}` } : {}), +}; + +function nextLink(linkHeader) { + if (!linkHeader) return null; + for (const part of linkHeader.split(",")) { + const [url, rel] = part.split(";").map((value) => value.trim()); + if (rel === 'rel="next"') return url.slice(1, -1); + } + return null; +} + +async function request(url) { + const response = await fetch(url, { headers }); + if (!response.ok) { + const body = await response.text(); + throw new Error(`GitHub API ${response.status} for ${url}: ${body.slice(0, 240)}`); + } + return response; +} + +async function fetchAll(path) { + const records = []; + let url = `${apiBase}${path}${path.includes("?") ? "&" : "?"}per_page=100`; + + while (url) { + const response = await request(url); + const page = await response.json(); + if (!Array.isArray(page)) throw new Error(`Expected an array from ${url}`); + records.push(...page); + url = nextLink(response.headers.get("Link")); + } + + return records; +} + +function isBot(user) { + return !user?.login || user.type === "Bot" || user.login.endsWith("[bot]"); +} + +function updateDate(record, date) { + if (!date) return; + if (!record.firstContributionAt || date < record.firstContributionAt) record.firstContributionAt = date; + if (!record.latestContributionAt || date > record.latestContributionAt) record.latestContributionAt = date; +} + +function ensureContributor(contributors, user) { + const key = user.login.toLowerCase(); + let contributor = contributors.get(key); + if (!contributor) { + contributor = { + login: user.login, + avatarUrl: user.avatar_url || `https://github.com/${user.login}.png`, + profileUrl: user.html_url || `https://github.com/${user.login}`, + commits: 0, + mergedPullRequests: 0, + firstContributionAt: null, + latestContributionAt: null, + }; + contributors.set(key, contributor); + } + return contributor; +} + +const [repositoryInfo, commitContributors, pullRequests] = await Promise.all([ + request(`${apiBase}/repos/${repository}`).then((response) => response.json()), + fetchAll(`/repos/${repository}/contributors?anon=0`), + fetchAll(`/repos/${repository}/pulls?state=closed&sort=created&direction=desc`), +]); + +const contributors = new Map(); + +for (const pullRequest of pullRequests) { + if (!pullRequest.merged_at || isBot(pullRequest.user)) continue; + const contributor = ensureContributor(contributors, pullRequest.user); + contributor.mergedPullRequests += 1; + updateDate(contributor, pullRequest.created_at); + updateDate(contributor, pullRequest.merged_at); +} + +for (const entry of commitContributors) { + if (isBot(entry)) continue; + // Commit counts enrich merged-PR contributors but never grant eligibility alone. + const contributor = contributors.get(entry.login.toLowerCase()); + if (contributor) contributor.commits = entry.contributions || 0; +} + +const rankedContributors = [...contributors.values()].filter((contributor) => contributor.mergedPullRequests > 0).sort((left, right) => { + return right.commits - left.commits || right.mergedPullRequests - left.mergedPullRequests || left.login.localeCompare(right.login); +}); + +const output = { + repository, + generatedAt: new Date().toISOString(), + stars: repositoryInfo.stargazers_count || 0, + contributors: rankedContributors, +}; + +await mkdir(resolve(outputPath, ".."), { recursive: true }); +await writeFile(outputPath, `${JSON.stringify(output, null, 2)}\n`); +console.log(`Wrote ${rankedContributors.length} contributors to ${outputPath}`); diff --git a/docs/worker.ts b/docs/worker.ts new file mode 100644 index 0000000000..24fa3c0acd --- /dev/null +++ b/docs/worker.ts @@ -0,0 +1,219 @@ +type AssetsBinding = { fetch(request: Request): Promise }; + +type Env = { + ASSETS: AssetsBinding; + GITHUB_CLIENT_ID?: string; + GITHUB_CLIENT_SECRET?: string; + GITHUB_OAUTH_CALLBACK_URL?: string; + SESSION_SECRET?: string; +}; + +type OAuthState = { + state: string; + verifier: string; + returnTo: string; + expiresAt: number; +}; + +type SessionUser = { + login: string; + avatarUrl: string; + profileUrl: string; + expiresAt: number; +}; + +const encoder = new TextEncoder(); +const STATE_COOKIE = "dbx_oauth_state"; +const SESSION_COOKIE = "dbx_contributor_session"; + +function base64UrlEncode(bytes: Uint8Array): string { + let binary = ""; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, ""); +} + +function base64UrlDecode(value: string): Uint8Array { + const base64 = value.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(value.length / 4) * 4, "="); + const binary = atob(base64); + return Uint8Array.from(binary, (character) => character.charCodeAt(0)); +} + +function asArrayBuffer(bytes: Uint8Array): ArrayBuffer { + return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; +} + +async function hmacKey(secret: string) { + return crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]); +} + +export async function signPayload(payload: object, secret: string): Promise { + const encodedPayload = base64UrlEncode(encoder.encode(JSON.stringify(payload))); + const signature = await crypto.subtle.sign("HMAC", await hmacKey(secret), encoder.encode(encodedPayload)); + return `${encodedPayload}.${base64UrlEncode(new Uint8Array(signature))}`; +} + +export async function verifySignedPayload(value: string | undefined, secret: string): Promise { + if (!value) return null; + const [payload, signature] = value.split("."); + if (!payload || !signature) return null; + + const valid = await crypto.subtle.verify("HMAC", await hmacKey(secret), asArrayBuffer(base64UrlDecode(signature)), encoder.encode(payload)); + if (!valid) return null; + + try { + return JSON.parse(new TextDecoder().decode(base64UrlDecode(payload))) as T; + } catch { + return null; + } +} + +export function sanitizeReturnTo(value: string | null): string { + if (!value || !value.startsWith("/") || value.startsWith("//")) return "/en/contributors"; + return value; +} + +function parseCookies(request: Request): Record { + return Object.fromEntries( + (request.headers.get("Cookie") ?? "") + .split(";") + .map((part) => part.trim()) + .filter(Boolean) + .map((part) => { + const separator = part.indexOf("="); + return separator < 0 ? [part, ""] : [part.slice(0, separator), decodeURIComponent(part.slice(separator + 1))]; + }), + ); +} + +function cookie(name: string, value: string, maxAge: number, path = "/"): string { + return `${name}=${encodeURIComponent(value)}; Max-Age=${maxAge}; Path=${path}; HttpOnly; Secure; SameSite=Lax`; +} + +function randomToken(size = 32): string { + const bytes = new Uint8Array(size); + crypto.getRandomValues(bytes); + return base64UrlEncode(bytes); +} + +async function codeChallenge(verifier: string): Promise { + return base64UrlEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(verifier)))); +} + +function json(data: unknown, status = 200, headers?: HeadersInit): Response { + return Response.json(data, { status, headers: { "Cache-Control": "no-store", ...headers } }); +} + +function requiredConfig(env: Env): { clientId: string; clientSecret: string; sessionSecret: string } | null { + if (!env.GITHUB_CLIENT_ID || !env.GITHUB_CLIENT_SECRET || !env.SESSION_SECRET) return null; + return { clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET, sessionSecret: env.SESSION_SECRET }; +} + +async function startOAuth(request: Request, env: Env): Promise { + const config = requiredConfig(env); + if (!config) return json({ error: "GitHub OAuth is not configured" }, 503); + + const url = new URL(request.url); + const state = randomToken(); + const verifier = randomToken(48); + const returnTo = sanitizeReturnTo(url.searchParams.get("returnTo")); + const callbackUrl = env.GITHUB_OAUTH_CALLBACK_URL || `${url.origin}/api/auth/github/callback`; + const stateCookie = await signPayload({ state, verifier, returnTo, expiresAt: Date.now() + 10 * 60 * 1000 } satisfies OAuthState, config.sessionSecret); + const authorizeUrl = new URL("https://github.com/login/oauth/authorize"); + authorizeUrl.searchParams.set("client_id", config.clientId); + authorizeUrl.searchParams.set("redirect_uri", callbackUrl); + authorizeUrl.searchParams.set("state", state); + authorizeUrl.searchParams.set("code_challenge", await codeChallenge(verifier)); + authorizeUrl.searchParams.set("code_challenge_method", "S256"); + + return new Response(null, { + status: 302, + headers: { + Location: authorizeUrl.toString(), + "Set-Cookie": cookie(STATE_COOKIE, stateCookie, 10 * 60, "/api/auth/github"), + "Cache-Control": "no-store", + }, + }); +} + +async function finishOAuth(request: Request, env: Env): Promise { + const config = requiredConfig(env); + if (!config) return json({ error: "GitHub OAuth is not configured" }, 503); + + const url = new URL(request.url); + const storedState = await verifySignedPayload(parseCookies(request)[STATE_COOKIE], config.sessionSecret); + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + if (!storedState || storedState.expiresAt < Date.now() || !code || state !== storedState.state) return json({ error: "Invalid or expired OAuth state" }, 400); + + const callbackUrl = env.GITHUB_OAUTH_CALLBACK_URL || `${url.origin}/api/auth/github/callback`; + const tokenResponse = await fetch("https://github.com/login/oauth/access_token", { + method: "POST", + headers: { Accept: "application/json", "Content-Type": "application/json", "User-Agent": "dbx-contributors" }, + body: JSON.stringify({ client_id: config.clientId, client_secret: config.clientSecret, code, redirect_uri: callbackUrl, code_verifier: storedState.verifier }), + }); + const tokenData = (await tokenResponse.json()) as { access_token?: string; error?: string }; + if (!tokenResponse.ok || !tokenData.access_token) return json({ error: tokenData.error || "GitHub token exchange failed" }, 502); + + const userResponse = await fetch("https://api.github.com/user", { + headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${tokenData.access_token}`, "User-Agent": "dbx-contributors", "X-GitHub-Api-Version": "2022-11-28" }, + }); + const githubUser = (await userResponse.json()) as { login?: string; avatar_url?: string; html_url?: string }; + if (!userResponse.ok || !githubUser.login) return json({ error: "Unable to read GitHub identity" }, 502); + + // The access token is intentionally discarded after reading the public identity. + const session = await signPayload( + { + login: githubUser.login, + avatarUrl: githubUser.avatar_url || `https://github.com/${githubUser.login}.png`, + profileUrl: githubUser.html_url || `https://github.com/${githubUser.login}`, + expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, + } satisfies SessionUser, + config.sessionSecret, + ); + + const headers = new Headers({ Location: storedState.returnTo, "Cache-Control": "no-store" }); + headers.append("Set-Cookie", cookie(STATE_COOKIE, "", 0, "/api/auth/github")); + headers.append("Set-Cookie", cookie(SESSION_COOKIE, session, 7 * 24 * 60 * 60)); + return new Response(null, { status: 302, headers }); +} + +async function currentUser(request: Request, env: Env): Promise { + const config = requiredConfig(env); + if (!config) return json({ authenticated: false, configured: false }); + + const session = await verifySignedPayload(parseCookies(request)[SESSION_COOKIE], config.sessionSecret); + if (!session || session.expiresAt < Date.now()) return json({ authenticated: false, configured: true }); + return json({ authenticated: true, configured: true, user: { login: session.login, avatarUrl: session.avatarUrl, profileUrl: session.profileUrl } }); +} + +function logout(): Response { + return json({ authenticated: false }, 200, { "Set-Cookie": cookie(SESSION_COOKIE, "", 0) }); +} + +async function contributorAvatar(request: Request): Promise { + const login = new URL(request.url).searchParams.get("login") ?? ""; + if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(login)) return json({ error: "Invalid GitHub login" }, 400); + + const response = await fetch(`https://github.com/${login}.png?size=256`, { redirect: "follow" }); + if (!response.ok || !response.body) return json({ error: "Avatar unavailable" }, 404); + + return new Response(response.body, { + headers: { + "Content-Type": response.headers.get("Content-Type") || "image/png", + "Cache-Control": "public, max-age=86400, s-maxage=604800", + "Cross-Origin-Resource-Policy": "same-origin", + }, + }); +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + if (url.pathname === "/api/auth/github/start" && request.method === "GET") return startOAuth(request, env); + if (url.pathname === "/api/auth/github/callback" && request.method === "GET") return finishOAuth(request, env); + if (url.pathname === "/api/auth/me" && request.method === "GET") return currentUser(request, env); + if (url.pathname === "/api/auth/logout" && request.method === "POST") return logout(); + if (url.pathname === "/api/contributor-avatar" && request.method === "GET") return contributorAvatar(request); + return env.ASSETS.fetch(request); + }, +}; diff --git a/docs/wrangler.json b/docs/wrangler.json index 11cb31c2f6..c4e494909e 100644 --- a/docs/wrangler.json +++ b/docs/wrangler.json @@ -1,7 +1,9 @@ { "name": "dbx", "compatibility_date": "2026-05-07", + "main": "./worker.ts", "assets": { - "directory": "./out" + "directory": "./out", + "binding": "ASSETS" } }