feat(ShopController): Ajoute des routes et templates pour boutique, catégories et produits.

 feat(templates): Implémente la structure de base de la boutique avec catégories et produits.
🐛 fix(ErrorListener): Corrige l'affichage des erreurs seulement en prod.
🌐 feat(translations): Ajoute les traductions FR et EN pour la boutique.
This commit is contained in:
Serreau Jovann
2025-11-19 12:48:11 +01:00
parent 158ff8f5a8
commit c16f7433fe
5 changed files with 218 additions and 41 deletions

View File

@@ -21,11 +21,34 @@ use Twig\Environment;
class ShopController extends AbstractController
{
/**
* Simulation des données produits qui seraient normalement récupérées depuis une base de données.
*/
#[Route(path: '/boutique', name: 'app_shop', options: ['sitemap' => false], methods: ['GET'])]
public function index(): Response
{
return $this->render('shop.twig');
// Correction du nom du template de 'shop.twig' à 'shop/index.html.twig'
// et passage des données centralisées.
return $this->render('shop.twig', [
'featuredProducts' => []
]);
}
#[Route(path: '/boutique/categorie/{slug}', name: 'app_shop_category', options: ['sitemap' => false], methods: ['GET'])]
public function indexCategorie(): Response
{
return $this->render('shop.twig', [
'featuredProducts' => []
]);
}
#[Route(path: '/boutique/produit/{slug}', name: 'app_product_show', options: ['sitemap' => false], methods: ['GET'])]
public function indexProductShow(): Response
{
return $this->render('shop.twig', [
'featuredProducts' => []
]);
}
}

View File

@@ -23,21 +23,23 @@ class ErrorListener
public function onException(ExceptionEvent $exceptionEvent) {
$exception = $exceptionEvent->getThrowable();
if($exception instanceof NotFoundHttpException) {
$response = new Response($this->environment->render('error/404.twig',[
'no_index' => true,
]));
$response->setStatusCode(Response::HTTP_NOT_FOUND);
$exceptionEvent->setResponse($response);
$exceptionEvent->stopPropagation();
} else {
$response = new Response($this->environment->render('error/error.twig',[
'no_index' => true,
]));
if($_ENV['APP_ENV'] == "prod") {
if ($exception instanceof NotFoundHttpException) {
$response = new Response($this->environment->render('error/404.twig', [
'no_index' => true,
]));
$response->setStatusCode(Response::HTTP_NOT_FOUND);
$exceptionEvent->setResponse($response);
$exceptionEvent->stopPropagation();
} else {
$response = new Response($this->environment->render('error/error.twig', [
'no_index' => true,
]));
$response->setStatusCode(Response::HTTP_NOT_FOUND);
$exceptionEvent->setResponse($response);
$exceptionEvent->stopPropagation();
$response->setStatusCode(Response::HTTP_NOT_FOUND);
$exceptionEvent->setResponse($response);
$exceptionEvent->stopPropagation();
}
}
}
}

View File

