Add application source code, configs and assets
- Controllers, Entity, Repository, Services, Twig extensions - Templates (account, emails, home, legal, security, unsubscribe) - Symfony config updates (bundles, security, framework, services) - Vite + Bun setup with PostCSS - Caddy config, CLAUDE.md, README - Update .gitignore (node_modules, .idea, cert) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
32
.env
32
.env
@@ -16,33 +16,33 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=
|
||||
APP_SECRET=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
|
||||
APP_SHARE_DIR=var/share
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> symfony/routing ###
|
||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
DEFAULT_URI=http://localhost
|
||||
DEFAULT_URI=https://esyweb.local
|
||||
###< symfony/routing ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||
#
|
||||
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
||||
DATABASE_URL="postgresql://app:secret@database:5432/ecosplay?serverVersion=16&charset=utf8"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> symfony/messenger ###
|
||||
# Choose one of the transports below
|
||||
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
||||
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||
MESSENGER_TRANSPORT_DSN=redis://redis:6379/messages
|
||||
###< symfony/messenger ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
MAILER_DSN=null://null
|
||||
MAILER_DSN=smtp://mailpit:1025
|
||||
###< symfony/mailer ###
|
||||
|
||||
###> vite ###
|
||||
# 0 = dev (HMR via localhost:5173), 1 = prod (manifest build)
|
||||
VITE_LOAD=0
|
||||
REAL_MAIL=0
|
||||
###< vite ###
|
||||
STRIPE_PK=
|
||||
STRIPE_SK=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_MODE=test
|
||||
SMIME_PASSPHRASE='KLreLnyR07x5h#3$AC'
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
/.env.local.php
|
||||
@@ -14,7 +13,6 @@
|
||||
/.phpunit.cache/
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> symfony/asset-mapper ###
|
||||
/public/assets/
|
||||
/assets/vendor/
|
||||
###< symfony/asset-mapper ###
|
||||
node_modules/
|
||||
.idea/
|
||||
cert/
|
||||
|
||||
27
CLAUDE.md
Normal file
27
CLAUDE.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# E-Ticket
|
||||
|
||||
## Contexte
|
||||
|
||||
Projet développé et géré par l'association **E-Cosplay**. L'association autorise Claude à effectuer des actions sur le dépôt (commits, modifications de fichiers, exécution de commandes).
|
||||
|
||||
## Stack technique
|
||||
|
||||
- Symfony 8
|
||||
- PHP 8.4
|
||||
- PostgreSQL
|
||||
- Redis
|
||||
- Messenger
|
||||
- Amazon SES
|
||||
- Cloudflare
|
||||
|
||||
## Description
|
||||
|
||||
Plateforme destinée aux associations pour la vente de tickets événementiels, la réservation de tables, l'organisation de brocantes et le vote en ligne.
|
||||
|
||||
## Règles
|
||||
|
||||
- **Interdiction** de supprimer des fichiers sans autorisation explicite de l'utilisateur.
|
||||
- En cas de doute, toujours poser la question avant d'agir.
|
||||
- Toujours pousser sur `main`, ne jamais créer de branche.
|
||||
- Toujours demander avant de push.
|
||||
- Les messages de commit doivent toujours détailler les modifications effectuées.
|
||||
94
README.md
Normal file
94
README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# E-Cosplay Ticket
|
||||
|
||||
Système de billetterie professionnelle pour associations cosplay. Génération, scan et contrôle de tickets pour événements.
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- **Génération de tickets** — Création de billets avec QR codes uniques pour chaque participant
|
||||
- **Scan & Contrôle** — Vérification des tickets par scan QR à l'entrée de l'événement
|
||||
- **Gestion des événements** — Création et configuration d'événements (date, lieu, capacité, tarifs)
|
||||
- **Gestion des participants** — Suivi des inscriptions et des présences
|
||||
- **SEO** — Sitemap XML, métadonnées Open Graph, données structurées (JSON-LD) pour le référencement des événements
|
||||
- **Notifications** — Envoi des tickets par email avec confirmation de commande
|
||||
- **Tableau de bord** — Statistiques en temps réel (ventes, entrées, taux de remplissage)
|
||||
|
||||
## Stack technique
|
||||
|
||||
| Composant | Technologie |
|
||||
|-----------|-------------|
|
||||
| Framework | Symfony 8.0 |
|
||||
| PHP | >= 8.4 |
|
||||
| Base de données | PostgreSQL 16 |
|
||||
| ORM | Doctrine 3.6 |
|
||||
| Frontend | Twig + Stimulus 3.2 + Turbo |
|
||||
| Assets | Asset Mapper (sans build) |
|
||||
| PDF | Dompdf 3.1 |
|
||||
| QR Code | endroid/qr-code 6.1 |
|
||||
| Emails | Symfony Mailer |
|
||||
| File d'attente | Symfony Messenger + Redis |
|
||||
| Tests | PHPUnit 13 |
|
||||
|
||||
## Prérequis
|
||||
|
||||
- PHP 8.4+
|
||||
- PostgreSQL 16
|
||||
- Redis 7+
|
||||
- Composer
|
||||
- Symfony CLI (recommandé)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Cloner le dépôt
|
||||
git clone https://github.com/your-org/e-cosplay-ticket.git
|
||||
cd e-cosplay-ticket
|
||||
|
||||
# Installer les dépendances
|
||||
composer install
|
||||
|
||||
# Configurer l'environnement
|
||||
cp .env .env.local
|
||||
# Éditer .env.local avec vos paramètres (DATABASE_URL, MAILER_DSN, etc.)
|
||||
|
||||
# Créer la base de données et exécuter les migrations
|
||||
php bin/console doctrine:database:create
|
||||
php bin/console doctrine:migrations:migrate
|
||||
|
||||
# Lancer le serveur de développement
|
||||
symfony server:start
|
||||
```
|
||||
|
||||
## Docker (développement)
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Services inclus :
|
||||
- **PostgreSQL 16** — port 5432
|
||||
- **Redis 7** — port 6379 (transport Messenger)
|
||||
- **Mailpit** — port 8025 (interface web pour les emails)
|
||||
|
||||
## Structure du projet
|
||||
|
||||
```
|
||||
src/
|
||||
├── Controller/ # Contrôleurs HTTP
|
||||
├── Entity/ # Entités Doctrine (Ticket, Event, User…)
|
||||
└── Repository/ # Repositories Doctrine
|
||||
templates/ # Templates Twig
|
||||
assets/ # JS (Stimulus) & CSS
|
||||
config/ # Configuration Symfony
|
||||
migrations/ # Migrations Doctrine
|
||||
tests/ # Tests PHPUnit
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
php bin/phpunit
|
||||
```
|
||||
|
||||
## Licence
|
||||
|
||||
Projet privé — Tous droits réservés.
|
||||
@@ -1,10 +1,2 @@
|
||||
import './stimulus_bootstrap.js';
|
||||
/*
|
||||
* Welcome to your app's main JavaScript file!
|
||||
*
|
||||
* This file will be included onto the page via the importmap() Twig function,
|
||||
* which should already be in your base.html.twig.
|
||||
*/
|
||||
import './styles/app.css';
|
||||
|
||||
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
|
||||
import "./app.scss"
|
||||
console.log("aa")
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
@import "tailwindcss";
|
||||
@import url('https://fonts.googleapis.com/css2?family=Intel+One+Mono:ital,wght@0,300..700;1,300..700&display=swap');
|
||||
|
||||
.bg-op {
|
||||
background: rgba(0,0,0,0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.epage{
|
||||
color: orangered;
|
||||
}
|
||||
|
||||
#userMenuDesktop {
|
||||
margin-top: -4px; /* Remonte légèrement le menu pour toucher le bouton */
|
||||
padding-top: 10px; /* Ajoute du padding interne pour garder la zone réactive */
|
||||
}
|
||||
|
||||
@media (max-width: 764px) {
|
||||
.list {
|
||||
display: block !important;
|
||||
.listitem{
|
||||
width: 100% !important;
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
#join_role {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"controllers": {
|
||||
"@symfony/ux-turbo": {
|
||||
"turbo-core": {
|
||||
"enabled": true,
|
||||
"fetch": "eager"
|
||||
},
|
||||
"mercure-turbo-stream": {
|
||||
"enabled": false,
|
||||
"fetch": "eager"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoints": []
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
|
||||
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
|
||||
|
||||
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
|
||||
// Use `form.requestSubmit()` to ensure that the submit event is triggered. Using `form.submit()` will not trigger the event
|
||||
// and thus this event-listener will not be executed.
|
||||
document.addEventListener('submit', function (event) {
|
||||
generateCsrfToken(event.target);
|
||||
}, true);
|
||||
|
||||
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
|
||||
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
|
||||
document.addEventListener('turbo:submit-start', function (event) {
|
||||
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
|
||||
Object.keys(h).map(function (k) {
|
||||
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
|
||||
});
|
||||
});
|
||||
|
||||
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
|
||||
document.addEventListener('turbo:submit-end', function (event) {
|
||||
removeCsrfToken(event.detail.formSubmission.formElement);
|
||||
});
|
||||
|
||||
export function generateCsrfToken (formElement) {
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return;
|
||||
}
|
||||
|
||||
let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
let csrfToken = csrfField.value;
|
||||
|
||||
if (!csrfCookie && nameCheck.test(csrfToken)) {
|
||||
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
|
||||
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
|
||||
}
|
||||
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
if (csrfCookie && tokenCheck.test(csrfToken)) {
|
||||
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
|
||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
||||
}
|
||||
}
|
||||
|
||||
export function generateCsrfHeaders (formElement) {
|
||||
const headers = {};
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
|
||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
||||
headers[csrfCookie] = csrfField.value;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function removeCsrfToken (formElement) {
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
|
||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
||||
const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
|
||||
|
||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
||||
}
|
||||
}
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default 'csrf-protection-controller';
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/*
|
||||
* This is an example Stimulus controller!
|
||||
*
|
||||
* Any element with a data-controller="hello" attribute will cause
|
||||
* this controller to be executed. The name "hello" comes from the filename:
|
||||
* hello_controller.js -> "hello"
|
||||
*
|
||||
* Delete this file or adapt it for your use!
|
||||
*/
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
||||
|
||||
const app = startStimulusApp();
|
||||
// register any custom, 3rd party controllers here
|
||||
// app.register('some_controller_name', SomeImportedController);
|
||||
@@ -1,3 +0,0 @@
|
||||
body {
|
||||
background-color: skyblue;
|
||||
}
|
||||
@@ -10,10 +10,12 @@
|
||||
"doctrine/doctrine-bundle": "^3.2",
|
||||
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||
"doctrine/orm": "^3.6",
|
||||
"dompdf/dompdf": "*",
|
||||
"endroid/qr-code-bundle": "*",
|
||||
"liip/imagine-bundle": "^2.17",
|
||||
"phpdocumentor/reflection-docblock": "^6.0",
|
||||
"phpstan/phpdoc-parser": "^2.3",
|
||||
"symfony/asset": "8.0.*",
|
||||
"symfony/asset-mapper": "8.0.*",
|
||||
"symfony/console": "8.0.*",
|
||||
"symfony/doctrine-messenger": "8.0.*",
|
||||
"symfony/dotenv": "8.0.*",
|
||||
@@ -30,25 +32,28 @@
|
||||
"symfony/process": "8.0.*",
|
||||
"symfony/property-access": "8.0.*",
|
||||
"symfony/property-info": "8.0.*",
|
||||
"symfony/redis-messenger": "8.0.*",
|
||||
"symfony/runtime": "8.0.*",
|
||||
"symfony/security-bundle": "8.0.*",
|
||||
"symfony/serializer": "8.0.*",
|
||||
"symfony/stimulus-bundle": "^2.32",
|
||||
"symfony/string": "8.0.*",
|
||||
"symfony/translation": "8.0.*",
|
||||
"symfony/twig-bundle": "8.0.*",
|
||||
"symfony/ux-turbo": "^2.32",
|
||||
"symfony/validator": "8.0.*",
|
||||
"symfony/web-link": "8.0.*",
|
||||
"symfony/yaml": "8.0.*",
|
||||
"twig/cssinliner-extra": "^3.23",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/twig": "^2.12|^3.0"
|
||||
"twig/inky-extra": "^3.23",
|
||||
"twig/twig": "^2.12|^3.0",
|
||||
"vich/uploader-bundle": "^2.9"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
"symfony/runtime": true,
|
||||
"endroid/installer": true
|
||||
},
|
||||
"bump-after-update": true,
|
||||
"sort-packages": true
|
||||
@@ -78,8 +83,7 @@
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd",
|
||||
"importmap:install": "symfony-cmd"
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||
},
|
||||
"post-install-cmd": [
|
||||
"@auto-scripts"
|
||||
|
||||
6343
composer.lock
generated
6343
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -7,10 +7,11 @@ return [
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
||||
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||
Endroid\QrCodeBundle\EndroidQrCodeBundle::class => ['all' => true],
|
||||
Liip\ImagineBundle\LiipImagineBundle::class => ['all' => true],
|
||||
Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
framework:
|
||||
asset_mapper:
|
||||
# The paths to make available to the asset mapper.
|
||||
paths:
|
||||
- assets/
|
||||
missing_import_mode: strict
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
asset_mapper:
|
||||
missing_import_mode: warn
|
||||
@@ -8,6 +8,11 @@ framework:
|
||||
#esi: true
|
||||
#fragments: true
|
||||
|
||||
when@dev:
|
||||
framework:
|
||||
trusted_proxies: '127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,REMOTE_ADDR'
|
||||
trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix']
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
test: true
|
||||
|
||||
23
config/packages/liip_imagine.yaml
Normal file
23
config/packages/liip_imagine.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
liip_imagine:
|
||||
driver: imagick
|
||||
|
||||
webp:
|
||||
generate: true
|
||||
quality: 80
|
||||
|
||||
filter_sets:
|
||||
thumbnail:
|
||||
quality: 80
|
||||
filters:
|
||||
thumbnail: { size: [300, 300], mode: inset }
|
||||
background: { size: [300, 300], position: center, color: '#ffffff' }
|
||||
|
||||
medium:
|
||||
quality: 85
|
||||
filters:
|
||||
thumbnail: { size: [600, 600], mode: inset }
|
||||
|
||||
large:
|
||||
quality: 90
|
||||
filters:
|
||||
thumbnail: { size: [1200, 1200], mode: inset }
|
||||
@@ -1,39 +1,36 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
users_in_memory: { memory: null }
|
||||
app_user_provider:
|
||||
entity:
|
||||
class: App\Entity\User
|
||||
property: email
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
# Ensure dev tools and static assets are always allowed
|
||||
pattern: ^/(_profiler|_wdt|assets|build)/
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: users_in_memory
|
||||
provider: app_user_provider
|
||||
form_login:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
enable_csrf: true
|
||||
logout:
|
||||
path: app_logout
|
||||
target: app_home
|
||||
|
||||
# Activate different ways to authenticate:
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
|
||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||
# switch_user: true
|
||||
|
||||
# Note: Only the *first* matching rule is applied
|
||||
access_control:
|
||||
# - { path: ^/admin, roles: ROLE_ADMIN }
|
||||
# - { path: ^/profile, roles: ROLE_USER }
|
||||
- { path: ^/mon-compte, roles: ROLE_USER }
|
||||
|
||||
when@test:
|
||||
security:
|
||||
password_hashers:
|
||||
# Password hashers are resource-intensive by design to ensure security.
|
||||
# In tests, it's safe to reduce their cost to improve performance.
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||
algorithm: auto
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
cost: 4
|
||||
time_cost: 3
|
||||
memory_cost: 10
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# Enable stateless CSRF protection for forms and logins/logouts
|
||||
framework:
|
||||
csrf_protection:
|
||||
check_header: true
|
||||
10
config/packages/vich_uploader.yaml
Normal file
10
config/packages/vich_uploader.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
vich_uploader:
|
||||
db_driver: orm
|
||||
metadata:
|
||||
type: attribute
|
||||
|
||||
mappings:
|
||||
event_image:
|
||||
uri_prefix: /uploads/events
|
||||
upload_destination: '%kernel.project_dir%/public/uploads/events'
|
||||
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
|
||||
@@ -280,7 +280,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* }>,
|
||||
* },
|
||||
* asset_mapper?: bool|array{ // Asset Mapper configuration
|
||||
* enabled?: bool|Param, // Default: true
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* paths?: array<string, scalar|Param|null>,
|
||||
* excluded_patterns?: list<scalar|Param|null>,
|
||||
* exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true
|
||||
@@ -938,20 +938,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* intercept_redirects?: bool|Param, // Default: false
|
||||
* excluded_ajax_paths?: scalar|Param|null, // Default: "^/((index|app(_[\\w]+)?)\\.php/)?_wdt"
|
||||
* }
|
||||
* @psalm-type StimulusConfig = array{
|
||||
* controller_paths?: list<scalar|Param|null>,
|
||||
* controllers_json?: scalar|Param|null, // Default: "%kernel.project_dir%/assets/controllers.json"
|
||||
* }
|
||||
* @psalm-type TurboConfig = array{
|
||||
* broadcast?: bool|array{
|
||||
* enabled?: bool|Param, // Default: true
|
||||
* entity_template_prefixes?: list<scalar|Param|null>,
|
||||
* doctrine_orm?: bool|array{ // Enable the Doctrine ORM integration
|
||||
* enabled?: bool|Param, // Default: true
|
||||
* },
|
||||
* },
|
||||
* default_transport?: scalar|Param|null, // Default: "default"
|
||||
* }
|
||||
* @psalm-type TwigExtraConfig = array{
|
||||
* cache?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
@@ -966,10 +952,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* },
|
||||
* cssinliner?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* enabled?: bool|Param, // Default: true
|
||||
* },
|
||||
* inky?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* enabled?: bool|Param, // Default: true
|
||||
* },
|
||||
* string?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
@@ -1455,6 +1441,146 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* generate_final_classes?: bool|Param, // Default: true
|
||||
* generate_final_entities?: bool|Param, // Default: false
|
||||
* }
|
||||
* @psalm-type EndroidQrCodeConfig = array<string, mixed>
|
||||
* @psalm-type LiipImagineConfig = array{
|
||||
* resolvers?: array<string, array{ // Default: []
|
||||
* web_path?: array{
|
||||
* web_root?: scalar|Param|null, // Default: "%kernel.project_dir%/public"
|
||||
* cache_prefix?: scalar|Param|null, // Default: "media/cache"
|
||||
* },
|
||||
* aws_s3?: array{
|
||||
* bucket?: scalar|Param|null,
|
||||
* cache?: scalar|Param|null, // Default: false
|
||||
* use_psr_cache?: bool|Param, // Default: false
|
||||
* acl?: scalar|Param|null, // Default: "public-read"
|
||||
* cache_prefix?: scalar|Param|null, // Default: ""
|
||||
* client_id?: scalar|Param|null, // Default: null
|
||||
* client_config?: list<mixed>,
|
||||
* get_options?: array<string, scalar|Param|null>,
|
||||
* put_options?: array<string, scalar|Param|null>,
|
||||
* proxies?: array<string, scalar|Param|null>,
|
||||
* },
|
||||
* flysystem?: array{
|
||||
* filesystem_service?: scalar|Param|null,
|
||||
* cache_prefix?: scalar|Param|null, // Default: ""
|
||||
* root_url?: scalar|Param|null,
|
||||
* visibility?: "public"|"private"|"noPredefinedVisibility"|Param, // Default: "public"
|
||||
* },
|
||||
* }>,
|
||||
* loaders?: array<string, array{ // Default: []
|
||||
* stream?: array{
|
||||
* wrapper?: scalar|Param|null,
|
||||
* context?: scalar|Param|null, // Default: null
|
||||
* },
|
||||
* filesystem?: array{
|
||||
* locator?: "filesystem"|"filesystem_insecure"|Param, // Using the "filesystem_insecure" locator is not recommended due to a less secure resolver mechanism, but is provided for those using heavily symlinked projects. // Default: "filesystem"
|
||||
* data_root?: list<scalar|Param|null>,
|
||||
* allow_unresolvable_data_roots?: bool|Param, // Default: false
|
||||
* bundle_resources?: array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* access_control_type?: "blacklist"|"whitelist"|Param, // Sets the access control method applied to bundle names in "access_control_list" into a blacklist or whitelist. // Default: "blacklist"
|
||||
* access_control_list?: list<scalar|Param|null>,
|
||||
* },
|
||||
* },
|
||||
* flysystem?: array{
|
||||
* filesystem_service?: scalar|Param|null,
|
||||
* },
|
||||
* asset_mapper?: array<mixed>,
|
||||
* chain?: array{
|
||||
* loaders?: list<scalar|Param|null>,
|
||||
* },
|
||||
* }>,
|
||||
* driver?: scalar|Param|null, // Default: "gd"
|
||||
* cache?: scalar|Param|null, // Default: "default"
|
||||
* cache_base_path?: scalar|Param|null, // Default: ""
|
||||
* data_loader?: scalar|Param|null, // Default: "default"
|
||||
* default_image?: scalar|Param|null, // Default: null
|
||||
* default_filter_set_settings?: array{
|
||||
* quality?: scalar|Param|null, // Default: 100
|
||||
* jpeg_quality?: scalar|Param|null, // Default: null
|
||||
* png_compression_level?: scalar|Param|null, // Default: null
|
||||
* png_compression_filter?: scalar|Param|null, // Default: null
|
||||
* format?: scalar|Param|null, // Default: null
|
||||
* animated?: bool|Param, // Default: false
|
||||
* cache?: scalar|Param|null, // Default: null
|
||||
* data_loader?: scalar|Param|null, // Default: null
|
||||
* default_image?: scalar|Param|null, // Default: null
|
||||
* filters?: array<string, array<string, mixed>>,
|
||||
* post_processors?: array<string, array<string, mixed>>,
|
||||
* },
|
||||
* controller?: array{
|
||||
* filter_action?: scalar|Param|null, // Default: "Liip\\ImagineBundle\\Controller\\ImagineController::filterAction"
|
||||
* filter_runtime_action?: scalar|Param|null, // Default: "Liip\\ImagineBundle\\Controller\\ImagineController::filterRuntimeAction"
|
||||
* redirect_response_code?: int|Param, // Default: 302
|
||||
* },
|
||||
* filter_sets?: array<string, array{ // Default: []
|
||||
* quality?: scalar|Param|null,
|
||||
* jpeg_quality?: scalar|Param|null,
|
||||
* png_compression_level?: scalar|Param|null,
|
||||
* png_compression_filter?: scalar|Param|null,
|
||||
* format?: scalar|Param|null,
|
||||
* animated?: bool|Param,
|
||||
* cache?: scalar|Param|null,
|
||||
* data_loader?: scalar|Param|null,
|
||||
* default_image?: scalar|Param|null,
|
||||
* filters?: array<string, array<string, mixed>>,
|
||||
* post_processors?: array<string, array<string, mixed>>,
|
||||
* }>,
|
||||
* twig?: array{
|
||||
* mode?: "none"|"lazy"|"legacy"|Param, // Twig mode: none/lazy/legacy (default) // Default: "legacy"
|
||||
* assets_version?: scalar|Param|null, // Default: null
|
||||
* },
|
||||
* enqueue?: bool|Param, // Enables integration with enqueue if set true. Allows resolve image caches in background by sending messages to MQ. // Default: false
|
||||
* messenger?: bool|array{ // Enables integration with symfony/messenger if set true. Warmup image caches in background by sending messages to MQ.
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* },
|
||||
* templating?: bool|Param, // Enables integration with symfony/templating component // Default: true
|
||||
* webp?: array{
|
||||
* generate?: bool|Param, // Default: false
|
||||
* quality?: int|Param, // Default: 100
|
||||
* cache?: scalar|Param|null, // Default: null
|
||||
* data_loader?: scalar|Param|null, // Default: null
|
||||
* post_processors?: array<string, array<string, mixed>>,
|
||||
* },
|
||||
* }
|
||||
* @psalm-type VichUploaderConfig = array{
|
||||
* default_filename_attribute_suffix?: scalar|Param|null, // Default: "_name"
|
||||
* db_driver?: scalar|Param|null,
|
||||
* storage?: scalar|Param|null, // Default: "file_system"
|
||||
* use_flysystem_to_resolve_uri?: bool|Param, // Default: false
|
||||
* twig?: scalar|Param|null, // twig requires templating // Default: true
|
||||
* form?: scalar|Param|null, // Default: true
|
||||
* metadata?: array{
|
||||
* cache?: scalar|Param|null, // Default: "file"
|
||||
* type?: scalar|Param|null, // Default: "attribute"
|
||||
* file_cache?: array{
|
||||
* dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/vich_uploader"
|
||||
* },
|
||||
* auto_detection?: bool|Param, // Default: true
|
||||
* directories?: list<array{ // Default: []
|
||||
* path?: scalar|Param|null,
|
||||
* namespace_prefix?: scalar|Param|null, // Default: ""
|
||||
* }>,
|
||||
* },
|
||||
* mappings?: array<string, array{ // Default: []
|
||||
* uri_prefix?: scalar|Param|null, // Default: "/uploads"
|
||||
* upload_destination?: scalar|Param|null, // Default: null
|
||||
* namer?: string|array{
|
||||
* service?: scalar|Param|null, // Default: null
|
||||
* options?: mixed, // Default: null
|
||||
* },
|
||||
* directory_namer?: string|array{
|
||||
* service?: scalar|Param|null, // Default: null
|
||||
* options?: mixed, // Default: null
|
||||
* },
|
||||
* delete_on_remove?: scalar|Param|null, // Default: true
|
||||
* erase_fields?: scalar|Param|null, // Default: true
|
||||
* delete_on_update?: scalar|Param|null, // Default: true
|
||||
* inject_on_load?: scalar|Param|null, // Default: false
|
||||
* namer_keep_extension?: scalar|Param|null, // Default: false
|
||||
* db_driver?: scalar|Param|null, // Default: null
|
||||
* }>,
|
||||
* }
|
||||
* @psalm-type ConfigType = array{
|
||||
* imports?: ImportsConfig,
|
||||
* parameters?: ParametersConfig,
|
||||
@@ -1463,11 +1589,12 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* doctrine?: DoctrineConfig,
|
||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||
* twig?: TwigConfig,
|
||||
* stimulus?: StimulusConfig,
|
||||
* turbo?: TurboConfig,
|
||||
* twig_extra?: TwigExtraConfig,
|
||||
* security?: SecurityConfig,
|
||||
* monolog?: MonologConfig,
|
||||
* endroid_qr_code?: EndroidQrCodeConfig,
|
||||
* liip_imagine?: LiipImagineConfig,
|
||||
* vich_uploader?: VichUploaderConfig,
|
||||
* "when@dev"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
* parameters?: ParametersConfig,
|
||||
@@ -1478,12 +1605,13 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* debug?: DebugConfig,
|
||||
* twig?: TwigConfig,
|
||||
* web_profiler?: WebProfilerConfig,
|
||||
* stimulus?: StimulusConfig,
|
||||
* turbo?: TurboConfig,
|
||||
* twig_extra?: TwigExtraConfig,
|
||||
* security?: SecurityConfig,
|
||||
* monolog?: MonologConfig,
|
||||
* maker?: MakerConfig,
|
||||
* endroid_qr_code?: EndroidQrCodeConfig,
|
||||
* liip_imagine?: LiipImagineConfig,
|
||||
* vich_uploader?: VichUploaderConfig,
|
||||
* },
|
||||
* "when@prod"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
@@ -1493,11 +1621,12 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* doctrine?: DoctrineConfig,
|
||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||
* twig?: TwigConfig,
|
||||
* stimulus?: StimulusConfig,
|
||||
* turbo?: TurboConfig,
|
||||
* twig_extra?: TwigExtraConfig,
|
||||
* security?: SecurityConfig,
|
||||
* monolog?: MonologConfig,
|
||||
* endroid_qr_code?: EndroidQrCodeConfig,
|
||||
* liip_imagine?: LiipImagineConfig,
|
||||
* vich_uploader?: VichUploaderConfig,
|
||||
* },
|
||||
* "when@test"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
@@ -1508,11 +1637,12 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||
* twig?: TwigConfig,
|
||||
* web_profiler?: WebProfilerConfig,
|
||||
* stimulus?: StimulusConfig,
|
||||
* turbo?: TurboConfig,
|
||||
* twig_extra?: TwigExtraConfig,
|
||||
* security?: SecurityConfig,
|
||||
* monolog?: MonologConfig,
|
||||
* endroid_qr_code?: EndroidQrCodeConfig,
|
||||
* liip_imagine?: LiipImagineConfig,
|
||||
* vich_uploader?: VichUploaderConfig,
|
||||
* },
|
||||
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
||||
* imports?: ImportsConfig,
|
||||
|
||||
2
config/routes/liip_imagine.yaml
Normal file
2
config/routes/liip_imagine.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
_liip_imagine:
|
||||
resource: "@LiipImagineBundle/Resources/config/routing.yaml"
|
||||
@@ -21,3 +21,15 @@ services:
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
|
||||
App\Twig\ViteAssetExtension:
|
||||
arguments:
|
||||
$manifest: '%kernel.project_dir%/public/build/.vite/manifest.json'
|
||||
|
||||
App\Service\MailerService:
|
||||
arguments:
|
||||
$smimeCertificate: '%kernel.project_dir%/cert/certificate.pem'
|
||||
$smimePrivateKey: '%kernel.project_dir%/cert/private-key.pem'
|
||||
$smimePassphrase: '%env(SMIME_PASSPHRASE)%'
|
||||
$fromEmail: 'contact@e-cosplay.fr'
|
||||
$realMail: '%env(bool:REAL_MAIL)%'
|
||||
|
||||
13
docker/caddy/Caddyfile
Normal file
13
docker/caddy/Caddyfile
Normal file
@@ -0,0 +1,13 @@
|
||||
:80 {
|
||||
root * /app/public
|
||||
|
||||
php_fastcgi php:9000 {
|
||||
trusted_proxies private_ranges
|
||||
}
|
||||
|
||||
file_server
|
||||
|
||||
encode gzip
|
||||
|
||||
try_files {path} /index.php?{query}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Returns the importmap for this application.
|
||||
*
|
||||
* - "path" is a path inside the asset mapper system. Use the
|
||||
* "debug:asset-map" command to see the full list of paths.
|
||||
*
|
||||
* - "entrypoint" (JavaScript only) set to true for any module that will
|
||||
* be used as an "entrypoint" (and passed to the importmap() Twig function).
|
||||
*
|
||||
* The "importmap:require" command can be used to add new entries to this file.
|
||||
*/
|
||||
return [
|
||||
'app' => [
|
||||
'path' => './assets/app.js',
|
||||
'entrypoint' => true,
|
||||
],
|
||||
'@hotwired/stimulus' => [
|
||||
'version' => '3.2.2',
|
||||
],
|
||||
'@symfony/stimulus-bundle' => [
|
||||
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
|
||||
],
|
||||
'@hotwired/turbo' => [
|
||||
'version' => '7.3.0',
|
||||
],
|
||||
];
|
||||
33
package.json
Normal file
33
package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "e-cosplay",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"idb": "^8.0.3",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-scss": "^4.0.9",
|
||||
"rollup-plugin-javascript-obfuscator": "^1.0.4",
|
||||
"sass": "^1.97.3",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafikart/drop-files-element": "^1.0.9",
|
||||
"@hotwired/turbo": "^8.0.23",
|
||||
"@sentry/browser": "^10.38.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"insights-js": "^1.2.11",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-favicon": "^1.0.8"
|
||||
}
|
||||
}
|
||||
7
postcss.config.cjs
Normal file
7
postcss.config.cjs
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
parser: 'postcss-scss',
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
'autoprefixer': {},
|
||||
},
|
||||
}
|
||||
18
src/Controller/AccountController.php
Normal file
18
src/Controller/AccountController.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[IsGranted('ROLE_USER')]
|
||||
class AccountController extends AbstractController
|
||||
{
|
||||
#[Route('/mon-compte', name: 'app_account')]
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('account/index.html.twig');
|
||||
}
|
||||
}
|
||||
16
src/Controller/HomeController.php
Normal file
16
src/Controller/HomeController.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class HomeController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_home')]
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('home/index.html.twig');
|
||||
}
|
||||
}
|
||||
46
src/Controller/LegalController.php
Normal file
46
src/Controller/LegalController.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class LegalController extends AbstractController
|
||||
{
|
||||
#[Route('/mentions-legales', name: 'app_mentions_legales')]
|
||||
public function mentionsLegales(): Response
|
||||
{
|
||||
return $this->render('legal/mentions_legales.html.twig');
|
||||
}
|
||||
|
||||
#[Route('/cgu', name: 'app_cgu')]
|
||||
public function cgu(): Response
|
||||
{
|
||||
return $this->render('legal/cgu.html.twig');
|
||||
}
|
||||
|
||||
#[Route('/cgv', name: 'app_cgv')]
|
||||
public function cgv(): Response
|
||||
{
|
||||
return $this->render('legal/cgv.html.twig');
|
||||
}
|
||||
|
||||
#[Route('/hebergement', name: 'app_hosting')]
|
||||
public function hosting(): Response
|
||||
{
|
||||
return $this->render('legal/hosting.html.twig');
|
||||
}
|
||||
|
||||
#[Route('/cookies', name: 'app_cookies')]
|
||||
public function cookies(): Response
|
||||
{
|
||||
return $this->render('legal/cookies.html.twig');
|
||||
}
|
||||
|
||||
#[Route('/rgpd', name: 'app_rgpd')]
|
||||
public function rgpd(): Response
|
||||
{
|
||||
return $this->render('legal/rgpd.html.twig');
|
||||
}
|
||||
}
|
||||
53
src/Controller/RegistrationController.php
Normal file
53
src/Controller/RegistrationController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
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\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class RegistrationController extends AbstractController
|
||||
{
|
||||
#[Route('/inscription', name: 'app_register')]
|
||||
public function register(
|
||||
Request $request,
|
||||
UserPasswordHasherInterface $passwordHasher,
|
||||
EntityManagerInterface $em,
|
||||
ValidatorInterface $validator,
|
||||
): Response {
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$user = new User();
|
||||
$user->setFirstName($request->request->getString('first_name'));
|
||||
$user->setLastName($request->request->getString('last_name'));
|
||||
$user->setEmail($request->request->getString('email'));
|
||||
|
||||
$plainPassword = $request->request->getString('password');
|
||||
$user->setPassword($passwordHasher->hashPassword($user, $plainPassword));
|
||||
|
||||
$errors = $validator->validate($user);
|
||||
if (count($errors) > 0) {
|
||||
foreach ($errors as $error) {
|
||||
$this->addFlash('error', $error->getMessage());
|
||||
}
|
||||
return $this->render('security/register.html.twig');
|
||||
}
|
||||
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Compte créé avec succès ! Connectez-vous.');
|
||||
return $this->redirectToRoute('app_login');
|
||||
}
|
||||
|
||||
return $this->render('security/register.html.twig');
|
||||
}
|
||||
}
|
||||
30
src/Controller/SecurityController.php
Normal file
30
src/Controller/SecurityController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
class SecurityController extends AbstractController
|
||||
{
|
||||
#[Route('/connexion', name: 'app_login')]
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
|
||||
return $this->render('security/login.html.twig', [
|
||||
'last_username' => $authenticationUtils->getLastUsername(),
|
||||
'error' => $authenticationUtils->getLastAuthenticationError(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/deconnexion', name: 'app_logout')]
|
||||
public function logout(): void
|
||||
{
|
||||
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
|
||||
}
|
||||
}
|
||||
34
src/Controller/UnsubscribeController.php
Normal file
34
src/Controller/UnsubscribeController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class UnsubscribeController extends AbstractController
|
||||
{
|
||||
#[Route('/unsubscribe/{token}', name: 'app_unsubscribe', methods: ['GET', 'POST'])]
|
||||
public function __invoke(Request $request, string $token): Response
|
||||
{
|
||||
$email = base64_decode($token, true);
|
||||
|
||||
if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
// TODO: persist unsubscribe (e.g. flag user entity or dedicated table)
|
||||
|
||||
return $this->render('unsubscribe/confirmed.html.twig', [
|
||||
'email' => $email,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('unsubscribe/index.html.twig', [
|
||||
'email' => $email,
|
||||
'token' => $token,
|
||||
]);
|
||||
}
|
||||
}
|
||||
122
src/Entity/User.php
Normal file
122
src/Entity/User.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
#[ORM\Table(name: '`user`')]
|
||||
#[UniqueEntity(fields: ['email'], message: 'Un compte existe déjà avec cet email.')]
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 180, unique: true)]
|
||||
private ?string $email = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $firstName = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $lastName = null;
|
||||
|
||||
/** @var list<string> */
|
||||
#[ORM\Column]
|
||||
private array $roles = [];
|
||||
|
||||
#[ORM\Column]
|
||||
private ?string $password = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(string $email): static
|
||||
{
|
||||
$this->email = $email;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFirstName(): ?string
|
||||
{
|
||||
return $this->firstName;
|
||||
}
|
||||
|
||||
public function setFirstName(string $firstName): static
|
||||
{
|
||||
$this->firstName = $firstName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastName(): ?string
|
||||
{
|
||||
return $this->lastName;
|
||||
}
|
||||
|
||||
public function setLastName(string $lastName): static
|
||||
{
|
||||
$this->lastName = $lastName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return (string) $this->email;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = $this->roles;
|
||||
$roles[] = 'ROLE_USER';
|
||||
return array_unique($roles);
|
||||
}
|
||||
|
||||
/** @param list<string> $roles */
|
||||
public function setRoles(array $roles): static
|
||||
{
|
||||
$this->roles = $roles;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): static
|
||||
{
|
||||
$this->password = $password;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
32
src/Repository/UserRepository.php
Normal file
32
src/Repository/UserRepository.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<User>
|
||||
*/
|
||||
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, User::class);
|
||||
}
|
||||
|
||||
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
|
||||
{
|
||||
if (!$user instanceof User) {
|
||||
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class));
|
||||
}
|
||||
|
||||
$user->setPassword($newHashedPassword);
|
||||
$this->getEntityManager()->persist($user);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
76
src/Service/MailerService.php
Normal file
76
src/Service/MailerService.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Symfony\Component\Mime\Crypto\SMimeSigner;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Mime\Header\Headers;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Mailer\Messenger\SendEmailMessage;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Twig\Environment;
|
||||
|
||||
class MailerService
|
||||
{
|
||||
private readonly SMimeSigner $signer;
|
||||
|
||||
public function __construct(
|
||||
private readonly MessageBusInterface $bus,
|
||||
private readonly Environment $twig,
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly string $smimeCertificate,
|
||||
private readonly string $smimePrivateKey,
|
||||
private readonly string $smimePassphrase,
|
||||
private readonly string $fromEmail,
|
||||
private readonly bool $realMail,
|
||||
) {
|
||||
$this->signer = new SMimeSigner(
|
||||
$this->smimeCertificate,
|
||||
$this->smimePrivateKey,
|
||||
$this->smimePassphrase,
|
||||
);
|
||||
}
|
||||
|
||||
public function send(
|
||||
string $to,
|
||||
string $subject,
|
||||
string $template,
|
||||
array $context = [],
|
||||
?string $replyTo = null,
|
||||
): void {
|
||||
if (!$this->realMail) {
|
||||
$to = $this->fromEmail;
|
||||
}
|
||||
|
||||
$html = $this->twig->render("emails/{$template}.html.twig", $context);
|
||||
|
||||
$messageId = Uuid::v4()->toRfc4122() . '@e-cosplay.fr';
|
||||
|
||||
$email = (new Email())
|
||||
->from($this->fromEmail)
|
||||
->to($to)
|
||||
->subject($subject)
|
||||
->html($html);
|
||||
|
||||
$email->getHeaders()->addIdHeader('Message-ID', $messageId);
|
||||
|
||||
$token = base64_encode($to);
|
||||
$unsubscribeUrl = $this->urlGenerator->generate('app_unsubscribe', [
|
||||
'token' => $token,
|
||||
], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
$email->getHeaders()
|
||||
->addTextHeader('List-Unsubscribe', "<mailto:contact@e-cosplay.fr?subject=unsubscribe>, <{$unsubscribeUrl}>")
|
||||
->addTextHeader('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click');
|
||||
|
||||
if ($replyTo) {
|
||||
$email->replyTo($replyTo);
|
||||
}
|
||||
|
||||
$signed = $this->signer->sign($email);
|
||||
|
||||
$this->bus->dispatch(new SendEmailMessage($signed));
|
||||
}
|
||||
|
||||
}
|
||||
109
src/Twig/ViteAssetExtension.php
Normal file
109
src/Twig/ViteAssetExtension.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use Detection\MobileDetect;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
|
||||
class ViteAssetExtension extends AbstractExtension
|
||||
{
|
||||
const CACHE_KEY = 'vite_manifest';
|
||||
private ?array $manifestData = null;
|
||||
private readonly bool $isDev;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $manifest,
|
||||
private readonly CacheItemPoolInterface $cache,
|
||||
|
||||
) {
|
||||
$this->isDev = $_ENV['VITE_LOAD'] === "0";
|
||||
}
|
||||
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('vite_asset', $this->asset(...), ['is_safe' => ['html']]),
|
||||
new TwigFunction('vite_favicons', $this->favicons(...), ['is_safe' => ['html']])
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le nonce pour les scripts via le Listener de Nelmio
|
||||
*/
|
||||
protected function getNonce(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
private function loadManifest(): void
|
||||
{
|
||||
if ($this->manifestData === null) {
|
||||
$item = $this->cache->getItem(self::CACHE_KEY);
|
||||
if ($item->isHit()) {
|
||||
$this->manifestData = $item->get();
|
||||
} else {
|
||||
if (!file_exists($this->manifest)) {
|
||||
$this->manifestData = [];
|
||||
return;
|
||||
}
|
||||
$this->manifestData = json_decode((string)file_get_contents($this->manifest), true);
|
||||
$item->set($this->manifestData);
|
||||
$this->cache->save($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function asset(string $entry, array $deps = []): string
|
||||
{
|
||||
return $this->isDev ? $this->assetDev($entry, $deps) : $this->assetProd($entry);
|
||||
}
|
||||
|
||||
public function assetDev(string $entry, array $deps): string
|
||||
{
|
||||
$nonce = $this->getNonce();
|
||||
return <<<HTML
|
||||
<script type="module" src="http://localhost:5173/assets/@vite/client" nonce="{$nonce}"></script>
|
||||
<script type="module" src="http://localhost:5173/assets/{$entry}" nonce="{$nonce}" defer></script>
|
||||
HTML;
|
||||
}
|
||||
|
||||
public function assetProd(string $entry): string
|
||||
{
|
||||
$this->loadManifest();
|
||||
$nonce = $this->getNonce();
|
||||
|
||||
$file = $this->manifestData[$entry]['file'] ?? '';
|
||||
$css = $this->manifestData[$entry]['css'] ?? [];
|
||||
|
||||
$html = <<<HTML
|
||||
<script type="module" src="/build/{$file}" crossorigin="anonymous" nonce="{$nonce}" defer></script>
|
||||
HTML;
|
||||
|
||||
foreach ($css as $cssFile) {
|
||||
$html .= '<link rel="stylesheet" href="/build/'.$cssFile.'" crossorigin="anonymous"/>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function favicons(): string
|
||||
{
|
||||
return $this->isDev ? '<link rel="icon" href="/favicon.ico">' : $this->faviconsProd();
|
||||
}
|
||||
|
||||
private function faviconsProd(): string
|
||||
{
|
||||
$this->loadManifest();
|
||||
$faviconHtml = "";
|
||||
foreach ($this->manifestData as $key => $favicon) {
|
||||
if(!str_contains($key, ".js") && isset($favicon['file'])) {
|
||||
$faviconHtml .= '<link rel="icon" href="/build/'.$favicon['file'].'" type="image/x-icon">';
|
||||
}
|
||||
}
|
||||
return $faviconHtml;
|
||||
}
|
||||
}
|
||||
63
symfony.lock
63
symfony.lock
@@ -35,6 +35,18 @@
|
||||
"migrations/.gitignore"
|
||||
]
|
||||
},
|
||||
"endroid/qr-code-bundle": {
|
||||
"version": "6.1.0"
|
||||
},
|
||||
"liip/imagine-bundle": {
|
||||
"version": "2.17",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "1.8",
|
||||
"ref": "d1227d002b70d1a1f941d91845fcd7ac7fbfc929"
|
||||
}
|
||||
},
|
||||
"phpunit/phpunit": {
|
||||
"version": "13.0",
|
||||
"recipe": {
|
||||
@@ -50,21 +62,6 @@
|
||||
"bin/phpunit"
|
||||
]
|
||||
},
|
||||
"symfony/asset-mapper": {
|
||||
"version": "8.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.4",
|
||||
"ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a"
|
||||
},
|
||||
"files": [
|
||||
"assets/app.js",
|
||||
"assets/styles/app.css",
|
||||
"config/packages/asset_mapper.yaml",
|
||||
"importmap.php"
|
||||
]
|
||||
},
|
||||
"symfony/console": {
|
||||
"version": "8.0",
|
||||
"recipe": {
|
||||
@@ -229,21 +226,6 @@
|
||||
"config/routes/security.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/stimulus-bundle": {
|
||||
"version": "2.32",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.24",
|
||||
"ref": "3357f2fa6627b93658d8e13baa416b2a94a50c5f"
|
||||
},
|
||||
"files": [
|
||||
"assets/controllers.json",
|
||||
"assets/controllers/csrf_protection_controller.js",
|
||||
"assets/controllers/hello_controller.js",
|
||||
"assets/stimulus_bootstrap.js"
|
||||
]
|
||||
},
|
||||
"symfony/translation": {
|
||||
"version": "8.0",
|
||||
"recipe": {
|
||||
@@ -270,18 +252,6 @@
|
||||
"templates/base.html.twig"
|
||||
]
|
||||
},
|
||||
"symfony/ux-turbo": {
|
||||
"version": "2.32",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.20",
|
||||
"ref": "287f7c6eb6e9b65e422d34c00795b360a787380b"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/ux_turbo.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/validator": {
|
||||
"version": "8.0",
|
||||
"recipe": {
|
||||
@@ -321,5 +291,14 @@
|
||||
},
|
||||
"twig/extra-bundle": {
|
||||
"version": "v3.23.0"
|
||||
},
|
||||
"vich/uploader-bundle": {
|
||||
"version": "2.9",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "1.13",
|
||||
"ref": "1b3064c2f6b255c2bc2f56461aaeb76b11e07e36"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
templates/account/index.html.twig
Normal file
36
templates/account/index.html.twig
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Mon compte - E-Cosplay Ticket{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="max-w-4xl mx-auto py-20 px-4">
|
||||
<h1 class="text-5xl font-black uppercase tracking-tighter mb-12">Mon Compte</h1>
|
||||
|
||||
<div class="bg-white border-4 border-gray-900 p-8 shadow-[8px_8px_0px_rgba(0,0,0,1)]">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<p class="text-xs font-black uppercase tracking-widest text-gray-400 mb-1">Prénom</p>
|
||||
<p class="text-2xl font-black">{{ app.user.firstName }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-black uppercase tracking-widest text-gray-400 mb-1">Nom</p>
|
||||
<p class="text-2xl font-black">{{ app.user.lastName }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-black uppercase tracking-widest text-gray-400 mb-1">Email</p>
|
||||
<p class="text-2xl font-black">{{ app.user.email }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-black uppercase tracking-widest text-gray-400 mb-1">Membre depuis</p>
|
||||
<p class="text-2xl font-black">{{ app.user.createdAt|date('d/m/Y') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 pt-8 border-t-4 border-gray-900 flex gap-4">
|
||||
<a href="{{ path('app_logout') }}" class="bg-red-500 text-white font-black uppercase tracking-widest px-6 py-3 border-4 border-gray-900 shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-1 hover:translate-y-1 transition-all">
|
||||
Déconnexion
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,17 +1,203 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
|
||||
{% block stylesheets %}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}E-Cosplay Ticket{% endblock %}</title>
|
||||
<meta name="description" content="{% block meta_description %}Billetterie en ligne pour associations - E-Cosplay{% endblock %}">
|
||||
<meta name="author" content="Association E-Cosplay">
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{% block og_url %}{{ app.request.uri }}{% endblock %}">
|
||||
<meta property="og:title" content="{% block og_title %}E-Cosplay Ticket{% endblock %}">
|
||||
<meta property="og:description" content="{% block og_description %}Billetterie en ligne pour associations{% endblock %}">
|
||||
<meta property="og:image" content="{% block og_image %}{{ absolute_url(asset('logo.png')) }}{% endblock %}">
|
||||
<meta property="og:locale" content="fr_FR">
|
||||
<meta property="og:site_name" content="E-Cosplay Ticket">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="robots" content="{% block robots %}index, follow{% endblock %}">
|
||||
<link rel="canonical" href="{% block canonical %}{{ app.request.uri }}{% endblock %}">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
{{ vite_favicons() }}
|
||||
{% block stylesheets %}{% endblock %}
|
||||
{% block javascripts %}
|
||||
{{ vite_asset('app.js') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||
{% block structured_data %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "E-Cosplay",
|
||||
"url": "https://www.e-cosplay.fr",
|
||||
"logo": "{{ absolute_url(asset('logo.png')) }}",
|
||||
"email": "contact@e-cosplay.fr",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "42 Rue de Saint-Quentin",
|
||||
"addressLocality": "Beautor",
|
||||
"postalCode": "02800",
|
||||
"addressCountry": "FR"
|
||||
},
|
||||
"sameAs": [
|
||||
"https://www.facebook.com/assocationecosplay",
|
||||
"https://www.instagram.com/asso_ecosplay/"
|
||||
]
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--accent-indigo: #4f46e5;
|
||||
--accent-yellow: #fabf04;
|
||||
--accent-pink: #ec4899;
|
||||
--brutal-black: #111827;
|
||||
--border-width: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}{% endblock %}
|
||||
<body class="flex flex-col min-h-screen font-sans antialiased text-gray-900 bg-[#fbfbfb] italic">
|
||||
|
||||
{# ── NAVBAR ── #}
|
||||
<header class="sticky top-0 z-50 bg-white border-b-4 border-gray-900">
|
||||
<nav class="mx-auto px-4 lg:px-8">
|
||||
<div class="flex justify-between items-center h-20">
|
||||
<div class="flex-shrink-0">
|
||||
<a href="{{ path('app_home') }}" class="flex items-center group">
|
||||
<div class="relative p-2 border-2 border-gray-900 shadow-[4px_4px_0px_rgba(0,0,0,1)] group-hover:translate-x-1 group-hover:translate-y-1 group-hover:shadow-none transition-all">
|
||||
<img class="h-8 w-auto" src="{{ asset('logo.png') }}" alt="E-Cosplay">
|
||||
</div>
|
||||
<span class="ml-4 text-2xl font-black uppercase tracking-tighter">E-Cosplay <span class="text-indigo-600">Ticket</span></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:flex items-center space-x-1">
|
||||
{% block nav_items %}
|
||||
<a href="{{ path('app_home') }}" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all bg-yellow-400 border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]">
|
||||
Accueil
|
||||
</a>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4 border-l-4 border-gray-900 pl-6 h-full">
|
||||
<div class="relative group">
|
||||
{% if app.user %}
|
||||
<a href="{{ path('app_account') }}" class="p-2 border-2 border-gray-900 bg-white text-gray-900 hover:bg-gray-900 hover:text-white transition-all flex items-center justify-center">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ path('app_login') }}" class="p-2 border-2 border-gray-900 bg-white text-gray-900 hover:bg-gray-900 hover:text-white transition-all flex items-center justify-center">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<button id="mobile-menu-btn" class="lg:hidden p-2 border-2 border-gray-900 bg-yellow-400 font-black">
|
||||
☰
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{# ── MOBILE MENU ── #}
|
||||
<div id="mobile-menu" class="hidden lg:hidden border-t-4 border-gray-900 bg-white">
|
||||
<div class="p-4 space-y-2 uppercase font-black">
|
||||
{% block nav_items_mobile %}
|
||||
<a href="{{ path('app_home') }}" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50">
|
||||
Accueil
|
||||
</a>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{# ── FLASH MESSAGES ── #}
|
||||
{% for type, messages in app.flashes %}
|
||||
{% for message in messages %}
|
||||
<div class="mx-auto mt-4 max-w-7xl px-4">
|
||||
<div class="border-4 border-gray-900 px-6 py-4 font-black shadow-[4px_4px_0px_rgba(0,0,0,1)]
|
||||
{% if type == 'success' %} bg-green-400
|
||||
{% elseif type == 'error' %} bg-red-400
|
||||
{% elseif type == 'warning' %} bg-yellow-400
|
||||
{% else %} bg-indigo-400 text-white {% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{# ── CONTENT ── #}
|
||||
<main role="main" class="flex-grow">
|
||||
{% block body %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{# ── FOOTER ── #}
|
||||
<footer class="bg-yellow-400 border-t-8 border-gray-900 text-gray-900 mt-auto">
|
||||
<div class="max-w-7xl mx-auto py-12 px-4 lg:px-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-12 mb-12 border-b-4 border-gray-900 pb-12">
|
||||
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-3xl font-black uppercase tracking-tighter border-b-4 border-gray-900 inline-block">Nous Contacter</h3>
|
||||
<p class="font-bold text-lg leading-tight">42 RUE DE SAINT-QUENTIN<br>02800 BEAUTOR, FRANCE</p>
|
||||
<a href="mailto:contact@e-cosplay.fr" class="inline-block bg-gray-900 text-white px-4 py-2 font-black uppercase text-sm hover:bg-indigo-600 transition-colors">
|
||||
contact@e-cosplay.fr
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<h3 class="text-3xl font-black uppercase tracking-tighter border-b-4 border-gray-900 inline-block">Nous Suivre</h3>
|
||||
<div class="flex gap-4">
|
||||
<a href="https://www.facebook.com/assocationecosplay" target="_blank" class="w-12 h-12 border-4 border-gray-900 flex items-center justify-center hover:bg-white transition-all font-black">f</a>
|
||||
<a href="https://www.instagram.com/asso_ecosplay/" target="_blank" class="w-12 h-12 border-4 border-gray-900 flex items-center justify-center hover:bg-white transition-all font-black">ig</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-3xl font-black uppercase tracking-tighter border-b-4 border-gray-900 inline-block">E-Cosplay Ticket</h3>
|
||||
<p class="font-bold text-gray-800 leading-snug">
|
||||
Plateforme de billetterie destinée aux associations !
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row justify-between items-center gap-6">
|
||||
<div class="text-center md:text-left">
|
||||
<p class="font-black uppercase text-sm">© {{ "now"|date("Y") }} E-COSPLAY.</p>
|
||||
<p class="text-[10px] font-bold opacity-70">RNA N°W022006988</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center gap-3">
|
||||
{% block footer_links %}
|
||||
<a href="{{ path('app_mentions_legales') }}" class="text-[10px] font-black uppercase bg-gray-900 text-white px-2 py-1 hover:bg-indigo-600 transition-colors">
|
||||
Mentions Légales
|
||||
</a>
|
||||
<a href="{{ path('app_cgu') }}" class="text-[10px] font-black uppercase bg-gray-900 text-white px-2 py-1 hover:bg-indigo-600 transition-colors">
|
||||
CGU
|
||||
</a>
|
||||
<a href="{{ path('app_cgv') }}" class="text-[10px] font-black uppercase bg-gray-900 text-white px-2 py-1 hover:bg-indigo-600 transition-colors">
|
||||
CGV
|
||||
</a>
|
||||
<a href="{{ path('app_hosting') }}" class="text-[10px] font-black uppercase bg-gray-900 text-white px-2 py-1 hover:bg-indigo-600 transition-colors">
|
||||
Hébergement
|
||||
</a>
|
||||
<a href="{{ path('app_cookies') }}" class="text-[10px] font-black uppercase bg-gray-900 text-white px-2 py-1 hover:bg-indigo-600 transition-colors">
|
||||
Politique de Cookies
|
||||
</a>
|
||||
<a href="{{ path('app_rgpd') }}" class="text-[10px] font-black uppercase bg-gray-900 text-white px-2 py-1 hover:bg-indigo-600 transition-colors">
|
||||
Politique RGPD
|
||||
</a>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
document.getElementById('mobile-menu-btn')?.addEventListener('click', () => {
|
||||
document.getElementById('mobile-menu')?.classList.toggle('hidden');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
103
templates/emails/base.html.twig
Normal file
103
templates/emails/base.html.twig
Normal file
@@ -0,0 +1,103 @@
|
||||
{% apply inline_css %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: #f4f4f5;
|
||||
color: #18181b;
|
||||
}
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
padding: 40px 0;
|
||||
background-color: #f4f4f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
.header {
|
||||
background-color: #7c3aed;
|
||||
color: #ffffff;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.content {
|
||||
padding: 32px;
|
||||
}
|
||||
.content h2 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 20px;
|
||||
color: #18181b;
|
||||
}
|
||||
.content p {
|
||||
margin: 0 0 16px;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #3f3f46;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 14px 28px;
|
||||
background-color: #7c3aed;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
.qr-code {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
}
|
||||
.qr-code img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
.footer {
|
||||
padding: 24px 32px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #a1a1aa;
|
||||
border-top: 1px solid #e4e4e7;
|
||||
}
|
||||
.footer a {
|
||||
color: #7c3aed;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🎫 E-Cosplay Ticket</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<div class="footer">
|
||||
{% block footer %}
|
||||
<p>© {{ "now"|date("Y") }} E-Cosplay — <a href="https://e-cosplay.fr">e-cosplay.fr</a></p>
|
||||
<p>Cet email a été envoyé depuis contact@e-cosplay.fr</p>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{% endapply %}
|
||||
9
templates/emails/confirmation.html.twig
Normal file
9
templates/emails/confirmation.html.twig
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends 'emails/base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Bonjour {{ participant }},</h2>
|
||||
<p>Votre inscription à <strong>{{ event }}</strong> est confirmée.</p>
|
||||
<p>Vous recevrez votre ticket avec le QR code par email avant l'événement.</p>
|
||||
<p>Si vous avez des questions, n'hésitez pas à nous contacter.</p>
|
||||
<p>À bientôt !</p>
|
||||
{% endblock %}
|
||||
14
templates/emails/ticket.html.twig
Normal file
14
templates/emails/ticket.html.twig
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends 'emails/base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Bonjour {{ participant }},</h2>
|
||||
<p>Votre ticket pour <strong>{{ event }}</strong> est prêt !</p>
|
||||
<p>Présentez le QR code ci-dessous à l'entrée de l'événement :</p>
|
||||
|
||||
<div class="qr-code">
|
||||
<img src="{{ qrCode }}" alt="QR Code ticket">
|
||||
</div>
|
||||
|
||||
<p>Conservez bien cet email, il constitue votre preuve d'inscription.</p>
|
||||
<p>À bientôt !</p>
|
||||
{% endblock %}
|
||||
105
templates/home/index.html.twig
Normal file
105
templates/home/index.html.twig
Normal file
@@ -0,0 +1,105 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Accueil - E-Cosplay Ticket{% endblock %}
|
||||
{% block meta_description %}Billetterie officielle E-Cosplay. Achetez vos tickets en ligne, recevez votre QR code et présentez-le à l'entrée.{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{# ── HERO ── #}
|
||||
<section class="relative min-h-[90vh] flex items-center justify-center bg-white border-b-8 border-gray-900 px-4 pt-20 pb-32">
|
||||
<div class="absolute inset-0 opacity-[0.03] pointer-events-none select-none overflow-hidden">
|
||||
<span class="text-[30rem] font-black uppercase leading-none block -rotate-12 translate-y-20">TICKET</span>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto relative z-10 text-center">
|
||||
<h1 class="text-6xl md:text-9xl font-black uppercase tracking-tighter leading-[0.85] mb-8">
|
||||
<span class="block">Plateforme de billetterie</span>
|
||||
<span class="block text-indigo-600">destinée aux associations !</span>
|
||||
</h1>
|
||||
|
||||
<p class="max-w-2xl mx-auto text-xl md:text-3xl font-bold text-gray-600 mb-12 border-l-8 border-indigo-600 pl-6 text-left md:text-center md:border-l-0 md:pl-0">
|
||||
Achetez vos tickets en ligne, recevez votre QR code et présentez-le à l'entrée.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-center gap-6">
|
||||
<a href="#events" class="group relative px-10 py-6 bg-indigo-600 text-white font-black uppercase tracking-widest border-4 border-gray-900 shadow-[8px_8px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-2 hover:translate-y-2 transition-all">
|
||||
Voir les événements
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# ── MARQUEE ── #}
|
||||
<div class="bg-gray-900 py-6 border-b-4 border-indigo-600 overflow-hidden">
|
||||
<div class="flex whitespace-nowrap animate-marquee">
|
||||
{% for i in 1..8 %}
|
||||
<span class="text-white font-black uppercase mx-8 text-2xl opacity-80">
|
||||
Ticket numérique // Scan QR // Paiement sécurisé // Associations
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── FEATURES ── #}
|
||||
<section class="bg-gray-50 py-24 px-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<h2 class="text-5xl md:text-7xl font-black uppercase tracking-tighter mb-20 text-center">
|
||||
Comment ça Marche
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||
<div class="group bg-white border-4 border-gray-900 p-8 shadow-[10px_10px_0px_rgba(0,0,0,1)] hover:shadow-[14px_14px_0px_#4f46e5] transition-all">
|
||||
<div class="w-16 h-16 bg-indigo-600 text-white flex items-center justify-center mb-8 border-2 border-gray-900 text-3xl group-hover:rotate-12 transition-transform">
|
||||
🎟️
|
||||
</div>
|
||||
<h3 class="text-3xl font-black uppercase tracking-tighter mb-4">Choisissez</h3>
|
||||
<p class="text-gray-600 font-bold leading-tight">Sélectionnez votre événement et le nombre de tickets souhaités.</p>
|
||||
</div>
|
||||
|
||||
<div class="group bg-white border-4 border-gray-900 p-8 shadow-[10px_10px_0px_rgba(0,0,0,1)] hover:shadow-[14px_14px_0px_#ef4444] transition-all md:translate-y-8">
|
||||
<div class="w-16 h-16 bg-red-600 text-white flex items-center justify-center mb-8 border-2 border-gray-900 text-3xl group-hover:rotate-12 transition-transform">
|
||||
💳
|
||||
</div>
|
||||
<h3 class="text-3xl font-black uppercase tracking-tighter mb-4">Payez</h3>
|
||||
<p class="text-gray-600 font-bold leading-tight">Paiement sécurisé par carte bancaire via Stripe.</p>
|
||||
</div>
|
||||
|
||||
<div class="group bg-white border-4 border-gray-900 p-8 shadow-[10px_10px_0px_rgba(0,0,0,1)] hover:shadow-[14px_14px_0px_#eab308] transition-all">
|
||||
<div class="w-16 h-16 bg-yellow-500 text-white flex items-center justify-center mb-8 border-2 border-gray-900 text-3xl group-hover:rotate-12 transition-transform">
|
||||
📱
|
||||
</div>
|
||||
<h3 class="text-3xl font-black uppercase tracking-tighter mb-4">Scannez</h3>
|
||||
<p class="text-gray-600 font-bold leading-tight">Recevez votre QR code par email signé et présentez-le à l'entrée.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# ── EVENTS ── #}
|
||||
<section id="events" class="bg-white py-24 px-4 border-t-4 border-gray-900">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="flex flex-col md:flex-row justify-between items-end mb-16 gap-6">
|
||||
<div>
|
||||
<p class="text-indigo-600 font-black uppercase tracking-[0.3em] mb-4">// Prochainement</p>
|
||||
<h2 class="text-5xl md:text-7xl font-black uppercase tracking-tighter leading-none">
|
||||
Événements à Venir
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-4 border-dashed border-gray-300 p-16 text-center">
|
||||
<p class="text-2xl font-black text-gray-300 uppercase">Aucun événement pour le moment</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@keyframes marquee {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-50%); }
|
||||
}
|
||||
.animate-marquee {
|
||||
display: flex;
|
||||
width: 200%;
|
||||
animation: marquee 40s linear infinite;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
12
templates/legal/_layout.html.twig
Normal file
12
templates/legal/_layout.html.twig
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="max-w-4xl mx-auto py-16 px-4">
|
||||
<h1 class="text-5xl md:text-7xl font-black uppercase tracking-tighter mb-12 leading-none">
|
||||
{% block legal_title %}{% endblock %}
|
||||
</h1>
|
||||
<div class="bg-white border-4 border-gray-900 p-8 md:p-12 shadow-[8px_8px_0px_rgba(0,0,0,1)] space-y-6 font-bold text-gray-700 leading-relaxed not-italic">
|
||||
{% block legal_content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
18
templates/legal/cgu.html.twig
Normal file
18
templates/legal/cgu.html.twig
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends 'legal/_layout.html.twig' %}
|
||||
|
||||
{% block title %}CGU - E-Cosplay Ticket{% endblock %}
|
||||
{% block legal_title %}Conditions Générales d'Utilisation{% endblock %}
|
||||
|
||||
{% block legal_content %}
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900">Article 1 — Objet et Promotion du Site</h2>
|
||||
<p>E-Cosplay Ticket est une plateforme de billetterie en ligne destinée aux associations, proposée à tarifs réduits. Les présentes CGU régissent l'utilisation de cette plateforme, éditée par l'association E-Cosplay.</p>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Article 2 — Accès au service</h2>
|
||||
<p>La plateforme est accessible gratuitement à tout utilisateur disposant d'un accès internet. L'achat de tickets nécessite la création d'un compte.</p>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Article 3 — Responsabilités</h2>
|
||||
<p>L'association E-Cosplay s'efforce d'assurer la disponibilité du service mais ne saurait être tenue responsable en cas d'interruption temporaire.</p>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Article 4 — Données personnelles</h2>
|
||||
<p>Voir notre <a href="{{ path('app_rgpd') }}" class="text-indigo-600 underline font-black">Politique RGPD</a>.</p>
|
||||
{% endblock %}
|
||||
18
templates/legal/cgv.html.twig
Normal file
18
templates/legal/cgv.html.twig
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends 'legal/_layout.html.twig' %}
|
||||
|
||||
{% block title %}CGV - E-Cosplay Ticket{% endblock %}
|
||||
{% block legal_title %}Conditions Générales de Vente{% endblock %}
|
||||
|
||||
{% block legal_content %}
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900">Article 1 — Objet</h2>
|
||||
<p>Les présentes CGV régissent la vente de tickets pour les événements organisés par l'association E-Cosplay via la plateforme E-Cosplay Ticket.</p>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Article 2 — Prix et paiement</h2>
|
||||
<p>Les prix sont indiqués en euros TTC. Le paiement est effectué en ligne par carte bancaire via la plateforme sécurisée Stripe.</p>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Article 3 — Tickets</h2>
|
||||
<p>Après paiement, un ticket numérique avec QR code unique est envoyé par email. Ce ticket est nominatif et non cessible sauf accord de l'organisateur.</p>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Article 4 — Annulation et remboursement</h2>
|
||||
<p>Toute demande d'annulation doit être adressée à contact@e-cosplay.fr. Les remboursements sont étudiés au cas par cas selon les conditions de l'événement.</p>
|
||||
{% endblock %}
|
||||
18
templates/legal/cookies.html.twig
Normal file
18
templates/legal/cookies.html.twig
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends 'legal/_layout.html.twig' %}
|
||||
|
||||
{% block title %}Politique de Cookies - E-Cosplay Ticket{% endblock %}
|
||||
{% block legal_title %}Politique de Cookies{% endblock %}
|
||||
|
||||
{% block legal_content %}
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900">Qu'est-ce qu'un cookie ?</h2>
|
||||
<p>Un cookie est un petit fichier texte déposé sur votre navigateur lors de votre visite. Il permet de mémoriser vos préférences et d'assurer le bon fonctionnement du site.</p>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Cookies utilisés</h2>
|
||||
<ul class="list-disc pl-6 space-y-2">
|
||||
<li><strong>Cookies essentiels :</strong> session utilisateur, jeton CSRF (sécurité des formulaires).</li>
|
||||
<li><strong>Cookies de performance :</strong> mesure d'audience anonymisée.</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Gestion des cookies</h2>
|
||||
<p>Vous pouvez configurer votre navigateur pour refuser les cookies. La désactivation des cookies essentiels peut affecter le fonctionnement du site.</p>
|
||||
{% endblock %}
|
||||
14
templates/legal/hosting.html.twig
Normal file
14
templates/legal/hosting.html.twig
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends 'legal/_layout.html.twig' %}
|
||||
|
||||
{% block title %}Hébergement - E-Cosplay Ticket{% endblock %}
|
||||
{% block legal_title %}Hébergement{% endblock %}
|
||||
|
||||
{% block legal_content %}
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900">Hébergeur du site</h2>
|
||||
<p>Ce site est hébergé par :<br>
|
||||
SARL SITECONSEIL<br>
|
||||
<a href="https://www.siteconseil.fr" target="_blank" class="text-indigo-600 underline font-black">www.siteconseil.fr</a></p>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Infrastructure</h2>
|
||||
<p>Les serveurs sont situés en France et conformes aux exigences du RGPD en matière de protection des données personnelles.</p>
|
||||
{% endblock %}
|
||||
151
templates/legal/mentions_legales.html.twig
Normal file
151
templates/legal/mentions_legales.html.twig
Normal file
@@ -0,0 +1,151 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Mentions Légales - E-Cosplay Ticket{% endblock %}
|
||||
{% block meta_description %}Mentions Légales de la plateforme E-Cosplay Ticket{% endblock %}
|
||||
|
||||
{% block structured_data %}
|
||||
{{ parent() }}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{ "@type": "ListItem", "position": 1, "name": "Accueil", "item": "{{ url('app_home') }}" },
|
||||
{ "@type": "ListItem", "position": 2, "name": "Mentions Légales", "item": "{{ url('app_mentions_legales') }}" }
|
||||
]
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="max-w-6xl mx-auto px-4 py-12 text-gray-800">
|
||||
|
||||
{# ── Header ── #}
|
||||
<header class="mb-20 relative py-10">
|
||||
<div class="relative z-10">
|
||||
<h1 class="text-5xl md:text-7xl font-black text-gray-900 tracking-tighter uppercase italic leading-none">
|
||||
Mentions Légales
|
||||
</h1>
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<div class="h-2 w-24 bg-yellow-500 skew-x-[-20deg]"></div>
|
||||
<div class="h-2 w-12 bg-gray-900 skew-x-[-20deg]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute top-0 right-0 -mr-8 opacity-[0.03] pointer-events-none select-none hidden md:block">
|
||||
<span class="text-[10rem] font-black italic uppercase tracking-tighter leading-none">LEGAL</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="space-y-20">
|
||||
|
||||
{# ── 1. Objet ── #}
|
||||
<section class="relative">
|
||||
<div class="inline-block bg-[#0f172a] text-yellow-500 px-6 py-3 font-black italic skew-x-[-15deg] mb-6 shadow-xl border-b-2 border-yellow-600">
|
||||
<span class="skew-x-[15deg] block uppercase tracking-wider text-lg">// 1. Objet et Promotion du Site</span>
|
||||
</div>
|
||||
<div class="bg-white border-2 border-gray-900 p-8 rounded-lg shadow-[8px_8px_0px_rgba(0,0,0,1)] italic">
|
||||
<p class="mb-4 text-gray-900 font-bold">E-Cosplay Ticket est une plateforme de billetterie en ligne destinée aux associations, proposée à tarifs réduits. Elle a pour objectif de faciliter la gestion, la génération et le contrôle de tickets pour les événements associatifs.</p>
|
||||
<ul class="space-y-2 text-sm text-gray-600">
|
||||
<li class="flex items-center gap-2"><span class="text-yellow-500 font-black">/</span> La génération de tickets numériques avec QR code unique.</li>
|
||||
<li class="flex items-center gap-2"><span class="text-yellow-500 font-black">/</span> Le scan et le contrôle d'accès aux événements.</li>
|
||||
<li class="flex items-center gap-2"><span class="text-yellow-500 font-black">/</span> Le paiement sécurisé via Stripe.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# ── 2. Éditeur ── #}
|
||||
<section class="relative">
|
||||
<div class="inline-block bg-[#0f172a] text-yellow-500 px-6 py-3 font-black italic skew-x-[-15deg] mb-6 shadow-xl border-b-2 border-yellow-600">
|
||||
<span class="skew-x-[15deg] block uppercase tracking-wider text-lg">// 2. Identification de l'Éditeur du Site</span>
|
||||
</div>
|
||||
<div class="bg-gray-900 text-white p-8 rounded-lg border-r-8 border-yellow-500 shadow-xl italic grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="space-y-2 text-sm">
|
||||
<p><strong class="text-yellow-500 uppercase tracking-widest text-xs">Nom de l'Association :</strong> E-Cosplay</p>
|
||||
<p><strong class="text-yellow-500 uppercase tracking-widest text-xs">Numéro RNA :</strong> W022006988</p>
|
||||
<p><strong class="text-yellow-500 uppercase tracking-widest text-xs">SIREN :</strong> 943121517</p>
|
||||
<p><strong class="text-yellow-500 uppercase tracking-widest text-xs">Adresse du siège social :</strong> 42 rue de Saint-Quentin, 02800 Beautor</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-start md:items-end">
|
||||
<p class="mb-2"><strong class="text-yellow-500 uppercase tracking-widest text-xs">Directeur de la publication :</strong></p>
|
||||
<span class="text-xl font-black uppercase text-white">Serreau Jovann</span>
|
||||
<p class="text-xs text-gray-400 mt-1">Directeur en charge de la gestion de la billetterie</p>
|
||||
<a href="mailto:contact@e-cosplay.fr" class="text-yellow-500 font-bold hover:underline mt-2">contact@e-cosplay.fr</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# ── 3 & 4 ── #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
<section>
|
||||
<div class="inline-block bg-[#0f172a] text-yellow-500 px-6 py-2 font-black italic skew-x-[-15deg] mb-6 shadow-lg border-b-2 border-yellow-600">
|
||||
<span class="skew-x-[15deg] block uppercase text-sm">// 3. Hébergement du Site</span>
|
||||
</div>
|
||||
<div class="bg-white border-2 border-gray-900 p-6 rounded-lg shadow-[6px_6px_0px_rgba(0,0,0,1)] italic text-sm">
|
||||
<p class="font-black text-gray-900 uppercase mb-2">Google Cloud Platform</p>
|
||||
<p class="text-gray-600">1600 Amphitheatre Parkway, Mountain View, CA 94043, États-Unis (Siège social)</p>
|
||||
<p class="mt-4 text-[10px] font-mono bg-gray-100 p-2">Google Cloud Netherlands B.V., O'Mahony's Corner, Block R, Spencer Dock, Dublin 1, Irlande.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="inline-block bg-[#0f172a] text-yellow-500 px-6 py-2 font-black italic skew-x-[-15deg] mb-6 shadow-lg border-b-2 border-yellow-600">
|
||||
<span class="skew-x-[15deg] block uppercase text-sm">// 4. Propriété Intellectuelle et Droit à l'Image</span>
|
||||
</div>
|
||||
<div class="bg-white border-2 border-gray-900 p-6 rounded-lg shadow-[6px_6px_0px_rgba(0,0,0,1)] italic text-sm">
|
||||
<p class="text-gray-700 leading-tight mb-2">L'association E-Cosplay est propriétaire des droits de propriété intellectuelle ou détient les droits d'usage sur tous les éléments accessibles sur le site.</p>
|
||||
<p class="font-bold text-red-600 uppercase text-xs tracking-tighter">Toute reproduction, représentation, modification, publication, adaptation de tout ou partie des éléments du site, quel que soit le moyen ou le procédé utilisé, est interdite, sauf autorisation écrite préalable de l'association E-Cosplay.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{# ── 5. RGPD ── #}
|
||||
<section class="relative">
|
||||
<div class="inline-block bg-[#0f172a] text-yellow-500 px-6 py-3 font-black italic skew-x-[-15deg] mb-6 shadow-xl border-b-2 border-yellow-600">
|
||||
<span class="skew-x-[15deg] block uppercase tracking-wider text-lg">// 5. Protection des Données Personnelles (RGPD)</span>
|
||||
</div>
|
||||
<div class="bg-white border-2 border-gray-900 p-8 rounded-lg shadow-[8px_8px_0px_rgba(0,0,0,1)] grid grid-cols-1 md:grid-cols-2 gap-8 italic">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600 mb-4">Conformément au Règlement Général sur la Protection des Données (RGPD), l'association E-Cosplay s'engage à protéger la confidentialité des données personnelles collectées. Pour toute information ou exercice de vos droits Informatique et Libertés sur les traitements de données personnelles, vous pouvez contacter notre Délégué à la Protection des Données (DPO).</p>
|
||||
<a href="mailto:rgpd@e-cosplay.fr" class="inline-block bg-yellow-500 text-gray-900 px-6 py-2 font-black uppercase text-xs skew-x-[-10deg]">
|
||||
<span class="skew-x-[10deg] block">Contact DPO</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-6 border-l-4 border-indigo-600">
|
||||
<p class="text-xs font-black uppercase text-indigo-600 mb-2 tracking-widest">Identifiant DPO</p>
|
||||
<p class="text-xl font-black text-gray-900">DPO-167945</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# ── 6 & 7 ── #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
<section>
|
||||
<div class="inline-block bg-[#0f172a] text-yellow-500 px-6 py-2 font-black italic skew-x-[-15deg] mb-6 shadow-lg border-b-2 border-yellow-600">
|
||||
<span class="skew-x-[15deg] block uppercase text-sm">// 6. Partenariats et Publicité</span>
|
||||
</div>
|
||||
<div class="bg-white border-2 border-gray-900 p-6 rounded-lg shadow-[6px_6px_0px_rgba(0,0,0,1)] italic text-sm text-gray-600">
|
||||
La présence de partenaires sur le site internet de l'association E-Cosplay est le fruit d'une collaboration formalisée.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="inline-block bg-[#0f172a] text-yellow-500 px-6 py-2 font-black italic skew-x-[-15deg] mb-6 shadow-lg border-b-2 border-yellow-600">
|
||||
<span class="skew-x-[15deg] block uppercase text-sm">// 7. Limitations de Responsabilité</span>
|
||||
</div>
|
||||
<div class="bg-white border-2 border-gray-900 p-6 rounded-lg shadow-[6px_6px_0px_rgba(0,0,0,1)] italic text-sm text-gray-600">
|
||||
L'association E-Cosplay ne pourra être tenue responsable des dommages directs et indirects causés au matériel de l'utilisateur, lors de l'accès au site, et résultant soit de l'utilisation d'un matériel ne répondant pas aux spécifications indiquées au point 4, soit de l'apparition d'un bug ou d'une incompatibilité.
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{# ── 8. Droit applicable ── #}
|
||||
<section class="relative">
|
||||
<div class="bg-gray-900 text-white p-8 rounded-lg border-b-8 border-red-600 shadow-xl italic text-center">
|
||||
<h2 class="text-red-500 font-black uppercase tracking-widest mb-4">// 8. Droit applicable et attribution de juridiction</h2>
|
||||
<p class="text-sm font-bold">Tout litige en relation avec l'utilisation du site E-Cosplay Ticket est soumis au droit français. Il est fait attribution exclusive de juridiction aux tribunaux compétents de Laon.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
30
templates/legal/rgpd.html.twig
Normal file
30
templates/legal/rgpd.html.twig
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends 'legal/_layout.html.twig' %}
|
||||
|
||||
{% block title %}Politique RGPD - E-Cosplay Ticket{% endblock %}
|
||||
{% block legal_title %}Politique RGPD{% endblock %}
|
||||
|
||||
{% block legal_content %}
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900">Responsable du traitement</h2>
|
||||
<p>Association E-Cosplay<br>
|
||||
42 Rue de Saint-Quentin, 02800 Beautor, France<br>
|
||||
Email : contact@e-cosplay.fr</p>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Données collectées</h2>
|
||||
<ul class="list-disc pl-6 space-y-2">
|
||||
<li>Nom, prénom, adresse email (inscription et achat de tickets).</li>
|
||||
<li>Données de paiement (traitées par Stripe, non stockées sur nos serveurs).</li>
|
||||
<li>Données de connexion (adresse IP, logs).</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Finalité du traitement</h2>
|
||||
<p>Les données sont collectées pour la gestion des inscriptions, la délivrance des tickets, l'envoi de communications liées aux événements et le respect de nos obligations légales.</p>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Durée de conservation</h2>
|
||||
<p>Les données sont conservées pendant la durée nécessaire aux finalités du traitement, puis supprimées conformément à la réglementation en vigueur.</p>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Vos droits</h2>
|
||||
<p>Conformément au RGPD, vous disposez d'un droit d'accès, de rectification, de suppression et de portabilité de vos données. Vous pouvez exercer ces droits en contactant <a href="mailto:contact@e-cosplay.fr" class="text-indigo-600 underline font-black">contact@e-cosplay.fr</a>.</p>
|
||||
|
||||
<h2 class="text-2xl font-black uppercase text-gray-900 mt-8">Désinscription</h2>
|
||||
<p>Vous pouvez vous désinscrire de nos communications à tout moment via le lien présent dans chaque email ou en nous contactant directement.</p>
|
||||
{% endblock %}
|
||||
40
templates/security/login.html.twig
Normal file
40
templates/security/login.html.twig
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Connexion - E-Cosplay Ticket{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="max-w-md mx-auto py-20 px-4">
|
||||
<h1 class="text-5xl font-black uppercase tracking-tighter mb-12 text-center">Connexion</h1>
|
||||
|
||||
{% if error %}
|
||||
<div class="border-4 border-gray-900 bg-red-400 px-6 py-4 font-black shadow-[4px_4px_0px_rgba(0,0,0,1)] mb-8">
|
||||
{{ error.messageKey|trans(error.messageData, 'security') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="bg-white border-4 border-gray-900 p-8 shadow-[8px_8px_0px_rgba(0,0,0,1)] space-y-6">
|
||||
<div>
|
||||
<label for="email" class="block text-xs font-black uppercase tracking-widest mb-2">Email</label>
|
||||
<input type="email" id="email" name="_username" value="{{ last_username }}" required autofocus
|
||||
class="w-full border-2 border-gray-900 p-3 font-bold focus:bg-indigo-50 outline-none">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-xs font-black uppercase tracking-widest mb-2">Mot de passe</label>
|
||||
<input type="password" id="password" name="_password" required
|
||||
class="w-full border-2 border-gray-900 p-3 font-bold focus:bg-indigo-50 outline-none">
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
|
||||
<button type="submit" class="w-full bg-indigo-600 text-white font-black uppercase tracking-widest py-4 border-4 border-gray-900 shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-1 hover:translate-y-1 transition-all">
|
||||
Se connecter
|
||||
</button>
|
||||
|
||||
<p class="text-center font-bold text-sm">
|
||||
Pas encore de compte ?
|
||||
<a href="{{ path('app_register') }}" class="text-indigo-600 underline font-black">Inscription</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
45
templates/security/register.html.twig
Normal file
45
templates/security/register.html.twig
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Inscription - E-Cosplay Ticket{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="max-w-md mx-auto py-20 px-4">
|
||||
<h1 class="text-5xl font-black uppercase tracking-tighter mb-12 text-center">Inscription</h1>
|
||||
|
||||
<form method="post" class="bg-white border-4 border-gray-900 p-8 shadow-[8px_8px_0px_rgba(0,0,0,1)] space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="first_name" class="block text-xs font-black uppercase tracking-widest mb-2">Prénom</label>
|
||||
<input type="text" id="first_name" name="first_name" required
|
||||
class="w-full border-2 border-gray-900 p-3 font-bold focus:bg-indigo-50 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label for="last_name" class="block text-xs font-black uppercase tracking-widest mb-2">Nom</label>
|
||||
<input type="text" id="last_name" name="last_name" required
|
||||
class="w-full border-2 border-gray-900 p-3 font-bold focus:bg-indigo-50 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-xs font-black uppercase tracking-widest mb-2">Email</label>
|
||||
<input type="email" id="email" name="email" required
|
||||
class="w-full border-2 border-gray-900 p-3 font-bold focus:bg-indigo-50 outline-none">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-xs font-black uppercase tracking-widest mb-2">Mot de passe</label>
|
||||
<input type="password" id="password" name="password" required minlength="8"
|
||||
class="w-full border-2 border-gray-900 p-3 font-bold focus:bg-indigo-50 outline-none">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-yellow-400 text-gray-900 font-black uppercase tracking-widest py-4 border-4 border-gray-900 shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-1 hover:translate-y-1 transition-all">
|
||||
Créer mon compte
|
||||
</button>
|
||||
|
||||
<p class="text-center font-bold text-sm">
|
||||
Déjà un compte ?
|
||||
<a href="{{ path('app_login') }}" class="text-indigo-600 underline font-black">Connexion</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
12
templates/unsubscribe/confirmed.html.twig
Normal file
12
templates/unsubscribe/confirmed.html.twig
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Désinscription confirmée - E-Cosplay Ticket{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<main class="min-h-screen flex items-center justify-center bg-zinc-100">
|
||||
<div class="bg-white rounded-xl shadow p-8 max-w-md w-full text-center">
|
||||
<h1 class="text-2xl font-bold mb-4">Désinscription confirmée</h1>
|
||||
<p class="text-zinc-600">L'adresse <strong>{{ email }}</strong> ne recevra plus nos emails.</p>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
17
templates/unsubscribe/index.html.twig
Normal file
17
templates/unsubscribe/index.html.twig
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Se désinscrire - E-Cosplay Ticket{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<main class="min-h-screen flex items-center justify-center bg-zinc-100">
|
||||
<div class="bg-white rounded-xl shadow p-8 max-w-md w-full text-center">
|
||||
<h1 class="text-2xl font-bold mb-4">Se désinscrire</h1>
|
||||
<p class="text-zinc-600 mb-6">Vous ne souhaitez plus recevoir nos emails à l'adresse <strong>{{ email }}</strong> ?</p>
|
||||
<form method="post" action="{{ path('app_unsubscribe', {token: token}) }}">
|
||||
<button type="submit" class="bg-red-600 hover:bg-red-700 text-white font-semibold py-3 px-6 rounded-lg">
|
||||
Confirmer la désinscription
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
95
vite.config.js
Normal file
95
vite.config.js
Normal file
@@ -0,0 +1,95 @@
|
||||
// vite.config.js
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
import JavaScriptObfuscator from 'rollup-plugin-javascript-obfuscator';
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import viteCompression from 'vite-plugin-compression';
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
root: './assets',
|
||||
base: '/assets/',
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
allowedHosts: ["esyweb.local"],
|
||||
|
||||
port: 5173,
|
||||
|
||||
open: false,
|
||||
|
||||
cors: {
|
||||
origin: ['https://esyweb.local']
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'assets'),
|
||||
},
|
||||
// Extensions de fichiers à résoudre automatiquement
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue', '.scss', '.css'],
|
||||
},
|
||||
build: {
|
||||
outDir: resolve(__dirname, 'public/build'),
|
||||
assetsDir: '',
|
||||
emptyOutDir: true,
|
||||
manifest: true,
|
||||
sourcemap: false,
|
||||
// Minification par défaut : esbuild est déjà très rapide et efficace
|
||||
minify: 'esbuild',
|
||||
cssMinify: 'esbuild',
|
||||
// NOUVEAU : Stratégie de découpage du code pour améliorer le cache client
|
||||
rollupOptions: {
|
||||
input: {
|
||||
app: resolve(__dirname, 'assets/app.js'),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// --- Plugins Vite ---
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
// NOUVEAU: Ajoutez Preact/React/Vue si vous utilisez ces frameworks
|
||||
// preact(),
|
||||
|
||||
// 1. Compression des Assets (Gzip et Brotli)
|
||||
// Crée des fichiers .gz et .br à côté des assets originaux.
|
||||
// Votre serveur web (Nginx/Apache) doit être configuré pour servir ces versions compressées.
|
||||
viteCompression({
|
||||
verbose: true,
|
||||
disable: false,
|
||||
threshold: 10240, // Compresse seulement les fichiers > 10KB
|
||||
algorithm: 'gzip',
|
||||
ext: '.gz',
|
||||
}),
|
||||
viteCompression({
|
||||
verbose: true,
|
||||
disable: false,
|
||||
threshold: 10240,
|
||||
algorithm: 'brotliCompress', // Brotli est plus efficace que Gzip
|
||||
ext: '.br',
|
||||
// Ne génère le brotli que si l'algorithme est disponible (par défaut)
|
||||
deleteOriginFile: false // Important : gardez les fichiers originaux
|
||||
}),
|
||||
|
||||
JavaScriptObfuscator({
|
||||
// Options recommandées pour un bon équilibre entre sécurité et performance
|
||||
compact: true,
|
||||
sourceMap: false,
|
||||
controlFlowFlattening: true, // Très efficace mais coûteux en performance à l'exécution
|
||||
deadCodeInjection: false,
|
||||
debugProtection: false,
|
||||
identifierNamesGenerator: 'hexadecimal', // Rend le code illisible
|
||||
log: false,
|
||||
numbersToExpressions: true,
|
||||
simplify: true,
|
||||
splitStrings: true,
|
||||
stringArray: true,
|
||||
stringArrayThreshold: 0.75,
|
||||
transformObjectKeys: true,
|
||||
unicodeEscapeSequence: false,
|
||||
}),
|
||||
|
||||
],
|
||||
define: {},
|
||||
});
|
||||
Reference in New Issue
Block a user