diff --git a/docker-compose.yaml b/docker-compose.yaml index 0d94829..27ba508 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,10 +15,23 @@ services: - ./backend:/backend environment: - FLASK_ENV=development + - DATABASE_URI=postgresql://postgres:changeme@postgres:5432/postgres + networks: - my_network command: python3 -m flask --app main run --debug --host=0.0.0.0 + react_frontend: + build: ./frontend + ports: + - "3000:3000" + volumes: + - ./frontend:/frontend + environment: + - REACT_APP_API_URL=http://localhost:5000 + networks: + - my_network + postgres: container_name: postgres_container image: postgres diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..9c3d6a3 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,17 @@ +# Use the official Python image as base image +FROM node:20 + +# Set the working directory in the container +WORKDIR /frontend + +# Copy the current directory contents into the container at /app +COPY . /frontend + +# Install any dependencies +RUN npm install + +EXPOSE 3000 + +# Command to run the Python script + +CMD ["npm", "start"] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3d2242e..3da7745 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,6 +18,7 @@ "axios": "^1.7.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^6.23.1", "react-scripts": "5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" @@ -3320,6 +3321,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -14890,6 +14899,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "dependencies": { + "@remix-run/router": "1.16.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "dependencies": { + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 796537b..3217fc0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "axios": "^1.7.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^6.23.1", "react-scripts": "5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx deleted file mode 100644 index 9667c05..0000000 --- a/frontend/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/hi/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 427a44c..0000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from "react" - -function App() { - - return ( -

Hi

