feat: add cart page, cart drawer, and shared cart infrastructure
- Cart context with pure functions for add/remove/update/hydrate - Price formatting via ex_money (replaces all float division) - CartHook on_mount with attach_hook for shared event handlers (open/close drawer, remove item, PubSub sync) - Accessible cart drawer with focus trap, scroll lock, aria-live - Cart page with increment/decrement quantity controls - Preview mode cart drawer support in theme editor - Cart persistence to session via JS hook + API endpoint - 19 tests covering all Cart pure functions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,9 +31,6 @@ defmodule SimpleshopThemeWeb.ShopLive.About do
|
||||
|> assign(:logo_image, logo_image)
|
||||
|> assign(:header_image, header_image)
|
||||
|> assign(:mode, :shop)
|
||||
|> assign(:cart_items, [])
|
||||
|> assign(:cart_count, 0)
|
||||
|> assign(:cart_subtotal, "£0.00")
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
@@ -49,6 +46,8 @@ defmodule SimpleshopThemeWeb.ShopLive.About do
|
||||
cart_items={@cart_items}
|
||||
cart_count={@cart_count}
|
||||
cart_subtotal={@cart_subtotal}
|
||||
cart_drawer_open={@cart_drawer_open}
|
||||
cart_status={@cart_status}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
defmodule SimpleshopThemeWeb.ShopLive.Cart do
|
||||
use SimpleshopThemeWeb, :live_view
|
||||
|
||||
alias SimpleshopTheme.Cart
|
||||
alias SimpleshopTheme.Settings
|
||||
alias SimpleshopTheme.Media
|
||||
alias SimpleshopTheme.Theme.{CSSCache, CSSGenerator, PreviewData}
|
||||
alias SimpleshopTheme.Theme.{CSSCache, CSSGenerator}
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
@@ -23,15 +24,6 @@ defmodule SimpleshopThemeWeb.ShopLive.Cart do
|
||||
logo_image = Media.get_logo()
|
||||
header_image = Media.get_header()
|
||||
|
||||
# For now, use preview data for cart items
|
||||
# In a real implementation, this would come from session/database
|
||||
cart_page_items = PreviewData.cart_items()
|
||||
|
||||
cart_page_subtotal =
|
||||
Enum.reduce(cart_page_items, 0, fn item, acc ->
|
||||
acc + item.product.price * item.quantity
|
||||
end)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:page_title, "Cart")
|
||||
@@ -39,34 +31,56 @@ defmodule SimpleshopThemeWeb.ShopLive.Cart do
|
||||
|> assign(:generated_css, generated_css)
|
||||
|> assign(:logo_image, logo_image)
|
||||
|> assign(:header_image, header_image)
|
||||
|> assign(:cart_page_items, cart_page_items)
|
||||
|> assign(:cart_page_subtotal, cart_page_subtotal)
|
||||
|> assign(:mode, :shop)
|
||||
|> assign(:cart_items, PreviewData.cart_drawer_items())
|
||||
|> assign(:cart_count, length(cart_page_items))
|
||||
|> assign(:cart_subtotal, format_subtotal(cart_page_subtotal))
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("increment", %{"id" => variant_id}, socket) do
|
||||
cart = Cart.add_item(socket.assigns.raw_cart, variant_id, 1)
|
||||
new_qty = Cart.get_quantity(cart, variant_id)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> SimpleshopThemeWeb.CartHook.broadcast_and_update(cart)
|
||||
|> assign(:cart_status, "Quantity updated to #{new_qty}")
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("decrement", %{"id" => variant_id}, socket) do
|
||||
current = Cart.get_quantity(socket.assigns.raw_cart, variant_id)
|
||||
cart = Cart.update_quantity(socket.assigns.raw_cart, variant_id, current - 1)
|
||||
new_qty = Cart.get_quantity(cart, variant_id)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> SimpleshopThemeWeb.CartHook.broadcast_and_update(cart)
|
||||
|> assign(:cart_status, "Quantity updated to #{new_qty}")
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
cart_page_subtotal = Cart.calculate_subtotal(assigns.cart_items)
|
||||
assigns = assign(assigns, :cart_page_subtotal, cart_page_subtotal)
|
||||
|
||||
~H"""
|
||||
<SimpleshopThemeWeb.PageTemplates.cart
|
||||
theme_settings={@theme_settings}
|
||||
logo_image={@logo_image}
|
||||
header_image={@header_image}
|
||||
cart_page_items={@cart_page_items}
|
||||
cart_items={@cart_items}
|
||||
cart_page_subtotal={@cart_page_subtotal}
|
||||
mode={@mode}
|
||||
cart_items={@cart_items}
|
||||
cart_count={@cart_count}
|
||||
cart_subtotal={@cart_subtotal}
|
||||
cart_drawer_open={@cart_drawer_open}
|
||||
cart_status={@cart_status}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
defp format_subtotal(subtotal_pence) do
|
||||
"£#{Float.round(subtotal_pence / 100, 2)}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,9 +39,6 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
|
||||
|> assign(:logo_image, logo_image)
|
||||
|> assign(:header_image, header_image)
|
||||
|> assign(:mode, :shop)
|
||||
|> assign(:cart_items, [])
|
||||
|> assign(:cart_count, 0)
|
||||
|> assign(:cart_subtotal, "£0.00")
|
||||
|> assign(:categories, PreviewData.categories())
|
||||
|> assign(:sort_options, @sort_options)
|
||||
|> assign(:current_sort, "featured")
|
||||
@@ -105,6 +102,8 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id="shop-container"
|
||||
phx-hook="CartPersist"
|
||||
class="shop-container min-h-screen pb-20 md:pb-0"
|
||||
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
|
||||
>
|
||||
@@ -169,7 +168,10 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
|
||||
<SimpleshopThemeWeb.ShopComponents.cart_drawer
|
||||
cart_items={@cart_items}
|
||||
subtotal={@cart_subtotal}
|
||||
cart_count={@cart_count}
|
||||
mode={@mode}
|
||||
open={@cart_drawer_open}
|
||||
cart_status={assigns[:cart_status]}
|
||||
/>
|
||||
|
||||
<SimpleshopThemeWeb.ShopComponents.search_modal hint_text={
|
||||
|
||||
@@ -31,9 +31,6 @@ defmodule SimpleshopThemeWeb.ShopLive.Contact do
|
||||
|> assign(:logo_image, logo_image)
|
||||
|> assign(:header_image, header_image)
|
||||
|> assign(:mode, :shop)
|
||||
|> assign(:cart_items, [])
|
||||
|> assign(:cart_count, 0)
|
||||
|> assign(:cart_subtotal, "£0.00")
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
@@ -49,6 +46,8 @@ defmodule SimpleshopThemeWeb.ShopLive.Contact do
|
||||
cart_items={@cart_items}
|
||||
cart_count={@cart_count}
|
||||
cart_subtotal={@cart_subtotal}
|
||||
cart_drawer_open={@cart_drawer_open}
|
||||
cart_status={@cart_status}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -37,9 +37,6 @@ defmodule SimpleshopThemeWeb.ShopLive.Home do
|
||||
|> assign(:header_image, header_image)
|
||||
|> assign(:preview_data, preview_data)
|
||||
|> assign(:mode, :shop)
|
||||
|> assign(:cart_items, [])
|
||||
|> assign(:cart_count, 0)
|
||||
|> assign(:cart_subtotal, "£0.00")
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
@@ -56,6 +53,8 @@ defmodule SimpleshopThemeWeb.ShopLive.Home do
|
||||
cart_items={@cart_items}
|
||||
cart_count={@cart_count}
|
||||
cart_subtotal={@cart_subtotal}
|
||||
cart_drawer_open={@cart_drawer_open}
|
||||
cart_status={@cart_status}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
defmodule SimpleshopThemeWeb.ShopLive.ProductShow do
|
||||
use SimpleshopThemeWeb, :live_view
|
||||
|
||||
alias SimpleshopTheme.Cart
|
||||
alias SimpleshopTheme.Settings
|
||||
alias SimpleshopTheme.Media
|
||||
alias SimpleshopTheme.Theme.{CSSCache, CSSGenerator, PreviewData}
|
||||
@@ -61,9 +62,6 @@ defmodule SimpleshopThemeWeb.ShopLive.ProductShow do
|
||||
|> assign(:related_products, related_products)
|
||||
|> assign(:quantity, 1)
|
||||
|> assign(:mode, :shop)
|
||||
|> assign(:cart_items, [])
|
||||
|> assign(:cart_count, 0)
|
||||
|> assign(:cart_subtotal, "£0.00")
|
||||
|> assign(:option_types, option_types)
|
||||
|> assign(:variants, variants)
|
||||
|> assign(:selected_options, selected_options)
|
||||
@@ -153,6 +151,25 @@ defmodule SimpleshopThemeWeb.ShopLive.ProductShow do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("add_to_cart", _params, socket) do
|
||||
variant = socket.assigns.selected_variant
|
||||
|
||||
if variant do
|
||||
cart = Cart.add_item(socket.assigns.raw_cart, variant.id, socket.assigns.quantity)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> SimpleshopThemeWeb.CartHook.broadcast_and_update(cart)
|
||||
|> assign(:cart_drawer_open, true)
|
||||
|> assign(:cart_status, "#{socket.assigns.product.name} added to cart")
|
||||
|
||||
{:noreply, socket}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
@@ -168,6 +185,8 @@ defmodule SimpleshopThemeWeb.ShopLive.ProductShow do
|
||||
cart_items={@cart_items}
|
||||
cart_count={@cart_count}
|
||||
cart_subtotal={@cart_subtotal}
|
||||
cart_drawer_open={@cart_drawer_open}
|
||||
cart_status={@cart_status}
|
||||
option_types={@option_types}
|
||||
selected_options={@selected_options}
|
||||
available_options={@available_options}
|
||||
|
||||
Reference in New Issue
Block a user