From 1305c8f5ce32cac333e45bf14a74fef5b36b071d Mon Sep 17 00:00:00 2001 From: Harsh Vardhan Chauhan Date: Sat, 10 Oct 2026 09:48:38 +0530 Subject: [PATCH] feat(leaderboard): add worldwide leaderboard with runtime display limit --- .env.example | 6 +++ .gitignore | 1 + README.md | 4 ++ src/app/leaderboard/[country]/page.tsx | 5 ++- src/app/leaderboard/page.tsx | 4 +- .../components/country-grid-client.tsx | 28 +++++++++++-- .../components/country-leaderboard-client.tsx | 9 ++++- .../services/calculate-leaderboard.ts | 3 +- .../leaderboard/services/display-limit.ts | 29 ++++++++++++++ src/features/leaderboard/services/index.ts | 1 + .../services/leaderboard-service.ts | 17 ++++---- .../leaderboard/tests/display-limit.test.ts | 40 +++++++++++++++++++ src/locales/ar.json | 4 ++ src/locales/en.json | 4 ++ 14 files changed, 137 insertions(+), 18 deletions(-) create mode 100644 src/features/leaderboard/services/display-limit.ts create mode 100644 src/features/leaderboard/tests/display-limit.test.ts diff --git a/.env.example b/.env.example index 5683e7c..ce7d06d 100644 --- a/.env.example +++ b/.env.example @@ -38,6 +38,12 @@ LEADERBOARD_CRON_SCHEDULE=0 0 * * * # Data source template for seed country profiles LEADERBOARD_SOURCE_URL_TEMPLATE=https://raw.githubusercontent.com/ashkulz/committers.top/gh-pages/_data/locations/{country}.yml +# Max users shown per country leaderboard (default: 500) +LEADERBOARD_DISPLAY_LIMIT=500 +# Max users shown on the worldwide leaderboard (/leaderboard/worldwide). +# Runtime variable; missing, non-numeric or <= 0 values fall back to 100. +WORLDWIDE_LEADERBOARD_DISPLAY_LIMIT=100 + LEADERBOARD_SEED_LIMIT=256 LEADERBOARD_REFRESH_LIMIT=500 LEADERBOARD_USER_STALE_DAYS=30 diff --git a/.gitignore b/.gitignore index ef5d71d..c3dfb51 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,7 @@ out .nuxt dist next-env.d.ts +.DS_Store # Gatsby files .cache/ diff --git a/README.md b/README.md index 15327c6..b5ac419 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,10 @@ Then open `http://localhost:3000` in your browser! pnpm leaderboard:calculate ``` +### Worldwide Leaderboard + +`/leaderboard/worldwide` ranks users from every country together. The number of users shown is controlled at runtime by `WORLDWIDE_LEADERBOARD_DISPLAY_LIMIT` (default `100`; missing, non-numeric, zero or negative values fall back to the default). Country leaderboards keep using `LEADERBOARD_DISPLAY_LIMIT` (default `500`). + ### Leaderboard Worker & Infrastructure The leaderboard score updates run via a dedicated background worker container using Docker & Supercronic. diff --git a/src/app/leaderboard/[country]/page.tsx b/src/app/leaderboard/[country]/page.tsx index b5cf525..baf8128 100644 --- a/src/app/leaderboard/[country]/page.tsx +++ b/src/app/leaderboard/[country]/page.tsx @@ -38,8 +38,11 @@ function getCountryInfo(country: string): CountryInfo { export async function generateMetadata({ params }: Props): Promise { const { country } = await params; const countryInfo = getCountryInfo(country); + const isWorldwide = country === "worldwide"; const pageTitle = `${countryInfo.title} Developer Leaderboard`; - const description = `Explore the ${countryInfo.title} GitHub developer leaderboard on DevImpact. Compare repository impact, merged pull request strength, and community contribution signals in one country ranking.`; + const description = isWorldwide + ? "Explore the worldwide GitHub developer leaderboard on DevImpact. Compare repository impact, merged pull request strength, and community contribution signals across all countries." + : `Explore the ${countryInfo.title} GitHub developer leaderboard on DevImpact. Compare repository impact, merged pull request strength, and community contribution signals in one country ranking.`; const keywords = [ `${countryInfo.title} developer leaderboard`, `${countryInfo.title} GitHub developers`, diff --git a/src/app/leaderboard/page.tsx b/src/app/leaderboard/page.tsx index ee71220..c90d6a1 100644 --- a/src/app/leaderboard/page.tsx +++ b/src/app/leaderboard/page.tsx @@ -106,7 +106,9 @@ export default function LeaderboardPage() {
- + country.slug !== "worldwide").length} + />
diff --git a/src/features/leaderboard/components/country-grid-client.tsx b/src/features/leaderboard/components/country-grid-client.tsx index cadeffa..b5a02c5 100644 --- a/src/features/leaderboard/components/country-grid-client.tsx +++ b/src/features/leaderboard/components/country-grid-client.tsx @@ -19,13 +19,16 @@ export function CountryGridClient({ countries }: Props) { const { t } = useTranslation(); const [search, setSearch] = useState(""); + const worldwide = useMemo(() => countries.find((c) => c.slug === "worldwide"), [countries]); + const countryList = useMemo(() => countries.filter((c) => c.slug !== "worldwide"), [countries]); + const filtered = useMemo(() => { - if (!search.trim()) return countries; + if (!search.trim()) return countryList; const q = search.trim().toLowerCase(); - return countries.filter( + return countryList.filter( (c) => c.slug.toLowerCase().includes(q) || c.title.toLowerCase().includes(q), ); - }, [countries, search]); + }, [countryList, search]); return ( @@ -37,7 +40,24 @@ export function CountryGridClient({ countries }: Props) { {t("leaderboard.header.description")} - {countries.length > 10 && ( + {worldwide && ( + + + + + {t("leaderboard.worldwide.cta.title")} + + + {t("leaderboard.worldwide.cta.description")} + + + + )} + + {countryList.length > 10 && (
(initialError); const [loading] = useState(false); + const isWorldwide = countrySlug?.trim().toLowerCase() === "worldwide"; if (failed) { return ( @@ -70,10 +71,14 @@ export function CountryLeaderboardClient({ {t("leaderboard.header.eyebrow")}

- {t("leaderboard.country.title", { title })} + {isWorldwide + ? t("leaderboard.worldwide.title") + : t("leaderboard.country.title", { title })}

- {t("leaderboard.country.description", { title })} + {isWorldwide + ? t("leaderboard.worldwide.description") + : t("leaderboard.country.description", { title })}

diff --git a/src/features/leaderboard/services/calculate-leaderboard.ts b/src/features/leaderboard/services/calculate-leaderboard.ts index 68b15dc..6b39f00 100644 --- a/src/features/leaderboard/services/calculate-leaderboard.ts +++ b/src/features/leaderboard/services/calculate-leaderboard.ts @@ -4,6 +4,7 @@ import { calculateUserScore } from "@/features/scoring"; import { persistUserScores } from "@/features/developer/services"; import { createCacheStore, getCacheConfigFromEnv } from "@/lib/cache"; import { getDatabaseStore, type DatabaseStore } from "@/lib/db"; +import { getDisplayLimit } from "./display-limit"; import type { CalculateLeaderboardResponse, LeaderboardMeta, @@ -212,7 +213,7 @@ export async function buildLeaderboardResult( sourceData: { title: string }, errors: string[], ): Promise<{ result: LeaderboardResult; meta: LeaderboardMeta }> { - const allUsers = await db.getLeaderboard(country, getEnvInt("LEADERBOARD_DISPLAY_LIMIT", 500)); + const allUsers = await db.getLeaderboard(country, getDisplayLimit(country)); const totalCount = await db.getLeaderboardCount(country); const scored: ScoredEntry[] = allUsers.map((row) => ({ diff --git a/src/features/leaderboard/services/display-limit.ts b/src/features/leaderboard/services/display-limit.ts new file mode 100644 index 0000000..f5c2afb --- /dev/null +++ b/src/features/leaderboard/services/display-limit.ts @@ -0,0 +1,29 @@ +export const WORLDWIDE_SLUG = "worldwide"; +export const DEFAULT_COUNTRY_DISPLAY_LIMIT = 500; +export const DEFAULT_WORLDWIDE_DISPLAY_LIMIT = 100; + +export function isWorldwideSlug(slug: string): boolean { + return slug.trim().toLowerCase() === WORLDWIDE_SLUG; +} + +function parsePositiveInt(raw: string | undefined, fallback: number): number { + const trimmed = raw?.trim(); + if (!trimmed || !/^\d+$/.test(trimmed)) return fallback; + const parsed = Number.parseInt(trimmed, 10); + return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback; +} + +/** + * Number of users to show for a leaderboard. + * - `worldwide` uses WORLDWIDE_LEADERBOARD_DISPLAY_LIMIT (default 100). + * - Country leaderboards use LEADERBOARD_DISPLAY_LIMIT (default 500). + * Missing, non-numeric, zero or negative values fall back to the default. + */ +export function getDisplayLimit(slug: string): number { + return isWorldwideSlug(slug) + ? parsePositiveInt( + process.env.WORLDWIDE_LEADERBOARD_DISPLAY_LIMIT, + DEFAULT_WORLDWIDE_DISPLAY_LIMIT, + ) + : parsePositiveInt(process.env.LEADERBOARD_DISPLAY_LIMIT, DEFAULT_COUNTRY_DISPLAY_LIMIT); +} diff --git a/src/features/leaderboard/services/index.ts b/src/features/leaderboard/services/index.ts index fa398f5..186f77c 100644 --- a/src/features/leaderboard/services/index.ts +++ b/src/features/leaderboard/services/index.ts @@ -1,2 +1,3 @@ export * from "./leaderboard-service"; export * from "./calculate-leaderboard"; +export * from "./display-limit"; diff --git a/src/features/leaderboard/services/leaderboard-service.ts b/src/features/leaderboard/services/leaderboard-service.ts index 4c3645c..2234b5c 100644 --- a/src/features/leaderboard/services/leaderboard-service.ts +++ b/src/features/leaderboard/services/leaderboard-service.ts @@ -1,6 +1,7 @@ import { createCacheStore, getCacheConfigFromEnv } from "@/lib/cache"; import { getDatabaseStore } from "@/lib/db"; import type { LeaderboardResult, ScoredLeaderboardEntry } from "../types"; +import { getDisplayLimit, isWorldwideSlug } from "./display-limit"; export type { LeaderboardResult, ScoredLeaderboardEntry }; @@ -8,11 +9,8 @@ function buildLeaderboardCacheKey(country: string, namespace: string): string { return `${namespace}:leaderboard:${country.trim().toLowerCase()}`; } -function getDisplayLimit(): number { - const raw = process.env.LEADERBOARD_DISPLAY_LIMIT?.trim(); - if (!raw) return 500; - const parsed = Number.parseInt(raw, 10); - return Number.isFinite(parsed) && parsed > 0 ? parsed : 500; +function getLeaderboardTitle(country: string): string { + return isWorldwideSlug(country) ? "Worldwide" : country; } function getCachedLeaderboard( @@ -30,7 +28,8 @@ function getCachedLeaderboard( } export async function getLeaderboardResult(country: string): Promise { - const displayLimit = getDisplayLimit(); + const displayLimit = getDisplayLimit(country); + const title = getLeaderboardTitle(country); const cacheConfig = getCacheConfigFromEnv(); const cacheStore = createCacheStore(cacheConfig); @@ -57,7 +56,7 @@ export async function getLeaderboardResult(country: string): Promise { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + test("detects the worldwide slug case-insensitively", () => { + expect(isWorldwideSlug("worldwide")).toBe(true); + expect(isWorldwideSlug(" Worldwide ")).toBe(true); + expect(isWorldwideSlug("egypt")).toBe(false); + }); + + test("uses WORLDWIDE_LEADERBOARD_DISPLAY_LIMIT for worldwide", () => { + vi.stubEnv("WORLDWIDE_LEADERBOARD_DISPLAY_LIMIT", "250"); + vi.stubEnv("LEADERBOARD_DISPLAY_LIMIT", "10"); + expect(getDisplayLimit("worldwide")).toBe(250); + expect(getDisplayLimit("egypt")).toBe(10); + }); + + test.each(["", "abc", "0", "-5", "12.5", "10abc"])( + "falls back to the default when the worldwide value is %j", + (value) => { + vi.stubEnv("WORLDWIDE_LEADERBOARD_DISPLAY_LIMIT", value); + expect(getDisplayLimit("worldwide")).toBe(DEFAULT_WORLDWIDE_DISPLAY_LIMIT); + }, + ); + test("falls back to defaults when variables are unset", () => { + vi.stubEnv("WORLDWIDE_LEADERBOARD_DISPLAY_LIMIT", ""); + vi.stubEnv("LEADERBOARD_DISPLAY_LIMIT", ""); + expect(getDisplayLimit("worldwide")).toBe(DEFAULT_WORLDWIDE_DISPLAY_LIMIT); + expect(getDisplayLimit("egypt")).toBe(DEFAULT_COUNTRY_DISPLAY_LIMIT); + }); +}); diff --git a/src/locales/ar.json b/src/locales/ar.json index f63b72d..546e4fd 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -232,6 +232,10 @@ "leaderboard.page.count": "تتوفر {count} قائمة متصدرين حسب الدولة للتصفح.", "leaderboard.country.title": "قائمة المتصدرين - {title}", "leaderboard.country.description": "استكشف المطورين في {title} مرتبين حسب جودة المستودعات وأثر طلبات السحب المدمجة والمساهمات في مجتمع المصدر المفتوح.", + "leaderboard.worldwide.title": "لوحة المتصدرين العالمية للمطورين", + "leaderboard.worldwide.description": "أفضل مطوري المصدر المفتوح من جميع الدول، مرتبين حسب أثر المستودعات وطلبات الدمج ومساهمات المجتمع.", + "leaderboard.worldwide.cta.title": "لوحة المتصدرين العالمية", + "leaderboard.worldwide.cta.description": "اطّلع على أفضل المطورين من جميع الدول في ترتيب واحد.", "leaderboard.title": "لوحة المتصدرين حسب الأثر", "leaderboard.description": "أفضل {processed} مطورين من حيث الأثر في عالم المصدر المفتوح.", "leaderboard.impactRank": "ترتيب الأثر", diff --git a/src/locales/en.json b/src/locales/en.json index 60e1294..b96281d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -232,6 +232,10 @@ "leaderboard.page.count": "{count} country leaderboards are available to browse.", "leaderboard.country.title": "{title} GitHub Developer Leaderboard", "leaderboard.country.description": "Explore developers in {title} ranked by repository quality, merged pull request impact, and open-source community contributions.", + "leaderboard.worldwide.title": "Worldwide Developer Leaderboard", + "leaderboard.worldwide.description": "The top open-source developers across all countries, ranked by repository impact, merged pull requests, and community contribution.", + "leaderboard.worldwide.cta.title": "Worldwide leaderboard", + "leaderboard.worldwide.cta.description": "See the top developers across all countries in one ranking.", "leaderboard.title": "Impact Leaderboard", "leaderboard.description": "The top {processed} developers with the strongest open-source impact.", "leaderboard.impactRank": "Impact Rank",