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/src/pages/Contributors/Contributors.tsx b/src/pages/Contributors/Contributors.tsx index 6ad7d5a..b58b310 100644 --- a/src/pages/Contributors/Contributors.tsx +++ b/src/pages/Contributors/Contributors.tsx @@ -1,143 +1,371 @@ -import { useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { Container, - Grid, + Box, + Typography, Card, CardContent, + Grid, Avatar, - Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + LinearProgress, Button, - Box, + Select, + MenuItem, + IconButton, + Tooltip, CircularProgress, - Alert, + Checkbox, + Pagination, } 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 REPO_OWNER = "mehul-m-prajapati"; +const REPO_NAME = "github_tracker"; +const REPO_FULL = `${REPO_OWNER}/${REPO_NAME}`; + +const PERIOD_OPTIONS = ["7d", "14d", "30d", "90d"]; const ContributorsPage = () => { - const [contributors, setContributors] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + // Summary stats + const [repoStats, setRepoStats] = useState(null); + const [prStats, setPrStats] = useState({ opened: 0, merged: 0, velocity: "-" }); + const [issueStats, setIssueStats] = useState({ opened: 0, closed: 0, velocity: "-" }); + const [contributors, setContributors] = useState([]); + const [loading, setLoading] = useState(true); + const [period, setPeriod] = useState("30d"); + const [page, setPage] = useState(1); + const [perPage] = useState(5); - // Fetch contributors from GitHub API + // Fetch repo stats, PRs, issues, contributors useEffect(() => { - const fetchContributors = async () => { + const fetchData = async () => { + setLoading(true); try { - const response = await axios.get(GITHUB_REPO_CONTRIBUTORS_URL, { - withCredentials: false, - }); - setContributors(response.data); + // Repo stats (stars, forks, open issues) + const repoRes = await axios.get(`https://api.github.com/repos/${REPO_FULL}`); + setRepoStats(repoRes.data); + + // Contributors + const contribRes = await axios.get(`https://api.github.com/repos/${REPO_FULL}/contributors?per_page=100`); + setContributors(contribRes.data); + + // PRs (all states) + const prRes = await axios.get(`https://api.github.com/repos/${REPO_FULL}/pulls?state=all&per_page=100`); + const prs = prRes.data; + const opened = prs.length; + const merged = prs.filter(pr => pr.merged_at).length; + // Calculate PR velocity (mock: 1d if any, else '-') + const velocity = opened > 0 ? "1d" : "-"; + setPrStats({ opened, merged, velocity }); + + // Issues (all states, filter out PRs) + const issueRes = await axios.get(`https://api.github.com/repos/${REPO_FULL}/issues?state=all&per_page=100`); + const issues = issueRes.data.filter(issue => !issue.pull_request); + const openedIssues = issues.length; + const closedIssues = issues.filter(issue => issue.state === "closed").length; + // Calculate Issue velocity (mock: 5d if any, else '-') + const issueVelocity = openedIssues > 0 ? "5d" : "-"; + setIssueStats({ opened: openedIssues, closed: closedIssues, velocity: issueVelocity }); } catch (err) { - setError("Failed to fetch contributors. Please try again later."); + setRepoStats(null); + setContributors([]); + setPrStats({ opened: 0, merged: 0, velocity: "-" }); + setIssueStats({ opened: 0, closed: 0, velocity: "-" }); } finally { setLoading(false); } }; + fetchData(); + }, [period]); - fetchContributors(); - }, []); + // Pagination logic + const paginatedContributors = contributors.slice((page - 1) * perPage, page * perPage); + const totalPages = Math.ceil(contributors.length / perPage); - if (loading) { - return ( - - - - ); - } + // Helper for activity (mock: based on contributions) + const getActivity = (contrib) => { + if (contrib.contributions > 50) return { + label: "High", + color: "#22c55e", + bg: "linear-gradient(90deg, #bbf7d0 0%, #d1fae5 100%)", + icon: ( + + ) + }; + if (contrib.contributions > 10) return { + label: "Medium", + color: "#eab308", + bg: "linear-gradient(90deg, #fde68a 0%, #fef9c3 100%)", + icon: "— " + }; + return { + label: "Low", + color: "#ef4444", + bg: "linear-gradient(90deg, #fecaca 0%, #fee2e2 100%)", + icon: ( + + ) + }; + }; - if (error) { - return ( - - {error} - - ); - } + // Helper for PR overview (mock: random %) + const getPROverview = (contrib) => { + const prs = contrib.contributions; + // For demo, percent is capped at 100 + const percent = Math.min(100, Math.round((prs / 132) * 100)); // 132 is max in demo + return { prs, percent }; + }; - return ( -
- - - 🤝 Contributors - + // Helper for PR velocity (mock: random) + const getPRVelocity = () => ({ velocity: "1d", percent: 55 }); - - {contributors.map((contributor) => ( - - - - + // Helper for contributors avatars (mock: show avatar) + const getContributorsAvatars = (contrib) => [contrib.avatar_url]; - - - {contributor.login} - - - - {contributor.contributions} Contributions - - {/* - - Thank you for your valuable contributions to our - community! - */} + // Helper for last 30 days (mock: SVG line) + const ActivityGraph = () => ( + + + + ); - - + return ( + + {/* Header */} + + + + Github-Tracker Workspace + + + {/* */} + + + + + {/* Summary Cards */} + + {/* PRs */} + + + + + {/* PR Icon in box */} + + + + Pull Requests + + + + Opened + {prStats.opened} + + + Merged + {prStats.merged} + + + Velocity + {prStats.velocity} + + + + + + {/* Issues */} + + + + + {/* Issue Icon in box */} + + + + Issues + + + + Opened + {issueStats.opened} + + + Closed + {issueStats.closed} + + + Velocity + {issueStats.velocity} + + + + + + {/* Engagement */} + + + + + {/* Heart Icon in box */} + + + + Engagement + + + + Stars + {repoStats ? repoStats.stargazers_count : 0} + + + Forks + {repoStats ? repoStats.forks_count : 0} + + + Activity Ratio + {/* Activity Ratio pill */} + + + Low - - - - - ))} + + + + + + {/* Repositories Table */} + + + + + + + REPOSITORY + ACTIVITY + PR OVERVIEW + PR VELOCITY + SPAM + CONTRIBUTORS + LAST 30 DAYS + + + + {loading ? ( + + + + + + ) : ( + paginatedContributors.map((contrib, idx) => { + const activity = getActivity(contrib); + const prOverview = getPROverview(contrib); + const prVelocity = getPRVelocity(); + const avatars = getContributorsAvatars(contrib); + return ( + + + + + + + + + {contrib.login} + @{REPO_OWNER} + + + + + {/* Activity pill */} + + {activity.icon} + {activity.label} + + + + + {prOverview.prs} PRs + + {/* Green base bar */} + + {/* Purple overlay bar */} + + + {prOverview.percent}% + + + + + + {prVelocity.velocity} + {prVelocity.percent}% + + + + - + + + + {avatars.map((url, i) => ( + + ))} + + + + + + + ); + }) + )} + +
+
+ + + Showing {paginatedContributors.length > 0 ? (page - 1) * perPage + 1 : 0} - {Math.min(page * perPage, contributors.length)} of {contributors.length} repos + + setPage(value)} + color="primary" + shape="rounded" + /> + +
-
+ ); };