Files
orion/app/templates/shared/macros/storefront/star-rating.html
Samir Boulahtit b58dd9d19d fix: correct shop macro paths to storefront in all macro files
Fix self-referencing documentation examples in storefront macros
that incorrectly used shared/macros/shop/ instead of storefront/.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 21:47:39 +01:00

390 lines
14 KiB
HTML

{#
Star Rating Components
======================
Reusable star rating display and input for product reviews.
Usage:
{% from 'shared/macros/storefront/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 %}