├── .env.example ├── public ├── _redirects ├── crl.png ├── crl-icon.png └── vite.svg ├── backend ├── .dockerignore ├── .env.sample ├── Dockerfile.dev ├── Dockerfile.prod ├── package.json ├── models │ └── User.js ├── server.js ├── routes │ └── auth.js └── config │ └── passportConfig.js ├── .dockerignore ├── postcss.config.cjs ├── tsconfig.json ├── vite.config.ts ├── Dockerfile.dev ├── src ├── utils │ └── constants.ts ├── vite-env.d.ts ├── index.css ├── pages │ ├── Home │ │ └── Home.tsx │ ├── ContributorProfile │ │ └── ContributorProfile.tsx │ ├── About │ │ └── About.tsx │ ├── Contributors │ │ └── Contributors.tsx │ ├── Signup │ │ └── Signup.tsx │ ├── Login │ │ └── Login.tsx │ ├── Tracker │ │ └── Tracker.tsx │ └── Contact │ │ └── Contact.tsx ├── main.tsx ├── hooks │ ├── useGitHubAuth.ts │ └── useGitHubData.ts ├── App.css ├── Routes │ └── Router.tsx ├── App.tsx ├── context │ └── ThemeContext.tsx ├── components │ ├── Footer.tsx │ ├── Hero.tsx │ ├── HowItWorks.tsx │ ├── ScrollProgressBar.tsx │ ├── Features.tsx │ └── Navbar.tsx └── assets │ └── react.svg ├── spec ├── support │ └── jasmine.mjs ├── user.model.spec.cjs └── auth.routes.spec.cjs ├── tailwind.config.js ├── .github ├── pull_request_template.md ├── workflows │ ├── greetings.yml │ ├── auto-label-gssoc.yml │ ├── auto-assign-issue.yml │ ├── post-pr-thankyou.yml │ └── unassign-stale-issues.yml └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── index.html ├── tsconfig.node.json ├── tsconfig.app.json ├── eslint.config.js ├── LICENSE ├── Dockerfile.prod ├── docker-compose.yml ├── CODE_OF_CONDUCT.md ├── package.json ├── .gitignore ├── CONTRIBUTING.md └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | VITE_BACKEND_URL=http://localhost:5000 -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | 3 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gitignore 3 | .git 4 | .env.* 5 | yarn.lock -------------------------------------------------------------------------------- /public/crl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitMetricsLab/github_tracker/HEAD/public/crl.png -------------------------------------------------------------------------------- /public/crl-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitMetricsLab/github_tracker/HEAD/public/crl-icon.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | backend 2 | node_modules 3 | dist 4 | build 5 | .gitignore 6 | .git 7 | .env.* 8 | .github 9 | yarn.lock -------------------------------------------------------------------------------- /backend/.env.sample: -------------------------------------------------------------------------------- 1 | PORT=5000 2 | MONGO_URI=mongodb://localhost:27017/githubTracker 3 | SESSION_SECRET=your-secret-key 4 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | 5 | 6 | COPY package.json . 7 | 8 | RUN npm install 9 | 10 | COPY . . 11 | 12 | EXPOSE 5173 13 | 14 | CMD ["npm","run","dev"] 15 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const GITHUB_REPO_API_BASE = "https://api.github.com/repos/GitMetricsLab/github_tracker"; 2 | 3 | // endpoints: 4 | export const GITHUB_REPO_CONTRIBUTORS_URL = `${GITHUB_REPO_API_BASE}/contributors`; 5 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_BACKEND_URL: string; 5 | // Add other variables if needed 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv; 10 | } 11 | -------------------------------------------------------------------------------- /spec/support/jasmine.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | spec_dir: "spec", 3 | spec_files: [ 4 | "**/*[sS]pec.?(m)js", 5 | "**/*[sS]pec.cjs" 6 | ], 7 | helpers: [ 8 | "helpers/**/*.?(m)js" 9 | ], 10 | env: { 11 | stopSpecOnExpectationFailure: false, 12 | random: true, 13 | forbidDuplicateNames: true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | .icon-merged { 7 | color: #2ea44f; /* Or use your theme color */ 8 | } 9 | .icon-pr-open { 10 | color: #0969da; 11 | } 12 | .icon-pr-closed { 13 | color: #cf222e; 14 | } 15 | .icon-issue-open { 16 | color: #2ea44f; 17 | } 18 | .icon-issue-closed { 19 | color: #cf222e; 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import Hero from "../../components/Hero"; 2 | import HowItWorks from "../../components/HowItWorks"; 3 | import Features from "../../components/Features"; 4 | 5 | function Home() { 6 | return ( 7 |
8 | 9 | 10 | 11 | 12 |
13 | ) 14 | } 15 | 16 | export default Home 17 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: 'class', 4 | content: [ 5 | "./index.html", // For any HTML files in the root 6 | "./src/**/*.{js,jsx,ts,tsx}", // For all JS/JSX/TS/TSX files inside src folder 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | } 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Related Issue 2 | - Closes: # 3 | 4 | --- 5 | 6 | ### Description 7 | 8 | 9 | --- 10 | 11 | ### How Has This Been Tested? 12 | 13 | 14 | --- 15 | 16 | ### Screenshots (if applicable) 17 | 18 | 19 | --- 20 | 21 | ### Type of Change 22 | 23 | - [ ] Bug fix 24 | - [ ] New feature 25 | - [ ] Code style update 26 | - [ ] Breaking change 27 | - [ ] Documentation update 28 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Github Tracker 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import "./index.css"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import ThemeWrapper from "./context/ThemeContext.tsx"; 7 | 8 | createRoot(document.getElementById("root")!).render( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); -------------------------------------------------------------------------------- /backend/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # Use a lightweight Node.js Alpine image 2 | FROM node:20-alpine 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | # Copy package.json and yarn.lock files for dependency installation 8 | COPY package.json ./ 9 | 10 | # Install all dependencies using Yarn 11 | RUN npm install 12 | 13 | # Copy the rest of the application files 14 | COPY . . 15 | 16 | # Expose the port for the application 17 | EXPOSE 5000 18 | 19 | # Start the server in dev mode using Yarn 20 | CMD ["npm","run", "dev"] 21 | -------------------------------------------------------------------------------- /backend/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | # Use a lightweight Node.js Alpine image 2 | FROM node:20-alpine 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | # Copy package.json and yarn.lock files for dependency installation 8 | COPY package.json ./ 9 | 10 | # Install production dependencies using Yarn 11 | RUN npm install --production 12 | 13 | # Copy the rest of the application files 14 | COPY . . 15 | 16 | # Expose the port for the application 17 | EXPOSE 5000 18 | 19 | # Start the server in production mode using Yarn 20 | CMD ["npm","run", "start"] 21 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | steps: 12 | - uses: actions/first-interaction@v1 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | pr-message: " 🎉 Thank you @${{ github.actor }} for your contribution. Please make sure your PR follows https://github.com/GitMetricsLab/github_tracker/blob/main/CONTRIBUTING.md#-pull-request-guidelines" 16 | -------------------------------------------------------------------------------- /src/hooks/useGitHubAuth.ts: -------------------------------------------------------------------------------- 1 | import { useState, useMemo } from 'react'; 2 | import { Octokit } from '@octokit/core'; 3 | 4 | export const useGitHubAuth = () => { 5 | const [username, setUsername] = useState(''); 6 | const [token, setToken] = useState(''); 7 | const [error, setError] = useState(''); 8 | 9 | const octokit = useMemo(() => { 10 | if (!username || !token) return null; 11 | return new Octokit({ auth: token }); 12 | }, [username, token]); 13 | 14 | const getOctokit = () => octokit; 15 | 16 | return { 17 | username, 18 | setUsername, 19 | token, 20 | setToken, 21 | error, 22 | setError, 23 | getOctokit, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "Bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "scripts": { 6 | "dev": "nodemon server.js", 7 | "start": "node server.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "description": "", 14 | "dependencies": { 15 | "bcryptjs": "^2.4.3", 16 | "body-parser": "^1.20.3", 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.4.5", 19 | "express": "^4.21.1", 20 | "express-session": "^1.18.1", 21 | "mongoose": "^8.8.2", 22 | "passport": "^0.7.0", 23 | "passport-local": "^1.0.0" 24 | }, 25 | "devDependencies": { 26 | "nodemon": "^3.1.9" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "Bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /.github/workflows/auto-label-gssoc.yml: -------------------------------------------------------------------------------- 1 | name: Auto Label Issues and PRs 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | pull_request_target: # Correct indentation here 7 | types: [opened] 8 | 9 | jobs: 10 | add-labels: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Add labels to new issues 15 | if: github.event_name == 'issues' 16 | uses: actions-ecosystem/action-add-labels@v1 17 | with: 18 | github_token: ${{ secrets.GITHUB_TOKEN }} # Use GITHUB_TOKEN for issues 19 | labels: | 20 | gssoc25 21 | 22 | - name: Add labels to new pull requests 23 | if: github.event_name == 'pull_request_target' 24 | uses: actions-ecosystem/action-add-labels@v1 25 | with: 26 | github_token: ${{ secrets.GITHUB_TOKEN }} # Use GITHUB_TOKEN for PRs 27 | labels: | 28 | gssoc25 29 | -------------------------------------------------------------------------------- /.github/workflows/auto-assign-issue.yml: -------------------------------------------------------------------------------- 1 | name: Auto Assign Issue Author 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | auto-assign: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write # Required for assigning issues 12 | 13 | steps: 14 | - name: Assign issue to creator 15 | uses: actions/github-script@v7 16 | with: 17 | script: | 18 | const issueCreator = context.payload.issue.user.login; 19 | const issueNumber = context.payload.issue.number; 20 | 21 | await github.rest.issues.addAssignees({ 22 | owner: context.repo.owner, 23 | repo: context.repo.repo, 24 | issue_number: issueNumber, 25 | assignees: [issueCreator], 26 | }); 27 | 28 | console.log(`Assigned issue #${issueNumber} to @${issueCreator}`); 29 | -------------------------------------------------------------------------------- /backend/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const bcrypt = require("bcryptjs"); 3 | 4 | const UserSchema = new mongoose.Schema({ 5 | username: { 6 | type: String, 7 | required: true, 8 | unique: true, 9 | }, 10 | email: { 11 | type: String, 12 | required: true, 13 | unique: true, 14 | }, 15 | password: { 16 | type: String, 17 | required: true, 18 | }, 19 | }); 20 | 21 | UserSchema.pre('save', async function (next) { 22 | 23 | if (!this.isModified('password')) 24 | return next(); 25 | 26 | try { 27 | const salt = await bcrypt.genSalt(10); 28 | this.password = await bcrypt.hash(this.password, salt); 29 | next(); 30 | } catch (err) { 31 | return next(err); 32 | } 33 | }); 34 | 35 | // Compare passwords during login 36 | UserSchema.methods.comparePassword = async function (enteredPassword) { 37 | return await bcrypt.compare(enteredPassword, this.password); 38 | }; 39 | 40 | module.exports = mongoose.model("User", UserSchema); 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "🐛 Bug Report" 2 | description: "Submit a bug report to help us improve" 3 | title: "🐛 Bug Report: " 4 | labels: ["type: bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: We value your time and your efforts to submit this bug report is appreciated. 🙏 9 | 10 | - type: textarea 11 | id: description 12 | validations: 13 | required: true 14 | attributes: 15 | label: "📜 Description" 16 | description: "A clear and concise description of what the bug is." 17 | placeholder: "It bugs out when ..." 18 | 19 | - type: dropdown 20 | id: browsers 21 | attributes: 22 | label: What browsers are you seeing the problem on? 23 | multiple: true 24 | options: 25 | - Firefox 26 | - Chrome 27 | - Safari 28 | - Microsoft Edge 29 | - Something else 30 | 31 | - type: textarea 32 | id: screenshots 33 | validations: 34 | required: false 35 | attributes: 36 | label: "📃 Relevant Screenshots (Links)" 37 | description: "Screenshot" 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature 2 | description: "Submit a proposal for a new feature" 3 | title: "🚀 Feature: " 4 | labels: [feature] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: We value your time and your efforts to submit this bug report is appreciated. 🙏 9 | - type: textarea 10 | id: feature-description 11 | validations: 12 | required: true 13 | attributes: 14 | label: "🔖 Feature description" 15 | description: "A clear and concise description of what the feature is." 16 | placeholder: "You should add ..." 17 | - type: textarea 18 | id: screenshot 19 | validations: 20 | required: false 21 | attributes: 22 | label: "🎤 Screenshot" 23 | description: "Add screenshot if applicable." 24 | - type: textarea 25 | id: alternative 26 | validations: 27 | required: false 28 | attributes: 29 | label: "🔄️ Additional Information" 30 | description: "A clear and concise description of any alternative solutions or additional solutions you've considered." 31 | placeholder: "I tried, ..." 32 | -------------------------------------------------------------------------------- /.github/workflows/post-pr-thankyou.yml: -------------------------------------------------------------------------------- 1 | name: Post-PR Merge Thank You 2 | 3 | on: 4 | pull_request_target: 5 | types: [closed] # Trigger when a PR is closed 6 | 7 | permissions: 8 | issues: write 9 | pull-requests: write 10 | 11 | jobs: 12 | post_merge_message: 13 | if: github.event.pull_request.merged == true # Only run if the PR was merged 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Post thank you message 18 | uses: actions/github-script@v7 19 | with: 20 | github-token: ${{ secrets.GITHUB_TOKEN }} # Ensure token is used 21 | script: | 22 | const prNumber = context.payload.pull_request.number; 23 | const owner = context.repo.owner; 24 | const repo = context.repo.repo; 25 | 26 | // Post a thank you message upon PR merge 27 | await github.rest.issues.createComment({ 28 | owner: owner, 29 | repo: repo, 30 | issue_number: prNumber, 31 | body: `🎉🎉 Thank you for your contribution! Your PR #${prNumber} has been merged! 🎉🎉` 32 | }); 33 | -------------------------------------------------------------------------------- /src/Routes/Router.tsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from "react-router-dom"; 2 | import Tracker from "../pages/Tracker/Tracker.tsx"; 3 | import About from "../pages/About/About"; 4 | import Contact from "../pages/Contact/Contact"; 5 | import Contributors from "../pages/Contributors/Contributors"; 6 | import Signup from "../pages/Signup/Signup.tsx"; 7 | import Login from "../pages/Login/Login.tsx"; 8 | import ContributorProfile from "../pages/ContributorProfile/ContributorProfile.tsx"; 9 | import Home from "../pages/Home/Home.tsx"; 10 | 11 | const Router = () => { 12 | return ( 13 | 14 | } /> 15 | } /> 16 | } /> 17 | } /> 18 | } /> 19 | } /> 20 | } /> 21 | } /> 22 | 23 | ); 24 | }; 25 | 26 | export default Router; 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mehul Prajapati 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Dockerfile.prod: -------------------------------------------------------------------------------- 1 | # Stage 1: Build the frontend 2 | FROM node:20-alpine AS build 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | # Copy package.json 8 | COPY package.json . 9 | 10 | # Install production dependencies using Yarn 11 | RUN npm install --production 12 | # Copy the rest of the application files 13 | COPY . . 14 | 15 | # Build the frontend using Yarn 16 | RUN npm run build 17 | 18 | # Stage 2: Serve the application with Nginx 19 | FROM nginx:alpine 20 | 21 | # Copy the build output from the previous stage to Nginx's serve directory 22 | COPY --from=build /app/dist /usr/share/nginx/html 23 | 24 | # Remove the default Nginx configuration file 25 | RUN rm /etc/nginx/conf.d/default.conf 26 | 27 | # Create a new Nginx configuration file 28 | RUN echo $'\ 29 | server { \n\ 30 | listen 3000; \n\ 31 | server_name localhost; \n\ 32 | root /usr/share/nginx/html; \n\ 33 | index index.html; \n\ 34 | \n\ 35 | location / { \n\ 36 | try_files $uri $uri/ /index.html; \n\ 37 | } \n\ 38 | }' > /etc/nginx/conf.d/default.conf 39 | 40 | # Expose port 3000 41 | EXPOSE 3000 42 | 43 | # Start Nginx 44 | CMD ["nginx", "-g", "daemon off;"] 45 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const mongoose = require('mongoose'); 3 | const session = require('express-session'); 4 | const passport = require('passport'); 5 | const bodyParser = require('body-parser'); 6 | require('dotenv').config(); 7 | const cors = require('cors'); 8 | 9 | // Passport configuration 10 | require('./config/passportConfig'); 11 | 12 | const app = express(); 13 | 14 | // CORS configuration 15 | app.use(cors('*')); 16 | 17 | // Middleware 18 | app.use(bodyParser.json()); 19 | app.use(session({ 20 | secret: process.env.SESSION_SECRET, 21 | resave: false, 22 | saveUninitialized: false, 23 | })); 24 | app.use(passport.initialize()); 25 | app.use(passport.session()); 26 | 27 | // Routes 28 | const authRoutes = require('./routes/auth'); 29 | app.use('/api/auth', authRoutes); 30 | 31 | // Connect to MongoDB 32 | mongoose.connect(process.env.MONGO_URI, {}).then(() => { 33 | console.log('Connected to MongoDB'); 34 | app.listen(process.env.PORT, () => { 35 | console.log(`Server running on port ${process.env.PORT}`); 36 | }); 37 | }).catch((err) => { 38 | console.log('MongoDB connection error:', err); 39 | }); 40 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from "./components/Navbar"; 2 | import Footer from "./components/Footer"; 3 | import ScrollProgressBar from "./components/ScrollProgressBar"; 4 | import { Toaster } from "react-hot-toast"; 5 | import Router from "./Routes/Router"; 6 | import ThemeWrapper from "./context/ThemeContext"; 7 | 8 | function App() { 9 | return ( 10 | 11 |
12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 | 39 |
40 |
41 | ); 42 | } 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /backend/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const passport = require("passport"); 3 | const User = require("../models/User"); 4 | const router = express.Router(); 5 | 6 | // Signup route 7 | router.post("/signup", async (req, res) => { 8 | 9 | const { username, email, password } = req.body; 10 | 11 | try { 12 | const existingUser = await User.findOne( {email} ); 13 | 14 | if (existingUser) 15 | return res.status(400).json( {message: 'User already exists'} ); 16 | 17 | const newUser = new User( {username, email, password} ); 18 | await newUser.save(); 19 | res.status(201).json( {message: 'User created successfully'} ); 20 | } catch (err) { 21 | res.status(500).json({ message: 'Error creating user', error: err.message }); 22 | } 23 | }); 24 | 25 | // Login route 26 | router.post("/login", passport.authenticate('local'), (req, res) => { 27 | res.status(200).json( { message: 'Login successful', user: req.user } ); 28 | }); 29 | 30 | // Logout route 31 | router.get("/logout", (req, res) => { 32 | 33 | req.logout((err) => { 34 | 35 | if (err) 36 | return res.status(500).json({ message: 'Logout failed', error: err.message }); 37 | else 38 | res.status(200).json({ message: 'Logged out successfully' }); 39 | }); 40 | }); 41 | 42 | module.exports = router; 43 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/config/passportConfig.js: -------------------------------------------------------------------------------- 1 | const passport = require("passport"); 2 | const LocalStrategy = require('passport-local').Strategy; 3 | const User = require("../models/User"); 4 | 5 | passport.use( 6 | new LocalStrategy( 7 | { usernameField: "email" }, 8 | async (email, password, done) => { 9 | try { 10 | const user = await User.findOne( {email} ); 11 | if (!user) { 12 | return done(null, false, { message: 'Email is invalid '}); 13 | } 14 | 15 | const isMatch = await user.comparePassword(password); 16 | if (!isMatch) { 17 | return done(null, false, { message: 'Invalid password' }); 18 | } 19 | 20 | return done(null, { 21 | id : user._id.toString(), 22 | username: user.username, 23 | email: user.email 24 | }); 25 | } catch (err) { 26 | return done(err); 27 | } 28 | } 29 | ) 30 | ); 31 | 32 | // Serialize user (store user info in session) 33 | passport.serializeUser((user, done) => { 34 | done(null, user.id); 35 | }); 36 | 37 | // Deserialize user (retrieve user from session) 38 | passport.deserializeUser(async (id, done) => { 39 | try { 40 | const user = await User.findById(id); 41 | done(null, user); 42 | } catch (err) { 43 | done(err, null); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /src/context/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | // src/ThemeContext.tsx 2 | import { createContext, useMemo, useState, useEffect, ReactNode } from 'react'; 3 | import { createTheme, ThemeProvider, Theme } from '@mui/material/styles'; 4 | 5 | interface ThemeContextType { 6 | mode: 'light' | 'dark'; 7 | toggleTheme: () => void; 8 | } 9 | 10 | export const ThemeContext = createContext(null); 11 | 12 | const THEME_STORAGE_KEY = 'theme'; 13 | 14 | const ThemeWrapper = ({ children }: { children: ReactNode }) => { 15 | const [mode, setMode] = useState<'light' | 'dark'>(() => { 16 | const savedMode = localStorage.getItem(THEME_STORAGE_KEY); 17 | return savedMode === 'dark' ? 'dark' : 'light'; 18 | }); 19 | 20 | // Sync mode with class and localStorage 21 | useEffect(() => { 22 | if (mode === 'dark') { 23 | document.documentElement.classList.add('dark'); 24 | } else { 25 | document.documentElement.classList.remove('dark'); 26 | } 27 | localStorage.setItem(THEME_STORAGE_KEY, mode); 28 | }, [mode]); 29 | 30 | const toggleTheme = () => { 31 | setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light')); 32 | }; 33 | 34 | const muiTheme: Theme = useMemo( 35 | () => createTheme({ palette: { mode } }), 36 | [mode] 37 | ); 38 | 39 | return ( 40 | 41 | 42 | {children} 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default ThemeWrapper; 49 | export type { ThemeContextType }; 50 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | frontend: 3 | image: frontend 4 | container_name: frontend-container 5 | build: 6 | context: . 7 | dockerfile: Dockerfile.dev 8 | ports: 9 | - "5173:5173" 10 | env_file: 11 | - .env 12 | volumes: 13 | - .:/app 14 | - /app/node_modules 15 | depends_on: 16 | - backend 17 | networks: 18 | - app-network-1 19 | profiles: 20 | - dev 21 | backend: 22 | image: backend 23 | container_name: backend-container 24 | build: 25 | context: ./backend 26 | dockerfile: Dockerfile.dev 27 | ports: 28 | - "5000:5000" 29 | env_file: 30 | - ./backend/.env 31 | volumes: 32 | - ./backend:/app 33 | - /app/node_modules 34 | networks: 35 | - app-network-1 36 | profiles: 37 | - dev 38 | frontend-prod: 39 | image: frontend-prod 40 | container_name: frontend-prod-container 41 | build: 42 | context: . 43 | dockerfile: Dockerfile.prod 44 | ports: 45 | - "3000:3000" 46 | env_file: 47 | - .env 48 | depends_on: 49 | - backend-prod 50 | networks: 51 | - app-network-2 52 | profiles: 53 | - prod 54 | backend-prod: 55 | image: backend-prod 56 | container_name: backend-prod-container 57 | build: 58 | context: backend 59 | dockerfile: Dockerfile.prod 60 | ports: 61 | - "5000:5000" 62 | env_file: 63 | - backend/.env 64 | networks: 65 | - app-network-2 66 | profiles: 67 | - prod 68 | networks: 69 | app-network-1: 70 | app-network-2: 71 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | ## 1. Purpose 4 | We aim to create an open and welcoming environment where contributors from all backgrounds feel valued and respected. 5 | This code of conduct outlines expectations for behavior to ensure everyone can contribute in a harassment-free environment. 6 | 7 | ## 2. Scope 8 | This Code of Conduct applies to all contributors, including maintainers and users. It applies to both project spaces and public spaces where an individual represents the project. 9 | 10 | ## 3. Our Standards 11 | In a welcoming environment, contributors should: 12 | 13 | Be kind and respectful to others. 14 | Collaborate with others in a constructive manner. 15 | Provide feedback in a respectful and considerate way. 16 | Be open to differing viewpoints and experiences. 17 | Show empathy and understanding towards others. 18 | Unacceptable behaviors include, but are not limited to: 19 | 20 | Use of derogatory comments, insults, or personal attacks. 21 | Harassment of any kind, including but not limited to: offensive comments related to gender, race, religion, or any other personal characteristics. 22 | The publication of private information without consent. 23 | Any behavior that could be perceived as discriminatory, intimidating, or threatening. 24 | 25 | ## 4. Enforcement 26 | Instances of unacceptable behavior may be reported to the project team. All complaints will be reviewed and investigated and will result in appropriate action. 27 | 28 | ## 5. Acknowledgment 29 | By contributing to Scribbie, you agree to adhere to this Code of Conduct and help create a safe, productive, and inclusive environment for all. 30 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { FaGithub } from 'react-icons/fa'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | function Footer() { 5 | return ( 6 |
7 |
8 |
9 | 20 |
21 | Contact Us 22 | About 23 |
24 |
25 |
26 |

