Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ out
.nuxt
dist
next-env.d.ts
.DS_Store

# Gatsby files
.cache/
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion src/app/leaderboard/[country]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ function getCountryInfo(country: string): CountryInfo {
export async function generateMetadata({ params }: Props): Promise<Metadata> {
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`,
Expand Down
4 changes: 3 additions & 1 deletion src/app/leaderboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ export default function LeaderboardPage() {
<main className="flex min-h-screen flex-col">
<AppHeader />
<div className="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-6 px-4 py-10">
<LeaderboardHero countryCount={countries.length} />
<LeaderboardHero
countryCount={countries.filter((country) => country.slug !== "worldwide").length}
/>

<CountryGridClient countries={countries} />
</div>
Expand Down
28 changes: 24 additions & 4 deletions src/features/leaderboard/components/country-grid-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Card className="border-0 p-6 shadow-lg backdrop-blur-sm">
Expand All @@ -37,7 +40,24 @@ export function CountryGridClient({ countries }: Props) {
<CardDescription>{t("leaderboard.header.description")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{countries.length > 10 && (
{worldwide && (
<Link
href={"/leaderboard/worldwide" as Route}
className="group flex items-center gap-4 rounded-2xl border border-primary/30 bg-gradient-to-r from-primary/10 via-background to-background px-5 py-4 transition-colors hover:border-primary/60"
>
<Globe className="h-8 w-8 shrink-0 text-primary" />
<span className="flex flex-col">
<span className="text-base font-semibold text-foreground group-hover:text-primary">
{t("leaderboard.worldwide.cta.title")}
</span>
<span className="text-sm text-muted-foreground">
{t("leaderboard.worldwide.cta.description")}
</span>
</span>
</Link>
)}

{countryList.length > 10 && (
<div className="relative max-w-sm">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function CountryLeaderboardClient({
const [errors] = useState(initialLeaderboard.errors);
const [failed] = useState<string | null>(initialError);
const [loading] = useState(false);
const isWorldwide = countrySlug?.trim().toLowerCase() === "worldwide";

if (failed) {
return (
Expand Down Expand Up @@ -70,10 +71,14 @@ export function CountryLeaderboardClient({
{t("leaderboard.header.eyebrow")}
</p>
<h1 className="text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
{t("leaderboard.country.title", { title })}
{isWorldwide
? t("leaderboard.worldwide.title")
: t("leaderboard.country.title", { title })}
</h1>
<p className="text-sm leading-7 text-muted-foreground sm:text-base">
{t("leaderboard.country.description", { title })}
{isWorldwide
? t("leaderboard.worldwide.description")
: t("leaderboard.country.description", { title })}
</p>
</div>
</section>
Expand Down
3 changes: 2 additions & 1 deletion src/features/leaderboard/services/calculate-leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) => ({
Expand Down
29 changes: 29 additions & 0 deletions src/features/leaderboard/services/display-limit.ts
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 deletions src/features/leaderboard/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./leaderboard-service";
export * from "./calculate-leaderboard";
export * from "./display-limit";
17 changes: 8 additions & 9 deletions src/features/leaderboard/services/leaderboard-service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
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 };

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(
Expand All @@ -30,7 +28,8 @@ function getCachedLeaderboard(
}

export async function getLeaderboardResult(country: string): Promise<LeaderboardResult> {
const displayLimit = getDisplayLimit();
const displayLimit = getDisplayLimit(country);
const title = getLeaderboardTitle(country);
const cacheConfig = getCacheConfigFromEnv();
const cacheStore = createCacheStore(cacheConfig);

Expand All @@ -57,7 +56,7 @@ export async function getLeaderboardResult(country: string): Promise<Leaderboard

if (rows.length === 0) {
return {
title: country,
title,
totalFromSource: 0,
scored: [],
errors: [],
Expand All @@ -81,7 +80,7 @@ export async function getLeaderboardResult(country: string): Promise<Leaderboard
});

const result: LeaderboardResult = {
title: country,
title,
totalFromSource: totalCount,
scored,
errors: [],
Expand All @@ -100,7 +99,7 @@ export async function getLeaderboardResult(country: string): Promise<Leaderboard
} catch (err: unknown) {
console.warn("Leaderboard database query error:", err);
return {
title: country,
title,
totalFromSource: 0,
scored: [],
errors: [err instanceof Error ? err.message : "Database unavailable"],
Expand Down
40 changes: 40 additions & 0 deletions src/features/leaderboard/tests/display-limit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { afterEach, describe, expect, test, vi } from "vitest";
import {
DEFAULT_COUNTRY_DISPLAY_LIMIT,
DEFAULT_WORLDWIDE_DISPLAY_LIMIT,
getDisplayLimit,
isWorldwideSlug,
} from "../services/display-limit";

describe("display limits", () => {
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);
});
});
4 changes: 4 additions & 0 deletions src/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "ترتيب الأثر",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading