Add organizer pages, SEO breadcrumbs, Open Graph, homepage redesign, and infrastructure updates

- Add public organizers list page (/organisateurs) with neo-brutalist card grid, social icons, and logo display
- Add organizer detail page (/organisateur/{id}-{slug}) with company info, SIRET, email, address, social links, and events placeholder
- Add slug-based URLs with 301 redirect on wrong slug, getSlug() method on User entity
- Add "Voir les evenements" button on organizer cards linking to detail page
- Add JSON-LD BreadcrumbList to all 17 pages that were missing breadcrumbs (login, forgot_password, register_success, email_verified, legal/*, attestation/*, account/*)
- Add Open Graph meta tags (og:title, og:description, og:image, og:type, og:locale, og:site_name) in base.html.twig with automatic inheritance from title/description blocks
- Add og:image with organizer logo on detail page
- Update sitemap: add /organisateurs to sitemap-main, generate organizer detail URLs in sitemap-orgas with logo images
- Update navbar to highlight "Organisateurs" on detail pages
- Redesign homepage with hero section, marquee, stats counters, how-it-works, and CTA sections
- Add Tailwind v4 @source "../templates" directive to app.scss and admin.scss
- Migrate Flysystem from S3 to local storage (uploads/events, uploads/logos)
- Update Liip Imagine config with FormatExtensionResolver for webp conversion
- Add User entity social fields (website, facebook, instagram, twitter, tiktok), logo upload (Vich), __serialize/__unserialize for session safety
- Add account page settings tab with profile, logo upload, and social media for organizers
- Add Stripe Connect status display and sub-account management in account page
- Delete WebpExtensionSubscriber (replaced by FormatExtensionResolver)
- Add migration for social fields and logo columns
- Add deploy.yml chmod tasks for uploads directories
- Add HomeController tests (detail success, wrong slug redirect, 404 cases)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-20 10:44:31 +01:00
parent da0ddf639b
commit 198d684fb8
26 changed files with 1018 additions and 223 deletions

View File

@@ -220,6 +220,25 @@
args:
chdir: /var/www/e-ticket
- name: Ensure uploads directories exist with correct permissions
file:
path: "/var/www/e-ticket/public/uploads/{{ item }}"
state: directory
owner: "1000"
group: "1000"
mode: "0755"
recurse: true
loop:
- logos
- name: Ensure var/payouts directory exists
file:
path: /var/www/e-ticket/var/payouts
state: directory
owner: "1000"
group: "1000"
mode: "0755"
- name: Ensure Caddy sites directory exists
file:
path: /etc/caddy/sites

View File

@@ -1 +1,2 @@
@import "tailwindcss";
@source "../templates";

View File

@@ -1,2 +1,3 @@
@import "tailwindcss";
@source "../templates";
@import 'https://fonts.googleapis.com/css2?family=Intel+One+Mono:ital,wght@0,300..700;1,300..700&display=swap';

View File

@@ -1,11 +1,9 @@
flysystem:
storages:
default.storage:
adapter: 'aws'
adapter: 'local'
options:
client: 's3_client'
bucket: '%env(S3_BUCKET)%'
prefix: 'uploads'
directory: '%kernel.project_dir%/public/uploads/events'
logos.storage:
adapter: 'local'

View File

@@ -2,6 +2,11 @@ liip_imagine:
driver: imagick
twig:
mode: lazy
cache: format_web_path
resolvers:
web_path:
web_path: ~
webp:
generate: true
@@ -13,7 +18,6 @@ liip_imagine:
format: webp
filters:
thumbnail: { size: [200, 72], mode: inset }
format: { format: 'webp' }
thumbnail:
quality: 80
@@ -21,25 +25,21 @@ liip_imagine:
filters:
thumbnail: { size: [300, 300], mode: inset }
background: { size: [300, 300], position: center, color: '#ffffff' }
format: { format: 'webp' }
medium:
quality: 85
format: webp
filters:
thumbnail: { size: [600, 600], mode: inset }
format: { format: 'webp' }
large:
quality: 90
format: webp
filters:
thumbnail: { size: [1200, 1200], mode: inset }
format: { format: 'webp' }
organizer_logo:
quality: 85
format: webp
filters:
thumbnail: { size: [400, 400], mode: inset }
format: { format: 'webp' }

View File

@@ -6,7 +6,7 @@ vich_uploader:
mappings:
event_image:
uri_prefix: '%env(S3_ENDPOINT)%/%env(S3_BUCKET)%/uploads/events'
uri_prefix: /uploads/events
upload_destination: default.storage
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
directory_namer:

View File

@@ -1782,6 +1782,37 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* }>,
* },
* }
* @psalm-type KnpuOauth2ClientConfig = array{
* http_client?: scalar|Param|null, // Service id of HTTP client to use (must implement GuzzleHttp\ClientInterface) // Default: null
* http_client_options?: array{
* timeout?: int|Param,
* proxy?: scalar|Param|null,
* verify?: bool|Param, // Use only with proxy option set
* },
* clients?: array<string, array<string, mixed>>,
* }
* @psalm-type KnpPaginatorConfig = array{
* default_options?: array{
* sort_field_name?: scalar|Param|null, // Default: "sort"
* sort_direction_name?: scalar|Param|null, // Default: "direction"
* filter_field_name?: scalar|Param|null, // Default: "filterField"
* filter_value_name?: scalar|Param|null, // Default: "filterValue"
* page_name?: scalar|Param|null, // Default: "page"
* distinct?: bool|Param, // Default: true
* page_out_of_range?: scalar|Param|null, // Default: "ignore"
* default_limit?: scalar|Param|null, // Default: 10
* },
* template?: array{
* pagination?: scalar|Param|null, // Default: "@KnpPaginator/Pagination/sliding.html.twig"
* rel_links?: scalar|Param|null, // Default: "@KnpPaginator/Pagination/rel_links.html.twig"
* filtration?: scalar|Param|null, // Default: "@KnpPaginator/Pagination/filtration.html.twig"
* sortable?: scalar|Param|null, // Default: "@KnpPaginator/Pagination/sortable_link.html.twig"
* },
* page_range?: scalar|Param|null, // Default: 5
* page_limit?: scalar|Param|null, // Default: null
* convert_exception?: bool|Param, // Default: false
* remove_first_page_param?: bool|Param, // Default: false
* }
* @psalm-type ConfigType = array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
@@ -1798,6 +1829,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* vich_uploader?: VichUploaderConfig,
* flysystem?: FlysystemConfig,
* nelmio_security?: NelmioSecurityConfig,
* knpu_oauth2_client?: KnpuOauth2ClientConfig,
* knp_paginator?: KnpPaginatorConfig,
* "when@dev"?: array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
@@ -1817,6 +1850,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* vich_uploader?: VichUploaderConfig,
* flysystem?: FlysystemConfig,
* nelmio_security?: NelmioSecurityConfig,
* knpu_oauth2_client?: KnpuOauth2ClientConfig,
* knp_paginator?: KnpPaginatorConfig,
* },
* "when@prod"?: array{
* imports?: ImportsConfig,
@@ -1834,6 +1869,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* vich_uploader?: VichUploaderConfig,
* flysystem?: FlysystemConfig,
* nelmio_security?: NelmioSecurityConfig,
* knpu_oauth2_client?: KnpuOauth2ClientConfig,
* knp_paginator?: KnpPaginatorConfig,
* },
* "when@test"?: array{
* imports?: ImportsConfig,
@@ -1852,6 +1889,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* vich_uploader?: VichUploaderConfig,
* flysystem?: FlysystemConfig,
* nelmio_security?: NelmioSecurityConfig,
* knpu_oauth2_client?: KnpuOauth2ClientConfig,
* knp_paginator?: KnpPaginatorConfig,
* },
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
* imports?: ImportsConfig,

View File

@@ -38,3 +38,11 @@ services:
arguments:
$manifest: '%kernel.project_dir%/public/build/.vite/manifest.json'
liip_imagine.cache.resolver.format_web_path:
class: Liip\ImagineBundle\Imagine\Cache\Resolver\FormatExtensionResolver
arguments:
- '@liip_imagine.cache.resolver.web_path'
- '@liip_imagine.filter.configuration'
tags:
- { name: liip_imagine.cache.resolver, resolver: format_web_path }

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260320085455 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ADD website VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE "user" ADD facebook VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE "user" ADD instagram VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE "user" ADD twitter VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE "user" ADD tiktok VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" DROP website');
$this->addSql('ALTER TABLE "user" DROP facebook');
$this->addSql('ALTER TABLE "user" DROP instagram');
$this->addSql('ALTER TABLE "user" DROP twitter');
$this->addSql('ALTER TABLE "user" DROP tiktok');
}
}

View File

@@ -56,6 +56,10 @@ class AccountController extends AbstractController
'isOrganizer' => $isOrganizer,
'payouts' => $payouts,
'subAccounts' => $subAccounts,
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Mon compte', 'url' => '/mon-compte'],
],
]);
}
@@ -80,6 +84,19 @@ class AccountController extends AbstractController
$user->setCity(trim($request->request->getString('city')));
}
if ($isOrganizer) {
$user->setWebsite(trim($request->request->getString('website')));
$user->setFacebook(trim($request->request->getString('facebook')));
$user->setInstagram(trim($request->request->getString('instagram')));
$user->setTwitter(trim($request->request->getString('twitter')));
$user->setTiktok(trim($request->request->getString('tiktok')));
$logoFile = $request->files->get('logo');
if ($logoFile) {
$user->setLogoFile($logoFile);
}
}
$em->flush();
$this->addFlash('success', 'Parametres mis a jour.');
@@ -212,6 +229,11 @@ class AccountController extends AbstractController
return $this->render('account/edit_subaccount.html.twig', [
'subAccount' => $subAccount,
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Mon compte', 'url' => '/mon-compte'],
['name' => 'Sous-compte', 'url' => '/mon-compte/sous-compte/' . $subAccount->getId()],
],
]);
}

View File

@@ -15,12 +15,20 @@ class AttestationController extends AbstractController
{
$payout = $em->getRepository(Payout::class)->findOneBy(['stripePayoutId' => $stripePayoutId]);
$breadcrumbs = [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Verification attestation', 'url' => '/attestation/check/' . $stripePayoutId],
];
if (!$payout) {
return $this->render('attestation/not_found.html.twig');
return $this->render('attestation/not_found.html.twig', [
'breadcrumbs' => $breadcrumbs,
]);
}
return $this->render('attestation/check.html.twig', [
'payout' => $payout,
'breadcrumbs' => $breadcrumbs,
]);
}
}

View File

@@ -15,6 +15,10 @@ class ForgotPasswordController extends AbstractController
{
private const TEMPLATE = 'security/forgot_password.html.twig';
private const CODE_EXPIRATION_MINUTES = 15;
private const BREADCRUMBS = [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Mot de passe oublie', 'url' => '/mot-de-passe-oublie'],
];
#[Route('/mot-de-passe-oublie', name: 'app_forgot_password', methods: ['GET', 'POST'])]
public function index(
@@ -39,6 +43,7 @@ class ForgotPasswordController extends AbstractController
return $this->render(self::TEMPLATE, [
'step' => 'email',
'email' => '',
'breadcrumbs' => self::BREADCRUMBS,
]);
}
@@ -52,6 +57,7 @@ class ForgotPasswordController extends AbstractController
return $this->render(self::TEMPLATE, [
'step' => 'email',
'email' => $email,
'breadcrumbs' => self::BREADCRUMBS,
]);
}
@@ -83,6 +89,7 @@ class ForgotPasswordController extends AbstractController
return $this->render(self::TEMPLATE, [
'step' => 'code',
'email' => $email,
'breadcrumbs' => self::BREADCRUMBS,
]);
}

View File

@@ -2,6 +2,8 @@
namespace App\Controller;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@@ -9,12 +11,61 @@ use Symfony\Component\Routing\Attribute\Route;
class HomeController extends AbstractController
{
#[Route('/', name: 'app_home')]
public function index(): Response
public function index(EntityManagerInterface $em): Response
{
$allUsers = $em->getRepository(User::class)->findAll();
$organizers = \count(array_filter($allUsers, fn (User $u) => \in_array('ROLE_ORGANIZER', $u->getRoles(), true) && $u->isApproved()));
return $this->render('home/index.html.twig', [
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
],
'stats' => [
'events' => 0,
'organizers' => $organizers,
'tickets' => 0,
],
]);
}
#[Route('/organisateurs', name: 'app_organizers')]
public function organizers(EntityManagerInterface $em): Response
{
$allUsers = $em->getRepository(User::class)->findAll();
$organizers = array_filter($allUsers, fn (User $u) => \in_array('ROLE_ORGANIZER', $u->getRoles(), true) && $u->isApproved());
return $this->render('home/organizers.html.twig', [
'organizers' => $organizers,
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Organisateurs', 'url' => '/organisateurs'],
],
]);
}
#[Route('/organisateur/{id}-{slug}', name: 'app_organizer_detail', requirements: ['id' => '\d+', 'slug' => '[a-z0-9-]+'])]
public function organizerDetail(int $id, string $slug, EntityManagerInterface $em): Response
{
$organizer = $em->getRepository(User::class)->find($id);
if (!$organizer || !\in_array('ROLE_ORGANIZER', $organizer->getRoles(), true) || !$organizer->isApproved()) {
throw $this->createNotFoundException('Organisateur introuvable.');
}
if ($slug !== $organizer->getSlug()) {
return $this->redirectToRoute('app_organizer_detail', [
'id' => $organizer->getId(),
'slug' => $organizer->getSlug(),
], 301);
}
return $this->render('home/organizer_detail.html.twig', [
'organizer' => $organizer,
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Organisateurs', 'url' => '/organisateurs'],
['name' => $organizer->getCompanyName() ?? $organizer->getFirstName() . ' ' . $organizer->getLastName(), 'url' => '/organisateur/' . $organizer->getId() . '-' . $organizer->getSlug()],
],
]);
}

View File

@@ -11,42 +11,77 @@ class LegalController extends AbstractController
#[Route('/mentions-legales', name: 'app_mentions_legales')]
public function mentionsLegales(): Response
{
return $this->render('legal/mentions_legales.html.twig');
return $this->render('legal/mentions_legales.html.twig', [
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Mentions legales', 'url' => '/mentions-legales'],
],
]);
}
#[Route('/cgu', name: 'app_cgu')]
public function cgu(): Response
{
return $this->render('legal/cgu.html.twig');
return $this->render('legal/cgu.html.twig', [
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'CGU', 'url' => '/cgu'],
],
]);
}
#[Route('/cgv', name: 'app_cgv')]
public function cgv(): Response
{
return $this->render('legal/cgv.html.twig');
return $this->render('legal/cgv.html.twig', [
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'CGV', 'url' => '/cgv'],
],
]);
}
#[Route('/hebergement', name: 'app_hosting')]
public function hosting(): Response
{
return $this->render('legal/hosting.html.twig');
return $this->render('legal/hosting.html.twig', [
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Hebergement', 'url' => '/hebergement'],
],
]);
}
#[Route('/cookies', name: 'app_cookies')]
public function cookies(): Response
{
return $this->render('legal/cookies.html.twig');
return $this->render('legal/cookies.html.twig', [
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Politique de cookies', 'url' => '/cookies'],
],
]);
}
#[Route('/rgpd', name: 'app_rgpd')]
public function rgpd(): Response
{
return $this->render('legal/rgpd.html.twig');
return $this->render('legal/rgpd.html.twig', [
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Politique RGPD', 'url' => '/rgpd'],
],
]);
}
#[Route('/conformite', name: 'app_conformite')]
public function conformite(): Response
{
return $this->render('legal/conformite.html.twig');
return $this->render('legal/conformite.html.twig', [
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Conformite', 'url' => '/conformite'],
],
]);
}
}

View File

@@ -74,6 +74,11 @@ class RegistrationController extends AbstractController
return $this->render('security/register_success.html.twig', [
'isOrganizer' => 'organizer' === $type,
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Inscription', 'url' => '/inscription'],
['name' => 'Compte cree', 'url' => '/inscription'],
],
]);
}
@@ -147,6 +152,10 @@ class RegistrationController extends AbstractController
return $this->render('security/email_verified.html.twig', [
'isOrganizer' => \in_array('ROLE_ORGANIZER', $user->getRoles(), true),
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Email verifie', 'url' => '/verification-email/' . $token],
],
]);
}
}

View File

@@ -19,6 +19,10 @@ class SecurityController extends AbstractController
return $this->render('security/login.html.twig', [
'last_username' => $authenticationUtils->getLastUsername(),
'error' => $authenticationUtils->getLastAuthenticationError(),
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Connexion', 'url' => '/connexion'],
],
]);
}

View File

@@ -2,7 +2,10 @@
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\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@@ -13,6 +16,11 @@ class SitemapController extends AbstractController
private const CONTENT_TYPE_XML = 'text/xml';
private const URLSET_TEMPLATE = 'sitemap/urlset.xml.twig';
public function __construct(
private readonly EntityManagerInterface $em,
) {
}
#[Route('/sitemap.xml', name: 'app_sitemap', methods: ['GET'])]
public function index(): Response
{
@@ -44,8 +52,9 @@ class SitemapController extends AbstractController
}
#[Route('/sitemap-main.xml', name: 'app_sitemap_main', methods: ['GET'])]
public function main(): Response
public function main(Request $request): Response
{
$baseUrl = $request->getSchemeAndHttpHost();
$urls = [
[
'loc' => $this->generateUrl('app_home', [], UrlGeneratorInterface::ABSOLUTE_URL),
@@ -62,6 +71,15 @@ class SitemapController extends AbstractController
'changefreq' => 'monthly',
'priority' => '0.7',
],
[
'loc' => $this->generateUrl('app_organizers', [], UrlGeneratorInterface::ABSOLUTE_URL),
'changefreq' => 'weekly',
'priority' => '0.8',
'images' => [[
'loc' => $baseUrl . '/logo.png',
'title' => 'E-Ticket - Nos organisateurs',
]],
],
];
return new Response(
@@ -78,9 +96,11 @@ class SitemapController extends AbstractController
}
#[Route('/sitemap-orgas-{page}.xml', name: 'app_sitemap_orgas', requirements: ['page' => '\d+'], methods: ['GET'])]
public function orgas(int $page = 1): Response
public function orgas(Request $request, int $page = 1): Response
{
return $this->renderUrlset($this->getOrgaUrls());
$baseUrl = $request->getSchemeAndHttpHost();
return $this->renderUrlset($this->getOrgaUrls($baseUrl));
}
/**
@@ -106,8 +126,29 @@ class SitemapController extends AbstractController
/**
* @return list<array<string, mixed>>
*/
private function getOrgaUrls(): array
private function getOrgaUrls(string $baseUrl): array
{
return [];
$allUsers = $this->em->getRepository(User::class)->findAll();
$organizers = array_filter($allUsers, fn (User $u) => \in_array('ROLE_ORGANIZER', $u->getRoles(), true) && $u->isApproved());
$urls = [];
foreach ($organizers as $organizer) {
$url = [
'loc' => $this->generateUrl('app_organizer_detail', ['id' => $organizer->getId(), 'slug' => $organizer->getSlug()], UrlGeneratorInterface::ABSOLUTE_URL),
'changefreq' => 'weekly',
'priority' => '0.6',
];
if ($organizer->getLogoName()) {
$url['images'] = [[
'loc' => $baseUrl . '/uploads/logos/' . $organizer->getLogoName(),
'title' => $organizer->getCompanyName() ?? $organizer->getFirstName() . ' ' . $organizer->getLastName(),
]];
}
$urls[] = $url;
}
return $urls;
}
}

