From ef9548dc17654e9d816d6cfd4c4a7ad4a810bf20 Mon Sep 17 00:00:00 2001 From: CodeByAlok24 Date: Mon, 11 May 2026 21:49:07 +0530 Subject: [PATCH 1/5] Fix lint and test setup --- package.json | 4 ++ spec/auth.routes.spec.cjs | 17 +++---- spec/user.model.spec.cjs | 13 +++--- src/components/Navbar.tsx | 2 +- src/context/ThemeContext.tsx | 11 +---- src/context/theme.ts | 8 ++++ src/hooks/useGitHubData.ts | 45 ++++++++++++++++--- src/pages/Contact/Contact.tsx | 4 +- .../ContributorProfile/ContributorProfile.tsx | 10 ++++- src/pages/Contributors/Contributors.tsx | 2 +- src/pages/Login/Login.tsx | 14 +++--- src/pages/Signup/Signup.tsx | 4 +- src/pages/Tracker/Tracker.tsx | 2 +- 13 files changed, 91 insertions(+), 45 deletions(-) create mode 100644 src/context/theme.ts diff --git a/package.json b/package.json index f2d89f5..1cb2ae9 100644 --- a/package.json +++ b/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/spec/auth.routes.spec.cjs b/spec/auth.routes.spec.cjs index c5ac003..e0d59ee 100644 --- a/spec/auth.routes.spec.cjs +++ b/spec/auth.routes.spec.cjs @@ -1,10 +1,12 @@ -const mongoose = require('mongoose'); +const { createRequire } = require('module'); const express = require('express'); const request = require('supertest'); const session = require('express-session'); -const passport = require('passport'); const User = require('../backend/models/User'); const authRoutes = require('../backend/routes/auth'); +const mongoose = User.base; +const backendRequire = createRequire(require.resolve('../backend/package.json')); +const passport = backendRequire('passport'); // Setup Express app for testing function createTestApp() { @@ -22,15 +24,14 @@ describe('Auth Routes', () => { let app; beforeAll(async () => { - await mongoose.connect('mongodb://127.0.0.1:27017/github_tracker_test', { - useNewUrlParser: true, - useUnifiedTopology: true, - }); + await mongoose.connect('mongodb://127.0.0.1:27017/github_tracker_test'); app = createTestApp(); }); afterAll(async () => { - await mongoose.connection.db.dropDatabase(); + if (mongoose.connection.db) { + await mongoose.connection.db.dropDatabase(); + } await mongoose.disconnect(); }); @@ -93,4 +94,4 @@ describe('Auth Routes', () => { expect(res.status).toBe(200); expect(res.body.message).toBe('Logged out successfully'); }); -}); \ No newline at end of file +}); diff --git a/spec/user.model.spec.cjs b/spec/user.model.spec.cjs index 236d9bd..9475852 100644 --- a/spec/user.model.spec.cjs +++ b/spec/user.model.spec.cjs @@ -1,17 +1,16 @@ -const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); const User = require('../backend/models/User'); +const mongoose = User.base; describe('User Model', () => { beforeAll(async () => { - await mongoose.connect('mongodb://127.0.0.1:27017/github_tracker_test', { - useNewUrlParser: true, - useUnifiedTopology: true, - }); + await mongoose.connect('mongodb://127.0.0.1:27017/github_tracker_test'); }); afterAll(async () => { - await mongoose.connection.db.dropDatabase(); + if (mongoose.connection.db) { + await mongoose.connection.db.dropDatabase(); + } await mongoose.disconnect(); }); @@ -47,4 +46,4 @@ describe('User Model', () => { const isNotMatch = await user.comparePassword('wrongpassword'); expect(isNotMatch).toBeFalse(); }); -}); \ No newline at end of file +}); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index c6cc86d..ede16a3 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,6 +1,6 @@ import { Link } from "react-router-dom"; import { useState, useContext } from "react"; -import { ThemeContext } from "../context/ThemeContext"; +import { ThemeContext } from "../context/theme"; import { Moon, Sun } from 'lucide-react'; diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx index b6866e3..4d80ff0 100644 --- a/src/context/ThemeContext.tsx +++ b/src/context/ThemeContext.tsx @@ -1,13 +1,7 @@ // src/ThemeContext.tsx -import { createContext, useMemo, useState, useEffect, ReactNode } from 'react'; +import { useMemo, useState, useEffect, ReactNode } from 'react'; import { createTheme, ThemeProvider, Theme } from '@mui/material/styles'; - -interface ThemeContextType { - mode: 'light' | 'dark'; - toggleTheme: () => void; -} - -export const ThemeContext = createContext(null); +import { ThemeContext } from './theme'; const THEME_STORAGE_KEY = 'theme'; @@ -46,4 +40,3 @@ const ThemeWrapper = ({ children }: { children: ReactNode }) => { }; export default ThemeWrapper; -export type { ThemeContextType }; diff --git a/src/context/theme.ts b/src/context/theme.ts new file mode 100644 index 0000000..8e364d2 --- /dev/null +++ b/src/context/theme.ts @@ -0,0 +1,8 @@ +import { createContext } from 'react'; + +export interface ThemeContextType { + mode: 'light' | 'dark'; + toggleTheme: () => void; +} + +export const ThemeContext = createContext(null); diff --git a/src/hooks/useGitHubData.ts b/src/hooks/useGitHubData.ts index a0ebe10..35d41cc 100644 --- a/src/hooks/useGitHubData.ts +++ b/src/hooks/useGitHubData.ts @@ -1,15 +1,44 @@ import { useState, useCallback } from 'react'; -export const useGitHubData = (getOctokit: () => any) => { - const [issues, setIssues] = useState([]); - const [prs, setPrs] = useState([]); +export interface GitHubItem { + id: number; + title: string; + state: string; + created_at: string; + pull_request?: { merged_at: string | null }; + repository_url: string; + html_url: string; +} + +interface GitHubSearchResponse { + data: { + items: GitHubItem[]; + total_count: number; + }; +} + +interface GitHubClient { + request: ( + route: 'GET /search/issues', + options: Record + ) => Promise; +} + +interface GitHubApiError { + status?: number; + message?: string; +} + +export const useGitHubData = (getOctokit: () => GitHubClient | null) => { + const [issues, setIssues] = useState([]); + const [prs, setPrs] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [totalIssues, setTotalIssues] = useState(0); const [totalPrs, setTotalPrs] = useState(0); const [rateLimited, setRateLimited] = useState(false); - const fetchPaginated = async (octokit: any, username: string, type: string, page = 1, per_page = 10) => { + const fetchPaginated = async (octokit: GitHubClient, username: string, type: string, page = 1, per_page = 10) => { const q = `author:${username} is:${type}`; const response = await octokit.request('GET /search/issues', { q, @@ -45,12 +74,14 @@ export const useGitHubData = (getOctokit: () => any) => { setPrs(prRes.items); setTotalIssues(issueRes.total); setTotalPrs(prRes.total); - } catch (err: any) { - if (err.status === 403) { + } catch (err: unknown) { + const githubError = err as GitHubApiError; + + if (githubError.status === 403) { setError('GitHub API rate limit exceeded. Please wait or use a token.'); setRateLimited(true); // Prevent further fetches } else { - setError(err.message || 'Failed to fetch data'); + setError(githubError.message || 'Failed to fetch data'); } } finally { setLoading(false); diff --git a/src/pages/Contact/Contact.tsx b/src/pages/Contact/Contact.tsx index a0bfccb..e6f6476 100644 --- a/src/pages/Contact/Contact.tsx +++ b/src/pages/Contact/Contact.tsx @@ -7,8 +7,8 @@ import { X, CheckCircle, } from "lucide-react"; -import { ThemeContext } from "../../context/ThemeContext"; -import type { ThemeContextType } from "../../context/ThemeContext"; +import { ThemeContext } from "../../context/theme"; +import type { ThemeContextType } from "../../context/theme"; function Contact() { const [showPopup, setShowPopup] = useState(false); diff --git a/src/pages/ContributorProfile/ContributorProfile.tsx b/src/pages/ContributorProfile/ContributorProfile.tsx index b4ab931..abd12ef 100644 --- a/src/pages/ContributorProfile/ContributorProfile.tsx +++ b/src/pages/ContributorProfile/ContributorProfile.tsx @@ -8,9 +8,15 @@ type PR = { repository_url: string; }; +type GitHubProfile = { + avatar_url: string; + login: string; + bio: string | null; +}; + export default function ContributorProfile() { const { username } = useParams(); - const [profile, setProfile] = useState(null); + const [profile, setProfile] = useState(null); const [prs, setPRs] = useState([]); const [loading, setLoading] = useState(true); @@ -28,7 +34,7 @@ export default function ContributorProfile() { ); const prsData = await prsRes.json(); setPRs(prsData.items); - } catch (error) { + } catch { toast.error("Failed to fetch user data."); } finally { setLoading(false); diff --git a/src/pages/Contributors/Contributors.tsx b/src/pages/Contributors/Contributors.tsx index 60270b1..d4fee52 100644 --- a/src/pages/Contributors/Contributors.tsx +++ b/src/pages/Contributors/Contributors.tsx @@ -37,7 +37,7 @@ const ContributorsPage = () => { withCredentials: false, }); setContributors(response.data); - } catch (err) { + } catch { setError("Failed to fetch contributors. Please try again later."); } finally { setLoading(false); diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index d6f21a7..bdc4375 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -1,8 +1,8 @@ import React, { useState, ChangeEvent, FormEvent, useContext } from "react"; import axios from "axios"; import { useNavigate, Link } from "react-router-dom"; -import { ThemeContext } from "../../context/ThemeContext"; -import type { ThemeContextType } from "../../context/ThemeContext"; +import { ThemeContext } from "../../context/theme"; +import type { ThemeContextType } from "../../context/theme"; const backendUrl = import.meta.env.VITE_BACKEND_URL; @@ -36,8 +36,12 @@ const Login: React.FC = () => { if (response.data.message === 'Login successful') { navigate("/home"); } - } catch (error: any) { - setMessage(error.response?.data?.message || "Something went wrong"); + } catch (error: unknown) { + if (axios.isAxiosError<{ message?: string }>(error)) { + setMessage(error.response?.data?.message || "Something went wrong"); + } else { + setMessage("Something went wrong"); + } } finally { setIsLoading(false); } @@ -159,4 +163,4 @@ const Login: React.FC = () => { ); }; -export default Login; \ No newline at end of file +export default Login; diff --git a/src/pages/Signup/Signup.tsx b/src/pages/Signup/Signup.tsx index d03a921..0704dca 100644 --- a/src/pages/Signup/Signup.tsx +++ b/src/pages/Signup/Signup.tsx @@ -52,7 +52,7 @@ const navigate = useNavigate(); // console.log("Redirecting to login page..."); // }, 2000); - } catch (error) { + } catch { setMessage("Something went wrong. Please try again."); } }; @@ -159,4 +159,4 @@ const navigate = useNavigate(); ); }; -export default SignUp; \ No newline at end of file +export default SignUp; diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx index 2bd4d30..d494b7e 100644 --- a/src/pages/Tracker/Tracker.tsx +++ b/src/pages/Tracker/Tracker.tsx @@ -83,7 +83,7 @@ const Home: React.FC = () => { if (username) { fetchData(username, page + 1, ROWS_PER_PAGE); } - }, [tab, page]); + }, [fetchData, page, tab, username]); const handleSubmit = (e: React.FormEvent): void => { e.preventDefault(); From 945708de43bd5183567b71b1d940101349324f7e Mon Sep 17 00:00:00 2001 From: CodeByAlok24 Date: Tue, 12 May 2026 00:35:55 +0530 Subject: [PATCH 2/5] Enhance tracker dashboard UI --- src/pages/Tracker/Tracker.tsx | 730 +++++++++++++++++++++------------- 1 file changed, 452 insertions(+), 278 deletions(-) diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx index d494b7e..0e12155 100644 --- a/src/pages/Tracker/Tracker.tsx +++ b/src/pages/Tracker/Tracker.tsx @@ -1,54 +1,77 @@ -import React, { useState, useEffect } from "react" +import React, { useEffect, useMemo, useState } from "react"; import { IssueOpenedIcon, IssueClosedIcon, GitPullRequestIcon, GitPullRequestClosedIcon, GitMergeIcon, -} from '@primer/octicons-react'; +} from "@primer/octicons-react"; import { - Container, + Alert, Box, - TextField, Button, - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TablePagination, - Link, + Chip, CircularProgress, - Alert, - Tabs, - Tab, - Select, - MenuItem, + Container, + Divider, FormControl, InputLabel, + Link, + MenuItem, + Paper, + Select, + Stack, + Tab, + TablePagination, + Tabs, + TextField, + Typography, } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import { useGitHubAuth } from "../../hooks/useGitHubAuth"; import { useGitHubData } from "../../hooks/useGitHubData"; +import type { GitHubItem } from "../../hooks/useGitHubData"; const ROWS_PER_PAGE = 10; +const stateOptions = ["all", "open", "closed", "merged"] as const; -interface GitHubItem { - id: number; - title: string; - state: string; - created_at: string; - pull_request?: { merged_at: string | null }; - repository_url: string; - html_url: string; -} +const formatDate = (dateString: string): string => + new Intl.DateTimeFormat("en", { + month: "short", + day: "numeric", + year: "numeric", + }).format(new Date(dateString)); -const Home: React.FC = () => { +const getRepositoryName = (repositoryUrl: string): string => + repositoryUrl.split("/").slice(-1)[0] || "repository"; - const theme = useTheme(); +const getItemStatus = (item: GitHubItem): string => { + if (item.pull_request?.merged_at) return "merged"; + return item.state; +}; + +const getStatusIcon = (item: GitHubItem) => { + if (item.pull_request) { + if (item.pull_request.merged_at) { + return ; + } + if (item.state === "closed") { + return ; + } + + return ; + } + + if (item.state === "closed") { + return ; + } + + return ; +}; + +const Tracker: React.FC = () => { + const theme = useTheme(); const { username, setUsername, @@ -70,7 +93,6 @@ const Home: React.FC = () => { const [tab, setTab] = useState(0); const [page, setPage] = useState(0); - const [issueFilter, setIssueFilter] = useState("all"); const [prFilter, setPrFilter] = useState("all"); const [searchTitle, setSearchTitle] = useState(""); @@ -78,274 +100,426 @@ const Home: React.FC = () => { const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); - // Fetch data when username, tab, or page changes useEffect(() => { if (username) { fetchData(username, page + 1, ROWS_PER_PAGE); } }, [fetchData, page, tab, username]); - const handleSubmit = (e: React.FormEvent): void => { - e.preventDefault(); - setPage(0); - fetchData(username, 1, ROWS_PER_PAGE); - }; + const activeFilter = tab === 0 ? issueFilter : prFilter; + const currentRawData = tab === 0 ? issues : prs; - const handlePageChange = (_: unknown, newPage: number) => { - setPage(newPage); - }; + const repositories = useMemo( + () => + Array.from( + new Set([...issues, ...prs].map((item) => getRepositoryName(item.repository_url))) + ).sort(), + [issues, prs] + ); - const formatDate = (dateString: string): string => - new Date(dateString).toLocaleDateString(); - - const filterData = (data: GitHubItem[], filterType: string): GitHubItem[] => { - let filtered = [...data]; - if (["open", "closed", "merged"].includes(filterType)) { - filtered = filtered.filter((item) => { - if (filterType === "merged") { - return !!item.pull_request?.merged_at - } - else if (filterType === "closed") { - return item.state === "closed" && !item.pull_request?.merged_at - } - else { - //open - return item.state === "open" - } - }); - } - if (searchTitle) { - filtered = filtered.filter((item) => - item.title.toLowerCase().includes(searchTitle.toLowerCase()) - ); - } - if (selectedRepo) { - filtered = filtered.filter((item) => - item.repository_url.includes(selectedRepo) - ); - } - if (startDate) { - filtered = filtered.filter( - (item) => new Date(item.created_at) >= new Date(startDate) - ); - } - if (endDate) { - filtered = filtered.filter( - (item) => new Date(item.created_at) <= new Date(endDate) - ); - } - return filtered; + const filteredData = useMemo(() => { + return currentRawData.filter((item) => { + const status = getItemStatus(item); + const matchesState = activeFilter === "all" || status === activeFilter; + const matchesTitle = item.title + .toLowerCase() + .includes(searchTitle.toLowerCase().trim()); + const matchesRepo = + !selectedRepo || getRepositoryName(item.repository_url) === selectedRepo; + const createdAt = new Date(item.created_at); + const matchesStart = !startDate || createdAt >= new Date(startDate); + const matchesEnd = !endDate || createdAt <= new Date(endDate); + + return matchesState && matchesTitle && matchesRepo && matchesStart && matchesEnd; + }); + }, [activeFilter, currentRawData, endDate, searchTitle, selectedRepo, startDate]); + + const summary = useMemo(() => { + const allItems = [...issues, ...prs]; + const openItems = allItems.filter((item) => getItemStatus(item) === "open").length; + const closedItems = allItems.filter((item) => getItemStatus(item) === "closed").length; + const mergedItems = allItems.filter((item) => getItemStatus(item) === "merged").length; + const completedItems = closedItems + mergedItems; + + return [ + { label: "Issues", value: totalIssues, helper: "Total found", color: "#0969da" }, + { label: "Pull requests", value: totalPrs, helper: "Total found", color: "#8250df" }, + { label: "Open", value: openItems, helper: "On this page", color: "#2ea44f" }, + { label: "Completed", value: completedItems, helper: "On this page", color: "#cf222e" }, + ]; + }, [issues, prs, totalIssues, totalPrs]); + + const clearFilters = () => { + setSearchTitle(""); + setSelectedRepo(""); + setStartDate(""); + setEndDate(""); + setIssueFilter("all"); + setPrFilter("all"); }; - const getStatusIcon = (item: GitHubItem) => { - - if (item.pull_request) { - - if (item.pull_request.merged_at) - return ; - - if (item.state === 'closed') - return ; - - return ; - } - - if (item.state === 'closed') - return ; - - return ; + const handleSubmit = (e: React.FormEvent): void => { + e.preventDefault(); + setPage(0); + fetchData(username, 1, ROWS_PER_PAGE); }; - - // Current data and filtered data according to tab and filters - const currentRawData = tab === 0 ? issues : prs; - const currentFilteredData = filterData(currentRawData, tab === 0 ? issueFilter : prFilter); + const hasData = issues.length > 0 || prs.length > 0; const totalCount = tab === 0 ? totalIssues : totalPrs; return ( - - {/* Auth Form */} - -
- - setUsername(e.target.value)} - required - sx={{ flex: 1, minWidth: 150 }} - /> - setToken(e.target.value)} - type="password" - required - sx={{ flex: 1, minWidth: 150 }} - /> - - -
-
- - {/* Filters */} - - setSearchTitle(e.target.value)} - sx={{ minWidth: 200 }} - /> - setSelectedRepo(e.target.value)} - sx={{ minWidth: 200 }} - /> - setStartDate(e.target.value)} - InputLabelProps={{ shrink: true }} - sx={{ minWidth: 150 }} - /> - setEndDate(e.target.value)} - InputLabelProps={{ shrink: true }} - sx={{ minWidth: 150 }} - /> - - - {/* Tabs + State Filter */} - - { - setTab(v); - setPage(0); - }} - sx={{ flex: 1 }} - > - - - - - State - - - - - {(authError || dataError) && ( - - {authError || dataError} - - )} - - {loading ? ( - - - - ) : ( - - - - - - - - - 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)} - - - ))} - - -
- - + {summary.map((item) => ( + + + + + {item.label} + + + {item.value} + + + {item.helper} + + + + + + ))} +
- - - )} -
+ + + + + { + setTab(value); + setPage(0); + }} + variant="scrollable" + scrollButtons="auto" + > + + + + + + + + + {stateOptions + .filter((option) => tab === 1 || option !== "merged") + .map((option) => ( + + tab === 0 ? setIssueFilter(option) : setPrFilter(option) + } + /> + ))} + + + + setSearchTitle(e.target.value)} + fullWidth + /> + + Repository + + + setStartDate(e.target.value)} + InputLabelProps={{ shrink: true }} + fullWidth + /> + setEndDate(e.target.value)} + InputLabelProps={{ shrink: true }} + fullWidth + /> + + + + + + + + {loading ? ( + + + Loading GitHub activity... + + ) : !hasData ? ( + + + Ready when you are + + + Enter a username and token to build an activity dashboard. + + + ) : filteredData.length === 0 ? ( + + + No matches found + + + Try clearing filters or changing the search text. + + + ) : ( + + {filteredData.map((item) => ( + + + + {getStatusIcon(item)} + + + {item.title} + + + + + + + + + + + + + ))} + + setPage(newPage)} + rowsPerPage={ROWS_PER_PAGE} + rowsPerPageOptions={[ROWS_PER_PAGE]} + /> + + )} + + + + + ); }; -export default Home; +export default Tracker; From 691bd0c302404f9ff54d61e983c7aee0eb500243 Mon Sep 17 00:00:00 2001 From: CodeByAlok24 Date: Tue, 12 May 2026 00:44:26 +0530 Subject: [PATCH 3/5] Polish home and contributors UI --- src/components/Features.tsx | 127 ++++----- src/components/Hero.tsx | 108 ++++++-- src/components/HowItWorks.tsx | 86 +++--- src/pages/Contributors/Contributors.tsx | 344 ++++++++++++++++++------ src/pages/Home/Home.tsx | 13 +- 5 files changed, 480 insertions(+), 198 deletions(-) diff --git a/src/components/Features.tsx b/src/components/Features.tsx index b8b2092..79d50ac 100644 --- a/src/components/Features.tsx +++ b/src/components/Features.tsx @@ -1,74 +1,79 @@ -import { BarChart3, Users, Search, Zap, Shield, Globe } from 'lucide-react'; +import { BarChart3, Filter, Github, LockKeyhole, MousePointerClick, Users } from "lucide-react"; -const Features = () => { - const features = [ - { - icon: BarChart3, - title: 'Activity Analytics', - description: 'Comprehensive charts and graphs showing commit patterns, contribution streaks, and repository activity over time.', - bgColor: 'bg-blue-100', - iconColor: 'text-blue-600' - }, - { - icon: Users, - title: 'Multi-User Tracking', - description: 'Monitor multiple GitHub users simultaneously and compare their activity levels and contribution patterns.', - bgColor: 'bg-green-100', - iconColor: 'text-green-600' - }, - { - icon: Search, - title: 'Smart Search', - description: 'Quickly find and add users to your tracking list with intelligent search and auto-suggestions.', - bgColor: 'bg-purple-100', - iconColor: 'text-purple-600' - }, - { - icon: Zap, - title: 'Real-time Updates', - description: 'Get instant notifications and updates when tracked users make new contributions or repositories.', - bgColor: 'bg-orange-100', - iconColor: 'text-orange-600' - }, - { - icon: Shield, - title: 'Privacy First', - description: 'All data is fetched from public GitHub APIs. We don\'t store personal information or require GitHub access.', - bgColor: 'bg-red-100', - iconColor: 'text-red-600' - }, - { - icon: Globe, - title: 'Export & Share', - description: 'Export activity reports and share insights with your team through various formats and integrations.', - bgColor: 'bg-indigo-100', - iconColor: 'text-indigo-600' - } - ]; +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 ( -
-
-
-

Powerful Features

-

- Everything you need to track, analyze, and understand GitHub activity patterns +

+
+
+

+ 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, index) => { - const IconComponent = feature.icon; +
+ {features.map((feature) => { + const Icon = feature.icon; return ( -
-
- +
+
+
-

{feature.title}

-

+

+ {feature.title} +

+

{feature.description}

-
+ ); })}
diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 1549f8e..068b259 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -1,29 +1,99 @@ -import { Search } from 'lucide-react'; -import { Link } from 'react-router-dom'; +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 ( -
-
-
-

- Track GitHub Activity - Like Never Before +
+
+
+
+ + Public GitHub insights in one workspace +
+ +

+ Track contributors, issues, and pull requests faster.

-

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

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

-
- - {/* - */} + +
+
+ +
+
+
+
+

+ 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} + +
+ ) + )} +
diff --git a/src/components/HowItWorks.tsx b/src/components/HowItWorks.tsx index 8abf1e1..74579df 100644 --- a/src/components/HowItWorks.tsx +++ b/src/components/HowItWorks.tsx @@ -1,45 +1,59 @@ +import { KeyRound, ListFilter, ScanSearch } from "lucide-react"; -const HowItWorks = () => { - const steps = [ - { - number: 1, - title: 'Search Users', - description: 'Enter GitHub usernames or search for users by name. Add them to your tracking dashboard.' - }, - { - number: 2, - title: 'Monitor Activity', - description: 'Watch insights of commits, pull requests, issues, and other GitHub activities.' - }, - { - number: 3, - title: 'Analyze Insights', - description: 'Review detailed analytics, export reports, and gain valuable insights into development patterns.' - } - ]; +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 ( -
-
-
-

How It Works

-

- Get started in minutes with our simple three-step process +

+
+
+

+ Workflow

+

+ From username to insight in three steps +

-
- {steps.map((step, index) => ( -
-
- {step.number} -
-

{step.title}

-

- {step.description} -

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

+ {step.title} +

+

+ {step.description} +

+
+ ); + })}
diff --git a/src/pages/Contributors/Contributors.tsx b/src/pages/Contributors/Contributors.tsx index d4fee52..e453778 100644 --- a/src/pages/Contributors/Contributors.tsx +++ b/src/pages/Contributors/Contributors.tsx @@ -1,19 +1,26 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { - Container, - Grid, - Card, - CardContent, + Alert, Avatar, - Typography, - Button, Box, + Button, + Card, + CardContent, + Chip, CircularProgress, - Alert, + Container, + FormControl, + InputLabel, + MenuItem, + Select, + Stack, + TextField, + Typography, } from "@mui/material"; import { FaGithub } from "react-icons/fa"; import { Link } from "react-router-dom"; import axios from "axios"; +import { Search, Trophy, Users } from "lucide-react"; import { GITHUB_REPO_CONTRIBUTORS_URL } from "../../utils/constants"; interface Contributor { @@ -28,8 +35,9 @@ const ContributorsPage = () => { const [contributors, setContributors] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [sortBy, setSortBy] = useState("contributions"); - // Fetch contributors from GitHub API useEffect(() => { const fetchContributors = async () => { try { @@ -47,97 +55,283 @@ const ContributorsPage = () => { fetchContributors(); }, []); + const filteredContributors = useMemo(() => { + const normalizedSearch = searchTerm.toLowerCase().trim(); + + return [...contributors] + .filter((contributor) => + contributor.login.toLowerCase().includes(normalizedSearch) + ) + .sort((a, b) => { + if (sortBy === "name") { + return a.login.localeCompare(b.login); + } + + return b.contributions - a.contributions; + }); + }, [contributors, searchTerm, sortBy]); + + const totalContributions = useMemo( + () => + contributors.reduce( + (total, contributor) => total + contributor.contributions, + 0 + ), + [contributors] + ); + + const topContributor = contributors[0]; + if (loading) { return ( - - + + + + Loading contributors... + ); } if (error) { return ( - + {error} - + ); } return ( -
- - - 🀝 Contributors - - - - {contributors.map((contributor) => ( - + + + + + + + + } + label="Community directory" + color="primary" + variant="outlined" + sx={{ mb: 2, fontWeight: 700 }} + /> + + Contributors + + + Discover the people helping GitHub Tracker move forward. Search, + sort, and open profile details from one clean directory. + + + + + + + {contributors.length} + + + Contributors + + + + + {totalContributions} + + + Contributions + + + + + {topContributor?.login ?? "-"} + + + Top contributor + + + + + + + + + + + setSearchTerm(event.target.value)} + fullWidth + InputProps={{ + startAdornment: , + }} + /> + + Sort by + + + + + + + {filteredContributors.length === 0 ? ( + + + No contributors found + + + Try another username or clear the search field. + + + ) : ( + + {filteredContributors.map((contributor, index) => ( - - - - - {contributor.login} - + + + + + {index < 3 && sortBy === "contributions" && ( + + + + )} + - - {contributor.contributions} Contributions + + + {contributor.login} - {/* - - Thank you for your valuable contributions to our - community! - */} - - - - + + + + + - + + + - - ))} - + ))} + + )} + -
+
); }; diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 03759ab..9ca08e8 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -4,13 +4,12 @@ import Features from "../../components/Features"; function Home() { return ( -
- - - - +
+ + +
- ) + ); } -export default Home +export default Home; From db142dca73f2860cfbcbcb60ed0c737bcb75d9d1 Mon Sep 17 00:00:00 2001 From: CodeByAlok24 Date: Tue, 12 May 2026 00:49:27 +0530 Subject: [PATCH 4/5] Add tracker lookup history --- src/hooks/useGitHubAuth.ts | 4 +- src/pages/Tracker/Tracker.tsx | 70 ++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/hooks/useGitHubAuth.ts b/src/hooks/useGitHubAuth.ts index 5284347..411da87 100644 --- a/src/hooks/useGitHubAuth.ts +++ b/src/hooks/useGitHubAuth.ts @@ -7,9 +7,9 @@ export const useGitHubAuth = () => { const [error, setError] = useState(''); const octokit = useMemo(() => { - if (!username || !token) return null; + if (!token) return null; return new Octokit({ auth: token }); - }, [username, token]); + }, [token]); const getOctokit = () => octokit; diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx index 0e12155..af3a5d4 100644 --- a/src/pages/Tracker/Tracker.tsx +++ b/src/pages/Tracker/Tracker.tsx @@ -33,8 +33,14 @@ import { useGitHubData } from "../../hooks/useGitHubData"; import type { GitHubItem } from "../../hooks/useGitHubData"; const ROWS_PER_PAGE = 10; +const LOOKUP_HISTORY_KEY = "github-tracker-lookup-history"; const stateOptions = ["all", "open", "closed", "merged"] as const; +interface LookupHistoryItem { + username: string; + searchedAt: string; +} + const formatDate = (dateString: string): string => new Intl.DateTimeFormat("en", { month: "short", @@ -70,6 +76,15 @@ const getStatusIcon = (item: GitHubItem) => { return ; }; +const getLookupHistory = (): LookupHistoryItem[] => { + try { + const savedHistory = localStorage.getItem(LOOKUP_HISTORY_KEY); + return savedHistory ? JSON.parse(savedHistory) : []; + } catch { + return []; + } +}; + const Tracker: React.FC = () => { const theme = useTheme(); const { @@ -99,6 +114,11 @@ const Tracker: React.FC = () => { const [selectedRepo, setSelectedRepo] = useState(""); const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); + const [lookupHistory, setLookupHistory] = useState([]); + + useEffect(() => { + setLookupHistory(getLookupHistory()); + }, []); useEffect(() => { if (username) { @@ -158,10 +178,37 @@ const Tracker: React.FC = () => { setPrFilter("all"); }; - const handleSubmit = (e: React.FormEvent): void => { + const saveLookupHistory = (nextUsername: string) => { + const normalizedUsername = nextUsername.trim(); + + if (!normalizedUsername) return; + + const nextHistory = [ + { username: normalizedUsername, searchedAt: new Date().toISOString() }, + ...lookupHistory.filter( + (item) => item.username.toLowerCase() !== normalizedUsername.toLowerCase() + ), + ].slice(0, 5); + + setLookupHistory(nextHistory); + localStorage.setItem(LOOKUP_HISTORY_KEY, JSON.stringify(nextHistory)); + }; + + const handleSubmit = async (e: React.FormEvent): Promise => { e.preventDefault(); setPage(0); - fetchData(username, 1, ROWS_PER_PAGE); + saveLookupHistory(username); + await fetchData(username, 1, ROWS_PER_PAGE); + }; + + const handleHistorySelect = async (historyUsername: string) => { + setUsername(historyUsername); + setPage(0); + + if (token) { + saveLookupHistory(historyUsername); + await fetchData(historyUsername, 1, ROWS_PER_PAGE); + } }; const hasData = issues.length > 0 || prs.length > 0; @@ -245,6 +292,25 @@ const Tracker: React.FC = () => { > {loading ? "Fetching activity..." : "Fetch activity"} + + {lookupHistory.length > 0 && ( + + + Recent username history + + + {lookupHistory.map((item) => ( + handleHistorySelect(item.username)} + /> + ))} + + + )} From 4336efccaa0df49f98ce3f294ff8c94f53362779 Mon Sep 17 00:00:00 2001 From: CodeByAlok24 Date: Thu, 14 May 2026 03:15:56 +0530 Subject: [PATCH 5/5] Improve the Login,signup with Mongodb --- .env.example | 1 - README.md | 61 ++++- backend/.env.sample | 3 - backend/models/User.js | 8 + backend/routes/auth.js | 44 ++++ backend/server.js | 10 +- docker-compose.yml | 6 +- Dockerfile.dev => frontend/Dockerfile.dev | 0 Dockerfile.prod => frontend/Dockerfile.prod | 0 eslint.config.js => frontend/eslint.config.js | 0 index.html => frontend/index.html | 0 package.json => frontend/package.json | 0 .../postcss.config.cjs | 0 {public => frontend/public}/_redirects | 0 {public => frontend/public}/crl-icon.png | Bin {public => frontend/public}/crl.png | Bin {public => frontend/public}/vite.svg | 0 {src => frontend/src}/App.css | 0 {src => frontend/src}/App.tsx | 4 +- {src => frontend/src}/Routes/Router.tsx | 0 {src => frontend/src}/assets/react.svg | 0 {src => frontend/src}/components/Features.tsx | 42 +++- frontend/src/components/Footer.tsx | 47 ++++ {src => frontend/src}/components/Hero.tsx | 51 ++-- .../src}/components/HowItWorks.tsx | 20 +- {src => frontend/src}/components/Navbar.tsx | 15 +- .../src}/components/ScrollProgressBar.tsx | 0 .../src}/context/ThemeContext.tsx | 0 {src => frontend/src}/context/theme.ts | 0 {src => frontend/src}/hooks/useGitHubAuth.ts | 0 {src => frontend/src}/hooks/useGitHubData.ts | 0 {src => frontend/src}/index.css | 0 {src => frontend/src}/main.tsx | 0 {src => frontend/src}/pages/About/About.tsx | 0 .../src}/pages/Contact/Contact.tsx | 0 .../ContributorProfile/ContributorProfile.tsx | 0 .../src}/pages/Contributors/Contributors.tsx | 0 {src => frontend/src}/pages/Home/Home.tsx | 0 {src => frontend/src}/pages/Login/Login.tsx | 83 +++++-- {src => frontend/src}/pages/Signup/Signup.tsx | 89 ++++--- .../src}/pages/Tracker/Tracker.tsx | 229 +++++++++++++++--- {src => frontend/src}/utils/constants.ts | 0 {src => frontend/src}/vite-env.d.ts | 0 .../tailwind.config.js | 0 .../tsconfig.app.json | 0 tsconfig.json => frontend/tsconfig.json | 0 .../tsconfig.node.json | 0 vite.config.ts => frontend/vite.config.ts | 0 src/components/Footer.tsx | 36 --- 49 files changed, 547 insertions(+), 202 deletions(-) delete mode 100644 .env.example delete mode 100644 backend/.env.sample rename Dockerfile.dev => frontend/Dockerfile.dev (100%) rename Dockerfile.prod => frontend/Dockerfile.prod (100%) rename eslint.config.js => frontend/eslint.config.js (100%) rename index.html => frontend/index.html (100%) rename package.json => frontend/package.json (100%) rename postcss.config.cjs => frontend/postcss.config.cjs (100%) rename {public => frontend/public}/_redirects (100%) rename {public => frontend/public}/crl-icon.png (100%) rename {public => frontend/public}/crl.png (100%) rename {public => frontend/public}/vite.svg (100%) rename {src => frontend/src}/App.css (100%) rename {src => frontend/src}/App.tsx (86%) rename {src => frontend/src}/Routes/Router.tsx (100%) rename {src => frontend/src}/assets/react.svg (100%) rename {src => frontend/src}/components/Features.tsx (67%) create mode 100644 frontend/src/components/Footer.tsx rename {src => frontend/src}/components/Hero.tsx (74%) rename {src => frontend/src}/components/HowItWorks.tsx (78%) rename {src => frontend/src}/components/Navbar.tsx (94%) rename {src => frontend/src}/components/ScrollProgressBar.tsx (100%) rename {src => frontend/src}/context/ThemeContext.tsx (100%) rename {src => frontend/src}/context/theme.ts (100%) rename {src => frontend/src}/hooks/useGitHubAuth.ts (100%) rename {src => frontend/src}/hooks/useGitHubData.ts (100%) rename {src => frontend/src}/index.css (100%) rename {src => frontend/src}/main.tsx (100%) rename {src => frontend/src}/pages/About/About.tsx (100%) rename {src => frontend/src}/pages/Contact/Contact.tsx (100%) rename {src => frontend/src}/pages/ContributorProfile/ContributorProfile.tsx (100%) rename {src => frontend/src}/pages/Contributors/Contributors.tsx (100%) rename {src => frontend/src}/pages/Home/Home.tsx (100%) rename {src => frontend/src}/pages/Login/Login.tsx (64%) rename {src => frontend/src}/pages/Signup/Signup.tsx (73%) rename {src => frontend/src}/pages/Tracker/Tracker.tsx (74%) rename {src => frontend/src}/utils/constants.ts (100%) rename {src => frontend/src}/vite-env.d.ts (100%) rename tailwind.config.js => frontend/tailwind.config.js (100%) rename tsconfig.app.json => frontend/tsconfig.app.json (100%) rename tsconfig.json => frontend/tsconfig.json (100%) rename tsconfig.node.json => frontend/tsconfig.node.json (100%) rename vite.config.ts => frontend/vite.config.ts (100%) delete mode 100644 src/components/Footer.tsx 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 100% rename from package.json rename to frontend/package.json 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/src/components/Features.tsx b/frontend/src/components/Features.tsx similarity index 67% rename from src/components/Features.tsx rename to frontend/src/components/Features.tsx index 79d50ac..f942ee5 100644 --- a/src/components/Features.tsx +++ b/frontend/src/components/Features.tsx @@ -1,48 +1,64 @@ -import { BarChart3, Filter, Github, LockKeyhole, MousePointerClick, Users } from "lucide-react"; +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.", + 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.", + 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.", + 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.", + 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.", + 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.", + 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 @@ -51,8 +67,8 @@ const Features = () => { A simpler way to understand GitHub activity

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

@@ -64,7 +80,9 @@ const Features = () => { key={feature.title} className="group rounded-xl border border-gray-200 bg-white p-6 transition hover:-translate-y-1 hover:border-blue-200 hover:shadow-lg dark:border-gray-800 dark:bg-gray-950 dark:hover:border-blue-900" > -
+

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/src/components/Hero.tsx b/frontend/src/components/Hero.tsx similarity index 74% rename from src/components/Hero.tsx rename to frontend/src/components/Hero.tsx index 068b259..3c92c99 100644 --- a/src/components/Hero.tsx +++ b/frontend/src/components/Hero.tsx @@ -10,7 +10,7 @@ const previewItems = [ const Hero = () => { return (
-
+
@@ -22,8 +22,9 @@ const Hero = () => {

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

@@ -66,7 +67,9 @@ const Hero = () => { key={item.label} className="rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-800 dark:bg-gray-900" > -

{item.value}

+

+ {item.value} +

{item.label}

@@ -75,24 +78,32 @@ const Hero = () => {
- {["Review new UI dashboard", "Fix API rate-limit state", "Improve contributor profile"].map( - (title, index) => ( -
-
- - - {title} - -
- - #{223 + index} + {[ + "Review new UI dashboard", + "Fix API rate-limit state", + "Improve contributor profile", + ].map((title, index) => ( +
+
+ + + {title}
- ) - )} + + #{223 + index} + +
+ ))}
diff --git a/src/components/HowItWorks.tsx b/frontend/src/components/HowItWorks.tsx similarity index 78% rename from src/components/HowItWorks.tsx rename to frontend/src/components/HowItWorks.tsx index 74579df..cfbc291 100644 --- a/src/components/HowItWorks.tsx +++ b/frontend/src/components/HowItWorks.tsx @@ -4,24 +4,30 @@ const steps = [ { icon: ScanSearch, title: "Enter a GitHub user", - description: "Add a username and token to fetch reliable GitHub activity data.", + 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.", + 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.", + description: + "Jump straight to GitHub when a result needs review or follow-up.", }, ]; const HowItWorks = () => { return ( -
-
+
+

Workflow @@ -43,7 +49,9 @@ const HowItWorks = () => {

- 0{index + 1} + + 0{index + 1} +

{step.title} diff --git a/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx similarity index 94% rename from src/components/Navbar.tsx rename to frontend/src/components/Navbar.tsx index ede16a3..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/theme"; -import { Moon, Sun } from 'lucide-react'; - +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 (