defmodule BerrypodWeb.ShopComponents.Cart do
@moduledoc false
use Phoenix.Component
import BerrypodWeb.ShopComponents.Base
alias Berrypod.Products.{Product, ProductImage}
defp close_cart_drawer_js do
Phoenix.LiveView.JS.push("close_cart_drawer")
end
@doc """
Renders the cart drawer (floating sidebar).
The drawer slides in from the right when opened. It displays cart items
and checkout options. Follows WAI-ARIA dialog pattern for accessibility.
## Attributes
* `cart_items` - List of cart items to display. Each item should have
`image`, `name`, `variant`, `price`, and `variant_id` keys. Default: []
* `subtotal` - The subtotal to display. Default: nil (shows "£0.00")
* `cart_count` - Number of items for screen reader description. Default: 0
* `mode` - Either `:live` (default) for real stores or `:preview` for theme editor.
In preview mode, "View basket" navigates via LiveView JS commands.
## Examples
<.cart_drawer cart_items={@cart.items} subtotal={@cart.subtotal} />
<.cart_drawer cart_items={demo_items} subtotal="£72.00" mode={:preview} />
"""
attr :cart_items, :list, default: []
attr :subtotal, :string, default: nil
attr :total, :string, default: nil
attr :cart_count, :integer, default: 0
attr :cart_status, :string, default: nil
attr :mode, :atom, default: :live
attr :open, :boolean, default: false
attr :shipping_estimate, :integer, default: nil
attr :country_code, :string, default: "GB"
attr :available_countries, :list, default: []
attr :stripe_connected, :boolean, default: true
def cart_drawer(assigns) do
assigns =
assign_new(assigns, :display_total, fn ->
assigns.total || assigns.subtotal || "£0.00"
end)
~H"""
<%!-- Screen reader announcements for cart changes --%>
{@cart_status}
Shopping basket with {@cart_count} {if @cart_count == 1, do: "item", else: "items"}. Press Escape to close.
<%= if @cart_items == [] do %>
<.cart_empty_state mode={@mode} />
<% else %>
<%= for item <- @cart_items do %>
<.cart_item_row item={item} size={:compact} show_quantity_controls mode={@mode} />
<% end %>
<% end %>
"""
end
@doc """
Shared cart item row component used by both drawer and cart page.
## Attributes
* `item` - Required. Cart item with `name`, `variant`, `price`, `quantity`, `image`, `variant_id`, `product_id`.
* `size` - Either `:compact` (drawer) or `:default` (cart page). Default: :default
* `show_quantity_controls` - Show +/- buttons. Default: false
* `mode` - Either `:live` or `:preview`. Default: :live
"""
attr :item, :map, required: true
attr :size, :atom, default: :default
attr :show_quantity_controls, :boolean, default: false
attr :mode, :atom, default: :live
def cart_item_row(assigns) do
~H"""
<%= if @mode != :preview do %>
<.link
patch={"/products/#{@item.product_id}"}
class={["cart-item-image", !@item.image && "cart-item-image--empty"]}
data-size={if @size == :compact, do: "compact"}
style={if @item.image, do: "background-image: url('#{@item.image}');"}
aria-label={"View #{@item.name}"}
>
<% else %>
<% end %>
<%= if @mode != :preview do %>
<.link
patch={"/products/#{@item.product_id}"}
class="cart-item-name-link"
>
{@item.name}
<% else %>
{@item.name}
<% end %>
<%= if @item.variant do %>
{@item.variant}
<% end %>
This item is currently unavailable
<%= if @show_quantity_controls do %>
<% else %>
Qty: {@item.quantity}
<% end %>
<.cart_remove_button variant_id={@item.variant_id} item_name={@item.name} />
{Berrypod.Cart.format_price(@item.price * @item.quantity)}
"""
end
@doc """
Cart empty state component.
"""
attr :mode, :atom, default: :live
def cart_empty_state(assigns) do
~H"""
Your basket is empty
<%= if @mode == :preview do %>
Continue shopping
<% else %>
<.link
patch="/collections/all"
class="cart-continue-link"
>
Continue shopping
<% end %>
"""
end
@doc """
Remove button for cart items.
"""
attr :variant_id, :string, required: true
attr :item_name, :string, default: "item"
def cart_remove_button(assigns) do
~H"""
"""
end
@doc """
Renders a cart item row.
## Attributes
* `item` - Required. Map with `product` (containing `image_url`, `name`, `price`), `variant`, and `quantity`.
* `currency` - Optional. Currency symbol. Defaults to "£".
## Examples
<.cart_item item={item} />
"""
attr :item, :map, required: true
def cart_item(assigns) do
~H"""
<.shop_card class="cart-page-item">
{@item.product.title}
{@item.variant}
−
{@item.quantity}
+
Remove
{Berrypod.Cart.format_price(@item.product.cheapest_price * @item.quantity)}
"""
end
defp cart_item_image(product) do
ProductImage.url(Product.primary_image(product), 400)
end
# Shared delivery line used by both cart_drawer and order_summary.
# Shows a country when rates are available, falls back to plain text.
attr :shipping_estimate, :integer, default: nil
attr :country_code, :string, default: "GB"
attr :available_countries, :list, default: []
attr :mode, :atom, default: :live
defp delivery_line(assigns) do
~H"""
Delivery to
<%= if @available_countries != [] and @mode != :preview do %>
<% else %>
{Berrypod.Shipping.country_name(@country_code)}
<% end %>
<%= if @shipping_estimate do %>
{Berrypod.Cart.format_price(@shipping_estimate)}
<% else %>
Calculated at checkout
<% end %>
"""
end
@doc """
Renders the order summary card.
## Attributes
* `subtotal` - Required. Subtotal amount (in pence/cents).
* `shipping_estimate` - Optional. Shipping estimate in pence.
* `country_code` - Optional. Current country code. Default "GB".
* `available_countries` - Optional. List of `{code, name}` tuples.
* `mode` - Either `:live` (default) or `:preview`.
## Examples
<.order_summary subtotal={3600} />
"""
attr :subtotal, :integer, required: true
attr :shipping_estimate, :integer, default: nil
attr :country_code, :string, default: "GB"
attr :available_countries, :list, default: []
attr :mode, :atom, default: :live
attr :stripe_connected, :boolean, default: true
def order_summary(assigns) do
assigns =
assign(assigns, :estimated_total, assigns.subtotal + (assigns.shipping_estimate || 0))
~H"""
<.shop_card class="order-summary-card">
Order summary
Subtotal
{Berrypod.Cart.format_price(@subtotal)}
<.delivery_line
shipping_estimate={@shipping_estimate}
country_code={@country_code}
available_countries={@available_countries}
mode={@mode}
/>
{if @shipping_estimate, do: "Estimated total", else: "Subtotal"}
{Berrypod.Cart.format_price(@estimated_total)}
<%= cond do %>
<% @mode == :preview -> %>
<.shop_button class="order-summary-checkout">
Checkout
<.shop_button_outline
phx-click="change_preview_page"
phx-value-page="collection"
class="order-summary-continue"
>
Continue shopping
<% !@stripe_connected -> %>
<.shop_button disabled class="order-summary-checkout">
Checkout
Checkout isn't available yet.
<.shop_link_outline
href="/collections/all"
class="order-summary-continue"
>
Continue shopping
<% true -> %>
<.shop_link_outline
href="/collections/all"
class="order-summary-continue"
>
Continue shopping
<% end %>
"""
end
@doc """
Renders a cart items list with order summary layout.
## Attributes
* `items` - Required. List of cart items.
* `subtotal` - Required. Subtotal in pence/cents.
* `currency` - Optional. Currency symbol. Defaults to "£".
* `mode` - Either `:live` (default) or `:preview`.
## Examples
<.cart_layout items={@cart_items} subtotal={3600} mode={:preview} />
"""
attr :items, :list, required: true
attr :subtotal, :integer, required: true
attr :mode, :atom, default: :live
def cart_layout(assigns) do
~H"""
<%= for item <- @items do %>
<.cart_item item={item} />
<% end %>
<.order_summary subtotal={@subtotal} mode={@mode} />
"""
end
end