feat(dons): Ajoute la fonctionnalité de dons avec Stripe et reçus PDF.

Ajoute une page de dons avec formulaire, intégration Stripe, webhooks,
génération de reçus PDF et envoi de mails de confirmation. Ajoute aussi
gestion des erreurs 404/500.
```
This commit is contained in:
Serreau Jovann
2025-11-18 20:48:34 +01:00
parent 61dd210b1e
commit a280eb29a4
24 changed files with 1231 additions and 8 deletions

5
.env
View File

@@ -51,3 +51,8 @@ VITE_LOAD=0
REDIS_DSN="redis://redis:6379" REDIS_DSN="redis://redis:6379"
REAL_MAIL=0 REAL_MAIL=0
PATH_URL=https://esyweb.local PATH_URL=https://esyweb.local
STRIPE_PK=pk_test_51SUA22173W4aeFB1nO6oFfDZ12HOTffDKtCshhZ8rkUg6kUO2ZaQC0tK72rhE79Tr8treeHX9KMcZtvcQZ0X8VSm00Q6GQ365V
STRIPE_SK=sk_test_51SUA22173W4aeFB16EB2LxGI0hNvNJzFshDI98zRImWBIhSfzqOGAz5TlPxSpUWbj3x4COm6kmSsaal9FpQR1A7M0022DvjbbR
STRIPE_WEBHOOKS_SIGN=whsec_0DOZJAwgMwkcHl2RWXI8h8YItj9q7v3A
DEV_URL=https://3ea1cf1b1555.ngrok-free.app

View File

@@ -71,6 +71,9 @@
VAULT_ADDR=http://127.0.0.1:8200 VAULT_ADDR=http://127.0.0.1:8200
VAULT_TOKEN=hvs.QLpUdiptXtSPo5Qf7i2nn2Xz VAULT_TOKEN=hvs.QLpUdiptXtSPo5Qf7i2nn2Xz
APP_DEBUG=true APP_DEBUG=true
STRIPE_PK=pk_live_51SUA1rP4ub49xK2ThoRH8efqGYNi1hrcWMzrqmDtJpMv12cmTzLa8ncJLUKLbOQNZTkm1jgptLfwt4hxEGqkVsHB00AK3ieZNl
STRIPE_SK=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
STRIPE_WEBHOOKS_SIGN=whsec_wNHtgjypqbfP7erAqifCOzZvW8kW9oB7
MAILER_DSN=ses+smtp://AKIAWTT2T22CWBRBBDYN:BBdgb6KxRQ8mNcpWFJsZCJxbSGNdgLhKFiITMErfBlQP@default?region=eu-west-3 MAILER_DSN=ses+smtp://AKIAWTT2T22CWBRBBDYN:BBdgb6KxRQ8mNcpWFJsZCJxbSGNdgLhKFiITMErfBlQP@default?region=eu-west-3
dest: "{{ path }}/.env.local" dest: "{{ path }}/.env.local"
when: ansible_os_family == "Debian" when: ansible_os_family == "Debian"

23
assets/PaymentForm.js Normal file
View File

@@ -0,0 +1,23 @@
export class PaymentForm extends HTMLFormElement{
connectedCallback() {
this.addEventListener('submit',(e)=>{
e.preventDefault();
let inputs ={};
this.querySelectorAll('input').forEach(input=>{
inputs[input.name] = input.value
})
this.querySelectorAll('textarea').forEach(input=>{
inputs[input.name] = input.value
})
fetch("/dons",{
method:"POST",
body:JSON.stringify(inputs)
}).then(rslt=>rslt.json())
.then((reslt)=>{
console.log(reslt)
})
})
}
}

View File

@@ -1,6 +1,7 @@
import './app.scss' import './app.scss'
import * as Turbo from "@hotwired/turbo" import * as Turbo from "@hotwired/turbo"
import {PaymentForm} from './PaymentForm'
/** /**
* Fonction générique pour basculer la visibilité d'un menu déroulant. * Fonction générique pour basculer la visibilité d'un menu déroulant.
* @param {HTMLElement} button - Le bouton qui déclenche l'action. * @param {HTMLElement} button - Le bouton qui déclenche l'action.
@@ -79,6 +80,8 @@ function initializeUI() {
// --- INITIALISATION DES COMPOSANTS APRÈS TURBO/CHARGEMENT --- // --- INITIALISATION DES COMPOSANTS APRÈS TURBO/CHARGEMENT ---
document.addEventListener('DOMContentLoaded', ()=>{ document.addEventListener('DOMContentLoaded', ()=>{
customElements.define('payment-don',PaymentForm,{extends:'form'})
initializeUI() initializeUI()
const env = document.querySelector('meta[name="env"]') const env = document.querySelector('meta[name="env"]')
if(env.getAttribute('content') == "prod") { if(env.getAttribute('content') == "prod") {

View File

@@ -43,6 +43,7 @@
"setasign/fpdi": "^2.6.4", "setasign/fpdi": "^2.6.4",
"spatie/mjml-php": "^1.2.5", "spatie/mjml-php": "^1.2.5",
"stancer/stancer": ">=2.0.1", "stancer/stancer": ">=2.0.1",
"stripe/stripe-php": "^18.2",
"symfony/amazon-mailer": "7.3.*", "symfony/amazon-mailer": "7.3.*",
"symfony/asset": "7.3.*", "symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.3.*", "symfony/asset-mapper": "7.3.*",

61
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "6c414bbc5665617d98a78419d58c159b", "content-hash": "fe5cfb5ef73b767b1d84ebbc20c2c1e4",
"packages": [ "packages": [
{ {
"name": "async-aws/core", "name": "async-aws/core",
@@ -8000,6 +8000,65 @@
}, },
"time": "2024-11-15T17:47:59+00:00" "time": "2024-11-15T17:47:59+00:00"
}, },
{
"name": "stripe/stripe-php",
"version": "v18.2.0",
"source": {
"type": "git",
"url": "https://github.com/stripe/stripe-php.git",
"reference": "0acd7bdac84ad0f940d5da30c417170ce59b4fbc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/0acd7bdac84ad0f940d5da30c417170ce59b4fbc",
"reference": "0acd7bdac84ad0f940d5da30c417170ce59b4fbc",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"php": ">=5.6.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.72.0",
"phpstan/phpstan": "^1.2",
"phpunit/phpunit": "^5.7 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"Stripe\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Stripe and contributors",
"homepage": "https://github.com/stripe/stripe-php/contributors"
}
],
"description": "Stripe PHP Library",
"homepage": "https://stripe.com/",
"keywords": [
"api",
"payment processing",
"stripe"
],
"support": {
"issues": "https://github.com/stripe/stripe-php/issues",
"source": "https://github.com/stripe/stripe-php/tree/v18.2.0"
},
"time": "2025-11-05T22:59:39+00:00"
},
{ {
"name": "symfony/amazon-mailer", "name": "symfony/amazon-mailer",
"version": "v7.3.0", "version": "v7.3.0",

View File

@@ -0,0 +1,32 @@
<?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 Version20251118192837 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 dons (id SERIAL NOT NULL, name VARCHAR(255) DEFAULT NULL, email VARCHAR(255) NOT NULL, message TEXT DEFAULT NULL, amount DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))');
}
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 dons');
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Controller;
use App\Dto\Contact\ContactType;
use App\Dto\Contact\DtoContact;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType;
use App\Service\Mailer\Mailer;
use App\Service\Payments\PaymentClient;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Doctrine\ORM\EntityManagerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Twig\Environment;
class DonsController extends AbstractController
{
#[Route(path: '/dons', name: 'app_dons', options: ['sitemap' => false], methods: ['GET','POST'])]
public function index(Request $request,PaymentClient $paymentClient): Response
{
if($request->query->has('success')) {
$dons = $request->getSession()->get('dons');
$request->getSession()->remove('dons');
return $this->render('dons_success.twig',[
'dons' => $dons,
]);
}
if($request->isMethod('POST')) {
$content = $request->request->all();
$payment = $paymentClient->paymentDon($content);
$content['id_payment'] = $payment->id;
$request->getSession()->set('dons',$content);
return new RedirectResponse($payment->url);
}
return $this->render('dons.twig',[
]);
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Controller;
use App\Dto\Contact\ContactType;
use App\Dto\Contact\DtoContact;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Entity\Dons;
use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType;
use App\Service\Mailer\Mailer;
use App\Service\Payments\PaymentClient;
use App\Service\Pdf\DonReceiptGenerator;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Doctrine\ORM\EntityManagerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Twig\Environment;
class WebhooksController extends AbstractController
{
#[Route(path: '/webhooks', name: 'app_webhooks', options: ['sitemap' => false], methods: ['POST'])]
public function index(DonReceiptGenerator $donReceiptGenerator,Request $request,PaymentClient $paymentClient,Mailer $mailer,EntityManagerInterface $entityManager,): Response
{
$content = $request->getContent();
if($paymentClient->validateWebhooks($content)) {
$content = json_decode($content);
$object = $content->data->object;
$metadata = $object->metadata;
if($object->status == "complete") {
$dons = new Dons();
$dons->setName($metadata->name);
$dons->setEmail($metadata->email);
$dons->setAmount($metadata->amount);
$dons->setMessage($metadata->message);
$entityManager->persist($dons);
$pdfGenerator = new DonReceiptGenerator();
$v= new \DateTime();
$donationData = [
'name' => $metadata->name,
'email' => $metadata->email,
'amount' => $metadata->amount,
'date' => $v->format('Y-m-d'),
'message' => $metadata->message ?? null,
];
$files = [];
$pdfContent = $pdfGenerator->generate(
$donationData,
'recu_don_E-Cosplay.pdf',
'S'
);
$files[] = new DataPart($pdfContent,'recu_don_E-Cosplay.pdf','application/pdf');
$mailer->send($metadata->email,$metadata->name,'[E-Cosplay] - Confirmation de votre don de '.$metadata->amount."","mails/dons.twig",[
'don' => $dons,
],$files);
$mailer->send($metadata->email,$metadata->name,'[E-Cosplay] - Confirmation d\'un don de '.$metadata->amount."","mails/dons_new.twig",[
'don' => $dons,
]);
$entityManager->flush();
}
}
return $this->json([]);
}
}

81
src/Entity/Dons.php Normal file
View File

@@ -0,0 +1,81 @@
<?php
namespace App\Entity;
use App\Repository\DonsRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: DonsRepository::class)]
class Dons
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $name = null;
#[ORM\Column(length: 255)]
private ?string $email = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $message = null;
#[ORM\Column]
private ?float $amount = null;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(?string $name): static
{
$this->name = $name;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = $email;
return $this;
}
public function getMessage(): ?string
{
return $this->message;
}
public function setMessage(?string $message): static
{
$this->message = $message;
return $this;
}
public function getAmount(): ?float
{
return $this->amount;
}
public function setAmount(float $amount): static
{
$this->amount = $amount;
return $this;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\EventSubscriber;
use Presta\SitemapBundle\Event\SitemapPopulateEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Twig\Environment;
#[AsEventListener(event: ExceptionEvent::class, method: 'onException', priority: 10)]
class ErrorListener
{
public function __construct(private readonly Environment $environment)
{
}
public function onException(ExceptionEvent $exceptionEvent) {
$exception = $exceptionEvent->getThrowable();
if($exception instanceof NotFoundHttpException) {
$response = new Response($this->environment->render('error/404.twig',[
'no_index' => true,
]));
$response->setStatusCode(Response::HTTP_NOT_FOUND);
$exceptionEvent->setResponse($response);
$exceptionEvent->stopPropagation();
} else {
$response = new Response($this->environment->render('error/error.twig',[
'no_index' => true,
]));
$response->setStatusCode(Response::HTTP_NOT_FOUND);
$exceptionEvent->setResponse($response);
$exceptionEvent->stopPropagation();
}
}
}

View File

@@ -71,6 +71,15 @@ class SitemapSubscriber
} }
$urlContainer->addUrl($urlEvents, 'default'); $urlContainer->addUrl($urlEvents, 'default');
$urlDons = new UrlConcrete($urlGenerator->generate('app_dons', [], UrlGeneratorInterface::ABSOLUTE_URL));
$urlDons = new GoogleImageUrlDecorator($urlDons);
$urlDons->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/logo.jpg','webp')));
$urlDons = new GoogleMultilangUrlDecorator($urlDons);
foreach ($langs as $lang) {
$urlDons->addLink($urlGenerator->generate('app_dons',['lang'=>$lang], UrlGeneratorInterface::ABSOLUTE_URL), $lang);
}
$urlContainer->addUrl($urlDons, 'default');
$urlAbout = new UrlConcrete($urlGenerator->generate('app_about', [], UrlGeneratorInterface::ABSOLUTE_URL)); $urlAbout = new UrlConcrete($urlGenerator->generate('app_about', [], UrlGeneratorInterface::ABSOLUTE_URL));
$decoratedUrlAbout = new GoogleImageUrlDecorator($urlAbout); $decoratedUrlAbout = new GoogleImageUrlDecorator($urlAbout);
$decoratedUrlAbout->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/logo.jpg','webp'))); $decoratedUrlAbout->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/logo.jpg','webp')));

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Dons;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Dons>
*/
class DonsRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Dons::class);
}
// /**
// * @return Dons[] Returns an array of Dons objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('d')
// ->andWhere('d.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('d.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Dons
// {
// return $this->createQueryBuilder('d')
// ->andWhere('d.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Service\Payments;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class PaymentClient
{
private string $url;
public function __construct(private readonly RequestStack $requestStack, private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em)
{
if($_ENV['APP_ENV'] == "prod")
$this->url = $this->requestStack->getCurrentRequest()->getSchemeAndHttpHost();
else
$this->url = $_ENV['DEV_URL'];
}
public function paymentDon(array $content)
{
$stripe = new \Stripe\StripeClient($_ENV['STRIPE_SK']);
$checkoutSession = $stripe->checkout->sessions->create([
'customer_email' => $content['email'],
'line_items' => [[
'price_data' => [
'currency' => 'eur',
'product_data' => [
'name' => "Dons ".$content['amount']."",
'description' => "Dons ".$content['amount']."",
],
'unit_amount' => $content['amount']*100,
],
'quantity' => 1,
]],
'metadata' => $content,
'mode' => 'payment',
'success_url' => $this->url.$this->urlGenerator->generate('app_dons',['success' => true]),
]);
return $checkoutSession;
}
public function validateWebhooks($payload)
{
try {
$event = \Stripe\Event::constructFrom(
json_decode($payload, true)
);
return true;
} catch(\UnexpectedValueException $e) {
return false;
}
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace App\Service\Pdf;
use Fpdf\Fpdf;
class DonReceiptGenerator extends Fpdf
{
private array $donationData;
/**
* Initialise le générateur de reçu avec les données du don.
* @param array $data Les données du don (amount, name, date, email, message, etc.).
*/
public function setDonationData(array $data): void
{
$this->donationData = $data;
}
// --- Méthodes FPDF obligatoires ou recommandées ---
/**
* Entête du document (Logo, Nom de l'association).
*/
public function Header(): void
{
// Logo de l'association E-Cosplay
// IMPORTANT : Ajustez le chemin de l'image pour qu'il soit absolu ou relatif à l'exécution du script
// Exemple pour Symfony : $_SERVER['DOCUMENT_ROOT'] . '/assets/images/logo.jpg'
$logoPath = $_SERVER['DOCUMENT_ROOT'] . '/assets/images/logo.jpg';
// Vérifie si le fichier existe avant de tenter de l'insérer
if (file_exists($logoPath)) {
$this->Image($logoPath, 10, 8, 30); // Position X=10, Y=8, Largeur=30mm (Hauteur auto)
} else {
// Optionnel : Afficher un texte si le logo n'est pas trouvé
$this->SetFont('Arial', 'B', 10);
$this->Cell(0, 10, utf8_decode('E-Cosplay Logo'), 0, 0, 'L');
$this->Ln(5); // Saut de ligne après le texte du logo
}
// Police de caractères (ex: Arial gras 15)
$this->SetFont('Arial', 'B', 15);
// Décalage à droite (après le logo si utilisé, ou ajuster)
// Si logo utilisé, le décalage doit être plus grand
$this->Cell(80); // Ajustez cette valeur si le logo est présent et plus grand/petit
// Titre - Utilisation de utf8_decode()
$this->Cell(30, 10, utf8_decode('Reçu de Don'), 0, 1, 'C');
// Informations de l'association
$this->SetFont('Arial', '', 10);
// Ligne 1: Nom de l'association
$this->Cell(0, 5, utf8_decode('E-Cosplay'), 0, 1, 'C');
// Ligne 2: Adresse
$this->Cell(0, 5, utf8_decode('42 RUE DE SAINT-QUENTIN 02800 BEAUTOR'), 0, 1, 'C');
// Ligne 3: SIREN et NAF
$this->Cell(0, 5, utf8_decode('SIREN: 943121517 | Code NAF: 93.29Z'), 0, 1, 'C');
// Saut de ligne
$this->Ln(15);
}
/**
* Pied de page du document.
*/
public function Footer(): void
{
// Positionnement à 1.5 cm du bas
$this->SetY(-15);
// Police Arial italique 8
$this->SetFont('Arial', 'I', 8);
// Numéro de page
$this->Cell(0, 10, utf8_decode('Page ') . $this->PageNo() . '/{nb}', 0, 0, 'C');
}
// --- Méthode de génération spécifique au reçu ---
/**
* Génère le corps principal du reçu de don.
*/
public function generateReceiptBody(): void
{
if (empty($this->donationData)) {
// Utilisation de utf8_decode()
$this->Cell(0, 10, utf8_decode('Erreur: Aucune donnée de don n\'est définie.'), 0, 1);
return;
}
$this->SetFont('Arial', '', 12);
// Affichage des informations du donateur
$this->SetTextColor(0, 0, 0); // Noir
$this->Cell(0, 10, utf8_decode('Donateur:'), 0, 1);
// Récupération et décodage des données du donateur
$name = utf8_decode($this->donationData['name'] ?? 'Non spécifié');
$email = utf8_decode($this->donationData['email'] ?? 'Non spécifié');
$this->SetX(20); // Décalage pour les détails
$this->Cell(0, 7, utf8_decode('Nom/Pseudo: ') . $name, 0, 1);
$this->SetX(20);
$this->Cell(0, 7, utf8_decode('E-mail: ') . $email, 0, 1);
$this->Ln(10);
// Affichage des détails du don
$this->SetFont('Arial', 'B', 14);
$this->SetFillColor(200, 220, 255); // Couleur de fond pour le titre
// Utilisation de utf8_decode()
$this->Cell(0, 10, utf8_decode('Détails de la Contribution'), 0, 1, 'L', true);
$this->SetFont('Arial', '', 12);
$this->Ln(3);
// Montant
$amount = $this->donationData['amount'] ?? 0;
$formattedAmount = number_format($amount, 2, ',', '.') . ' EUR';
$this->SetX(20);
$this->Cell(50, 8, utf8_decode('Montant Reçu:'), 0, 0);
$this->SetFont('Arial', 'B', 12);
$this->Cell(0, 8, $formattedAmount, 0, 1);
$this->SetFont('Arial', '', 12);
// Date
$date = $this->donationData['date'] ?? date('Y-m-d');
$this->SetX(20);
$this->Cell(50, 8, utf8_decode('Date de la Transaction:'), 0, 0);
$this->Cell(0, 8, $date, 0, 1);
// Message (si présent)
if (!empty($this->donationData['message'])) {
$message = utf8_decode($this->donationData['message']);
$this->Ln(5);
$this->Cell(0, 8, utf8_decode('Message du Donateur:'), 0, 1);
$this->SetX(20);
$this->MultiCell(0, 6, $message);
}
$this->Ln(15);
// Mention de non-déductibilité (Crucial) - Utilisation de utf8_decode()
$nonDeductibilityText = 'NOTE IMPORTANTE : Votre don à notre association ne vous permet pas de bénéficier d\'une réduction d\'impôt en vertu des lois fiscales actuelles.';
$this->SetFont('Arial', 'B', 10);
$this->SetTextColor(150, 0, 0); // Rouge foncé
$this->MultiCell(0, 5, utf8_decode($nonDeductibilityText), 0, 'C');
$this->SetTextColor(0, 0, 0); // Revenir au noir
$this->Ln(10);
// Signature (Seule la ligne "Fait à [Ville], le [Date]" est conservée)
$this->SetFont('Arial', 'I', 10);
$this->Cell(0, 5, utf8_decode('Fait à BEAUTOR, le ') . date('d/m/Y'), 0, 1, 'R');
// La ligne "Signature du Responsable" a été retirée.
}
/**
* Méthode publique pour générer et obtenir le PDF.
* @param array $data Les données du don.
* @param string $filename Le nom du fichier de sortie.
* @param string $dest La destination (I: navigateur, D: téléchargement, F: fichier, S: string).
*/
public function generate(array $data, string $filename = 'Reçu_Don.pdf', string $dest = 'I')
{
$this->setDonationData($data);
// Initialisation du PDF
$this->AliasNbPages();
$this->AddPage();
// Génération du contenu
$this->generateReceiptBody();
// Sortie du document
// Le nom de fichier doit aussi être décodé pour l'en-tête HTTP si dest='D'
return $this->Output($dest, utf8_decode($filename));
}
}

View File

@@ -10,14 +10,14 @@
{# OPEN GRAPH / TWITTER CARD / SEO META #} {# OPEN GRAPH / TWITTER CARD / SEO META #}
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" <meta property="og:url"
content="{{ url(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) }}"> content="{{ url(app.request.attributes.get('_route','app_home'), app.request.attributes.get('_route_params',[])) }}">
<meta property="og:title" content="E-Cosplay | {{ block('title') }}"> <meta property="og:title" content="E-Cosplay | {{ block('title') }}">
<meta property="og:description" content="{{ block('meta_description') }}"> <meta property="og:description" content="{{ block('meta_description') }}">
<meta property="og:image" content="{{ asset('assets/images/logo.jpg') | imagine_filter('webp') }}"> <meta property="og:image" content="{{ asset('assets/images/logo.jpg') | imagine_filter('webp') }}">
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" <meta name="twitter:url"
content="{{ url(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) }}"> content="{{ url(app.request.attributes.get('_route','app_home'), app.request.attributes.get('_route_params',[])) }}">
<meta name="twitter:title" content="E-Cosplay | {{ block('title') }}"> <meta name="twitter:title" content="E-Cosplay | {{ block('title') }}">
<meta name="twitter:description" content="{{ block('meta_description') }}"> <meta name="twitter:description" content="{{ block('meta_description') }}">
<meta name="twitter:image" content="{{ asset('assets/images/logo.jpg') | imagine_filter('webp') }}"> <meta name="twitter:image" content="{{ asset('assets/images/logo.jpg') | imagine_filter('webp') }}">
@@ -126,6 +126,7 @@
{ 'name': 'Nos membres'|trans, 'route': 'app_members' }, { 'name': 'Nos membres'|trans, 'route': 'app_members' },
{ 'name': 'Nos événements'|trans, 'route': 'app_events' }, { 'name': 'Nos événements'|trans, 'route': 'app_events' },
{ 'name': 'Boutiques'|trans, 'route': 'app_shop' }, { 'name': 'Boutiques'|trans, 'route': 'app_shop' },
{ 'name': 'Dons'|trans, 'route': 'app_dons' },
{ 'name': 'Contact'|trans, 'route': 'app_contact' } { 'name': 'Contact'|trans, 'route': 'app_contact' }
] %} ] %}
<header class="bg-white shadow-md sticky top-0 z-40"> <header class="bg-white shadow-md sticky top-0 z-40">
@@ -157,8 +158,8 @@
{# SÉLECTEUR DE LANGUE (Desktop) #} {# SÉLECTEUR DE LANGUE (Desktop) #}
<div class="flex items-center space-x-2 border-l border-gray-200 pl-4"> <div class="flex items-center space-x-2 border-l border-gray-200 pl-4">
{% set current_route = app.request.attributes.get('_route') %} {% set current_route = app.request.attributes.get('_route','app_home') %}
{% set current_params = app.request.attributes.get('_route_params') %} {% set current_params = app.request.attributes.get('_route_params',[]) %}
{# Fonction pour générer le chemin avec le paramètre 'lang' (doit être définie dans une extension Twig) #} {# Fonction pour générer le chemin avec le paramètre 'lang' (doit être définie dans une extension Twig) #}
{# En attendant, nous générons l'URL manuellement #} {# En attendant, nous générons l'URL manuellement #}
@@ -167,7 +168,6 @@
{% for lang in ['fr', 'en'] %} {% for lang in ['fr', 'en'] %}
{% set is_active_lang = (app.request.locale == lang) %} {% set is_active_lang = (app.request.locale == lang) %}
{% set lang_params = current_params|merge(current_query)|merge({'lang': lang}) %} {% set lang_params = current_params|merge(current_query)|merge({'lang': lang}) %}
{# Générer l'URL en conservant les paramètres existants + le paramètre 'lang' #} {# Générer l'URL en conservant les paramètres existants + le paramètre 'lang' #}
{% set lang_url = path(current_route, lang_params) %} {% set lang_url = path(current_route, lang_params) %}
@@ -282,8 +282,8 @@
{# SÉLECTEUR DE LANGUE (Mobile - Compact) #} {# SÉLECTEUR DE LANGUE (Mobile - Compact) #}
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
{% set current_route = app.request.attributes.get('_route') %} {% set current_route = app.request.attributes.get('_route','app_home') %}
{% set current_params = app.request.attributes.get('_route_params') %} {% set current_params = app.request.attributes.get('_route_params',[]) %}
{% set current_query = app.request.query.all %} {% set current_query = app.request.query.all %}
{% for lang in ['fr', 'en'] %} {% for lang in ['fr', 'en'] %}

177
templates/dons.twig Normal file
View File

@@ -0,0 +1,177 @@
{% extends 'base.twig' %}
{# --- METADATA & SCHEMA (Inchangé) --- #}
{% block title %}{{'dons.title'|trans}}{% endblock %}
{% block meta_description %}{{'dons.description'|trans}}{% endblock %}
{% block canonical_url %}<link rel="canonical" href="{{ url('app_dons') }}" />{% 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.dons'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}"
}
]
}
</script>
{% endblock %}
{# --- BODY AVEC TAILWIND CSS --- #}
{% block body %}
<main class="py-12 md:py-20 bg-gray-50">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{# Section Header #}
<header class="text-center mb-12">
<h1 class="text-4xl sm:text-5xl font-extrabold text-indigo-700 tracking-tight">
{{ 'dons.page_title'|trans }}
</h1>
<p class="mt-4 text-xl text-gray-600 max-w-2xl mx-auto">
{{ 'dons.introduction'|trans|raw }}
</p>
</header>
{# Impact Section Card #}
<article class="bg-white shadow-xl rounded-lg p-6 md:p-8 mb-10 border-t-4 border-indigo-500">
<h2 class="text-2xl font-bold text-gray-800 mb-4">{{ 'dons.impact_title'|trans }}</h2>
<p class="text-gray-600 mb-6">{{ 'dons.impact_text'|trans }}</p>
<h3 class="text-xl font-semibold text-indigo-600 mb-4">{{ 'dons.support_for_title'|trans }}</h3>
{# List of supported actions (Grid/Flex for better visual) #}
<ul class="space-y-4">
<li class="flex items-start">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p class="ml-3 text-gray-700">
<span class="font-medium text-gray-800">{{ 'dons.item.events_title'|trans }} :</span> {{ 'dons.item.events'|trans }}
</p>
</li>
<li class="flex items-start">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<p class="ml-3 text-gray-700">
<span class="font-medium text-gray-800">{{ 'dons.item.equipment_title'|trans }} :</span> {{ 'dons.item.equipment'|trans }}
</p>
</li>
<li class="flex items-start">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9h12" />
</svg>
</div>
<p class="ml-3 text-gray-700">
<span class="font-medium text-gray-800">{{ 'dons.item.website_hosting_title'|trans }} :</span> {{ 'dons.item.website_hosting'|trans }}
</p>
</li>
<li class="flex items-start">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v15m0 0l-3-3m3 3l3-3" />
</svg>
</div>
<p class="ml-3 text-gray-700">
<span class="font-medium text-gray-800">{{ 'dons.item.other_needs_title'|trans }} :</span> {{ 'dons.item.other_needs'|trans }}
</p>
</li>
</ul>
</article>
{# Call to Action / Donation Form #}
<div class="p-6 md:p-8 bg-indigo-50 rounded-lg shadow-xl">
<header class="text-center mb-6">
<h2 class="text-3xl font-bold text-gray-800 mb-2">{{ 'dons.make_a_donation_title'|trans }}</h2>
<p class="text-gray-700">{{ 'dons.call_to_action_text'|trans }}</p>
</header>
{# Début du formulaire #}
<form data-turbo="false" action="{{ path('app_dons') }}" method="post" class="space-y-6">
{# Nom / Pseudo #}
<div>
<label for="name" class="block text-sm font-medium text-gray-700">
{{ 'form.name_label'|trans }} <span class="text-red-500">*</span>
</label>
<input type="text" id="name" name="name" required
placeholder="{{ 'form.name_placeholder'|trans }}"
class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
</div>
{# Email #}
<div>
<label for="email" class="block text-sm font-medium text-gray-700">
{{ 'form.email_label'|trans }} <span class="text-red-500">*</span>
</label>
<input type="email" id="email" name="email" required
placeholder="{{ 'form.email_placeholder'|trans }}"
class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
</div>
{# Montant (Ammount) #}
<div>
<label for="amount" class="block text-sm font-medium text-gray-700">
{{ 'form.amount_label'|trans }} <span class="text-red-500">*</span>
</label>
<div class="mt-1 relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span class="text-gray-500 sm:text-sm">€</span>
</div>
<input type="number" id="amount" name="amount" required min="1" step="0.01"
placeholder="10.00"
class="block w-full pl-8 pr-12 py-2 border border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span class="text-gray-500 sm:text-sm">EUR</span>
</div>
</div>
</div>
{# Message (Facultatif) #}
<div>
<label for="message" class="block text-sm font-medium text-gray-700">
{{ 'form.message_label'|trans }} ({{ 'form.optional'|trans }})
</label>
<textarea id="message" name="message" rows="3"
placeholder="{{ 'form.message_placeholder'|trans }}"
class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"></textarea>
</div>
{# Bouton de soumission #}
<div class="text-center pt-4">
<button type="submit"
class="inline-flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-full shadow-lg text-white bg-indigo-600 hover:bg-indigo-700 transition duration-300 ease-in-out transform hover:scale-105 w-full sm:w-auto">
<svg class="w-6 h-6 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
{{ 'form.submit_button_dons'|trans }}
</button>
</div>
</form>
{# Fin du formulaire #}
<p class="mt-6 text-center text-sm text-gray-500">
{{ 'dons.thanks_note'|trans }}
</p>
</div>
</div>
</main>
{% endblock %}

View File

@@ -0,0 +1,81 @@
{% extends 'base.twig' %}
{# --- DÉFINITION DES VARIABLES (Assurez-vous que 'dons' est passé par votre contrôleur) --- #}
{% set donor_email = dons.email is defined ? dons.email : 'non-fourni' %}
{% set donation_amount = dons.amount is defined ? dons.amount : 0 %}
{# --- METADATA & SCHEMA --- #}
{% block title %}{{'thank_you.title'|trans}}{% endblock %}
{% block meta_description %}{{'thank_you.email_sent_info'|trans}}{% endblock %}
{% block canonical_url %}<link rel="canonical" href="{{ url('app_dons') }}" />{% endblock %}
{# Pas de breadcrumb nécessaire pour une page de confirmation de transaction #}
{# --- BODY AVEC TAILWIND CSS --- #}
{% block body %}
<main class="py-12 md:py-20 bg-gray-50 min-h-screen">
<div class="max-w-xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow-2xl rounded-lg p-8 md:p-10 text-center border-t-8 border-green-500 transform hover:shadow-3xl transition duration-500">
{# Icône de Succès #}
<div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mb-6">
<svg class="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<h1 class="text-3xl font-bold text-gray-800 mb-4">
{{ 'thank_you.title'|trans }}
</h1>
<p class="text-xl text-gray-700 mb-6">
{{ 'thank_you.message_main'|trans|raw }}
</p>
{# MONTANT DU DON #}
{% if donation_amount > 0 %}
<div class="bg-green-50 p-4 rounded-lg mb-6 border border-green-200">
<p class="text-lg font-semibold text-green-700">
{{ 'thank_you.amount_received'|trans }}
</p>
<p class="text-4xl font-extrabold text-green-600 mt-1">
{# Utilisation du filtre pour le formatage monétaire #}
{{ donation_amount|format_currency('EUR', locale='fr') }}
</p>
</div>
{% endif %}
<hr class="my-6 border-gray-200">
{# Informations sur l'email de confirmation (avec précision sur le paiement) #}
<div class="p-4 bg-indigo-50 rounded-lg text-left">
<h2 class="text-lg font-semibold text-indigo-700 mb-2">
{{ 'thank_you.email_sent_title'|trans }}
</h2>
{# Cette clé de traduction contient le message de clarification #}
<p class="text-gray-600 text-sm">
{{ 'thank_you.email_sent_info'|trans }}
</p>
{# Afficher l'email du donateur #}
<p class="mt-2 text-indigo-600 font-medium text-sm truncate">
{{ 'thank_you.email_recipient'|trans }} :
<span class="font-bold">{{ donor_email }}</span>
</p>
</div>
{# Invitation à explorer #}
<div class="mt-8">
<a href="{{ url('app_home') }}"
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-lg text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 transform hover:scale-105">
<svg class="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
{{ 'thank_you.back_home_button'|trans }}
</a>
</div>
</div>
</div>
</main>
{% endblock %}

25
templates/error/404.twig Normal file
View File

@@ -0,0 +1,25 @@
{% extends 'base.twig' %}
{# --- METADATA & SCHEMA --- #}
{% block title %}Erreur 500 - {{ 'error.generic_title'|trans({}, 'messages') }}{% endblock %}
{% block meta_description %}{{ 'error.generic_description'|trans({}, 'messages') }}{% endblock %}
{# --- BODY --- #}
{% block body %}
<div class="container mx-auto px-4 py-20 text-center">
<h1 class="text-9xl font-extrabold text-red-700 mb-4">500</h1>
<h2 class="text-3xl font-bold text-gray-800 mb-6">
{{ 'error.generic_title'|trans({}, 'messages') }}
</h2>
<p class="text-lg text-gray-600 mb-8">
{{ 'error.generic_description'|trans({}, 'messages') }}
</p>
{# Utiliser path('home') ou path('app_home') selon la convention de votre application #}
<a href="{{ path('app_home') | default('/') }}" class="inline-block px-6 py-3 text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition duration-150 shadow-md">
Retourner à l'accueil
</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends 'base.twig' %}
{# --- METADATA & SCHEMA --- #}
{% block title %}Erreur 500 - {{ 'error.generic_title'|trans({}, 'messages') }}{% endblock %}
{% block meta_description %}{{ 'error.generic_description'|trans({}, 'messages') }}{% endblock %}
{# --- BODY --- #}
{% block body %}
<div class="container mx-auto px-4 py-20 text-center">
<h1 class="text-9xl font-extrabold text-red-700 mb-4">500</h1>
<h2 class="text-3xl font-bold text-gray-800 mb-6">
{{ 'error.generic_title'|trans({}, 'messages') }}
</h2>
<p class="text-lg text-gray-600 mb-8">
{{ 'error.generic_description'|trans({}, 'messages') }}
</p>
{# L'utilisation de path('home') ou path('app_home') dépend de votre configuration de routes #}
<a href="{{ path('app_home') | default('/') }}" class="inline-block px-6 py-3 text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition duration-150 shadow-md">
Retourner à l'accueil
</a>
</div>
{% endblock %}

96
templates/mails/dons.twig Normal file
View File

@@ -0,0 +1,96 @@
{% extends 'mails/base.twig' %}
{% block content %}
{# --- Section Header (Logo/Titre) --- #}
<mj-section background-color="#4f46e5" padding-bottom="0">
<mj-column>
<mj-text align="center" font-size="24px" color="#ffffff" font-family="Helvetica Neue, Arial">
Confirmation de votre Don
</mj-text>
<mj-text align="center" font-size="16px" color="#a5a2fa" font-family="Helvetica Neue, Arial" padding-top="0" padding-bottom="20px">
Un immense merci pour votre générosité !
</mj-text>
</mj-column>
</mj-section>
{# --- Section Confirmation & Montant --- #}
<mj-section background-color="#ffffff">
<mj-column>
{# Montant du Don (Mis en évidence) #}
<mj-text font-size="16px" color="#555555" font-family="Helvetica Neue, Arial" align="center">
Nous avons bien reçu votre soutien d'un montant de :
</mj-text>
<mj-text font-size="36px" font-weight="bold" color="#10b981" font-family="Helvetica Neue, Arial" align="center" padding-bottom="20px">
{{ datas.don.amount|format_currency('EUR', locale='fr') }}
</mj-text>
<mj-divider border-color="#e0e0e0" border-width="1px" padding="0 20px"></mj-divider>
{# Message de remerciement standard #}
<mj-text font-size="16px" color="#555555" font-family="Helvetica Neue, Arial" padding-top="20px">
Bonjour {% if datas.don.name %}{{ datas.don.name }}{% else %}Cher Donateur{% endif %},
</mj-text>
<mj-text font-size="16px" color="#555555" font-family="Helvetica Neue, Arial">
Votre don a été confirmé avec succès. Votre contribution est essentielle pour l'organisation de nos événements, l'achat de matériel et le maintien de nos activités.
</mj-text>
</mj-column>
</mj-section>
{# --- Section Détails du Don --- #}
<mj-section background-color="#ffffff" padding-top="0">
<mj-column border="1px solid #e0eeef" padding="10px" border-radius="4px">
<mj-text font-size="18px" font-weight="bold" color="#4f46e5" font-family="Helvetica Neue, Arial" padding-bottom="10px">
Détails de votre transaction
</mj-text>
{# Nom / Pseudo #}
<mj-table font-size="14px" color="#555555" font-family="Helvetica Neue, Arial" padding-bottom="5px">
<tr>
<td style="width: 150px; padding-bottom: 5px;">Nom/Pseudo :</td>
<td style="font-weight: bold; padding-bottom: 5px;">{% if datas.don.name %}{{ datas.don.name }}{% else %}Anonyme{% endif %}</td>
</tr>
</mj-table>
{# Montant #}
<mj-table font-size="14px" color="#555555" font-family="Helvetica Neue, Arial" padding-bottom="5px">
<tr>
<td style="width: 150px; padding-bottom: 5px;">Montant :</td>
<td style="font-weight: bold; color: #10b981; padding-bottom: 5px;">{{ datas.don.amount|format_currency('EUR', locale='fr') }}</td>
</tr>
</mj-table>
{# Message facultatif #}
{% if datas.don.message %}
<mj-table font-size="14px" color="#555555" font-family="Helvetica Neue, Arial" padding-top="10px">
<tr>
<td style="width: 150px; vertical-align: top;">Message :</td>
<td><em style="color: #777;">"{{ datas.don.message }}"</em></td>
</tr>
</mj-table>
{% endif %}
</mj-column>
</mj-section>
{# --- Section Reçu Fiscal & Non-déductibilité (MIS À JOUR) --- #}
<mj-section background-color="#f4f4f4">
<mj-column>
{# Message concernant le Reçu Fiscal en Pièce Jointe #}
<mj-text font-size="15px" color="#333333" font-family="Helvetica Neue, Arial" align="center" padding-bottom="5px">
<span style="font-size: 20px;">📎</span>
<span style="font-weight: bold; color: #4f46e5;">Votre reçu se trouve en pièce jointe de cet e-mail.</span>
</mj-text>
{# Message d'avertissement sur la réduction d'impôt #}
<mj-text font-size="14px" color="#993300" font-family="Helvetica Neue, Arial" align="center" font-style="italic" padding-bottom="15px">
<strong>NOTE IMPORTANTE :</strong> Notez que notre association ne vous permet pas de bénéficier d'une réduction d'impôt.
</mj-text>
<mj-text font-size="12px" color="#999999" font-family="Helvetica Neue, Arial" align="center">
Si vous avez des questions, n'hésitez pas à nous contacter.
</mj-text>
</mj-column>
</mj-section>
{% endblock %}

View File

@@ -0,0 +1,50 @@
{% extends 'mails/base.twig' %}
{% block content %}
{# --- Section Header (Alerte Nouveau Don) --- #}
<mj-section background-color="#ff9900" padding-bottom="20px">
<mj-column>
<mj-text align="center" font-size="28px" color="#ffffff" font-family="Helvetica Neue, Arial" font-weight="bold">
🚨 NOUVEAU DON ARRIVÉ ! 🚨
</mj-text>
</mj-column>
</mj-section>
{# --- Section Détails Clés --- #}
<mj-section background-color="#ffffff" padding-top="20px">
<mj-column>
<mj-text font-size="16px" color="#333333" font-family="Helvetica Neue, Arial" padding-bottom="10px">
Un nouvel acte de générosité a été enregistré. Voici les détails :
</mj-text>
{# Montant du Don (Mis en évidence) #}
<mj-text font-size="20px" font-weight="bold" color="#ff9900" font-family="Helvetica Neue, Arial" align="center">
Montant : {{ datas.don.amount|format_currency('EUR', locale='fr') }}
</mj-text>
<mj-divider border-color="#e0e0e0" border-width="1px" padding="10px 20px"></mj-divider>
{# Informations du Donateur #}
<mj-table font-size="15px" color="#555555" font-family="Helvetica Neue, Arial" padding-top="10px">
<tr>
<td style="width: 150px; padding-bottom: 5px; font-weight: bold;">Donateur :</td>
<td style="padding-bottom: 5px;">{% if datas.don.name %}{{ datas.don.name }}{% else %}Anonyme / Non spécifié{% endif %}</td>
</tr>
<tr>
<td style="width: 150px; padding-bottom: 5px; font-weight: bold;">E-mail :</td>
<td style="padding-bottom: 5px; color: #4f46e5;">{{ datas.don.email }}</td>
</tr>
</mj-table>
{# Message facultatif #}
{% if datas.don.message %}
<mj-text font-size="16px" font-weight="bold" color="#333333" font-family="Helvetica Neue, Arial" padding-top="20px">
Message laissé par le donateur :
</mj-text>
<mj-text font-size="15px" color="#555555" font-family="Helvetica Neue, Arial" border="1px solid #e0e0e0" padding="10px" background-color="#fafafa" border-radius="4px">
"{{ datas.don.message }}"
</mj-text>
{% endif %}
</mj-column>
</mj-section>
{% endblock %}

View File

@@ -568,3 +568,52 @@ logged_in_as: Signed in as
logout_link: Log Out logout_link: Log Out
page.login: Login page.login: Login
logged_admin: Administration logged_admin: Administration
# messages.en.yaml
# --- PAGE DE DON (dons.html.twig) ---
dons.title: Support Us - Make a Donation to the Association
dons.description: Your donation helps support our association, organize events, purchase equipment, and fund the website.
dons.page_title: Make a Donation and Support Our Association
dons.introduction: Your generosity is essential for the life and development of our association. Every donation, large or small, allows us to realize our projects and fulfill our mission.
dons.impact_title: The Impact of Your Donation
dons.impact_text: By making a donation, you contribute directly to all our activities. Your support concretely helps us to
dons.support_for_title: What do your contributions fund?
dons.item.events_title: Event Organization
dons.item.events: Organizing events, workshops, and gatherings for the community.
dons.item.equipment_title: Equipment Purchase
dons.item.equipment: Purchasing and maintaining the necessary equipment for our activities (tools, specific gear, etc.).
dons.item.website_hosting_title: Website Funding
dons.item.website_hosting: Funding the hosting and maintenance of this website, ensuring our online presence.
dons.item.other_needs_title: Operating Costs
dons.item.other_needs: Covering operating expenses and unforeseen needs of the association.
dons.make_a_donation_title: Take Action
dons.call_to_action_text: Click the button below to make your donation securely through our partner platform.
dons.thanks_note: A big thank you for your valuable support.
# --- FORMULAIRE DE DON (Clés utilisées dans le bloc form) ---
form.name_label: Your Name or Nickname
form.name_placeholder: Ex. John Doe or A Friend
form.email_label: Your Email Address
form.email_placeholder: youraddress@example.com
form.amount_label: Donation Amount
form.message_label: Message of Encouragement
form.message_placeholder: Leave a short note for the team (optional)
form.optional: optional
form.submit_button_dons: Donate and Continue to Payment
# --- PAGE DE CONFIRMATION DE DON (dons/confirmation.html.twig) ---
thank_you.title: A Huge Thank You for Your Donation!
thank_you.message_main: "Your generosity is invaluable and will allow us to continue our mission: <strong>organizing events, purchasing equipment, and sustaining our association.</strong>"
thank_you.mount_received: Amount of Your Support Received # Note: Corrected 'mount' to 'amount' in translation
thank_you.email_sent_title: Your Confirmation is on its Way
thank_you.email_sent_info: Once the payment is confirmed by our partner, you will receive an email containing all the details of your donation and your tax receipt. This process usually takes a few minutes.
thank_you.email_recipient: Email sent to
thank_you.back_home_button: Return to Home
thank_you.amount_received: Amount of Your Support Received
error.not_found_title: Page Not Found
error.not_found_description: We are sorry, but the page you are looking for does not exist or has been moved. Please check the address or return to the homepage.
error.generic_title: Oops, an error occurred
error.sgeneric_description: We encountered an unexpected problem on the server. Please try again later. If the error persists, contact technical support.

View File

@@ -510,3 +510,49 @@ logged_in_as: Connecté en tant que
logout_link: Déconnexion logout_link: Déconnexion
page.login: Connexion page.login: Connexion
logged_admin: Administration logged_admin: Administration
dons.impact_title: L'impact de votre Don
dons.impact_text: En faisant un don, vous contribuez directement à l'ensemble de nos activités. Votre soutien nous aide concrètement à
dons.title: Soutenez-nous - Faites un Don à l'Association
dons.description: Votre don permet de soutenir notre association, d'organiser des événements, d'acheter du matériel et de financer le site internet.
dons.page_title: Faites un Don et Soutenez Notre Association
dons.introduction: Votre générosité est essentielle pour la vie et le développement de notre association. Chaque don, petit ou grand, nous permet de concrétiser nos projets et d'assurer notre mission.
dons.support_for_title: À quoi servent vos contributions ?
dons.item.events: Organiser des événements, des ateliers et des rencontres pour la communauté.
dons.item.equipment: Acheter et entretenir le matériel nécessaire à nos activités (outils, équipements spécifiques, etc.).
dons.item.website_hosting: Financer l'hébergement et la maintenance de ce site internet, garantissant notre présence en ligne.
dons.item.other_needs: Couvrir les frais de fonctionnement et les besoins imprévus de l'association.
dons.item.events_title: Organisation d'événements
dons.item.equipment_title: Achat de matériel
dons.item.website_hosting_title: Financement du site web
dons.item.other_needs_title: Frais de fonctionnement
form.name_label: Votre nom ou pseudo
form.name_placeholder: Ex. Jean Dupont ou Un Ami
form.email_label: Votre adresse e-mail
form.email_placeholder: votreadresse@exemple.com
form.amount_label: Montant du don
form.message_label: Message d'encouragement
form.message_placeholder: Laissez un petit mot pour l'équipe (facultatif)
form.optional: facultatif
form.submit_button_dons: Faire un don et continuer vers le paiement
dons.thanks_note: Un grand merci pour votre précieux soutien.
dons.make_a_donation_title: Passer à l'action
dons.call_to_action_text: Cliquez sur le bouton ci-dessous pour effectuer votre don en toute sécurité via notre plateforme partenaire.
thank_you.title: Un immense Merci pour votre Don !
thank_you.message_main: "Votre générosité est précieuse et nous permettra de continuer notre mission : <strong>organiser des événements, acheter du matériel et faire vivre notre association.</strong>"
thank_you.mount_received: Montant de votre soutien reçu
thank_you.email_sent_title: Votre confirmation est en route
thank_you.email_sent_info: Une fois le paiement confirmé par notre partenaire, vous recevrez un e-mail contenant tous les détails de votre don et votre reçu fiscal. Ce processus prend généralement quelques minutes.
thank_you. email_recipient: E-mail envoyé à
thank_you.back_home_button: Retourner à l'accueil
thank_you.amount_received: Montant de votre soutien reçu
error.not_found_title: Page introuvable
error.not_found_description: Nous sommes désolés, mais la page que vous recherchez n'existe pas ou a été déplacée. Veuillez vérifier l'adresse ou retourner à la page d'accueil.
error.generic_title: Oups, une erreur s'est produite
error.generic_description: Nous avons rencontré un problème inattendu sur le serveur. Veuillez réessayer ultérieurement. Si l'erreur persiste, contactez le support technique.