From c1767142bd5f6a487dd798b60aa1cfd9761e4e74 Mon Sep 17 00:00:00 2001 From: Miran Date: Fri, 24 Oct 2025 11:03:46 +0530 Subject: [PATCH 1/3] feat: add username check on home page --- components/slides/HomePage.js | 41 ++++++++++++++++++++++----- package-lock.json | 51 +++++++--------------------------- pages/api/validate-username.js | 39 ++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 48 deletions(-) create mode 100644 pages/api/validate-username.js diff --git a/components/slides/HomePage.js b/components/slides/HomePage.js index f1bfed2..908a05f 100644 --- a/components/slides/HomePage.js +++ b/components/slides/HomePage.js @@ -15,15 +15,39 @@ import { useObserver } from "mobx-react"; export default function HomePage() { const [isVisible, setIsVisible] = useState(false); const [alertVisible, setAlertVisible] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); const gprmStore = useGPRMStore(); const [input, setInput] = useState(gprmStore.data.username); - function onNext() { - if (input != "" && input.replace(/ /g, "") != "") { + async function onNext(e) { + // Prevent the browser's default form submit which causes a full page reload + if (e && typeof e.preventDefault === "function") e.preventDefault(); + + if (input === "" || input.replace(/ /g, "") === "") { + invalidUsername("Enter a Valid GitHub Username!"); + return; + } + + try { + const response = await fetch( + `/api/validate-username?username=${encodeURIComponent(input)}` + ); + const data = await response.json(); + + if (!data.exists && !data.rateLimited && !data.error) { + invalidUsername("GitHub username does not exist!"); + return; + } + + // If username exists or we hit rate limit/error, proceed as normal + gprmStore.data.username = input; + setIsVisible(true); + topFunction(); + } catch (error) { + console.error("Error validating username:", error); + // On error, proceed as before gprmStore.data.username = input; setIsVisible(true); topFunction(); - } else { - invalidUsername(); } } // When the user clicks on the button, scroll to the top of the document @@ -32,8 +56,9 @@ export default function HomePage() { document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera } - function invalidUsername() { + function invalidUsername(message) { if (alertVisible !== true) { + setErrorMessage(message); setAlertVisible(true); setTimeout(() => { setAlertVisible(false); @@ -63,7 +88,7 @@ export default function HomePage() { className="border-b-2 border-green-200 bg-transparent w-full sm:w-11/12 md:w-10/12 lg:w-8/12 text-xl sm:text-3xl md:text-xl lg:text-2xl 2xl:text-3xl outline-none focus:border-green-300 focus:border-b-4 inline" placeholder="Enter Your GitHub Username" /> - @@ -80,7 +105,9 @@ export default function HomePage() { {alertVisible && ( - + )} diff --git a/package-lock.json b/package-lock.json index 9555e3f..5522357 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4688,20 +4688,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", - "dev": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", @@ -5229,14 +5215,12 @@ "@firebase/auth-interop-types": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz", - "integrity": "sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==", - "requires": {} + "integrity": "sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==" }, "@firebase/auth-types": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.11.0.tgz", - "integrity": "sha512-q7Bt6cx+ySj9elQHTsKulwk3+qDezhzRBFC9zlQ1BjgMueUOnGMcvqmU0zuKlQ4RhLSH7MNAdBV2znVaoN3Vxw==", - "requires": {} + "integrity": "sha512-q7Bt6cx+ySj9elQHTsKulwk3+qDezhzRBFC9zlQ1BjgMueUOnGMcvqmU0zuKlQ4RhLSH7MNAdBV2znVaoN3Vxw==" }, "@firebase/component": { "version": "0.5.10", @@ -5355,8 +5339,7 @@ "@firebase/firestore-types": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.0.tgz", - "integrity": "sha512-I6c2m1zUhZ5SH0cWPmINabDyH5w0PPFHk2UHsjBpKdZllzJZ2TwTkXbDtpHUZNmnc/zAa0WNMNMvcvbb/xJLKA==", - "requires": {} + "integrity": "sha512-I6c2m1zUhZ5SH0cWPmINabDyH5w0PPFHk2UHsjBpKdZllzJZ2TwTkXbDtpHUZNmnc/zAa0WNMNMvcvbb/xJLKA==" }, "@firebase/functions": { "version": "0.7.7", @@ -5633,8 +5616,7 @@ "@firebase/storage-types": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.6.0.tgz", - "integrity": "sha512-1LpWhcCb1ftpkP/akhzjzeFxgVefs6eMD2QeKiJJUGH1qOiows2w5o0sKCUSQrvrRQS1lz3SFGvNR1Ck/gqxeA==", - "requires": {} + "integrity": "sha512-1LpWhcCb1ftpkP/akhzjzeFxgVefs6eMD2QeKiJJUGH1qOiows2w5o0sKCUSQrvrRQS1lz3SFGvNR1Ck/gqxeA==" }, "@firebase/util": { "version": "1.4.3", @@ -5680,8 +5662,7 @@ "@heroicons/react": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-1.0.5.tgz", - "integrity": "sha512-UDMyLM2KavIu2vlWfMspapw9yii7aoLwzI2Hudx4fyoPwfKfxU8r3cL8dEBXOjcLG0/oOONZzbT14M1HoNtEcg==", - "requires": {} + "integrity": "sha512-UDMyLM2KavIu2vlWfMspapw9yii7aoLwzI2Hudx4fyoPwfKfxU8r3cL8dEBXOjcLG0/oOONZzbT14M1HoNtEcg==" }, "@humanwhocodes/config-array": { "version": "0.9.2", @@ -5962,8 +5943,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "acorn-node": { "version": "1.8.2", @@ -6799,8 +6779,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz", "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", - "dev": true, - "requires": {} + "dev": true }, "eslint-scope": { "version": "7.1.0", @@ -7623,8 +7602,7 @@ "mobx-react-lite": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.4.0.tgz", - "integrity": "sha512-bRuZp3C0itgLKHu/VNxi66DN/XVkQG7xtoBVWxpvC5FhAqbOCP21+nPhULjnzEqd7xBMybp6KwytdUpZKEgpIQ==", - "requires": {} + "integrity": "sha512-bRuZp3C0itgLKHu/VNxi66DN/XVkQG7xtoBVWxpvC5FhAqbOCP21+nPhULjnzEqd7xBMybp6KwytdUpZKEgpIQ==" }, "ms": { "version": "2.1.2", @@ -8290,8 +8268,7 @@ "styled-jsx": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.0.tgz", - "integrity": "sha512-qUqsWoBquEdERe10EW8vLp3jT25s/ssG1/qX5gZ4wu15OZpmSMFI2v+fWlRhLfykA5rFtlJ1ME8A8pm/peV4WA==", - "requires": {} + "integrity": "sha512-qUqsWoBquEdERe10EW8vLp3jT25s/ssG1/qX5gZ4wu15OZpmSMFI2v+fWlRhLfykA5rFtlJ1ME8A8pm/peV4WA==" }, "supports-preserve-symlinks-flag": { "version": "1.0.0", @@ -8453,13 +8430,6 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, - "typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", - "dev": true, - "peer": true - }, "uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", @@ -8608,8 +8578,7 @@ "ws": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz", - "integrity": "sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==", - "requires": {} + "integrity": "sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==" }, "xtend": { "version": "4.0.2", diff --git a/pages/api/validate-username.js b/pages/api/validate-username.js new file mode 100644 index 0000000..bf0ca10 --- /dev/null +++ b/pages/api/validate-username.js @@ -0,0 +1,39 @@ +export default async function handler(req, res) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const { username } = req.query; + if (!username) { + return res.status(400).json({ error: 'Username is required' }); + } + + const githubToken = process.env.GITHUB_ACCESS_TOKEN; + const headers = { + 'Accept': 'application/vnd.github.v3+json', + ...(githubToken && { 'Authorization': `token ${githubToken}` }) + }; + + try { + const response = await fetch(`https://api.github.com/users/${username}`, { headers }); + + if (response.status === 403) { + // Rate limit hit, return success to allow fallback behavior + return res.status(200).json({ exists: true, rateLimited: true }); + } + + if (response.status === 404) { + return res.status(200).json({ exists: false }); + } + + if (!response.ok) { + throw new Error(`GitHub API responded with ${response.status}`); + } + + return res.status(200).json({ exists: true }); + } catch (error) { + console.error('Error validating username:', error); + // On error, return success to allow fallback behavior + return res.status(200).json({ exists: true, error: true }); + } +} \ No newline at end of file From 59fb483a448e010c060691e0681898899b8447b7 Mon Sep 17 00:00:00 2001 From: Miran Date: Sat, 25 Oct 2025 22:32:17 +0530 Subject: [PATCH 2/3] add timeout for validate username api call --- components/slides/HomePage.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/slides/HomePage.js b/components/slides/HomePage.js index 908a05f..5b67863 100644 --- a/components/slides/HomePage.js +++ b/components/slides/HomePage.js @@ -28,9 +28,15 @@ export default function HomePage() { } try { + // Create AbortController for timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout + const response = await fetch( - `/api/validate-username?username=${encodeURIComponent(input)}` + `/api/validate-username?username=${encodeURIComponent(input)}`, + { signal: controller.signal } ); + clearTimeout(timeoutId); const data = await response.json(); if (!data.exists && !data.rateLimited && !data.error) { From 2924d3d304fa556d430d9f3cf8de2dee0ac58ae6 Mon Sep 17 00:00:00 2001 From: Miran Date: Sat, 25 Oct 2025 23:07:38 +0530 Subject: [PATCH 3/3] remove api and github PAT logic --- components/slides/HomePage.js | 38 +++++++++------------------- hooks/useGitHubUsernameValidator.js | 37 +++++++++++++++++++++++++++ pages/api/validate-username.js | 39 ----------------------------- 3 files changed, 49 insertions(+), 65 deletions(-) create mode 100644 hooks/useGitHubUsernameValidator.js delete mode 100644 pages/api/validate-username.js diff --git a/components/slides/HomePage.js b/components/slides/HomePage.js index 5b67863..c6bc893 100644 --- a/components/slides/HomePage.js +++ b/components/slides/HomePage.js @@ -2,6 +2,7 @@ import React, { useState } from "react"; import AnimatedText from "../elements/AnimatedText"; import Features from "../home-components/Features"; import GitHubAvailability from "../home-components/GitHubAvailability"; +import { useGitHubUsernameValidator } from "../../hooks/useGitHubUsernameValidator"; import ToastError from "../elements/toaster/ToastError"; import AboutMe from "./AboutMe"; import FAQ from "../home-components/FAQ"; @@ -18,6 +19,8 @@ export default function HomePage() { const [errorMessage, setErrorMessage] = useState(""); const gprmStore = useGPRMStore(); const [input, setInput] = useState(gprmStore.data.username); + const validateUsername = useGitHubUsernameValidator(); + async function onNext(e) { // Prevent the browser's default form submit which causes a full page reload if (e && typeof e.preventDefault === "function") e.preventDefault(); @@ -27,34 +30,17 @@ export default function HomePage() { return; } - try { - // Create AbortController for timeout - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout - - const response = await fetch( - `/api/validate-username?username=${encodeURIComponent(input)}`, - { signal: controller.signal } - ); - clearTimeout(timeoutId); - const data = await response.json(); - - if (!data.exists && !data.rateLimited && !data.error) { - invalidUsername("GitHub username does not exist!"); - return; - } + const result = await validateUsername(input); - // If username exists or we hit rate limit/error, proceed as normal - gprmStore.data.username = input; - setIsVisible(true); - topFunction(); - } catch (error) { - console.error("Error validating username:", error); - // On error, proceed as before - gprmStore.data.username = input; - setIsVisible(true); - topFunction(); + if (!result.exists && !result.rateLimited && !result.error) { + invalidUsername("GitHub username does not exist!"); + return; } + + // If username exists or we hit rate limit/error, proceed as normal + gprmStore.data.username = input; + setIsVisible(true); + topFunction(); } // When the user clicks on the button, scroll to the top of the document function topFunction() { diff --git a/hooks/useGitHubUsernameValidator.js b/hooks/useGitHubUsernameValidator.js new file mode 100644 index 0000000..fd75fb1 --- /dev/null +++ b/hooks/useGitHubUsernameValidator.js @@ -0,0 +1,37 @@ +import { useCallback } from "react"; + +export function useGitHubUsernameValidator() { + const validateUsername = useCallback(async (username) => { + try { + // Create AbortController for timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout + + const response = await fetch(`https://api.github.com/users/${username}`, { + signal: controller.signal, + }); + clearTimeout(timeoutId); + + if (response.status === 403) { + // Rate limit hit, allow continuation + return { exists: true, rateLimited: true }; + } + + if (response.status === 404) { + return { exists: false }; + } + + if (!response.ok) { + throw new Error(`GitHub API responded with ${response.status}`); + } + + return { exists: true }; + } catch (error) { + console.error("Error validating username:", error); + // On error or timeout, return success to allow fallback behavior + return { exists: true, error: true }; + } + }, []); + + return validateUsername; +} diff --git a/pages/api/validate-username.js b/pages/api/validate-username.js deleted file mode 100644 index bf0ca10..0000000 --- a/pages/api/validate-username.js +++ /dev/null @@ -1,39 +0,0 @@ -export default async function handler(req, res) { - if (req.method !== 'GET') { - return res.status(405).json({ error: 'Method not allowed' }); - } - - const { username } = req.query; - if (!username) { - return res.status(400).json({ error: 'Username is required' }); - } - - const githubToken = process.env.GITHUB_ACCESS_TOKEN; - const headers = { - 'Accept': 'application/vnd.github.v3+json', - ...(githubToken && { 'Authorization': `token ${githubToken}` }) - }; - - try { - const response = await fetch(`https://api.github.com/users/${username}`, { headers }); - - if (response.status === 403) { - // Rate limit hit, return success to allow fallback behavior - return res.status(200).json({ exists: true, rateLimited: true }); - } - - if (response.status === 404) { - return res.status(200).json({ exists: false }); - } - - if (!response.ok) { - throw new Error(`GitHub API responded with ${response.status}`); - } - - return res.status(200).json({ exists: true }); - } catch (error) { - console.error('Error validating username:', error); - // On error, return success to allow fallback behavior - return res.status(200).json({ exists: true, error: true }); - } -} \ No newline at end of file