@@ -1,9 +1,10 @@
{% extends 'base.twig' %}
{# --- METADATA & SEO --- #}
{% block title %}{{'shop.title'|trans}}{% endblock %}
{% block meta_description %}{{'shop.description'|trans}}{% endblock %}
{% block canonical_url %}<link rel="canonical" href="{{ url('app_shop') }}" />{% endblock %}
{% block breadcrumb_schema %}
<script type="application/ld+json">
{
@@ -25,41 +26,174 @@
]
}
</script>
{% for product in featuredProducts %}
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "Product",
"name": "{{ product.name }}",
"image": "{{ product.image }}",
"description": "{{ product.short_desc }}",
"sku": "EC-{{ product.id }}",
"brand": {
"@type": "Brand",
"name": "E-COSPLAY"
},
"shippingDetails": {
"@type": "OfferShippingDetails",
"shippingRate": {
"@type": "MonetaryAmount",
"value": 6.00,
"currency": "EUR"
},
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "FR"
},
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 1,
"unitCode": "DAY"
},
"transitTime": {
"@type": "QuantitativeValue",
"minValue": 1,
"maxValue": 5,
"unitCode": "DAY"
}
}
},
"offers": {
"@type": "Offer",
"url": "{{ app.request.schemeAndHttpHost }}{{ path('app_product_show', {'slug': product.name|lower|replace({' ': '-'})}) }}",
"priceCurrency": "EUR",
"price": "{{ product.price }}",
"itemCondition": "https://schema.org/{% if product.state == 'new' %}NewCondition{% else %}UsedCondition{% endif %}",
"availability": "https://schema.org/InStock",
"hasMerchantReturnPolicy": {
"@type": "MerchantReturnPolicy",
"applicableCountry": "FR",
"returnPolicyCategory": "https://schema.org/{% if product.is_handmade %}MerchantReturnNotPermitted {% else %}MerchantReturnFiniteReturnWindow{% endif %}",
"merchantReturnDays": {% if product.is_handmade %}0{%else%}14{% endif %},
"returnFees": "https://schema.org/{% if product.is_handmade %}ReturnFeesCustomerResponsibility{%else%}ReturnFeesCustomerResponsibility{% endif %}",
"returnMethod": "https://schema.org/ReturnByMail"
}
}
}
</script>
{% endfor %}
{% endblock %}
{% block body %}
<div class="container mx-auto p-4 md:p-8 pt-12">
<h1 class="text-4xl font-extrabold text-gray-900 mb-8 text-center">
{{ 'shop.welcome_title'|trans }}
</h1>
<div class="max-w-3xl mx-auto text-center py-16 md:py-24 bg-white rounded-xl shadow-lg border border-gray-100">
{# --- LAYOUT PRINCIPAL : SIDEBAR & CONTENU --- #}
<div class="flex flex-col md:flex-row gap-8">
<span class="text-6xl text-indigo-500 mb-4 inline-block">
{# Icône de Panier/Boutique #}
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shopping-bag"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>
</span>
{# --- 1. SIDEBAR / NAVIGATION DES CATÉGORIES (Mobile: Top, Desktop: Left) --- #}
<aside class="md:w-1/4 bg-white p-6 rounded-xl shadow-lg border border-gray-100 h-fit">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2">
{{ 'shop.categories_title'|trans }}
</h3>
<h1 class="text-4xl font-extrabold text-gray-800 mt-4 mb-3">
{{ 'shop.status_title'|trans }}
</h1>
<nav class="space-y-2">
{% set categories = [
{'key': 'cosplay', 'route': 'app_shop_category', 'icon': ''},
{'key': 'wig', 'route': 'app_shop_category', 'icon': ''},
{'key': 'props', 'route': 'app_shop_category', 'icon': ''},
{'key': 'retouches', 'route': 'app_shop_category', 'icon': ''},
] %}
<p class="text-xl text-gray-600 mb-8">
{{ 'shop.status_message'|trans }}
</p>
{% for category in categories %}
<a href="{{ url(category.route, {'slug': category.key}) }}"
class="flex items-center gap-3 p-3 rounded-lg text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 transition duration-150 group">
{{ category.icon|raw }}
<span class="font-medium">
{{ ('shop.category_' ~ category.key)|trans }}
</span>
</a>
{% endfor %}
</nav>
</aside>
{# Appel à l'action pour revenir ou contacter #}
<div class="flex flex-col sm:flex-row justify-center gap-4 mt-6">
<a href="{{ url('app_home') }}" class="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 transition duration-300 shadow-md">
{{ 'shop.button_home'|trans }}
</a>
<a href="{{ url('app_contact') }}" class="inline-flex items-center justify-center px-6 py-3 border border-gray-300 text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 transition duration-300 shadow-md">
{{ 'shop.button_contact'|trans }}
</a>
</div>
{# --- 2. CONTENU PRINCIPAL / AFFICHAGE DES PRODUITS --- #}
<main class="md:w-3/4">
<p class="text-xl text-gray-600 mb-8">
{{ 'shop.description'|trans }}
</p>
{# Optionnel : Message pour les notifications #}
<p class="text-sm text-gray-500 mt-10 border-t pt-6 max-w-sm mx-auto">
{{ 'shop.status_notification'|trans }}
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{% for product in featuredProducts %}
<div class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-xl transition duration-300 transform hover:-translate-y-1 border border-gray-100 relative">
{# ÉTIQUETTE PROMO (Absolute positioning) #}
{% if product.is_promo %}
<div class="absolute top-2 left-2 bg-red-600 text-white text-xs font-bold px-3 py-1 rounded-full shadow-lg z-10">
{{ 'shop.tag_promo'|trans }}
</div>
{% endif %}
{# Image et Étiquette de Préférence (État) #}
<div class="relative">
<img src="{{ product.image }}" alt="Image de {{ product.name }}" class="w-full h-48 object-cover">
{# Étiquette de préférence (Neuf/Occasion) #}
<div class="absolute bottom-0 right-0 bg-gray-900 text-white text-xs font-semibold px-2 py-1 rounded-tl-lg opacity-80">
{{ ('shop.state_' ~ product.state)|trans }}
</div>
</div>
<div class="p-4">
{# NOM #}
<h3 class="text-xl font-bold text-gray-900 mb-2 truncate">
{{ product.name }}
</h3>
{# TAGS SUPPLÉMENTAIRES #}
<div class="flex gap-2 mb-3 flex-wrap">
{% if product.is_handmade %}
<span class="text-xs font-medium bg-green-100 text-green-800 px-2 py-0.5 rounded-full">
{{ 'shop.tag_handmade'|trans }}
</span>
{% endif %}
{# Exemple de tag "Sur-mesure" basé sur une condition #}
{% if product.id == 1 %}
<span class="text-xs font-medium bg-blue-100 text-blue-800 px-2 py-0.5 rounded-full">
{{ 'shop.tag_custom'|trans }}
</span>
{% endif %}
</div>
{# DESCRIPTION COURTE #}
<p class="text-sm text-gray-600 mb-4 line-clamp-2" title="{{ product.short_desc }}">
{{ product.short_desc }}
</p>
{# PRIX TTC et Bouton #}
<div class="flex justify-between items-center pt-3 border-t border-gray-100">
<span class="text-2xl font-extrabold text-indigo-600">
{{ product.price | number_format(2, ',', ' ') }} € TTC
</span>
<a href="{{ path('app_product_show', {'slug': product.name|lower|replace({' ': '-'})}) }}" class="text-indigo-600 hover:text-indigo-800 text-sm font-semibold inline-flex items-center group">
En savoir plus
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right ml-1 group-hover:translate-x-0.5 transition-transform"><path d="m9 18l6-6-6-6"/></svg>
</a>
</div>
</div>
</div>
{% endfor %}
</div>
</main>
</div>
</div>
{% endblock %}

View File

@@ -619,3 +619,4 @@ error.not_found_description: We are sorry, but the page you are looking for does
error.generic_title: Oops, an error occurred
error.sgeneric_description: We encountered an unexpected problem on the server. Please try again later. If the error persists, contact technical support.
breadcrumb.dons: Dons
Dons: Dons

View File

@@ -466,7 +466,7 @@ home_page.description: "Bienvenue dans la communauté e-cosplay ! Votre référe
members_description: 'Découvrez les membres actifs de notre association de cosplay ! Rencontrez les bénévoles, juges et organisateurs qui donnent vie à nos événements et activités.'
contact_page.description: 'Contactez-nous pour toute question sur le cosplay, les événements ou les partenariats ! Formulaire de contact direct et emails des fondatrices disponibles ici.'
shop.title: "Boutique | En construction"
shop.description: "Découvrez bientôt notre boutique officielle pour les produits dérivés de l'association."
shop.description: "Découvrez bientôt notre boutique officielle de l'association."
breadcrumb.shop: "Boutique"
shop.status_title: "Notre Boutique Arrive Bientôt !"
shop.status_message: "Nous travaillons activement pour préparer notre boutique en ligne. Vous y trouverez des produits exclusifs de l'association."
@@ -557,3 +557,20 @@ error.not_found_description: Nous sommes désolés, mais la page que vous recher
error.generic_title: Oups, une erreur s'est produite
error.generic_description: Nous avons rencontré un problème inattendu sur le serveur. Veuillez réessayer ultérieurement. Si l'erreur persiste, contactez le support technique.
breadcrumb.dons: Dons
Dons: Dons
error:
shop.welcome_title: Bienvenue dans la Boutique E-Cosplay
shop.categories_title: Catégories
shop.category_cosplay: Cosplays Complets
shop.category_wig: Perruques & Extensions
shop.category_props: Accessoires & Props
shop.category_retouches: Modifications & Retouches
shop.product_name: Article de Cosplay
shop.product_short_desc: Prêt à porter, haute qualité, édition limitée.
shop.button_all_products: Voir tous les produits
shop.tag_handmade: Fait-main
shop.tag_custom: Sur-mesure
shop.tag_promo: PROMO
shop.state_new: Neuf
shop.state_used: Occasion