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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ data/fetch-progress.json
data/geocode-cache.json
data/pipeline.log
*.log
dist/
17 changes: 17 additions & 0 deletions app/api/nominate/route.js
Original file line number Diff line number Diff line change
@@ -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 });
}
12 changes: 12 additions & 0 deletions app/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -258,6 +268,7 @@ export default function Home() {
claimStatus={claimStatus}
sidebarOpen={sidebarOpen}
onToggleSidebar={handleToggleSidebar}
onAddMe={handleAddMe}
/>
<SearchBar
developers={developers}
Expand Down Expand Up @@ -318,6 +329,7 @@ export default function Home() {
{compareDevs.length === 2 && (
<ComparePanel devs={compareDevs} onClose={handleCloseCompare} />
)}
{showAddMe && <AddMeModal onClose={handleCloseAddMe} />}
</main>
</div>
);
Expand Down
116 changes: 116 additions & 0 deletions components/AddMeModal.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles['modal-overlay']} onClick={onClose}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Add me to DevGlobe">
<button className={styles['modal__close']} onClick={onClose} aria-label="Close" type="button">✕</button>

{status === 'success' ? (
<div className={styles['modal__success']}>
<div className={styles['modal__success-icon']}>🎉</div>
<h2 className={styles['modal__title']}>You're on the list!</h2>
<p className={styles['modal__message']}>{SUCCESS_MESSAGE}</p>
<button className="btn btn--primary" onClick={onClose} type="button">Done</button>
</div>
) : (
<>
<h2 className={styles['modal__title']}>Add me to DevGlobe</h2>
<p className={styles['modal__subtitle']}>
Submit your GitHub username to be featured on the globe. We'll review and add you within a week.
</p>
<form className={styles['modal__form']} onSubmit={handleSubmit}>
<label className={styles['modal__label']} htmlFor="nominate-username">
GitHub username <span className={styles['modal__required']}>*</span>
</label>
<input
id="nominate-username"
ref={inputRef}
className={styles['modal__input']}
type="text"
placeholder="octocat"
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete="off"
spellCheck="false"
/>

<label className={styles['modal__label']} htmlFor="nominate-location">
Location <span className={styles['modal__optional']}>(optional)</span>
</label>
<input
id="nominate-location"
className={styles['modal__input']}
type="text"
placeholder="San Francisco, CA"
value={location}
onChange={(e) => setLocation(e.target.value)}
autoComplete="off"
/>

{status === 'error' && <div className={styles['modal__error']}>{error}</div>}

<button
className={`btn btn--primary ${styles['modal__submit']}`}
type="submit"
disabled={status === 'submitting'}
>
{status === 'submitting' ? 'Submitting...' : 'Submit nomination'}
</button>
</form>
</>
)}
</div>
</div>
);
}
143 changes: 143 additions & 0 deletions components/AddMeModal.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 6 additions & 1 deletion components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<header className="header">
<div className="header__brand" onClick={onHome} style={{ cursor: 'pointer' }}>
Expand All @@ -12,6 +12,11 @@ export default function Header({ onHome, theme, onToggleTheme, user, onLogout, o
<span className="header__subtitle">Where Developers and AI Agents Connect</span>
</div>
<div className="header__actions">
<button type="button" onClick={onAddMe} className="btn btn--join">
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true" focusable="false">
<path d="M6 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm-2 5c-2.97 0-6 1.49-6 3v1h12v-1c0-1.51-3.03-3-6-3zm-4.9 3c.4-1 2.2-2 4.9-2s4.5 1 4.9 2H1.1zM12.5 4h-1a.5.5 0 0 0 0 1h1v1a.5.5 0 0 0 1 0V5h1a.5.5 0 0 0 0-1h-1V3a.5.5 0 0 0-1 0v1z"></path>
</svg>Add Me To Globe
</button>
<button
type="button"
className="btn btn--sidebar-toggle"
Expand Down
Loading