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
27 changes: 25 additions & 2 deletions app/api/card/route.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export async function GET(request) {

const agent = classifyAgent(dev);
const power = getPowerTier(dev.score || 0);
const hasCityRank = Boolean(dev.cityRank && dev.city);
const rankCardWidth = hasCityRank ? '166' : dev.countryRank ? '255' : '522';

return new ImageResponse(
(
Expand Down Expand Up @@ -269,7 +271,7 @@ export async function GET(request) {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
width: dev.countryRank ? '255' : '522',
width: rankCardWidth,
height: '82',
padding: '12px 18px',
background: 'linear-gradient(135deg, rgba(34,211,238,0.15), rgba(34,211,238,0.04))',
Expand All @@ -289,7 +291,7 @@ export async function GET(request) {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
width: '255',
width: rankCardWidth,
height: '82',
padding: '12px 18px',
background: 'linear-gradient(135deg, rgba(251,146,60,0.15), rgba(251,146,60,0.04))',
Expand All @@ -304,6 +306,27 @@ export async function GET(request) {
<div style={{ display: 'flex', color: '#fed7aa', fontSize: '11', fontWeight: '700', letterSpacing: '1.2' }}>IN {dev.country.toUpperCase()}</div>
</div>
)}
{hasCityRank && (
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
width: rankCardWidth,
height: '82',
padding: '12px 18px',
background: 'linear-gradient(135deg, rgba(139,92,246,0.15), rgba(139,92,246,0.04))',
border: '1px solid rgba(139,92,246,0.32)',
borderRadius: '8',
}}
>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '7' }}>
<span style={{ color: '#c4b5fd', fontSize: '34', fontWeight: '800' }}>#{formatNum(dev.cityRank)}</span>
<span style={{ color: '#64748b', fontSize: '14' }}>of {formatNum(dev.cityTotal)}</span>
</div>
<div style={{ display: 'flex', color: '#ddd6fe', fontSize: '11', fontWeight: '700', letterSpacing: '1.2' }}>IN {dev.city.toUpperCase()}</div>
</div>
)}
</div>

{/* Score + Tier */}
Expand Down
22 changes: 16 additions & 6 deletions components/DetailPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,22 @@ export default function DetailPanel({ dev, onClose, claimedLogins, openCardOnMou
</span>
)}
{dev.countryRank && (
<span
className="rank-badge rank-badge--country"
title={`Ranked ${formatNum(dev.countryRank)} of ${formatNum(dev.countryTotal)} developers in ${dev.country}`}
>
#{formatNum(dev.countryRank)} in {dev.country}
</span>
<div className="rank-badge-stack">
<span
className="rank-badge rank-badge--country"
title={`Ranked ${formatNum(dev.countryRank)} of ${formatNum(dev.countryTotal)} developers in ${dev.country}`}
>
#{formatNum(dev.countryRank)} in {dev.country}
</span>
{dev.cityRank && (
<span
className="rank-badge rank-badge--city"
title={`Ranked ${formatNum(dev.cityRank)} of ${formatNum(dev.cityTotal)} developers in ${dev.city}`}
>
#{formatNum(dev.cityRank)} in {dev.city}
</span>
)}
</div>
)}
</div>
<div className="detail-header__links">
Expand Down
29 changes: 29 additions & 0 deletions lib/country.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ const COUNTRY_ALIASES = {
'macedonia': 'North Macedonia',
};

// Canonical names for common city aliases and alternate spellings.
const CITY_ALIASES = {
'bangalore': 'Bengaluru',
'bombay': 'Mumbai',
'delhi': 'New Delhi',
'nyc': 'New York',
'new york city': 'New York',
'san fran': 'San Francisco',
'sf': 'San Francisco',
};

export function normalizeCountry(name) {
if (!name) return '';
const trimmed = name.trim();
Expand All @@ -64,7 +75,19 @@ export function countryKey(name) {
return normalizeCountry(name).toLowerCase();
}

export function normalizeCity(name) {
if (!name) return '';
const normalized = name.trim().replace(/\s+/g, ' ');
return CITY_ALIASES[normalized.toLowerCase()] || normalized;
}

// Comparable form — use whenever city names from different locations are matched
export function cityKey(name) {
return normalizeCity(name).toLowerCase();
}

export function extractCountry(location) {
if (!location) return '';
const parts = location.split(/[,\-–]/).map(s => s.trim());
const lastPart = parts[parts.length - 1];

Comment thread
sajeetharan marked this conversation as resolved.
Expand All @@ -87,3 +110,9 @@ export function extractCountry(location) {

return lastPart;
}

export function extractCity(location) {
if (!location) return '';
const parts = location.split(/[,\-–]/).map(s => s.trim()).filter(Boolean);
return parts[0] || '';
}
38 changes: 33 additions & 5 deletions lib/ranking.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
import { countryKey, extractCountry, normalizeCountry } from './country.js';
import {
cityKey,
countryKey,
extractCity,
extractCountry,
normalizeCity,
normalizeCountry,
} from './country.js';

export function addDeveloperRanks(developers) {
const countryGroups = new Map();
const cityGroups = new Map();

developers.forEach((developer) => {
if (!developer.location) return;
const country = normalizeCountry(extractCountry(developer.location));
const key = countryKey(country);
if (!key) return;
const countryGroupKey = countryKey(country);
if (!countryGroupKey) return;

if (!countryGroups.has(key)) countryGroups.set(key, { country, logins: [] });
countryGroups.get(key).logins.push(developer.login);
if (!countryGroups.has(countryGroupKey)) countryGroups.set(countryGroupKey, { country, logins: [] });
countryGroups.get(countryGroupKey).logins.push(developer.login);

const city = normalizeCity(extractCity(developer.location));
const cityGroupKey = cityKey(city);
if (!cityGroupKey) return;

const key = `${countryGroupKey}:${cityGroupKey}`;
if (!cityGroups.has(key)) cityGroups.set(key, { city, logins: [] });
cityGroups.get(key).logins.push(developer.login);
});

const countryRanks = new Map();
Expand All @@ -24,10 +40,22 @@ export function addDeveloperRanks(developers) {
});
});

const cityRanks = new Map();
cityGroups.forEach(({ city, logins }) => {
logins.forEach((login, index) => {
cityRanks.set(login, {
city,
cityRank: index + 1,
cityTotal: logins.length,
});
});
});

return developers.map((developer, index) => ({
...developer,
globalRank: index + 1,
globalTotal: developers.length,
...(countryRanks.get(developer.login) || {}),
...(cityRanks.get(developer.login) || {}),
}));
}
13 changes: 13 additions & 0 deletions styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,13 @@ body {
font-weight: 600;
}

.rank-badge-stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}

.rank-badge--global {
background: rgba(8, 145, 178, 0.12);
border: 1px solid rgba(8, 145, 178, 0.4);
Expand All @@ -769,6 +776,12 @@ body {
color: #fbbf24;
}

.rank-badge--city {
background: rgba(139, 92, 246, 0.15);
border: 1px solid rgba(139, 92, 246, 0.45);
color: #a78bfa;
}

.detail-header__links {
display: flex;
gap: 8px;
Expand Down
Loading