From 68d72d3c3ba77955d19d4abb6700d366fdd6a71c Mon Sep 17 00:00:00 2001 From: Gabriel Borges Date: Sun, 11 Aug 2024 19:41:01 -0300 Subject: [PATCH 1/5] fix: application setup --- frontend/Dockerfile | 5 ++--- frontend/package-lock.json | 3 ++- frontend/package.json | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 9c3d6a3..366cfd3 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -4,6 +4,7 @@ FROM node:20 # Set the working directory in the container WORKDIR /frontend + # Copy the current directory contents into the container at /app COPY . /frontend @@ -12,6 +13,4 @@ RUN npm install EXPOSE 3000 -# Command to run the Python script - -CMD ["npm", "start"] +CMD ["npm", "run", "start"] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8382188..10f847c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,7 +21,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.51.5", "react-router-dom": "^6.23.1", - "react-scripts": "5.0.1", + "react-scripts": "^5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4", "yup": "^1.4.0" @@ -15054,6 +15054,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", + "license": "MIT", "dependencies": { "@babel/core": "^7.16.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", diff --git a/frontend/package.json b/frontend/package.json index 70831c4..5be1eae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.51.5", "react-router-dom": "^6.23.1", - "react-scripts": "5.0.1", + "react-scripts": "^5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4", "yup": "^1.4.0" @@ -27,7 +27,6 @@ "test": "react-scripts test", "eject": "react-scripts eject", "lint": "eslint --fix --ext .ts,.tsx ." - }, "browserslist": { "production": [ From 92c8e0bd30e38c3987afec82c2a6175b058c7605 Mon Sep 17 00:00:00 2001 From: Gabriel Borges Date: Sun, 11 Aug 2024 19:51:53 -0300 Subject: [PATCH 2/5] fix: backend migrations setup --- Makefile | 5 ++++- README.md | 4 ++++ backend/.gitignore | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 04cbeb6..f9c1d84 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ BACKEND_CONTAINER_NAME=python_backend -.PHONY: docker/start docker/start-build docker/clean backend/test backend/migrate backend/upgrade +.PHONY: docker/start docker/start-build docker/clean backend/test backend/migrate backend/upgrade backend/init # Docker commands: @@ -15,6 +15,9 @@ docker/clean: # Backend commands: +backend/init: + docker compose exec $(BACKEND_CONTAINER_NAME) python3 -m flask --app main db init + backend/migrate: @if [ "$(message)" = "" ]; then \ echo "Please provide a migration message. Usage: make migrate message='Your migration message'"; \ diff --git a/README.md b/README.md index 3348031..63dcac5 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ Frontend Application (React): Provides a user-friendly interface for browsing an 2. To start the services run `make docker/start` +3. After starting the services, run the following make commands to setup the database and schemas: `backend/init`, `backend/migration`, `backend/upgrade` + ### Makefile Commands This Makefile provides convenient targets to automate common development tasks. @@ -47,6 +49,8 @@ This Makefile provides convenient targets to automate common development tasks. - **docker/clean**: Remove volumes and delete all images associated with the containers defined in the `docker-compose.yml` file. +- **backend/init**: Inits Flask migration folder. + - **backend/migrate**: Runs Flask migration with a specified message. - **backend/upgrade**: Upgrades the database schema using Flask. diff --git a/backend/.gitignore b/backend/.gitignore index e1c5f70..02b404d 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -24,7 +24,6 @@ var/ *.egg-info/ .installed.cfg *.egg - # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. @@ -90,4 +89,5 @@ ENV/ # Rope project settings .ropeproject -*.code-workspace \ No newline at end of file +*.code-workspace +migrations/ \ No newline at end of file From 4551001336f391728fb7b4f1130c79ff9601e4cb Mon Sep 17 00:00:00 2001 From: Gabriel Borges Date: Sun, 11 Aug 2024 19:55:06 -0300 Subject: [PATCH 3/5] refactor: remove unused file --- frontend/src/components/AuthorForm.tsx | 63 -------------------------- 1 file changed, 63 deletions(-) delete mode 100644 frontend/src/components/AuthorForm.tsx diff --git a/frontend/src/components/AuthorForm.tsx b/frontend/src/components/AuthorForm.tsx deleted file mode 100644 index b2161b9..0000000 --- a/frontend/src/components/AuthorForm.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useState } from "react"; -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"; -import { AuthorFormCreate } from "types/author"; - -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: AuthorFormCreate) => { - 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; From df2065c4ca9425cb57fdb5f5cf00c3dcfdf21b65 Mon Sep 17 00:00:00 2001 From: Gabriel Borges Date: Sun, 11 Aug 2024 20:12:12 -0300 Subject: [PATCH 4/5] refactor: Add swr for better fetch performance and loading state control --- frontend/package-lock.json | 29 ++++++++++++ frontend/package.json | 1 + frontend/src/pages/Authors/Authors.tsx | 64 ++++++++++++-------------- 3 files changed, 60 insertions(+), 34 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 10f847c..8cb93f8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,6 +22,7 @@ "react-hook-form": "^7.51.5", "react-router-dom": "^6.23.1", "react-scripts": "^5.0.1", + "swr": "^2.2.5", "typescript": "^4.9.5", "web-vitals": "^2.1.4", "yup": "^1.4.0" @@ -5919,6 +5920,12 @@ "node": ">=0.10.0" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -16661,6 +16668,19 @@ "boolbase": "~1.0.0" } }, + "node_modules/swr": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz", + "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==", + "license": "MIT", + "dependencies": { + "client-only": "^0.0.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -17291,6 +17311,15 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5be1eae..dccb9af 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "react-hook-form": "^7.51.5", "react-router-dom": "^6.23.1", "react-scripts": "^5.0.1", + "swr": "^2.2.5", "typescript": "^4.9.5", "web-vitals": "^2.1.4", "yup": "^1.4.0" diff --git a/frontend/src/pages/Authors/Authors.tsx b/frontend/src/pages/Authors/Authors.tsx index d1b051b..62d0258 100644 --- a/frontend/src/pages/Authors/Authors.tsx +++ b/frontend/src/pages/Authors/Authors.tsx @@ -1,23 +1,14 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import ManagementService from "services/managementService"; import { useFilterHook } from "components/Hooks/UseFilterHook"; import { Author } from "types/author"; +import useSWR from "swr"; const managementService = new ManagementService(); const Authors: React.FC = () => { - const [authors, setAuthors] = useState([]); const { handleChangeFilter, shouldFilterInWith } = useFilterHook(); - - useEffect(() => { - loadAuthors(); - }, []); - - const loadAuthors = async () => { - const authors = await managementService.getAuthors(); - - setAuthors(authors); - }; + const { data: authorsData, error, isLoading } = useSWR("authors", () => managementService.getAuthors()); return ( <> @@ -60,26 +51,26 @@ const Authors: React.FC = () => {
- - - - - - - - - - - {authors?.length > 0 && - authors.map( + {authorsData && authorsData.length > 0 ? ( +
- Author - - Nationality - - Birth date - - E-mail -
+ + + + + + + + + + {authorsData.map( ({ name, nationality, birthDate, email, id }: Author) => shouldFilterInWith(name, nationality, birthDate, email, id) && ( @@ -92,8 +83,13 @@ const Authors: React.FC = () => { ), )} - -
+ Author + + Nationality + + Birth date + + E-mail +
+ + + ) : isLoading ? ( + Loading... + ) : ( + No authors registered + )}
); From 8d408da89349c6ca6bcfec09f6b5331a07a9e10c Mon Sep 17 00:00:00 2001 From: Gabriel Borges Date: Sun, 11 Aug 2024 20:36:43 -0300 Subject: [PATCH 5/5] perf: :zap: improves authors filtering Starts using react useMemo, so It only calculates the filteredAuthors if either the filter or the fetched data changes. Also, it is now filtering before mapping which also improves the performance --- .../src/components/Hooks/UseFilterHook.tsx | 28 +++--------- frontend/src/pages/Authors/Authors.tsx | 43 +++++++++++-------- frontend/src/pages/Books/Books.tsx | 6 +-- 3 files changed, 34 insertions(+), 43 deletions(-) diff --git a/frontend/src/components/Hooks/UseFilterHook.tsx b/frontend/src/components/Hooks/UseFilterHook.tsx index 1d9b6df..28043e1 100644 --- a/frontend/src/components/Hooks/UseFilterHook.tsx +++ b/frontend/src/components/Hooks/UseFilterHook.tsx @@ -1,33 +1,17 @@ -import { ChangeEvent, useState } from "react"; +import { useState } from "react"; export const useFilterHook = () => { const [filter, setFilter] = useState(null); - const handleChangeFilter = (e: ChangeEvent): void => { - setFilter(e.target.value); - }; - - const shouldFilterInWith = (...properties: (unknown | undefined)[]): boolean => { + const isFilterFoundInProperties = (...properties: (unknown | undefined)[]): boolean => { if (!filter) return true; - let anyPropertyMatch = false; - - for (const property of properties) { - if (typeof property === "undefined") { - continue; - } - - if (String(property).toLowerCase().includes(filter.toLowerCase())) { - anyPropertyMatch = true; - break; - } - } - - return anyPropertyMatch; + return properties.some((property) => property && String(property).toLowerCase().includes(filter.toLowerCase())); }; return { - handleChangeFilter, - shouldFilterInWith, + filter, + setFilter, + isFilterFoundInProperties, }; }; diff --git a/frontend/src/pages/Authors/Authors.tsx b/frontend/src/pages/Authors/Authors.tsx index 62d0258..6dcbe0c 100644 --- a/frontend/src/pages/Authors/Authors.tsx +++ b/frontend/src/pages/Authors/Authors.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import ManagementService from "services/managementService"; import { useFilterHook } from "components/Hooks/UseFilterHook"; import { Author } from "types/author"; @@ -7,8 +7,16 @@ import useSWR from "swr"; const managementService = new ManagementService(); const Authors: React.FC = () => { - const { handleChangeFilter, shouldFilterInWith } = useFilterHook(); - const { data: authorsData, error, isLoading } = useSWR("authors", () => managementService.getAuthors()); + const { filter, setFilter, isFilterFoundInProperties } = useFilterHook(); + const { data: authorsData, isLoading } = useSWR("authors", () => managementService.getAuthors()); + + const filteredAuthors = useMemo(() => { + if (!filter) return authorsData; + + return authorsData?.filter(({ name, nationality, birthDate, email, id }: Author) => + isFilterFoundInProperties(name, nationality, birthDate, email, id), + ); + }, [authorsData, filter]); return ( <> @@ -37,7 +45,7 @@ const Authors: React.FC = () => { setFilter(e.target.value)} className="block py-3 ps-10 text-sm border rounded-lg w-80 focus:ring-blue-500 focus:border-blue-500 " placeholder="Search for items" /> @@ -51,7 +59,7 @@ const Authors: React.FC = () => {
- {authorsData && authorsData.length > 0 ? ( + {filteredAuthors && filteredAuthors.length > 0 ? ( @@ -70,19 +78,18 @@ const Authors: React.FC = () => { - {authorsData.map( - ({ name, nationality, birthDate, email, id }: Author) => - shouldFilterInWith(name, nationality, birthDate, email, id) && ( - - - - - - - ), - )} + {filteredAuthors.map(({ id, name, nationality, birthDate, email }) => ( + <> + + + + + + + + ))}
- {name} - {nationality}{birthDate}{email}
+ {name} + {nationality}{birthDate}{email}
) : isLoading ? ( diff --git a/frontend/src/pages/Books/Books.tsx b/frontend/src/pages/Books/Books.tsx index 75ef893..5b933f4 100644 --- a/frontend/src/pages/Books/Books.tsx +++ b/frontend/src/pages/Books/Books.tsx @@ -7,7 +7,7 @@ const managementService = new ManagementService(); const Books: React.FC = () => { const [books, setBooks] = useState([]); - const { handleChangeFilter, shouldFilterInWith } = useFilterHook(); + const { setFilter, isFilterFoundInProperties } = useFilterHook(); useEffect(() => { loadBooks(); @@ -49,7 +49,7 @@ const Books: React.FC = () => { setFilter(e.target.value)} className="block py-3 ps-10 text-sm border rounded-lg w-80 focus:ring-blue-500 focus:border-blue-500 " placeholder="Search for items" /> @@ -81,7 +81,7 @@ const Books: React.FC = () => { {books?.length > 0 && books.map( ({ id, title, pages, authorsNames }: Book) => - shouldFilterInWith(title, pages, id, authorsNames) && ( + isFilterFoundInProperties(title, pages, id, authorsNames) && ( {title}