From 28532fb109827843271532a8445fde6adc292ee4 Mon Sep 17 00:00:00 2001 From: Runar Ingebrigtsen Date: Thu, 22 Jan 2026 23:52:53 +0100 Subject: [PATCH] autosubmit, live search --- app/controllers/entries_controller.rb | 15 +- app/javascript/application.js | 2 + app/javascript/controllers/application.js | 7 + app/javascript/controllers/index.js | 4 + .../controllers/search_controller.js | 23 +++ app/views/entries/_filters.html.erb | 77 ++++++++ app/views/entries/_results.html.erb | 117 +++++++++++ app/views/entries/_results_frame.html.erb | 3 + app/views/entries/_shell.html.erb | 7 + app/views/entries/index.html.erb | 184 +----------------- app/views/layouts/application.html.erb | 1 + config/importmap.rb | 5 + 12 files changed, 263 insertions(+), 182 deletions(-) create mode 100644 app/javascript/application.js create mode 100644 app/javascript/controllers/application.js create mode 100644 app/javascript/controllers/index.js create mode 100644 app/javascript/controllers/search_controller.js create mode 100644 app/views/entries/_filters.html.erb create mode 100644 app/views/entries/_results.html.erb create mode 100644 app/views/entries/_results_frame.html.erb create mode 100644 app/views/entries/_shell.html.erb create mode 100644 config/importmap.rb diff --git a/app/controllers/entries_controller.rb b/app/controllers/entries_controller.rb index 0d74df7..b5e0723 100644 --- a/app/controllers/entries_controller.rb +++ b/app/controllers/entries_controller.rb @@ -1,5 +1,5 @@ class EntriesController < ApplicationController - before_action :set_entry, only: [:show] + before_action :set_entry, only: [ :show ] def index @supported_languages = SupportedLanguage.where(active: true).order(:sort_order, :name) @@ -7,7 +7,7 @@ class EntriesController < ApplicationController @category = params[:category].presence @query = params[:q].to_s.strip @starts_with = params[:starts_with].presence - @page = [params[:page].to_i, 1].max + @page = [ params[:page].to_i, 1 ].max @per_page = 25 entries_scope = Entry.all @@ -25,13 +25,13 @@ class EntriesController < ApplicationController @verified_count = Entry.where(verified: true).count @needs_review_count = @entry_count - @verified_count @complete_entries_count = @supported_languages.reduce(Entry.all) do |scope, language| - scope.where.not(language.code => [nil, ""]) + scope.where.not(language.code => [ nil, "" ]) end.count @missing_entries_count = @entry_count - @complete_entries_count @language_completion = @supported_languages.index_with do |language| next 0 if @entry_count.zero? - (Entry.where.not(language.code => [nil, ""]).count * 100.0 / @entry_count).round + (Entry.where.not(language.code => [ nil, "" ]).count * 100.0 / @entry_count).round end if @language_code.present? @@ -40,6 +40,13 @@ class EntriesController < ApplicationController else @display_languages = @supported_languages end + + if turbo_frame_request? + turbo_frame_id = request.headers["Turbo-Frame"] + if turbo_frame_id == "entries_results" + render partial: "entries/results_frame", layout: false + end + end end def show diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000..76c8ec2 --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1,2 @@ +import "@hotwired/turbo-rails" +import "controllers" diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 0000000..38c9def --- /dev/null +++ b/app/javascript/controllers/application.js @@ -0,0 +1,7 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() +application.debug = false +window.Stimulus = application + +export { application } diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 0000000..750e1a3 --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,4 @@ +import { application } from "controllers/application" +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" + +eagerLoadControllersFrom("controllers", application) diff --git a/app/javascript/controllers/search_controller.js b/app/javascript/controllers/search_controller.js new file mode 100644 index 0000000..cfb5631 --- /dev/null +++ b/app/javascript/controllers/search_controller.js @@ -0,0 +1,23 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static values = { + debounce: { type: Number, default: 250 } + } + + connect() { + console.log("Search controller connected", this.element) + this.submitTimeoutId = null + } + + queueSubmit(event) { + console.log("queueSubmit fired", event.target.value) + clearTimeout(this.submitTimeoutId) + this.submitTimeoutId = setTimeout(() => this.submit(), this.debounceValue) + } + + submit() { + console.log("submit() called") + this.element.requestSubmit() + } +} diff --git a/app/views/entries/_filters.html.erb b/app/views/entries/_filters.html.erb new file mode 100644 index 0000000..bd094fa --- /dev/null +++ b/app/views/entries/_filters.html.erb @@ -0,0 +1,77 @@ +
+
+ <%= form_with url: entries_path, + method: :get, + data: { + controller: "search", + turbo_frame: "entries_results" + } do |form| %> +
+
+ + + +
+ <% if @query.present? %> +
+ <%= link_to "×", + entries_path(category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence), + class: "text-slate-400 hover:text-slate-600 text-2xl leading-none", + aria: { label: "Clear search" }, + data: { turbo_frame: "entries_results" } %> +
+ <% end %> + <%= form.text_field :q, + value: @query, + placeholder: "Search words, phrases, or biblical terms...", + class: "block w-full pl-11 pr-10 py-4 bg-white border border-slate-200 rounded-2xl shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition", + data: { action: "input->search#queueSubmit" } %> +
+ <% end %> + +
+ <% base_params = { q: @query.presence, category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence }.compact %> + <% all_category_params = base_params.except(:category) %> + <%= link_to "All", + entries_path(all_category_params), + class: "px-4 py-1.5 rounded-full #{@category.blank? ? 'bg-indigo-100 text-indigo-700' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'} text-xs font-bold uppercase tracking-wider", + data: { turbo_frame: "entries_results" } %> + <% Entry.categories.keys.each do |category_name| %> + <%= link_to category_name.tr('_', ' ').capitalize, + entries_path(base_params.merge(category: category_name)), + class: "px-4 py-1.5 rounded-full #{@category == category_name ? 'bg-indigo-100 text-indigo-700' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'} text-xs font-bold uppercase tracking-wider", + data: { turbo_frame: "entries_results" } %> + <% end %> +
+ +
+ <% all_language_params = base_params.except(:language, :starts_with, :page) %> + <%= link_to "All Languages", + entries_path(all_language_params), + class: "px-3 py-1.5 rounded-full #{@language_code.blank? ? 'bg-slate-900 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-slate-300'} text-xs font-semibold uppercase tracking-wider", + data: { turbo_frame: "entries_results" } %> + <% @supported_languages.each do |language| %> + <%= link_to "#{language.name} (#{language.code.upcase})", + entries_path(all_language_params.merge(language: language.code)), + class: "px-3 py-1.5 rounded-full #{@language_code == language.code ? 'bg-slate-900 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-slate-300'} text-xs font-semibold uppercase tracking-wider", + data: { turbo_frame: "entries_results" } %> + <% end %> +
+ + <% if @language_code.present? %> +
+ <% alphabet_params = base_params.merge(language: @language_code).except(:starts_with, :page) %> + <%= link_to "All", + entries_path(alphabet_params), + class: "px-2.5 py-1 rounded-md #{@starts_with.blank? ? 'bg-indigo-600 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'}", + data: { turbo_frame: "entries_results" } %> + <% alphabet_letters(@language_code).each do |letter| %> + <%= link_to letter, + entries_path(alphabet_params.merge(starts_with: letter)), + class: "px-2.5 py-1 rounded-md #{@starts_with == letter ? 'bg-indigo-600 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'}", + data: { turbo_frame: "entries_results" } %> + <% end %> +
+ <% end %> +
+
diff --git a/app/views/entries/_results.html.erb b/app/views/entries/_results.html.erb new file mode 100644 index 0000000..ba929c3 --- /dev/null +++ b/app/views/entries/_results.html.erb @@ -0,0 +1,117 @@ +
+
+
+
+
Fully Translated
+
<%= number_with_delimiter(@complete_entries_count) %>
+
+
+
Needs Review
+
<%= number_with_delimiter(@needs_review_count) %>
+
+
+
Missing Translations
+
<%= number_with_delimiter(@missing_entries_count) %>
+
+
+
+
+ +
+
+
+
+

