Skip to content

Commit 3dbd9ba

Browse files
committed
feat: add issues tracking page with filters and sorting
1 parent 7a15543 commit 3dbd9ba

3 files changed

Lines changed: 252 additions & 0 deletions

File tree

src/Routes/Router.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Signup from "../pages/Signup/Signup.tsx";
77
import Login from "../pages/Login/Login.tsx";
88
import ContributorProfile from "../pages/ContributorProfile/ContributorProfile.tsx";
99
import Home from "../pages/Home/Home.tsx";
10+
import Issues from "../pages/Issues/Issues.tsx";
1011

1112
const Router = () => {
1213
return (
@@ -19,6 +20,7 @@ const Router = () => {
1920
<Route path="/contact" element={<Contact />} />
2021
<Route path="/contributors" element={<Contributors />} />
2122
<Route path="/contributor/:username" element={<ContributorProfile />} />
23+
<Route path="/issues" element={<Issues />} />
2224
</Routes>
2325
);
2426
};

src/components/Navbar.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ const Navbar: React.FC = () => {
4040
>
4141
Tracker
4242
</Link>
43+
<Link
44+
to="/issues"
45+
className="text-lg font-medium hover:text-gray-300 transition-all px-2 py-1 border border-transparent hover:border-gray-400 rounded"
46+
>
47+
Issues
48+
</Link>
4349
<Link
4450
to="/contributors"
4551
className="text-lg font-medium hover:text-gray-300 transition-all px-2 py-1 border border-transparent hover:border-gray-400 rounded"
@@ -117,6 +123,13 @@ const Navbar: React.FC = () => {
117123
>
118124
Contributors
119125
</Link>
126+
<Link
127+
to="/issues"
128+
className="block text-lg font-medium hover:text-gray-300 transition-all px-2 py-1 border border-transparent hover:border-gray-400 rounded"
129+
onClick={() => setIsOpen(false)}
130+
>
131+
Issues
132+
</Link>
120133
<Link
121134
to="/login"
122135
className="block text-lg font-medium hover:text-gray-300 transition-all px-2 py-1 border border-transparent hover:border-gray-400 rounded"

src/pages/Issues/Issues.tsx

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import React, { useState, useEffect, useCallback } from "react";
2+
import {
3+
Container,
4+
Box,
5+
Paper,
6+
Table,
7+
TableBody,
8+
TableCell,
9+
TableContainer,
10+
TableHead,
11+
TableRow,
12+
TablePagination,
13+
Link,
14+
CircularProgress,
15+
Alert,
16+
FormControl,
17+
InputLabel,
18+
Select,
19+
MenuItem,
20+
Typography,
21+
SelectChangeEvent,
22+
} from "@mui/material";
23+
import { useTheme } from "@mui/material/styles";
24+
import { IssueOpenedIcon, IssueClosedIcon } from "@primer/octicons-react";
25+
26+
const ROWS_PER_PAGE = 10;
27+
28+
interface IssueItem {
29+
id: number;
30+
title: string;
31+
state: string;
32+
created_at: string;
33+
repository_url: string;
34+
html_url: string;
35+
}
36+
37+
const Issues: React.FC = () => {
38+
const theme = useTheme();
39+
40+
const [issues, setIssues] = useState<IssueItem[]>([]);
41+
const [totalIssues, setTotalIssues] = useState(0);
42+
const [loading, setLoading] = useState(false);
43+
const [error, setError] = useState("");
44+
const [page, setPage] = useState(0);
45+
46+
const [language, setLanguage] = useState("");
47+
const [tag, setTag] = useState("");
48+
const [sortOrder, setSortOrder] = useState("desc");
49+
50+
const fetchIssues = useCallback(async (currentPage: number, currentLanguage: string, currentTag: string, currentOrder: string) => {
51+
setLoading(true);
52+
setError("");
53+
54+
try {
55+
let q = "is:issue is:open";
56+
if (currentLanguage) {
57+
q += ` language:${currentLanguage}`;
58+
}
59+
if (currentTag) {
60+
q += ` label:"${currentTag}"`;
61+
}
62+
63+
const response = await fetch(
64+
`https://api.github.com/search/issues?q=${encodeURIComponent(q)}&sort=created&order=${currentOrder}&per_page=${ROWS_PER_PAGE}&page=${currentPage + 1}`
65+
);
66+
67+
if (!response.ok) {
68+
if (response.status === 403) {
69+
throw new Error("GitHub API rate limit exceeded.");
70+
}
71+
throw new Error("Failed to fetch data");
72+
}
73+
74+
const data = await response.json();
75+
setIssues(data.items);
76+
setTotalIssues(data.total_count > 1000 ? 1000 : data.total_count); // GitHub limits search results to 1000
77+
} catch (err: unknown) {
78+
if (err instanceof Error) {
79+
setError(err.message || "Failed to fetch issues");
80+
} else {
81+
setError("Failed to fetch issues");
82+
}
83+
} finally {
84+
setLoading(false);
85+
}
86+
}, []);
87+
88+
useEffect(() => {
89+
fetchIssues(page, language, tag, sortOrder);
90+
}, [page, language, tag, sortOrder, fetchIssues]);
91+
92+
const handlePageChange = (_: unknown, newPage: number) => {
93+
setPage(newPage);
94+
};
95+
96+
const formatDate = (dateString: string): string =>
97+
new Date(dateString).toLocaleDateString();
98+
99+
return (
100+
<Container maxWidth="lg" sx={{ mt: 4, mb: 4, minHeight: "80vh", color: theme.palette.text.primary }}>
101+
<Typography variant="h4" gutterBottom>
102+
Explore GitHub Issues
103+
</Typography>
104+
105+
<Paper elevation={1} sx={{ p: 2, mb: 4, backgroundColor: theme.palette.background.paper }}>
106+
<Box sx={{ display: "flex", gap: 2, flexWrap: "wrap", alignItems: "center" }}>
107+
<FormControl sx={{ minWidth: 150, flex: 1 }}>
108+
<InputLabel>Language</InputLabel>
109+
<Select
110+
value={language}
111+
label="Language"
112+
onChange={(e: SelectChangeEvent) => {
113+
setLanguage(e.target.value as string);
114+
setPage(0);
115+
}}
116+
>
117+
<MenuItem value="">All Languages</MenuItem>
118+
<MenuItem value="javascript">JavaScript</MenuItem>
119+
<MenuItem value="typescript">TypeScript</MenuItem>
120+
<MenuItem value="python">Python</MenuItem>
121+
<MenuItem value="java">Java</MenuItem>
122+
<MenuItem value="c++">C++</MenuItem>
123+
<MenuItem value="go">Go</MenuItem>
124+
<MenuItem value="ruby">Ruby</MenuItem>
125+
<MenuItem value="php">PHP</MenuItem>
126+
</Select>
127+
</FormControl>
128+
129+
<FormControl sx={{ minWidth: 150, flex: 1 }}>
130+
<InputLabel>Tags / Labels</InputLabel>
131+
<Select
132+
value={tag}
133+
label="Tags / Labels"
134+
onChange={(e: SelectChangeEvent) => {
135+
setTag(e.target.value as string);
136+
setPage(0);
137+
}}
138+
>
139+
<MenuItem value="">All Tags</MenuItem>
140+
<MenuItem value="good first issue">Good First Issue</MenuItem>
141+
<MenuItem value="bug">Bug</MenuItem>
142+
<MenuItem value="enhancement">Enhancement</MenuItem>
143+
<MenuItem value="help wanted">Help Wanted</MenuItem>
144+
<MenuItem value="documentation">Documentation</MenuItem>
145+
</Select>
146+
</FormControl>
147+
148+
<FormControl sx={{ minWidth: 150, flex: 1 }}>
149+
<InputLabel>Sort by Time</InputLabel>
150+
<Select
151+
value={sortOrder}
152+
label="Sort by Time"
153+
onChange={(e: SelectChangeEvent) => {
154+
setSortOrder(e.target.value as string);
155+
setPage(0);
156+
}}
157+
>
158+
<MenuItem value="desc">Newest</MenuItem>
159+
<MenuItem value="asc">Oldest</MenuItem>
160+
</Select>
161+
</FormControl>
162+
</Box>
163+
</Paper>
164+
165+
{error && (
166+
<Alert severity="error" sx={{ mb: 3 }}>
167+
{error}
168+
</Alert>
169+
)}
170+
171+
{loading ? (
172+
<Box display="flex" justifyContent="center" my={4}>
173+
<CircularProgress />
174+
</Box>
175+
) : (
176+
<Box sx={{ overflowY: "auto" }}>
177+
<TableContainer component={Paper}>
178+
<Table size="small">
179+
<TableHead>
180+
<TableRow>
181+
<TableCell>Title</TableCell>
182+
<TableCell align="center">Repository</TableCell>
183+
<TableCell align="center">State</TableCell>
184+
<TableCell>Created</TableCell>
185+
</TableRow>
186+
</TableHead>
187+
<TableBody>
188+
{issues.map((item) => (
189+
<TableRow key={item.id}>
190+
<TableCell sx={{ display: "flex", alignItems: "center", gap: 1 }}>
191+
{item.state === "closed" ? (
192+
<IssueClosedIcon size={16} className="icon-issue-closed" />
193+
) : (
194+
<IssueOpenedIcon size={16} className="icon-issue-open" />
195+
)}
196+
<Link
197+
href={item.html_url}
198+
target="_blank"
199+
rel="noopener noreferrer"
200+
underline="hover"
201+
sx={{ color: theme.palette.primary.main }}
202+
>
203+
{item.title}
204+
</Link>
205+
</TableCell>
206+
<TableCell align="center">
207+
{item.repository_url.split("/").slice(-2).join("/")}
208+
</TableCell>
209+
<TableCell align="center">{item.state}</TableCell>
210+
<TableCell>{formatDate(item.created_at)}</TableCell>
211+
</TableRow>
212+
))}
213+
{issues.length === 0 && !loading && !error && (
214+
<TableRow>
215+
<TableCell colSpan={4} align="center">
216+
No issues found.
217+
</TableCell>
218+
</TableRow>
219+
)}
220+
</TableBody>
221+
</Table>
222+
<TablePagination
223+
component="div"
224+
count={totalIssues}
225+
page={page}
226+
onPageChange={handlePageChange}
227+
rowsPerPage={ROWS_PER_PAGE}
228+
rowsPerPageOptions={[ROWS_PER_PAGE]}
229+
/>
230+
</TableContainer>
231+
</Box>
232+
)}
233+
</Container>
234+
);
235+
};
236+
237+
export default Issues;

0 commit comments

Comments
 (0)