|
| 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'; |
1 | 14 | import {
|
2 | 15 | Dialog,
|
3 | 16 | DialogContent,
|
| 17 | + DialogDescription, |
4 | 18 | DialogFooter,
|
5 | 19 | DialogHeader,
|
6 | 20 | DialogTitle,
|
7 |
| - DialogTrigger, |
8 | 21 | } 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 | + }; |
9 | 85 |
|
10 |
| -export const AdminEditForm = () => { |
11 | 86 | 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 | + </> |
22 | 309 | );
|
23 | 310 | };
|
0 commit comments