|
| 1 | +defmodule PeekAppSDK.UI.Odyssey.ProductPicker do |
| 2 | + @moduledoc """ |
| 3 | + A live component for selecting products with a toggle between "All" and "Specific". |
| 4 | + When "Specific" is selected, displays checkboxes for each product. |
| 5 | +
|
| 6 | + Integrates with forms via a hidden input field and a JS hook. |
| 7 | +
|
| 8 | + ## Examples |
| 9 | +
|
| 10 | + <.odyssey_product_picker |
| 11 | + field={@form[:whitelisted_products]} |
| 12 | + products={@products} |
| 13 | + selected_ids={@selected_ids} |
| 14 | + /> |
| 15 | + """ |
| 16 | + |
| 17 | + use Phoenix.LiveComponent |
| 18 | + use Phoenix.Component |
| 19 | + |
| 20 | + import PeekAppSDK.UI.Odyssey.ToggleButton, only: [odyssey_toggle_button: 1] |
| 21 | + |
| 22 | + @impl true |
| 23 | + def mount(socket) do |
| 24 | + {:ok, socket} |
| 25 | + end |
| 26 | + |
| 27 | + @impl true |
| 28 | + def update(assigns, socket) do |
| 29 | + socket = assign(socket, assigns) |
| 30 | + |
| 31 | + socket = |
| 32 | + socket |
| 33 | + |> assign_new(:apply_to_mode, fn -> determine_mode(socket.assigns.selected_ids) end) |
| 34 | + |
| 35 | + {:ok, socket} |
| 36 | + end |
| 37 | + |
| 38 | + defp determine_mode([]), do: "all" |
| 39 | + defp determine_mode(_), do: "specific" |
| 40 | + |
| 41 | + @impl true |
| 42 | + def render(assigns) do |
| 43 | + ~H""" |
| 44 | + <div id={@id} phx-hook="OdysseyProductPicker" data-integration="product-picker"> |
| 45 | + <input |
| 46 | + type="hidden" |
| 47 | + name={@field.name} |
| 48 | + value={encode_selected_products(@selected_ids)} |
| 49 | + id={"#{@id}_hidden_field"} |
| 50 | + /> |
| 51 | +
|
| 52 | + <div class="space-y-4"> |
| 53 | + <.odyssey_toggle_button |
| 54 | + options={[ |
| 55 | + %{value: "all", label: @all_label}, |
| 56 | + %{value: "specific", label: @specific_label} |
| 57 | + ]} |
| 58 | + selected={@apply_to_mode} |
| 59 | + on_change="toggle_apply_to" |
| 60 | + phx-target={@myself} |
| 61 | + label={@label} |
| 62 | + disabled={@disabled} |
| 63 | + /> |
| 64 | +
|
| 65 | + <div |
| 66 | + :if={@apply_to_mode == "specific"} |
| 67 | + class="space-y-2 pl-4 max-h-64 overflow-y-auto" |
| 68 | + phx-update="ignore" |
| 69 | + id={"#{@id}_checkboxes"} |
| 70 | + > |
| 71 | + <div :for={product <- @products} class="flex items-center gap-3"> |
| 72 | + <input |
| 73 | + type="checkbox" |
| 74 | + id={"#{@id}_product_#{product.id}"} |
| 75 | + checked={product.id in @selected_ids} |
| 76 | + data-product-id={product.id} |
| 77 | + disabled={@disabled} |
| 78 | + class="checkbox checkbox-sm product-picker-checkbox" |
| 79 | + /> |
| 80 | + <div |
| 81 | + class="w-3 h-3 rounded-sm flex-shrink-0" |
| 82 | + style={"background-color: #{product.color_hex}"} |
| 83 | + > |
| 84 | + </div> |
| 85 | + <label for={"#{@id}_product_#{product.id}"} class="text-sm text-gray-700 cursor-pointer"> |
| 86 | + {product.name} |
| 87 | + </label> |
| 88 | + </div> |
| 89 | + </div> |
| 90 | + </div> |
| 91 | + </div> |
| 92 | + """ |
| 93 | + end |
| 94 | + |
| 95 | + @impl true |
| 96 | + def handle_event("toggle_apply_to", %{"value" => mode}, socket) do |
| 97 | + selected_ids = |
| 98 | + case mode do |
| 99 | + "all" -> [] |
| 100 | + "specific" -> socket.assigns.selected_ids |
| 101 | + end |
| 102 | + |
| 103 | + socket = |
| 104 | + socket |
| 105 | + |> assign(:apply_to_mode, mode) |
| 106 | + |> assign(:selected_ids, selected_ids) |
| 107 | + |> push_event("update-product-selection", %{ |
| 108 | + field_id: "#{socket.assigns.id}_hidden_field", |
| 109 | + value: encode_selected_products(selected_ids) |
| 110 | + }) |
| 111 | + |
| 112 | + {:noreply, socket} |
| 113 | + end |
| 114 | + |
| 115 | + defp encode_selected_products([]), do: "" |
| 116 | + defp encode_selected_products(ids), do: Enum.join(ids, ",") |
| 117 | + |
| 118 | + @doc """ |
| 119 | + Renders a product picker component. |
| 120 | +
|
| 121 | + ## Examples |
| 122 | +
|
| 123 | + <.odyssey_product_picker |
| 124 | + field={@form[:whitelisted_products]} |
| 125 | + products={@products} |
| 126 | + selected_ids={["prod_1", "prod_2"]} |
| 127 | + /> |
| 128 | + """ |
| 129 | + attr :field, :any, required: true, doc: "a Phoenix.HTML.FormField struct" |
| 130 | + attr :id, :string, doc: "component id, defaults to form_field_product_picker" |
| 131 | + attr :products, :list, required: true, doc: "list of %{id, name, color_hex} maps" |
| 132 | + attr :selected_ids, :list, default: [], doc: "list of pre-selected product IDs" |
| 133 | + attr :all_label, :string, default: "All Products", doc: "label for the 'all' toggle option" |
| 134 | + attr :specific_label, :string, default: "Specific Products", doc: "label for the 'specific' toggle option" |
| 135 | + attr :label, :string, required: false, doc: "label for the toggle button" |
| 136 | + attr :disabled, :boolean, default: false, doc: "whether the picker is disabled" |
| 137 | + |
| 138 | + def odyssey_product_picker(assigns) do |
| 139 | + assigns = |
| 140 | + assigns |
| 141 | + |> assign_new(:id, fn %{field: field} -> |
| 142 | + "#{field.form.name}_#{field.field}_product_picker" |
| 143 | + end) |
| 144 | + |> assign_new(:label, fn -> nil end) |
| 145 | + |> assign(:module, __MODULE__) |
| 146 | + |
| 147 | + ~H""" |
| 148 | + <.live_component {assigns} /> |
| 149 | + """ |
| 150 | + end |
| 151 | +end |
0 commit comments