feat: add Priority 5 Social Proof & Trust e-commerce macros
Add social proof and trust signal components for shop templates: - star-rating.html: Star rating display with half-star/exact precision, interactive rating input, rating summary with distribution bars, compact rating for lists - reviews.html: Review cards with verified badges, helpful voting, image attachments, review forms, sortable review lists - trust-badges.html: Trust badges grid, trust banners (3 variants), payment method icons, guarantee badges, security seals, checkout trust sections All macros support: - Alpine.js reactive bindings - Dark mode via Tailwind dark: prefix - Multiple size variants - Flexible layouts (grid, inline, vertical) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
459
app/templates/shared/macros/shop/reviews.html
Normal file
459
app/templates/shared/macros/shop/reviews.html
Normal file
@@ -0,0 +1,459 @@
|
||||
{#
|
||||
Review Components
|
||||
=================
|
||||
Product review display and submission components.
|
||||
|
||||
Usage:
|
||||
{% from 'shared/macros/shop/reviews.html' import review_list, review_card, review_form, review_summary %}
|
||||
#}
|
||||
|
||||
{% from 'shared/macros/shop/star-rating.html' import star_rating, rating_input, rating_summary %}
|
||||
|
||||
|
||||
{#
|
||||
Review Card
|
||||
===========
|
||||
Individual product review display.
|
||||
|
||||
Parameters:
|
||||
- review: Static review object
|
||||
- review_var: Alpine.js expression for review (dynamic)
|
||||
- show_avatar: Show reviewer avatar (default: true)
|
||||
- show_verified: Show verified purchase badge (default: true)
|
||||
- show_helpful: Show helpful buttons (default: true)
|
||||
- show_images: Show review images (default: true)
|
||||
- on_helpful: Callback for helpful button click
|
||||
|
||||
Review structure:
|
||||
{
|
||||
id: 1,
|
||||
author_name: 'John D.',
|
||||
author_avatar: null,
|
||||
rating: 5,
|
||||
title: 'Great product!',
|
||||
content: 'Really happy with this purchase...',
|
||||
verified: true,
|
||||
created_at: '2025-01-15',
|
||||
helpful_count: 42,
|
||||
images: []
|
||||
}
|
||||
|
||||
Usage:
|
||||
{{ review_card(review_var='review') }}
|
||||
#}
|
||||
{% macro review_card(
|
||||
review=none,
|
||||
review_var=none,
|
||||
show_avatar=true,
|
||||
show_verified=true,
|
||||
show_helpful=true,
|
||||
show_images=true,
|
||||
on_helpful=none
|
||||
) %}
|
||||
{% if review_var %}
|
||||
<article class="border-b border-gray-200 dark:border-gray-700 pb-6 last:border-0 last:pb-0">
|
||||
{# Header #}
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
{% if show_avatar %}
|
||||
<div class="w-10 h-10 rounded-full overflow-hidden bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center flex-shrink-0">
|
||||
<template x-if="{{ review_var }}.author_avatar">
|
||||
<img :src="{{ review_var }}.author_avatar" :alt="{{ review_var }}.author_name" class="w-full h-full object-cover">
|
||||
</template>
|
||||
<template x-if="!{{ review_var }}.author_avatar">
|
||||
<span class="text-sm font-medium text-purple-600 dark:text-purple-400" x-text="{{ review_var }}.author_name?.charAt(0)?.toUpperCase()"></span>
|
||||
</template>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="font-medium text-gray-900 dark:text-white" x-text="{{ review_var }}.author_name"></span>
|
||||
{% if show_verified %}
|
||||
<span
|
||||
x-show="{{ review_var }}.verified"
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-400 bg-green-100 dark:bg-green-900/30 rounded"
|
||||
>
|
||||
<span x-html="$icon('badge-check', 'w-3 h-3')"></span>
|
||||
Verified Purchase
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400" x-text="{{ review_var }}.created_at"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<template x-for="i in 5" :key="i">
|
||||
<span :class="i <= {{ review_var }}.rating ? 'text-yellow-400' : 'text-gray-300 dark:text-gray-600'">
|
||||
<span x-html="$icon('star', 'w-4 h-4')"></span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Title #}
|
||||
<h4
|
||||
x-show="{{ review_var }}.title"
|
||||
class="font-medium text-gray-900 dark:text-white mb-2"
|
||||
x-text="{{ review_var }}.title"
|
||||
></h4>
|
||||
|
||||
{# Content #}
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4" x-text="{{ review_var }}.content"></p>
|
||||
|
||||
{% if show_images %}
|
||||
{# Review Images #}
|
||||
<div
|
||||
x-show="{{ review_var }}.images?.length > 0"
|
||||
class="flex flex-wrap gap-2 mb-4"
|
||||
>
|
||||
<template x-for="(image, index) in {{ review_var }}.images || []" :key="index">
|
||||
<button
|
||||
type="button"
|
||||
class="w-16 h-16 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 hover:border-purple-500 transition-colors"
|
||||
>
|
||||
<img :src="image" alt="Review image" class="w-full h-full object-cover">
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_helpful %}
|
||||
{# Helpful Actions #}
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">Was this review helpful?</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
{% if on_helpful %}@click="{{ on_helpful }}({{ review_var }}.id, true)"{% endif %}
|
||||
class="inline-flex items-center gap-1 px-3 py-1 text-sm text-gray-600 dark:text-gray-400 hover:text-green-600 dark:hover:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/20 rounded transition-colors"
|
||||
>
|
||||
<span x-html="$icon('thumb-up', 'w-4 h-4')"></span>
|
||||
<span>Yes</span>
|
||||
<span x-show="{{ review_var }}.helpful_count > 0" class="text-gray-400" x-text="'(' + {{ review_var }}.helpful_count + ')'"></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
{% if on_helpful %}@click="{{ on_helpful }}({{ review_var }}.id, false)"{% endif %}
|
||||
class="inline-flex items-center gap-1 px-3 py-1 text-sm text-gray-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
|
||||
>
|
||||
<span x-html="$icon('thumb-down', 'w-4 h-4')"></span>
|
||||
<span>No</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Review List
|
||||
===========
|
||||
List of reviews with optional sorting and pagination.
|
||||
|
||||
Parameters:
|
||||
- reviews_var: Alpine.js expression for reviews array
|
||||
- loading_var: Alpine.js expression for loading state
|
||||
- empty_message: Message when no reviews (default: 'No reviews yet')
|
||||
- show_sort: Show sort dropdown (default: true)
|
||||
- sort_var: Alpine.js var for sort value (default: 'reviewSort')
|
||||
- on_sort: Callback when sort changes
|
||||
|
||||
Usage:
|
||||
{{ review_list(reviews_var='reviews', loading_var='loadingReviews') }}
|
||||
#}
|
||||
{% macro review_list(
|
||||
reviews_var='reviews',
|
||||
loading_var='loading',
|
||||
empty_message='No reviews yet. Be the first to review this product!',
|
||||
show_sort=true,
|
||||
sort_var='reviewSort',
|
||||
on_sort=none
|
||||
) %}
|
||||
<div class="space-y-6">
|
||||
{% if show_sort %}
|
||||
{# Sort Controls #}
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span x-text="{{ reviews_var }}?.length || 0"></span> reviews
|
||||
</span>
|
||||
<div x-data="{ open: false }" class="relative" @click.away="open = false">
|
||||
<button
|
||||
type="button"
|
||||
@click="open = !open"
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white border border-gray-300 dark:border-gray-600 rounded-lg"
|
||||
>
|
||||
<span>Sort by:</span>
|
||||
<span class="font-medium" x-text="{
|
||||
'newest': 'Newest',
|
||||
'oldest': 'Oldest',
|
||||
'highest': 'Highest Rated',
|
||||
'lowest': 'Lowest Rated',
|
||||
'helpful': 'Most Helpful'
|
||||
}[{{ sort_var }}] || 'Newest'"></span>
|
||||
<span x-html="$icon('chevron-down', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition
|
||||
class="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50"
|
||||
>
|
||||
{% for opt in [
|
||||
{'value': 'newest', 'label': 'Newest'},
|
||||
{'value': 'oldest', 'label': 'Oldest'},
|
||||
{'value': 'highest', 'label': 'Highest Rated'},
|
||||
{'value': 'lowest', 'label': 'Lowest Rated'},
|
||||
{'value': 'helpful', 'label': 'Most Helpful'}
|
||||
] %}
|
||||
<button
|
||||
type="button"
|
||||
@click="{{ sort_var }} = '{{ opt.value }}'; {% if on_sort %}{{ on_sort }};{% endif %} open = false"
|
||||
class="w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
:class="{{ sort_var }} === '{{ opt.value }}' ? 'text-purple-600 dark:text-purple-400 font-medium' : 'text-gray-700 dark:text-gray-300'"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Loading State #}
|
||||
<template x-if="{{ loading_var }}">
|
||||
<div class="space-y-6">
|
||||
{% for i in range(3) %}
|
||||
<div class="animate-pulse">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="flex-1">
|
||||
<div class="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded mb-1"></div>
|
||||
<div class="h-3 w-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||
</div>
|
||||
<div class="flex gap-0.5">
|
||||
{% for j in range(5) %}
|
||||
<div class="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-4 w-48 bg-gray-200 dark:bg-gray-700 rounded mb-2"></div>
|
||||
<div class="space-y-1">
|
||||
<div class="h-3 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||
<div class="h-3 w-3/4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{# Empty State #}
|
||||
<template x-if="!{{ loading_var }} && (!{{ reviews_var }} || {{ reviews_var }}.length === 0)">
|
||||
<div class="text-center py-8">
|
||||
<span x-html="$icon('chat-alt-2', 'w-12 h-12 text-gray-300 dark:text-gray-600 mx-auto')"></span>
|
||||
<p class="mt-2 text-gray-500 dark:text-gray-400">{{ empty_message }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{# Reviews #}
|
||||
<template x-if="!{{ loading_var }} && {{ reviews_var }}?.length > 0">
|
||||
<div class="space-y-6">
|
||||
<template x-for="review in {{ reviews_var }}" :key="review.id">
|
||||
{{ review_card(review_var='review') }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Review Form
|
||||
===========
|
||||
Form for submitting a product review.
|
||||
|
||||
Parameters:
|
||||
- rating_model: Alpine.js model for rating (default: 'newReview.rating')
|
||||
- title_model: Alpine.js model for title (default: 'newReview.title')
|
||||
- content_model: Alpine.js model for content (default: 'newReview.content')
|
||||
- images_model: Alpine.js model for images (default: 'newReview.images')
|
||||
- submitting_var: Alpine.js var for submitting state (default: 'submittingReview')
|
||||
- on_submit: Submit handler
|
||||
- show_images: Allow image upload (default: true)
|
||||
- require_purchase: Show purchase requirement message (default: false)
|
||||
|
||||
Usage:
|
||||
{{ review_form(on_submit='submitReview()') }}
|
||||
#}
|
||||
{% macro review_form(
|
||||
rating_model='newReview.rating',
|
||||
title_model='newReview.title',
|
||||
content_model='newReview.content',
|
||||
images_model='newReview.images',
|
||||
submitting_var='submittingReview',
|
||||
on_submit='submitReview()',
|
||||
show_images=true,
|
||||
require_purchase=false
|
||||
) %}
|
||||
<form
|
||||
@submit.prevent="{{ on_submit }}"
|
||||
class="space-y-6 bg-gray-50 dark:bg-gray-900 rounded-lg p-6"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Write a Review</h3>
|
||||
|
||||
{% if require_purchase %}
|
||||
<div class="flex items-start gap-2 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<span x-html="$icon('information-circle', 'w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5')"></span>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">You must have purchased this product to leave a review.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Rating #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Your Rating <span class="text-red-500">*</span>
|
||||
</label>
|
||||
{{ rating_input(model=rating_model, size='lg') }}
|
||||
</div>
|
||||
|
||||
{# Title #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Review Title
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="{{ title_model }}"
|
||||
placeholder="Sum up your experience"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:border-purple-500 dark:focus:border-purple-400 focus:ring-2 focus:ring-purple-500/20 outline-none"
|
||||
>
|
||||
</div>
|
||||
|
||||
{# Content #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Your Review <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
x-model="{{ content_model }}"
|
||||
rows="4"
|
||||
placeholder="Share your experience with this product..."
|
||||
required
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:border-purple-500 dark:focus:border-purple-400 focus:ring-2 focus:ring-purple-500/20 outline-none resize-none"
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Minimum 50 characters</p>
|
||||
</div>
|
||||
|
||||
{% if show_images %}
|
||||
{# Image Upload #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Add Photos (Optional)
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template x-for="(image, index) in {{ images_model }} || []" :key="index">
|
||||
<div class="relative w-20 h-20 rounded-lg overflow-hidden group">
|
||||
<img :src="image" alt="Review image" class="w-full h-full object-cover">
|
||||
<button
|
||||
type="button"
|
||||
@click="{{ images_model }}.splice(index, 1)"
|
||||
class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity"
|
||||
>
|
||||
<span x-html="$icon('x', 'w-6 h-6 text-white')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<label
|
||||
x-show="!{{ images_model }} || {{ images_model }}.length < 5"
|
||||
class="w-20 h-20 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg flex items-center justify-center cursor-pointer hover:border-purple-500 dark:hover:border-purple-400 transition-colors"
|
||||
>
|
||||
<input type="file" accept="image/*" class="hidden" @change="
|
||||
const file = $event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (!{{ images_model }}) {{ images_model }} = [];
|
||||
{{ images_model }}.push(e.target.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
">
|
||||
<span x-html="$icon('camera', 'w-6 h-6 text-gray-400')"></span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Up to 5 photos</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Submit Button #}
|
||||
<div class="flex items-center gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="{{ submitting_var }} || !{{ rating_model }} || !{{ content_model }}"
|
||||
class="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors"
|
||||
>
|
||||
<span x-show="!{{ submitting_var }}">Submit Review</span>
|
||||
<span x-show="{{ submitting_var }}" class="flex items-center gap-2">
|
||||
<span x-html="$icon('refresh', 'w-4 h-4 animate-spin')"></span>
|
||||
Submitting...
|
||||
</span>
|
||||
</button>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
By submitting, you agree to our review guidelines.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Review Summary Section
|
||||
======================
|
||||
Complete review summary with rating distribution and write review button.
|
||||
|
||||
Parameters:
|
||||
- rating_var: Alpine.js expression for average rating
|
||||
- count_var: Alpine.js expression for total reviews
|
||||
- distribution_var: Alpine.js expression for distribution
|
||||
- show_write_button: Show write review button (default: true)
|
||||
- on_write: Callback for write review button
|
||||
|
||||
Usage:
|
||||
{{ review_summary_section(rating_var='product.rating', count_var='product.review_count', distribution_var='product.rating_distribution') }}
|
||||
#}
|
||||
{% macro review_summary_section(
|
||||
rating_var='rating',
|
||||
count_var='reviewCount',
|
||||
distribution_var='ratingDistribution',
|
||||
show_write_button=true,
|
||||
on_write='showReviewForm = true'
|
||||
) %}
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-6">
|
||||
<div class="flex flex-col lg:flex-row lg:items-start gap-6">
|
||||
{# Rating Summary #}
|
||||
<div class="flex-1">
|
||||
{{ rating_summary(rating_var=rating_var, count_var=count_var, distribution_var=distribution_var) }}
|
||||
</div>
|
||||
|
||||
{% if show_write_button %}
|
||||
{# Write Review CTA #}
|
||||
<div class="lg:border-l lg:border-gray-200 dark:lg:border-gray-700 lg:pl-6">
|
||||
<div class="text-center">
|
||||
<h4 class="font-medium text-gray-900 dark:text-white mb-2">Share your thoughts</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Help others by sharing your experience with this product.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
@click="{{ on_write }}"
|
||||
class="inline-flex items-center gap-2 px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors"
|
||||
>
|
||||
<span x-html="$icon('pencil', 'w-5 h-5')"></span>
|
||||
Write a Review
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
389
app/templates/shared/macros/shop/star-rating.html
Normal file
389
app/templates/shared/macros/shop/star-rating.html
Normal file
@@ -0,0 +1,389 @@
|
||||
{#
|
||||
Star Rating Components
|
||||
======================
|
||||
Reusable star rating display and input for product reviews.
|
||||
|
||||
Usage:
|
||||
{% from 'shared/macros/shop/star-rating.html' import star_rating, rating_input, rating_summary %}
|
||||
#}
|
||||
|
||||
|
||||
{#
|
||||
Star Rating Display
|
||||
===================
|
||||
Static star rating display.
|
||||
|
||||
Parameters:
|
||||
- rating: Numeric rating value (0-5)
|
||||
- rating_var: Alpine.js expression for rating (dynamic)
|
||||
- max: Maximum stars (default: 5)
|
||||
- size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' (default: 'md')
|
||||
- show_value: Show numeric value (default: false)
|
||||
- show_count: Show review count (default: false)
|
||||
- count: Number of reviews
|
||||
- count_var: Alpine.js expression for count
|
||||
- precision: 'full' | 'half' | 'exact' (default: 'half')
|
||||
- color: Star color class (default: 'text-yellow-400')
|
||||
- empty_color: Empty star color (default: 'text-gray-300 dark:text-gray-600')
|
||||
|
||||
Usage:
|
||||
{{ star_rating(rating=4.5) }}
|
||||
{{ star_rating(rating_var='product.rating', show_count=true, count_var='product.review_count') }}
|
||||
#}
|
||||
{% macro star_rating(
|
||||
rating=none,
|
||||
rating_var=none,
|
||||
max=5,
|
||||
size='md',
|
||||
show_value=false,
|
||||
show_count=false,
|
||||
count=none,
|
||||
count_var=none,
|
||||
precision='half',
|
||||
color='text-yellow-400',
|
||||
empty_color='text-gray-300 dark:text-gray-600'
|
||||
) %}
|
||||
{% set sizes = {
|
||||
'xs': 'w-3 h-3',
|
||||
'sm': 'w-4 h-4',
|
||||
'md': 'w-5 h-5',
|
||||
'lg': 'w-6 h-6',
|
||||
'xl': 'w-8 h-8'
|
||||
} %}
|
||||
{% set text_sizes = {
|
||||
'xs': 'text-xs',
|
||||
'sm': 'text-sm',
|
||||
'md': 'text-sm',
|
||||
'lg': 'text-base',
|
||||
'xl': 'text-lg'
|
||||
} %}
|
||||
<div class="flex items-center gap-1">
|
||||
{% if rating is not none %}
|
||||
{# Static rating #}
|
||||
<div class="flex items-center gap-0.5">
|
||||
{% for i in range(1, max + 1) %}
|
||||
{% if precision == 'half' %}
|
||||
{% set fill = 'full' if rating >= i else ('half' if rating >= i - 0.5 else 'empty') %}
|
||||
{% elif precision == 'exact' %}
|
||||
{% set fill_percent = ((rating - (i - 1)) * 100) | int %}
|
||||
{% set fill_percent = [0, [fill_percent, 100] | min] | max %}
|
||||
{% else %}
|
||||
{% set fill = 'full' if rating >= i - 0.5 else 'empty' %}
|
||||
{% endif %}
|
||||
|
||||
{% if precision == 'exact' %}
|
||||
<span class="relative {{ sizes[size] }}">
|
||||
<span class="absolute inset-0 {{ empty_color }}">
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
<span class="absolute inset-0 {{ color }} overflow-hidden" style="width: {{ fill_percent }}%">
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
</span>
|
||||
{% elif fill == 'half' %}
|
||||
<span class="relative {{ sizes[size] }}">
|
||||
<span class="absolute inset-0 {{ empty_color }}">
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
<span class="absolute inset-0 {{ color }} overflow-hidden" style="width: 50%">
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="{{ color if fill == 'full' else empty_color }}">
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if show_value %}
|
||||
<span class="{{ text_sizes[size] }} font-medium text-gray-900 dark:text-white ml-1">{{ rating }}</span>
|
||||
{% endif %}
|
||||
{% if show_count and count is not none %}
|
||||
<span class="{{ text_sizes[size] }} text-gray-500 dark:text-gray-400">({{ count }})</span>
|
||||
{% endif %}
|
||||
|
||||
{% elif rating_var %}
|
||||
{# Dynamic rating from Alpine.js #}
|
||||
<div class="flex items-center gap-0.5">
|
||||
<template x-for="i in {{ max }}" :key="i">
|
||||
<span class="relative {{ sizes[size] }}">
|
||||
<template x-if="{% if precision == 'exact' %}true{% else %}{{ rating_var }} >= i - 0.5 && {{ rating_var }} < i{% endif %}">
|
||||
<span class="relative {{ sizes[size] }}">
|
||||
<span class="absolute inset-0 {{ empty_color }}">
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
<span class="absolute inset-0 {{ color }} overflow-hidden" :style="'width: ' + {% if precision == 'exact' %}Math.min(100, Math.max(0, ({{ rating_var }} - (i - 1)) * 100)){% else %}50{% endif %} + '%'">
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="{{ rating_var }} >= i">
|
||||
<span class="{{ color }}">
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="{{ rating_var }} < i - 0.5">
|
||||
<span class="{{ empty_color }}">
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
{% if show_value %}
|
||||
<span class="{{ text_sizes[size] }} font-medium text-gray-900 dark:text-white ml-1" x-text="{{ rating_var }}?.toFixed(1)"></span>
|
||||
{% endif %}
|
||||
{% if show_count and count_var %}
|
||||
<span class="{{ text_sizes[size] }} text-gray-500 dark:text-gray-400" x-text="'(' + {{ count_var }} + ')'"></span>
|
||||
{% elif show_count and count is not none %}
|
||||
<span class="{{ text_sizes[size] }} text-gray-500 dark:text-gray-400">({{ count }})</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Rating Input
|
||||
============
|
||||
Interactive star rating input for submitting reviews.
|
||||
|
||||
Parameters:
|
||||
- model: Alpine.js model for rating value (default: 'rating')
|
||||
- max: Maximum stars (default: 5)
|
||||
- size: 'sm' | 'md' | 'lg' (default: 'md')
|
||||
- allow_half: Allow half-star ratings (default: false)
|
||||
- allow_clear: Allow clearing rating (default: true)
|
||||
- disabled_var: Alpine.js expression for disabled state
|
||||
- on_change: Callback when rating changes
|
||||
|
||||
Usage:
|
||||
{{ rating_input(model='reviewRating') }}
|
||||
{{ rating_input(model='rating', size='lg', allow_half=true) }}
|
||||
#}
|
||||
{% macro rating_input(
|
||||
model='rating',
|
||||
max=5,
|
||||
size='md',
|
||||
allow_half=false,
|
||||
allow_clear=true,
|
||||
disabled_var=none,
|
||||
on_change=none
|
||||
) %}
|
||||
{% set sizes = {
|
||||
'sm': 'w-6 h-6',
|
||||
'md': 'w-8 h-8',
|
||||
'lg': 'w-10 h-10'
|
||||
} %}
|
||||
<div
|
||||
x-data="{
|
||||
hoverRating: 0,
|
||||
setRating(value) {
|
||||
if ({{ disabled_var if disabled_var else 'false' }}) return;
|
||||
if ({{ model }} === value && {{ 'true' if allow_clear else 'false' }}) {
|
||||
{{ model }} = 0;
|
||||
} else {
|
||||
{{ model }} = value;
|
||||
}
|
||||
{% if on_change %}{{ on_change }};{% endif %}
|
||||
},
|
||||
getStarValue(index, isHalf) {
|
||||
return isHalf ? index - 0.5 : index;
|
||||
}
|
||||
}"
|
||||
class="flex items-center gap-1"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': {{ disabled_var if disabled_var else 'false' }} }"
|
||||
>
|
||||
{% for i in range(1, max + 1) %}
|
||||
<button
|
||||
type="button"
|
||||
@click="setRating({{ i }})"
|
||||
@mouseenter="hoverRating = {{ i }}"
|
||||
@mouseleave="hoverRating = 0"
|
||||
:disabled="{{ disabled_var if disabled_var else 'false' }}"
|
||||
class="relative {{ sizes[size] }} focus:outline-none transition-transform hover:scale-110 disabled:hover:scale-100"
|
||||
:class="{ 'cursor-pointer': !{{ disabled_var if disabled_var else 'false' }} }"
|
||||
>
|
||||
{% if allow_half %}
|
||||
{# Half-star support #}
|
||||
<span
|
||||
class="absolute inset-0 text-gray-300 dark:text-gray-600"
|
||||
x-show="(hoverRating || {{ model }}) < {{ i - 0.5 }}"
|
||||
>
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
<span
|
||||
class="absolute inset-0"
|
||||
x-show="(hoverRating || {{ model }}) >= {{ i - 0.5 }} && (hoverRating || {{ model }}) < {{ i }}"
|
||||
>
|
||||
<span class="absolute inset-0 text-gray-300 dark:text-gray-600">
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
<span class="absolute inset-0 text-yellow-400 overflow-hidden" style="width: 50%">
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="absolute inset-0 text-yellow-400"
|
||||
x-show="(hoverRating || {{ model }}) >= {{ i }}"
|
||||
>
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
{# Half-star click area #}
|
||||
<span
|
||||
class="absolute inset-y-0 left-0 w-1/2"
|
||||
@click.stop="setRating({{ i - 0.5 }})"
|
||||
></span>
|
||||
<span
|
||||
class="absolute inset-y-0 right-0 w-1/2"
|
||||
@click.stop="setRating({{ i }})"
|
||||
></span>
|
||||
{% else %}
|
||||
{# Full star only #}
|
||||
<span
|
||||
class="text-yellow-400"
|
||||
x-show="(hoverRating || {{ model }}) >= {{ i }}"
|
||||
>
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
<span
|
||||
class="text-gray-300 dark:text-gray-600"
|
||||
x-show="(hoverRating || {{ model }}) < {{ i }}"
|
||||
>
|
||||
<span x-html="$icon('star', '{{ sizes[size] }}')"></span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
|
||||
{# Rating label #}
|
||||
<span
|
||||
x-show="{{ model }} > 0"
|
||||
class="ml-2 text-sm text-gray-600 dark:text-gray-400"
|
||||
x-text="[
|
||||
'',
|
||||
'Poor',
|
||||
'Fair',
|
||||
'Good',
|
||||
'Very Good',
|
||||
'Excellent'
|
||||
][Math.ceil({{ model }})] || ''"
|
||||
></span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Rating Summary
|
||||
==============
|
||||
Rating distribution summary (typically shown on product pages).
|
||||
|
||||
Parameters:
|
||||
- rating_var: Alpine.js expression for average rating
|
||||
- count_var: Alpine.js expression for total reviews
|
||||
- distribution_var: Alpine.js expression for distribution object {5: count, 4: count, ...}
|
||||
- show_bars: Show distribution bars (default: true)
|
||||
- size: 'sm' | 'md' | 'lg' (default: 'md')
|
||||
|
||||
Usage:
|
||||
{{ rating_summary(rating_var='product.rating', count_var='product.review_count', distribution_var='product.rating_distribution') }}
|
||||
#}
|
||||
{% macro rating_summary(
|
||||
rating_var='rating',
|
||||
count_var='reviewCount',
|
||||
distribution_var='ratingDistribution',
|
||||
show_bars=true,
|
||||
size='md'
|
||||
) %}
|
||||
{% set sizes = {
|
||||
'sm': {'star': 'w-4 h-4', 'text': 'text-3xl', 'bar_h': 'h-1.5'},
|
||||
'md': {'star': 'w-5 h-5', 'text': 'text-4xl', 'bar_h': 'h-2'},
|
||||
'lg': {'star': 'w-6 h-6', 'text': 'text-5xl', 'bar_h': 'h-2.5'}
|
||||
} %}
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
{# Average Rating #}
|
||||
<div class="text-center md:text-left">
|
||||
<div class="{{ sizes[size].text }} font-bold text-gray-900 dark:text-white" x-text="{{ rating_var }}?.toFixed(1)"></div>
|
||||
<div class="flex items-center justify-center md:justify-start gap-0.5 mt-2">
|
||||
<template x-for="i in 5" :key="i">
|
||||
<span
|
||||
:class="i <= Math.round({{ rating_var }}) ? 'text-yellow-400' : 'text-gray-300 dark:text-gray-600'"
|
||||
>
|
||||
<span x-html="$icon('star', '{{ sizes[size].star }}')"></span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Based on <span x-text="{{ count_var }}"></span> reviews
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if show_bars %}
|
||||
{# Rating Distribution #}
|
||||
<div class="flex-1 space-y-2">
|
||||
{% for i in range(5, 0, -1) %}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-8 text-sm text-gray-600 dark:text-gray-400">{{ i }} <span x-html="$icon('star', 'w-3 h-3 inline')"></span></span>
|
||||
<div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full {{ sizes[size].bar_h }} overflow-hidden">
|
||||
<div
|
||||
class="bg-yellow-400 {{ sizes[size].bar_h }} rounded-full transition-all duration-300"
|
||||
:style="'width: ' + ({{ count_var }} > 0 ? ({{ distribution_var }}[{{ i }}] || 0) / {{ count_var }} * 100 : 0) + '%'"
|
||||
></div>
|
||||
</div>
|
||||
<span class="w-10 text-sm text-gray-600 dark:text-gray-400 text-right" x-text="({{ distribution_var }}[{{ i }}] || 0)"></span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Compact Rating
|
||||
==============
|
||||
Inline compact rating for lists and cards.
|
||||
|
||||
Parameters:
|
||||
- rating: Static rating value
|
||||
- rating_var: Alpine.js expression for rating
|
||||
- count: Review count
|
||||
- count_var: Alpine.js expression for count
|
||||
- size: 'xs' | 'sm' | 'md' (default: 'sm')
|
||||
|
||||
Usage:
|
||||
{{ compact_rating(rating=4.5, count=127) }}
|
||||
{{ compact_rating(rating_var='item.rating', count_var='item.reviews') }}
|
||||
#}
|
||||
{% macro compact_rating(
|
||||
rating=none,
|
||||
rating_var=none,
|
||||
count=none,
|
||||
count_var=none,
|
||||
size='sm'
|
||||
) %}
|
||||
{% set sizes = {
|
||||
'xs': {'star': 'w-3 h-3', 'text': 'text-xs'},
|
||||
'sm': {'star': 'w-4 h-4', 'text': 'text-sm'},
|
||||
'md': {'star': 'w-5 h-5', 'text': 'text-base'}
|
||||
} %}
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-yellow-400">
|
||||
<span x-html="$icon('star', '{{ sizes[size].star }}')"></span>
|
||||
</span>
|
||||
{% if rating is not none %}
|
||||
<span class="{{ sizes[size].text }} font-medium text-gray-900 dark:text-white">{{ rating }}</span>
|
||||
{% if count is not none %}
|
||||
<span class="{{ sizes[size].text }} text-gray-500 dark:text-gray-400">({{ count }})</span>
|
||||
{% endif %}
|
||||
{% elif rating_var %}
|
||||
<span class="{{ sizes[size].text }} font-medium text-gray-900 dark:text-white" x-text="{{ rating_var }}?.toFixed(1)"></span>
|
||||
{% if count_var %}
|
||||
<span class="{{ sizes[size].text }} text-gray-500 dark:text-gray-400" x-text="'(' + {{ count_var }} + ')'"></span>
|
||||
{% elif count is not none %}
|
||||
<span class="{{ sizes[size].text }} text-gray-500 dark:text-gray-400">({{ count }})</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
436
app/templates/shared/macros/shop/trust-badges.html
Normal file
436
app/templates/shared/macros/shop/trust-badges.html
Normal file
@@ -0,0 +1,436 @@
|
||||
{#
|
||||
Trust Badge Components
|
||||
======================
|
||||
Trust signals and security indicators for e-commerce.
|
||||
|
||||
Usage:
|
||||
{% from 'shared/macros/shop/trust-badges.html' import trust_badges, trust_banner, payment_icons, guarantee_badge %}
|
||||
#}
|
||||
|
||||
|
||||
{#
|
||||
Trust Badges
|
||||
============
|
||||
Grid of trust indicators.
|
||||
|
||||
Parameters:
|
||||
- badges: List of badge types to show (default: all)
|
||||
- layout: 'grid' | 'inline' | 'vertical' (default: 'grid')
|
||||
- size: 'sm' | 'md' | 'lg' (default: 'md')
|
||||
- show_text: Show badge text (default: true)
|
||||
- free_shipping_threshold: Threshold for free shipping (default: none)
|
||||
|
||||
Badge Types:
|
||||
- secure_payment
|
||||
- free_shipping
|
||||
- easy_returns
|
||||
- support_24_7
|
||||
- money_back
|
||||
- ssl_secured
|
||||
- fast_delivery
|
||||
- quality_guarantee
|
||||
|
||||
Usage:
|
||||
{{ trust_badges(badges=['secure_payment', 'free_shipping', 'easy_returns']) }}
|
||||
{{ trust_badges(layout='inline', size='sm') }}
|
||||
#}
|
||||
{% macro trust_badges(
|
||||
badges=none,
|
||||
layout='grid',
|
||||
size='md',
|
||||
show_text=true,
|
||||
free_shipping_threshold=none
|
||||
) %}
|
||||
{% set all_badges = [
|
||||
{
|
||||
'id': 'secure_payment',
|
||||
'icon': 'lock-closed',
|
||||
'title': 'Secure Payment',
|
||||
'description': '256-bit SSL encryption'
|
||||
},
|
||||
{
|
||||
'id': 'free_shipping',
|
||||
'icon': 'truck',
|
||||
'title': 'Free Shipping',
|
||||
'description': 'On orders over $' ~ (free_shipping_threshold or 50)
|
||||
},
|
||||
{
|
||||
'id': 'easy_returns',
|
||||
'icon': 'refresh',
|
||||
'title': 'Easy Returns',
|
||||
'description': '30-day return policy'
|
||||
},
|
||||
{
|
||||
'id': 'support_24_7',
|
||||
'icon': 'support',
|
||||
'title': '24/7 Support',
|
||||
'description': 'Always here to help'
|
||||
},
|
||||
{
|
||||
'id': 'money_back',
|
||||
'icon': 'cash',
|
||||
'title': 'Money Back',
|
||||
'description': '100% guarantee'
|
||||
},
|
||||
{
|
||||
'id': 'ssl_secured',
|
||||
'icon': 'shield-check',
|
||||
'title': 'SSL Secured',
|
||||
'description': 'Protected checkout'
|
||||
},
|
||||
{
|
||||
'id': 'fast_delivery',
|
||||
'icon': 'lightning-bolt',
|
||||
'title': 'Fast Delivery',
|
||||
'description': '2-5 business days'
|
||||
},
|
||||
{
|
||||
'id': 'quality_guarantee',
|
||||
'icon': 'badge-check',
|
||||
'title': 'Quality Guarantee',
|
||||
'description': 'Premium products'
|
||||
}
|
||||
] %}
|
||||
{% set selected_badges = badges if badges else ['secure_payment', 'free_shipping', 'easy_returns', 'support_24_7'] %}
|
||||
{% set sizes = {
|
||||
'sm': {'icon': 'w-5 h-5', 'title': 'text-xs', 'desc': 'text-xs', 'padding': 'p-2', 'gap': 'gap-1'},
|
||||
'md': {'icon': 'w-6 h-6', 'title': 'text-sm', 'desc': 'text-xs', 'padding': 'p-3', 'gap': 'gap-2'},
|
||||
'lg': {'icon': 'w-8 h-8', 'title': 'text-base', 'desc': 'text-sm', 'padding': 'p-4', 'gap': 'gap-3'}
|
||||
} %}
|
||||
{% set layouts = {
|
||||
'grid': 'grid grid-cols-2 md:grid-cols-4 gap-4',
|
||||
'inline': 'flex flex-wrap items-center justify-center gap-6',
|
||||
'vertical': 'flex flex-col gap-3'
|
||||
} %}
|
||||
|
||||
<div class="{{ layouts[layout] }}">
|
||||
{% for badge_id in selected_badges %}
|
||||
{% for badge in all_badges %}
|
||||
{% if badge.id == badge_id %}
|
||||
<div class="flex items-center {{ sizes[size].gap }} {{ sizes[size].padding if layout == 'grid' else '' }} {{ 'bg-gray-50 dark:bg-gray-800 rounded-lg' if layout == 'grid' else '' }}">
|
||||
<div class="flex-shrink-0 text-purple-600 dark:text-purple-400">
|
||||
<span x-html="$icon('{{ badge.icon }}', '{{ sizes[size].icon }}')"></span>
|
||||
</div>
|
||||
{% if show_text %}
|
||||
<div class="{{ 'min-w-0' if layout == 'grid' else '' }}">
|
||||
<p class="font-medium text-gray-900 dark:text-white {{ sizes[size].title }} {{ 'truncate' if layout == 'grid' else '' }}">{{ badge.title }}</p>
|
||||
{% if layout != 'inline' %}
|
||||
<p class="text-gray-500 dark:text-gray-400 {{ sizes[size].desc }} {{ 'truncate' if layout == 'grid' else '' }}">{{ badge.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Trust Banner
|
||||
============
|
||||
Full-width trust banner for product pages or checkout.
|
||||
|
||||
Parameters:
|
||||
- variant: 'default' | 'compact' | 'detailed' (default: 'default')
|
||||
- show_icons: Show payment/security icons (default: true)
|
||||
|
||||
Usage:
|
||||
{{ trust_banner() }}
|
||||
{{ trust_banner(variant='compact') }}
|
||||
#}
|
||||
{% macro trust_banner(
|
||||
variant='default',
|
||||
show_icons=true
|
||||
) %}
|
||||
{% if variant == 'compact' %}
|
||||
<div class="flex flex-wrap items-center justify-center gap-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span class="flex items-center gap-2">
|
||||
<span x-html="$icon('truck', 'w-5 h-5 text-purple-600 dark:text-purple-400')"></span>
|
||||
Free shipping over $50
|
||||
</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<span x-html="$icon('refresh', 'w-5 h-5 text-purple-600 dark:text-purple-400')"></span>
|
||||
30-day returns
|
||||
</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<span x-html="$icon('shield-check', 'w-5 h-5 text-purple-600 dark:text-purple-400')"></span>
|
||||
Secure checkout
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% elif variant == 'detailed' %}
|
||||
<div class="bg-gradient-to-r from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20 rounded-xl p-6">
|
||||
<div class="grid md:grid-cols-3 gap-6">
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 mx-auto mb-3 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center">
|
||||
<span x-html="$icon('truck', 'w-6 h-6 text-purple-600 dark:text-purple-400')"></span>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Free Shipping</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">On all orders over $50. International shipping available.</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 mx-auto mb-3 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
|
||||
<span x-html="$icon('refresh', 'w-6 h-6 text-green-600 dark:text-green-400')"></span>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Easy Returns</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">30-day hassle-free return policy. No questions asked.</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 mx-auto mb-3 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
|
||||
<span x-html="$icon('shield-check', 'w-6 h-6 text-blue-600 dark:text-blue-400')"></span>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Secure Payment</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Your payment info is protected with 256-bit encryption.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if show_icons %}
|
||||
<div class="mt-6 pt-6 border-t border-purple-200 dark:border-purple-800">
|
||||
{{ payment_icons(size='sm') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{# Default variant #}
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span x-html="$icon('shield-check', 'w-8 h-8 text-green-600 dark:text-green-400')"></span>
|
||||
<div>
|
||||
<p class="font-medium text-gray-900 dark:text-white">Secure Shopping Guarantee</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Shop with confidence - your data is protected</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if show_icons %}
|
||||
{{ payment_icons(size='sm') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Payment Icons
|
||||
=============
|
||||
Display accepted payment method icons.
|
||||
|
||||
Parameters:
|
||||
- methods: List of payment methods (default: common cards)
|
||||
- size: 'xs' | 'sm' | 'md' | 'lg' (default: 'md')
|
||||
- grayscale: Show in grayscale (default: false)
|
||||
|
||||
Methods: visa, mastercard, amex, paypal, apple_pay, google_pay, discover, klarna
|
||||
|
||||
Usage:
|
||||
{{ payment_icons() }}
|
||||
{{ payment_icons(methods=['visa', 'mastercard', 'paypal'], size='lg') }}
|
||||
#}
|
||||
{% macro payment_icons(
|
||||
methods=none,
|
||||
size='md',
|
||||
grayscale=false
|
||||
) %}
|
||||
{% set all_methods = {
|
||||
'visa': {'name': 'Visa', 'color': 'text-blue-600'},
|
||||
'mastercard': {'name': 'Mastercard', 'color': 'text-orange-500'},
|
||||
'amex': {'name': 'American Express', 'color': 'text-blue-500'},
|
||||
'paypal': {'name': 'PayPal', 'color': 'text-blue-700'},
|
||||
'apple_pay': {'name': 'Apple Pay', 'color': 'text-gray-900 dark:text-white'},
|
||||
'google_pay': {'name': 'Google Pay', 'color': 'text-gray-700'},
|
||||
'discover': {'name': 'Discover', 'color': 'text-orange-600'},
|
||||
'klarna': {'name': 'Klarna', 'color': 'text-pink-500'}
|
||||
} %}
|
||||
{% set selected = methods if methods else ['visa', 'mastercard', 'amex', 'paypal'] %}
|
||||
{% set sizes = {
|
||||
'xs': 'h-4',
|
||||
'sm': 'h-6',
|
||||
'md': 'h-8',
|
||||
'lg': 'h-10'
|
||||
} %}
|
||||
|
||||
<div class="flex items-center gap-2 {{ 'grayscale opacity-60' if grayscale else '' }}">
|
||||
{% for method in selected %}
|
||||
{% if method in all_methods %}
|
||||
<div
|
||||
class="px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded {{ sizes[size] }} flex items-center justify-center"
|
||||
title="{{ all_methods[method].name }}"
|
||||
>
|
||||
<span class="text-xs font-bold {{ all_methods[method].color }}">{{ all_methods[method].name[:4] }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Guarantee Badge
|
||||
===============
|
||||
Individual guarantee/warranty badge.
|
||||
|
||||
Parameters:
|
||||
- type: 'money_back' | 'warranty' | 'authentic' | 'satisfaction' (default: 'money_back')
|
||||
- days: Number of days (for money_back/warranty)
|
||||
- size: 'sm' | 'md' | 'lg' (default: 'md')
|
||||
- variant: 'default' | 'outlined' | 'filled' (default: 'default')
|
||||
|
||||
Usage:
|
||||
{{ guarantee_badge(type='money_back', days=30) }}
|
||||
{{ guarantee_badge(type='warranty', days=365, variant='filled') }}
|
||||
#}
|
||||
{% macro guarantee_badge(
|
||||
type='money_back',
|
||||
days=30,
|
||||
size='md',
|
||||
variant='default'
|
||||
) %}
|
||||
{% set badges = {
|
||||
'money_back': {
|
||||
'icon': 'cash',
|
||||
'title': days ~ '-Day Money Back',
|
||||
'subtitle': 'Guarantee'
|
||||
},
|
||||
'warranty': {
|
||||
'icon': 'shield-check',
|
||||
'title': (days // 365) ~ '-Year Warranty' if days >= 365 else days ~ '-Day Warranty',
|
||||
'subtitle': 'Included'
|
||||
},
|
||||
'authentic': {
|
||||
'icon': 'badge-check',
|
||||
'title': '100% Authentic',
|
||||
'subtitle': 'Guaranteed'
|
||||
},
|
||||
'satisfaction': {
|
||||
'icon': 'emoji-happy',
|
||||
'title': 'Satisfaction',
|
||||
'subtitle': 'Guaranteed'
|
||||
}
|
||||
} %}
|
||||
{% set badge = badges[type] %}
|
||||
{% set sizes = {
|
||||
'sm': {'icon': 'w-8 h-8', 'title': 'text-xs', 'subtitle': 'text-xs', 'padding': 'p-3'},
|
||||
'md': {'icon': 'w-10 h-10', 'title': 'text-sm', 'subtitle': 'text-xs', 'padding': 'p-4'},
|
||||
'lg': {'icon': 'w-12 h-12', 'title': 'text-base', 'subtitle': 'text-sm', 'padding': 'p-5'}
|
||||
} %}
|
||||
{% set variants = {
|
||||
'default': 'bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700',
|
||||
'outlined': 'border-2 border-purple-600 dark:border-purple-400',
|
||||
'filled': 'bg-purple-600 text-white'
|
||||
} %}
|
||||
|
||||
<div class="{{ variants[variant] }} rounded-lg {{ sizes[size].padding }} text-center">
|
||||
<div class="mx-auto mb-2 {{ 'text-purple-600 dark:text-purple-400' if variant != 'filled' else 'text-white' }}">
|
||||
<span x-html="$icon('{{ badge.icon }}', '{{ sizes[size].icon }} mx-auto')"></span>
|
||||
</div>
|
||||
<p class="font-bold {{ sizes[size].title }} {{ 'text-gray-900 dark:text-white' if variant != 'filled' else 'text-white' }}">{{ badge.title }}</p>
|
||||
<p class="{{ sizes[size].subtitle }} {{ 'text-gray-500 dark:text-gray-400' if variant != 'filled' else 'text-purple-100' }}">{{ badge.subtitle }}</p>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Security Seals
|
||||
==============
|
||||
Security certification seals.
|
||||
|
||||
Parameters:
|
||||
- seals: List of seal types to show
|
||||
- layout: 'horizontal' | 'vertical' (default: 'horizontal')
|
||||
|
||||
Seal Types: ssl, pci, mcafee, norton, trustpilot
|
||||
|
||||
Usage:
|
||||
{{ security_seals(seals=['ssl', 'pci']) }}
|
||||
#}
|
||||
{% macro security_seals(
|
||||
seals=none,
|
||||
layout='horizontal'
|
||||
) %}
|
||||
{% set all_seals = [
|
||||
{'id': 'ssl', 'name': 'SSL Secure', 'icon': 'lock-closed'},
|
||||
{'id': 'pci', 'name': 'PCI Compliant', 'icon': 'shield-check'},
|
||||
{'id': 'verified', 'name': 'Verified Business', 'icon': 'badge-check'},
|
||||
{'id': 'secure_checkout', 'name': 'Secure Checkout', 'icon': 'shield-check'}
|
||||
] %}
|
||||
{% set selected = seals if seals else ['ssl', 'verified'] %}
|
||||
|
||||
<div class="flex {{ 'flex-col' if layout == 'vertical' else 'flex-wrap' }} items-center gap-3">
|
||||
{% for seal_id in selected %}
|
||||
{% for seal in all_seals %}
|
||||
{% if seal.id == seal_id %}
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<span x-html="$icon('{{ seal.icon }}', 'w-5 h-5 text-green-600 dark:text-green-400')"></span>
|
||||
<span class="text-sm font-medium text-green-700 dark:text-green-300">{{ seal.name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Checkout Trust Section
|
||||
======================
|
||||
Combined trust elements for checkout pages.
|
||||
|
||||
Parameters:
|
||||
- show_guarantee: Show money-back guarantee (default: true)
|
||||
- show_payment: Show payment icons (default: true)
|
||||
- show_security: Show security seals (default: true)
|
||||
|
||||
Usage:
|
||||
{{ checkout_trust_section() }}
|
||||
#}
|
||||
{% macro checkout_trust_section(
|
||||
show_guarantee=true,
|
||||
show_payment=true,
|
||||
show_security=true
|
||||
) %}
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 space-y-6">
|
||||
{% if show_guarantee %}
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span x-html="$icon('shield-check', 'w-8 h-8 text-green-600 dark:text-green-400')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white">100% Secure Checkout</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Your payment information is encrypted and secure. We never store your card details.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_payment %}
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">We accept:</p>
|
||||
{{ payment_icons(size='md') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_security %}
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
{{ security_seals() }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<div class="flex flex-wrap items-center justify-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span class="flex items-center gap-1">
|
||||
<span x-html="$icon('lock-closed', 'w-4 h-4')"></span>
|
||||
256-bit SSL
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<span x-html="$icon('refresh', 'w-4 h-4')"></span>
|
||||
30-day returns
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<span x-html="$icon('support', 'w-4 h-4')"></span>
|
||||
24/7 support
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
Reference in New Issue
Block a user