feat: add cities feature with weather arts

- Implement CitiesController for listing and showing cities
- Create City and WeatherArt models with associations
- Add views for cities index and show, displaying weather arts
- Include routes for cities and active storage for images
- Update migrations for weather arts and seed data for testing

This commit introduces a comprehensive cities feature that allows users to view cities along with their associated weather art. The implementation includes necessary database migrations, routes, and controller actions to support this new functionality.
This commit is contained in:
songtianlun 2025-01-18 21:42:31 +08:00
parent f4857f73fc
commit 8e8c60254f
16 changed files with 357 additions and 16 deletions

View File

@ -0,0 +1,11 @@
class CitiesController < ApplicationController
def index
@cities = City.all
@cities = City.all.includes(:weather_arts).page(params[:page])
end
def show
@city = City.find(params[:id])
@weather_arts = @city.weather_arts.latest.page(params[:page])
end
end

View File

@ -1,5 +1,7 @@
class StaticPagesController < ApplicationController
def home
@featured_cities = City.featured.limit(5)
@latest_arts = WeatherArt.latest.limit(6)
end
def help

View File

@ -0,0 +1,2 @@
module CitiesHelper
end

View File

@ -1,2 +1,13 @@
class City < ApplicationRecord
has_many :weather_arts
scope :featured, -> { where(featured: true) }
def current_weather_art
weather_arts.find_by(weather_date: Date.current)
end
def weather_art_for_date(date)
weather_arts.find_by(weather_date: date)
end
end

View File

@ -1,2 +1,12 @@
class WeatherArt < ApplicationRecord
belongs_to :city
has_one_attached :image
scope :latest, -> { order(created_at: :desc) }
def image_url
# 这里实现获取图片URL的逻辑可以是AWS S3或其他存储服务
Rails.application.routes.url_helpers.rails_blob_path(image, only_path: true) if image.attached?
end
end

View File

@ -0,0 +1,20 @@
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">Cities</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<% @cities.each do |city| %>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title"><%= city.name %></h2>
<p><%= city.country %></p>
<div class="card-actions justify-end">
<%= link_to 'View Weather History', city_path(city),
class: "btn btn-primary" %>
</div>
</div>
</div>
<% end %>
</div>
<%= paginate @cities %>
</div>

View File

