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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ Author
+ |
+
+ Nationality
+ |
+
+ Birth date
+ |
+
+ E-mail
+ |
+
+
+ {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
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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ Book title
+ |
+
+ Pages
+ |
+
+ Authors
+ |
+
+
+ {books?.length > 0 && books.map(({ id, title, pages, authorsNames }: Book) => (
+ shouldFilterInWith(title, pages, id, authorsNames)
+ && (
+
+
+ {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