diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index af3ddc1e13bb222529633a8b4444c7ed44ca7ce9..d1a2e4707b4f9b020934623eb98861a0f981bced 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -13,11 +13,17 @@ class HomeController extends AbstractController #[Route('/', name: 'homepage')] public function index(): Response { + + $joint_creation_URL = isset($_GET["joint_creation_URL"]) ? $_GET["joint_creation_URL"] : null; $user = $this->getUser(); // Récupère l'utilisateur connecté $links = [ ]; + if ($joint_creation_URL) { + $links["joint_creation_URL"] = $joint_creation_URL; + } + // Ajoutez le lien "Admin Dashboard" uniquement si l'utilisateur est admin if ($user && $user->isAdmin()) { $links['Admin Dashboard'] = $this->generateUrl('admin_dashboard'); @@ -37,6 +43,8 @@ class HomeController extends AbstractController + + } return $this->render('home/index.html.twig', [ diff --git a/src/Controller/InvitationController.php b/src/Controller/InvitationController.php new file mode 100644 index 0000000000000000000000000000000000000000..36675c95413370dfc5c12d8611a9314258faabda --- /dev/null +++ b/src/Controller/InvitationController.php @@ -0,0 +1,137 @@ +<?php + +namespace App\Controller; + +use App\Entity\Invitation; +use App\Entity\Wishlist; +use App\Form\InvitationType; +use App\Repository\InvitationRepository; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +#[Route('/invitation')] +final class InvitationController extends AbstractController +{ + #[Route(name: 'app_invitation_index', methods: ['GET'])] + public function index(InvitationRepository $invitationRepository): Response + { + return $this->render('invitation/index.html.twig', [ + 'invitations' => $invitationRepository->findAll(), + ]); + } + + #[Route('/new', name: 'app_invitation_new', methods: ['GET', 'POST'])] + public function createInvitation(Request $request, EntityManagerInterface $entityManager): Response + { + $invitation = new Invitation(); + + $user = $this->getUser() ; + + if (!$user) { + return $this->createAccessDeniedException('User is not connected'); + } + + $invitation->setInviter($user); + $invitation->setWishlist($entityManager->find(Wishlist::class, $request->get(key: 'wishlist_id'))); + $entityManager->persist($invitation); + $entityManager->flush(); + + 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 + { + if ($this->isCsrfTokenValid('delete'.$invitation->getId(), $request->getPayload()->getString('_token'))) { + $entityManager->remove($invitation); + $entityManager->flush(); + } + + return $this->redirectToRoute('app_invitation_index', [], Response::HTTP_SEE_OTHER); + } + + + private function generateJointCreationURL(int $invitation_id): string { + $secretKey = 'top_secret_key_789/*-'; + $hash = hash_hmac('sha256', (string) $invitation_id, $secretKey); + $token = base64_encode($invitation_id . '|' . $hash); + + $serverIp = $_SERVER['SERVER_ADDR'] ?? '127.0.0.1'; + return sprintf('http://%s?invitation_token=%s', $serverIp, rtrim(strtr($token, '+/', '-_'), '=')); + } + + + private function verifyJointCreationToken(string $token): ?int { + $secretKey = 'top_secret_key_789/*-'; + + $token = strtr($token, '-_', '+/'); + $token = base64_decode($token); + + if (!$token) { + return null; + } + + $parts = explode('|', $token); + if (count($parts) !== 2) { + return null; + } + + [$invitation_id, $hash] = $parts; + + $expectedHash = hash_hmac('sha256', $invitation_id, $secretKey); + + if (!hash_equals($expectedHash, $hash)) { + return null; + } + + return (int) $invitation_id; + } + +} + + /* $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/RegistrationController.php b/src/Controller/RegistrationController.php index d2259d0b9e72eea820c889faa5f2920235e3f54c..72a0ab9a1cc11479b7f7d0a250a04e84c7584c72 100644 --- a/src/Controller/RegistrationController.php +++ b/src/Controller/RegistrationController.php @@ -20,22 +20,39 @@ class RegistrationController extends AbstractController $form = $this->createForm(RegistrationFormType::class, $user); $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - // Hacher le mot de passe - $hashedPassword = $passwordHasher->hashPassword($user, plainPassword: $form->get('password')->getData()); - $user->setPassword($hashedPassword); + if ($form->isSubmitted() ) { + + if ($form->isValid()){ + // Hacher le mot de passe + $hashedPassword = $passwordHasher->hashPassword($user, plainPassword: $form->get('password')->getData()); + $user->setPassword($hashedPassword); - // Sauvegarder l'utilisateur - $entityManager->persist($user); - $entityManager->flush(); + // Sauvegarder l'utilisateur + $entityManager->persist($user); + $entityManager->flush(); + + // Rediriger vers la page de connexion + return $this->redirectToRoute('login'); + } + + if (!$form->isValid()) { + $errors = []; + foreach ($form->getErrors(true) as $error) { + $errors[] = $error->getMessage(); + } + return $this->render('registration/register.html.twig', [ + 'registrationForm' => $form->createView(), + 'formErrors' => $errors, + ]); + } - // Rediriger vers la page de connexion - return $this->redirectToRoute('login'); } else { // Si le formulaire n'est pas valide, les erreurs seront disponibles dans la vue } + + return $this->render('registration/register.html.twig', [ 'registrationForm' => $form->createView(), ]); diff --git a/src/Entity/Invitation.php b/src/Entity/Invitation.php new file mode 100644 index 0000000000000000000000000000000000000000..9bc4384c1a2525d8b64713fc5c995750e7bbdf0d --- /dev/null +++ b/src/Entity/Invitation.php @@ -0,0 +1,61 @@ +<?php + +namespace App\Entity; + +use App\Repository\InvitationRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity(repositoryClass: InvitationRepository::class)] +class Invitation +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + + + #[ORM\OneToOne(cascade: ['persist', 'remove'])] + #[ORM\JoinColumn(nullable: false)] + private ?Wishlist $wishlist = null; + + #[ORM\OneToOne(cascade: ['persist', 'remove'])] + #[ORM\JoinColumn(nullable: false)] + private ?User $inviter = null; + + + public function getId(): ?int + { + return $this->id; + } + + + + + + public function getWishlist(): ?Wishlist + { + return $this->wishlist; + } + + public function setWishlist(Wishlist $wishlist): static + { + $this->wishlist = $wishlist; + + return $this; + } + + public function getInviter(): ?User + { + return $this->inviter; + } + + public function setInviter(User $inviter): static + { + $this->inviter = $inviter; + + return $this; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index 8a0a1ed285e932b5dbdf9e3a0f4eca855a710401..e08c1f5cc3565d2e7599478d8e54cc8053003942 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -65,13 +65,22 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\OneToMany(mappedBy: 'buyer', targetEntity: PurchaseProof::class, cascade: ['persist', 'remove'])] private Collection $purchaseProofs; + /** + * @var Collection<int, Invitation> + */ + #[ORM\ManyToMany(targetEntity: Invitation::class)] + private Collection $invitations; + + + + public function __construct() { $this->wishlists = new ArrayCollection(); - $this->invitations = new ArrayCollection(); $this->roles = ['ROLE_USER']; $this->isLocked = false; + $this->invitations = new ArrayCollection(); } public function getId(): ?int @@ -187,4 +196,29 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface // } + /** + * @return Collection<int, Invitation> + */ + public function getInvitations(): Collection + { + return $this->invitations; + } + + public function addInvitation(Invitation $invitation): static + { + if (!$this->invitations->contains($invitation)) { + $this->invitations->add($invitation); + } + + return $this; + } + + public function removeInvitation(Invitation $invitation): static + { + $this->invitations->removeElement($invitation); + + return $this; + } + + } diff --git a/src/Form/InvitationType.php b/src/Form/InvitationType.php new file mode 100644 index 0000000000000000000000000000000000000000..88900e313b6321522b2433f8f7f10567f4957ecb --- /dev/null +++ b/src/Form/InvitationType.php @@ -0,0 +1,31 @@ +<?php + +namespace App\Form; + +use App\Entity\Invitation; +use App\Entity\Wishlist; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class InvitationType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('wishlist', EntityType::class, [ + 'class' => Wishlist::class, + 'choice_label' => 'id', + ]) + + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Invitation::class, + ]); + } +} diff --git a/src/Repository/InvitationRepository.php b/src/Repository/InvitationRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..8cea308cb85d66e9b42c63fc66fe910d4bd0a933 --- /dev/null +++ b/src/Repository/InvitationRepository.php @@ -0,0 +1,43 @@ +<?php + +namespace App\Repository; + +use App\Entity\Invitation; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @extends ServiceEntityRepository<Invitation> + */ +class InvitationRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Invitation::class); + } + + // /** + // * @return Invitation[] Returns an array of Invitation objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('i') + // ->andWhere('i.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('i.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Invitation + // { + // return $this->createQueryBuilder('i') + // ->andWhere('i.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/templates/invitation/_delete_form.html.twig b/templates/invitation/_delete_form.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..f596b807656b17d9057a7bdfca9c310924ec4f74 --- /dev/null +++ b/templates/invitation/_delete_form.html.twig @@ -0,0 +1,4 @@ +<form method="post" action="{{ path('app_invitation_delete', {'id': invitation.id}) }}" onsubmit="return confirm('Are you sure you want to delete this item?');"> + <input type="hidden" name="_token" value="{{ csrf_token('delete' ~ invitation.id) }}"> + <button class="btn">Delete</button> +</form> diff --git a/templates/invitation/_form.html.twig b/templates/invitation/_form.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..bf20b98fb01ed38c82b670ff3fe5d7e207e80f16 --- /dev/null +++ b/templates/invitation/_form.html.twig @@ -0,0 +1,4 @@ +{{ form_start(form) }} + {{ form_widget(form) }} + <button class="btn">{{ button_label|default('Save') }}</button> +{{ form_end(form) }} diff --git a/templates/invitation/edit.html.twig b/templates/invitation/edit.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..e39ad7e582d218de4b99815debf376f97ccf0ec1 --- /dev/null +++ b/templates/invitation/edit.html.twig @@ -0,0 +1,13 @@ +{% 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 new file mode 100644 index 0000000000000000000000000000000000000000..b6d63b6fa238a3b8257914938b104dd67133383b --- /dev/null +++ b/templates/invitation/index.html.twig @@ -0,0 +1,33 @@ +{% extends 'base.html.twig' %} + +{% block title %}Invitation index{% endblock %} + +{% block body %} + <h1>Invitation index</h1> + + <table class="table"> + <thead> + <tr> + <th>Id</th> + <th>actions</th> + </tr> + </thead> + <tbody> + {% for invitation in invitations %} + <tr> + <td>{{ invitation.id }}</td> + <td> + <a href="{{ path('app_invitation_show', {'id': invitation.id}) }}">show</a> + <a href="{{ path('app_invitation_edit', {'id': invitation.id}) }}">edit</a> + </td> + </tr> + {% else %} + <tr> + <td colspan="2">no records found</td> + </tr> + {% endfor %} + </tbody> + </table> + + <a href="{{ path('app_invitation_new') }}">Create new</a> +{% endblock %} diff --git a/templates/invitation/new.html.twig b/templates/invitation/new.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..40c305be129741579e91ee3a0e217b144ce0c969 --- /dev/null +++ b/templates/invitation/new.html.twig @@ -0,0 +1,11 @@ +{% extends 'base.html.twig' %} + +{% block title %}New Invitation{% endblock %} + +{% block body %} + <h1>Create new Invitation</h1> + + {{ include('invitation/_form.html.twig') }} + + <a href="{{ path('app_invitation_index') }}">back to list</a> +{% endblock %} diff --git a/templates/invitation/show.html.twig b/templates/invitation/show.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..514e69ce5a080888831ed35af2b6e554642f856d --- /dev/null +++ b/templates/invitation/show.html.twig @@ -0,0 +1,22 @@ +{% 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 72227a94646d92b3540afaa00e16c1b6602432ca..f775e2ad4a9e9e5493905d7a6b69729efd4eb961 100644 --- a/templates/wishlist/index.html.twig +++ b/templates/wishlist/index.html.twig @@ -1,104 +1,36 @@ {% extends 'base.html.twig' %} -{% block title %}My Wishlists{% endblock %} - -{% block body %} -<style> - body { - font-family: Arial, sans-serif; - background: linear-gradient(135deg, #00B8DE, #99CC33) fixed; - color: white; - text-align: center; - padding: 20px; - } - .container { - max-width: 800px; - margin: auto; - display: flex; - flex-direction: column; - gap: 15px; - } - header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px; - } - .search-bar { - padding: 8px; - border-radius: 8px; - border: 2px solid #99CC33; - } - .add-wishlist-btn { - background: white; - color: #00B8DE; - padding: 10px 15px; - text-decoration: none; - border-radius: 8px; - font-weight: bold; - transition: 0.3s; - display: inline-block; - margin: 15px 0; - } - .add-wishlist-btn:hover { - background: #99CC33; - color: white; - transform: scale(1.1); - } - .wishlist { - background: rgba(255, 255, 255, 0.2); - padding: 15px; - border-radius: 10px; - border: 2px solid #99CC33; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - width: 100%; - max-width: 600px; - margin: auto; - } - .wishlist h2 { - margin: 10px 0; - } - .wishlist-items { - display: flex; - gap: 10px; - justify-content: center; - flex-wrap: wrap; - } - .wishlist-item { - background: white; - color: #00B8DE; - padding: 10px; - border-radius: 5px; - font-size: 1.5em; - } - .wishlist-footer { - margin-top: 10px; - font-size: 0.9em; - color: #e0f7fa; - } - .wishlist-actions { - display: flex; - gap: 10px; - justify-content: center; - margin-top: 10px; - } - .wishlist-actions button { - background: white; - border: none; - padding: 10px; - border-radius: 5px; - cursor: pointer; - transition: 0.3s; - } - .wishlist-actions button:hover { - background: #99CC33; - color: white; - } - @media (max-width: 768px) { - .wishlist { - width: 90%; - } - } +{% block title %} My Wishlists {% endblock %} + +{% block body %} + + +<style> +.modal { + display: none; /* Hide by default */ + position: fixed; + z-index: 1000; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + background-color: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); +} + +.modal-content { + text-align: center; +} + +.close { + position: absolute; + top: 10px; + right: 20px; + font-size: 24px; + cursor: pointer; +} + </style> <header> @@ -111,31 +43,94 @@ <div class="container"> {% for wishlist in wishlists %} - <div class="wishlist"> - <h2> - <a href="{{ path('app_wishlist_show', { 'id': wishlist.id }) }}"> - {{ wishlist.name }} - </a> - </h2> - - <div class="wishlist-items"> - {% for item in wishlist.items %} - <div class="wishlist-item">📷</div> - {% else %} - <p class="empty-state">No items yet.</p> - {% endfor %} - </div> - - <p class="wishlist-footer">{{ wishlist.deadline ? wishlist.deadline|date('Y-m-d') : 'No deadline' }}</p> - - <div class="wishlist-actions"> - <button title="Share wishlist">↗</button> - <button title="Edit title">✏</button> - <button title="Delete wishlist">🗑</button> - </div> + <div class="wishlist"> + <h2>{{ wishlist.name }}</h2> + <div class="wishlist-items"> + {% for item in wishlist.items %} + <div class="wishlist-item">📷</div> + {% endfor %} </div> - {% else %} - <p>You have no wishlists yet. Start by <a href="{{ path('app_wishlist_new') }}">creating one</a>.</p> + <p class="wishlist-footer">{{ wishlist.deadline | date('F j, Y') }}</p> + + <!-- Share Button --> + <button type="button" class="share-btn" data-wishlist-id="{{ wishlist.id }}">↗</button> + + <button title="Edit title">✏</button> + <button title="Delete wishlist">🗑</button> + </div> {% endfor %} </div> -{% endblock %} \ No newline at end of file + +<!-- Modal Popup --> +<div id="share-modal" class="modal"> + <div class="modal-content"> + <span class="close">×</span> + <h3>Share Wishlist</h3> + <button id="invite-co-worker">Invite a Co-Worker</button> + <button id="share-wishes">Share Your Wishes!</button> + </div> +</div> + + +<script> +document.addEventListener("DOMContentLoaded", function () { + const shareButtons = document.querySelectorAll(".share-btn"); + const modal = document.getElementById("share-modal"); + const closeBtn = document.querySelector(".close"); + + let selectedWishlistId = null; // Store the selected wishlist ID + + shareButtons.forEach(button => { + button.addEventListener("click", function () { + selectedWishlistId = this.dataset.wishlistId; + modal.style.display = "block"; + }); + }); + + closeBtn.addEventListener("click", function () { + modal.style.display = "none"; + }); + + window.addEventListener("click", function (event) { + if (event.target === modal) { + modal.style.display = "none"; + } + }); + + // Handle Invite a Co-Worker Click + document.getElementById("invite-co-worker").addEventListener("click", async function () { + try { + const response = await fetch("{{path('app_invitation_new')}}", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ wishlist_id: selectedWishlistId }) + }); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + const responseData = await response.json(); + navigator.clipboard.writeText(responseData.url).then(() => { + alert("URL was copied to clipboard successfully!"); + }).catch(err => { + alert("Error copying URL to clipboard: " + err); + }); + } catch (error) { + console.error("Error:", error); + alert("Failed to send invitation."); + } + }); + + // Handle Share Your Wishes Click + document.getElementById("share-wishes").addEventListener("click", function () { + alert("Sharing your wishes! (Implement your sharing logic here)"); + }); +}); + + +</script> + +{% endblock %}