diff --git a/package.json b/package.json index f2d89f5..13abd32 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.15.6", "@mui/material": "^5.15.6", + "@octokit/rest": "^22.0.0", "@primer/octicons-react": "^19.15.5", "@vitejs/plugin-react": "^4.3.3", "axios": "^1.7.7", diff --git a/src/hooks/useGitHubAuth.ts b/src/hooks/useGitHubAuth.ts index 5284347..ab0beaf 100644 --- a/src/hooks/useGitHubAuth.ts +++ b/src/hooks/useGitHubAuth.ts @@ -1,15 +1,18 @@ import { useState, useMemo } from 'react'; -import { Octokit } from '@octokit/core'; +import { Octokit } from '@octokit/rest'; export const useGitHubAuth = () => { const [username, setUsername] = useState(''); const [token, setToken] = useState(''); const [error, setError] = useState(''); + // FIX: The Octokit instance only depends on the token for authentication. const octokit = useMemo(() => { - if (!username || !token) return null; + // Only create an instance if a token exists. + if (!token) return null; + return new Octokit({ auth: token }); - }, [username, token]); + }, [token]); // Dependency array should only contain `token`. const getOctokit = () => octokit; @@ -22,4 +25,4 @@ export const useGitHubAuth = () => { setError, getOctokit, }; -}; +}; \ No newline at end of file diff --git a/src/hooks/useGitHubData.ts b/src/hooks/useGitHubData.ts index a0ebe10..2030f71 100644 --- a/src/hooks/useGitHubData.ts +++ b/src/hooks/useGitHubData.ts @@ -1,56 +1,69 @@ import { useState, useCallback } from 'react'; +import { Octokit } from '@octokit/rest'; +import { Endpoints } from "@octokit/types"; -export const useGitHubData = (getOctokit: () => any) => { - const [issues, setIssues] = useState([]); - const [prs, setPrs] = useState([]); +// FIX: Derive the item type directly from the library instead of defining it manually. +type IssueSearchResponse = Endpoints["GET /search/issues"]["response"]; +export type GitHubItem = IssueSearchResponse["data"]["items"][0]; + +export const useGitHubData = (getOctokit: () => Octokit | 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 q = `author:${username} is:${type}`; - const response = await octokit.request('GET /search/issues', { - q, - sort: 'created', - order: 'desc', - per_page, - page, - }); - - return { - items: response.data.items, - total: response.data.total_count, - }; - }; - const fetchData = useCallback( - async (username: string, page = 1, perPage = 10) => { - + async ( + username: string, + page: number, + perPage: number, + type: 'issue' | 'pr', + state: string + ) => { const octokit = getOctokit(); - if (!octokit || !username || rateLimited) return; setLoading(true); setError(''); + setRateLimited(false); try { - const [issueRes, prRes] = await Promise.all([ - fetchPaginated(octokit, username, 'issue', page, perPage), - fetchPaginated(octokit, username, 'pr', page, perPage), - ]); + let query = `author:${username} is:${type}`; + if (state !== 'all') { + query += state === 'merged' ? ` is:merged` : ` state:${state}`; + } + + const response = await octokit.request('GET /search/issues', { + q: query, + sort: 'created', + order: 'desc', + per_page: perPage, + page, + }); - setIssues(issueRes.items); - setPrs(prRes.items); - setTotalIssues(issueRes.total); - setTotalPrs(prRes.total); + if (type === 'issue') { + setIssues(response.data.items); + setTotalIssues(response.data.total_count); + } else { + setPrs(response.data.items); + setTotalPrs(response.data.total_count); + } } catch (err: any) { if (err.status === 403) { - setError('GitHub API rate limit exceeded. Please wait or use a token.'); - setRateLimited(true); // Prevent further fetches + setError('GitHub API rate limit exceeded. Please wait or use a valid token.'); + setRateLimited(true); } else { setError(err.message || 'Failed to fetch data'); + if (type === 'issue') { + setIssues([]); + setTotalIssues(0); + } else { + setPrs([]); + setTotalPrs(0); + } } } finally { setLoading(false); @@ -59,13 +72,5 @@ export const useGitHubData = (getOctokit: () => any) => { [getOctokit, rateLimited] ); - return { - issues, - prs, - totalIssues, - totalPrs, - loading, - error, - fetchData, - }; -}; + return { issues, prs, totalIssues, totalPrs, loading, error, fetchData }; +}; \ No newline at end of file diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx index 7d949ec..47d6f6f 100644 --- a/src/pages/Tracker/Tracker.tsx +++ b/src/pages/Tracker/Tracker.tsx @@ -1,11 +1,11 @@ -import React, { useState, useEffect } from "react" +import React, { useState, useEffect, useRef } from "react"; import { IssueOpenedIcon, IssueClosedIcon, GitPullRequestIcon, GitPullRequestClosedIcon, GitMergeIcon, -} from '@primer/octicons-react'; +} from "@primer/octicons-react"; import { Container, Box, @@ -31,24 +31,16 @@ import { } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import { useGitHubAuth } from "../../hooks/useGitHubAuth"; -import { useGitHubData } from "../../hooks/useGitHubData"; +import { useGitHubData, type GitHubItem } 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(); + // FIX 1: Add a ref to track the initial render. + const isInitialMount = useRef(true); + const { username, setUsername, @@ -78,17 +70,31 @@ const Home: React.FC = () => { const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); - // Fetch data when username, tab, or page changes +// FIX 2: Add a new useEffect to handle fetching data for page/filter changes. useEffect(() => { - if (username) { - fetchData(username, page + 1, ROWS_PER_PAGE); + // This check prevents the effect from running on the very first load. + if (isInitialMount.current) { + isInitialMount.current = false; + return; } - }, [tab, page]); + // Don't fetch if the user isn't set. + if (!username) return; + + const type = tab === 0 ? "issue" : "pr"; + const filter = tab === 0 ? issueFilter : prFilter; + fetchData(username, page + 1, ROWS_PER_PAGE, type, filter); + + // This effect now correctly runs only when these values change after the initial load. + }, [page, tab, issueFilter, prFilter]); + + // FIX: Pass the current filters when submitting the form for the first fetch. const handleSubmit = (e: React.FormEvent): void => { e.preventDefault(); - setPage(0); - fetchData(username, 1, ROWS_PER_PAGE); + setPage(0); // Reset page on new search + const type = tab === 0 ? "issue" : "pr"; + const filter = tab === 0 ? issueFilter : prFilter; + fetchData(username, 1, ROWS_PER_PAGE, type, filter); }; const handlePageChange = (_: unknown, newPage: number) => { @@ -98,15 +104,13 @@ const Home: React.FC = () => { const formatDate = (dateString: string): string => new Date(dateString).toLocaleDateString(); - const filterData = (data: GitHubItem[], filterType: string): GitHubItem[] => { + // FIX: Remove the state-based filtering, as the API will now handle it. + // The other filters (title, repo, date) can remain as they filter the current page's data. + const filterData = (data: GitHubItem[]): GitHubItem[] => { let filtered = [...data]; - if (["open", "closed", "merged"].includes(filterType)) { - filtered = filtered.filter((item) => - filterType === "merged" - ? !!item.pull_request?.merged_at - : item.state === filterType - ); - } + + // The 'state' filtering logic ('open', 'closed', 'merged') is removed from here. + if (searchTitle) { filtered = filtered.filter((item) => item.title.toLowerCase().includes(searchTitle.toLowerCase()) @@ -131,28 +135,21 @@ const Home: React.FC = () => { }; const getStatusIcon = (item: GitHubItem) => { - if (item.pull_request) { - - if (item.pull_request.merged_at) - return ; - - if (item.state === 'closed') - return ; - - return ; + if (item.pull_request.merged_at) + return ; + if (item.state === "closed") + return ; + return ; } - - if (item.state === 'closed') - return ; - - return ; + if (item.state === "closed") + return ; + return ; }; - - // Current data and filtered data according to tab and filters + // The 'filterType' argument is no longer needed here. const currentRawData = tab === 0 ? issues : prs; - const currentFilteredData = filterData(currentRawData, tab === 0 ? issueFilter : prFilter); + const currentFilteredData = filterData(currentRawData); const totalCount = tab === 0 ? totalIssues : totalPrs; return ( @@ -230,7 +227,7 @@ const Home: React.FC = () => { value={tab} onChange={(_, v) => { setTab(v); - setPage(0); + setPage(0); // Reset page when switching tabs }} sx={{ flex: 1 }} > @@ -241,11 +238,12 @@ const Home: React.FC = () => { State