```
✨ feat(EventsController): Affiche la liste des événements et les détails
Ajoute l'affichage des événements et de leurs détails. Ajoute aussi la gestion des affiches.
```
This commit is contained in:
@@ -1,3 +1,2 @@
|
||||
import './admin.scss'
|
||||
|
||||
import * as Turbo from "@hotwired/turbo"
|
||||
|
||||
@@ -52,6 +52,28 @@ vich_uploader:
|
||||
inject_on_load: true
|
||||
delete_on_update: true
|
||||
delete_on_remove: true
|
||||
product:
|
||||
uri_prefix: /storage/product
|
||||
upload_destination: '%kernel.project_dir%/public/storage/product'
|
||||
namer: Vich\UploaderBundle\Naming\UniqidNamer # Replaced namer
|
||||
inject_on_load: true
|
||||
delete_on_update: true
|
||||
delete_on_remove: true
|
||||
events:
|
||||
uri_prefix: /storage/events
|
||||
upload_destination: '%kernel.project_dir%/public/storage/events'
|
||||
namer: Vich\UploaderBundle\Naming\UniqidNamer # Replaced namer
|
||||
inject_on_load: true
|
||||
delete_on_update: true
|
||||
delete_on_remove: true
|
||||
event_picture:
|
||||
uri_prefix: /event_picture/events
|
||||
upload_destination: '%kernel.project_dir%/public/storage/event_picture'
|
||||
namer: Vich\UploaderBundle\Naming\UniqidNamer # Replaced namer
|
||||
directory_namer: App\VichUploader\DirectoryNamer\EventName
|
||||
inject_on_load: true
|
||||
delete_on_update: true
|
||||
delete_on_remove: true
|
||||
#mappings:
|
||||
# products:
|
||||
# uri_prefix: /images/products
|
||||
|
||||
35
migrations/Version20251202200532.php
Normal file
35
migrations/Version20251202200532.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20251202200532 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE event (id SERIAL NOT NULL, title VARCHAR(255) NOT NULL, start_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, end_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, location VARCHAR(255) NOT NULL, organizer VARCHAR(255) NOT NULL, events_file_name VARCHAR(255) DEFAULT NULL, events_dimensions JSON DEFAULT NULL, events_size VARCHAR(255) DEFAULT NULL, events_mine_type VARCHAR(255) DEFAULT NULL, events_original_name VARCHAR(255) DEFAULT NULL, update_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('COMMENT ON COLUMN event.start_at IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN event.end_at IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN event.update_at IS \'(DC2Type:datetime_immutable)\'');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE SCHEMA public');
|
||||
$this->addSql('DROP TABLE event');
|
||||
}
|
||||
}
|
||||
@@ -14,17 +14,21 @@ use App\Entity\Ag\MainMember;
|
||||
use App\Entity\Ag\MainOrder;
|
||||
use App\Entity\Ag\MainSigned;
|
||||
use App\Entity\Ag\MainVote;
|
||||
use App\Entity\Event;
|
||||
use App\Entity\Members;
|
||||
use App\Entity\MembersCotisations;
|
||||
use App\Entity\Products;
|
||||
use App\Form\EventType;
|
||||
use App\Form\MembersType;
|
||||
use App\Form\ProductsType;
|
||||
use App\Form\RequestPasswordConfirmType;
|
||||
use App\Form\RequestPasswordRequestType;
|
||||
use App\Repository\Ag\MainRepository;
|
||||
use App\Repository\EventRepository;
|
||||
use App\Repository\MembersCotisationsRepository;
|
||||
use App\Repository\MembersRepository;
|
||||
use App\Repository\ProductsRepository;
|
||||
use App\Service\Events\AdvertEvent;
|
||||
use App\Service\Mailer\Mailer;
|
||||
use App\Service\Payments\PaymentClient;
|
||||
use App\Service\Pdf\AgAdh;
|
||||
@@ -242,9 +246,43 @@ class AdminController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/admin/events', name: 'admin_events', options: ['sitemap' => false], methods: ['GET'])]
|
||||
public function adminEvents(): Response
|
||||
public function adminEvents(EventRepository $eventRepository): Response
|
||||
{
|
||||
return $this->render('admin/dashboard.twig', [
|
||||
return $this->render('admin/events.twig', [
|
||||
'events' => $eventRepository->findBy([],['id' => 'DESC']),
|
||||
]);
|
||||
}
|
||||
#[Route(path: '/admin/events/delete', name: 'admin_events_delete', options: ['sitemap' => false], methods: ['GET','POST'])]
|
||||
public function adminEventDelete(?Event $event,EntityManagerInterface $entityManager): Response
|
||||
{
|
||||
|
||||
}
|
||||
#[Route(path: '/admin/events/add', name: 'admin_events_create', options: ['sitemap' => false], methods: ['GET','POST'])]
|
||||
#[Route(path: '/admin/events/{id}', name: 'admin_events_edit', options: ['sitemap' => false], methods: ['GET','POST'])]
|
||||
public function adminEventAdd(?Event $event,Request $request,EntityManagerInterface $entityManager,EventDispatcherInterface $eventDispatcher): Response
|
||||
{
|
||||
$isNew = false;
|
||||
if(is_null($event)) {
|
||||
$event = new Event();
|
||||
$isNew = true;
|
||||
}
|
||||
$form = $this->createForm(EventType::class,$event);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$event->setAffiche($request->files->all()['event']['affiche']);
|
||||
$event->setUpdateAt(new \DateTimeImmutable('now'));
|
||||
if($isNew) {
|
||||
$sendEvent = new AdvertEvent($event);
|
||||
$eventDispatcher->dispatch($sendEvent);
|
||||
}
|
||||
$entityManager->persist($event);
|
||||
$entityManager->flush();
|
||||
|
||||
return $this->redirectToRoute('admin_events');
|
||||
}
|
||||
return $this->render('admin/events/add.twig', [
|
||||
'form' => $form->createView(),
|
||||
'event' => $event
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\Account;
|
||||
use App\Entity\AccountResetPasswordRequest;
|
||||
use App\Entity\Event;
|
||||
use App\Form\RequestPasswordConfirmType;
|
||||
use App\Form\RequestPasswordRequestType;
|
||||
use App\Repository\EventRepository;
|
||||
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
|
||||
use App\Service\ResetPassword\Event\ResetPasswordEvent;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -23,17 +25,21 @@ class EventsController extends AbstractController
|
||||
{
|
||||
|
||||
#[Route(path: '/events', name: 'app_events', options: ['sitemap' => true], methods: ['GET'])]
|
||||
public function index(): Response
|
||||
public function index(EventRepository $eventRepository): Response
|
||||
{
|
||||
return $this->render('event.twig',[
|
||||
'events' => [
|
||||
|
||||
]
|
||||
'events' => $eventRepository->findBy([],['startAt' => 'DESC']),
|
||||
]);
|
||||
}
|
||||
#[Route(path: '/events/{id}', name: 'app_event_details', options: ['sitemap' => false], methods: ['GET'])]
|
||||
public function eventDetails(): Response
|
||||
public function eventDetails(?Event $event): Response
|
||||
{
|
||||
if(is_null($event)) {
|
||||
return $this->redirectToRoute('app_events');
|
||||
}
|
||||
return $this->render('event_view.twig',[
|
||||
'event' => $event,
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
226
src/Entity/Event.php
Normal file
226
src/Entity/Event.php
Normal file
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\EventRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
use Vich\UploaderBundle\Mapping\Annotation as Vich;
|
||||
|
||||
#[Vich\Uploadable()]
|
||||
#[ORM\Entity(repositoryClass: EventRepository::class)]
|
||||
class Event
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $title = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $startAt = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $endAt = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $location = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $organizer = null;
|
||||
|
||||
#[Vich\UploadableField(mapping: 'events',fileNameProperty: 'eventsFileName', size: 'eventsSize', mimeType: 'eventsMineType', originalName: 'eventsOriginalName',dimensions: 'eventsDimensions')]
|
||||
private ?File $affiche = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?string $eventsFileName = null;
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?array $eventsDimensions = [];
|
||||
#[ORM\Column(length: 255,nullable: true)]
|
||||
private ?string $eventsSize = null;
|
||||
#[ORM\Column(length: 255,nullable: true)]
|
||||
private ?string $eventsMineType = null;
|
||||
#[ORM\Column(length: 255,nullable: true)]
|
||||
private ?string $eventsOriginalName = null;
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTimeImmutable $updateAt;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStartAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->startAt;
|
||||
}
|
||||
|
||||
public function setStartAt(\DateTimeImmutable $startAt): static
|
||||
{
|
||||
$this->startAt = $startAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEndAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->endAt;
|
||||
}
|
||||
|
||||
public function setEndAt(\DateTimeImmutable $endAt): static
|
||||
{
|
||||
$this->endAt = $endAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLocation(): ?string
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function setLocation(string $location): static
|
||||
{
|
||||
$this->location = $location;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOrganizer(): ?string
|
||||
{
|
||||
return $this->organizer;
|
||||
}
|
||||
|
||||
public function setOrganizer(string $organizer): static
|
||||
{
|
||||
$this->organizer = $organizer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeImmutable|null
|
||||
*/
|
||||
public function getUpdateAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->updateAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return File|null
|
||||
*/
|
||||
public function getAffiche(): ?File
|
||||
{
|
||||
return $this->affiche;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|null
|
||||
*/
|
||||
public function getEventsDimensions(): ?array
|
||||
{
|
||||
return $this->eventsDimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getEventsFileName(): ?string
|
||||
{
|
||||
return $this->eventsFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getEventsMineType(): ?string
|
||||
{
|
||||
return $this->eventsMineType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getEventsOriginalName(): ?string
|
||||
{
|
||||
return $this->eventsOriginalName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getEventsSize(): ?string
|
||||
{
|
||||
return $this->eventsSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeImmutable|null $updateAt
|
||||
*/
|
||||
public function setUpdateAt(?\DateTimeImmutable $updateAt): void
|
||||
{
|
||||
$this->updateAt = $updateAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param File|null $affiche
|
||||
*/
|
||||
public function setAffiche(?File $affiche): void
|
||||
{
|
||||
$this->affiche = $affiche;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $eventsDimensions
|
||||
*/
|
||||
public function setEventsDimensions(?array $eventsDimensions): void
|
||||
{
|
||||
$this->eventsDimensions = $eventsDimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $eventsFileName
|
||||
*/
|
||||
public function setEventsFileName(?string $eventsFileName): void
|
||||
{
|
||||
$this->eventsFileName = $eventsFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $eventsMineType
|
||||
*/
|
||||
public function setEventsMineType(?string $eventsMineType): void
|
||||
{
|
||||
$this->eventsMineType = $eventsMineType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $eventsOriginalName
|
||||
*/
|
||||
public function setEventsOriginalName(?string $eventsOriginalName): void
|
||||
{
|
||||
$this->eventsOriginalName = $eventsOriginalName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $eventsSize
|
||||
*/
|
||||
public function setEventsSize(?string $eventsSize): void
|
||||
{
|
||||
$this->eventsSize = $eventsSize;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Repository\EventRepository;
|
||||
use App\Repository\ProductsRepository;
|
||||
use Cocur\Slugify\Slugify;
|
||||
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
|
||||
@@ -16,7 +17,7 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
#[AsEventListener(event: SitemapPopulateEvent::class, method: 'onSitemapPopulate', priority: 10)]
|
||||
class SitemapSubscriber
|
||||
{
|
||||
public function __construct(private readonly ProductsRepository $productsRepository,private CacheManager $cacheManager)
|
||||
public function __construct(private readonly EventRepository $eventRepository,private readonly ProductsRepository $productsRepository,private CacheManager $cacheManager)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -116,6 +117,17 @@ class SitemapSubscriber
|
||||
$urlContainer->addUrl($decoratedUrlAbout, 'default');
|
||||
|
||||
$s = new Slugify();
|
||||
foreach ($this->eventRepository->findAll() as $eventItem) {
|
||||
$urlAbout = new UrlConcrete($urlGenerator->generate('app_event_details', ['id'=>$eventItem->getid()], UrlGeneratorInterface::ABSOLUTE_URL));
|
||||
$decoratedUrlAbout = new GoogleImageUrlDecorator($urlAbout);
|
||||
$decoratedUrlAbout->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/logo.jpg','webp')));
|
||||
$decoratedUrlAbout->addImage(new GoogleImage($this->cacheManager->resolve($eventItem->getAffiche(),'webp')));
|
||||
$decoratedUrlAbout = new GoogleMultilangUrlDecorator($decoratedUrlAbout);
|
||||
foreach ($langs as $lang) {
|
||||
$decoratedUrlAbout->addLink($urlGenerator->generate('app_event_details',['id'=>$eventItem->getid()], UrlGeneratorInterface::ABSOLUTE_URL), $lang);
|
||||
}
|
||||
$urlContainer->addUrl($decoratedUrlAbout, 'events');
|
||||
}
|
||||
foreach ($this->productsRepository->findAll() as $product) {
|
||||
$slug = $s->slugify($product->getName()."-".$product->getId());
|
||||
$urlAbout = new UrlConcrete($urlGenerator->generate('app_product_show', ['slug'=>$slug], UrlGeneratorInterface::ABSOLUTE_URL));
|
||||
|
||||
62
src/Form/EventType.php
Normal file
62
src/Form/EventType.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Event;
|
||||
use App\Entity\Members;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class EventType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('affiche', FileType::class, [ // Renommé pour plus de clarté
|
||||
'label' => 'Affiche de l\'événement (Max 2Mo)',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
'attr' => [
|
||||
'placeholder' => 'Choisir un fichier...',
|
||||
]
|
||||
])
|
||||
->add('title', TextType::class, [
|
||||
'label' => 'Titre',
|
||||
'required' => true,
|
||||
])
|
||||
->add('location', TextType::class, [
|
||||
'label' => 'Location',
|
||||
'required' => true,
|
||||
])
|
||||
->add('organizer', TextType::class, [
|
||||
'label' => 'Organisateur',
|
||||
'required' => true,
|
||||
])
|
||||
->add('startAt',DateType::class,[
|
||||
'label' => 'Date de début', // Correction du libellé
|
||||
'required' => true,
|
||||
'widget' => 'single_text', // Affiche un champ de date simple au lieu d'une liste déroulante
|
||||
'input' => 'datetime',
|
||||
])
|
||||
->add('endAt',DateType::class,[
|
||||
'label' => 'Date de rejoint', // Correction du libellé
|
||||
'required' => true,
|
||||
'widget' => 'single_text', // Affiche un champ de date simple au lieu d'une liste déroulante
|
||||
'input' => 'datetime',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Event::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
43
src/Repository/EventRepository.php
Normal file
43
src/Repository/EventRepository.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Event;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Event>
|
||||
*/
|
||||
class EventRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Event::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Event[] Returns an array of Event objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('e')
|
||||
// ->andWhere('e.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('e.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?Event
|
||||
// {
|
||||
// return $this->createQueryBuilder('e')
|
||||
// ->andWhere('e.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
23
src/Service/Events/AdvertEvent.php
Normal file
23
src/Service/Events/AdvertEvent.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Events;
|
||||
|
||||
use App\Entity\Event;
|
||||
|
||||
class AdvertEvent
|
||||
{
|
||||
private Event $event;
|
||||
|
||||
public function __construct(Event $event)
|
||||
{
|
||||
$this->event = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Event
|
||||
*/
|
||||
public function getEvent(): Event
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
}
|
||||
34
src/Service/Events/EventSubscriber.php
Normal file
34
src/Service/Events/EventSubscriber.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Events;
|
||||
|
||||
use App\Repository\MembersRepository;
|
||||
use App\Repository\SubRepository;
|
||||
use App\Service\Mailer\Event\CreatedAdminEvent;
|
||||
use App\Service\Mailer\Mailer;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
#[AsEventListener(event: AdvertEvent::class, method: 'onAdvertEvent')]
|
||||
class EventSubscriber
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly SubRepository $subRepository,
|
||||
private readonly MembersRepository $membersRepository,
|
||||
private readonly Mailer $mailer
|
||||
) {
|
||||
}
|
||||
|
||||
public function onAdvertEvent(AdvertEvent $advertEvent): void
|
||||
{
|
||||
$event = $advertEvent->getEvent();
|
||||
foreach ($this->membersRepository->findAll() as $member) {
|
||||
$this->mailer->send($member->getEmail(),$member->getPseudo(),"[E-Cosplay] - Nouveaux événement","mails/event/new.twig",[
|
||||
'event' => $event,
|
||||
'url' => $this->urlGenerator->generate('app_event_details',['id'=>$event->getId()],UrlGeneratorInterface::ABSOLUTE_URL),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
104
templates/admin/events.twig
Normal file
104
templates/admin/events.twig
Normal file
@@ -0,0 +1,104 @@
|
||||
{% extends 'admin/base.twig' %}
|
||||
|
||||
{% block title 'Événement(s)' %}
|
||||
{% block page_title 'Liste des Événements' %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<!-- Conteneur principal: utiliser bg-gray-900 pour l'arrière-plan du corps -->
|
||||
|
||||
<div class="bg-gray-900 text-gray-100 min-h-screen p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<!-- Titre en blanc -->
|
||||
<h2 class="text-2xl font-semibold text-gray-100">Gestion des Événements</h2>
|
||||
<!-- Bouton Créer un événement: le style reste contrasté -->
|
||||
<a href="{{ path('admin_events_create') }}"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium
|
||||
rounded-lg shadow-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2
|
||||
focus:ring-offset-2 focus:ring-indigo-500 focus:ring-offset-gray-900 transition duration-150">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
||||
Créer un événement
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Tableau des événements -->
|
||||
<!-- Fond du tableau en gris foncé, ombre conservée -->
|
||||
<div class="bg-gray-800 shadow-xl overflow-hidden sm:rounded-lg">
|
||||
{% if events is empty %}
|
||||
<!-- Texte vide en gris clair -->
|
||||
<div class="p-6 text-center text-gray-400">
|
||||
Aucun événement trouvé. Commencez par en créer un !
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-700">
|
||||
<!-- En-tête du tableau -->
|
||||
<thead class="bg-gray-700">
|
||||
<tr>
|
||||
<!-- Texte de l'en-tête en gris clair/blanc -->
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">
|
||||
Titre
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider hidden sm:table-cell">
|
||||
Lieu
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider hidden md:table-cell">
|
||||
Organisateur
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">
|
||||
Début
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider hidden md:table-cell">
|
||||
Fin
|
||||
</th>
|
||||
<th scope="col" class="relative px-6 py-3">
|
||||
<span class="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- Corps du tableau -->
|
||||
<tbody class="bg-gray-800 divide-y divide-gray-700">
|
||||
{% for event in events %}
|
||||
<!-- Ligne au survol en gris légèrement plus clair -->
|
||||
<tr class="hover:bg-gray-700 transition duration-100">
|
||||
<!-- Titre en blanc -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-100">
|
||||
{{ event.title }}
|
||||
</td>
|
||||
<!-- Texte des cellules en gris clair -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400 hidden sm:table-cell">
|
||||
{{ event.location }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400 hidden md:table-cell">
|
||||
{{ event.organizer }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
||||
{{ event.startAt|date('d/m/Y') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400 hidden md:table-cell">
|
||||
{{ event.endAt|date('d/m/Y') }}
|
||||
</td>
|
||||
<!-- Actions : lien Modifier en indigo, lien Supprimer en rouge -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<a href="{{ path('admin_events_edit', {id: event.id}) }}" class="text-indigo-400 hover:text-indigo-300 mr-3">
|
||||
Modifier
|
||||
</a>
|
||||
<!-- Bouton de suppression -->
|
||||
<form method="POST" action="{{ path('admin_events_delete', {id: event.id}) }}" class="inline-block" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cet événement ? Cette action est irréversible.');">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ event.id) }}">
|
||||
<button type="submit" class="text-red-400 hover:text-red-300 focus:outline-none">
|
||||
Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
171
templates/admin/events/add.twig
Normal file
171
templates/admin/events/add.twig
Normal file
@@ -0,0 +1,171 @@
|
||||
{% extends 'admin/base.twig' %}
|
||||
|
||||
{% block title %}Ajouter/Éditer un événement{% endblock %}
|
||||
{% block page_title %}
|
||||
{{ form.vars.value.id ? 'Éditer l\' événement: ' ~ form.vars.value.title : 'Créer un nouveau événement' }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block body %}
|
||||
{% form_theme form 'form_admin.twig' %}
|
||||
|
||||
<!-- Conteneur principal en mode sombre -->
|
||||
|
||||
<div class="bg-gray-900 min-h-screen p-6">
|
||||
<div class="max-w-4xl mx-auto bg-gray-800 p-8 rounded-xl shadow-2xl">
|
||||
|
||||
<h1 class="text-3xl font-bold text-gray-100 mb-6"> {{ form.vars.value.id ? 'Éditer l\' événement: ' ~ form.vars.value.title : 'Créer un nouveau événement' }}</h1>
|
||||
|
||||
{{ form_start(form, {'attr': {'class': 'space-y-6'}}) }}
|
||||
|
||||
{% set event = form.vars.data %}
|
||||
|
||||
<!-- Style générique pour les form_row en dark mode -->
|
||||
{% set input_classes = 'w-full px-4 py-3 border border-gray-600 rounded-lg bg-gray-700 text-gray-100 placeholder-gray-400 focus:ring-indigo-500 focus:border-indigo-500 transition duration-150' %}
|
||||
{% set label_classes = 'text-white block text-sm font-medium text-gray-100 mb-1' %}
|
||||
|
||||
<!-- Champ Titre -->
|
||||
<div class="form-group">
|
||||
{{ form_row(form.title, {
|
||||
'label_attr': {'class': label_classes},
|
||||
'attr': {'class': input_classes}
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
<!-- Champ Lieu -->
|
||||
<div class="form-group">
|
||||
{{ form_row(form.location, {
|
||||
'label_attr': {'class': label_classes},
|
||||
'attr': {'class': input_classes}
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
<!-- Champs Date -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Date de Début -->
|
||||
<div class="form-group">
|
||||
{{ form_row(form.startAt, {
|
||||
'label_attr': {'class': label_classes},
|
||||
'attr': {'class': input_classes}
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
<!-- Date de Fin -->
|
||||
<div class="form-group">
|
||||
{{ form_row(form.endAt, {
|
||||
'label_attr': {'class': label_classes},
|
||||
'attr': {'class': input_classes}
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Champ Organisateur -->
|
||||
<div class="form-group">
|
||||
{{ form_row(form.organizer, {
|
||||
'label_attr': {'class': label_classes},
|
||||
'attr': {'class': input_classes}
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
{# --- DÉBUT : CHAMP D'UPLOAD D'IMAGE STYLISÉ (Dark Mode) --- #}
|
||||
{# --- SECTION IMAGE DE PROFIL / UPLOAD --- #}
|
||||
<h3 class="text-2xl font-bold text-white border-b border-gray-700 pb-3 pt-6 mb-6">
|
||||
Affiche
|
||||
</h3>
|
||||
|
||||
<div class="flex items-center space-x-6">
|
||||
|
||||
{# Zone cliquable pour l'image existante, le placeholder ou la prévisualisation #}
|
||||
<label for="{{ form.affiche.vars.id }}"
|
||||
class="relative cursor-pointer bg-gray-700 rounded-full w-32 h-32
|
||||
flex items-center justify-center border-2 border-dashed border-gray-600
|
||||
hover:border-indigo-500 transition duration-300 overflow-hidden shadow-lg"
|
||||
id="preview-container">
|
||||
|
||||
{% set currentImageUrl = event.eventsFileName ? asset(vich_uploader_asset(event, 'affiche')) : '' %}
|
||||
|
||||
{# Image de prévisualisation ou Image actuelle #}
|
||||
<img id="profile-preview-img"
|
||||
src="{{ currentImageUrl }}"
|
||||
alt="Photo de profil"
|
||||
class="w-full h-full object-cover {{ currentImageUrl ? '' : 'hidden' }}">
|
||||
|
||||
{# Icône par défaut (visible seulement si aucune image actuelle) #}
|
||||
<svg id="default-user-icon"
|
||||
class="h-12 w-12 text-gray-500 {{ currentImageUrl ? 'hidden' : '' }}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
|
||||
|
||||
{# Overlay pour indiquer que c'est cliquable (s'applique à l'ensemble du label) #}
|
||||
<div class="absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center
|
||||
opacity-0 hover:opacity-100 transition duration-300 text-white font-bold text-xs p-2 text-center">
|
||||
Cliquer pour changer
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
{# Champ de fichier réel (caché) #}
|
||||
{{ form_widget(form.affiche, {'attr': {'class': 'sr-only'}}) }}
|
||||
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-gray-400">
|
||||
Sélectionnez une nouvelle affiche. Le fichier actuel sera remplacé. (Max 2Mo).
|
||||
</p>
|
||||
{# Affichage des erreurs spécifiques au champ image #}
|
||||
{{ form_errors(form.affiche) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{# --- FIN : CHAMP D'UPLOAD D'IMAGE STYLISÉ --- #}
|
||||
|
||||
<!-- Bouton de soumission -->
|
||||
<div class="pt-4">
|
||||
<button type="submit"
|
||||
class="inline-flex justify-center w-full px-4 py-3 border border-transparent text-lg font-medium rounded-lg shadow-sm text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:ring-offset-gray-800 transition duration-150">
|
||||
Enregistrer l'événement
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Récupère l'ID réel du champ de fichier généré par Symfony
|
||||
const fileInput = document.getElementById('{{ form.affiche.vars.id }}');
|
||||
const previewImg = document.getElementById('profile-preview-img');
|
||||
const defaultIcon = document.getElementById('default-user-icon');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', function() {
|
||||
// Vérifie si un fichier a été sélectionné
|
||||
if (this.files && this.files[0]) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
// 1. Met à jour l'attribut src de l'image avec la data URL du fichier
|
||||
previewImg.src = e.target.result;
|
||||
|
||||
// 2. Assure que l'image est visible
|
||||
previewImg.classList.remove('hidden');
|
||||
|
||||
// 3. Cache l'icône par défaut si elle est là
|
||||
if (defaultIcon) {
|
||||
defaultIcon.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Lit le contenu du fichier
|
||||
reader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -7,23 +7,23 @@
|
||||
{% block breadcrumb_schema %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "{{ 'breadcrumb.home'|trans }}",
|
||||
"item": "{{ app.request.schemeAndHttpHost }}"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "{{ 'breadcrumb.events'|trans }}",
|
||||
"item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "{{ 'breadcrumb.home'|trans }}",
|
||||
"item": "{{ app.request.schemeAndHttpHost }}"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "{{ 'breadcrumb.events'|trans }}",
|
||||
"item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -38,53 +38,73 @@
|
||||
{% if events is defined and events is not empty %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||
{% for event in events %}
|
||||
<div class="bg-white rounded-xl shadow-lg border border-gray-100 p-6 flex flex-col justify-between transition duration-300 hover:shadow-xl hover:border-indigo-200">
|
||||
<div>
|
||||
{# Event Title #}
|
||||
<h2 class="text-2xl font-bold text-indigo-700 mb-4">{{ event.title }}</h2>
|
||||
<div class="bg-white rounded-xl shadow-lg border border-gray-100 flex flex-col justify-between transition duration-300 hover:shadow-xl hover:border-indigo-200 overflow-hidden">
|
||||
|
||||
<div class="space-y-3 text-gray-600">
|
||||
{# --- AJOUT DE L'AFFICHE (IMAGE) --- #}
|
||||
{% set imageUrl = event.eventsFileName ? vich_uploader_asset(event, 'affiche') : null %}
|
||||
|
||||
{# Date Block (Start Date / End Date) #}
|
||||
<p class="flex items-start text-sm">
|
||||
<svg class="w-4 h-4 mt-1 mr-2 flex-shrink-0 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
||||
<div>
|
||||
<span class="font-medium text-gray-800">{{ 'events.list.date_label'|trans|default('Date') }}:</span>
|
||||
<div class="text-xs mt-0.5">
|
||||
{{ event.start_date|date('d/m/Y H:i') }}
|
||||
{% if event.start_date|date('Ymd') != event.end_date|date('Ymd') %}
|
||||
- {{ event.end_date|date('d/m/Y H:i') }}
|
||||
{% else %}
|
||||
- {{ event.end_date|date('H:i') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
{# Location Block #}
|
||||
<p class="flex items-start text-sm">
|
||||
<svg class="w-4 h-4 mt-0.5 mr-2 flex-shrink-0 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
<div>
|
||||
<span class="font-medium text-gray-800">{{ 'events.list.location_label'|trans|default('Location') }}:</span>
|
||||
<div class="text-xs mt-0.5">{{ event.location }}</div>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
{# Organizer Block #}
|
||||
<p class="flex items-start text-sm">
|
||||
<svg class="w-4 h-4 mt-0.5 mr-2 flex-shrink-0 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
|
||||
<div>
|
||||
<span class="font-medium text-gray-800">{{ 'events.list.organizer_label'|trans|default('Organizer') }}:</span>
|
||||
<div class="text-xs mt-0.5">{{ event.organizer }}</div>
|
||||
</div>
|
||||
</p>
|
||||
{% if imageUrl %}
|
||||
<div class="h-48 bg-gray-200 overflow-hidden">
|
||||
<img src="{{ asset(imageUrl) | imagine_filter('webp') }}"
|
||||
alt="Affiche de l'événement {{ event.title }}"
|
||||
class="w-full h-full object-cover">
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{# Placeholder si aucune image n'est disponible #}
|
||||
<div class="h-48 flex items-center justify-center bg-gray-100 text-gray-400">
|
||||
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 18m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# --- FIN AJOUT DE L'AFFICHE --- #}
|
||||
|
||||
{# Details Link (assuming an 'id' or slug is available) #}
|
||||
<a href="{{ url('app_event_details', {id: event.id|default(loop.index)}) }}" class="mt-6 text-center w-full py-2 bg-indigo-50 hover:bg-indigo-100 text-indigo-600 font-semibold rounded-md text-sm transition duration-300 border border-indigo-100">
|
||||
{{ 'events.list.details_button'|trans|default('View Details') }}
|
||||
</a>
|
||||
<div class="p-6 flex flex-col justify-between flex-grow">
|
||||
<div>
|
||||
{# Event Title #}
|
||||
<h2 class="text-2xl font-bold text-indigo-700 mb-4">{{ event.title }}</h2>
|
||||
|
||||
<div class="space-y-3 text-gray-600">
|
||||
|
||||
{# Date Block (Start Date / End Date) #}
|
||||
<p class="flex items-start text-sm">
|
||||
<svg class="w-4 h-4 mt-1 mr-2 flex-shrink-0 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
||||
<div>
|
||||
<span class="font-medium text-gray-800">{{ 'events.list.date_label'|trans|default('Date') }}:</span>
|
||||
<div class="text-xs mt-0.5">
|
||||
{{ event.startAt|date('d/m/Y H:i') }}
|
||||
{% if event.startAt|date('Ymd') != event.endAt|date('Ymd') %}
|
||||
- {{ event.endAt|date('d/m/Y H:i') }}
|
||||
{% else %}
|
||||
- {{ event.endAt|date('H:i') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
{# Location Block #}
|
||||
<p class="flex items-start text-sm">
|
||||
<svg class="w-4 h-4 mt-0.5 mr-2 flex-shrink-0 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
<div>
|
||||
<span class="font-medium text-gray-800">{{ 'events.list.location_label'|trans|default('Location') }}:</span>
|
||||
<div class="text-xs mt-0.5">{{ event.location }}</div>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
{# Organizer Block #}
|
||||
<p class="flex items-start text-sm">
|
||||
<svg class="w-4 h-4 mt-0.5 mr-2 flex-shrink-0 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
|
||||
<div>
|
||||
<span class="font-medium text-gray-800">{{ 'events.list.organizer_label'|trans|default('Organizer') }}:</span>
|
||||
<div class="text-xs mt-0.5">{{ event.organizer }}</div>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Details Link (assuming an 'id' or slug is available) #}
|
||||
<a href="{{ url('app_event_details', {id: event.id|default(loop.index)}) }}" class="mt-6 text-center w-full py-2 bg-indigo-50 hover:bg-indigo-100 text-indigo-600 font-semibold rounded-md text-sm transition duration-300 border border-indigo-100">
|
||||
{{ 'events.list.details_button'|trans|default('View Details') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -92,10 +112,10 @@
|
||||
{# Fallback if no events are found (repurposing the original styling) #}
|
||||
<div class="max-w-3xl mx-auto text-center py-16 md:py-24 bg-white rounded-xl shadow-lg border border-gray-100">
|
||||
|
||||
<span class="text-6xl text-gray-400 mb-4 inline-block">
|
||||
{# Icon: Calendar with an X #}
|
||||
<svg class="lucide lucide-calendar-x" xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/><path d="m14.5 12.5-5 5"/><path d="m9.5 12.5 5 5"/></svg>
|
||||
</span>
|
||||
<span class="text-6xl text-gray-400 mb-4 inline-block">
|
||||
{# Icon: Calendar with an X #}
|
||||
<svg class="lucide lucide-calendar-x" xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/><path d="m14.5 12.5-5 5"/><path d="m9.5 12.5 5 5"/></svg>
|
||||
</span>
|
||||
|
||||
<h2 class="text-4xl font-extrabold text-gray-800 mt-4 mb-3">
|
||||
{{ 'events.no_events_title'|trans|default('No Upcoming Events') }}
|
||||
@@ -113,4 +133,6 @@
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
116
templates/event_view.twig
Normal file
116
templates/event_view.twig
Normal file
@@ -0,0 +1,116 @@
|
||||
{% extends 'base.twig' %}
|
||||
|
||||
{% block title %}{{ event.title }}{% endblock %}
|
||||
{% block meta_description %}{{ event.title }}{% endblock %}
|
||||
|
||||
{% block canonical_url %}<link rel="canonical" href="{{ url('app_event_details',{id:event.id}) }}" />{% endblock %}
|
||||
{% block breadcrumb_schema %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "{{ 'breadcrumb.home'|trans }}",
|
||||
"item": "{{ app.request.schemeAndHttpHost }}"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "{{ 'breadcrumb.events'|trans }}",
|
||||
"item": "{{ url('app_events') }}"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 3,
|
||||
"name": "{{ event.title }}",
|
||||
"item": "{{ app.request.schemeAndHttpHost }}{{ path('app_event_details',{id:event.id}) }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mx-auto p-4 md:p-8 pt-12">
|
||||
<div class="max-w-4xl mx-auto bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||
|
||||
{% set imageUrl = event.eventsFileName ? vich_uploader_asset(event, 'affiche') : null %}
|
||||
|
||||
{# Image / Affiche de l'événement #}
|
||||
{% if imageUrl %}
|
||||
<div class="h-64 sm:h-96 w-full overflow-hidden">
|
||||
<img src="{{ asset(imageUrl) }}"
|
||||
alt="Affiche de l'événement {{ event.title }}"
|
||||
class="w-full h-full object-cover">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="p-6 sm:p-10">
|
||||
|
||||
{# Titre principal #}
|
||||
<h1 class="text-4xl font-extrabold text-gray-900 mb-6 border-b pb-4">
|
||||
{{ event.title }}
|
||||
</h1>
|
||||
|
||||
{# Détails clés (Date, Lieu, Organisateur) #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8 border-b pb-6">
|
||||
|
||||
{# Date Block #}
|
||||
<div class="flex items-start">
|
||||
<svg class="w-6 h-6 mr-3 flex-shrink-0 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-indigo-600">{{ 'events.details.date'|trans|default('Date') }}</p>
|
||||
<p class="text-lg font-medium text-gray-800">
|
||||
{{ event.startAt|date('d/m/Y') }}
|
||||
{% if event.startAt|date('Ymd') != event.endAt|date('Ymd') %}
|
||||
- {{ event.endAt|date('d/m/Y') }}
|
||||
{% else %}
|
||||
- {{ event.endAt|date('H:i') }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Location Block #}
|
||||
<div class="flex items-start">
|
||||
<svg class="w-6 h-6 mr-3 flex-shrink-0 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-indigo-600">{{ 'events.details.location'|trans|default('Lieu') }}</p>
|
||||
<p class="text-lg font-medium text-gray-800">{{ event.location }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Organizer Block #}
|
||||
<div class="flex items-start">
|
||||
<svg class="w-6 h-6 mr-3 flex-shrink-0 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-indigo-600">{{ 'events.details.organizer'|trans|default('Organisateur') }}</p>
|
||||
<p class="text-lg font-medium text-gray-800">{{ event.organizer }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Description #}
|
||||
{% if event.description is defined and event.description is not empty %}
|
||||
<h2 class="text-2xl font-bold text-gray-800 mb-4">
|
||||
{{ 'events.details.description_title'|trans|default('Description') }}
|
||||
</h2>
|
||||
<div class="prose max-w-none text-gray-600 leading-relaxed mb-10">
|
||||
{{ event.description|raw }} {# Assuming description is HTML/Markdown content #}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Bouton de retour #}
|
||||
<a href="{{ url('app_events') }}" class="inline-flex items-center text-indigo-600 hover:text-indigo-800 transition duration-150 font-medium mt-4">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
{{ 'events.details.back_to_list'|trans|default('Retour à la liste des événements') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
75
templates/mails/event/new.twig
Normal file
75
templates/mails/event/new.twig
Normal file
@@ -0,0 +1,75 @@
|
||||
{% extends 'mails/base.twig' %}
|
||||
|
||||
{% block subject %}
|
||||
Nouveaux événement : {{ datas.event.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<mj-section background-color="#ffffff" padding-top="30px" padding-bottom="20px">
|
||||
<mj-column>
|
||||
<mj-text font-size="20px" color="#1F2937" font-weight="bold" line-height="28px">
|
||||
Découvrez notre nouvel événement !
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" color="#4B5563" padding-top="10px">
|
||||
Nous sommes ravis de vous annoncer la création d'un nouvel événement. Voici les détails :
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
{# Détails de l'événement #}
|
||||
<mj-section background-color="#F3F4F6" padding="20px">
|
||||
<mj-column>
|
||||
|
||||
<mj-text font-size="14px" color="#6B7280" padding-bottom="5px">
|
||||
Titre de l'événement :
|
||||
</mj-text>
|
||||
<mj-text font-size="18px" color="#1F2937" font-weight="600" padding-bottom="15px">
|
||||
{{ datas.event.title }}
|
||||
</mj-text>
|
||||
|
||||
<mj-divider border-color="#D1D5DB" border-width="1px" padding="0 0 15px 0" />
|
||||
|
||||
{# Organisateur #}
|
||||
<mj-text font-size="14px" color="#6B7280" padding-bottom="5px">
|
||||
Organisateur :
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" color="#1F2937" padding-bottom="15px">
|
||||
{{ datas.event.organizer }}
|
||||
</mj-text>
|
||||
|
||||
{# Lieu #}
|
||||
<mj-text font-size="14px" color="#6B7280" padding-bottom="5px">
|
||||
Lieu :
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" color="#1F2937" padding-bottom="15px">
|
||||
{{ datas.event.location }}
|
||||
</mj-text>
|
||||
|
||||
{# Dates #}
|
||||
<mj-table padding-bottom="15px">
|
||||
<tr>
|
||||
<td style="width: 50%; padding-right: 10px;">
|
||||
<span style="font-size: 14px; color: #6B7280;">Début :</span><br />
|
||||
<span style="font-size: 16px; color: #1F2937;">{{ datas.event.startAt|date('d/m/Y') }}</span>
|
||||
</td>
|
||||
<td style="width: 50%; padding-left: 10px;">
|
||||
<span style="font-size: 14px; color: #6B7280;">Fin :</span><br />
|
||||
<span style="font-size: 16px; color: #1F2937;">{{ datas.event.endAt|date('d/m/Y') }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</mj-table>
|
||||
|
||||
<mj-divider border-color="#D1D5DB" border-width="1px" padding="10px 0 20px 0" />
|
||||
|
||||
{# Appel à l'action (CTA) vers l'URL publique #}
|
||||
<mj-button background-color="#4F46E5" color="#ffffff" border-radius="8px"
|
||||
href="{{ datas.url }}"
|
||||
padding="10px 25px" font-size="16px">
|
||||
Voir les détails de l'événement
|
||||
</mj-button>
|
||||
|
||||
</mj-column>
|
||||
|
||||
|
||||
</mj-section>
|
||||
{% endblock %}
|
||||
17
templates/txt-mails/event/new.twig
Normal file
17
templates/txt-mails/event/new.twig
Normal file
@@ -0,0 +1,17 @@
|
||||
Nouveaux événement : {{ datas.event.title }}
|
||||
|
||||
Découvrez notre nouvel événement !
|
||||
|
||||
Nous sommes ravis de vous annoncer la création d'un nouvel événement. Voici les détails :
|
||||
|
||||
Titre de l'événement : {{ datas.event.title }}
|
||||
Organisateur : {{ datas.event.organizer }}
|
||||
Lieu : {{ datas.event.location }}
|
||||
Début : {{ datas.event.startAt|date('d/m/Y') }}
|
||||
Fin : {{ datas.event.endAt|date('d/m/Y') }}
|
||||
|
||||
Voir les détails de l'événement :
|
||||
{{ datas.url }}
|
||||
|
||||
À très vite,
|
||||
L'équipe [Nom de votre organisation]
|
||||
@@ -670,3 +670,12 @@ adh_page_validate:
|
||||
footer_action_title: "我们的行动"
|
||||
footer_realise: '导演是'
|
||||
Documents: '文件'
|
||||
list_main_title: 即将举行的活动
|
||||
events.list.date_label: 日期
|
||||
events.list.location_label: 地点
|
||||
events.list.organizer_label: 主办方
|
||||
events.list.details_button: 查看详情
|
||||
events.details.date: 日期
|
||||
events.details.location: 地点
|
||||
events.details.organizer: 主办方
|
||||
events.details.back_to_list: 返回活动列表
|
||||
|
||||
@@ -738,3 +738,14 @@ adh_page_validate:
|
||||
footer_action_title: "Our actions"
|
||||
footer_realise: 'Directed by'
|
||||
Documents: 'Documents'
|
||||
|
||||
list_main_title: Upcoming Events
|
||||
events.list.date_label: Date
|
||||
events.list.location_label: Location
|
||||
events.list.organizer_label: Organizer
|
||||
events.list.details_button: View Details
|
||||
|
||||
events.details.date: Date
|
||||
events.details.location: Location
|
||||
events.details.organizer: Organizer
|
||||
events.details.back_to_list: Back to Events List
|
||||
|
||||
@@ -680,3 +680,16 @@ adh_page_validate:
|
||||
thanks: "Merci pour votre participation !"
|
||||
footer_realise: 'Réalisé par'
|
||||
Documents: 'Documents'
|
||||
|
||||
|
||||
list_main_title: Événements à Venir
|
||||
events.list.date_label: Date
|
||||
events.list.location_label: Lieu
|
||||
events.list.organizer_label: Organisateur
|
||||
events.list.details_button: Voir les détails
|
||||
|
||||
|
||||
events.details.date: Date
|
||||
events.details.location: Lieu
|
||||
events.details.organizer: Organisateur
|
||||
events.details.back_to_list: Retour à la liste des événements
|
||||
|
||||
Reference in New Issue
Block a user