Compare commits

..

24 Commits

Author SHA1 Message Date
b0bd392ce3 -Ajout des colonnes catégories et edition 2025-01-30 15:37:44 +01:00
51c3be30db Merge remote-tracking branch 'origin/develop' into develop 2025-01-30 15:34:52 +01:00
c93bfd585e - Le pseudo et le mail est maintenant unique. 2025-01-30 15:30:49 +01:00
0524212df6 -Ajout de la connexion page favoris à la db 2025-01-30 15:28:48 +01:00
26950905d6 - Modification des Favoris bdd + code 2025-01-30 14:59:09 +01:00
4a84f5ec7d - Ajout dans inscription de la confirmation du mot de passe. 2025-01-30 14:48:12 +01:00
f6f7edcf34 - Ajout des favoris 2025-01-30 11:37:17 +01:00
2eaaa1d049 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	templates/apiSearch/index.html.twig
2025-01-30 11:36:01 +01:00
1b96974bcb - Séperation de la modal dans un fichier twig et js.
- Ajout d ela section commentaire dans la modal.
2025-01-30 11:32:41 +01:00
4acc1955a9 - Réglages du bug pour enlever des favoris 2025-01-30 11:26:54 +01:00
60e6d9daf7 - Ajout des favoris 2025-01-30 11:05:12 +01:00
4533246181 - Ajout du nombre de résultats recherche de livres 2025-01-30 09:53:22 +01:00
a8e2510a03 - Ajout d'une modal lorsqu'on clic sur un livre. 2025-01-30 09:24:43 +01:00
31f64b9e5a - Ajout d'une pagination
- Résultats plu pertinent, affichage des livres seulement
2025-01-30 09:19:46 +01:00
ed268e96fa - Réglage d'un bug add favoris 2025-01-29 19:02:29 +01:00
5ad47bb626 - Favoris qui s'affiche dans les recherches 2025-01-29 18:58:04 +01:00
2e3783399e - Modification du design du front pour les résultats de recherche. 2025-01-29 16:27:03 +01:00
4d541a860f - Suppression des favoris 2025-01-29 16:24:00 +01:00
da74ab2ea4 - Ajout du add favoris 2025-01-29 16:00:57 +01:00
c20f3bc933 - Ajout de la table Favoris.
- Relation entre user et favoris en onetomany.
2025-01-29 15:18:01 +01:00
74288b17a9 - Modification du design du front pour les résultats de recherche. 2025-01-29 14:43:28 +01:00
271c7c67e9 - Gestion navbar dynamique 2025-01-29 14:39:49 +01:00
ced8d321bc - Login fonctionnel 2025-01-29 14:11:28 +01:00
ae09474b49 - Ajout nom de la recherche 2025-01-29 13:06:50 +01:00
22 changed files with 1088 additions and 163 deletions

View File

@ -2,7 +2,7 @@ APP_ENV=dev
APP_SECRET= APP_SECRET=
DB_ROOT_PASSWORD=rootpswd DB_ROOT_PASSWORD=rootpswd
DB_DATABASE=cloudsprint DB_DATABASE=booknest
DB_USERNAME=dev DB_USERNAME=dev
DB_PASSWORD=cfai42 DB_PASSWORD=cfai42
DB_HOST=db DB_HOST=db

28
assets/js/modal.js Normal file
View 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');
});
}
});
});

View File

@ -19,6 +19,7 @@
"symfony/runtime": "7.0.*", "symfony/runtime": "7.0.*",
"symfony/security-bundle": "7.0.*", "symfony/security-bundle": "7.0.*",
"symfony/twig-bundle": "7.0.*", "symfony/twig-bundle": "7.0.*",
"symfony/validator": "7.0.*",
"symfony/webpack-encore-bundle": "^2.2", "symfony/webpack-encore-bundle": "^2.2",
"symfony/yaml": "7.0.*", "symfony/yaml": "7.0.*",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^2.12|^3.0",

