diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c211844c..b61d616f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,130 +1,130 @@ - # 🌟 Contributing to GitHub Tracker +# 🌟 Contributing to GitHub Tracker - Thank you for showing interest in **GitHub Tracker**! πŸš€ - Whether you're here to fix a bug, propose an enhancement, or add a new feature, we’re thrilled to welcome you aboard. Let’s build something awesome together! +Thank you for showing interest in **GitHub Tracker**! πŸš€ +Whether you're here to fix a bug, propose an enhancement, or add a new feature, we’re thrilled to welcome you aboard. Let’s build something awesome together! -
+
- ## πŸ§‘β€βš–οΈ Code of Conduct +## πŸ§‘β€βš–οΈ Code of Conduct - Please make sure to read and adhere to our [Code of Conduct](https://github.com/GitMetricsLab/github_tracker/CODE_OF_CONDUCT.md) before contributing. We aim to foster a respectful and inclusive environment for everyone. +Please make sure to read and adhere to our [Code of Conduct](https://github.com/GitMetricsLab/github_tracker/CODE_OF_CONDUCT.md) before contributing. We aim to foster a respectful and inclusive environment for everyone. -
+
- ## πŸ›  Project Structure +## πŸ›  Project Structure - ```bash - github_tracker/ - β”œβ”€β”€ backend/ # Node.js + Express backend - β”‚ β”œβ”€β”€ routes/ # API routes - β”‚ β”œβ”€β”€ controllers/ # Logic handlers - β”‚ └── index.js # Entry point for server - β”‚ - β”œβ”€β”€ frontend/ # React + Vite frontend - β”‚ β”œβ”€β”€ components/ # Reusable UI components - β”‚ β”œβ”€β”€ pages/ # Main pages/routes - β”‚ └── main.jsx # Root file - β”‚ - β”œβ”€β”€ public/ # Static assets like images - β”‚ - β”œβ”€β”€ .gitignore - β”œβ”€β”€ README.md - β”œβ”€β”€ package.json - β”œβ”€β”€ tailwind.config.js - └── CONTRIBUTING.md - ``` +```bash +github_tracker/ +β”œβ”€β”€ backend/ # Node.js + Express backend +β”‚ β”œβ”€β”€ routes/ # API routes +β”‚ β”œβ”€β”€ controllers/ # Logic handlers +β”‚ └── index.js # Entry point for server +β”‚ +β”œβ”€β”€ frontend/ # React + Vite frontend +β”‚ β”œβ”€β”€ components/ # Reusable UI components +β”‚ β”œβ”€β”€ pages/ # Main pages/routes +β”‚ └── main.jsx # Root file +β”‚ +β”œβ”€β”€ public/ # Static assets like images +β”‚ +β”œβ”€β”€ .gitignore +β”œβ”€β”€ README.md +β”œβ”€β”€ package.json +β”œβ”€β”€ tailwind.config.js +└── CONTRIBUTING.md +``` - --- +--- - ## 🀝 How to Contribute +## 🀝 How to Contribute - ### 🧭 First-Time Contribution Steps +### 🧭 First-Time Contribution Steps - 1. **Fork the Repository** 🍴 - Click "Fork" to create your own copy under your GitHub account. +1. **Fork the Repository** 🍴 + Click "Fork" to create your own copy under your GitHub account. - 2. **Clone Your Fork** πŸ“₯ - ```bash - git clone https://github.com//github_tracker.git - ``` +2. **Clone Your Fork** πŸ“₯ + ```bash + git clone https://github.com//github_tracker.git + ``` - 3. **Navigate to the Project Folder** πŸ“ - ```bash - cd github_tracker - ``` +3. **Navigate to the Project Folder** πŸ“ + ```bash + cd github_tracker + ``` - 4. **Create a New Branch** 🌿 - ```bash - git checkout -b your-feature-name - ``` +4. **Create a New Branch** 🌿 + ```bash + git checkout -b your-feature-name + ``` - 5. **Make Your Changes** ✍ - After modifying files, stage and commit: +5. **Make Your Changes** ✍ + After modifying files, stage and commit: - ```bash - git add . - git commit -m "✨ Added [feature/fix]: your message" - ``` + ```bash + git add . + git commit -m "✨ Added [feature/fix]: your message" + ``` - 6. **Push Your Branch to GitHub** πŸš€ - ```bash - git push origin your-feature-name - ``` +6. **Push Your Branch to GitHub** πŸš€ + ```bash + git push origin your-feature-name + ``` - 7. **Open a Pull Request** πŸ” - Go to the original repo and click **Compare & pull request**. - - --- +7. **Open a Pull Request** πŸ” + Go to the original repo and click **Compare & pull request**. + +--- - ## 🚦 Pull Request Guidelines +## 🚦 Pull Request Guidelines - ### **Split Big Changes into Multiple Commits** - - When making large or complex changes, break them into smaller, logical commits. - - Each commit should represent a single purpose or unit of change (e.g. refactoring, adding a feature, fixing a bug). - --- - - βœ… Ensure your code builds and runs without errors. - - πŸ§ͺ Include tests where applicable. - - πŸ’¬ Add comments if the logic is non-trivial. - - πŸ“Έ Attach screenshots for UI-related changes. - - πŸ”– Use meaningful commit messages and titles. +### **Split Big Changes into Multiple Commits** +- When making large or complex changes, break them into smaller, logical commits. +- Each commit should represent a single purpose or unit of change (e.g. refactoring, adding a feature, fixing a bug). +--- +- βœ… Ensure your code builds and runs without errors. +- πŸ§ͺ Include tests where applicable. +- πŸ’¬ Add comments if the logic is non-trivial. +- πŸ“Έ Attach screenshots for UI-related changes. +- πŸ”– Use meaningful commit messages and titles. - --- +--- - ## 🐞 Reporting Issues +## 🐞 Reporting Issues - If you discover a bug or have a suggestion: +If you discover a bug or have a suggestion: - ➑️ [Open an Issue](https://github.com/GitMetricsLab/github_tracker/issues/new/choose) +➑️ [Open an Issue](https://github.com/GitMetricsLab/github_tracker/issues/new/choose) - Please include: +Please include: - - **Steps to Reproduce** - - **Expected vs. Actual Behavior** - - **Screenshots/Logs (if any)** +- **Steps to Reproduce** +- **Expected vs. Actual Behavior** +- **Screenshots/Logs (if any)** - --- +--- - ## 🧠 Good Coding Practices +## 🧠 Good Coding Practices - 1. **Consistent Style** - Stick to the project's linting and formatting conventions (e.g., ESLint, Prettier, Tailwind classes). +1. **Consistent Style** + Stick to the project's linting and formatting conventions (e.g., ESLint, Prettier, Tailwind classes). - 2. **Meaningful Naming** - Use self-explanatory names for variables and functions. +2. **Meaningful Naming** + Use self-explanatory names for variables and functions. - 3. **Avoid Duplication** - Keep your code DRY (Don't Repeat Yourself). +3. **Avoid Duplication** + Keep your code DRY (Don't Repeat Yourself). - 4. **Testing** - Add unit or integration tests for any new logic. +4. **Testing** + Add unit or integration tests for any new logic. - 5. **Review Others’ PRs** - Help others by reviewing their PRs too! +5. **Review Others’ PRs** + Help others by reviewing their PRs too! - --- +--- - ## πŸ™Œ Thank You! +## πŸ™Œ Thank You! - We’re so glad you’re here. Your time and effort are deeply appreciated. Feel free to reach out via Issues or Discussions if you need any help. +We’re so glad you’re here. Your time and effort are deeply appreciated. Feel free to reach out via Issues or Discussions if you need any help. - **Happy Coding!** πŸ’»πŸš€ +**Happy Coding!** πŸ’»πŸš€ diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index b0c02ac8..00000000 --- a/backend/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -SESSION_SECRET=your_strong_random_secret_here -MONGO_URI=mongodb://localhost:27017/github_tracker -PORT=5000 -CLIENT_URL=http://localhost:5173 diff --git a/backend/config/passportConfig.js b/backend/config/passportConfig.js index b19ee9b3..842f50ca 100644 --- a/backend/config/passportConfig.js +++ b/backend/config/passportConfig.js @@ -9,13 +9,12 @@ passport.use( try { const user = await User.findOne( {email} ); if (!user) { - // Generic message prevents user enumeration - return done(null, false, { message: 'Invalid credentials' }); + return done(null, false, { message: 'Email is invalid '}); } const isMatch = await user.comparePassword(password); if (!isMatch) { - return done(null, false, { message: 'Invalid credentials' }); + return done(null, false, { message: 'Invalid password' }); } return done(null, { @@ -35,10 +34,10 @@ passport.serializeUser((user, done) => { done(null, user.id); }); -// Deserialize user β€” exclude password hash from req.user on every request +// Deserialize user (retrieve user from session) passport.deserializeUser(async (id, done) => { try { - const user = await User.findById(id).select('-password'); + const user = await User.findById(id); done(null, user); } catch (err) { done(err, null); diff --git a/backend/package.json b/backend/package.json index 3b31f9bd..74ab9dd7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,7 +17,6 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", - "express-rate-limit": "^7.5.1", "express-session": "^1.18.1", "mongoose": "^8.8.2", "passport": "^0.7.0", diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 7e6381cf..7c2cda78 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -26,29 +26,13 @@ router.post("/signup", validateRequest(signupSchema), async (req, res) => { return res.status(400).json({ message: 'User already exists' }); } - res.status(500).json({ message: 'Error creating user' }); + res.status(500).json({ message: 'Error creating user', error: err.message }); } }); -// Login route β€” session is regenerated after successful authentication -// to prevent session fixation; only safe fields returned in the response -router.post("/login", validateRequest(loginSchema), (req, res, next) => { - passport.authenticate('local', (err, user, info) => { - if (err) return next(err); - if (!user) return res.status(401).json({ message: info?.message || 'Invalid credentials' }); - - req.session.regenerate((regenerateErr) => { - if (regenerateErr) return next(regenerateErr); - - req.logIn(user, (loginErr) => { - if (loginErr) return next(loginErr); - res.status(200).json({ - message: 'Login successful', - user: { id: user.id, username: user.username, email: user.email }, - }); - }); - }); - })(req, res, next); +// Login route +router.post("/login", validateRequest(loginSchema), passport.authenticate('local'), (req, res) => { + res.status(200).json( { message: 'Login successful', user: req.user } ); }); // Logout route @@ -57,7 +41,7 @@ router.get("/logout", (req, res) => { req.logout((err) => { if (err) - return res.status(500).json({ message: 'Logout failed' }); + return res.status(500).json({ message: 'Logout failed', error: err.message }); else res.status(200).json({ message: 'Logged out successfully' }); }); diff --git a/backend/server.js b/backend/server.js index e24f15a8..e9b43f83 100644 --- a/backend/server.js +++ b/backend/server.js @@ -3,7 +3,6 @@ const mongoose = require('mongoose'); const session = require('express-session'); const passport = require('passport'); const bodyParser = require('body-parser'); -const rateLimit = require('express-rate-limit'); require('dotenv').config(); const cors = require('cors'); @@ -15,10 +14,7 @@ const logger = require('./logger'); const app = express(); // CORS configuration -app.use(cors({ - origin: process.env.CLIENT_URL || 'http://localhost:5173', - credentials: true, -})); +app.use(cors('*')); // Middleware app.use(bodyParser.json()); @@ -30,19 +26,6 @@ app.use(session({ app.use(passport.initialize()); app.use(passport.session()); -// Rate limiting β€” 10 attempts per 15-minute window per IP on auth endpoints -const authLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, - max: 10, - standardHeaders: true, - legacyHeaders: false, - message: { message: 'Too many attempts, please try again after 15 minutes.' }, - skipSuccessfulRequests: true, -}); - -app.use('/api/auth/login', authLimiter); -app.use('/api/auth/signup', authLimiter); - // Routes const authRoutes = require('./routes/auth'); app.use('/api/auth', authRoutes); diff --git a/package.json b/package.json index 5d166404..43ad31cc 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "github-tracker", + "name": "GitHub Tracker", "private": true, "version": "0.0.0", "type": "module", diff --git a/src/App.tsx b/src/App.tsx index d6ab9121..8eafb448 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,6 @@ import Footer from "./components/Footer"; import ScrollProgressBar from "./components/ScrollProgressBar"; import { Toaster } from "react-hot-toast"; import Router from "./Routes/Router"; -import ScrollToTopButton from "./components/ScrollToTopButton"; const FULLSCREEN_ROUTES = ["/signup", "/login"]; @@ -13,41 +12,35 @@ function App() { const isFullscreen = FULLSCREEN_ROUTES.includes(location.pathname); return ( -
- {!isFullscreen && } - - {!isFullscreen && } - -
- -
- - {!isFullscreen &&
} - {!isFullscreen && } - + {!isFullscreen && } + + {!isFullscreen && } + +
+ +
+ + {!isFullscreen &&
} + + -
+ }} + /> + ); } diff --git a/src/Routes/Router.tsx b/src/Routes/Router.tsx index dc6e167e..51eebc32 100644 --- a/src/Routes/Router.tsx +++ b/src/Routes/Router.tsx @@ -1,4 +1,3 @@ -import { lazy, Suspense } from "react"; import { Routes, Route } from "react-router-dom"; import Tracker from "../pages/Tracker/Tracker.tsx"; import About from "../pages/About/About"; @@ -7,7 +6,6 @@ import Contributors from "../pages/Contributors/Contributors"; import Signup from "../pages/Signup/Signup.tsx"; import Login from "../pages/Login/Login.tsx"; import ContributorProfile from "../pages/ContributorProfile/ContributorProfile.tsx"; -import Custom404 from "../pages/404.tsx"; import Home from "../pages/Home/Home.tsx"; import Activity from "../pages/Activity.tsx"; @@ -22,7 +20,6 @@ const Router = () => { } /> } /> } /> - } /> {/* βœ… new route */} } /> diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index a1fb6860..411bf2ff 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,18 +1,14 @@ -import React, { useState } from 'react'; -import { FaGithub, FaDiscord, FaArrowRight } from 'react-icons/fa'; -import { FaXTwitter } from 'react-icons/fa6'; +import { useState } from 'react'; import { Link } from 'react-router-dom'; - import { FaGithub, + FaTwitter, FaDiscord, FaArrowRight, FaEnvelope, FaInfoCircle, } from 'react-icons/fa'; -import { FaXTwitter } from 'react-icons/fa6'; - function Footer() { const [email, setEmail] = useState(''); @@ -38,7 +34,6 @@ function Footer() { " >
- {/* Upper Section */}
@@ -150,38 +145,35 @@ function Footer() { {/* Social Icons */}
- - {/* GitHub */} - {/* X / Twitter */} e.preventDefault()} - className="text-zinc-600 dark:text-zinc-400 hover:text-zinc-100 transition-all duration-300 hover:-translate-y-1 hover:scale-110 opacity-70 cursor-not-allowed" - aria-label="X" + href="https://x.com/your_handle" + target="_blank" + rel="noopener noreferrer" + className="text-zinc-400 dark:text-zinc-500 hover:text-sky-500 dark:hover:text-zinc-100 transition-all duration-300 hover:-translate-y-1 hover:scale-110" + aria-label="Twitter" > - + - {/* Discord */} e.preventDefault()} - className="text-zinc-600 dark:text-zinc-400 hover:text-indigo-500 dark:hover:text-zinc-100 transition-all duration-300 hover:-translate-y-1 hover:scale-110 opacity-70 cursor-not-allowed" + href="https://discord.gg/your_invite" + target="_blank" + rel="noopener noreferrer" + className="text-zinc-400 dark:text-zinc-500 hover:text-indigo-500 dark:hover:text-zinc-100 transition-all duration-300 hover:-translate-y-1 hover:scale-110" aria-label="Discord" > -
@@ -189,4 +181,4 @@ function Footer() { ); } -export default Footer; \ No newline at end of file +export default Footer; diff --git a/src/components/ScrollToTopButton.tsx b/src/components/ScrollToTopButton.tsx deleted file mode 100644 index 9d877da6..00000000 --- a/src/components/ScrollToTopButton.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useEffect, useState } from "react"; -import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; - -const ScrollToTopButton = () => { - const [visible, setVisible] = useState(false); - - useEffect(() => { - const toggleVisibility = () => { - if (window.scrollY > 10) { - setVisible(true); - } else { - setVisible(false); - } - }; - - window.addEventListener("scroll", toggleVisibility); - - return () => { - window.removeEventListener("scroll", toggleVisibility); - }; - }, []); - - const scrollToTop = () => { - window.scrollTo({ - top: 0, - behavior: "smooth", - }); - }; - - return ( - visible && ( - - ) - ); -}; - -export default ScrollToTopButton; diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts index 7b6bc06c..e3706e6b 100644 --- a/src/hooks/useDebounce.ts +++ b/src/hooks/useDebounce.ts @@ -1,28 +1,17 @@ -import { useEffect, useState } from 'react'; +import { useState, useEffect } from 'react'; -/** - * Custom hook for debouncing values - * Delays updating a state value until after the user stops changing it for a specified duration - * - * @param value - The value to debounce - * @param delay - The debounce delay in milliseconds (default: 300ms) - * @returns The debounced value - */ -export const useDebounce = ( - value: T, - delay: number = 300 -): T => { +export function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { - // Set up the timeout const handler = setTimeout(() => { setDebouncedValue(value); }, delay); - // Clean up the timeout if value changes before delay is complete - return () => clearTimeout(handler); + return () => { + clearTimeout(handler); + }; }, [value, delay]); return debouncedValue; -}; \ No newline at end of file +} diff --git a/src/hooks/useGitHubAuth.ts b/src/hooks/useGitHubAuth.ts index 17831683..a0c24b2a 100644 --- a/src/hooks/useGitHubAuth.ts +++ b/src/hooks/useGitHubAuth.ts @@ -1,45 +1,25 @@ -import { useState, useEffect } from 'react'; -import { Octokit } from 'octokit'; +import { useState, useMemo } from 'react'; +import { Octokit } from '@octokit/core'; export const useGitHubAuth = () => { - const [username, setUsername] = useState(() => sessionStorage.getItem('tracker_username') || ''); - const [token, setToken] = useState(() => sessionStorage.getItem('tracker_token') || ''); - const [error, setError] = useState(''); + const [username, setUsername] = useState(''); + const [token, setToken] = useState(''); - useEffect(() => { - if (username) { - sessionStorage.setItem('tracker_username', username); - } else { - sessionStorage.removeItem('tracker_username'); - } - if (token) { - sessionStorage.setItem('tracker_token', token); - } else { - sessionStorage.removeItem('tracker_token'); + const octokit = useMemo(() => { + if (!username) return null; + if(token){ + return new Octokit({ auth: token }); } + return new Octokit(); }, [username, token]); - const getOctokit = () => { - try { - setError(''); - if (!username) return null; - if (token) { - return new Octokit({ auth: token }); - } - return new Octokit(); - } catch (err: any) { - setError(err instanceof Error ? err.message : String(err)); - return null; - } - }; + const getOctokit = () => octokit; return { username, setUsername, token, setToken, - error, - setError, getOctokit, }; }; diff --git a/src/hooks/useGitHubData.ts b/src/hooks/useGitHubData.ts index 2d86f86f..f4c78cf6 100644 --- a/src/hooks/useGitHubData.ts +++ b/src/hooks/useGitHubData.ts @@ -1,4 +1,3 @@ -import { useState, useCallback, useRef, useEffect } from 'react'; import { useState, useCallback, useRef } from 'react'; import { Octokit } from '@octokit/core'; @@ -33,276 +32,221 @@ export const useGitHubData = ( const [totalPrs, setTotalPrs] = useState(0); const [rateLimited, setRateLimited] = useState(false); -// Prevent stale responses overwriting latest data -const lastRequestId = useRef(0); - -// Store AbortController to cancel in-flight requests -const abortControllerRef = useRef(null); - -// Cleanup function to cancel any pending requests -const cancelPendingRequest = useCallback(() => { - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - abortControllerRef.current = null; - } -}, []); - -// Cleanup on component unmount -useEffect(() => { - return () => { - cancelPendingRequest(); - }; -}, [cancelPendingRequest]); - -const fetchPaginated = async ( - octokit: Octokit, - username: string, - type: 'issue' | 'pr', - page = 1, - perPage = 10, - filters: FetchFilters = {}, - signal?: AbortSignal -) => { - let q = `author:${username} is:${type}`; - - if (filters.search) { - q += ` ${filters.search} in:title`; - } - - if (filters.repo) { - q += ` repo:${filters.repo}`; - } - - if (filters.startDate) { - q += ` created:>=${filters.startDate}`; - } - - if (filters.endDate) { - q += ` created:<=${filters.endDate}`; - } - - if ( - filters.state === 'open' || - filters.state === 'closed' - ) { - q += ` is:${filters.state}`; - } - - if ( - filters.state === 'merged' && - type === 'pr' - ) { - q += ` is:merged`; - } - - const response = await octokit.request( - 'GET /search/issues', - { - q, - sort: 'created', - order: 'desc', - per_page: perPage, - page, - request: { - signal, - }, + // Prevent stale responses overwriting latest data + const lastRequestId = useRef(0); + + const fetchPaginated = async ( + octokit: Octokit, + username: string, + type: 'issue' | 'pr', + page = 1, + perPage = 10, + filters: FetchFilters = {} + ) => { + let q = `author:${username} is:${type}`; + + if (filters.search) { + q += ` ${filters.search} in:title`; } - ); - - return { - items: response.data.items as GitHubItem[], - total: response.data.total_count, - }; -}; - - const fetchData = useCallback( -async ( - username: string, - page = 1, - perPage = 10, - activeTab: 'issue' | 'pr' | 'both' = 'both', - filters: FetchFilters = {} -) => { - // Validate inputs - if (!username || username.trim().length < 2) { - setError('Please enter a valid GitHub username.'); - setIssues([]); - setPrs([]); - setTotalIssues(0); - setTotalPrs(0); - return; - } - - const octokit = getOctokit(); - - if (!octokit || rateLimited) { - return; - } - - // Prevent stale responses - const requestId = ++lastRequestId.current; - - // Cancel any existing in-flight requests - cancelPendingRequest(); - - // Create new AbortController for this request - abortControllerRef.current = new AbortController(); - - const signal = abortControllerRef.current.signal; - - setLoading(true); - setError(''); - - try { - const shouldFetchIssues = - activeTab === 'issue' || activeTab === 'both'; - - const shouldFetchPrs = - activeTab === 'pr' || activeTab === 'both'; - - const requests: Promise[] = []; - - if (shouldFetchIssues) { - requests.push( - fetchPaginated( - octokit, - username, - 'issue', - page, - perPage, - filters, - signal - ) - ); + if (filters.repo) { + q += ` repo:${filters.repo}`; } - if (shouldFetchPrs) { - requests.push( - fetchPaginated( - octokit, - username, - 'pr', - page, - perPage, - filters, - signal - ) - ); - } - - const results = await Promise.allSettled(requests); - - // Ignore stale or aborted requests - if ( - requestId !== lastRequestId.current || - signal.aborted - ) { - return; - } - - let resultIndex = 0; - - if (shouldFetchIssues) { - const issueResult = results[resultIndex]; - - if (issueResult.status === 'fulfilled') { - setIssues(issueResult.value.items); - setTotalIssues(issueResult.value.total); - } else { - setIssues([]); - setTotalIssues(0); + if (filters.startDate) { + q += ` created:>=${filters.startDate}`; } - resultIndex++; - } - - if (shouldFetchPrs) { - const prResult = results[resultIndex]; + if (filters.endDate) { + q += ` created:<=${filters.endDate}`; + } - if (prResult.status === 'fulfilled') { - setPrs(prResult.value.items); - setTotalPrs(prResult.value.total); - } else { - setPrs([]); - setTotalPrs(0); + if (filters.state === 'open' || filters.state === 'closed') { + q += ` is:${filters.state}`; } - } - const hasRejected = results.some( - (result) => result.status === 'rejected' - ); + if (filters.state === 'merged' && type === 'pr') { + q += ` is:merged`; + } - if (hasRejected) { - setError( - 'Some GitHub data could not be fetched completely.' + const response = await octokit.request( + 'GET /search/issues', + { + q, + sort: 'created', + order: 'desc', + per_page: perPage, + page, + } ); - } - - setRateLimited(false); - -} catch (err: unknown) { - - // Ignore aborted requests - if ((err as Error).name === 'AbortError') { - return; - } - // Ignore stale requests - if (requestId !== lastRequestId.current) { - return; - } - - const error = err as { - status?: number; - message?: string; + return { + items: response.data.items as GitHubItem[], + total: response.data.total_count, + }; }; - const errorMessage = - error.message?.toLowerCase() || ''; - - if (error.status === 403) { - setError( - 'GitHub API rate limit exceeded. Please provide a PAT to continue.' - ); - setRateLimited(true); - - } else if ( - errorMessage.includes('do not exist') - ) { - setError( - 'User not found. Please check the GitHub username.' - ); - - } else if ( - errorMessage.includes('validation failed') - ) { - setError( - 'Invalid GitHub username or insufficient permissions.' - ); - - } else if ( - error.status === 401 || - errorMessage.includes('permission') - ) { - setError( - 'Private repository detected. Please provide a PAT.' - ); - - } else if (error.status === 404) { - setError('Resource not found.'); + const fetchData = useCallback( + async ( + username: string, + page = 1, + perPage = 10, + activeTab: 'issue' | 'pr' | 'both' = 'both', + filters: FetchFilters = {} + ) => { + const octokit = getOctokit(); + + if (!octokit || !username.trim() || rateLimited) { + return; + } + + const requestId = ++lastRequestId.current; + + setLoading(true); + setError(''); + + try { + const shouldFetchIssues = + activeTab === 'issue' || activeTab === 'both'; + + const shouldFetchPrs = + activeTab === 'pr' || activeTab === 'both'; + + const requests: Promise[] = []; + + if (shouldFetchIssues) { + requests.push( + fetchPaginated( + octokit, + username, + 'issue', + page, + perPage, + filters + ) + ); + } + + if (shouldFetchPrs) { + requests.push( + fetchPaginated( + octokit, + username, + 'pr', + page, + perPage, + filters + ) + ); + } + + const results = await Promise.allSettled(requests); + + // Ignore stale requests + if (requestId !== lastRequestId.current) { + return; + } + + let resultIndex = 0; + + if (shouldFetchIssues) { + const issueResult = results[resultIndex]; + + if (issueResult.status === 'fulfilled') { + setIssues(issueResult.value.items); + setTotalIssues(issueResult.value.total); + } else { + setIssues([]); + setTotalIssues(0); + } + + resultIndex++; + } + + if (shouldFetchPrs) { + const prResult = results[resultIndex]; + + if (prResult.status === 'fulfilled') { + setPrs(prResult.value.items); + setTotalPrs(prResult.value.total); + } else { + setPrs([]); + setTotalPrs(0); + } + } + + const hasRejected = results.some( + (result) => result.status === 'rejected' + ); + + if (hasRejected) { + setError( + 'Some GitHub data could not be fetched completely.' + ); + } + + setRateLimited(false); + } catch (err: unknown) { + if (requestId !== lastRequestId.current) { + return; + } + + const error = err as { + status?: number; + message?: string; + }; + + const errorMessage = + error.message?.toLowerCase() || ''; + + if (error.status === 403) { + setError( + 'GitHub API rate limit exceeded. Please provide a PAT to continue.' + ); + setRateLimited(true); + } else if ( + errorMessage.includes('do not exist') + ) { + setError( + 'User not found. Please check the GitHub username.' + ); + } else if ( + errorMessage.includes('validation failed') + ) { + setError( + 'Invalid GitHub username or insufficient permissions.' + ); + } else if ( + error.status === 401 || + errorMessage.includes('permission') + ) { + setError( + 'Private repository detected. Please provide a PAT.' + ); + } else if (error.status === 404) { + setError('Resource not found.'); + } else { + setError( + 'Unable to fetch GitHub data. Please verify the username, token, or network connection.' + ); + } + } finally { + if (requestId === lastRequestId.current) { + setLoading(false); + } + } + }, + [getOctokit, rateLimited] + ); - } else { - setError( - 'Unable to fetch GitHub data. Please verify the username, token, or network connection.' - ); - } - -} finally { - if ( - requestId === lastRequestId.current && - !signal.aborted - ) { - setLoading(false); - } -} \ No newline at end of file + return { + issues, + prs, + totalIssues, + totalPrs, + loading, + error, + rateLimited, + fetchData, + }; +}; diff --git a/src/pages/404.tsx b/src/pages/404.tsx deleted file mode 100644 index c6d4bcd2..00000000 --- a/src/pages/404.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Link } from "react-router-dom"; - -export default function Custom404() { - return ( -
-
-

- 404 -

- -

- Page Not Found -

- -

- The page you are looking for does not exist or has been moved. -

- - - Go Back Home - -
-
- ); -} \ No newline at end of file diff --git a/src/pages/Contact/Contact.tsx b/src/pages/Contact/Contact.tsx index bd6d64bc..a0bfccbd 100644 --- a/src/pages/Contact/Contact.tsx +++ b/src/pages/Contact/Contact.tsx @@ -6,118 +6,17 @@ import { Send, X, CheckCircle, - Shield, - Clock, - Calendar, - User, - MessageSquare, } from "lucide-react"; import { ThemeContext } from "../../context/ThemeContext"; import type { ThemeContextType } from "../../context/ThemeContext"; -type FormData = { - name: string; - email: string; - subject: string; - message: string; -}; - -type FormErrors = { - name: string; - email: string; - subject: string; - message: string; -}; - function Contact() { const [showPopup, setShowPopup] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - - const [formData, setFormData] = useState({ - name: "", - email: "", - subject: "", - message: "", - }); - - const [errors, setErrors] = useState({ - name: "", - email: "", - subject: "", - message: "", - }); - const themeContext = useContext(ThemeContext) as ThemeContextType; const { mode } = themeContext; - const handleInputChange = ( - e: React.ChangeEvent< - HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement - >, - ) => { - const { name, value } = e.target; - - setFormData((prev) => ({ - ...prev, - [name]: value, - })); - - // Clear error for the current field - if (errors[name as keyof FormErrors]) { - setErrors((prev) => ({ - ...prev, - [name]: "", - })); - } - }; - - const validateForm = () => { - const newErrors: FormErrors = { - name: "", - email: "", - subject: "", - message: "", - }; - - let isValid = true; - - if (!formData.name.trim()) { - newErrors.name = "Full name is required."; - isValid = false; - } - - if (!formData.email.trim()) { - newErrors.email = "Email address is required."; - isValid = false; - } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { - newErrors.email = "Please enter a valid email address."; - isValid = false; - } - - if (!formData.subject) { - newErrors.subject = "Please select a subject."; - isValid = false; - } - - if (!formData.message.trim()) { - newErrors.message = "Message is required."; - isValid = false; - } else if (formData.message.trim().length < 20) { - newErrors.message = "Please provide at least 20 characters."; - isValid = false; - } - - setErrors(newErrors); - return isValid; - }; - - const handleSubmit = async ( - e?: React.FormEvent | React.MouseEvent, - ) => { - e?.preventDefault(); - - if (!validateForm()) return; - + const handleSubmit = async () => { setIsSubmitting(true); // Simulate API call @@ -126,14 +25,6 @@ function Contact() { setIsSubmitting(false); setShowPopup(true); - // Reset form - setFormData({ - name: "", - email: "", - subject: "", - message: "", - }); - // Auto-close popup after 5 seconds setTimeout(() => { setShowPopup(false); @@ -160,7 +51,7 @@ function Contact() {
- {/* Header Section (unchanged) */} + {/* Header Section */}

GitHub Tracker @@ -192,7 +83,7 @@ function Contact() {

- {/* Contact Info Cards (unchanged) */} + {/* Contact Info Cards */}

- We're here to help you track and manage your GitHub repositories - more effectively + We're here to help you track and manage your GitHub + repositories more effectively

@@ -237,10 +128,8 @@ function Contact() { Icon: Github, }, ]; - const { title, iconBg, detail, sub, Icon } = contactTypes[index]; - return (

{title}

{detail}

{sub} @@ -290,7 +187,7 @@ function Contact() {

- {/* Enhanced Contact Form */} + {/* Contact Form */}

Send us a Message

-
-
- {/* Name + Email */} -
- {/* Full Name */} -
- -
- - -
- {errors.name && ( -

{errors.name}

- )} -
+
+
+ {/* Full Name */} +
+ + +
- {/* Email */} -
- -
- - -
- {errors.email && ( -

- {errors.email} -

- )} -
+ {/* Email */} +
+ +
{/* Subject */}
- {errors.subject && ( -

- {errors.subject} -

- )}
{/* Message */} -
+
-
- - + + - - {/* Trust Indicators */} -
-
- - Your information is secure -
- -
- - One business day response -
- -
- - Mon–Fri support +
- +
diff --git a/src/pages/Contributors/Contributors.tsx b/src/pages/Contributors/Contributors.tsx index 56816ad1..d4fee52c 100644 --- a/src/pages/Contributors/Contributors.tsx +++ b/src/pages/Contributors/Contributors.tsx @@ -1,6 +1,4 @@ import { useEffect, useState } from "react"; -import { TextField, InputAdornment } from "@mui/material"; -import { FaSearch } from "react-icons/fa"; import { Container, Grid, @@ -30,7 +28,6 @@ const ContributorsPage = () => { const [contributors, setContributors] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [searchTerm, setSearchTerm] = useState(""); // Fetch contributors from GitHub API useEffect(() => { @@ -50,10 +47,6 @@ const ContributorsPage = () => { fetchContributors(); }, []); - const filteredContributors = contributors.filter((contributor) => - contributor.login.toLowerCase().includes(searchTerm.toLowerCase()), - ); - if (loading) { return ( @@ -72,136 +65,74 @@ const ContributorsPage = () => { return (
- + 🀝 Contributors - - setSearchTerm(e.target.value)} - sx={{ - width: { - xs: "100%", - sm: "400px", - }, - maxWidth: "100%", - backgroundColor: "white", - borderRadius: "10px", - }} - InputProps={{ - startAdornment: ( - - - - ), - }} - /> - - - {filteredContributors.length === 0 && ( - - No contributors found. - - )} - {filteredContributors.map((contributor) => ( - - - + {contributors.map((contributor) => ( + + - - - - {contributor.login} - + + + + + {contributor.login} + - - {contributor.contributions} Contributions - - {/* + + {contributor.contributions} Contributions + + {/* Thank you for your valuable contributions to our community! */} - - + + - - - - + + + + ))} diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index 265c3047..92b7073e 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -2,7 +2,6 @@ import React, { useState, ChangeEvent, FormEvent, useContext } from "react"; import axios from "axios"; import { useNavigate, Link } from "react-router-dom"; import { ThemeContext } from "../../context/ThemeContext"; -import { Eye, EyeOff } from "lucide-react"; import type { ThemeContextType } from "../../context/ThemeContext"; const backendUrl = import.meta.env.VITE_BACKEND_URL; @@ -16,7 +15,6 @@ const Login: React.FC = () => { const [formData, setFormData] = useState({ email: "", password: "" }); const [message, setMessage] = useState(""); const [isLoading, setIsLoading] = useState(false); - const [showPassword, setShowPassword] = useState(false); const navigate = useNavigate(); const themeContext = useContext(ThemeContext) as ThemeContextType; @@ -108,32 +106,22 @@ const Login: React.FC = () => { />
-
- - - -
+
+ +
{/* Filters */} - + setSearchTitle(e.target.value)} sx={{ minWidth: 200 }} /> - setSelectedRepo(e.target.value)} sx={{ minWidth: 200 }} /> - { InputLabelProps={{ shrink: true }} sx={{ minWidth: 150 }} /> - { - - - State - - + State @@ -427,65 +331,57 @@ const Home: React.FC = () => { )} {loading ? ( - + ) : ( + + + Title - - - Repository - - - - State - - + Repository + State Created - {currentData.map((item) => ( + {currentFilteredData.map((item) => ( - - {getStatusIcon(item)} - - {item.title} - + + + {getStatusIcon(item)} + + {item.title} + + - { - item.repository_url - .split("/") - .slice(-1)[0] - } + {item.repository_url.split("/").slice(-1)[0]} - {item.pull_request?.merged_at - ? "merged" - : item.state} + {item.pull_request?.merged_at ? "merged" : item.state} {formatDate(item.created_at)} + ))} +
{ page={page} onPageChange={handlePageChange} rowsPerPage={ROWS_PER_PAGE} - rowsPerPageOptions={[ - ROWS_PER_PAGE, - ]} + rowsPerPageOptions={[ROWS_PER_PAGE]} /> +
)} @@ -505,4 +400,4 @@ const Home: React.FC = () => { ); }; -export default Home; \ No newline at end of file +export default Home; diff --git a/tsconfig.app.json b/tsconfig.app.json index a616131d..f867de0d 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -19,7 +19,8 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true }, "include": ["src"] }