From 1475714c7aeb05f9c83662314ff49df57600f3e8 Mon Sep 17 00:00:00 2001 From: Philip Cheung Date: Sat, 26 Oct 2024 20:24:36 +0800 Subject: [PATCH] added organ in cms --- frontend/src/client/models.ts | 27 ++ frontend/src/client/services.ts | 91 +++++++ .../src/components/Common/ActionsMenu.tsx | 11 +- .../src/components/Common/DeleteAlert.tsx | 11 +- frontend/src/components/Organ/AddOrgan.tsx | 226 ++++++++++++++++ frontend/src/components/Organ/EditOrgan.tsx | 246 ++++++++++++++++++ frontend/src/routeTree.gen.ts | 13 + frontend/src/routes/_layout/organ.tsx | 161 ++++++++++++ 8 files changed, 782 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/Organ/AddOrgan.tsx create mode 100644 frontend/src/components/Organ/EditOrgan.tsx create mode 100644 frontend/src/routes/_layout/organ.tsx diff --git a/frontend/src/client/models.ts b/frontend/src/client/models.ts index cd77280..ab7778b 100644 --- a/frontend/src/client/models.ts +++ b/frontend/src/client/models.ts @@ -141,6 +141,33 @@ export type WebSettingUpdate = { whatsapp: string, } +export type OrgansPublic = { + data: Array + 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 diff --git a/frontend/src/client/services.ts b/frontend/src/client/services.ts index 0ef6a93..a1c08b8 100644 --- a/frontend/src/client/services.ts +++ b/frontend/src/client/services.ts @@ -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 { + 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 { + 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 { + 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 { + 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 diff --git a/frontend/src/components/Common/ActionsMenu.tsx b/frontend/src/components/Common/ActionsMenu.tsx index 76a0323..7f7e398 100644 --- a/frontend/src/components/Common/ActionsMenu.tsx +++ b/frontend/src/components/Common/ActionsMenu.tsx @@ -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 ( + + ) case 'Image': return ( { 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,11 +85,13 @@ 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") { key = "course" - } else if(type === "Info_Image"){ + } else if (type === "Info_Image") { key = "course" } else if (type === "Sechedule") { key = "course" diff --git a/frontend/src/components/Organ/AddOrgan.tsx b/frontend/src/components/Organ/AddOrgan.tsx new file mode 100644 index 0000000..0d63b0f --- /dev/null +++ b/frontend/src/components/Organ/AddOrgan.tsx @@ -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(null) +// const { ref, ...rest } = register as { ref: (instance: HTMLInputElement | null) => void } + +// const handleClick = () => inputRef.current?.click() + +// return ( +// +// { +// ref(e) +// inputRef.current = e +// }} +// /> +// <> +// {children} +// +// +// ) +// } + + + +const AddOrgan = ({ isOpen, onClose }: AddOrganProps) => { + const queryClient = useQueryClient() + const showToast = useCustomToast() + const { + register, + handleSubmit, + reset, + formState: { errors, isSubmitting }, + } = useForm({ + mode: "onBlur", + criteriaMode: "all", + defaultValues: { + index: 0, + title: "", + description: "", + image: undefined, + }, + }) + const [editorState, setEditorState] = useState(EditorState.createEmpty()); + const [content, setContent] = useState(''); + + + + 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 = (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 ( + <> + + + + Add Organ + + + + + Title + + {errors.title && ( + {errors.title.message} + )} + + + { + 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 }, + }} + /> + + + Index + + + + + + + + + {errors.index && ( + {errors.index.message} + )} + + + + {'Image Upload'} + + + + + {errors.image && errors?.image.message} + + + + + + + + + + + + + ) +} + +export default AddOrgan diff --git a/frontend/src/components/Organ/EditOrgan.tsx b/frontend/src/components/Organ/EditOrgan.tsx new file mode 100644 index 0000000..21228a2 --- /dev/null +++ b/frontend/src/components/Organ/EditOrgan.tsx @@ -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(null) +// const { ref, ...rest } = register as { ref: (instance: HTMLInputElement | null) => void } + +// const handleClick = () => inputRef.current?.click() + +// return ( +// +// { +// ref(e) +// inputRef.current = e +// }} +// /> +// <> +// {children} +// +// +// ) +// } + + + +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({ + mode: "onBlur", + criteriaMode: "all", + defaultValues: { + index: organ.index, + title: organ.title, + description: organ.description, + image: undefined, + }, + }) + const [editorState, setEditorState] = useState(() => { + const contentBlock = htmlToDraft(organ.description); + if (contentBlock) { + const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks); + return EditorState.createWithContent(contentState); + } + return EditorState.createEmpty(); + }); + // const [content, setContent] = useState(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 = (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 ( + <> + + + + Edit Organ + + + + + + + Title + + {errors.title && ( + {errors.title.message} + )} + + + { + 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 }, + }} + /> + + + Index + + + + + + + + {errors.index && ( + {errors.index.message} + )} + + + + {'Image Upload'} + + + + {errors.image && errors?.image.message} + + + + + + + + + + + + + ) +} +export default EditOrgan diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 32bb908..02c7246 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -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, diff --git a/frontend/src/routes/_layout/organ.tsx b/frontend/src/routes/_layout/organ.tsx new file mode 100644 index 0000000..875a9e4 --- /dev/null +++ b/frontend/src/routes/_layout/organ.tsx @@ -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 ( + <> + + + + + + + + + + + + + {isPending ? ( + + + {new Array(4).fill(null).map((_, index) => ( + + ))} + + + ) : ( + + {organ?.data.map((organ) => ( + + + + + + + + + + ))} + + )} +
IDTitleDescriptionImageIndexActions
+ +
{organ.id} + {parse(organ.title)} + + {parse(organ.description) || "N/A"} + + + + {organ.index} + + +
+
+ + + Page {page} + + + + ) +} + +function Organ() { + + return ( + + + Electric organ Management + + + + + + + + ) +}