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
2 changes: 1 addition & 1 deletion app/api/developer/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function GET(request) {
const container = client.database(DATABASE).container(CONTAINER);

const { resources } = await container.items.query({
query: "SELECT c.id, c.login, c.name, c.avatarUrl, c.bio, c.githubUrl, c.location, c.lat, c.lng, c.followers, c.totalStars, c.totalForks, c.totalWatchers, c.totalCommits, c.topLanguage, c.languages, c.publicRepos, c.topRepos, c.soReputation, c.soAnswers, c.soAcceptRate, c.soBadges, c.soUserId, c.specialTags, c.claimed, c.claimedAt, c.metricsUpdatedAt, c.aiProfile FROM c WHERE (c.id = @id OR c.login = @id) AND (NOT IS_DEFINED(c.nomination) OR c.nomination.status = 'approved')",
query: "SELECT c.id, c.login, c.name, c.avatarUrl, c.bio, c.githubUrl, c.location, c.lat, c.lng, c.followers, c.totalStars, c.totalForks, c.totalWatchers, c.totalCommits, c.topLanguage, c.languages, c.publicRepos, c.topRepos, c.soReputation, c.soAnswers, c.soAcceptRate, c.soBadges, c.soUserId, c.score, c.scoreDimensions, c.scoreWeights, c.scoreHasSO, c.scorePercentile, c.specialTags, c.claimed, c.claimedAt, c.metricsUpdatedAt, c.aiProfile FROM c WHERE (c.id = @id OR c.login = @id) AND (NOT IS_DEFINED(c.nomination) OR c.nomination.status = 'approved')",
parameters: [{ name: '@id', value: id }]
}).fetchAll();

Expand Down
2 changes: 1 addition & 1 deletion app/api/developers/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function GET() {
const container = client.database(DATABASE).container(CONTAINER);

const { resources } = await container.items
.query("SELECT c.id, c.login, c.name, c.avatarUrl, c.githubUrl, c.location, c.lat, c.lng, c.followers, c.publicRepos, c.totalStars, c.totalForks, c.totalWatchers, c.totalCommits, c.topLanguage, c.soUserId, c.soReputation, c.soAnswers, c.soAcceptRate, c.soBadges, c.specialTags, c.claimed, c.metricsUpdatedAt, c.collaborators, c.aiProfile FROM c WHERE NOT IS_DEFINED(c.nomination) OR c.nomination.status = 'approved'")
.query("SELECT c.id, c.login, c.name, c.avatarUrl, c.location, c.lat, c.lng, c.followers, c.publicRepos, c.totalStars, c.totalForks, c.totalCommits, c.topLanguage, c.soUserId, c.soReputation, c.soAnswers, c.soBadges, c.score, c.specialTags, c.claimed, c.metricsUpdatedAt, c.aiProfile FROM c WHERE NOT IS_DEFINED(c.nomination) OR c.nomination.status = 'approved'")
.fetchAll();

return NextResponse.json(projectAgentReadinessList(resources), {
Expand Down
17 changes: 9 additions & 8 deletions app/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ import AiProfileModal from '../components/AiProfileModal.jsx';
import IntroductionInboxModal from '../components/IntroductionInboxModal.jsx';
import QuickTour from '../components/QuickTour.jsx';
import PlatformActivityBanner from '../components/PlatformActivityBanner.jsx';
import { scoreAll } from '../lib/scoring.js';
import { addDeveloperRanks } from '../lib/ranking.js';
import { enrichWithCollaborators } from '../lib/collaboration.js';
import { withOssWorth } from '../lib/oss-worth.js';
import { prepareDeveloperDataset } from '../lib/developer-dataset.js';
import dynamic from 'next/dynamic';

const Globe = dynamic(() => import('../components/Globe.jsx'), { ssr: false });
Expand All @@ -29,6 +26,7 @@ export default function Home() {
const [filtered, setFiltered] = useState(() => cachedDeveloperDataset?.developers || []);
const [selectedDev, setSelectedDev] = useState(null);
const [loading, setLoading] = useState(() => !cachedDeveloperDataset);
const [loadingStage, setLoadingStage] = useState('connecting');
const [error, setError] = useState(null);
const [flyTarget, setFlyTarget] = useState(null);
const [selectedCountry, setSelectedCountry] = useState('');
Expand Down Expand Up @@ -132,7 +130,7 @@ export default function Home() {
const devRes = await fetch('/api/developers', { cache: 'no-store' });
if (devRes.ok) {
const raw = await devRes.json();
const scored = enrichWithCollaborators(addDeveloperRanks(scoreAll(raw))).map(withOssWorth);
const scored = prepareDeveloperDataset(raw);
setDevelopers(scored);
setFiltered(scored);
const claimed = new Set(raw.filter(d => d.claimed).map(d => d.login));
Expand Down Expand Up @@ -220,8 +218,10 @@ export default function Home() {

const res = await fetch('/api/developers', { signal: AbortSignal.timeout(30000) });
if (!res.ok) throw new Error(`Failed to load data: ${res.status}`);
setLoadingStage('downloading');
const raw = await res.json();
const scored = enrichWithCollaborators(addDeveloperRanks(scoreAll(raw))).map(withOssWorth);
setLoadingStage('preparing');
const scored = prepareDeveloperDataset(raw);
setDevelopers(scored);
setFiltered(scored);
// Build set of all claimed logins from data
Expand Down Expand Up @@ -419,12 +419,13 @@ export default function Home() {
setSidebarOpen(false);
}, [developers]);

if (loading || error) {
if (error) {
return <LoadingOverlay error={error} datasetCount={datasetCount} />;
}

return (
<div id="app" className={tourStep ? 'tour-active' : ''}>
<div id="app" className={tourStep ? 'tour-active' : ''} aria-busy={loading}>
{loading && <LoadingOverlay datasetCount={datasetCount} stage={loadingStage} />}
<Header
onHome={handleHome}
theme={theme}
Expand Down
11 changes: 6 additions & 5 deletions components/DetailPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,13 @@ export default function DetailPanel({ dev, onClose, onCardGenerated, claimedLogi
return () => { cancelled = true; };
}, [dev.id]);

const merged = { ...dev, ...fullData };

// Radar chart
useEffect(() => {
if (!dev.scoreDimensions || !radarRef.current) return;
renderRadar(radarRef.current, dev.scoreDimensions);
}, [dev.scoreDimensions]);
if (!merged.scoreDimensions || !radarRef.current) return;
renderRadar(radarRef.current, merged.scoreDimensions);
}, [merged.scoreDimensions]);

// Heatmap
useEffect(() => {
Expand All @@ -108,7 +110,6 @@ export default function DetailPanel({ dev, onClose, onCardGenerated, claimedLogi
renderLanguages(langRef.current, langs);
}, [fullData, dev.topLanguage]);

const merged = { ...dev, ...fullData };
const repos = merged.topRepos || [];
const soRep = merged.soReputation || 0;
const soAnswers = merged.soAnswers || 0;
Expand Down Expand Up @@ -233,7 +234,7 @@ export default function DetailPanel({ dev, onClose, onCardGenerated, claimedLogi
<div className="chart-section">
<h3>Score Breakdown</h3>
<div ref={radarRef} />
<ScoreExplanation dev={dev} />
<ScoreExplanation dev={merged} />
</div>

<div className="chart-section">
Expand Down
23 changes: 20 additions & 3 deletions components/Globe.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,25 @@ const Globe = forwardRef(function Globe({
const [countryFeatures, setCountryFeatures] = useState([]);
const [hoverCountry, setHoverCountry] = useState(null);
const [hoverDev, setHoverDev] = useState(null);
const [pointLimit, setPointLimit] = useState(800);
const isLight = theme === 'light';

useEffect(() => {
setPointLimit(800);
const expandedLimit = window.innerWidth < 768 ? 1200 : 2500;

if (window.requestIdleCallback) {
const idleId = window.requestIdleCallback(
() => setPointLimit(expandedLimit),
{ timeout: 2500 }
);
return () => window.cancelIdleCallback(idleId);
}

const timerId = window.setTimeout(() => setPointLimit(expandedLimit), 1200);
return () => window.clearTimeout(timerId);
}, [developers]);

const geoDevs = useMemo(() => {
let list = developers.filter(d => d.lat != null && d.lng != null);

Expand All @@ -234,8 +251,8 @@ const Globe = forwardRef(function Globe({

return list
.sort((a, b) => b.score - a.score)
.slice(0, 5000);
}, [developers, selectedCountry]);
.slice(0, pointLimit);
}, [developers, pointLimit, selectedCountry]);

const featuredGeoDevs = useMemo(() => (
agentNetworkVisible ? geoDevs.filter(developer => developer.agentReady) : geoDevs
Expand Down Expand Up @@ -630,7 +647,7 @@ const Globe = forwardRef(function Globe({
pointAltitude={displayPointAltitude}
pointRadius={displayPointRadius}
pointColor={displayPointColor}
pointResolution={6}
pointResolution={5}
htmlElementsData={avatarDevs}
htmlLat={avatarLat}
htmlLng={avatarLng}
Expand Down
187 changes: 55 additions & 132 deletions components/LoadingOverlay.jsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,17 @@
'use client';

import React, { useState, useEffect } from 'react';
const STAGE_COPY = {
connecting: 'Connecting to the developer index',
downloading: 'Downloading developer profiles',
preparing: 'Preparing ranks and map points',
};

const FACTS = [
'Mapping contributions across 150+ countries…',
'Calculating star power and commit velocity…',
'Ranking the world\'s top contributors…',
'Building your interactive 3D globe…',
];

const FEATURED_PROFILES = ['torvalds', 'gaearon', 'sindresorhus', 'tj', 'addyosmani'];

function AnimatedCounter({ target, duration = 2000, suffix = '' }) {
const [count, setCount] = useState(0);

useEffect(() => {
let frame;
const startedAt = performance.now();

const update = (now) => {
const progress = Math.min((now - startedAt) / duration, 1);
setCount(Math.floor(target * progress));
if (progress < 1) frame = requestAnimationFrame(update);
};

frame = requestAnimationFrame(update);
return () => cancelAnimationFrame(frame);
}, [target, duration]);

return <>{count.toLocaleString()}{suffix}</>;
}

export default function LoadingOverlay({ error, datasetCount }) {
const facts = [
datasetCount === null
? 'Counting indexed open-source developers…'
: `Indexing ${datasetCount.toLocaleString()} open-source developers…`,
...FACTS,
];
const [factIndex, setFactIndex] = useState(0);
const [dots, setDots] = useState('');

useEffect(() => {
const factTimer = setInterval(() => {
setFactIndex(prev => (prev + 1) % facts.length);
}, 3000);
return () => clearInterval(factTimer);
}, [facts.length]);

useEffect(() => {
const dotTimer = setInterval(() => {
setDots(prev => prev.length >= 3 ? '' : prev + '.');
}, 400);
return () => clearInterval(dotTimer);
}, []);
export default function LoadingOverlay({ error, datasetCount, stage = 'connecting' }) {

if (error) {
return (
<div className="loading-overlay">
<div style={{ textAlign: 'center', maxWidth: 400 }}>
<div className="loading-overlay" role="alert">
<div className="loading-panel loading-panel--error">
<div style={{ fontSize: 48, marginBottom: 16 }}>⚠️</div>
<div style={{ fontSize: 16, marginBottom: 8 }}>Failed to load data</div>
<div style={{ fontSize: 13, color: '#94a3b8' }}>{error}</div>
Expand All @@ -74,87 +27,57 @@ export default function LoadingOverlay({ error, datasetCount }) {
}

return (
<div className="loading-overlay">
<div className="loading-scene" aria-hidden="true">
<div className="loading-scene__orbit"><span /></div>
<div className="loading-globe">
<svg viewBox="0 0 220 220" className="loading-globe__svg">
<defs>
<radialGradient id="loadingGlobeFill" cx="35%" cy="28%">
<stop offset="0%" stopColor="currentColor" stopOpacity="0.28" />
<stop offset="75%" stopColor="currentColor" stopOpacity="0.06" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</radialGradient>
<clipPath id="loadingGlobeClip"><circle cx="110" cy="110" r="78" /></clipPath>
</defs>
<circle cx="110" cy="110" r="78" className="loading-globe__surface" />
<g clipPath="url(#loadingGlobeClip)" className="loading-globe__grid">
<ellipse cx="110" cy="110" rx="78" ry="29" />
<ellipse cx="110" cy="110" rx="78" ry="55" />
<ellipse cx="110" cy="110" rx="31" ry="78" />
<ellipse cx="110" cy="110" rx="58" ry="78" />
<path d="M32 110h156M110 32v156" />
</g>
<g className="loading-globe__routes" clipPath="url(#loadingGlobeClip)">
<path d="M58 126 Q103 55 158 94" />
<path d="M75 74 Q126 143 168 126" />
<path d="M48 105 Q104 128 145 65" />
</g>
<g className="loading-globe__nodes">
<circle cx="58" cy="126" r="4" />
<circle cx="158" cy="94" r="4" />
<circle cx="75" cy="74" r="3" />
<circle cx="168" cy="126" r="3" />
<circle cx="145" cy="65" r="3" />
</g>
<circle cx="110" cy="110" r="78" className="loading-globe__outline" />
</svg>
<div className="loading-overlay" role="status" aria-live="polite" aria-label="Loading DevGlobe">
<div className="loading-panel">
<div className="loading-scene" aria-hidden="true">
<div className="loading-scene__orbit"><span /></div>
<div className="loading-globe">
<svg viewBox="0 0 220 220" className="loading-globe__svg">
<defs>
<radialGradient id="loadingGlobeFill" cx="35%" cy="28%">
<stop offset="0%" stopColor="currentColor" stopOpacity="0.28" />
<stop offset="75%" stopColor="currentColor" stopOpacity="0.06" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</radialGradient>
<clipPath id="loadingGlobeClip"><circle cx="110" cy="110" r="78" /></clipPath>
</defs>
<circle cx="110" cy="110" r="78" className="loading-globe__surface" />
<g clipPath="url(#loadingGlobeClip)" className="loading-globe__grid">
<ellipse cx="110" cy="110" rx="78" ry="29" />
<ellipse cx="110" cy="110" rx="78" ry="55" />
<ellipse cx="110" cy="110" rx="31" ry="78" />
<ellipse cx="110" cy="110" rx="58" ry="78" />
<path d="M32 110h156M110 32v156" />
</g>
<g className="loading-globe__routes" clipPath="url(#loadingGlobeClip)">
<path d="M58 126 Q103 55 158 94" />
<path d="M75 74 Q126 143 168 126" />
<path d="M48 105 Q104 128 145 65" />
</g>
<g className="loading-globe__nodes">
<circle cx="58" cy="126" r="4" />
<circle cx="158" cy="94" r="4" />
<circle cx="75" cy="74" r="3" />
<circle cx="168" cy="126" r="3" />
<circle cx="145" cy="65" r="3" />
</g>
<circle cx="110" cy="110" r="78" className="loading-globe__outline" />
</svg>
</div>
</div>
</div>

{/* Branding */}
<h1 className="loading-brand">
<img src="/devglobe.png" alt="DevGlobe logo" className="loading-brand__logo" />
<span>DevGlobe</span>
</h1>
<p className="loading-tagline">Where Developers and AI Agents Connect</p>

<nav className="loading-profiles" aria-label="Featured developer profiles">
{FEATURED_PROFILES.map(login => (
<a key={login} href={`/share/${login}`}>@{login}</a>
))}
</nav>

{/* Stats preview */}
<div className="loading-stats">
<div className="loading-stat">
<span className="loading-stat__value">
{datasetCount === null ? '…' : <AnimatedCounter target={datasetCount} duration={1800} />}
</span>
<span className="loading-stat__label">Developers</span>
</div>
<div className="loading-stat__divider" />
<div className="loading-stat">
<span className="loading-stat__value"><AnimatedCounter target={150} duration={2000} suffix="+" /></span>
<span className="loading-stat__label">Countries</span>
<div className="loading-brand">
<img src="/devglobe.png" alt="" className="loading-brand__logo" />
<span>DevGlobe</span>
</div>
<div className="loading-stat__divider" />
<div className="loading-stat">
<span className="loading-stat__value"><AnimatedCounter target={50} duration={1800} suffix="M+" /></span>
<span className="loading-stat__label">Stars Tracked</span>
<h2 className="loading-title">Loading developer map</h2>
<p className="loading-status">
{STAGE_COPY[stage] || STAGE_COPY.connecting}
{datasetCount !== null ? ` · ${datasetCount.toLocaleString()} profiles` : ''}
</p>
<div className="loading-progress" aria-hidden="true">
<div className="loading-progress__bar" />
</div>
</div>

{/* Rotating facts */}
<div className="loading-fact" key={factIndex}>
{facts[factIndex]}
</div>

{/* Progress indicator */}
<div className="loading-progress">
<div className="loading-progress__bar" />
</div>
<div className="loading-status">Preparing your globe{dots}</div>
</div>
);
}
15 changes: 15 additions & 0 deletions lib/developer-dataset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { addDeveloperRanks } from './ranking.js';
import { withOssWorth } from './oss-worth.js';

export function prepareDeveloperDataset(developers = []) {
const ranked = [...developers]
.map(developer => ({
...developer,
score: Number.isFinite(developer.score) ? developer.score : 0,
}))
.sort((left, right) =>
right.score - left.score || String(left.login || '').localeCompare(String(right.login || ''))
);

return addDeveloperRanks(ranked).map(withOssWorth);
}
Loading