feat: enhance weather arts and cities features

- Add slug column to weather_arts for friendly URLs.
- Update weather arts retrieval in the controller to use slug.
- Implement region and country filtering in cities index view.
- Optimize city queries with scopes for active status and region/country.
- Improve UI layout and design for the cities index page.

These changes allow better user experience by enabling cleaner URLs for weather arts and facilitating efficient filtering of cities based on selected regions and countries.
This commit is contained in:
songtianlun 2025-01-22 14:04:58 +08:00
parent be7856935f
commit 9cb1467301
9 changed files with 169 additions and 42 deletions

View File

@ -1,4 +1,9 @@
ActiveAdmin.register WeatherArt do ActiveAdmin.register WeatherArt do
controller do
def find_resource
scoped_collection.friendly.find(params[:id])
end
end
# See permitted parameters documentation: # See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters # https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
# #

View File

@ -1,6 +1,18 @@
class CitiesController < ApplicationController class CitiesController < ApplicationController
def index def index
@cities = City.all.order(:name) @cities = City.all.order(:name)
@regions = Region.includes(:countries).order(:name)
@cities = City.includes(:country, country: :region).active.order(:name)
if params[:region]
@current_region = Region.friendly.find(params[:region])
@cities = @cities.by_region(@current_region.id)
end
if params[:country]
@current_country = Country.friendly.find(params[:country])
@cities = @cities.by_country(@current_country.id)
end
end end
def show def show

View File

@ -1,6 +1,6 @@
class WeatherArtsController < ApplicationController class WeatherArtsController < ApplicationController
def show def show
@city = City.friendly.find(params[:city_id]) @city = City.friendly.find(params[:city_id])
@weather_art = @city.weather_arts.find(params[:id]) @weather_art = @city.weather_arts.friendly.find(params[:slug])
end end
end end

View File

@ -1,6 +1,6 @@
class City < ApplicationRecord class City < ApplicationRecord
extend FriendlyId extend FriendlyId
friendly_id :name, use: :slugged friendly_id :slug_candidates, use: :slugged
belongs_to :country belongs_to :country
has_many :weather_arts, dependent: :destroy has_many :weather_arts, dependent: :destroy
@ -11,10 +11,21 @@ class City < ApplicationRecord
delegate :region, to: :country delegate :region, to: :country
scope :by_region, ->(region_id) { joins(:country).where(countries: { region_id: region_id }) }
scope :by_country, ->(country_id) { where(country_id: country_id) }
scope :active, -> { where(active: true) }
def to_s def to_s
name name
end end
def slug_candidates
[
:name,
[ :country, :name ]
]
end
def localized_name def localized_name
I18n.t("cities.#{name.parameterize.underscore}") I18n.t("cities.#{name.parameterize.underscore}")
end end

View File

@ -1,11 +1,21 @@
class WeatherArt < ApplicationRecord class WeatherArt < ApplicationRecord
belongs_to :city extend FriendlyId
friendly_id :weather_date, use: :slugged
belongs_to :city
has_one_attached :image has_one_attached :image
validates :weather_date, presence: true validates :weather_date, presence: true
validates :city_id, presence: true validates :city_id, presence: true
def should_generate_new_friendly_id?
weather_date_changed? || city_id_changed? || super
end
def to_s
"#{city.name} - #{weather_date.strftime('%Y-%m-%d')}"
end
def self.ransackable_associations(auth_object = nil) def self.ransackable_associations(auth_object = nil)
[ "city", "image_attachment", "image_blob" ] [ "city", "image_attachment", "image_blob" ]
end end

View File

