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>
390 lines
14 KiB
HTML
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 %}
|