From 265736d9b2351d73bf0de890dd901272972d1c4d Mon Sep 17 00:00:00 2001 From: Muhammad Date: Sat, 15 Aug 2026 14:34:12 +0500 Subject: [PATCH 01/14] Create route.js --- app/badge/[login]/route.js | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 app/badge/[login]/route.js diff --git a/app/badge/[login]/route.js b/app/badge/[login]/route.js new file mode 100644 index 0000000..7a38200 --- /dev/null +++ b/app/badge/[login]/route.js @@ -0,0 +1,45 @@ +import { BADGE_STATS, DEFAULT_BADGE_STAT, renderBadgeSvg, resolveBadgeStat } from '../../../../lib/badge.js'; +import { getBadgeDeveloper } from '../../../../lib/badge-lookup.js'; + +export const runtime = 'nodejs'; + +const LOGIN_PATTERN = /^[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?$/i; + +function svgResponse(svg, { cache = true } = {}) { + return new Response(svg, { + status: 200, + headers: { + 'Content-Type': 'image/svg+xml; charset=utf-8', + // Badges are meant to be embedded (README, personal sites) and read on + // every page view, so cache briefly at the edge instead of per-request. + 'Cache-Control': cache + ? 'public, max-age=0, s-maxage=3600, stale-while-revalidate=86400' + : 'no-store', + }, + }); +} + +export async function GET(request, { params }) { + const { login: rawLogin } = await params; + const login = rawLogin.replace(/\.svg$/i, ''); + + if (!LOGIN_PATTERN.test(login)) { + return svgResponse(renderBadgeSvg({ value: 'invalid login', unranked: true }), { cache: false }); + } + + const { searchParams } = new URL(request.url); + const statParam = searchParams.get('stat') || DEFAULT_BADGE_STAT; + if (!BADGE_STATS.includes(statParam)) { + return svgResponse(renderBadgeSvg({ value: 'invalid stat', unranked: true }), { cache: false }); + } + + try { + const developer = await getBadgeDeveloper(login); + const { value, unranked } = resolveBadgeStat(developer, statParam); + return svgResponse(renderBadgeSvg({ value, unranked })); + } catch (error) { + console.error('Badge render failed:', error.message); + // Degrade to an "unranked" badge rather than a broken image in READMEs. + return svgResponse(renderBadgeSvg({ value: 'unavailable', unranked: true }), { cache: false }); + } +} From 96653eed4c892c91e4d5c231d303a9faaac2b170 Mon Sep 17 00:00:00 2001 From: Muhammad Date: Sat, 15 Aug 2026 14:35:13 +0500 Subject: [PATCH 02/14] Create BadgeSnippet component for stat badges This component allows users to generate and copy embeddable badges for their DevGlobe stats in Markdown or HTML format. --- components/BadgeSnippet.jsx | 82 +++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 components/BadgeSnippet.jsx diff --git a/components/BadgeSnippet.jsx b/components/BadgeSnippet.jsx new file mode 100644 index 0000000..21c60c4 --- /dev/null +++ b/components/BadgeSnippet.jsx @@ -0,0 +1,82 @@ +'use client'; + +import { useState } from 'react'; + +const STAT_OPTIONS = [ + { value: 'globalRank', label: 'Global Rank' }, + { value: 'countryRank', label: 'Country Rank' }, + { value: 'cityRank', label: 'City Rank' }, + { value: 'score', label: 'Score' }, + { value: 'stars', label: 'Stars' }, +]; + +export default function BadgeSnippet({ login, siteUrl }) { + const [stat, setStat] = useState('globalRank'); + const [format, setFormat] = useState('markdown'); + const [copied, setCopied] = useState(false); + + const statQuery = stat === 'globalRank' ? '' : `?stat=${stat}`; + const badgeUrl = `${siteUrl}/api/badge/${encodeURIComponent(login)}.svg${statQuery}`; + const profileUrl = `${siteUrl}/share/${encodeURIComponent(login)}`; + + const snippets = { + markdown: `[![devglobe](${badgeUrl})](${profileUrl})`, + html: `devglobe badge`, + }; + + async function handleCopy() { + try { + await navigator.clipboard.writeText(snippets[format]); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // Clipboard API can fail silently (permissions, insecure context); + // the snippet is still selectable text, so no further action needed. + } + } + + return ( +
+ Embeddable badge +

Get your badge

+

+ Embed a live-updating rank badge in your GitHub README or personal site. It refreshes automatically as your DevGlobe stats update. +

+ +
+ {STAT_OPTIONS.map(option => ( + + ))} +
+ +
+ {['markdown', 'html'].map(option => ( + + ))} +
+ +
+ {snippets[format]} + +
+
+ ); +} From 33af0dc97989261729b13a78175b2f45c4a256fe Mon Sep 17 00:00:00 2001 From: Muhammad Date: Sat, 15 Aug 2026 14:36:14 +0500 Subject: [PATCH 03/14] Add onCardGenerated prop to DetailPanel component --- components/DetailPanel.jsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/components/DetailPanel.jsx b/components/DetailPanel.jsx index b366363..637708e 100644 --- a/components/DetailPanel.jsx +++ b/components/DetailPanel.jsx @@ -10,7 +10,7 @@ import { classifyAgent } from '../lib/agent-class.js'; import { AI_TOOLS } from '../lib/ai-profile.js'; import SpecialTags from './SpecialTags.jsx'; -export default function DetailPanel({ dev, onClose, claimedLogins, openCardOnMount = false, claimSuccess = false }) { +export default function DetailPanel({ dev, onClose, onCardGenerated, claimedLogins, openCardOnMount = false, claimSuccess = false }) { const [fullData, setFullData] = useState(null); const [showCard, setShowCard] = useState(false); const radarRef = useRef(null); @@ -19,6 +19,7 @@ export default function DetailPanel({ dev, onClose, claimedLogins, openCardOnMou const handleGenerateCard = () => { track('card_generated', { login: dev.login }); + onCardGenerated?.(dev.login); setShowCard(true); }; @@ -132,6 +133,7 @@ export default function DetailPanel({ dev, onClose, claimedLogins, openCardOnMou {merged.soUserId && ( StackOverflow ↗ )} + Get Badge ↗