`.
### 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 (
-
+
{/* Logo Section */}
{
onClick={toggleTheme}
className="text-sm font-semibold px-3 py-1 rounded border border-gray-500 hover:text-gray-300 hover:border-gray-300 transition duration-200"
>
- {mode === "dark" ? : }
+ {mode === "dark" ? (
+
+ ) : (
+
+ )}
diff --git a/src/components/ScrollProgressBar.tsx b/frontend/src/components/ScrollProgressBar.tsx
similarity index 100%
rename from src/components/ScrollProgressBar.tsx
rename to frontend/src/components/ScrollProgressBar.tsx
diff --git a/src/context/ThemeContext.tsx b/frontend/src/context/ThemeContext.tsx
similarity index 80%
rename from src/context/ThemeContext.tsx
rename to frontend/src/context/ThemeContext.tsx
index b6866e3..4d80ff0 100644
--- a/src/context/ThemeContext.tsx
+++ b/frontend/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/frontend/src/context/theme.ts b/frontend/src/context/theme.ts
new file mode 100644
index 0000000..8e364d2
--- /dev/null
+++ b/frontend/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/useGitHubAuth.ts b/frontend/src/hooks/useGitHubAuth.ts
similarity index 87%
rename from src/hooks/useGitHubAuth.ts
rename to frontend/src/hooks/useGitHubAuth.ts
index 5284347..411da87 100644
--- a/src/hooks/useGitHubAuth.ts
+++ b/frontend/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/hooks/useGitHubData.ts b/frontend/src/hooks/useGitHubData.ts
similarity index 60%
rename from src/hooks/useGitHubData.ts
rename to frontend/src/hooks/useGitHubData.ts
index a0ebe10..35d41cc 100644
--- a/src/hooks/useGitHubData.ts
+++ b/frontend/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/index.css b/frontend/src/index.css
similarity index 100%
rename from src/index.css
rename to frontend/src/index.css
diff --git a/src/main.tsx b/frontend/src/main.tsx
similarity index 100%
rename from src/main.tsx
rename to frontend/src/main.tsx
diff --git a/src/pages/About/About.tsx b/frontend/src/pages/About/About.tsx
similarity index 100%
rename from src/pages/About/About.tsx
rename to frontend/src/pages/About/About.tsx
diff --git a/src/pages/Contact/Contact.tsx b/frontend/src/pages/Contact/Contact.tsx
similarity index 99%
rename from src/pages/Contact/Contact.tsx
rename to frontend/src/pages/Contact/Contact.tsx
index a0bfccb..e6f6476 100644
--- a/src/pages/Contact/Contact.tsx
+++ b/frontend/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/frontend/src/pages/ContributorProfile/ContributorProfile.tsx
similarity index 94%
rename from src/pages/ContributorProfile/ContributorProfile.tsx
rename to frontend/src/pages/ContributorProfile/ContributorProfile.tsx
index b4ab931..abd12ef 100644
--- a/src/pages/ContributorProfile/ContributorProfile.tsx
+++ b/frontend/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/frontend/src/pages/Contributors/Contributors.tsx b/frontend/src/pages/Contributors/Contributors.tsx
new file mode 100644
index 0000000..e453778
--- /dev/null
+++ b/frontend/src/pages/Contributors/Contributors.tsx
@@ -0,0 +1,338 @@
+import { useEffect, useMemo, useState } from "react";
+import {
+ Alert,
+ Avatar,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ CircularProgress,
+ 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 {
+ id: number;
+ login: string;
+ avatar_url: string;
+ contributions: number;
+ html_url: string;
+}
+
+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");
+
+ useEffect(() => {
+ const fetchContributors = async () => {
+ try {
+ const response = await axios.get(GITHUB_REPO_CONTRIBUTORS_URL, {
+ withCredentials: false,
+ });
+ setContributors(response.data);
+ } catch {
+ setError("Failed to fetch contributors. Please try again later.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ 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 (
+
+
+
+
+
+
+
+ }
+ 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
+ setSortBy(event.target.value)}
+ >
+ Most contributions
+ Name A-Z
+
+
+
+
+
+
+ {filteredContributors.length === 0 ? (
+
+
+ No contributors found
+
+
+ Try another username or clear the search field.
+
+
+ ) : (
+
+ {filteredContributors.map((contributor, index) => (
+
+
+
+
+
+ {index < 3 && sortBy === "contributions" && (
+
+
+
+ )}
+
+
+
+
+ {contributor.login}
+
+
+
+
+
+
+ Profile
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+};
+
+export default ContributorsPage;
diff --git a/src/pages/Home/Home.tsx b/frontend/src/pages/Home/Home.tsx
similarity index 63%
rename from src/pages/Home/Home.tsx
rename to frontend/src/pages/Home/Home.tsx
index 03759ab..9ca08e8 100644
--- a/src/pages/Home/Home.tsx
+++ b/frontend/src/pages/Home/Home.tsx
@@ -4,13 +4,12 @@ import Features from "../../components/Features";
function Home() {
return (
-
-
-
-
-
+
+
+
+
- )
+ );
}
-export default Home
+export default Home;
diff --git a/src/pages/Login/Login.tsx b/frontend/src/pages/Login/Login.tsx
similarity index 59%
rename from src/pages/Login/Login.tsx
rename to frontend/src/pages/Login/Login.tsx
index d6f21a7..9da59df 100644
--- a/src/pages/Login/Login.tsx
+++ b/frontend/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;
@@ -12,7 +12,10 @@ interface LoginFormData {
}
const Login: React.FC = () => {
- const [formData, setFormData] = useState
({ email: "", password: "" });
+ const [formData, setFormData] = useState({
+ email: "",
+ password: "",
+ });
const [message, setMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
@@ -30,14 +33,24 @@ const Login: React.FC = () => {
setIsLoading(true);
try {
- const response = await axios.post(`${backendUrl}/api/auth/login`, formData);
+ const response = await axios.post(
+ `${backendUrl}/api/auth/login`,
+ formData,
+ {
+ withCredentials: true,
+ },
+ );
setMessage(response.data.message);
- if (response.data.message === 'Login successful') {
- navigate("/home");
+ if (response.data.message === "Login successful") {
+ navigate("/track");
+ }
+ } catch (error: unknown) {
+ if (axios.isAxiosError<{ message?: string }>(error)) {
+ setMessage(error.response?.data?.message || "Something went wrong");
+ } else {
+ setMessage("Something went wrong");
}
- } catch (error: any) {
- setMessage(error.response?.data?.message || "Something went wrong");
} finally {
setIsLoading(false);
}
@@ -53,34 +66,54 @@ const Login: React.FC = () => {
>
{/* Animated background elements */}
{/* Branding */}
-
+
-
+
GitHubTracker
-
+
Track your GitHub journey
{/* Form Card */}
-
-
+
+
Welcome Back
@@ -130,18 +163,22 @@ const Login: React.FC = () => {
{/* Message */}
{message && (
-
+
{message}
)}
{/* Footer Text */}
-
+
Don't have an account?
{
-
+
);
};
-export default Login;
\ No newline at end of file
+export default Login;
diff --git a/src/pages/Signup/Signup.tsx b/frontend/src/pages/Signup/Signup.tsx
similarity index 73%
rename from src/pages/Signup/Signup.tsx
rename to frontend/src/pages/Signup/Signup.tsx
index d03a921..6f52613 100644
--- a/src/pages/Signup/Signup.tsx
+++ b/frontend/src/pages/Signup/Signup.tsx
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import axios from "axios";
-import { useNavigate ,Link } from "react-router-dom";
+import { useNavigate, Link } from "react-router-dom";
import { User, Mail, Lock } from "lucide-react";
const backendUrl = import.meta.env.VITE_BACKEND_URL;
interface SignUpFormData {
@@ -10,13 +10,13 @@ interface SignUpFormData {
}
const SignUp: React.FC = () => {
- const [formData, setFormData] = useState({
- username: "",
- email: "",
- password: ""
+ const [formData, setFormData] = useState({
+ username: "",
+ email: "",
+ password: "",
});
const [message, setMessage] = useState("");
-const navigate = useNavigate();
+ const navigate = useNavigate();
const handleChange = (e: React.ChangeEvent) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
@@ -25,34 +25,35 @@ const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
- const response = await axios.post(`${backendUrl}/api/auth/signup`,
- formData // Include cookies for session
+ const response = await axios.post(
+ `${backendUrl}/api/auth/signup`,
+ formData,
+ { withCredentials: true },
);
setMessage(response.data.message); // Show success message from backend
// Navigate to login page after successful signup
- if (response.data.message === 'User created successfully') {
- navigate("/login");}
-
-
- // // Simulate API call (replace with your actual backend integration)
- // try {
- // // Mock successful signup
- // setMessage("Account created successfully! Redirecting to login...");
-
- // // In your actual implementation, integrate with your backend here:
- // // const response = await fetch(`${backendUrl}/api/auth/signup`, {
- // // method: 'POST',
- // // headers: { 'Content-Type': 'application/json' },
- // // body: JSON.stringify(formData)
- // // });
-
- // setTimeout(() => {
- // // Navigate to login page in your actual implementation
- // console.log("Redirecting to login page...");
- // }, 2000);
-
- } catch (error) {
+ if (response.data.message === "User created successfully") {
+ navigate("/login");
+ }
+
+ // // Simulate API call (replace with your actual backend integration)
+ // try {
+ // // Mock successful signup
+ // setMessage("Account created successfully! Redirecting to login...");
+
+ // // In your actual implementation, integrate with your backend here:
+ // // const response = await fetch(`${backendUrl}/api/auth/signup`, {
+ // // method: 'POST',
+ // // headers: { 'Content-Type': 'application/json' },
+ // // body: JSON.stringify(formData)
+ // // });
+
+ // setTimeout(() => {
+ // // Navigate to login page in your actual implementation
+ // console.log("Redirecting to login page...");
+ // }, 2000);
+ } catch {
setMessage("Something went wrong. Please try again.");
}
};
@@ -68,17 +69,23 @@ const navigate = useNavigate();
{/* Logo and Title */}
-
-
-
+
+
+
GitHubTracker
Join your GitHub journey
{/* Sign Up Form */}
-
Create Account
-
+
+ Create Account
+
+
@@ -134,18 +141,20 @@ const navigate = useNavigate();
{message && (
-
+
{message}
)}
- Already have an account?{' '}
+ Already have an account?{" "}
Sign in here
@@ -159,4 +168,4 @@ const navigate = useNavigate();
);
};
-export default SignUp;
\ No newline at end of file
+export default SignUp;
diff --git a/frontend/src/pages/Tracker/Tracker.tsx b/frontend/src/pages/Tracker/Tracker.tsx
new file mode 100644
index 0000000..bdf29bc
--- /dev/null
+++ b/frontend/src/pages/Tracker/Tracker.tsx
@@ -0,0 +1,740 @@
+import React, { useEffect, useMemo, useState } from "react";
+import axios from "axios";
+import {
+ IssueOpenedIcon,
+ IssueClosedIcon,
+ GitPullRequestIcon,
+ GitPullRequestClosedIcon,
+ GitMergeIcon,
+} from "@primer/octicons-react";
+import {
+ Alert,
+ Box,
+ Button,
+ Chip,
+ CircularProgress,
+ 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 backendUrl = import.meta.env.VITE_BACKEND_URL;
+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",
+ day: "numeric",
+ year: "numeric",
+ }).format(new Date(dateString));
+
+const getRepositoryName = (repositoryUrl: string): string =>
+ repositoryUrl.split("/").slice(-1)[0] || "repository";
+
+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 getLookupHistory = (): LookupHistoryItem[] => {
+ try {
+ const savedHistory = localStorage.getItem(LOOKUP_HISTORY_KEY);
+ return savedHistory ? JSON.parse(savedHistory) : [];
+ } catch {
+ return [];
+ }
+};
+
+const loadTrackerHistoryFromBackend = async (): Promise<
+ LookupHistoryItem[]
+> => {
+ try {
+ const response = await axios.get(`${backendUrl}/api/auth/tracker-history`, {
+ withCredentials: true,
+ });
+ return response.data.trackerHistory || [];
+ } catch (err) {
+ console.log(
+ "Could not load tracker history from backend, using localStorage",
+ );
+ return getLookupHistory();
+ }
+};
+
+const saveTrackerHistoryToBackend = async (
+ username: string,
+ searchedAt: string,
+) => {
+ try {
+ await axios.post(
+ `${backendUrl}/api/auth/tracker-history`,
+ { username, searchedAt },
+ { withCredentials: true },
+ );
+ } catch (err) {
+ console.log("Could not save tracker history to backend");
+ }
+};
+
+const Tracker: React.FC = () => {
+ const theme = useTheme();
+ const {
+ username,
+ setUsername,
+ token,
+ setToken,
+ error: authError,
+ getOctokit,
+ } = useGitHubAuth();
+
+ const {
+ issues,
+ prs,
+ totalIssues,
+ totalPrs,
+ loading,
+ error: dataError,
+ fetchData,
+ } = useGitHubData(getOctokit);
+
+ const [tab, setTab] = useState(0);
+ const [page, setPage] = useState(0);
+ const [issueFilter, setIssueFilter] = useState("all");
+ const [prFilter, setPrFilter] = useState("all");
+ const [searchTitle, setSearchTitle] = useState("");
+ const [selectedRepo, setSelectedRepo] = useState("");
+ const [startDate, setStartDate] = useState("");
+ const [endDate, setEndDate] = useState("");
+ const [lookupHistory, setLookupHistory] = useState([]);
+
+ useEffect(() => {
+ const loadHistory = async () => {
+ const history = await loadTrackerHistoryFromBackend();
+ setLookupHistory(history);
+ };
+ loadHistory();
+ }, []);
+
+ useEffect(() => {
+ if (username) {
+ fetchData(username, page + 1, ROWS_PER_PAGE);
+ }
+ }, [fetchData, page, tab, username]);
+
+ const activeFilter = tab === 0 ? issueFilter : prFilter;
+ const currentRawData = tab === 0 ? issues : prs;
+
+ const repositories = useMemo(
+ () =>
+ Array.from(
+ new Set(
+ [...issues, ...prs].map((item) =>
+ getRepositoryName(item.repository_url),
+ ),
+ ),
+ ).sort(),
+ [issues, prs],
+ );
+
+ 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 saveLookupHistory = async (nextUsername: string) => {
+ const normalizedUsername = nextUsername.trim();
+
+ if (!normalizedUsername) return;
+
+ const searchedAt = new Date().toISOString();
+ const nextHistory = [
+ { username: normalizedUsername, searchedAt },
+ ...lookupHistory.filter(
+ (item) =>
+ item.username.toLowerCase() !== normalizedUsername.toLowerCase(),
+ ),
+ ].slice(0, 5);
+
+ setLookupHistory(nextHistory);
+ localStorage.setItem(LOOKUP_HISTORY_KEY, JSON.stringify(nextHistory));
+
+ // Save to backend
+ await saveTrackerHistoryToBackend(normalizedUsername, searchedAt);
+ };
+
+ const handleSubmit = async (
+ e: React.FormEvent,
+ ): Promise => {
+ e.preventDefault();
+ setPage(0);
+ await saveLookupHistory(username);
+ await fetchData(username, 1, ROWS_PER_PAGE);
+ };
+
+ const handleHistorySelect = async (historyUsername: string) => {
+ setUsername(historyUsername);
+ setPage(0);
+
+ if (token) {
+ await saveLookupHistory(historyUsername);
+ await fetchData(historyUsername, 1, ROWS_PER_PAGE);
+ }
+ };
+
+ const hasData = issues.length > 0 || prs.length > 0;
+ const totalCount = tab === 0 ? totalIssues : totalPrs;
+
+ return (
+
+
+
+
+
+
+
+
+ Track GitHub activity without the clutter
+
+
+ Search a profile, filter by state or repository, and scan
+ issues and pull requests in one focused workspace.
+
+
+
+
+
+ setUsername(e.target.value)}
+ required
+ fullWidth
+ />
+ setToken(e.target.value)}
+ type="password"
+ required
+ fullWidth
+ helperText="Used only in your browser session for GitHub API requests."
+ />
+
+ {loading ? "Fetching activity..." : "Fetch activity"}
+
+
+ {lookupHistory.length > 0 && (
+
+
+ Recent username history
+
+
+ {lookupHistory.map((item) => (
+ handleHistorySelect(item.username)}
+ />
+ ))}
+
+
+ )}
+
+
+
+
+
+ {(authError || dataError) && (
+ {authError || dataError}
+ )}
+
+
+ {summary.map((item) => (
+
+
+
+
+ {item.label}
+
+
+ {item.value}
+
+
+ {item.helper}
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+ {
+ setTab(value);
+ setPage(0);
+ }}
+ variant="scrollable"
+ scrollButtons="auto"
+ >
+
+
+
+
+
+ Clear filters
+
+
+
+
+ {stateOptions
+ .filter((option) => tab === 1 || option !== "merged")
+ .map((option) => (
+
+ tab === 0
+ ? setIssueFilter(option)
+ : setPrFilter(option)
+ }
+ />
+ ))}
+
+
+
+ setSearchTitle(e.target.value)}
+ fullWidth
+ />
+
+ Repository
+ setSelectedRepo(e.target.value)}
+ >
+ All repositories
+ {repositories.map((repo) => (
+
+ {repo}
+
+ ))}
+
+
+ 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}
+
+
+
+
+
+
+
+
+
+
+ View on GitHub
+
+
+
+ ))}
+
+ setPage(newPage)}
+ rowsPerPage={ROWS_PER_PAGE}
+ rowsPerPageOptions={[ROWS_PER_PAGE]}
+ />
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default Tracker;
diff --git a/src/utils/constants.ts b/frontend/src/utils/constants.ts
similarity index 100%
rename from src/utils/constants.ts
rename to frontend/src/utils/constants.ts
diff --git a/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
similarity index 100%
rename from src/vite-env.d.ts
rename to frontend/src/vite-env.d.ts
diff --git a/tailwind.config.js b/frontend/tailwind.config.js
similarity index 100%
rename from tailwind.config.js
rename to frontend/tailwind.config.js
diff --git a/tsconfig.app.json b/frontend/tsconfig.app.json
similarity index 100%
rename from tsconfig.app.json
rename to frontend/tsconfig.app.json
diff --git a/tsconfig.json b/frontend/tsconfig.json
similarity index 100%
rename from tsconfig.json
rename to frontend/tsconfig.json
diff --git a/tsconfig.node.json b/frontend/tsconfig.node.json
similarity index 100%
rename from tsconfig.node.json
rename to frontend/tsconfig.node.json
diff --git a/vite.config.ts b/frontend/vite.config.ts
similarity index 100%
rename from vite.config.ts
rename to frontend/vite.config.ts
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/Features.tsx b/src/components/Features.tsx
deleted file mode 100644
index b8b2092..0000000
--- a/src/components/Features.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { BarChart3, Users, Search, Zap, Shield, Globe } 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'
- }
- ];
-
- return (
-
-
-
-
Powerful Features
-
- Everything you need to track, analyze, and understand GitHub activity patterns
-
-
-
-
- {features.map((feature, index) => {
- const IconComponent = feature.icon;
- return (
-
-
-
-
-
{feature.title}
-
- {feature.description}
-
-
- );
- })}
-
-
-
- );
-};
-
-export default Features;
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
deleted file mode 100644
index 878366f..0000000
--- a/src/components/Footer.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-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/src/components/Hero.tsx
deleted file mode 100644
index 1549f8e..0000000
--- a/src/components/Hero.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Search } from 'lucide-react';
-import { Link } from 'react-router-dom';
-
-const Hero = () => {
- return (
-
-
-
-
- Track GitHub Activity
- Like Never Before
-
-
- 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.
-
-
-
-
- Start Tracking
-
- {/*
-
- View Demo
-
- */}
-
-
-
-
- );
-};
-
-export default Hero;
diff --git a/src/components/HowItWorks.tsx b/src/components/HowItWorks.tsx
deleted file mode 100644
index 8abf1e1..0000000
--- a/src/components/HowItWorks.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-
-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.'
- }
- ];
-
- return (
-
-
-
-
How It Works
-
- Get started in minutes with our simple three-step process
-
-
-
-
- {steps.map((step, index) => (
-
-
- {step.number}
-
-
{step.title}
-
- {step.description}
-
-
- ))}
-
-
-
- );
-};
-
-export default HowItWorks;
diff --git a/src/pages/Contributors/Contributors.tsx b/src/pages/Contributors/Contributors.tsx
deleted file mode 100644
index 60270b1..0000000
--- a/src/pages/Contributors/Contributors.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import { useEffect, useState } from "react";
-import {
- Container,
- Grid,
- Card,
- CardContent,
- Avatar,
- Typography,
- Button,
- Box,
- CircularProgress,
- Alert,
-} from "@mui/material";
-import { FaGithub } from "react-icons/fa";
-import { Link } from "react-router-dom";
-import axios from "axios";
-import { GITHUB_REPO_CONTRIBUTORS_URL } from "../../utils/constants";
-
-interface Contributor {
- id: number;
- login: string;
- avatar_url: string;
- contributions: number;
- html_url: string;
-}
-
-const ContributorsPage = () => {
- const [contributors, setContributors] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
-
- // Fetch contributors from GitHub API
- useEffect(() => {
- const fetchContributors = async () => {
- try {
- const response = await axios.get(GITHUB_REPO_CONTRIBUTORS_URL, {
- withCredentials: false,
- });
- setContributors(response.data);
- } catch (err) {
- setError("Failed to fetch contributors. Please try again later.");
- } finally {
- setLoading(false);
- }
- };
-
- fetchContributors();
- }, []);
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- if (error) {
- return (
-
- {error}
-
- );
- }
-
- return (
-
-
-
- π€ Contributors
-
-
-
- {contributors.map((contributor) => (
-
-
-
-
-
-
- {contributor.login}
-
-
-
- {contributor.contributions} Contributions
-
- {/*
-
- Thank you for your valuable contributions to our
- community!
- */}
-
-
-
-
- }
- href={contributor.html_url}
- target="_blank"
- sx={{
- backgroundColor: "#333333",
- textTransform: "none",
- color: "#FFFFFF",
- "&:hover": {
- backgroundColor: "#555555",
- },
- }}
- >
- GitHub
-
-
-
-
- ))}
-
-
-
- );
-};
-
-export default ContributorsPage;
diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx
deleted file mode 100644
index 2bd4d30..0000000
--- a/src/pages/Tracker/Tracker.tsx
+++ /dev/null
@@ -1,351 +0,0 @@
-import React, { useState, useEffect } from "react"
-import {
- IssueOpenedIcon,
- IssueClosedIcon,
- GitPullRequestIcon,
- GitPullRequestClosedIcon,
- GitMergeIcon,
-} from '@primer/octicons-react';
-import {
- Container,
- Box,
- TextField,
- Button,
- Paper,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- TablePagination,
- Link,
- CircularProgress,
- Alert,
- Tabs,
- Tab,
- Select,
- MenuItem,
- FormControl,
- InputLabel,
-} from "@mui/material";
-import { useTheme } from "@mui/material/styles";
-import { useGitHubAuth } from "../../hooks/useGitHubAuth";
-import { useGitHubData } from "../../hooks/useGitHubData";
-
-const ROWS_PER_PAGE = 10;
-
-interface GitHubItem {
- id: number;
- title: string;
- state: string;
- created_at: string;
- pull_request?: { merged_at: string | null };
- repository_url: string;
- html_url: string;
-}
-
-const Home: React.FC = () => {
-
- const theme = useTheme();
-
- const {
- username,
- setUsername,
- token,
- setToken,
- error: authError,
- getOctokit,
- } = useGitHubAuth();
-
- const {
- issues,
- prs,
- totalIssues,
- totalPrs,
- loading,
- error: dataError,
- fetchData,
- } = useGitHubData(getOctokit);
-
- const [tab, setTab] = useState(0);
- const [page, setPage] = useState(0);
-
- const [issueFilter, setIssueFilter] = useState("all");
- const [prFilter, setPrFilter] = useState("all");
- const [searchTitle, setSearchTitle] = useState("");
- const [selectedRepo, setSelectedRepo] = useState("");
- 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);
- }
- }, [tab, page]);
-
- const handleSubmit = (e: React.FormEvent): void => {
- e.preventDefault();
- setPage(0);
- fetchData(username, 1, ROWS_PER_PAGE);
- };
-
- const handlePageChange = (_: unknown, newPage: number) => {
- setPage(newPage);
- };
-
- 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 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 ;
- };
-
-
- // 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 totalCount = tab === 0 ? totalIssues : totalPrs;
-
- return (
-
- {/* Auth Form */}
-
-
-
-
- {/* 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
-
- tab === 0
- ? setIssueFilter(e.target.value)
- : setPrFilter(e.target.value)
- }
- label="State"
- sx={{
- backgroundColor: theme.palette.background.paper,
- color: theme.palette.text.primary,
- borderRadius: "4px",
- "& .MuiSelect-select": { padding: "10px" },
- "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
- borderColor: theme.palette.primary.main,
- },
- }}
- >
- All
- Open
- Closed
- {tab === 1 && Merged }
-
-
-
-
- {(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)}
-
-
- ))}
-
-
-
-
-
-
-
-
- )}
-
- );
-};
-
-export default Home;