diff --git a/package.json b/package.json index 5103cb16..d7ac80a8 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,16 @@ "preview": "vite preview" }, "dependencies": { + "octokit": "^4.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hot-toast": "^2.4.1", "react-icons": "^5.3.0", - "react-router-dom": "^6.28.0" + "react-router-dom": "^6.28.0", + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.15.6", + "@mui/material": "^5.15.6" }, "devDependencies": { "@eslint/js": "^9.13.0", diff --git a/src/App.jsx b/src/App.jsx index d0ed609b..b1b6a612 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Routes, Route, Navigate } from 'react-router-dom'; import Navbar from './components/Navbar'; import Footer from './components/Footer'; diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index b86b1380..5389175a 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -1,5 +1,4 @@ // src/components/Footer.jsx -import React from 'react'; import { FaGithub } from 'react-icons/fa'; // Import GitHub icon from react-icons function Footer() { diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 6fb581b3..a062672b 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,5 +1,4 @@ // src/components/Navbar.jsx -import React from 'react'; function Navbar() { return ( diff --git a/src/main.jsx b/src/main.jsx index 24c9de67..c5ef4972 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,4 +1,3 @@ -import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; // Import Tailwind CSS import App from './App'; diff --git a/src/pages/Home/Home.jsx b/src/pages/Home/Home.jsx index b5093659..897718d8 100644 --- a/src/pages/Home/Home.jsx +++ b/src/pages/Home/Home.jsx @@ -1,365 +1,208 @@ -'use client'; -import { useState, useEffect, useRef } from "react"; - -const Home = () => { - const [username, setUserName] = useState(""); // Track username input - const [token, setToken] = useState(""); // Track GitHub token input - const [userData, setUserData] = useState(null); // Store user profile data - const [issues, setIssues] = useState([]); // Store issues data - const [pullRequests, setPullRequests] = useState([]); // Store PR data - const [loading, setLoading] = useState(false); // Loading state - const [error, setError] = useState(null); // Error state - const [issueStatus, setIssueStatus] = useState("all"); // Issue status filter - const [prStatus, setPrStatus] = useState("all"); // PR status filter - const [labels, setLabels] = useState(""); // Labels filter - const [currentPageIssues, setCurrentPageIssues] = useState(1); // Current page for issues - const [currentPagePRs, setCurrentPagePRs] = useState(1); // Current page for PRs - const [issuesPerPage, setIssuesPerPage] = useState(5); // Number of issues per page - const [prPerPage, setPrPerPage] = useState(5); // Number of PRs per page - const inputRef = useRef(null); - - // Fetch user data, issues, and pull requests with pagination - const fetchData = async () => { - if (!username || !token) return; // If no username or token, do nothing - - setLoading(true); // Set loading to true - setError(null); // Reset any previous errors - setUserData(null); - setIssues([]); - setPullRequests([]); +import { Octokit } from '@octokit/core'; +import { createElement as h, useState, useCallback } from 'react'; +import { + Container, + Box, + TextField, + Button, + Typography, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TablePagination, + Link, + CircularProgress, + Alert, + Tabs, + Tab, + Select, + MenuItem, + FormControl, + InputLabel, +} from '@mui/material'; + +function GithubDashboard() { + const [username, setUsername] = useState(''); + const [token, setToken] = useState(''); + const [issues, setIssues] = useState([]); + const [prs, setPrs] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [tab, setTab] = useState(0); + const [page, setPage] = useState(0); + const [rowsPerPage] = useState(5); + const [issueFilter, setIssueFilter] = useState('all'); + const [prFilter, setPrFilter] = useState('all'); + + const fetchData = useCallback(async () => { + if (!username || !token) return; + + setLoading(true); + setError(''); try { - // Fetch user data from GitHub API - const userResponse = await fetch(`https://api.github.com/users/${username}`, { - headers: { - 'Authorization': `token ${token}` // Include token in the request - } + const octokit = new Octokit({ auth: token }); + + // Fetch issues + const issuesResponse = await octokit.request('GET /search/issues', { + q: `author:${username} is:issue`, + sort: 'created', + order: 'desc', + per_page: 100, }); - if (!userResponse.ok) throw new Error('User not found'); - const userData = await userResponse.json(); - setUserData(userData); // Set the fetched user data - - // Fetch repositories from GitHub API - const reposResponse = await fetch(`https://api.github.com/users/${username}/repos`, { - headers: { - 'Authorization': `token ${token}` // Include token in the request - } + // Fetch PRs + const prsResponse = await octokit.request('GET /search/issues', { + q: `author:${username} is:pr`, + sort: 'created', + order: 'desc', + per_page: 100, }); - if (!reposResponse.ok) throw new Error('Failed to fetch repositories'); - const reposData = await reposResponse.json(); - - // Fetch issues and pull requests for each repository - const allIssues = []; - const allPullRequests = []; - - for (const repo of reposData) { - // Fetch issues for each repository with pagination - const issuesResponse = await fetch(`https://api.github.com/repos/${username}/${repo.name}/issues?state=${issueStatus}&labels=${labels}&per_page=${issuesPerPage}&page=${currentPageIssues}`, { - headers: { - 'Authorization': `token ${token}` // Include token in the request - } - }); - const issuesData = await issuesResponse.json(); - - allIssues.push(...issuesData); - - // Fetch pull requests for each repository with pagination - const pullsResponse = await fetch(`https://api.github.com/repos/${username}/${repo.name}/pulls?state=${prStatus}&labels=${labels}&per_page=${prPerPage}&page=${currentPagePRs}`, { - headers: { - 'Authorization': `token ${token}` // Include token in the request - } - }); - const pullsData = await pullsResponse.json(); - - allPullRequests.push(...pullsData); - } - - setIssues(allIssues); // Set issues data - setPullRequests(allPullRequests); // Set pull requests data + setIssues(issuesResponse.data.items); + setPrs(prsResponse.data.items); } catch (err) { - setError(err.message); // Set error message if API call fails + setError(err.message); } finally { - setLoading(false); // Set loading to false after request + setLoading(false); } - }; - - // Handle input changes - const handleUser = (e) => setUserName(e.target.value); - const handleToken = (e) => setToken(e.target.value); - const handleIssueStatus = (e) => setIssueStatus(e.target.value); - const handlePrStatus = (e) => setPrStatus(e.target.value); - const handleLabelFilter = (e) => setLabels(e.target.value); + }, [username, token]); - const handlePageChangeIssues = (newPage) => { - setCurrentPageIssues(newPage); + const handleSubmit = (e) => { + e.preventDefault(); + fetchData(); }; - const handlePageChangePRs = (newPage) => { - setCurrentPagePRs(newPage); + const handleChangePage = (event, newPage) => { + setPage(newPage); }; - // Handle form submit - const handleSubmit = (e) => { - e.preventDefault(); // Prevent page reload - fetchData(); // Fetch data when form is submitted + const formatDate = (dateString) => { + return new Date(dateString).toLocaleDateString(); }; - return ( -
- {/* Main Content */} -
- - {/* Search Form */} -
-
- {/* GitHub Logo inside search bar */} -
- - - -
- - {/* Search Input GitHub username */} - -
- - {/* Add margin-top between username and token input */} -
{/* Same width for token input */} - -
- - {/* Submit Button */} - -
- - {/* Display user data */} - {loading &&

Loading...

} - - {error &&

{error}

} - - {userData && ( -
-

User Profile

- User Avatar -

{userData.name}

-

{userData.login}

-

{userData.bio}

- - Visit Profile - -
- )} - - {/* Display filters */} -
-
-
- - -
- -
- - -
- -
- - -
-
-
- - {/* Display Issues */} - {issues.length > 0 && ( -
-

Issues

- - - - - - - - - - - {issues.map(issue => ( - - - - - - - ))} - -
TitleStatusLabelsLink
{issue.title}{issue.state} - {issue.labels.length > 0 ? ( - issue.labels.map(label => ( - {label.name} - )) - ) : ( - No labels - )} - - - View Issue - -
- - {/* Pagination Controls for Issues */} -
- - Page {currentPageIssues} - -
-
- )} - - {/* Display Pull Requests */} - {pullRequests.length > 0 && ( -
-

Pull Requests

- - - - - - - - - - - {pullRequests.map(pr => ( - - - - - - - ))} - -
TitleStatusLabelsLink
{pr.title}{pr.state} - {pr.labels.length > 0 ? ( - pr.labels.map(label => ( - {label.name} - )) - ) : ( - No labels - )} - - - View PR - -
- - {/* Pagination Controls for PRs */} -
- - Page {currentPagePRs} - -
-
- )} -
-
- ); -}; + const filterData = (data, filterType) => { + switch (filterType) { + case 'open': + return data.filter(item => item.state === 'open'); + case 'closed': + return data.filter(item => item.state === 'closed' && !item.pull_request?.merged_at); + case 'merged': + return data.filter(item => item.pull_request?.merged_at); + default: + return data; + } + }; -export default Home; + const currentData = tab === 0 + ? filterData(issues, issueFilter) + : filterData(prs, prFilter); + const displayData = currentData.slice(page * rowsPerPage, (page + 1) * rowsPerPage); + + return h(Container, { maxWidth: 'lg', sx: { py: 4 } }, [ + h(Paper, { elevation: 3, sx: { p: 3, mb: 4 } }, [ + h(Typography, { variant: 'h4', component: 'h1', gutterBottom: true }, + 'GitHub Dashboard' + ), + h('form', { onSubmit: handleSubmit }, [ + h(Box, { sx: { display: 'flex', gap: 2, mb: 3 } }, [ + h(TextField, { + label: 'GitHub Username', + value: username, + onChange: (e) => setUsername(e.target.value), + required: true, + sx: { flex: 1 }, + }), + h(TextField, { + label: 'Personal Access Token', + value: token, + onChange: (e) => setToken(e.target.value), + type: 'password', + required: true, + sx: { flex: 1 }, + }), + h(Button, { + type: 'submit', + variant: 'contained', + sx: { minWidth: '120px' }, + }, 'Fetch Data'), + ]), + ]), + ]), + + error && h(Alert, { severity: 'error', sx: { mb: 3 } }, error), + + loading ? + h(Box, { display: 'flex', justifyContent: 'center', my: 4 }, + h(CircularProgress) + ) : + h(Box, null, [ + h(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2, mb: 3 } }, [ + h(Tabs, { + value: tab, + onChange: (e, newValue) => setTab(newValue), + sx: { flex: 1 }, + }, [ + h(Tab, { label: `Issues (${filterData(issues, issueFilter).length})` }), + h(Tab, { label: `Pull Requests (${filterData(prs, prFilter).length})` }), + ]), + h(FormControl, { sx: { minWidth: 120 } }, [ + h(InputLabel, null, 'Filter'), + h(Select, { + value: tab === 0 ? issueFilter : prFilter, + onChange: (e) => tab === 0 ? setIssueFilter(e.target.value) : setPrFilter(e.target.value), + label: 'Filter', + }, [ + h(MenuItem, { value: 'all' }, 'All'), + h(MenuItem, { value: 'open' }, 'Open'), + h(MenuItem, { value: 'closed' }, 'Closed'), + ...(tab === 1 ? [h(MenuItem, { value: 'merged' }, 'Merged')] : []), + ]), + ]), + ]), + + h(TableContainer, { component: Paper }, [ + h(Table, null, [ + h(TableHead, null, + h(TableRow, null, [ + h(TableCell, null, 'Title'), + h(TableCell, null, 'Repository'), + h(TableCell, null, 'State'), + h(TableCell, null, 'Created'), + ]) + ), + h(TableBody, null, + displayData.map((item) => + h(TableRow, { key: item.id }, [ + h(TableCell, null, + h(Link, { + href: item.html_url, + target: '_blank', + rel: 'noopener noreferrer', + }, item.title) + ), + h(TableCell, null, item.repository_url.split('/').slice(-1)[0]), + h(TableCell, null, item.pull_request?.merged_at ? 'merged' : item.state), + h(TableCell, null, formatDate(item.created_at)), + ]) + ) + ), + ]), + h(TablePagination, { + component: 'div', + count: currentData.length, + page, + onPageChange: handleChangePage, + rowsPerPage, + rowsPerPageOptions: [5], + }), + ]), + ]), + ]); +} + +export default GithubDashboard;