View File

@@ -58,6 +58,21 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column(length: 20, nullable: true)]
private ?string $phone = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $website = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $facebook = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $instagram = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $twitter = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $tiktok = null;
#[Vich\UploadableField(mapping: 'organizer_logo', fileNameProperty: 'logoName')]
private ?File $logoFile = null;
@@ -271,6 +286,66 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this;
}
public function getWebsite(): ?string
{
return $this->website;
}
public function setWebsite(?string $website): static
{
$this->website = $website;
return $this;
}
public function getFacebook(): ?string
{
return $this->facebook;
}
public function setFacebook(?string $facebook): static
{
$this->facebook = $facebook;
return $this;
}
public function getInstagram(): ?string
{
return $this->instagram;
}
public function setInstagram(?string $instagram): static
{
$this->instagram = $instagram;
return $this;
}
public function getTwitter(): ?string
{
return $this->twitter;
}
public function setTwitter(?string $twitter): static
{
$this->twitter = $twitter;
return $this;
}
public function getTiktok(): ?string
{
return $this->tiktok;
}
public function setTiktok(?string $tiktok): static
{
$this->tiktok = $tiktok;
return $this;
}
public function getLogoFile(): ?File
{
return $this->logoFile;
@@ -499,4 +574,39 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
{
// Required by UserInterface — no temporary credentials to clear
}
public function getSlug(): string
{
$name = $this->companyName ?? $this->firstName . ' ' . $this->lastName;
$slug = mb_strtolower(trim($name));
$slug = transliterator_transliterate('Any-Latin; Latin-ASCII', $slug) ?: $slug;
$slug = (string) preg_replace('/[^a-z0-9]+/', '-', $slug);
$slug = trim($slug, '-');
return '' === $slug ? 'organisateur' : $slug;
}
/**
* @return array<string, mixed>
*/
public function __serialize(): array
{
return [
'id' => $this->id,
'email' => $this->email,
'password' => $this->password,
'roles' => $this->roles,
];
}
/**
* @param array<string, mixed> $data
*/
public function __unserialize(array $data): void
{
$this->id = $data['id'] ?? null;
$this->email = $data['email'] ?? null;
$this->password = $data['password'] ?? null;
$this->roles = $data['roles'] ?? [];
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace App\EventSubscriber;
use Liip\ImagineBundle\Events\CacheResolveEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class WebpExtensionSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
'liip_imagine.post_resolve' => 'onPostResolve',
];
}
public function onPostResolve(CacheResolveEvent $event): void
{
$url = $event->getUrl();
if (null === $url) {
return;
}
$event->setUrl(preg_replace('/\.(png|jpg|jpeg|gif|bmp|tiff)$/i', '.webp', $url));
}
}

