first commit
This commit is contained in:
		
							
								
								
									
										235
									
								
								frontend/src/components/AboutUs/AddAboutUs.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								frontend/src/components/AboutUs/AddAboutUs.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,235 @@ | ||||
| import React, { useRef, ReactNode, useState } from 'react'; | ||||
| import { | ||||
|   Button, | ||||
|   FormControl, | ||||
|   FormErrorMessage, | ||||
|   FormLabel, | ||||
|   Input, | ||||
|   Modal, | ||||
|   ModalBody, | ||||
|   ModalCloseButton, | ||||
|   ModalContent, | ||||
|   ModalFooter, | ||||
|   ModalHeader, | ||||
|   ModalOverlay, | ||||
|   InputGroup, | ||||
|   NumberInput, | ||||
|   NumberInputField, | ||||
|   NumberInputStepper, | ||||
|   NumberIncrementStepper, | ||||
|   NumberDecrementStepper, | ||||
| } from "@chakra-ui/react" | ||||
| import { writeFileSync, createReadStream } from "fs"; | ||||
| import { useMutation, useQueryClient } from "@tanstack/react-query" | ||||
| import { type SubmitHandler, useForm, UseFormRegisterReturn } from "react-hook-form" | ||||
| import { type ApiError, type AboutUsCreate, AboutUsService } from "../../client" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { handleError } from "../../utils" | ||||
| import { EditorState, ContentState, convertToRaw } from 'draft-js'; | ||||
| import { Editor } from "react-draft-wysiwyg"; | ||||
| import draftToHtml from 'draftjs-to-html'; | ||||
| import htmlToDraft from 'html-to-draftjs'; | ||||
| import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css"; | ||||
|  | ||||
| interface AddAboutUsProps { | ||||
|   isOpen: boolean | ||||
|   onClose: () => void | ||||
| } | ||||
|  | ||||
| type FileUploadProps = { | ||||
|   register: UseFormRegisterReturn | ||||
|   accept?: string | ||||
|   multiple?: boolean | ||||
|   children?: ReactNode | ||||
| } | ||||
|  | ||||
| const FileUpload = (props: FileUploadProps) => { | ||||
|   const { register, accept, multiple, children } = props | ||||
|   const inputRef = useRef<HTMLInputElement | null>(null) | ||||
|   const { ref, ...rest } = register as { ref: (instance: HTMLInputElement | null) => void } | ||||
|  | ||||
|   const handleClick = () => inputRef.current?.click() | ||||
|  | ||||
|   return ( | ||||
|     <InputGroup onClick={handleClick}> | ||||
|       <input | ||||
|         type={'file'} | ||||
|         multiple={multiple || false} | ||||
|         hidden | ||||
|         accept={accept} | ||||
|         {...rest} | ||||
|         ref={(e) => { | ||||
|           ref(e) | ||||
|           inputRef.current = e | ||||
|         }} | ||||
|       /> | ||||
|       <> | ||||
|         {children} | ||||
|       </> | ||||
|     </InputGroup> | ||||
|   ) | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| const AddAboutUs = ({ isOpen, onClose }: AddAboutUsProps) => { | ||||
|   const queryClient = useQueryClient() | ||||
|   const showToast = useCustomToast() | ||||
|   const { | ||||
|     register, | ||||
|     handleSubmit, | ||||
|     reset, | ||||
|     formState: { errors, isSubmitting }, | ||||
|   } = useForm<AboutUsCreate>({ | ||||
|     mode: "onBlur", | ||||
|     criteriaMode: "all", | ||||
|     defaultValues: { | ||||
|       index: 0, | ||||
|       description: "", | ||||
|       image: undefined, | ||||
|     }, | ||||
|   }) | ||||
|   const [editorState, setEditorState] = useState<EditorState>(EditorState.createEmpty()); | ||||
|   const [content, setContent] = useState<string>(''); | ||||
|  | ||||
|   const validateFiles = (value: File) => { | ||||
|     if (typeof value === 'string') return true; | ||||
|  | ||||
|  | ||||
|     const fsMb = value.size / (1024 * 1024) | ||||
|     const MAX_FILE_SIZE = 10 | ||||
|     if (fsMb > MAX_FILE_SIZE) { | ||||
|       return 'Max file size 10mb' | ||||
|     } | ||||
|  | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|  | ||||
|   type FormValues = { | ||||
|     file_: FileList | ||||
|   } | ||||
|  | ||||
|  | ||||
|   const mutation = useMutation({ | ||||
|     mutationFn: (data: AboutUsCreate) => | ||||
|       AboutUsService.createAboutUs({ formData: data }), | ||||
|     onSuccess: () => { | ||||
|       showToast("Success!", "About Us created successfully.", "success") | ||||
|       reset() | ||||
|       setEditorState(EditorState.createEmpty()); | ||||
|       onClose() | ||||
|     }, | ||||
|     onError: (err: ApiError) => { | ||||
|       handleError(err, showToast) | ||||
|     }, | ||||
|     onSettled: () => { | ||||
|       queryClient.invalidateQueries({ queryKey: ["aboutUs"] }) | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const onSubmit: SubmitHandler<AboutUsCreate> = (data) => { | ||||
|     if (data.image instanceof FileList && data.image.length > 0) { | ||||
|       data.image = data.image[0] | ||||
|     } | ||||
|     mutation.mutate(data) | ||||
|     console.log(data) | ||||
|      | ||||
|   } | ||||
|  | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Modal | ||||
|         isOpen={isOpen} | ||||
|         onClose={onClose} | ||||
|         size={'xl'} | ||||
|         isCentered | ||||
|       > | ||||
|         <ModalOverlay /> | ||||
|         <ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> | ||||
|           <ModalHeader>Add About Us</ModalHeader> | ||||
|           <ModalCloseButton /> | ||||
|  | ||||
|           <ModalBody pb={30}> | ||||
|             <FormControl isRequired isInvalid={!!errors.description}> | ||||
|               <Editor | ||||
|                 editorState={editorState} | ||||
|                 wrapperClassName="wrapper-class" | ||||
|                 editorClassName="demo-editor" | ||||
|                 onEditorStateChange={newState => { | ||||
|                   setEditorState(newState); | ||||
|                   setContent(draftToHtml(convertToRaw(newState.getCurrentContent()))); | ||||
|                   reset({ | ||||
|                     description: content, | ||||
|                   }); | ||||
|  | ||||
|                 }} | ||||
|                 toolbar={{ | ||||
|                   options: ['inline', 'blockType', 'fontSize', 'list', 'textAlign', 'history', 'embedded', 'emoji', 'image'], | ||||
|                   inline: { inDropdown: true }, | ||||
|                   list: { inDropdown: true }, | ||||
|                   textAlign: { inDropdown: true }, | ||||
|                   link: { inDropdown: true }, | ||||
|                   history: { inDropdown: true }, | ||||
|                 }} | ||||
|               /> | ||||
|             </FormControl> | ||||
|             <FormControl isRequired isInvalid={!!errors.index}> | ||||
|               <FormLabel htmlFor="index">Index</FormLabel > | ||||
|               <NumberInput min={0} max={20}  > | ||||
|                 <NumberInputField {...register("index", { | ||||
|                   required: "index is required.", | ||||
|                 })} /> | ||||
|                 <NumberInputStepper> | ||||
|                   <NumberIncrementStepper /> | ||||
|                   <NumberDecrementStepper /> | ||||
|                 </NumberInputStepper> | ||||
|               </NumberInput> | ||||
|               {/* <Input | ||||
|                 id="index" | ||||
|                 {...register("index", { | ||||
|                   required: "index is required.", | ||||
|                 })} | ||||
|                 placeholder="Index" | ||||
|                 type="Number" | ||||
|               /> */} | ||||
|               {errors.index && ( | ||||
|                 <FormErrorMessage>{errors.index.message}</FormErrorMessage> | ||||
|               )} | ||||
|             </FormControl> | ||||
|  | ||||
|             <FormControl isInvalid={!!errors.image} isRequired> | ||||
|               <FormLabel>{'Image Upload'}</FormLabel> | ||||
|  | ||||
|               {/* <FileUpload | ||||
|                 accept={'image/*'} | ||||
|                 multiple={false} | ||||
|                 register={register('image', { validate: validateFiles })} | ||||
|               > | ||||
|                 <Button > | ||||
|                   Upload | ||||
|                 </Button> | ||||
|               </FileUpload> */} | ||||
|               <input type="file" {...register("image", { | ||||
|                 required: "index is required.", | ||||
|               })} /> | ||||
|               <FormErrorMessage> | ||||
|                 {errors.image && errors?.image.message} | ||||
|               </FormErrorMessage> | ||||
|             </FormControl> | ||||
|  | ||||
|           </ModalBody> | ||||
|  | ||||
|           <ModalFooter gap={3}> | ||||
|             <Button variant="primary" type="submit" isLoading={isSubmitting}> | ||||
|               Save | ||||
|             </Button> | ||||
|             <Button onClick={onClose}>Cancel</Button> | ||||
|           </ModalFooter> | ||||
|         </ModalContent> | ||||
|       </Modal> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
| export default AddAboutUs | ||||
							
								
								
									
										232
									
								
								frontend/src/components/AboutUs/EditAboutUs.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								frontend/src/components/AboutUs/EditAboutUs.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,232 @@ | ||||
| import React, { useRef, ReactNode, useState } from 'react'; | ||||
| import { | ||||
|     Button, | ||||
|     FormControl, | ||||
|     FormErrorMessage, | ||||
|     FormLabel, | ||||
|     Input, | ||||
|     Modal, | ||||
|     ModalBody, | ||||
|     ModalCloseButton, | ||||
|     ModalContent, | ||||
|     ModalFooter, | ||||
|     ModalHeader, | ||||
|     ModalOverlay, | ||||
|     InputGroup, | ||||
|     NumberInput, | ||||
|     NumberInputField, | ||||
|     NumberInputStepper, | ||||
|     NumberIncrementStepper, | ||||
|     NumberDecrementStepper, | ||||
|     Box, | ||||
|     Image, | ||||
| } from "@chakra-ui/react" | ||||
| import { writeFileSync, createReadStream } from "fs"; | ||||
| import { useMutation, useQueryClient } from "@tanstack/react-query" | ||||
| import { type SubmitHandler, useForm, UseFormRegisterReturn } from "react-hook-form" | ||||
| import { type ApiError, type AboutUsCreate, AboutUsService, AboutUsUpdate, AboutUsPublic } from "../../client" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { handleError } from "../../utils" | ||||
| import { EditorState, ContentState, convertToRaw } from 'draft-js'; | ||||
| import { Editor } from "react-draft-wysiwyg"; | ||||
| import  draftToHtml  from 'draftjs-to-html'; | ||||
| import htmlToDraft from 'html-to-draftjs'; | ||||
| import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css"; | ||||
|  | ||||
| interface EditAboutUsProps { | ||||
|     isOpen: boolean | ||||
|     onClose: () => void | ||||
|     aboutUs: AboutUsPublic | ||||
| } | ||||
|  | ||||
| type FileUploadProps = { | ||||
|     register: UseFormRegisterReturn | ||||
|     accept?: string | ||||
|     multiple?: boolean | ||||
|     children?: ReactNode | ||||
| } | ||||
|  | ||||
| const FileUpload = (props: FileUploadProps) => { | ||||
|     const { register, accept, multiple, children } = props | ||||
|     const inputRef = useRef<HTMLInputElement | null>(null) | ||||
|     const { ref, ...rest } = register as { ref: (instance: HTMLInputElement | null) => void } | ||||
|  | ||||
|     const handleClick = () => inputRef.current?.click() | ||||
|  | ||||
|     return ( | ||||
|         <InputGroup onClick={handleClick}> | ||||
|             <input | ||||
|                 type={'file'} | ||||
|                 multiple={multiple || false} | ||||
|                 hidden | ||||
|                 accept={accept} | ||||
|                 {...rest} | ||||
|                 ref={(e) => { | ||||
|                     ref(e) | ||||
|                     inputRef.current = e | ||||
|                 }} | ||||
|             /> | ||||
|             <> | ||||
|                 {children} | ||||
|             </> | ||||
|         </InputGroup> | ||||
|     ) | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| const EditAboutUs = ({ aboutUs, isOpen, onClose }: EditAboutUsProps) => { | ||||
|     const url = import.meta.env.VITE_API_URL | ||||
|     const queryClient = useQueryClient() | ||||
|     const showToast = useCustomToast() | ||||
|     const { | ||||
|         register, | ||||
|         handleSubmit, | ||||
|         reset, | ||||
|         formState: { errors, isSubmitting }, | ||||
|     } = useForm<AboutUsUpdate>({ | ||||
|         mode: "onBlur", | ||||
|         criteriaMode: "all", | ||||
|         defaultValues: { | ||||
|             index: aboutUs.index, | ||||
|             description: aboutUs.description, | ||||
|             image: undefined, | ||||
|         }, | ||||
|     }) | ||||
|     const [editorState, setEditorState] = useState<EditorState>(() => { | ||||
|         const contentBlock = htmlToDraft(aboutUs.description); | ||||
|         if (contentBlock) { | ||||
|             const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks); | ||||
|             return EditorState.createWithContent(contentState); | ||||
|         } | ||||
|         return EditorState.createEmpty(); | ||||
|     }); | ||||
|     const [content, setContent] = useState<string>(aboutUs.description); | ||||
|  | ||||
|     const validateFiles = (value: File) => { | ||||
|         if (typeof value === 'string') return true; | ||||
|  | ||||
|  | ||||
|         const fsMb = value.size / (1024 * 1024) | ||||
|         const MAX_FILE_SIZE = 10 | ||||
|         if (fsMb > MAX_FILE_SIZE) { | ||||
|             return 'Max file size 10mb' | ||||
|         } | ||||
|  | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|  | ||||
|     type FormValues = { | ||||
|         file_: FileList | ||||
|     } | ||||
|  | ||||
|  | ||||
|     const mutation = useMutation({ | ||||
|         mutationFn: (data: AboutUsUpdate) => | ||||
|             AboutUsService.updateAboutUs({ id: aboutUs.id, formData: data }), | ||||
|         onSuccess: () => { | ||||
|             showToast("Success!", "About Us update successfully.", "success") | ||||
|             reset() | ||||
|             setEditorState(EditorState.createEmpty()); | ||||
|             onClose() | ||||
|         }, | ||||
|         onError: (err: ApiError) => { | ||||
|             handleError(err, showToast) | ||||
|         }, | ||||
|         onSettled: () => { | ||||
|             queryClient.invalidateQueries({ queryKey: ["aboutUs"] }) | ||||
|         }, | ||||
|     }) | ||||
|  | ||||
|     const onSubmit: SubmitHandler<AboutUsUpdate> = (data) => { | ||||
|         if (data.image instanceof FileList && data.image.length > 0) { | ||||
|             data.image = data.image[0] | ||||
|         }else{ | ||||
|             data.image = null | ||||
|         } | ||||
|         mutation.mutate(data) | ||||
|         console.log(data) | ||||
|  | ||||
|     } | ||||
|  | ||||
|  | ||||
|     return ( | ||||
|         <> | ||||
|             <Modal | ||||
|                 isOpen={isOpen} | ||||
|                 onClose={onClose} | ||||
|                 size={'xl'} | ||||
|                 isCentered | ||||
|             > | ||||
|                 <ModalOverlay /> | ||||
|                 <ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> | ||||
|                     <ModalHeader>Edit About Us</ModalHeader> | ||||
|                     <ModalCloseButton /> | ||||
|                     <ModalBody pb={30}> | ||||
|                         <Box boxSize='auto'> | ||||
|                             <Image src={url + "/" + aboutUs.image}  /> | ||||
|                         </Box> | ||||
|                         <FormControl isRequired isInvalid={!!errors.description}> | ||||
|                             <Editor | ||||
|                                 editorState={editorState} | ||||
|                                 wrapperClassName="wrapper-class" | ||||
|                                 editorClassName="demo-editor" | ||||
|                                 onEditorStateChange={newState => { | ||||
|                                     setEditorState(newState); | ||||
|                                     const newContent = draftToHtml(convertToRaw(newState.getCurrentContent())); | ||||
|                                     setContent(newContent); | ||||
|                                     reset({ | ||||
|                                         description: newContent, | ||||
|                                     }); | ||||
|  | ||||
|                                 }} | ||||
|                                 toolbar={{ | ||||
|                                     options: ['inline', 'blockType', 'fontSize', 'list', 'textAlign', 'history', 'embedded', 'emoji', 'image'], | ||||
|                                     inline: { inDropdown: true }, | ||||
|                                     list: { inDropdown: true }, | ||||
|                                     textAlign: { inDropdown: true }, | ||||
|                                     link: { inDropdown: true }, | ||||
|                                     history: { inDropdown: true }, | ||||
|                                 }} | ||||
|                             /> | ||||
|                         </FormControl> | ||||
|                         <FormControl isRequired isInvalid={!!errors.index}> | ||||
|                             <FormLabel htmlFor="index">Index</FormLabel > | ||||
|                             <NumberInput min={0} max={20}  > | ||||
|                                 <NumberInputField {...register("index", { | ||||
|                                     required: "index is required.", | ||||
|                                 })} /> | ||||
|                                 <NumberInputStepper> | ||||
|                                     <NumberIncrementStepper /> | ||||
|                                     <NumberDecrementStepper /> | ||||
|                                 </NumberInputStepper> | ||||
|                             </NumberInput> | ||||
|                             {errors.index && ( | ||||
|                                 <FormErrorMessage>{errors.index.message}</FormErrorMessage> | ||||
|                             )} | ||||
|                         </FormControl> | ||||
|  | ||||
|                         <FormControl isInvalid={!!errors.image} isRequired> | ||||
|                             <FormLabel>{'Image Upload'}</FormLabel> | ||||
|  | ||||
|                             <input type="file" {...register("image")} /> | ||||
|                             <FormErrorMessage> | ||||
|                                 {errors.image && errors?.image.message} | ||||
|                             </FormErrorMessage> | ||||
|                         </FormControl> | ||||
|  | ||||
|                     </ModalBody> | ||||
|  | ||||
|                     <ModalFooter gap={3}> | ||||
|                         <Button variant="primary" type="submit" isLoading={isSubmitting}> | ||||
|                             Save | ||||
|                         </Button> | ||||
|                         <Button onClick={onClose}>Cancel</Button> | ||||
|                     </ModalFooter> | ||||
|                 </ModalContent> | ||||
|             </Modal> | ||||
|         </> | ||||
|     ) | ||||
| } | ||||
| export default EditAboutUs | ||||
							
								
								
									
										182
									
								
								frontend/src/components/Admin/AddUser.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								frontend/src/components/Admin/AddUser.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| import { | ||||
|   Button, | ||||
|   Checkbox, | ||||
|   Flex, | ||||
|   FormControl, | ||||
|   FormErrorMessage, | ||||
|   FormLabel, | ||||
|   Input, | ||||
|   Modal, | ||||
|   ModalBody, | ||||
|   ModalCloseButton, | ||||
|   ModalContent, | ||||
|   ModalFooter, | ||||
|   ModalHeader, | ||||
|   ModalOverlay, | ||||
| } from "@chakra-ui/react" | ||||
| import { useMutation, useQueryClient } from "@tanstack/react-query" | ||||
| import { type SubmitHandler, useForm } from "react-hook-form" | ||||
|  | ||||
| import { type UserCreate, UsersService } from "../../client" | ||||
| import type { ApiError } from "../../client/core/ApiError" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { emailPattern, handleError } from "../../utils" | ||||
|  | ||||
| interface AddUserProps { | ||||
|   isOpen: boolean | ||||
|   onClose: () => void | ||||
| } | ||||
|  | ||||
| interface UserCreateForm extends UserCreate { | ||||
|   confirm_password: string | ||||
| } | ||||
|  | ||||
| const AddUser = ({ isOpen, onClose }: AddUserProps) => { | ||||
|   const queryClient = useQueryClient() | ||||
|   const showToast = useCustomToast() | ||||
|   const { | ||||
|     register, | ||||
|     handleSubmit, | ||||
|     reset, | ||||
|     getValues, | ||||
|     formState: { errors, isSubmitting }, | ||||
|   } = useForm<UserCreateForm>({ | ||||
|     mode: "onBlur", | ||||
|     criteriaMode: "all", | ||||
|     defaultValues: { | ||||
|       email: "", | ||||
|       full_name: "", | ||||
|       password: "", | ||||
|       confirm_password: "", | ||||
|       is_superuser: false, | ||||
|       is_active: false, | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const mutation = useMutation({ | ||||
|     mutationFn: (data: UserCreate) => | ||||
|       UsersService.createUser({ requestBody: data }), | ||||
|     onSuccess: () => { | ||||
|       showToast("Success!", "User created successfully.", "success") | ||||
|       reset() | ||||
|       onClose() | ||||
|     }, | ||||
|     onError: (err: ApiError) => { | ||||
|       handleError(err, showToast) | ||||
|     }, | ||||
|     onSettled: () => { | ||||
|       queryClient.invalidateQueries({ queryKey: ["users"] }) | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const onSubmit: SubmitHandler<UserCreateForm> = (data) => { | ||||
|     mutation.mutate(data) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Modal | ||||
|         isOpen={isOpen} | ||||
|         onClose={onClose} | ||||
|         size={{ base: "sm", md: "md" }} | ||||
|         isCentered | ||||
|       > | ||||
|         <ModalOverlay /> | ||||
|         <ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> | ||||
|           <ModalHeader>Add User</ModalHeader> | ||||
|           <ModalCloseButton /> | ||||
|           <ModalBody pb={6}> | ||||
|             <FormControl isRequired isInvalid={!!errors.email}> | ||||
|               <FormLabel htmlFor="email">Email</FormLabel> | ||||
|               <Input | ||||
|                 id="email" | ||||
|                 {...register("email", { | ||||
|                   required: "Email is required", | ||||
|                   pattern: emailPattern, | ||||
|                 })} | ||||
|                 placeholder="Email" | ||||
|                 type="email" | ||||
|               /> | ||||
|               {errors.email && ( | ||||
|                 <FormErrorMessage>{errors.email.message}</FormErrorMessage> | ||||
|               )} | ||||
|             </FormControl> | ||||
|             <FormControl mt={4} isInvalid={!!errors.full_name}> | ||||
|               <FormLabel htmlFor="name">Full name</FormLabel> | ||||
|               <Input | ||||
|                 id="name" | ||||
|                 {...register("full_name")} | ||||
|                 placeholder="Full name" | ||||
|                 type="text" | ||||
|               /> | ||||
|               {errors.full_name && ( | ||||
|                 <FormErrorMessage>{errors.full_name.message}</FormErrorMessage> | ||||
|               )} | ||||
|             </FormControl> | ||||
|             <FormControl mt={4} isRequired isInvalid={!!errors.password}> | ||||
|               <FormLabel htmlFor="password">Set Password</FormLabel> | ||||
|               <Input | ||||
|                 id="password" | ||||
|                 {...register("password", { | ||||
|                   required: "Password is required", | ||||
|                   minLength: { | ||||
|                     value: 8, | ||||
|                     message: "Password must be at least 8 characters", | ||||
|                   }, | ||||
|                 })} | ||||
|                 placeholder="Password" | ||||
|                 type="password" | ||||
|               /> | ||||
|               {errors.password && ( | ||||
|                 <FormErrorMessage>{errors.password.message}</FormErrorMessage> | ||||
|               )} | ||||
|             </FormControl> | ||||
|             <FormControl | ||||
|               mt={4} | ||||
|               isRequired | ||||
|               isInvalid={!!errors.confirm_password} | ||||
|             > | ||||
|               <FormLabel htmlFor="confirm_password">Confirm Password</FormLabel> | ||||
|               <Input | ||||
|                 id="confirm_password" | ||||
|                 {...register("confirm_password", { | ||||
|                   required: "Please confirm your password", | ||||
|                   validate: (value) => | ||||
|                     value === getValues().password || | ||||
|                     "The passwords do not match", | ||||
|                 })} | ||||
|                 placeholder="Password" | ||||
|                 type="password" | ||||
|               /> | ||||
|               {errors.confirm_password && ( | ||||
|                 <FormErrorMessage> | ||||
|                   {errors.confirm_password.message} | ||||
|                 </FormErrorMessage> | ||||
|               )} | ||||
|             </FormControl> | ||||
|             <Flex mt={4}> | ||||
|               <FormControl> | ||||
|                 <Checkbox {...register("is_superuser")} colorScheme="teal"> | ||||
|                   Is superuser? | ||||
|                 </Checkbox> | ||||
|               </FormControl> | ||||
|               <FormControl> | ||||
|                 <Checkbox {...register("is_active")} colorScheme="teal"> | ||||
|                   Is active? | ||||
|                 </Checkbox> | ||||
|               </FormControl> | ||||
|             </Flex> | ||||
|           </ModalBody> | ||||
|           <ModalFooter gap={3}> | ||||
|             <Button variant="primary" type="submit" isLoading={isSubmitting}> | ||||
|               Save | ||||
|             </Button> | ||||
|             <Button onClick={onClose}>Cancel</Button> | ||||
|           </ModalFooter> | ||||
|         </ModalContent> | ||||
|       </Modal> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default AddUser | ||||
							
								
								
									
										180
									
								
								frontend/src/components/Admin/EditUser.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								frontend/src/components/Admin/EditUser.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| import { | ||||
|   Button, | ||||
|   Checkbox, | ||||
|   Flex, | ||||
|   FormControl, | ||||
|   FormErrorMessage, | ||||
|   FormLabel, | ||||
|   Input, | ||||
|   Modal, | ||||
|   ModalBody, | ||||
|   ModalCloseButton, | ||||
|   ModalContent, | ||||
|   ModalFooter, | ||||
|   ModalHeader, | ||||
|   ModalOverlay, | ||||
| } from "@chakra-ui/react" | ||||
| import { useMutation, useQueryClient } from "@tanstack/react-query" | ||||
| import { type SubmitHandler, useForm } from "react-hook-form" | ||||
|  | ||||
| import { | ||||
|   type ApiError, | ||||
|   type UserPublic, | ||||
|   type UserUpdate, | ||||
|   UsersService, | ||||
| } from "../../client" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { emailPattern, handleError } from "../../utils" | ||||
|  | ||||
| interface EditUserProps { | ||||
|   user: UserPublic | ||||
|   isOpen: boolean | ||||
|   onClose: () => void | ||||
| } | ||||
|  | ||||
| interface UserUpdateForm extends UserUpdate { | ||||
|   confirm_password: string | ||||
| } | ||||
|  | ||||
| const EditUser = ({ user, isOpen, onClose }: EditUserProps) => { | ||||
|   const queryClient = useQueryClient() | ||||
|   const showToast = useCustomToast() | ||||
|  | ||||
|   const { | ||||
|     register, | ||||
|     handleSubmit, | ||||
|     reset, | ||||
|     getValues, | ||||
|     formState: { errors, isSubmitting, isDirty }, | ||||
|   } = useForm<UserUpdateForm>({ | ||||
|     mode: "onBlur", | ||||
|     criteriaMode: "all", | ||||
|     defaultValues: user, | ||||
|   }) | ||||
|  | ||||
|   const mutation = useMutation({ | ||||
|     mutationFn: (data: UserUpdateForm) => | ||||
|       UsersService.updateUser({ userId: user.id, requestBody: data }), | ||||
|     onSuccess: () => { | ||||
|       showToast("Success!", "User updated successfully.", "success") | ||||
|       onClose() | ||||
|     }, | ||||
|     onError: (err: ApiError) => { | ||||
|       handleError(err, showToast) | ||||
|     }, | ||||
|     onSettled: () => { | ||||
|       queryClient.invalidateQueries({ queryKey: ["users"] }) | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const onSubmit: SubmitHandler<UserUpdateForm> = async (data) => { | ||||
|     if (data.password === "") { | ||||
|       data.password = undefined | ||||
|     } | ||||
|     mutation.mutate(data) | ||||
|   } | ||||
|  | ||||
|   const onCancel = () => { | ||||
|     reset() | ||||
|     onClose() | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Modal | ||||
|         isOpen={isOpen} | ||||
|         onClose={onClose} | ||||
|         size={{ base: "sm", md: "md" }} | ||||
|         isCentered | ||||
|       > | ||||
|         <ModalOverlay /> | ||||
|         <ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> | ||||
|           <ModalHeader>Edit User</ModalHeader> | ||||
|           <ModalCloseButton /> | ||||
|           <ModalBody pb={6}> | ||||
|             <FormControl isInvalid={!!errors.email}> | ||||
|               <FormLabel htmlFor="email">Email</FormLabel> | ||||
|               <Input | ||||
|                 id="email" | ||||
|                 {...register("email", { | ||||
|                   required: "Email is required", | ||||
|                   pattern: emailPattern, | ||||
|                 })} | ||||
|                 placeholder="Email" | ||||
|                 type="email" | ||||
|               /> | ||||
|               {errors.email && ( | ||||
|                 <FormErrorMessage>{errors.email.message}</FormErrorMessage> | ||||
|               )} | ||||
|             </FormControl> | ||||
|             <FormControl mt={4}> | ||||
|               <FormLabel htmlFor="name">Full name</FormLabel> | ||||
|               <Input id="name" {...register("full_name")} type="text" /> | ||||
|             </FormControl> | ||||
|             <FormControl mt={4} isInvalid={!!errors.password}> | ||||
|               <FormLabel htmlFor="password">Set Password</FormLabel> | ||||
|               <Input | ||||
|                 id="password" | ||||
|                 {...register("password", { | ||||
|                   minLength: { | ||||
|                     value: 8, | ||||
|                     message: "Password must be at least 8 characters", | ||||
|                   }, | ||||
|                 })} | ||||
|                 placeholder="Password" | ||||
|                 type="password" | ||||
|               /> | ||||
|               {errors.password && ( | ||||
|                 <FormErrorMessage>{errors.password.message}</FormErrorMessage> | ||||
|               )} | ||||
|             </FormControl> | ||||
|             <FormControl mt={4} isInvalid={!!errors.confirm_password}> | ||||
|               <FormLabel htmlFor="confirm_password">Confirm Password</FormLabel> | ||||
|               <Input | ||||
|                 id="confirm_password" | ||||
|                 {...register("confirm_password", { | ||||
|                   validate: (value) => | ||||
|                     value === getValues().password || | ||||
|                     "The passwords do not match", | ||||
|                 })} | ||||
|                 placeholder="Password" | ||||
|                 type="password" | ||||
|               /> | ||||
|               {errors.confirm_password && ( | ||||
|                 <FormErrorMessage> | ||||
|                   {errors.confirm_password.message} | ||||
|                 </FormErrorMessage> | ||||
|               )} | ||||
|             </FormControl> | ||||
|             <Flex> | ||||
|               <FormControl mt={4}> | ||||
|                 <Checkbox {...register("is_superuser")} colorScheme="teal"> | ||||
|                   Is superuser? | ||||
|                 </Checkbox> | ||||
|               </FormControl> | ||||
|               <FormControl mt={4}> | ||||
|                 <Checkbox {...register("is_active")} colorScheme="teal"> | ||||
|                   Is active? | ||||
|                 </Checkbox> | ||||
|               </FormControl> | ||||
|             </Flex> | ||||
|           </ModalBody> | ||||
|  | ||||
|           <ModalFooter gap={3}> | ||||
|             <Button | ||||
|               variant="primary" | ||||
|               type="submit" | ||||
|               isLoading={isSubmitting} | ||||
|               isDisabled={!isDirty} | ||||
|             > | ||||
|               Save | ||||
|             </Button> | ||||
|             <Button onClick={onCancel}>Cancel</Button> | ||||
|           </ModalFooter> | ||||
|         </ModalContent> | ||||
|       </Modal> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default EditUser | ||||
							
								
								
									
										133
									
								
								frontend/src/components/Common/ActionsMenu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								frontend/src/components/Common/ActionsMenu.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| import { | ||||
|   Button, | ||||
|   Menu, | ||||
|   MenuButton, | ||||
|   MenuItem, | ||||
|   MenuList, | ||||
|   useDisclosure, | ||||
| } from "@chakra-ui/react" | ||||
| import { BsThreeDotsVertical } from "react-icons/bs" | ||||
| import { FiEdit, FiTrash } from "react-icons/fi" | ||||
| import { Link } from "@tanstack/react-router" | ||||
| import type { ItemPublic, UserPublic, AboutUsPublic, AboutUsUpdate, CoursePublic, ImagePublic, SchedulePublic } from "../../client" | ||||
| import EditUser from "../Admin/EditUser" | ||||
| import EditItem from "../Items/EditItem" | ||||
| import EditCourseImage from "../CourseImage/editCourseImage" | ||||
| import EditAboutUs from "../AboutUs/EditAboutUs" | ||||
| import EditSechedule from "../Courses/EditSechedule" | ||||
| import Delete from "./DeleteAlert" | ||||
|  | ||||
| interface ActionsMenuProps { | ||||
|   type: string | ||||
|   value: ItemPublic | UserPublic | AboutUsPublic | CoursePublic | ImagePublic | SchedulePublic | ||||
|   disabled?: boolean | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| const ActionsMenu = ({ type, value, disabled }: ActionsMenuProps) => { | ||||
|   const editUserModal = useDisclosure() | ||||
|   const deleteModal = useDisclosure() | ||||
|  | ||||
|   const renderEditModel = (type: string) => { | ||||
|     switch (type) { | ||||
|       case 'User': | ||||
|         return ( | ||||
|           <EditUser | ||||
|             user={value as UserPublic} | ||||
|             isOpen={editUserModal.isOpen} | ||||
|             onClose={editUserModal.onClose} | ||||
|           /> | ||||
|         ) | ||||
|       case 'Item': | ||||
|         return ( | ||||
|           <EditItem | ||||
|             item={value as ItemPublic} | ||||
|             isOpen={editUserModal.isOpen} | ||||
|             onClose={editUserModal.onClose} | ||||
|           /> | ||||
|         ) | ||||
|       case 'AboutUs': | ||||
|         return ( | ||||
|           <EditAboutUs | ||||
|             aboutUs={value as AboutUsPublic} | ||||
|             isOpen={editUserModal.isOpen} | ||||
|             onClose={editUserModal.onClose} | ||||
|           /> | ||||
|         ) | ||||
|       case 'Image': | ||||
|         return ( | ||||
|           <EditCourseImage | ||||
|             type='Image' | ||||
|             imageDetails={value as ImagePublic} | ||||
|             isOpen={editUserModal.isOpen} | ||||
|             onClose={editUserModal.onClose} | ||||
|           /> | ||||
|         ) | ||||
|       case 'Info_Image': | ||||
|         return ( | ||||
|           <EditCourseImage | ||||
|             type='Info_Image' | ||||
|             imageDetails={value as ImagePublic} | ||||
|             isOpen={editUserModal.isOpen} | ||||
|             onClose={editUserModal.onClose} | ||||
|           /> | ||||
|         ) | ||||
|  | ||||
|       case "Sechedule": | ||||
|         return ( | ||||
|           <EditSechedule | ||||
|             type="Sechedule" | ||||
|             sechedule={value as SchedulePublic} | ||||
|             isOpen={editUserModal.isOpen} | ||||
|             onClose={editUserModal.onClose} | ||||
|           /> | ||||
|         ) | ||||
|       default: | ||||
|         return null | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Menu> | ||||
|         <MenuButton | ||||
|           isDisabled={disabled} | ||||
|           as={Button} | ||||
|           rightIcon={<BsThreeDotsVertical />} | ||||
|           variant="unstyled" | ||||
|         /> | ||||
|         <MenuList> | ||||
|           {type === 'Message' ? (<></>) : (<MenuItem | ||||
|             onClick={editUserModal.onOpen} | ||||
|             icon={<FiEdit fontSize="16px" />} | ||||
|           > | ||||
|             Edit {type} | ||||
|           </MenuItem>) | ||||
|           } | ||||
|  | ||||
|           <MenuItem | ||||
|             onClick={deleteModal.onOpen} | ||||
|             icon={<FiTrash fontSize="16px" />} | ||||
|             color="ui.danger" | ||||
|           > | ||||
|             Delete {type} | ||||
|           </MenuItem> | ||||
|         </MenuList> | ||||
|         { | ||||
|           renderEditModel(type) | ||||
|         } | ||||
|         <Delete | ||||
|           type={type} | ||||
|           id={value.id} | ||||
|           isOpen={deleteModal.isOpen} | ||||
|           onClose={deleteModal.onClose} | ||||
|         /> | ||||
|       </Menu> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
| export default ActionsMenu | ||||
							
								
								
									
										149
									
								
								frontend/src/components/Common/DeleteAlert.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								frontend/src/components/Common/DeleteAlert.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| import { | ||||
|   AlertDialog, | ||||
|   AlertDialogBody, | ||||
|   AlertDialogContent, | ||||
|   AlertDialogFooter, | ||||
|   AlertDialogHeader, | ||||
|   AlertDialogOverlay, | ||||
|   Button, | ||||
| } from "@chakra-ui/react" | ||||
| import { useMutation, useQueryClient } from "@tanstack/react-query" | ||||
| import React from "react" | ||||
| import { useForm } from "react-hook-form" | ||||
|  | ||||
| import { ItemsService, UsersService, ClientMessagesService, AboutUsService, CoursesService, ImageService, Info_imageService,secheduleService } from "../../client" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
|  | ||||
| interface DeleteProps { | ||||
|   type: string | ||||
|   id: string | ||||
|   isOpen: boolean | ||||
|   onClose: () => void | ||||
| } | ||||
|  | ||||
| const Delete = ({ type, id, isOpen, onClose }: DeleteProps) => { | ||||
|   const queryClient = useQueryClient() | ||||
|   const showToast = useCustomToast() | ||||
|   const cancelRef = React.useRef<HTMLButtonElement | null>(null) | ||||
|   const { | ||||
|     handleSubmit, | ||||
|     formState: { isSubmitting }, | ||||
|   } = useForm() | ||||
|  | ||||
|   const deleteEntity = async (id: string) => { | ||||
|     if (type === "Item") { | ||||
|       await ItemsService.deleteItem({ id: id }) | ||||
|     } else if (type === "User") { | ||||
|       await UsersService.deleteUser({ userId: id }) | ||||
|     } else if (type === "Message") { | ||||
|       await ClientMessagesService.deleteMessage({ id: id }) | ||||
|     } else if (type === "AboutUs") { | ||||
|       await AboutUsService.deleteAboutUs({ id: id }) | ||||
|     } else if (type === "Course") { | ||||
|       await CoursesService.deleteCourse({ id: id }) | ||||
|     } else if (type === "Image") { | ||||
|       await ImageService.deleteImage({ id: id }) | ||||
|     } else if (type === "Info_Image") { | ||||
|       await Info_imageService.deleteImage({ id: id }) | ||||
|     } else if (type === "Sechedule") { | ||||
|       await secheduleService.deleteSechedule({ id: id }) | ||||
|     } | ||||
|     else { | ||||
|       throw new Error(`Unexpected type: ${type}`) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const mutation = useMutation({ | ||||
|     mutationFn: deleteEntity, | ||||
|     onSuccess: (data) => { | ||||
|       showToast( | ||||
|         "Success", | ||||
|         `The ${type.toLowerCase()} was deleted successfully.`, | ||||
|         "success", | ||||
|       ) | ||||
|       console.log(data) | ||||
|       //queryClient.setQueryData(['course'], data) | ||||
|       onClose() | ||||
|     }, | ||||
|     onError: () => { | ||||
|       showToast( | ||||
|         "An error occurred.", | ||||
|         `An error occurred while deleting the ${type.toLowerCase()}.`, | ||||
|         "error", | ||||
|       ) | ||||
|     }, | ||||
|     onSettled: () => { | ||||
|       var key = '' | ||||
|       if (type === "Item") { | ||||
|         key = "items" | ||||
|       } else if (type === "User") { | ||||
|         key = "users" | ||||
|       } else if (type === "Message") { | ||||
|         key = "messages" | ||||
|       } else if (type === "AboutUs") { | ||||
|         key = "aboutUs" | ||||
|       } else if (type === "Course") { | ||||
|         key = "courses" | ||||
|       } else if (type === "Image") { | ||||
|         key = "course" | ||||
|       } else if(type === "Info_Image"){ | ||||
|         key = "course" | ||||
|       } else if (type === "Sechedule") { | ||||
|         key = "course" | ||||
|       } | ||||
|       else { | ||||
|         throw new Error(`Unexpected type: ${type}`) | ||||
|       } | ||||
|       queryClient.invalidateQueries({ | ||||
|         queryKey: [key], | ||||
|       }) | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const onSubmit = async () => { | ||||
|     mutation.mutate(id) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <AlertDialog | ||||
|         isOpen={isOpen} | ||||
|         onClose={onClose} | ||||
|         leastDestructiveRef={cancelRef} | ||||
|         size={{ base: "sm", md: "md" }} | ||||
|         isCentered | ||||
|       > | ||||
|         <AlertDialogOverlay> | ||||
|           <AlertDialogContent as="form" onSubmit={handleSubmit(onSubmit)}> | ||||
|             <AlertDialogHeader>Delete {type}</AlertDialogHeader> | ||||
|  | ||||
|             <AlertDialogBody> | ||||
|               {type === "User" && ( | ||||
|                 <span> | ||||
|                   All items associated with this user will also be{" "} | ||||
|                   <strong>permantly deleted. </strong> | ||||
|                 </span> | ||||
|               )} | ||||
|               Are you sure? You will not be able to undo this action. | ||||
|             </AlertDialogBody> | ||||
|  | ||||
|             <AlertDialogFooter gap={3}> | ||||
|               <Button variant="danger" type="submit" isLoading={isSubmitting}> | ||||
|                 Delete | ||||
|               </Button> | ||||
|               <Button | ||||
|                 ref={cancelRef} | ||||
|                 onClick={onClose} | ||||
|                 isDisabled={isSubmitting} | ||||
|               > | ||||
|                 Cancel | ||||
|               </Button> | ||||
|             </AlertDialogFooter> | ||||
|           </AlertDialogContent> | ||||
|         </AlertDialogOverlay> | ||||
|       </AlertDialog> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default Delete | ||||
							
								
								
									
										46
									
								
								frontend/src/components/Common/Navbar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								frontend/src/components/Common/Navbar.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| import type { ComponentType, ElementType } from "react" | ||||
|  | ||||
| import { Button, Flex, Icon, useDisclosure } from "@chakra-ui/react" | ||||
| import { FaPlus } from "react-icons/fa" | ||||
|  | ||||
| interface NavbarProps { | ||||
|   type: string | ||||
|   addModalAs: ComponentType | ElementType | ||||
|   value?: string | ||||
| } | ||||
|  | ||||
| const Navbar = ({ type, addModalAs, value }: NavbarProps) => { | ||||
|   const addModal = useDisclosure() | ||||
|  | ||||
|   const AddModal = addModalAs | ||||
|   return ( | ||||
|     <> | ||||
|       <Flex py={8} gap={4}> | ||||
|         {/* TODO: Complete search functionality */} | ||||
|         {/* <InputGroup w={{ base: '100%', md: 'auto' }}> | ||||
|                     <InputLeftElement pointerEvents='none'> | ||||
|                         <Icon as={FaSearch} color='ui.dim' /> | ||||
|                     </InputLeftElement> | ||||
|                     <Input type='text' placeholder='Search' fontSize={{ base: 'sm', md: 'inherit' }} borderRadius='8px' /> | ||||
|                 </InputGroup> */} | ||||
|         <Button | ||||
|           variant="primary" | ||||
|           gap={1} | ||||
|           fontSize={{ base: "sm", md: "inherit" }} | ||||
|           onClick={addModal.onOpen} | ||||
|         > | ||||
|           <Icon as={FaPlus} /> Add {type} | ||||
|         </Button> | ||||
|         {type === "Sechedule" ? ( | ||||
|           <AddModal isOpen={addModal.isOpen} onClose={addModal.onClose} courseId={value} /> | ||||
|         ) : ( | ||||
|           <AddModal isOpen={addModal.isOpen} onClose={addModal.onClose} /> | ||||
|         ) | ||||
|         } | ||||
|  | ||||
|       </Flex> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default Navbar | ||||
							
								
								
									
										41
									
								
								frontend/src/components/Common/NotFound.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								frontend/src/components/Common/NotFound.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import { Button, Container, Text } from "@chakra-ui/react" | ||||
| import { Link } from "@tanstack/react-router" | ||||
|  | ||||
| const NotFound = () => { | ||||
|   return ( | ||||
|     <> | ||||
|       <Container | ||||
|         h="100vh" | ||||
|         alignItems="stretch" | ||||
|         justifyContent="center" | ||||
|         textAlign="center" | ||||
|         maxW="sm" | ||||
|         centerContent | ||||
|       > | ||||
|         <Text | ||||
|           fontSize="8xl" | ||||
|           color="ui.main" | ||||
|           fontWeight="bold" | ||||
|           lineHeight="1" | ||||
|           mb={4} | ||||
|         > | ||||
|           404 | ||||
|         </Text> | ||||
|         <Text fontSize="md">Oops!</Text> | ||||
|         <Text fontSize="md">Page not found.</Text> | ||||
|         <Button | ||||
|           as={Link} | ||||
|           to="/" | ||||
|           color="ui.main" | ||||
|           borderColor="ui.main" | ||||
|           variant="outline" | ||||
|           mt={4} | ||||
|         > | ||||
|           Go back | ||||
|         </Button> | ||||
|       </Container> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default NotFound | ||||
							
								
								
									
										116
									
								
								frontend/src/components/Common/Sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								frontend/src/components/Common/Sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| import { | ||||
|   Box, | ||||
|   Drawer, | ||||
|   DrawerBody, | ||||
|   DrawerCloseButton, | ||||
|   DrawerContent, | ||||
|   DrawerOverlay, | ||||
|   Flex, | ||||
|   IconButton, | ||||
|   Image, | ||||
|   Text, | ||||
|   useColorModeValue, | ||||
|   useDisclosure, | ||||
| } from "@chakra-ui/react" | ||||
| import { useQueryClient } from "@tanstack/react-query" | ||||
| import { FiLogOut, FiMenu } from "react-icons/fi" | ||||
|  | ||||
| import Logo from "/assets/images/logo.png" | ||||
| import type { UserPublic } from "../../client" | ||||
| import useAuth from "../../hooks/useAuth" | ||||
| import SidebarItems from "./SidebarItems" | ||||
|  | ||||
| const Sidebar = () => { | ||||
|   const queryClient = useQueryClient() | ||||
|   const bgColor = useColorModeValue("ui.light", "ui.dark") | ||||
|   const textColor = useColorModeValue("ui.dark", "ui.light") | ||||
|   const secBgColor = useColorModeValue("ui.secondary", "ui.darkSlate") | ||||
|   const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"]) | ||||
|   const { isOpen, onOpen, onClose } = useDisclosure() | ||||
|   const { logout } = useAuth() | ||||
|  | ||||
|   const handleLogout = async () => { | ||||
|     logout() | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {/* Mobile */} | ||||
|       <IconButton | ||||
|         onClick={onOpen} | ||||
|         display={{ base: "flex", md: "none" }} | ||||
|         aria-label="Open Menu" | ||||
|         position="absolute" | ||||
|         fontSize="20px" | ||||
|         m={4} | ||||
|         icon={<FiMenu />} | ||||
|       /> | ||||
|       <Drawer isOpen={isOpen} placement="left" onClose={onClose}> | ||||
|         <DrawerOverlay /> | ||||
|         <DrawerContent maxW="250px"> | ||||
|           <DrawerCloseButton /> | ||||
|           <DrawerBody py={8}> | ||||
|             <Flex flexDir="column" justify="space-between"> | ||||
|               <Box> | ||||
|                 <Image src={Logo} alt="logo" p={8} /> | ||||
|                 <SidebarItems onClose={onClose} /> | ||||
|                 <Flex | ||||
|                   as="button" | ||||
|                   onClick={handleLogout} | ||||
|                   p={2} | ||||
|                   color="ui.danger" | ||||
|                   fontWeight="bold" | ||||
|                   alignItems="center" | ||||
|                 > | ||||
|                   <FiLogOut /> | ||||
|                   <Text ml={2}>Log out</Text> | ||||
|                 </Flex> | ||||
|               </Box> | ||||
|               {currentUser?.email && ( | ||||
|                 <Text color={textColor} noOfLines={2} fontSize="sm" p={2}> | ||||
|                   Logged in as: {currentUser.email} | ||||
|                 </Text> | ||||
|               )} | ||||
|             </Flex> | ||||
|           </DrawerBody> | ||||
|         </DrawerContent> | ||||
|       </Drawer> | ||||
|  | ||||
|       {/* Desktop */} | ||||
|       <Box | ||||
|         bg={bgColor} | ||||
|         p={3} | ||||
|         h="100vh" | ||||
|         position="sticky" | ||||
|         top="0" | ||||
|         display={{ base: "none", md: "flex" }} | ||||
|       > | ||||
|         <Flex | ||||
|           flexDir="column" | ||||
|           justify="space-between" | ||||
|           bg={secBgColor} | ||||
|           p={4} | ||||
|           borderRadius={12} | ||||
|         > | ||||
|           <Box> | ||||
|             <Image src={Logo} alt="Logo" w="180px" maxW="2xs" p={6} /> | ||||
|             <SidebarItems /> | ||||
|           </Box> | ||||
|           {currentUser?.email && ( | ||||
|             <Text | ||||
|               color={textColor} | ||||
|               noOfLines={2} | ||||
|               fontSize="sm" | ||||
|               p={2} | ||||
|               maxW="180px" | ||||
|             > | ||||
|               Logged in as: {currentUser.email} | ||||
|             </Text> | ||||
|           )} | ||||
|         </Flex> | ||||
|       </Box> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default Sidebar | ||||
							
								
								
									
										60
									
								
								frontend/src/components/Common/SidebarItems.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								frontend/src/components/Common/SidebarItems.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| import { Box, Flex, Icon, Text, useColorModeValue } from "@chakra-ui/react" | ||||
| import { useQueryClient } from "@tanstack/react-query" | ||||
| import { Link } from "@tanstack/react-router" | ||||
| import { FiBriefcase, FiHome, FiSettings, FiUsers, FiMessageSquare, FiAlignLeft, FiBook } from "react-icons/fi" | ||||
|  | ||||
| import type { UserPublic } from "../../client" | ||||
|  | ||||
| const items = [ | ||||
|   { icon: FiHome, title: "Dashboard", path: "/" }, | ||||
|   { icon: FiBriefcase, title: "Items", path: "/items" }, | ||||
|   { icon: FiBook, title: "Courses", path: "/Courses/courses" },  | ||||
|   { icon: FiMessageSquare, title: "Messages", path: "/clientMessages" }, | ||||
|   { icon: FiAlignLeft, title: "About Us", path: "/aboutUs" }, | ||||
|   { icon: FiSettings, title: "User Settings", path: "/settings" }, | ||||
|   { icon: FiSettings, title: "Web Settings", path: "/webSetting" }, | ||||
| ] | ||||
|  | ||||
| interface SidebarItemsProps { | ||||
|   onClose?: () => void | ||||
| } | ||||
|  | ||||
| const SidebarItems = ({ onClose }: SidebarItemsProps) => { | ||||
|   const queryClient = useQueryClient() | ||||
|   const textColor = useColorModeValue("ui.main", "ui.light") | ||||
|   const bgActive = useColorModeValue("#E2E8F0", "#4A5568") | ||||
|   const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"]) | ||||
|  | ||||
|   const finalItems = currentUser?.is_superuser | ||||
|     ? [...items, { icon: FiUsers, title: "Admin", path: "/admin" }] | ||||
|     : items | ||||
|  | ||||
|   const listItems = finalItems.map(({ icon, title, path }) => ( | ||||
|     <Flex | ||||
|       as={Link} | ||||
|       to={path} | ||||
|       w="100%" | ||||
|       p={2} | ||||
|       key={title} | ||||
|       activeProps={{ | ||||
|         style: { | ||||
|           background: bgActive, | ||||
|           borderRadius: "12px", | ||||
|         }, | ||||
|       }} | ||||
|       color={textColor} | ||||
|       onClick={onClose} | ||||
|     > | ||||
|       <Icon as={icon} alignSelf="center" /> | ||||
|       <Text ml={2}>{title}</Text> | ||||
|     </Flex> | ||||
|   )) | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Box>{listItems}</Box> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default SidebarItems | ||||
							
								
								
									
										59
									
								
								frontend/src/components/Common/UserMenu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								frontend/src/components/Common/UserMenu.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| import { | ||||
|   Box, | ||||
|   IconButton, | ||||
|   Menu, | ||||
|   MenuButton, | ||||
|   MenuItem, | ||||
|   MenuList, | ||||
| } from "@chakra-ui/react" | ||||
| import { Link } from "@tanstack/react-router" | ||||
| import { FaUserAstronaut } from "react-icons/fa" | ||||
| import { FiLogOut, FiUser } from "react-icons/fi" | ||||
|  | ||||
| import useAuth from "../../hooks/useAuth" | ||||
|  | ||||
| const UserMenu = () => { | ||||
|   const { logout } = useAuth() | ||||
|  | ||||
|   const handleLogout = async () => { | ||||
|     logout() | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {/* Desktop */} | ||||
|       <Box | ||||
|         display={{ base: "none", md: "block" }} | ||||
|         position="fixed" | ||||
|         top={4} | ||||
|         right={4} | ||||
|       > | ||||
|         <Menu> | ||||
|           <MenuButton | ||||
|             as={IconButton} | ||||
|             aria-label="Options" | ||||
|             icon={<FaUserAstronaut color="white" fontSize="18px" />} | ||||
|             bg="ui.main" | ||||
|             isRound | ||||
|             data-testid="user-menu" | ||||
|           /> | ||||
|           <MenuList> | ||||
|             <MenuItem icon={<FiUser fontSize="18px" />} as={Link} to="settings"> | ||||
|               My profile | ||||
|             </MenuItem> | ||||
|             <MenuItem | ||||
|               icon={<FiLogOut fontSize="18px" />} | ||||
|               onClick={handleLogout} | ||||
|               color="ui.danger" | ||||
|               fontWeight="bold" | ||||
|             > | ||||
|               Log out | ||||
|             </MenuItem> | ||||
|           </MenuList> | ||||
|         </Menu> | ||||
|       </Box> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default UserMenu | ||||
							
								
								
									
										124
									
								
								frontend/src/components/CourseImage/editCourseImage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								frontend/src/components/CourseImage/editCourseImage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| import React, { useRef, ReactNode, useState } from 'react'; | ||||
| import { | ||||
|     Button, | ||||
|     FormControl, | ||||
|     FormErrorMessage, | ||||
|     FormLabel, | ||||
|     Input, | ||||
|     Modal, | ||||
|     ModalBody, | ||||
|     ModalCloseButton, | ||||
|     ModalContent, | ||||
|     ModalFooter, | ||||
|     ModalHeader, | ||||
|     ModalOverlay, | ||||
|     InputGroup, | ||||
|     NumberInput, | ||||
|     NumberInputField, | ||||
|     NumberInputStepper, | ||||
|     NumberIncrementStepper, | ||||
|     NumberDecrementStepper, | ||||
| } from "@chakra-ui/react" | ||||
| import { useMutation, useQueryClient } from "@tanstack/react-query" | ||||
| import { type SubmitHandler, useForm, UseFormRegisterReturn } from "react-hook-form" | ||||
| import { type ApiError, ImageService, Info_imageService, ImageUpdate, ImagePublic } from "../../client" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { handleError } from "../../utils" | ||||
| import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css"; | ||||
|  | ||||
| interface EditCourseImageProps { | ||||
|     type: string | ||||
|     imageDetails: ImagePublic | ||||
|     isOpen: boolean | ||||
|     onClose: () => void | ||||
| } | ||||
|  | ||||
| const EditCourseImage = ({ type, imageDetails, isOpen, onClose }: EditCourseImageProps) => { | ||||
|     const queryClient = useQueryClient() | ||||
|     const showToast = useCustomToast() | ||||
|     const { | ||||
|         register, | ||||
|         handleSubmit, | ||||
|         reset, | ||||
|         formState: { errors, isSubmitting }, | ||||
|     } = useForm<ImageUpdate>({ | ||||
|         mode: "onBlur", | ||||
|         criteriaMode: "all", | ||||
|         defaultValues: { | ||||
|             index: imageDetails.index, | ||||
|         }, | ||||
|     }) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     const mutation = useMutation({ | ||||
|         mutationFn: (data: ImageUpdate) => | ||||
|             type === 'Image' ? | ||||
|                 ImageService.updateImage({ index: data.index, id: imageDetails.id }) : | ||||
|                 Info_imageService.updateImage({ index: data.index, id: imageDetails.id }), | ||||
|         onSuccess: (data) => { | ||||
|             console.log(data) | ||||
|             queryClient.setQueryData(['course'], data) | ||||
|             reset() | ||||
|             onClose() | ||||
|         }, | ||||
|         onError: (err: ApiError) => { | ||||
|             handleError(err, showToast) | ||||
|         }, | ||||
|         onSettled: () => { | ||||
|             queryClient.invalidateQueries({ queryKey: ["courses", "course"] }) | ||||
|         }, | ||||
|     }) | ||||
|  | ||||
|     const onSubmit: SubmitHandler<ImageUpdate> = (data) => { | ||||
|         // data.index = Number(data.index) | ||||
|         mutation.mutate(data) | ||||
|         console.log(data) | ||||
|     } | ||||
|  | ||||
|  | ||||
|     return ( | ||||
|         <> | ||||
|             <Modal | ||||
|                 isOpen={isOpen} | ||||
|                 onClose={onClose} | ||||
|                 size={'xl'} | ||||
|                 isCentered | ||||
|             > | ||||
|                 <ModalOverlay /> | ||||
|                 <ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> | ||||
|                     <ModalHeader>{type === 'Image' ? "Edit Image" : "Edit Info Image"}</ModalHeader> | ||||
|                     <ModalCloseButton /> | ||||
|  | ||||
|                     <ModalBody pb={30}> | ||||
|  | ||||
|                         <FormControl isRequired isInvalid={!!errors.index}> | ||||
|                             <FormLabel htmlFor="index">Index</FormLabel > | ||||
|                             <NumberInput min={0} max={20}  > | ||||
|                                 <NumberInputField {...register("index", { | ||||
|                                     required: "index is required.", | ||||
|                                 })} /> | ||||
|                                 <NumberInputStepper> | ||||
|                                     <NumberIncrementStepper /> | ||||
|                                     <NumberDecrementStepper /> | ||||
|                                 </NumberInputStepper> | ||||
|                             </NumberInput> | ||||
|  | ||||
|  | ||||
|                         </FormControl> | ||||
|  | ||||
|                     </ModalBody> | ||||
|  | ||||
|                     <ModalFooter gap={3}> | ||||
|                         <Button variant="primary" type="submit" isLoading={isSubmitting}> | ||||
|                             Save | ||||
|                         </Button> | ||||
|                         <Button onClick={onClose}>Cancel</Button> | ||||
|                     </ModalFooter> | ||||
|                 </ModalContent> | ||||
|             </Modal> | ||||
|         </> | ||||
|     ) | ||||
| } | ||||
| export default EditCourseImage | ||||
							
								
								
									
										160
									
								
								frontend/src/components/Courses/AddSechedule.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								frontend/src/components/Courses/AddSechedule.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,160 @@ | ||||
| import { | ||||
|     Button, | ||||
|     FormControl, | ||||
|     FormErrorMessage, | ||||
|     FormLabel, | ||||
|     Input, | ||||
|     Modal, | ||||
|     ModalBody, | ||||
|     ModalCloseButton, | ||||
|     ModalContent, | ||||
|     ModalFooter, | ||||
|     ModalHeader, | ||||
|     ModalOverlay, | ||||
| } from "@chakra-ui/react" | ||||
| import { useEffect, useState } from "react" | ||||
| import { useMutation, useQueryClient } from "@tanstack/react-query" | ||||
| import { type SubmitHandler, useForm } from "react-hook-form" | ||||
| import DateTimePicker from 'react-datetime-picker'; | ||||
| import 'react-calendar/dist/Calendar.css'; | ||||
| import { type ApiError, type ScheduleCreate, secheduleService } from "../../client" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { handleError } from "../../utils" | ||||
| import moment from "moment"; | ||||
|  | ||||
| interface AddSecheduleProps { | ||||
|     isOpen: boolean | ||||
|     onClose: () => void | ||||
|     courseId: string | ||||
| } | ||||
|  | ||||
| const AddSechedule = ({ isOpen, onClose, courseId }: AddSecheduleProps) => { | ||||
|     const queryClient = useQueryClient() | ||||
|     const showToast = useCustomToast() | ||||
|     const { | ||||
|         register, | ||||
|         handleSubmit, | ||||
|         reset, | ||||
|         setValue, | ||||
|         getValues, | ||||
|         formState: { errors, isSubmitting }, | ||||
|     } = useForm<ScheduleCreate>({ | ||||
|         mode: "onBlur", | ||||
|         criteriaMode: "all", | ||||
|         defaultValues: { | ||||
|             date: "", | ||||
|             title: "", | ||||
|             info1: "", | ||||
|             info2: "", | ||||
|             course_id: courseId, | ||||
|         }, | ||||
|     }) | ||||
|  | ||||
|     useEffect(() => { | ||||
|         setValue("course_id", courseId); | ||||
|         setValue("date", moment.utc(new Date()).format()); | ||||
|     }, [setValue, courseId]); | ||||
|  | ||||
|     const [datetime, setDatetime] = useState<Date>(new Date()); | ||||
|  | ||||
|     const mutation = useMutation({ | ||||
|         mutationFn: (data: ScheduleCreate) => | ||||
|             secheduleService.createSechedule({ requestBody: data }), | ||||
|         onSuccess: (data) => { | ||||
|             showToast("Success!", "Sechedule created successfully.", "success") | ||||
|             console.log(data) | ||||
|             reset( | ||||
|             { | ||||
|                     date: moment.utc(new Date()).format(), | ||||
|                     title: "", | ||||
|                     info1: "", | ||||
|                     info2: "", | ||||
|                     course_id: courseId, | ||||
|                 } | ||||
|             ) | ||||
|             onClose() | ||||
|         }, | ||||
|         onError: (err: ApiError) => { | ||||
|             handleError(err, showToast) | ||||
|         }, | ||||
|         onSettled: () => { | ||||
|             queryClient.invalidateQueries({ queryKey: ["course"] }) | ||||
|         }, | ||||
|     }) | ||||
|  | ||||
|     const onSubmit: SubmitHandler<ScheduleCreate> = (data) => { | ||||
|         mutation.mutate(data) | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <> | ||||
|             <Modal | ||||
|                 isOpen={isOpen} | ||||
|                 onClose={onClose} | ||||
|                 size={{ base: "sm", md: "md" }} | ||||
|                 isCentered | ||||
|             > | ||||
|                 <ModalOverlay /> | ||||
|                 <ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> | ||||
|                     <ModalHeader>Add Sechedule</ModalHeader> | ||||
|                     <ModalCloseButton /> | ||||
|                     <ModalBody pb={6}> | ||||
|                         <DateTimePicker onChange={(newDatetime: Date | null) => { | ||||
|                             if (newDatetime instanceof Date) { | ||||
|                                 setDatetime(newDatetime) | ||||
|                                 setValue("date", moment.utc(newDatetime).format()) | ||||
|                             } | ||||
|                         }} value={datetime} /> | ||||
|                         <FormControl isRequired isInvalid={!!errors.title}> | ||||
|                             <FormLabel htmlFor="title">Title</FormLabel> | ||||
|                             <Input | ||||
|                                 id="title" | ||||
|                                 {...register("title", { | ||||
|                                     required: "Title is required.", | ||||
|                                 })} | ||||
|                                 placeholder="Title" | ||||
|                                 type="text" | ||||
|                             /> | ||||
|                             {errors.title && ( | ||||
|                                 <FormErrorMessage>{errors.title.message}</FormErrorMessage> | ||||
|                             )} | ||||
|                         </FormControl> | ||||
|                         <FormControl mt={4}> | ||||
|                             <FormLabel htmlFor="info1">Info 1</FormLabel> | ||||
|                             <Input | ||||
|                                 id="description" | ||||
|                                 {...register("info1")} | ||||
|                                 placeholder="info1" | ||||
|                                 type="text" | ||||
|                             /> | ||||
|                         </FormControl> | ||||
|                         <FormControl mt={4}> | ||||
|                             <FormLabel htmlFor="info2">Info 2</FormLabel> | ||||
|                             <Input | ||||
|                                 id="description" | ||||
|                                 {...register("info2")} | ||||
|                                 placeholder="info2" | ||||
|                                 type="text" | ||||
|                             /> | ||||
|                         </FormControl> | ||||
|  | ||||
|                     </ModalBody> | ||||
|  | ||||
|                     <ModalFooter gap={3}> | ||||
|                         <Button onClick={() => { | ||||
|                             const values = getValues() | ||||
|                             console.log(values) | ||||
|                         }}> | ||||
|                             test | ||||
|                         </Button> | ||||
|                         <Button variant="primary" type="submit" isLoading={isSubmitting}> | ||||
|                             Save | ||||
|                         </Button> | ||||
|                         <Button onClick={onClose}>Cancel</Button> | ||||
|                     </ModalFooter> | ||||
|                 </ModalContent> | ||||
|             </Modal> | ||||
|         </> | ||||
|     ) | ||||
| } | ||||
| export default AddSechedule | ||||
							
								
								
									
										262
									
								
								frontend/src/components/Courses/CourseDetails.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								frontend/src/components/Courses/CourseDetails.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,262 @@ | ||||
| import { | ||||
|     Button, | ||||
|     FormControl, | ||||
|     FormErrorMessage, | ||||
|     FormLabel, | ||||
|     Input, | ||||
|     Textarea, | ||||
|     Modal, | ||||
|     ModalBody, | ||||
|     ModalCloseButton, | ||||
|     ModalContent, | ||||
|     ModalFooter, | ||||
|     ModalHeader, | ||||
|     ModalOverlay, | ||||
|     Container, | ||||
|     Heading, | ||||
|     Box | ||||
| } from "@chakra-ui/react" | ||||
| import { useQuery, useQueryClient, useMutation, QueryClient } from "@tanstack/react-query" | ||||
| import { createFileRoute, useNavigate, Await, useRouter } from "@tanstack/react-router" | ||||
| import { useEffect, useState } from "react" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { CoursesService, type ApiError, CourseCreate, CourseDetailsPublic } from "../../client" | ||||
| import { handleError } from "../../utils" | ||||
| import { type SubmitHandler, useForm } from "react-hook-form" | ||||
| import { EditorState, ContentState, convertToRaw } from 'draft-js'; | ||||
| import { Editor } from "react-draft-wysiwyg"; | ||||
| import draftToHtml from 'draftjs-to-html'; | ||||
| import htmlToDraft from 'html-to-draftjs'; | ||||
| import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css"; | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| const CourseDetails = () => { | ||||
|     const queryClient = useQueryClient(); | ||||
|     const courseDetails = queryClient.getQueryData(['course']) as CourseDetailsPublic | undefined; | ||||
|    | ||||
|     const toolbar = { | ||||
|         options: ['inline', 'blockType', 'fontSize', 'list', 'textAlign', 'history', 'embedded', 'emoji', 'image'], | ||||
|         inline: { inDropdown: true }, | ||||
|         list: { inDropdown: true }, | ||||
|         textAlign: { inDropdown: true }, | ||||
|         link: { inDropdown: true }, | ||||
|         history: { inDropdown: true }, | ||||
|     } | ||||
|  | ||||
|     const showToast = useCustomToast() | ||||
|  | ||||
|     const [contentEditorState, setContentEditorState] = useState<EditorState>(EditorState.createEmpty()); | ||||
|     const [contents, setContent] = useState<string>(''); | ||||
|     const [infoEditorState, setInfoEditorState] = useState<EditorState>(EditorState.createEmpty()); | ||||
|     const [info, setInfo] = useState<string>(''); | ||||
|     const [longDescriptionEditorState, setLongDescriptionEditorState] = useState<EditorState>(EditorState.createEmpty()); | ||||
|     const [longDescription, setlongDescription] = useState<string>(''); | ||||
|     const [remarksEditorState, setRemarksEditorState] = useState<EditorState>(EditorState.createEmpty()); | ||||
|     const [remarks, setRemarks] = useState<string>(''); | ||||
|     const { | ||||
|         register, | ||||
|         handleSubmit, | ||||
|         reset, | ||||
|         getValues, | ||||
|         setValue, | ||||
|         unregister, | ||||
|         formState: { isSubmitting, errors, isDirty }, | ||||
|     } = useForm<CourseCreate>({ | ||||
|         mode: "onBlur", | ||||
|         criteriaMode: "all", | ||||
|         defaultValues: { | ||||
|             title: courseDetails?.title, | ||||
|             sort_description: courseDetails?.sort_description, | ||||
|             long_description: courseDetails?.long_description, | ||||
|             remark: courseDetails?.remark, | ||||
|             information: courseDetails?.information, | ||||
|             contant: courseDetails?.contant, | ||||
|         } | ||||
|     }) | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (courseDetails) { | ||||
|             setValue('title', courseDetails.title); | ||||
|             setValue('sort_description', courseDetails.sort_description); | ||||
|             // Update other form fields as needed | ||||
|         } | ||||
|         if (courseDetails?.long_description) { | ||||
|             const contentBlock = htmlToDraft(courseDetails.long_description); | ||||
|             if (contentBlock) { | ||||
|                 const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks); | ||||
|                 const editorState = EditorState.createWithContent(contentState); | ||||
|                 setLongDescriptionEditorState(editorState); | ||||
|                 setValue('long_description', longDescription); | ||||
|             } | ||||
|         } | ||||
|         if (courseDetails?.remark) { | ||||
|             const contentBlock = htmlToDraft(courseDetails.remark); | ||||
|             if (contentBlock) { | ||||
|                 const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks); | ||||
|                 const editorState = EditorState.createWithContent(contentState); | ||||
|                 setRemarksEditorState(editorState); | ||||
|                 setValue('remark', remarks); | ||||
|             } | ||||
|         } | ||||
|         if (courseDetails?.information) { | ||||
|             const contentBlock = htmlToDraft(courseDetails.information); | ||||
|             if (contentBlock) { | ||||
|                 const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks); | ||||
|                 const editorState = EditorState.createWithContent(contentState); | ||||
|                 setInfoEditorState(editorState); | ||||
|                 setValue('information', info); | ||||
|             } | ||||
|         } | ||||
|         if (courseDetails?.contant) { | ||||
|             const contentBlock = htmlToDraft(courseDetails.contant); | ||||
|             if (contentBlock) { | ||||
|                 const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks); | ||||
|                 const editorState = EditorState.createWithContent(contentState); | ||||
|                 setContentEditorState(editorState); | ||||
|                 setValue('contant', contents); | ||||
|             } | ||||
|         } | ||||
|     }, [courseDetails]); | ||||
|  | ||||
|     const mutation = useMutation({ | ||||
|         mutationFn: (data: CourseCreate) => | ||||
|             CoursesService.updateCourse({ id: courseDetails?.id ?? '', requestBody: data }), | ||||
|         onSuccess: () => { | ||||
|             showToast("Success!", "Course create successfully.", "success") | ||||
|  | ||||
|         }, | ||||
|         onError: (err: ApiError) => { | ||||
|             handleError(err, showToast) | ||||
|         }, | ||||
|         onSettled: () => { | ||||
|             queryClient.invalidateQueries({ queryKey: ["courses"] }) | ||||
|             //history.go(-1) | ||||
|         }, | ||||
|     }) | ||||
|  | ||||
|     const onSubmit: SubmitHandler<CourseCreate> = async (data) => { | ||||
|         mutation.mutate(data) | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <Container maxW="full"> | ||||
|             <Heading size="sm" py={4}> | ||||
|                 Course Details | ||||
|             </Heading> | ||||
|             <Box | ||||
|                 w={{ sm: "full", md: "50%" }} | ||||
|                 as="form" | ||||
|                 onSubmit={handleSubmit(onSubmit)} | ||||
|             > | ||||
|                 <FormControl isInvalid={!!errors.title}> | ||||
|                     <FormLabel htmlFor="title">Title</FormLabel> | ||||
|                     <Input | ||||
|                         defaultValue={courseDetails?.title} | ||||
|                         id="title" | ||||
|                         type="text" | ||||
|                         {...register("title", { | ||||
|                             required: "title is required", | ||||
|                         })} | ||||
|                     /> | ||||
|                     {errors.title && ( | ||||
|                         <FormErrorMessage>{errors.title.message}</FormErrorMessage> | ||||
|                     )} | ||||
|                 </FormControl> | ||||
|                 <FormControl mt={4}></FormControl> | ||||
|                 <FormControl > | ||||
|                     <FormLabel htmlFor="sort_description">Short Description</FormLabel> | ||||
|                     <Textarea | ||||
|                         id="sort_description" | ||||
|                         {...register("sort_description", { | ||||
|                             required: "sort_description is required", | ||||
|                         })} | ||||
|                     /> | ||||
|                 </FormControl> | ||||
|                 <FormControl mt={4}></FormControl> | ||||
|                 <FormControl > | ||||
|                     <FormLabel htmlFor="long_description">Long Description</FormLabel> | ||||
|                     <Editor | ||||
|                         editorState={longDescriptionEditorState} | ||||
|                         wrapperClassName="wrapper-class" | ||||
|                         editorClassName="demo-editor" | ||||
|                         onEditorStateChange={newState => { | ||||
|                             setLongDescriptionEditorState(newState); | ||||
|                             setlongDescription(draftToHtml(convertToRaw(newState.getCurrentContent()))); | ||||
|                             setValue("long_description", longDescription); | ||||
|                         }} | ||||
|                         toolbar={toolbar} | ||||
|                     /> | ||||
|                 </FormControl> | ||||
|                 <FormControl mt={4}></FormControl> | ||||
|                 <FormControl > | ||||
|                     <FormLabel htmlFor="information">Information</FormLabel> | ||||
|                     <Editor | ||||
|                         editorState={infoEditorState} | ||||
|                         wrapperClassName="wrapper-class" | ||||
|                         editorClassName="demo-editor" | ||||
|                         onEditorStateChange={newState => { | ||||
|                             setInfoEditorState(newState); | ||||
|                             setInfo(draftToHtml(convertToRaw(newState.getCurrentContent()))); | ||||
|                             setValue("information", info); | ||||
|                         }} | ||||
|                         toolbar={toolbar} | ||||
|                     /> | ||||
|                 </FormControl> | ||||
|                 <FormControl mt={4}></FormControl> | ||||
|                 <FormControl > | ||||
|                     <FormLabel htmlFor="contant">Content</FormLabel> | ||||
|                     <Editor | ||||
|                         editorState={contentEditorState} | ||||
|                         wrapperClassName="wrapper-class" | ||||
|                         editorClassName="demo-editor" | ||||
|                         onEditorStateChange={newState => { | ||||
|                             setContentEditorState(newState); | ||||
|                             setContent(draftToHtml(convertToRaw(newState.getCurrentContent()))); | ||||
|                             setValue("contant", contents); | ||||
|                         }} | ||||
|                         toolbar={toolbar} | ||||
|                     /> | ||||
|                 </FormControl> | ||||
|                 <FormControl mt={4}></FormControl> | ||||
|                 <FormControl > | ||||
|                     <FormLabel htmlFor="remark">Remark</FormLabel> | ||||
|                     <Editor | ||||
|                         editorState={remarksEditorState} | ||||
|                         wrapperClassName="wrapper-class" | ||||
|                         editorClassName="demo-editor" | ||||
|                         onEditorStateChange={newState => { | ||||
|                             setRemarksEditorState(newState); | ||||
|                             setRemarks(draftToHtml(convertToRaw(newState.getCurrentContent()))); | ||||
|                             setValue("remark", remarks); | ||||
|                         }} | ||||
|                         toolbar={toolbar} | ||||
|                     /> | ||||
|                 </FormControl> | ||||
|                 <FormControl mt={4}></FormControl> | ||||
|                 <button | ||||
|                     type="button" | ||||
|                     onClick={() => { | ||||
|                         const values = getValues() | ||||
|                         console.log(values) | ||||
|                         // history.go(-1)// { test: "test-input", test1: "test1-input" } | ||||
|                     }} | ||||
|                 > | ||||
|                     Get Values | ||||
|                 </button> | ||||
|                 <Button | ||||
|                     variant="primary" | ||||
|                     type="submit" | ||||
|                     isLoading={isSubmitting} | ||||
|                     isDisabled={!isDirty} | ||||
|                 > | ||||
|                     Save | ||||
|                 </Button> | ||||
|                 <FormControl mt={20}></FormControl> | ||||
|             </Box> | ||||
|         </Container> | ||||
|     ) | ||||
| } | ||||
| export default CourseDetails; | ||||
							
								
								
									
										186
									
								
								frontend/src/components/Courses/CourseImages.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								frontend/src/components/Courses/CourseImages.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | ||||
| import { | ||||
|     Button, | ||||
|     FormControl, | ||||
|     FormErrorMessage, | ||||
|     FormLabel, | ||||
|     Input, | ||||
|     Textarea, | ||||
|     Modal, | ||||
|     ModalBody, | ||||
|     ModalCloseButton, | ||||
|     ModalContent, | ||||
|     ModalFooter, | ||||
|     ModalHeader, | ||||
|     ModalOverlay, | ||||
|     Container, | ||||
|     Heading, | ||||
|     Box, | ||||
|     NumberInput, | ||||
|     NumberInputField, | ||||
|     NumberInputStepper, | ||||
|     NumberIncrementStepper, | ||||
|     NumberDecrementStepper, | ||||
|     Image, | ||||
|     Flex, | ||||
|     HStack, | ||||
|     VStack, | ||||
|     Text, | ||||
|     Td, | ||||
|     Icon, | ||||
|     Grid | ||||
| } from "@chakra-ui/react" | ||||
| import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query" | ||||
| import { createFileRoute, useNavigate, Await, useRouter } from "@tanstack/react-router" | ||||
| import { FaPlus, FaPen, FaTrashAlt } from "react-icons/fa" | ||||
| import { useEffect, useState } from "react" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { ImageService, type ApiError, CourseDetailsPublic, ImageCreate } from "../../client" | ||||
| import { handleError } from "../../utils" | ||||
| import { type SubmitHandler, useForm } from "react-hook-form" | ||||
| import ActionsMenu from "../../components/Common/ActionsMenu" | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| const CourseImages = () => { | ||||
|  | ||||
|     const queryClient = useQueryClient(); | ||||
|     const courseDetails = queryClient.getQueryData(['course']) as CourseDetailsPublic | undefined; | ||||
|     const url = import.meta.env.VITE_API_URL | ||||
|     const showToast = useCustomToast() | ||||
|     const { | ||||
|         register, | ||||
|         handleSubmit, | ||||
|         reset, | ||||
|         getValues, | ||||
|         setValue, | ||||
|         unregister, | ||||
|         formState: { isSubmitting, errors, isDirty }, | ||||
|     } = useForm<ImageCreate>({ | ||||
|         mode: "onBlur", | ||||
|         criteriaMode: "all", | ||||
|         defaultValues: { | ||||
|             course_id: '', | ||||
|             image: undefined, | ||||
|             index: 0, | ||||
|         } | ||||
|     }) | ||||
|  | ||||
|     const mutation = useMutation({ | ||||
|         mutationFn: (data: ImageCreate) => | ||||
|             ImageService.createImage({ formData: data }), | ||||
|         onSuccess: (data) => { | ||||
|             showToast("Success!", "Image added successfully.", "success") | ||||
|             console.log(data) | ||||
|             queryClient.setQueryData(['course'], data) | ||||
|             reset() | ||||
|         }, | ||||
|         onError: (err: ApiError) => { | ||||
|             handleError(err, showToast) | ||||
|         }, | ||||
|         onSettled: () => { | ||||
|             queryClient.invalidateQueries({ queryKey: ["courses"] }) | ||||
|             | ||||
|         }, | ||||
|     }) | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (courseDetails) { | ||||
|             setValue("course_id", courseDetails.id) | ||||
|         } | ||||
|     }, [courseDetails]); | ||||
|  | ||||
|     const onSubmit: SubmitHandler<ImageCreate> = async (data) => { | ||||
|         if (data.image instanceof FileList && data.image.length > 0) { | ||||
|             data.image = data.image[0] | ||||
|         } | ||||
|         if (courseDetails?.images && courseDetails.images.length >= 5) { | ||||
|             showToast("Error!", "You can only add 5 images", "error") | ||||
|             return | ||||
|         } else { | ||||
|             mutation.mutate(data) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     return ( | ||||
|         <Container maxW="full"> | ||||
|             <Box | ||||
|                 w={{ sm: "full", md: "50%" }} | ||||
|                 as="form" | ||||
|                 onSubmit={handleSubmit(onSubmit)} | ||||
|             > | ||||
|                 <Box w="100%" maxW="full" overflowX="auto"> | ||||
|                     <VStack align="flex-start"> | ||||
|                         <HStack spacing={4} overflowX="auto"> | ||||
|  | ||||
|                             {courseDetails?.images.map((image, index) => ( | ||||
|                                 <Box width={200} height={200} > | ||||
|                                     <Grid templateColumns='repeat(2, 1fr)' gap={1}> | ||||
|                                         <Text >{image.index}</Text> | ||||
|                                         <ActionsMenu type={"Image"} value={image} /> | ||||
|                                     </Grid> | ||||
|  | ||||
|                                     <Image key={index} src={url + "/" + image.image} objectFit="cover" /> | ||||
|  | ||||
|                                 </Box> | ||||
|                             ))} | ||||
|                         </HStack> | ||||
|                     </VStack> | ||||
|                 </Box> | ||||
|                 <Heading size="sm" py={4}> | ||||
|                     Add Course Image | ||||
|                 </Heading> | ||||
|                 <FormControl mt={4}></FormControl> | ||||
|                 <FormControl isRequired isInvalid={!!errors.index}> | ||||
|                     <FormLabel htmlFor="index">Index</FormLabel > | ||||
|                     <NumberInput min={0} max={20}  > | ||||
|                         <NumberInputField {...register("index", { | ||||
|                             required: "index is required.", | ||||
|                         })} /> | ||||
|                         <NumberInputStepper> | ||||
|                             <NumberIncrementStepper /> | ||||
|                             <NumberDecrementStepper /> | ||||
|                         </NumberInputStepper> | ||||
|                     </NumberInput> | ||||
|                     {errors.index && ( | ||||
|                         <FormErrorMessage>{errors.index.message}</FormErrorMessage> | ||||
|                     )} | ||||
|                 </FormControl> | ||||
|                 <FormControl mt={4}></FormControl> | ||||
|                 <FormControl isInvalid={!!errors.image} isRequired> | ||||
|                     <FormLabel>{'Image Upload'}</FormLabel> | ||||
|                     <input type="file" {...register("image", { | ||||
|                         required: "index is required.", | ||||
|                     })} /> | ||||
|                     <FormErrorMessage> | ||||
|                         {errors.image && errors?.image.message} | ||||
|                     </FormErrorMessage> | ||||
|                 </FormControl> | ||||
|                 <button | ||||
|                     type="button" | ||||
|                     onClick={() => { | ||||
|                         const values = getValues() | ||||
|                         console.log(values) | ||||
|                         // history.go(-1)// { test: "test-input", test1: "test1-input" } | ||||
|                     }} | ||||
|                 > | ||||
|                     Get Values | ||||
|                 </button> | ||||
|                 <Button | ||||
|                     variant="primary" | ||||
|                     type="submit" | ||||
|                     isLoading={isSubmitting} | ||||
|                     isDisabled={!isDirty} | ||||
|                 > | ||||
|                     Save | ||||
|                 </Button> | ||||
|                 <FormControl mt={20}></FormControl> | ||||
|             </Box> | ||||
|         </Container > | ||||
|     ) | ||||
| } | ||||
| export default CourseImages; | ||||
							
								
								
									
										186
									
								
								frontend/src/components/Courses/CourseInfoImages.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								frontend/src/components/Courses/CourseInfoImages.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | ||||
| import { | ||||
|     Button, | ||||
|     FormControl, | ||||
|     FormErrorMessage, | ||||
|     FormLabel, | ||||
|     Input, | ||||
|     Textarea, | ||||
|     Modal, | ||||
|     ModalBody, | ||||
|     ModalCloseButton, | ||||
|     ModalContent, | ||||
|     ModalFooter, | ||||
|     ModalHeader, | ||||
|     ModalOverlay, | ||||
|     Container, | ||||
|     Heading, | ||||
|     Box, | ||||
|     NumberInput, | ||||
|     NumberInputField, | ||||
|     NumberInputStepper, | ||||
|     NumberIncrementStepper, | ||||
|     NumberDecrementStepper, | ||||
|     Image, | ||||
|     Flex, | ||||
|     HStack, | ||||
|     VStack, | ||||
|     Text, | ||||
|     Td, | ||||
|     Icon, | ||||
|     Grid | ||||
| } from "@chakra-ui/react" | ||||
| import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query" | ||||
| import { createFileRoute, useNavigate, Await, useRouter } from "@tanstack/react-router" | ||||
| import { FaPlus, FaPen, FaTrashAlt } from "react-icons/fa" | ||||
| import { useEffect, useState } from "react" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { Info_imageService, type ApiError, CourseDetailsPublic, ImageCreate } from "../../client" | ||||
| import { handleError } from "../../utils" | ||||
| import { type SubmitHandler, useForm } from "react-hook-form" | ||||
| import ActionsMenu from "../../components/Common/ActionsMenu" | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| const CourseInfoImages = () => { | ||||
|  | ||||
|     const queryClient = useQueryClient(); | ||||
|     const courseDetails = queryClient.getQueryData(['course']) as CourseDetailsPublic | undefined; | ||||
|     const url = import.meta.env.VITE_API_URL | ||||
|     const showToast = useCustomToast() | ||||
|     const { | ||||
|         register, | ||||
|         handleSubmit, | ||||
|         reset, | ||||
|         getValues, | ||||
|         setValue, | ||||
|         unregister, | ||||
|         formState: { isSubmitting, errors, isDirty }, | ||||
|     } = useForm<ImageCreate>({ | ||||
|         mode: "onBlur", | ||||
|         criteriaMode: "all", | ||||
|         defaultValues: { | ||||
|             course_id: '', | ||||
|             image: undefined, | ||||
|             index: 0, | ||||
|         } | ||||
|     }) | ||||
|  | ||||
|     const mutation = useMutation({ | ||||
|         mutationFn: (data: ImageCreate) => | ||||
|             Info_imageService.createImage({ formData: data }), | ||||
|         onSuccess: (data) => { | ||||
|             showToast("Success!", "Image added successfully.", "success") | ||||
|             console.log(data) | ||||
|             queryClient.setQueryData(['course'], data) | ||||
|             reset() | ||||
|         }, | ||||
|         onError: (err: ApiError) => { | ||||
|             handleError(err, showToast) | ||||
|         }, | ||||
|         onSettled: () => { | ||||
|             queryClient.invalidateQueries({ queryKey: ["courses"] }) | ||||
|             | ||||
|         }, | ||||
|     }) | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (courseDetails) { | ||||
|             setValue("course_id", courseDetails.id) | ||||
|         } | ||||
|     }, [courseDetails]); | ||||
|  | ||||
|     const onSubmit: SubmitHandler<ImageCreate> = async (data) => { | ||||
|         if (data.image instanceof FileList && data.image.length > 0) { | ||||
|             data.image = data.image[0] | ||||
|         } | ||||
|         if (courseDetails?.info_images && courseDetails.info_images.length >= 5) { | ||||
|             showToast("Error!", "You can only add 5 images", "error") | ||||
|             return | ||||
|         } else { | ||||
|             mutation.mutate(data) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     return ( | ||||
|         <Container maxW="full"> | ||||
|             <Box | ||||
|                 w={{ sm: "full", md: "50%" }} | ||||
|                 as="form" | ||||
|                 onSubmit={handleSubmit(onSubmit)} | ||||
|             > | ||||
|                 <Box w="100%" maxW="full" overflowX="auto"> | ||||
|                     <VStack align="flex-start"> | ||||
|                         <HStack spacing={4} overflowX="auto"> | ||||
|  | ||||
|                             {courseDetails?.info_images.map((image, index) => ( | ||||
|                                 <Box width={200} height={200} > | ||||
|                                     <Grid templateColumns='repeat(2, 1fr)' gap={1}> | ||||
|                                         <Text >{image.index}</Text> | ||||
|                                         <ActionsMenu type={"Info_Image"} value={image} /> | ||||
|                                     </Grid> | ||||
|  | ||||
|                                     <Image key={index} src={url + "/" + image.image} objectFit="cover" /> | ||||
|  | ||||
|                                 </Box> | ||||
|                             ))} | ||||
|                         </HStack> | ||||
|                     </VStack> | ||||
|                 </Box> | ||||
|                 <Heading size="sm" py={4}> | ||||
|                     Add Course Image | ||||
|                 </Heading> | ||||
|                 <FormControl mt={4}></FormControl> | ||||
|                 <FormControl isRequired isInvalid={!!errors.index}> | ||||
|                     <FormLabel htmlFor="index">Index</FormLabel > | ||||
|                     <NumberInput min={0} max={20}  > | ||||
|                         <NumberInputField {...register("index", { | ||||
|                             required: "index is required.", | ||||
|                         })} /> | ||||
|                         <NumberInputStepper> | ||||
|                             <NumberIncrementStepper /> | ||||
|                             <NumberDecrementStepper /> | ||||
|                         </NumberInputStepper> | ||||
|                     </NumberInput> | ||||
|                     {errors.index && ( | ||||
|                         <FormErrorMessage>{errors.index.message}</FormErrorMessage> | ||||
|                     )} | ||||
|                 </FormControl> | ||||
|                 <FormControl mt={4}></FormControl> | ||||
|                 <FormControl isInvalid={!!errors.image} isRequired> | ||||
|                     <FormLabel>{'Image Upload'}</FormLabel> | ||||
|                     <input type="file" {...register("image", { | ||||
|                         required: "index is required.", | ||||
|                     })} /> | ||||
|                     <FormErrorMessage> | ||||
|                         {errors.image && errors?.image.message} | ||||
|                     </FormErrorMessage> | ||||
|                 </FormControl> | ||||
|                 {/* <button | ||||
|                     type="button" | ||||
|                     onClick={() => { | ||||
|                         const values = getValues() | ||||
|                         console.log(values) | ||||
|                         // history.go(-1)// { test: "test-input", test1: "test1-input" } | ||||
|                     }} | ||||
|                 > | ||||
|                     Get Values | ||||
|                 </button> */} | ||||
|                 <Button | ||||
|                     variant="primary" | ||||
|                     type="submit" | ||||
|                     isLoading={isSubmitting} | ||||
|                     isDisabled={!isDirty} | ||||
|                 > | ||||
|                     Save | ||||
|                 </Button> | ||||
|                 <FormControl mt={20}></FormControl> | ||||
|             </Box> | ||||
|         </Container > | ||||
|     ) | ||||
| } | ||||
| export default CourseInfoImages; | ||||
							
								
								
									
										149
									
								
								frontend/src/components/Courses/EditSechedule.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								frontend/src/components/Courses/EditSechedule.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| import { | ||||
|     Button, | ||||
|     FormControl, | ||||
|     FormErrorMessage, | ||||
|     FormLabel, | ||||
|     Input, | ||||
|     Modal, | ||||
|     ModalBody, | ||||
|     ModalCloseButton, | ||||
|     ModalContent, | ||||
|     ModalFooter, | ||||
|     ModalHeader, | ||||
|     ModalOverlay, | ||||
| } from "@chakra-ui/react" | ||||
| import { useEffect, useState } from "react" | ||||
| import { useMutation, useQueryClient } from "@tanstack/react-query" | ||||
| import { type SubmitHandler, useForm } from "react-hook-form" | ||||
| import DateTimePicker from 'react-datetime-picker'; | ||||
| import 'react-calendar/dist/Calendar.css'; | ||||
| import { type ApiError, type ScheduleCreate, SchedulePublic, ScheduleUpdate, secheduleService } from "../../client" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { handleError } from "../../utils" | ||||
| import moment from "moment"; | ||||
|  | ||||
| interface EditItemProps { | ||||
|     sechedule: SchedulePublic | ||||
|     isOpen: boolean | ||||
|     onClose: () => void | ||||
|     type: string | ||||
| } | ||||
|  | ||||
| const EditSechedule = ({ sechedule, type, isOpen, onClose }: EditItemProps) => { | ||||
|     const queryClient = useQueryClient() | ||||
|     const showToast = useCustomToast() | ||||
|     const { | ||||
|         register, | ||||
|         handleSubmit, | ||||
|         reset, | ||||
|         setValue, | ||||
|         getValues, | ||||
|         formState: { isSubmitting, errors, isDirty }, | ||||
|     } = useForm<ScheduleUpdate>({ | ||||
|         mode: "onBlur", | ||||
|         criteriaMode: "all", | ||||
|         defaultValues: { | ||||
|             date: sechedule.date, | ||||
|             title: sechedule.title, | ||||
|             info1: sechedule.info1, | ||||
|             info2: sechedule.info2, | ||||
|         }, | ||||
|     }) | ||||
|  | ||||
|     const [datetime, setDatetime] = useState<string>(sechedule.date); | ||||
|  | ||||
|     const mutation = useMutation({ | ||||
|         mutationFn: (data: ScheduleUpdate) => | ||||
|             secheduleService.updateSechedule({ id: sechedule.id, requestBody: data }), | ||||
|         onSuccess: (data) => { | ||||
|             showToast("Success!", "Sechedule edit successfully.", "success") | ||||
|             console.log(data) | ||||
|             reset() | ||||
|             onClose() | ||||
|         }, | ||||
|         onError: (err: ApiError) => { | ||||
|             handleError(err, showToast) | ||||
|         }, | ||||
|         onSettled: () => { | ||||
|             queryClient.invalidateQueries({ queryKey: ["course"] }) | ||||
|         }, | ||||
|     }) | ||||
|  | ||||
|     const onSubmit: SubmitHandler<ScheduleUpdate> = (data) => { | ||||
|         mutation.mutate(data) | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <> | ||||
|             <Modal | ||||
|                 isOpen={isOpen} | ||||
|                 onClose={onClose} | ||||
|                 size={{ base: "sm", md: "md" }} | ||||
|                 isCentered | ||||
|             > | ||||
|                 <ModalOverlay /> | ||||
|                 <ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> | ||||
|                     <ModalHeader>Edit Sechedule</ModalHeader> | ||||
|                     <ModalCloseButton /> | ||||
|                     <ModalBody pb={6}> | ||||
|                         <DateTimePicker onChange={(newDatetime: Date | null) => { | ||||
|                             if (newDatetime instanceof Date) { | ||||
|                                 const formattedDate = moment.utc(newDatetime).format() | ||||
|                                 setDatetime(formattedDate) | ||||
|                                 setValue("date", formattedDate) | ||||
|                             } | ||||
|                         }} value={moment.utc(datetime).toDate()} /> | ||||
|                         <FormControl isRequired isInvalid={!!errors.title}> | ||||
|                             <FormLabel htmlFor="title">Title</FormLabel> | ||||
|                             <Input | ||||
|                                 id="title" | ||||
|                                 {...register("title", { | ||||
|                                     required: "Title is required.", | ||||
|                                 })} | ||||
|                                 placeholder="Title" | ||||
|                                 type="text" | ||||
|                             /> | ||||
|                             {errors.title && ( | ||||
|                                 <FormErrorMessage>{errors.title.message}</FormErrorMessage> | ||||
|                             )} | ||||
|                         </FormControl> | ||||
|                         <FormControl mt={4}> | ||||
|                             <FormLabel htmlFor="info1">Info 1</FormLabel> | ||||
|                             <Input | ||||
|                                 id="description" | ||||
|                                 {...register("info1")} | ||||
|                                 placeholder="info1" | ||||
|                                 type="text" | ||||
|                             /> | ||||
|                         </FormControl> | ||||
|                         <FormControl mt={4}> | ||||
|                             <FormLabel htmlFor="info2">Info 2</FormLabel> | ||||
|                             <Input | ||||
|                                 id="description" | ||||
|                                 {...register("info2")} | ||||
|                                 placeholder="info2" | ||||
|                                 type="text" | ||||
|                             /> | ||||
|                         </FormControl> | ||||
|  | ||||
|                     </ModalBody> | ||||
|  | ||||
|                     <ModalFooter gap={3}> | ||||
|                         <Button onClick={() => { | ||||
|                             const values = getValues() | ||||
|                             console.log(values) | ||||
|                         }}> | ||||
|                             test | ||||
|                         </Button> | ||||
|                         <Button variant="primary" type="submit" isLoading={isSubmitting}> | ||||
|                             Save | ||||
|                         </Button> | ||||
|                         <Button onClick={onClose}>Cancel</Button> | ||||
|                     </ModalFooter> | ||||
|                 </ModalContent> | ||||
|             </Modal> | ||||
|         </> | ||||
|     ) | ||||
| } | ||||
|  | ||||
| export default EditSechedule | ||||
							
								
								
									
										76
									
								
								frontend/src/components/Courses/Sechedule.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								frontend/src/components/Courses/Sechedule.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| import { | ||||
|     Button, | ||||
|     Container, | ||||
|     Flex, | ||||
|     Heading, | ||||
|     SkeletonText, | ||||
|     Table, | ||||
|     TableContainer, | ||||
|     Tbody, | ||||
|     Td, | ||||
|     Th, | ||||
|     Thead, | ||||
|     Tr, | ||||
| } from "@chakra-ui/react" | ||||
| import moment from 'moment'; | ||||
| import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query" | ||||
| import { createFileRoute, useNavigate, Await, useRouter } from "@tanstack/react-router" | ||||
| import { FaPlus, FaPen, FaTrashAlt } from "react-icons/fa" | ||||
| import { useEffect, useState } from "react" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { Info_imageService, type ApiError, CourseDetailsPublic, ImageCreate } from "../../client" | ||||
| import { handleError } from "../../utils" | ||||
| import { type SubmitHandler, useForm } from "react-hook-form" | ||||
| import ActionsMenu from "../../components/Common/ActionsMenu" | ||||
| import DateTimePicker from 'react-datetime-picker'; | ||||
| import 'react-datetime-picker/dist/DateTimePicker.css'; | ||||
| import Navbar from "../../components/Common/Navbar" | ||||
| import AddSechedule from "./AddSechedule"; | ||||
|  | ||||
| const Sechedule = () => { | ||||
|     const queryClient = useQueryClient(); | ||||
|     const courseDetails = queryClient.getQueryData(['course']) as CourseDetailsPublic | undefined; | ||||
|  | ||||
|     const showToast = useCustomToast() | ||||
|  | ||||
|     return ( | ||||
|         <Container maxW="full"> | ||||
|             <Navbar type={"Sechedule"} addModalAs={AddSechedule} value={courseDetails?.id} /> | ||||
|             <TableContainer> | ||||
|                 <Table size={{ base: "sm", md: "md" }}> | ||||
|                     <Thead> | ||||
|                         <Tr> | ||||
|                             <Th>Date</Th> | ||||
|                             <Th>Title</Th> | ||||
|                             <Th>Info 1</Th> | ||||
|                             <Th>Info 2</Th> | ||||
|                             <Th>Actions</Th> | ||||
|                         </Tr> | ||||
|                     </Thead> | ||||
|                     <Tbody> | ||||
|                         {courseDetails?.schedule?.map((schedule, index) => ( | ||||
|                             <Tr key={schedule.id}> | ||||
|                                 <Td maxWidth="20px"> | ||||
|                                     {moment(schedule.date).utcOffset("+08:00").format('DD-MM-YYYY HH:mm')} | ||||
|                                 </Td> | ||||
|                                 <Td maxWidth="50px"> | ||||
|                                     {schedule.title} | ||||
|                                 </Td> | ||||
|                                 <Td maxWidth="300px"> | ||||
|                                     {schedule.info1} | ||||
|                                 </Td> | ||||
|                                 <Td maxWidth="300px"> | ||||
|                                     {schedule.info2} | ||||
|                                 </Td> | ||||
|                                 <Td> | ||||
|                                     <ActionsMenu type={"Sechedule"} value={schedule} /> | ||||
|                                 </Td> | ||||
|                             </Tr> | ||||
|                         ))} | ||||
|                     </Tbody> | ||||
|                 </Table> | ||||
|             </TableContainer> | ||||
|         </Container> | ||||
|     ) | ||||
| } | ||||
| export default Sechedule; | ||||
							
								
								
									
										114
									
								
								frontend/src/components/Items/AddItem.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								frontend/src/components/Items/AddItem.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| import { | ||||
|   Button, | ||||
|   FormControl, | ||||
|   FormErrorMessage, | ||||
|   FormLabel, | ||||
|   Input, | ||||
|   Modal, | ||||
|   ModalBody, | ||||
|   ModalCloseButton, | ||||
|   ModalContent, | ||||
|   ModalFooter, | ||||
|   ModalHeader, | ||||
|   ModalOverlay, | ||||
| } from "@chakra-ui/react" | ||||
| import { useMutation, useQueryClient } from "@tanstack/react-query" | ||||
| import { type SubmitHandler, useForm } from "react-hook-form" | ||||
|  | ||||
| import { type ApiError, type ItemCreate, ItemsService } from "../../client" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { handleError } from "../../utils" | ||||
|  | ||||
| interface AddItemProps { | ||||
|   isOpen: boolean | ||||
|   onClose: () => void | ||||
| } | ||||
|  | ||||
| const AddItem = ({ isOpen, onClose }: AddItemProps) => { | ||||
|   const queryClient = useQueryClient() | ||||
|   const showToast = useCustomToast() | ||||
|   const { | ||||
|     register, | ||||
|     handleSubmit, | ||||
|     reset, | ||||
|     formState: { errors, isSubmitting }, | ||||
|   } = useForm<ItemCreate>({ | ||||
|     mode: "onBlur", | ||||
|     criteriaMode: "all", | ||||
|     defaultValues: { | ||||
|       title: "", | ||||
|       description: "", | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const mutation = useMutation({ | ||||
|     mutationFn: (data: ItemCreate) => | ||||
|       ItemsService.createItem({ requestBody: data }), | ||||
|     onSuccess: () => { | ||||
|       showToast("Success!", "Item created successfully.", "success") | ||||
|       reset() | ||||
|       onClose() | ||||
|     }, | ||||
|     onError: (err: ApiError) => { | ||||
|       handleError(err, showToast) | ||||
|     }, | ||||
|     onSettled: () => { | ||||
|       queryClient.invalidateQueries({ queryKey: ["items"] }) | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const onSubmit: SubmitHandler<ItemCreate> = (data) => { | ||||
|     mutation.mutate(data) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Modal | ||||
|         isOpen={isOpen} | ||||
|         onClose={onClose} | ||||
|         size={{ base: "sm", md: "md" }} | ||||
|         isCentered | ||||
|       > | ||||
|         <ModalOverlay /> | ||||
|         <ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> | ||||
|           <ModalHeader>Add Item</ModalHeader> | ||||
|           <ModalCloseButton /> | ||||
|           <ModalBody pb={6}> | ||||
|             <FormControl isRequired isInvalid={!!errors.title}> | ||||
|               <FormLabel htmlFor="title">Title</FormLabel> | ||||
|               <Input | ||||
|                 id="title" | ||||
|                 {...register("title", { | ||||
|                   required: "Title is required.", | ||||
|                 })} | ||||
|                 placeholder="Title" | ||||
|                 type="text" | ||||
|               /> | ||||
|               {errors.title && ( | ||||
|                 <FormErrorMessage>{errors.title.message}</FormErrorMessage> | ||||
|               )} | ||||
|             </FormControl> | ||||
|             <FormControl mt={4}> | ||||
|               <FormLabel htmlFor="description">Description</FormLabel> | ||||
|               <Input | ||||
|                 id="description" | ||||
|                 {...register("description")} | ||||
|                 placeholder="Description" | ||||
|                 type="text" | ||||
|               /> | ||||
|             </FormControl> | ||||
|           </ModalBody> | ||||
|  | ||||
|           <ModalFooter gap={3}> | ||||
|             <Button variant="primary" type="submit" isLoading={isSubmitting}> | ||||
|               Save | ||||
|             </Button> | ||||
|             <Button onClick={onClose}>Cancel</Button> | ||||
|           </ModalFooter> | ||||
|         </ModalContent> | ||||
|       </Modal> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default AddItem | ||||
							
								
								
									
										124
									
								
								frontend/src/components/Items/EditItem.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								frontend/src/components/Items/EditItem.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| import { | ||||
|   Button, | ||||
|   FormControl, | ||||
|   FormErrorMessage, | ||||
|   FormLabel, | ||||
|   Input, | ||||
|   Modal, | ||||
|   ModalBody, | ||||
|   ModalCloseButton, | ||||
|   ModalContent, | ||||
|   ModalFooter, | ||||
|   ModalHeader, | ||||
|   ModalOverlay, | ||||
| } from "@chakra-ui/react" | ||||
| import { useMutation, useQueryClient } from "@tanstack/react-query" | ||||
| import { type SubmitHandler, useForm } from "react-hook-form" | ||||
|  | ||||
| import { | ||||
|   type ApiError, | ||||
|   type ItemPublic, | ||||
|   type ItemUpdate, | ||||
|   ItemsService, | ||||
| } from "../../client" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { handleError } from "../../utils" | ||||
|  | ||||
| interface EditItemProps { | ||||
|   item: ItemPublic | ||||
|   isOpen: boolean | ||||
|   onClose: () => void | ||||
| } | ||||
|  | ||||
| const EditItem = ({ item, isOpen, onClose }: EditItemProps) => { | ||||
|   const queryClient = useQueryClient() | ||||
|   const showToast = useCustomToast() | ||||
|   const { | ||||
|     register, | ||||
|     handleSubmit, | ||||
|     reset, | ||||
|     formState: { isSubmitting, errors, isDirty }, | ||||
|   } = useForm<ItemUpdate>({ | ||||
|     mode: "onBlur", | ||||
|     criteriaMode: "all", | ||||
|     defaultValues: item, | ||||
|   }) | ||||
|  | ||||
|   const mutation = useMutation({ | ||||
|     mutationFn: (data: ItemUpdate) => | ||||
|       ItemsService.updateItem({ id: item.id, requestBody: data }), | ||||
|     onSuccess: () => { | ||||
|       showToast("Success!", "Item updated successfully.", "success") | ||||
|       onClose() | ||||
|     }, | ||||
|     onError: (err: ApiError) => { | ||||
|       handleError(err, showToast) | ||||
|     }, | ||||
|     onSettled: () => { | ||||
|       queryClient.invalidateQueries({ queryKey: ["items"] }) | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const onSubmit: SubmitHandler<ItemUpdate> = async (data) => { | ||||
|     mutation.mutate(data) | ||||
|   } | ||||
|  | ||||
|   const onCancel = () => { | ||||
|     reset() | ||||
|     onClose() | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Modal | ||||
|         isOpen={isOpen} | ||||
|         onClose={onClose} | ||||
|         size={{ base: "sm", md: "md" }} | ||||
|         isCentered | ||||
|       > | ||||
|         <ModalOverlay /> | ||||
|         <ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> | ||||
|           <ModalHeader>Edit Item</ModalHeader> | ||||
|           <ModalCloseButton /> | ||||
|           <ModalBody pb={6}> | ||||
|             <FormControl isInvalid={!!errors.title}> | ||||
|               <FormLabel htmlFor="title">Title</FormLabel> | ||||
|               <Input | ||||
|                 id="title" | ||||
|                 {...register("title", { | ||||
|                   required: "Title is required", | ||||
|                 })} | ||||
|                 type="text" | ||||
|               /> | ||||
|               {errors.title && ( | ||||
|                 <FormErrorMessage>{errors.title.message}</FormErrorMessage> | ||||
|               )} | ||||
|             </FormControl> | ||||
|             <FormControl mt={4}> | ||||
|               <FormLabel htmlFor="description">Description</FormLabel> | ||||
|               <Input | ||||
|                 id="description" | ||||
|                 {...register("description")} | ||||
|                 placeholder="Description" | ||||
|                 type="text" | ||||
|               /> | ||||
|             </FormControl> | ||||
|           </ModalBody> | ||||
|           <ModalFooter gap={3}> | ||||
|             <Button | ||||
|               variant="primary" | ||||
|               type="submit" | ||||
|               isLoading={isSubmitting} | ||||
|               isDisabled={!isDirty} | ||||
|             > | ||||
|               Save | ||||
|             </Button> | ||||
|             <Button onClick={onCancel}>Cancel</Button> | ||||
|           </ModalFooter> | ||||
|         </ModalContent> | ||||
|       </Modal> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default EditItem | ||||
							
								
								
									
										38
									
								
								frontend/src/components/UserSettings/Appearance.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								frontend/src/components/UserSettings/Appearance.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import { | ||||
|   Badge, | ||||
|   Container, | ||||
|   Heading, | ||||
|   Radio, | ||||
|   RadioGroup, | ||||
|   Stack, | ||||
|   useColorMode, | ||||
| } from "@chakra-ui/react" | ||||
|  | ||||
| const Appearance = () => { | ||||
|   const { colorMode, toggleColorMode } = useColorMode() | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Container maxW="full"> | ||||
|         <Heading size="sm" py={4}> | ||||
|           Appearance | ||||
|         </Heading> | ||||
|         <RadioGroup onChange={toggleColorMode} value={colorMode}> | ||||
|           <Stack> | ||||
|             {/* TODO: Add system default option */} | ||||
|             <Radio value="light" colorScheme="teal"> | ||||
|               Light Mode | ||||
|               <Badge ml="1" colorScheme="teal"> | ||||
|                 Default | ||||
|               </Badge> | ||||
|             </Radio> | ||||
|             <Radio value="dark" colorScheme="teal"> | ||||
|               Dark Mode | ||||
|             </Radio> | ||||
|           </Stack> | ||||
|         </RadioGroup> | ||||
|       </Container> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
| export default Appearance | ||||
							
								
								
									
										122
									
								
								frontend/src/components/UserSettings/ChangePassword.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								frontend/src/components/UserSettings/ChangePassword.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| import { | ||||
|   Box, | ||||
|   Button, | ||||
|   Container, | ||||
|   FormControl, | ||||
|   FormErrorMessage, | ||||
|   FormLabel, | ||||
|   Heading, | ||||
|   Input, | ||||
|   useColorModeValue, | ||||
| } from "@chakra-ui/react" | ||||
| import { useMutation } from "@tanstack/react-query" | ||||
| import { type SubmitHandler, useForm } from "react-hook-form" | ||||
|  | ||||
| import { type ApiError, type UpdatePassword, UsersService } from "../../client" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { confirmPasswordRules, handleError, passwordRules } from "../../utils" | ||||
|  | ||||
| interface UpdatePasswordForm extends UpdatePassword { | ||||
|   confirm_password: string | ||||
| } | ||||
|  | ||||
| const ChangePassword = () => { | ||||
|   const color = useColorModeValue("inherit", "ui.light") | ||||
|   const showToast = useCustomToast() | ||||
|   const { | ||||
|     register, | ||||
|     handleSubmit, | ||||
|     reset, | ||||
|     getValues, | ||||
|     formState: { errors, isSubmitting }, | ||||
|   } = useForm<UpdatePasswordForm>({ | ||||
|     mode: "onBlur", | ||||
|     criteriaMode: "all", | ||||
|   }) | ||||
|  | ||||
|   const mutation = useMutation({ | ||||
|     mutationFn: (data: UpdatePassword) => | ||||
|       UsersService.updatePasswordMe({ requestBody: data }), | ||||
|     onSuccess: () => { | ||||
|       showToast("Success!", "Password updated successfully.", "success") | ||||
|       reset() | ||||
|     }, | ||||
|     onError: (err: ApiError) => { | ||||
|       handleError(err, showToast) | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const onSubmit: SubmitHandler<UpdatePasswordForm> = async (data) => { | ||||
|     mutation.mutate(data) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Container maxW="full"> | ||||
|         <Heading size="sm" py={4}> | ||||
|           Change Password | ||||
|         </Heading> | ||||
|         <Box | ||||
|           w={{ sm: "full", md: "50%" }} | ||||
|           as="form" | ||||
|           onSubmit={handleSubmit(onSubmit)} | ||||
|         > | ||||
|           <FormControl isRequired isInvalid={!!errors.current_password}> | ||||
|             <FormLabel color={color} htmlFor="current_password"> | ||||
|               Current Password | ||||
|             </FormLabel> | ||||
|             <Input | ||||
|               id="current_password" | ||||
|               {...register("current_password")} | ||||
|               placeholder="Password" | ||||
|               type="password" | ||||
|               w="auto" | ||||
|             /> | ||||
|             {errors.current_password && ( | ||||
|               <FormErrorMessage> | ||||
|                 {errors.current_password.message} | ||||
|               </FormErrorMessage> | ||||
|             )} | ||||
|           </FormControl> | ||||
|           <FormControl mt={4} isRequired isInvalid={!!errors.new_password}> | ||||
|             <FormLabel htmlFor="password">Set Password</FormLabel> | ||||
|             <Input | ||||
|               id="password" | ||||
|               {...register("new_password", passwordRules())} | ||||
|               placeholder="Password" | ||||
|               type="password" | ||||
|               w="auto" | ||||
|             /> | ||||
|             {errors.new_password && ( | ||||
|               <FormErrorMessage>{errors.new_password.message}</FormErrorMessage> | ||||
|             )} | ||||
|           </FormControl> | ||||
|           <FormControl mt={4} isRequired isInvalid={!!errors.confirm_password}> | ||||
|             <FormLabel htmlFor="confirm_password">Confirm Password</FormLabel> | ||||
|             <Input | ||||
|               id="confirm_password" | ||||
|               {...register("confirm_password", confirmPasswordRules(getValues))} | ||||
|               placeholder="Password" | ||||
|               type="password" | ||||
|               w="auto" | ||||
|             /> | ||||
|             {errors.confirm_password && ( | ||||
|               <FormErrorMessage> | ||||
|                 {errors.confirm_password.message} | ||||
|               </FormErrorMessage> | ||||
|             )} | ||||
|           </FormControl> | ||||
|           <Button | ||||
|             variant="primary" | ||||
|             mt={4} | ||||
|             type="submit" | ||||
|             isLoading={isSubmitting} | ||||
|           > | ||||
|             Save | ||||
|           </Button> | ||||
|         </Box> | ||||
|       </Container> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
| export default ChangePassword | ||||
							
								
								
									
										35
									
								
								frontend/src/components/UserSettings/DeleteAccount.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								frontend/src/components/UserSettings/DeleteAccount.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import { | ||||
|   Button, | ||||
|   Container, | ||||
|   Heading, | ||||
|   Text, | ||||
|   useDisclosure, | ||||
| } from "@chakra-ui/react" | ||||
|  | ||||
| import DeleteConfirmation from "./DeleteConfirmation" | ||||
|  | ||||
| const DeleteAccount = () => { | ||||
|   const confirmationModal = useDisclosure() | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Container maxW="full"> | ||||
|         <Heading size="sm" py={4}> | ||||
|           Delete Account | ||||
|         </Heading> | ||||
|         <Text> | ||||
|           Permanently delete your data and everything associated with your | ||||
|           account. | ||||
|         </Text> | ||||
|         <Button variant="danger" mt={4} onClick={confirmationModal.onOpen}> | ||||
|           Delete | ||||
|         </Button> | ||||
|         <DeleteConfirmation | ||||
|           isOpen={confirmationModal.isOpen} | ||||
|           onClose={confirmationModal.onClose} | ||||
|         /> | ||||
|       </Container> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
| export default DeleteAccount | ||||
							
								
								
									
										96
									
								
								frontend/src/components/UserSettings/DeleteConfirmation.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								frontend/src/components/UserSettings/DeleteConfirmation.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| import { | ||||
|   AlertDialog, | ||||
|   AlertDialogBody, | ||||
|   AlertDialogContent, | ||||
|   AlertDialogFooter, | ||||
|   AlertDialogHeader, | ||||
|   AlertDialogOverlay, | ||||
|   Button, | ||||
| } from "@chakra-ui/react" | ||||
| import { useMutation, useQueryClient } from "@tanstack/react-query" | ||||
| import React from "react" | ||||
| import { useForm } from "react-hook-form" | ||||
|  | ||||
| import { type ApiError, UsersService } from "../../client" | ||||
| import useAuth from "../../hooks/useAuth" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { handleError } from "../../utils" | ||||
|  | ||||
| interface DeleteProps { | ||||
|   isOpen: boolean | ||||
|   onClose: () => void | ||||
| } | ||||
|  | ||||
| const DeleteConfirmation = ({ isOpen, onClose }: DeleteProps) => { | ||||
|   const queryClient = useQueryClient() | ||||
|   const showToast = useCustomToast() | ||||
|   const cancelRef = React.useRef<HTMLButtonElement | null>(null) | ||||
|   const { | ||||
|     handleSubmit, | ||||
|     formState: { isSubmitting }, | ||||
|   } = useForm() | ||||
|   const { logout } = useAuth() | ||||
|  | ||||
|   const mutation = useMutation({ | ||||
|     mutationFn: () => UsersService.deleteUserMe(), | ||||
|     onSuccess: () => { | ||||
|       showToast( | ||||
|         "Success", | ||||
|         "Your account has been successfully deleted.", | ||||
|         "success", | ||||
|       ) | ||||
|       logout() | ||||
|       onClose() | ||||
|     }, | ||||
|     onError: (err: ApiError) => { | ||||
|       handleError(err, showToast) | ||||
|     }, | ||||
|     onSettled: () => { | ||||
|       queryClient.invalidateQueries({ queryKey: ["currentUser"] }) | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const onSubmit = async () => { | ||||
|     mutation.mutate() | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <AlertDialog | ||||
|         isOpen={isOpen} | ||||
|         onClose={onClose} | ||||
|         leastDestructiveRef={cancelRef} | ||||
|         size={{ base: "sm", md: "md" }} | ||||
|         isCentered | ||||
|       > | ||||
|         <AlertDialogOverlay> | ||||
|           <AlertDialogContent as="form" onSubmit={handleSubmit(onSubmit)}> | ||||
|             <AlertDialogHeader>Confirmation Required</AlertDialogHeader> | ||||
|  | ||||
|             <AlertDialogBody> | ||||
|               All your account data will be{" "} | ||||
|               <strong>permanently deleted.</strong> If you are sure, please | ||||
|               click <strong>"Confirm"</strong> to proceed. This action cannot be | ||||
|               undone. | ||||
|             </AlertDialogBody> | ||||
|  | ||||
|             <AlertDialogFooter gap={3}> | ||||
|               <Button variant="danger" type="submit" isLoading={isSubmitting}> | ||||
|                 Confirm | ||||
|               </Button> | ||||
|               <Button | ||||
|                 ref={cancelRef} | ||||
|                 onClick={onClose} | ||||
|                 isDisabled={isSubmitting} | ||||
|               > | ||||
|                 Cancel | ||||
|               </Button> | ||||
|             </AlertDialogFooter> | ||||
|           </AlertDialogContent> | ||||
|         </AlertDialogOverlay> | ||||
|       </AlertDialog> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default DeleteConfirmation | ||||
							
								
								
									
										157
									
								
								frontend/src/components/UserSettings/UserInformation.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								frontend/src/components/UserSettings/UserInformation.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | ||||
| import { | ||||
|   Box, | ||||
|   Button, | ||||
|   Container, | ||||
|   Flex, | ||||
|   FormControl, | ||||
|   FormErrorMessage, | ||||
|   FormLabel, | ||||
|   Heading, | ||||
|   Input, | ||||
|   Text, | ||||
|   useColorModeValue, | ||||
| } from "@chakra-ui/react" | ||||
| import { useMutation, useQueryClient } from "@tanstack/react-query" | ||||
| import { useState } from "react" | ||||
| import { type SubmitHandler, useForm } from "react-hook-form" | ||||
|  | ||||
| import { | ||||
|   type ApiError, | ||||
|   type UserPublic, | ||||
|   type UserUpdateMe, | ||||
|   UsersService, | ||||
| } from "../../client" | ||||
| import useAuth from "../../hooks/useAuth" | ||||
| import useCustomToast from "../../hooks/useCustomToast" | ||||
| import { emailPattern, handleError } from "../../utils" | ||||
|  | ||||
| const UserInformation = () => { | ||||
|   const queryClient = useQueryClient() | ||||
|   const color = useColorModeValue("inherit", "ui.light") | ||||
|   const showToast = useCustomToast() | ||||
|   const [editMode, setEditMode] = useState(false) | ||||
|   const { user: currentUser } = useAuth() | ||||
|   const { | ||||
|     register, | ||||
|     handleSubmit, | ||||
|     reset, | ||||
|     getValues, | ||||
|     formState: { isSubmitting, errors, isDirty }, | ||||
|   } = useForm<UserPublic>({ | ||||
|     mode: "onBlur", | ||||
|     criteriaMode: "all", | ||||
|     defaultValues: { | ||||
|       full_name: currentUser?.full_name, | ||||
|       email: currentUser?.email, | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const toggleEditMode = () => { | ||||
|     setEditMode(!editMode) | ||||
|   } | ||||
|  | ||||
|   const mutation = useMutation({ | ||||
|     mutationFn: (data: UserUpdateMe) => | ||||
|       UsersService.updateUserMe({ requestBody: data }), | ||||
|     onSuccess: () => { | ||||
|       showToast("Success!", "User updated successfully.", "success") | ||||
|     }, | ||||
|     onError: (err: ApiError) => { | ||||
|       handleError(err, showToast) | ||||
|     }, | ||||
|     onSettled: () => { | ||||
|       queryClient.invalidateQueries() | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const onSubmit: SubmitHandler<UserUpdateMe> = async (data) => { | ||||
|     mutation.mutate(data) | ||||
|   } | ||||
|  | ||||
|   const onCancel = () => { | ||||
|     reset() | ||||
|     toggleEditMode() | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Container maxW="full"> | ||||
|         <Heading size="sm" py={4}> | ||||
|           User Information | ||||
|         </Heading> | ||||
|         <Box | ||||
|           w={{ sm: "full", md: "50%" }} | ||||
|           as="form" | ||||
|           onSubmit={handleSubmit(onSubmit)} | ||||
|         > | ||||
|           <FormControl> | ||||
|             <FormLabel color={color} htmlFor="name"> | ||||
|               Full name | ||||
|             </FormLabel> | ||||
|             {editMode ? ( | ||||
|               <Input | ||||
|                 id="name" | ||||
|                 {...register("full_name", { maxLength: 30 })} | ||||
|                 type="text" | ||||
|                 size="md" | ||||
|                 w="auto" | ||||
|               /> | ||||
|             ) : ( | ||||
|               <Text | ||||
|                 size="md" | ||||
|                 py={2} | ||||
|                 color={!currentUser?.full_name ? "ui.dim" : "inherit"} | ||||
|                 isTruncated | ||||
|                 maxWidth="250px" | ||||
|               > | ||||
|                 {currentUser?.full_name || "N/A"} | ||||
|               </Text> | ||||
|             )} | ||||
|           </FormControl> | ||||
|           <FormControl mt={4} isInvalid={!!errors.email}> | ||||
|             <FormLabel color={color} htmlFor="email"> | ||||
|               Email | ||||
|             </FormLabel> | ||||
|             {editMode ? ( | ||||
|               <Input | ||||
|                 id="email" | ||||
|                 {...register("email", { | ||||
|                   required: "Email is required", | ||||
|                   pattern: emailPattern, | ||||
|                 })} | ||||
|                 type="email" | ||||
|                 size="md" | ||||
|                 w="auto" | ||||
|               /> | ||||
|             ) : ( | ||||
|               <Text size="md" py={2} isTruncated maxWidth="250px"> | ||||
|                 {currentUser?.email} | ||||
|               </Text> | ||||
|             )} | ||||
|             {errors.email && ( | ||||
|               <FormErrorMessage>{errors.email.message}</FormErrorMessage> | ||||
|             )} | ||||
|           </FormControl> | ||||
|           <Flex mt={4} gap={3}> | ||||
|             <Button | ||||
|               variant="primary" | ||||
|               onClick={toggleEditMode} | ||||
|               type={editMode ? "button" : "submit"} | ||||
|               isLoading={editMode ? isSubmitting : false} | ||||
|               isDisabled={editMode ? !isDirty || !getValues("email") : false} | ||||
|             > | ||||
|               {editMode ? "Save" : "Edit"} | ||||
|             </Button> | ||||
|             {editMode && ( | ||||
|               <Button onClick={onCancel} isDisabled={isSubmitting}> | ||||
|                 Cancel | ||||
|               </Button> | ||||
|             )} | ||||
|           </Flex> | ||||
|         </Box> | ||||
|       </Container> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default UserInformation | ||||
		Reference in New Issue
	
	Block a user