Skip to content

Commit 24ffd5c

Browse files
committed
chore/ui: Add CRUD for questions
Signed-off-by: SeeuSim <[email protected]>
1 parent 1f80d09 commit 24ffd5c

File tree

3 files changed

+160
-38
lines changed

3 files changed

+160
-38
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { TrashIcon } from '@radix-ui/react-icons';
2+
import { useMutation } from '@tanstack/react-query';
3+
import { Loader2 } from 'lucide-react';
4+
import { Dispatch, FC, SetStateAction } from 'react';
5+
import { useNavigate } from 'react-router-dom';
6+
7+
import {
8+
AlertDialog,
9+
AlertDialogAction,
10+
AlertDialogCancel,
11+
AlertDialogContent,
12+
AlertDialogDescription,
13+
AlertDialogFooter,
14+
AlertDialogHeader,
15+
AlertDialogTitle,
16+
} from '@/components/ui/alert-dialog';
17+
18+
type AdminDeleteFormProps = {
19+
isOpen: boolean;
20+
setIsOpen: Dispatch<SetStateAction<boolean>>;
21+
questionId: number;
22+
};
23+
24+
export const AdminDeleteForm: FC<AdminDeleteFormProps> = ({ isOpen, setIsOpen, questionId }) => {
25+
const navigate = useNavigate();
26+
const {
27+
mutate: deleteQuestion,
28+
isPending,
29+
isSuccess,
30+
} = useMutation({
31+
mutationFn: async (questionId: number) => {
32+
setTimeout(() => {
33+
navigate('/');
34+
}, 1000);
35+
},
36+
});
37+
38+
return (
39+
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
40+
<AlertDialogContent>
41+
<AlertDialogHeader>
42+
<AlertDialogTitle className='flex'>
43+
Are you sure you want to delete question:&nbsp;`<pre>{questionId}</pre>`?
44+
</AlertDialogTitle>
45+
</AlertDialogHeader>
46+
<AlertDialogDescription />
47+
<AlertDialogFooter className='flex w-full justify-between'>
48+
<AlertDialogCancel disabled={isPending || isSuccess}>Cancel</AlertDialogCancel>
49+
<AlertDialogAction
50+
onClick={(event) => {
51+
event.preventDefault();
52+
deleteQuestion(questionId);
53+
}}
54+
disabled={isPending || isSuccess}
55+
className='flex items-center gap-2'
56+
>
57+
<span>{isPending ? 'Deleting...' : isSuccess ? 'Deleted Successfully' : 'Delete'}</span>
58+
{isPending ? <Loader2 className='size-4 animate-spin' /> : <TrashIcon />}
59+
</AlertDialogAction>
60+
</AlertDialogFooter>
61+
</AlertDialogContent>
62+
</AlertDialog>
63+
);
64+
};

