Add invitation landing page with E-Ticket showcase, accept/refuse buttons
- /invitation/{token} GET: landing page with platform presentation
- Sets status to 'opened' on first view
- Neo-brutalist design: offer banner, features grid, message block
- Accept/refuse via POST forms (not GET links)
- Shows current status if already responded
- Email links to landing page instead of direct accept/refuse
- Admin uses viewUrl instead of acceptUrl/refuseUrl
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -475,20 +475,13 @@ class AdminController extends AbstractController
|
||||
$em->persist($invitation);
|
||||
$em->flush();
|
||||
|
||||
$acceptUrl = $this->generateUrl('app_invitation_respond', [
|
||||
$viewUrl = $this->generateUrl('app_invitation_view', [
|
||||
'token' => $invitation->getToken(),
|
||||
'action' => 'accept',
|
||||
], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
$refuseUrl = $this->generateUrl('app_invitation_respond', [
|
||||
'token' => $invitation->getToken(),
|
||||
'action' => 'refuse',
|
||||
], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
$html = $this->renderView('email/organizer_invitation.html.twig', [
|
||||
'invitation' => $invitation,
|
||||
'acceptUrl' => $acceptUrl,
|
||||
'refuseUrl' => $refuseUrl,
|
||||
'viewUrl' => $viewUrl,
|
||||
]);
|
||||
|
||||
$mailerService->sendEmail(
|
||||
@@ -534,20 +527,13 @@ class AdminController extends AbstractController
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$acceptUrl = $this->generateUrl('app_invitation_respond', [
|
||||
$viewUrl = $this->generateUrl('app_invitation_view', [
|
||||
'token' => $invitation->getToken(),
|
||||
'action' => 'accept',
|
||||
], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
$refuseUrl = $this->generateUrl('app_invitation_respond', [
|
||||
'token' => $invitation->getToken(),
|
||||
'action' => 'refuse',
|
||||
], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
$html = $this->renderView('email/organizer_invitation.html.twig', [
|
||||
'invitation' => $invitation,
|
||||
'acceptUrl' => $acceptUrl,
|
||||
'refuseUrl' => $refuseUrl,
|
||||
'viewUrl' => $viewUrl,
|
||||
]);
|
||||
|
||||
$mailerService->sendEmail(
|
||||
|
||||
@@ -238,11 +238,29 @@ class HomeController extends AbstractController
|
||||
return $this->render('home/offline.html.twig');
|
||||
}
|
||||
|
||||
#[Route('/invitation/{token}/{action}', name: 'app_invitation_respond', requirements: ['action' => 'accept|refuse'], methods: ['GET'])]
|
||||
#[Route('/invitation/{token}', name: 'app_invitation_view', methods: ['GET'])]
|
||||
public function viewInvitation(string $token, EntityManagerInterface $em): Response
|
||||
{
|
||||
$invitation = $em->getRepository(OrganizerInvitation::class)->findOneBy(['token' => $token]);
|
||||
if (!$invitation) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
if (OrganizerInvitation::STATUS_SENT === $invitation->getStatus()) {
|
||||
$invitation->setStatus(OrganizerInvitation::STATUS_OPENED);
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
return $this->render('home/invitation_landing.html.twig', [
|
||||
'invitation' => $invitation,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/invitation/{token}/{action}', name: 'app_invitation_respond', requirements: ['action' => 'accept|refuse'], methods: ['POST'])]
|
||||
public function respondInvitation(string $token, string $action, EntityManagerInterface $em): Response
|
||||
{
|
||||
$invitation = $em->getRepository(OrganizerInvitation::class)->findOneBy(['token' => $token]);
|
||||
if (!$invitation || OrganizerInvitation::STATUS_SENT !== $invitation->getStatus()) {
|
||||
if (!$invitation || !\in_array($invitation->getStatus(), [OrganizerInvitation::STATUS_SENT, OrganizerInvitation::STATUS_OPENED], true)) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
{% endif %}
|
||||
|
||||
<p style="text-align: center; margin: 28px 0;">
|
||||
<a href="{{ acceptUrl }}" class="btn">Voir l'invitation</a>
|
||||
<a href="{{ viewUrl }}" class="btn">Voir l'invitation</a>
|
||||
</p>
|
||||
|
||||
<div style="padding: 20px; background: #111827; color: #fff; margin: 24px 0;">
|
||||
@@ -58,7 +58,7 @@
|
||||
</div>
|
||||
|
||||
<p style="text-align: center; font-size: 13px; color: #6b7280; margin-top: 16px;">
|
||||
Pas interesse ? <a href="{{ refuseUrl }}" style="color: #6b7280; text-decoration: underline;">Refuser l'invitation</a>
|
||||
Pas interesse ? <a href="{{ viewUrl }}" style="color: #6b7280; text-decoration: underline;">Repondre a l'invitation</a>
|
||||
</p>
|
||||
|
||||
<p style="font-size: 11px; color: #9ca3af; margin-top: 24px;">Cette invitation a ete envoyee a {{ invitation.email }} le {{ invitation.createdAt|date('d/m/Y') }}.</p>
|
||||
|
||||
112
templates/home/invitation_landing.html.twig
Normal file
112
templates/home/invitation_landing.html.twig
Normal file
@@ -0,0 +1,112 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Invitation - {{ invitation.companyName }} - E-Ticket{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="bg-[#fbfbfb] overflow-x-hidden italic font-sans">
|
||||
|
||||
<section class="relative bg-white border-b-8 border-gray-900 px-4 pt-20 pb-16">
|
||||
<div class="absolute inset-0 opacity-[0.03] pointer-events-none select-none overflow-hidden">
|
||||
<span class="text-[8rem] md:text-[20rem] font-black uppercase leading-none block -rotate-12 translate-y-10">INVITATION</span>
|
||||
</div>
|
||||
|
||||
<div class="max-w-3xl mx-auto relative z-10 text-center">
|
||||
<div class="inline-block px-4 py-1 border-3 border-gray-900 bg-[#fabf04] font-black uppercase text-xs tracking-widest mb-6 shadow-[4px_4px_0px_rgba(0,0,0,1)]">Invitation organisateur</div>
|
||||
<h1 class="text-4xl md:text-6xl font-black uppercase tracking-tighter leading-[0.85] mb-4">{{ invitation.companyName }}</h1>
|
||||
<p class="text-lg font-bold text-gray-600">Bonjour {{ invitation.firstName }}, vous etes invite(e) a rejoindre <strong>E-Ticket</strong>.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if invitation.offer or invitation.commissionRate is not null %}
|
||||
<section class="bg-gray-900 text-white py-8 px-4">
|
||||
<div class="max-w-3xl mx-auto text-center">
|
||||
<p class="text-xs font-black uppercase tracking-widest text-[#fabf04] mb-2">Votre offre</p>
|
||||
<p class="text-2xl font-black uppercase tracking-tighter">
|
||||
{% if invitation.offer == 'free' %}Gratuit{% elseif invitation.offer == 'basic' %}Basic{% elseif invitation.offer == 'custom' %}Sur-mesure{% else %}{{ invitation.offer }}{% endif %}
|
||||
</p>
|
||||
{% if invitation.commissionRate is not null %}
|
||||
<p class="text-sm font-bold text-gray-400 mt-1">Taux de commission E-Ticket : {{ invitation.commissionRate }}% <span class="text-gray-500">(hors frais Stripe)</span></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if invitation.message %}
|
||||
<section class="py-8 px-4">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="border-4 border-gray-900 bg-white p-6 shadow-[6px_6px_0px_rgba(0,0,0,1)]">
|
||||
<p class="text-xs font-black uppercase tracking-widest text-gray-400 mb-3">Message de l'equipe</p>
|
||||
<p class="text-lg font-bold text-gray-700 leading-relaxed">{{ invitation.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="py-12 px-4">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<h2 class="text-3xl font-black uppercase tracking-tighter text-center mb-8">Decouvrir E-Ticket by E-Cosplay</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)]">
|
||||
<div class="text-3xl mb-3">🎫</div>
|
||||
<h3 class="font-black uppercase text-sm tracking-widest mb-2">Evenements</h3>
|
||||
<p class="text-sm font-bold text-gray-600">Creez et gerez vos evenements en quelques clics. Billetterie, brocantes, votes.</p>
|
||||
</div>
|
||||
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)]">
|
||||
<div class="text-3xl mb-3">💳</div>
|
||||
<h3 class="font-black uppercase text-sm tracking-widest mb-2">Paiement securise</h3>
|
||||
<p class="text-sm font-bold text-gray-600">Paiement en ligne via Stripe avec encaissement direct sur votre compte.</p>
|
||||
</div>
|
||||
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)]">
|
||||
<div class="text-3xl mb-3">📈</div>
|
||||
<h3 class="font-black uppercase text-sm tracking-widest mb-2">Statistiques</h3>
|
||||
<p class="text-sm font-bold text-gray-600">Suivez vos ventes en temps reel, commandes, billets vendus et chiffre d'affaires.</p>
|
||||
</div>
|
||||
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)]">
|
||||
<div class="text-3xl mb-3">🎟</div>
|
||||
<h3 class="font-black uppercase text-sm tracking-widest mb-2">Billets PDF</h3>
|
||||
<p class="text-sm font-bold text-gray-600">Generez des billets PDF personnalises avec QR code, envoyes automatiquement par email.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if invitation.status in ['sent', 'opened'] %}
|
||||
<section class="py-12 px-4 border-t-8 border-gray-900 bg-white">
|
||||
<div class="max-w-xl mx-auto text-center">
|
||||
<h2 class="text-2xl font-black uppercase tracking-tighter mb-6">Votre reponse</h2>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<form method="post" action="{{ path('app_invitation_respond', {token: invitation.token, action: 'accept'}) }}">
|
||||
<button type="submit" class="w-full sm:w-auto px-8 py-4 border-4 border-gray-900 bg-[#fabf04] font-black uppercase text-sm tracking-widest shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:shadow-[8px_8px_0px_rgba(0,0,0,1)] hover:translate-y-[-2px] transition-all cursor-pointer">
|
||||
Accepter l'invitation
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="{{ path('app_invitation_respond', {token: invitation.token, action: 'refuse'}) }}">
|
||||
<button type="submit" class="w-full sm:w-auto px-8 py-4 border-4 border-gray-900 bg-white font-black uppercase text-sm tracking-widest shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:bg-red-600 hover:text-white transition-all cursor-pointer">
|
||||
Refuser
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="py-12 px-4 border-t-8 border-gray-900 bg-white">
|
||||
<div class="max-w-xl mx-auto text-center">
|
||||
{% if invitation.status == 'accepted' %}
|
||||
<div class="text-5xl mb-4 text-green-600">✓</div>
|
||||
<p class="font-black uppercase text-lg tracking-tighter">Invitation acceptee</p>
|
||||
<p class="text-sm font-bold text-gray-500 mt-2">Votre compte sera bientot active.</p>
|
||||
{% elseif invitation.status == 'refused' %}
|
||||
<div class="text-5xl mb-4 text-gray-400">✕</div>
|
||||
<p class="font-black uppercase text-lg tracking-tighter">Invitation refusee</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="py-8 px-4 bg-gray-900 text-white text-center">
|
||||
<p class="text-xs font-bold uppercase tracking-widest opacity-50">E-Ticket by E-Cosplay — Plateforme de billetterie en ligne</p>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user