diff --git a/backend/composer.json b/backend/composer.json index 748b5a3d..2ec05fe8 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -17,6 +17,7 @@ "symfony/dotenv": "7.0.*", "symfony/flex": "^2", "symfony/framework-bundle": "7.0.*", + "symfony/monolog-bundle": "^3.10", "symfony/property-access": "7.0.*", "symfony/property-info": "7.0.*", "symfony/runtime": "7.0.*", diff --git a/backend/composer.lock b/backend/composer.lock index 542eb5ce..5154f08b 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "886b551f6158dbe3f823f47e7aceb111", + "content-hash": "4e57d2d5b43f3fdc8b9208008aec493b", "packages": [ { "name": "doctrine/cache", @@ -1225,6 +1225,107 @@ }, "time": "2023-08-16T21:49:04+00:00" }, + { + "name": "monolog/monolog", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", + "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.4", + "phpunit/phpunit": "^10.5.17", + "predis/predis": "^1.1 || ^2", + "ruflin/elastica": "^7", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.6.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2024-04-12T21:02:21+00:00" + }, { "name": "nelmio/cors-bundle", "version": "2.4.0", @@ -2914,6 +3015,165 @@ ], "time": "2024-04-03T06:12:25+00:00" }, + { + "name": "symfony/monolog-bridge", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "aaa40a0a6512976a6e07d5def7ce9476862ebd65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/aaa40a0a6512976a6e07d5def7ce9476862ebd65", + "reference": "aaa40a0a6512976a6e07d5def7ce9476862ebd65", + "shasum": "" + }, + "require": { + "monolog/monolog": "^3", + "php": ">=8.2", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/mailer": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/monolog-bridge/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0", + "php": ">=7.2.5", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^6.3 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-06T17:08:13+00:00" + }, { "name": "symfony/polyfill-intl-grapheme", "version": "v1.29.0", diff --git a/backend/config/bundles.php b/backend/config/bundles.php index e4e91d7d..19000cca 100644 --- a/backend/config/bundles.php +++ b/backend/config/bundles.php @@ -6,4 +6,5 @@ Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], ]; diff --git a/backend/config/packages/dev/monolog.yaml b/backend/config/packages/dev/monolog.yaml new file mode 100644 index 00000000..1ca7cf96 --- /dev/null +++ b/backend/config/packages/dev/monolog.yaml @@ -0,0 +1,9 @@ +monolog: + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: warning + buffer_size: 50 + max_files: 1 + channels: ['!event', '!doctrine', '!console'] diff --git a/backend/config/packages/monolog.yaml b/backend/config/packages/monolog.yaml new file mode 100644 index 00000000..9db7d8a7 --- /dev/null +++ b/backend/config/packages/monolog.yaml @@ -0,0 +1,62 @@ +monolog: + channels: + - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists + +when@dev: + monolog: + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + channels: ["!event"] + # uncomment to get logging in your browser + # you may have to allow bigger header sizes in your Web server configuration + #firephp: + # type: firephp + # level: info + #chromephp: + # type: chromephp + # level: info + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine", "!console"] + +when@test: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + channels: ["!event"] + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + +when@prod: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + buffer_size: 50 # How many messages should be saved? Prevent memory leaks + nested: + type: stream + path: php://stderr + level: debug + formatter: monolog.formatter.json + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine"] + deprecation: + type: stream + channels: [deprecation] + path: php://stderr + formatter: monolog.formatter.json diff --git a/backend/config/packages/nelmio_cors.yaml b/backend/config/packages/nelmio_cors.yaml index c7665081..b2eae473 100644 --- a/backend/config/packages/nelmio_cors.yaml +++ b/backend/config/packages/nelmio_cors.yaml @@ -1,10 +1,24 @@ nelmio_cors: defaults: origin_regex: true - allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] - allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] + allow_origin: ['*'] + allow_methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] allow_headers: ['Content-Type', 'Authorization'] expose_headers: ['Link'] max_age: 3600 paths: - '^/': null + '^/movies/': + allow_origin: ['http://192.168.1.74:5173','http://localhost:5173'] + allow_methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] + allow_headers: ['Content-Type', 'Authorization'] + max_age: 3600 + '^/genres/': + allow_origin: ['http://192.168.1.74:5173','http://localhost:5173'] + allow_methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] + allow_headers: ['Content-Type', 'Authorization'] + max_age: 3600 + '^/actors/': + allow_origin: ['http://192.168.1.74:5173','http://localhost:5173'] + allow_methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] + allow_headers: ['Content-Type', 'Authorization'] + max_age: 3600 \ No newline at end of file diff --git a/backend/config/services.yaml b/backend/config/services.yaml index 2d6a76f9..a3f45b1b 100644 --- a/backend/config/services.yaml +++ b/backend/config/services.yaml @@ -6,6 +6,7 @@ parameters: services: + # default configuration for services in *this* file _defaults: autowire: true # Automatically injects dependencies in your services. @@ -20,5 +21,5 @@ services: - '../src/Entity/' - '../src/Kernel.php' - # add more service definitions when explicit configuration is needed - # please note that last definitions always *replace* previous ones + Psr\Log\LoggerInterface: '@monolog.logger' + diff --git a/backend/src/Controller/ActorController.php b/backend/src/Controller/ActorController.php new file mode 100644 index 00000000..7edece16 --- /dev/null +++ b/backend/src/Controller/ActorController.php @@ -0,0 +1,41 @@ +query->get('order', 'asc'); + $genres = $this->actorRepository->findByParams($order, true); + return $this->json($genres); + } catch (EntityNotFoundException $e) { + $this->logger->error("Data not found - INPUT: order: $order. \n" . $e->getMessage(). ' - FILE: '. $e->getTraceAsString() ); + return $this->json(['error' => 'Data not found'], Response::HTTP_NOT_FOUND); + } catch (\Exception $e) { + $this->logger->error("Error - INPUT: order: $order. \n" . $e->getMessage(). ' - FILE: '. $e->getTraceAsString() ); + return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + +} \ No newline at end of file diff --git a/backend/src/Controller/GenreController.php b/backend/src/Controller/GenreController.php new file mode 100644 index 00000000..a80059c0 --- /dev/null +++ b/backend/src/Controller/GenreController.php @@ -0,0 +1,44 @@ +query->get('order', 'asc'); + $genres = $this->genreRepository->findByParams($order, true); + return $this->json($genres); + } catch (EntityNotFoundException $e) { + $this->logger->error("Data not found - INPUT: order: $order. \n" . $e->getMessage(). ' - FILE: '. $e->getTraceAsString() ); + return $this->json(['error' => 'Data not found'], Response::HTTP_NOT_FOUND); + } catch (\Exception $e) { + $this->logger->error("Error - INPUT: order: $order. \n" . $e->getMessage(). ' - FILE: '. $e->getTraceAsString() ); + return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + +} \ No newline at end of file diff --git a/backend/src/Controller/MovieController.php b/backend/src/Controller/MovieController.php new file mode 100644 index 00000000..874c7a2f --- /dev/null +++ b/backend/src/Controller/MovieController.php @@ -0,0 +1,81 @@ +query->get('order') ) ) { + $explode = explode( '_', $request->query->get('order')); + $filters[$explode[0]] = $explode[1]; + } + $filters['genreId'] = $request->query->get('genre', null ); + $filters['actorId'] = $request->query->get('actor', null ); + + /* + Essendo il metodo findByFilter riutilizzabile in altri parti del codice, + è preferibile creare un array con i valori da passare, cosi da rendere il DTO riutilizzabile in più contesti. + E cosi permette la manipolazione del dato. + Altri approcci: + 1. Creazione di DTO utilizzando i valori GET direttamente non permette di manipolare i dati se necessario prima di passarli al DTO, ad esempio se ho necessita in uno dei campi di recuperare tramite entita un dato ad esso collegato + 2. Passaggio dell'intero oggetto Request al DTO: Rende il DTO meno riutilizzabile + */ + $movieSearchparams = MovieSearchParams::create($filters); + $movies = $this->movieRepository->findByFilter($movieSearchparams, true); + + /* + Vantaggi dell'Uso di Array Associativi + Performance: L'uso di array riduce l'overhead della creazione di oggetti, in questo conteto dove l'api deve restituire il risultato senza manipolarlo è più corretto. + Semplicità: Gli array sono spesso più semplici da gestire e trasformare in altre strutture + $data = $this->serializer->serialize($movies, "json", ["groups" => "default"]); + */ + + /* Se non si vuole utilizzare il bundle cors + $response = new JsonResponse(); + $response->headers->set('Access-Control-Allow-Origin', 'http://79.43.214.148:5173'); + $response->headers->set('Access-Control-Allow-Origin', 'http://localhost:5173'); + $response->setContent($data); + return $response; + */ + + return $this->json($movies); + } catch (EntityNotFoundException $e) { + $this->logger->error('Data not found - INPUT: '.print_r($movieSearchparams->toArray(),true) . $e->getMessage(). ' - FILE: '. $e->getTraceAsString() ); + return $this->json(['error' => 'Data not found'], Response::HTTP_NOT_FOUND); + } catch (Exception $e) { + $this->logger->error('Error - INPUT: '.print_r($movieSearchparams->toArray(),true) . $e->getMessage(). ' - FILE: '. $e->getTraceAsString() ); + return $this->json(['error' => 'Internal server error'], Response::HTTP_INTERNAL_SERVER_ERROR); + } + //QueryException e PDOException estendono Exception quindi non le intercetto in modo differente ma le gestisco con lo stesso catch + //In caso in una delle eccezioni fosse necessario fare una gestione particolare aggiungiamo il catch specifico + } + + +} diff --git a/backend/src/DTO/MovieSearchParams.php b/backend/src/DTO/MovieSearchParams.php new file mode 100644 index 00000000..037e599c --- /dev/null +++ b/backend/src/DTO/MovieSearchParams.php @@ -0,0 +1,49 @@ +releaseDate = $params['releaseDate'] ?? null; + $movieSearchParams->rating = $params['rating'] ?? null; + $movieSearchParams->genreId = $params['genreId'] ?? null; + $movieSearchParams->actorId = $params['actorId'] ?? null; + return $movieSearchParams; + } + + public function getReleaseDate(): ?string + { + return $this->releaseDate; + } + + public function getRating(): ?string + { + return $this->rating; + } + + public function getGenreId(): ?int + { + return $this->genreId; + } + public function getActorId(): ?int + { + return $this->actorId; + } + + public function toArray(): array + { + $array = [ + 'releaseDate' => $this->releaseDate, + 'rating' => $this->rating, + 'genreId' => $this->genreId, + 'actorId' => $this->actorId + ]; + return $array; + } +} diff --git a/backend/src/Repository/ActorRepository.php b/backend/src/Repository/ActorRepository.php index 309bf1e5..eda52120 100644 --- a/backend/src/Repository/ActorRepository.php +++ b/backend/src/Repository/ActorRepository.php @@ -21,6 +21,15 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, Actor::class); } + public function findByParams( ?string $order = 'asc', ?bool $toArray = false): array + { + $qb = $this->createQueryBuilder('a'); + $qb->addOrderBy('a.name', $order); + + $query = $qb->getQuery(); + return $toArray ? $query->getArrayResult() : $query->getResult(); + } + // /** // * @return Actor[] Returns an array of Actor objects // */ diff --git a/backend/src/Repository/GenreRepository.php b/backend/src/Repository/GenreRepository.php index cd863634..e1096fb5 100644 --- a/backend/src/Repository/GenreRepository.php +++ b/backend/src/Repository/GenreRepository.php @@ -21,6 +21,15 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, Genre::class); } + public function findByParams( ?string $order = 'asc', ?bool $toArray = false): array + { + $qb = $this->createQueryBuilder('g'); + $qb->addOrderBy('g.name', $order); + + $query = $qb->getQuery(); + return $toArray ? $query->getArrayResult() : $query->getResult(); + } + // /** // * @return Genre[] Returns an array of Genre objects // */ diff --git a/backend/src/Repository/MovieRepository.php b/backend/src/Repository/MovieRepository.php index f09e730b..f06964cf 100644 --- a/backend/src/Repository/MovieRepository.php +++ b/backend/src/Repository/MovieRepository.php @@ -5,6 +5,8 @@ use App\Entity\Movie; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; +use Doctrine\DBAL\ForwardCompatibility\Result; +use MovieSearchParams; /** * @extends ServiceEntityRepository @@ -21,6 +23,31 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, Movie::class); } + public function findByFilter(MovieSearchParams $filters, ?bool $toArray = false): array +{ + $qb = $this->createQueryBuilder('m'); + + if ($filters->getGenreId() !== null) { + $qb->innerJoin('m.movieGenres', 'mg')->andWhere('mg.genre = :genreId')->setParameter('genreId', $filters->getGenreId()); + } + + if ($filters->getActorId() !== null) { + $qb->innerJoin('m.movieActors', 'ma')->andWhere('ma.actor = :actorId')->setParameter('actorId', $filters->getActorId()); + } + + if ($filters->getReleaseDate() !== null) { + $qb->addOrderBy('m.releaseDate', $filters->getReleaseDate()); + } + + if ($filters->getRating() !== null) { + $qb->addOrderBy('m.rating', $filters->getRating()); + } + + $query = $qb->getQuery(); + + return $toArray ? $query->getArrayResult() : $query->getResult(); + } + // /** // * @return Movie[] Returns an array of Movie objects // */ diff --git a/backend/symfony.lock b/backend/symfony.lock index c4933231..65904a37 100644 --- a/backend/symfony.lock +++ b/backend/symfony.lock @@ -90,6 +90,18 @@ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" } }, + "symfony/monolog-bundle": { + "version": "3.10", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.7", + "ref": "aff23899c4440dd995907613c1dd709b6f59503f" + }, + "files": [ + "config/packages/monolog.yaml" + ] + }, "symfony/routing": { "version": "7.0", "recipe": { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 65936286..6659bc6b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,8 +13,8 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", @@ -23,7 +23,9 @@ "eslint-plugin-react-refresh": "^0.4.6", "postcss": "^8.4.38", "tailwindcss": "^3.4.3", - "vite": "^5.2.0" + "typescript": "^5.4.5", + "vite": "^5.2.0", + "vite-tsconfig-paths": "^4.3.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1314,9 +1316,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.79", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.79.tgz", - "integrity": "sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", + "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -1324,9 +1326,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.25", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.25.tgz", - "integrity": "sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "dev": true, "dependencies": { "@types/react": "*" @@ -2881,6 +2883,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -4931,6 +4939,26 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/tsconfck": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.3.tgz", + "integrity": "sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==", + "dev": true, + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5028,6 +5056,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -5142,6 +5183,25 @@ } } }, + "node_modules/vite-tsconfig-paths": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", + "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 21a4cbeb..8cc1831f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,8 +16,8 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", @@ -26,6 +26,8 @@ "eslint-plugin-react-refresh": "^0.4.6", "postcss": "^8.4.38", "tailwindcss": "^3.4.3", - "vite": "^5.2.0" + "typescript": "^5.4.5", + "vite": "^5.2.0", + "vite-tsconfig-paths": "^4.3.2" } } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx deleted file mode 100644 index 5cfedb3f..00000000 --- a/frontend/src/App.jsx +++ /dev/null @@ -1,133 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Button, Rating, Spinner } from 'flowbite-react'; - -const App = props => { - const [movies, setMovies] = useState([]); - const [loading, setLoading] = useState(true); - - const fetchMovies = () => { - setLoading(true); - - return fetch('http://localhost:8000/movies') - .then(response => response.json()) - .then(data => { - setMovies(data); - setLoading(false); - }); - } - - useEffect(() => { - fetchMovies(); - }, []); - - return ( - - - - - {movies.map((item, key) => ( - - ))} - - - ); -}; - -const Layout = props => { - return ( -
-
- {props.children} -
-
- ); -}; - -const Heading = props => { - return ( -
-

- Movie Collection -

- -

- Explore the whole collection of movies -

-
- ); -}; - -const MovieList = props => { - if (props.loading) { - return ( -
- -
- ); - } - - return ( -
- {props.children} -
- ); -}; - -const MovieItem = props => { - return ( -
-
- {props.title} -
- -
-
- {props.year || props.rating - ?
- {props.year} - - {props.rating - ? - - - - {props.rating} - - - : null - } -
- : null - } - -

- {props.title} -

- -

- {props.plot.substr(0, 80)}... -

-
- - {props.wikipediaUrl - ? - : null - } -
-
- ); -}; - -export default App; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 00000000..164ab807 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,89 @@ +import React, { useEffect, useState } from 'react'; + +import Heading from './components/Heading'; +import FilterMovies from './components/Movie/FilterMovie'; +import Layout from './components/Layout'; +import MovieItem from './components/Movie/MovieItem'; +import MovieList from './components/Movie/MovieList'; +import { getFormProps } from './services/form'; +import { + apiFetchActors, + apiFetchGenres, + apiFetchMovies } from './api/backend'; +import { + MovieInterface, MoviesInterface +} from './api/interface/movieInterface'; +import { GenresInterface } from './api/interface/genreInterface'; +import { ActorsInterface } from './api/interface/actorInterface'; +import { FormPropsInterface } from './services/interface/formPropsInterface'; + +const App:React.FC = props => { + const [movies, setMovies] = useState([]); + const [genres, setGenres] = useState([]); + const [actors, setActors] = useState([]); + const [loading, setLoading] = useState(true); + + const init = ():void => { + setLoading(true); + Promise.all([fetchMovies(), fetchGenres(), fetchActors()]) + .then(() => setLoading(false)) + .catch(() => setLoading(false)); + }; + + const handleSubmitForm = async (event: React.FormEvent) => { + event.preventDefault(); + const formProps:FormPropsInterface = getFormProps(event); + const params:string = `${Object.keys(formProps) + .filter((key) => formProps[key] !== '') + .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(formProps[key])}`) + .join('&')}`; + + const fetchedMovies:MoviesInterface|null = await apiFetchMovies(params); + if (fetchedMovies !== null) { + setMovies(fetchedMovies); + } + }; + + const fetchMovies = async () => { + const fetchedMovies: MoviesInterface|null = await apiFetchMovies(); + if (fetchedMovies !== null) { + setMovies(fetchedMovies); + } + }; + + const fetchGenres = async () => { + const fetchedGenres: GenresInterface|null = await apiFetchGenres(); + if (fetchedGenres !== null) { + setGenres(fetchedGenres); + } + }; + + const fetchActors = async () => { + const fetchedActors: ActorsInterface|null = await apiFetchActors(); + if (fetchedActors !== null) { + setActors(fetchedActors); + } + }; + + useEffect(() => { + init(); + }, []); + + return ( + + + + {movies && movies.length > 0 ? ( + + {movies.map((item: MovieInterface, key: number) => ( + + ))} + + ) : ( + 'La ricerca non ha prodotto risultati' + )} + + ); +}; + +export default App; \ No newline at end of file diff --git a/frontend/src/api/apiEndpoints.ts b/frontend/src/api/apiEndpoints.ts new file mode 100644 index 00000000..aabeaa37 --- /dev/null +++ b/frontend/src/api/apiEndpoints.ts @@ -0,0 +1,17 @@ +//Utilizzo il mac per sviluppare, ma ho un server linux in casa sul quale faccio girare i progetti di sviluppo con docker +//preferisco questa soluzione perchè le prestazioni di docker su mac che gira con una macchina virtuale sono troppo lente, +//rispetto alla soluzione di installarlo su una macchina fisica linux. Quindi questo è il mio ip +// const API_ENDPOINTS = { +// MOVIES: 'http://192.168.1.74:8000/movies', +// GENRES: 'http://192.168.1.74:8000/genres', +// ACTORS: 'http://192.168.1.74:8000/actors', +// }; + + +const API_ENDPOINTS = { + MOVIES: 'http://localhost:8000/movies', + GENRES: 'http://localhost:8000/genres', + ACTORS: 'http://localhost:8000/actors', +}; + +export default API_ENDPOINTS; \ No newline at end of file diff --git a/frontend/src/api/backend.ts b/frontend/src/api/backend.ts new file mode 100644 index 00000000..e5d51dfc --- /dev/null +++ b/frontend/src/api/backend.ts @@ -0,0 +1,56 @@ +import API_ENDPOINTS from "./apiEndpoints"; +import { ActorsInterface } from "./interface/actorInterface"; +import { GenresInterface } from "./interface/genreInterface"; +import { MoviesInterface } from "./interface/movieInterface"; + +const apiFetchMovies = async (params?:string):Promise => { + const url:string = params === undefined ? API_ENDPOINTS.MOVIES : `${API_ENDPOINTS.MOVIES}?${params}`; + return fetch(url) + .then(response => { + if (!response.ok) { + throw new Error('Errore nella richiesta HTTP'); + } + return response.json(); + }) + .then(data => { + return data; + }) + .catch(error => { + console.error('Si è verificato un errore durante il recupero dei film:', error); + return null; + }); +} + +const apiFetchGenres = async ():Promise => { + return fetch(API_ENDPOINTS.GENRES) + .then(response => { + if (!response.ok) { + throw new Error('Errore nella richiesta HTTP'); + } + return response.json(); + }) + .then(data => { + return data; + }) + .catch(error => { + console.error('Si è verificato un errore durante il recupero dei film:', error); + }); +} + +const apiFetchActors = async ():Promise => { + return fetch(API_ENDPOINTS.ACTORS) + .then(response => { + if (!response.ok) { + throw new Error('Errore nella richiesta HTTP'); + } + return response.json(); + }) + .then(data => { + return data; + }) + .catch(error => { + console.error('Si è verificato un errore durante il recupero dei film:', error); + }); +} + +export {apiFetchMovies, apiFetchGenres, apiFetchActors}; \ No newline at end of file diff --git a/frontend/src/api/interface/actorInterface.ts b/frontend/src/api/interface/actorInterface.ts new file mode 100644 index 00000000..60d8c545 --- /dev/null +++ b/frontend/src/api/interface/actorInterface.ts @@ -0,0 +1,8 @@ +interface ActorInterface { + id: number; + name: string; +} + +interface ActorsInterface extends Array {} + +export {ActorsInterface,ActorInterface}; \ No newline at end of file diff --git a/frontend/src/api/interface/genreInterface.ts b/frontend/src/api/interface/genreInterface.ts new file mode 100644 index 00000000..00831ac7 --- /dev/null +++ b/frontend/src/api/interface/genreInterface.ts @@ -0,0 +1,8 @@ +interface GenreInterface { + id: number; + name: string; +} + +interface GenresInterface extends Array {} + +export {GenresInterface,GenreInterface}; \ No newline at end of file diff --git a/frontend/src/api/interface/movieInterface.ts b/frontend/src/api/interface/movieInterface.ts new file mode 100644 index 00000000..b3696c6f --- /dev/null +++ b/frontend/src/api/interface/movieInterface.ts @@ -0,0 +1,15 @@ +interface MovieInterface { + id: number; + title: string; + imageUrl: string; + plot: string; + year: number; + releaseDate: string; + duration: string; + rating: number; + wikipediaUrl: string; +} + +interface MoviesInterface extends Array {} + +export {MoviesInterface,MovieInterface}; \ No newline at end of file diff --git a/frontend/src/components/Heading.tsx b/frontend/src/components/Heading.tsx new file mode 100644 index 00000000..3107ab89 --- /dev/null +++ b/frontend/src/components/Heading.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +const Heading:React.FC = props => { + return ( +
+

+ Movie Collection +

+ +

+ Explore the whole collection of movies +

+
+ ); +}; + +export default Heading; \ 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 00000000..732e0a39 --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +interface LayoutProps { + children:any +} + +const Layout:React.FC = props => { + return ( +
+
+ {props.children} +
+
+ ); +}; + +export default Layout; \ No newline at end of file diff --git a/frontend/src/components/Movie/FilterMovie.tsx b/frontend/src/components/Movie/FilterMovie.tsx new file mode 100644 index 00000000..eb85cf5c --- /dev/null +++ b/frontend/src/components/Movie/FilterMovie.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { GenresInterface } from "../../api/interface/genreInterface"; +import { ActorsInterface } from "../../api/interface/actorInterface"; + +interface FilterMoviesProps { + genres:GenresInterface, + actors:ActorsInterface, + handleSubmitForm: (event:React.FormEvent) => void +} + +const FilterMovies:React.FC = ({ genres, actors, handleSubmitForm }) => { + return ( +
+
{ handleSubmitForm(event)} }> +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ ); +}; + +export default FilterMovies; \ No newline at end of file diff --git a/frontend/src/components/Movie/MovieItem.tsx b/frontend/src/components/Movie/MovieItem.tsx new file mode 100644 index 00000000..c3049160 --- /dev/null +++ b/frontend/src/components/Movie/MovieItem.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import { Button, Rating } from "flowbite-react"; +import { MovieInterface } from "../../api/interface/movieInterface"; + +const MovieItem:React.FC = props => { + return ( +
+
+ {props.title} +
+
+
+ {props.year || props.rating + ?
+ {props.year} + + {props.rating + ? + + + + {props.rating} + + + : null + } +
+ : null + } +

+ {props.title} +

+

+ {props.plot.substr(0, 80)}... +

+
+ + {props.wikipediaUrl + ? + : null + } +
+
+ ); +}; + +export default MovieItem; \ No newline at end of file diff --git a/frontend/src/components/Movie/MovieList.tsx b/frontend/src/components/Movie/MovieList.tsx new file mode 100644 index 00000000..0333a741 --- /dev/null +++ b/frontend/src/components/Movie/MovieList.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { Spinner } from "flowbite-react"; + +interface MovieListProps { + loading:boolean, + children:any +} + +const MovieList:React.FC = props => { + if (props.loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {props.children} +
+ ); +}; + +export default MovieList; \ No newline at end of file diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index 54b39dd1..c307da86 100644 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -1,10 +1,10 @@ -import React from 'react' +import React from 'react' import ReactDOM from 'react-dom/client' -import App from './App.jsx' +import App from './App' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( , -) +) \ No newline at end of file diff --git a/frontend/src/services/form.ts b/frontend/src/services/form.ts new file mode 100644 index 00000000..6280a4ef --- /dev/null +++ b/frontend/src/services/form.ts @@ -0,0 +1,19 @@ +import { FormPropsInterface } from "./interface/formPropsInterface"; + +const getFormProps = (event: React.FormEvent):FormPropsInterface|null => { + try { + event.preventDefault(); + event.stopPropagation(); + const formData = new FormData(event.currentTarget); + + // @ts-ignore + const formProps = Object.fromEntries(formData.entries()); + return formProps; + + } catch (error) { + console.error(error); + return null; + } +}; + +export { getFormProps }; \ No newline at end of file diff --git a/frontend/src/services/interface/formPropsInterface.ts b/frontend/src/services/interface/formPropsInterface.ts new file mode 100644 index 00000000..08c47312 --- /dev/null +++ b/frontend/src/services/interface/formPropsInterface.ts @@ -0,0 +1,7 @@ +interface FormPropsInterface { + order?: string, + genre?: string, + actor?: string +} + +export {FormPropsInterface}; \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..f1e6d55c --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "jsx": "react", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + } +} \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 7df86a83..0db7e190 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,10 +1,12 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import tsconfigPaths from 'vite-tsconfig-paths'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ - react() + react(), + tsconfigPaths() ], server: { host: '0.0.0.0',