Hooked up search functionality to the search form
This commit is contained in:
parent
ed759e5e09
commit
dd8940893f
12 changed files with 127 additions and 18 deletions
|
|
@ -52,8 +52,6 @@ defmodule Pinchflat.Media do
|
||||||
virtual field to the result set.
|
virtual field to the result set.
|
||||||
|
|
||||||
Returns [%MediaItem{}, ...].
|
Returns [%MediaItem{}, ...].
|
||||||
|
|
||||||
TODO: test limit
|
|
||||||
"""
|
"""
|
||||||
def search(search_term, opts \\ []) do
|
def search(search_term, opts \\ []) do
|
||||||
limit = Keyword.get(opts, :limit, 50)
|
limit = Keyword.get(opts, :limit, 50)
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,21 @@ defmodule Pinchflat.Utils.StringUtils do
|
||||||
|> Base.encode16(case: :lower)
|
|> Base.encode16(case: :lower)
|
||||||
|> String.slice(0..(length - 1))
|
|> String.slice(0..(length - 1))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Truncates a string to the given length and adds `...` if the string is longer than the given length.
|
||||||
|
Will break on a word boundary. Nothing happens if the string is shorter than the given length.
|
||||||
|
|
||||||
|
Returns binary()
|
||||||
|
"""
|
||||||
|
def truncate(string, length) do
|
||||||
|
if String.length(string) > length do
|
||||||
|
string
|
||||||
|
|> String.slice(0..(length - 1))
|
||||||
|
|> String.replace(~r/\s+\S*$/, "")
|
||||||
|
|> Kernel.<>("...")
|
||||||
|
else
|
||||||
|
string
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,8 @@ defmodule PinchflatWeb do
|
||||||
import PinchflatWeb.CustomComponents.TableComponents
|
import PinchflatWeb.CustomComponents.TableComponents
|
||||||
import PinchflatWeb.CustomComponents.ButtonComponents
|
import PinchflatWeb.CustomComponents.ButtonComponents
|
||||||
|
|
||||||
|
alias Pinchflat.Utils.StringUtils
|
||||||
|
|
||||||
# Shortcut for generating JS commands
|
# Shortcut for generating JS commands
|
||||||
alias Phoenix.LiveView.JS
|
alias Phoenix.LiveView.JS
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<header class="sticky top-0 z-999 flex w-full bg-white drop-shadow-1 dark:bg-boxdark dark:drop-shadow-none">
|
<header class="sticky top-0 z-999 flex w-full bg-white drop-shadow-1 dark:bg-boxdark dark:drop-shadow-none">
|
||||||
<div class="flex flex-grow items-center justify-end px-4 py-4 shadow-2 md:px-6 2xl:px-11">
|
<div class="flex flex-grow items-center justify-between lg:justify-end px-4 py-4 shadow-2 md:px-6 2xl:px-11">
|
||||||
<div class="flex items-center gap-2 sm:gap-4 lg:hidden">
|
<div class="flex items-center gap-2 sm:gap-4 lg:hidden">
|
||||||
<button
|
<button
|
||||||
class="z-99999 block rounded-sm border border-stroke bg-white p-1.5 shadow-sm dark:border-strokedark dark:bg-boxdark lg:hidden"
|
class="z-99999 block rounded-sm border border-stroke bg-white p-1.5 shadow-sm dark:border-strokedark dark:bg-boxdark lg:hidden"
|
||||||
|
|
@ -7,22 +7,23 @@
|
||||||
>
|
>
|
||||||
<.icon name="hero-bars-3" />
|
<.icon name="hero-bars-3" />
|
||||||
</button>
|
</button>
|
||||||
<a class="block flex-shrink-0 lg:hidden" href="#">
|
<a class="hidden sm:block flex-shrink-0 lg:hidden" href="#">
|
||||||
<h2 class="text-title-md2 font-bold text-white">Pinchflat</h2>
|
<h2 class="text-title-md2 font-bold text-white">Pinchflat</h2>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden sm:block bg-meta-4 rounded-md">
|
<div class="bg-meta-4 rounded-md">
|
||||||
<%!-- Aspirational (for now) --%>
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<span class="absolute left-2 top-1/2 -translate-y-1/2 flex">
|
<span class="absolute left-2 top-1/2 -translate-y-1/2 flex">
|
||||||
<.icon name="hero-magnifying-glass" />
|
<.icon name="hero-magnifying-glass" />
|
||||||
</span>
|
</span>
|
||||||
|
<form action={~p"/search"}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
name="q"
|
||||||
placeholder="Type to search..."
|
placeholder="Type to search..."
|
||||||
class="w-full bg-transparent pl-9 pr-4 border-0 focus:ring-0 focus:outline-none xl:w-125"
|
class="w-full bg-transparent pl-9 pr-4 border-0 focus:ring-0 focus:outline-none xl:w-125"
|
||||||
/>
|
/>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
</.link>
|
</.link>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
<div class="rounded-sm border border-stroke bg-white px-5 py-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5">
|
||||||
<div class="max-w-full overflow-x-auto">
|
<div class="max-w-full overflow-x-auto">
|
||||||
<div class="flex flex-col gap-10 dark:text-white">
|
<div class="flex flex-col gap-10 dark:text-white">
|
||||||
<h3 class="mt-14 font-bold text-xl">Relationships</h3>
|
<h3 class="mt-14 font-bold text-xl">Relationships</h3>
|
||||||
|
|
@ -35,10 +35,10 @@
|
||||||
<.list_items_from_map map={Map.from_struct(@source)} />
|
<.list_items_from_map map={Map.from_struct(@source)} />
|
||||||
|
|
||||||
<h3 class="font-bold text-xl">Downloaded Media</h3>
|
<h3 class="font-bold text-xl">Downloaded Media</h3>
|
||||||
<%= if length(@downloaded_media) > 0 do %>
|
<%= if match?([_|_], @downloaded_media) do %>
|
||||||
<.table rows={@downloaded_media} table_class="text-black dark:text-white">
|
<.table rows={@downloaded_media} table_class="text-black dark:text-white">
|
||||||
<:col :let={media_item} label="Title">
|
<:col :let={media_item} label="Title">
|
||||||
<%= String.slice(media_item.title, 0..50) %>...
|
<%= StringUtils.truncate(media_item.title, 50) %>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="" class="flex place-content-evenly">
|
<:col :let={media_item} label="" class="flex place-content-evenly">
|
||||||
<.link
|
<.link
|
||||||
|
|
@ -54,10 +54,10 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<h3 class="font-bold text-xl">Pending Media</h3>
|
<h3 class="font-bold text-xl">Pending Media</h3>
|
||||||
<%= if length(@pending_media) > 0 do %>
|
<%= if match?([_|_], @pending_media) do %>
|
||||||
<.table rows={@pending_media} table_class="text-black dark:text-white">
|
<.table rows={@pending_media} table_class="text-black dark:text-white">
|
||||||
<:col :let={media_item} label="Title">
|
<:col :let={media_item} label="Title">
|
||||||
<%= String.slice(media_item.title, 0..50) %>...
|
<%= StringUtils.truncate(media_item.title, 50) %>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="" class="flex place-content-evenly">
|
<:col :let={media_item} label="" class="flex place-content-evenly">
|
||||||
<.link
|
<.link
|
||||||
|
|
|
||||||
12
lib/pinchflat_web/controllers/searches/search_controller.ex
Normal file
12
lib/pinchflat_web/controllers/searches/search_controller.ex
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
defmodule PinchflatWeb.Searches.SearchController do
|
||||||
|
use PinchflatWeb, :controller
|
||||||
|
|
||||||
|
alias Pinchflat.Media
|
||||||
|
|
||||||
|
def show(conn, params) do
|
||||||
|
search_term = Map.get(params, "q", "")
|
||||||
|
search_results = Media.search(search_term)
|
||||||
|
|
||||||
|
render(conn, :show, search_term: search_term, search_results: search_results)
|
||||||
|
end
|
||||||
|
end
|
||||||
25
lib/pinchflat_web/controllers/searches/search_html.ex
Normal file
25
lib/pinchflat_web/controllers/searches/search_html.ex
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
defmodule PinchflatWeb.Searches.SearchHTML do
|
||||||
|
use PinchflatWeb, :html
|
||||||
|
|
||||||
|
embed_templates "search_html/*"
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Highlight search terms in a string of text based on `[PF_HIGHLIGHT]` and `[/PF_HIGHLIGHT]` tags
|
||||||
|
"""
|
||||||
|
attr :text, :string, required: true
|
||||||
|
|
||||||
|
def highlight_search_terms(assigns) do
|
||||||
|
split_string = String.split(assigns.text, ~r{\[PF_HIGHLIGHT\]|\[/PF_HIGHLIGHT\]}, include_captures: true)
|
||||||
|
assigns = assign(assigns, split_string: split_string)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<%= for fragment <- @split_string do %>
|
||||||
|
<%= render_fragment(fragment) %>
|
||||||
|
<% end %>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_fragment("[PF_HIGHLIGHT]"), do: raw(~s(<span class="font-bold italic">))
|
||||||
|
defp render_fragment("[/PF_HIGHLIGHT]"), do: raw("</span>")
|
||||||
|
defp render_fragment(text), do: text
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
||||||
|
<h2 class="text-title-md2 font-bold text-black dark:text-white">
|
||||||
|
<span class="hidden sm:inline">Search </span>Results for "<%= StringUtils.truncate(@search_term, 50) %>"
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-sm border border-stroke bg-white px-5 py-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5">
|
||||||
|
<div class="max-w-full overflow-x-auto">
|
||||||
|
<div class="flex flex-col gap-10 dark:text-white">
|
||||||
|
<%= if match?([_|_], @search_results) do %>
|
||||||
|
<.table rows={@search_results} table_class="text-black dark:text-white">
|
||||||
|
<:col :let={result} label="Title">
|
||||||
|
<%= StringUtils.truncate(result.title, 40) %>
|
||||||
|
</:col>
|
||||||
|
<:col :let={result} label="Excerpt">
|
||||||
|
<.highlight_search_terms text={result.matching_search_term} />
|
||||||
|
</:col>
|
||||||
|
<:col :let={result} label="" class="flex place-content-evenly">
|
||||||
|
<.link navigate={~p"/media/#{result.id}"} class="hover:text-secondary duration-200 ease-in-out mx-0.5">
|
||||||
|
<.icon name="hero-eye" />
|
||||||
|
</.link>
|
||||||
|
</:col>
|
||||||
|
</.table>
|
||||||
|
<% else %>
|
||||||
|
<p class="font-bold text-lg text-center text-black dark:text-white">No results found</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -21,6 +21,7 @@ defmodule PinchflatWeb.Router do
|
||||||
|
|
||||||
resources "/media_profiles", MediaProfiles.MediaProfileController
|
resources "/media_profiles", MediaProfiles.MediaProfileController
|
||||||
resources "/media", Media.MediaItemController, only: [:show]
|
resources "/media", Media.MediaItemController, only: [:show]
|
||||||
|
resources "/search", Searches.SearchController, only: [:show], singleton: true
|
||||||
|
|
||||||
resources "/sources", MediaSources.SourceController do
|
resources "/sources", MediaSources.SourceController do
|
||||||
resources "/media", Media.MediaItemController, only: [:show]
|
resources "/media", Media.MediaItemController, only: [:show]
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ defmodule Pinchflat.MediaTest do
|
||||||
assert String.contains?(res.matching_search_term, "The [PF_HIGHLIGHT]quick[/PF_HIGHLIGHT] brown fox")
|
assert String.contains?(res.matching_search_term, "The [PF_HIGHLIGHT]quick[/PF_HIGHLIGHT] brown fox")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "optionall lets you specify a limit" do
|
test "optionally lets you specify a limit" do
|
||||||
media_item_fixture(%{title: "The small gray dog"})
|
media_item_fixture(%{title: "The small gray dog"})
|
||||||
|
|
||||||
assert [_] = Media.search("dog", limit: 1)
|
assert [_] = Media.search("dog", limit: 1)
|
||||||
|
|
|
||||||
|
|
@ -27,4 +27,18 @@ defmodule Pinchflat.Utils.StringUtilsTest do
|
||||||
assert String.length(StringUtils.random_string(64)) == 64
|
assert String.length(StringUtils.random_string(64)) == 64
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "truncate/2" do
|
||||||
|
test "truncates a string to the given length and adds ..." do
|
||||||
|
assert StringUtils.truncate("hello world", 5) == "hello..."
|
||||||
|
end
|
||||||
|
|
||||||
|
test "breaks on a word boundary" do
|
||||||
|
assert StringUtils.truncate("hello world", 7) == "hello..."
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not truncate a string shorter than the given length" do
|
||||||
|
assert StringUtils.truncate("hello", 10) == "hello"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
10
test/pinchflat_web/controllers/search_controller_test.exs
Normal file
10
test/pinchflat_web/controllers/search_controller_test.exs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
defmodule PinchflatWeb.SearchControllerTest do
|
||||||
|
use PinchflatWeb.ConnCase
|
||||||
|
|
||||||
|
describe "show search" do
|
||||||
|
test "renders the page", %{conn: conn} do
|
||||||
|
conn = get(conn, ~p"/search")
|
||||||
|
assert html_response(conn, 200) =~ "Results"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue