Skip to content

Commit 443da50

Browse files
committed
feat(leaderboard): add worldwide leaderboard with runtime display limit
1 parent 017631e commit 443da50

14 files changed

Lines changed: 137 additions & 18 deletions

File tree

‎.env.example‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ LEADERBOARD_CRON_SCHEDULE=0 0 * * *
3838
# Data source template for seed country profiles
3939
LEADERBOARD_SOURCE_URL_TEMPLATE=https://raw.githubusercontent.com/ashkulz/committers.top/gh-pages/_data/locations/{country}.yml
4040

41+
# Max users shown per country leaderboard (default: 500)
42+
LEADERBOARD_DISPLAY_LIMIT=500
43+
# Max users shown on the worldwide leaderboard (/leaderboard/worldwide).
44+
# Runtime variable; missing, non-numeric or <= 0 values fall back to 100.
45+
WORLDWIDE_LEADERBOARD_DISPLAY_LIMIT=100
46+
4147
LEADERBOARD_SEED_LIMIT=256
4248
LEADERBOARD_REFRESH_LIMIT=500
4349
LEADERBOARD_USER_STALE_DAYS=30

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ out
8282
.nuxt
8383
dist
8484
next-env.d.ts
85+
.DS_Store
8586

8687
# Gatsby files
8788
.cache/

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ Then open `http://localhost:3000` in your browser!
206206
pnpm leaderboard:calculate
207207
```
208208

209+
### Worldwide Leaderboard
210+
211+
`/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`).
212+
209213
### Leaderboard Worker & Infrastructure
210214

211215
The leaderboard score updates run via a dedicated background worker container using Docker & Supercronic.

‎src/app/leaderboard/[country]/page.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ function getCountryInfo(country: string): CountryInfo {
3838
export async function generateMetadata({ params }: Props): Promise<Metadata> {
3939
const { country } = await params;
4040
const countryInfo = getCountryInfo(country);
41+
const isWorldwide = country === "worldwide";
4142
const pageTitle = `${countryInfo.title} Developer Leaderboard`;
42-
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.`;
43+
const description = isWorldwide
44+
? "Explore the worldwide GitHub developer leaderboard on DevImpact. Compare repository impact, merged pull request strength, and community contribution signals across all countries."
45+
: `Explore the ${countryInfo.title} GitHub developer leaderboard on DevImpact. Compare repository impact, merged pull request strength, and community contribution signals in one country ranking.`;
4346
const keywords = [
4447
`${countryInfo.title} developer leaderboard`,
4548
`${countryInfo.title} GitHub developers`,

‎src/app/leaderboard/page.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ export default function LeaderboardPage() {
106106
<main className="flex min-h-screen flex-col">
107107
<AppHeader />
108108
<div className="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-6 px-4 py-10">
109-
<LeaderboardHero countryCount={countries.length} />
109+
<LeaderboardHero
110+
countryCount={countries.filter((country) => country.slug !== "worldwide").length}
111+
/>
110112

111113
<CountryGridClient countries={countries} />
112114
</div>

‎src/features/leaderboard/components/country-grid-client.tsx‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ export function CountryGridClient({ countries }: Props) {
1919
const { t } = useTranslation();
2020
const [search, setSearch] = useState("");
2121

22+
const worldwide = useMemo(() => countries.find((c) => c.slug === "worldwide"), [countries]);
23+
const countryList = useMemo(() => countries.filter((c) => c.slug !== "worldwide"), [countries]);
24+
2225
const filtered = useMemo(() => {
23-
if (!search.trim()) return countries;
26+
if (!search.trim()) return countryList;
2427
const q = search.trim().toLowerCase();
25-
return countries.filter(
28+
return countryList.filter(
2629
(c) => c.slug.toLowerCase().includes(q) || c.title.toLowerCase().includes(q),
2730
);
28-
}, [countries, search]);
31+
}, [countryList, search]);
2932

3033
return (
3134
<Card className="border-0 p-6 shadow-lg backdrop-blur-sm">
@@ -37,7 +40,24 @@ export function CountryGridClient({ countries }: Props) {
3740
<CardDescription>{t("leaderboard.header.description")}</CardDescription>
3841
</CardHeader>
3942
<CardContent className="space-y-4">
40-
{countries.length > 10 && (
43+
{worldwide && (
44+
<Link
45+
href={"/leaderboard/worldwide" as Route}
46+
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"
47+
>
48+
<Globe className="h-8 w-8 shrink-0 text-primary" />
49+
<span className="flex flex-col">
50+
<span className="text-base font-semibold text-foreground group-hover:text-primary">
51+
{t("leaderboard.worldwide.cta.title")}
52+
</span>
53+
<span className="text-sm text-muted-foreground">
54+
{t("leaderboard.worldwide.cta.description")}
55+
</span>
56+
</span>
57+
</Link>
58+
)}
59+
60+
{countryList.length > 10 && (
4161
<div className="relative max-w-sm">
4262
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
4363
<Input

‎src/features/leaderboard/components/country-leaderboard-client.tsx‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function CountryLeaderboardClient({
3030
const [errors] = useState(initialLeaderboard.errors);
3131
const [failed] = useState<string | null>(initialError);
3232
const [loading] = useState(false);
33+
const isWorldwide = countrySlug?.trim().toLowerCase() === "worldwide";
3334

3435
if (failed) {
3536
return (
@@ -70,10 +71,14 @@ export function CountryLeaderboardClient({
7071
{t("leaderboard.header.eyebrow")}
7172
</p>
7273
<h1 className="text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
73-
{t("leaderboard.country.title", { title })}
74+
{isWorldwide
75+
? t("leaderboard.worldwide.title")
76+
: t("leaderboard.country.title", { title })}
7477
</h1>
7578
<p className="text-sm leading-7 text-muted-foreground sm:text-base">
76-
{t("leaderboard.country.description", { title })}
79+
{isWorldwide
80+
? t("leaderboard.worldwide.description")
81+
: t("leaderboard.country.description", { title })}
7782
</p>
7883
</div>
7984
</section>

‎src/features/leaderboard/services/calculate-leaderboard.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { calculateUserScore } from "@/features/scoring";
44
import { persistUserScores } from "@/features/developer/services";
55
import { createCacheStore, getCacheConfigFromEnv } from "@/lib/cache";
66
import { getDatabaseStore, type DatabaseStore } from "@/lib/db";
7+
import { getDisplayLimit } from "./display-limit";
78
import type {
89
CalculateLeaderboardResponse,
910
LeaderboardMeta,
@@ -212,7 +213,7 @@ export async function buildLeaderboardResult(
212213
sourceData: { title: string },
213214
errors: string[],
214215
): Promise<{ result: LeaderboardResult; meta: LeaderboardMeta }> {
215-
const allUsers = await db.getLeaderboard(country, getEnvInt("LEADERBOARD_DISPLAY_LIMIT", 500));
216+
const allUsers = await db.getLeaderboard(country, getDisplayLimit(country));
216217
const totalCount = await db.getLeaderboardCount(country);
217218

218219
const scored: ScoredEntry[] = allUsers.map((row) => ({
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export const WORLDWIDE_SLUG = "worldwide";
2+
export const DEFAULT_COUNTRY_DISPLAY_LIMIT = 500;
3+
export const DEFAULT_WORLDWIDE_DISPLAY_LIMIT = 100;
4+
5+
export function isWorldwideSlug(slug: string): boolean {
6+
return slug.trim().toLowerCase() === WORLDWIDE_SLUG;
7+
}
8+
9+
function parsePositiveInt(raw: string | undefined, fallback: number): number {
10+
const trimmed = raw?.trim();
11+
if (!trimmed || !/^\d+$/.test(trimmed)) return fallback;
12+
const parsed = Number.parseInt(trimmed, 10);
13+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
14+
}
15+
16+
/**
17+
* Number of users to show for a leaderboard.
18+
* - `worldwide` uses WORLDWIDE_LEADERBOARD_DISPLAY_LIMIT (default 100).
19+
* - Country leaderboards use LEADERBOARD_DISPLAY_LIMIT (default 500).
20+
* Missing, non-numeric, zero or negative values fall back to the default.
21+
*/
22+
export function getDisplayLimit(slug: string): number {
23+
return isWorldwideSlug(slug)
24+
? parsePositiveInt(
25+
process.env.WORLDWIDE_LEADERBOARD_DISPLAY_LIMIT,
26+
DEFAULT_WORLDWIDE_DISPLAY_LIMIT,
27+
)
28+
: parsePositiveInt(process.env.LEADERBOARD_DISPLAY_LIMIT, DEFAULT_COUNTRY_DISPLAY_LIMIT);
29+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./leaderboard-service";
22
export * from "./calculate-leaderboard";
3+
export * from "./display-limit";

0 commit comments

Comments
 (0)