97
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c53910010d80c77569e2b6c0828c9aa2", "content-hash": "c7a74db8c19635e6115f7567b0077bb0",
"packages": [ "packages": [
{ {
"name": "doctrine/cache", "name": "doctrine/cache",
@ -5374,6 +5374,101 @@
], ],
"time": "2023-11-26T15:16:53+00:00" "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", "name": "symfony/var-dumper",
"version": "v7.0.2", "version": "v7.0.2",

View File

@ -7,9 +7,11 @@ framework:
# Enables session support. Note that the session will ONLY be started if you read or write from it. # 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. # Remove or comment this section to explicitly disable session support.
session: session:
handler_id: null enabled: true
cookie_secure: auto handler_id: null # Utilise le gestionnaire de sessions par défaut
cookie_samesite: lax 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 #esi: true
#fragments: true #fragments: true

View File

@ -10,23 +10,18 @@ security:
class: App\Entity\User class: App\Entity\User
property: email property: email
firewalls: firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main: main:
lazy: true lazy: true
provider: app_user_provider provider: app_user_provider
form_login:
# activate different ways to authenticate login_path: app_login
# https://symfony.com/doc/current/security.html#the-firewall check_path: app_login
username_parameter: _username
# https://symfony.com/doc/current/security/impersonating_user.html password_parameter: _password
# switch_user: true default_target_path: home # Assure-toi que cette route existe
# Ajout de la gestion de la déconnexion
logout: logout:
path: /logout # Route pour déconnecter l'utilisateur path: /logout
target: /login # Redirection vers la page de login après déconnexion target: /login
# Easy way to control access for large sections of your site # Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used # Note: Only the *first* access control that matches will be used

View File

@ -7,6 +7,9 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use App\Repository\FavorisRepository;
use App\Entity\Favoris;
use Symfony\Component\HttpFoundation\JsonResponse;
class APISearchController extends AbstractController class APISearchController extends AbstractController
{ {
@ -21,21 +24,80 @@ class APISearchController extends AbstractController
{ {
// Récupérer le paramètre "q" depuis la requête // Récupérer le paramètre "q" depuis la requête
$query = $request->query->get('q'); $query = $request->query->get('q');
$nb_pages = $request->query->get('page') ?? '1';
// Appeler le service GoogleBooks avec la requête // 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')] #[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 // Appeler la méthode search et récupérer les résultats
$datas = $this->search($request); $datas = $this->search($request);
$query = $request->query->get('q');
$user = $this->getUser();
$favoris = $favorisRepository->getFavorisByUser($user);
// Afficher les résultats dans le template // Afficher les résultats dans le template
return $this->render('apiSearch/index.html.twig', [ return $this->render('apiSearch/index.html.twig', [
'controller_name' => 'APISearchController', 'controller_name' => 'APISearchController',
'datas' => $datas, 'datas' => $datas,
'query' => $query,
'favoris' => $favoris,
]); ]);
} }
} }

View 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,
]);
}
}

View File

@ -1,5 +1,4 @@
<?php <?php
namespace App\Controller; namespace App\Controller;
use App\Entity\User; use App\Entity\User;
@ -10,16 +9,19 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class RegistrationController extends AbstractController class RegistrationController extends AbstractController
{ {
private $entityManager; 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->entityManager = $entityManager;
$this->passwordHasher = $passwordHasher; $this->passwordHasher = $passwordHasher;
$this->validator = $validator;
} }
#[Route('/registration', name: 'app_registration')] #[Route('/registration', name: 'app_registration')]
@ -29,19 +31,43 @@ class RegistrationController extends AbstractController
$form = $this->createForm(RegistrationType::class, $user); $form = $this->createForm(RegistrationType::class, $user);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted()) {
// Hacher le mot de passe avant de persister l'utilisateur $existingEmail = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $user->getEmail()]);
$plainPassword = $user->getPassword(); // Récupération du mot de passe brut $existingPseudo = $this->entityManager->getRepository(User::class)->findOneBy(['pseudo' => $user->getPseudo()]);
$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é
// Persist de l'utilisateur haché if ($existingEmail) {
$this->entityManager->persist($user); $form->get('email')->addError(new \Symfony\Component\Form\FormError('Cet email est déjà utilisé.'));
$this->entityManager->flush(); }
// Message flash de succès if ($existingPseudo) {
$this->addFlash('success', 'Votre compte a été créé avec succès !'); $form->get('pseudo')->addError(new \Symfony\Component\Form\FormError('Ce pseudo est déjà pris.'));
return $this->redirectToRoute('home'); }
$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', [ return $this->render('registration/index.html.twig', [