View File

@@ -279,7 +279,7 @@
{% elseif tab == 'settings' %}
<div style="border:4px solid #111827;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;padding:1.5rem;">
<h2 class="text-sm font-black uppercase tracking-widest" style="margin-bottom:1rem;">Parametres du compte</h2>
<form method="post" action="{{ path('app_account_settings') }}" style="display:flex;flex-direction:column;gap:1.5rem;">
<form method="post" action="{{ path('app_account_settings') }}" enctype="multipart/form-data" style="display:flex;flex-direction:column;gap:1.5rem;">
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;">
<div style="flex:1;min-width:200px;">
<label for="settings_last_name" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.5rem;" class="font-black uppercase text-gray-400">Nom</label>
@@ -329,6 +329,58 @@
{% endif %}
{% if isOrganizer %}
<div style="border:4px solid #111827;background:#fabf04;padding:1.5rem;">
<h3 style="font-size:10px;letter-spacing:0.1em;margin-bottom:1rem;" class="font-black uppercase">Logo de l'organisation</h3>
{% if app.user.logoName %}
<div style="margin-bottom:1rem;">
<img src="{{ ('/uploads/logos/' ~ app.user.logoName) | imagine_filter('organizer_logo') }}" alt="Logo" style="max-height:80px;border:2px solid #111827;">
</div>
{% endif %}
<input type="file" name="logo" accept="image/png,image/jpeg,image/webp"
style="padding:0.5rem;border:2px solid #111827;background:white;font-weight:700;width:100%;">
<p class="text-xs font-bold" style="margin-top:0.5rem;">PNG, JPEG ou WebP. Max 2 Mo.</p>
</div>
<div style="border:4px solid #111827;background:white;padding:1.5rem;">
<h3 style="font-size:10px;letter-spacing:0.1em;margin-bottom:1rem;" class="font-black uppercase text-gray-400">Reseaux sociaux & site internet</h3>
<div style="display:flex;flex-direction:column;gap:1rem;">
<div>
<label for="settings_website" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Site internet</label>
<input type="url" id="settings_website" name="website" value="{{ app.user.website ?? '' }}"
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;"
placeholder="https://mon-association.fr">
</div>
<div style="display:flex;flex-wrap:wrap;gap:1rem;">
<div style="flex:1;min-width:200px;">
<label for="settings_facebook" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Facebook</label>
<input type="url" id="settings_facebook" name="facebook" value="{{ app.user.facebook ?? '' }}"
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;"
placeholder="https://facebook.com/...">
</div>
<div style="flex:1;min-width:200px;">
<label for="settings_instagram" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Instagram</label>
<input type="url" id="settings_instagram" name="instagram" value="{{ app.user.instagram ?? '' }}"
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;"
placeholder="https://instagram.com/...">
</div>
</div>
<div style="display:flex;flex-wrap:wrap;gap:1rem;">
<div style="flex:1;min-width:200px;">
<label for="settings_twitter" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">X (Twitter)</label>
<input type="url" id="settings_twitter" name="twitter" value="{{ app.user.twitter ?? '' }}"
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;"
placeholder="https://x.com/...">
</div>
<div style="flex:1;min-width:200px;">
<label for="settings_tiktok" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">TikTok</label>
<input type="url" id="settings_tiktok" name="tiktok" value="{{ app.user.tiktok ?? '' }}"
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;"
placeholder="https://tiktok.com/@...">
</div>
</div>
</div>
</div>
<div style="border:4px solid #111827;background:#f9fafb;padding:1rem 1.5rem;">
<p class="text-xs font-bold text-gray-500">Les informations de votre organisation (raison sociale, SIRET, adresse) ne peuvent etre modifiees que par l'equipe E-Ticket. Contactez <a href="mailto:contact@e-cosplay.fr" class="text-indigo-600 hover:underline">contact@e-cosplay.fr</a> pour toute modification.</p>
</div>

