From 8959a4428b7f5f9681775fd448d86f69b2f03aa7 Mon Sep 17 00:00:00 2001 From: Gabriel Borges <31298094+gabrielborgesdm@users.noreply.github.com> Date: Sat, 25 May 2024 01:08:01 -0300 Subject: [PATCH] Frontend fixes, Author form * fix: use layout as a parent element * feat: define a height limit for tables * feat(frontend): create author form --- frontend/package-lock.json | 65 +++++++++++++- frontend/package.json | 5 +- frontend/src/components/AuthorForm.tsx | 84 +++++++++++++++++++ frontend/src/components/Form/Form.tsx | 48 +++++++++++ .../src/components/Form/FormSubmitButtons.tsx | 34 ++++++++ frontend/src/components/Form/Input.tsx | 31 +++++++ frontend/src/components/Layout.tsx | 16 ++-- .../src/components/{Navbar => }/Navbar.tsx | 0 .../src/components/Schemas/AuthorSchema.tsx | 13 +++ frontend/src/index.tsx | 27 ++++-- frontend/src/pages/Authors/AuthorCreate.tsx | 11 +++ frontend/src/pages/Authors/Authors.tsx | 84 ++++++++++--------- frontend/src/pages/Books/Books.tsx | 69 +++++++-------- frontend/src/services/managementService.ts | 15 +++- frontend/src/types/author.ts | 8 +- frontend/src/types/book.ts | 6 ++ 16 files changed, 424 insertions(+), 92 deletions(-) create mode 100644 frontend/src/components/AuthorForm.tsx create mode 100644 frontend/src/components/Form/Form.tsx create mode 100644 frontend/src/components/Form/FormSubmitButtons.tsx create mode 100644 frontend/src/components/Form/Input.tsx rename frontend/src/components/{Navbar => }/Navbar.tsx (100%) create mode 100644 frontend/src/components/Schemas/AuthorSchema.tsx create mode 100644 frontend/src/pages/Authors/AuthorCreate.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3da7745..defbda2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^3.4.2", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -18,10 +19,12 @@ "axios": "^1.7.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.51.5", "react-router-dom": "^6.23.1", "react-scripts": "5.0.1", "typescript": "^4.9.5", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "yup": "^1.4.0" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", @@ -2414,6 +2417,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.4.2.tgz", + "integrity": "sha512-1m9uAVIO8wVf7VCDAGsuGA0t6Z3m6jVGAN50HkV9vYLl0yixKK/Z1lr01vaRvYCkIKGoy1noVRxMzQYb4y/j1Q==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -14521,6 +14532,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -14886,6 +14902,21 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-hook-form": { + "version": "7.51.5", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.5.tgz", + "integrity": "sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -16741,6 +16772,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -16773,6 +16809,11 @@ "node": ">=0.6" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -18228,6 +18269,28 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 3217fc0..13bfda7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@hookform/resolvers": "^3.4.2", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -13,10 +14,12 @@ "axios": "^1.7.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.51.5", "react-router-dom": "^6.23.1", "react-scripts": "5.0.1", "typescript": "^4.9.5", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "yup": "^1.4.0" }, "scripts": { "start": "react-scripts start", diff --git a/frontend/src/components/AuthorForm.tsx b/frontend/src/components/AuthorForm.tsx new file mode 100644 index 0000000..1d9edb3 --- /dev/null +++ b/frontend/src/components/AuthorForm.tsx @@ -0,0 +1,84 @@ +import React, { useState } from "react"; +import { AuthorCreate } from "types/author"; +import Input from "./Form/Input"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup" +import AuthorSchema from "./Schemas/AuthorSchema"; +import Form, { FormMessage } from "./Form/Form"; +import ManagementService from "services/managementService"; + +const managementService = new ManagementService() + + +const AuthorForm: React.FC = () => { + const messageInitialState = { value: "", isError: false } + const [isLoading, setIsLoading] = useState(false) + const [message, setMessage] = useState(messageInitialState) + const { + register, + handleSubmit, + reset, + + formState: { errors }, + } = useForm({ resolver: yupResolver(AuthorSchema) }) + + + + const onSubmit: SubmitHandler = async (data: AuthorCreate) => { + setIsLoading(true) + const author = await managementService.createAuthor(data) + console.log(author, author === undefined) + setMessage({ + value: author ? "Author created with success" : "It wansn't possible to create the author, try again", + isError: author === undefined + }) + setIsLoading(false) + // onReset() + } + + const onReset = () => { + setMessage(messageInitialState) + reset(undefined) + } + + return ( +
+ <> + + + + + + + +
+ ) +} + +export default AuthorForm diff --git a/frontend/src/components/Form/Form.tsx b/frontend/src/components/Form/Form.tsx new file mode 100644 index 0000000..da950ea --- /dev/null +++ b/frontend/src/components/Form/Form.tsx @@ -0,0 +1,48 @@ +import React, { ReactNode } from "react"; +import FormControlButtons from "./FormSubmitButtons"; + +export interface FormMessage { + value: string; + isError: boolean; +} +interface FormProps { + title: string; + description: string; + isLoading: boolean; + onSubmit: () => Promise; + onReset: () => void; + message: FormMessage; + children: ReactNode; +} + +const Form: React.FC = ({ title, description, onSubmit, onReset, message, isLoading, children }) => { + return ( +
+ {message.value && +
+ {message.isError ? "Ops!" : "Yes!"} + {message.value} +
+ } +
+
+

{title}

+

{description}

+
+
+ {children} +
+
+
+ + + + ) +} + +export default Form \ No newline at end of file diff --git a/frontend/src/components/Form/FormSubmitButtons.tsx b/frontend/src/components/Form/FormSubmitButtons.tsx new file mode 100644 index 0000000..7b4b4c3 --- /dev/null +++ b/frontend/src/components/Form/FormSubmitButtons.tsx @@ -0,0 +1,34 @@ + +interface FormControlButtonsProps { + reset: () => void; + isLoading: boolean; +} +const FormControlButtons: React.FC = ({ isLoading, reset }) => { + + return ( +
+ + +
+ ) +} + + +export default FormControlButtons \ No newline at end of file diff --git a/frontend/src/components/Form/Input.tsx b/frontend/src/components/Form/Input.tsx new file mode 100644 index 0000000..2fcfc9a --- /dev/null +++ b/frontend/src/components/Form/Input.tsx @@ -0,0 +1,31 @@ +import { InputHTMLAttributes } from "react"; +import { UseFormRegister } from "react-hook-form"; + +interface InputProps extends InputHTMLAttributes { + label: string; + name: string; + register: UseFormRegister; + errorMessage?: string +} + +const Input: React.FC = ({ label, name, register, errorMessage, ...otherOptions }) => { + + return ( +
+ +
+ +

{errorMessage}

+
+
+ ) +} + +export default Input \ No newline at end of file diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 2fd39b9..b659f43 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,14 +1,14 @@ -import { Navbar } from "./Navbar/Navbar"; +import { Outlet } from "react-router-dom"; +import { Navbar } from "./Navbar"; -interface LayoutProps { - element: React.ReactNode; -} - -const Layout: React.FC = ({ element }) => ( +const Layout: React.FC = () => ( <> - {element} +
+ +
); -export default Layout; \ No newline at end of file +export default Layout + diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar.tsx similarity index 100% rename from frontend/src/components/Navbar/Navbar.tsx rename to frontend/src/components/Navbar.tsx diff --git a/frontend/src/components/Schemas/AuthorSchema.tsx b/frontend/src/components/Schemas/AuthorSchema.tsx new file mode 100644 index 0000000..6dc8bf8 --- /dev/null +++ b/frontend/src/components/Schemas/AuthorSchema.tsx @@ -0,0 +1,13 @@ +import * as yup from "yup" + +const AuthorSchema = yup + .object({ + name: yup.string().required().min(1).max(300), + email: yup.string().email().min(1).max(320), + nationality: yup.string().min(1).max(100), + birthDate: yup.string().matches(/^(?:(?:19|20)\d{2})-(?:(?:0[1-9]|1[0-2]))-(?:(?:0[1-9]|[12]\d|3[01]))$/, { message: "Follow the format AAAA-MM-DD" }), + }) + .required() + + +export default AuthorSchema diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 2edafc8..b0542fd 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -6,20 +6,33 @@ import { } from "react-router-dom"; import Books from 'pages/Books/Books'; -import './index.css'; -import { Navbar } from 'components/Navbar/Navbar'; import Authors from 'pages/Authors/Authors'; import Layout from 'components/Layout'; +import './index.css'; +import AuthorCreate from 'pages/Authors/AuthorCreate'; + const router = createBrowserRouter([ { path: "/", - element: } />, - }, - { - path: "/authors", - element: } />, + element: , + children: [ + { + path: "", + element: , + }, + { + path: "authors", + element: , + + }, + { + path: "authors/create", + element: , + } + ] }, + ]); const root = ReactDOM.createRoot( diff --git a/frontend/src/pages/Authors/AuthorCreate.tsx b/frontend/src/pages/Authors/AuthorCreate.tsx new file mode 100644 index 0000000..1f30417 --- /dev/null +++ b/frontend/src/pages/Authors/AuthorCreate.tsx @@ -0,0 +1,11 @@ +import AuthorForm from "components/AuthorForm"; +import React from "react"; + + +const AuthorCreate: React.FC = () => { + return ( + + ) +} + +export default AuthorCreate \ No newline at end of file diff --git a/frontend/src/pages/Authors/Authors.tsx b/frontend/src/pages/Authors/Authors.tsx index 7990143..f2bf9b1 100644 --- a/frontend/src/pages/Authors/Authors.tsx +++ b/frontend/src/pages/Authors/Authors.tsx @@ -22,7 +22,7 @@ const Authors: React.FC = () => { return ( -
+ <>
@@ -33,51 +33,55 @@ const Authors: React.FC = () => {
- +

- - - +
+
+ + - - - - - - - {authors?.length > 0 && authors.map(({ name, nationality, birthDate, email, id }: Author) => ( - shouldFilterInWith(name, nationality, birthDate, email, id) - && ( - - + + + - - - - ) - ))} -
- Author - - Nationality - - Birth date - - E-mail -
- {name} + + Author + + Nationality + + Birth date + + E-mail - {nationality} - - {birthDate} - - {email} -
- + + + {authors?.length > 0 && authors.map(({ name, nationality, birthDate, email, id }: Author) => ( + shouldFilterInWith(name, nationality, birthDate, email, id) + && ( + + + {name} + + + {nationality} + + + {birthDate} + + + {email} + + + ) + ))} + + + + ); }; -export default Authors; \ No newline at end of file +export default Authors; diff --git a/frontend/src/pages/Books/Books.tsx b/frontend/src/pages/Books/Books.tsx index ca2f8e5..6c83f8f 100644 --- a/frontend/src/pages/Books/Books.tsx +++ b/frontend/src/pages/Books/Books.tsx @@ -25,7 +25,7 @@ const Books: React.FC = () => { return ( -
+ <>
@@ -41,41 +41,44 @@ const Books: React.FC = () => {

- - - +
+
+ + - - - - - - {books?.length > 0 && books.map(({ id, title, pages, authorsNames }: Book) => ( - shouldFilterInWith(title, pages, id, authorsNames) - && ( - - + + - - - ) - ))} -
- Book title - - Pages - - Authors -
- {title} + + Book title + + Pages + + Authors - {Number.isInteger(pages) ? `${pages} pages` : "Not informed"} - - { - {authorsNames} - } -
-
+ + + {books?.length > 0 && books.map(({ id, title, pages, authorsNames }: Book) => ( + shouldFilterInWith(title, pages, id, authorsNames) && ( + + + {title} + + + {Number.isInteger(pages) ? `${pages} pages` : "Not informed"} + + + { + {authorsNames} + } + + + ) + ))} + + +
+ ); }; diff --git a/frontend/src/services/managementService.ts b/frontend/src/services/managementService.ts index b35c6d5..0c089bc 100644 --- a/frontend/src/services/managementService.ts +++ b/frontend/src/services/managementService.ts @@ -1,6 +1,6 @@ import { Book } from "types/book" import axios from "services/requestClientService" -import { Author } from "types/author" +import { Author, AuthorCreate } from "types/author" export default class ManagementService { baseUrl = process.env.REACT_APP_API_URL @@ -38,4 +38,17 @@ export default class ManagementService { return authors } + + createAuthor = async (data: AuthorCreate): Promise => { + try { + const url = `${this.baseUrl}/authors/` + const response = await axios.post(url, data) + + return response.data + } catch (error) { + console.log(error) + + } + + } } diff --git a/frontend/src/types/author.ts b/frontend/src/types/author.ts index f14f93e..5bb6a83 100644 --- a/frontend/src/types/author.ts +++ b/frontend/src/types/author.ts @@ -4,5 +4,11 @@ export interface Author { email?: string; nationality: string; birthDate?: string; - books: [] +} + +export interface AuthorCreate { + name?: string; + email?: string; + nationality?: string; + birthDate?: string; } \ No newline at end of file diff --git a/frontend/src/types/book.ts b/frontend/src/types/book.ts index ebf2a7e..d9ad508 100644 --- a/frontend/src/types/book.ts +++ b/frontend/src/types/book.ts @@ -6,4 +6,10 @@ export interface Book { pages?: number; authors: Author[], authorsNames?: string; +} + +export interface BookCreate { + title: string; + pages?: number; + authors?: Author[], } \ No newline at end of file