From c3bb6f453bba98989083176ffa74a1d4e2aabce4 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Sun, 6 Jul 2025 22:49:33 -0700 Subject: [PATCH] Implement dynamic navbar autocomplete search functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add real-time autocomplete endpoint with fast name-only queries - Replace static search form with interactive Alpine.js component - Include 300ms debounced search with keyboard navigation support - Add instructional help dropdown when search input is focused but empty - Implement proper content type icons with hex colors instead of CSS classes - Add database indexes for optimized name searches across all content types - Support arrow key navigation, Enter to select/search, Escape to close - Maintain full search functionality as fallback for comprehensive results 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/controllers/search_controller.rb | 58 +++++ .../tailwind/navbar/_logged_in.html.erb | 216 ++++++++++++++++-- config/routes.rb | 1 + ...24945_add_name_indexes_to_content_types.rb | 47 ++++ 4 files changed, 307 insertions(+), 15 deletions(-) create mode 100644 db/migrate/20250707024945_add_name_indexes_to_content_types.rb diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 10d11b15..78210ed6 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -129,6 +129,64 @@ class SearchController < ApplicationController @seen_result_pages = {} end + def autocomplete + @query = params[:q]&.strip + + # Return empty results for short or empty queries + if @query.blank? || @query.length < 2 + render json: [] + return + end + + results = [] + + # Fast name-only search across all content types + Rails.application.config.content_types[:all].each do |content_type| + begin + model_class = content_type.name.constantize + + # Only search content types that have name columns + if model_class.column_names.include?('name') + # Fast query with limit to prevent slow searches + matching_entities = model_class + .where(user_id: current_user.id) + .where("LOWER(name) LIKE LOWER(?)", "%#{@query}%") + .limit(5) # Limit per content type + .select(:id, :name, :created_at) # Only select needed columns + + matching_entities.each do |entity| + results << { + id: entity.id, + name: entity.name, + type: content_type.name, + icon: content_type.icon, + color: content_type.hex_color, + url: main_app.polymorphic_path(entity), + created_at: entity.created_at + } + end + end + rescue => e + # Skip any content types that have issues + Rails.logger.debug "Autocomplete: Skipping #{content_type.name}: #{e.message}" + end + + # Stop if we have enough results + break if results.size >= 15 + end + + # Sort by relevance: exact matches first, then by name + results.sort_by! do |result| + exact_match = result[:name].downcase == @query.downcase ? 0 : 1 + [exact_match, result[:name].downcase] + end + + # Limit total results + results = results.first(12) + + render json: results + end + helper_method :highlight_search_terms private diff --git a/app/views/layouts/tailwind/navbar/_logged_in.html.erb b/app/views/layouts/tailwind/navbar/_logged_in.html.erb index c5817177..ec30c69d 100644 --- a/app/views/layouts/tailwind/navbar/_logged_in.html.erb +++ b/app/views/layouts/tailwind/navbar/_logged_in.html.erb @@ -27,21 +27,98 @@
<% if user_signed_in? %> - <%= form_tag main_app.search_path, method: :get, class: 'hidden md:inline-flex items-center relative' do %> + +
@@ -136,4 +213,113 @@
<% end %>
- \ No newline at end of file + + + \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 5c9ea5c0..a6d43bba 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -354,6 +354,7 @@ Rails.application.routes.draw do end get 'search/', to: 'search#results' + get 'search/autocomplete', to: 'search#autocomplete' authenticate :user, lambda { |u| u.site_administrator? || Rails.env.development? } do scope 'admin' do diff --git a/db/migrate/20250707024945_add_name_indexes_to_content_types.rb b/db/migrate/20250707024945_add_name_indexes_to_content_types.rb new file mode 100644 index 00000000..5871357b --- /dev/null +++ b/db/migrate/20250707024945_add_name_indexes_to_content_types.rb @@ -0,0 +1,47 @@ +class AddNameIndexesToContentTypes < ActiveRecord::Migration[6.1] + def up + # Add optimized name search indexes for all content types + content_tables = %w[ + universes characters locations items buildings conditions continents countries creatures deities + floras foods governments groups jobs landmarks languages lores magics planets races religions + scenes schools sports technologies towns traditions vehicles + ] + + content_tables.each do |table_name| + next unless table_exists?(table_name) + next unless column_exists?(table_name, :name) + next unless column_exists?(table_name, :user_id) + + if ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql') + # PostgreSQL: Add trigram index for fast LIKE searches + begin + execute "CREATE EXTENSION IF NOT EXISTS pg_trgm" + add_index table_name, [:user_id, :name], name: "idx_#{table_name}_user_name" + add_index table_name, :name, using: :gin, opclass: {name: :gin_trgm_ops}, name: "idx_#{table_name}_name_trgm" + rescue => e + Rails.logger.warn "Could not add trigram index to #{table_name}: #{e.message}" + # Fallback to regular index + add_index table_name, [:user_id, :name], name: "idx_#{table_name}_user_name" unless index_exists?(table_name, [:user_id, :name]) + end + else + # SQLite and other databases: Regular indexes + add_index table_name, [:user_id, :name], name: "idx_#{table_name}_user_name" unless index_exists?(table_name, [:user_id, :name]) + end + end + end + + def down + content_tables = %w[ + universes characters locations items buildings conditions continents countries creatures deities + floras foods governments groups jobs landmarks languages lores magics planets races religions + scenes schools sports technologies towns traditions vehicles + ] + + content_tables.each do |table_name| + next unless table_exists?(table_name) + + remove_index table_name, name: "idx_#{table_name}_user_name" if index_exists?(table_name, [:user_id, :name], name: "idx_#{table_name}_user_name") + remove_index table_name, name: "idx_#{table_name}_name_trgm" if index_exists?(table_name, :name, name: "idx_#{table_name}_name_trgm") + end + end +end