diff --git a/public/css/style.css b/public/css/style.css index f6ed8fd9b3d9ed086dc8f9c86093b27b95f1951d..0198bae8164b1da7777ac9eeeab2423663febdc1 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -8,14 +8,7 @@ body { background: #f4f4f4; } -header { - display: flex; - align-items: center; - background: #00B8DE; - color: white; - padding: 10px; - gap: 15px; -} + .user-icon { width: 35px; @@ -28,7 +21,7 @@ h1 { margin: 0; } -h1 a { +a { color: white; text-decoration: none; } @@ -182,7 +175,7 @@ body { header { display: flex; align-items: center; - background: #00B8DE; + background: #14223c; color: white; padding: 10px; gap: 15px; diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index 2f90bb92e7070c47876bf413018207e8b8d6b06d..8df8aa9271fce0a6cb4cdd90c26d5477009befb8 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -29,9 +29,12 @@ class HomeController extends AbstractController if ($invitation_id) { $user = $this->getUser(); if ($user) { - $user->addInvitation($entityManager->find(Invitation::class, $invitation_id)); - $entityManager->persist($user); - $entityManager->flush(); + $invitation = $entityManager->find(Invitation::class, $invitation_id) ; + if ($invitation) { + $user->addInvitation($invitation); + $entityManager->flush(); + } + $request->getSession()->remove("invitation_token"); } } diff --git a/src/Controller/InvitationController.php b/src/Controller/InvitationController.php index ef4d7693a56e1280b954a1288c13d87533af4ba1..df5e06ac19a039dcc00bb8ededb34a4cfa472cfa 100644 --- a/src/Controller/InvitationController.php +++ b/src/Controller/InvitationController.php @@ -34,10 +34,10 @@ final class InvitationController extends AbstractController { $invitation = new Invitation(); - $user = $this->getUser() ; + $user = $this->getUser() ; - $data = json_decode($request->getContent(),true) ; - $wishlist_id = $data["wishlist_id"] ?? null ; + $data = json_decode($request->getContent(),true) ; + $wishlist_id = $data["wishlist_id"] ?? null ; if (!$wishlist_id) { return new JsonResponse(["error"=>"Missing wishlist_id"] , Response::HTTP_BAD_REQUEST) ; @@ -45,43 +45,26 @@ final class InvitationController extends AbstractController if (!$user) { return $this->createAccessDeniedException('User is not connected'); - } + } + $wishlist = $entityManager->find(Wishlist::class, $wishlist_id); - $invitation->setInviter($user); - $invitation->setWishlist($entityManager->find(Wishlist::class, $wishlist_id)); - $entityManager->persist($invitation); - $entityManager->flush(); + $existing_invitation = $entityManager->getRepository(Invitation::class)->findOneBy(['wishlist'=>$wishlist , 'inviter'=>$user]) ; + if (!$existing_invitation) { + $invitation->setInviter($user); + $invitation->setWishlist($entityManager->find(Wishlist::class, $wishlist_id)); + $entityManager->persist($invitation); + $entityManager->flush(); + + } else { + $invitation = $existing_invitation ; + } return new JsonResponse(["joint_creation_URL"=> InvitationController::generateJointCreationURL($invitation->getId())] , Response::HTTP_CREATED); } - #[Route('/{id}', name: 'app_invitation_show', methods: ['GET'])] - public function show(Invitation $invitation): Response - { - return $this->render('invitation/show.html.twig', [ - 'invitation' => $invitation, - ]); - } - - #[Route('/{id}/edit', name: 'app_invitation_edit', methods: ['GET', 'POST'])] - public function edit(Request $request, Invitation $invitation, EntityManagerInterface $entityManager): Response - { - $form = $this->createForm(InvitationType::class, $invitation); - $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $entityManager->flush(); - - return $this->redirectToRoute('app_invitation_index', [], Response::HTTP_SEE_OTHER); - } - - return $this->render('invitation/edit.html.twig', [ - 'invitation' => $invitation, - 'form' => $form, - ]); - } #[Route('/{id}', name: 'app_invitation_delete', methods: ['POST'])] public function delete(Request $request, Invitation $invitation, EntityManagerInterface $entityManager): Response @@ -96,19 +79,21 @@ final class InvitationController extends AbstractController #[Route('/accept_invitation/{id}' , name: 'app_accept_invitation' , methods:['POST', 'GET'])] - public function acceptInvitation(Invitation $invitation ){ + public function acceptInvitation(Invitation $invitation , EntityManagerInterface $entityManager ){ $user = $this->getUser(); if ($user) { $user->acceptInvitation($invitation->getId()); - return new Response('Invitation accepté avec succès!', Response::HTTP_OK); + $entityManager->flush(); + return $this->redirectToRoute('app_invitation_index', [], Response::HTTP_SEE_OTHER); } else { - return $this->createAccessDeniedException("Vous pouvez accèder cette API sans authentification!") ; + return $this->createAccessDeniedException("Vous ne pouvez pas accèder à cette API sans authentification!") ; } } + private function generateJointCreationURL(int $invitation_id): string { $secretKey = 'top_secret_key_789/*-'; @@ -116,23 +101,23 @@ final class InvitationController extends AbstractController $token = base64_encode($invitation_id . '|' . $hash); $serverIp = $_SERVER['SERVER_ADDR'] ?? '127.0.0.1'; - return sprintf('http://%s/login?invitation_token=%s', $serverIp, rtrim(strtr($token, '+/', '-_'), '=')); + return sprintf('http://%s:8000/login?invitation_token=%s', $serverIp, rtrim(strtr($token, '+/', '-_'), '=')); } public static function verifyJointCreationToken(string $token): ?int { - $secretKey = 'top_secret_key_789/*-'; + $secretKey = 'top_secret_key_789/*-'; - $token = strtr($token, '-_', '+/'); + $token = strtr($token, '-_', '+/'); $token = base64_decode($token); if (!$token) { - return null; + return null; } $parts = explode('|', $token); if (count($parts) !== 2) { - return null; + return null; } [$invitation_id, $hash] = $parts; @@ -140,7 +125,7 @@ final class InvitationController extends AbstractController $expectedHash = hash_hmac('sha256', $invitation_id, $secretKey); if (!hash_equals($expectedHash, $hash)) { - return null; + return null; } return (int) $invitation_id; @@ -150,17 +135,3 @@ final class InvitationController extends AbstractController } - /* $form = $this->createForm(InvitationType::class, $invitation); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - $entityManager->persist($invitation); - $entityManager->flush(); - - return $this->redirectToRoute('app_invitation_index', [], Response::HTTP_SEE_OTHER); - } - - return $this->render('invitation/new.html.twig', [ - 'invitation' => $invitation, - 'form' => $form, - ]); */ \ No newline at end of file diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 411f3e0493822f1762c2810a786d006cfa61482f..9c98ecfdee3ee61e8cbfa479bca2ef45a5bdab1e 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -2,6 +2,8 @@ namespace App\Controller; +use App\Entity\Wishlist; +use App\Form\WishlistType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -44,7 +46,10 @@ final class UserController extends AbstractController $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $wishlist->setOwner($this->getUser()); + $user = $this->getUser(); + $wishlist->addAuthor($user); + $user->addToAuthorWishlists($wishlist); + $entityManager->persist($wishlist); $entityManager->flush(); diff --git a/src/Controller/WishlistController.php b/src/Controller/WishlistController.php index bc5046206b7d498c9e7002bd9c2a3e0afa8de6e6..96e067f5e346f2921e6218bd75d4c05e22cf4e10 100644 --- a/src/Controller/WishlistController.php +++ b/src/Controller/WishlistController.php @@ -34,7 +34,9 @@ final class WishlistController extends AbstractController $form->handleRequest($request); // Handle the form submission if ($form->isSubmitted() && $form->isValid()) { - $wishlist->setOwner($this->getUser()); + $user = $this->getUser(); + $wishlist->addAuthor($user); + $user->addToAuthorWishlists($wishlist); $entityManager->persist($wishlist); // Persist the new wishlist to the database $entityManager->flush(); // Save changes to the database @@ -111,7 +113,11 @@ public function show(Wishlist $wishlist, Request $request): Response { // Validate the CSRF token before deleting the wishlist if ($this->isCsrfTokenValid('delete'.$wishlist->getId(), $request->getPayload()->getString('_token'))) { - $entityManager->remove($wishlist); // Remove the wishlist from the database + $user = $this->getUser(); + $user->removeWishlist($wishlist); + if ($wishlist->getAuthors()->isEmpty()){ // If the wishlist has no authors + $entityManager->remove($wishlist); // Remove the wishlist from the database + } $entityManager->flush(); // Save changes to the database } diff --git a/src/Entity/Invitation.php b/src/Entity/Invitation.php index 49ae991da3a87cd4b39c4b9337fba3cea5a3c69c..739d442bdb4d7eab0f2d8956c5b25a026e690003 100644 --- a/src/Entity/Invitation.php +++ b/src/Entity/Invitation.php @@ -17,12 +17,12 @@ class Invitation - #[ORM\ManyToOne(cascade: ['persist', 'remove'])] - #[ORM\JoinColumn(nullable: false)] + #[ORM\ManyToOne()] + #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] private ?Wishlist $wishlist = null; - #[ORM\ManyToOne(cascade: ['persist', 'remove'])] - #[ORM\JoinColumn(nullable: false)] + #[ORM\ManyToOne()] + #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] private ?User $inviter = null; diff --git a/src/Entity/User.php b/src/Entity/User.php index 5d5149d80ef571f366fc8b91cdb4111b0495b0a5..a3d2c2acdf614f10e5b42b48f25c15f10bef3942 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -56,7 +56,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column(length: 255, nullable: true)] private ?string $image = null; - #[ORM\OneToMany(mappedBy: 'owner', targetEntity: Wishlist::class , cascade: ['remove'])] + #[ORM\ManyToMany(inversedBy: 'authors', targetEntity: Wishlist::class , orphanRemoval: true)] + #[ORM\JoinTable(name: 'users_wishlists')] private Collection $wishlists; // #[ORM\OneToMany(mappedBy: 'invitedUser', targetEntity: Item::class)] @@ -72,9 +73,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface private Collection $invitations; - - - public function __construct() { $this->wishlists = new ArrayCollection(); @@ -177,24 +175,19 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this->wishlists; } - public function addToAuthorWhishlists(Wishlist $wishlist){ + public function addToAuthorWishlists(Wishlist $wishlist){ if (!$this->wishlists->contains($wishlist)) { $this->wishlists[] = $wishlist; - } + $wishlist->addAuthor($this) ; + } } - // public function getInvitations(): Collection - // { - // return $this->invitations; - // } - // public function addInvitation(Item $invitation): static - // { - // if (!$this->invitations->contains($invitation)) { - // $this->invitations[] = $invitation; - // $invitation->setInvitedUser($this); - // } - - // } + public function removeWishlist(Wishlist $wishlist){ + if ($this->wishlists->contains($wishlist)) { + $this->wishlists->removeElement($wishlist); + $wishlist->removeAuthor($this); + } + } /** * @return Collection<int, Invitation> @@ -225,7 +218,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface if ($this->invitations[$i]->getId() == $invitation_id) { $wishlist = $this->invitations[$i]->getWishlist() ; unset($this->invitations[$i]) ; - $this->addToAuthorWhishlists($wishlist); + $this->addToAuthorWishlists($wishlist); } } } diff --git a/src/Entity/Wishlist.php b/src/Entity/Wishlist.php index b6434e9ea1e19b89ad42e0ca3fe9691540422440..80d417e7ecd6dea57c6b4cc3cfa399e3560099a2 100644 --- a/src/Entity/Wishlist.php +++ b/src/Entity/Wishlist.php @@ -34,24 +34,34 @@ class Wishlist private Collection $items; - #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'wishlists' )] - #[ORM\JoinColumn(nullable: false)] - private ?User $owner = null; + #[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'wishlists' )] + private Collection $authors ; - public function getOwner(): ?User + public function getAuthors(): Collection { - return $this->owner; + return $this->authors; } - public function setOwner(?User $owner): static + public function addAuthor(?User $author): void { - $this->owner = $owner; - return $this; + if (!$this->authors->contains($author)) { + $this->authors->add($author); + $author->addToAuthorWishlists($this); + } + } + + + public function removeAuthor(?User $author): void { + if ($this->authors->contains($author)) { + $this->authors->removeElement($author); + $author->removeWishlist($this) ; + } } public function __construct() { $this->items = new ArrayCollection(); + $this->authors = new ArrayCollection(); } public function getId(): ?int diff --git a/templates/invitation/edit.html.twig b/templates/invitation/edit.html.twig deleted file mode 100644 index e39ad7e582d218de4b99815debf376f97ccf0ec1..0000000000000000000000000000000000000000 --- a/templates/invitation/edit.html.twig +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}Edit Invitation{% endblock %} - -{% block body %} - <h1>Edit Invitation</h1> - - {{ include('invitation/_form.html.twig', {'button_label': 'Update'}) }} - - <a href="{{ path('app_invitation_index') }}">back to list</a> - - {{ include('invitation/_delete_form.html.twig') }} -{% endblock %} diff --git a/templates/invitation/index.html.twig b/templates/invitation/index.html.twig index 44f53435d78af835b325ed4225d59f26b0d4fc80..1172c234ad991967bdca1767dd0d503d74fc7e89 100644 --- a/templates/invitation/index.html.twig +++ b/templates/invitation/index.html.twig @@ -28,9 +28,7 @@ {% for invitation in invitations %} <div class="wishlist"> <h2> - <a href="{{ path('app_invitation_show', {'id': invitation.id}) }}"> Wishlist {{ invitation.id }} - </a> </h2> <div class="wishlist-footer"> <p>Inviter: {{ invitation.inviter.firstName ?? '' }} {{ invitation.inviter.lastName ?? 'Unavailable' }}</p> diff --git a/templates/invitation/show.html.twig b/templates/invitation/show.html.twig deleted file mode 100644 index 514e69ce5a080888831ed35af2b6e554642f856d..0000000000000000000000000000000000000000 --- a/templates/invitation/show.html.twig +++ /dev/null @@ -1,22 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}Invitation{% endblock %} - -{% block body %} - <h1>Invitation</h1> - - <table class="table"> - <tbody> - <tr> - <th>Id</th> - <td>{{ invitation.id }}</td> - </tr> - </tbody> - </table> - - <a href="{{ path('app_invitation_index') }}">back to list</a> - - <a href="{{ path('app_invitation_edit', {'id': invitation.id}) }}">edit</a> - - {{ include('invitation/_delete_form.html.twig') }} -{% endblock %} diff --git a/templates/wishlist/index.html.twig b/templates/wishlist/index.html.twig index 2f584c23e2ff920e261bda52c462556c8c4a75a9..6af50c0a50d9e97641c7992d01f1a117be99e8da 100644 --- a/templates/wishlist/index.html.twig +++ b/templates/wishlist/index.html.twig @@ -32,6 +32,46 @@ text-align: center; margin-top: 20px; } + + +.sub-header { + background : #00B8DE ; + padding : 20px ; + display: flex; + flex-direction: row-reverse; + +} + +header { + display: flex; + align-items: center; + background: #14223c; + color: white; + padding: 10px; + gap: 15px; +} + +body { + background : #EDF3F4 ; +} + +.add-wishlist-btn{ + background: #14223c; + +} + +a { + text-decoration:none ; + color : #EDF3F4 ; +} + +a:hover{ + color : #EDF3F4 ; +} + +.wishlist > a { + color: #00B8DE; +} </style> <header> @@ -40,7 +80,9 @@ <input type="text" class="search-bar" placeholder="Search..."> </header> -<a href="{{ path('app_wishlist_new') }}" class="add-wishlist-btn">Add wishlist</a> +<div class="sub-header"> +<button class="add-wishlist-btn"> <a href="{{ path('app_wishlist_new') }}"> Add wishlist</a> </button> +</div> <div class="container"> {% for wishlist in wishlists %} diff --git a/templates/wishlist/new.html.twig b/templates/wishlist/new.html.twig index 4f84df8ea8b969131ec82da8fab4cd07a70da231..32d5f9e35279b93639a5c9cf9ba8c7580a2d2fd0 100644 --- a/templates/wishlist/new.html.twig +++ b/templates/wishlist/new.html.twig @@ -8,7 +8,7 @@ <style> body { font-family: Arial, sans-serif; - background: linear-gradient(135deg, #00B8DE, #99CC33) fixed; + background: #EDF3F4 ; color: white; text-align: center; padding: 20px; @@ -21,11 +21,9 @@ max-width: 600px; margin: 0 auto; padding: 20px; - background: rgba(255, 255, 255, 0.2); + background: #00B8DE ; border-radius: 10px; - border: 2px solid #99CC33; box-shadow: 0 4px 8px rgba(0,0,0,0.2); - backdrop-filter: blur(10px); } .form-group { margin-bottom: 1.5rem;