refactor: extract product_grid to shared ShopComponents module

Create a reusable product grid component that handles responsive
column layouts. Supports two modes:

- Theme-based columns: Uses theme_settings.grid_columns for lg breakpoint
- Fixed columns: Use columns={:fixed_4} for 2/4 column layouts

Used in: home (featured), collection, pdp (related), error (suggestions)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jamey Greenwood 2026-01-17 14:45:34 +00:00
parent 9746bf183c
commit b2869514cb
5 changed files with 95 additions and 33 deletions

View File

@ -781,4 +781,82 @@ defmodule SimpleshopThemeWeb.ShopComponents do
defp title_style(:featured), do: "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(:compact), do: "font-family: var(--t-font-heading); color: var(--t-text-primary);"
defp title_style(:minimal), do: "color: var(--t-text-primary);" defp title_style(:minimal), do: "color: var(--t-text-primary);"
@doc """
Renders a responsive product grid container.
This component wraps product cards in a responsive grid layout. It supports
theme-based column settings or fixed column layouts for specific use cases.
## Attributes
* `theme_settings` - Optional. When provided, uses `grid_columns` setting for lg breakpoint.
* `columns` - Optional. Fixed column count for lg breakpoint (overrides theme_settings).
Use `:fixed_4` for a fixed 2/4 column layout (error page, related products).
* `gap` - Optional. Gap size. Defaults to standard gap.
* `class` - Optional. Additional CSS classes to add.
## Slots
* `inner_block` - Required. The product cards to render inside the grid.
## Examples
<.product_grid theme_settings={@theme_settings}>
<%= for product <- @products do %>
<.product_card product={product} theme_settings={@theme_settings} />
<% end %>
</.product_grid>
<.product_grid columns={:fixed_4} gap="gap-6">
...
</.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
~H"""
<div class={grid_classes(@theme_settings, @columns, @gap, @class)}>
<%= 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)
|> Enum.reject(&(&1 == ""))
|> Enum.join(" ")
end
end end

View File

@ -52,15 +52,7 @@
</div> </div>
<!-- Product Grid --> <!-- Product Grid -->
<div class={[ <.product_grid theme_settings={@theme_settings}>
"product-grid grid grid-cols-1 sm:grid-cols-2",
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
]}>
<%= for product <- @preview_data.products do %> <%= for product <- @preview_data.products do %>
<.product_card <.product_card
product={product} product={product}
@ -70,7 +62,7 @@
show_category={true} show_category={true}
/> />
<% end %> <% end %>
</div> </.product_grid>
</div> </div>
</main> </main>

View File

@ -40,7 +40,7 @@
</button> </button>
</div> </div>
<div class="product-grid mt-12 grid gap-4 grid-cols-2 md:grid-cols-4 max-w-xl mx-auto"> <.product_grid columns={:fixed_4} gap="gap-4" class="mt-12 max-w-xl mx-auto">
<%= for product <- Enum.take(@preview_data.products, 4) do %> <%= for product <- Enum.take(@preview_data.products, 4) do %>
<.product_card <.product_card
product={product} product={product}
@ -49,7 +49,7 @@
variant={:minimal} variant={:minimal}
/> />
<% end %> <% end %>
</div> </.product_grid>
</div> </div>
</main> </main>

View File

@ -52,15 +52,7 @@
Featured products Featured products
</h2> </h2>
<div class={[ <.product_grid theme_settings={@theme_settings}>
"product-grid grid grid-cols-1 sm:grid-cols-2",
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
]}>
<%= for product <- Enum.take(@preview_data.products, 4) do %> <%= for product <- Enum.take(@preview_data.products, 4) do %>
<.product_card <.product_card
product={product} product={product}
@ -69,7 +61,7 @@
variant={:featured} variant={:featured}
/> />
<% end %> <% end %>
</div> </.product_grid>
<div class="text-center mt-8"> <div class="text-center mt-8">
<button <button

View File

@ -402,7 +402,7 @@
You might also like You might also like
</h2> </h2>
<div class="product-grid grid gap-6 grid-cols-2 md:grid-cols-4"> <.product_grid columns={:fixed_4} gap="gap-6">
<%= for related_product <- Enum.slice(@preview_data.products, 1, 4) do %> <%= for related_product <- Enum.slice(@preview_data.products, 1, 4) do %>
<.product_card <.product_card
product={related_product} product={related_product}
@ -411,7 +411,7 @@
variant={:compact} variant={:compact}
/> />
<% end %> <% end %>
</div> </.product_grid>
</div> </div>
<% end %> <% end %>
</main> </main>