refactor: rename shop to storefront for consistency
Rename all "shop" directories and references to "storefront" to match the API and route naming convention already in use. Renamed directories: - app/templates/shop/ → app/templates/storefront/ - static/shop/ → static/storefront/ - app/templates/shared/macros/shop/ → .../macros/storefront/ - docs/frontend/shop/ → docs/frontend/storefront/ Renamed files: - shop.css → storefront.css - shop-layout.js → storefront-layout.js Updated references in: - app/routes/storefront_pages.py (21 template references) - app/modules/cms/routes/pages/vendor.py - app/templates/storefront/base.html (static paths) - All storefront templates (extends/includes) - docs/architecture/frontend-structure.md This aligns the template/static naming with: - Route file: storefront_pages.py - API directory: app/api/v1/storefront/ - Module routes: */routes/api/storefront.py - URL paths: /storefront/* Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
389
app/templates/shared/macros/storefront/star-rating.html
Normal file
389
app/templates/shared/macros/storefront/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 %}
|
||||
Reference in New Issue
Block a user