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:
jamey
2026-02-05 22:11:16 +00:00
parent 880e7a2888
commit 1bc08bfb23
27 changed files with 1163 additions and 155 deletions

View File

@@ -33,6 +33,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
|> assign(:header_image, header_image)
|> assign(:customise_open, false)
|> assign(:sidebar_collapsed, false)
|> assign(:cart_drawer_open, false)
|> allow_upload(:logo_upload,
accept: ~w(.png .jpg .jpeg .webp .svg),
max_entries: 1,
@@ -287,6 +288,16 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
{:noreply, assign(socket, :sidebar_collapsed, !socket.assigns.sidebar_collapsed)}
end
@impl true
def handle_event("open_cart_drawer", _params, socket) do
{:noreply, assign(socket, :cart_drawer_open, true)}
end
@impl true
def handle_event("close_cart_drawer", _params, socket) do
{:noreply, assign(socket, :cart_drawer_open, false)}
end
@impl true
def handle_event("noop", _params, socket) do
{:noreply, socket}
@@ -303,6 +314,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
attr :theme_settings, :map, required: true
attr :logo_image, :any, required: true
attr :header_image, :any, required: true
attr :cart_drawer_open, :boolean, default: false
defp preview_page(%{page: :home} = assigns) do
~H"""
@@ -315,6 +327,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
cart_items={PreviewData.cart_drawer_items()}
cart_count={2}
cart_subtotal="£72.00"
cart_drawer_open={@cart_drawer_open}
/>
"""
end
@@ -330,6 +343,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
cart_items={PreviewData.cart_drawer_items()}
cart_count={2}
cart_subtotal="£72.00"
cart_drawer_open={@cart_drawer_open}
/>
"""
end
@@ -379,6 +393,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
cart_items={PreviewData.cart_drawer_items()}
cart_count={2}
cart_subtotal="£72.00"
cart_drawer_open={@cart_drawer_open}
option_types={@option_types}
selected_options={@selected_options}
available_options={@available_options}
@@ -409,6 +424,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
cart_items={PreviewData.cart_drawer_items()}
cart_count={2}
cart_subtotal="£72.00"
cart_drawer_open={@cart_drawer_open}
/>
"""
end
@@ -423,6 +439,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
cart_items={PreviewData.cart_drawer_items()}
cart_count={2}
cart_subtotal="£72.00"
cart_drawer_open={@cart_drawer_open}
/>
"""
end
@@ -437,6 +454,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
cart_items={PreviewData.cart_drawer_items()}
cart_count={2}
cart_subtotal="£72.00"
cart_drawer_open={@cart_drawer_open}
/>
"""
end
@@ -455,6 +473,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
cart_items={PreviewData.cart_drawer_items()}
cart_count={2}
cart_subtotal="£72.00"
cart_drawer_open={@cart_drawer_open}
/>
"""
end

View File

@@ -1164,6 +1164,7 @@
theme_settings={@theme_settings}
logo_image={@logo_image}
header_image={@header_image}
cart_drawer_open={@cart_drawer_open}
/>
</div>
</div>