diff --git a/.gitignore b/.gitignore
index 6d98d03..42dcbfb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ data/fetch-progress.json
data/geocode-cache.json
data/pipeline.log
*.log
+dist/
diff --git a/app/api/nominate/route.js b/app/api/nominate/route.js
new file mode 100644
index 0000000..bed50f5
--- /dev/null
+++ b/app/api/nominate/route.js
@@ -0,0 +1,17 @@
+/**
+ * Next.js API route — "Add me to DevGlobe" self-nomination
+ *
+ * Endpoint: POST /api/nominate
+ * Body: { username: string, location?: string }
+ *
+ * Validates the GitHub username via the GitHub API and stores the
+ * nomination in the Cosmos DB 'nominations' container for admin review.
+ */
+import { NextResponse } from 'next/server';
+import { submitNomination } from '../../../lib/nominate.js';
+
+export async function POST(request) {
+ const { username, location } = await request.json().catch(() => ({}));
+ const result = await submitNomination({ username, location });
+ return NextResponse.json(result.body, { status: result.status });
+}
diff --git a/app/page.jsx b/app/page.jsx
index 54d24ed..6068110 100644
--- a/app/page.jsx
+++ b/app/page.jsx
@@ -7,6 +7,7 @@ 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 AddMeModal from '../components/AddMeModal.jsx';
import { scoreAll } from '../lib/scoring.js';
import { addDeveloperRanks } from '../lib/ranking.js';
import dynamic from 'next/dynamic';
@@ -29,6 +30,7 @@ export default function Home() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [cardRequest, setCardRequest] = useState(0);
const [cardContext, setCardContext] = useState(null);
+ const [showAddMe, setShowAddMe] = useState(false);
const globeRef = useRef(null);
useEffect(() => {
@@ -231,6 +233,14 @@ export default function Home() {
setCompareDevs([]);
}, []);
+ const handleAddMe = useCallback(() => {
+ setShowAddMe(true);
+ }, []);
+
+ const handleCloseAddMe = useCallback(() => {
+ setShowAddMe(false);
+ }, []);
+
const handleHome = useCallback(() => {
setCardRequest(0);
setCardContext(null);
@@ -258,6 +268,7 @@ export default function Home() {
claimStatus={claimStatus}
sidebarOpen={sidebarOpen}
onToggleSidebar={handleToggleSidebar}
+ onAddMe={handleAddMe}
/>
)}
+ {showAddMe && }
);
diff --git a/components/AddMeModal.jsx b/components/AddMeModal.jsx
new file mode 100644
index 0000000..f87402b
--- /dev/null
+++ b/components/AddMeModal.jsx
@@ -0,0 +1,116 @@
+import React, { useState, useEffect, useRef } from 'react';
+import styles from './AddMeModal.module.css';
+
+const SUCCESS_MESSAGE = "Thanks! We'll review and add you within a week.";
+
+export default function AddMeModal({ onClose }) {
+ const [username, setUsername] = useState('');
+ const [location, setLocation] = useState('');
+ const [status, setStatus] = useState('idle'); // idle | submitting | success | error
+ const [error, setError] = useState('');
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ inputRef.current?.focus();
+ const onKeyDown = (e) => {
+ if (e.key === 'Escape') onClose();
+ };
+ document.addEventListener('keydown', onKeyDown);
+ return () => document.removeEventListener('keydown', onKeyDown);
+ }, [onClose]);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (status === 'submitting') return;
+
+ const clean = username.trim().replace(/^@/, '');
+ if (!clean) {
+ setStatus('error');
+ setError('Please enter your GitHub username.');
+ return;
+ }
+
+ setStatus('submitting');
+ setError('');
+ try {
+ const res = await fetch('/api/nominate', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username: clean, location: location.trim() }),
+ });
+ const data = await res.json();
+ if (!res.ok) {
+ setStatus('error');
+ setError(data.error || 'Something went wrong. Please try again.');
+ return;
+ }
+ setStatus('success');
+ } catch (err) {
+ setStatus('error');
+ setError('Network error. Please try again.');
+ }
+ };
+
+ return (
+
+
e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Add me to DevGlobe">
+
+
+ {status === 'success' ? (
+
+
🎉
+
You're on the list!
+
{SUCCESS_MESSAGE}
+
+
+ ) : (
+ <>
+
Add me to DevGlobe
+
+ Submit your GitHub username to be featured on the globe. We'll review and add you within a week.
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/components/AddMeModal.module.css b/components/AddMeModal.module.css
new file mode 100644
index 0000000..9665974
--- /dev/null
+++ b/components/AddMeModal.module.css
@@ -0,0 +1,143 @@
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(10, 14, 23, 0.7);
+ backdrop-filter: blur(4px);
+ z-index: 1000;
+ padding: 16px;
+}
+
+.modal {
+ position: relative;
+ width: 100%;
+ max-width: 420px;
+ max-height: 90vh;
+ overflow-y: auto;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ box-shadow: var(--shadow);
+ padding: 28px;
+ animation: modalIn 0.18s ease-out;
+}
+
+@keyframes modalIn {
+ from { opacity: 0; transform: translateY(8px) scale(0.98); }
+ to { opacity: 1; transform: none; }
+}
+
+.modal__close {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: none;
+ border: none;
+ border-radius: 50%;
+ color: var(--text-muted);
+ font-size: 14px;
+ cursor: pointer;
+}
+
+.modal__close:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.modal__title {
+ font-size: 20px;
+ font-weight: 700;
+ margin-bottom: 8px;
+}
+
+.modal__subtitle {
+ font-size: 13px;
+ color: var(--text-secondary);
+ line-height: 1.5;
+ margin-bottom: 20px;
+}
+
+.modal__form {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.modal__label {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ font-size: 13px;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.modal__required {
+ color: #ef4444;
+}
+
+.modal__optional {
+ color: var(--text-muted);
+ font-weight: 400;
+}
+
+.modal__input {
+ padding: 10px 12px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ color: var(--text-primary);
+ font-size: 14px;
+ font-family: var(--font);
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+.modal__input:focus {
+ border-color: var(--accent-blue);
+}
+
+.modal__error {
+ padding: 10px 12px;
+ background: rgba(239, 68, 68, 0.12);
+ border: 1px solid rgba(239, 68, 68, 0.4);
+ border-radius: 8px;
+ color: #fca5a5;
+ font-size: 13px;
+}
+
+.modal__submit {
+ margin-top: 4px;
+ padding: 10px 14px;
+ border-radius: 8px;
+}
+
+.modal__success {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ gap: 12px;
+ padding: 12px 0;
+}
+
+.modal__success-icon {
+ font-size: 44px;
+}
+
+.modal__success .modal__title {
+ margin-bottom: 0;
+}
+
+.modal__message {
+ font-size: 14px;
+ color: var(--text-secondary);
+ line-height: 1.5;
+}
diff --git a/components/Header.jsx b/components/Header.jsx
index 8f61a7e..5331bc5 100644
--- a/components/Header.jsx
+++ b/components/Header.jsx
@@ -3,7 +3,7 @@
import React from 'react';
import UserMenu from './UserMenu.jsx';
-export default function Header({ onHome, theme, onToggleTheme, user, onLogout, onClaim, claimStatus, sidebarOpen, onToggleSidebar }) {
+export default function Header({ onHome, theme, onToggleTheme, user, onLogout, onClaim, claimStatus, sidebarOpen, onToggleSidebar, onAddMe }) {
return (
@@ -12,6 +12,11 @@ export default function Header({ onHome, theme, onToggleTheme, user, onLogout, o
Where Developers and AI Agents Connect
+