first commit

This commit is contained in:
2024-09-17 12:11:39 +08:00
commit 42fbcc5d89
236 changed files with 27582 additions and 0 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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;

View 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;

View 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;

View 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

View 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;

View 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

View 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

View 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

View 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

View 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

View 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

View 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