170
src/Entity/Favoris.php Normal file
View 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;
}
}

View File

@ -3,6 +3,8 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
@ -31,6 +33,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
private ?string $lastName = null; 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 public function getId(): ?int
{ {
return $this->id; return $this->id;
@ -103,16 +117,56 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
public function getRoles(): array public function getRoles(): array
{ {
// TODO: Implement getRoles() method.
$roles = $this->roles;
if (empty($roles)) {
$roles[] = 'ROLE_USER';
}
return $roles;
} }
public function eraseCredentials(): void public function eraseCredentials(): void
{ {
// TODO: Implement eraseCredentials() method.
} }
public function getUserIdentifier(): string 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;
} }
} }

View File

@ -12,39 +12,44 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class RegistrationType extends AbstractType class RegistrationType extends AbstractType
{ {
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
$builder $builder
->add('email', EmailType::class, [ ->add('email', EmailType::class, [
'label' => 'Email', 'label' => 'Email',
'attr' => ['class' => 'form-control'] 'attr' => ['class' => 'form-control']
]) ])
->add('password', PasswordType::class, [ ->add('password', PasswordType::class, [
'label' => 'Mot de passe', 'label' => 'Mot de passe',
'attr' => ['class' => 'form-control'] 'attr' => ['class' => 'form-control']
]) ])
->add('pseudo', TextType::class, [ ->add('confirmPassword', PasswordType::class, [
'label' => 'Pseudo', 'label' => 'Confirmer le mot de passe',
'attr' => ['class' => 'form-control'] 'attr' => ['class' => 'form-control'],
]) 'mapped' => false, // Ce champ n'est pas mappé à l'entité User
->add('firstname', TextType::class, [ ])
'label' => 'Prénom', ->add('pseudo', TextType::class, [
'attr' => ['class' => 'form-control'] 'label' => 'Pseudo',
]) 'attr' => ['class' => 'form-control']
->add('lastname', TextType::class, [ ])
'label' => 'Nom', ->add('firstname', TextType::class, [
'attr' => ['class' => 'form-control'] 'label' => 'Prénom',
]) 'attr' => ['class' => 'form-control']
->add('submit', SubmitType::class, [ ])
'label' => 'S\'inscrire', ->add('lastname', TextType::class, [
'attr' => ['class' => 'btn btn-primary'] 'label' => 'Nom',
]); 'attr' => ['class' => 'form-control']
} ])
->add('submit', SubmitType::class, [
'label' => 'S\'inscrire',
'attr' => ['class' => 'btn btn-primary']
]);
}
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'data_class' => User::class, 'data_class' => User::class,
]); ]);
} }
} }

View 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()
// ;
// }
}

View File

@ -13,20 +13,22 @@ class GoogleBooksService
$this->client = $client; $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'; $url = 'https://www.googleapis.com/books/v1/volumes';
$response = $this->client->request('GET', $url, [ $response = $this->client->request('GET', $url, [
'query' => [ 'query' => [
'q' => $query, 'q' => $query,
'langRestrict' => $lang, 'langRestrict' => $lang,
'maxResults' => 10,
'startIndex' => $nb_pages,
'printType' => 'books',
], ],
]); ]);
// Convertir la réponse JSON en tableau PHP // Convertir la réponse JSON en tableau PHP
$dataArray = $response->toArray(); $dataArray = $response->toArray();
return $dataArray; return $dataArray;
} }
} }

View File

