Compare commits
24 Commits
52933658cf
...
main
Author | SHA1 | Date | |
---|---|---|---|
b0bd392ce3 | |||
51c3be30db | |||
c93bfd585e | |||
0524212df6 | |||
26950905d6 | |||
4a84f5ec7d | |||
f6f7edcf34 | |||
2eaaa1d049 | |||
1b96974bcb | |||
4acc1955a9 | |||
60e6d9daf7 | |||
4533246181 | |||
a8e2510a03 | |||
31f64b9e5a | |||
ed268e96fa | |||
5ad47bb626 | |||
2e3783399e | |||
4d541a860f | |||
da74ab2ea4 | |||
c20f3bc933 | |||
74288b17a9 | |||
271c7c67e9 | |||
ced8d321bc | |||
ae09474b49 |
@ -2,7 +2,7 @@ APP_ENV=dev
|
||||
APP_SECRET=
|
||||
|
||||
DB_ROOT_PASSWORD=rootpswd
|
||||
DB_DATABASE=cloudsprint
|
||||
DB_DATABASE=booknest
|
||||
DB_USERNAME=dev
|
||||
DB_PASSWORD=cfai42
|
||||
DB_HOST=db
|
||||
|
28
assets/js/modal.js
Normal file
28
assets/js/modal.js
Normal file
@ -0,0 +1,28 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const datas = {{ datas | tojson }};
|
||||
|
||||
datas.items.forEach((book, index) => {
|
||||
const openModalBtn = document.getElementById(`openModalBtn-${index + 1}`);
|
||||
const closeModalSvg = document.getElementById(`closeModalSvg-${index + 1}`);
|
||||
const closeModalBtn = document.getElementById(`closeModalBtn-${index + 1}`);
|
||||
const modal = document.getElementById(`myModal-${index + 1}`);
|
||||
|
||||
if (openModalBtn) {
|
||||
openModalBtn.addEventListener('click', () => {
|
||||
modal.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
if (closeModalSvg) {
|
||||
closeModalSvg.addEventListener('click', () => {
|
||||
modal.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
if (closeModalBtn) {
|
||||
closeModalBtn.addEventListener('click', () => {
|
||||
modal.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
@ -19,6 +19,7 @@
|
||||
"symfony/runtime": "7.0.*",
|
||||
"symfony/security-bundle": "7.0.*",
|
||||
"symfony/twig-bundle": "7.0.*",
|
||||
"symfony/validator": "7.0.*",
|
||||
"symfony/webpack-encore-bundle": "^2.2",
|
||||
"symfony/yaml": "7.0.*",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
|
97
composer.lock
generated
97
composer.lock
generated
@ -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": "c53910010d80c77569e2b6c0828c9aa2",
|
||||
"content-hash": "c7a74db8c19635e6115f7567b0077bb0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "doctrine/cache",
|
||||
@ -5374,6 +5374,101 @@
|
||||
],
|
||||
"time": "2023-11-26T15:16:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/validator",
|
||||
"version": "v7.0.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/validator.git",
|
||||
"reference": "b3e4d838cdae9f2882402c2ad8018a27d469c075"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/validator/zipball/b3e4d838cdae9f2882402c2ad8018a27d469c075",
|
||||
"reference": "b3e4d838cdae9f2882402c2ad8018a27d469c075",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-mbstring": "~1.0",
|
||||
"symfony/polyfill-php83": "^1.27",
|
||||
"symfony/translation-contracts": "^2.5|^3"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/lexer": "<1.1",
|
||||
"symfony/dependency-injection": "<6.4",
|
||||
"symfony/doctrine-bridge": "<7.0",
|
||||
"symfony/expression-language": "<6.4",
|
||||
"symfony/http-kernel": "<6.4",
|
||||
"symfony/intl": "<6.4",
|
||||
"symfony/property-info": "<6.4",
|
||||
"symfony/translation": "<6.4.3|>=7.0,<7.0.3",
|
||||
"symfony/yaml": "<6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"egulias/email-validator": "^2.1.10|^3|^4",
|
||||
"symfony/cache": "^6.4|^7.0",
|
||||
"symfony/config": "^6.4|^7.0",
|
||||
"symfony/console": "^6.4|^7.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0",
|
||||
"symfony/expression-language": "^6.4|^7.0",
|
||||
"symfony/finder": "^6.4|^7.0",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/http-foundation": "^6.4|^7.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0",
|
||||
"symfony/intl": "^6.4|^7.0",
|
||||
"symfony/mime": "^6.4|^7.0",
|
||||
"symfony/property-access": "^6.4|^7.0",
|
||||
"symfony/property-info": "^6.4|^7.0",
|
||||
"symfony/translation": "^6.4.3|^7.0.3",
|
||||
"symfony/yaml": "^6.4|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Validator\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/",
|
||||
"/Resources/bin/"
|
||||
]
|
||||
},
|
||||
"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 tools to validate values",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/validator/tree/v7.0.10"
|
||||
},
|
||||
"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-07-26T12:31:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/var-dumper",
|
||||
"version": "v7.0.2",
|
||||
|
@ -7,9 +7,11 @@ framework:
|
||||
# Enables session support. Note that the session will ONLY be started if you read or write from it.
|
||||
# Remove or comment this section to explicitly disable session support.
|
||||
session:
|
||||
handler_id: null
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
enabled: true
|
||||
handler_id: null # Utilise le gestionnaire de sessions par défaut
|
||||
cookie_lifetime: 3600 # La durée de vie des cookies de session
|
||||
cookie_secure: auto # Assure-toi que les cookies sont sécurisés (seulement en HTTPS)
|
||||
cookie_samesite: lax # La politique SameSite des cookies
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
|
@ -10,23 +10,18 @@ security:
|
||||
class: App\Entity\User
|
||||
property: email
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: app_user_provider
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
|
||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||
# switch_user: true
|
||||
|
||||
# Ajout de la gestion de la déconnexion
|
||||
form_login:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
username_parameter: _username
|
||||
password_parameter: _password
|
||||
default_target_path: home # Assure-toi que cette route existe
|
||||
logout:
|
||||
path: /logout # Route pour déconnecter l'utilisateur
|
||||
target: /login # Redirection vers la page de login après déconnexion
|
||||
path: /logout
|
||||
target: /login
|
||||
|
||||
# Easy way to control access for large sections of your site
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
|
@ -7,6 +7,9 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use App\Repository\FavorisRepository;
|
||||
use App\Entity\Favoris;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
|
||||
class APISearchController extends AbstractController
|
||||
{
|
||||
@ -21,21 +24,80 @@ class APISearchController extends AbstractController
|
||||
{
|
||||
// Récupérer le paramètre "q" depuis la requête
|
||||
$query = $request->query->get('q');
|
||||
$nb_pages = $request->query->get('page') ?? '1';
|
||||
|
||||
|
||||
// Appeler le service GoogleBooks avec la requête
|
||||
return $this->googleBooksService->searchBooks($query);
|
||||
return $this->googleBooksService->searchBooks($query, 'fr', ($nb_pages - 1) *10);
|
||||
}
|
||||
|
||||
#[Route('/toggleLike/{idGoogle}', name: 'like', methods: "POST")]
|
||||
public function addFavoris(Request $request, FavorisRepository $favorisRepository, String $idGoogle)
|
||||
{
|
||||
$content = $request->getContent();
|
||||
$data = json_decode($content, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE || empty($data)) {
|
||||
return new JsonResponse(['status' => 'error', 'message' => 'Invalid data'], 400);
|
||||
}
|
||||
|
||||
$title = $data['title'] ?? '';
|
||||
$authors = $data['authors'] ?? '';
|
||||
$images = $data['images'] ?? '';
|
||||
$description = $data['description'] ?? '';
|
||||
$date = $data['date'] ?? '';
|
||||
$pages = $data['pages'] ?? '';
|
||||
$edition = $data['edition'] ?? '';
|
||||
$categorie = $data['categorie'] ?? '';
|
||||
|
||||
|
||||
$favoris = new Favoris();
|
||||
$user = $this->getUser();
|
||||
$favoris->setUser($user);
|
||||
$favoris->setIdGoogle($idGoogle);
|
||||
$favoris->setTitle($title);
|
||||
$favoris->setAuthors($authors);
|
||||
$favoris->setImage($images);
|
||||
$favoris->setDescription($description);
|
||||
$favoris->setPublication($date);
|
||||
$favoris->setPages($pages);
|
||||
$favoris->setPublication($date);
|
||||
$favoris->setEdition($edition);
|
||||
$favoris->setCategorie($categorie);
|
||||
|
||||
|
||||
$favorisRepository->addFavoris($favoris);
|
||||
return $this->json(['success' => true, 'message' => 'Favoris ajouté', "status" => "added"]);
|
||||
}
|
||||
|
||||
#[Route('/untoggleLike/{idGoogle}', name: 'unlike', methods: "POST")]
|
||||
public function removeFavoris(FavorisRepository $favorisRepository, String $idGoogle)
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$favorisRepository->removeFavoris($user, $idGoogle);
|
||||
|
||||
return $this->json(['success' => true, 'message' => 'Favoris supprimé', "status" => "removed"]);
|
||||
}
|
||||
|
||||
#[Route('/api/search', name: 'api_search')]
|
||||
public function index(Request $request): Response
|
||||
public function index(Request $request, FavorisRepository $favorisRepository): Response
|
||||
{
|
||||
ini_set('memory_limit', '512M');
|
||||
|
||||
// Appeler la méthode search et récupérer les résultats
|
||||
$datas = $this->search($request);
|
||||
$query = $request->query->get('q');
|
||||
|
||||
$user = $this->getUser();
|
||||
|
||||
$favoris = $favorisRepository->getFavorisByUser($user);
|
||||
|
||||
// Afficher les résultats dans le template
|
||||
return $this->render('apiSearch/index.html.twig', [
|
||||
'controller_name' => 'APISearchController',
|
||||
'datas' => $datas,
|
||||
'query' => $query,
|
||||
'favoris' => $favoris,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
33
src/Controller/FavorisController.php
Normal file
33
src/Controller/FavorisController.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\FavorisRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use App\Service\GoogleBooksService;
|
||||
|
||||
class FavorisController extends AbstractController
|
||||
{
|
||||
private GoogleBooksService $googleBooksService;
|
||||
private array $favorisResult = [];
|
||||
|
||||
public function __construct(GoogleBooksService $googleBooksService)
|
||||
{
|
||||
$this->googleBooksService = $googleBooksService;
|
||||
}
|
||||
|
||||
#[Route('/favoris', name: 'app_favoris')]
|
||||
public function index(FavorisRepository $favorisRepository): Response
|
||||
{
|
||||
$favorisUser = $favorisRepository->findBy(['user' => $this->getUser()]);
|
||||
ini_set('memory_limit', '512M');
|
||||
array_push($this->favorisResult, $favorisRepository->getFavorisByUser($this->getUser()));
|
||||
return $this->render('favoris/index.html.twig', [
|
||||
'controller_name' => 'FavorisController',
|
||||
'datas' => $this->favorisResult,
|
||||
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
@ -10,16 +9,19 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class RegistrationController extends AbstractController
|
||||
{
|
||||
private $entityManager;
|
||||
private $passwordHasher; // Correction du nom de la variable
|
||||
private $passwordHasher;
|
||||
private $validator;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, UserPasswordHasherInterface $passwordHasher) // Correction ici
|
||||
public function __construct(EntityManagerInterface $entityManager, UserPasswordHasherInterface $passwordHasher, ValidatorInterface $validator)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->passwordHasher = $passwordHasher;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
#[Route('/registration', name: 'app_registration')]
|
||||
@ -29,19 +31,43 @@ class RegistrationController extends AbstractController
|
||||
$form = $this->createForm(RegistrationType::class, $user);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
// Hacher le mot de passe avant de persister l'utilisateur
|
||||
$plainPassword = $user->getPassword(); // Récupération du mot de passe brut
|
||||
$hashedPassword = $this->passwordHasher->hashPassword($user, $plainPassword); // Hachage du mot de passe
|
||||
$user->setPassword($hashedPassword); // Remplacer le mot de passe brut par le mot de passe haché
|
||||
if ($form->isSubmitted()) {
|
||||
$existingEmail = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $user->getEmail()]);
|
||||
$existingPseudo = $this->entityManager->getRepository(User::class)->findOneBy(['pseudo' => $user->getPseudo()]);
|
||||
|
||||
// Persist de l'utilisateur haché
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
if ($existingEmail) {
|
||||
$form->get('email')->addError(new \Symfony\Component\Form\FormError('Cet email est déjà utilisé.'));
|
||||
}
|
||||
|
||||
// Message flash de succès
|
||||
$this->addFlash('success', 'Votre compte a été créé avec succès !');
|
||||
return $this->redirectToRoute('home');
|
||||
if ($existingPseudo) {
|
||||
$form->get('pseudo')->addError(new \Symfony\Component\Form\FormError('Ce pseudo est déjà pris.'));
|
||||
}
|
||||
|
||||
$plainPassword = $user->getPassword();
|
||||
$confirmPassword = $form->get('confirmPassword')->getData();
|
||||
|
||||
if ($plainPassword !== $confirmPassword) {
|
||||
$this->addFlash('error', 'Les mots de passe ne correspondent pas.');
|
||||
return $this->redirectToRoute('app_registration');
|
||||
}
|
||||
|
||||
$errors = $this->validator->validate($user);
|
||||
if (count($errors) > 0) {
|
||||
foreach ($errors as $error) {
|
||||
$form->addError(new \Symfony\Component\Form\FormError($error->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
if ($form->isValid()) {
|
||||
$hashedPassword = $this->passwordHasher->hashPassword($user, $plainPassword);
|
||||
$user->setPassword($hashedPassword);
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Votre compte a été créé avec succès !');
|
||||
return $this->redirectToRoute('home');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('registration/index.html.twig', [
|
||||
|
170
src/Entity/Favoris.php
Normal file
170
src/Entity/Favoris.php
Normal file
@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\FavorisRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: FavorisRepository::class)]
|
||||
class Favoris
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $id_google = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'favoris')]
|
||||
private ?User $user = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $title = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $authors = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $edition = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $publication = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $categorie = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $pages = null;
|
||||
|
||||
#[ORM\Column(length: 2555, nullable: true)]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $image = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getIdGoogle(): ?string
|
||||
{
|
||||
return $this->id_google;
|
||||
}
|
||||
|
||||
public function setIdGoogle(string $id_google): static
|
||||
{
|
||||
$this->id_google = $id_google;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(?User $user): static
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAuthors(): ?string
|
||||
{
|
||||
return $this->authors;
|
||||
}
|
||||
|
||||
public function setAuthors(?string $authors): static
|
||||
{
|
||||
$this->authors = $authors;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEdition(): ?string
|
||||
{
|
||||
return $this->edition;
|
||||
}
|
||||
|
||||
public function setEdition(?string $edition): static
|
||||
{
|
||||
$this->edition = $edition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPublication(): ?string
|
||||
{
|
||||
return $this->publication;
|
||||
}
|
||||
|
||||
public function setPublication(?string $publication): static
|
||||
{
|
||||
$this->publication = $publication;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCategorie(): ?string
|
||||
{
|
||||
return $this->categorie;
|
||||
}
|
||||
|
||||
public function setCategorie(?string $categorie): static
|
||||
{
|
||||
$this->categorie = $categorie;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPages(): ?string
|
||||
{
|
||||
return $this->pages;
|
||||
}
|
||||
|
||||
public function setPages(?string $pages): static
|
||||
{
|
||||
$this->pages = $pages;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(string $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getImage(): ?string
|
||||
{
|
||||
return $this->image;
|
||||
}
|
||||
|
||||
public function setImage(?string $image): static
|
||||
{
|
||||
$this->image = $image;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
@ -31,6 +33,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $lastName = null;
|
||||
|
||||
#[ORM\Column(type: "json")]
|
||||
private array $roles = [];
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'user', targetEntity: Favoris::class)]
|
||||
private Collection $favoris;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->favoris = new ArrayCollection();
|
||||
}
|
||||
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@ -103,16 +117,56 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
|
||||
public function getRoles(): array
|
||||
{
|
||||
// TODO: Implement getRoles() method.
|
||||
|
||||
$roles = $this->roles;
|
||||
|
||||
if (empty($roles)) {
|
||||
$roles[] = 'ROLE_USER';
|
||||
}
|
||||
|
||||
return $roles;
|
||||
|
||||
}
|
||||
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// TODO: Implement eraseCredentials() method.
|
||||
|
||||
}
|
||||
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
// TODO: Implement getUserIdentifier() method.
|
||||
|
||||
return $this->email; // Ou $this->pseudo si tu préfères utiliser le pseudo
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Favoris>
|
||||
*/
|
||||
public function getFavoris(): Collection
|
||||
{
|
||||
return $this->favoris;
|
||||
}
|
||||
|
||||
public function addFavori(Favoris $favori): static
|
||||
{
|
||||
if (!$this->favoris->contains($favori)) {
|
||||
$this->favoris->add($favori);
|
||||
$favori->setUser($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeFavori(Favoris $favori): static
|
||||
{
|
||||
if ($this->favoris->removeElement($favori)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($favori->getUser() === $this) {
|
||||
$favori->setUser(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -12,39 +12,44 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class RegistrationType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'Email',
|
||||
'attr' => ['class' => 'form-control']
|
||||
])
|
||||
->add('password', PasswordType::class, [
|
||||
'label' => 'Mot de passe',
|
||||
'attr' => ['class' => 'form-control']
|
||||
])
|
||||
->add('pseudo', TextType::class, [
|
||||
'label' => 'Pseudo',
|
||||
'attr' => ['class' => 'form-control']
|
||||
])
|
||||
->add('firstname', TextType::class, [
|
||||
'label' => 'Prénom',
|
||||
'attr' => ['class' => 'form-control']
|
||||
])
|
||||
->add('lastname', TextType::class, [
|
||||
'label' => 'Nom',
|
||||
'attr' => ['class' => 'form-control']
|
||||
])
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'S\'inscrire',
|
||||
'attr' => ['class' => 'btn btn-primary']
|
||||
]);
|
||||
}
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'Email',
|
||||
'attr' => ['class' => 'form-control']
|
||||
])
|
||||
->add('password', PasswordType::class, [
|
||||
'label' => 'Mot de passe',
|
||||
'attr' => ['class' => 'form-control']
|
||||
])
|
||||
->add('confirmPassword', PasswordType::class, [
|
||||
'label' => 'Confirmer le mot de passe',
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'mapped' => false, // Ce champ n'est pas mappé à l'entité User
|
||||
])
|
||||
->add('pseudo', TextType::class, [
|
||||
'label' => 'Pseudo',
|
||||
'attr' => ['class' => 'form-control']
|
||||
])
|
||||
->add('firstname', TextType::class, [
|
||||
'label' => 'Prénom',
|
||||
'attr' => ['class' => 'form-control']
|
||||
])
|
||||
->add('lastname', TextType::class, [
|
||||
'label' => 'Nom',
|
||||
'attr' => ['class' => 'form-control']
|
||||
])
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'S\'inscrire',
|
||||
'attr' => ['class' => 'btn btn-primary']
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
]);
|
||||
}
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
84
src/Repository/FavorisRepository.php
Normal file
84
src/Repository/FavorisRepository.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Favoris;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Favoris>
|
||||
*
|
||||
* @method Favoris|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Favoris|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Favoris[] findAll()
|
||||
* @method Favoris[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class FavorisRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Favoris::class);
|
||||
}
|
||||
|
||||
public function addFavoris(Favoris $favoris): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($favoris);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function removeFavoris($userId, $idGoogle): void
|
||||
{
|
||||
// Récupérer l'entity manager
|
||||
$entityManager = $this->getEntityManager();
|
||||
|
||||
// Trouver le favoris de cet utilisateur pour le livre donné
|
||||
$favoris = $this->findOneBy([
|
||||
'user' => $userId,
|
||||
'id_google' => $idGoogle
|
||||
]);
|
||||
|
||||
if (!$favoris) {
|
||||
throw new \Exception($idGoogle . ' n\'est pas dans les favoris de cet utilisateur' . $userId);
|
||||
}
|
||||
|
||||
// Supprimer l'entité favoris de la base de données
|
||||
$entityManager->remove($favoris);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function getFavorisByUser($userId): array
|
||||
{
|
||||
return $this->createQueryBuilder('f')
|
||||
->andWhere('f.user = :userId')
|
||||
->setParameter('userId', $userId)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Favoris[] Returns an array of Favoris objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('f')
|
||||
// ->andWhere('f.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('f.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?Favoris
|
||||
// {
|
||||
// return $this->createQueryBuilder('f')
|
||||
// ->andWhere('f.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
@ -13,20 +13,22 @@ class GoogleBooksService
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
public function searchBooks(string $query, string $lang = 'fr'): array
|
||||
public function searchBooks(string $query, string $lang = 'fr', string $nb_pages): array
|
||||
{
|
||||
$url = 'https://www.googleapis.com/books/v1/volumes';
|
||||
$response = $this->client->request('GET', $url, [
|
||||
'query' => [
|
||||
'q' => $query,
|
||||
'langRestrict' => $lang,
|
||||
'maxResults' => 10,
|
||||
'startIndex' => $nb_pages,
|
||||
'printType' => 'books',
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
// Convertir la réponse JSON en tableau PHP
|
||||
$dataArray = $response->toArray();
|
||||
|
||||
return $dataArray;
|
||||
}
|
||||
}
|
||||
|
12
symfony.lock
12
symfony.lock
@ -135,6 +135,18 @@
|
||||
"templates/base.html.twig"
|
||||
]
|
||||
},
|
||||
"symfony/validator": {
|
||||
"version": "7.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/validator.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/web-profiler-bundle": {
|
||||
"version": "7.0",
|
||||
"recipe": {
|
||||
|
@ -1,23 +1,270 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Hello APITestController!{% endblock %}
|
||||
{% block title %}Recherche | {{ query }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
|
||||
<div>
|
||||
{% for book in datas.items %}
|
||||
<div class="flex justify-center">
|
||||
<div class="border rounded-xl w-1/2 py-2 flex flex-col justify-center mt-5">
|
||||
<h1 class="font-bold p-2 text-2xl ">{{ book.volumeInfo.title }}</h1>
|
||||
|
||||
{% if book.volumeInfo.imageLinks is defined and book.volumeInfo.imageLinks.smallThumbnail is defined %}
|
||||
<img src="{{ book.volumeInfo.imageLinks.smallThumbnail }}" class="p-2" style="width: 20%" height="20px">
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<div class="w-1/2 py-2 flex flex-col justify-center mt-5 mx-auto">
|
||||
<h1 class="text-2xl font-bold">Résultats pour :
|
||||
{{ query }}</h1>
|
||||
</div>
|
||||
<div class="w-1/2 pb-1 flex flex-col justify-center mx-auto">
|
||||
<p>
|
||||
{{ datas.totalItems }} résultats trouvés
|
||||
</p>
|
||||
</div>
|
||||
{% for book in datas.items %}
|
||||
{% include '/apiSearch/modal.html.twig' with { 'loop': loop} %}
|
||||
<div class="relative flex rounded-xl border bg-white shadow-lg hover:shadow-xl transition-shadow duration-300 w-1/2 mx-auto py-2 mt-5">
|
||||
<div class="flex flex-col justify-start">
|
||||
<div class="flex flex-col p-2 text-2xl">
|
||||
<h1 class="font-bold">{{ book.volumeInfo.title }}</h1>
|
||||
{% if book.volumeInfo.authors is defined %}
|
||||
<span class="text-lg">{{ book.volumeInfo.authors | join(", ") }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="flex flex-row w-1/4">
|
||||
{% if book.volumeInfo.imageLinks is defined and book.volumeInfo.imageLinks.smallThumbnail is defined %}
|
||||
<img src="{{ book.volumeInfo.imageLinks.smallThumbnail }}" class="p-2">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="w-9/12">
|
||||
{% if book.volumeInfo.publisher is defined %}
|
||||
<p class="p-2 italic font-bold">Aux éditions :
|
||||
<span class="font-normal">{{ book.volumeInfo.publisher }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if book.volumeInfo.publishedDate is defined %}
|
||||
<p class="p-2 italic font-bold">Date de publication :
|
||||
<span class="font-normal">{{ book.volumeInfo.publishedDate }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if book.volumeInfo.categories is defined %}
|
||||
{% for categorie in book.volumeInfo.categories %}
|
||||
<p class="p-2 italic font-bold">Catégorie :
|
||||
<span class="font-normal">{{ categorie }}</span>
|
||||
</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<p class="p-2 italic font-bold">Nombres de pages :
|
||||
<span class="font-normal">{{ book.volumeInfo.pageCount }}</span>
|
||||
</p>
|
||||
<p class="p-2 italic font-bold">Description :
|
||||
{% if book.searchInfo is defined %}
|
||||
<span class="font-normal">{{ book.searchInfo.textSnippet | raw }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full p-2">
|
||||
<button id="openModalBtn-{{ loop.index }}" class="dark:bg-[#263B46] text-white px-4 py-2 rounded-lg mt-4 w-full">
|
||||
Voir plus
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% if app.user %}
|
||||
{% set isLiked = book.id in favoris|map(f => f.getIdGoogle()) %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="{{ isLiked ? 'red' : 'none' }}"
|
||||
stroke="{{ isLiked ? 'red' : 'currentColor' }}"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="like-button absolute top-2 right-2 w-6 h-6 cursor-pointer transition-colors duration-300"
|
||||
data-id-google="{{ book.id }}"
|
||||
data-liked="{{ isLiked ? 'true' : 'false' }}"
|
||||
data-edition="{{ book.volumeInfo.publisher | default('') }}"
|
||||
data-categorie="{{ book.volumeInfo.categories is defined ? book.volumeInfo.categories | join(', ') : '' }}"
|
||||
data-title="{{ book.volumeInfo.title | default('Titre non disponible') }}"
|
||||
data-authors="{{ book.volumeInfo.authors is defined ? book.volumeInfo.authors | join(', ') : '' }}"
|
||||
data-images="{{ book.volumeInfo.imageLinks.smallThumbnail is defined ? book.volumeInfo.imageLinks.smallThumbnail : '' }}"
|
||||
data-description="{{ book.volumeInfo.description is defined and book.volumeInfo.description is not empty ? book.volumeInfo.description | raw : '' }}"
|
||||
data-date="{{ book.volumeInfo.publishedDate | default('') }}"
|
||||
data-pages="{{ book.volumeInfo.pageCount | default('0') }}">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"/>
|
||||
</svg>
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div id="myModal-{{ loop.index }}" class="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center hidden z-50">
|
||||
<div class="relative bg-white p-6 rounded-lg w-1/3">
|
||||
<div class="flex flex-row w-1/4">
|
||||
{% if book.volumeInfo.imageLinks is defined and book.volumeInfo.imageLinks.smallThumbnail is defined %}
|
||||
<img src="{{ book.volumeInfo.imageLinks.smallThumbnail }}" class="p-2">
|
||||
{% endif %}
|
||||
</div>
|
||||
<svg id="closeModalSvg-{{ loop.index }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor"
|
||||
class="absolute top-2 right-2 w-6 h-6 cursor-pointer text-gray-500 hover:text-gray-700">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
|
||||
{% if book.volumeInfo.title is defined and book.volumeInfo.title is not empty %}
|
||||
<p class="p-2 italic font-bold">Titre : <span class="font-normal">{{ book.volumeInfo.title }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
{% if book.volumeInfo.authors is defined %}
|
||||
<p class="p-2 italic font-bold"> Auteur(s) : <span class="font-normal"> {{ book.volumeInfo.authors | join(", ") }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
{% if book.volumeInfo.publisher is defined and book.volumeInfo.publisher is not empty %}
|
||||
<p class="p-2 italic font-bold">Aux éditions : <span class="font-normal">{{ book.volumeInfo.publisher }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
{% if book.volumeInfo.publishedDate is defined and book.volumeInfo.publishedDate is not empty %}
|
||||
<p class="p-2 italic font-bold">Date de publication : <span class="font-normal">{{ book.volumeInfo.publishedDate }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
{% if book.volumeInfo.categories is defined and book.volumeInfo.categories is not empty %}
|
||||
{% for categorie in book.volumeInfo.categories %}
|
||||
<p class="p-2 italic font-bold">Catégorie : <span class="font-normal">{{ categorie }}</span></p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if book.volumeInfo.pageCount is defined and book.volumeInfo.pageCount is not empty %}
|
||||
<p class="p-2 italic font-bold">Nombre de pages : <span class="font-normal">{{ book.volumeInfo.pageCount }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
{% if book.volumeInfo is defined and book.volumeInfo.description is defined and book.volumeInfo.description is not empty %}
|
||||
<p class="p-2 italic font-bold">Description : <span class="font-normal">{{ book.volumeInfo.description | raw }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
<button id="closeModalBtn-{{ loop.index }}" class="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600 mt-4">
|
||||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% set nb_items = datas.totalItems %}
|
||||
{% set nb_pages = nb_items / 10 %}
|
||||
{% set current_page = app.request.get('page') ? app.request.get('page') : 1 %}
|
||||
|
||||
{% if nb_pages > 1 %}
|
||||
<div class="flex justify-center mt-8 mb-8">
|
||||
<ul class="flex">
|
||||
{# Flèche "Précédent" #}
|
||||
{% if current_page > 1 %}
|
||||
<li class="mx-2">
|
||||
<a href="{{ path('api_search', {'q': query, 'page': current_page - 1}) }}" class="px-3 py-3 dark:bg-[#263B46] text-white rounded-lg mr-32">Précédent</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="mx-2">
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{# Liens des pages proches de la page actuelle #}
|
||||
{% for i in current_page - 3..current_page + 3 %}
|
||||
{% if i > 0 and i <= nb_pages %}
|
||||
<li class="mx-2">
|
||||
<a href="{{ path('api_search', {'q': query, 'page': i}) }}" class="px-4 py-3 text-white {{ i == current_page ? 'dark:bg-[#9db1bd] text-gray-700' : 'bg-gray-200 text-black' }} rounded-lg">{{ i }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{# Flèche "Suivant" #}
|
||||
{% if current_page < nb_pages %}
|
||||
<li class="mx-2">
|
||||
<a href="{{ path('api_search', {'q': query, 'page': current_page + 1}) }}" class="px-3 py-3 dark:bg-[#263B46] text-white rounded-lg ml-32">Suivant</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="mx-2">
|
||||
<span class="px-3 py-3 dark:bg-[#263B46] text-white rounded-lg cursor-not-allowed ml-32">Suivant</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleLike(idGoogle, liked, element, bookDetails) {
|
||||
const url = liked
|
||||
? "{{ path('unlike', {'idGoogle': 'PLACEHOLDER'}) }}".replace('PLACEHOLDER', idGoogle)
|
||||
: "{{ path('like', {'idGoogle': 'PLACEHOLDER'}) }}".replace('PLACEHOLDER', idGoogle);
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(bookDetails)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if ((liked && data.status === 'removed') || (!liked && data.status === 'added')) {
|
||||
// Mettre à jour l'état visuel
|
||||
const newLikedState = !liked;
|
||||
element.setAttribute('data-liked', newLikedState.toString());
|
||||
element.querySelector('path').setAttribute('fill', newLikedState ? 'red' : 'none');
|
||||
element.querySelector('path').setAttribute('stroke', newLikedState ? 'red' : 'currentColor');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur AJAX', error);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.like-button').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const idGoogle = this.dataset.idGoogle;
|
||||
const liked = this.getAttribute('data-liked') === 'true'; // Lire l'état actuel
|
||||
const bookDetails = {
|
||||
title: this.dataset.title,
|
||||
authors: this.dataset.authors,
|
||||
images: this.dataset.images,
|
||||
description: this.dataset.description,
|
||||
date: this.dataset.date,
|
||||
pages: this.dataset.pages,
|
||||
edition: this.dataset.edition,
|
||||
categorie: this.dataset.categorie
|
||||
};
|
||||
|
||||
toggleLike(idGoogle, liked, this, bookDetails);
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
{% for book in datas.items %}
|
||||
const openModalBtn{{ loop.index }} = document.getElementById('openModalBtn-{{ loop.index }}');
|
||||
const closeModalSvg{{ loop.index }} = document.getElementById('closeModalSvg-{{ loop.index }}');
|
||||
const closeModalBtn{{ loop.index }} = document.getElementById('closeModalBtn-{{ loop.index }}');
|
||||
const modal{{ loop.index }} = document.getElementById('myModal-{{ loop.index }}');
|
||||
|
||||
if (openModalBtn{{ loop.index }}) {
|
||||
openModalBtn{{ loop.index }}.addEventListener('click', () => {
|
||||
modal{{ loop.index }}.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
if (closeModalSvg{{ loop.index }}) {
|
||||
closeModalSvg{{ loop.index }}.addEventListener('click', () => {
|
||||
modal{{ loop.index }}.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
if (closeModalBtn{{ loop.index }}) {
|
||||
closeModalBtn{{ loop.index }}.addEventListener('click', () => {
|
||||
modal{{ loop.index }}.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
{% endfor %}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
76
templates/apiSearch/modal.html.twig
Normal file
76
templates/apiSearch/modal.html.twig
Normal file
@ -0,0 +1,76 @@
|
||||
{% block body %}
|
||||
|
||||
<div id="myModal-{{ loop.index }}" class="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center hidden z-50">
|
||||
<div class="relative bg-white p-6 rounded-lg w-1/3 max-h-screen overflow-y-auto">
|
||||
<div class="flex mx-auto flex-row w-1/4">
|
||||
{% if book.volumeInfo.imageLinks is defined and book.volumeInfo.imageLinks.smallThumbnail is defined %}
|
||||
<img src="{{ book.volumeInfo.imageLinks.smallThumbnail }}" class="p-2">
|
||||
{% endif %}
|
||||
</div>
|
||||
<svg id="closeModalSvg-{{ loop.index }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor"
|
||||
class="absolute top-2 right-2 w-6 h-6 cursor-pointer text-gray-500 hover:text-gray-700">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
|
||||
{% if book.volumeInfo.title is defined and book.volumeInfo.title is not empty %}
|
||||
<p class="p-2 italic font-bold">Titre : <span class="font-normal">{{ book.volumeInfo.title }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
{% if book.volumeInfo.authors is defined %}
|
||||
<p class="p-2 italic font-bold"> Auteur(s) : <span class="font-normal"> {{ book.volumeInfo.authors | join(", ") }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
{% if book.volumeInfo.publisher is defined and book.volumeInfo.publisher is not empty %}
|
||||
<p class="p-2 italic font-bold">Aux éditions : <span class="font-normal">{{ book.volumeInfo.publisher }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
{% if book.volumeInfo.publishedDate is defined and book.volumeInfo.publishedDate is not empty %}
|
||||
<p class="p-2 italic font-bold">Date de publication : <span class="font-normal">{{ book.volumeInfo.publishedDate }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
{% if book.volumeInfo.categories is defined and book.volumeInfo.categories is not empty %}
|
||||
{% for categorie in book.volumeInfo.categories %}
|
||||
<p class="p-2 italic font-bold">Catégorie : <span class="font-normal">{{ categorie }}</span></p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if book.volumeInfo.pageCount is defined and book.volumeInfo.pageCount is not empty %}
|
||||
<p class="p-2 italic font-bold">Nombre de pages : <span class="font-normal">{{ book.volumeInfo.pageCount }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
{% if book.volumeInfo is defined and book.volumeInfo.description is defined and book.volumeInfo.description is not empty %}
|
||||
<p class="p-2 italic font-bold">Description : <span class="font-normal">{{ book.volumeInfo.description | raw }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
<hr class="mt-6">
|
||||
|
||||
<div class="mt-4">
|
||||
<h3 class="font-bold text-lg">Laisser un commentaire :</h3>
|
||||
<form id="commentForm-{{ loop.index }}" action="" method="POST" class="space-y-4">
|
||||
<textarea id="commentText-{{ loop.index }}" name="comment" rows="4" class="w-full p-2 border border-gray-300 rounded-lg" placeholder="Écrivez votre commentaire ici..." required></textarea>
|
||||
<button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">Envoyer</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<hr class="mt-8">
|
||||
<div id="commentsSection-{{ loop.index }}" class="mt-4 max-h-60 overflow-y-auto">
|
||||
<h3 class="font-bold text-lg">Commentaires :</h3>
|
||||
<div class="mt-2">
|
||||
<div class="p-2 border-b border-gray-200">
|
||||
<p class="font-semibold">Auteur : Fabien</p>
|
||||
<p>Très bon livre pour les débutants.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="closeModalBtn-{{ loop.index }}" class="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600 mt-4">
|
||||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block javascripts %}
|
||||
<script src="{{ asset('js/modal.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
@ -23,5 +23,7 @@
|
||||
{% endblock %}
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@ -21,19 +21,19 @@
|
||||
</a>
|
||||
<!-- Left navigation links -->
|
||||
<ul class="list-style-none me-auto flex flex-col ps-0 lg:flex-row" data-twe-navbar-nav-ref>
|
||||
<li
|
||||
class="mb-4 lg:mb-0 lg:pe-2" data-twe-nav-item-ref>
|
||||
<!-- Dashboard link -->
|
||||
<a class="text-neutral-500 transition duration-200 hover:text-neutral-700 hover:ease-in-out focus:text-neutral-700 disabled:text-black/30 motion-reduce:transition-none dark:text-neutral-200 dark:hover:text-neutral-300 dark:focus:text-neutral-300 lg:px-2 [&.active]:text-black/90 dark:[&.active]:text-zinc-400" href="/" data-twe-nav-link-ref>Accueil</a>
|
||||
</li>
|
||||
<!-- Team link -->
|
||||
<li>
|
||||
</li>
|
||||
{% if app.user is empty %}
|
||||
<!-- Inscription link -->
|
||||
<li class="mb-4 lg:mb-0 lg:pe-2" data-twe-nav-item-ref>
|
||||
<a class="text-neutral-500 transition duration-200 hover:text-neutral-700 hover:ease-in-out focus:text-neutral-700 disabled:text-black/30 motion-reduce:transition-none dark:text-neutral-200 dark:hover:text-neutral-300 dark:focus:text-neutral-300 lg:px-2 [&.active]:text-black/90 dark:[&.active]:text-neutral-400" href="/registration" data-twe-nav-link-ref>Inscription</a>
|
||||
</li>
|
||||
<!-- Projects link -->
|
||||
<!-- login link -->
|
||||
<li class="mb-4 lg:mb-0 lg:pe-2" data-twe-nav-item-ref>
|
||||
<a class="text-neutral-500 transition duration-200 hover:text-neutral-700 hover:ease-in-out focus:text-neutral-700 disabled:text-black/30 motion-reduce:transition-none dark:text-neutral-200 dark:hover:text-neutral-300 dark:focus:text-neutral-300 lg:px-2 [&.active]:text-black/90 dark:[&.active]:text-neutral-400" href="/login" data-twe-nav-link-ref>Connexion</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -57,28 +57,32 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<a class="me-4 text-neutral-600 transition duration-200 hover:text-neutral-700 hover:ease-in-out focus:text-neutral-700 disabled:text-black/30 motion-reduce:transition-none dark:text-neutral-200 dark:hover:text-neutral-300 dark:focus:text-neutral-300 [&.active]:text-black/90 dark:[&.active]:text-neutral-400" href="#">
|
||||
<span class="[&>svg]:w-5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24" fill="currentColor" class="h-5 w-5">
|
||||
<path d="M2.25 2.25a.75.75 0 000 1.5h1.386c.17 0 .318.114.362.278l2.558 9.592a3.752 3.752 0 00-2.806 3.63c0 .414.336.75.75.75h15.75a.75.75 0 000-1.5H5.378A2.25 2.25 0 017.5 15h11.218a.75.75 0 00.674-.421 60.358 60.358 0 002.96-7.228.75.75 0 00-.525-.965A60.864 60.864 0 005.68 4.509l-.232-.867A1.875 1.875 0 003.636 2.25H2.25zM3.75 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM16.5 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0z"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
<div
|
||||
class="relative flex items-center">
|
||||
<!-- Cart Icon -->
|
||||
<a class="me-4 text-neutral-600 transition duration-200 hover:text-neutral-700 hover:ease-in-out focus:text-neutral-700 disabled:text-black/30 motion-reduce:transition-none dark:text-neutral-200 dark:hover:text-neutral-300 dark:focus:text-neutral-300 [&.active]:text-black/90 dark:[&.active]:text-neutral-400" href="#">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewbox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"/>
|
||||
{% if app.user %}
|
||||
<div
|
||||
class="relative flex items-center">
|
||||
<!-- Cart Icon -->
|
||||
<a class="me-4 text-neutral-600 transition duration-200 hover:text-neutral-700 hover:ease-in-out focus:text-neutral-700 disabled:text-black/30 motion-reduce:transition-none dark:text-neutral-200 dark:hover:text-neutral-300 dark:focus:text-neutral-300 [&.active]:text-black/90 dark:[&.active]:text-neutral-400" href="/favoris">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewbox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"/>
|
||||
</svg>
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
||||
<a class="me-4 text-neutral-600 transition duration-200 hover:text-neutral-700 hover:ease-in-out focus:text-neutral-700 disabled:text-black/30 motion-reduce:transition-none dark:text-neutral-200 dark:hover:text-neutral-300 dark:focus:text-neutral-300 [&.active]:text-black/90 dark:[&.active]:text-neutral-400" href="/logout">
|
||||
<span class="[&>svg]:w-5">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15m-3 0-3-3m0 0 3-3m-3 3H15" />
|
||||
</svg>
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
|
11
templates/favoris/index.html.twig
Normal file
11
templates/favoris/index.html.twig
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Mes favoris{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% for data in datas %}
|
||||
{% for book in data %}
|
||||
<p>{{ book.title }}</p>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
@ -24,25 +24,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="text-red-500 text-sm mt-2">
|
||||
{{ error.messageKey|trans(error.messageData, 'security') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<button type="submit" class="w-full px-4 py-2 bg-indigo-600 text-white font-semibold rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
Se connecter
|
||||
</button>
|
||||
{% for type, messages in app.flashes %}
|
||||
<div class="mb-4">
|
||||
{% for message in messages %}
|
||||
<div class="text-sm {% if type == 'success' %}text-green-600{% else %}text-red-600{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -1,5 +1,3 @@
|
||||
{# templates/registration/register.html.twig #}
|
||||
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Inscription{% endblock %}
|
||||
@ -11,30 +9,6 @@
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form_label(form.email, 'Email', {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}}) }}
|
||||
<div class="mt-1">
|
||||
{{ form_widget(form.email, {'attr': {'class': 'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm'}}) }}
|
||||
</div>
|
||||
{{ form_errors(form.email) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form_label(form.password, 'Mot de passe', {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}}) }}
|
||||
<div class="mt-1">
|
||||
{{ form_widget(form.password, {'attr': {'class': 'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm'}}) }}
|
||||
</div>
|
||||
{{ form_errors(form.password) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form_label(form.pseudo, 'Pseudo', {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}}) }}
|
||||
<div class="mt-1">
|
||||
{{ form_widget(form.pseudo, {'attr': {'class': 'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm'}}) }}
|
||||
</div>
|
||||
{{ form_errors(form.pseudo) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form_label(form.firstname, 'Prénom', {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}}) }}
|
||||
<div class="mt-1">
|
||||
@ -51,27 +25,81 @@
|
||||
{{ form_errors(form.lastname) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form_label(form.pseudo, 'Pseudo', {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}}) }}
|
||||
<div class="mt-1">
|
||||
{{ form_widget(form.pseudo, {
|
||||
'attr': {
|
||||
'class': form.pseudo.vars.errors|length > 0 ? 'w-full px-3 py-2 border border-red-500 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm' : 'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm'
|
||||
}
|
||||
}) }}
|
||||
</div>
|
||||
<div class="text-sm text-red-600 mt-2">
|
||||
{{ form_errors(form.pseudo) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form_label(form.email, 'Email', {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}}) }}
|
||||
<div class="mt-1">
|
||||
{{ form_widget(form.email, {
|
||||
'attr': {
|
||||
'class': form.email.vars.errors|length > 0 ? 'w-full px-3 py-2 border border-red-500 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm' : 'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm'
|
||||
}
|
||||
}) }}
|
||||
</div>
|
||||
<div class="text-sm text-red-600 mt-2">
|
||||
{{ form_errors(form.email) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form_label(form.password, 'Mot de passe', {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}}) }}
|
||||
<div class="mt-1">
|
||||
{{ form_widget(form.password, {'attr': {'class': 'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm'}}) }}
|
||||
</div>
|
||||
{{ form_errors(form.password) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form_label(form.confirmPassword, 'Confirmer le mot de passe', {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}}) }}
|
||||
<div class="mt-1">
|
||||
{{ form_widget(form.confirmPassword, {'attr': {'class': 'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm'}}) }}
|
||||
</div>
|
||||
{{ form_errors(form.confirmPassword) }}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
{{ form_widget(form.submit, {
|
||||
'attr': {'class': 'w-full px-4 py-2 bg-indigo-600 text-white font-semibold rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500'}
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
{% for type, messages in app.flashes %}
|
||||
<div class="mb-4">
|
||||
<div class="mb-4 mt-4">
|
||||
{% for message in messages %}
|
||||
<div class="text-sm {% if type == 'success' %}text-green-600{% else %}text-red-600{% endif %}">
|
||||
{{ message }}
|
||||
<div class="text-sm p-4 rounded-lg shadow-lg
|
||||
{% if type == 'success' %}
|
||||
bg-green-100 text-green-700 border-l-4 border-green-500
|
||||
{% else %}
|
||||
bg-red-100 text-red-700 border-l-4 border-red-500
|
||||
{% endif %}
|
||||
transition-all transform hover:scale-105">
|
||||
<div class="font-semibold">
|
||||
{% if type == 'success' %}
|
||||
Succès:
|
||||
{% else %}
|
||||
Erreur:
|
||||
{% endif %}
|
||||
</div>
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ form_end(form) }}
|
||||
{% if error %}
|
||||
<div class="text-red-500 text-sm mt-2">
|
||||
{{ error.messageKey|trans(error.messageData, 'security') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
Reference in New Issue
Block a user