feat: add city search functionality

- Implement search_by_name scope in City model
- Add SearchController for handling search input
- Include _search_city partial in cities index view
- Update cities_controller to filter cities based on search query

This commit introduces a new feature that allows users to search for
cities by name using an input field. The search is implemented as a
scope in the City model, and it is integrated into the existing
CitiesController. A dedicated SearchController manages the input
submission with a debouncing mechanism for better performance. The
search field is rendered in the cities index view, enhancing user
interactivity and experience.
This commit is contained in:
songtianlun 2025-02-12 14:00:03 +08:00
parent 51d626a67f
commit 799f3222a9
6 changed files with 82 additions and 4 deletions

View File

@ -6,6 +6,10 @@ class CitiesController < ApplicationController
@regions = Region.includes(:countries).order(:name)
@cities = City.includes(:country, country: :region).order(:name)
if params[:query].present?
@cities = @cities.search_by_name(params[:query])
end
if params[:region]
@current_region = Region.friendly.find(params[:region])
@cities = @cities.by_region(@current_region.id) if @current_region

View File

@ -6,7 +6,9 @@ import { application } from "./application"
import HelloController from "./hello_controller"
import PhotoSwipeLightBoxController from "./photo_swipe_lightbox_controller"
import FlashMessageController from "./flash_controller"
import SearchController from "./search_controller"
application.register("hello", HelloController)
application.register("photo-swipe-lightbox", PhotoSwipeLightBoxController)
application.register("flash", FlashMessageController)
application.register("search", SearchController)

View File

@ -0,0 +1,13 @@
// app/javascript/controllers/search_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input"]
submit() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.element.requestSubmit()
}, 300)
}
}

View File

@ -97,6 +97,15 @@ class City < ApplicationRecord
.order("COUNT(ahoy_events.id) DESC, cities.name ASC").limit(limit)
end
}
scope :search_by_name, ->(query) {
return all if query.blank?
decoded_query = URI.decode_www_form_component(query).downcase
where(
"LOWER(cities.name) LIKE :query", query: "%#{decoded_query}%"
)
}
def to_s

View File

@ -0,0 +1,30 @@
<div class="mt-8 max-w-2xl mx-auto">
<%= form_with url: cities_path, method: :get, class: "relative", data: { controller: "search" } do |f| %>
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
<svg class="w-5 h-5 text-base-content/50" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<%= f.text_field :query,
value: params[:query] ? URI.decode_www_form_component(params[:query]) : nil,
class: "w-full pl-12 pr-4 py-3 rounded-full bg-base-200/50 backdrop-blur-sm border border-base-300 focus:outline-none focus:ring-2 focus:ring-primary/50",
placeholder: "Search cities...",
autocomplete: "off",
data: {
action: "input->search#submit",
search_target: "input"
} %>
<% if params[:query].present? %>
<%= link_to cities_path, class: "absolute inset-y-0 right-0 flex items-center pr-4" do %>
<svg class="w-5 h-5 text-base-content/50 hover:text-base-content" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<% end %>
<% end %>
</div>
<%= f.hidden_field :region, value: params[:region] if params[:region] %>
<%= f.hidden_field :country, value: params[:country] if params[:country] %>
<% end %>
</div>

View File

@ -22,6 +22,8 @@
Discover AI-generated weather art from cities around the world
</p>
<%= render 'cities/search_city' %>
<!-- 特色图片信息 -->
<% if featured_art %>
<div class="inline-block mt-6 px-4 py-2 bg-base-100/80 backdrop-blur-sm rounded-full text-sm">
@ -99,7 +101,7 @@
<% end %>
</div>
<div class="text-sm text-base-content/70">
<div class="text-sm text-base-content/70 hidden">
<%= @cities.count %> <%= 'city'.pluralize(@cities.count) %>
<% if @current_country %>
in <%= @current_country.name %>
@ -111,11 +113,29 @@
</div>
</div>
<div class="container mx-auto px-4 py-8">
<div class="container mx-auto px-4 pb-16">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<%= render partial: 'city', collection: @cities %>
</div>
<% if @cities.empty? %>
<div class="text-center py-16">
<div class="text-base-content/50">
<svg class="w-16 h-16 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
</svg>
<h3 class="text-xl font-semibold mb-2">No cities found</h3>
<p class="text-base-content/70">
Try adjusting your search or filters to find what you're looking for.
</p>
</div>
</div>
<% else %>
<!-- 现有的城市网格代码 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<%= render partial: 'city', collection: @cities %>
</div>
<% end %>
<%= render 'shared/pagination',
collection: @cities,