added organ in cms

This commit is contained in:
Philip Cheung 2024-10-26 20:24:36 +08:00
parent dcf4e3efe8
commit 1475714c7a
8 changed files with 782 additions and 4 deletions

View File

@ -141,6 +141,33 @@ export type WebSettingUpdate = {
whatsapp: string,
}
export type OrgansPublic = {
data: Array<AboutUsPublic>
count: number
}
export type OrganPublic = {
index: number,
description: string,
title: string,
image: string,
id: string
}
export type OrganCreate = {
index: number,
description: string,
title: string,
image: File,
}
export type OrganUpdate = {
index: number,
description: string,
title: string,
image?: File | undefined | null,
}
export type AboutUssPublic = {
data: Array<AboutUsPublic>

View File

@ -21,6 +21,10 @@ import type {
ClientMessagesPublic,
WebSettingPublic,
WebSettingUpdate,
OrganPublic,
OrgansPublic,
OrganUpdate,
OrganCreate,
AboutUssPublic,
AboutUsUpdate,
AboutUsCreate,
@ -460,6 +464,93 @@ export class ClientMessagesService {
}
export type TDataReadOrgan = {
limit?: number
skip?: number
}
export type TDataCreateOrgan = {
formData: OrganCreate
}
export type TDataUpdateOrgan = {
id: string
formData: OrganUpdate
}
export type TDataDeleteOrgan = {
id: string
}
export class OrganService {
/**
* Read Organ
* Retrieve organ.
* @returns OrganPublic Successful Response
* @throws ApiError
*/
public static readOrgan(
data: TDataReadOrgan = {},
): CancelablePromise<OrgansPublic> {
const { limit = 100, skip = 0 } = data
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/organ/",
query: {
skip,
limit,
},
errors: {
422: "Validation Error",
},
})
}
public static createOrgan(
data: TDataCreateOrgan,
): CancelablePromise<OrganPublic> {
const { formData } = data
return __request(OpenAPI, {
method: "POST",
url: "/api/v1/organ/",
formData: formData,
mediaType: "multipart/form-data",
errors: {
422: "Validation Error",
},
})
}
public static updateOrgan(
data: TDataUpdateOrgan,
): CancelablePromise<OrganPublic> {
const { id, formData } = data
return __request(OpenAPI, {
method: "PATCH",
url: "/api/v1/organ/{id}",
path: {
id: id,
},
formData: formData,
mediaType: "multipart/form-data",
errors: {
422: "Validation Error",
},
})
}
public static deleteOrgan(data: TDataDeleteOrgan): CancelablePromise<Message> {
const { id } = data
return __request(OpenAPI, {
method: "DELETE",
url: "/api/v1/organ/{id}",
path: {
id,
},
errors: {
422: "Validation Error",
},
})
}
}
export type TDataReadAboutUs = {
limit?: number
skip?: number

View File

@ -9,12 +9,13 @@ import {
import { BsThreeDotsVertical } from "react-icons/bs"
import { FiEdit, FiTrash } from "react-icons/fi"
import type { ItemPublic, UserPublic, AboutUsPublic, CoursePublic, ImagePublic, SchedulePublic } from "../../client"
import type { ItemPublic, UserPublic, AboutUsPublic, CoursePublic, ImagePublic, SchedulePublic, OrganPublic } 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 EditOrgan from "../Organ/EditOrgan"
import Delete from "./DeleteAlert"
interface ActionsMenuProps {
@ -58,6 +59,14 @@ const ActionsMenu = ({ type, value, disabled }: ActionsMenuProps) => {
onClose={editUserModal.onClose}
/>
)
case 'Organ':
return (
<EditOrgan
organ={value as OrganPublic}
isOpen={editUserModal.isOpen}
onClose={editUserModal.onClose}
/>
)
case 'Image':
return (
<EditCourseImage

View File

@ -11,7 +11,7 @@ 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 { ItemsService, UsersService, ClientMessagesService, AboutUsService, CoursesService, ImageService, Info_imageService, secheduleService, OrganService } from "../../client"
import useCustomToast from "../../hooks/useCustomToast"
interface DeleteProps {
@ -39,7 +39,10 @@ const Delete = ({ type, id, isOpen, onClose }: DeleteProps) => {
await ClientMessagesService.deleteMessage({ id: id })
} else if (type === "AboutUs") {
await AboutUsService.deleteAboutUs({ id: id })
} else if (type === "Course") {
} else if (type === "Organ") {
await OrganService.deleteOrgan({ id: id })
}
else if (type === "Course") {
await CoursesService.deleteCourse({ id: id })
} else if (type === "Image") {
await ImageService.deleteImage({ id: id })
@ -82,6 +85,8 @@ const Delete = ({ type, id, isOpen, onClose }: DeleteProps) => {
key = "messages"
} else if (type === "AboutUs") {
key = "aboutUs"
} else if(type === "Organ"){
key = "organs"
} else if (type === "Course") {
key = "courses"
} else if (type === "Image") {

View File

@ -0,0 +1,226 @@
import { useState } from 'react';
import {
Button,
FormControl,
FormErrorMessage,
FormLabel,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Input,
} from "@chakra-ui/react"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { type SubmitHandler, useForm } from "react-hook-form"
import { type ApiError, type OrganCreate, OrganService } from "../../client"
import useCustomToast from "../../hooks/useCustomToast"
import { handleError } from "../../utils"
import { EditorState, convertToRaw } from 'draft-js';
import { Editor } from "react-draft-wysiwyg";
import draftToHtml from 'draftjs-to-html';
import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css";
interface AddOrganProps {
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 AddOrgan = ({ isOpen, onClose }: AddOrganProps) => {
const queryClient = useQueryClient()
const showToast = useCustomToast()
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
} = useForm<OrganCreate>({
mode: "onBlur",
criteriaMode: "all",
defaultValues: {
index: 0,
title: "",
description: "",
image: undefined,
},
})
const [editorState, setEditorState] = useState<EditorState>(EditorState.createEmpty());
const [content, setContent] = useState<string>('');
const mutation = useMutation({
mutationFn: (data: OrganCreate) =>
OrganService.createOrgan({ formData: data }),
onSuccess: () => {
showToast("Success!", "Organ created successfully.", "success")
reset()
setEditorState(EditorState.createEmpty());
onClose()
},
onError: (err: ApiError) => {
handleError(err, showToast)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["organ"] })
},
})
const onSubmit: SubmitHandler<OrganCreate> = (data) => {
if (isSubmitting) return
try {
if (data.image instanceof FileList && data.image.length > 0) {
data.image = data.image[0]
}
mutation.mutate(data)
console.log(data)
}
catch (error) {
console.error("Error:", error);
}
}
return (
<>
<Modal
isOpen={isOpen}
onClose={onClose}
size={'xl'}
isCentered
>
<ModalOverlay />
<ModalContent as="form" onSubmit={handleSubmit(onSubmit)}>
<ModalHeader>Add Organ</ModalHeader>
<ModalCloseButton />
<ModalBody pb={30}>
<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 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>
{errors.index && (
<FormErrorMessage>{errors.index.message}</FormErrorMessage>
)}
</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>
</ModalBody>
<ModalFooter gap={3}>
<Button variant="primary" type="submit" isLoading={isSubmitting || mutation.isPending}>
Save
</Button>
<Button onClick={onClose}>Cancel</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
)
}
export default AddOrgan

View File

@ -0,0 +1,246 @@
import {useState } from 'react';
import {
Button,
FormControl,
FormErrorMessage,
FormLabel,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Box,
Image,
} from "@chakra-ui/react"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { type SubmitHandler, useForm } from "react-hook-form"
import { type ApiError, OrganService, OrganUpdate, OrganPublic } 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 EditOrganProps {
isOpen: boolean
onClose: () => void
organ: OrganPublic
}
// 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 EditOrgan = ({ organ, isOpen, onClose }: EditOrganProps) => {
//const url = import.meta.env.VITE_API_URL
const queryClient = useQueryClient()
const showToast = useCustomToast()
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
} = useForm<OrganUpdate>({
mode: "onBlur",
criteriaMode: "all",
defaultValues: {
index: organ.index,
title: organ.title,
description: organ.description,
image: undefined,
},
})
const [editorState, setEditorState] = useState<EditorState>(() => {
const contentBlock = htmlToDraft(organ.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: OrganUpdate) =>
OrganService.updateOrgan({ id: organ.id, formData: data }),
onSuccess: () => {
showToast("Success!", "Organ update successfully.", "success")
reset()
setEditorState(EditorState.createEmpty());
onClose()
},
onError: (err: ApiError) => {
handleError(err, showToast)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["organ"] })
},
})
const onSubmit: SubmitHandler<OrganUpdate> = (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 Organ</ModalHeader>
<ModalCloseButton />
<ModalBody pb={30}>
<Box boxSize='auto'>
<Image src={import.meta.env.VITE_IMAGE_URL+"/"+organ.image} />
</Box>
<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 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 EditOrgan

View File

@ -23,6 +23,7 @@ import { Route as LayoutItemsImport } from './routes/_layout/items'
import { Route as LayoutClientMessagesImport } from './routes/_layout/clientMessages'
import { Route as LayoutAdminImport } from './routes/_layout/admin'
import { Route as LayoutAboutUsImport } from './routes/_layout/aboutUs'
import { Route as LayoutOrganImport } from './routes/_layout/organ'
import { Route as LayoutCoursesCoursesImport } from './routes/_layout/Courses/Courses'
import { Route as LayoutCoursesAddCourseImport } from './routes/_layout/Courses/AddCourse'
import { Route as LayoutCoursesIdEditCourseImport } from './routes/_layout/Courses/$id.EditCourse'
@ -89,6 +90,11 @@ const LayoutAboutUsRoute = LayoutAboutUsImport.update({
getParentRoute: () => LayoutRoute,
} as any)
const LayoutOrganRoute = LayoutOrganImport.update({
path: '/organ',
getParentRoute: () => LayoutRoute,
} as any)
const LayoutCoursesCoursesRoute = LayoutCoursesCoursesImport.update({
path: '/Courses/Courses',
getParentRoute: () => LayoutRoute,
@ -132,6 +138,12 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LayoutAboutUsImport
parentRoute: typeof LayoutImport
}
'/_layout/organ': {
preLoaderRoute: typeof LayoutOrganImport
parentRoute: typeof LayoutImport
}
'/_layout/admin': {
preLoaderRoute: typeof LayoutAdminImport
parentRoute: typeof LayoutImport
@ -176,6 +188,7 @@ declare module '@tanstack/react-router' {
export const routeTree = rootRoute.addChildren([
LayoutRoute.addChildren([
LayoutAboutUsRoute,
LayoutOrganRoute,
LayoutAdminRoute,
LayoutClientMessagesRoute,
LayoutItemsRoute,

View File

@ -0,0 +1,161 @@
import {
Button,
Container,
Flex,
Heading,
SkeletonText,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@chakra-ui/react"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { createFileRoute, useNavigate } from "@tanstack/react-router"
import { useEffect } from "react"
import { z } from "zod"
import parse from 'html-react-parser';
import { OrganService } from "../../client"
import ActionsMenu from "../../components/Common/ActionsMenu"
import Navbar from "../../components/Common/Navbar"
import AddOrgan from "../../components/Organ/AddOrgan"
const organSearchSchema = z.object({
page: z.number().catch(1),
})
export const Route = createFileRoute("/_layout/organ")({
component: Organ,
validateSearch: (search) => organSearchSchema.parse(search),
})
const PER_PAGE = 100
function getItemsQueryOptions({ page }: { page: number }) {
return {
queryFn: () =>
OrganService.readOrgan({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }),
queryKey: ["organ", { page }],
}
}
function OrganTable() {
const queryClient = useQueryClient()
const { page } = Route.useSearch()
const navigate = useNavigate({ from: Route.fullPath })
const setPage = (page: number) =>
navigate({ search: (prev) => ({ ...prev, page }) })
const {
data: organ,
isPending,
isPlaceholderData,
} = useQuery({
...getItemsQueryOptions({ page }),
placeholderData: (prevData) => prevData,
})
organ?.data.sort((a, b) => a.index - b.index)
const hasNextPage = !isPlaceholderData && organ?.data.length === PER_PAGE
const hasPreviousPage = page > 1
useEffect(() => {
if (hasNextPage) {
queryClient.prefetchQuery(getItemsQueryOptions({ page: page + 1 }))
}
}, [page, queryClient, hasNextPage])
return (
<>
<TableContainer>
<Table size={{ base: "sm", md: "md" }}>
<Thead>
<Tr>
<Th>ID</Th>
<Th>Title</Th>
<Th>Description</Th>
<Th>Image</Th>
<Th>Index</Th>
<Th>Actions</Th>
</Tr>
</Thead>
{isPending ? (
<Tbody>
<Tr>
{new Array(4).fill(null).map((_, index) => (
<Td key={index}>
<SkeletonText noOfLines={1} paddingBlock="16px" />
</Td>
))}
</Tr>
</Tbody>
) : (
<Tbody>
{organ?.data.map((organ) => (
<Tr key={organ.id} opacity={isPlaceholderData ? 0.5 : 1}>
<Td isTruncated maxWidth="50">{organ.id}</Td>
<Td
whiteSpace="pre-line"
maxWidth="350px"
>
{parse(organ.title)}
</Td>
<Td
whiteSpace="pre-line"
maxWidth="350px"
>
{parse(organ.description) || "N/A"}
</Td>
<Td>
<img src={import.meta.env.VITE_IMAGE_URL + "/" + organ.image} width="100px" height="100px" />
</Td>
<Td isTruncated maxWidth="10px">
{organ.index}
</Td>
<Td>
<ActionsMenu type={"AboutUs"} value={organ} />
</Td>
</Tr>
))}
</Tbody>
)}
</Table>
</TableContainer>
<Flex
gap={4}
alignItems="center"
mt={4}
direction="row"
justifyContent="flex-end"
>
<Button onClick={() => setPage(page - 1)} isDisabled={!hasPreviousPage}>
Previous
</Button>
<span>Page {page}</span>
<Button isDisabled={!hasNextPage} onClick={() => setPage(page + 1)}>
Next
</Button>
</Flex>
</>
)
}
function Organ() {
return (
<Container maxW="full">
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}>
Electric organ Management
</Heading>
<Navbar type={"AboutUs"} addModalAs={AddOrgan} />
<OrganTable />
</Container>
)
}