Skip to content

Commit 1f80d09

Browse files
committed
chore/ui: Add update form
Signed-off-by: SeeuSim <[email protected]>
1 parent 8ce07d4 commit 1f80d09

File tree

2 files changed

+302
-20
lines changed

2 files changed

+302
-20
lines changed
Lines changed: 299 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,310 @@
1+
import { zodResolver } from '@hookform/resolvers/zod';
2+
import { Cross2Icon, DotsVerticalIcon, Pencil1Icon, PlusIcon } from '@radix-ui/react-icons';
3+
import { useMutation } from '@tanstack/react-query';
4+
import { type FC,useState } from 'react';
5+
import { useForm } from 'react-hook-form';
6+
import Markdown from 'react-markdown';
7+
import rehypeKatex from 'rehype-katex';
8+
import remarkGfm from 'remark-gfm';
9+
import remarkMath from 'remark-math';
10+
import { z } from 'zod';
11+
12+
import { Badge } from '@/components/ui/badge';
13+
import { Button } from '@/components/ui/button';
114
import {
215
Dialog,
316
DialogContent,
17+
DialogDescription,
418
DialogFooter,
519
DialogHeader,
620
DialogTitle,
7-
DialogTrigger,
821
} from '@/components/ui/dialog';
22+
import {
23+
DropdownMenu,
24+
DropdownMenuContent,
25+
DropdownMenuItem,
26+
DropdownMenuTrigger,
27+
} from '@/components/ui/dropdown-menu';
28+
import {
29+
Form,
30+
FormControl,
31+
FormField,
32+
FormItem,
33+
FormLabel,
34+
FormMessage,
35+
} from '@/components/ui/form';
36+
import { Input } from '@/components/ui/input';
37+
import { ScrollArea } from '@/components/ui/scroll-area';
38+
import {
39+
Select,
40+
SelectContent,
41+
SelectItem,
42+
SelectTrigger,
43+
SelectValue,
44+
} from '@/components/ui/select';
45+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
46+
import { Textarea } from '@/components/ui/textarea';
47+
import type { IGetQuestionDetailsResponse } from '@/types/question-types';
48+
49+
type AdminEditFormProps = {
50+
questionDetails: IGetQuestionDetailsResponse['question'];
51+
};
52+
53+
const formSchema = z.object({
54+
title: z.string().min(1),
55+
difficulty: z.enum(['Easy', 'Medium', 'Hard']),
56+
topic: z.string().min(1).array().min(1),
57+
description: z.string().min(1),
58+
});
59+
60+
export const AdminEditForm: FC<AdminEditFormProps> = ({ questionDetails }) => {
61+
const [isFormOpen, setIsFormOpen] = useState(false);
62+
const [addedTopic, setAddedTopic] = useState('');
63+
const { mutate: _sendUpdate, isPending } = useMutation({});
64+
65+
const form = useForm<z.infer<typeof formSchema>>({
66+
resolver: zodResolver(formSchema),
67+
defaultValues: {
68+
...questionDetails,
69+
description: questionDetails.description,
70+
difficulty: questionDetails.difficulty as z.infer<typeof formSchema>['difficulty'],
71+
},
72+
mode: 'onSubmit',
73+
});
74+
75+
const onSubmit = (_formValues: z.infer<typeof formSchema>) => {};
76+
77+
const addTopic = (topic: string) => {
78+
const val = new Set(form.getValues('topic').map((v) => v.toLowerCase()));
79+
val.add(topic.toLowerCase());
80+
form.setValue(
81+
'topic',
82+
Array.from(val).map((v) => v.replace(/^[a-z]/, (c) => c.toUpperCase()))
83+
);
84+
};
985

10-
export const AdminEditForm = () => {
1186
return (
12-
<Dialog>
13-
<DialogTrigger />
14-
<DialogContent>
15-
<DialogHeader>
16-
<DialogTitle />
17-
</DialogHeader>
18-
<DialogContent />
19-
<DialogFooter />
20-
</DialogContent>
21-
</Dialog>
87+
<>
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>
108+
<Dialog open={isFormOpen} onOpenChange={setIsFormOpen}>
109+
<DialogContent className='border-border flex h-dvh w-dvw max-w-screen-lg flex-col'>
110+
<DialogHeader className=''>
111+
<DialogTitle className='text-primary'>Edit Question Details</DialogTitle>
112+
</DialogHeader>
113+
<DialogDescription className='h-full'>
114+
<Form {...form}>
115+
<form
116+
className='text-primary flex h-full flex-col gap-4'
117+
onSubmit={form.handleSubmit(onSubmit)}
118+
>
119+
<FormField
120+
control={form.control}
121+
name='title'
122+
render={({ field }) => (
123+
<FormItem>
124+
<FormLabel>Title</FormLabel>
125+
<FormControl>
126+
<Input disabled={isPending} placeholder='someQuestionTitle' {...field} />
127+
</FormControl>
128+
<FormMessage />
129+
</FormItem>
130+
)}
131+
/>
132+
<div className='flex w-full flex-col gap-4 sm:grid sm:grid-cols-3 sm:gap-8'>
133+
<FormField
134+
control={form.control}
135+
name='difficulty'
136+
render={({ field }) => (
137+
<FormItem>
138+
<FormLabel>Difficulty</FormLabel>
139+
<FormControl>
140+
<Select
141+
disabled={isPending}
142+
value={field.value}
143+
onValueChange={field.onChange}
144+
>
145+
<SelectTrigger>
146+
<SelectValue />
147+
</SelectTrigger>
148+
<SelectContent>
149+
{(['Easy', 'Medium', 'Hard'] as const).map((value, index) => (
150+
<SelectItem key={index} value={value}>
151+
{value}
152+
</SelectItem>
153+
))}
154+
</SelectContent>
155+
</Select>
156+
</FormControl>
157+
<FormMessage />
158+
</FormItem>
159+
)}
160+
/>
161+
<FormField
162+
control={form.control}
163+
name='topic'
164+
render={({ field }) => (
165+
<FormItem className='flex flex-col gap-1 sm:col-span-2'>
166+
<div className='flex items-end gap-2'>
167+
<FormLabel>Topics</FormLabel>
168+
</div>
169+
<FormControl>
170+
<div className='flex flex-1 items-start gap-2'>
171+
<div className='mr-2 flex gap-2'>
172+
<Input
173+
disabled={isPending}
174+
value={addedTopic}
175+
onChange={(e) => {
176+
setAddedTopic(e.currentTarget.value);
177+
}}
178+
className='w-[150px]'
179+
placeholder='New topic'
180+
onKeyDown={(event) => {
181+
if (event.key === 'Enter' && addedTopic) {
182+
event.preventDefault();
183+
addTopic(addedTopic);
184+
setAddedTopic('');
185+
}
186+
}}
187+
/>
188+
<Button
189+
onClick={() => {
190+
addTopic(addedTopic);
191+
setAddedTopic('');
192+
}}
193+
disabled={isPending || !addedTopic}
194+
className='flex w-min gap-1'
195+
size='sm'
196+
>
197+
<span>Add</span>
198+
<PlusIcon />
199+
</Button>
200+
<Button
201+
className=''
202+
disabled={isPending}
203+
onClick={() => {
204+
form.setValue('topic', questionDetails.topic);
205+
}}
206+
size='sm'
207+
>
208+
Reset
209+
</Button>
210+
</div>
211+
<div className='scrollbar-thumb-primary flex max-h-[64px] flex-1 flex-wrap gap-2 overflow-auto'>
212+
{field.value.map((value, index) => (
213+
<Badge
214+
key={index}
215+
className='flex size-min gap-2 whitespace-nowrap rounded-full'
216+
>
217+
<span>{value}</span>
218+
<Cross2Icon
219+
className='hover:scale-105 hover:cursor-pointer'
220+
onClick={() => {
221+
if (isPending) {
222+
return;
223+
}
224+
225+
form.setValue(
226+
'topic',
227+
field.value.filter((_value, idx) => idx !== index)
228+
);
229+
}}
230+
/>
231+
</Badge>
232+
))}
233+
</div>
234+
</div>
235+
</FormControl>
236+
<FormMessage />
237+
</FormItem>
238+
)}
239+
/>
240+
</div>
241+
<FormField
242+
control={form.control}
243+
name='description'
244+
render={({ field }) => (
245+
<FormItem className='flex flex-1 flex-col'>
246+
<Tabs defaultValue='edit' className='flex flex-1 flex-col'>
247+
<TabsList className='flex w-min'>
248+
<TabsTrigger value='edit'>Description</TabsTrigger>
249+
<TabsTrigger value='preview'>Preview</TabsTrigger>
250+
</TabsList>
251+
<TabsContent
252+
value='edit'
253+
className='hidden data-[state=active]:flex data-[state=active]:flex-1'
254+
>
255+
<FormControl>
256+
<Textarea
257+
{...field}
258+
className='flex flex-1 resize-none'
259+
disabled={isPending}
260+
placeholder='lorem ipsum ...'
261+
/>
262+
</FormControl>
263+
</TabsContent>
264+
<TabsContent
265+
value='preview'
266+
className='hidden data-[state=active]:flex data-[state=active]:flex-1'
267+
>
268+
<ScrollArea className='border-border flex h-full max-h-[calc(100dvh-336px)] max-w-screen-md flex-1 rounded-lg border p-4'>
269+
<Markdown
270+
rehypePlugins={[rehypeKatex]}
271+
remarkPlugins={[remarkMath, remarkGfm]}
272+
className='prose prose-neutral text-card-foreground prose-strong:text-card-foreground leading-normal'
273+
components={{
274+
code: ({ children, className, ...rest }) => {
275+
return (
276+
<code
277+
{...rest}
278+
className='bg-secondary text-secondary-foreground rounded px-1.5 py-1 font-mono'
279+
>
280+
{children}
281+
</code>
282+
);
283+
},
284+
}}
285+
>
286+
{form.watch('description')}
287+
</Markdown>
288+
</ScrollArea>
289+
</TabsContent>
290+
</Tabs>
291+
<FormMessage />
292+
</FormItem>
293+
)}
294+
/>
295+
</form>
296+
</Form>
297+
</DialogDescription>
298+
<DialogFooter>
299+
<div className='flex w-full items-center justify-between'>
300+
<Button variant='secondary' size='sm'>
301+
Cancel
302+
</Button>
303+
<Button size='sm'>Save Changes</Button>
304+
</div>
305+
</DialogFooter>
306+
</DialogContent>
307+
</Dialog>
308+
</>
22309
);
23310
};

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import { Pencil1Icon } from '@radix-ui/react-icons';
21
import Markdown from 'react-markdown';
32
import rehypeKatex from 'rehype-katex';
43
import remarkGfm from 'remark-gfm';
54
import remarkMath from 'remark-math';
65

76
import { Badge } from '@/components/ui/badge';
8-
import { Button } from '@/components/ui/button';
97
import { CardContent, CardHeader, CardTitle } from '@/components/ui/card';
108
import { ScrollArea } from '@/components/ui/scroll-area';
119
import { Separator } from '@/components/ui/separator';
1210
import { useAuthedRoute } from '@/stores/auth-store';
1311
import type { IGetQuestionDetailsResponse } from '@/types/question-types';
1412

13+
import { AdminEditForm } from './admin-edit-form';
14+
1515
export const QuestionDetails = ({
1616
questionDetails,
1717
}: {
@@ -26,12 +26,7 @@ export const QuestionDetails = ({
2626
<CardTitle className='text-2xl'>
2727
{questionDetails.id}.&nbsp;{questionDetails.title}
2828
</CardTitle>
29-
{isAdmin && (
30-
<Button className='min-h-none ml-8 flex !h-6 gap-1 rounded-md px-2' size='sm'>
31-
<Pencil1Icon />
32-
<span>Edit</span>
33-
</Button>
34-
)}
29+
{isAdmin && <AdminEditForm questionDetails={questionDetails} />}
3530
</div>
3631
<div className='flex flex-wrap items-center gap-1'>
3732
<Badge

0 commit comments

Comments
 (0)