All modules, configs, paths, and references updated. 836 tests pass, zero warnings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
167 lines
5.0 KiB
Elixir
167 lines
5.0 KiB
Elixir
defmodule BerrypodWeb.CartHook do
|
|
@moduledoc """
|
|
LiveView on_mount hook for cart state and shared event handling.
|
|
|
|
Mounted in the public_shop live_session to give all shop LiveViews
|
|
cart state, PubSub sync, and shared event handlers via attach_hook.
|
|
|
|
Handles these events so individual LiveViews don't have to:
|
|
- `open_cart_drawer` / `close_cart_drawer` - toggle drawer visibility
|
|
- `remove_item` - remove item from cart
|
|
- `increment` / `decrement` - change item quantity
|
|
- `change_country` - update shipping country
|
|
- `{:cart_updated, cart}` info - cross-tab cart sync via PubSub
|
|
|
|
LiveViews with custom cart logic (e.g. add_to_cart) can call
|
|
`update_cart_assigns/2` and `broadcast_and_update/2` directly.
|
|
"""
|
|
|
|
import Phoenix.Component, only: [assign: 3]
|
|
import Phoenix.LiveView, only: [attach_hook: 4, connected?: 1, push_event: 3]
|
|
|
|
alias Berrypod.Cart
|
|
alias Berrypod.Shipping
|
|
|
|
def on_mount(:mount_cart, _params, session, socket) do
|
|
cart_items = Cart.get_from_session(session)
|
|
country_code = session["country_code"] || "GB"
|
|
available_countries = Shipping.list_available_countries_with_names()
|
|
|
|
socket =
|
|
socket
|
|
|> assign(:country_code, country_code)
|
|
|> assign(:available_countries, available_countries)
|
|
|> update_cart_assigns(cart_items)
|
|
|> assign(:cart_drawer_open, false)
|
|
|> assign(:cart_status, nil)
|
|
|> attach_hook(:cart_events, :handle_event, &handle_cart_event/3)
|
|
|> attach_hook(:cart_info, :handle_info, &handle_cart_info/2)
|
|
|
|
socket =
|
|
if connected?(socket) do
|
|
csrf_token = Map.get(session, "_csrf_token", "default")
|
|
topic = "cart:#{csrf_token}"
|
|
Phoenix.PubSub.subscribe(Berrypod.PubSub, topic)
|
|
assign(socket, :cart_topic, topic)
|
|
else
|
|
assign(socket, :cart_topic, nil)
|
|
end
|
|
|
|
{:cont, socket}
|
|
end
|
|
|
|
# Shared event handlers
|
|
|
|
defp handle_cart_event("open_cart_drawer", _params, socket) do
|
|
{:halt, assign(socket, :cart_drawer_open, true)}
|
|
end
|
|
|
|
defp handle_cart_event("close_cart_drawer", _params, socket) do
|
|
{:halt, assign(socket, :cart_drawer_open, false)}
|
|
end
|
|
|
|
defp handle_cart_event("change_country", %{"country" => code}, socket) do
|
|
socket =
|
|
socket
|
|
|> assign(:country_code, code)
|
|
|> update_cart_assigns(socket.assigns.raw_cart)
|
|
|> push_event("persist_country", %{code: code})
|
|
|
|
{:halt, socket}
|
|
end
|
|
|
|
defp handle_cart_event("remove_item", %{"id" => variant_id}, socket) do
|
|
cart = Cart.remove_item(socket.assigns.raw_cart, variant_id)
|
|
|
|
socket =
|
|
socket
|
|
|> broadcast_and_update(cart)
|
|
|> assign(:cart_status, "Item removed from cart")
|
|
|
|
{:halt, socket}
|
|
end
|
|
|
|
defp handle_cart_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
|
|
|> broadcast_and_update(cart)
|
|
|> assign(:cart_status, "Quantity updated to #{new_qty}")
|
|
|
|
{:halt, socket}
|
|
end
|
|
|
|
defp handle_cart_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
|
|
|> broadcast_and_update(cart)
|
|
|> assign(:cart_status, "Quantity updated to #{new_qty}")
|
|
|
|
{:halt, socket}
|
|
end
|
|
|
|
defp handle_cart_event(_event, _params, socket), do: {:cont, socket}
|
|
|
|
# Shared info handlers
|
|
|
|
defp handle_cart_info({:cart_updated, cart}, socket) do
|
|
{:halt, update_cart_assigns(socket, cart)}
|
|
end
|
|
|
|
defp handle_cart_info(_msg, socket), do: {:cont, socket}
|
|
|
|
# Public helpers for LiveViews with custom cart logic
|
|
|
|
@doc """
|
|
Updates all cart-related assigns from raw cart data.
|
|
"""
|
|
def update_cart_assigns(socket, cart) do
|
|
%{items: items, count: count, subtotal: subtotal} = Cart.build_state(cart)
|
|
country_code = socket.assigns[:country_code] || "GB"
|
|
subtotal_pence = Cart.calculate_subtotal(items)
|
|
|
|
shipping_estimate =
|
|
case Shipping.calculate_for_cart(items, country_code) do
|
|
{:ok, cost} when cost > 0 -> cost
|
|
_ -> nil
|
|
end
|
|
|
|
cart_total =
|
|
Cart.format_price(subtotal_pence + (shipping_estimate || 0))
|
|
|
|
socket
|
|
|> assign(:raw_cart, cart)
|
|
|> assign(:cart_items, items)
|
|
|> assign(:cart_count, count)
|
|
|> assign(:cart_subtotal, subtotal)
|
|
|> assign(:cart_total, cart_total)
|
|
|> assign(:shipping_estimate, shipping_estimate)
|
|
end
|
|
|
|
@doc """
|
|
Broadcasts cart update to other tabs and updates local assigns.
|
|
|
|
Uses broadcast_from to avoid notifying self (prevents double-update).
|
|
"""
|
|
def broadcast_and_update(socket, cart) do
|
|
if socket.assigns.cart_topic do
|
|
Phoenix.PubSub.broadcast_from(
|
|
Berrypod.PubSub,
|
|
self(),
|
|
socket.assigns.cart_topic,
|
|
{:cart_updated, cart}
|
|
)
|
|
end
|
|
|
|
socket
|
|
|> update_cart_assigns(cart)
|
|
|> push_event("persist_cart", %{items: Cart.serialize(cart)})
|
|
end
|
|
end
|