Compare commits

...

13 Commits

Author SHA1 Message Date
85b5c6eb91 - Ajout Entity Avis
- Déplacement js dans le dossier public
2025-02-13 23:12:57 +01:00
da9e7057bc - Ajout d'une image par défaut lorsqu'il n'y en a aucune. 2025-02-13 22:33:27 +01:00
918c6fe6dc - Ajout de la favicon 2025-01-30 16:22:16 +01:00
e624949fc3 - Ajout de la favicon 2025-01-30 16:21:36 +01:00
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
17 changed files with 676 additions and 164 deletions

View File

@ -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

View File

@ -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
View File

@ -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",

BIN
public/img/logo-cropped.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -9,6 +9,7 @@ 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
{
@ -31,27 +32,51 @@ class APISearchController extends AbstractController
}
#[Route('/toggleLike/{idGoogle}', name: 'like', methods: "POST")]
public function addFavoris(FavorisRepository $favorisRepository, String $idGoogle)
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é']);
return $this->json(['success' => true, 'message' => 'Favoris ajouté', "status" => "added"]);
}
#[Route('/untoggleLike/{idGoogle}', name: 'unlike', methods: "POST")]
public function removeFavoris(FavorisRepository $favorisRepository, String $idGoogle)
{
$favoris = new Favoris();
$user = $this->getUser();
$favoris->setUser($user);
$favoris->setIdGoogle($idGoogle);
$favorisRepository->removeFavoris($user, $idGoogle);
return $this->json(['success' => true, 'message' => 'Favoris supprimé']);
return $this->json(['success' => true, 'message' => 'Favoris supprimé', "status" => "removed"]);
}
#[Route('/api/search', name: 'api_search')]

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
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,44 @@ 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', [

View File

@ -19,6 +19,30 @@ class Favoris
#[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;
@ -47,4 +71,100 @@ class Favoris
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

@ -39,9 +39,13 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\OneToMany(mappedBy: 'user', targetEntity: Favoris::class)]
private Collection $favoris;
#[ORM\OneToMany(mappedBy: 'id_user', targetEntity: Avis::class)]
private Collection $avis;
public function __construct()
{
$this->favoris = new ArrayCollection();
$this->avis = new ArrayCollection();
}
@ -169,4 +173,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this;
}
/**
* @return Collection<int, Avis>
*/
public function getAvis(): Collection
{
return $this->avis;
}
public function addAvi(Avis $avi): static
{
if (!$this->avis->contains($avi)) {
$this->avis->add($avi);
$avi->setIdUser($this);
}
return $this;
}
public function removeAvi(Avis $avi): static
{
if ($this->avis->removeElement($avi)) {
// set the owning side to null (unless already changed)
if ($avi->getIdUser() === $this) {
$avi->setIdUser(null);
}
}
return $this;
}
}

View File

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

View File

@ -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": {
@ -158,6 +170,7 @@
},
"files": [
"assets/app.js",
"assets/js/modal.js",
"assets/styles/app.css",
"config/packages/webpack_encore.yaml",
"package.json",

View File

@ -14,6 +14,7 @@
</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">
@ -23,12 +24,15 @@
{% 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">
<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 w-[200px] h-[250] object-cover">
{% else %}
<img src="https://fakeimg.pl/550x750?text=no+cover" class="p-2 object-cover">
{% 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>
@ -55,7 +59,7 @@
<span class="font-normal">{{ book.searchInfo.textSnippet | raw }}</span>
{% endif %}
</p>
</div>
</div>
<div class="w-full p-2">
@ -68,21 +72,31 @@
{% if app.user %}
{% set isLiked = book.id in favoris|map(f => f.getIdGoogle()) %}
<svg
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 transition-colors duration-300"
x-data="{ liked: {{ isLiked ? 'true' : 'false' }} }"
:fill="liked ? 'red' : 'none'"
:stroke="liked ? 'red' : 'currentColor'"
@click="toggleLike('{{ book.id }}', liked, $el); liked = !liked">
<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>
<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">
@ -143,16 +157,15 @@
{% set current_page = app.request.get('page') ? app.request.get('page') : 1 %}
{% if nb_pages > 1 %}
<div class="flex justify-center mt-5">
<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-1 bg-blue-500 text-black rounded-lg">Précédent</a>
<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">
<span class="px-3 py-1 bg-gray-500 text-white rounded-lg cursor-not-allowed">← Précédent</span>
</li>
{% endif %}
@ -160,7 +173,7 @@
{% 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-3 py-1 {{ i == current_page ? 'bg-blue-500 text-gray-700' : 'bg-gray-200 text-black' }} rounded-lg">{{ i }}</a>
<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 %}
@ -168,62 +181,95 @@
{# 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-1 bg-blue-500 text-black rounded-lg">Suivant</a>
<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-1 bg-gray-500 text-white rounded-lg cursor-not-allowed">Suivant</span>
<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) {
if (liked) {
fetch("{{ path('unlike', {'idGoogle': 'PLACEHOLDER'}) }}".replace('PLACEHOLDER', idGoogle), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({})
}).then(response => response.json()).then(data => {
if (data.status === 'removed') {
console.log('Livre retiré des favoris');
// Mettre à jour l'état visuel
element.setAttribute('x-data', '{ liked: false }');
element.querySelector('path').setAttribute('fill', 'none');
element.querySelector('path').setAttribute('stroke', 'currentColor');
}
}).catch(error => {
console.error('Erreur AJAX', error);
});
} else {
fetch("{{ path('like', {'idGoogle': 'PLACEHOLDER'}) }}".replace('PLACEHOLDER', idGoogle), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({})
}).then(response => response.json()).then(data => {
if (data.status === 'added') {
console.log('Livre ajouté aux favoris');
// Mettre à jour l'état visuel
element.setAttribute('x-data', '{ liked: true }');
element.querySelector('path').setAttribute('fill', 'red');
element.querySelector('path').setAttribute('stroke', 'red');
}
}).catch(error => {
console.error('Erreur AJAX', error);
});
}
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>
{% endblock %}

View File

@ -0,0 +1,78 @@
{% 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 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 w-[200px] h-[250] object-cover">
{% else %}
<img src="https://fakeimg.pl/550x750?text=no+cover" class="p-2 object-cover">
{% 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

@ -7,7 +7,7 @@
{% block title %}Bienvenue!
{% endblock %}
</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
<link rel="icon" href="{{ asset('img/logo-cropped.ico') }}" type="image/x-icon"/>
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}

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,21 +24,13 @@
</div>
</div>
<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>

View File

@ -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,16 +25,74 @@
{{ 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>