Translation Table

+
+
<%= @total_entries %> entries
+
+ +
+
+ + + + <% preferred_language_code = @language_code.presence || "fi" %> + <% preferred_language = @display_languages.find { |language| language.code == preferred_language_code } %> + + <% table_languages = @display_languages.reject { |language| language.code == preferred_language_code } %> + <% if preferred_language_code != "en" %> + <% english_language, other_languages = table_languages.partition { |language| language.code == "en" } %> + <% table_languages = english_language + other_languages %> + <% end %> + <% table_languages.each do |language| %> + + <% end %> + + + + <% if @entries.empty? %> + + + + <% else %> + <% @entries.each do |entry| %> + <% translation_values = table_languages.map { |language| entry.public_send(language.code) } %> + <% missing_any = translation_values.any?(&:blank?) %> + + + <% table_languages.each do |language| %> + <% translation = entry.public_send(language.code) %> + + <% end %> + + <% end %> + <% end %> + +
+
<%= preferred_language&.name || "Finnish" %> + <%= preferred_language_code.upcase %>
+
Category / Status
+
+
<%= language.name %> + <%= language.code.upcase %>
+
+ <%= @language_completion.fetch(language, 0) %>% complete +
+
+ No entries matched your filters. +
+ <% primary_text = entry.public_send(preferred_language_code).presence || "Untitled" %> +
<%= primary_text %>
+
+ <%= format_entry_category(entry) %> + <% if entry.verified? %> + Verified + <% else %> + Unverified + <% end %> + <%= link_to "View", + entry_path(entry), + class: "text-indigo-600 font-semibold hover:underline" %> +
+
+ <% if translation.present? %> + <%= translation %> + <% else %> + + <% end %> +
+
+
+ +
+
Page <%= @page %> of <%= [@total_pages, 1].max %>
+
+ <% previous_page = @page > 1 ? @page - 1 : nil %> + <% next_page = @page < @total_pages ? @page + 1 : nil %> + <% pagination_params = { q: @query.presence, category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence }.compact %> + <%= link_to "Previous", previous_page ? entries_path(pagination_params.merge(page: previous_page)) : "#", + class: "px-3 py-1.5 rounded-md border border-slate-200 #{previous_page ? 'hover:border-indigo-300' : 'text-slate-300 cursor-not-allowed'}", + data: { turbo_frame: "entries_results" } %> + <%= link_to "Next", next_page ? entries_path(pagination_params.merge(page: next_page)) : "#", + class: "px-3 py-1.5 rounded-md border border-slate-200 #{next_page ? 'hover:border-indigo-300' : 'text-slate-300 cursor-not-allowed'}", + data: { turbo_frame: "entries_results" } %> +
+
+
+
diff --git a/app/views/entries/_results_frame.html.erb b/app/views/entries/_results_frame.html.erb new file mode 100644 index 0000000..20704b9 --- /dev/null +++ b/app/views/entries/_results_frame.html.erb @@ -0,0 +1,3 @@ +<%= turbo_frame_tag "entries_results" do %> + <%= render "entries/results" %> +<% end %> diff --git a/app/views/entries/_shell.html.erb b/app/views/entries/_shell.html.erb new file mode 100644 index 0000000..a171ace --- /dev/null +++ b/app/views/entries/_shell.html.erb @@ -0,0 +1,7 @@ +
+ <%= render "entries/filters" %> + + <%= turbo_frame_tag "entries_results" do %> + <%= render "entries/results" %> + <% end %> +
diff --git a/app/views/entries/index.html.erb b/app/views/entries/index.html.erb index 0456bed..e2eef6e 100644 --- a/app/views/entries/index.html.erb +++ b/app/views/entries/index.html.erb @@ -16,186 +16,14 @@ <%= link_to "Sign In", "#", class: "bg-indigo-600 text-white px-4 py-2 rounded-lg text-sm font-semibold hover:bg-indigo-700 transition" %> - -
- <%= form_with url: entries_path, method: :get, local: true do |form| %> -
-
- - - -
- <% if @query.present? %> -
- <%= link_to "×", - entries_path(category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence), - class: "text-slate-400 hover:text-slate-600 text-2xl leading-none", - aria: { label: "Clear search" } %> -
- <% end %> - <%= form.text_field :q, - value: @query, - placeholder: "Search words, phrases, or biblical terms...", - class: "block w-full pl-11 pr-10 py-4 bg-white border border-slate-200 rounded-2xl shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" %> -
- <% end %> - -
- <% base_params = { q: @query.presence, category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence }.compact %> - <% all_category_params = base_params.except(:category) %> - <%= link_to "All", - entries_path(all_category_params), - class: "px-4 py-1.5 rounded-full #{@category.blank? ? 'bg-indigo-100 text-indigo-700' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'} text-xs font-bold uppercase tracking-wider" %> - <% Entry.categories.keys.each do |category_name| %> - <%= link_to category_name.tr('_', ' ').capitalize, - entries_path(base_params.merge(category: category_name)), - class: "px-4 py-1.5 rounded-full #{@category == category_name ? 'bg-indigo-100 text-indigo-700' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'} text-xs font-bold uppercase tracking-wider" %> - <% end %> -
- -
- <% all_language_params = base_params.except(:language, :starts_with, :page) %> - <%= link_to "All Languages", - entries_path(all_language_params), - class: "px-3 py-1.5 rounded-full #{@language_code.blank? ? 'bg-slate-900 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-slate-300'} text-xs font-semibold uppercase tracking-wider" %> - <% @supported_languages.each do |language| %> - <%= link_to "#{language.name} (#{language.code.upcase})", - entries_path(all_language_params.merge(language: language.code)), - class: "px-3 py-1.5 rounded-full #{@language_code == language.code ? 'bg-slate-900 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-slate-300'} text-xs font-semibold uppercase tracking-wider" %> - <% end %> -
- - <% if @language_code.present? %> -
- <% alphabet_params = base_params.merge(language: @language_code).except(:starts_with, :page) %> - <%= link_to "All", - entries_path(alphabet_params), - class: "px-2.5 py-1 rounded-md #{@starts_with.blank? ? 'bg-indigo-600 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'}" %> - <% alphabet_letters(@language_code).each do |letter| %> - <%= link_to letter, - entries_path(alphabet_params.merge(starts_with: letter)), - class: "px-2.5 py-1 rounded-md #{@starts_with == letter ? 'bg-indigo-600 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'}" %> - <% end %> -
- <% end %> -
-
-
-
-
-
Fully Translated
-
<%= number_with_delimiter(@complete_entries_count) %>
-
-
-
Needs Review
-
<%= number_with_delimiter(@needs_review_count) %>
-
-
-
Missing Translations
-
<%= number_with_delimiter(@missing_entries_count) %>
-
-
-
-
+
+ <%= render "entries/filters" %> -
-
-
-
-

