defmodule PhoenixKit.Modules.Shop.Web.CartPage do @moduledoc """ Public cart page LiveView for E-Commerce module. Supports real-time cart synchronization across multiple browser tabs via PubSub subscription. When cart is updated in one tab, all other tabs receive the update automatically. """ use PhoenixKitWeb, :live_view alias PhoenixKit.Modules.Billing.Currency alias PhoenixKit.Modules.Languages.DialectMapper alias PhoenixKit.Modules.Shop alias PhoenixKit.Modules.Shop.Events alias PhoenixKit.Modules.Shop.ShippingMethod alias PhoenixKit.Modules.Shop.Translations @impl true def mount(_params, session, socket) do # Get session_id from session (for guest users) session_id = session["shop_session_id"] || generate_session_id() # Get current language for localized URLs current_language = socket.assigns[:current_locale] || Translations.default_language() # Get current user if logged in user = get_current_user(socket) user_id = if user, do: user.id, else: nil user_uuid = if user, do: user.uuid, else: nil # Get or create cart {:ok, cart} = Shop.get_or_create_cart(user_id: user_id, user_uuid: user_uuid, session_id: session_id) # Subscribe to cart events for real-time sync across tabs if connected?(socket) do Events.subscribe_to_cart(cart) end # Get available shipping methods shipping_methods = Shop.get_available_shipping_methods(cart) # Auto-select cheapest shipping method if none selected {:ok, cart} = Shop.auto_select_shipping_method(cart, shipping_methods) # Get default currency from Billing currency = Shop.get_default_currency() # Check if user is authenticated authenticated = not is_nil(socket.assigns[:phoenix_kit_current_user]) socket = socket |> assign(:page_title, "Shopping Cart") |> assign(:cart, cart) |> assign(:session_id, session_id) |> assign(:shipping_methods, shipping_methods) |> assign(:currency, currency) |> assign(:authenticated, authenticated) |> assign(:current_language, current_language) {:ok, socket} end @impl true def handle_event("update_quantity", %{"item_id" => item_id, "quantity" => quantity}, socket) do quantity = max(1, String.to_integer(quantity)) update_item_quantity(socket, item_id, quantity) end @impl true def handle_event("remove_item", %{"item_id" => item_id}, socket) do item = Enum.find(socket.assigns.cart.items, &(&1.uuid == item_id)) if item do case Shop.remove_from_cart(item) do {:ok, updated_cart} -> shipping_methods = Shop.get_available_shipping_methods(updated_cart) {:noreply, socket |> assign(:cart, updated_cart) |> assign(:shipping_methods, shipping_methods) |> put_flash(:info, "Item removed from cart")} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to remove item")} end else {:noreply, socket} end end @impl true def handle_event("select_shipping", %{"method_uuid" => method_uuid}, socket) do method = Enum.find(socket.assigns.shipping_methods, &(&1.uuid == method_uuid)) cart = socket.assigns.cart if method do # Country will be set at checkout based on billing info case Shop.set_cart_shipping(cart, method, nil) do {:ok, updated_cart} -> {:noreply, assign(socket, :cart, updated_cart)} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to set shipping method")} end else {:noreply, socket} end end @impl true def handle_event("proceed_to_checkout", _params, socket) do cart = socket.assigns.cart cond do cart.items == [] -> {:noreply, put_flash(socket, :error, "Your cart is empty")} is_nil(cart.shipping_method_id) -> {:noreply, put_flash(socket, :error, "Please select a shipping method")} true -> {:noreply, push_navigate(socket, to: Shop.checkout_url(socket.assigns.current_language))} end end defp update_item_quantity(socket, item_id, quantity) do item = Enum.find(socket.assigns.cart.items, &(&1.uuid == item_id)) if item do case Shop.update_cart_item(item, quantity) do {:ok, updated_cart} -> shipping_methods = Shop.get_available_shipping_methods(updated_cart) {:noreply, socket |> assign(:cart, updated_cart) |> assign(:shipping_methods, shipping_methods)} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to update quantity")} end else {:noreply, socket} end end # ============================================ # PUBSUB EVENT HANDLERS # ============================================ @impl true def handle_info({:cart_updated, cart}, socket) do shipping_methods = Shop.get_available_shipping_methods(cart) {:noreply, socket |> assign(:cart, cart) |> assign(:shipping_methods, shipping_methods)} end @impl true def handle_info({:item_added, cart, _item}, socket) do shipping_methods = Shop.get_available_shipping_methods(cart) {:noreply, socket |> assign(:cart, cart) |> assign(:shipping_methods, shipping_methods)} end @impl true def handle_info({:item_removed, cart, _item_id}, socket) do shipping_methods = Shop.get_available_shipping_methods(cart) {:noreply, socket |> assign(:cart, cart) |> assign(:shipping_methods, shipping_methods)} end @impl true def handle_info({:quantity_updated, cart, _item}, socket) do shipping_methods = Shop.get_available_shipping_methods(cart) {:noreply, socket |> assign(:cart, cart) |> assign(:shipping_methods, shipping_methods)} end @impl true def handle_info({:shipping_selected, cart}, socket) do {:noreply, assign(socket, :cart, cart)} end @impl true def handle_info({:payment_selected, cart}, socket) do {:noreply, assign(socket, :cart, cart)} end @impl true def handle_info({:cart_cleared, cart}, socket) do shipping_methods = Shop.get_available_shipping_methods(cart) {:noreply, socket |> assign(:cart, cart) |> assign(:shipping_methods, shipping_methods)} end @impl true def render(assigns) do ~H""" <.shop_layout {assigns}>
Review your items before checkout
Add some products to get started
<.link navigate={Shop.catalog_url(@current_language)} class="btn btn-primary"> Browse Products| Product | Quantity | Price | |
|---|---|---|---|
|
<%= if item.product_image do %>
<%= if item.product_slug do %>
<.link
navigate={product_item_url(item, @current_language)}
class="w-16 h-16 bg-base-200 rounded-lg overflow-hidden flex-shrink-0 block"
>
<.icon name="hero-cube" class="w-8 h-8 opacity-30" />
<% end %>
<% end %>
<%= if item.product_slug do %>
<.link
navigate={product_item_url(item, @current_language)}
class="hover:text-primary transition-colors"
>
{item.product_title}
<% else %>
{item.product_title}
<% end %>
<%= if item.product_sku do %>
SKU: {item.product_sku}
<% end %>
<%= if item.selected_specs && item.selected_specs != %{} do %>
<%= for {key, value} <- item.selected_specs do %>
{humanize_key(key)}:
{value}
<% end %>
<% end %>
<%= if item.compare_at_price && Decimal.compare(item.compare_at_price, item.unit_price) == :gt do %>
{format_price(item.compare_at_price, @currency)}
On sale!
<% end %>
|
{format_price(item.line_total, @currency)}
{format_price(item.unit_price, @currency)} each
|
Secure checkout powered by PhoenixKit
<% end %>