From 54994e0b592125bab167d23c6f5ac710629d2b90 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Fri, 13 Nov 2026 00:54:08 +0300 Subject: [PATCH 1/6] perf(api): parallelize comparisons, leaderboard queries, and add edge cache headers - Parallelize developer fetching in compareUsers via Promise.allSettled - Run leaderboard rows and total count queries concurrently via Promise.all - Add Edge Cache-Control headers to /api/compare --- src/app/api/compare/route.ts | 9 ++- .../comparison/services/compare-service.ts | 65 +++++++++-------- .../comparison/tests/compare.route.test.ts | 71 +++++++++++-------- .../services/leaderboard-service.ts | 6 +- 4 files changed, 90 insertions(+), 61 deletions(-) diff --git a/src/app/api/compare/route.ts b/src/app/api/compare/route.ts index 6fa709f..28aff35 100644 --- a/src/app/api/compare/route.ts +++ b/src/app/api/compare/route.ts @@ -27,7 +27,14 @@ export async function GET(request: Request) { const users = await compareUsers(usernames, selectedLanguages); const winnerData = calculateWinner(users); const insights = createComparisonInsights(users, locale); - return NextResponse.json({ success: true, users, ...winnerData, insights }); + return NextResponse.json( + { success: true, users, ...winnerData, insights }, + { + headers: { + "Cache-Control": "public, s-maxage=1800, stale-while-revalidate=86400", + }, + }, + ); } catch (error: unknown) { console.error("GitHub score error:", error); return formatApiErrorResponse(error); diff --git a/src/features/comparison/services/compare-service.ts b/src/features/comparison/services/compare-service.ts index 98c2323..4d19c93 100644 --- a/src/features/comparison/services/compare-service.ts +++ b/src/features/comparison/services/compare-service.ts @@ -255,36 +255,45 @@ export async function compareUsers( usernames: string[], selectedLanguages: string[], ): Promise { + const settled = await Promise.allSettled( + usernames.map(async (username) => { + try { + const { user } = await getUserProfile(username, selectedLanguages); + return { + username: user.username, + name: user.name, + avatarUrl: user.avatarUrl, + repoScore: user.repoScore, + prScore: user.prScore, + contributionScore: user.contributionScore, + finalScore: user.finalScore, + normalizedRepoScore: user.normalizedRepoScore ?? user.repoScore, + normalizedPRScore: user.normalizedPRScore ?? user.prScore, + normalizedContributionScore: user.normalizedContributionScore ?? user.contributionScore, + normalizedFinalScore: user.normalizedFinalScore ?? user.finalScore, + topRepos: user.topRepos as ComparedUserResult["topRepos"], + topPullRequests: user.topPullRequests as ComparedUserResult["topPullRequests"], + topCommunityContributions: (user.topCommunityContributions ?? + []) as ComparedUserResult["topCommunityContributions"], + languageScores: user.languageScores as ComparedUserResult["languageScores"], + signals: user.signals, + explanations: user.explanations, + } as unknown as ComparedUserResult; + } catch (error: unknown) { + throw new CompareUserFetchError( + username, + error instanceof UserFetchError ? error.causeError : error, + ); + } + }), + ); + const results: ComparedUserResult[] = []; - for (const username of usernames) { - try { - const { user } = await getUserProfile(username, selectedLanguages); - results.push({ - username: user.username, - name: user.name, - avatarUrl: user.avatarUrl, - repoScore: user.repoScore, - prScore: user.prScore, - contributionScore: user.contributionScore, - finalScore: user.finalScore, - normalizedRepoScore: user.normalizedRepoScore ?? user.repoScore, - normalizedPRScore: user.normalizedPRScore ?? user.prScore, - normalizedContributionScore: user.normalizedContributionScore ?? user.contributionScore, - normalizedFinalScore: user.normalizedFinalScore ?? user.finalScore, - topRepos: user.topRepos as ComparedUserResult["topRepos"], - topPullRequests: user.topPullRequests as ComparedUserResult["topPullRequests"], - topCommunityContributions: (user.topCommunityContributions ?? - []) as ComparedUserResult["topCommunityContributions"], - languageScores: user.languageScores as ComparedUserResult["languageScores"], - signals: user.signals, - explanations: user.explanations, - } as unknown as ComparedUserResult); - } catch (error: unknown) { - throw new CompareUserFetchError( - username, - error instanceof UserFetchError ? error.causeError : error, - ); + for (const item of settled) { + if (item.status === "rejected") { + throw item.reason; } + results.push(item.value); } return results; } diff --git a/src/features/comparison/tests/compare.route.test.ts b/src/features/comparison/tests/compare.route.test.ts index cb1bef8..a96a060 100644 --- a/src/features/comparison/tests/compare.route.test.ts +++ b/src/features/comparison/tests/compare.route.test.ts @@ -121,21 +121,24 @@ describe("GET /api/compare", () => { }); test("returns structured friendly error when GitHub rate limit is hit", async () => { - mocks.getUserData.mockRejectedValueOnce( - new GitHubApiError({ - message: "API rate limit exceeded for user.", - kind: "PRIMARY_RATE_LIMIT", - status: 200, - rateLimit: { - limit: 5000, - remaining: 0, - used: 5000, - resetAt: Math.floor(Date.now() / 1000) + 60, - resource: "graphql", - }, - retryAfterMs: 60_000, - }), - ); + mocks.getUserData.mockImplementation(async (login: string) => { + if (login === "user-a") { + throw new GitHubApiError({ + message: "API rate limit exceeded for user.", + kind: "PRIMARY_RATE_LIMIT", + status: 200, + rateLimit: { + limit: 5000, + remaining: 0, + used: 5000, + resetAt: Math.floor(Date.now() / 1000) + 60, + resource: "graphql", + }, + retryAfterMs: 60_000, + }); + } + return { data: makeUser(login, login), metrics: { duration: 0, errors: [] } }; + }); const response = await GET( makeRequest({ @@ -155,20 +158,23 @@ describe("GET /api/compare", () => { }); test("returns resource-limit errors instead of masking them as not found", async () => { - mocks.getUserData.mockRejectedValueOnce( - new GitHubApiError({ - message: "Resource limits for this query exceeded.", - kind: "RESOURCE_LIMIT", - status: 200, - rateLimit: { - limit: 5000, - remaining: 4993, - used: 7, - resetAt: Math.floor(Date.now() / 1000) + 60, - resource: "graphql", - }, - }), - ); + mocks.getUserData.mockImplementation(async (login: string) => { + if (login === "petebacondarwin") { + throw new GitHubApiError({ + message: "Resource limits for this query exceeded.", + kind: "RESOURCE_LIMIT", + status: 200, + rateLimit: { + limit: 5000, + remaining: 4993, + used: 7, + resetAt: Math.floor(Date.now() / 1000) + 60, + resource: "graphql", + }, + }); + } + return { data: makeUser(login, login), metrics: { duration: 0, errors: [] } }; + }); const response = await GET( makeRequest({ @@ -306,7 +312,12 @@ describe("GET /api/compare", () => { }); test("returns targeted username for not-found errors", async () => { - mocks.getUserData.mockRejectedValueOnce(new Error("User not found")); + mocks.getUserData.mockImplementation(async (login: string) => { + if (login === "missing-user") { + throw new Error("User not found"); + } + return { data: makeUser(login, login), metrics: { duration: 0, errors: [] } }; + }); const response = await GET( makeRequest({ diff --git a/src/features/leaderboard/services/leaderboard-service.ts b/src/features/leaderboard/services/leaderboard-service.ts index 2234b5c..c3e3e62 100644 --- a/src/features/leaderboard/services/leaderboard-service.ts +++ b/src/features/leaderboard/services/leaderboard-service.ts @@ -51,8 +51,10 @@ export async function getLeaderboardResult(country: string): Promise Date: Fri, 13 Nov 2026 01:35:56 +0300 Subject: [PATCH 2/6] perf(bundle): lazy-load recharts and replace react-icons with inline svg --- package.json | 1 - pnpm-lock.yaml | 242 +++++++----------- src/components/layout/github-link.tsx | 18 +- .../components/result-dashboard.tsx | 12 +- 4 files changed, 124 insertions(+), 149 deletions(-) diff --git a/package.json b/package.json index f1920f5..1451684 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-icons": "^5.6.0", "recharts": "^2.12.7", "redis": "^5.12.1", "tailwind-merge": "^2.5.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08376df..08d16af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: version: 1.25.0(react@19.2.7) next: specifier: ^16.4.1 - version: 16.4.1(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 16.4.1(@babel/core@7.29.7)(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -44,9 +44,6 @@ importers: react-dom: specifier: ^19.2.4 version: 19.2.7(react@19.2.7) - react-icons: - specifier: ^5.6.0 - version: 5.7.0(react@19.2.7) recharts: specifier: ^2.12.7 version: 2.15.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -98,13 +95,13 @@ importers: version: 17.4.2 eslint: specifier: ^9.17.0 - version: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) + version: 9.39.5(jiti@1.21.7) eslint-config-next: specifier: ^16.2.2 - version: 16.2.10(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + version: 16.2.10(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0)) + version: 10.1.8(eslint@9.39.5(jiti@1.21.7)) husky: specifier: ^9.1.7 version: 9.1.7 @@ -610,105 +607,89 @@ packages: resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.3.4': resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.3.4': resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.3.4': resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.3.4': resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.3.4': resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.3.4': resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.3.4': resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.35.5': resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.35.5': resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.35.5': resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.35.5': resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.35.5': resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.35.5': resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.35.5': resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.35.5': resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.35.5': resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} @@ -782,28 +763,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.4.1': resolution: {integrity: sha512-1SuJyDxW/PR9dHJat/ZRgcYLhuSZ+1psP4DlKb2tKIuXbjygqpLrJWRSq+onZQBv8LcmU67EIrmb6c3XTaJLjw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.4.1': resolution: {integrity: sha512-Istng1hMGp9TGO4lUqeKLd70MPN/OOkucVTP2Nmj+gLkwBgmPqUw8CJYBCC7nbl/B/23bBCcjCI5Hsh6hazC1A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.4.1': resolution: {integrity: sha512-aSC9wMNiZRRFVpPkPWBzRwLQxIGm/CiQcXyFF1XNcgodb+uMxdk7aSSGGpDXCbnvZdf4itjHxpOd5h7i7zaiYw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.4.1': resolution: {integrity: sha512-t/myvwFDlypMUjRvbBf8qLPyuaB0iXYYdBtwMt6DM6f+60wtGr4bSYholdPqNeV5VUXZhrpMwfza829kmhS3iA==} @@ -1625,42 +1602,36 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.2.15': resolution: {integrity: sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.2.15': resolution: {integrity: sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.2.15': resolution: {integrity: sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.2.15': resolution: {integrity: sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.2.15': resolution: {integrity: sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.2.15': resolution: {integrity: sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==} @@ -3074,28 +3045,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.33.0: resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.33.0: resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.33.0: resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.33.0: resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} @@ -3556,11 +3523,6 @@ packages: peerDependencies: react: ^19.2.7 - react-icons@5.7.0: - resolution: {integrity: sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==} - peerDependencies: - react: '*' - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4231,20 +4193,20 @@ snapshots: '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.7(supports-color@7.2.0)': + '@babel/core@7.29.7': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helpers': 7.29.7 '@babel/parser': 7.29.7 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4269,19 +4231,19 @@ snapshots: '@babel/helper-globals@7.29.7': {} - '@babel/helper-module-imports@7.29.7(supports-color@7.2.0)': + '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -4308,7 +4270,7 @@ snapshots: '@babel/parser': 7.29.7 '@babel/types': 7.29.7 - '@babel/traverse@7.29.7(supports-color@7.2.0)': + '@babel/traverse@7.29.7': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 @@ -4316,7 +4278,7 @@ snapshots: '@babel/parser': 7.29.7 '@babel/template': 7.29.7 '@babel/types': 7.29.7 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -4559,17 +4521,17 @@ snapshots: '@esbuild/win32-x64@0.18.20': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.5(jiti@1.21.7))': dependencies: - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) + eslint: 9.39.5(jiti@1.21.7) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.2(supports-color@7.2.0)': + '@eslint/config-array@0.21.2': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -4582,10 +4544,10 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.6(supports-color@7.2.0)': + '@eslint/eslintrc@3.3.6': dependencies: ajv: 6.15.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -5775,15 +5737,15 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/type-utils': 8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/utils': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.64.0 - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) + eslint: 9.39.5(jiti@1.21.7) ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -5791,23 +5753,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.64.0 '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.64.0(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.64.0 - debug: 4.4.3(supports-color@7.2.0) - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) + debug: 4.4.3 + eslint: 9.39.5(jiti@1.21.7) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.64.0(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/project-service@8.64.0(typescript@6.0.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@6.0.3) '@typescript-eslint/types': 8.64.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -5821,13 +5783,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - debug: 4.4.3(supports-color@7.2.0) - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) + '@typescript-eslint/typescript-estree': 8.64.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) + debug: 4.4.3 + eslint: 9.39.5(jiti@1.21.7) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -5835,13 +5797,13 @@ snapshots: '@typescript-eslint/types@8.64.0': {} - '@typescript-eslint/typescript-estree@8.64.0(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.64.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/project-service': 8.64.0(typescript@6.0.3) '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@6.0.3) '@typescript-eslint/types': 8.64.0 '@typescript-eslint/visitor-keys': 8.64.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.5 tinyglobby: 0.2.17 @@ -5850,13 +5812,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@1.21.7)) '@typescript-eslint/scope-manager': 8.64.0 '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) + '@typescript-eslint/typescript-estree': 8.64.0(typescript@6.0.3) + eslint: 9.39.5(jiti@1.21.7) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -6312,17 +6274,13 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - debug@3.2.7(supports-color@7.2.0): + debug@3.2.7: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 7.2.0 - debug@4.4.3(supports-color@7.2.0): + debug@4.4.3: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 7.2.0 decimal.js-light@2.5.1: {} @@ -6531,18 +6489,18 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.2.10(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + eslint-config-next@16.2.10(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3): dependencies: '@next/eslint-plugin-next': 16.2.10 - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) - eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0)) - eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0)) - eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0) + eslint: 9.39.5(jiti@1.21.7) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@1.21.7)) + eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@1.21.7)) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5(jiti@1.21.7)) globals: 16.4.0 - typescript-eslint: 8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + typescript-eslint: 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: @@ -6551,56 +6509,56 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0)): + eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@1.21.7)): dependencies: - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) + eslint: 9.39.5(jiti@1.21.7) - eslint-import-resolver-node@0.3.10(supports-color@7.2.0): + eslint-import-resolver-node@0.3.10: dependencies: - debug: 3.2.7(supports-color@7.2.0) + debug: 3.2.7 is-core-module: 2.16.2 resolve: 2.0.0-next.7 transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@1.21.7)): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3(supports-color@7.2.0) - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) + debug: 4.4.3 + eslint: 9.39.5(jiti@1.21.7) get-tsconfig: 4.14.0 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)): dependencies: - debug: 3.2.7(supports-color@7.2.0) + debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) - eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0) + '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) + eslint: 9.39.5(jiti@1.21.7) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@1.21.7)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 - debug: 3.2.7(supports-color@7.2.0) + debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) - eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0) + eslint: 9.39.5(jiti@1.21.7) + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -6612,13 +6570,13 @@ snapshots: string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@1.21.7)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -6628,7 +6586,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) + eslint: 9.39.5(jiti@1.21.7) hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -6637,18 +6595,18 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@7.1.1(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0): + eslint-plugin-react-hooks@7.1.1(eslint@9.39.5(jiti@1.21.7)): dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/parser': 7.29.7 - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) + eslint: 9.39.5(jiti@1.21.7) hermes-parser: 0.25.1 zod: 4.4.3 zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0)): + eslint-plugin-react@7.37.5(eslint@9.39.5(jiti@1.21.7)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -6656,7 +6614,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.4.0 - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) + eslint: 9.39.5(jiti@1.21.7) estraverse: 5.3.0 hasown: 2.0.4 jsx-ast-utils: 3.3.5 @@ -6681,14 +6639,14 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0): + eslint@9.39.5(jiti@1.21.7): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@1.21.7)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2(supports-color@7.2.0) + '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.6(supports-color@7.2.0) + '@eslint/eslintrc': 3.3.6 '@eslint/js': 9.39.5 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.8 @@ -6698,7 +6656,7 @@ snapshots: ajv: 6.15.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -7298,7 +7256,7 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - next@16.4.1(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next@16.4.1(@babel/core@7.29.7)(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@next/env': 16.4.1 '@swc/helpers': 0.5.23 @@ -7307,7 +7265,7 @@ snapshots: postcss: 8.5.23 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.7) + styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7) optionalDependencies: '@next/swc-darwin-arm64': 16.4.1 '@next/swc-darwin-x64': 16.4.1 @@ -7626,10 +7584,6 @@ snapshots: react: 19.2.7 scheduler: 0.27.0 - react-icons@5.7.0(react@19.2.7): - dependencies: - react: 19.2.7 - react-is@16.13.1: {} react-is@17.0.2: {} @@ -8013,12 +7967,12 @@ snapshots: strip-json-comments@3.1.1: {} - styled-jsx@5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.7): + styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.7): dependencies: client-only: 0.0.1 react: 19.2.7 optionalDependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 sucrase@3.35.1: dependencies: @@ -8165,13 +8119,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + typescript-eslint@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.64.0(eslint@9.39.5(jiti@1.21.7)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 9.39.5(jiti@1.21.7)(supports-color@7.2.0) + '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.64.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) + eslint: 9.39.5(jiti@1.21.7) typescript: 6.0.3 transitivePeerDependencies: - supports-color diff --git a/src/components/layout/github-link.tsx b/src/components/layout/github-link.tsx index 91f49a3..effe823 100644 --- a/src/components/layout/github-link.tsx +++ b/src/components/layout/github-link.tsx @@ -1,9 +1,21 @@ "use client"; -import { FaGithub } from "react-icons/fa"; - import { cn } from "@/utils/cn"; +function GitHubIcon({ className }: { className?: string }) { + return ( + + ); +} + type GithubLinkProps = { variant?: "compact" | "prominent"; }; @@ -35,7 +47,7 @@ export function GithubLink({ variant = "compact" }: GithubLinkProps) { /> - {isProminent ? ( diff --git a/src/features/comparison/components/result-dashboard.tsx b/src/features/comparison/components/result-dashboard.tsx index 1bb08a0..366a862 100644 --- a/src/features/comparison/components/result-dashboard.tsx +++ b/src/features/comparison/components/result-dashboard.tsx @@ -6,8 +6,18 @@ import type { Route } from "next"; import { Check, Copy, ExternalLink, Trophy, Share2, XCircle } from "lucide-react"; import { useSearchParams } from "next/navigation"; import { useClipboardCopy } from "@/hooks"; +import dynamic from "next/dynamic"; import { Avatar } from "@/components/layout/avatar"; -import { ComparisonChart } from "./comparison-chart"; + +const ComparisonChart = dynamic( + () => import("./comparison-chart").then((mod) => mod.ComparisonChart), + { + ssr: false, + loading: () => ( +
+ ), + }, +); import { TopList } from "./top-list"; import { InsightsList } from "./insights-list"; import { ScoreCard } from "@/components/cards"; From d4a094d5b1e320c03e37b61e4632e7ebf8026e3c Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Fri, 13 Nov 2026 02:09:56 +0300 Subject: [PATCH 3/6] perf(leaderboard): add client-side pagination to reduce DOM footprint and improve filter performance --- .../components/leaderboard-table.tsx | 73 +++++++++++++++++- .../tests/leaderboard-table.test.tsx | 77 +++++++++++++++++++ src/locales/ar.json | 2 + src/locales/en.json | 2 + 4 files changed, 150 insertions(+), 4 deletions(-) diff --git a/src/features/leaderboard/components/leaderboard-table.tsx b/src/features/leaderboard/components/leaderboard-table.tsx index 5b43bf5..2ac61a4 100644 --- a/src/features/leaderboard/components/leaderboard-table.tsx +++ b/src/features/leaderboard/components/leaderboard-table.tsx @@ -3,7 +3,15 @@ import { useState, useMemo } from "react"; import Link from "next/link"; import type { Route } from "next"; -import { AlertTriangle, ExternalLink, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react"; +import { + AlertTriangle, + ExternalLink, + ArrowUpDown, + ArrowUp, + ArrowDown, + ChevronLeft, + ChevronRight, +} from "lucide-react"; import { Avatar } from "@/components/layout/avatar"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; @@ -50,6 +58,8 @@ function getGithubProfileUrl(username: string): string { return `https://github.com/${username}`; } +const PAGE_SIZE = 50; + export function LeaderboardTable({ users, failedUsers, @@ -60,10 +70,12 @@ export function LeaderboardTable({ }: Props) { const { t } = useTranslation(); const [search, setSearch] = useState(""); + const [page, setPage] = useState(1); const [sortField, setSortField] = useState("impactRank"); const [sortDirection, setSortDirection] = useState("asc"); const handleSort = (field: SortField) => { + setPage(1); if (sortField === field) { setSortDirection((d) => (d === "asc" ? "desc" : "asc")); return; @@ -72,6 +84,11 @@ export function LeaderboardTable({ setSortDirection(field === "impactRank" ? "asc" : "desc"); }; + const handleSearch = (value: string) => { + setSearch(value); + setPage(1); + }; + const filtered = useMemo(() => { const q = search.trim().toLowerCase(); @@ -96,6 +113,13 @@ export function LeaderboardTable({ }); }, [users, search, sortField, sortDirection]); + const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); + const currentPage = Math.min(page, totalPages); + const paginatedUsers = useMemo(() => { + const start = (currentPage - 1) * PAGE_SIZE; + return filtered.slice(start, start + PAGE_SIZE); + }, [filtered, currentPage]); + if (users.length === 0) return null; return ( @@ -132,7 +156,7 @@ export function LeaderboardTable({ )} @@ -280,7 +304,10 @@ export function LeaderboardTable({ type="button" variant="secondary" size="sm" - onClick={() => setSearch("")} + onClick={() => { + setSearch(""); + setPage(1); + }} > {t("leaderboard.clearSearch")} @@ -288,7 +315,7 @@ export function LeaderboardTable({ ) : ( - filtered.map((user) => ( + paginatedUsers.map((user) => (
+ {filtered.length > PAGE_SIZE && ( +
+

+ {t("leaderboard.pagination", { + from: (currentPage - 1) * PAGE_SIZE + 1, + to: Math.min(currentPage * PAGE_SIZE, filtered.length), + total: filtered.length, + })} +

+
+ + + {currentPage} / {totalPages} + + +
+
+ )} diff --git a/src/features/leaderboard/tests/leaderboard-table.test.tsx b/src/features/leaderboard/tests/leaderboard-table.test.tsx index df015f3..b7f7a40 100644 --- a/src/features/leaderboard/tests/leaderboard-table.test.tsx +++ b/src/features/leaderboard/tests/leaderboard-table.test.tsx @@ -27,6 +27,9 @@ vi.mock("@/components/providers/language-provider", () => ({ "leaderboard.partialErrors": "Partial Errors", "leaderboard.noDevelopersSearch": 'No developers found matching "{query}".', "leaderboard.clearSearch": "Clear search", + "leaderboard.pagination": "Showing {from}–{to} of {total}", + "leaderboard.previous": "Previous", + "leaderboard.next": "Next", "comparison.final.score": "Final Score", "comparison.repo.score": "Repo Score", "comparison.pr.score": "PR Score", @@ -339,3 +342,77 @@ describe("LeaderboardTable empty search state", () => { expect(screen.getByText("Alice")).toBeDefined(); }); }); + +describe("LeaderboardTable pagination", () => { + afterEach(() => { + cleanup(); + }); + + const manyUsers = Array.from({ length: 75 }, (_, i) => ({ + username: `user${i + 1}`, + name: `User ${i + 1}`, + avatarUrl: `https://example.com/user${i + 1}.png`, + impactRank: i + 1, + finalScore: 100 - i, + repoScore: 50, + prScore: 30, + contributionScore: 20, + })); + + const renderMany = () => + render( + , + ); + + test("renders 50 rows on page 1 and shows pagination controls", () => { + renderMany(); + + const rows = screen.getAllByRole("row").slice(1); + expect(rows).toHaveLength(50); + expect(screen.getByText("Showing 1–50 of 75")).toBeDefined(); + expect(screen.getByText("1 / 2")).toBeDefined(); + }); + + test("navigates to next page and back to previous page", async () => { + const user = userEvent.setup(); + renderMany(); + + const nextBtn = screen.getByRole("button", { name: /next/i }); + const prevBtn = screen.getByRole("button", { name: /previous/i }); + + expect(prevBtn.hasAttribute("disabled")).toBe(true); + + await user.click(nextBtn); + + const rowsPage2 = screen.getAllByRole("row").slice(1); + expect(rowsPage2).toHaveLength(25); + expect(screen.getByText("Showing 51–75 of 75")).toBeDefined(); + expect(screen.getByText("2 / 2")).toBeDefined(); + expect(nextBtn.hasAttribute("disabled")).toBe(true); + + await user.click(prevBtn); + + const rowsPage1 = screen.getAllByRole("row").slice(1); + expect(rowsPage1).toHaveLength(50); + expect(screen.getByText("Showing 1–50 of 75")).toBeDefined(); + }); + + test("resets to page 1 when search query changes", async () => { + const user = userEvent.setup(); + renderMany(); + + await user.click(screen.getByRole("button", { name: /next/i })); + expect(screen.getByText("2 / 2")).toBeDefined(); + + const searchInput = screen.getByPlaceholderText("Search"); + await user.type(searchInput, "user"); + + expect(screen.getByText("Showing 1–50 of 75")).toBeDefined(); + }); +}); diff --git a/src/locales/ar.json b/src/locales/ar.json index 286e007..6bf8b5a 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -251,6 +251,8 @@ "leaderboard.partialErrors": "لم يمكن حساب درجة {count} مطورين", "leaderboard.search": "البحث عن مطور...", "leaderboard.pagination": "عرض {from}–{to} من {total}", + "leaderboard.previous": "السابق", + "leaderboard.next": "التالي", "leaderboard.back": "جميع الدول", "leaderboard.loading": "جاري حساب درجات المطورين...", "leaderboard.scoringHint": "قد يستغرق هذا لحظة عند التحميل الأول. يتم تخزين النتائج مؤقتًا للزيارات اللاحقة.", diff --git a/src/locales/en.json b/src/locales/en.json index 79c8911..2816e05 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -251,6 +251,8 @@ "leaderboard.partialErrors": "{count} developers could not be scored", "leaderboard.search": "Search developer...", "leaderboard.pagination": "Showing {from}–{to} of {total}", + "leaderboard.previous": "Previous", + "leaderboard.next": "Next", "leaderboard.back": "All countries", "leaderboard.loading": "Scoring developers…", "leaderboard.scoringHint": "This may take a moment on first load. Results are cached for subsequent visits.", From 26403681b4b73a5e2e63c1f73aec1649ede58fb4 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Fri, 13 Nov 2026 02:45:46 +0300 Subject: [PATCH 4/6] perf(db): make worker country selection atomic and avoid redundant DDL --- scripts/calculate-next-country.ts | 18 +++++----- scripts/init-db.ts | 4 +++ .../components/leaderboard-table.tsx | 4 +-- src/lib/db/db-store.ts | 20 ++++++++--- src/lib/db/tests/db-store.test.ts | 36 +++++++++++++++++++ 5 files changed, 67 insertions(+), 15 deletions(-) diff --git a/scripts/calculate-next-country.ts b/scripts/calculate-next-country.ts index d3ca2eb..79125da 100644 --- a/scripts/calculate-next-country.ts +++ b/scripts/calculate-next-country.ts @@ -43,12 +43,16 @@ async function main() { logger.info(`DB: ${(process.env.DATABASE_URL ?? "").slice(0, 40)}...`); const db = getDatabaseStore(); - await db.initializeSchema(); - await db.seedCalculationCountries(); - // 1. Pick the next country to calculate + // 1. Pick and atomically claim the next country to calculate logger.info("Picking next country to calculate..."); - const next = await db.getNextCountryToCalculate(); + let next = await db.getNextCountryToCalculate(); + if (!next) { + // Attempt one-time seed if the calculation table is unseeded + await db.seedCalculationCountries(); + next = await db.getNextCountryToCalculate(); + } + if (!next) { logger.info("No countries to calculate. All are up-to-date or running."); process.exit(0); @@ -56,11 +60,7 @@ async function main() { activeCountrySlug = next.slug; - logger.info(`Selected: ${next.title} (${next.slug})`); - - // 2. Mark as running - await db.startCalculation(next.slug); - logger.info(`Marked '${next.slug}' as running.`); + logger.info(`Selected and claimed: ${next.title} (${next.slug})`); try { // 3. Run the calculation (same function used by the API endpoint) diff --git a/scripts/init-db.ts b/scripts/init-db.ts index 18d7406..f2d64ca 100644 --- a/scripts/init-db.ts +++ b/scripts/init-db.ts @@ -31,6 +31,10 @@ async function main() { await store.initializeSchema(); console.log(" ✓ Schema created (github_users table + indexes)"); + // Seed calculation countries + await store.seedCalculationCountries(); + console.log(" ✓ Seeded calculation countries"); + console.log("\nDone. Database is ready."); process.exit(0); } diff --git a/src/features/leaderboard/components/leaderboard-table.tsx b/src/features/leaderboard/components/leaderboard-table.tsx index 2ac61a4..690a0a6 100644 --- a/src/features/leaderboard/components/leaderboard-table.tsx +++ b/src/features/leaderboard/components/leaderboard-table.tsx @@ -411,7 +411,7 @@ export function LeaderboardTable({