@ -1,35 +1,119 @@
<div class="container mx-auto px-4 py-16"> <div class="min-h-screen">
<div class="text-center mb-16 space-y-4">
<h1 class="text-4xl md:text-5xl font-display font-bold">Explore Cities</h1> <!-- 页面标题 -->
<p class="text-xl text-base-content/70 max-w-2xl mx-auto"> <div class="bg-gradient-to-r from-primary/10 to-secondary/10 py-12">
Discover AI-generated weather art from cities around the world <div class="container mx-auto px-4">
</p> <h1 class="text-4xl md:text-5xl font-display font-bold text-center mb-4">
Explore Cities
</h1>
<p class="text-xl text-center text-base-content/70 max-w-2xl mx-auto">
Discover AI-generated weather art from cities around the world
</p>
</div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <!-- 筛选导航 -->
<% @cities.each do |city| %> <div class="container mx-auto px-4 py-8">
<% latest_art = city.weather_arts.last %> <div class="flex flex-wrap gap-4 items-center justify-center mb-8">
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 group"> <%= link_to "All Regions",
<% if latest_art&.image&.attached? %> cities_path,
<figure class="relative aspect-[16/9] overflow-hidden"> class: "btn btn-outline #{'btn-primary' unless @current_region}" %>
<%= image_tag latest_art.image,
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
</figure>
<% end %>
<div class="card-body relative"> <% @regions.each do |region| %>
<h2 class="card-title font-display text-2xl"><%= city.localized_name %></h2> <%= link_to region.name,
<div class="text-base-content/70"> cities_path(region: region.slug),
<p>Lat: <%= city.latitude %></p> class: "btn btn-outline #{'btn-primary' if @current_region == region}" %>
<p>Long: <%= city.longitude %></p> <% end %>
</div> </div>
<div class="card-actions justify-end mt-4">
<%= link_to "View Weather Art", city_path(city), <% if @current_region %>
class: "btn btn-primary" %> <div class="flex flex-wrap gap-4 items-center justify-center mb-8">
</div> <%= link_to "All Countries in #{@current_region.name}",
</div> cities_path(region: @current_region.slug),
class: "btn btn-sm btn-ghost #{'btn-active' unless @current_country}" %>
<% @current_region.countries.order(:name).each do |country| %>
<%= link_to country.name,
cities_path(region: @current_region.slug, country: country.slug),
class: "btn btn-sm btn-ghost #{'btn-active' if @current_country == country}" %>
<% end %>
</div> </div>
<% end %> <% end %>
<!-- 当前选择显示 -->
<div class="text-center mb-8">
<div class="breadcrumbs text-sm justify-center">
<ul>
<li><%= link_to "All Regions", cities_path %></li>
<% if @current_region %>
<li><%= @current_region.name %></li>
<% end %>
<% if @current_country %>
<li><%= @current_country.name %></li>
<% end %>
</ul>
</div>
</div>
</div>
<!-- 城市网格 -->
<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">
<% @cities.each do |city| %>
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 group overflow-hidden">
<% if city.latest_weather_art&.image&.attached? %>
<figure class="relative aspect-[16/9]">
<%= image_tag city.latest_weather_art.image,
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
<div class="absolute bottom-0 left-0 right-0 p-6">
<h3 class="text-2xl font-display text-white mb-1">
<%= city.name %>
</h3>
<p class="text-white/80 text-sm">
<%= city.country.name %>, <%= city.region.name %>
</p>
</div>
</figure>
<% else %>
<div class="card-body">
<h3 class="card-title font-display"><%= city.name %></h3>
<p class="text-base-content/70">
<%= city.country.name %>, <%= city.region.name %>
</p>
<div class="flex gap-2 text-sm text-base-content/60">
<span>Lat: <%= city.latitude %></span>
<span>Long: <%= city.longitude %></span>
</div>
</div>
<% end %>
<div class="card-body <%= 'pt-0' if city.latest_weather_art&.image&.attached? %>">
<div class="flex gap-4 text-sm text-base-content/70">
<div class="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<%= city.timezone %>
</div>
<% if city.latest_weather_art %>
<div class="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<%= city.latest_weather_art.weather_date.strftime("%b %d, %Y") %>
</div>
<% end %>
</div>
<div class="card-actions justify-end mt-4">
<%= link_to "View Details", city_path(city),
class: "btn btn-primary btn-sm" %>
</div>
</div>
</div>
<% end %>
</div>
</div> </div>
</div> </div>

View File

@ -1,8 +1,8 @@
Rails.application.routes.draw do Rails.application.routes.draw do
root "home#index" root "home#index"
resources :cities, only: [ :index, :show ] do resources :cities, only: [:index, :show] do
resources :weather_arts, only: [ :show ] resources :weather_arts, path: 'weather', only: [:show], param: :slug
end end
# namespace :admin do # namespace :admin do

View File

@ -0,0 +1,6 @@
class AddSlugToWeatherArts < ActiveRecord::Migration[8.0]
def change
add_column :weather_arts, :slug, :string
add_index :weather_arts, :slug, unique: true
end
end

17
db/schema.rb generated
View File

@ -10,17 +10,14 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_01_21_020653) do ActiveRecord::Schema[8.0].define(version: 2025_01_22_053220) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
create_table "active_admin_comments", force: :cascade do |t| create_table "active_admin_comments", force: :cascade do |t|
t.string "namespace" t.string "namespace"
t.text "body" t.text "body"
t.string "resource_type" t.string "resource_type"
t.bigint "resource_id" t.integer "resource_id"
t.string "author_type" t.string "author_type"
t.bigint "author_id" t.integer "author_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["author_type", "author_id"], name: "index_active_admin_comments_on_author" t.index ["author_type", "author_id"], name: "index_active_admin_comments_on_author"
@ -80,7 +77,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_21_020653) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "slug" t.string "slug"
t.bigint "country_id", null: false t.integer "country_id", null: false
t.index ["country_id"], name: "index_cities_on_country_id" t.index ["country_id"], name: "index_cities_on_country_id"
t.index ["slug"], name: "index_cities_on_slug", unique: true t.index ["slug"], name: "index_cities_on_slug", unique: true
end end
@ -89,7 +86,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_21_020653) do
t.string "name" t.string "name"
t.string "code" t.string "code"
t.string "slug" t.string "slug"
t.bigint "region_id", null: false t.integer "region_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["code"], name: "index_countries_on_code", unique: true t.index ["code"], name: "index_countries_on_code", unique: true
@ -119,7 +116,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_21_020653) do
end end
create_table "weather_arts", force: :cascade do |t| create_table "weather_arts", force: :cascade do |t|
t.bigint "city_id", null: false t.integer "city_id", null: false
t.date "weather_date" t.date "weather_date"
t.string "description" t.string "description"
t.decimal "temperature" t.decimal "temperature"
@ -134,7 +131,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_21_020653) do
t.text "prompt" t.text "prompt"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "slug"
t.index ["city_id"], name: "index_weather_arts_on_city_id" t.index ["city_id"], name: "index_weather_arts_on_city_id"
t.index ["slug"], name: "index_weather_arts_on_slug", unique: true
end end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"