-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
First, we'll implement the `Episode.containing` scope to transform a search term into an SQL [ILIKE][] query. Next, create the `Search` class to serve as a model for a term-based `Episode` query. We'll construct instances based on the `query` and `page` parameters submitted as `URLSearchParams`. Then, we'll declare the `Search#episodes` method to construct an appropriate `Episode.containing` query, along with a `Search#search_results` method to paginate the collection of `Episode` records and transform them into `SearchResult` instances. Both `Search` and `SearchResult` inherit from the new `ApplicationModel` (not to be confused with `ApplicationRecord`) base class. Finally, we'll introduce the `/podcasts/:podcast_id/search_results` route along with the `SearchResultsController#index` action to fetch results and render them as a list of `search_results/search_result` partials. The `search_results/search_result` partial composes calls to the [highlight][] and [excerpt][] view helpers to find and highlight portions of the result that match the search term. For now, we'll render a [mirage][] search field in the main navigation to link to the `search_results#index` route and template. In the future, we'll be able to progressively enhance search to be globally accessible throughout the application. [ILIKE]: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE [highlight]: https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-highlight [excerpt]: https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-excerpt [mirage]: https://en.wikipedia.org/wiki/Mirage Co-authored-by: Steve Polito <[email protected]> Co-authored-by: Sean Doyle <[email protected]>
- Loading branch information
1 parent
2099437
commit 0febcec
Showing
19 changed files
with
463 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
class SearchResultsController < ApplicationController | ||
include PodcastScoped | ||
|
||
def index | ||
@search = Search.new(search_params.merge(podcast: @podcast)) | ||
@page, @search_results = @search.search_results | ||
end | ||
|
||
private | ||
|
||
def search_params | ||
params.permit!.slice(:page, :query) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
class ApplicationModel | ||
include ActiveModel::Model | ||
include ActiveModel::Attributes | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
class Search < ApplicationModel | ||
include Pagy::Backend | ||
|
||
attribute :podcast | ||
attribute :page | ||
attribute :query, :string | ||
|
||
def search_results | ||
page, paginated_episodes = pagy episodes | ||
|
||
paginated_search_results = paginated_episodes.map { |episode| SearchResult.new(episode:, query:) } | ||
|
||
[page, paginated_search_results] | ||
end | ||
|
||
def episodes | ||
if query.present? | ||
podcast.episodes.most_recent_first.containing(query) | ||
else | ||
podcast.episodes.none | ||
end | ||
end | ||
|
||
private | ||
|
||
def params = {page:} | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
class SearchResult < ApplicationModel | ||
attribute :query, :string | ||
attribute :episode | ||
|
||
delegate_missing_to :episode | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<article aria-labelledby="<%= dom_id(search_result, :title) %>" class="py-10 sm:py-12"> | ||
<div class="lg:px-8"> | ||
<div class="lg:max-w-4xl"> | ||
<div class="mx-auto px-4 sm:px-6 md:max-w-2xl md:px-4 lg:px-0"> | ||
<div class="flex flex-col items-start"> | ||
<h2 id="<%= dom_id(search_result, :title) %>" class="mt-2 text-lg font-bold text-slate-900"> | ||
<%= link_to search_result do %> | ||
<%= highlight search_result.title, search_result.query %> | ||
<% end %> | ||
</h2> | ||
|
||
<%= time_tag search_result.published_at.to_date, class: "order-first font-mono text-sm leading-7 text-slate-500" %> | ||
|
||
<p class="mt-1 text-base leading-7 text-slate-700"> | ||
<%= highlight( | ||
excerpt(search_result.subtitle, search_result.query), | ||
search_result.query | ||
) %> | ||
</p> | ||
|
||
<p class="mt-1 text-base leading-7 text-slate-700"> | ||
<%= highlight( | ||
excerpt(search_result.transcript.to_plain_text, search_result.query), | ||
search_result.query | ||
) %> | ||
</p> | ||
|
||
<div class="mt-4 flex items-center gap-4"> | ||
<form action="<%= url_for(search_result) %>"> | ||
<button class="group flex items-center text-sm font-bold leading-6 text-pink-500 hover:text-pink-700 active:text-pink-900"> | ||
<% with_options class: "h-2.5 w-2.5 fill-current" do |styled| %> | ||
<div class="block group-aria-pressed:hidden"> | ||
<span class="sr-only">Play episode <%= search_result.title %></span> | ||
<%= styled.inline_svg_tag "icons/play.svg" %> | ||
</div> | ||
|
||
<div class="hidden group-aria-pressed:block"> | ||
<span class="sr-only">Pause episode <%= search_result.title %></span> | ||
<%= styled.inline_svg_tag "icons/pause.svg" %> | ||
</div> | ||
<% end %> | ||
|
||
<span class="ml-3" aria-hidden="true">Listen</span> | ||
</button> | ||
</form> | ||
|
||
<span aria-hidden="true" class="text-sm font-bold text-slate-400">/</span> | ||
|
||
<%= link_to "Show notes", search_result, class: "flex items-center text-sm font-bold leading-6 text-pink-500 hover:text-pink-700 active:text-pink-900", aria: {label: "Show notes for episode #{search_result.title}"} %> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</article> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
<%= render "episodes/podcast/frame", podcast: @podcast do %> | ||
<div> | ||
<div class="pt-16 pb-12 sm:pb-4 lg:pt-12"> | ||
<div class="lg:px-8"> | ||
<div class="lg:max-w-4xl"> | ||
<div class="mx-auto px-4 sm:px-6 md:max-w-2xl md:px-4 lg:px-0"> | ||
<h1 id="main_title" class="text-2xl font-bold leading-7 text-slate-900"> | ||
<% if @search.query.present? %> | ||
Search Results for "<%= @search.query %>" | ||
<% else %> | ||
Search | ||
<% end %> | ||
</h1> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<div class="pt-16 pb-12 sm:pb-4 lg:pt-12"> | ||
<div class="lg:px-8"> | ||
<div class="lg:max-w-4xl"> | ||
<div class="mx-auto px-4 sm:px-6 md:max-w-2xl md:px-4 lg:px-0"> | ||
<%= form_with model: @search, scope: "", url: false, method: :get, class: "flex items-center gap-4" do |form| %> | ||
<div class="relative mt-1 rounded-md shadow-sm"> | ||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> | ||
<%= inline_svg_tag "icons/search.svg", class: "h-2.5 w-2.5" %> | ||
</div> | ||
<%= form.label :query, class: "sr-only" %> | ||
<%= form.text_field :query, class: "w-full rounded-md border-gray-300 pl-10 text-sm placeholder:font-mono placeholder:text-sm placeholder:leading-7 placeholder:text-slate-500", | ||
placeholder: "Search", autofocus: true, | ||
aria: {describedby: dom_id(@search, :prompt)} %> | ||
</div> | ||
|
||
<button class="text-sm font-bold leading-6 text-pink-500 hover:text-pink-700 active:text-pink-900"> | ||
Search | ||
</button> | ||
<% end %> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<div class="lg:px-8"> | ||
<div class="lg:max-w-4xl"> | ||
<div class="mx-auto px-4 sm:px-6 md:max-w-2xl md:px-4 lg:px-0"> | ||
<p id="<%= dom_id(@search, :prompt) %>" class="font-mono text-sm font-medium leading-7 text-slate-900"> | ||
Search for episodes by their title, subtitle, or transcript. | ||
</p> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<div class="divide-y divide-slate-100 sm:mt-4 lg:mt-8 lg:border-t lg:border-slate-100"> | ||
<div> | ||
<% if @page.prev %> | ||
<div class="py-10 sm:py-12"> | ||
<div class="lg:px-8"> | ||
<div class="lg:max-w-4xl"> | ||
<div class="flex justify-center mx-auto px-4 sm:px-6 md:max-w-2xl md:px-4 lg:px-0"> | ||
<%= link_to pagy_url_for(@page, @page.prev), rel: "prev", | ||
class: "flex animate-bounce items-center justify-center rounded-full bg-white p-2 shadow-lg text-pink-500 ring-1 ring-slate-900/5 focus:ring hover:text-pink-700 active:text-pink-900" do %> | ||
<span class="sr-only">Load newer episodes</span> | ||
<%= inline_svg_tag "icons/up_arrow.svg", class: "h-6 w-6" %> | ||
<% end %> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
<% end %> | ||
|
||
<% if @search.query.present? %> | ||
<%= render(@search_results) || render("searches/search/empty", search: @search) %> | ||
<% end %> | ||
|
||
<% if @page.next %> | ||
<div> | ||
<div class="py-10 sm:py-12"> | ||
<div class="lg:px-8"> | ||
<div class="lg:max-w-4xl"> | ||
<div class="flex justify-center mx-auto px-4 sm:px-6 md:max-w-2xl md:px-4 lg:px-0"> | ||
<%= link_to pagy_url_for(@page, @page.next), rel: "next", | ||
class: "flex animate-bounce items-center justify-center rounded-full bg-white p-2 shadow-lg text-pink-500 ring-1 ring-slate-900/5 focus:ring hover:text-pink-700 active:text-pink-900" do %> | ||
<span class="sr-only">Load older episodes</span> | ||
<%= inline_svg_tag "icons/down_arrow.svg", class: "h-6 w-6" %> | ||
<% end %> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
<% end %> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<%= render "episodes/player" %> | ||
<% end %> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<div class="py-10 sm:py-12"> | ||
<div class="lg:px-8"> | ||
<div class="lg:max-w-4xl"> | ||
<div class="mx-auto px-4 sm:px-6 md:max-w-2xl md:px-4 lg:px-0"> | ||
<p class="font-mono text-sm font-medium leading-7 text-slate-900"> | ||
We couldn't find any episodes matching "<%= search.query %>". | ||
</p> | ||
</div> | ||
</div> | ||
</div> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
require "test_helper" | ||
|
||
module SearchResults | ||
class IndexTest < ActionDispatch::IntegrationTest | ||
test "renders a prompt when the query is blank" do | ||
podcast = create(:podcast) | ||
|
||
get podcast_search_results_path(podcast) | ||
|
||
within :main, "Search" do | ||
within :element, "form", method: "get", action: false do | ||
assert_field "Query", type: "text", described_by: "Search for episodes by their title, subtitle, or transcript." | ||
assert_button "Search" | ||
end | ||
end | ||
end | ||
|
||
test "renders a prompt when there are no results" do | ||
podcast = create(:podcast) | ||
query = "term" | ||
|
||
get podcast_search_results_path(podcast), params: {query:} | ||
|
||
within :main, %(Search Results for "#{query}") do | ||
within :element, "form", method: "get", action: false do | ||
assert_field "Query", type: "text", with: query | ||
assert_button "Search" | ||
end | ||
|
||
assert_text %(We couldn't find any episodes matching "#{query}".) | ||
end | ||
end | ||
|
||
test "renders a list of the Podcast's Episodes that match the query" do | ||
podcast = create(:podcast) | ||
included = create(:episode, podcast:, title: "A title match", subtitle: "A subtitle match", transcript: "A matching transcript") | ||
excluded = create(:episode, podcast:, title: "ignored") | ||
query = "match" | ||
|
||
get podcast_search_results_path(podcast), params: {query:} | ||
|
||
within :main, %(Search Results for "match") do | ||
assert_no_selector :article, excluded.title | ||
|
||
within :article, included.title do | ||
within :link, text: included.title do | ||
assert_selector "mark", text: query | ||
end | ||
within "p", text: included.subtitle do | ||
assert_selector "mark", text: query | ||
end | ||
within "p", text: included.transcript.to_plain_text do | ||
assert_selector "mark", text: query | ||
end | ||
assert_link included.title, href: podcast_episode_path(podcast, included) | ||
assert_form action: podcast_episode_path(podcast, included), method: false, buttons: [["Play episode #{included.title}", text: "Listen"]] | ||
assert_link "Show notes for episode #{included.title}", text: "Show notes", href: podcast_episode_path(podcast, included) | ||
end | ||
end | ||
end | ||
|
||
test "renders an empty placeholder for the player" do | ||
podcast = create(:podcast) | ||
|
||
get podcast_search_results_path(podcast) | ||
|
||
within :element, id: "player" do | ||
assert_selector :element, id: "audio" | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.