27 | © {new Date().getFullYear()}{" "} 28 | GitHub Tracker. All rights reserved. 29 |

30 |
31 |
32 |
33 | ); 34 | } 35 | 36 | export default Footer; 37 | -------------------------------------------------------------------------------- /src/components/Hero.tsx: -------------------------------------------------------------------------------- 1 | import { Search } from 'lucide-react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const Hero = () => { 5 | return ( 6 |
7 |
8 |
9 |

10 | Track GitHub Activity 11 | Like Never Before 12 |

13 |

14 | Monitor and analyze GitHub user activity with powerful insights. Perfect for developers, 15 | project managers, and teams who want to understand contribution patterns and repository engagement. 16 |

17 |
18 | 22 | {/* 23 | */} 27 |
28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default Hero; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-react-tailwind-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview", 11 | "docker:dev": "docker compose --profile dev up --build", 12 | "docker:prod": "docker compose --profile prod up -d --build" 13 | }, 14 | "dependencies": { 15 | "@emotion/react": "^11.11.3", 16 | "@emotion/styled": "^11.11.0", 17 | "@mui/icons-material": "^5.15.6", 18 | "@mui/material": "^5.15.6", 19 | "@primer/octicons-react": "^19.15.5", 20 | "@vitejs/plugin-react": "^4.3.3", 21 | "axios": "^1.7.7", 22 | "framer-motion": "^12.23.12", 23 | "lucide-react": "^0.525.0", 24 | "octokit": "^4.0.2", 25 | "postcss": "^8.4.47", 26 | "react": "^18.3.1", 27 | "react-dom": "^18.3.1", 28 | "react-hot-toast": "^2.4.1", 29 | "react-icons": "^5.3.0", 30 | "react-router-dom": "^6.28.0", 31 | "tailwindcss": "^3.4.14" 32 | }, 33 | "devDependencies": { 34 | "@eslint/js": "^9.13.0", 35 | "@types/jasmine": "^5.1.8", 36 | "@types/node": "^22.10.1", 37 | "@types/react": "^18.3.23", 38 | "@types/react-dom": "^18.3.7", 39 | "@types/react-redux": "^7.1.34", 40 | "@types/react-router-dom": "^5.3.3", 41 | "@vitejs/plugin-react-swc": "^3.5.0", 42 | "autoprefixer": "^10.4.20", 43 | "bcryptjs": "^3.0.2", 44 | "eslint": "^9.13.0", 45 | "eslint-plugin-react": "^7.37.2", 46 | "eslint-plugin-react-hooks": "^5.0.0", 47 | "eslint-plugin-react-refresh": "^0.4.14", 48 | "express-session": "^1.18.2", 49 | "globals": "^15.11.0", 50 | "jasmine": "^5.9.0", 51 | "passport": "^0.7.0", 52 | "passport-local": "^1.0.0", 53 | "supertest": "^7.1.4", 54 | "vite": "^5.4.10" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ------------------------------------- 2 | # Node & Dependency Files 3 | # ------------------------------------- 4 | node_modules/ 5 | package-lock.json 6 | pnpm-lock.yaml 7 | yarn.lock 8 | 9 | # ------------------------------------- 10 | # Build Output 11 | # ------------------------------------- 12 | dist/ 13 | build/ 14 | backend/dist/ 15 | backend/build/ 16 | 17 | # ------------------------------------- 18 | # Vite 19 | # ------------------------------------- 20 | .vite/ 21 | 22 | # ------------------------------------- 23 | # Environment Variables 24 | # ------------------------------------- 25 | .env 26 | .env.*.local 27 | 28 | # ------------------------------------- 29 | # Logs & Debug Files 30 | # ------------------------------------- 31 | logs/ 32 | *.log 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | pnpm-debug.log* 37 | lerna-debug.log* 38 | 39 | # ------------------------------------- 40 | # Temporary Files & Caches 41 | # ------------------------------------- 42 | tmp/ 43 | temp/ 44 | *.tsbuildinfo 45 | .npm/ 46 | .yarn/ 47 | .yarn-integrity 48 | .vscode/.tsbuildinfo 49 | 50 | # ------------------------------------- 51 | # Coverage Reports 52 | # ------------------------------------- 53 | coverage/ 54 | 55 | # ------------------------------------- 56 | # Docker 57 | # ------------------------------------- 58 | *.local 59 | docker-compose.override.yml 60 | 61 | # ------------------------------------- 62 | # Editor/IDE Files 63 | # ------------------------------------- 64 | .vscode/ 65 | !.vscode/extensions.json 66 | .idea/ 67 | *.sublime-workspace 68 | *.sublime-project 69 | 70 | # Visual Studio 71 | *.suo 72 | *.ntvs* 73 | *.njsproj 74 | *.sln 75 | *.sw? 76 | 77 | # ------------------------------------- 78 | # OS-Specific & Misc 79 | # ------------------------------------- 80 | .DS_Store 81 | Thumbs.db 82 | *.swp 83 | .AppleDouble 84 | -------------------------------------------------------------------------------- /spec/user.model.spec.cjs: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const bcrypt = require('bcryptjs'); 3 | const User = require('../backend/models/User'); 4 | 5 | describe('User Model', () => { 6 | beforeAll(async () => { 7 | await mongoose.connect('mongodb://127.0.0.1:27017/github_tracker_test', { 8 | useNewUrlParser: true, 9 | useUnifiedTopology: true, 10 | }); 11 | }); 12 | 13 | afterAll(async () => { 14 | await mongoose.connection.db.dropDatabase(); 15 | await mongoose.disconnect(); 16 | }); 17 | 18 | afterEach(async () => { 19 | await User.deleteMany({}); 20 | }); 21 | 22 | it('should create a user with hashed password', async () => { 23 | const userData = { username: 'testuser', email: 'test@example.com', password: 'password123' }; 24 | const user = new User(userData); 25 | await user.save(); 26 | expect(user.password).not.toBe(userData.password); 27 | const isMatch = await bcrypt.compare('password123', user.password); 28 | expect(isMatch).toBeTrue(); 29 | }); 30 | 31 | it('should not hash password again if not modified', async () => { 32 | const userData = { username: 'testuser2', email: 'test2@example.com', password: 'password123' }; 33 | const user = new User(userData); 34 | await user.save(); 35 | const originalHash = user.password; 36 | user.username = 'updateduser'; 37 | await user.save(); 38 | expect(user.password).toBe(originalHash); 39 | }); 40 | 41 | it('should compare passwords correctly', async () => { 42 | const userData = { username: 'testuser3', email: 'test3@example.com', password: 'password123' }; 43 | const user = new User(userData); 44 | await user.save(); 45 | const isMatch = await user.comparePassword('password123'); 46 | expect(isMatch).toBeTrue(); 47 | const isNotMatch = await user.comparePassword('wrongpassword'); 48 | expect(isNotMatch).toBeFalse(); 49 | }); 50 | }); -------------------------------------------------------------------------------- /src/components/HowItWorks.tsx: -------------------------------------------------------------------------------- 1 | 2 | const HowItWorks = () => { 3 | const steps = [ 4 | { 5 | number: 1, 6 | title: 'Search Users', 7 | description: 'Enter GitHub usernames or search for users by name. Add them to your tracking dashboard.' 8 | }, 9 | { 10 | number: 2, 11 | title: 'Monitor Activity', 12 | description: 'Watch insights of commits, pull requests, issues, and other GitHub activities.' 13 | }, 14 | { 15 | number: 3, 16 | title: 'Analyze Insights', 17 | description: 'Review detailed analytics, export reports, and gain valuable insights into development patterns.' 18 | } 19 | ]; 20 | 21 | return ( 22 |
23 |
24 |
25 |

