Skip to content

Commit 6fc94ea

Browse files
authored
Merge pull request #46 from CS3219-AY2425S1/feat/admin-fe
Admin Frontend
2 parents 45db24a + 6191e71 commit 6fc94ea

File tree

29 files changed

+3670
-493
lines changed

29 files changed

+3670
-493
lines changed

peerprep-fe/pnpm-lock.yaml

Lines changed: 2770 additions & 242 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

peerprep-fe/src/app/(main)/components/Main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22
import { useFilteredProblems } from '@/hooks/useFilteredProblems';
33
import FilterBar from './filter/FilterBar';
4-
import ProblemTable from './problems/ProblemTable';
4+
import ProblemTable from '../../../components/problems/ProblemTable';
55

66
export default function MainComponent() {
77
const { problems, filters, updateFilter, removeFilter, isLoading } =

peerprep-fe/src/app/(main)/components/filter/FilterBar.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,7 @@ import { TopicsPopover } from './TopicsPopover';
88
import { FilterState } from '@/hooks/useFilteredProblems';
99
import { useState, useEffect } from 'react';
1010
import { useDebounce } from '@/hooks/useDebounce';
11-
12-
const DIFFICULTY_OPTIONS = [
13-
{ value: '1', label: 'Easy' },
14-
{ value: '2', label: 'Medium' },
15-
{ value: '3', label: 'Hard' },
16-
];
17-
18-
const STATUS_OPTIONS = [
19-
{ value: 'todo', label: 'Todo' },
20-
{ value: 'solved', label: 'Solved' },
21-
];
11+
import { DIFFICULTY_OPTIONS, STATUS_OPTIONS } from '@/lib/constants';
2212

2313
interface FilterBarProps {
2414
filters: FilterState;
@@ -27,12 +17,16 @@ interface FilterBarProps {
2717
value: string | string[] | null,
2818
) => void;
2919
removeFilter: (key: keyof FilterState, value?: string) => void;
20+
isAdmin?: boolean;
21+
buttonCallback?: () => void;
3022
}
3123

3224
export default function FilterBar({
3325
filters,
3426
updateFilter,
3527
removeFilter,
28+
isAdmin = false,
29+
buttonCallback,
3630
}: FilterBarProps) {
3731
const [searchTerm, setSearchTerm] = useState('');
3832
const debouncedSearchTerm = useDebounce(searchTerm, 300); // 300ms delay
@@ -83,9 +77,18 @@ export default function FilterBar({
8377
>
8478
<Settings className="h-4 w-4" />
8579
</Button>
86-
<Button className="bg-green-600 text-white hover:bg-green-700">
87-
Match
88-
</Button>
80+
{!isAdmin ? (
81+
<Button className="bg-green-600 text-white hover:bg-green-700">
82+
Match
83+
</Button>
84+
) : (
85+
<Button
86+
className="bg-blue-600 text-white hover:bg-blue-700"
87+
onClick={buttonCallback}
88+
>
89+
Add
90+
</Button>
91+
)}
8992
</div>
9093
<div className="flex flex-wrap gap-2">
9194
{filters.difficulty && (

peerprep-fe/src/app/(main)/components/problems/ProblemDialog.tsx

Lines changed: 0 additions & 47 deletions
This file was deleted.

peerprep-fe/src/app/(main)/components/problems/ProblemRow.tsx

Lines changed: 0 additions & 79 deletions
This file was deleted.
Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
'use client';
22
import MainComponent from './components/Main';
3-
import { useAuth } from '@/hooks/useAuth';
4-
import LoadingSpinner from '@/components/loading/LoadingSpinner';
53

64
export default function Home() {
7-
const { isLoading } = useAuth();
8-
if (isLoading) {
9-
return <LoadingSpinner />;
10-
}
11-
125
return <MainComponent />;
136
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
import Navbar from '@/components/navbar/Navbar';
3+
4+
const MainLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
5+
return (
6+
<>
7+
<Navbar />
8+
<main>{children}</main>
9+
</>
10+
);
11+
};
12+
13+
export default MainLayout;

peerprep-fe/src/app/admin/page.tsx

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
'use client';
2+
import React, { useState } from 'react';
3+
import { useFilteredProblems } from '@/hooks/useFilteredProblems';
4+
import FilterBar from '../(main)/components/filter/FilterBar';
5+
import ProblemTable from '../../components/problems/ProblemTable';
6+
import { axiosQuestionClient } from '@/network/axiosClient';
7+
import { Problem } from '@/types/types';
8+
import { isAxiosError } from 'axios';
9+
import ProblemInputDialog from '@/components/problems/ProblemInputDialog';
10+
import InformationDialog from '@/components/dialogs/InformationDialog';
11+
12+
function AdminPage() {
13+
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
14+
const [informationDialog, setInformationDialog] = useState('');
15+
const {
16+
problems,
17+
filters,
18+
updateFilter,
19+
removeFilter,
20+
isLoading,
21+
refetchFilter,
22+
} = useFilteredProblems();
23+
24+
const handleDelete = async (id: number) => {
25+
const res = await axiosQuestionClient.delete(`/questions/${id}`);
26+
if (res.status !== 200) {
27+
throw new Error('Failed to delete problem');
28+
}
29+
refetchFilter();
30+
return res;
31+
};
32+
33+
const handleEdit = async (problem: Problem) => {
34+
try {
35+
const res = await axiosQuestionClient.put(`/questions/${problem._id}`, {
36+
difficulty: problem.difficulty,
37+
description: problem.description,
38+
examples: problem.examples,
39+
constraints: problem.constraints,
40+
tags: problem.tags,
41+
title_slug: problem.title_slug,
42+
title: problem.title,
43+
});
44+
45+
refetchFilter();
46+
return res;
47+
} catch (e: unknown) {
48+
if (isAxiosError(e)) {
49+
switch (e.status) {
50+
case 400:
51+
throw new Error('Invalid question data. Please check your input.');
52+
case 409:
53+
throw new Error('Question already exists');
54+
case 404:
55+
throw new Error('Question not found');
56+
default:
57+
throw new Error('Failed to update question');
58+
}
59+
}
60+
if (e instanceof Error) {
61+
throw new Error(e.message);
62+
} else {
63+
throw new Error('An unknown error occurred');
64+
}
65+
}
66+
};
67+
68+
const handleAdd = async (problem: Problem) => {
69+
// TODO: Add proper validation of fields
70+
if (
71+
problem.description === '' ||
72+
problem.title === '' ||
73+
problem.tags.length === 0
74+
) {
75+
setInformationDialog('Please fill in all required fields');
76+
return;
77+
}
78+
try {
79+
const res = await axiosQuestionClient.post(`/questions`, {
80+
difficulty: problem.difficulty,
81+
description: problem.description,
82+
examples: problem.examples,
83+
constraints: problem.constraints,
84+
tags: problem.tags,
85+
title_slug: problem.title_slug,
86+
title: problem.title,
87+
});
88+
89+
refetchFilter();
90+
setIsAddDialogOpen(false);
91+
return res;
92+
} catch (e: unknown) {
93+
if (isAxiosError(e)) {
94+
switch (e.status) {
95+
case 400:
96+
throw new Error('Invalid question data. Please check your input.');
97+
case 409:
98+
throw new Error('Question already exists');
99+
case 404:
100+
throw new Error('Question not found');
101+
default:
102+
throw new Error('Failed to update question');
103+
}
104+
}
105+
if (e instanceof Error) {
106+
throw new Error(e.message);
107+
} else {
108+
throw new Error('An unknown error occurred');
109+
}
110+
}
111+
};
112+
113+
return (
114+
<div className="min-h-screen bg-gray-900 p-6 pt-24 text-gray-100">
115+
<div className="mx-auto max-w-7xl">
116+
<FilterBar
117+
filters={filters}
118+
updateFilter={updateFilter}
119+
removeFilter={removeFilter}
120+
isAdmin
121+
buttonCallback={() => setIsAddDialogOpen(true)}
122+
/>
123+
<ProblemTable
124+
problems={problems}
125+
isLoading={isLoading}
126+
showActions={true}
127+
handleDelete={handleDelete}
128+
handleEdit={handleEdit}
129+
/>
130+
</div>
131+
<ProblemInputDialog
132+
isOpen={isAddDialogOpen}
133+
onClose={() => setIsAddDialogOpen(false)}
134+
requestCallback={handleAdd}
135+
requestTitle="Add"
136+
/>
137+
138+
<InformationDialog
139+
isOpen={informationDialog !== ''}
140+
onClose={() => setInformationDialog('')}
141+
title="Status"
142+
description={informationDialog}
143+
/>
144+
</div>
145+
);
146+
}
147+
148+
export default AdminPage;

0 commit comments

Comments
 (0)