From 246fdb626ab891639e759e0fc076903585ae6160 Mon Sep 17 00:00:00 2001
From: Pradip Kumar Kohar <980pk4456@gmail.com>
Date: Thu, 6 Aug 2026 14:30:26 +0530
Subject: [PATCH] Add country rankings and globe country filtering
---
app/page.jsx | 15 +++++++++++++--
components/DetailPanel.jsx | 15 +++++++++++++--
components/Globe.jsx | 12 +++++++++---
lib/rankings.js | 39 ++++++++++++++++++++++++++++++++++++++
styles/main.css | 23 +++++++++++++++++++++-
5 files changed, 96 insertions(+), 8 deletions(-)
create mode 100644 lib/rankings.js
diff --git a/app/page.jsx b/app/page.jsx
index 5566625..bedda9f 100644
--- a/app/page.jsx
+++ b/app/page.jsx
@@ -1,6 +1,6 @@
'use client';
-import React, { useState, useEffect, useCallback, useRef } from 'react';
+import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import Header from '../components/Header.jsx';
import SearchBar from '../components/SearchBar.jsx';
import Leaderboard from '../components/Leaderboard.jsx';
@@ -8,6 +8,7 @@ import DetailPanel from '../components/DetailPanel.jsx';
import ComparePanel from '../components/ComparePanel.jsx';
import LoadingOverlay from '../components/LoadingOverlay.jsx';
import { scoreAll } from '../lib/scoring.js';
+import { buildCountryRankings } from '../lib/rankings.js';
import dynamic from 'next/dynamic';
const Globe = dynamic(() => import('../components/Globe.jsx'), { ssr: false });
@@ -28,6 +29,11 @@ export default function Home() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const globeRef = useRef(null);
+ const countryRankings = useMemo(
+ () => buildCountryRankings(developers),
+ [developers]
+ );
+
useEffect(() => {
// Mirrors the blocking script in layout.jsx so React state matches the
// theme already applied to before hydration.
@@ -261,7 +267,12 @@ export default function Home() {
setSidebarOpen(false)} />
)}
{selectedDev && (
-
+
)}
{compareDevs.length === 2 && (
diff --git a/components/DetailPanel.jsx b/components/DetailPanel.jsx
index d31b940..cf1fc73 100644
--- a/components/DetailPanel.jsx
+++ b/components/DetailPanel.jsx
@@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
import { formatNum } from '../lib/format.js';
-export default function DetailPanel({ dev, onClose, claimedLogins }) {
+export default function DetailPanel({ dev, onClose, claimedLogins, countryRankings }) {
const [fullData, setFullData] = useState(null);
const [showCard, setShowCard] = useState(false);
const radarRef = useRef(null);
@@ -52,6 +52,7 @@ export default function DetailPanel({ dev, onClose, claimedLogins }) {
const soAnswers = merged.soAnswers || 0;
const soAcceptRate = merged.soAcceptRate || 0;
const soBadges = merged.soBadges || 0;
+ const countryRank = countryRankings?.get(dev.login || dev.id);
return (
@@ -74,7 +75,17 @@ export default function DetailPanel({ dev, onClose, claimedLogins }) {
)}
📍 {dev.location || 'Unknown location'}
-
Score: {dev.score}/100
+
+ Score: {dev.score}/100
+ {countryRank && (
+
+ #{countryRank.countryRank} in {countryRank.country}
+
+ )}
+
GitHub ↗
{merged.soUserId && (
diff --git a/components/Globe.jsx b/components/Globe.jsx
index 7a0f469..1b62e83 100644
--- a/components/Globe.jsx
+++ b/components/Globe.jsx
@@ -128,11 +128,17 @@ const Globe = forwardRef(function Globe({
const isLight = theme === 'light';
const geoDevs = useMemo(() => {
- return developers
- .filter(d => d.lat != null && d.lng != null)
+ let list = developers.filter(d => d.lat != null && d.lng != null);
+
+ if (selectedCountry) {
+ const wanted = countryKey(selectedCountry);
+ list = list.filter(d => d.location && countryKey(extractCountry(d.location)) === wanted);
+ }
+
+ return list
.sort((a, b) => b.score - a.score)
.slice(0, 5000);
- }, [developers]);
+ }, [developers, selectedCountry]);
const labelDevs = useMemo(() => {
return geoDevs.filter(d => d.score >= 80);
diff --git a/lib/rankings.js b/lib/rankings.js
new file mode 100644
index 0000000..582dd1e
--- /dev/null
+++ b/lib/rankings.js
@@ -0,0 +1,39 @@
+import { extractCountry, normalizeCountry, countryKey } from './country.js';
+
+/**
+ * Build per-developer country ranks from existing composite scores.
+ * Returns Map
.
+ */
+export function buildCountryRankings(developers) {
+ const byCountry = new Map();
+
+ for (const dev of developers) {
+ if (!dev.location) continue;
+
+ const country = normalizeCountry(extractCountry(dev.location));
+ const key = countryKey(country);
+ if (!key || country.length <= 1) continue;
+
+ if (!byCountry.has(key)) {
+ byCountry.set(key, { name: country, devs: [] });
+ }
+ byCountry.get(key).devs.push(dev);
+ }
+
+ const byLogin = new Map();
+
+ for (const { name, devs } of byCountry.values()) {
+ devs.sort((a, b) => b.score - a.score);
+ const total = devs.length;
+ devs.forEach((dev, index) => {
+ const login = dev.login || dev.id;
+ byLogin.set(login, {
+ country: name,
+ countryRank: index + 1,
+ countryTotal: total,
+ });
+ });
+ }
+
+ return byLogin;
+}
diff --git a/styles/main.css b/styles/main.css
index db8a52e..2973186 100644
--- a/styles/main.css
+++ b/styles/main.css
@@ -723,9 +723,16 @@ body {
margin-top: 2px;
}
+.detail-header__badges {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px;
+ margin-top: 6px;
+}
+
.detail-header__score-badge {
display: inline-block;
- margin-top: 6px;
padding: 3px 10px;
background: var(--accent-blue);
border-radius: 12px;
@@ -733,6 +740,20 @@ body {
font-weight: 600;
}
+.rank-badge {
+ display: inline-block;
+ padding: 3px 10px;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.rank-badge--country {
+ background: rgba(251, 191, 36, 0.15);
+ border: 1px solid rgba(251, 191, 36, 0.45);
+ color: #fbbf24;
+}
+
.detail-header__links {
display: flex;
gap: 8px;