replace Tailwind utilities in product + cart components with CSS (Phase 5b)
Remove ~140 Tailwind utility classes from product.ex and cart.ex, replacing with semantic CSS classes in components.css. Delete helper functions that generated Tailwind class strings (card_classes, image_container_classes, content_padding_class, title_classes, hero_cta_classes, grid_classes). Use data-* attributes for variant styling, grid columns, and sticky positioning. Update theme-layer2 selectors for renamed classes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
mode={@mode}
|
||||
/>
|
||||
|
||||
<.product_grid columns={:fixed_4} gap="gap-4" class="mt-12 max-w-xl mx-auto">
|
||||
<.product_grid columns={:fixed_4}>
|
||||
<%= for product <- Enum.take(assigns[:products] || [], 4) do %>
|
||||
<.product_card
|
||||
product={product}
|
||||
|
||||
@@ -92,7 +92,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
aria-label="Close cart"
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5"
|
||||
class="cart-drawer-close-icon"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -134,7 +134,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
<%= if @mode == :preview do %>
|
||||
<button
|
||||
type="button"
|
||||
class="cart-drawer-checkout w-full mb-2"
|
||||
class="cart-drawer-checkout"
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
@@ -143,7 +143,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
|
||||
<button
|
||||
type="submit"
|
||||
class="cart-drawer-checkout w-full mb-2"
|
||||
class="cart-drawer-checkout"
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
@@ -213,24 +213,24 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
|
||||
<div class="cart-item-actions">
|
||||
<%= if @show_quantity_controls do %>
|
||||
<div class="cart-qty-group flex items-center">
|
||||
<div class="cart-qty-group">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="decrement"
|
||||
phx-value-id={@item.variant_id}
|
||||
class="cart-qty-btn px-3 py-1"
|
||||
class="cart-qty-btn"
|
||||
aria-label={"Decrease quantity of #{@item.name}"}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span class="cart-qty-display px-3 py-1 border-x">
|
||||
<span class="cart-qty-display">
|
||||
{@item.quantity}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="increment"
|
||||
phx-value-id={@item.variant_id}
|
||||
class="cart-qty-btn px-3 py-1"
|
||||
class="cart-qty-btn"
|
||||
aria-label={"Increase quantity of #{@item.name}"}
|
||||
>
|
||||
+
|
||||
@@ -246,7 +246,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cart-item-price-col text-right">
|
||||
<div class="cart-item-price-col">
|
||||
<p class="cart-item-price" data-size={if @size == :compact, do: "compact"}>
|
||||
{SimpleshopTheme.Cart.format_price(@item.price * @item.quantity)}
|
||||
</p>
|
||||
@@ -262,9 +262,9 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
|
||||
def cart_empty_state(assigns) do
|
||||
~H"""
|
||||
<div class="cart-empty text-center py-8">
|
||||
<div class="cart-empty">
|
||||
<svg
|
||||
class="cart-empty-icon w-16 h-16 mx-auto mb-4"
|
||||
class="cart-empty-icon"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -276,7 +276,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<path d="M16 10a4 4 0 01-8 0"></path>
|
||||
</svg>
|
||||
<p class="mb-4">Your basket is empty</p>
|
||||
<p>Your basket is empty</p>
|
||||
<%= if @mode == :preview do %>
|
||||
<button
|
||||
type="button"
|
||||
@@ -334,43 +334,42 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
|
||||
def cart_item(assigns) do
|
||||
~H"""
|
||||
<.shop_card class="flex gap-4 p-4">
|
||||
<div class="cart-page-image w-24 h-24 flex-shrink-0 bg-gray-200 overflow-hidden">
|
||||
<.shop_card class="cart-page-item">
|
||||
<div class="cart-page-image">
|
||||
<img
|
||||
src={cart_item_image(@item.product)}
|
||||
alt={@item.product.title}
|
||||
width="96"
|
||||
height="96"
|
||||
loading="lazy"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<h3 class="cart-page-item-name font-semibold mb-1">
|
||||
<div class="cart-page-item-info">
|
||||
<h3 class="cart-page-item-name">
|
||||
{@item.product.title}
|
||||
</h3>
|
||||
<p class="cart-page-item-variant text-sm mb-2">
|
||||
<p class="cart-page-item-variant">
|
||||
{@item.variant}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="cart-qty-group flex items-center">
|
||||
<button class="cart-qty-btn px-3 py-1">−</button>
|
||||
<span class="cart-qty-display px-3 py-1 border-x">
|
||||
<div class="cart-page-item-actions">
|
||||
<div class="cart-qty-group">
|
||||
<button class="cart-qty-btn">−</button>
|
||||
<span class="cart-qty-display">
|
||||
{@item.quantity}
|
||||
</span>
|
||||
<button class="cart-qty-btn px-3 py-1">+</button>
|
||||
<button class="cart-qty-btn">+</button>
|
||||
</div>
|
||||
|
||||
<button class="cart-page-item-remove text-sm">
|
||||
<button class="cart-page-item-remove">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<p class="cart-page-item-price font-bold text-lg">
|
||||
<div class="cart-page-item-price-col">
|
||||
<p class="cart-page-item-price">
|
||||
{SimpleshopTheme.Cart.format_price(@item.product.cheapest_price * @item.quantity)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -391,8 +390,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
|
||||
defp delivery_line(assigns) do
|
||||
~H"""
|
||||
<div class="delivery-line flex justify-between items-center">
|
||||
<span class="flex items-center gap-1">
|
||||
<div class="delivery-line">
|
||||
<span class="delivery-line-label">
|
||||
Delivery to
|
||||
<%= if @available_countries != [] and @mode != :preview do %>
|
||||
<form phx-change="change_country">
|
||||
@@ -445,15 +444,15 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
assign(assigns, :estimated_total, assigns.subtotal + (assigns.shipping_estimate || 0))
|
||||
|
||||
~H"""
|
||||
<.shop_card class="p-6 sticky top-4">
|
||||
<h2 class="order-summary-heading text-xl font-bold mb-6">
|
||||
<.shop_card class="order-summary-card">
|
||||
<h2 class="order-summary-heading">
|
||||
Order summary
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-col gap-3 mb-6">
|
||||
<div class="flex justify-between">
|
||||
<span class="order-summary-label">Subtotal</span>
|
||||
<span class="order-summary-value">
|
||||
<div class="order-summary-lines">
|
||||
<div class="order-summary-line">
|
||||
<span>Subtotal</span>
|
||||
<span>
|
||||
{SimpleshopTheme.Cart.format_price(@subtotal)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -463,12 +462,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
available_countries={@available_countries}
|
||||
mode={@mode}
|
||||
/>
|
||||
<div class="order-summary-divider border-t pt-3">
|
||||
<div class="flex justify-between text-lg">
|
||||
<span class="order-summary-value font-semibold">
|
||||
<div class="order-summary-divider">
|
||||
<div class="order-summary-total">
|
||||
<span>
|
||||
{if @shipping_estimate, do: "Estimated total", else: "Subtotal"}
|
||||
</span>
|
||||
<span class="order-summary-value font-bold">
|
||||
<span>
|
||||
{SimpleshopTheme.Cart.format_price(@estimated_total)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -476,26 +475,26 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
</div>
|
||||
|
||||
<%= if @mode == :preview do %>
|
||||
<.shop_button class="w-full px-6 py-3 font-semibold transition-all mb-3">
|
||||
<.shop_button class="order-summary-checkout">
|
||||
Checkout
|
||||
</.shop_button>
|
||||
<.shop_button_outline
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="collection"
|
||||
class="w-full px-6 py-3 font-semibold transition-all"
|
||||
class="order-summary-continue"
|
||||
>
|
||||
Continue shopping
|
||||
</.shop_button_outline>
|
||||
<% else %>
|
||||
<form action="/checkout" method="post" class="mb-3">
|
||||
<form action="/checkout" method="post" class="order-summary-checkout-form">
|
||||
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
|
||||
<.shop_button type="submit" class="w-full px-6 py-3 font-semibold transition-all">
|
||||
<.shop_button type="submit" class="order-summary-checkout">
|
||||
Checkout
|
||||
</.shop_button>
|
||||
</form>
|
||||
<.shop_link_outline
|
||||
href="/collections/all"
|
||||
class="block w-full px-6 py-3 font-semibold transition-all text-center"
|
||||
class="order-summary-continue"
|
||||
>
|
||||
Continue shopping
|
||||
</.shop_link_outline>
|
||||
@@ -524,13 +523,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
|
||||
def cart_layout(assigns) do
|
||||
~H"""
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div class="lg:col-span-2">
|
||||
<div class="flex flex-col gap-4">
|
||||
<%= for item <- @items do %>
|
||||
<.cart_item item={item} />
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="cart-layout">
|
||||
<div class="cart-items-stack">
|
||||
<%= for item <- @items do %>
|
||||
<.cart_item item={item} />
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -62,7 +62,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
~H"""
|
||||
<article
|
||||
class={card_classes(@variant)}
|
||||
class="product-card"
|
||||
data-variant={@variant}
|
||||
data-clickable={@clickable_resolved || nil}
|
||||
>
|
||||
@@ -103,7 +103,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|> assign(:has_hover_image, assigns.theme_settings.hover_image && hover_image != nil)
|
||||
|
||||
~H"""
|
||||
<div class={["product-card-image-wrap", image_container_classes(@variant)]}>
|
||||
<div class="product-card-image-wrap">
|
||||
<%= if @show_badges do %>
|
||||
<.product_badge product={@product} />
|
||||
<% end %>
|
||||
@@ -118,13 +118,13 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
alt={@product.title}
|
||||
variant={@variant}
|
||||
priority={@priority}
|
||||
class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
|
||||
class="product-image-primary"
|
||||
/>
|
||||
<.product_card_image
|
||||
image={@hover_image}
|
||||
alt={@product.title}
|
||||
variant={@variant}
|
||||
class="product-image-hover w-full h-full object-cover"
|
||||
class="product-image-hover"
|
||||
/>
|
||||
</div>
|
||||
<% else %>
|
||||
@@ -133,7 +133,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
alt={@product.title}
|
||||
variant={@variant}
|
||||
priority={@priority}
|
||||
class="product-image-primary w-full h-full object-cover"
|
||||
class="product-image-primary"
|
||||
/>
|
||||
<% end %>
|
||||
<%= if @has_hover_image do %>
|
||||
@@ -143,22 +143,22 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class={content_padding_class(@variant)}>
|
||||
<div class="product-card-content">
|
||||
<%= if @show_category && Map.get(@product, :category) do %>
|
||||
<%= if @mode == :preview do %>
|
||||
<p class="product-card-category text-xs mb-1">
|
||||
<p class="product-card-category">
|
||||
{@product.category}
|
||||
</p>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={"/collections/#{Slug.slugify(@product.category)}"}
|
||||
class="product-card-category text-xs mb-1 block hover:underline"
|
||||
class="product-card-category"
|
||||
>
|
||||
{@product.category}
|
||||
</.link>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<h3 class={["product-card-title", title_classes(@variant)]}>
|
||||
<h3 class="product-card-title">
|
||||
<%= if @clickable do %>
|
||||
<%= if @mode == :preview do %>
|
||||
<a
|
||||
@@ -185,7 +185,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<.product_price product={@product} variant={@variant} />
|
||||
<% end %>
|
||||
<%= if @show_delivery_text do %>
|
||||
<p class="product-card-delivery text-xs mt-1">
|
||||
<p class="product-card-delivery">
|
||||
Made to order
|
||||
</p>
|
||||
<% end %>
|
||||
@@ -226,7 +226,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<%= cond do %>
|
||||
<% is_nil(@src) -> %>
|
||||
<div
|
||||
class={[@class, "product-card-placeholder flex items-center justify-center"]}
|
||||
class={[@class, "product-card-placeholder"]}
|
||||
role="img"
|
||||
aria-label={@alt}
|
||||
>
|
||||
@@ -238,7 +238,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
stroke="currentColor"
|
||||
width="48"
|
||||
height="48"
|
||||
class="size-12 opacity-40"
|
||||
class="placeholder-icon"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@@ -297,33 +297,33 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<% :default -> %>
|
||||
<div>
|
||||
<%= if @product.on_sale do %>
|
||||
<span class="product-price--sale text-lg font-bold">
|
||||
<span class="product-price--sale">
|
||||
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
|
||||
</span>
|
||||
<span class="product-price--compare text-sm line-through ml-2">
|
||||
<span class="product-price--compare">
|
||||
{SimpleshopTheme.Cart.format_price(@product.compare_at_price)}
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="product-price--regular text-lg font-bold">
|
||||
<span class="product-price--regular">
|
||||
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% :featured -> %>
|
||||
<p class="product-price--secondary text-sm">
|
||||
<p class="product-price--secondary">
|
||||
<%= if @product.on_sale do %>
|
||||
<span class="product-price--compare line-through mr-1">
|
||||
<span class="product-price--compare">
|
||||
{SimpleshopTheme.Cart.format_price(@product.compare_at_price)}
|
||||
</span>
|
||||
<% end %>
|
||||
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
|
||||
</p>
|
||||
<% :compact -> %>
|
||||
<p class="product-price--regular font-bold">
|
||||
<p class="product-price--regular">
|
||||
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
|
||||
</p>
|
||||
<% :minimal -> %>
|
||||
<p class="product-price--secondary text-xs">
|
||||
<p class="product-price--secondary">
|
||||
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
|
||||
</p>
|
||||
<% end %>
|
||||
@@ -342,32 +342,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
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 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"
|
||||
|
||||
@doc """
|
||||
Renders a responsive product grid container.
|
||||
|
||||
@@ -394,57 +368,33 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<% end %>
|
||||
</.product_grid>
|
||||
|
||||
<.product_grid columns={:fixed_4} gap="gap-6">
|
||||
<.product_grid columns={:fixed_4}>
|
||||
...
|
||||
</.product_grid>
|
||||
"""
|
||||
attr :theme_settings, :map, default: nil
|
||||
attr :columns, :atom, default: nil
|
||||
attr :gap, :string, default: nil
|
||||
attr :class, :string, default: nil
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
def product_grid(assigns) do
|
||||
cols =
|
||||
cond do
|
||||
assigns.columns == :fixed_4 -> "fixed-4"
|
||||
assigns.theme_settings != nil -> assigns.theme_settings.grid_columns || "3"
|
||||
true -> "3"
|
||||
end
|
||||
|
||||
assigns = assign(assigns, :data_columns, cols)
|
||||
|
||||
~H"""
|
||||
<div class={grid_classes(@theme_settings, @columns, @gap, @class)}>
|
||||
<div class={["product-grid", @class]} data-columns={@data_columns}>
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp grid_classes(theme_settings, columns, gap, extra_class) do
|
||||
base = "product-grid grid"
|
||||
|
||||
cols =
|
||||
cond do
|
||||
columns == :fixed_4 ->
|
||||
"grid-cols-2 md:grid-cols-4"
|
||||
|
||||
theme_settings != nil ->
|
||||
responsive_cols = "grid-cols-1 sm:grid-cols-2"
|
||||
|
||||
lg_cols =
|
||||
case theme_settings.grid_columns do
|
||||
"2" -> "lg:grid-cols-2"
|
||||
"3" -> "lg:grid-cols-3"
|
||||
"4" -> "lg:grid-cols-4"
|
||||
_ -> "lg:grid-cols-3"
|
||||
end
|
||||
|
||||
"#{responsive_cols} #{lg_cols}"
|
||||
|
||||
true ->
|
||||
"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
|
||||
end
|
||||
|
||||
gap_class = gap || ""
|
||||
|
||||
[base, cols, gap_class, extra_class]
|
||||
|> Enum.reject(&(is_nil(&1) or &1 == ""))
|
||||
|> Enum.join(" ")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a centered hero section with title, description, and optional CTAs.
|
||||
|
||||
@@ -509,11 +459,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
~H"""
|
||||
<%= case @variant do %>
|
||||
<% :default -> %>
|
||||
<section class="hero-section text-center" data-background={@background}>
|
||||
<h1 class="t-heading text-3xl md:text-4xl mb-4">
|
||||
<section class="hero-section" data-background={@background}>
|
||||
<h1 class="t-heading">
|
||||
{@title}
|
||||
</h1>
|
||||
<p class="hero-description text-lg max-w-lg mx-auto mb-8">
|
||||
<p class="hero-description">
|
||||
{@description}
|
||||
</p>
|
||||
<.hero_cta
|
||||
@@ -526,29 +476,29 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
/>
|
||||
</section>
|
||||
<% :page -> %>
|
||||
<div class="hero-section--page text-center">
|
||||
<h1 class="t-heading text-4xl md:text-5xl mb-6">
|
||||
<div class="hero-section--page">
|
||||
<h1 class="t-heading">
|
||||
{@title}
|
||||
</h1>
|
||||
<p class="hero-description text-lg mb-12 max-w-2xl mx-auto">
|
||||
<p class="hero-description">
|
||||
{@description}
|
||||
</p>
|
||||
</div>
|
||||
<% :error -> %>
|
||||
<div class="text-center">
|
||||
<div class="hero-error">
|
||||
<%= if @pre_title do %>
|
||||
<h1 class="hero-pre-title text-8xl md:text-9xl mb-4">
|
||||
<h1 class="hero-pre-title">
|
||||
{@pre_title}
|
||||
</h1>
|
||||
<% end %>
|
||||
<h2 class="t-heading text-3xl md:text-4xl mb-6">
|
||||
<h2 class="t-heading">
|
||||
{@title}
|
||||
</h2>
|
||||
<p class="hero-description text-lg mb-8 max-w-md mx-auto">
|
||||
<p class="hero-description">
|
||||
{@description}
|
||||
</p>
|
||||
<%= if @cta_text || @secondary_cta_text do %>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<div class="hero-cta-group">
|
||||
<.hero_cta
|
||||
:if={@cta_text}
|
||||
text={@cta_text}
|
||||
@@ -579,19 +529,27 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
attr :variant, :atom, required: true
|
||||
|
||||
defp hero_cta(assigns) do
|
||||
base_class =
|
||||
case assigns.variant do
|
||||
:primary -> "themed-button hero-cta"
|
||||
:secondary -> "themed-button-outline hero-cta"
|
||||
end
|
||||
|
||||
assigns = assign(assigns, :cta_class, base_class)
|
||||
|
||||
~H"""
|
||||
<%= if @mode == :preview do %>
|
||||
<button
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page={@page}
|
||||
class={hero_cta_classes(@variant)}
|
||||
class={@cta_class}
|
||||
>
|
||||
{@text}
|
||||
</button>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={@href || "/"}
|
||||
class={["inline-block", hero_cta_classes(@variant)]}
|
||||
class={@cta_class}
|
||||
>
|
||||
{@text}
|
||||
</.link>
|
||||
@@ -599,11 +557,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
"""
|
||||
end
|
||||
|
||||
defp hero_cta_classes(:primary), do: "themed-button px-8 py-3 font-semibold transition-all"
|
||||
|
||||
defp hero_cta_classes(:secondary),
|
||||
do: "themed-button-outline px-8 py-3 font-semibold transition-all"
|
||||
|
||||
@doc """
|
||||
Renders a row of category circles for navigation.
|
||||
|
||||
@@ -626,17 +579,17 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
~H"""
|
||||
<section class="category-nav-section">
|
||||
<h2 class="sr-only">Shop by Category</h2>
|
||||
<nav class="grid grid-cols-3 gap-4 max-w-3xl mx-auto" aria-label="Product categories">
|
||||
<nav class="category-nav" aria-label="Product categories">
|
||||
<%= for category <- Enum.take(@categories, @limit) do %>
|
||||
<%= if @mode == :preview do %>
|
||||
<a
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="collection"
|
||||
class="category-card flex flex-col items-center gap-3 p-4 rounded-lg transition-colors hover:bg-black/5"
|
||||
class="category-card"
|
||||
>
|
||||
<div
|
||||
class="w-24 h-24 rounded-full bg-gray-200 bg-cover bg-center transition-transform hover:scale-105"
|
||||
class="category-image"
|
||||
style={
|
||||
if(category[:image_url],
|
||||
do: "background-image: url('#{category.image_url}');",
|
||||
@@ -645,17 +598,17 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
}
|
||||
>
|
||||
</div>
|
||||
<span class="category-name text-sm font-medium">
|
||||
<span class="category-name">
|
||||
{category.name}
|
||||
</span>
|
||||
</a>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={"/collections/#{category.slug}"}
|
||||
class="category-card flex flex-col items-center gap-3 p-4 rounded-lg transition-colors hover:bg-black/5"
|
||||
class="category-card"
|
||||
>
|
||||
<div
|
||||
class="w-24 h-24 rounded-full bg-gray-200 bg-cover bg-center transition-transform hover:scale-105"
|
||||
class="category-image"
|
||||
style={
|
||||
if(category[:image_url],
|
||||
do: "background-image: url('#{category.image_url}');",
|
||||
@@ -664,7 +617,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
}
|
||||
>
|
||||
</div>
|
||||
<span class="category-name text-sm font-medium">
|
||||
<span class="category-name">
|
||||
{category.name}
|
||||
</span>
|
||||
</.link>
|
||||
@@ -710,7 +663,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
def featured_products_section(assigns) do
|
||||
~H"""
|
||||
<section class="featured-section">
|
||||
<h2 class="t-heading text-2xl mb-6">
|
||||
<h2 class="t-heading">
|
||||
{@title}
|
||||
</h2>
|
||||
|
||||
@@ -726,19 +679,19 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<% end %>
|
||||
</.product_grid>
|
||||
|
||||
<div class="text-center mt-8">
|
||||
<div class="featured-cta-wrap">
|
||||
<%= if @mode == :preview do %>
|
||||
<button
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page={@cta_page}
|
||||
class="outline-button px-6 py-3 font-medium transition-all"
|
||||
class="outline-button"
|
||||
>
|
||||
{@cta_text}
|
||||
</button>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={@cta_href}
|
||||
class="outline-button inline-block px-6 py-3 font-medium transition-all"
|
||||
class="outline-button"
|
||||
>
|
||||
{@cta_text}
|
||||
</.link>
|
||||
@@ -784,7 +737,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def image_text_section(assigns) do
|
||||
~H"""
|
||||
<section class="image-text-section grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
||||
<section class="image-text-section">
|
||||
<%= if @image_position == :left do %>
|
||||
<.image_text_image image_url={@image_url} />
|
||||
<.image_text_content
|
||||
@@ -815,7 +768,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
defp image_text_image(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
class="image-text-image h-72 bg-cover bg-center"
|
||||
class="image-text-image"
|
||||
style={"background-image: url('#{@image_url}');"}
|
||||
>
|
||||
</div>
|
||||
@@ -832,10 +785,10 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
defp image_text_content(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<h2 class="t-heading text-2xl mb-4">
|
||||
<h2 class="t-heading">
|
||||
{@title}
|
||||
</h2>
|
||||
<p class="image-text-body text-base mb-4">
|
||||
<p class="image-text-body">
|
||||
{@description}
|
||||
</p>
|
||||
<%= if @link_text do %>
|
||||
@@ -844,14 +797,14 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page={@link_page}
|
||||
class="accent-link text-sm font-medium transition-colors"
|
||||
class="accent-link"
|
||||
>
|
||||
{@link_text}
|
||||
</a>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={@link_href || "/"}
|
||||
class="accent-link text-sm font-medium transition-colors"
|
||||
class="accent-link"
|
||||
>
|
||||
{@link_text}
|
||||
</.link>
|
||||
@@ -880,9 +833,9 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def collection_header(assigns) do
|
||||
~H"""
|
||||
<div class="collection-header-wrap border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<h1 class="t-heading text-3xl md:text-4xl mb-2">
|
||||
<div class="collection-header-wrap">
|
||||
<div class="collection-header-inner">
|
||||
<h1 class="t-heading">
|
||||
{@title}
|
||||
</h1>
|
||||
<%= if @subtitle do %>
|
||||
@@ -924,9 +877,9 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def filter_bar(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||
<div class="filter-bar">
|
||||
<!-- Category Pills -->
|
||||
<div class="filter-pills-container flex gap-2 overflow-x-auto">
|
||||
<div class="filter-pills-container">
|
||||
<button class={"filter-pill#{if @active_category == "All", do: " filter-pill-active", else: ""}"}>
|
||||
All
|
||||
</button>
|
||||
@@ -938,7 +891,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
</div>
|
||||
|
||||
<!-- Sort Dropdown -->
|
||||
<.shop_select options={@sort_options} class="px-4 py-2" />
|
||||
<.shop_select options={@sort_options} />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
@@ -980,12 +933,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page={item.page}
|
||||
class="hover:underline"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
<% else %>
|
||||
<.link navigate={item.href || "/"} class="hover:underline">{item.label}</.link>
|
||||
<.link navigate={item.href || "/"}>{item.label}</.link>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
@@ -1022,12 +974,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def related_products_section(assigns) do
|
||||
~H"""
|
||||
<div class="related-section py-12">
|
||||
<h2 class="t-heading text-2xl mb-6">
|
||||
<div class="related-section">
|
||||
<h2 class="t-heading">
|
||||
{@title}
|
||||
</h2>
|
||||
|
||||
<.product_grid columns={:fixed_4} gap="gap-6">
|
||||
<.product_grid columns={:fixed_4}>
|
||||
<%= for product <- Enum.take(@products, @limit) do %>
|
||||
<.product_card
|
||||
product={product}
|
||||
@@ -1062,7 +1014,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
~H"""
|
||||
<div class="pdp-gallery">
|
||||
<%!-- Image area (relative container for dots + desktop nav) --%>
|
||||
<div class="pdp-gallery-frame relative">
|
||||
<div class="pdp-gallery-frame">
|
||||
<%!-- Scroll-snap carousel (2+ images) or single image --%>
|
||||
<%= if length(@images) > 1 do %>
|
||||
<div
|
||||
@@ -1129,7 +1081,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<div class="pdp-gallery-single">
|
||||
<%= if @images == [] do %>
|
||||
<div
|
||||
class="product-card-placeholder w-full h-full flex items-center justify-center"
|
||||
class="product-card-placeholder"
|
||||
role="img"
|
||||
aria-label={@product_name}
|
||||
>
|
||||
@@ -1141,7 +1093,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
stroke="currentColor"
|
||||
width="48"
|
||||
height="48"
|
||||
class="size-12 opacity-40"
|
||||
class="placeholder-icon"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@@ -1156,7 +1108,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
alt={@product_name}
|
||||
width="600"
|
||||
height="600"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<div
|
||||
class="pdp-lightbox-click"
|
||||
@@ -1184,7 +1135,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<%= for {url, idx} <- Enum.with_index(@images) do %>
|
||||
<button
|
||||
type="button"
|
||||
class={"aspect-square bg-gray-200 overflow-hidden pdp-thumbnail#{if idx == 0, do: " pdp-thumbnail-active", else: ""}"}
|
||||
class={"pdp-thumbnail#{if idx == 0, do: " pdp-thumbnail-active", else: ""}"}
|
||||
aria-label={"View image #{idx + 1} of #{length(@images)}"}
|
||||
phx-click={
|
||||
Phoenix.LiveView.JS.dispatch("pdp:scroll-to",
|
||||
@@ -1207,7 +1158,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
width="150"
|
||||
height="150"
|
||||
loading="lazy"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
<% end %>
|
||||
@@ -1337,23 +1287,23 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
~H"""
|
||||
<div>
|
||||
<h1 class="t-heading text-3xl md:text-4xl mb-4">
|
||||
<h1 class="t-heading pdp-title">
|
||||
{@product.title}
|
||||
</h1>
|
||||
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="pdp-price-row">
|
||||
<%= if @product.on_sale do %>
|
||||
<span class="product-price--sale text-3xl font-bold">
|
||||
<span class="product-price--sale">
|
||||
{SimpleshopTheme.Cart.format_price(@price)}
|
||||
</span>
|
||||
<span class="product-price--compare text-xl line-through">
|
||||
<span class="product-price--compare">
|
||||
{SimpleshopTheme.Cart.format_price(@product.compare_at_price)}
|
||||
</span>
|
||||
<span class="sale-badge px-2 py-1 text-sm font-bold text-white rounded">
|
||||
<span class="sale-badge">
|
||||
SAVE {round((@product.compare_at_price - @price) / @product.compare_at_price * 100)}%
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="product-price--regular text-3xl font-bold">
|
||||
<span class="product-price--regular">
|
||||
{SimpleshopTheme.Cart.format_price(@price)}
|
||||
</span>
|
||||
<% end %>
|
||||
@@ -1390,14 +1340,14 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def variant_selector(assigns) do
|
||||
~H"""
|
||||
<div class="mb-6">
|
||||
<div class="variant-label block font-semibold mb-2">
|
||||
<div class="variant-selector">
|
||||
<div class="variant-label">
|
||||
{@option_type.name}<span
|
||||
:if={@selected}
|
||||
class="variant-label-value"
|
||||
>: {@selected}</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="variant-options">
|
||||
<%= if @option_type.type == :color do %>
|
||||
<.color_swatch
|
||||
:for={value <- @option_type.values}
|
||||
@@ -1437,10 +1387,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
phx-click={if @mode == :shop, do: "select_option"}
|
||||
phx-value-option={@option_name}
|
||||
phx-value-selected={@title}
|
||||
class={[
|
||||
"color-swatch w-10 h-10 rounded-full border-2 transition-all relative",
|
||||
@selected && "ring-2 ring-offset-2"
|
||||
]}
|
||||
class="color-swatch"
|
||||
style={"background-color: #{@hex};"}
|
||||
title={@title}
|
||||
aria-label={"Select #{@title}"}
|
||||
@@ -1463,7 +1410,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
phx-click={if @mode == :shop, do: "select_option"}
|
||||
phx-value-option={@option_name}
|
||||
phx-value-selected={@title}
|
||||
class="size-btn px-4 py-2 font-medium transition-all"
|
||||
class="size-btn"
|
||||
aria-pressed={to_string(@selected)}
|
||||
>
|
||||
{@title}
|
||||
@@ -1493,22 +1440,22 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def quantity_selector(assigns) do
|
||||
~H"""
|
||||
<div class="mb-8">
|
||||
<label class="qty-label block font-semibold mb-2">
|
||||
<div class="quantity-selector">
|
||||
<label class="qty-label">
|
||||
Quantity
|
||||
</label>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="qty-group flex items-center">
|
||||
<div class="qty-row">
|
||||
<div class="qty-group">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="decrement_quantity"
|
||||
disabled={@quantity <= @min}
|
||||
aria-label="Decrease quantity"
|
||||
class="qty-btn px-4 py-2 disabled:opacity-30"
|
||||
class="qty-btn"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span class="qty-display px-4 py-2 border-x-2 min-w-12 text-center tabular-nums">
|
||||
<span class="qty-display">
|
||||
{@quantity}
|
||||
</span>
|
||||
<button
|
||||
@@ -1516,15 +1463,15 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
phx-click="increment_quantity"
|
||||
disabled={@quantity >= @max}
|
||||
aria-label="Increase quantity"
|
||||
class="qty-btn px-4 py-2 disabled:opacity-30"
|
||||
class="qty-btn"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<%= if @in_stock do %>
|
||||
<span class="stock-in text-sm">In stock</span>
|
||||
<span class="stock-in">In stock</span>
|
||||
<% else %>
|
||||
<span class="stock-out text-sm font-semibold">Out of stock</span>
|
||||
<span class="stock-out">Out of stock</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1553,16 +1500,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def add_to_cart_button(assigns) do
|
||||
~H"""
|
||||
<div class={[
|
||||
"atc-wrap mb-4",
|
||||
@sticky &&
|
||||
"sticky bottom-0 z-10 py-3 md:relative md:py-0 -mx-4 px-4 md:mx-0 md:px-0 border-t md:border-0"
|
||||
]}>
|
||||
<div class="atc-wrap" data-sticky={to_string(@sticky)}>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={if @mode == :preview, do: open_cart_drawer_js(), else: "add_to_cart"}
|
||||
disabled={@disabled}
|
||||
class="atc-btn w-full px-6 py-4 text-lg font-semibold transition-all"
|
||||
class="atc-btn"
|
||||
>
|
||||
{@text}
|
||||
</button>
|
||||
@@ -1599,11 +1542,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def accordion_item(assigns) do
|
||||
~H"""
|
||||
<details open={@open} class="group">
|
||||
<summary class="accordion-summary flex justify-between items-center py-4 cursor-pointer list-none [&::-webkit-details-marker]:hidden">
|
||||
<span class="font-semibold">{@title}</span>
|
||||
<details open={@open}>
|
||||
<summary class="accordion-summary">
|
||||
<span class="accordion-title">{@title}</span>
|
||||
<svg
|
||||
class="w-5 h-5 transition-transform duration-200 group-open:rotate-180"
|
||||
class="accordion-icon"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
@@ -1613,7 +1556,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</summary>
|
||||
<div class="accordion-body pb-4">
|
||||
<div class="accordion-body">
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
</details>
|
||||
@@ -1651,25 +1594,25 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
end)
|
||||
|
||||
~H"""
|
||||
<div class="details-wrap mt-8 divide-y">
|
||||
<div class="details-wrap">
|
||||
<.accordion_item title="Description" open={true}>
|
||||
<p class="leading-relaxed">
|
||||
<p class="details-description">
|
||||
{@product.description}. Crafted with attention to detail and quality materials, this product is designed to last. Perfect for everyday use or special occasions.
|
||||
</p>
|
||||
</.accordion_item>
|
||||
|
||||
<%= if @show_size_guide do %>
|
||||
<.accordion_item title="Size Guide">
|
||||
<table class="w-full text-sm">
|
||||
<table class="details-table">
|
||||
<thead>
|
||||
<tr class="details-table-row">
|
||||
<th class="details-th text-left py-2 font-semibold">
|
||||
<th class="details-th">
|
||||
Size
|
||||
</th>
|
||||
<th class="details-th text-left py-2 font-semibold">
|
||||
<th class="details-th">
|
||||
Chest (cm)
|
||||
</th>
|
||||
<th class="details-th text-left py-2 font-semibold">
|
||||
<th class="details-th">
|
||||
Length (cm)
|
||||
</th>
|
||||
</tr>
|
||||
@@ -1677,9 +1620,9 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<tbody>
|
||||
<%= for {size_row, idx} <- Enum.with_index(@sizes) do %>
|
||||
<tr class={idx < length(@sizes) - 1 && "details-table-row"}>
|
||||
<td class="py-2">{size_row.size}</td>
|
||||
<td class="py-2">{size_row.chest}</td>
|
||||
<td class="py-2">{size_row.length}</td>
|
||||
<td class="details-td">{size_row.size}</td>
|
||||
<td class="details-td">{size_row.chest}</td>
|
||||
<td class="details-td">{size_row.length}</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
@@ -1688,16 +1631,16 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<% end %>
|
||||
|
||||
<.accordion_item title="Shipping & Returns">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="details-shipping">
|
||||
<div>
|
||||
<p class="details-subheading font-semibold mb-1">Delivery</p>
|
||||
<p class="text-sm">
|
||||
<p class="details-subheading">Delivery</p>
|
||||
<p class="details-shipping-text">
|
||||
Free UK delivery on orders over £40. Standard delivery 3-5 working days. Express delivery available at checkout.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="details-subheading font-semibold mb-1">Returns</p>
|
||||
<p class="text-sm">
|
||||
<p class="details-subheading">Returns</p>
|
||||
<p class="details-shipping-text">
|
||||
We offer a 30-day return policy. Items must be unused and in original packaging. Please contact us to arrange a return.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user