View File

@@ -55,14 +55,24 @@
{
"@type": "ListItem",
"position": {{ loop.index }},
"name": "{{ breadcrumb.name }}",
"item": "{{ breadcrumb.url }}"
"name": "{{ breadcrumb.name }}"{% if breadcrumb.url is defined and breadcrumb.url is not empty %},
"item": "{{ breadcrumb.url }}"{% endif %}
}{% if not loop.last %},{% endif %}
{% endfor %}
]
}
</script>
{% endif %}
{% block og %}
<meta property="og:title" content="{% block og_title %}{{ block('title') }}{% endblock %}">
<meta property="og:description" content="{% block og_description %}{{ block('description') }}{% endblock %}">
<meta property="og:type" content="{% block og_type %}website{% endblock %}">
<meta property="og:locale" content="fr_FR">
<meta property="og:site_name" content="E-Ticket">
{% block og_image %}
<meta property="og:image" content="https://ticket.e-cosplay.fr/logo.png">
{% endblock %}
{% endblock %}
{% block stylesheets %}{% endblock %}
{% block javascripts %}
{{ vite_asset('app.js') }}
@@ -85,7 +95,9 @@
{% set current_route = app.request.attributes.get('_route') %}
<a href="{{ path('app_home') }}" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all {{ current_route == 'app_home' ? 'bg-yellow-400 border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]' : 'hover:text-indigo-600' }}"><span itemprop="name">Accueil</span></a>
<a href="#" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all hover:text-indigo-600"><span itemprop="name">Evenements</span></a>
<a href="{{ path('app_organizers') }}" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all {{ current_route in ['app_organizers', 'app_organizer_detail'] ? 'bg-yellow-400 border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]' : 'hover:text-indigo-600' }}"><span itemprop="name">Organisateurs</span></a>
<a href="{{ path('app_contact') }}" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all {{ current_route == 'app_contact' ? 'bg-yellow-400 border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]' : 'hover:text-indigo-600' }}"><span itemprop="name">Contact</span></a>
<a href="https://www.e-cosplay.fr" target="_blank" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all hover:text-indigo-600"><span itemprop="name">E-Cosplay</span></a>
</div>
<div class="flex items-center space-x-4 border-l-4 border-gray-900 pl-6 h-full">
@@ -113,7 +125,9 @@
<div class="p-4 space-y-2 uppercase font-black italic">
<a href="{{ path('app_home') }}" class="block p-3 border-2 {{ current_route == 'app_home' ? 'border-gray-900 bg-yellow-400' : 'border-transparent hover:border-gray-900 hover:bg-gray-50' }}" role="menuitem">Accueil</a>
<a href="#" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Evenements</a>
<a href="{{ path('app_organizers') }}" class="block p-3 border-2 {{ current_route in ['app_organizers', 'app_organizer_detail'] ? 'border-gray-900 bg-yellow-400' : 'border-transparent hover:border-gray-900 hover:bg-gray-50' }}" role="menuitem">Organisateurs</a>
<a href="{{ path('app_contact') }}" class="block p-3 border-2 {{ current_route == 'app_contact' ? 'border-gray-900 bg-yellow-400' : 'border-transparent hover:border-gray-900 hover:bg-gray-50' }}" role="menuitem">Contact</a>
<a href="https://www.e-cosplay.fr" target="_blank" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">E-Cosplay</a>
{% if app.user %}
<a href="{{ path('app_account') }}" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Mon espace</a>
{% else %}

View File

