feat(cms): add testimonials, gallery, contact_info section types (3D)

New section partials for hosting templates:
- _testimonials.html: customer review cards with star ratings, avatars
- _gallery.html: responsive image grid with hover captions
- _contact_info.html: phone/email/address cards with icons + hours

Updated renderers:
- Platform homepage-default.html: imports + renders new section types
- Storefront landing-full.html: added section-based rendering path
  that takes over when page.sections is set (POC builder pages),
  falls back to hardcoded HTML layout for non-section pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 22:54:15 +02:00
parent bc951a36d9
commit b3051b423a
5 changed files with 229 additions and 0 deletions

View File

@@ -7,6 +7,9 @@
{% from 'cms/platform/sections/_products.html' import render_products %} {% from 'cms/platform/sections/_products.html' import render_products %}
{% from 'cms/platform/sections/_features.html' import render_features %} {% from 'cms/platform/sections/_features.html' import render_features %}
{% from 'cms/platform/sections/_pricing.html' import render_pricing with context %} {% from 'cms/platform/sections/_pricing.html' import render_pricing with context %}
{% from 'cms/platform/sections/_testimonials.html' import render_testimonials %}
{% from 'cms/platform/sections/_gallery.html' import render_gallery %}
{% from 'cms/platform/sections/_contact_info.html' import render_contact_info %}
{% from 'cms/platform/sections/_cta.html' import render_cta %} {% from 'cms/platform/sections/_cta.html' import render_cta %}
{% block title %} {% block title %}
@@ -51,6 +54,21 @@
{{ render_pricing(page.sections.pricing, lang, default_lang, tiers) }} {{ render_pricing(page.sections.pricing, lang, default_lang, tiers) }}
{% endif %} {% endif %}
{# Testimonials Section #}
{% if page.sections.testimonials %}
{{ render_testimonials(page.sections.testimonials, lang, default_lang) }}
{% endif %}
{# Gallery Section #}
{% if page.sections.gallery %}
{{ render_gallery(page.sections.gallery, lang, default_lang) }}
{% endif %}
{# Contact Info Section #}
{% if page.sections.contact_info %}
{{ render_contact_info(page.sections.contact_info, lang, default_lang) }}
{% endif %}
{# CTA Section #} {# CTA Section #}
{% if page.sections.cta %} {% if page.sections.cta %}
{{ render_cta(page.sections.cta, lang, default_lang) }} {{ render_cta(page.sections.cta, lang, default_lang) }}

View File

@@ -0,0 +1,66 @@
{# Section partial: Contact Information #}
{#
Parameters:
- contact_info: dict with enabled, title, email, phone, address, hours, map_embed_url
- lang: Current language code
- default_lang: Fallback language
#}
{% macro render_contact_info(contact_info, lang, default_lang) %}
{% if contact_info and contact_info.enabled %}
<section class="py-16 lg:py-24 bg-gray-50 dark:bg-gray-900">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
{% set title = contact_info.title.translations.get(lang) or contact_info.title.translations.get(default_lang) or 'Contact' %}
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
{{ title }}
</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-4xl mx-auto">
{% if contact_info.phone %}
<div class="text-center p-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm">
<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
<span class="text-purple-600 dark:text-purple-300 text-xl">&#128222;</span>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-2">Phone</h3>
<a href="tel:{{ contact_info.phone }}" class="text-purple-600 dark:text-purple-400 hover:underline">
{{ contact_info.phone }}
</a>
</div>
{% endif %}
{% if contact_info.email %}
<div class="text-center p-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm">
<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
<span class="text-purple-600 dark:text-purple-300 text-xl">&#128231;</span>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-2">Email</h3>
<a href="mailto:{{ contact_info.email }}" class="text-purple-600 dark:text-purple-400 hover:underline">
{{ contact_info.email }}
</a>
</div>
{% endif %}
{% if contact_info.address %}
<div class="text-center p-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm">
<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
<span class="text-purple-600 dark:text-purple-300 text-xl">&#128205;</span>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-2">Address</h3>
<p class="text-gray-600 dark:text-gray-400">{{ contact_info.address }}</p>
</div>
{% endif %}
</div>
{% if contact_info.hours %}
<div class="mt-8 text-center">
<p class="text-gray-600 dark:text-gray-400">
<span class="font-semibold">Hours:</span> {{ contact_info.hours }}
</p>
</div>
{% endif %}
</div>
</section>
{% endif %}
{% endmacro %}

View File

@@ -0,0 +1,44 @@
{# Section partial: Image Gallery #}
{#
Parameters:
- gallery: dict with enabled, title, images (list of {src, alt, caption})
- lang: Current language code
- default_lang: Fallback language
#}
{% macro render_gallery(gallery, lang, default_lang) %}
{% if gallery and gallery.enabled %}
<section class="py-16 lg:py-24 bg-white dark:bg-gray-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{# Section header #}
<div class="text-center mb-12">
{% set title = gallery.title.translations.get(lang) or gallery.title.translations.get(default_lang) or '' %}
{% if title %}
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
{{ title }}
</h2>
{% endif %}
</div>
{# Image grid #}
{% if gallery.images %}
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{% for image in gallery.images %}
<div class="relative group overflow-hidden rounded-lg aspect-square">
<img src="{{ image.src }}"
alt="{{ image.alt or '' }}"
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
loading="lazy">
{% if image.caption %}
<div class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-4 opacity-0 group-hover:opacity-100 transition-opacity">
<p class="text-sm text-white">{{ image.caption }}</p>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
</div>
</section>
{% endif %}
{% endmacro %}

View File

@@ -0,0 +1,72 @@
{# Section partial: Testimonials #}
{#
Parameters:
- testimonials: dict with enabled, title, subtitle, items
- lang: Current language code
- default_lang: Fallback language
#}
{% macro render_testimonials(testimonials, lang, default_lang) %}
{% if testimonials and testimonials.enabled %}
<section class="py-16 lg:py-24 bg-gray-50 dark:bg-gray-900">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{# Section header #}
<div class="text-center mb-12">
{% set title = testimonials.title.translations.get(lang) or testimonials.title.translations.get(default_lang) or '' %}
{% if title %}
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
{{ title }}
</h2>
{% endif %}
</div>
{# Testimonial cards #}
{% if testimonials.items %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{% for item in testimonials.items %}
<div class="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-sm border border-gray-100 dark:border-gray-700">
<div class="flex items-center mb-4">
<div class="flex text-yellow-400">
{% for _ in range(5) %}
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
{% endfor %}
</div>
</div>
{% set content = item.content %}
{% if content is mapping %}
{% set content = content.translations.get(lang) or content.translations.get(default_lang) or '' %}
{% endif %}
<p class="text-gray-600 dark:text-gray-300 mb-6 italic">"{{ content }}"</p>
<div class="flex items-center">
{% if item.avatar %}
<img src="{{ item.avatar }}" alt="" class="w-10 h-10 rounded-full mr-3">
{% else %}
<div class="w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center mr-3">
<span class="text-sm font-bold text-purple-600 dark:text-purple-300">
{% set author = item.author %}
{% if author is mapping %}{% set author = author.translations.get(lang) or author.translations.get(default_lang) or '?' %}{% endif %}
{{ author[0]|upper if author else '?' }}
</span>
</div>
{% endif %}
<div>
{% set author = item.author %}
{% if author is mapping %}{% set author = author.translations.get(lang) or author.translations.get(default_lang) or '' %}{% endif %}
<p class="text-sm font-semibold text-gray-900 dark:text-white">{{ author }}</p>
{% set role = item.role %}
{% if role is mapping %}{% set role = role.translations.get(lang) or role.translations.get(default_lang) or '' %}{% endif %}
{% if role %}
<p class="text-xs text-gray-500 dark:text-gray-400">{{ role }}</p>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-center text-gray-400 dark:text-gray-500">Coming soon</p>
{% endif %}
</div>
</section>
{% endif %}
{% endmacro %}

View File

@@ -10,6 +10,34 @@
{% block alpine_data %}storefrontLayoutData(){% endblock %} {% block alpine_data %}storefrontLayoutData(){% endblock %}
{% block content %} {% block content %}
{# ═══════════════════════════════════════════════════════════════════ #}
{# SECTION-BASED RENDERING (when page.sections is configured) #}
{# Used by POC builder templates — takes priority over hardcoded HTML #}
{# ═══════════════════════════════════════════════════════════════════ #}
{% if page and page.sections %}
{% from 'cms/platform/sections/_hero.html' import render_hero %}
{% from 'cms/platform/sections/_features.html' import render_features %}
{% from 'cms/platform/sections/_testimonials.html' import render_testimonials %}
{% from 'cms/platform/sections/_gallery.html' import render_gallery %}
{% from 'cms/platform/sections/_contact_info.html' import render_contact_info %}
{% from 'cms/platform/sections/_cta.html' import render_cta %}
{% set lang = request.state.language|default("fr") %}
{% set default_lang = 'fr' %}
<div class="min-h-screen">
{% if page.sections.hero %}{{ render_hero(page.sections.hero, lang, default_lang) }}{% endif %}
{% if page.sections.features %}{{ render_features(page.sections.features, lang, default_lang) }}{% endif %}
{% if page.sections.testimonials %}{{ render_testimonials(page.sections.testimonials, lang, default_lang) }}{% endif %}
{% if page.sections.gallery %}{{ render_gallery(page.sections.gallery, lang, default_lang) }}{% endif %}
{% if page.sections.contact_info %}{{ render_contact_info(page.sections.contact_info, lang, default_lang) }}{% endif %}
{% if page.sections.cta %}{{ render_cta(page.sections.cta, lang, default_lang) }}{% endif %}
</div>
{% else %}
{# ═══════════════════════════════════════════════════════════════════ #}
{# HARDCODED LAYOUT (original full landing page — no sections JSON) #}
{# ═══════════════════════════════════════════════════════════════════ #}
<div class="min-h-screen"> <div class="min-h-screen">
{# Hero Section - Split Design #} {# Hero Section - Split Design #}
@@ -255,4 +283,5 @@
</section> </section>
</div> </div>
{% endif %}
{% endblock %} {% endblock %}