Translation Table

-

Public glossary entries with verified status and language coverage.

-
-
<%= @total_entries %> entries
-
- -
-
- - - - <% preferred_language_code = @language_code.presence || "fi" %> - <% preferred_language = @display_languages.find { |language| language.code == preferred_language_code } %> - - <% table_languages = @display_languages.reject { |language| language.code == preferred_language_code } %> - <% if preferred_language_code != "en" %> - <% english_language, other_languages = table_languages.partition { |language| language.code == "en" } %> - <% table_languages = english_language + other_languages %> - <% end %> - <% table_languages.each do |language| %> - - <% end %> - - - - <% if @entries.empty? %> - - - - <% else %> - <% @entries.each do |entry| %> - <% translation_values = table_languages.map { |language| entry.public_send(language.code) } %> - <% missing_any = translation_values.any?(&:blank?) %> - - - <% table_languages.each do |language| %> - <% translation = entry.public_send(language.code) %> - - <% end %> - - <% end %> - <% end %> - -
-
<%= preferred_language&.name || "Finnish" %> - <%= preferred_language_code.upcase %>
-
Category / Status
-
-
<%= language.name %> - <%= language.code.upcase %>
-
- <%= @language_completion.fetch(language, 0) %>% complete -
-
- No entries matched your filters. -
- <% primary_text = entry.public_send(preferred_language_code).presence || "Untitled" %> -
<%= primary_text %>
-
- <%= format_entry_category(entry) %> - <% if entry.verified? %> - Verified - <% else %> - Unverified - <% end %> - <%= link_to "View", - entry_path(entry), - class: "text-indigo-600 font-semibold hover:underline" %> -
-
- <% if translation.present? %> - <%= translation %> - <% else %> - - <% end %> -
-
-
- -
-
Page <%= @page %> of <%= [@total_pages, 1].max %>
-
- <% previous_page = @page > 1 ? @page - 1 : nil %> - <% next_page = @page < @total_pages ? @page + 1 : nil %> - <% pagination_params = { q: @query.presence, category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence }.compact %> - <%= link_to "Previous", previous_page ? entries_path(pagination_params.merge(page: previous_page)) : "#", - class: "px-3 py-1.5 rounded-md border border-slate-200 #{previous_page ? 'hover:border-indigo-300' : 'text-slate-300 cursor-not-allowed'}" %> - <%= link_to "Next", next_page ? entries_path(pagination_params.merge(page: next_page)) : "#", - class: "px-3 py-1.5 rounded-md border border-slate-200 #{next_page ? 'hover:border-indigo-300' : 'text-slate-300 cursor-not-allowed'}" %> -
-
-
-
+ <%= turbo_frame_tag "entries_results" do %> + <%= render "entries/results" %> + <% end %> +
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 20c0fe1..b8fc566 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -20,6 +20,7 @@ <%# Includes all stylesheet files in app/assets/stylesheets %> <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 0000000..b8ad4bd --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,5 @@ +pin "application" +pin "@hotwired/turbo-rails", to: "turbo.min.js" +pin "@hotwired/stimulus", to: "stimulus.min.js" +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" +pin_all_from "app/javascript/controllers", under: "controllers"