- ); -} - -export default App; diff --git a/frontend/src/components/Hooks/UseFilterHook.tsx b/frontend/src/components/Hooks/UseFilterHook.tsx new file mode 100644 index 0000000..29814d5 --- /dev/null +++ b/frontend/src/components/Hooks/UseFilterHook.tsx @@ -0,0 +1,33 @@ +import { ChangeEvent, 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 => { + 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 { + handleChangeFilter, + shouldFilterInWith, + } +} \ No newline at end of file diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..2fd39b9 --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,14 @@ +import { Navbar } from "./Navbar/Navbar"; + +interface LayoutProps { + element: React.ReactNode; +} + +const Layout: React.FC = ({ element }) => ( + <> + + {element} + +); + +export default Layout; \ No newline at end of file diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx new file mode 100644 index 0000000..16a7c8d --- /dev/null +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -0,0 +1,63 @@ +import React, { useState } from "react"; +import { useLocation } from 'react-router-dom'; + +export const Navbar: React.FC = () => { + + const location = useLocation(); + + const [nav, setNav] = useState(false); + + const handleNav = () => { + setNav(!nav); + }; + + const verifyCurrentRouteAndApplyStylingClasses = (path: string) => { + console.log(location.pathname) + if (location.pathname === path) { + return "bg-gray-900 text-white rounded-md px-3 py-2 text-sm font-medium" + } + + return "text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium" + } + + + return ( + + ) +} \ No newline at end of file diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 032464f..2edafc8 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,19 +1,34 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; +import { + createBrowserRouter, + RouterProvider, +} from "react-router-dom"; + +import Books from 'pages/Books/Books'; import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import { Navbar } from 'components/Navbar/Navbar'; +import Authors from 'pages/Authors/Authors'; +import Layout from 'components/Layout'; + +const router = createBrowserRouter([ + { + path: "/", + element: } />, + }, + { + path: "/authors", + element: } />, + }, +]); const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); + root.render( - + ); -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); diff --git a/frontend/src/pages/Authors/Authors.tsx b/frontend/src/pages/Authors/Authors.tsx new file mode 100644 index 0000000..7990143 --- /dev/null +++ b/frontend/src/pages/Authors/Authors.tsx @@ -0,0 +1,83 @@ +import React, { useEffect, useState } from 'react'; +import ManagementService from 'services/managementService'; +import { useFilterHook } from 'components/Hooks/UseFilterHook'; +import { Author } from 'types/author'; + +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) + } + + + return ( +
+
+ +
+
+ +
+ +
+ +
+
+ + + + + + + + + + + {authors?.length > 0 && authors.map(({ name, nationality, birthDate, email, id }: Author) => ( + shouldFilterInWith(name, nationality, birthDate, email, id) + && ( + + + + + + + ) + ))} +
+ Author + + Nationality + + Birth date + + E-mail +
+ {name} + + {nationality} + + {birthDate} + + {email} +
+
+ ); +}; + +export default Authors; \ No newline at end of file diff --git a/frontend/src/pages/Books/Books.tsx b/frontend/src/pages/Books/Books.tsx new file mode 100644 index 0000000..ca2f8e5 --- /dev/null +++ b/frontend/src/pages/Books/Books.tsx @@ -0,0 +1,82 @@ +import React, { useEffect, useState } from 'react'; +import { Book } from 'types/book'; +import ManagementService from 'services/managementService'; +import { useFilterHook } from 'components/Hooks/UseFilterHook'; + +const managementService = new ManagementService() + + +const Books: React.FC = () => { + const [books, setBooks] = useState([]) + const { handleChangeFilter, shouldFilterInWith } = useFilterHook(); + + useEffect(() => { + loadBooks() + }, []) + + const loadBooks = async () => { + const books = await managementService.getBooks() + books.forEach(book => { + book.authorsNames = book.authors.map(author => author.name).join("; ") + }) + + setBooks(books) + } + + + return ( +
+
+ +
+
+ +
+ +
+ +
+
+ + + + + + + + + + {books?.length > 0 && books.map(({ id, title, pages, authorsNames }: Book) => ( + shouldFilterInWith(title, pages, id, authorsNames) + && ( + + + + + + ) + ))} +
+ Book title + + Pages + + Authors +
+ {title} + + {Number.isInteger(pages) ? `${pages} pages` : "Not informed"} + + { + {authorsNames} + } +
+
+ ); +}; + +export default Books; \ No newline at end of file diff --git a/frontend/src/reportWebVitals.ts b/frontend/src/reportWebVitals.ts deleted file mode 100644 index 49a2a16..0000000 --- a/frontend/src/reportWebVitals.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ReportHandler } from 'web-vitals'; - -const reportWebVitals = (onPerfEntry?: ReportHandler) => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/frontend/src/services/managementService.ts b/frontend/src/services/managementService.ts new file mode 100644 index 0000000..b35c6d5 --- /dev/null +++ b/frontend/src/services/managementService.ts @@ -0,0 +1,41 @@ +import { Book } from "types/book" +import axios from "services/requestClientService" +import { Author } from "types/author" + +export default class ManagementService { + baseUrl = process.env.REACT_APP_API_URL + + getBooks = async (): Promise => { + let books = [] + try { + const url = `${this.baseUrl}/books/` + const response = await axios.get(url) + + if (response.data?.length) { + books = response.data + } + } catch (error) { + console.log(error) + + } + + return books + } + + getAuthors = async (): Promise => { + let authors = [] + try { + const url = `${this.baseUrl}/authors/` + const response = await axios.get(url) + + if (response.data?.length) { + authors = response.data + } + } catch (error) { + console.log(error) + + } + + return authors + } +} diff --git a/frontend/src/services/requestClientService.ts b/frontend/src/services/requestClientService.ts new file mode 100644 index 0000000..937bdd6 --- /dev/null +++ b/frontend/src/services/requestClientService.ts @@ -0,0 +1,9 @@ +import axios from "axios" + +const getAxiosClient = () => { + axios.defaults.headers['Access-Control-Allow-Origin'] = "*" + + return axios +} + +export default getAxiosClient() \ No newline at end of file diff --git a/frontend/src/types/author.ts b/frontend/src/types/author.ts new file mode 100644 index 0000000..f14f93e --- /dev/null +++ b/frontend/src/types/author.ts @@ -0,0 +1,8 @@ +export interface Author { + id: number; + name: string; + email?: string; + nationality: string; + birthDate?: string; + books: [] +} \ No newline at end of file diff --git a/frontend/src/types/book.ts b/frontend/src/types/book.ts new file mode 100644 index 0000000..ebf2a7e --- /dev/null +++ b/frontend/src/types/book.ts @@ -0,0 +1,9 @@ +import { Author } from "./author"; + +export interface Book { + id: number; + title: string; + pages?: number; + authors: Author[], + authorsNames?: string; +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f66f22c..0000000 --- a/requirements.txt +++ /dev/null @@ -1,85 +0,0 @@ -alembic==1.13.1 -annotated-types==0.6.0 -astroid==3.1.0 -attrs==23.2.0 -black==24.4.2 -blinker==1.8.2 -build==1.2.1 -cachetools==5.3.3 -certifi==2024.2.2 -cffi==1.16.0 -charset-normalizer==3.3.2 -click==8.1.7 -cryptography==42.0.6 -dill==0.3.6 -dnslib==0.9.24 -dnspython==2.6.1 -ecdsa==0.19.0 -email_validator==2.1.1 -exceptiongroup==1.2.1 -flake8==7.0.0 -flake8-black==0.3.6 -Flask==3.0.3 -flask-marshmallow==1.2.1 -Flask-Migrate==4.0.7 -flask-openapi3==3.1.1 -Flask-SQLAlchemy==3.1.1 -greenlet==3.0.3 -idna==3.7 -inflection==0.5.1 -iniconfig==2.0.0 -isort==5.13.2 -itsdangerous==2.2.0 -Jinja2==3.1.4 -jsonschema==4.22.0 -jsonschema-specifications==2023.12.1 -localstack-core==3.4.0 -localstack-ext==3.4.0 -Mako==1.3.3 -markdown-it-py==3.0.0 -MarkupSafe==2.1.5 -marshmallow==3.21.2 -marshmallow-sqlalchemy==1.0.0 -mccabe==0.7.0 -mdurl==0.1.2 -mistune==3.0.2 -mypy-extensions==1.0.0 -packaging==24.0 -pathspec==0.12.1 -pbr==6.0.0 -platformdirs==4.2.1 -pluggy==1.5.0 -plux==1.9.0 -psutil==5.9.8 -psycopg2-binary==2.9.9 -pyaes==1.6.1 -pyasn1==0.6.0 -pycodestyle==2.11.1 -pycparser==2.22 -pyflakes==3.2.0 -Pygments==2.18.0 -pylint==3.1.0 -pyproject_hooks==1.1.0 -pytest==8.2.0 -pytest-flask==1.3.0 -python-dateutil==2.9.0.post0 -python-dotenv==1.0.1 -python-jose==3.3.0 -PyYAML==6.0.1 -referencing==0.35.1 -requests==2.31.0 -rich==13.7.1 -rpds-py==0.18.1 -rsa==4.9 -semver==3.0.2 -six==1.16.0 -SQLAlchemy==2.0.30 -stevedore==5.2.0 -tabulate==0.9.0 -tailer==0.4.1 -tomli==2.0.1 -tomlkit==0.12.5 -typing_extensions==4.11.0 -typish==1.9.3 -urllib3==2.2.1 -Werkzeug==3.0.3