Skip to content

Commit 0a30922

Browse files
committed
Add question list page
1 parent 9266593 commit 0a30922

File tree

7 files changed

+463
-9
lines changed

7 files changed

+463
-9
lines changed

frontend/src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import NewQuestion from "./pages/NewQuestion";
44
import QuestionDetail from "./pages/QuestionDetail";
55
import QuestionEdit from "./pages/QuestionEdit";
66
import PageNotFound from "./pages/PageNotFound";
7+
import QuestionList from "./pages/QuestionList";
78

89
function App() {
910
return (
1011
<BrowserRouter>
1112
<Routes>
1213
<Route path="/" element={<Layout />}>
1314
<Route path="questions">
14-
<Route index element={<>question page list</>} />
15+
<Route index element={<QuestionList />} />
1516
<Route path="new" element={<NewQuestion />} />
1617
<Route path=":questionId" element={<QuestionDetail />} />
1718
<Route path=":questionId/edit" element={<QuestionEdit />} />

frontend/src/components/Layout/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const Layout: React.FC = () => {
99
display: "flex",
1010
flexDirection: "column",
1111
minHeight: "100vh",
12+
minWidth: "755px",
1213
// minInlineSize: "100vw",
1314
}}
1415
>

frontend/src/main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { CssBaseline } from "@mui/material";
88
createRoot(document.getElementById("root")!).render(
99
<StrictMode>
1010
<ThemeProvider theme={theme}>
11-
<CssBaseline />
11+
<CssBaseline enableColorScheme />
1212
<App />
1313
</ThemeProvider>
1414
</StrictMode>
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
import {
2+
Autocomplete,
3+
Box,
4+
Button,
5+
Chip,
6+
Grid2,
7+
IconButton,
8+
InputAdornment,
9+
ListItemIcon,
10+
Menu,
11+
MenuItem,
12+
Stack,
13+
Table,
14+
TableBody,
15+
TableCell,
16+
TableContainer,
17+
TableHead,
18+
TablePagination,
19+
TableRow,
20+
TextField,
21+
Typography,
22+
useTheme,
23+
} from "@mui/material";
24+
import { useEffect, useReducer, useState } from "react";
25+
import AppMargin from "../../components/AppMargin";
26+
import { useNavigate } from "react-router-dom";
27+
import reducer, { deleteQuestionById, getQuestionList, initialState } from "../../reducers/questionReducer";
28+
import { categoryList, complexityList } from "../../utils/constants";
29+
import useDebounce from "../../utils/debounce";
30+
import { blue, grey } from "@mui/material/colors";
31+
import { Add, Delete, Edit, MoreVert, Search } from "@mui/icons-material";
32+
33+
// TODO: get dynamic category list from DB
34+
35+
const tableHeaders = ["Title", "Complexity", "Categories"];
36+
const searchCharacterLimit = 255;
37+
const categorySelectionLimit = 10;
38+
const rowsPerPage = 10;
39+
const isAdmin = false; // TODO: check using auth context
40+
41+
const QuestionList: React.FC = () => {
42+
const [page, setPage] = useState<number>(0);
43+
const [searchInput, setSearchInput] = useState<string>("");
44+
const [searchFilter, setSearchFilter] = useDebounce<string>("", 1000);
45+
const [complexityFilter, setComplexityFilter] = useDebounce<string[]>([], 1000);
46+
const [categoryFilter, setCategoryFilter] = useDebounce<string[]>([], 1000);
47+
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
48+
const [state, dispatch] = useReducer(reducer, initialState);
49+
const navigate = useNavigate();
50+
const theme = useTheme();
51+
52+
// For handling edit / delete question for the admin user
53+
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
54+
const menuOpen = Boolean(anchorEl);
55+
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
56+
setAnchorEl(event.currentTarget);
57+
};
58+
const handleMenuClose = () => {
59+
setAnchorEl(null);
60+
};
61+
const handleQuestionDelete = (questionId: string) => {
62+
// TODO
63+
// handleMenuClose();
64+
deleteQuestionById(questionId, dispatch);
65+
};
66+
67+
useEffect(() => {
68+
getQuestionList(
69+
page,
70+
rowsPerPage,
71+
searchFilter,
72+
complexityFilter,
73+
categoryFilter,
74+
dispatch
75+
)
76+
}, [page, searchFilter, complexityFilter, categoryFilter]);
77+
78+
return (
79+
<AppMargin>
80+
<Box sx={{ marginTop: theme.spacing(4), marginBottom: theme.spacing(4) }}>
81+
<Stack direction="row" sx={{ justifyContent: "space-between", alignItems: "center" }}>
82+
<Typography component="h1" variant="h3">
83+
Questions
84+
</Typography>
85+
{isAdmin &&
86+
<Button
87+
variant="contained"
88+
disableElevation
89+
startIcon={<Add />}
90+
sx={{ height: 40 }}
91+
onClick={() => navigate("new")}
92+
>Create</Button>
93+
}
94+
</Stack>
95+
<Grid2
96+
container
97+
rowSpacing={1}
98+
columnSpacing={2}
99+
sx={{
100+
marginTop: theme.spacing(2),
101+
"& fieldset": { borderRadius: theme.spacing(2.5) },
102+
"& .MuiTextField-root": { width: "100%" },
103+
}}
104+
>
105+
<Grid2 size={12}>
106+
<TextField
107+
slotProps={{
108+
input: {
109+
endAdornment:
110+
<InputAdornment position="end">
111+
<IconButton type="button">
112+
<Search />
113+
</IconButton>
114+
</InputAdornment>,
115+
},
116+
htmlInput: {
117+
maxLength: searchCharacterLimit,
118+
},
119+
formHelperText: {
120+
sx: { textAlign: "right" }
121+
}
122+
}}
123+
label="Title"
124+
onChange={(input) => {
125+
setSearchInput(input.target.value);
126+
setSearchFilter(input.target.value);
127+
}}
128+
helperText={searchInput.length + ` / ${searchCharacterLimit} characters` }
129+
disabled={state.questions.length === 0}
130+
/>
131+
</Grid2>
132+
<Grid2 size={4}>
133+
<Autocomplete
134+
multiple
135+
disableCloseOnSelect
136+
options={complexityList}
137+
onChange={(_, selectedOptions) => {
138+
setComplexityFilter(selectedOptions);
139+
}}
140+
renderInput={(params) => <TextField {...params} label="Complexity" />}
141+
disabled={state.questions.length === 0}
142+
/>
143+
</Grid2>
144+
<Grid2 size={8}>
145+
<Autocomplete
146+
multiple
147+
disableCloseOnSelect
148+
options={categoryList}
149+
getOptionDisabled={(option) =>
150+
selectedCategories.length > categorySelectionLimit && !selectedCategories.includes(option as string)
151+
}
152+
onChange={(_, selectedOptions) => {
153+
setSelectedCategories(selectedOptions);
154+
setCategoryFilter(selectedOptions);
155+
}}
156+
renderInput={(params) =>
157+
<TextField
158+
{...params}
159+
label="Category"
160+
helperText={selectedCategories.length + ` / ${categorySelectionLimit} selections`}
161+
slotProps={{
162+
formHelperText: {
163+
sx: { textAlign: "right" }
164+
}
165+
}}
166+
/>
167+
}
168+
disabled={state.questions.length === 0}
169+
/>
170+
</Grid2>
171+
</Grid2>
172+
<TableContainer>
173+
<Table
174+
sx={{
175+
"& .MuiTableCell-root": { padding: theme.spacing(1.2) },
176+
whiteSpace: "nowrap",
177+
}}
178+
>
179+
<TableHead>
180+
<TableRow>
181+
{tableHeaders.map((header) => (
182+
<TableCell key={header}>
183+
<Typography component="span" variant="h5">{header}</Typography>
184+
</TableCell>
185+
))}
186+
</TableRow>
187+
</TableHead>
188+
<TableBody>
189+
{state.questions.slice(0, rowsPerPage).map((question) => (
190+
<TableRow key={question.questionId}>
191+
<TableCell
192+
sx={{
193+
width: "50%",
194+
maxWidth: "250px",
195+
overflow: "hidden",
196+
textOverflow: "ellipsis",
197+
}}
198+
>
199+
<Typography
200+
component="span"
201+
sx={{ "&:hover": { cursor: "pointer", color: "primary.main" } }}
202+
onClick={() => navigate(`${question.questionId}`)}
203+
>{question.title}</Typography>
204+
</TableCell>
205+
<TableCell
206+
sx={{
207+
borderLeft: '1px solid #E0E0E0',
208+
borderRight: '1px solid #E0E0E0',
209+
}}
210+
>
211+
<Typography
212+
component="span"
213+
sx={{
214+
color:
215+
question.complexity === "Easy" ? "success.main" :
216+
question.complexity === "Medium" ? "#D2C350" :
217+
question.complexity === "Hard" ? "error.main" : grey[500]
218+
}}
219+
>{question.complexity}</Typography>
220+
</TableCell>
221+
<TableCell
222+
sx={{
223+
width: "50%",
224+
maxWidth: "250px",
225+
overflow: "auto",
226+
}}
227+
>
228+
<Stack direction="row">
229+
{question.categories.map((category) => (
230+
<Chip
231+
key={category}
232+
label={category}
233+
color="primary"
234+
sx={{
235+
marginLeft: theme.spacing(0.5),
236+
marginRight: theme.spacing(0.5),
237+
}}
238+
/>
239+
))}
240+
<Chip sx={{ visibility: "hidden", width: theme.spacing(0.5) }}/>
241+
</Stack>
242+
</TableCell>
243+
{isAdmin &&
244+
<TableCell sx={{ borderTop: '1px solid #E0E0E0' }}>
245+
<IconButton
246+
type="button"
247+
onClick={handleMenuOpen}
248+
>
249+
<MoreVert />
250+
</IconButton>
251+
<Menu
252+
anchorEl={anchorEl}
253+
open={menuOpen}
254+
onClose={handleMenuClose}
255+
>
256+
<MenuItem onClick={() => navigate(`${question.questionId}/edit`)}>
257+
<ListItemIcon>
258+
<Edit sx={{ fontSize: "large", color: blue[800] }} />
259+
</ListItemIcon>
260+
Edit
261+
</MenuItem>
262+
<MenuItem onClick={() => handleQuestionDelete(question.questionId)}>
263+
<ListItemIcon>
264+
<Delete sx={{ fontSize: "large", color: "error.main" }} />
265+
</ListItemIcon>
266+
Delete
267+
</MenuItem>
268+
</Menu>
269+
</TableCell>
270+
}
271+
</TableRow>
272+
))}
273+
</TableBody>
274+
</Table>
275+
</TableContainer>
276+
<TablePagination
277+
rowsPerPageOptions={[rowsPerPage]}
278+
component="div"
279+
count={state.questionCount}
280+
rowsPerPage={rowsPerPage}
281+
page={page}
282+
onPageChange={(_, page) => setPage(page)}
283+
/>
284+
{state.questions.length === 0 &&
285+
<Stack direction="column" spacing={1} sx={{ alignItems: "center", fontStyle: "italic" }}>
286+
<Typography>
287+
Unfortunately, there are no questions available now.
288+
</Typography>
289+
<Typography>
290+
Please try again later!
291+
</Typography>
292+
</Stack>
293+
}
294+
</Box>
295+
</AppMargin>
296+
);
297+
};
298+
299+
export default QuestionList;

0 commit comments

Comments
 (0)