frontend/src/components/blocks/questions/admin-edit-form.tsx

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { zodResolver } from '@hookform/resolvers/zod';
2-
import { Cross2Icon, DotsVerticalIcon, Pencil1Icon, PlusIcon } from '@radix-ui/react-icons';
2+
import { Cross2Icon, PlusIcon } from '@radix-ui/react-icons';
33
import { useMutation } from '@tanstack/react-query';
4-
import { type FC,useState } from 'react';
4+
import { Loader2 } from 'lucide-react';
5+
import { type Dispatch, type FC, type SetStateAction, useState } from 'react';
56
import { useForm } from 'react-hook-form';
67
import Markdown from 'react-markdown';
78
import rehypeKatex from 'rehype-katex';
@@ -19,12 +20,6 @@ import {
1920
DialogHeader,
2021
DialogTitle,
2122
} from '@/components/ui/dialog';
22-
import {
23-
DropdownMenu,
24-
DropdownMenuContent,
25-
DropdownMenuItem,
26-
DropdownMenuTrigger,
27-
} from '@/components/ui/dropdown-menu';
2823
import {
2924
Form,
3025
FormControl,
@@ -47,6 +42,8 @@ import { Textarea } from '@/components/ui/textarea';
4742
import type { IGetQuestionDetailsResponse } from '@/types/question-types';
4843

4944
type AdminEditFormProps = {
45+
isFormOpen: boolean;
46+
setIsFormOpen: Dispatch<SetStateAction<boolean>>;
5047
questionDetails: IGetQuestionDetailsResponse['question'];
5148
};
5249

@@ -57,10 +54,19 @@ const formSchema = z.object({
5754
description: z.string().min(1),
5855
});
5956

60-
export const AdminEditForm: FC<AdminEditFormProps> = ({ questionDetails }) => {
61-
const [isFormOpen, setIsFormOpen] = useState(false);
57+
export const AdminEditForm: FC<AdminEditFormProps> = ({
58+
questionDetails,
59+
isFormOpen,
60+
setIsFormOpen,
61+
}) => {
6262
const [addedTopic, setAddedTopic] = useState('');
63-
const { mutate: _sendUpdate, isPending } = useMutation({});
63+
const {
64+
mutate: sendUpdate,
65+
isPending,
66+
isSuccess,
67+
} = useMutation({
68+
mutationFn: async (values: z.infer<typeof formSchema>) => {},
69+
});
6470

6571
const form = useForm<z.infer<typeof formSchema>>({
6672
resolver: zodResolver(formSchema),
@@ -72,7 +78,9 @@ export const AdminEditForm: FC<AdminEditFormProps> = ({ questionDetails }) => {
7278
mode: 'onSubmit',
7379
});
7480

75-
const onSubmit = (_formValues: z.infer<typeof formSchema>) => {};
81+
const onSubmit = (formValues: z.infer<typeof formSchema>) => {
82+
sendUpdate(formValues);
83+
};
7684

7785
const addTopic = (topic: string) => {
7886
const val = new Set(form.getValues('topic').map((v) => v.toLowerCase()));
@@ -85,26 +93,6 @@ export const AdminEditForm: FC<AdminEditFormProps> = ({ questionDetails }) => {
8593

8694
return (
8795
<>
88-
<DropdownMenu>
89-
<DropdownMenuTrigger asChild>
90-
<Button
91-
className='min-h-none ml-auto flex !h-6 items-center gap-2 rounded-lg px-2'
92-
size='sm'
93-
>
94-
<span>Actions</span>
95-
<DotsVerticalIcon />
96-
</Button>
97-
</DropdownMenuTrigger>
98-
<DropdownMenuContent className='border-border'>
99-
<DropdownMenuItem
100-
onClick={() => setIsFormOpen((isOpen) => !isOpen)}
101-
className='flex w-full justify-between gap-2 hover:cursor-pointer'
102-
>
103-
<span>Edit</span>
104-
<Pencil1Icon />
105-
</DropdownMenuItem>
106-
</DropdownMenuContent>
107-
</DropdownMenu>
10896
<Dialog open={isFormOpen} onOpenChange={setIsFormOpen}>
10997
<DialogContent className='border-border flex h-dvh w-dvw max-w-screen-lg flex-col'>
11098
<DialogHeader className=''>
@@ -142,10 +130,10 @@ export const AdminEditForm: FC<AdminEditFormProps> = ({ questionDetails }) => {
142130
value={field.value}
143131
onValueChange={field.onChange}
144132
>
145-
<SelectTrigger>
133+
<SelectTrigger className='focus:ring-secondary-foreground/50'>
146134
<SelectValue />
147135
</SelectTrigger>
148-
<SelectContent>
136+
<SelectContent className='border-secondary-foreground/50'>
149137
{(['Easy', 'Medium', 'Hard'] as const).map((value, index) => (
150138
<SelectItem key={index} value={value}>
151139
{value}
@@ -201,7 +189,7 @@ export const AdminEditForm: FC<AdminEditFormProps> = ({ questionDetails }) => {
201189
className=''
202190
disabled={isPending}
203191
onClick={() => {
204-
form.setValue('topic', questionDetails.topic);
192+
form.resetField('topic');
205193
}}
206194
size='sm'
207195
>
@@ -297,10 +285,30 @@ export const AdminEditForm: FC<AdminEditFormProps> = ({ questionDetails }) => {
297285
</DialogDescription>
298286
<DialogFooter>
299287
<div className='flex w-full items-center justify-between'>
300-
<Button variant='secondary' size='sm'>
288+
<Button
289+
disabled={isPending || isSuccess}
290+
variant='secondary'
291+
size='sm'
292+
onClick={() => {
293+
form.reset();
294+
setIsFormOpen(false);
295+
}}
296+
>
301297
Cancel
302298
</Button>
303-
<Button size='sm'>Save Changes</Button>
299+
<Button
300+
disabled={isPending || isSuccess}
301+
onClick={() => {
302+
onSubmit(form.getValues());
303+
}}
304+
size='sm'
305+
className='flex gap-2'
306+
>
307+
<span>
308+
{isPending ? 'Submitting' : isSuccess ? 'Updated! Closing form' : 'Save Changes'}
309+
</span>
310+
{isPending && <Loader2 className='size-4 animate-spin' />}
311+
</Button>
304312
</div>
305313
</DialogFooter>
306314
</DialogContent>

frontend/src/components/blocks/questions/details.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
1+
import { DotsVerticalIcon, Pencil1Icon, TrashIcon } from '@radix-ui/react-icons';
2+
import { useState } from 'react';
13
import Markdown from 'react-markdown';
24
import rehypeKatex from 'rehype-katex';
35
import remarkGfm from 'remark-gfm';
46
import remarkMath from 'remark-math';
57

68
import { Badge } from '@/components/ui/badge';
9+
import { Button } from '@/components/ui/button';
710
import { CardContent, CardHeader, CardTitle } from '@/components/ui/card';
11+
import {
12+
DropdownMenu,
13+
DropdownMenuContent,
14+
DropdownMenuItem,
15+
DropdownMenuTrigger,
16+
} from '@/components/ui/dropdown-menu';
817
import { ScrollArea } from '@/components/ui/scroll-area';
918
import { Separator } from '@/components/ui/separator';
1019
import { useAuthedRoute } from '@/stores/auth-store';
1120
import type { IGetQuestionDetailsResponse } from '@/types/question-types';
1221

22+
import { AdminDeleteForm } from './admin-delete-form';
1323
import { AdminEditForm } from './admin-edit-form';
1424

1525
export const QuestionDetails = ({
@@ -18,6 +28,8 @@ export const QuestionDetails = ({
1828
questionDetails: IGetQuestionDetailsResponse['question'];
1929
}) => {
2030
const { isAdmin } = useAuthedRoute();
31+
const [isFormOpen, setIsFormOpen] = useState(false);
32+
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
2133
return (
2234
<ScrollArea className='h-full'>
2335
<CardHeader>
@@ -26,7 +38,45 @@ export const QuestionDetails = ({
2638
<CardTitle className='text-2xl'>
2739
{questionDetails.id}.&nbsp;{questionDetails.title}
2840
</CardTitle>
29-
{isAdmin && <AdminEditForm questionDetails={questionDetails} />}
41+
{isAdmin && (
42+
<>
43+
<DropdownMenu>
44+
<DropdownMenuTrigger asChild>
45+
<Button
46+
className='min-h-none ml-auto flex !h-6 items-center gap-2 rounded-lg px-2'
47+
size='sm'
48+
>
49+
<span>Actions</span>
50+
<DotsVerticalIcon />
51+
</Button>
52+
</DropdownMenuTrigger>
53+
<DropdownMenuContent className='border-border flex flex-col gap-1'>
54+
<DropdownMenuItem
55+
onClick={() => setIsFormOpen((isOpen) => !isOpen)}
56+
className='flex w-full justify-between gap-2 hover:cursor-pointer'
57+
>
58+
<span>Edit</span>
59+
<Pencil1Icon />
60+
</DropdownMenuItem>
61+
<DropdownMenuItem
62+
onClick={() => setIsDeleteDialogOpen((isOpen) => !isOpen)}
63+
className='flex w-full justify-between gap-2 bg-red-50/20 text-red-600 hover:cursor-pointer focus:bg-red-100 focus:text-red-600 dark:bg-red-950/40 dark:text-red-500 focus:dark:bg-red-900'
64+
>
65+
<span>Delete</span>
66+
<TrashIcon />
67+
</DropdownMenuItem>
68+
</DropdownMenuContent>
69+
</DropdownMenu>
70+
<AdminEditForm {...{ questionDetails, isFormOpen, setIsFormOpen }} />
71+
<AdminDeleteForm
72+
{...{
73+
isOpen: isDeleteDialogOpen,
74+
setIsOpen: setIsDeleteDialogOpen,
75+
questionId: Number.parseInt(questionDetails.id!),
76+
}}
77+
/>
78+
</>
79+
)}
3080
</div>
3181
<div className='flex flex-wrap items-center gap-1'>
3282
<Badge

0 commit comments

Comments
 (0)