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:
parent
be7856935f
commit
9cb1467301
@ -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
|
||||||
#
|
#
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<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
|
Discover AI-generated weather art from cities around the world
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选导航 -->
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="flex flex-wrap gap-4 items-center justify-center mb-8">
|
||||||
|
<%= link_to "All Regions",
|
||||||
|
cities_path,
|
||||||
|
class: "btn btn-outline #{'btn-primary' unless @current_region}" %>
|
||||||
|
|
||||||
|
<% @regions.each do |region| %>
|
||||||
|
<%= link_to region.name,
|
||||||
|
cities_path(region: region.slug),
|
||||||
|
class: "btn btn-outline #{'btn-primary' if @current_region == region}" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if @current_region %>
|
||||||
|
<div class="flex flex-wrap gap-4 items-center justify-center mb-8">
|
||||||
|
<%= link_to "All Countries in #{@current_region.name}",
|
||||||
|
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>
|
||||||
|
<% 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">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
<% @cities.each do |city| %>
|
<% @cities.each do |city| %>
|
||||||
<% latest_art = city.weather_arts.last %>
|
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 group overflow-hidden">
|
||||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 group">
|
<% if city.latest_weather_art&.image&.attached? %>
|
||||||
<% if latest_art&.image&.attached? %>
|
<figure class="relative aspect-[16/9]">
|
||||||
<figure class="relative aspect-[16/9] overflow-hidden">
|
<%= image_tag city.latest_weather_art.image,
|
||||||
<%= image_tag latest_art.image,
|
|
||||||
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
|
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 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>
|
</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 %>
|
<% end %>
|
||||||
|
|
||||||
<div class="card-body relative">
|
<div class="card-body <%= 'pt-0' if city.latest_weather_art&.image&.attached? %>">
|
||||||
<h2 class="card-title font-display text-2xl"><%= city.localized_name %></h2>
|
<div class="flex gap-4 text-sm text-base-content/70">
|
||||||
<div class="text-base-content/70">
|
<div class="flex items-center gap-1">
|
||||||
<p>Lat: <%= city.latitude %></p>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<p>Long: <%= city.longitude %></p>
|
<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>
|
</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">
|
<div class="card-actions justify-end mt-4">
|
||||||
<%= link_to "View Weather Art", city_path(city),
|
<%= link_to "View Details", city_path(city),
|
||||||
class: "btn btn-primary" %>
|
class: "btn btn-primary btn-sm" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
@ -2,7 +2,7 @@ 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
|
||||||
|
6
db/migrate/20250122053220_add_slug_to_weather_arts.rb
Normal file
6
db/migrate/20250122053220_add_slug_to_weather_arts.rb
Normal 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
17
db/schema.rb
generated
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user