diff --git a/packages/client/public/index.html b/packages/client/public/index.html index aa069f2..cc4df8c 100644 --- a/packages/client/public/index.html +++ b/packages/client/public/index.html @@ -10,34 +10,16 @@ content="Web site created using create-react-app" /> - - + React App
- +
diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 7f6e6ae..038b8ed 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -6,6 +6,7 @@ import { getFetch, httpBatchLink, loggerLink } from "@trpc/client"; import routes from "./router"; import { trpc } from "./trpc"; import AuthMiddleware from "./middleware/AuthMiddleware"; +import { CookiesProvider } from "react-cookie"; function AppContent() { const content = useRoutes(routes); @@ -42,14 +43,16 @@ function App() { }) ); return ( - - - - - - - - + + + + + + + + + + ); } diff --git a/packages/client/src/components/FileUpload.tsx b/packages/client/src/components/FileUpload.tsx index 20ca694..a817ee2 100644 --- a/packages/client/src/components/FileUpload.tsx +++ b/packages/client/src/components/FileUpload.tsx @@ -3,9 +3,6 @@ import { Controller, useController, useFormContext } from 'react-hook-form'; import useStore from '../store'; import Spinner from './Spinner'; -const CLOUDINARY_UPLOAD_PRESET = 'trpc-api'; -const CLOUDINARY_URL = 'https://api.cloudinary.com/v1_1/Codevo/image/upload'; - type FileUpLoaderProps = { name: string; }; @@ -20,17 +17,20 @@ const FileUpLoader: React.FC = ({ name }) => { const onFileDrop = useCallback( async (e: React.SyntheticEvent) => { const target = e.target as HTMLInputElement; - if (!target.files) return; + if (!target.files || target.files.length === 0) return; const newFile = Object.values(target.files).map((file: File) => file); const formData = new FormData(); formData.append('file', newFile[0]); - formData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET); + formData.append('upload_preset', 'trpc-api'); store.setUploadingImage(true); - const data = await fetch(CLOUDINARY_URL, { - method: 'POST', - body: formData, - }) + const data = await fetch( + 'https://api.cloudinary.com/v1_1/Codevo/image/upload', + { + method: 'POST', + body: formData, + } + ) .then((res) => { store.setUploadingImage(false); diff --git a/packages/client/src/components/Header.tsx b/packages/client/src/components/Header.tsx index 74a14f3..5a0b712 100644 --- a/packages/client/src/components/Header.tsx +++ b/packages/client/src/components/Header.tsx @@ -1,9 +1,14 @@ +import { useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { Link } from "react-router-dom"; import useStore from "../store"; import { trpc } from "../trpc"; +import PostModal from "./modals/post.modal"; +import CreatePost from "./posts/create.post"; +import Spinner from "./Spinner"; const Header = () => { + const [openPostModal, setOpenPostModal] = useState(false); const store = useStore(); const user = store.authUser; @@ -24,49 +29,65 @@ const Header = () => { }; return ( -
- -
+ + + + + + + +
+ {store.pageLoading && } +
+ ); }; diff --git a/packages/client/src/components/Message.tsx b/packages/client/src/components/Message.tsx new file mode 100644 index 0000000..1c7a8a4 --- /dev/null +++ b/packages/client/src/components/Message.tsx @@ -0,0 +1,17 @@ +import React, { FC } from 'react'; + +type IMessageProps = { + children: React.ReactNode; +}; +const Message: FC = ({ children }) => { + return ( +
+ {children} +
+ ); +}; + +export default Message; diff --git a/packages/client/src/components/TextInput.tsx b/packages/client/src/components/TextInput.tsx new file mode 100644 index 0000000..76a5599 --- /dev/null +++ b/packages/client/src/components/TextInput.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { useFormContext } from 'react-hook-form'; +import { twMerge } from 'tailwind-merge'; + +type TextInputProps = { + label: string; + name: string; + type?: string; +}; + +const TextInput: React.FC = ({ + label, + name, + type = 'text', +}) => { + const { + register, + formState: { errors }, + } = useFormContext(); + return ( +
+ + +

+ {errors[name]?.message as string} +

+
+ ); +}; + +export default TextInput; diff --git a/packages/client/src/components/modals/post.modal.tsx b/packages/client/src/components/modals/post.modal.tsx new file mode 100644 index 0000000..9a75322 --- /dev/null +++ b/packages/client/src/components/modals/post.modal.tsx @@ -0,0 +1,30 @@ +import ReactDom from 'react-dom'; +import React, { FC } from 'react'; + +type IPostModal = { + openPostModal: boolean; + setOpenPostModal: (openPostModal: boolean) => void; + children: React.ReactNode; +}; + +const PostModal: FC = ({ + openPostModal, + setOpenPostModal, + children, +}) => { + if (!openPostModal) return null; + return ReactDom.createPortal( + <> +
setOpenPostModal(false)} + >
+
+ {children} +
+ , + document.getElementById('post-modal') as HTMLElement + ); +}; + +export default PostModal; diff --git a/packages/client/src/components/posts/create.post.tsx b/packages/client/src/components/posts/create.post.tsx new file mode 100644 index 0000000..53f4480 --- /dev/null +++ b/packages/client/src/components/posts/create.post.tsx @@ -0,0 +1,108 @@ +import { FC, useEffect } from "react"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import { twMerge } from "tailwind-merge"; +import { object, string, TypeOf } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import FileUpLoader from "../FileUpload"; +import { LoadingButton } from "../LoadingButton"; +import TextInput from "../TextInput"; +import { toast } from "react-toastify"; +import useStore from "../../store"; +import { trpc } from "../../trpc"; +import { useQueryClient } from "@tanstack/react-query"; + +const createPostSchema = object({ + title: string().min(1, "Title is required"), + category: string().min(1, "Category is required"), + content: string().min(1, "Content is required"), + image: string().min(1, "Image is required"), +}); + +type CreatePostInput = TypeOf; + +type ICreatePostProp = { + setOpenPostModal: (openPostModal: boolean) => void; +}; + +const CreatePost: FC = ({ setOpenPostModal }) => { + const store = useStore(); + const queryClient = useQueryClient(); + const { isLoading, mutate: createPost } = trpc.createPost.useMutation({ + onSuccess(data) { + store.setPageLoading(false); + setOpenPostModal(false); + queryClient.refetchQueries([["getPosts"]]); + toast("Post created successfully", { + type: "success", + position: "top-right", + }); + }, + onError(error: any) { + store.setPageLoading(false); + setOpenPostModal(false); + toast(error.message, { + type: "error", + position: "top-right", + }); + }, + }); + const methods = useForm({ + resolver: zodResolver(createPostSchema), + }); + + const { + register, + handleSubmit, + formState: { errors }, + } = methods; + + useEffect(() => { + if (isLoading) { + store.setPageLoading(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoading]); + + const onSubmitHandler: SubmitHandler = async (data) => { + createPost(data); + }; + return ( +
+

Create Post

+ + +
+ + +
+ +