Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Signup from "../pages/Signup/Signup.tsx";
import Login from "../pages/Login/Login.tsx";
import ContributorProfile from "../pages/ContributorProfile/ContributorProfile.tsx";
import Home from "../pages/Home/Home.tsx";
import Issues from "../pages/Issues/Issues.tsx";

const Router = () => {
return (
Expand All @@ -19,6 +20,7 @@ const Router = () => {
<Route path="/contact" element={<Contact />} />
<Route path="/contributors" element={<Contributors />} />
<Route path="/contributor/:username" element={<ContributorProfile />} />
<Route path="/issues" element={<Issues />} />
</Routes>
);
};
Expand Down
13 changes: 13 additions & 0 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ const Navbar: React.FC = () => {
>
Tracker
</Link>
<Link
to="/issues"
className="text-lg font-medium hover:text-gray-300 transition-all px-2 py-1 border border-transparent hover:border-gray-400 rounded"
>
Issues
</Link>
<Link
to="/contributors"
className="text-lg font-medium hover:text-gray-300 transition-all px-2 py-1 border border-transparent hover:border-gray-400 rounded"
Expand Down Expand Up @@ -117,6 +123,13 @@ const Navbar: React.FC = () => {
>
Contributors
</Link>
<Link
to="/issues"
className="block text-lg font-medium hover:text-gray-300 transition-all px-2 py-1 border border-transparent hover:border-gray-400 rounded"
onClick={() => setIsOpen(false)}
>
Issues
</Link>
<Link
to="/login"
className="block text-lg font-medium hover:text-gray-300 transition-all px-2 py-1 border border-transparent hover:border-gray-400 rounded"
Expand Down
237 changes: 237 additions & 0 deletions src/pages/Issues/Issues.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import React, { useState, useEffect, useCallback } from "react";
import {
Container,
Box,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TablePagination,
Link,
CircularProgress,
Alert,
FormControl,
InputLabel,
Select,
MenuItem,
Typography,
SelectChangeEvent,
} from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { IssueOpenedIcon, IssueClosedIcon } from "@primer/octicons-react";

const ROWS_PER_PAGE = 10;

interface IssueItem {
id: number;
title: string;
state: string;
created_at: string;
repository_url: string;
html_url: string;
}

const Issues: React.FC = () => {
const theme = useTheme();

const [issues, setIssues] = useState<IssueItem[]>([]);
const [totalIssues, setTotalIssues] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [page, setPage] = useState(0);

const [language, setLanguage] = useState("");
const [tag, setTag] = useState("");
const [sortOrder, setSortOrder] = useState("desc");

const fetchIssues = useCallback(async (currentPage: number, currentLanguage: string, currentTag: string, currentOrder: string) => {
setLoading(true);
setError("");

try {
let q = "is:issue is:open";
if (currentLanguage) {
q += ` language:${currentLanguage}`;
}
if (currentTag) {
q += ` label:"${currentTag}"`;
}

const response = await fetch(
`https://api.github.com/search/issues?q=${encodeURIComponent(q)}&sort=created&order=${currentOrder}&per_page=${ROWS_PER_PAGE}&page=${currentPage + 1}`
);

if (!response.ok) {
if (response.status === 403) {
throw new Error("GitHub API rate limit exceeded.");
}
throw new Error("Failed to fetch data");
}

const data = await response.json();
setIssues(data.items);
setTotalIssues(data.total_count > 1000 ? 1000 : data.total_count); // GitHub limits search results to 1000
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message || "Failed to fetch issues");
} else {
setError("Failed to fetch issues");
}
} finally {
setLoading(false);
}
}, []);

useEffect(() => {
fetchIssues(page, language, tag, sortOrder);
}, [page, language, tag, sortOrder, fetchIssues]);
Comment on lines +50 to +90
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether request cancellation is implemented for Issues fetching.
rg -n --type=tsx -e 'AbortController' -e 'controller\.abort' -e 'signal' -e 'fetchIssues\(' src/pages/Issues/Issues.tsx

Repository: GitMetricsLab/github_tracker

Length of output: 100


🏁 Script executed:

fd -i "issues.tsx" -o -i "issues.ts"

Repository: GitMetricsLab/github_tracker

Length of output: 189


🏁 Script executed:

fd -type f "Issues" src/pages/

Repository: GitMetricsLab/github_tracker

Length of output: 243


🏁 Script executed:

find src/pages/ -name "*Issues*" -type f

Repository: GitMetricsLab/github_tracker

Length of output: 100


🏁 Script executed:

cat src/pages/Issues/Issues.tsx

Repository: GitMetricsLab/github_tracker

Length of output: 7909


Prevent stale results from out-of-order fetch responses.

Multiple in-flight requests can race here; an older response can arrive last and overwrite the latest filter/page state. This happens because rapid filter/page/sort changes trigger new fetchIssues calls via the dependency array [page, language, tag, sortOrder, fetchIssues] without canceling the previous request.

💡 Suggested fix (abort previous request + ignore abort errors)
-  const fetchIssues = useCallback(async (currentPage: number, currentLanguage: string, currentTag: string, currentOrder: string) => {
+  const fetchIssues = useCallback(
+    async (
+      currentPage: number,
+      currentLanguage: string,
+      currentTag: string,
+      currentOrder: string,
+      signal: AbortSignal
+    ) => {
     setLoading(true);
     setError("");
@@
-      const response = await fetch(
-        `https://api.github.com/search/issues?q=${encodeURIComponent(q)}&sort=created&order=${currentOrder}&per_page=${ROWS_PER_PAGE}&page=${currentPage + 1}`
-      );
+      const response = await fetch(
+        `https://api.github.com/search/issues?q=${encodeURIComponent(q)}&sort=created&order=${currentOrder}&per_page=${ROWS_PER_PAGE}&page=${currentPage + 1}`,
+        { signal }
+      );
@@
       const data = await response.json();
+      if (signal.aborted) return;
       setIssues(data.items);
       setTotalIssues(data.total_count > 1000 ? 1000 : data.total_count); // GitHub limits search results to 1000
     } catch (err: unknown) {
+      if (err instanceof DOMException && err.name === "AbortError") return;
       if (err instanceof Error) {
         setError(err.message || "Failed to fetch issues");
       } else {
         setError("Failed to fetch issues");
       }
     } finally {
       setLoading(false);
     }
-  }, []);
+  }, []);
@@
-  useEffect(() => {
-    fetchIssues(page, language, tag, sortOrder);
-  }, [page, language, tag, sortOrder, fetchIssues]);
+  useEffect(() => {
+    const controller = new AbortController();
+    fetchIssues(page, language, tag, sortOrder, controller.signal);
+    return () => controller.abort();
+  }, [page, language, tag, sortOrder, fetchIssues]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fetchIssues = useCallback(async (currentPage: number, currentLanguage: string, currentTag: string, currentOrder: string) => {
setLoading(true);
setError("");
try {
let q = "is:issue is:open";
if (currentLanguage) {
q += ` language:${currentLanguage}`;
}
if (currentTag) {
q += ` label:"${currentTag}"`;
}
const response = await fetch(
`https://api.github.com/search/issues?q=${encodeURIComponent(q)}&sort=created&order=${currentOrder}&per_page=${ROWS_PER_PAGE}&page=${currentPage + 1}`
);
if (!response.ok) {
if (response.status === 403) {
throw new Error("GitHub API rate limit exceeded.");
}
throw new Error("Failed to fetch data");
}
const data = await response.json();
setIssues(data.items);
setTotalIssues(data.total_count > 1000 ? 1000 : data.total_count); // GitHub limits search results to 1000
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message || "Failed to fetch issues");
} else {
setError("Failed to fetch issues");
}
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchIssues(page, language, tag, sortOrder);
}, [page, language, tag, sortOrder, fetchIssues]);
const fetchIssues = useCallback(
async (
currentPage: number,
currentLanguage: string,
currentTag: string,
currentOrder: string,
signal: AbortSignal
) => {
setLoading(true);
setError("");
try {
let q = "is:issue is:open";
if (currentLanguage) {
q += ` language:${currentLanguage}`;
}
if (currentTag) {
q += ` label:"${currentTag}"`;
}
const response = await fetch(
`https://api.github.com/search/issues?q=${encodeURIComponent(q)}&sort=created&order=${currentOrder}&per_page=${ROWS_PER_PAGE}&page=${currentPage + 1}`,
{ signal }
);
if (!response.ok) {
if (response.status === 403) {
throw new Error("GitHub API rate limit exceeded.");
}
throw new Error("Failed to fetch data");
}
const data = await response.json();
if (signal.aborted) return;
setIssues(data.items);
setTotalIssues(data.total_count > 1000 ? 1000 : data.total_count); // GitHub limits search results to 1000
} catch (err: unknown) {
if (err instanceof DOMException && err.name === "AbortError") return;
if (err instanceof Error) {
setError(err.message || "Failed to fetch issues");
} else {
setError("Failed to fetch issues");
}
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
const controller = new AbortController();
fetchIssues(page, language, tag, sortOrder, controller.signal);
return () => controller.abort();
}, [page, language, tag, sortOrder, fetchIssues]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/Issues/Issues.tsx` around lines 50 - 90, fetchIssues can race when
multiple requests are in-flight; create an AbortController ref (e.g.,
abortControllerRef) and before starting a new fetch in fetchIssues abort any
existing controller, then create a new controller and pass its signal to fetch;
in the catch block ignore abort errors (check err.name === 'AbortError' or err
instanceof DOMException) and only setError for real failures; also abort any
pending request in the useEffect cleanup (return () =>
abortControllerRef.current?.abort()) so stale responses never overwrite newer
state.


const handlePageChange = (_: unknown, newPage: number) => {
setPage(newPage);
};

const formatDate = (dateString: string): string =>
new Date(dateString).toLocaleDateString();

return (
<Container maxWidth="lg" sx={{ mt: 4, mb: 4, minHeight: "80vh", color: theme.palette.text.primary }}>
<Typography variant="h4" gutterBottom>
Explore GitHub Issues
</Typography>

<Paper elevation={1} sx={{ p: 2, mb: 4, backgroundColor: theme.palette.background.paper }}>
<Box sx={{ display: "flex", gap: 2, flexWrap: "wrap", alignItems: "center" }}>
<FormControl sx={{ minWidth: 150, flex: 1 }}>
<InputLabel>Language</InputLabel>
<Select
value={language}
label="Language"
onChange={(e: SelectChangeEvent) => {
setLanguage(e.target.value as string);
setPage(0);
}}
>
<MenuItem value="">All Languages</MenuItem>
<MenuItem value="javascript">JavaScript</MenuItem>
<MenuItem value="typescript">TypeScript</MenuItem>
<MenuItem value="python">Python</MenuItem>
<MenuItem value="java">Java</MenuItem>
<MenuItem value="c++">C++</MenuItem>
<MenuItem value="go">Go</MenuItem>
<MenuItem value="ruby">Ruby</MenuItem>
<MenuItem value="php">PHP</MenuItem>
</Select>
</FormControl>

<FormControl sx={{ minWidth: 150, flex: 1 }}>
<InputLabel>Tags / Labels</InputLabel>
<Select
value={tag}
label="Tags / Labels"
onChange={(e: SelectChangeEvent) => {
setTag(e.target.value as string);
setPage(0);
}}
>
<MenuItem value="">All Tags</MenuItem>
<MenuItem value="good first issue">Good First Issue</MenuItem>
<MenuItem value="bug">Bug</MenuItem>
<MenuItem value="enhancement">Enhancement</MenuItem>
<MenuItem value="help wanted">Help Wanted</MenuItem>
<MenuItem value="documentation">Documentation</MenuItem>
</Select>
</FormControl>

<FormControl sx={{ minWidth: 150, flex: 1 }}>
<InputLabel>Sort by Time</InputLabel>
<Select
value={sortOrder}
label="Sort by Time"
onChange={(e: SelectChangeEvent) => {
setSortOrder(e.target.value as string);
setPage(0);
}}
>
<MenuItem value="desc">Newest</MenuItem>
<MenuItem value="asc">Oldest</MenuItem>
</Select>
</FormControl>
</Box>
</Paper>

{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}

{loading ? (
<Box display="flex" justifyContent="center" my={4}>
<CircularProgress />
</Box>
) : (
<Box sx={{ overflowY: "auto" }}>
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Title</TableCell>
<TableCell align="center">Repository</TableCell>
<TableCell align="center">State</TableCell>
<TableCell>Created</TableCell>
</TableRow>
</TableHead>
<TableBody>
{issues.map((item) => (
<TableRow key={item.id}>
<TableCell sx={{ display: "flex", alignItems: "center", gap: 1 }}>
{item.state === "closed" ? (
<IssueClosedIcon size={16} className="icon-issue-closed" />
) : (
<IssueOpenedIcon size={16} className="icon-issue-open" />
)}
<Link
href={item.html_url}
target="_blank"
rel="noopener noreferrer"
underline="hover"
sx={{ color: theme.palette.primary.main }}
>
{item.title}
</Link>
</TableCell>
<TableCell align="center">
{item.repository_url.split("/").slice(-2).join("/")}
</TableCell>
<TableCell align="center">{item.state}</TableCell>
<TableCell>{formatDate(item.created_at)}</TableCell>
</TableRow>
))}
{issues.length === 0 && !loading && !error && (
<TableRow>
<TableCell colSpan={4} align="center">
No issues found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
component="div"
count={totalIssues}
page={page}
onPageChange={handlePageChange}
rowsPerPage={ROWS_PER_PAGE}
rowsPerPageOptions={[ROWS_PER_PAGE]}
/>
</TableContainer>
</Box>
)}
</Container>
);
};

export default Issues;
Loading