From 67169e8a4f0d6f7875e6cca01fd6834d5e1147d2 Mon Sep 17 00:00:00 2001 From: babu bahir Date: Fri, 7 Aug 2026 15:04:57 +0530 Subject: [PATCH 1/8] first --- api/lib/nominate.js | 140 ++++++++++++++++++++ api/nominate.js | 31 +++++ package-lock.json | 239 ++++++++++++++++++++++++++++++++++ package.json | 5 +- scripts/review-nominations.js | 236 +++++++++++++++++++++++++++++++++ server.js | 16 +++ src/App.jsx | 13 +- src/components/AddMeModal.jsx | 115 ++++++++++++++++ src/components/Header.jsx | 7 +- styles/main.css | 174 +++++++++++++++++++++++++ 10 files changed, 973 insertions(+), 3 deletions(-) create mode 100644 api/lib/nominate.js create mode 100644 api/nominate.js create mode 100644 scripts/review-nominations.js create mode 100644 src/components/AddMeModal.jsx diff --git a/api/lib/nominate.js b/api/lib/nominate.js new file mode 100644 index 0000000..8bca3fe --- /dev/null +++ b/api/lib/nominate.js @@ -0,0 +1,140 @@ +/** + * Shared logic for the "Add me to DevGlobe" self-nomination flow. + * + * Used by: + * - server.js (local dev server, Cosmos DB emulator) + * - api/nominate.js (Vercel serverless function) + * + * Flow: + * 1. Validate the GitHub username exists via the GitHub API. + * 2. Reject if the developer is already in the main dataset. + * 3. Reject duplicate pending nominations. + * 4. Store the nomination in the 'nominations' container (status: pending). + */ +import { CosmosClient } from '@azure/cosmos'; + +const DATABASE = 'devglobe'; +const DEVELOPERS_CONTAINER = 'developers'; +const NOMINATIONS_CONTAINER = 'nominations'; +const GITHUB_API = 'https://api.github.com'; + +// GitHub usernames: alphanumeric + single hyphens, 1-39 chars, cannot end with hyphen +const USERNAME_RE = /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i; + +function normalizeUsername(raw) { + if (!raw) return ''; + return String(raw).trim().replace(/^@/, ''); +} + +async function verifyGitHubUser(username) { + const headers = { + Accept: 'application/vnd.github+json', + 'User-Agent': 'devglobe-nomination', + }; + if (process.env.GITHUB_TOKEN) { + headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + } + + const res = await fetch(`${GITHUB_API}/users/${encodeURIComponent(username)}`, { headers }); + if (res.status === 404) return { ok: false, notFound: true }; + if (!res.ok) throw new Error(`GitHub API returned ${res.status}`); + return { ok: true, user: await res.json() }; +} + +async function getClient() { + return new CosmosClient({ + endpoint: process.env.COSMOS_ENDPOINT, + key: process.env.COSMOS_KEY, + }); +} + +async function ensureNominationsContainer(client) { + const { database } = await client.databases.createIfNotExists({ id: DATABASE }); + const { container } = await database.containers.createIfNotExists({ + id: NOMINATIONS_CONTAINER, + partitionKey: { paths: ['/username'] }, + }); + return { database, container }; +} + +export async function submitNomination({ username, location }) { + if (!process.env.COSMOS_ENDPOINT || !process.env.COSMOS_KEY) { + return { status: 500, body: { error: 'Cosmos DB credentials not configured' } }; + } + + const cleanUsername = normalizeUsername(username); + if (!USERNAME_RE.test(cleanUsername)) { + return { + status: 400, + body: { error: 'Please enter a valid GitHub username (letters, numbers, hyphens).' }, + }; + } + + let ghUser; + try { + const verified = await verifyGitHubUser(cleanUsername); + if (!verified.ok && verified.notFound) { + return { status: 404, body: { error: 'GitHub user does not exist.' } }; + } + if (!verified.ok) { + return { status: 502, body: { error: 'Could not verify the GitHub username. Please try again.' } }; + } + ghUser = verified.user; + } catch (err) { + console.error('GitHub validation error:', err.message); + return { status: 502, body: { error: 'Could not verify the GitHub username. Please try again.' } }; + } + + const client = await getClient(); + const { database, container } = await ensureNominationsContainer(client); + const developersContainer = database.container(DEVELOPERS_CONTAINER); + + try { + // Already in the main dataset? + const { resources: existing } = await developersContainer.items + .query({ + query: 'SELECT VALUE c.login FROM c WHERE c.login = @login', + parameters: [{ name: '@login', value: cleanUsername }], + }) + .fetchAll(); + if (existing.length > 0) { + return { status: 409, body: { error: 'This developer is already on the globe.' } }; + } + + // Duplicate pending nomination? + const { resources: dupes } = await container.items + .query({ + query: 'SELECT VALUE c FROM c WHERE c.username = @username AND c.status = @status', + parameters: [ + { name: '@username', value: cleanUsername }, + { name: '@status', value: 'pending' }, + ], + }) + .fetchAll(); + if (dupes.length > 0) { + return { status: 409, body: { error: 'This username is already in the review queue.' } }; + } + + const now = new Date().toISOString(); + const nomination = { + id: cleanUsername, + username: cleanUsername, + name: ghUser.name || cleanUsername, + avatarUrl: ghUser.avatar_url, + location: String(location || '').trim(), + status: 'pending', + createdAt: now, + githubUrl: ghUser.html_url, + }; + + await container.items.upsert(nomination); + + return { + status: 201, + body: { message: "Thanks! We'll review and add you within a week.", username: cleanUsername }, + }; + } catch (err) { + console.error('Nomination storage error:', err.message); + return { status: 500, body: { error: 'Failed to store nomination.' } }; + } +} diff --git a/api/nominate.js b/api/nominate.js new file mode 100644 index 0000000..ec9f8a9 --- /dev/null +++ b/api/nominate.js @@ -0,0 +1,31 @@ +/** + * Vercel Serverless Function — "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 { submitNomination } from './lib/nominate.js'; + +export default async function handler(req, res) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + res.setHeader('Content-Type', 'application/json'); + + if (req.method === 'OPTIONS') { + res.status(204).end(); + return; + } + + if (req.method !== 'POST') { + res.status(405).json({ error: 'Method not allowed' }); + return; + } + + const { username, location } = req.body || {}; + const result = await submitNomination({ username, location }); + res.status(result.status).json(result.body); +} diff --git a/package-lock.json b/package-lock.json index 202e985..ad19382 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "vite": "^8.1.5" }, "devDependencies": { + "concurrently": "^10.0.4", "serve": "^14.2.0" } }, @@ -1127,6 +1128,64 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-convert/-/color-convert-2.0.1.tgz", @@ -1195,6 +1254,57 @@ "dev": true, "license": "MIT" }, + "node_modules/concurrently": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.4.tgz", + "integrity": "sha512-trZql+7l/0+WRAsAnEdctr4+iiOS6ZrViI6H8QWcCF9MFS/LT0dKpe8vluB1to6it+OxSI4VospFTIFMW8DJRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "5.6.2", + "rxjs": "7.8.2", + "shell-quote": "1.9.0", + "supports-color": "10.2.2", + "tree-kill": "1.2.2", + "yargs": "18.0.0" + }, + "bin": { + "conc": "dist/bin/index.js", + "concurrently": "dist/bin/index.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/content-disposition": { "version": "0.5.2", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-disposition/-/content-disposition-0.5.2.tgz", @@ -1853,6 +1963,16 @@ "node": ">= 0.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/escape-html/-/escape-html-1.0.3.tgz", @@ -2189,6 +2309,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3510,6 +3653,16 @@ "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=", "license": "BSD-3-Clause" }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3729,6 +3882,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/side-channel/-/side-channel-1.1.1.tgz", @@ -4041,6 +4207,16 @@ "node": ">=0.6" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", @@ -4277,6 +4453,69 @@ "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index f2069de..e40f96e 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Interactive 3D globe visualization of top GitHub developers", "scripts": { "dev": "vite", + "dev:all": "concurrently -n api,web -c blue,green \"npm run server\" \"npm run dev\"", "build": "vite build", "preview": "vite preview", "server": "node server.js", @@ -11,7 +12,8 @@ "fetch-github": "node scripts/fetch-github.js", "fetch-stackoverflow": "node scripts/fetch-stackoverflow.js", "geocode": "node scripts/geocode.js", - "upload-cosmos": "node scripts/upload-cosmosdb.js" + "upload-cosmos": "node scripts/upload-cosmosdb.js", + "review-nominations": "node scripts/review-nominations.js" }, "dependencies": { "@azure/cosmos": "^4.10.0", @@ -27,6 +29,7 @@ "vite": "^8.1.5" }, "devDependencies": { + "concurrently": "^10.0.4", "serve": "^14.2.0" }, "type": "module" diff --git a/scripts/review-nominations.js b/scripts/review-nominations.js new file mode 100644 index 0000000..9a1ca8c --- /dev/null +++ b/scripts/review-nominations.js @@ -0,0 +1,236 @@ +/** + * Admin review flow for "Add me to DevGlobe" nominations. + * + * Usage: + * node scripts/review-nominations.js list # show nominations + * node scripts/review-nominations.js approve # add to main dataset + * node scripts/review-nominations.js reject # reject nomination + * node scripts/review-nominations.js status # show single nomination + * + * Approving fetches the user's GitHub data, geocodes their location, + * and upserts the developer document into the 'developers' container. + */ +import 'dotenv/config'; +import { CosmosClient } from '@azure/cosmos'; + +const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT; +const COSMOS_KEY = process.env.COSMOS_KEY; +const DATABASE = 'devglobe'; +const DEVELOPERS_CONTAINER = 'developers'; +const NOMINATIONS_CONTAINER = 'nominations'; + +// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator +if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; +} + +if (!COSMOS_ENDPOINT || !COSMOS_KEY) { + console.error('Error: COSMOS_ENDPOINT and COSMOS_KEY are required in .env'); + process.exit(1); +} + +const client = new CosmosClient({ endpoint: COSMOS_ENDPOINT, key: COSMOS_KEY }); + +async function ensureContainers() { + const { database } = await client.databases.createIfNotExists({ id: DATABASE }); + const { container: nominations } = await database.containers.createIfNotExists({ + id: NOMINATIONS_CONTAINER, + partitionKey: { paths: ['/username'] }, + }); + const { container: developers } = await database.containers.createIfNotExists({ + id: DEVELOPERS_CONTAINER, + partitionKey: { paths: ['/location'] }, + }); + return { database, nominations, developers }; +} + +async function listNominations(container) { + const { resources } = await container.items + .query({ query: 'SELECT * FROM c ORDER BY c.createdAt' }) + .fetchAll(); + if (resources.length === 0) { + console.log('No nominations found.'); + return; + } + console.log(`Found ${resources.length} nomination(s):\n`); + for (const n of resources) { + const date = new Date(n.createdAt).toLocaleString(); + console.log( + ` [${n.status}] ${n.username.padEnd(24)} (${n.name || '—'})` + + `${n.location ? ` — ${n.location}` : ''} — submitted ${date}` + ); + } +} + +async function getNomination(container, username) { + const { resources } = await container.items + .query({ + query: 'SELECT * FROM c WHERE c.username = @username', + parameters: [{ name: '@username', value: username }], + }) + .fetchAll(); + return resources[0] || null; +} + +async function setStatus(container, nomination, status) { + const doc = { ...nomination, status, reviewedAt: new Date().toISOString() }; + await container.items.upsert(doc); + console.log(` ✓ "${nomination.username}" marked as ${status}`); +} + +async function fetchGitHubUser(username) { + const headers = { Accept: 'application/vnd.github+json', 'User-Agent': 'devglobe-review' }; + if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + + const userRes = await fetch(`https://api.github.com/users/${username}`, { headers }); + if (!userRes.ok) { + throw new Error(`GitHub API returned ${userRes.status} for user lookup`); + } + const user = await userRes.json(); + + const reposRes = await fetch( + `https://api.github.com/users/${username}/repos?sort=stargazers_count&direction=desc&per_page=5&type=owner`, + { headers } + ); + const repos = reposRes.ok ? await reposRes.json() : []; + + const totalStars = repos.reduce((sum, r) => sum + (r.stargazers_count || 0), 0); + const totalForks = repos.reduce((sum, r) => sum + (r.forks_count || 0), 0); + const langCounts = {}; + repos.forEach(r => { + if (r.language) langCounts[r.language] = (langCounts[r.language] || 0) + 1; + }); + const totalLangRepos = Object.values(langCounts).reduce((s, v) => s + v, 0); + const languages = Object.entries(langCounts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 4) + .map(([name, count]) => ({ name, percent: Math.round((count / totalLangRepos) * 100) })); + const topLanguage = languages[0]?.name || null; + + return { + login: user.login, + name: user.name || user.login, + avatarUrl: user.avatar_url, + bio: user.bio, + location: user.location || 'Unknown', + followers: user.followers || 0, + totalStars, + totalForks, + totalWatchers: totalForks, + topLanguage, + languages, + topRepos: repos.map(r => ({ name: r.name, stars: r.stargazers_count, forks: r.forks_count })), + totalCommits: 0, + }; +} + +function fallbackGeocode(location) { + const known = { + 'san francisco': { lat: 37.7749, lng: -122.4194 }, + 'new york': { lat: 40.7128, lng: -74.006 }, + 'london': { lat: 51.5074, lng: -0.1278 }, + 'berlin': { lat: 52.52, lng: 13.405 }, + 'toronto': { lat: 43.6532, lng: -79.3832 }, + 'seattle': { lat: 47.6062, lng: -122.3321 }, + 'bangalore': { lat: 12.9716, lng: 77.5946 }, + 'singapore': { lat: 1.3521, lng: 103.8198 }, + 'sydney': { lat: -33.8688, lng: 151.2093 }, + 'usa': { lat: 39.8283, lng: -98.5795 }, + 'united states': { lat: 39.8283, lng: -98.5795 }, + }; + const normalized = (location || '').toLowerCase(); + for (const [key, coords] of Object.entries(known)) { + if (normalized.includes(key)) return coords; + } + return null; +} + +async function geocode(location) { + if (!location) return null; + const fallback = fallbackGeocode(location); + if (fallback) return fallback; + + if (process.env.GEOCODE_API_KEY) { + try { + const params = new URLSearchParams({ q: location, key: process.env.GEOCODE_API_KEY, limit: '1', no_annotations: '1' }); + const res = await fetch(`https://api.opencagedata.com/geocode/v1/json?${params}`); + const data = await res.json(); + if (data.results?.[0]?.geometry) { + const { lat, lng } = data.results[0].geometry; + return { lat, lng }; + } + } catch { /* fall through */ } + } + return null; +} + +async function approve(nominations, developers, username) { + const nomination = await getNomination(nominations, username); + if (!nomination) { + console.error(`No nomination found for "${username}".`); + process.exit(1); + } + if (nomination.status === 'approved') { + console.error(`"${username}" is already approved.`); + process.exit(1); + } + + console.log(`Approving "${username}"...`); + const dev = await fetchGitHubUser(username); + const coords = await geocode(dev.location); + + const doc = { + id: dev.login, + ...dev, + location: dev.location || 'Unknown', + ...(coords ? { lat: coords.lat, lng: coords.lng } : {}), + }; + + await developers.items.upsert(doc); + await setStatus(nominations, nomination, 'approved'); + console.log(` ✓ Added "${username}" to the developers dataset.`); +} + +async function reject(nominations, username) { + const nomination = await getNomination(nominations, username); + if (!nomination) { + console.error(`No nomination found for "${username}".`); + process.exit(1); + } + await setStatus(nominations, nomination, 'rejected'); +} + +async function main() { + const [cmd, username] = process.argv.slice(2); + const { nominations, developers } = await ensureContainers(); + + switch (cmd) { + case 'list': + await listNominations(nominations); + break; + case 'status': + if (!username) { console.error('Usage: review-nominations.js status '); process.exit(1); } + console.log(JSON.stringify(await getNomination(nominations, username), null, 2)); + break; + case 'approve': + if (!username) { console.error('Usage: review-nominations.js approve '); process.exit(1); } + await approve(nominations, developers, username); + break; + case 'reject': + if (!username) { console.error('Usage: review-nominations.js reject '); process.exit(1); } + await reject(nominations, username); + break; + default: + console.log(`Usage: node scripts/review-nominations.js [username] + list Show all nominations + status Show details for one nomination + approve Approve and add to the developers dataset + reject Reject the nomination`); + process.exit(cmd ? 1 : 0); + } +} + +main().catch(err => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/server.js b/server.js index 64de138..8e3f24d 100644 --- a/server.js +++ b/server.js @@ -6,13 +6,22 @@ import { CosmosClient } from '@azure/cosmos'; import dotenv from 'dotenv'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; +import { submitNomination } from './api/lib/nominate.js'; dotenv.config(); +// The Cosmos DB emulator uses a self-signed cert; only bypass TLS verification +// when talking to the local emulator (never for real Azure endpoints). +if (process.env.COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(process.env.COSMOS_ENDPOINT)) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; +} + const __dirname = dirname(fileURLToPath(import.meta.url)); const app = express(); const PORT = process.env.PORT || 3000; +app.use(express.json()); + const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT; const COSMOS_KEY = process.env.COSMOS_KEY; const DATABASE = 'devglobe'; @@ -178,6 +187,13 @@ app.get('/api/developer', async (req, res) => { } }); +// Add-me self-nomination endpoint +app.post('/api/nominate', async (req, res) => { + const { username, location } = req.body || {}; + const result = await submitNomination({ username, location }); + res.status(result.status).json(result.body); +}); + // Serve static files app.use(express.static(__dirname)); diff --git a/src/App.jsx b/src/App.jsx index c6b5050..6065fc7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,6 +5,7 @@ import Globe from './components/Globe.jsx'; import Leaderboard from './components/Leaderboard.jsx'; import DetailPanel from './components/DetailPanel.jsx'; import LoadingOverlay from './components/LoadingOverlay.jsx'; +import AddMeModal from './components/AddMeModal.jsx'; import { scoreAll } from './utils/scoring.js'; export default function App() { @@ -14,6 +15,7 @@ export default function App() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [flyTarget, setFlyTarget] = useState(null); + const [showAddMe, setShowAddMe] = useState(false); const globeRef = useRef(null); useEffect(() => { @@ -60,13 +62,21 @@ export default function App() { setFlyTarget(null); }, [developers]); + const handleAddMe = useCallback(() => { + setShowAddMe(true); + }, []); + + const handleCloseAddMe = useCallback(() => { + setShowAddMe(false); + }, []); + if (loading || error) { return ; } return (
-
+
)} + {showAddMe && }
); diff --git a/src/components/AddMeModal.jsx b/src/components/AddMeModal.jsx new file mode 100644 index 0000000..c8c4455 --- /dev/null +++ b/src/components/AddMeModal.jsx @@ -0,0 +1,115 @@ +import React, { useState, useEffect, useRef } from 'react'; + +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. +

