Packages
phoenix_kit
1.7.66
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/shop/web/components/shop_cards.ex
defmodule PhoenixKit.Modules.Shop.Web.Components.ShopCards do
@moduledoc """
Reusable product display components for the shop storefront.
Provides product card and pagination components shared between
the main catalog page and category pages.
"""
use Phoenix.Component
import PhoenixKitWeb.Components.Core.Icon, only: [icon: 1]
alias PhoenixKit.Modules.Shop
alias PhoenixKit.Modules.Shop.Translations
alias PhoenixKit.Modules.Shop.Web.Components.FilterHelpers
alias PhoenixKit.Modules.Shop.Web.Helpers
@doc """
Renders a product card with image, title, price, and optional category badge.
"""
attr :product, :map, required: true
attr :currency, :any, required: true
attr :language, :string, default: "en"
attr :filter_qs, :string, default: ""
attr :show_category, :boolean, default: false
def product_card(assigns) do
assigns =
assigns
|> assign(:product_title, Translations.get(assigns.product, :title, assigns.language))
|> assign(:product_url, Shop.product_url(assigns.product, assigns.language))
|> assign(:product_image_url, Helpers.first_image(assigns.product))
|> assign(
:category_name,
if(assigns.show_category && assigns.product.category,
do: Translations.get(assigns.product.category, :name, assigns.language),
else: nil
)
)
~H"""
<.link
navigate={@product_url <> @filter_qs}
class="card bg-base-100 shadow-md hover:shadow-xl transition-all hover:-translate-y-1"
>
<figure class="h-48 bg-base-200">
<%= if @product_image_url do %>
<img
src={@product_image_url}
alt={@product_title}
class="w-full h-full object-cover"
/>
<% else %>
<div class="w-full h-full flex items-center justify-center">
<.icon name="hero-cube" class="w-16 h-16 opacity-30" />
</div>
<% end %>
</figure>
<div class="card-body p-4">
<h3 class="card-title text-base line-clamp-2">{@product_title}</h3>
<div class="flex items-center gap-2">
<span class="text-lg font-bold text-primary">
{Helpers.format_price(@product.price, @currency)}
</span>
<%= if @product.compare_at_price && Decimal.compare(@product.compare_at_price, @product.price) == :gt do %>
<span class="text-sm text-base-content/40 line-through">
{Helpers.format_price(@product.compare_at_price, @currency)}
</span>
<% end %>
</div>
<%= if @category_name do %>
<div class="mt-2">
<span class="badge badge-ghost badge-sm">{@category_name}</span>
</div>
<% end %>
</div>
</.link>
"""
end
@doc """
Renders a "load more" button + page links for product grids.
"""
attr :page, :integer, required: true
attr :total_pages, :integer, required: true
attr :total_products, :integer, required: true
attr :per_page, :integer, required: true
attr :base_path, :string, required: true
attr :active_filters, :map, default: %{}
attr :enabled_filters, :list, default: []
def shop_pagination(assigns) do
remaining = assigns.total_products - assigns.page * assigns.per_page
assigns =
assigns
|> assign(:remaining, max(0, remaining))
|> assign(:has_more, assigns.page < assigns.total_pages)
~H"""
<%= if @total_pages > 1 do %>
<div class="mt-8 space-y-4">
<%!-- Load More Button --%>
<%= if @has_more do %>
<div class="flex justify-center">
<button phx-click="load_more" class="btn btn-primary btn-lg gap-2">
<.icon name="hero-arrow-down" class="w-5 h-5" /> Show More
<span class="badge badge-ghost">{@remaining}</span>
</button>
</div>
<% end %>
<%!-- Page Links for SEO and direct access --%>
<nav class="flex flex-wrap justify-center gap-2 text-sm">
<%= for p <- 1..@total_pages do %>
<.link
patch={
FilterHelpers.build_filter_url(@base_path, @active_filters, @enabled_filters, page: p)
}
class={[
"px-3 py-1 rounded transition-colors",
if(p <= @page,
do: "bg-primary text-primary-content",
else: "bg-base-200 hover:bg-base-300 text-base-content/70"
)
]}
>
{p}
</.link>
<% end %>
</nav>
<%!-- Status text --%>
<p class="text-center text-sm text-base-content/50">
Showing {min(@page * @per_page, @total_products)} of {@total_products} products
</p>
</div>
<% end %>
"""
end
end