From f70f0c2af9ceebe35759ef7760fec0988512674f Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Fri, 20 Mar 2026 17:54:02 +0100 Subject: [PATCH] Add public events page, event detail route, copy URL button, organizer events list - Add /evenements public page with Meilisearch search, KnpPaginator (12/page), event cards grid - Add /evenement/{orgaSlug}/{id}-{eventSlug} public route with slug redirect - Add Event::getSlug() method - Update homepage stats with real event count - Update organizer detail page to list their public events - Update navbar: link Evenements to /evenements with active state - Add copy URL button on edit event page (visible only when online) - Add initCopyUrl() in app.js with clipboard API Co-Authored-By: Claude Opus 4.6 (1M context) --- assets/app.js | 15 ++++ src/Controller/HomeController.php | 88 ++++++++++++++++++++++- src/Entity/Event.php | 10 +++ templates/account/edit_event.html.twig | 13 ++++ templates/base.html.twig | 4 +- templates/home/event_detail.html.twig | 22 ++++++ templates/home/events.html.twig | 84 ++++++++++++++++++++++ templates/home/organizer_detail.html.twig | 14 +++- tests/Entity/EventTest.php | 13 ++++ 9 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 templates/home/event_detail.html.twig create mode 100644 templates/home/events.html.twig diff --git a/assets/app.js b/assets/app.js index 2872c9f..4b4c036 100644 --- a/assets/app.js +++ b/assets/app.js @@ -4,9 +4,24 @@ import { initTabs } from "./modules/tabs.js" import { registerEditor } from "./modules/editor.js" import { initCookieConsent } from "./modules/cookie-consent.js" +function initCopyUrl() { + const btn = document.getElementById('copy-url-btn') + if (!btn) return + const url = document.getElementById('event-url')?.textContent?.trim() + if (!url) return + + btn.addEventListener('click', () => { + globalThis.navigator.clipboard.writeText(url).then(() => { + btn.textContent = 'Copie !' + setTimeout(() => { btn.textContent = 'Copier le lien' }, 2000) + }) + }) +} + document.addEventListener('DOMContentLoaded', () => { initMobileMenu() initTabs() registerEditor() initCookieConsent() + initCopyUrl() }) diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index 5c9c042..7d70958 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -2,9 +2,13 @@ namespace App\Controller; +use App\Entity\Event; use App\Entity\User; +use App\Service\MeilisearchService; use Doctrine\ORM\EntityManagerInterface; +use Knp\Component\Pager\PaginatorInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -18,19 +22,57 @@ class HomeController extends AbstractController { $allUsers = $em->getRepository(User::class)->findAll(); $organizers = \count(array_filter($allUsers, fn (User $u) => \in_array('ROLE_ORGANIZER', $u->getRoles(), true) && $u->isApproved())); + $eventsCount = \count($em->getRepository(Event::class)->findBy(['isOnline' => true, 'isSecret' => false])); return $this->render('home/index.html.twig', [ 'breadcrumbs' => [ self::BREADCRUMB_HOME, ], 'stats' => [ - 'events' => 0, + 'events' => $eventsCount, 'organizers' => $organizers, 'tickets' => 0, ], ]); } + #[Route('/evenements', name: 'app_events')] + public function events(Request $request, EntityManagerInterface $em, PaginatorInterface $paginator, MeilisearchService $meilisearch): Response + { + $searchQuery = $request->query->getString('q', ''); + + if ('' !== $searchQuery) { + try { + $searchResults = $meilisearch->search('event_global', $searchQuery); + $eventIds = array_map(fn (array $hit) => $hit['id'], $searchResults['hits'] ?? []); + $eventsQuery = $eventIds + ? $em->getRepository(Event::class)->findBy(['id' => $eventIds]) + : []; + } catch (\Throwable) { + $eventsQuery = $em->getRepository(Event::class)->findBy( + ['isOnline' => true, 'isSecret' => false], + ['startAt' => 'ASC'], + ); + } + } else { + $eventsQuery = $em->getRepository(Event::class)->findBy( + ['isOnline' => true, 'isSecret' => false], + ['startAt' => 'ASC'], + ); + } + + $events = $paginator->paginate($eventsQuery, $request->query->getInt('page', 1), 12); + + return $this->render('home/events.html.twig', [ + 'events' => $events, + 'searchQuery' => $searchQuery, + 'breadcrumbs' => [ + self::BREADCRUMB_HOME, + ['name' => 'Evenements', 'url' => '/evenements'], + ], + ]); + } + #[Route('/organisateurs', name: 'app_organizers')] public function organizers(EntityManagerInterface $em): Response { @@ -62,12 +104,56 @@ class HomeController extends AbstractController ], 301); } + $events = $em->getRepository(Event::class)->findBy( + ['account' => $organizer, 'isOnline' => true, 'isSecret' => false], + ['startAt' => 'ASC'], + ); + return $this->render('home/organizer_detail.html.twig', [ + 'organizer' => $organizer, + 'events' => $events, + 'breadcrumbs' => [ + self::BREADCRUMB_HOME, + self::BREADCRUMB_ORGANIZERS, + ['name' => $organizer->getCompanyName() ?? $organizer->getFirstName().' '.$organizer->getLastName(), 'url' => '/organisateur/'.$organizer->getId().'-'.$organizer->getSlug()], + ], + ]); + } + + #[Route('/evenement/{orgaSlug}/{id}-{eventSlug}', name: 'app_event_detail', requirements: ['id' => '\d+', 'orgaSlug' => '[a-z0-9-]+', 'eventSlug' => '[a-z0-9-]+'])] + public function eventDetail(int $id, string $orgaSlug, string $eventSlug, EntityManagerInterface $em): Response + { + $event = $em->getRepository(Event::class)->find($id); + + if (!$event || !$event->isOnline()) { + throw $this->createNotFoundException('Evenement introuvable.'); + } + + $organizer = $event->getAccount(); + if ($orgaSlug !== $organizer->getSlug()) { + return $this->redirectToRoute('app_event_detail', [ + 'orgaSlug' => $organizer->getSlug(), + 'id' => $event->getId(), + 'eventSlug' => $event->getSlug(), + ], 301); + } + + if ($eventSlug !== $event->getSlug()) { + return $this->redirectToRoute('app_event_detail', [ + 'orgaSlug' => $organizer->getSlug(), + 'id' => $event->getId(), + 'eventSlug' => $event->getSlug(), + ], 301); + } + + return $this->render('home/event_detail.html.twig', [ + 'event' => $event, 'organizer' => $organizer, 'breadcrumbs' => [ self::BREADCRUMB_HOME, self::BREADCRUMB_ORGANIZERS, ['name' => $organizer->getCompanyName() ?? $organizer->getFirstName().' '.$organizer->getLastName(), 'url' => '/organisateur/'.$organizer->getId().'-'.$organizer->getSlug()], + ['name' => $event->getTitle(), 'url' => '/evenement/'.$organizer->getSlug().'/'.$event->getId().'-'.$event->getSlug()], ], ]); } diff --git a/src/Entity/Event.php b/src/Entity/Event.php index 8413f04..609cc37 100644 --- a/src/Entity/Event.php +++ b/src/Entity/Event.php @@ -177,6 +177,16 @@ class Event return $this; } + public function getSlug(): string + { + $slug = mb_strtolower(trim($this->title ?? '')); + $slug = transliterator_transliterate('Any-Latin; Latin-ASCII', $slug) ?: $slug; + $slug = (string) preg_replace('/[^a-z0-9]+/', '-', $slug); + $slug = trim($slug, '-'); + + return '' === $slug ? 'evenement' : $slug; + } + public function isSecret(): bool { return $this->isSecret; diff --git a/templates/account/edit_event.html.twig b/templates/account/edit_event.html.twig index 7b941bb..41b0c80 100644 --- a/templates/account/edit_event.html.twig +++ b/templates/account/edit_event.html.twig @@ -64,6 +64,19 @@ + {% if event.online %} +
+
+

URL publique

+

{{ absolute_url(path('app_event_detail', {orgaSlug: event.account.slug, id: event.id, eventSlug: event.slug})) }}

+
+ +
+ + {% endif %} +
diff --git a/templates/base.html.twig b/templates/base.html.twig index eeb3e98..2711f2c 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -94,7 +94,7 @@