diff --git a/.env.example b/.env.example deleted file mode 100644 index bbf3c1e..0000000 --- a/.env.example +++ /dev/null @@ -1 +0,0 @@ -VITE_BACKEND_URL=http://localhost:5000 \ No newline at end of file diff --git a/README.md b/README.md index a747b53..9e5561f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # 🌟 **GitHub Tracker** 🌟 + **Track Activity of Users on GitHub** @@ -6,7 +7,7 @@ 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! πŸš€πŸ‘©β€πŸ’»

- github-tracker + github-tracker

@@ -41,47 +42,93 @@ Welcome to **GitHub Tracker**, a web app designed to help you monitor and analyz --- ## πŸš€ Setup Guide + 1. Clone the repository to your local machine: + ```bash $ git clone https://github.com/yourusername/github-tracker.git ``` 2. Navigate to the project directory: + ```bash $ cd github-tracker ``` 3. Run the frontend + ```bash +$ cd frontend $ npm i $ npm run dev ``` 4. Run the backend + ```bash +$ cd backend $ npm i $ npm start ``` +## πŸ” MongoDB & Authentication Setup + +The application uses MongoDB for user authentication (Sign up, Login, Logout). + +### Frontend Environment Setup + +Create a `.env` file in the `frontend/` folder: + +``` +VITE_BACKEND_URL=http://localhost:5000 +``` + +See `frontend/.env.example` for reference. + +### Backend Environment Setup + +Create a `.env` file in the `backend/` folder: + +``` +MONGO_URI=mongodb+srv://alokhacs222729_db_user:IvORbhhzQg71OF2q@cluster0.gevcwty.mongodb.net/?appName=Cluster0 +PORT=5000 +SESSION_SECRET=your_session_secret_key_here +``` + +See `backend/.env.example` for reference. + +**Key Points:** + +- The `MONGO_URI` connects to MongoDB Atlas for user database storage +- Users can sign up with email and password +- Passwords are securely hashed using bcryptjs +- Sessions are maintained via express-session and Passport.js +- Both frontend and backend are required for authentication to work + ## πŸ§ͺ Backend Unit & Integration Testing with Jasmine This project uses the Jasmine framework for backend unit and integration tests. The tests cover: + - User model (password hashing, schema, password comparison) - Authentication routes (signup, login, logout) - Passport authentication logic (via integration tests) ### Prerequisites + - **Node.js** and **npm** installed - **MongoDB** running locally (default: `mongodb://127.0.0.1:27017`) ### Installation + Install all required dependencies: + ```sh npm install npm install --save-dev jasmine @types/jasmine supertest express-session passport passport-local bcryptjs ``` ### Running the Tests + 1. **Start MongoDB** (if not already running): ```sh mongod @@ -92,24 +139,26 @@ npm install --save-dev jasmine @types/jasmine supertest express-session passport ``` ### Test Files + - `spec/user.model.spec.cjs` β€” Unit tests for the User model - `spec/auth.routes.spec.cjs` β€” Integration tests for authentication routes ### Jasmine Configuration + The Jasmine config (`spec/support/jasmine.mjs`) is set to recognize `.cjs`, `.js`, and `.mjs` test files: + ```js -spec_files: [ - "**/*[sS]pec.?(m)js", - "**/*[sS]pec.cjs" -] +spec_files: ["**/*[sS]pec.?(m)js", "**/*[sS]pec.cjs"]; ``` ### Troubleshooting + - **No specs found:** Ensure your test files have the correct extension and are in the `spec/` directory. - **MongoDB connection errors:** Make sure MongoDB is running and accessible. - **Missing modules:** Install any missing dev dependencies with `npm install --save-dev `. ### What Was Covered + - Jasmine is set up and configured for backend testing. - All major backend modules are covered by unit/integration tests. - Tests are passing and verified. @@ -131,8 +180,6 @@ spec_files: [ - - ---