How It Works

26 |

27 | Get started in minutes with our simple three-step process 28 |

29 |
30 | 31 |
32 | {steps.map((step, index) => ( 33 |
34 |
35 | {step.number} 36 |
37 |

{step.title}

38 |

39 | {step.description} 40 |

41 |
42 | ))} 43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default HowItWorks; 50 | -------------------------------------------------------------------------------- /src/hooks/useGitHubData.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | 3 | export const useGitHubData = (getOctokit: () => any) => { 4 | const [issues, setIssues] = useState([]); 5 | const [prs, setPrs] = useState([]); 6 | const [loading, setLoading] = useState(false); 7 | const [error, setError] = useState(''); 8 | const [totalIssues, setTotalIssues] = useState(0); 9 | const [totalPrs, setTotalPrs] = useState(0); 10 | const [rateLimited, setRateLimited] = useState(false); 11 | 12 | const fetchPaginated = async (octokit: any, username: string, type: string, page = 1, per_page = 10) => { 13 | const q = `author:${username} is:${type}`; 14 | const response = await octokit.request('GET /search/issues', { 15 | q, 16 | sort: 'created', 17 | order: 'desc', 18 | per_page, 19 | page, 20 | }); 21 | 22 | return { 23 | items: response.data.items, 24 | total: response.data.total_count, 25 | }; 26 | }; 27 | 28 | const fetchData = useCallback( 29 | async (username: string, page = 1, perPage = 10) => { 30 | 31 | const octokit = getOctokit(); 32 | 33 | if (!octokit || !username || rateLimited) return; 34 | 35 | setLoading(true); 36 | setError(''); 37 | 38 | try { 39 | const [issueRes, prRes] = await Promise.all([ 40 | fetchPaginated(octokit, username, 'issue', page, perPage), 41 | fetchPaginated(octokit, username, 'pr', page, perPage), 42 | ]); 43 | 44 | setIssues(issueRes.items); 45 | setPrs(prRes.items); 46 | setTotalIssues(issueRes.total); 47 | setTotalPrs(prRes.total); 48 | } catch (err: any) { 49 | if (err.status === 403) { 50 | setError('GitHub API rate limit exceeded. Please wait or use a token.'); 51 | setRateLimited(true); // Prevent further fetches 52 | } else { 53 | setError(err.message || 'Failed to fetch data'); 54 | } 55 | } finally { 56 | setLoading(false); 57 | } 58 | }, 59 | [getOctokit, rateLimited] 60 | ); 61 | 62 | return { 63 | issues, 64 | prs, 65 | totalIssues, 66 | totalPrs, 67 | loading, 68 | error, 69 | fetchData, 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /src/components/ScrollProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const ScrollProgressBar = () => { 4 | const [scrollWidth, setScrollWidth] = useState(0); 5 | const [isAnimating, setIsAnimating] = useState(true); // Tracks if the page load animation is active 6 | 7 | useEffect(() => { 8 | // Simulate the page load animation 9 | const animationTimeout = setTimeout(() => { 10 | setIsAnimating(false); // End the animation after 2 seconds 11 | }, 2000); 12 | 13 | // Clean up timeout 14 | return () => clearTimeout(animationTimeout); 15 | }, []); 16 | 17 | const handleScroll = () => { 18 | const scrollTop = document.documentElement.scrollTop; 19 | const scrollHeight = 20 | document.documentElement.scrollHeight - 21 | document.documentElement.clientHeight; 22 | const width = (scrollTop / scrollHeight) * 100; 23 | setScrollWidth(width); 24 | }; 25 | 26 | useEffect(() => { 27 | if (!isAnimating) { 28 | window.addEventListener("scroll", handleScroll); 29 | } 30 | return () => window.removeEventListener("scroll", handleScroll); 31 | }, [isAnimating]); 32 | 33 | return ( 34 | <> 35 | {/* Left-to-right animation during page load */} 36 | {isAnimating && ( 37 |
49 | )} 50 | 51 | {/* Scroll progress bar after animation ends */} 52 | {!isAnimating && ( 53 |
65 | )} 66 | 67 | {/* Animation Keyframes */} 68 | 80 | 81 | ); 82 | }; 83 | 84 | export default ScrollProgressBar; 85 | -------------------------------------------------------------------------------- /src/pages/ContributorProfile/ContributorProfile.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom"; 2 | import { useEffect, useState } from "react"; 3 | import toast from "react-hot-toast"; 4 | 5 | type PR = { 6 | title: string; 7 | html_url: string; 8 | repository_url: string; 9 | }; 10 | 11 | export default function ContributorProfile() { 12 | const { username } = useParams(); 13 | const [profile, setProfile] = useState(null); 14 | const [prs, setPRs] = useState([]); 15 | const [loading, setLoading] = useState(true); 16 | 17 | useEffect(() => { 18 | async function fetchData() { 19 | if (!username) return; 20 | 21 | try { 22 | const userRes = await fetch(`https://api.github.com/users/${username}`); 23 | const userData = await userRes.json(); 24 | setProfile(userData); 25 | 26 | const prsRes = await fetch( 27 | `https://api.github.com/search/issues?q=author:${username}+type:pr` 28 | ); 29 | const prsData = await prsRes.json(); 30 | setPRs(prsData.items); 31 | } catch (error) { 32 | toast.error("Failed to fetch user data."); 33 | } finally { 34 | setLoading(false); 35 | } 36 | } 37 | 38 | fetchData(); 39 | }, [username]); 40 | 41 | const handleCopyLink = () => { 42 | navigator.clipboard.writeText(window.location.href); 43 | toast.success("🔗 Shareable link copied to clipboard!"); 44 | }; 45 | 46 | if (loading) return
Loading...
; 47 | 48 | if (!profile) 49 | return ( 50 |
User not found.
51 | ); 52 | 53 | return ( 54 |
55 |
56 | Avatar 61 |

{profile.login}

62 |

{profile.bio}

63 | 69 |
70 | 71 |

Pull Requests

72 | {prs.length > 0 ? ( 73 |
    74 | {prs.map((pr, i) => { 75 | const repoName = pr.repository_url.split("/").slice(-2).join("/"); 76 | return ( 77 |
  • 78 | 84 | [{repoName}] {pr.title} 85 | 86 |
  • 87 | ); 88 | })} 89 |
90 | ) : ( 91 |

No pull requests found for this user.

92 | )} 93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/components/Features.tsx: -------------------------------------------------------------------------------- 1 | import { BarChart3, Users, Search, Zap, Shield, Globe } from 'lucide-react'; 2 | 3 | const Features = () => { 4 | const features = [ 5 | { 6 | icon: BarChart3, 7 | title: 'Activity Analytics', 8 | description: 'Comprehensive charts and graphs showing commit patterns, contribution streaks, and repository activity over time.', 9 | bgColor: 'bg-blue-100', 10 | iconColor: 'text-blue-600' 11 | }, 12 | { 13 | icon: Users, 14 | title: 'Multi-User Tracking', 15 | description: 'Monitor multiple GitHub users simultaneously and compare their activity levels and contribution patterns.', 16 | bgColor: 'bg-green-100', 17 | iconColor: 'text-green-600' 18 | }, 19 | { 20 | icon: Search, 21 | title: 'Smart Search', 22 | description: 'Quickly find and add users to your tracking list with intelligent search and auto-suggestions.', 23 | bgColor: 'bg-purple-100', 24 | iconColor: 'text-purple-600' 25 | }, 26 | { 27 | icon: Zap, 28 | title: 'Real-time Updates', 29 | description: 'Get instant notifications and updates when tracked users make new contributions or repositories.', 30 | bgColor: 'bg-orange-100', 31 | iconColor: 'text-orange-600' 32 | }, 33 | { 34 | icon: Shield, 35 | title: 'Privacy First', 36 | description: 'All data is fetched from public GitHub APIs. We don\'t store personal information or require GitHub access.', 37 | bgColor: 'bg-red-100', 38 | iconColor: 'text-red-600' 39 | }, 40 | { 41 | icon: Globe, 42 | title: 'Export & Share', 43 | description: 'Export activity reports and share insights with your team through various formats and integrations.', 44 | bgColor: 'bg-indigo-100', 45 | iconColor: 'text-indigo-600' 46 | } 47 | ]; 48 | 49 | return ( 50 |
51 |
52 |
53 |

Powerful Features

54 |

55 | Everything you need to track, analyze, and understand GitHub activity patterns 56 |

57 |
58 | 59 |
60 | {features.map((feature, index) => { 61 | const IconComponent = feature.icon; 62 | return ( 63 |
64 |
65 | 66 |
67 |

{feature.title}

68 |

69 | {feature.description} 70 |

71 |
72 | ); 73 | })} 74 |
75 |
76 |
77 | ); 78 | }; 79 | 80 | export default Features; 81 | -------------------------------------------------------------------------------- /spec/auth.routes.spec.cjs: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const express = require('express'); 3 | const request = require('supertest'); 4 | const session = require('express-session'); 5 | const passport = require('passport'); 6 | const User = require('../backend/models/User'); 7 | const authRoutes = require('../backend/routes/auth'); 8 | 9 | // Setup Express app for testing 10 | function createTestApp() { 11 | const app = express(); 12 | app.use(express.json()); 13 | app.use(session({ secret: 'test', resave: false, saveUninitialized: false })); 14 | app.use(passport.initialize()); 15 | app.use(passport.session()); 16 | require('../backend/config/passportConfig'); 17 | app.use('/auth', authRoutes); 18 | return app; 19 | } 20 | 21 | describe('Auth Routes', () => { 22 | let app; 23 | 24 | beforeAll(async () => { 25 | await mongoose.connect('mongodb://127.0.0.1:27017/github_tracker_test', { 26 | useNewUrlParser: true, 27 | useUnifiedTopology: true, 28 | }); 29 | app = createTestApp(); 30 | }); 31 | 32 | afterAll(async () => { 33 | await mongoose.connection.db.dropDatabase(); 34 | await mongoose.disconnect(); 35 | }); 36 | 37 | afterEach(async () => { 38 | await User.deleteMany({}); 39 | }); 40 | 41 | it('should sign up a new user', async () => { 42 | const res = await request(app) 43 | .post('/auth/signup') 44 | .send({ username: 'testuser', email: 'test@example.com', password: 'password123' }); 45 | expect(res.status).toBe(201); 46 | expect(res.body.message).toBe('User created successfully'); 47 | const user = await User.findOne({ email: 'test@example.com' }); 48 | expect(user).toBeTruthy(); 49 | }); 50 | 51 | it('should not sign up a user with existing email', async () => { 52 | await new User({ username: 'testuser', email: 'test@example.com', password: 'password123' }).save(); 53 | const res = await request(app) 54 | .post('/auth/signup') 55 | .send({ username: 'testuser2', email: 'test@example.com', password: 'password456' }); 56 | expect(res.status).toBe(400); 57 | expect(res.body.message).toBe('User already exists'); 58 | }); 59 | 60 | it('should login a user with correct credentials', async () => { 61 | await request(app) 62 | .post('/auth/signup') 63 | .send({ username: 'testuser', email: 'test@example.com', password: 'password123' }); 64 | const agent = request.agent(app); 65 | const res = await agent 66 | .post('/auth/login') 67 | .send({ email: 'test@example.com', password: 'password123' }); 68 | expect(res.status).toBe(200); 69 | expect(res.body.message).toBe('Login successful'); 70 | expect(res.body.user.email).toBe('test@example.com'); 71 | }); 72 | 73 | it('should not login a user with wrong password', async () => { 74 | await request(app) 75 | .post('/auth/signup') 76 | .send({ username: 'testuser', email: 'test@example.com', password: 'password123' }); 77 | const agent = request.agent(app); 78 | const res = await agent 79 | .post('/auth/login') 80 | .send({ email: 'test@example.com', password: 'wrongpassword' }); 81 | expect(res.status).toBe(401); 82 | }); 83 | 84 | it('should logout a logged-in user', async () => { 85 | await request(app) 86 | .post('/auth/signup') 87 | .send({ username: 'testuser', email: 'test@example.com', password: 'password123' }); 88 | const agent = request.agent(app); 89 | await agent 90 | .post('/auth/login') 91 | .send({ email: 'test@example.com', password: 'password123' }); 92 | const res = await agent.get('/auth/logout'); 93 | expect(res.status).toBe(200); 94 | expect(res.body.message).toBe('Logged out successfully'); 95 | }); 96 | }); -------------------------------------------------------------------------------- /.github/workflows/unassign-stale-issues.yml: -------------------------------------------------------------------------------- 1 | name: Unassign Issues with No PR After 7 Days 2 | 3 | on: 4 | schedule: 5 | - cron: '0 2 * * *' # Runs daily at 2 AM UTC 6 | workflow_dispatch: # Manual trigger support 7 | 8 | jobs: 9 | unassign-stale-issues: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check for stale assigned issues 13 | uses: actions/github-script@v7 14 | with: 15 | github-token: ${{ secrets.PAT }} 16 | script: | 17 | const daysBeforeUnassign = 7; 18 | const now = new Date(); 19 | 20 | // Get all open issues with assignees 21 | const issues = await github.paginate(github.rest.issues.listForRepo, { 22 | owner: context.repo.owner, 23 | repo: context.repo.repo, 24 | state: 'open', 25 | assignee: '*' 26 | }); 27 | 28 | for (const issue of issues) { 29 | if (issue.pull_request) continue; // Skip PRs 30 | 31 | // Get timeline events to find assignment and PR link events 32 | const timeline = await github.paginate(github.rest.issues.listEventsForTimeline, { 33 | owner: context.repo.owner, 34 | repo: context.repo.repo, 35 | issue_number: issue.number, 36 | mediaType: { previews: ["mockingbird"] } 37 | }); 38 | 39 | // Find the first assignment event date 40 | const assignedEvent = timeline.filter(e => e.event === 'assigned').at(-1);// latest 41 | if (!assignedEvent) { 42 | // No assignment event found, skip issue 43 | continue; 44 | } 45 | const assignedAt = new Date(assignedEvent.created_at); 46 | // Find the first PR linked event date 47 | const prLinkedEvent = timeline 48 | .filter( 49 | e => 50 | ['connected', 'cross-referenced'].includes(e.event) && 51 | new Date(e.created_at) >= assignedAt 52 | ) 53 | .at(0); // earliest link *after* assignment 54 | const prLinkedAt = prLinkedEvent ? new Date(prLinkedEvent.created_at) : null; 55 | 56 | // Calculate days since assignment 57 | const daysSinceAssignment = (now - assignedAt) / (1000 * 60 * 60 * 24); 58 | 59 | // Check if issue is still assigned (in case assignees changed later) 60 | const currentlyAssigned = issue.assignees.length > 0; 61 | 62 | if (currentlyAssigned && daysSinceAssignment >= daysBeforeUnassign) { 63 | // If PR not linked or linked after 7 days 64 | if (!prLinkedAt || prLinkedAt > new Date(assignedAt.getTime() + daysBeforeUnassign * 24 * 60 * 60 * 1000)) { 65 | console.log(`Unassigning issue #${issue.number} - no PR linked within ${daysBeforeUnassign} days of assignment.`); 66 | 67 | // Remove assignees 68 | await github.rest.issues.removeAssignees({ 69 | owner: context.repo.owner, 70 | repo: context.repo.repo, 71 | issue_number: issue.number, 72 | assignees: issue.assignees.map(user => user.login) 73 | }); 74 | 75 | // Post comment 76 | await github.rest.issues.createComment({ 77 | owner: context.repo.owner, 78 | repo: context.repo.repo, 79 | issue_number: issue.number, 80 | body: `⏰ This issue was automatically unassigned because no pull request was linked within ${daysBeforeUnassign} days of assignment. Please request reassignment if you are still working on it.` 81 | }); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/pages/About/About.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import { Lightbulb, Users, Settings, Search } from "lucide-react"; 3 | 4 | const features = [ 5 | { 6 | icon: , 7 | title: "Simple Issue Tracking", 8 | description: "Track your GitHub issues seamlessly with intuitive filters and search options.", 9 | }, 10 | { 11 | icon: , 12 | title: "Team Collaboration", 13 | description: "Collaborate with your team in real-time, manage issues and pull requests effectively.", 14 | }, 15 | { 16 | icon: , 17 | title: "Customizable Settings", 18 | description: "Customize your issue tracking workflow to match your team's needs.", 19 | }, 20 | ]; 21 | 22 | const About = () => { 23 | return ( 24 |
25 | 26 | {/* Hero Section */} 27 |
28 | 34 | About Us 35 | 36 | 42 | Welcome to GitHub Tracker — your smart solution to manage GitHub issues without chaos. 43 | 44 |
45 | 46 | {/* Mission Section */} 47 |
48 | 53 | 54 |

Our Mission

55 |

56 | We aim to provide an efficient and user-friendly way to track GitHub issues and pull requests. 57 | Our goal is to make it easy for developers to stay organized and focused on their projects 58 | without getting bogged down by the details. 59 |

60 |
61 |
62 | 63 | {/* Features Section */} 64 |
65 |

What We Do

66 |
67 | {features.map((feature, idx) => ( 68 | 76 |
{feature.icon}
77 |

{feature.title}

78 |

{feature.description}

79 |
80 | ))} 81 |
82 |
83 |
84 | ); 85 | }; 86 | 87 | export default About; 88 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 🌟 Contributing to GitHub Tracker 2 | 3 | Thank you for showing interest in **GitHub Tracker**! 🚀 4 | 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! 5 | 6 |
7 | 8 | ## 🧑‍⚖️ Code of Conduct 9 | 10 | 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. 11 | 12 |
13 | 14 | ## 🛠 Project Structure 15 | 16 | ```bash 17 | github_tracker/ 18 | ├── backend/ # Node.js + Express backend 19 | │ ├── routes/ # API routes 20 | │ ├── controllers/ # Logic handlers 21 | │ └── index.js # Entry point for server 22 | │ 23 | ├── frontend/ # React + Vite frontend 24 | │ ├── components/ # Reusable UI components 25 | │ ├── pages/ # Main pages/routes 26 | │ └── main.jsx # Root file 27 | │ 28 | ├── public/ # Static assets like images 29 | │ 30 | ├── .gitignore 31 | ├── README.md 32 | ├── package.json 33 | ├── tailwind.config.js 34 | └── CONTRIBUTING.md 35 | ``` 36 | 37 | --- 38 | 39 | ## 🤝 How to Contribute 40 | 41 | ### 🧭 First-Time Contribution Steps 42 | 43 | 1. **Fork the Repository** 🍴 44 | Click "Fork" to create your own copy under your GitHub account. 45 | 46 | 2. **Clone Your Fork** 📥 47 | ```bash 48 | git clone https://github.com//github_tracker.git 49 | ``` 50 | 51 | 3. **Navigate to the Project Folder** 📁 52 | ```bash 53 | cd github_tracker 54 | ``` 55 | 56 | 4. **Create a New Branch** 🌿 57 | ```bash 58 | git checkout -b your-feature-name 59 | ``` 60 | 61 | 5. **Make Your Changes** ✍ 62 | After modifying files, stage and commit: 63 | 64 | ```bash 65 | git add . 66 | git commit -m "✨ Added [feature/fix]: your message" 67 | ``` 68 | 69 | 6. **Push Your Branch to GitHub** 🚀 70 | ```bash 71 | git push origin your-feature-name 72 | ``` 73 | 74 | 7. **Open a Pull Request** 🔁 75 | Go to the original repo and click **Compare & pull request**. 76 | 77 | --- 78 | 79 | ## 🚦 Pull Request Guidelines 80 | 81 | ### **Split Big Changes into Multiple Commits** 82 | - When making large or complex changes, break them into smaller, logical commits. 83 | - Each commit should represent a single purpose or unit of change (e.g. refactoring, adding a feature, fixing a bug). 84 | --- 85 | - ✅ Ensure your code builds and runs without errors. 86 | - 🧪 Include tests where applicable. 87 | - 💬 Add comments if the logic is non-trivial. 88 | - 📸 Attach screenshots for UI-related changes. 89 | - 🔖 Use meaningful commit messages and titles. 90 | 91 | --- 92 | 93 | ## 🐞 Reporting Issues 94 | 95 | If you discover a bug or have a suggestion: 96 | 97 | ➡️ [Open an Issue](https://github.com/GitMetricsLab/github_tracker/issues/new/choose) 98 | 99 | Please include: 100 | 101 | - **Steps to Reproduce** 102 | - **Expected vs. Actual Behavior** 103 | - **Screenshots/Logs (if any)** 104 | 105 | --- 106 | 107 | ## 🧠 Good Coding Practices 108 | 109 | 1. **Consistent Style** 110 | Stick to the project's linting and formatting conventions (e.g., ESLint, Prettier, Tailwind classes). 111 | 112 | 2. **Meaningful Naming** 113 | Use self-explanatory names for variables and functions. 114 | 115 | 3. **Avoid Duplication** 116 | Keep your code DRY (Don't Repeat Yourself). 117 | 118 | 4. **Testing** 119 | Add unit or integration tests for any new logic. 120 | 121 | 5. **Review Others’ PRs** 122 | Help others by reviewing their PRs too! 123 | 124 | --- 125 | 126 | ## 🙌 Thank You! 127 | 128 | 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. 129 | 130 | **Happy Coding!** 💻🚀 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌟 **GitHub Tracker** 🌟 2 | 3 | 4 | **Track Activity of Users on GitHub** 5 | 6 | Welcome to **GitHub Tracker**, a web app designed to help you monitor and analyze the activity of GitHub users. Whether you’re a developer, a project manager, or just curious, this tool simplifies tracking contributions and activity across repositories! 🚀👩‍💻 7 | 8 |

