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:
@@ -62,68 +62,13 @@
|
||||
end
|
||||
]}>
|
||||
<%= for product <- @preview_data.products do %>
|
||||
<div
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="pdp"
|
||||
class="product-card group overflow-hidden transition-all"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card); cursor: pointer;"
|
||||
>
|
||||
<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>
|
||||
<.product_card
|
||||
product={product}
|
||||
theme_settings={@theme_settings}
|
||||
mode={:preview}
|
||||
variant={:default}
|
||||
show_category={true}
|
||||
/>
|
||||
<% end %>
|
||||
</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">
|
||||
<%= for product <- Enum.take(@preview_data.products, 4) do %>
|
||||
<div
|
||||
class="product-card group overflow-hidden"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-subtle); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<div class="product-image-container aspect-square bg-gray-200 overflow-hidden relative">
|
||||
<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>
|
||||
<.product_card
|
||||
product={product}
|
||||
theme_settings={@theme_settings}
|
||||
mode={:preview}
|
||||
variant={:minimal}
|
||||
/>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,53 +62,12 @@
|
||||
end
|
||||
]}>
|
||||
<%= for product <- Enum.take(@preview_data.products, 4) do %>
|
||||
<div
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="pdp"
|
||||
class="product-card group overflow-hidden transition-all hover:-translate-y-1"
|
||||
style="background-color: var(--t-surface-raised); border-radius: var(--t-radius-card); cursor: pointer;"
|
||||
>
|
||||
<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>
|
||||
<.product_card
|
||||
product={product}
|
||||
theme_settings={@theme_settings}
|
||||
mode={:preview}
|
||||
variant={:featured}
|
||||
/>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -404,42 +404,12 @@
|
||||
|
||||
<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 %>
|
||||
<a
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="pdp"
|
||||
class="product-card group overflow-hidden cursor-pointer"
|
||||
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>
|
||||
<.product_card
|
||||
product={related_product}
|
||||
theme_settings={@theme_settings}
|
||||
mode={:preview}
|
||||
variant={:compact}
|
||||
/>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user