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:
@@ -7,6 +7,9 @@
|
||||
{% from 'cms/platform/sections/_products.html' import render_products %}
|
||||
{% from 'cms/platform/sections/_features.html' import render_features %}
|
||||
{% 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 %}
|
||||
|
||||
{% block title %}
|
||||
@@ -51,6 +54,21 @@
|
||||
{{ render_pricing(page.sections.pricing, lang, default_lang, tiers) }}
|
||||
{% 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 #}
|
||||
{% if page.sections.cta %}
|
||||
{{ render_cta(page.sections.cta, lang, default_lang) }}
|
||||
|
||||
@@ -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">📞</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">📧</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">📍</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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -10,6 +10,34 @@
|
||||
{% block alpine_data %}storefrontLayoutData(){% endblock %}
|
||||
|
||||
{% 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">
|
||||
|
||||
{# Hero Section - Split Design #}
|
||||
@@ -255,4 +283,5 @@
|
||||
</section>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user