defmodule MusicLibraryWeb.SearchComponents do @moduledoc """ Universal search modal and related components. """ use MusicLibraryWeb, :html import MusicLibraryWeb.RecordComponents, only: [ format_label: 1, type_label: 1, record_cover: 1, artist_image: 1, release_status_tooltip: 1 ] alias MusicLibrary.Records.Record alias MusicLibraryWeb.Components.BarcodeScanner alias MusicLibraryWeb.Markdown @doc """ Renders a search result item for records. ## Examples <.search_result_record record={record} /> """ attr :record, :map, required: true attr :type, :atom, required: true, values: [:collection, :wishlist] attr :rest, :global, include: ~w(phx-click phx-value-id) def search_result_record(assigns) do ~H"""
<.record_cover record={@record} class="aspect-square size-12 rounded-md object-cover" width={96} />

{@record.title} <.release_status_tooltip record={@record} />

{Record.artist_names(@record)}

{format_label(@record.format)} · {type_label(@record.type)} · <.icon name="hero-calendar-days" class="-mt-1 size-4" aria-hidden="true" data-slot="icon" /> {Record.format_release_date(@record.release_date)} · <.icon name="hero-banknotes" class="size-4" aria-hidden="true" data-slot="icon" /> {Record.format_as_date(@record.purchased_at)}

""" end @doc """ Renders a search result item for artists. ## Examples <.search_result_artist artist={artist} /> """ attr :artist, :map, required: true attr :image_data_hash, :string, required: false attr :rest, :global, include: ~w(phx-click phx-value-id) def search_result_artist(assigns) do ~H"""
<.artist_image class="aspect-square size-12 rounded-md object-cover" artist={@artist} width={96} image_hash={@image_data_hash} />

{@artist.name}

{@artist.disambiguation}

""" end @doc """ Renders a search result item for record sets. ## Examples <.search_result_record_set record_set={record_set} /> """ attr :record_set, :map, required: true attr :rest, :global, include: ~w(phx-click phx-value-id) def search_result_record_set(assigns) do ~H"""
<.icon name="hero-queue-list" class="size-6 text-zinc-400 dark:text-zinc-500" />

{@record_set.name}

{render_description(@record_set.description)}
""" end @doc """ Renders a search result item for navigation links. ## Examples <.search_result_navigation label="Collection" icon="hero-circle-stack" /> """ attr :label, :string, required: true attr :icon, :string, default: nil attr :rest, :global, include: ~w(phx-click phx-value-path phx-target) slot :custom_icon def search_result_navigation(assigns) do ~H"""
<%= if @icon do %> <.icon name={@icon} class="size-4 text-zinc-500 dark:text-zinc-400" /> <% else %> {render_slot(@custom_icon)} <% end %>

{@label}

{gettext("Go to →")}
""" end @doc """ Renders a search result group with a title and items. ## Examples <.search_result_group title="Collection" count={3} total_count={10}> <.search_result_record :for={record <- @records} record={record} /> <:actions> """ attr :title, :string, required: true attr :count, :integer, required: true attr :total_count, :integer, default: nil attr :class, :string, default: "" slot :inner_block, required: true slot :actions, doc: "Actions like 'view all' buttons" def search_result_group(assigns) do ~H"""

{@title} @count}> {gettext("(%{count} of %{total})", count: @count, total: @total_count)} ({@count})

{render_slot(@inner_block)}
{render_slot(action)}
""" end @doc """ Renders a view all results button. ## Examples <.view_all_button count={25} target="collection" /> """ attr :count, :integer, required: true attr :target, :string, required: true attr :rest, :global, include: ~w(phx-click phx-value-query) def view_all_button(assigns) do ~H""" """ end @doc """ Renders keyboard shortcut hints. ## Examples <.results_footer /> """ attr :total_results, :integer, default: 0 attr :has_navigable_items, :boolean, default: false def results_footer(assigns) do assigns = assign(assigns, :show_nav_hints, assigns.total_results > 0 or assigns.has_navigable_items) ~H"""
{gettext("Navigate")}
Enter {gettext("Select")}
Esc {gettext("Close")}
0} class="text-zinc-500 dark:text-zinc-400"> {ngettext("1 result", "%{count} results", @total_results)}
""" end @doc """ Renders an empty search state. ## Examples <.empty_state /> """ def empty_state(assigns) do ~H"""
<.icon name="hero-magnifying-glass" class="mx-auto mb-4 size-12 text-zinc-400" />

Cmd/Ctrl+K {gettext("to open this search")}

""" end @doc """ Renders a no results state. ## Examples <.no_results query="radiohead" /> """ attr :query, :string, required: true attr :target, :any, required: true def no_results(assigns) do ~H"""
<.icon name="hero-face-frown" class="mx-auto mb-4 size-12 text-zinc-400" />

{gettext("No results found for '%{query}'", query: @query)}

<.search_result_group title={gettext("Quick actions")} count={4}> <.search_result_navigation label={gettext("Add to wishlist")} icon="hero-star" phx-click="navigate_to_link" phx-value-path={~p"/wishlist/import?#{[import_query: @query]}"} phx-target={@target} /> <.search_result_navigation label={gettext("Add to collection")} icon="hero-plus-circle" phx-click="navigate_to_link" phx-value-path={~p"/collection/import?#{[import_query: @query]}"} phx-target={@target} /> <.search_result_navigation label={gettext("Search to scrobble")} icon="hero-play" phx-click="navigate_to_link" phx-value-path={~p"/scrobble?#{[query: @query]}"} phx-target={@target} /> <.search_result_navigation label={gettext("Scan a record")} phx-click="navigate_to_link" phx-value-path={~p"/collection/scan"} phx-target={@target} > <:custom_icon> """ end # sobelow_skip ["XSS.Raw"] # Markdown.to_html/1 sanitizes HTML via MDEx (ammonia) defp render_description(description) do description |> String.slice(0, 100) |> Markdown.to_html() |> raw() end end