diff --git a/backend/.env.sample b/backend/.env.sample deleted file mode 100644 index 98f9688..0000000 --- a/backend/.env.sample +++ /dev/null @@ -1,3 +0,0 @@ -PORT=5000 -MONGO_URI=mongodb://localhost:27017/githubTracker -SESSION_SECRET=your-secret-key diff --git a/backend/models/User.js b/backend/models/User.js index 779294f..1b9d23c 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -16,6 +16,14 @@ const UserSchema = new mongoose.Schema({ type: String, required: true, }, + trackerHistory: { + type: Array, + default: [], + }, + lastTrackedAt: { + type: Date, + default: Date.now, + }, }); UserSchema.pre('save', async function (next) { diff --git a/backend/routes/auth.js b/backend/routes/auth.js index e26c7a9..e87653b 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -39,4 +39,48 @@ router.get("/logout", (req, res) => { }); }); +// Get user tracker history +router.get("/tracker-history", async (req, res) => { + if (!req.user) { + return res.status(401).json({ message: 'Not authenticated' }); + } + try { + const user = await User.findById(req.user.id).select('trackerHistory'); + res.status(200).json({ trackerHistory: user?.trackerHistory || [] }); + } catch (err) { + res.status(500).json({ message: 'Error fetching tracker history', error: err.message }); + } +}); + +// Save tracker search to history +router.post("/tracker-history", async (req, res) => { + if (!req.user) { + return res.status(401).json({ message: 'Not authenticated' }); + } + const { username, searchedAt } = req.body; + try { + const user = await User.findById(req.user.id); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + // Remove duplicate if exists (keep only unique searches) + user.trackerHistory = user.trackerHistory.filter( + item => item.username.toLowerCase() !== username.toLowerCase() + ); + + // Add new search at the beginning + user.trackerHistory.unshift({ username, searchedAt: new Date(searchedAt) }); + + // Keep only last 10 searches + user.trackerHistory = user.trackerHistory.slice(0, 10); + user.lastTrackedAt = new Date(); + + await user.save(); + res.status(200).json({ message: 'Tracker history saved', trackerHistory: user.trackerHistory }); + } catch (err) { + res.status(500).json({ message: 'Error saving tracker history', error: err.message }); + } +}); + module.exports = router; diff --git a/backend/server.js b/backend/server.js index 3f19f00..4200044 100644 --- a/backend/server.js +++ b/backend/server.js @@ -12,7 +12,10 @@ require('./config/passportConfig'); const app = express(); // CORS configuration -app.use(cors('*')); +app.use(cors({ + origin: process.env.FRONTEND_URL || 'http://localhost:5173', + credentials: true +})); // Middleware app.use(bodyParser.json()); @@ -20,6 +23,11 @@ app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, + cookie: { + httpOnly: true, + secure: false, // set to true in production with HTTPS + sameSite: 'lax' + } })); app.use(passport.initialize()); app.use(passport.session()); diff --git a/docker-compose.yml b/docker-compose.yml index e2cdd0f..1d33ccf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,14 +3,14 @@ services: image: frontend container_name: frontend-container build: - context: . + context: ./frontend dockerfile: Dockerfile.dev ports: - "5173:5173" env_file: - .env volumes: - - .:/app + - ./frontend:/app - /app/node_modules depends_on: - backend @@ -39,7 +39,7 @@ services: image: frontend-prod container_name: frontend-prod-container build: - context: . + context: ./frontend dockerfile: Dockerfile.prod ports: - "3000:3000" diff --git a/Dockerfile.dev b/frontend/Dockerfile.dev similarity index 100% rename from Dockerfile.dev rename to frontend/Dockerfile.dev diff --git a/Dockerfile.prod b/frontend/Dockerfile.prod similarity index 100% rename from Dockerfile.prod rename to frontend/Dockerfile.prod diff --git a/eslint.config.js b/frontend/eslint.config.js similarity index 100% rename from eslint.config.js rename to frontend/eslint.config.js diff --git a/index.html b/frontend/index.html similarity index 100% rename from index.html rename to frontend/index.html diff --git a/package.json b/frontend/package.json similarity index 93% rename from package.json rename to frontend/package.json index f2d89f5..1cb2ae9 100644 --- a/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "dev": "vite --host", "build": "vite build", "lint": "eslint .", + "test": "jasmine", "preview": "vite preview", "docker:dev": "docker compose --profile dev up --build", "docker:prod": "docker compose --profile prod up -d --build" @@ -45,12 +46,15 @@ "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", + "express": "^5.2.1", "express-session": "^1.18.2", "globals": "^15.11.0", "jasmine": "^5.9.0", + "mongoose": "^9.6.2", "passport": "^0.7.0", "passport-local": "^1.0.0", "supertest": "^7.1.4", + "typescript-eslint": "^8.59.2", "vite": "^5.4.10" } } diff --git a/postcss.config.cjs b/frontend/postcss.config.cjs similarity index 100% rename from postcss.config.cjs rename to frontend/postcss.config.cjs diff --git a/public/_redirects b/frontend/public/_redirects similarity index 100% rename from public/_redirects rename to frontend/public/_redirects diff --git a/public/crl-icon.png b/frontend/public/crl-icon.png similarity index 100% rename from public/crl-icon.png rename to frontend/public/crl-icon.png diff --git a/public/crl.png b/frontend/public/crl.png similarity index 100% rename from public/crl.png rename to frontend/public/crl.png diff --git a/public/vite.svg b/frontend/public/vite.svg similarity index 100% rename from public/vite.svg rename to frontend/public/vite.svg diff --git a/src/App.css b/frontend/src/App.css similarity index 100% rename from src/App.css rename to frontend/src/App.css diff --git a/src/App.tsx b/frontend/src/App.tsx similarity index 86% rename from src/App.tsx rename to frontend/src/App.tsx index b00eba8..cabbd8b 100644 --- a/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,12 +8,12 @@ import ThemeWrapper from "./context/ThemeContext"; function App() { return ( -

