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;