simpleshop_theme/lib/simpleshop_theme_web/cart_hook.ex

116 lines
3.4 KiB
Elixir
Raw Normal View History

defmodule SimpleshopThemeWeb.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
- `{:cart_updated, cart}` info - cross-tab cart sync via PubSub
LiveViews with custom cart logic (add_to_cart, increment, decrement)
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 SimpleshopTheme.Cart
def on_mount(:mount_cart, _params, session, socket) do
cart_items = Cart.get_from_session(session)
hydrated = Cart.hydrate(cart_items)
socket =
socket
|> assign(:raw_cart, cart_items)
|> assign(:cart_items, hydrated)
|> assign(:cart_count, Cart.item_count(cart_items))
|> assign(:cart_subtotal, Cart.format_subtotal(hydrated))
|> 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(SimpleshopTheme.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("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(_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
hydrated = Cart.hydrate(cart)
socket
|> assign(:raw_cart, cart)
|> assign(:cart_items, hydrated)
|> assign(:cart_count, Cart.item_count(cart))
|> assign(:cart_subtotal, Cart.format_subtotal(hydrated))
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(
SimpleshopTheme.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