+
-
+
diff --git a/src/Routes/Router.tsx b/frontend/src/Routes/Router.tsx similarity index 100% rename from src/Routes/Router.tsx rename to frontend/src/Routes/Router.tsx diff --git a/src/assets/react.svg b/frontend/src/assets/react.svg similarity index 100% rename from src/assets/react.svg rename to frontend/src/assets/react.svg diff --git a/frontend/src/components/Features.tsx b/frontend/src/components/Features.tsx new file mode 100644 index 0000000..f942ee5 --- /dev/null +++ b/frontend/src/components/Features.tsx @@ -0,0 +1,103 @@ +import { + BarChart3, + Filter, + Github, + LockKeyhole, + MousePointerClick, + Users, +} from "lucide-react"; + +const features = [ + { + icon: BarChart3, + title: "Activity summaries", + description: + "See issue, pull request, open, and completed counts before diving into details.", + accent: "text-blue-600 bg-blue-50 dark:bg-blue-950", + }, + { + icon: Filter, + title: "Fast filtering", + description: + "Filter by state, repository, date range, and title without leaving the dashboard.", + accent: "text-emerald-600 bg-emerald-50 dark:bg-emerald-950", + }, + { + icon: MousePointerClick, + title: "Actionable cards", + description: + "Open matching GitHub items directly from clean, scan-friendly activity cards.", + accent: "text-violet-600 bg-violet-50 dark:bg-violet-950", + }, + { + icon: Users, + title: "Contributor directory", + description: + "Explore project contributors with search, ranking, and profile links.", + accent: "text-cyan-600 bg-cyan-50 dark:bg-cyan-950", + }, + { + icon: LockKeyhole, + title: "Session-only token", + description: + "Your GitHub token is used in the browser session to request GitHub API data.", + accent: "text-rose-600 bg-rose-50 dark:bg-rose-950", + }, + { + icon: Github, + title: "Built for open source", + description: + "Designed around practical maintainer and contributor workflows.", + accent: "text-slate-700 bg-slate-100 dark:bg-slate-800 dark:text-slate-200", + }, +]; + +const Features = () => { + return ( +
+
+
+

+ Product highlights +

+

+ A simpler way to understand GitHub activity +

+

+ The interface focuses on the details users actually need while + keeping controls predictable and quick to scan. +

+
+ +
+ {features.map((feature) => { + const Icon = feature.icon; + return ( +
+
+ +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ); + })} +
+
+
+ ); +}; + +export default Features; diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx new file mode 100644 index 0000000..78c2e66 --- /dev/null +++ b/frontend/src/components/Footer.tsx @@ -0,0 +1,47 @@ +import { FaGithub } from "react-icons/fa"; +import { Link } from "react-router-dom"; + +function Footer() { + return ( +
+
+
+ +
+ + Contact Us + + + About + +
+
+
+

+ © {new Date().getFullYear()}{" "} + GitHub Tracker. All rights + reserved. +

+
+
+
+ ); +} + +export default Footer; diff --git a/frontend/src/components/Hero.tsx b/frontend/src/components/Hero.tsx new file mode 100644 index 0000000..3c92c99 --- /dev/null +++ b/frontend/src/components/Hero.tsx @@ -0,0 +1,115 @@ +import { ArrowRight, GitPullRequest, Search, ShieldCheck } from "lucide-react"; +import { Link } from "react-router-dom"; + +const previewItems = [ + { label: "Open issues", value: "42", tone: "text-emerald-600" }, + { label: "Pull requests", value: "18", tone: "text-blue-600" }, + { label: "Active repos", value: "9", tone: "text-violet-600" }, +]; + +const Hero = () => { + return ( +
+
+
+
+ + Public GitHub insights in one workspace +
+ +

+ Track contributors, issues, and pull requests faster. +

+ +

+ GitHub Tracker turns profile activity into a clean dashboard for + maintainers, open-source contributors, and teams who need quick + contribution context. +

+ +
+ + + Start tracking + + + View contributors + + +
+
+ +
+
+
+
+

+ Dashboard preview +

+

+ octocat activity +

+
+ + Live ready + +
+ +
+ {previewItems.map((item) => ( +
+

+ {item.value} +

+

+ {item.label} +

+
+ ))} +
+ +
+ {[ + "Review new UI dashboard", + "Fix API rate-limit state", + "Improve contributor profile", + ].map((title, index) => ( +
+
+ + + {title} + +
+ + #{223 + index} + +
+ ))} +
+
+
+
+
+ ); +}; + +export default Hero; diff --git a/frontend/src/components/HowItWorks.tsx b/frontend/src/components/HowItWorks.tsx new file mode 100644 index 0000000..cfbc291 --- /dev/null +++ b/frontend/src/components/HowItWorks.tsx @@ -0,0 +1,71 @@ +import { KeyRound, ListFilter, ScanSearch } from "lucide-react"; + +const steps = [ + { + icon: ScanSearch, + title: "Enter a GitHub user", + description: + "Add a username and token to fetch reliable GitHub activity data.", + }, + { + icon: ListFilter, + title: "Refine the activity", + description: + "Switch between issues and pull requests, then filter by status, repo, or date.", + }, + { + icon: KeyRound, + title: "Open the right context", + description: + "Jump straight to GitHub when a result needs review or follow-up.", + }, +]; + +const HowItWorks = () => { + return ( +
+
+
+

+ Workflow +

+

+ From username to insight in three steps +

+
+ +
+ {steps.map((step, index) => { + const Icon = step.icon; + return ( +
+
+
+ +
+ + 0{index + 1} + +
+

+ {step.title} +

+

+ {step.description} +

+
+ ); + })} +
+
+
+ ); +}; + +export default HowItWorks; diff --git a/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx similarity index 93% rename from src/components/Navbar.tsx rename to frontend/src/components/Navbar.tsx index c6cc86d..edf413d 100644 --- a/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,22 +1,19 @@ import { Link } from "react-router-dom"; import { useState, useContext } from "react"; -import { ThemeContext } from "../context/ThemeContext"; -import { Moon, Sun } from 'lucide-react'; - +import { ThemeContext } from "../context/theme"; +import { Moon, Sun } from "lucide-react"; const Navbar: React.FC = () => { - const [isOpen, setIsOpen] = useState(false); const themeContext = useContext(ThemeContext); - if (!themeContext) - return null; + if (!themeContext) return null; const { toggleTheme, mode } = themeContext; return (
- - - - Title - Repository - State - Created - - - - - {currentFilteredData.map((item) => ( - - - - {getStatusIcon(item)} - - {item.title} - - - - - - {item.repository_url.split("/").slice(-1)[0]} - - - - {item.pull_request?.merged_at ? "merged" : item.state} - - - {formatDate(item.created_at)} - - - ))} - - -
- - - - - - )} - - ); -}; - -export default Home;