9 | github-tracker 10 |

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
🌟 Stars🍴 Forks🐛 Issues🔔 Open PRs🔕 Close PRs
StarsForksIssuesOpen Pull RequestsClosed Pull Requests
31 | 32 | --- 33 | 34 | ## 🛠️ Tech Stack 35 | 36 | - **Frontend**: React.js + Vite 37 | - **Styling**: TailwindCSS + Material UI 38 | - **Data Fetching**: Axios + React Query 39 | - **Backend**: Node.js + Express 40 | 41 | --- 42 | 43 | ## 🚀 Setup Guide 44 | 1. Clone the repository to your local machine: 45 | ```bash 46 | $ git clone https://github.com/yourusername/github-tracker.git 47 | ``` 48 | 49 | 2. Navigate to the project directory: 50 | ```bash 51 | $ cd github-tracker 52 | ``` 53 | 54 | 3. Run the frontend 55 | ```bash 56 | $ npm i 57 | $ npm run dev 58 | ``` 59 | 60 | 4. Run the backend 61 | ```bash 62 | $ npm i 63 | $ npm start 64 | ``` 65 | 66 | ## 🧪 Backend Unit & Integration Testing with Jasmine 67 | 68 | This project uses the Jasmine framework for backend unit and integration tests. The tests cover: 69 | - User model (password hashing, schema, password comparison) 70 | - Authentication routes (signup, login, logout) 71 | - Passport authentication logic (via integration tests) 72 | 73 | ### Prerequisites 74 | - **Node.js** and **npm** installed 75 | - **MongoDB** running locally (default: `mongodb://127.0.0.1:27017`) 76 | 77 | ### Installation 78 | Install all required dependencies: 79 | ```sh 80 | npm install 81 | npm install --save-dev jasmine @types/jasmine supertest express-session passport passport-local bcryptjs 82 | ``` 83 | 84 | ### Running the Tests 85 | 1. **Start MongoDB** (if not already running): 86 | ```sh 87 | mongod 88 | ``` 89 | 2. **Run Jasmine tests:** 90 | ```sh 91 | npx jasmine 92 | ``` 93 | 94 | ### Test Files 95 | - `spec/user.model.spec.cjs` — Unit tests for the User model 96 | - `spec/auth.routes.spec.cjs` — Integration tests for authentication routes 97 | 98 | ### Jasmine Configuration 99 | The Jasmine config (`spec/support/jasmine.mjs`) is set to recognize `.cjs`, `.js`, and `.mjs` test files: 100 | ```js 101 | spec_files: [ 102 | "**/*[sS]pec.?(m)js", 103 | "**/*[sS]pec.cjs" 104 | ] 105 | ``` 106 | 107 | ### Troubleshooting 108 | - **No specs found:** Ensure your test files have the correct extension and are in the `spec/` directory. 109 | - **MongoDB connection errors:** Make sure MongoDB is running and accessible. 110 | - **Missing modules:** Install any missing dev dependencies with `npm install --save-dev `. 111 | 112 | ### What Was Covered 113 | - Jasmine is set up and configured for backend testing. 114 | - All major backend modules are covered by unit/integration tests. 115 | - Tests are passing and verified. 116 | 117 | --- 118 | 119 | [![Star History Chart](https://api.star-history.com/svg?repos=GitMetricsLab/github_tracker&type=Date)](https://www.star-history.com/#GitMetricsLab/github_tracker&Date) 120 | 121 | --- 122 | 123 | # 👀 Our Contributors 124 | 125 | - We extend our heartfelt gratitude for your invaluable contribution to our project. 126 | - Make sure you show some love by giving ⭐ to our repository. 127 | 128 |
129 | 130 | 131 | 132 |
133 | 134 | 135 | 136 | --- 137 | 138 |