@ -135,6 +135,18 @@
"templates/base.html.twig" "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": { "symfony/web-profiler-bundle": {
"version": "7.0", "version": "7.0",
"recipe": { "recipe": {

View File

@ -1,23 +1,270 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Hello APITestController!{% endblock %} {% block title %}Recherche | {{ query }}{% endblock %}
{% block body %} {% block body %}
<div>
<div class="w-1/2 py-2 flex flex-col justify-center mt-5 mx-auto">
<div> <h1 class="text-2xl font-bold">Résultats pour :
{% for book in datas.items %} {{ query }}</h1>
<div class="flex justify-center"> </div>
<div class="border rounded-xl w-1/2 py-2 flex flex-col justify-center mt-5"> <div class="w-1/2 pb-1 flex flex-col justify-center mx-auto">
<h1 class="font-bold p-2 text-2xl ">{{ book.volumeInfo.title }}</h1> <p>
{{ datas.totalItems }} résultats trouvés
{% if book.volumeInfo.imageLinks is defined and book.volumeInfo.imageLinks.smallThumbnail is defined %} </p>
<img src="{{ book.volumeInfo.imageLinks.smallThumbnail }}" class="p-2" style="width: 20%" height="20px">
{% endif %}
</div>
</div> </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 %} {% endblock %}

View 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 %}

View File

@ -23,5 +23,7 @@
{% endblock %} {% endblock %}
{% block body %}{% endblock %} {% block body %}{% endblock %}
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
</body> </body>
</html> </html>

View File

@ -21,19 +21,19 @@
</a> </a>
<!-- Left navigation links --> <!-- Left navigation links -->
<ul class="list-style-none me-auto flex flex-col ps-0 lg:flex-row" data-twe-navbar-nav-ref> <ul class="list-style-none me-auto flex flex-col ps-0 lg:flex-row" data-twe-navbar-nav-ref>
<li <li>
class="mb-4 lg:mb-0 lg:pe-2" data-twe-nav-item-ref> </li>
<!-- Dashboard link --> {% if app.user is empty %}
<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> <!-- Inscription link -->
</li>
<!-- Team link -->
<li class="mb-4 lg:mb-0 lg:pe-2" data-twe-nav-item-ref> <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> <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> </li>
<!-- Projects link --> <!-- login link -->
<li class="mb-4 lg:mb-0 lg:pe-2" data-twe-nav-item-ref> <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> <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> </li>
{% endif %}
</ul> </ul>
</div> </div>
@ -57,28 +57,32 @@
</form> </form>
</div> </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>
<div {% if app.user %}
class="relative flex items-center"> <div
<!-- Cart Icon --> class="relative flex items-center">
<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="#"> <!-- Cart Icon -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewbox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <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">
<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 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> </svg>
</span>
</a>
</span>
</div> </a>
</div>
{% endif %}
</div> </div>
</nav> </nav>

View 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 %}

View File

@ -24,25 +24,13 @@
</div> </div>
</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"> <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"> <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 Se connecter
</button> </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> </div>
</form> </form>
</div> </div>

View File

@ -1,5 +1,3 @@
{# templates/registration/register.html.twig #}
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Inscription{% endblock %} {% block title %}Inscription{% endblock %}
@ -11,30 +9,6 @@
{{ form_start(form) }} {{ 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"> <div class="mb-4">
{{ form_label(form.firstname, 'Prénom', {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}}) }} {{ form_label(form.firstname, 'Prénom', {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}}) }}
<div class="mt-1"> <div class="mt-1">
@ -51,27 +25,81 @@
{{ form_errors(form.lastname) }} {{ form_errors(form.lastname) }}
</div> </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"> <div class="mt-6 text-center">
{{ form_widget(form.submit, { {{ 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'} '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> </div>
{% for type, messages in app.flashes %} {% for type, messages in app.flashes %}
<div class="mb-4"> <div class="mb-4 mt-4">
{% for message in messages %} {% for message in messages %}
<div class="text-sm {% if type == 'success' %}text-green-600{% else %}text-red-600{% endif %}"> <div class="text-sm p-4 rounded-lg shadow-lg
{{ message }} {% 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> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endfor %} {% endfor %}
{{ form_end(form) }} {{ form_end(form) }}
{% if error %}
<div class="text-red-500 text-sm mt-2">
{{ error.messageKey|trans(error.messageData, 'security') }}
</div>
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}