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
15 changes: 13 additions & 2 deletions app/page.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'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';
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 });
Expand All @@ -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 <html> before hydration.
Expand Down Expand Up @@ -261,7 +267,12 @@ export default function Home() {
<div className="sidebar-backdrop" onClick={() => setSidebarOpen(false)} />
)}
{selectedDev && (
<DetailPanel dev={selectedDev} onClose={handleCloseDetail} claimedLogins={claimedLogins} />
<DetailPanel
dev={selectedDev}
onClose={handleCloseDetail}
claimedLogins={claimedLogins}
countryRankings={countryRankings}
/>
)}
{compareDevs.length === 2 && (
<ComparePanel devs={compareDevs} onClose={handleCloseCompare} />
Expand Down
15 changes: 13 additions & 2 deletions components/DetailPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 (
<div className="detail-panel open">
Expand All @@ -74,7 +75,17 @@ export default function DetailPanel({ dev, onClose, claimedLogins }) {
)}
</div>
<div className="detail-header__location">📍 {dev.location || 'Unknown location'}</div>
<span className="detail-header__score-badge">Score: {dev.score}/100</span>
<div className="detail-header__badges">
<span className="detail-header__score-badge">Score: {dev.score}/100</span>
{countryRank && (
<span
className="rank-badge rank-badge--country"
title={`Ranked ${countryRank.countryRank} of ${countryRank.countryTotal} developers in ${countryRank.country}`}
>
#{countryRank.countryRank} in {countryRank.country}
</span>
)}
</div>
<div className="detail-header__links">
<a href={`https://github.com/${dev.login}`} target="_blank" rel="noreferrer">GitHub ↗</a>
{merged.soUserId && (
Expand Down
12 changes: 9 additions & 3 deletions components/Globe.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
39 changes: 39 additions & 0 deletions lib/rankings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { extractCountry, normalizeCountry, countryKey } from './country.js';

/**
* Build per-developer country ranks from existing composite scores.
* Returns Map<login, { country, countryRank, countryTotal }>.
*/
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;
}
23 changes: 22 additions & 1 deletion styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -723,16 +723,37 @@ 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;
font-size: 12px;
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;
Expand Down
Loading