+
+ + setUsername(e.target.value)} + autoComplete="off" + spellCheck="false" + /> + + + setLocation(e.target.value)} + autoComplete="off" + /> + + {status === 'error' &&
{error}
} + + +
+ + )} +
+
+ ); +} diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 4abab56..468748d 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -1,6 +1,6 @@ import React from 'react'; -export default function Header({ onHome }) { +export default function Header({ onHome, onAddMe }) { return (
@@ -9,6 +9,11 @@ export default function Header({ onHome }) { Visualizing the World's Top Open-Source Contributors
+ diff --git a/styles/main.css b/styles/main.css index 5472a74..3b3db42 100644 --- a/styles/main.css +++ b/styles/main.css @@ -87,6 +87,9 @@ body { font-family: var(--font); font-weight: 500; text-decoration: none; + background: none; + border: none; + cursor: pointer; transition: background 0.2s, transform 0.15s; white-space: nowrap; } @@ -95,6 +98,32 @@ body { transform: translateY(-1px); } +.btn--join { + background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); + color: #fff; +} + +.btn--join:hover { + background: linear-gradient(135deg, #4b8efc, #9a6cfc); + box-shadow: 0 0 12px rgba(99, 102, 241, 0.4); +} + +.btn--primary { + background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); + color: #fff; + justify-content: center; +} + +.btn--primary:hover { + background: linear-gradient(135deg, #4b8efc, #9a6cfc); +} + +.btn--primary:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + .btn--star { background: rgba(255, 255, 255, 0.08); border: 1px solid var(--border); @@ -879,6 +908,151 @@ body { color: var(--text-secondary); } +/* Add-me nomination modal */ +.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; +} + /* Responsive */ @media (max-width: 1024px) { .sidebar { From d142ef79a990a13ca8320479353c09a267f8ed66 Mon Sep 17 00:00:00 2001 From: babu bahir Date: Fri, 7 Aug 2026 15:41:30 +0530 Subject: [PATCH 2/8] fix: bypass TLS verification for local Cosmos DB emulator --- .gitignore | 1 + api/developer.js | 6 ++++++ api/developers.js | 6 ++++++ api/lib/nominate.js | 4 ++++ api/search.js | 5 +++++ scripts/check-data.js | 5 +++++ scripts/enrich-data.js | 5 +++++ scripts/generate-embeddings.js | 5 +++++ scripts/pipeline-to-cosmos.js | 5 +++++ scripts/search-developers.js | 5 +++++ scripts/setup-vector-search.js | 5 +++++ scripts/upload-cosmosdb.js | 5 +++++ 12 files changed, 57 insertions(+) diff --git a/.gitignore b/.gitignore index 7de1c50..4c49cec 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ data/fetch-progress.json data/geocode-cache.json data/pipeline.log *.log +dist/ diff --git a/api/developer.js b/api/developer.js index 4c81f63..433421c 100644 --- a/api/developer.js +++ b/api/developer.js @@ -7,6 +7,12 @@ import { CosmosClient } from '@azure/cosmos'; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT; const COSMOS_KEY = process.env.COSMOS_KEY; + +// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator +if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; +} + const DATABASE = 'devglobe'; const CONTAINER = 'developers'; diff --git a/api/developers.js b/api/developers.js index 8aa8dd8..448cf34 100644 --- a/api/developers.js +++ b/api/developers.js @@ -8,6 +8,12 @@ import { CosmosClient } from '@azure/cosmos'; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT; const COSMOS_KEY = process.env.COSMOS_KEY; + +// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator +if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; +} + const DATABASE = 'devglobe'; const CONTAINER = 'developers'; diff --git a/api/lib/nominate.js b/api/lib/nominate.js index 8bca3fe..321ea6f 100644 --- a/api/lib/nominate.js +++ b/api/lib/nominate.js @@ -42,6 +42,10 @@ async function verifyGitHubUser(username) { } async function getClient() { + // Cosmos DB emulator uses a self-signed cert — bypass only for local emulator + if (process.env.COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(process.env.COSMOS_ENDPOINT)) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + } return new CosmosClient({ endpoint: process.env.COSMOS_ENDPOINT, key: process.env.COSMOS_KEY, diff --git a/api/search.js b/api/search.js index d4f297c..afe0f72 100644 --- a/api/search.js +++ b/api/search.js @@ -13,6 +13,11 @@ const OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT; const OPENAI_KEY = process.env.AZURE_OPENAI_KEY; const EMBEDDING_DEPLOYMENT = process.env.EMBEDDING_DEPLOYMENT || 'text-embedding-3-small'; +// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator +if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; +} + const DATABASE = 'devglobe'; const CONTAINER = 'developers'; diff --git a/scripts/check-data.js b/scripts/check-data.js index 20032c9..f78082b 100644 --- a/scripts/check-data.js +++ b/scripts/check-data.js @@ -2,6 +2,11 @@ import { CosmosClient } from '@azure/cosmos'; import dotenv from 'dotenv'; dotenv.config(); +// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator +if (process.env.COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(process.env.COSMOS_ENDPOINT)) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; +} + const client = new CosmosClient({ endpoint: process.env.COSMOS_ENDPOINT, key: process.env.COSMOS_KEY }); const container = client.database('devglobe').container('developers'); diff --git a/scripts/enrich-data.js b/scripts/enrich-data.js index 7d615c8..e357bf2 100644 --- a/scripts/enrich-data.js +++ b/scripts/enrich-data.js @@ -14,6 +14,11 @@ const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT; const COSMOS_KEY = process.env.COSMOS_KEY; +// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator +if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; +} + const client = new CosmosClient({ endpoint: COSMOS_ENDPOINT, key: COSMOS_KEY }); const container = client.database('devglobe').container('developers'); diff --git a/scripts/generate-embeddings.js b/scripts/generate-embeddings.js index 784cf25..49faaeb 100644 --- a/scripts/generate-embeddings.js +++ b/scripts/generate-embeddings.js @@ -15,6 +15,11 @@ const OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT; // e.g., https://your const OPENAI_KEY = process.env.AZURE_OPENAI_KEY; const EMBEDDING_DEPLOYMENT = process.env.EMBEDDING_DEPLOYMENT || 'text-embedding-3-small'; +// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator +if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; +} + const DATABASE_NAME = 'devglobe'; const CONTAINER_NAME = 'developers'; const BATCH_SIZE = 100; // OpenAI supports up to 2048 inputs per request diff --git a/scripts/pipeline-to-cosmos.js b/scripts/pipeline-to-cosmos.js index 36cd861..efdc225 100644 --- a/scripts/pipeline-to-cosmos.js +++ b/scripts/pipeline-to-cosmos.js @@ -15,6 +15,11 @@ const GEOCODE_API_KEY = process.env.GEOCODE_API_KEY || ''; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT || 'https://devglobe-cosmos.documents.azure.com:443/'; const COSMOS_KEY = process.env.COSMOS_KEY; +// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator +if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; +} + const LOG_FILE = 'data/pipeline.log'; mkdirSync('data', { recursive: true }); diff --git a/scripts/search-developers.js b/scripts/search-developers.js index b7ab3fe..fd18f86 100644 --- a/scripts/search-developers.js +++ b/scripts/search-developers.js @@ -20,6 +20,11 @@ const OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT; const OPENAI_KEY = process.env.AZURE_OPENAI_KEY; const EMBEDDING_DEPLOYMENT = process.env.EMBEDDING_DEPLOYMENT || 'text-embedding-3-small'; +// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator +if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; +} + const DATABASE_NAME = 'devglobe'; const CONTAINER_NAME = 'developers'; diff --git a/scripts/setup-vector-search.js b/scripts/setup-vector-search.js index 210eaa8..25454c4 100644 --- a/scripts/setup-vector-search.js +++ b/scripts/setup-vector-search.js @@ -16,6 +16,11 @@ const COSMOS_KEY = process.env.COSMOS_KEY; const DATABASE_NAME = 'devglobe'; const CONTAINER_NAME = 'developers'; +// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator +if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; +} + async function main() { const client = new CosmosClient({ endpoint: COSMOS_ENDPOINT, key: COSMOS_KEY }); const database = client.database(DATABASE_NAME); diff --git a/scripts/upload-cosmosdb.js b/scripts/upload-cosmosdb.js index e42205d..45130d7 100644 --- a/scripts/upload-cosmosdb.js +++ b/scripts/upload-cosmosdb.js @@ -12,6 +12,11 @@ import { readFileSync, existsSync } from 'fs'; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT || 'https://devglobe-cosmos.documents.azure.com:443/'; const COSMOS_KEY = process.env.COSMOS_KEY; +// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator +if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; +} + if (!COSMOS_KEY) { console.error('Error: COSMOS_KEY environment variable is required'); console.error('Add it to your .env file'); From e38a7a1a3f60cfd7278cd30bc1e2a63b601a0604 Mon Sep 17 00:00:00 2001 From: babu bahir Date: Fri, 7 Aug 2026 16:03:44 +0530 Subject: [PATCH 3/8] removed process.env.NODE_TLS_REJECT_UNAUTHORIZED --- api/developer.js | 5 ----- api/developers.js | 5 ----- api/lib/nominate.js | 4 ---- api/search.js | 5 ----- scripts/check-data.js | 5 ----- scripts/enrich-data.js | 5 ----- scripts/generate-embeddings.js | 5 ----- scripts/pipeline-to-cosmos.js | 5 ----- scripts/review-nominations.js | 5 ----- scripts/search-developers.js | 5 ----- scripts/setup-vector-search.js | 5 ----- scripts/upload-cosmosdb.js | 5 ----- server.js | 6 ------ 13 files changed, 65 deletions(-) diff --git a/api/developer.js b/api/developer.js index 433421c..3296eb8 100644 --- a/api/developer.js +++ b/api/developer.js @@ -8,11 +8,6 @@ import { CosmosClient } from '@azure/cosmos'; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT; const COSMOS_KEY = process.env.COSMOS_KEY; -// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator -if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -} - const DATABASE = 'devglobe'; const CONTAINER = 'developers'; diff --git a/api/developers.js b/api/developers.js index 448cf34..0b2d013 100644 --- a/api/developers.js +++ b/api/developers.js @@ -9,11 +9,6 @@ import { CosmosClient } from '@azure/cosmos'; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT; const COSMOS_KEY = process.env.COSMOS_KEY; -// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator -if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -} - const DATABASE = 'devglobe'; const CONTAINER = 'developers'; diff --git a/api/lib/nominate.js b/api/lib/nominate.js index 321ea6f..8bca3fe 100644 --- a/api/lib/nominate.js +++ b/api/lib/nominate.js @@ -42,10 +42,6 @@ async function verifyGitHubUser(username) { } async function getClient() { - // Cosmos DB emulator uses a self-signed cert — bypass only for local emulator - if (process.env.COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(process.env.COSMOS_ENDPOINT)) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - } return new CosmosClient({ endpoint: process.env.COSMOS_ENDPOINT, key: process.env.COSMOS_KEY, diff --git a/api/search.js b/api/search.js index afe0f72..d4f297c 100644 --- a/api/search.js +++ b/api/search.js @@ -13,11 +13,6 @@ const OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT; const OPENAI_KEY = process.env.AZURE_OPENAI_KEY; const EMBEDDING_DEPLOYMENT = process.env.EMBEDDING_DEPLOYMENT || 'text-embedding-3-small'; -// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator -if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -} - const DATABASE = 'devglobe'; const CONTAINER = 'developers'; diff --git a/scripts/check-data.js b/scripts/check-data.js index f78082b..20032c9 100644 --- a/scripts/check-data.js +++ b/scripts/check-data.js @@ -2,11 +2,6 @@ import { CosmosClient } from '@azure/cosmos'; import dotenv from 'dotenv'; dotenv.config(); -// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator -if (process.env.COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(process.env.COSMOS_ENDPOINT)) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -} - const client = new CosmosClient({ endpoint: process.env.COSMOS_ENDPOINT, key: process.env.COSMOS_KEY }); const container = client.database('devglobe').container('developers'); diff --git a/scripts/enrich-data.js b/scripts/enrich-data.js index e357bf2..7d615c8 100644 --- a/scripts/enrich-data.js +++ b/scripts/enrich-data.js @@ -14,11 +14,6 @@ const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT; const COSMOS_KEY = process.env.COSMOS_KEY; -// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator -if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -} - const client = new CosmosClient({ endpoint: COSMOS_ENDPOINT, key: COSMOS_KEY }); const container = client.database('devglobe').container('developers'); diff --git a/scripts/generate-embeddings.js b/scripts/generate-embeddings.js index 49faaeb..784cf25 100644 --- a/scripts/generate-embeddings.js +++ b/scripts/generate-embeddings.js @@ -15,11 +15,6 @@ const OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT; // e.g., https://your const OPENAI_KEY = process.env.AZURE_OPENAI_KEY; const EMBEDDING_DEPLOYMENT = process.env.EMBEDDING_DEPLOYMENT || 'text-embedding-3-small'; -// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator -if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -} - const DATABASE_NAME = 'devglobe'; const CONTAINER_NAME = 'developers'; const BATCH_SIZE = 100; // OpenAI supports up to 2048 inputs per request diff --git a/scripts/pipeline-to-cosmos.js b/scripts/pipeline-to-cosmos.js index efdc225..36cd861 100644 --- a/scripts/pipeline-to-cosmos.js +++ b/scripts/pipeline-to-cosmos.js @@ -15,11 +15,6 @@ const GEOCODE_API_KEY = process.env.GEOCODE_API_KEY || ''; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT || 'https://devglobe-cosmos.documents.azure.com:443/'; const COSMOS_KEY = process.env.COSMOS_KEY; -// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator -if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -} - const LOG_FILE = 'data/pipeline.log'; mkdirSync('data', { recursive: true }); diff --git a/scripts/review-nominations.js b/scripts/review-nominations.js index 9a1ca8c..c96c5dd 100644 --- a/scripts/review-nominations.js +++ b/scripts/review-nominations.js @@ -19,11 +19,6 @@ const DATABASE = 'devglobe'; const DEVELOPERS_CONTAINER = 'developers'; const NOMINATIONS_CONTAINER = 'nominations'; -// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator -if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -} - if (!COSMOS_ENDPOINT || !COSMOS_KEY) { console.error('Error: COSMOS_ENDPOINT and COSMOS_KEY are required in .env'); process.exit(1); diff --git a/scripts/search-developers.js b/scripts/search-developers.js index fd18f86..b7ab3fe 100644 --- a/scripts/search-developers.js +++ b/scripts/search-developers.js @@ -20,11 +20,6 @@ const OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT; const OPENAI_KEY = process.env.AZURE_OPENAI_KEY; const EMBEDDING_DEPLOYMENT = process.env.EMBEDDING_DEPLOYMENT || 'text-embedding-3-small'; -// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator -if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -} - const DATABASE_NAME = 'devglobe'; const CONTAINER_NAME = 'developers'; diff --git a/scripts/setup-vector-search.js b/scripts/setup-vector-search.js index 25454c4..210eaa8 100644 --- a/scripts/setup-vector-search.js +++ b/scripts/setup-vector-search.js @@ -16,11 +16,6 @@ const COSMOS_KEY = process.env.COSMOS_KEY; const DATABASE_NAME = 'devglobe'; const CONTAINER_NAME = 'developers'; -// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator -if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -} - async function main() { const client = new CosmosClient({ endpoint: COSMOS_ENDPOINT, key: COSMOS_KEY }); const database = client.database(DATABASE_NAME); diff --git a/scripts/upload-cosmosdb.js b/scripts/upload-cosmosdb.js index 45130d7..e42205d 100644 --- a/scripts/upload-cosmosdb.js +++ b/scripts/upload-cosmosdb.js @@ -12,11 +12,6 @@ import { readFileSync, existsSync } from 'fs'; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT || 'https://devglobe-cosmos.documents.azure.com:443/'; const COSMOS_KEY = process.env.COSMOS_KEY; -// Cosmos DB emulator uses a self-signed cert — bypass only for local emulator -if (COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(COSMOS_ENDPOINT)) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -} - if (!COSMOS_KEY) { console.error('Error: COSMOS_KEY environment variable is required'); console.error('Add it to your .env file'); diff --git a/server.js b/server.js index 8e3f24d..5f8fb57 100644 --- a/server.js +++ b/server.js @@ -10,12 +10,6 @@ import { submitNomination } from './api/lib/nominate.js'; dotenv.config(); -// The Cosmos DB emulator uses a self-signed cert; only bypass TLS verification -// when talking to the local emulator (never for real Azure endpoints). -if (process.env.COSMOS_ENDPOINT && /localhost|127\.0\.0\.1/.test(process.env.COSMOS_ENDPOINT)) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -} - const __dirname = dirname(fileURLToPath(import.meta.url)); const app = express(); const PORT = process.env.PORT || 3000; From 4ec4127c0901c80183218f89d6d357600ec133ea Mon Sep 17 00:00:00 2001 From: babu bahir Date: Fri, 7 Aug 2026 16:11:44 +0530 Subject: [PATCH 4/8] removed white space --- api/developer.js | 1 - api/developers.js | 1 - 2 files changed, 2 deletions(-) diff --git a/api/developer.js b/api/developer.js index 3296eb8..4c81f63 100644 --- a/api/developer.js +++ b/api/developer.js @@ -7,7 +7,6 @@ import { CosmosClient } from '@azure/cosmos'; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT; const COSMOS_KEY = process.env.COSMOS_KEY; - const DATABASE = 'devglobe'; const CONTAINER = 'developers'; diff --git a/api/developers.js b/api/developers.js index 0b2d013..8aa8dd8 100644 --- a/api/developers.js +++ b/api/developers.js @@ -8,7 +8,6 @@ import { CosmosClient } from '@azure/cosmos'; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT; const COSMOS_KEY = process.env.COSMOS_KEY; - const DATABASE = 'devglobe'; const CONTAINER = 'developers'; From fda43d619037a540a33c17e9bd9c2b74d0513578 Mon Sep 17 00:00:00 2001 From: babu bahir Date: Fri, 7 Aug 2026 16:22:40 +0530 Subject: [PATCH 5/8] refactored --- package.json | 2 - src/components/AddMeModal.jsx | 37 +++---- src/components/AddMeModal.module.css | 143 ++++++++++++++++++++++++++ styles/main.css | 145 --------------------------- 4 files changed, 162 insertions(+), 165 deletions(-) create mode 100644 src/components/AddMeModal.module.css diff --git a/package.json b/package.json index e40f96e..c5a4017 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "description": "Interactive 3D globe visualization of top GitHub developers", "scripts": { "dev": "vite", - "dev:all": "concurrently -n api,web -c blue,green \"npm run server\" \"npm run dev\"", "build": "vite build", "preview": "vite preview", "server": "node server.js", @@ -29,7 +28,6 @@ "vite": "^8.1.5" }, "devDependencies": { - "concurrently": "^10.0.4", "serve": "^14.2.0" }, "type": "module" diff --git a/src/components/AddMeModal.jsx b/src/components/AddMeModal.jsx index c8c4455..f87402b 100644 --- a/src/components/AddMeModal.jsx +++ b/src/components/AddMeModal.jsx @@ -1,4 +1,5 @@ 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."; @@ -51,31 +52,31 @@ export default function AddMeModal({ onClose }) { }; return ( -
-
e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Add me to DevGlobe"> - +
+
e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Add me to DevGlobe"> + {status === 'success' ? ( -
-
🎉
-

You're on the list!

-

{SUCCESS_MESSAGE}

+
+
🎉
+

You're on the list!

+

{SUCCESS_MESSAGE}

) : ( <> -

Add me to DevGlobe

-

+

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/lib/nominate.js b/lib/nominate.js index eccc533..de576b7 100644 --- a/lib/nominate.js +++ b/lib/nominate.js @@ -21,9 +21,9 @@ const GITHUB_API = 'https://api.github.com'; // GitHub usernames: alphanumeric + single hyphens, 1-39 chars, cannot end with hyphen const USERNAME_RE = /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i; -function normalizeUsername(raw) { +export function normalizeUsername(raw) { if (!raw) return ''; - return String(raw).trim().replace(/^@/, ''); + return String(raw).trim().replace(/^@/, '').toLowerCase(); } async function verifyGitHubUser(username) { @@ -93,7 +93,7 @@ export async function submitNomination({ username, location }) { // Already in the main dataset? const { resources: existing } = await developersContainer.items .query({ - query: 'SELECT VALUE c.login FROM c WHERE c.login = @login', + query: 'SELECT VALUE c.login FROM c WHERE LOWER(c.login) = @login', parameters: [{ name: '@login', value: cleanUsername }], }) .fetchAll(); @@ -104,7 +104,7 @@ export async function submitNomination({ username, location }) { // Duplicate pending nomination? const { resources: dupes } = await container.items .query({ - query: 'SELECT VALUE c FROM c WHERE c.username = @username AND c.status = @status', + query: 'SELECT VALUE c FROM c WHERE LOWER(c.username) = @username AND c.status = @status', parameters: [ { name: '@username', value: cleanUsername }, { name: '@status', value: 'pending' }, diff --git a/scripts/review-nominations.js b/scripts/review-nominations.js index c96c5dd..440ebdc 100644 --- a/scripts/review-nominations.js +++ b/scripts/review-nominations.js @@ -12,6 +12,7 @@ */ import 'dotenv/config'; import { CosmosClient } from '@azure/cosmos'; +import { normalizeUsername } from '../lib/nominate.js'; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT; const COSMOS_KEY = process.env.COSMOS_KEY; @@ -60,8 +61,8 @@ async function listNominations(container) { async function getNomination(container, username) { const { resources } = await container.items .query({ - query: 'SELECT * FROM c WHERE c.username = @username', - parameters: [{ name: '@username', value: username }], + query: 'SELECT * FROM c WHERE LOWER(c.username) = @username', + parameters: [{ name: '@username', value: username.toLowerCase() }], }) .fetchAll(); return resources[0] || null; @@ -172,12 +173,13 @@ async function approve(nominations, developers, username) { console.log(`Approving "${username}"...`); const dev = await fetchGitHubUser(username); - const coords = await geocode(dev.location); + const location = (nomination.location || '').trim() || dev.location; + const coords = await geocode(location); const doc = { id: dev.login, ...dev, - location: dev.location || 'Unknown', + location: location || 'Unknown', ...(coords ? { lat: coords.lat, lng: coords.lng } : {}), }; @@ -196,7 +198,8 @@ async function reject(nominations, username) { } async function main() { - const [cmd, username] = process.argv.slice(2); + const [cmd, rawUsername] = process.argv.slice(2); + const username = normalizeUsername(rawUsername); const { nominations, developers } = await ensureContainers(); switch (cmd) {