✨ 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:
@@ -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' => []
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user