@ -0,0 +1,130 @@
<!-- app/views/cities/show.html.erb -->
<div class="container mx-auto px-4 py-8">
<!-- 城市信息头部 -->
<div class="bg-base-200 rounded-box p-8 mb-8">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<h1 class="text-4xl font-bold mb-2"><%= @city.name %></h1>
<div class="flex items-center gap-2 text-base-content/70">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd" />
</svg>
<span><%= @city.country %></span>
</div>
</div>
<!-- 当前天气信息 -->
<% if @current_weather_art = @city.current_weather_art %>
<div class="stats shadow">
<div class="stat">
<div class="stat-title">Temperature</div>
<div class="stat-value"><%= @current_weather_art.temperature %>°C</div>
</div>
<div class="stat">
<div class="stat-title">Condition</div>
</div>
</div>
<% end %>
</div>
</div>
<!-- 日期筛选器 -->
<div class="flex flex-col md:flex-row gap-4 items-center justify-between mb-8">
<div class="flex items-center gap-4">
<h2 class="text-2xl font-bold">Weather Art History</h2>
<div class="badge badge-primary"><%= @weather_arts.total_count %> artworks</div>
</div>
<%= form_tag city_path(@city), method: :get, class: "join" do %>
<%= date_field_tag :start_date, params[:start_date],
class: "join-item input input-bordered w-full max-w-xs",
placeholder: "Start Date" %>
<%= date_field_tag :end_date, params[:end_date],
class: "join-item input input-bordered w-full max-w-xs",
placeholder: "End Date" %>
<%= submit_tag "Filter", class: "join-item btn btn-primary" %>
<% end %>
</div>
<!-- 天气艺术网格 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<% @weather_arts.each do |art| %>
<div class="card bg-base-100 shadow-xl group hover:scale-105 transition-transform duration-200"
data-controller="modal"
data-action="click->modal#open">
<!-- 卡片主体 -->
<figure class="relative">
<%= image_tag art.image_url,
class: "w-full h-64 object-cover" %>
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/70 to-transparent text-white">
<div class="text-lg font-bold"><%= art.weather_date.strftime("%B %d, %Y") %></div>
<div class="flex items-center gap-2">
<span><%= art.temperature %>°C</span>
<span class="text-xs">|</span>
<span><%= art.description %></span>
</div>
</div>
</figure>
<!-- 模态框内容 -->
<dialog id="modal_<%= art.id %>" class="modal">
<div class="modal-box max-w-4xl">
<h3 class="font-bold text-lg mb-4">
Weather Art - <%= art.weather_date.strftime("%B %d, %Y") %>
</h3>
<%= image_tag art.image_url, class: "w-full rounded-lg mb-4" %>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div class="stat bg-base-200 rounded-box">
<div class="stat-title">Temperature</div>
<div class="stat-value text-lg"><%= art.temperature %>°C</div>
</div>
<div class="stat bg-base-200 rounded-box">
<div class="stat-title">Humidity</div>
<div class="stat-value text-lg"><%= art.humidity %>%</div>
</div>
<div class="stat bg-base-200 rounded-box">
<div class="stat-title">Wind Speed</div>
<div class="stat-value text-lg"><%= art.wind_speed %></div>
</div>
<div class="stat bg-base-200 rounded-box">
<div class="stat-title">Condition</div>
<div class="stat-value text-lg"><%= art.description %></div>
</div>
</div>
<div class="bg-base-200 p-4 rounded-box mb-4">
<h4 class="font-bold mb-2">AI Prompt</h4>
<p class="text-sm"><%= art.prompt %></p>
</div>
<div class="modal-action">
<form method="dialog">
<button class="btn">Close</button>
</form>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</div>
<% end %>
</div>
<!-- 分页 -->
<div class="flex justify-center">
<%= paginate @weather_arts %>
</div>
</div>
<!-- Stimulus Controller for Modal -->
<%# app/javascript/controllers/modal_controller.js %>
<script type="module">
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
open(event) {
const modalId = this.element.querySelector('dialog').id
const modal = document.getElementById(modalId)
modal.showModal()
}
}
</script>

View File

@ -14,6 +14,8 @@
<ul class="menu menu-horizontal px-1">
<li><%= link_to "Home", root_url %></li>
<li><%= link_to "Help", help_url %></li>
<li><%= link_to 'Calendar', "#" %></li>
<li><%= link_to 'Cities', "#" %></li>
<% if logged_in? %>
<li><%= link_to "Users", users_path %></li>
<li>
@ -56,6 +58,8 @@
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow">
<li><%= link_to "Home", root_url %></li>
<li><%= link_to "Help", help_url %></li>
<li><%= link_to 'Cities', "#" %></li>
<li><%= link_to 'Calendar', "#" %></li>
<% if logged_in? %>
<li><%= link_to "Users", users_path %></li>
<li class="dropdown">

View File

@ -1,15 +1,17 @@
<% provide(:title, "Home") %>
<div class="hero bg-base-200 min-h-screen">
<div class="hero-content text-center">
<div class="hero min-h-screen" >
<div class="hero-overlay bg-opacity-60"></div>
<div class="hero-content text-center text-neutral-content">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Welcome to the Today Ai Weather</h1>
<p class="py-6">
This is the home page for the
Today Ai Weather application.
</p>
<%= link_to "Sing up now!", signup_path, class:"btn btn-lg btn-primary" %>
<h1 class="mb-5 text-5xl font-bold">AI Weather Art</h1>
<p class="mb-5">Experience weather through AI-generated artwork for cities around the world.</p>
<div class="flex gap-4 justify-center">
<%= link_to 'Explore by Calendar', "#", class: "btn btn-primary" %>
<%= link_to 'Browse Cities', cities_path, class: "btn btn-secondary" %>
</div>
</div>
</div>
</div>
<%#= link_to image_tag("kitten.jpg", alt:"Kitten", width:"200") %>