@@ -4,107 +4,180 @@
{% block description %}E-Ticket, plateforme de billetterie en ligne pour associations : vente de tickets, reservation de tables, brocantes et vote en ligne.{% endblock %}
{% block body %}
<div class="bg-[#fbfbfb] overflow-x-hidden italic font-sans">
<section style="background:#111827;color:white;padding:5rem 1rem;text-align:center;">
<div style="max-width:50rem;margin:0 auto;">
<h1 class="text-4xl font-black uppercase tracking-tighter italic" style="margin-bottom:1rem;">
La billetterie pensee pour les <span style="color:#fabf04;">associations</span>
<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">ETICKET</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">La billetterie pensee pour les associations</span>
</h1>
<p class="text-lg font-bold" style="opacity:0.8;margin-bottom:2rem;">
<p class="max-w-2xl mx-auto text-xl md:text-3xl font-bold text-gray-600 mb-12 border-l-8 border-yellow-400 pl-6 text-left md:text-center md:border-l-0 md:pl-0">
Vendez vos billets, gerez vos evenements et simplifiez votre organisation. Simple, securise, transparent.
</p>
<div style="display:flex;flex-wrap:wrap;gap:1rem;justify-content:center;">
<a href="{{ path('app_register') }}" style="padding:1rem 2rem;border:3px solid white;background:#fabf04;color:#111827;" class="font-black uppercase text-sm hover:shadow-none transition-all shadow-[4px_4px_0px_rgba(255,255,255,0.3)]">Creer mon evenement</a>
<a href="#acheteur" style="padding:1rem 2rem;border:3px solid white;background:transparent;color:white;" class="font-black uppercase text-sm hover:bg-white hover:text-gray-900 transition-all">Trouver un evenement</a>
<div class="flex flex-col sm:flex-row justify-center gap-6">
<a href="{{ path('app_register') }}" class="group relative px-10 py-6 bg-yellow-400 text-gray-900 font-black uppercase italic 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">
Creer mon evenement
</a>
<a href="{{ path('app_search') }}" class="group relative px-10 py-6 bg-white text-gray-900 font-black uppercase italic 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">
Trouver un evenement
</a>
</div>
</div>
</section>
<section style="max-width:70rem;margin:0 auto;padding:4rem 1rem;">
<div style="text-align:center;margin-bottom:3rem;">
<h2 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;">Comment ca marche ?</h2>
</div>
<div id="acheteur" style="margin-bottom:4rem;">
<h3 class="text-2xl font-black uppercase" style="margin-bottom:1.5rem;color:#4f46e5;">Pour les acheteurs</h3>
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;">
<div style="flex:1;min-width:200px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-4xl font-black" style="color:#fabf04;">1</p>
<p class="font-black uppercase text-sm" style="margin-top:0.5rem;">Choisissez</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Parcourez les evenements et selectionnez vos billets.</p>
</div>
<div style="flex:1;min-width:200px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-4xl font-black" style="color:#4f46e5;">2</p>
<p class="font-black uppercase text-sm" style="margin-top:0.5rem;">Payez en ligne</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Paiement securise par carte bancaire via Stripe.</p>
</div>
<div style="flex:1;min-width:200px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-4xl font-black" style="color:#ec4899;">3</p>
<p class="font-black uppercase text-sm" style="margin-top:0.5rem;">Recevez votre billet</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Billet electronique avec QR Code envoye par email signe.</p>
</div>
<div class="bg-gray-900 py-6 border-b-4 border-yellow-400 overflow-hidden">
<div class="flex whitespace-nowrap animate-marquee italic">
<span class="text-white font-black uppercase mx-8 text-2xl opacity-80">Billets // Brocantes // Reservations // Associations // Evenements // QR Code // Paiement securise</span>
<span class="text-white font-black uppercase mx-8 text-2xl opacity-80">Billets // Brocantes // Reservations // Associations // Evenements // QR Code // Paiement securise</span>
<span class="text-white font-black uppercase mx-8 text-2xl opacity-80">Billets // Brocantes // Reservations // Associations // Evenements // QR Code // Paiement securise</span>
<span class="text-white font-black uppercase mx-8 text-2xl opacity-80">Billets // Brocantes // Reservations // Associations // Evenements // QR Code // Paiement securise</span>
<span class="text-white font-black uppercase mx-8 text-2xl opacity-80">Billets // Brocantes // Reservations // Associations // Evenements // QR Code // Paiement securise</span>
</div>
</div>
<section class="py-16 px-4 bg-white border-b-4 border-gray-900">
<div class="max-w-7xl mx-auto">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-8 text-center">
<div class="group border-4 border-gray-900 p-8 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:shadow-[10px_10px_0px_#fabf04] hover:translate-y-[-4px] transition-all">
<p class="text-6xl md:text-8xl font-black tracking-tighter">{{ stats.events }}</p>
<p class="text-sm font-black uppercase tracking-widest text-gray-500 mt-2">Evenements geres</p>
</div>
<div class="group border-4 border-gray-900 p-8 bg-yellow-400 shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:shadow-[10px_10px_0px_#4f46e5] hover:translate-y-[-4px] transition-all">
<p class="text-6xl md:text-8xl font-black tracking-tighter">{{ stats.organizers }}</p>
<p class="text-sm font-black uppercase tracking-widest mt-2">Organisateurs</p>
</div>
<div class="group border-4 border-gray-900 p-8 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:shadow-[10px_10px_0px_#ec4899] hover:translate-y-[-4px] transition-all">
<p class="text-6xl md:text-8xl font-black tracking-tighter">{{ stats.tickets }}</p>
<p class="text-sm font-black uppercase tracking-widest text-gray-500 mt-2">Billets vendus</p>
<p class="text-xs font-bold text-gray-400 mt-1 italic">Billets, reservations, brocantes, votes</p>
</div>
</div>
</div>
</section>
<section class="py-24 px-4 bg-white border-b-4 border-gray-900">
<div class="max-w-7xl mx-auto items-center">
<div>
<h3 class="text-2xl font-black uppercase" style="margin-bottom:1.5rem;color:#ec4899;">Pour les organisateurs</h3>
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;">
<div style="flex:1;min-width:200px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-4xl font-black" style="color:#fabf04;">1</p>
<p class="font-black uppercase text-sm" style="margin-top:0.5rem;">Creez votre evenement</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Configurez vos billets, tarifs et nombre de places en quelques minutes.</p>
</div>
<div style="flex:1;min-width:200px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-4xl font-black" style="color:#4f46e5;">2</p>
<p class="font-black uppercase text-sm" style="margin-top:0.5rem;">Vendez en ligne</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Partagez le lien de votre evenement. Les paiements sont automatiques.</p>
</div>
<div style="flex:1;min-width:200px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-4xl font-black" style="color:#ec4899;">3</p>
<p class="font-black uppercase text-sm" style="margin-top:0.5rem;">Scannez a l'entree</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Validez les billets QR Code depuis n'importe quel smartphone.</p>
<p class="text-yellow-500 font-black uppercase tracking-[0.3em] mb-4">// Comment ca marche</p>
<h2 class="text-5xl md:text-7xl font-black uppercase tracking-tighter mb-8 leading-none">
Pour les acheteurs
</h2>
<div class="space-y-6 text-xl font-bold text-gray-700 leading-relaxed">
<p class="border-l-8 border-yellow-400 pl-6 italic">Parcourez les evenements, selectionnez vos billets, payez en ligne en toute securite via Stripe et recevez votre billet electronique avec QR Code par email signe.</p>
<p>C'est simple, rapide et securise. Votre billet est disponible instantanement apres le paiement.</p>
</div>
</div>
</div>
</section>
<section style="background:#fabf04;border-top:4px solid #111827;border-bottom:4px solid #111827;padding:4rem 1rem;">
<div style="max-width:70rem;margin:0 auto;">
<div style="text-align:center;margin-bottom:3rem;">
<h2 class="text-3xl font-black uppercase tracking-tighter italic">Pourquoi E-Ticket ?</h2>
<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">
Pour les organisateurs
</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_#fabf04] transition-all">
<div class="w-16 h-16 bg-yellow-400 text-gray-900 flex items-center justify-center mb-8 border-2 border-gray-900 text-3xl font-black group-hover:rotate-12 transition-transform">
1
</div>
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;">
<div style="flex:1;min-width:220px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-2xl font-black" style="margin-bottom:0.5rem;">&#128274;</p>
<p class="font-black uppercase text-sm">Securise</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Paiement Stripe, HTTPS, emails signes S/MIME, protection Cloudflare.</p>
<h3 class="text-3xl font-black uppercase tracking-tighter mb-4">Creez votre evenement</h3>
<p class="text-gray-600 font-bold leading-tight italic">Configurez vos billets, tarifs et nombre de places en quelques minutes. Evenements, brocantes, reservations de tables.</p>
</div>
<div style="flex:1;min-width:220px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-2xl font-black" style="margin-bottom:0.5rem;">&#128176;</p>
<p class="font-black uppercase text-sm">Transparent</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Commission claire a 3%, negociable. Aucun frais cache.</p>
<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 md:translate-y-8">
<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 font-black group-hover:rotate-12 transition-transform">
2
</div>
<div style="flex:1;min-width:220px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-2xl font-black" style="margin-bottom:0.5rem;">&#9889;</p>
<p class="font-black uppercase text-sm">Simple</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Creez un evenement en 5 minutes. Aucune competence technique requise.</p>
<h3 class="text-3xl font-black uppercase tracking-tighter mb-4">Vendez en ligne</h3>
<p class="text-gray-600 font-bold leading-tight italic">Partagez le lien de votre evenement. Les paiements sont automatiques via Stripe Connect. Vous recevez vos virements directement.</p>
</div>
<div style="flex:1;min-width:220px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-2xl font-black" style="margin-bottom:0.5rem;">&#127915;</p>
<p class="font-black uppercase text-sm">Fait pour les assos</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Billets, brocantes, tables, votes. Tout ce dont votre association a besoin.</p>
<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_#ec4899] transition-all">
<div class="w-16 h-16 bg-pink-500 text-white flex items-center justify-center mb-8 border-2 border-gray-900 text-3xl font-black group-hover:rotate-12 transition-transform">
3
</div>
<h3 class="text-3xl font-black uppercase tracking-tighter mb-4">Scannez a l'entree</h3>
<p class="text-gray-600 font-bold leading-tight italic">Validez les billets QR Code depuis n'importe quel smartphone. Gerez les sous-comptes pour votre equipe.</p>
</div>
</div>
</div>
</section>
<section style="max-width:50rem;margin:0 auto;padding:4rem 1rem;text-align:center;">
<h2 class="text-3xl font-black uppercase tracking-tighter italic" style="margin-bottom:1rem;">Pret a lancer votre evenement ?</h2>
<p class="text-lg font-bold text-gray-600" style="margin-bottom:2rem;">Rejoignez E-Ticket et commencez a vendre vos billets des aujourd'hui.</p>
<div style="display:flex;flex-wrap:wrap;gap:1rem;justify-content:center;">
<a href="{{ path('app_register') }}" style="padding:1rem 2rem;border:3px solid #111827;background:#111827;color:white;" class="font-black uppercase text-sm hover:bg-indigo-600 transition-all shadow-[4px_4px_0px_rgba(0,0,0,0.3)]">Creer un compte organisateur</a>
<a href="{{ path('app_tarifs') }}" style="padding:1rem 2rem;border:3px solid #111827;background:white;color:#111827;" class="font-black uppercase text-sm hover:bg-gray-100 transition-all">Voir les tarifs</a>
<section 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-yellow-500 font-black uppercase tracking-[0.3em] mb-4">// Pourquoi E-Ticket</p>
<h2 class="text-5xl md:text-7xl font-black uppercase tracking-tighter leading-none">
Fait pour les associations
</h2>
</div>
<p class="text-xl font-bold text-gray-500 max-w-sm italic">Une plateforme pensee par une association, pour les associations.</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
<div class="group bg-white border-4 border-gray-900 p-8 shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:bg-gray-50 hover:translate-y-[-4px] transition-all">
<div class="text-4xl mb-4">&#128274;</div>
<h3 class="text-2xl font-black uppercase tracking-tighter mb-2">Securise</h3>
<p class="text-gray-600 font-bold leading-tight italic">Paiement Stripe, HTTPS, emails signes S/MIME, protection Cloudflare.</p>
</div>
<div class="group bg-white border-4 border-gray-900 p-8 shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:bg-gray-50 hover:translate-y-[-4px] transition-all">
<div class="text-4xl mb-4">&#128176;</div>
<h3 class="text-2xl font-black uppercase tracking-tighter mb-2">Transparent</h3>
<p class="text-gray-600 font-bold leading-tight italic">Commission claire a 3%, negociable. Aucun frais cache.</p>
</div>
<div class="group bg-white border-4 border-gray-900 p-8 shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:bg-gray-50 hover:translate-y-[-4px] transition-all">
<div class="text-4xl mb-4">&#9889;</div>
<h3 class="text-2xl font-black uppercase tracking-tighter mb-2">Simple</h3>
<p class="text-gray-600 font-bold leading-tight italic">Creez un evenement en 5 minutes. Aucune competence technique requise.</p>
</div>
<div class="group bg-white border-4 border-gray-900 p-8 shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:bg-gray-50 hover:translate-y-[-4px] transition-all">
<div class="text-4xl mb-4">&#127915;</div>
<h3 class="text-2xl font-black uppercase tracking-tighter mb-2">Complet</h3>
<p class="text-gray-600 font-bold leading-tight italic">Billets, brocantes, tables, votes, sous-comptes, virements automatiques.</p>
</div>
</div>
</div>
</section>
<section class="relative py-24 px-4 bg-yellow-400 overflow-hidden">
<div class="absolute inset-0 bg-gray-900 skew-y-2 transform origin-bottom-right translate-y-12"></div>
<div class="max-w-7xl mx-auto relative z-10 flex flex-col lg:flex-row items-center justify-between gap-12">
<div class="text-center lg:text-left text-white">
<h2 class="text-5xl md:text-7xl font-black uppercase tracking-tighter mb-4">
Pret a lancer votre evenement ?
</h2>
<p class="text-2xl font-bold text-yellow-300 uppercase tracking-tighter italic">
Rejoignez E-Ticket et commencez a vendre vos billets des aujourd'hui.
</p>
</div>
<a href="{{ path('app_register') }}" class="group relative px-16 py-8 bg-yellow-400 text-gray-900 font-black uppercase italic tracking-widest text-2xl border-4 border-gray-900 shadow-[10px_10px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-2 hover:translate-y-2 transition-all">
Creer un compte
</a>
</div>
</section>
</div>
<style>
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.animate-marquee {
display: flex;
width: 200%;
animation: marquee 40s linear infinite;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,137 @@
{% extends 'base.html.twig' %}
{% block title %}{{ organizer.companyName ?? organizer.firstName ~ ' ' ~ organizer.lastName }} - E-Ticket{% endblock %}
{% block description %}Decouvrez les evenements de {{ organizer.companyName ?? organizer.firstName ~ ' ' ~ organizer.lastName }} sur E-Ticket{% endblock %}
{% block og_image %}
{% if organizer.logoName %}
<meta property="og:image" content="{{ absolute_url('/uploads/logos/' ~ organizer.logoName) }}">
{% else %}
<meta property="og:image" content="https://ticket.e-cosplay.fr/logo.png">
{% endif %}
{% endblock %}
{% block body %}
<div class="bg-[#fbfbfb] overflow-x-hidden italic font-sans">
<section class="relative bg-white border-b-8 border-gray-900 px-4 pt-20 pb-16">
<div class="absolute inset-0 opacity-[0.03] pointer-events-none select-none overflow-hidden">
<span class="text-[20rem] font-black uppercase leading-none block -rotate-12 translate-y-10">ORGA</span>
</div>
<div class="max-w-4xl mx-auto relative z-10">
<a href="{{ path('app_organizers') }}" class="inline-flex items-center gap-2 text-sm font-black uppercase tracking-widest text-gray-500 hover:text-gray-900 transition-colors mb-8">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M15 19l-7-7 7-7"/></svg>
Retour aux organisateurs
</a>
<div class="flex flex-col md:flex-row items-center md:items-start gap-8">
{% if organizer.logoName %}
<div class="border-4 border-gray-900 flex items-center justify-center overflow-hidden bg-white p-3 shadow-[6px_6px_0px_rgba(0,0,0,1)]">
<img src="{{ ('/uploads/logos/' ~ organizer.logoName) | imagine_filter('organizer_logo') }}" alt="{{ organizer.companyName }}" style="height:140px;width:auto;" class="object-contain">
</div>
{% else %}
<div class="w-32 h-32 bg-yellow-400 border-4 border-gray-900 flex items-center justify-center shadow-[6px_6px_0px_rgba(0,0,0,1)]">
<span class="text-5xl font-black">{{ organizer.firstName|first|upper }}{{ organizer.lastName|first|upper }}</span>
</div>
{% endif %}
<div class="text-center md:text-left">
<h1 class="text-4xl md:text-6xl font-black uppercase tracking-tighter leading-[0.85] mb-4">
{{ organizer.companyName ?? organizer.firstName ~ ' ' ~ organizer.lastName }}
</h1>
{% if organizer.city %}
<p class="text-sm font-black uppercase tracking-widest text-gray-400">
{{ organizer.postalCode }} {{ organizer.city }}
</p>
{% endif %}
{% if organizer.website or organizer.facebook or organizer.instagram or organizer.twitter or organizer.tiktok %}
<div class="flex gap-3 mt-4 flex-wrap justify-center md:justify-start items-center">
{% if organizer.website %}
<a href="{{ organizer.website }}" target="_blank" class="w-9 h-9 flex items-center justify-center border-2 border-gray-900 bg-gray-100 hover:bg-gray-900 hover:text-white transition-all" title="Site internet">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/></svg>
</a>
{% endif %}
{% if organizer.facebook %}
<a href="{{ organizer.facebook }}" target="_blank" class="w-9 h-9 flex items-center justify-center border-2 border-gray-900 bg-[#1877F2] text-white hover:opacity-80 transition-all" title="Facebook">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
</a>
{% endif %}
{% if organizer.instagram %}
<a href="{{ organizer.instagram }}" target="_blank" class="w-9 h-9 flex items-center justify-center border-2 border-gray-900 text-white hover:opacity-80 transition-all" style="background:linear-gradient(45deg,#f09433,#e6683c,#dc2743,#cc2366,#bc1888);" title="Instagram">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>
</a>
{% endif %}
{% if organizer.twitter %}
<a href="{{ organizer.twitter }}" target="_blank" class="w-9 h-9 flex items-center justify-center border-2 border-gray-900 bg-black text-white hover:opacity-80 transition-all" title="X (Twitter)">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</a>
{% endif %}
{% if organizer.tiktok %}
<a href="{{ organizer.tiktok }}" target="_blank" class="w-9 h-9 flex items-center justify-center border-2 border-gray-900 bg-black text-white hover:opacity-80 transition-all" title="TikTok">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/></svg>
</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
</section>
<section class="py-12 px-4">
<div class="max-w-4xl mx-auto">
<div class="bg-white border-4 border-gray-900 shadow-[6px_6px_0px_rgba(0,0,0,1)] overflow-hidden">
<div style="padding:0.75rem 1.5rem;background:#111827;">
<h2 class="text-sm font-black uppercase tracking-widest text-white">Informations</h2>
</div>
<div class="p-6">
<table style="width:100%;border-collapse:collapse;">
<tbody>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.75rem 1rem;" class="font-black text-xs uppercase tracking-widest text-gray-400">Raison sociale</td>
<td style="padding:0.75rem 1rem;" class="text-sm font-bold">{{ organizer.companyName ?? organizer.firstName ~ ' ' ~ organizer.lastName }}</td>
</tr>
{% if organizer.siret %}
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.75rem 1rem;" class="font-black text-xs uppercase tracking-widest text-gray-400">SIRET</td>
<td style="padding:0.75rem 1rem;" class="text-sm font-mono font-bold">{{ organizer.siret }}</td>
</tr>
{% endif %}
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.75rem 1rem;" class="font-black text-xs uppercase tracking-widest text-gray-400">Email</td>
<td style="padding:0.75rem 1rem;" class="text-sm font-bold">
<a href="mailto:{{ organizer.email }}" class="text-indigo-600 hover:underline">{{ organizer.email }}</a>
</td>
</tr>
{% if organizer.city %}
<tr>
<td style="padding:0.75rem 1rem;" class="font-black text-xs uppercase tracking-widest text-gray-400">Adresse</td>
<td style="padding:0.75rem 1rem;" class="text-sm font-bold">
{% if organizer.address %}{{ organizer.address }}, {% endif %}{{ organizer.postalCode }} {{ organizer.city }}
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</section>
<section class="pb-16 px-4">
<div class="max-w-4xl mx-auto">
<div class="bg-white border-4 border-gray-900 shadow-[6px_6px_0px_rgba(0,0,0,1)] overflow-hidden">
<div style="padding:0.75rem 1.5rem;background:#111827;">
<h2 class="text-sm font-black uppercase tracking-widest text-white">Evenements</h2>
</div>
<div class="p-12 text-center">
<p class="text-gray-400 font-black text-lg uppercase">Aucun evenement pour le moment</p>
<p class="text-gray-500 font-bold mt-2 italic text-sm">Les evenements de cet organisateur apparaitront ici.</p>
</div>
</div>
</div>
</section>
</div>
{% endblock %}

View File

@@ -0,0 +1,120 @@
{% extends 'base.html.twig' %}
{% block title %}Organisateurs - E-Ticket{% endblock %}
{% block description %}Decouvrez les organisateurs d'evenements sur E-Ticket{% endblock %}
{% block og_title %}Nos organisateurs - E-Ticket{% endblock %}
{% block og_description %}{{ organizers|length }} organisateur{{ organizers|length > 1 ? 's' : '' }} font confiance a E-Ticket pour gerer leurs evenements.{% endblock %}
{% block body %}
<div class="bg-[#fbfbfb] overflow-x-hidden italic font-sans">
<section class="relative flex items-center justify-center bg-white border-b-8 border-gray-900 px-4 pt-20 pb-16">
<div class="absolute inset-0 opacity-[0.03] pointer-events-none select-none overflow-hidden">
<span class="text-[20rem] font-black uppercase leading-none block -rotate-12 translate-y-10">ORGA</span>
</div>
<div class="max-w-7xl mx-auto relative z-10 text-center">
<h1 class="text-5xl md:text-8xl font-black uppercase tracking-tighter leading-[0.85] mb-4">
<span class="block">Nos organisateurs</span>
</h1>
<p class="max-w-2xl mx-auto text-xl font-bold text-gray-600 italic">
{{ organizers|length }} organisateur{{ organizers|length > 1 ? 's' : '' }} font confiance a E-Ticket pour gerer leurs evenements.
</p>
</div>
</section>
<section class="py-16 px-4">
<div class="max-w-7xl mx-auto">
{% if organizers|length > 0 %}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
{% for orga in organizers %}
<div class="group bg-white border-4 border-gray-900 p-8 flex flex-col items-center shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:bg-gray-50 hover:translate-y-[-4px] transition-all">
{% if orga.logoName %}
<div class="border-4 border-gray-900 flex items-center justify-center mb-6 group-hover:rotate-3 transition-transform overflow-hidden bg-white p-2">
<img src="{{ ('/uploads/logos/' ~ orga.logoName) | imagine_filter('organizer_logo') }}" alt="{{ orga.companyName }}" style="height:120px;width:auto;" class="object-contain">
</div>
{% else %}
<div class="w-24 h-24 bg-yellow-400 border-4 border-gray-900 flex items-center justify-center mb-6 group-hover:rotate-3 transition-transform">
<span class="text-3xl font-black">{{ orga.firstName|first|upper }}{{ orga.lastName|first|upper }}</span>
</div>
{% endif %}
<h2 class="text-2xl font-black uppercase tracking-tighter text-center group-hover:text-indigo-600 transition-colors">
{{ orga.companyName ?? orga.firstName ~ ' ' ~ orga.lastName }}
</h2>
{% if orga.city %}
<p class="text-xs font-black uppercase tracking-widest text-gray-400 mt-2">
{{ orga.postalCode }} {{ orga.city }}
</p>
{% endif %}
{% if orga.website or orga.facebook or orga.instagram or orga.twitter or orga.tiktok %}
<div class="flex gap-3 mt-4 flex-wrap justify-center items-center">
{% if orga.website %}
<a href="{{ orga.website }}" target="_blank" class="w-9 h-9 flex items-center justify-center border-2 border-gray-900 bg-gray-100 hover:bg-gray-900 hover:text-white transition-all" title="Site internet">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/></svg>
</a>
{% endif %}
{% if orga.facebook %}
<a href="{{ orga.facebook }}" target="_blank" class="w-9 h-9 flex items-center justify-center border-2 border-gray-900 bg-[#1877F2] text-white hover:opacity-80 transition-all" title="Facebook">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
</a>
{% endif %}
{% if orga.instagram %}
<a href="{{ orga.instagram }}" target="_blank" class="w-9 h-9 flex items-center justify-center border-2 border-gray-900 text-white hover:opacity-80 transition-all" style="background:linear-gradient(45deg,#f09433,#e6683c,#dc2743,#cc2366,#bc1888);" title="Instagram">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>
</a>
{% endif %}
{% if orga.twitter %}
<a href="{{ orga.twitter }}" target="_blank" class="w-9 h-9 flex items-center justify-center border-2 border-gray-900 bg-black text-white hover:opacity-80 transition-all" title="X (Twitter)">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</a>
{% endif %}
{% if orga.tiktok %}
<a href="{{ orga.tiktok }}" target="_blank" class="w-9 h-9 flex items-center justify-center border-2 border-gray-900 bg-black text-white hover:opacity-80 transition-all" title="TikTok">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/></svg>
</a>
{% endif %}
</div>
{% endif %}
<a href="{{ path('app_organizer_detail', {id: orga.id, slug: orga.slug}) }}" class="mt-6 inline-block px-6 py-3 bg-yellow-400 text-gray-900 font-black uppercase italic text-xs tracking-widest border-3 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">
Voir les evenements
</a>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-16">
<div class="border-4 border-gray-900 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)] p-12 max-w-lg mx-auto">
<p class="text-gray-400 font-black text-lg uppercase">Aucun organisateur pour le moment</p>
<p class="text-gray-500 font-bold mt-2 italic">Soyez le premier a rejoindre E-Ticket !</p>
<a href="{{ path('app_register') }}" class="inline-block mt-6 px-8 py-4 bg-yellow-400 text-gray-900 font-black uppercase italic tracking-widest 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">
Devenir organisateur
</a>
</div>
</div>
{% endif %}
</div>
</section>
<section class="relative py-16 px-4 bg-yellow-400 overflow-hidden">
<div class="absolute inset-0 bg-gray-900 skew-y-2 transform origin-bottom-right translate-y-12"></div>
<div class="max-w-7xl mx-auto relative z-10 flex flex-col lg:flex-row items-center justify-between gap-8">
<div class="text-center lg:text-left text-white">
<h2 class="text-4xl md:text-6xl font-black uppercase tracking-tighter mb-2">
Vous etes une association ?
</h2>
<p class="text-xl font-bold text-yellow-300 uppercase tracking-tighter italic">
Rejoignez E-Ticket et gerez vos evenements simplement.
</p>
</div>
<a href="{{ path('app_register') }}" class="group relative px-12 py-6 bg-yellow-400 text-gray-900 font-black uppercase italic tracking-widest text-xl 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">
Creer un compte
</a>
</div>
</section>
</div>
{% endblock %}

View File

@@ -2,6 +2,8 @@
namespace App\Tests\Controller;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class HomeControllerTest extends WebTestCase
@@ -21,4 +23,117 @@ class HomeControllerTest extends WebTestCase
self::assertResponseIsSuccessful();
}
public function testOrganizersReturnsSuccess(): void
{
$client = static::createClient();
$client->request('GET', '/organisateurs');
self::assertResponseIsSuccessful();
}
public function testOrganizerDetailReturnsSuccess(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$organizer = $em->getRepository(User::class)->findOneBy([]);
$found = false;
if ($organizer) {
foreach ($em->getRepository(User::class)->findAll() as $user) {
if (\in_array('ROLE_ORGANIZER', $user->getRoles(), true) && $user->isApproved()) {
$organizer = $user;
$found = true;
break;
}
}
}
if (!$found) {
$organizer = new User();
$organizer->setEmail('test-orga-detail@example.com');
$organizer->setFirstName('Test');
$organizer->setLastName('Orga');
$organizer->setPassword('hashed');
$organizer->setRoles(['ROLE_ORGANIZER']);
$organizer->setIsApproved(true);
$organizer->setIsVerified(true);
$organizer->setCompanyName('Asso Test');
$organizer->setSiret('12345678901234');
$em->persist($organizer);
$em->flush();
}
$client->request('GET', '/organisateur/' . $organizer->getId() . '-' . $organizer->getSlug());
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('h1', $organizer->getCompanyName() ?? $organizer->getFirstName());
}
public function testOrganizerDetailRedirectsOnWrongSlug(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$organizer = new User();
$organizer->setEmail('test-orga-slug-' . uniqid() . '@example.com');
$organizer->setFirstName('Slug');
$organizer->setLastName('Test');
$organizer->setPassword('hashed');
$organizer->setRoles(['ROLE_ORGANIZER']);
$organizer->setIsApproved(true);
$organizer->setIsVerified(true);
$organizer->setCompanyName('Mon Asso');
$em->persist($organizer);
$em->flush();
$client->request('GET', '/organisateur/' . $organizer->getId() . '-mauvais-slug');
self::assertResponseRedirects('/organisateur/' . $organizer->getId() . '-mon-asso', 301);
}
public function testOrganizerDetailNotFoundReturns404(): void
{
$client = static::createClient();
$client->request('GET', '/organisateur/999999-inexistant');
self::assertResponseStatusCodeSame(404);
}
public function testOrganizerDetailNonApprovedReturns404(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$organizer = new User();
$organizer->setEmail('test-orga-noapprove-' . uniqid() . '@example.com');
$organizer->setFirstName('Test');
$organizer->setLastName('NonApproved');
$organizer->setPassword('hashed');
$organizer->setRoles(['ROLE_ORGANIZER']);
$organizer->setIsApproved(false);
$organizer->setIsVerified(true);
$em->persist($organizer);
$em->flush();
$client->request('GET', '/organisateur/' . $organizer->getId() . '-' . $organizer->getSlug());
self::assertResponseStatusCodeSame(404);
}
public function testOrganizerDetailBuyerReturns404(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$buyer = new User();
$buyer->setEmail('test-buyer-detail-' . uniqid() . '@example.com');
$buyer->setFirstName('Buyer');
$buyer->setLastName('Test');
$buyer->setPassword('hashed');
$buyer->setRoles(['ROLE_USER']);
$buyer->setIsVerified(true);
$em->persist($buyer);
$em->flush();
$client->request('GET', '/organisateur/' . $buyer->getId() . '-' . $buyer->getSlug());
self::assertResponseStatusCodeSame(404);
}
}

