defmodule ToweropsWeb.Live.Components.GlobalSearchComponent do @moduledoc """ Global search overlay (Cmd+K / Ctrl+K) that searches across devices, sites, accounts, and alerts. """ use ToweropsWeb, :live_component alias Towerops.Search @impl true def mount(socket) do {:ok, assign(socket, query: "", results: %{}, selected_index: 0, open: false)} end @impl true def update(assigns, socket) do {:ok, assign(socket, assigns)} end @impl true def handle_event("open", _params, socket) do {:noreply, assign(socket, open: true, query: "", results: %{}, selected_index: 0)} end @impl true def handle_event("close", _params, socket) do {:noreply, assign(socket, open: false, query: "", results: %{}, selected_index: 0)} end @impl true def handle_event("search", %{"value" => query}, socket) do results = if String.length(String.trim(query)) >= 2 do Search.search(socket.assigns.organization_id, String.trim(query)) else %{} end {:noreply, assign(socket, query: query, results: results, selected_index: 0)} end @impl true def handle_event("keydown", %{"key" => "ArrowDown"}, socket) do max = flat_results_count(socket.assigns.results) - 1 idx = min(socket.assigns.selected_index + 1, max(max, 0)) {:noreply, assign(socket, selected_index: idx)} end @impl true def handle_event("keydown", %{"key" => "ArrowUp"}, socket) do idx = max(socket.assigns.selected_index - 1, 0) {:noreply, assign(socket, selected_index: idx)} end @impl true def handle_event("keydown", %{"key" => "Enter"}, socket) do flat = flat_results(socket.assigns.results) case Enum.at(flat, socket.assigns.selected_index) do nil -> {:noreply, socket} result -> {:noreply, push_navigate(socket, to: result.url)} end end @impl true def handle_event("keydown", _params, socket) do {:noreply, socket} end @impl true def render(assigns) do flat = flat_results(assigns.results) assigns = assign(assigns, flat_results: flat) ~H""" """ end defp category_label(:devices), do: "Devices" defp category_label(:sites), do: "Sites" defp category_label(:accounts), do: "Accounts" defp category_label(:alerts), do: "Alerts" defp category_label(other), do: to_string(other) defp category_icon(:devices), do: "hero-server" defp category_icon(:sites), do: "hero-building-office" defp category_icon(:accounts), do: "hero-user-group" defp category_icon(:alerts), do: "hero-bell-alert" defp category_icon(_), do: "hero-magnifying-glass" defp flat_results(results) do Enum.flat_map([:devices, :sites, :accounts, :alerts], fn cat -> Map.get(results, cat, []) end) end defp flat_results_count(results), do: length(flat_results(results)) defp global_index(results, category, local_idx) do order = [:devices, :sites, :accounts, :alerts] offset = order |> Enum.take_while(&(&1 != category)) |> Enum.reduce(0, fn cat, acc -> acc + length(Map.get(results, cat, [])) end) offset + local_idx end end