refactor: extract product_card to shared ShopComponents module
Create a reusable product card component with four variants: - :default (collection page) - full details with category - :featured (home page) - hover animation, badges - :compact (pdp related) - smaller, no badges/delivery - :minimal (error 404) - smallest, not clickable Accessibility improvements: - Use <a> for all clickable cards (keyboard nav, screen readers) - Use <div> only for non-clickable decorative cards Unified badge logic with priority: Sold out > New > Sale Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
50941d278f
commit
9746bf183c
@ -516,4 +516,269 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
|||||||
</style>
|
</style>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a product card with configurable variants.
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
* `product` - Required. The product map with `name`, `image_url`, `price`, etc.
|
||||||
|
* `theme_settings` - Required. The theme settings map.
|
||||||
|
* `mode` - Either `:live` (default) or `:preview`.
|
||||||
|
* `variant` - The visual variant:
|
||||||
|
- `:default` - Collection page style with border, category, full details
|
||||||
|
- `:featured` - Home page style with hover lift, no border
|
||||||
|
- `:compact` - PDP related products with aspect-square, minimal info
|
||||||
|
- `:minimal` - Error 404 style, smallest, not clickable
|
||||||
|
* `show_category` - Show category label. Defaults based on variant.
|
||||||
|
* `show_badges` - Show product badges. Defaults based on variant.
|
||||||
|
* `show_delivery_text` - Show "Free delivery" text. Defaults based on variant.
|
||||||
|
* `clickable` - Whether the card navigates. Defaults based on variant.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<.product_card product={product} theme_settings={@theme_settings} />
|
||||||
|
<.product_card product={product} theme_settings={@theme_settings} variant={:featured} mode={:preview} />
|
||||||
|
"""
|
||||||
|
attr :product, :map, required: true
|
||||||
|
attr :theme_settings, :map, required: true
|
||||||
|
attr :mode, :atom, default: :live
|
||||||
|
attr :variant, :atom, default: :default
|
||||||
|
attr :show_category, :boolean, default: nil
|
||||||
|
attr :show_badges, :boolean, default: nil
|
||||||
|
attr :show_delivery_text, :boolean, default: nil
|
||||||
|
attr :clickable, :boolean, default: nil
|
||||||
|
|
||||||
|
def product_card(assigns) do
|
||||||
|
# Apply variant defaults for nil values
|
||||||
|
defaults = variant_defaults(assigns.variant)
|
||||||
|
|
||||||
|
assigns =
|
||||||
|
assigns
|
||||||
|
|> assign_new(:show_category_resolved, fn ->
|
||||||
|
if assigns.show_category == nil, do: defaults.show_category, else: assigns.show_category
|
||||||
|
end)
|
||||||
|
|> assign_new(:show_badges_resolved, fn ->
|
||||||
|
if assigns.show_badges == nil, do: defaults.show_badges, else: assigns.show_badges
|
||||||
|
end)
|
||||||
|
|> assign_new(:show_delivery_text_resolved, fn ->
|
||||||
|
if assigns.show_delivery_text == nil,
|
||||||
|
do: defaults.show_delivery_text,
|
||||||
|
else: assigns.show_delivery_text
|
||||||
|
end)
|
||||||
|
|> assign_new(:clickable_resolved, fn ->
|
||||||
|
if assigns.clickable == nil, do: defaults.clickable, else: assigns.clickable
|
||||||
|
end)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<%= if @clickable_resolved do %>
|
||||||
|
<%= if @mode == :preview do %>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
phx-click="change_preview_page"
|
||||||
|
phx-value-page="pdp"
|
||||||
|
class={card_classes(@variant)}
|
||||||
|
style={card_style(@variant)}
|
||||||
|
>
|
||||||
|
<.product_card_inner
|
||||||
|
product={@product}
|
||||||
|
theme_settings={@theme_settings}
|
||||||
|
variant={@variant}
|
||||||
|
show_category={@show_category_resolved}
|
||||||
|
show_badges={@show_badges_resolved}
|
||||||
|
show_delivery_text={@show_delivery_text_resolved}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<% else %>
|
||||||
|
<a
|
||||||
|
href={"/products/#{@product[:slug] || @product[:id]}"}
|
||||||
|
class={card_classes(@variant)}
|
||||||
|
style={card_style(@variant)}
|
||||||
|
>
|
||||||
|
<.product_card_inner
|
||||||
|
product={@product}
|
||||||
|
theme_settings={@theme_settings}
|
||||||
|
variant={@variant}
|
||||||
|
show_category={@show_category_resolved}
|
||||||
|
show_badges={@show_badges_resolved}
|
||||||
|
show_delivery_text={@show_delivery_text_resolved}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<div
|
||||||
|
class={card_classes(@variant)}
|
||||||
|
style={card_style(@variant)}
|
||||||
|
>
|
||||||
|
<.product_card_inner
|
||||||
|
product={@product}
|
||||||
|
theme_settings={@theme_settings}
|
||||||
|
variant={@variant}
|
||||||
|
show_category={@show_category_resolved}
|
||||||
|
show_badges={@show_badges_resolved}
|
||||||
|
show_delivery_text={@show_delivery_text_resolved}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
attr :product, :map, required: true
|
||||||
|
attr :theme_settings, :map, required: true
|
||||||
|
attr :variant, :atom, required: true
|
||||||
|
attr :show_category, :boolean, required: true
|
||||||
|
attr :show_badges, :boolean, required: true
|
||||||
|
attr :show_delivery_text, :boolean, required: true
|
||||||
|
|
||||||
|
defp product_card_inner(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class={image_container_classes(@variant)}>
|
||||||
|
<%= if @show_badges do %>
|
||||||
|
<.product_badge product={@product} />
|
||||||
|
<% end %>
|
||||||
|
<img
|
||||||
|
src={@product.image_url}
|
||||||
|
alt={@product.name}
|
||||||
|
loading={if @variant == :minimal, do: nil, else: "lazy"}
|
||||||
|
decoding={if @variant == :minimal, do: nil, else: "async"}
|
||||||
|
class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
|
||||||
|
/>
|
||||||
|
<%= if @theme_settings.hover_image && @product[:hover_image_url] do %>
|
||||||
|
<img
|
||||||
|
src={@product.hover_image_url}
|
||||||
|
alt={@product.name}
|
||||||
|
loading={if @variant == :minimal, do: nil, else: "lazy"}
|
||||||
|
decoding={if @variant == :minimal, do: nil, else: "async"}
|
||||||
|
class="product-image-hover w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class={content_padding_class(@variant)}>
|
||||||
|
<%= if @show_category && @product[:category] do %>
|
||||||
|
<p class="text-xs mb-1" style="color: var(--t-text-tertiary);">
|
||||||
|
<%= @product.category %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<h3 class={title_classes(@variant)} style={title_style(@variant)}>
|
||||||
|
<%= @product.name %>
|
||||||
|
</h3>
|
||||||
|
<%= if @theme_settings.show_prices do %>
|
||||||
|
<.product_price product={@product} variant={@variant} />
|
||||||
|
<% end %>
|
||||||
|
<%= if @show_delivery_text do %>
|
||||||
|
<p class="text-xs mt-1" style="color: var(--t-text-tertiary);">
|
||||||
|
Free delivery over £40
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
attr :product, :map, required: true
|
||||||
|
|
||||||
|
defp product_badge(assigns) do
|
||||||
|
~H"""
|
||||||
|
<%= cond do %>
|
||||||
|
<% Map.get(@product, :in_stock, true) == false -> %>
|
||||||
|
<span class="product-badge badge-sold-out">Sold out</span>
|
||||||
|
<% @product[:is_new] -> %>
|
||||||
|
<span class="product-badge badge-new">New</span>
|
||||||
|
<% @product[:on_sale] -> %>
|
||||||
|
<span class="product-badge badge-sale">Sale</span>
|
||||||
|
<% true -> %>
|
||||||
|
<% end %>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
attr :product, :map, required: true
|
||||||
|
attr :variant, :atom, required: true
|
||||||
|
|
||||||
|
defp product_price(assigns) do
|
||||||
|
~H"""
|
||||||
|
<%= case @variant do %>
|
||||||
|
<% :default -> %>
|
||||||
|
<div>
|
||||||
|
<%= if @product.on_sale do %>
|
||||||
|
<span class="text-lg font-bold" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
|
||||||
|
£<%= @product.price / 100 %>
|
||||||
|
</span>
|
||||||
|
<span class="text-sm line-through ml-2" style="color: var(--t-text-tertiary);">
|
||||||
|
£<%= @product.compare_at_price / 100 %>
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<span class="text-lg font-bold" style="color: var(--t-text-primary);">
|
||||||
|
£<%= @product.price / 100 %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% :featured -> %>
|
||||||
|
<p class="text-sm" style="color: var(--t-text-secondary);">
|
||||||
|
<%= if @product.on_sale do %>
|
||||||
|
<span class="line-through mr-1" style="color: var(--t-text-tertiary);">£<%= @product.compare_at_price / 100 %></span>
|
||||||
|
<% end %>
|
||||||
|
£<%= @product.price / 100 %>
|
||||||
|
</p>
|
||||||
|
<% :compact -> %>
|
||||||
|
<p class="font-bold" style="color: var(--t-text-primary);">
|
||||||
|
£<%= @product.price / 100 %>
|
||||||
|
</p>
|
||||||
|
<% :minimal -> %>
|
||||||
|
<p class="text-xs" style="color: var(--t-text-secondary);">
|
||||||
|
£<%= @product.price / 100 %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp variant_defaults(:default),
|
||||||
|
do: %{show_category: true, show_badges: true, show_delivery_text: true, clickable: true}
|
||||||
|
|
||||||
|
defp variant_defaults(:featured),
|
||||||
|
do: %{show_category: false, show_badges: true, show_delivery_text: true, clickable: true}
|
||||||
|
|
||||||
|
defp variant_defaults(:compact),
|
||||||
|
do: %{show_category: false, show_badges: false, show_delivery_text: false, clickable: true}
|
||||||
|
|
||||||
|
defp variant_defaults(:minimal),
|
||||||
|
do: %{show_category: false, show_badges: false, show_delivery_text: false, clickable: false}
|
||||||
|
|
||||||
|
defp card_classes(:default), do: "product-card group overflow-hidden transition-all"
|
||||||
|
defp card_classes(:featured), do: "product-card group overflow-hidden transition-all hover:-translate-y-1"
|
||||||
|
defp card_classes(:compact), do: "product-card group overflow-hidden cursor-pointer"
|
||||||
|
defp card_classes(:minimal), do: "product-card group overflow-hidden"
|
||||||
|
|
||||||
|
defp card_style(:default),
|
||||||
|
do: "background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card); cursor: pointer;"
|
||||||
|
|
||||||
|
defp card_style(:featured),
|
||||||
|
do: "background-color: var(--t-surface-raised); border-radius: var(--t-radius-card); cursor: pointer;"
|
||||||
|
|
||||||
|
defp card_style(:compact),
|
||||||
|
do: "background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||||
|
|
||||||
|
defp card_style(:minimal),
|
||||||
|
do: "background-color: var(--t-surface-raised); border: 1px solid var(--t-border-subtle); border-radius: var(--t-radius-card);"
|
||||||
|
|
||||||
|
defp image_container_classes(:compact),
|
||||||
|
do: "product-image-container aspect-square bg-gray-200 overflow-hidden relative"
|
||||||
|
|
||||||
|
defp image_container_classes(:minimal),
|
||||||
|
do: "product-image-container aspect-square bg-gray-200 overflow-hidden relative"
|
||||||
|
|
||||||
|
defp image_container_classes(_),
|
||||||
|
do: "product-image-container bg-gray-200 overflow-hidden relative"
|
||||||
|
|
||||||
|
defp content_padding_class(:compact), do: "p-3"
|
||||||
|
defp content_padding_class(:minimal), do: "p-2"
|
||||||
|
defp content_padding_class(_), do: ""
|
||||||
|
|
||||||
|
defp title_classes(:default), do: "font-semibold mb-2"
|
||||||
|
defp title_classes(:featured), do: "text-sm font-medium mb-1"
|
||||||
|
defp title_classes(:compact), do: "font-semibold text-sm mb-1"
|
||||||
|
defp title_classes(:minimal), do: "text-xs font-semibold truncate"
|
||||||
|
|
||||||
|
defp title_style(:default), do: "font-family: var(--t-font-heading); color: var(--t-text-primary);"
|
||||||
|
defp title_style(:featured), do: "color: var(--t-text-primary);"
|
||||||
|
defp title_style(:compact), do: "font-family: var(--t-font-heading); color: var(--t-text-primary);"
|
||||||
|
defp title_style(:minimal), do: "color: var(--t-text-primary);"
|
||||||
end
|
end
|
||||||
|
|||||||
@ -62,68 +62,13 @@
|
|||||||
end
|
end
|
||||||
]}>
|
]}>
|
||||||
<%= for product <- @preview_data.products do %>
|
<%= for product <- @preview_data.products do %>
|
||||||
<div
|
<.product_card
|
||||||
phx-click="change_preview_page"
|
product={product}
|
||||||
phx-value-page="pdp"
|
theme_settings={@theme_settings}
|
||||||
class="product-card group overflow-hidden transition-all"
|
mode={:preview}
|
||||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card); cursor: pointer;"
|
variant={:default}
|
||||||
>
|
show_category={true}
|
||||||
<div class="product-image-container bg-gray-200 overflow-hidden relative">
|
|
||||||
<!-- Product Badge -->
|
|
||||||
<%= cond do %>
|
|
||||||
<% not product.in_stock -> %>
|
|
||||||
<span class="product-badge badge-sold-out">Sold out</span>
|
|
||||||
<% product.on_sale -> %>
|
|
||||||
<span class="product-badge badge-sale">Sale</span>
|
|
||||||
<% true -> %>
|
|
||||||
<% end %>
|
|
||||||
<!-- Primary Image -->
|
|
||||||
<img
|
|
||||||
src={product.image_url}
|
|
||||||
alt={product.name}
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
|
|
||||||
/>
|
/>
|
||||||
<!-- Hover Image -->
|
|
||||||
<%= if @theme_settings.hover_image && product[:hover_image_url] do %>
|
|
||||||
<img
|
|
||||||
src={product.hover_image_url}
|
|
||||||
alt={product.name}
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
class="product-image-hover w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-xs mb-1" style="color: var(--t-text-tertiary);">
|
|
||||||
<%= product.category %>
|
|
||||||
</p>
|
|
||||||
<h3 class="font-semibold mb-2" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
|
|
||||||
<%= product.name %>
|
|
||||||
</h3>
|
|
||||||
<%= if @theme_settings.show_prices do %>
|
|
||||||
<div>
|
|
||||||
<%= if product.on_sale do %>
|
|
||||||
<span class="text-lg font-bold" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
|
|
||||||
£<%= product.price / 100 %>
|
|
||||||
</span>
|
|
||||||
<span class="text-sm line-through ml-2" style="color: var(--t-text-tertiary);">
|
|
||||||
£<%= product.compare_at_price / 100 %>
|
|
||||||
</span>
|
|
||||||
<% else %>
|
|
||||||
<span class="text-lg font-bold" style="color: var(--t-text-primary);">
|
|
||||||
£<%= product.price / 100 %>
|
|
||||||
</span>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<p class="text-xs mt-1" style="color: var(--t-text-tertiary);">
|
|
||||||
Free delivery over £40
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -42,35 +42,12 @@
|
|||||||
|
|
||||||
<div class="product-grid mt-12 grid gap-4 grid-cols-2 md:grid-cols-4 max-w-xl mx-auto">
|
<div class="product-grid mt-12 grid gap-4 grid-cols-2 md:grid-cols-4 max-w-xl mx-auto">
|
||||||
<%= for product <- Enum.take(@preview_data.products, 4) do %>
|
<%= for product <- Enum.take(@preview_data.products, 4) do %>
|
||||||
<div
|
<.product_card
|
||||||
class="product-card group overflow-hidden"
|
product={product}
|
||||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-subtle); border-radius: var(--t-radius-card);"
|
theme_settings={@theme_settings}
|
||||||
>
|
mode={:preview}
|
||||||
<div class="product-image-container aspect-square bg-gray-200 overflow-hidden relative">
|
variant={:minimal}
|
||||||
<img
|
|
||||||
src={product.image_url}
|
|
||||||
alt={product.name}
|
|
||||||
class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
|
|
||||||
/>
|
/>
|
||||||
<%= if @theme_settings.hover_image && product[:hover_image_url] do %>
|
|
||||||
<img
|
|
||||||
src={product.hover_image_url}
|
|
||||||
alt={product.name}
|
|
||||||
class="product-image-hover w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div class="p-2">
|
|
||||||
<p class="text-xs font-semibold truncate" style="color: var(--t-text-primary);">
|
|
||||||
<%= product.name %>
|
|
||||||
</p>
|
|
||||||
<%= if @theme_settings.show_prices do %>
|
|
||||||
<p class="text-xs" style="color: var(--t-text-secondary);">
|
|
||||||
£<%= product.price / 100 %>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -62,53 +62,12 @@
|
|||||||
end
|
end
|
||||||
]}>
|
]}>
|
||||||
<%= for product <- Enum.take(@preview_data.products, 4) do %>
|
<%= for product <- Enum.take(@preview_data.products, 4) do %>
|
||||||
<div
|
<.product_card
|
||||||
phx-click="change_preview_page"
|
product={product}
|
||||||
phx-value-page="pdp"
|
theme_settings={@theme_settings}
|
||||||
class="product-card group overflow-hidden transition-all hover:-translate-y-1"
|
mode={:preview}
|
||||||
style="background-color: var(--t-surface-raised); border-radius: var(--t-radius-card); cursor: pointer;"
|
variant={:featured}
|
||||||
>
|
|
||||||
<div class="product-image-container bg-gray-200 overflow-hidden relative">
|
|
||||||
<%= if product[:is_new] do %>
|
|
||||||
<span class="product-badge badge-new">New</span>
|
|
||||||
<% end %>
|
|
||||||
<%= if product.on_sale do %>
|
|
||||||
<span class="product-badge badge-sale">Sale</span>
|
|
||||||
<% end %>
|
|
||||||
<img
|
|
||||||
src={product.image_url}
|
|
||||||
alt={product.name}
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
|
|
||||||
/>
|
/>
|
||||||
<%= if @theme_settings.hover_image && product[:hover_image_url] do %>
|
|
||||||
<img
|
|
||||||
src={product.hover_image_url}
|
|
||||||
alt={product.name}
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
class="product-image-hover w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-sm font-medium mb-1" style="color: var(--t-text-primary);">
|
|
||||||
<%= product.name %>
|
|
||||||
</h3>
|
|
||||||
<%= if @theme_settings.show_prices do %>
|
|
||||||
<p class="text-sm" style="color: var(--t-text-secondary);">
|
|
||||||
<%= if product.on_sale do %>
|
|
||||||
<span class="line-through mr-1" style="color: var(--t-text-tertiary);">£<%= product.compare_at_price / 100 %></span>
|
|
||||||
<% end %>
|
|
||||||
£<%= product.price / 100 %>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
<p class="text-xs mt-1" style="color: var(--t-text-tertiary);">
|
|
||||||
Free delivery over £40
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -404,42 +404,12 @@
|
|||||||
|
|
||||||
<div class="product-grid grid gap-6 grid-cols-2 md:grid-cols-4">
|
<div class="product-grid grid gap-6 grid-cols-2 md:grid-cols-4">
|
||||||
<%= for related_product <- Enum.slice(@preview_data.products, 1, 4) do %>
|
<%= for related_product <- Enum.slice(@preview_data.products, 1, 4) do %>
|
||||||
<a
|
<.product_card
|
||||||
href="#"
|
product={related_product}
|
||||||
phx-click="change_preview_page"
|
theme_settings={@theme_settings}
|
||||||
phx-value-page="pdp"
|
mode={:preview}
|
||||||
class="product-card group overflow-hidden cursor-pointer"
|
variant={:compact}
|
||||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
|
||||||
>
|
|
||||||
<div class="product-image-container aspect-square bg-gray-200 overflow-hidden relative">
|
|
||||||
<img
|
|
||||||
src={related_product.image_url}
|
|
||||||
alt={related_product.name}
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
|
|
||||||
/>
|
/>
|
||||||
<%= if @theme_settings.hover_image && related_product[:hover_image_url] do %>
|
|
||||||
<img
|
|
||||||
src={related_product.hover_image_url}
|
|
||||||
alt={related_product.name}
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
class="product-image-hover w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div class="p-3">
|
|
||||||
<h3 class="font-semibold text-sm mb-1" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
|
|
||||||
<%= related_product.name %>
|
|
||||||
</h3>
|
|
||||||
<%= if @theme_settings.show_prices do %>
|
|
||||||
<p class="font-bold" style="color: var(--t-text-primary);">
|
|
||||||
£<%= related_product.price / 100 %>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user