View File

@@ -1,81 +0,0 @@
<?php
namespace App\Tests\EventSubscriber;
use App\EventSubscriber\WebpExtensionSubscriber;
use Liip\ImagineBundle\Events\CacheResolveEvent;
use PHPUnit\Framework\TestCase;
class WebpExtensionSubscriberTest extends TestCase
{
private WebpExtensionSubscriber $subscriber;
protected function setUp(): void
{
$this->subscriber = new WebpExtensionSubscriber();
}
public function testGetSubscribedEvents(): void
{
$events = WebpExtensionSubscriber::getSubscribedEvents();
self::assertArrayHasKey('liip_imagine.post_resolve', $events);
self::assertSame('onPostResolve', $events['liip_imagine.post_resolve']);
}
public function testRewritesPngToWebp(): void
{
$event = new CacheResolveEvent('logo.png', 'thumbnail', '/media/cache/thumbnail/logo.png');
$this->subscriber->onPostResolve($event);
self::assertSame('/media/cache/thumbnail/logo.webp', $event->getUrl());
}
public function testRewritesJpgToWebp(): void
{
$event = new CacheResolveEvent('photo.jpg', 'medium', '/media/cache/medium/photo.jpg');
$this->subscriber->onPostResolve($event);
self::assertSame('/media/cache/medium/photo.webp', $event->getUrl());
}
public function testRewritesJpegToWebp(): void
{
$event = new CacheResolveEvent('image.jpeg', 'large', '/media/cache/large/image.jpeg');
$this->subscriber->onPostResolve($event);
self::assertSame('/media/cache/large/image.webp', $event->getUrl());
}
public function testRewritesGifToWebp(): void
{
$event = new CacheResolveEvent('anim.gif', 'thumbnail', '/media/cache/thumbnail/anim.gif');
$this->subscriber->onPostResolve($event);
self::assertSame('/media/cache/thumbnail/anim.webp', $event->getUrl());
}
public function testDoesNotRewriteWebp(): void
{
$event = new CacheResolveEvent('already.webp', 'thumbnail', '/media/cache/thumbnail/already.webp');
$this->subscriber->onPostResolve($event);
self::assertSame('/media/cache/thumbnail/already.webp', $event->getUrl());
}
public function testCaseInsensitive(): void
{
$event = new CacheResolveEvent('logo.PNG', 'thumbnail', '/media/cache/thumbnail/logo.PNG');
$this->subscriber->onPostResolve($event);
self::assertSame('/media/cache/thumbnail/logo.webp', $event->getUrl());
}
public function testNullUrlIsIgnored(): void
{
$event = new CacheResolveEvent('logo.png', 'thumbnail');
$this->subscriber->onPostResolve($event);
self::assertNull($event->getUrl());
}
}