Skip to content

Commit 0b29aff

Browse files
committed
add edit operations for admins
1 parent 2493c78 commit 0b29aff

File tree

7 files changed

+170
-31
lines changed

7 files changed

+170
-31
lines changed

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { useFilteredProblems } from '@/hooks/useFilteredProblems';
44
import FilterBar from '../(main)/components/filter/FilterBar';
55
import ProblemTable from '../../components/problems/ProblemTable';
66
import { axiosQuestionClient } from '@/network/axiosClient';
7+
import { Problem } from '@/types/types';
8+
import { isAxiosError } from 'axios';
79

810
function AdminPage() {
911
const {
@@ -18,13 +20,47 @@ function AdminPage() {
1820
const handleDelete = async (id: number) => {
1921
const res = await axiosQuestionClient.delete(`/questions/${id}`);
2022
if (res.status !== 200) {
21-
// Add error handling for a failed delete
2223
throw new Error('Failed to delete problem');
2324
}
2425
refetchFilter();
2526
return res;
2627
};
2728

29+
const handleEdit = async (problem: Problem) => {
30+
try {
31+
const res = await axiosQuestionClient.put(`/questions/${problem._id}`, {
32+
difficulty: problem.difficulty,
33+
description: problem.description,
34+
examples: problem.examples,
35+
constraints: problem.constraints,
36+
tags: problem.tags,
37+
title_slug: problem.title_slug,
38+
title: problem.title,
39+
});
40+
41+
refetchFilter();
42+
return res;
43+
} catch (e: unknown) {
44+
if (isAxiosError(e)) {
45+
switch (e.status) {
46+
case 400:
47+
throw new Error('Invalid question data. Please check your input.');
48+
case 409:
49+
throw new Error('Question already exists');
50+
case 404:
51+
throw new Error('Question not found');
52+
default:
53+
throw new Error('Failed to update question');
54+
}
55+
}
56+
if (e instanceof Error) {
57+
throw new Error(e.message);
58+
} else {
59+
throw new Error('An unknown error occurred');
60+
}
61+
}
62+
};
63+
2864
return (
2965
<div className="min-h-screen bg-gray-900 p-6 pt-24 text-gray-100">
3066
<div className="mx-auto max-w-7xl">
@@ -38,6 +74,7 @@ function AdminPage() {
3874
isLoading={isLoading}
3975
showActions={true}
4076
handleDelete={handleDelete}
77+
handleEdit={handleEdit}
4178
/>
4279
</div>
4380
</div>

peerprep-fe/src/components/problems/ProblemInputDialog.tsx

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import {
33
Dialog,
44
DialogContent,
@@ -7,14 +7,17 @@ import {
77
DialogTitle,
88
} from '../ui/dialog';
99
import { Button } from '../ui/button';
10-
import { getDifficultyString } from '@/lib/utils';
11-
import { ProblemDialogData } from '@/types/types';
10+
import { Problem } from '@/types/types';
11+
import { Textarea } from '../ui/textarea';
12+
import { Input } from '../ui/input';
13+
import { FilterSelect } from '@/app/(main)/components/filter/FilterSelect';
14+
import { TopicsPopover } from '@/app/(main)/components/filter/TopicsPopover';
1215

1316
type Props = {
1417
isOpen: boolean;
1518
onClose: () => void;
16-
problem: ProblemDialogData | null;
17-
requestCallback: () => void;
19+
problem: Problem;
20+
requestCallback: (problem: Problem) => void;
1821
requestTitle: string;
1922
};
2023

@@ -25,25 +28,76 @@ function ProblemInputDialog({
2528
requestCallback,
2629
requestTitle,
2730
}: Props) {
31+
const [problemData, setProblemData] = useState<Problem>(problem);
32+
33+
const handleSubmit = async () => {
34+
requestCallback(problemData);
35+
};
36+
2837
if (!problem) return null;
2938

3039
return (
3140
<Dialog open={isOpen} onOpenChange={onClose}>
3241
<DialogContent className="bg-black">
3342
<DialogHeader>
34-
<DialogTitle>{problem.title}</DialogTitle>
35-
<DialogDescription>
36-
Difficulty: {getDifficultyString(problem.difficulty)}
37-
</DialogDescription>
43+
<DialogTitle>Edit Question</DialogTitle>
44+
<DialogDescription />
3845
</DialogHeader>
39-
<div className="mt-4">
40-
<h3 className="mb-2 text-lg font-semibold">Description:</h3>
41-
<p>{problem.description}</p>
42-
</div>
43-
<div className="mt-6 flex justify-end">
44-
<Button variant="secondary" onClick={requestCallback}>
45-
{requestTitle}
46-
</Button>
46+
<div className="space-y-4">
47+
<div className="space-y-2">
48+
<p>Description</p>
49+
<Input
50+
name="title"
51+
placeholder="Question Title"
52+
defaultValue={problemData.title}
53+
onChange={(e) => {
54+
setProblemData({ ...problemData, title: e.target.value });
55+
}}
56+
required
57+
/>
58+
</div>
59+
<div className="space-y-2">
60+
<p>Difficulty</p>
61+
<FilterSelect
62+
placeholder="difficulty"
63+
options={[
64+
{ value: '1', label: 'Easy' },
65+
{ value: '2', label: 'Medium' },
66+
{ value: '3', label: 'Hard' },
67+
]}
68+
onChange={(value) => {
69+
setProblemData({ ...problemData, difficulty: Number(value) });
70+
}}
71+
value={String(problemData.difficulty)}
72+
/>
73+
</div>
74+
<div className="space-y-2">
75+
<p>Description</p>
76+
<Textarea
77+
name="description"
78+
placeholder="Question Description"
79+
defaultValue={problemData.description}
80+
onChange={(e) => {
81+
setProblemData({ ...problemData, description: e.target.value });
82+
}}
83+
className="min-h-[100px]"
84+
required
85+
/>
86+
</div>
87+
<div className="space-y-2">
88+
<p>Topics</p>
89+
<TopicsPopover
90+
selectedTopics={problemData.tags}
91+
onChange={(value) => {
92+
setProblemData({ ...problemData, tags: value });
93+
}}
94+
/>
95+
</div>
96+
<div className="mt-2 flex justify-end">
97+
<Button variant="secondary" onClick={handleSubmit}>
98+
{requestTitle}
99+
</Button>
100+
</div>
47101
</div>
48102
</DialogContent>
49103
</Dialog>

peerprep-fe/src/components/problems/ProblemRow.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22
import React, { useState } from 'react';
33
import { CheckCircle2, Pencil, Trash2 } from 'lucide-react';
4-
import { Problem, ProblemDialogData } from '@/types/types';
4+
import { Problem } from '@/types/types';
55
import { Button } from '../ui/button';
66
import { getDifficultyString } from '@/lib/utils';
77
import ProblemInputDialog from './ProblemInputDialog';
@@ -24,32 +24,42 @@ interface Props {
2424
handleDelete:
2525
| ((id: number) => Promise<AxiosResponse<unknown, unknown>>)
2626
| undefined;
27+
handleEdit:
28+
| ((problem: Problem) => Promise<AxiosResponse<unknown, unknown>>)
29+
| undefined;
2730
}
2831

2932
export default function ProblemRow({
3033
problem,
3134
showActions,
3235
handleDelete,
36+
handleEdit,
3337
}: Props) {
3438
const [isDialogOpen, setIsDialogOpen] = useState(false);
3539
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
3640
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
3741
const [informationDialog, setInformationDialog] = useState('');
3842

39-
const problemDetails: ProblemDialogData = {
40-
_id: problem._id,
41-
title: problem.title,
42-
difficulty: problem.difficulty,
43-
description: problem.description,
44-
};
45-
4643
const handleDeleteClick = async () => {
4744
if (!handleDelete) return;
4845
handleDelete(problem._id).catch(() => {
4946
setInformationDialog('Failed to delete problem');
5047
});
5148
};
5249

50+
const handleEditClick = async (problemData: Problem) => {
51+
if (!handleEdit) return;
52+
if (problemData === problem) {
53+
setInformationDialog('No changes made');
54+
return;
55+
}
56+
57+
handleEdit(problemData).catch((e: Error) => {
58+
console.log(e);
59+
setInformationDialog(e.message);
60+
});
61+
};
62+
5363
return (
5464
<>
5565
<tr className="border-b border-gray-800">
@@ -135,10 +145,8 @@ export default function ProblemRow({
135145
<ProblemInputDialog
136146
isOpen={isEditDialogOpen}
137147
onClose={() => setIsEditDialogOpen(false)}
138-
problem={problemDetails}
139-
requestCallback={() => {
140-
console.log('Hello');
141-
}}
148+
problem={problem}
149+
requestCallback={handleEditClick}
142150
requestTitle="Update"
143151
/>
144152

peerprep-fe/src/components/problems/ProblemTable.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@ interface ProblemTableProps {
1111
handleDelete?:
1212
| ((id: number) => Promise<AxiosResponse<unknown, unknown>>)
1313
| undefined;
14+
handleEdit?:
15+
| ((problem: Problem) => Promise<AxiosResponse<unknown, unknown>>)
16+
| undefined;
1417
}
1518

1619
export default function ProblemTable({
1720
problems,
1821
isLoading,
1922
showActions = false,
2023
handleDelete,
24+
handleEdit,
2125
}: ProblemTableProps) {
2226
return (
2327
<div className="overflow-x-auto">
@@ -59,6 +63,7 @@ export default function ProblemTable({
5963
problem={problem}
6064
showActions={showActions}
6165
handleDelete={handleDelete}
66+
handleEdit={handleEdit}
6267
/>
6368
))}
6469
</tbody>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as React from 'react';
2+
3+
import { cn } from '@/lib/utils';
4+
5+
// Prevent interface extending without adding any new properties
6+
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
7+
8+
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
9+
({ className, ...props }, ref) => {
10+
return (
11+
<textarea
12+
className={cn(
13+
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
14+
className,
15+
)}
16+
ref={ref}
17+
{...props}
18+
/>
19+
);
20+
},
21+
);
22+
Textarea.displayName = 'Textarea';
23+
24+
export { Textarea };

peerprep-fe/src/types/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ interface ProblemDialogData {
1717
description: string;
1818
}
1919

20+
interface ProblemRequestData {
21+
title: string;
22+
difficulty: number;
23+
description: string;
24+
examples: string[];
25+
constraints: string;
26+
tags: string[];
27+
}
28+
2029
interface FilterBadgeProps {
2130
filterType: 'Difficulty' | 'Status' | 'Topics';
2231
value: string;
@@ -42,6 +51,7 @@ interface User {
4251
export type {
4352
Problem,
4453
ProblemDialogData,
54+
ProblemRequestData,
4555
FilterBadgeProps,
4656
FilterSelectProps,
4757
User,

question-service/src/routes/questionsController.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ router.put('/:id', async (req: Request, res: Response) => {
6666
title: parsedResult.data.title,
6767
});
6868

69-
if (existingQuestion) {
69+
// Check if the question already exists with separate id
70+
if (existingQuestion && existingQuestion._id.toString() !== id) {
7071
return res.status(409).json({
7172
error: 'This question title already exist',
7273
});

0 commit comments

Comments
 (0)