View File

@ -23,6 +23,10 @@ Rails.application.routes.draw do
get "pages/home"
resources "cities"
get 'calendar', to: 'calendar#index'
resources :users
resources :account_activations, only: [ :edit ]
resources :password_resets, only: [ :new, :create, :edit, :update ]

View File

@ -1,9 +1,18 @@
class CreateWeatherArts < ActiveRecord::Migration[8.0]
def change
create_table :weather_arts do |t|
t.date :weather_date
t.string :weather_condition
t.text :description
t.references :city, null: false, foreign_key: true
t.date :weather_date, null: false
t.string :description
t.integer :temperature
t.integer :feeling_temp
t.integer :humidity
t.integer :wind_scale
t.integer :wind_speed
t.decimal :precipitation
t.integer :pressure
t.integer :visibility
t.integer :cloud
t.string :prompt
t.timestamps

View File

@ -0,0 +1,57 @@
# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types
create_table :active_storage_blobs, id: primary_key_type do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :key ], unique: true
end
create_table :active_storage_attachments, id: primary_key_type do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.references :blob, null: false, type: foreign_key_type
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
create_table :active_storage_variant_records, id: primary_key_type do |t|
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
t.string :variation_digest, null: false
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[ primary_key_type, foreign_key_type ]
end
end

50
db/schema.rb generated
View File

@ -10,7 +10,35 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_01_17_095400) do
ActiveRecord::Schema[8.0].define(version: 2025_01_18_071658) do
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.string "service_name", null: false
t.bigint "byte_size", null: false
t.string "checksum"
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
create_table "active_storage_variant_records", force: :cascade do |t|
t.bigint "blob_id", null: false
t.string "variation_digest", null: false
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
create_table "cities", force: :cascade do |t|
t.string "name"
t.string "country"
@ -38,11 +66,25 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_17_095400) do
end
create_table "weather_arts", force: :cascade do |t|
t.date "weather_date"
t.string "weather_condition"
t.text "description"
t.integer "city_id", null: false
t.date "weather_date", null: false
t.string "description"
t.integer "temperature"
t.integer "feeling_temp"
t.integer "humidity"
t.integer "wind_scale"
t.integer "wind_speed"
t.decimal "precipitation"
t.integer "pressure"
t.integer "visibility"
t.integer "cloud"
t.string "prompt"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["city_id"], name: "index_weather_arts_on_city_id"
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "weather_arts", "cities"
end

View File

@ -16,7 +16,7 @@ User.create!(name: "Example User",
activated: true,
activated_at: Time.zone.now)
99.times do |n|
9.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
@ -27,3 +27,33 @@ User.create!(name: "Example User",
activated: true,
activated_at: Time.zone.now)
end
City.create!(
name: "Guangzhou",
country: "China",
latitude: 23.125178,
longitude: 113.280637,
featured: true,
)
sample_art = WeatherArt.create!(
city_id: City.first.id,
weather_date: Time.zone.today,
description: "Sunny",
temperature: 21,
feeling_temp: 19,
humidity: 28,
wind_scale: 2,
wind_speed: 8,
precipitation: 0.0,
pressure: 1008,
visibility: 30,
cloud: 0,
prompt: "Artistic sunny weather in Guangzhou"
)
sample_art.image.attach(
io: File.open("db/seeds/images/sample_guangzhou_weather_art.png"),
filename: "sample_guangzhou_art.png",
content_type: "image/png"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -0,0 +1,7 @@
require "test_helper"
class CitiesControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end