139 | 140 | ⬆️ Back to Top 141 | 142 |

143 | -------------------------------------------------------------------------------- /src/pages/Contributors/Contributors.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { 3 | Container, 4 | Grid, 5 | Card, 6 | CardContent, 7 | Avatar, 8 | Typography, 9 | Button, 10 | Box, 11 | CircularProgress, 12 | Alert, 13 | } from "@mui/material"; 14 | import { FaGithub } from "react-icons/fa"; 15 | import { Link } from "react-router-dom"; 16 | import axios from "axios"; 17 | import { GITHUB_REPO_CONTRIBUTORS_URL } from "../../utils/constants"; 18 | 19 | interface Contributor { 20 | id: number; 21 | login: string; 22 | avatar_url: string; 23 | contributions: number; 24 | html_url: string; 25 | } 26 | 27 | const ContributorsPage = () => { 28 | const [contributors, setContributors] = useState([]); 29 | const [loading, setLoading] = useState(true); 30 | const [error, setError] = useState(null); 31 | 32 | // Fetch contributors from GitHub API 33 | useEffect(() => { 34 | const fetchContributors = async () => { 35 | try { 36 | const response = await axios.get(GITHUB_REPO_CONTRIBUTORS_URL, { 37 | withCredentials: false, 38 | }); 39 | setContributors(response.data); 40 | } catch (err) { 41 | setError("Failed to fetch contributors. Please try again later."); 42 | } finally { 43 | setLoading(false); 44 | } 45 | }; 46 | 47 | fetchContributors(); 48 | }, []); 49 | 50 | if (loading) { 51 | return ( 52 | 53 | 54 | 55 | ); 56 | } 57 | 58 | if (error) { 59 | return ( 60 | 61 | {error} 62 | 63 | ); 64 | } 65 | 66 | return ( 67 |
68 | 69 | 70 | 🤝 Contributors 71 | 72 | 73 | 74 | {contributors.map((contributor) => ( 75 | 76 | 92 | 96 | 101 | 102 | 103 | {contributor.login} 104 | 105 | 106 | 107 | {contributor.contributions} Contributions 108 | 109 | {/* 110 | 111 | Thank you for your valuable contributions to our 112 | community! 113 | */} 114 | 115 | 116 | 117 | 118 | 134 | 135 | 136 | 137 | ))} 138 | 139 | 140 |
141 | ); 142 | }; 143 | 144 | export default ContributorsPage; 145 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { useState, useContext } from "react"; 3 | import { ThemeContext } from "../context/ThemeContext"; 4 | import { Moon, Sun } from 'lucide-react'; 5 | 6 | 7 | const Navbar: React.FC = () => { 8 | 9 | const [isOpen, setIsOpen] = useState(false); 10 | const themeContext = useContext(ThemeContext); 11 | 12 | if (!themeContext) 13 | return null; 14 | 15 | const { toggleTheme, mode } = themeContext; 16 | 17 | return ( 18 | 140 | ); 141 | }; 142 | 143 | export default Navbar; 144 | -------------------------------------------------------------------------------- /src/pages/Signup/Signup.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import axios from "axios"; 3 | import { useNavigate ,Link } from "react-router-dom"; 4 | import { User, Mail, Lock } from "lucide-react"; 5 | const backendUrl = import.meta.env.VITE_BACKEND_URL; 6 | interface SignUpFormData { 7 | username: string; 8 | email: string; 9 | password: string; 10 | } 11 | 12 | const SignUp: React.FC = () => { 13 | const [formData, setFormData] = useState({ 14 | username: "", 15 | email: "", 16 | password: "" 17 | }); 18 | const [message, setMessage] = useState(""); 19 | const navigate = useNavigate(); 20 | const handleChange = (e: React.ChangeEvent) => { 21 | const { name, value } = e.target; 22 | setFormData({ ...formData, [name]: value }); 23 | }; 24 | 25 | const handleSubmit = async (e: React.FormEvent) => { 26 | e.preventDefault(); 27 | try { 28 | const response = await axios.post(`${backendUrl}/api/auth/signup`, 29 | formData // Include cookies for session 30 | ); 31 | setMessage(response.data.message); // Show success message from backend 32 | 33 | // Navigate to login page after successful signup 34 | if (response.data.message === 'User created successfully') { 35 | navigate("/login");} 36 | 37 | 38 | // // Simulate API call (replace with your actual backend integration) 39 | // try { 40 | // // Mock successful signup 41 | // setMessage("Account created successfully! Redirecting to login..."); 42 | 43 | // // In your actual implementation, integrate with your backend here: 44 | // // const response = await fetch(`${backendUrl}/api/auth/signup`, { 45 | // // method: 'POST', 46 | // // headers: { 'Content-Type': 'application/json' }, 47 | // // body: JSON.stringify(formData) 48 | // // }); 49 | 50 | // setTimeout(() => { 51 | // // Navigate to login page in your actual implementation 52 | // console.log("Redirecting to login page..."); 53 | // }, 2000); 54 | 55 | } catch (error) { 56 | setMessage("Something went wrong. Please try again."); 57 | } 58 | }; 59 | 60 | return ( 61 |
62 | {/* Background decorative elements */} 63 |
64 |
65 |
66 |
67 | 68 |
69 | {/* Logo and Title */} 70 |
71 |
72 | Logo 73 |
74 |

GitHubTracker

75 |

Join your GitHub journey

76 |
77 | 78 | {/* Sign Up Form */} 79 |
80 |

Create Account

81 | 82 |
83 |
84 |
85 | 86 |
87 | 96 |
97 | 98 |
99 |
100 | 101 |
102 | 111 |
112 | 113 |
114 |
115 | 116 |
117 | 126 |
127 | 128 | 134 |
135 | 136 | {message && ( 137 |
142 | {message} 143 |
144 | )} 145 | 146 |
147 |

148 | Already have an account?{' '} 149 | 150 | 153 | 154 |

155 |
156 |
157 |
158 |
159 | ); 160 | }; 161 | 162 | export default SignUp; -------------------------------------------------------------------------------- /src/pages/Login/Login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ChangeEvent, FormEvent, useContext } from "react"; 2 | import axios from "axios"; 3 | import { useNavigate, Link } from "react-router-dom"; 4 | import { ThemeContext } from "../../context/ThemeContext"; 5 | import type { ThemeContextType } from "../../context/ThemeContext"; 6 | 7 | const backendUrl = import.meta.env.VITE_BACKEND_URL; 8 | 9 | interface LoginFormData { 10 | email: string; 11 | password: string; 12 | } 13 | 14 | const Login: React.FC = () => { 15 | const [formData, setFormData] = useState({ email: "", password: "" }); 16 | const [message, setMessage] = useState(""); 17 | const [isLoading, setIsLoading] = useState(false); 18 | 19 | const navigate = useNavigate(); 20 | const themeContext = useContext(ThemeContext) as ThemeContextType; 21 | const { mode } = themeContext; 22 | 23 | const handleChange = (e: ChangeEvent) => { 24 | const { name, value } = e.target; 25 | setFormData({ ...formData, [name]: value }); 26 | }; 27 | 28 | const handleSubmit = async (e: FormEvent) => { 29 | e.preventDefault(); 30 | setIsLoading(true); 31 | 32 | try { 33 | const response = await axios.post(`${backendUrl}/api/auth/login`, formData); 34 | setMessage(response.data.message); 35 | 36 | if (response.data.message === 'Login successful') { 37 | navigate("/home"); 38 | } 39 | } catch (error: any) { 40 | setMessage(error.response?.data?.message || "Something went wrong"); 41 | } finally { 42 | setIsLoading(false); 43 | } 44 | }; 45 | 46 | return ( 47 |
54 | {/* Animated background elements */} 55 |
56 |
57 |
58 |
59 |
60 |
61 | 62 |
63 | {/* Branding */} 64 |
65 |
66 | Logo 67 |
68 | 69 |

74 | GitHubTracker 75 |

76 |

77 | Track your GitHub journey 78 |

79 |
80 | 81 | {/* Form Card */} 82 |
83 |

84 | Welcome Back 85 |

86 | 87 |
88 |
89 | 103 |
104 | 105 |
106 | 120 |
121 | 122 | 129 |
130 | 131 | {/* Message */} 132 | {message && ( 133 |
138 | {message} 139 |
140 | )} 141 | 142 | {/* Footer Text */} 143 |
144 |

145 | Don't have an account? 146 | 150 | Sign up here 151 | 152 |

153 |
154 |
155 |
156 | 157 |
158 |
159 | ); 160 | }; 161 | 162 | export default Login; -------------------------------------------------------------------------------- /src/pages/Tracker/Tracker.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react" 2 | import { 3 | IssueOpenedIcon, 4 | IssueClosedIcon, 5 | GitPullRequestIcon, 6 | GitPullRequestClosedIcon, 7 | GitMergeIcon, 8 | } from '@primer/octicons-react'; 9 | import { 10 | Container, 11 | Box, 12 | TextField, 13 | Button, 14 | Paper, 15 | Table, 16 | TableBody, 17 | TableCell, 18 | TableContainer, 19 | TableHead, 20 | TableRow, 21 | TablePagination, 22 | Link, 23 | CircularProgress, 24 | Alert, 25 | Tabs, 26 | Tab, 27 | Select, 28 | MenuItem, 29 | FormControl, 30 | InputLabel, 31 | } from "@mui/material"; 32 | import { useTheme } from "@mui/material/styles"; 33 | import { useGitHubAuth } from "../../hooks/useGitHubAuth"; 34 | import { useGitHubData } from "../../hooks/useGitHubData"; 35 | 36 | const ROWS_PER_PAGE = 10; 37 | 38 | interface GitHubItem { 39 | id: number; 40 | title: string; 41 | state: string; 42 | created_at: string; 43 | pull_request?: { merged_at: string | null }; 44 | repository_url: string; 45 | html_url: string; 46 | } 47 | 48 | const Home: React.FC = () => { 49 | 50 | const theme = useTheme(); 51 | 52 | const { 53 | username, 54 | setUsername, 55 | token, 56 | setToken, 57 | error: authError, 58 | getOctokit, 59 | } = useGitHubAuth(); 60 | 61 | const { 62 | issues, 63 | prs, 64 | totalIssues, 65 | totalPrs, 66 | loading, 67 | error: dataError, 68 | fetchData, 69 | } = useGitHubData(getOctokit); 70 | 71 | const [tab, setTab] = useState(0); 72 | const [page, setPage] = useState(0); 73 | 74 | const [issueFilter, setIssueFilter] = useState("all"); 75 | const [prFilter, setPrFilter] = useState("all"); 76 | const [searchTitle, setSearchTitle] = useState(""); 77 | const [selectedRepo, setSelectedRepo] = useState(""); 78 | const [startDate, setStartDate] = useState(""); 79 | const [endDate, setEndDate] = useState(""); 80 | 81 | // Fetch data when username, tab, or page changes 82 | useEffect(() => { 83 | if (username) { 84 | fetchData(username, page + 1, ROWS_PER_PAGE); 85 | } 86 | }, [tab, page]); 87 | 88 | const handleSubmit = (e: React.FormEvent): void => { 89 | e.preventDefault(); 90 | setPage(0); 91 | fetchData(username, 1, ROWS_PER_PAGE); 92 | }; 93 | 94 | const handlePageChange = (_: unknown, newPage: number) => { 95 | setPage(newPage); 96 | }; 97 | 98 | const formatDate = (dateString: string): string => 99 | new Date(dateString).toLocaleDateString(); 100 | 101 | const filterData = (data: GitHubItem[], filterType: string): GitHubItem[] => { 102 | let filtered = [...data]; 103 | if (["open", "closed", "merged"].includes(filterType)) { 104 | filtered = filtered.filter((item) => { 105 | if (filterType === "merged") { 106 | return !!item.pull_request?.merged_at 107 | } 108 | else if (filterType === "closed") { 109 | return item.state === "closed" && !item.pull_request?.merged_at 110 | } 111 | else { 112 | //open 113 | return item.state === "open" 114 | } 115 | }); 116 | } 117 | if (searchTitle) { 118 | filtered = filtered.filter((item) => 119 | item.title.toLowerCase().includes(searchTitle.toLowerCase()) 120 | ); 121 | } 122 | if (selectedRepo) { 123 | filtered = filtered.filter((item) => 124 | item.repository_url.includes(selectedRepo) 125 | ); 126 | } 127 | if (startDate) { 128 | filtered = filtered.filter( 129 | (item) => new Date(item.created_at) >= new Date(startDate) 130 | ); 131 | } 132 | if (endDate) { 133 | filtered = filtered.filter( 134 | (item) => new Date(item.created_at) <= new Date(endDate) 135 | ); 136 | } 137 | return filtered; 138 | }; 139 | 140 | const getStatusIcon = (item: GitHubItem) => { 141 | 142 | if (item.pull_request) { 143 | 144 | if (item.pull_request.merged_at) 145 | return ; 146 | 147 | if (item.state === 'closed') 148 | return ; 149 | 150 | return ; 151 | } 152 | 153 | if (item.state === 'closed') 154 | return ; 155 | 156 | return ; 157 | }; 158 | 159 | 160 | // Current data and filtered data according to tab and filters 161 | const currentRawData = tab === 0 ? issues : prs; 162 | const currentFilteredData = filterData(currentRawData, tab === 0 ? issueFilter : prFilter); 163 | const totalCount = tab === 0 ? totalIssues : totalPrs; 164 | 165 | return ( 166 | 167 | {/* Auth Form */} 168 | 169 |
170 | 171 | setUsername(e.target.value)} 175 | required 176 | sx={{ flex: 1, minWidth: 150 }} 177 | /> 178 | setToken(e.target.value)} 182 | type="password" 183 | required 184 | sx={{ flex: 1, minWidth: 150 }} 185 | /> 186 | 189 | 190 |
191 |
192 | 193 | {/* Filters */} 194 | 195 | setSearchTitle(e.target.value)} 199 | sx={{ minWidth: 200 }} 200 | /> 201 | setSelectedRepo(e.target.value)} 205 | sx={{ minWidth: 200 }} 206 | /> 207 | setStartDate(e.target.value)} 212 | InputLabelProps={{ shrink: true }} 213 | sx={{ minWidth: 150 }} 214 | /> 215 | setEndDate(e.target.value)} 220 | InputLabelProps={{ shrink: true }} 221 | sx={{ minWidth: 150 }} 222 | /> 223 | 224 | 225 | {/* Tabs + State Filter */} 226 | 236 | { 239 | setTab(v); 240 | setPage(0); 241 | }} 242 | sx={{ flex: 1 }} 243 | > 244 | 245 | 246 | 247 | 248 | State 249 | 272 | 273 | 274 | 275 | {(authError || dataError) && ( 276 | 277 | {authError || dataError} 278 | 279 | )} 280 | 281 | {loading ? ( 282 | 283 | 284 | 285 | ) : ( 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | Title 295 | Repository 296 | State 297 | Created 298 | 299 | 300 | 301 | 302 | {currentFilteredData.map((item) => ( 303 | 304 | 305 | 306 | {getStatusIcon(item)} 307 | 314 | {item.title} 315 | 316 | 317 | 318 | 319 | 320 | {item.repository_url.split("/").slice(-1)[0]} 321 | 322 | 323 | 324 | {item.pull_request?.merged_at ? "merged" : item.state} 325 | 326 | 327 | {formatDate(item.created_at)} 328 | 329 | 330 | ))} 331 | 332 | 333 |
334 | 335 | 343 | 344 |
345 |
346 | )} 347 |
348 | ); 349 | }; 350 | 351 | export default Home; 352 | -------------------------------------------------------------------------------- /src/pages/Contact/Contact.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from "react"; 2 | import { 3 | Github, 4 | Mail, 5 | Phone, 6 | Send, 7 | X, 8 | CheckCircle, 9 | } from "lucide-react"; 10 | import { ThemeContext } from "../../context/ThemeContext"; 11 | import type { ThemeContextType } from "../../context/ThemeContext"; 12 | 13 | function Contact() { 14 | const [showPopup, setShowPopup] = useState(false); 15 | const [isSubmitting, setIsSubmitting] = useState(false); 16 | const themeContext = useContext(ThemeContext) as ThemeContextType; 17 | const { mode } = themeContext; 18 | 19 | const handleSubmit = async () => { 20 | setIsSubmitting(true); 21 | 22 | // Simulate API call 23 | await new Promise((resolve) => setTimeout(resolve, 1500)); 24 | 25 | setIsSubmitting(false); 26 | setShowPopup(true); 27 | 28 | // Auto-close popup after 5 seconds 29 | setTimeout(() => { 30 | setShowPopup(false); 31 | }, 5000); 32 | }; 33 | 34 | const handleClosePopup = () => { 35 | setShowPopup(false); 36 | }; 37 | 38 | return ( 39 |
46 | {/* Animated background elements */} 47 |
48 |
49 |
50 |
51 |
52 | 53 |
54 | {/* Header Section */} 55 |
56 |
57 |
62 | Logo 67 |
68 |

72 | GitHub Tracker 73 |

74 |
75 |

80 | Get in touch with us to discuss your project tracking needs or 81 | report any issues 82 |

83 |
84 | 85 |
86 | {/* Contact Info Cards */} 87 |
88 |
89 |

94 | Let's Connect 95 |

96 |

101 | We're here to help you track and manage your GitHub 102 | repositories more effectively 103 |

104 |
105 | 106 |
107 | {[...Array(3)].map((_, index) => { 108 | const contactTypes = [ 109 | { 110 | title: "Phone Support", 111 | iconBg: "from-blue-500 to-cyan-500", 112 | detail: "(123) 456-7890", 113 | sub: "Mon-Fri, 9AM-6PM EST", 114 | Icon: Phone, 115 | }, 116 | { 117 | title: "Email Us", 118 | iconBg: "from-purple-500 to-pink-500", 119 | detail: "support@githubtracker.com", 120 | sub: "We'll respond within 24 hours", 121 | Icon: Mail, 122 | }, 123 | { 124 | title: "GitHub Issues", 125 | iconBg: "from-green-500 to-teal-500", 126 | detail: "github.com/yourorg/githubtracker", 127 | sub: "Report bugs & feature requests", 128 | Icon: Github, 129 | }, 130 | ]; 131 | const { title, iconBg, detail, sub, Icon } = 132 | contactTypes[index]; 133 | return ( 134 |
142 |
143 |
146 | 153 |
154 |
155 |

162 | {title} 163 |

164 |

171 | {detail} 172 |

173 |

180 | {sub} 181 |

182 |
183 |
184 |
185 | ); 186 | })} 187 |
188 |
189 | 190 | {/* Contact Form */} 191 |
198 |

203 | Send us a Message 204 |

205 | 206 |
207 |
208 | {/* Full Name */} 209 |
210 | 219 | 229 |
230 | 231 | {/* Email */} 232 |
233 | 242 | 252 |
253 | 254 | {/* Subject */} 255 |
256 | 265 | 282 |
283 | 284 | {/* Message */} 285 |
286 | 295 | 305 | 306 | 318 |
319 |
320 |
321 |
322 |
323 |
324 | 325 | {/* Success Popup */} 326 | {showPopup && ( 327 |
334 | 335 |
336 | Thank you for contacting us! We will get back to you shortly. 337 |
338 | 345 |
346 | )} 347 |
348 | ); 349 | } 350 | 351 | export default Contact; 352 | --------------------------------------------------------------------------------