Compare commits
52 Commits
51f0491110
...
fdbde69445
Author | SHA1 | Date | |
---|---|---|---|
|
fdbde69445 | ||
f918a42619 | |||
adb671e668 | |||
50321533f7 | |||
dd6cd0451d | |||
5f30e08a6e | |||
bf10e41c1e | |||
9d1ff31c53 | |||
155669866a | |||
8cacf2a9ff | |||
b0bdb72f8e | |||
cea07ccfea | |||
5c8308a991 | |||
5feaee4922 | |||
ead795266e | |||
1ca468f9af | |||
1f35664590 | |||
40631fe95b | |||
f7c5ae4ee7 | |||
742c94ced1 | |||
c37a93bcdf | |||
7ebf9cefae | |||
dd37e2835b | |||
4e1fb58abf | |||
5bc06007b2 | |||
84c224cf8d | |||
032ff0552a | |||
3203face6b | |||
8364d42759 | |||
9ce473dddb | |||
fe55437c96 | |||
ec3669249f | |||
f6270b1ad4 | |||
9dd7044a77 | |||
dd6bb9972c | |||
23fc14af59 | |||
fedb954d34 | |||
7612dd6bd9 | |||
b4af78aa77 | |||
b05cf10017 | |||
06a861c639 | |||
2cd23a6047 | |||
80a75d3fbb | |||
f477f205ab | |||
1f47ba59c9 | |||
6544f0247c | |||
a0516f731c | |||
18f751938f | |||
2759646145 | |||
665f6f29b6 | |||
bafb90f5fb | |||
f33fb4d2ba |
2
.gitignore
vendored
2
.gitignore
vendored
@ -38,3 +38,5 @@
|
||||
|
||||
/node_modules
|
||||
.idea
|
||||
|
||||
public/sitemap.xml.gz
|
||||
|
@ -75,7 +75,7 @@ COPY --from=build /rails /rails
|
||||
# Run and own only the runtime files as a non-root user for security
|
||||
RUN groupadd --system --gid 1000 rails && \
|
||||
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
|
||||
chown -R rails:rails db log storage tmp
|
||||
chown -R rails:rails db log storage tmp public
|
||||
USER 1000:1000
|
||||
|
||||
# Entrypoint prepares the database.
|
||||
|
9
Gemfile
9
Gemfile
@ -45,13 +45,18 @@ gem "devise", "~> 4.9"
|
||||
gem "activeadmin", "~> 3.2"
|
||||
gem "friendly_id", "~> 5.5"
|
||||
|
||||
gem 'kaminari', '~> 1.2'
|
||||
gem "kaminari", "~> 1.2"
|
||||
|
||||
gem "meta-tags", "~> 2.22"
|
||||
gem "sitemap_generator", "~> 6.3"
|
||||
|
||||
gem "ahoy_matey", "~> 5.2"
|
||||
|
||||
# gem "whenever", "~> 1.0"
|
||||
gem "ruby-openai", "~> 7.3"
|
||||
gem "httparty", "~> 0.22.0"
|
||||
gem "down", "~> 5.4"
|
||||
gem "aws-sdk-s3", "~> 1.177"
|
||||
gem "aws-sdk-s3", "~> 1.178"
|
||||
gem "sidekiq", "~> 7.3"
|
||||
gem "sidekiq-scheduler", "~> 5.0"
|
||||
|
||||
|
27
Gemfile.lock
27
Gemfile.lock
@ -84,22 +84,26 @@ GEM
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ahoy_matey (5.2.1)
|
||||
activesupport (>= 6.1)
|
||||
device_detector (>= 1)
|
||||
safely_block (>= 0.4)
|
||||
arbre (1.7.0)
|
||||
activesupport (>= 3.0.0)
|
||||
ruby2_keywords (>= 0.0.2)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1035.0)
|
||||
aws-sdk-core (3.215.0)
|
||||
aws-partitions (1.1042.0)
|
||||
aws-sdk-core (3.217.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.96.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (1.97.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.177.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-s3 (1.178.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.11.0)
|
||||
@ -136,6 +140,7 @@ GEM
|
||||
debug (1.10.0)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
device_detector (1.1.3)
|
||||
devise (4.9.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
@ -233,6 +238,8 @@ GEM
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
meta-tags (2.22.1)
|
||||
actionpack (>= 6.0.0, < 8.1)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.4)
|
||||
msgpack (1.7.5)
|
||||
@ -383,6 +390,7 @@ GEM
|
||||
rubyzip (2.4.1)
|
||||
rufus-scheduler (3.9.2)
|
||||
fugit (~> 1.1, >= 1.11.1)
|
||||
safely_block (0.4.1)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.28.0)
|
||||
base64 (~> 0.2)
|
||||
@ -400,6 +408,8 @@ GEM
|
||||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 6, < 8)
|
||||
tilt (>= 1.4.0, < 3)
|
||||
sitemap_generator (6.3.0)
|
||||
builder (~> 3.0)
|
||||
solid_cable (3.0.5)
|
||||
actioncable (>= 7.2)
|
||||
activejob (>= 7.2)
|
||||
@ -481,7 +491,8 @@ PLATFORMS
|
||||
|
||||
DEPENDENCIES
|
||||
activeadmin (~> 3.2)
|
||||
aws-sdk-s3 (~> 1.177)
|
||||
ahoy_matey (~> 5.2)
|
||||
aws-sdk-s3 (~> 1.178)
|
||||
bootsnap
|
||||
brakeman
|
||||
capybara
|
||||
@ -495,6 +506,7 @@ DEPENDENCIES
|
||||
jsbundling-rails
|
||||
kamal
|
||||
kaminari (~> 1.2)
|
||||
meta-tags (~> 2.22)
|
||||
pg (~> 1.5)
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
@ -504,6 +516,7 @@ DEPENDENCIES
|
||||
selenium-webdriver
|
||||
sidekiq (~> 7.3)
|
||||
sidekiq-scheduler (~> 5.0)
|
||||
sitemap_generator (~> 6.3)
|
||||
solid_cable
|
||||
solid_cache
|
||||
solid_queue
|
||||
|
31
app/admin/ahoy_events.rb
Normal file
31
app/admin/ahoy_events.rb
Normal file
@ -0,0 +1,31 @@
|
||||
ActiveAdmin.register Ahoy::Event do
|
||||
# See permitted parameters documentation:
|
||||
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
||||
#
|
||||
# Uncomment all parameters which should be permitted for assignment
|
||||
#
|
||||
# permit_params :visit_id, :user_id, :name, :properties, :time
|
||||
#
|
||||
# or
|
||||
#
|
||||
# permit_params do
|
||||
# permitted = [:visit_id, :user_id, :name, :properties, :time]
|
||||
# permitted << :other if params[:action] == 'create' && current_user.admin?
|
||||
# permitted
|
||||
# end
|
||||
menu priority: 101, label: "事件统计"
|
||||
|
||||
actions :index
|
||||
|
||||
index do
|
||||
column :id
|
||||
column :name
|
||||
column :time
|
||||
column :properties
|
||||
column :user_id
|
||||
end
|
||||
|
||||
filter :name
|
||||
filter :time
|
||||
filter :properties
|
||||
end
|
34
app/admin/ahoy_management.rb
Normal file
34
app/admin/ahoy_management.rb
Normal file
@ -0,0 +1,34 @@
|
||||
# app/admin/ahoy_management.rb
|
||||
ActiveAdmin.register_page "Ahoy Management" do
|
||||
menu label: "访问数据管理", parent: "系统管理"
|
||||
|
||||
content title: "访问数据管理" do
|
||||
columns do
|
||||
column do
|
||||
panel "数据统计" do
|
||||
attributes_table_for :ahoy do
|
||||
row("总事件数") { Ahoy::Event.count }
|
||||
row("总访问数") { Ahoy::Visit.count }
|
||||
row("最早事件") { Ahoy::Event.minimum(:time)&.strftime("%Y-%m-%d %H:%M:%S") }
|
||||
row("最早访问") { Ahoy::Visit.minimum(:started_at)&.strftime("%Y-%m-%d %H:%M:%S") }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "操作" do
|
||||
div class: "buttons" do
|
||||
button_to "立即清理旧数据", admin_ahoy_management_cleanup_path, method: :post,
|
||||
data: { confirm: "确定要清理3个月前的数据吗?" },
|
||||
class: "button"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
page_action :cleanup, method: :post do
|
||||
CleanAhoyDataWorker.perform_async
|
||||
redirect_to admin_ahoy_management_path, notice: "清理任务已加入队列"
|
||||
end
|
||||
end
|
35
app/admin/ahoy_visits.rb
Normal file
35
app/admin/ahoy_visits.rb
Normal file
@ -0,0 +1,35 @@
|
||||
ActiveAdmin.register Ahoy::Visit do
|
||||
# See permitted parameters documentation:
|
||||
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
||||
#
|
||||
# Uncomment all parameters which should be permitted for assignment
|
||||
#
|
||||
# permit_params :visit_token, :visitor_token, :user_id, :ip, :user_agent, :referrer, :referring_domain, :landing_page, :browser, :os, :device_type, :country, :region, :city, :latitude, :longitude, :utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign, :app_version, :os_version, :platform, :started_at
|
||||
#
|
||||
# or
|
||||
#
|
||||
# permit_params do
|
||||
# permitted = [:visit_token, :visitor_token, :user_id, :ip, :user_agent, :referrer, :referring_domain, :landing_page, :browser, :os, :device_type, :country, :region, :city, :latitude, :longitude, :utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign, :app_version, :os_version, :platform, :started_at]
|
||||
# permitted << :other if params[:action] == 'create' && current_user.admin?
|
||||
# permitted
|
||||
# end
|
||||
|
||||
menu priority: 100, label: "访问统计"
|
||||
|
||||
actions :index
|
||||
|
||||
index do
|
||||
column :id
|
||||
column :visitor_token
|
||||
column :ip
|
||||
column :user_agent
|
||||
column :started_at
|
||||
column :city
|
||||
column :country
|
||||
column :region
|
||||
end
|
||||
|
||||
filter :started_at
|
||||
filter :city
|
||||
filter :country
|
||||
end
|
@ -36,18 +36,17 @@ ActiveAdmin.register City do
|
||||
|
||||
filter :name
|
||||
filter :active
|
||||
filter :country, as: :select
|
||||
|
||||
form do |f|
|
||||
f.inputs do
|
||||
f.input :active
|
||||
f.input :name
|
||||
f.input :country
|
||||
f.input :latitude
|
||||
f.input :longitude
|
||||
f.input :priority
|
||||
f.input :timezone
|
||||
f.input :region
|
||||
f.input :last_weather_fetch
|
||||
f.input :last_image_generation
|
||||
end
|
||||
f.actions
|
||||
end
|
||||
|
@ -1,4 +1,9 @@
|
||||
ActiveAdmin.register Country do
|
||||
controller do
|
||||
def find_resource
|
||||
scoped_collection.friendly.find(params[:id])
|
||||
end
|
||||
end
|
||||
# See permitted parameters documentation:
|
||||
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
||||
#
|
||||
|
@ -11,6 +11,53 @@ ActiveAdmin.register_page "Dashboard" do
|
||||
end
|
||||
end
|
||||
|
||||
columns do
|
||||
column do
|
||||
panel "访问统计" do
|
||||
para "总访问量: #{Ahoy::Visit.count}"
|
||||
para "总事件数: #{Ahoy::Event.count}"
|
||||
para "独立访客数: #{Ahoy::Visit.distinct.count(:visitor_token)}"
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "热门城市" do
|
||||
table_for City.by_popularity.limit(10) do
|
||||
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
|
||||
column("访问量") { |city| city.view_count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "热门天气艺术" do
|
||||
table_for WeatherArt.by_popularity.limit(10) do
|
||||
column("作品") { |art| link_to(art.to_s, admin_weather_art_path(art)) }
|
||||
column("访问量") { |art| art.view_count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "冷门活跃城市" do
|
||||
table_for City.least_popular_active.limit(10) do
|
||||
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
|
||||
column("访问量") { |city| city.view_count }
|
||||
# column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# 添加一个事件列表面板
|
||||
panel "最近事件" do
|
||||
table_for Ahoy::Event.order(time: :desc).limit(10) do
|
||||
column :time
|
||||
column :name
|
||||
column :properties
|
||||
end
|
||||
end
|
||||
|
||||
# Here is an example of a simple dashboard with columns and panels.
|
||||
#
|
||||
# columns do
|
||||
|
@ -1,2 +0,0 @@
|
||||
//= require active_admin/base
|
||||
import "@activeadmin/activeadmin";
|
24
app/concerns/seo_concern.rb
Normal file
24
app/concerns/seo_concern.rb
Normal file
@ -0,0 +1,24 @@
|
||||
# app/concerns/seo_concern.rb
|
||||
module SeoConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :prepare_meta_tags
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prepare_meta_tags
|
||||
set_meta_tags(
|
||||
site: "TodayAIWeather",
|
||||
description: "Discover AI-generated weather art from cities around the world. Real-time weather visualization through artificial intelligence.",
|
||||
keywords: "weather, AI art, weather visualization, city weather, artificial intelligence",
|
||||
og: {
|
||||
title: :title,
|
||||
description: :description,
|
||||
type: "website",
|
||||
url: request.original_url
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
@ -1,7 +1,15 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include SeoConcern
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
allow_browser versions: :modern
|
||||
before_action :set_locale
|
||||
after_action :track_action
|
||||
|
||||
protected
|
||||
|
||||
def track_action
|
||||
ahoy.track "Viewed Application", request.path_parameters
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
@ -3,14 +3,14 @@ class ArtsController < ApplicationController
|
||||
@regions = Region.all
|
||||
@current_region = Region.find(params[:region]) if params[:region].present?
|
||||
|
||||
@weather_arts = WeatherArt.includes(city: [:country, { country: :region }])
|
||||
@weather_arts = WeatherArt.includes(city: [ :country, { country: :region } ])
|
||||
|
||||
if @current_region
|
||||
@weather_arts = @weather_arts.joins(city: :country)
|
||||
.where(countries: { region_id: @current_region.id })
|
||||
end
|
||||
|
||||
@weather_arts = if params[:sort] == 'oldest'
|
||||
@weather_arts = if params[:sort] == "oldest"
|
||||
@weather_arts.order(created_at: :asc)
|
||||
else
|
||||
@weather_arts.order(created_at: :desc)
|
||||
|
@ -1,8 +1,7 @@
|
||||
class CitiesController < ApplicationController
|
||||
def index
|
||||
@cities = City.all.order(:name)
|
||||
@regions = Region.includes(:countries).order(:name)
|
||||
@cities = City.includes(:country, country: :region).active.order(:name)
|
||||
@cities = City.includes(:country, country: :region).order(:name)
|
||||
|
||||
if params[:region]
|
||||
@current_region = Region.friendly.find(params[:region])
|
||||
@ -13,9 +12,31 @@ class CitiesController < ApplicationController
|
||||
@current_country = Country.friendly.find(params[:country])
|
||||
@cities = @cities.by_country(@current_country.id)
|
||||
end
|
||||
|
||||
@cities = @cities.page(params[:page]).per(10)
|
||||
|
||||
set_meta_tags(
|
||||
title: @current_region ? "Cities in #{@current_region.name}" : "Explore Cities",
|
||||
description: "Discover weather art for cities #{@current_region ? "in #{@current_region.name}" : 'worldwide'}. Real-time AI-generated weather visualization.",
|
||||
keywords: "#{@current_region&.name}, cities, weather art, AI visualization"
|
||||
)
|
||||
end
|
||||
|
||||
def show
|
||||
@city = City.friendly.find(params[:id])
|
||||
ahoy.track "View City", {
|
||||
city_id: @city.id,
|
||||
name: @city.name,
|
||||
event_type: "city_view"
|
||||
}
|
||||
|
||||
set_meta_tags(
|
||||
title: @city.name,
|
||||
description: "Experience #{@city.name}'s weather through AI-generated art. Daily updates of weather conditions visualized through artificial intelligence.",
|
||||
keywords: "#{@city.name}, #{@city.country.name}, weather art, AI visualization",
|
||||
og: {
|
||||
image: @city.latest_weather_art&.image&.attached? ? url_for(@city.latest_weather_art.image) : nil
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -2,5 +2,10 @@ class HomeController < ApplicationController
|
||||
def index
|
||||
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(6)
|
||||
@featured_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(5)
|
||||
set_meta_tags(
|
||||
title: "AI-Generated Weather Art",
|
||||
description: "Experience weather through artistic AI visualization. Daily updated weather art for cities worldwide.",
|
||||
keywords: "AI weather art, weather visualization, city weather, artificial intelligence"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
33
app/controllers/sitemaps_controller.rb
Normal file
33
app/controllers/sitemaps_controller.rb
Normal file
@ -0,0 +1,33 @@
|
||||
class SitemapsController < ApplicationController
|
||||
def show
|
||||
path = params[:path]
|
||||
bucket_name =
|
||||
Rails.env.production? ?
|
||||
ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:aws, :bucket)) :
|
||||
ENV.fetch("AWS_DEV_BUCKET", Rails.application.credentials.dig(:aws_dev, :bucket))
|
||||
Rails.logger.info "Sitemap: #{path}"
|
||||
|
||||
begin
|
||||
s3_client = Aws::S3::Client.new
|
||||
response = s3_client.get_object(
|
||||
bucket: bucket_name,
|
||||
key: "sitemaps/#{path}"
|
||||
)
|
||||
|
||||
expires_in 12.hours, public: true
|
||||
content_type = response.content_type || "application/xml"
|
||||
|
||||
send_data(
|
||||
response.body.read,
|
||||
filename: path,
|
||||
type: content_type,
|
||||
disposition: "inline"
|
||||
)
|
||||
rescue Aws::S3::Errors::NoSuchKey
|
||||
render status: :not_found
|
||||
rescue Aws::S3::Errors::ServiceError => e
|
||||
Rails.logger.error "S3 Error: #{e.message}"
|
||||
render status: :internal_server_error
|
||||
end
|
||||
end
|
||||
end
|
@ -2,5 +2,25 @@ class WeatherArtsController < ApplicationController
|
||||
def show
|
||||
@city = City.friendly.find(params[:city_id])
|
||||
@weather_art = @city.weather_arts.friendly.find(params[:slug])
|
||||
|
||||
ahoy.track "View Weather Art", {
|
||||
weather_art_id: @weather_art.id,
|
||||
city_id: @weather_art.city_id,
|
||||
event_type: "weather_art_view"
|
||||
}
|
||||
ahoy.track "View City", {
|
||||
city_id: @city.id,
|
||||
name: @city.name,
|
||||
event_type: "city_view"
|
||||
}
|
||||
|
||||
set_meta_tags(
|
||||
title: "#{@city.name} Weather Art - #{@weather_art.weather_date.strftime('%B %d, %Y')}",
|
||||
description: "#{@city.name}'s weather visualized through AI art. #{@weather_art.description} at #{@weather_art.temperature}°C.",
|
||||
keywords: "#{@city.name}, weather art, #{@weather_art.description}, AI visualization",
|
||||
og: {
|
||||
image: @weather_art.image.attached? ? url_for(@weather_art.image) : nil
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -1,2 +1,24 @@
|
||||
module ApplicationHelper
|
||||
def weather_art_schema(weather_art)
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ImageObject",
|
||||
"name": "#{weather_art.city.name} Weather Art",
|
||||
"description": weather_art.description,
|
||||
"datePublished": weather_art.created_at.iso8601,
|
||||
"contentUrl": url_for(weather_art.image),
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "TodayAIWeather"
|
||||
},
|
||||
"locationCreated": {
|
||||
"@type": "Place",
|
||||
"name": weather_art.city.name,
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressCountry": weather_art.city.country.name
|
||||
}
|
||||
}
|
||||
}.to_json.html_safe if weather_art.image.attached?
|
||||
end
|
||||
end
|
||||
|
2
app/helpers/sitemaps_helper.rb
Normal file
2
app/helpers/sitemaps_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module SitemapsHelper
|
||||
end
|
@ -1,2 +1,51 @@
|
||||
module WeatherArtsHelper
|
||||
def weather_description_icon(description)
|
||||
case description&.downcase
|
||||
when /rain/
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>'.html_safe
|
||||
when /cloud/
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||
</svg>'.html_safe
|
||||
when /sun|clear/
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>'.html_safe
|
||||
else
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>'.html_safe
|
||||
end
|
||||
end
|
||||
def weather_stat_icon(type)
|
||||
case type
|
||||
when "temperature"
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>'.html_safe
|
||||
when "wind"
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>'.html_safe
|
||||
when "humidity"
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
||||
</svg>'.html_safe
|
||||
when "visibility"
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>'.html_safe
|
||||
when "pressure"
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>'.html_safe
|
||||
when "cloud"
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||
</svg>'.html_safe
|
||||
end
|
||||
end
|
||||
end
|
||||
|
6
app/javascript/active_admin.js
Normal file
6
app/javascript/active_admin.js
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
import './add_jquery'
|
||||
import "jquery/dist/jquery"
|
||||
import "jquery-ui/dist/jquery-ui"
|
||||
import "jquery-ujs"
|
||||
import "@activeadmin/activeadmin"
|
4
app/javascript/add_jquery.js
Normal file
4
app/javascript/add_jquery.js
Normal file
@ -0,0 +1,4 @@
|
||||
import jquery from 'jquery'
|
||||
import $ from 'jquery'
|
||||
window.jQuery = jquery
|
||||
window.$ = $
|
@ -6,3 +6,4 @@ import "@fontsource/raleway/400.css";
|
||||
import "@fontsource/raleway/600.css";
|
||||
|
||||
import "./controllers"
|
||||
import "./active_admin"
|
||||
|
14
app/models/ahoy/event.rb
Normal file
14
app/models/ahoy/event.rb
Normal file
@ -0,0 +1,14 @@
|
||||
class Ahoy::Event < ApplicationRecord
|
||||
# include Ahoy::QueryMethods
|
||||
|
||||
self.table_name = "ahoy_events"
|
||||
|
||||
belongs_to :visit
|
||||
belongs_to :user, optional: true
|
||||
|
||||
serialize :properties, coder: JSON
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
[ "id", "id_value", "name", "properties", "time", "user_id", "visit_id" ]
|
||||
end
|
||||
end
|
10
app/models/ahoy/visit.rb
Normal file
10
app/models/ahoy/visit.rb
Normal file
@ -0,0 +1,10 @@
|
||||
class Ahoy::Visit < ApplicationRecord
|
||||
self.table_name = "ahoy_visits"
|
||||
|
||||
has_many :events, class_name: "Ahoy::Event"
|
||||
belongs_to :user, optional: true
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
[ "app_version", "browser", "city", "country", "device_type", "id", "ip", "landing_page", "latitude", "longitude", "os", "os_version", "platform", "referrer", "referring_domain", "region", "started_at", "user_agent", "user_id", "utm_campaign", "utm_content", "utm_medium", "utm_source", "utm_term", "visit_token", "visitor_token" ]
|
||||
end
|
||||
end
|
@ -5,6 +5,9 @@ class City < ApplicationRecord
|
||||
|
||||
has_many :weather_arts, dependent: :destroy
|
||||
|
||||
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :city_id
|
||||
has_many :events, class_name: "Ahoy::Event", foreign_key: :city_id
|
||||
|
||||
delegate :region, to: :country
|
||||
|
||||
validates :name, presence: true
|
||||
@ -17,6 +20,41 @@ class City < ApplicationRecord
|
||||
scope :by_country, ->(country_id) { where(country_id: country_id) }
|
||||
scope :active, -> { where(active: true) }
|
||||
|
||||
|
||||
scope :by_popularity, -> {
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
|
||||
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
|
||||
.group("cities.id")
|
||||
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count DESC")
|
||||
else
|
||||
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties->>'city_id')::integer = cities.id
|
||||
AND ahoy_events.properties->>'event_type' = 'city_view'")
|
||||
.group("cities.id")
|
||||
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count DESC")
|
||||
end
|
||||
}
|
||||
|
||||
scope :least_popular_active, -> {
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
active
|
||||
.joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
|
||||
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
|
||||
.group("cities.id")
|
||||
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count ASC, cities.name ASC")
|
||||
else
|
||||
active
|
||||
.joins("LEFT JOIN ahoy_events ON (ahoy_events.properties->>'city_id')::integer = cities.id
|
||||
AND ahoy_events.properties->>'event_type' = 'city_view'")
|
||||
.group("cities.id")
|
||||
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count ASC, cities.name ASC")
|
||||
end
|
||||
}
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
@ -45,10 +83,32 @@ class City < ApplicationRecord
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
[ "active", "country", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at" ]
|
||||
[ "active", "country_id", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at" ]
|
||||
end
|
||||
|
||||
def last_weather_fetch
|
||||
# latest_weather_art&.created_at
|
||||
Rails.cache.fetch("city/#{id}/last_weather_fetch", expires_in: 1.hour) do
|
||||
latest_weather_art&.created_at
|
||||
end
|
||||
end
|
||||
|
||||
def last_image_generation
|
||||
# latest_weather_art&.image&.created_at
|
||||
Rails.cache.fetch("city/#{id}/last_image_generation", expires_in: 1.hour) do
|
||||
latest_weather_art&.image&.created_at
|
||||
end
|
||||
end
|
||||
|
||||
def latest_weather_art
|
||||
weather_arts.order(weather_date: :desc).first
|
||||
end
|
||||
|
||||
def view_count
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
Ahoy::Event.where("json_extract(properties, '$.event_type') = 'city_view' AND json_extract(properties, '$.city_id') = ?", self.id).count
|
||||
else
|
||||
Ahoy::Event.where("properties->>'event_type' = 'city_view' AND (properties->>'city_id')::integer = ?", self.id).count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -5,9 +5,28 @@ class WeatherArt < ApplicationRecord
|
||||
belongs_to :city
|
||||
has_one_attached :image
|
||||
|
||||
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id
|
||||
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
|
||||
|
||||
validates :weather_date, presence: true
|
||||
validates :city_id, presence: true
|
||||
|
||||
scope :by_popularity, -> {
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.weather_art_id') = weather_arts.id
|
||||
AND json_extract(ahoy_events.properties, '$.event_type') = 'weather_art_view'")
|
||||
.group("weather_arts.id")
|
||||
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count DESC")
|
||||
else
|
||||
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties->>'weather_art_id')::integer = weather_arts.id
|
||||
AND ahoy_events.properties->>'event_type' = 'weather_art_view'")
|
||||
.group("weather_arts.id")
|
||||
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count DESC")
|
||||
end
|
||||
}
|
||||
|
||||
def should_generate_new_friendly_id?
|
||||
weather_date_changed? || city_id_changed? || super
|
||||
end
|
||||
@ -23,4 +42,12 @@ class WeatherArt < ApplicationRecord
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
[ "city_id", "cloud", "created_at", "description", "feeling_temp", "humidity", "id", "id_value", "precipitation", "pressure", "prompt", "temperature", "updated_at", "visibility", "weather_date", "wind_scale", "wind_speed" ]
|
||||
end
|
||||
|
||||
def view_count
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
Ahoy::Event.where("json_extract(properties, '$.event_type') = 'weather_art_view' AND json_extract(properties, '$.weather_art_id') = ?", self.id).count
|
||||
else
|
||||
Ahoy::Event.where("properties->>'event_type' = 'weather_art_view' AND (properties->>'weather_art_id')::integer = ?", self.id).count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -101,21 +101,17 @@
|
||||
</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 xl:grid-cols-4 gap-6">
|
||||
<% @weather_arts.each do |art| %>
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 group overflow-hidden">
|
||||
<!-- 图片部分 -->
|
||||
<figure class="relative aspect-square overflow-hidden">
|
||||
<% if art.image.attached? %>
|
||||
<%= image_tag 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 via-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
|
||||
<!-- 悬停信息 -->
|
||||
<div class="absolute inset-0 p-6 flex flex-col justify-end translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300">
|
||||
<div class="text-white space-y-2">
|
||||
<h3 class="text-xl font-display font-bold">
|
||||
@ -168,20 +164,9 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<% if @weather_arts.total_pages > 1 %>
|
||||
<div class="flex justify-center mt-12">
|
||||
<div class="btn-group">
|
||||
<%= link_to_prev_page @weather_arts, "Previous",
|
||||
class: "btn btn-outline #{'btn-disabled' unless @weather_arts.prev_page}" %>
|
||||
<% @weather_arts.total_pages.times do |i| %>
|
||||
<%= link_to i + 1, arts_path(page: i + 1, region: params[:region], sort: params[:sort]),
|
||||
class: "btn btn-outline #{'btn-active' if @weather_arts.current_page == i + 1}" %>
|
||||
<% end %>
|
||||
<%= link_to_next_page @weather_arts, "Next",
|
||||
class: "btn btn-outline #{'btn-disabled' unless @weather_arts.next_page}" %>
|
||||
<%= render 'shared/pagination',
|
||||
collection: @weather_arts,
|
||||
collection_name: 'weather arts' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
@ -12,7 +12,6 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- 标题内容 -->
|
||||
<div class="relative pt-24 pb-32">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-3xl mx-auto text-center space-y-6">
|
||||
@ -38,13 +37,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选导航 - 使用下拉菜单 -->
|
||||
<div class="sticky top-16 z-20 bg-base-100/95 backdrop-blur-sm border-b border-base-200">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="py-3 flex items-center justify-between gap-4">
|
||||
<!-- 左侧筛选器 -->
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- 区域选择下拉框 -->
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-ghost gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@ -73,7 +69,6 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 国家选择下拉框 (如果选择了区域) -->
|
||||
<% if @current_region %>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-ghost gap-2">
|
||||
@ -104,7 +99,6 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- 右侧结果统计 -->
|
||||
<div class="text-sm text-base-content/70">
|
||||
<%= @cities.count %> <%= 'city'.pluralize(@cities.count) %>
|
||||
<% if @current_country %>
|
||||
@ -117,10 +111,16 @@
|
||||
</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>
|
||||
|
||||
<%= render 'shared/pagination',
|
||||
collection: @cities,
|
||||
collection_name: 'cities' %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
@ -1,91 +1,149 @@
|
||||
<div class="min-h-screen">
|
||||
<!-- 城市头部信息 -->
|
||||
<section class="relative h-[40vh] overflow-hidden">
|
||||
<div class="relative min-h-screen bg-base-200">
|
||||
<!-- 背景效果 -->
|
||||
<% if @city.latest_weather_art&.image&.attached? %>
|
||||
<div class="fixed inset-0 -z-10">
|
||||
<%= image_tag @city.latest_weather_art.image,
|
||||
class: "w-full h-full object-cover" %>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-base-100 via-base-100/50 to-transparent"></div>
|
||||
class: "absolute w-full h-full object-cover scale-110 filter blur-2xl opacity-25" %>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-base-200/90 to-base-200/70 backdrop-blur-md"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-4xl">
|
||||
<div class="flex items-center space-x-4 mb-4">
|
||||
<!-- 主要内容 -->
|
||||
<div class="relative z-10">
|
||||
<!-- 返回导航 -->
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
<%= link_to cities_path,
|
||||
class: "btn btn-ghost btn-circle" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
class: "btn btn-ghost btn-lg gap-2 bg-base-100/50 backdrop-blur-sm hover:bg-base-100/70 transition-all duration-300" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Cities
|
||||
<% end %>
|
||||
<h1 class="text-4xl md:text-5xl font-display font-bold"><%= @city.localized_name %></h1>
|
||||
</div>
|
||||
|
||||
<div class="stats bg-base-100/80 backdrop-blur-sm shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Latitude</div>
|
||||
<div class="stat-value text-2xl"><%= @city.latitude %></div>
|
||||
<!-- 城市信息头部 -->
|
||||
<div class="container mx-auto px-4 mb-12">
|
||||
<div class="max-w-4xl mx-auto text-center space-y-6">
|
||||
<h1 class="text-4xl md:text-6xl font-display font-bold">
|
||||
<span class="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
<%= @city.localized_name %>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-wrap justify-center items-center gap-3">
|
||||
<div class="badge badge-lg badge-primary gap-2">
|
||||
<%= @city.country.name %>, <%= @city.region %>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Longitude</div>
|
||||
<div class="stat-value text-2xl"><%= @city.longitude %></div>
|
||||
<div class="badge badge-lg badge-secondary gap-2">
|
||||
<%= Time.current.in_time_zone(@city.timezone).strftime("%Y-%m-%d %H:%M") %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要统计信息 -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-8">
|
||||
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<%= weather_stat_icon("temperature") %>
|
||||
<div class="stat-title font-medium">Latest Weather</div>
|
||||
</div>
|
||||
<div class="stat-value text-2xl"><%= @city.latest_weather_art&.temperature %>°C</div>
|
||||
<div class="stat-desc mt-1"><%= @city.latest_weather_art&.description %></div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<%= weather_stat_icon("location") %>
|
||||
<div class="stat-title font-medium">Coordinates</div>
|
||||
</div>
|
||||
<div class="stat-value text-xl">
|
||||
<%= @city.latitude %>°N,
|
||||
<%= @city.longitude %>°E
|
||||
</div>
|
||||
<div class="stat-desc mt-1">Geographical Location</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<%= weather_stat_icon("history") %>
|
||||
<div class="stat-title font-medium">Records</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Weather Arts</div>
|
||||
<div class="stat-value text-2xl"><%= @city.weather_arts.count %></div>
|
||||
<div class="stat-desc mt-1">Total Weather Arts</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 天气艺术历史记录 -->
|
||||
<section class="container mx-auto px-4 py-16">
|
||||
<div class="space-y-8">
|
||||
<h2 class="text-3xl font-display font-bold">Weather Art History</h2>
|
||||
<div class="container mx-auto px-4 pb-16">
|
||||
<div class="max-w-7xl mx-auto space-y-8">
|
||||
<!-- 标题和更新时间 -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<h2 class="text-3xl font-display font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Weather Art History
|
||||
</h2>
|
||||
<div class="card bg-base-100/80 backdrop-blur-sm shadow-lg p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" 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>
|
||||
<span class="font-medium">Last Updated:</span>
|
||||
<span class="text-base-content/70">
|
||||
<%= time_ago_in_words(@city.last_weather_fetch) if @city.last_weather_fetch %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-6">
|
||||
<% @city.weather_arts.order(weather_date: :desc).each do |art| %>
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300">
|
||||
<figure class="relative aspect-[4/3] overflow-hidden">
|
||||
<div class="card bg-base-100/80 backdrop-blur-sm shadow-xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1">
|
||||
<figure class="relative aspect-video overflow-hidden">
|
||||
<% if art.image.attached? %>
|
||||
<%= image_tag art.image,
|
||||
class: "w-full h-full object-cover transform hover:scale-105 transition-transform duration-500" %>
|
||||
<% end %>
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/60 to-transparent">
|
||||
<div class="text-white">
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/70 to-transparent">
|
||||
<div class="flex items-center justify-between text-white">
|
||||
<div class="text-2xl font-bold"><%= art.temperature %>°C</div>
|
||||
<div class="text-sm opacity-90"><%= art.weather_date.strftime("%B %d, %Y") %></div>
|
||||
<div class="text-right">
|
||||
<div class="font-medium"><%= art.weather_date.strftime("%H:%M") %></div>
|
||||
<div class="text-sm opacity-80"><%= art.weather_date.strftime("%B %d, %Y") %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</figure>
|
||||
|
||||
<div class="card-body">
|
||||
<h3 class="card-title font-display"><%= art.description %></h3>
|
||||
<h3 class="card-title font-display">
|
||||
<%= weather_description_icon(art.description) %>
|
||||
<%= art.description %>
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 my-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||||
</svg>
|
||||
<div class="grid grid-cols-2 gap-4 my-4">
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<%= weather_stat_icon("humidity") %>
|
||||
<span>Humidity: <%= art.humidity %>%</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<%= weather_stat_icon("wind") %>
|
||||
<span>Wind: <%= art.wind_scale %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end">
|
||||
<%= link_to "View Details", city_weather_art_path(@city, art),
|
||||
class: "btn btn-primary btn-outline" %>
|
||||
</div>
|
||||
<%= link_to city_weather_art_path(@city, art),
|
||||
class: "btn btn-primary btn-block" do %>
|
||||
View Details
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -58,7 +58,7 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="text-center mt-12">
|
||||
<div class="text-center mt-12 mb-12">
|
||||
<%= link_to arts_path, class: "btn btn-primary btn-lg gap-2" do %>
|
||||
View All Weather Arts
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
@ -1,10 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title><%= content_for(:title) || "Today Ai Weather" %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<%= display_meta_tags(
|
||||
site: 'TodayAIWeather',
|
||||
reverse: true,
|
||||
og: {
|
||||
site_name: 'TodayAIWeather',
|
||||
type: 'website',
|
||||
url: request.original_url
|
||||
},
|
||||
alternate: {
|
||||
"zh-CN" => url_for(locale: 'zh-CN'),
|
||||
"en" => url_for(locale: 'en')
|
||||
}
|
||||
) %>
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
@ -20,6 +33,23 @@
|
||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
<script defer data-domain="todayaiweather.com" src="https://plausible.frytea.com/js/script.js"></script>
|
||||
|
||||
<script defer src="https://busuanzi.frytea.com/js"></script>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-PX1C92V5L7"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-PX1C92V5L7');
|
||||
</script>
|
||||
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7296634171837358"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen bg-base-100 font-sans">
|
||||
@ -28,7 +58,7 @@
|
||||
<div class="container mx-auto">
|
||||
<div class="flex-1">
|
||||
<%= link_to root_path, class: "text-2xl font-display font-bold hover:text-primary transition-colors" do %>
|
||||
AI Weather Art
|
||||
Today AI Weather
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
@ -44,10 +74,25 @@
|
||||
</main>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer footer-center p-8 bg-base-200 text-base-content mt-16">
|
||||
<div>
|
||||
<p class="font-display">Copyright © 2024 - All rights reserved by AI Weather Art</p>
|
||||
<footer class="footer footer-center p-8 bg-base-200 text-base-content">
|
||||
<div class="container mx-auto flex flex-col gap-2">
|
||||
<div id="busuanzi_container" class="text-xs opacity-70">
|
||||
<div class="space-x-2">
|
||||
<span>Page Views: <span id="busuanzi_page_pv"></span></span>
|
||||
<span>|</span>
|
||||
<span>Page Visitors: <span id="busuanzi_page_uv"></span></span>
|
||||
<span>|</span>
|
||||
<span>Total Views: <span id="busuanzi_site_pv"></span></span>
|
||||
<span>|</span>
|
||||
<span>Total Visitors: <span id="busuanzi_site_uv"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="font-display opacity-80">
|
||||
Copyright © 2024 - All rights reserved by AI Weather Art
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
86
app/views/shared/_pagination.html.erb
Normal file
86
app/views/shared/_pagination.html.erb
Normal file
@ -0,0 +1,86 @@
|
||||
<%# app/views/shared/_pagination.html.erb %>
|
||||
<% if collection.total_pages > 1 %>
|
||||
<div class="flex flex-col items-center mt-16 gap-6">
|
||||
<!-- 页码信息 -->
|
||||
<div class="text-base-content/70 font-light">
|
||||
<span class="px-4 py-2 bg-base-200/50 rounded-full">
|
||||
Page <%= collection.current_page %> of <%= collection.total_pages %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div class="join shadow-lg">
|
||||
<!-- 首页 -->
|
||||
<%= link_to url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
||||
</svg>
|
||||
<% end %>
|
||||
|
||||
<!-- 上一页 -->
|
||||
<%= link_to url_for(page: collection.prev_page || 1, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<% end %>
|
||||
|
||||
<!-- 页码 -->
|
||||
<% page_window = 2 # 当前页面前后显示的页码数 %>
|
||||
<% start_page = [1, collection.current_page - page_window].max %>
|
||||
<% end_page = [collection.total_pages, collection.current_page + page_window].min %>
|
||||
|
||||
<% if start_page > 1 %>
|
||||
<%= link_to 1, url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn btn-ghost hover:bg-primary/5" %>
|
||||
<% if start_page > 2 %>
|
||||
<button class="join-item btn btn-ghost btn-disabled">...</button>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% (start_page..end_page).each do |page| %>
|
||||
<% if page == collection.current_page %>
|
||||
<button class="join-item btn btn-ghost bg-primary/10 font-medium">
|
||||
<%= page %>
|
||||
</button>
|
||||
<% else %>
|
||||
<%= link_to page, url_for(page: page, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn btn-ghost hover:bg-primary/5" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if end_page < collection.total_pages %>
|
||||
<% if end_page < collection.total_pages - 1 %>
|
||||
<button class="join-item btn btn-ghost btn-disabled">...</button>
|
||||
<% end %>
|
||||
<%= link_to collection.total_pages,
|
||||
url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn btn-ghost hover:bg-primary/5" %>
|
||||
<% end %>
|
||||
|
||||
<!-- 下一页 -->
|
||||
<%= link_to url_for(page: collection.next_page || collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<% end %>
|
||||
|
||||
<!-- 末页 -->
|
||||
<%= link_to url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- 结果统计 -->
|
||||
<div class="text-sm text-base-content/60 font-light">
|
||||
Showing <%= collection.offset_value + 1 %> to
|
||||
<%= collection.last_page? ? collection.total_count : collection.offset_value + collection.limit_value %>
|
||||
of <%= collection.total_count %> <%= collection_name || 'items' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
11
app/views/weather_arts/_weather_stat.html.erb
Normal file
11
app/views/weather_arts/_weather_stat.html.erb
Normal file
@ -0,0 +1,11 @@
|
||||
<!-- app/views/weather_arts/_weather_stat.html.erb -->
|
||||
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="<%= icon %>" />
|
||||
</svg>
|
||||
<div class="stat-title font-medium"><%= title %></div>
|
||||
</div>
|
||||
<div class="stat-value text-2xl"><%= value %></div>
|
||||
<div class="stat-desc mt-1"><%= desc %></div>
|
||||
</div>
|
@ -1,8 +1,25 @@
|
||||
<div class="min-h-screen">
|
||||
<% content_for :head do %>
|
||||
<script type="application/ld+json">
|
||||
<%= weather_art_schema(@weather_art) %>
|
||||
</script>
|
||||
<% end %>
|
||||
|
||||
<div class="relative min-h-screen bg-base-200">
|
||||
<!-- 背景图片 -->
|
||||
<% if @weather_art.image.attached? %>
|
||||
<div class="fixed inset-0 -z-10">
|
||||
<%= image_tag @weather_art.image,
|
||||
class: "absolute w-full h-full object-cover scale-110 filter blur-2xl opacity-25" %>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-base-200/90 to-base-200/70 backdrop-blur-md"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="relative z-10">
|
||||
<!-- 返回导航 -->
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
<%= link_to city_path(@weather_art.city),
|
||||
class: "btn btn-ghost gap-2" do %>
|
||||
class: "btn btn-ghost btn-lg gap-2 bg-base-100/50 backdrop-blur-sm hover:bg-base-100/70 transition-all duration-300" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
@ -10,72 +27,129 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="container mx-auto px-4 pb-16">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<!-- 头部信息 -->
|
||||
<div class="text-center space-y-4 mb-12">
|
||||
<h1 class="text-4xl md:text-5xl font-display font-bold">
|
||||
<%= @weather_art.city.name %>
|
||||
<div class="inline-flex items-center gap-2 text-sm font-medium px-4 py-2 rounded-full bg-base-100/50 backdrop-blur-sm">
|
||||
<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" />
|
||||
</svg>
|
||||
<%= @weather_art.city.full_name %>
|
||||
</div>
|
||||
|
||||
<h1 class="text-4xl md:text-6xl font-display font-bold">
|
||||
<span class="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Weather Art
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-xl text-base-content/70">
|
||||
|
||||
<div class="flex flex-wrap justify-center items-center gap-3">
|
||||
<div class="badge badge-lg badge-primary gap-2">
|
||||
<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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<%= @weather_art.weather_date.strftime("%B %d, %Y") %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="badge badge-lg badge-secondary gap-2">
|
||||
<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>
|
||||
<%= @weather_art.weather_date.strftime("%H:%M") %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要卡片 -->
|
||||
<div class="card lg:card-side bg-base-100 shadow-2xl">
|
||||
<figure class="lg:w-1/2 relative aspect-square lg:aspect-auto">
|
||||
<div class="card lg:card-side bg-base-100/80 backdrop-blur-md shadow-2xl">
|
||||
<figure class="lg:w-1/2 relative aspect-square lg:aspect-auto group">
|
||||
<% if @weather_art.image.attached? %>
|
||||
<%= image_tag @weather_art.image,
|
||||
class: "w-full h-full object-cover" %>
|
||||
class: "w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" %>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-base-100/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<% end %>
|
||||
</figure>
|
||||
|
||||
<div class="card-body lg:w-1/2">
|
||||
<h2 class="card-title font-display text-2xl mb-6">
|
||||
<div class="prose max-w-none mb-8">
|
||||
<h2 class="card-title font-display text-3xl mb-4 flex items-center gap-3">
|
||||
<%= weather_description_icon(@weather_art.description) %>
|
||||
<%= @weather_art.description %>
|
||||
</h2>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
|
||||
<!-- 天气数据网格 -->
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="stat bg-base-200 rounded-box">
|
||||
<div class="stat-title">Temperature</div>
|
||||
<div class="stat-value"><%= @weather_art.temperature %>°C</div>
|
||||
<div class="stat-desc">Feels like <%= @weather_art.feeling_temp %>°C</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<%= weather_stat_icon("temperature") %>
|
||||
<div class="stat-title font-medium">Temperature</div>
|
||||
</div>
|
||||
<div class="stat-value text-2xl"><%= @weather_art.temperature %>°C</div>
|
||||
<div class="stat-desc mt-1">Feels like <%= @weather_art.feeling_temp %>°C</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-200 rounded-box">
|
||||
<div class="stat-title">Wind</div>
|
||||
<div class="stat-value"><%= @weather_art.wind_scale %></div>
|
||||
<div class="stat-desc"><%= @weather_art.wind_speed %> km/h</div>
|
||||
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<%= weather_stat_icon("wind") %>
|
||||
<div class="stat-title font-medium">Wind</div>
|
||||
</div>
|
||||
<div class="stat-value text-2xl"><%= @weather_art.wind_scale %></div>
|
||||
<div class="stat-desc mt-1"><%= @weather_art.wind_speed %> km/h</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-200 rounded-box">
|
||||
<div class="stat-title">Humidity</div>
|
||||
<div class="stat-value"><%= @weather_art.humidity %>%</div>
|
||||
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<%= weather_stat_icon("humidity") %>
|
||||
<div class="stat-title font-medium">Humidity</div>
|
||||
</div>
|
||||
<div class="stat-value text-2xl"><%= @weather_art.humidity %>%</div>
|
||||
<div class="stat-desc mt-1">Relative humidity</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-200 rounded-box">
|
||||
<div class="stat-title">Visibility</div>
|
||||
<div class="stat-value"><%= @weather_art.visibility %> km</div>
|
||||
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<%= weather_stat_icon("visibility") %>
|
||||
<div class="stat-title font-medium">Visibility</div>
|
||||
</div>
|
||||
<div class="stat-value text-2xl"><%= @weather_art.visibility %> km</div>
|
||||
<div class="stat-desc mt-1">Clear view distance</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-200 rounded-box">
|
||||
<div class="stat-title">Pressure</div>
|
||||
<div class="stat-value"><%= @weather_art.pressure %> hPa</div>
|
||||
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<%= weather_stat_icon("pressure") %>
|
||||
<div class="stat-title font-medium">Pressure</div>
|
||||
</div>
|
||||
<div class="stat-value text-2xl"><%= @weather_art.pressure %> hPa</div>
|
||||
<div class="stat-desc mt-1">Atmospheric pressure</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-200 rounded-box">
|
||||
<div class="stat-title">Cloud Cover</div>
|
||||
<div class="stat-value"><%= @weather_art.cloud %>%</div>
|
||||
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<%= weather_stat_icon("cloud") %>
|
||||
<div class="stat-title font-medium">Cloud Cover</div>
|
||||
</div>
|
||||
<div class="stat-value text-2xl"><%= @weather_art.cloud %>%</div>
|
||||
<div class="stat-desc mt-1">Sky coverage</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Prompt -->
|
||||
<div class="mt-8 bg-base-200 p-6 rounded-box">
|
||||
<h3 class="font-display font-bold text-lg mb-3">AI Prompt</h3>
|
||||
<p class="text-base-content/70"><%= @weather_art.prompt %></p>
|
||||
<div class="mt-8">
|
||||
<div class="bg-base-200/50 backdrop-blur-sm p-6 rounded-box border border-base-300">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<h3 class="font-display font-bold text-lg">AI Prompt</h3>
|
||||
</div>
|
||||
<p class="text-base-content/70 leading-relaxed">
|
||||
<%= @weather_art.prompt %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,34 +1,36 @@
|
||||
class BatchGenerateWeatherArtsWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
GENERATION_INTERVAL = 24.hours
|
||||
MAX_DURATION = 50.minutes
|
||||
SLEEP_DURATION = 120.seconds
|
||||
|
||||
def perform(*args)
|
||||
start_time = Time.current
|
||||
max_duration = 50.minutes
|
||||
|
||||
cities_to_process = get_eligible_cities
|
||||
|
||||
cities_to_process.each do |city|
|
||||
break if Time.current - start_time > max_duration
|
||||
break if Time.current - start_time > MAX_DURATION
|
||||
Rails.logger.info "Generating weather art for #{city.name}"
|
||||
|
||||
# GenerateWeatherArtJob.perform_now(city)
|
||||
GenerateWeatherArtWorker.perform_async(city.id)
|
||||
sleep 10.seconds
|
||||
sleep SLEEP_DURATION
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_eligible_cities
|
||||
cutoff_time = Time.current - GENERATION_INTERVAL
|
||||
|
||||
City.active
|
||||
.where(active: true)
|
||||
.where("last_weather_fetch IS NULL OR last_weather_fetch < ?", Date.today)
|
||||
# .select { |city| early_morning_in_timezone?(city.timezone) }
|
||||
.joins("LEFT JOIN (
|
||||
SELECT city_id, MAX(created_at) as last_generation_time
|
||||
FROM weather_arts
|
||||
GROUP BY city_id
|
||||
) latest_arts ON cities.id = latest_arts.city_id")
|
||||
.where("latest_arts.last_generation_time IS NULL OR latest_arts.last_generation_time < ?", cutoff_time)
|
||||
.order(:priority)
|
||||
end
|
||||
|
||||
# def early_morning_in_timezone?(timezone)
|
||||
# return false if timezone.blank?
|
||||
|
||||
# time = Time.current.in_time_zone(timezone)
|
||||
# time.hour == 2
|
||||
# end
|
||||
end
|
||||
|
32
app/workers/clean_ahoy_data_worker.rb
Normal file
32
app/workers/clean_ahoy_data_worker.rb
Normal file
@ -0,0 +1,32 @@
|
||||
# app/workers/clean_ahoy_data_worker.rb
|
||||
class CleanAhoyDataWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: :default, retry: false
|
||||
|
||||
def perform
|
||||
cleanup_old_events
|
||||
cleanup_old_visits
|
||||
log_cleanup_results
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cleanup_old_events
|
||||
cutoff_date = 3.months.ago
|
||||
deleted_events_count = Ahoy::Event.where("time < ?", cutoff_date).delete_all
|
||||
Rails.logger.info "Deleted #{deleted_events_count} old Ahoy events"
|
||||
end
|
||||
|
||||
def cleanup_old_visits
|
||||
cutoff_date = 3.months.ago
|
||||
deleted_visits_count = Ahoy::Visit.where("started_at < ?", cutoff_date).delete_all
|
||||
Rails.logger.info "Deleted #{deleted_visits_count} old Ahoy visits"
|
||||
end
|
||||
|
||||
def log_cleanup_results
|
||||
Rails.logger.info "Ahoy cleanup completed at #{Time.current}"
|
||||
Rails.logger.info "Remaining events: #{Ahoy::Event.count}"
|
||||
Rails.logger.info "Remaining visits: #{Ahoy::Visit.count}"
|
||||
end
|
||||
end
|
@ -1,46 +1,68 @@
|
||||
class GenerateWeatherArtWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(*args)
|
||||
city_id = args[0]
|
||||
city = City.find(city_id)
|
||||
return if city.last_weather_fetch&.today?
|
||||
def perform(city_id)
|
||||
@city = City.find(city_id)
|
||||
|
||||
weather_service = WeatherService.new
|
||||
ai_service = AiService.new
|
||||
|
||||
# 获取天气数据
|
||||
weather_data = weather_service.get_weather(city.latitude, city.longitude)
|
||||
weather_data = fetch_weather_data
|
||||
return unless weather_data
|
||||
|
||||
# 生成提示词
|
||||
prompt = ai_service.generate_prompt(city, weather_data)
|
||||
prompt = generate_prompt(weather_data)
|
||||
return unless prompt
|
||||
|
||||
# 生成图像
|
||||
image_url = ai_service.generate_image(prompt)
|
||||
image_url = generate_image(prompt)
|
||||
return unless image_url
|
||||
|
||||
# 创建天气艺术记录
|
||||
create_weather_art(weather_data, prompt, image_url)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error generating weather art for city #{city_id}: #{e.message}"
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :city
|
||||
|
||||
def fetch_weather_data
|
||||
WeatherService.new.get_weather(city.latitude, city.longitude)
|
||||
end
|
||||
|
||||
def generate_prompt(weather_data)
|
||||
AiService.new.generate_prompt(city, weather_data)
|
||||
end
|
||||
|
||||
def generate_image(prompt)
|
||||
AiService.new.generate_image(prompt)
|
||||
end
|
||||
|
||||
def create_weather_art(weather_data, prompt, image_url)
|
||||
tempfile = nil
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
weather_art = city.weather_arts.create!(
|
||||
weather_date: Date.today,
|
||||
**weather_data,
|
||||
prompt: prompt
|
||||
prompt: prompt,
|
||||
**weather_data
|
||||
)
|
||||
|
||||
# 下载并附加图像
|
||||
tempfile = Down.download(image_url)
|
||||
|
||||
weather_art.image.attach(
|
||||
io: tempfile,
|
||||
filename: "#{city.country.name}-#{city.name.parameterize}-#{Time.current.strftime('%Y%m%d-%H%M%S')}.png"
|
||||
io: File.open(tempfile.path),
|
||||
filename: generate_filename,
|
||||
content_type: "image/png"
|
||||
)
|
||||
|
||||
# 更新城市状态
|
||||
city.update!(
|
||||
last_weather_fetch: Time.current,
|
||||
last_image_generation: Time.current
|
||||
)
|
||||
rescue => e
|
||||
Rails.logger.error "Error generating weather art for #{city.name}: #{e.message}"
|
||||
weather_art
|
||||
end
|
||||
ensure
|
||||
if tempfile
|
||||
tempfile.close
|
||||
tempfile.unlink
|
||||
end
|
||||
end
|
||||
|
||||
def generate_filename
|
||||
"#{city.country.name}-#{city.name.parameterize}-#{Time.current.strftime('%Y%m%d-%H%M%S')}.png"
|
||||
end
|
||||
end
|
||||
|
58
app/workers/refresh_sitemap_worker.rb
Normal file
58
app/workers/refresh_sitemap_worker.rb
Normal file
@ -0,0 +1,58 @@
|
||||
class RefreshSitemapWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform
|
||||
host = Rails.env.production? ? "https://todayaiweather.com" : "http://127.0.0.1:3000"
|
||||
Rails.application.routes.default_url_options[:host] = host
|
||||
SitemapGenerator::Sitemap.default_host = host
|
||||
if Rails.env.production?
|
||||
SitemapGenerator::Sitemap.adapter = SitemapGenerator::AwsSdkAdapter.new(
|
||||
ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:aws, :bucket)),
|
||||
aws_access_key_id: ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :access_key_id)),
|
||||
aws_secret_access_key: ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :secret_access_key)),
|
||||
aws_region: ENV.fetch("AWS_REGION", "wnam"),
|
||||
endpoint: ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:aws, :endpoint)),
|
||||
)
|
||||
else
|
||||
SitemapGenerator::Sitemap.adapter = SitemapGenerator::AwsSdkAdapter.new(
|
||||
ENV.fetch("AWS_DEV_BUCKET", Rails.application.credentials.dig(:aws_dev, :bucket)),
|
||||
aws_access_key_id: ENV.fetch("AWS_DEV_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :access_key_id)),
|
||||
aws_secret_access_key: ENV.fetch("AWS_DEV_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :secret_access_key)),
|
||||
aws_region: ENV.fetch("AWS_DEV_REGION", "wnam"),
|
||||
endpoint: ENV.fetch("AWS_DEV_ENDPOINT", Rails.application.credentials.dig(:aws_dev, :endpoint)),
|
||||
)
|
||||
end
|
||||
SitemapGenerator::Sitemap.sitemaps_path = "sitemaps/"
|
||||
|
||||
SitemapGenerator::Sitemap.create do
|
||||
add root_path, changefreq: "daily", priority: 1.0
|
||||
add cities_path, changefreq: "daily", priority: 0.9
|
||||
add arts_path, changefreq: "daily", priority: 0.9
|
||||
|
||||
City.find_each do |city|
|
||||
add city_path(city),
|
||||
changefreq: "daily",
|
||||
priority: 0.8,
|
||||
lastmod: city.updated_at
|
||||
end
|
||||
|
||||
WeatherArt.includes(:city).find_each do |art|
|
||||
if art.image.attached?
|
||||
add city_weather_art_path(art.city, art),
|
||||
changefreq: "daily",
|
||||
priority: 0.7,
|
||||
lastmod: art.updated_at,
|
||||
images: [ {
|
||||
loc: url_for(art.image),
|
||||
title: "#{art.city.name} Weather Art - #{art.weather_date.strftime('%B %d, %Y')}"
|
||||
} ]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# SitemapGenerator::Sitemap.ping_search_engines if Rails.env.production?
|
||||
Rails.logger.info "Sitemap has been generated and uploaded to S3 successfully"
|
||||
rescue => e
|
||||
Rails.logger.error "Error refreshing sitemap: #{e.message}"
|
||||
end
|
||||
end
|
@ -3,6 +3,7 @@ services:
|
||||
image: songtianlun/today_ai_weather:latest
|
||||
#ports:
|
||||
# - "3000:3000"
|
||||
pull_policy: always
|
||||
environment:
|
||||
- RAILS_ENV=production
|
||||
- DATABASE_URL=postgresql://postgres:${PG_PASSWORD}@db:5432/db
|
||||
|
14
config/initializers/ahoy.rb
Normal file
14
config/initializers/ahoy.rb
Normal file
@ -0,0 +1,14 @@
|
||||
class Ahoy::Store < Ahoy::DatabaseStore
|
||||
end
|
||||
|
||||
# set to true for JavaScript tracking
|
||||
Ahoy.api = true
|
||||
|
||||
# set to true for geocoding (and add the geocoder gem to your Gemfile)
|
||||
# we recommend configuring local geocoding as well
|
||||
# see https://github.com/ankane/ahoy#geocoding
|
||||
Ahoy.geocode = false
|
||||
|
||||
Ahoy.visit_duration = 30.minutes
|
||||
Ahoy.server_side_visits = :when_needed
|
||||
RETENTION_PERIOD = 3.months
|
19
config/initializers/aws.rb
Normal file
19
config/initializers/aws.rb
Normal file
@ -0,0 +1,19 @@
|
||||
if Rails.env.production?
|
||||
Aws.config.update({
|
||||
region: ENV.fetch("AWS_REGION", "wnam"),
|
||||
credentials: Aws::Credentials.new(
|
||||
ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :access_key_id)),
|
||||
ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :secret_access_key))
|
||||
),
|
||||
endpoint: ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:aws, :endpoint))
|
||||
})
|
||||
else
|
||||
Aws.config.update({
|
||||
region: ENV.fetch("AWS_DEV_REGION", "wnam"),
|
||||
credentials: Aws::Credentials.new(
|
||||
ENV.fetch("AWS_DEV_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :access_key_id)),
|
||||
ENV.fetch("AWS_DEV_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :secret_access_key))
|
||||
),
|
||||
endpoint: ENV.fetch("AWS_DEV_ENDPOINT", Rails.application.credentials.dig(:aws_dev, :endpoint))
|
||||
})
|
||||
end
|
52
config/initializers/meta_tags.rb
Normal file
52
config/initializers/meta_tags.rb
Normal file
@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Use this setup block to configure all options available in MetaTags.
|
||||
MetaTags.configure do |config|
|
||||
config.title_limit = 70
|
||||
config.description_limit = 160
|
||||
config.keywords_limit = 255
|
||||
# How many characters should the title meta tag have at most. Default is 70.
|
||||
# Set to nil or 0 to remove limits.
|
||||
# config.title_limit = 70
|
||||
|
||||
# When true, site title will be truncated instead of title. Default is false.
|
||||
# config.truncate_site_title_first = false
|
||||
|
||||
# Add HTML attributes to the <title> HTML tag. Default is {}.
|
||||
# config.title_tag_attributes = {}
|
||||
|
||||
# Natural separator when truncating. Default is " " (space character).
|
||||
# Set to nil to disable natural separator.
|
||||
# This also allows you to use a whitespace regular expression (/\s/) or
|
||||
# a Unicode space (/\p{Space}/).
|
||||
# config.truncate_on_natural_separator = " "
|
||||
|
||||
# Maximum length of the page description. Default is 300.
|
||||
# Set to nil or 0 to remove limits.
|
||||
# config.description_limit = 300
|
||||
|
||||
# Maximum length of the keywords meta tag. Default is 255.
|
||||
# config.keywords_limit = 255
|
||||
|
||||
# Default separator for keywords meta tag (used when an Array passed with
|
||||
# the list of keywords). Default is ", ".
|
||||
# config.keywords_separator = ', '
|
||||
|
||||
# When true, keywords will be converted to lowercase, otherwise they will
|
||||
# appear on the page as is. Default is true.
|
||||
# config.keywords_lowercase = true
|
||||
|
||||
# When true, the output will not include new line characters between meta tags.
|
||||
# Default is false.
|
||||
# config.minify_output = false
|
||||
|
||||
# When false, generated meta tags will be self-closing (<meta ... />) instead
|
||||
# of open (`<meta ...>`). Default is true.
|
||||
# config.open_meta_tags = true
|
||||
|
||||
# List of additional meta tags that should use "property" attribute instead
|
||||
# of "name" attribute in <meta> tags.
|
||||
# config.property_tags.push(
|
||||
# 'x-hearthstone:deck',
|
||||
# )
|
||||
end
|
9
config/initializers/schedule_tasks.rb
Normal file
9
config/initializers/schedule_tasks.rb
Normal file
@ -0,0 +1,9 @@
|
||||
# config/initializers/schedule_tasks.rb
|
||||
Rails.application.config.after_initialize do
|
||||
begin
|
||||
RefreshSitemapWorker.perform_async
|
||||
Rails.logger.info "Startup task (RefreshSitemapWorker) scheduled successfully"
|
||||
rescue => e
|
||||
Rails.logger.error "Error scheduling startup task: #{e.message}"
|
||||
end
|
||||
end
|
@ -5,7 +5,7 @@ Sidekiq.configure_server do |config|
|
||||
config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1") }
|
||||
config.logger.level = Logger::INFO
|
||||
config.on(:startup) do
|
||||
schedule_file = "config/sidekiq.yml"
|
||||
schedule_file = "config/sidekiq_scheduler.yml"
|
||||
if File.exist?(schedule_file)
|
||||
Sidekiq::Scheduler.enabled = true
|
||||
Sidekiq::Scheduler.dynamic = true
|
||||
|
@ -6,7 +6,7 @@ Rails.application.routes.draw do
|
||||
resources :cities, only: [ :index, :show ] do
|
||||
resources :weather_arts, path: "weather", only: [ :show ], param: :slug
|
||||
end
|
||||
resources :arts, only: [:index]
|
||||
resources :arts, only: [ :index ]
|
||||
|
||||
# namespace :admin do
|
||||
# resources :cities
|
||||
@ -18,12 +18,14 @@ Rails.application.routes.draw do
|
||||
get "cities/index"
|
||||
get "cities/show"
|
||||
get "home/index"
|
||||
get "sitemaps/*path", to: "sitemaps#show", format: false
|
||||
|
||||
devise_for :admin_users, ActiveAdmin::Devise.config
|
||||
ActiveAdmin.routes(self)
|
||||
|
||||
# mount Sidekiq::Web => '/sidekiq'
|
||||
authenticate :admin_user do
|
||||
mount Sidekiq::Web => "/sidekiq"
|
||||
mount Sidekiq::Web => "/admin/tasks"
|
||||
end
|
||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
batch_generate_weather:
|
||||
cron: '0 */1 * * *'
|
||||
class: BatchGenerateWeatherArtsWorker
|
||||
description: "Generate weather arts every 2 hours"
|
||||
enabled: true
|
18
config/sidekiq_scheduler.yml
Normal file
18
config/sidekiq_scheduler.yml
Normal file
@ -0,0 +1,18 @@
|
||||
batch_generate_weather:
|
||||
cron: '0 */1 * * *'
|
||||
class: BatchGenerateWeatherArtsWorker
|
||||
description: "Generate weather arts every 2 hours"
|
||||
enabled: true
|
||||
|
||||
refresh_sitemap:
|
||||
cron: '0 5 * * *'
|
||||
class: RefreshSitemapWorker
|
||||
queue: default
|
||||
description: "Refresh sitemap daily"
|
||||
enabled: true
|
||||
|
||||
clean_ahoy_data:
|
||||
cron: '0 0 * * 0'
|
||||
class: CleanAhoyDataWorker
|
||||
queue: default
|
||||
enabled: true
|
58
config/sitemap.rb
Normal file
58
config/sitemap.rb
Normal file
@ -0,0 +1,58 @@
|
||||
# Set the host name for URL creation
|
||||
host = Rails.env.production? ? "https://todayaiweather.com" : "http://127.0.0.1:3000"
|
||||
Rails.application.routes.default_url_options[:host] = host
|
||||
SitemapGenerator::Sitemap.adapter = SitemapGenerator::AwsSdkAdapter.new(
|
||||
Rails.application.credentials.dig(:aws, :bucket),
|
||||
aws_access_key_id: Rails.application.credentials.dig(:aws, :access_key_id),
|
||||
aws_secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key),
|
||||
aws_region: Rails.application.credentials.dig(:aws, :region)
|
||||
)
|
||||
SitemapGenerator::Sitemap.sitemaps_path = "sitemaps/"
|
||||
|
||||
SitemapGenerator::Sitemap.default_host = host
|
||||
|
||||
SitemapGenerator::Sitemap.create do
|
||||
add root_path, changefreq: "daily", priority: 1.0
|
||||
add cities_path, changefreq: "daily", priority: 0.9
|
||||
add arts_path, changefreq: "daily", priority: 0.9
|
||||
|
||||
City.find_each do |city|
|
||||
add city_path(city),
|
||||
changefreq: "daily",
|
||||
priority: 0.8,
|
||||
lastmod: city.updated_at
|
||||
end
|
||||
|
||||
WeatherArt.includes(:city).find_each do |art|
|
||||
add city_weather_art_path(art.city, art),
|
||||
changefreq: "daily",
|
||||
priority: 0.7,
|
||||
lastmod: art.updated_at,
|
||||
images: [ {
|
||||
loc: url_for(art.image),
|
||||
title: "#{art.city.name} Weather Art - #{art.weather_date.strftime('%B %d, %Y')}"
|
||||
} ] if art.image.attached?
|
||||
end
|
||||
# Put links creation logic here.
|
||||
#
|
||||
# The root path '/' and sitemap index file are added automatically for you.
|
||||
# Links are added to the Sitemap in the order they are specified.
|
||||
#
|
||||
# Usage: add(path, options={})
|
||||
# (default options are used if you don't specify)
|
||||
#
|
||||
# Defaults: :priority => 0.5, :changefreq => 'weekly',
|
||||
# :lastmod => Time.now, :host => default_host
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# Add '/articles'
|
||||
#
|
||||
# add articles_path, :priority => 0.7, :changefreq => 'daily'
|
||||
#
|
||||
# Add all articles:
|
||||
#
|
||||
# Article.find_each do |article|
|
||||
# add article_path(article), :lastmod => article.updated_at
|
||||
# end
|
||||
end
|
@ -19,11 +19,11 @@ build:
|
||||
# bucket: your_own_bucket-<%= Rails.env %>
|
||||
amazon_dev:
|
||||
service: S3
|
||||
access_key_id: <%= ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :access_key_id)) %>
|
||||
secret_access_key: <%= ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :secret_access_key)) %>
|
||||
region: <%= ENV.fetch("AWS_REGION", "wnam") %>
|
||||
bucket: <%= ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:aws_dev, :bucket)) %>
|
||||
endpoint: <%= ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:aws_dev, :endpoint)) %>
|
||||
access_key_id: <%= ENV.fetch("AWS_DEV_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :access_key_id)) %>
|
||||
secret_access_key: <%= ENV.fetch("AWS_DEV_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :secret_access_key)) %>
|
||||
region: <%= ENV.fetch("AWS_DEV_REGION", "wnam") %>
|
||||
bucket: <%= ENV.fetch("AWS_DEV_BUCKET", Rails.application.credentials.dig(:aws_dev, :bucket)) %>
|
||||
endpoint: <%= ENV.fetch("AWS_DEV_ENDPOINT", Rails.application.credentials.dig(:aws_dev, :endpoint)) %>
|
||||
|
||||
amazon:
|
||||
service: S3
|
||||
|
@ -0,0 +1,8 @@
|
||||
class RemoveLastFetchFieldsFromCities < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
remove_column :cities, :last_weather_fetch
|
||||
remove_column :cities, :last_image_generation
|
||||
|
||||
add_index :weather_arts, [ :city_id, :weather_date ]
|
||||
end
|
||||
end
|
61
db/migrate/20250126155239_create_ahoy_visits_and_events.rb
Normal file
61
db/migrate/20250126155239_create_ahoy_visits_and_events.rb
Normal file
@ -0,0 +1,61 @@
|
||||
class CreateAhoyVisitsAndEvents < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :ahoy_visits do |t|
|
||||
t.string :visit_token
|
||||
t.string :visitor_token
|
||||
|
||||
# the rest are recommended but optional
|
||||
# simply remove any you don't want
|
||||
|
||||
# user
|
||||
t.references :user
|
||||
|
||||
# standard
|
||||
t.string :ip
|
||||
t.text :user_agent
|
||||
t.text :referrer
|
||||
t.string :referring_domain
|
||||
t.text :landing_page
|
||||
|
||||
# technology
|
||||
t.string :browser
|
||||
t.string :os
|
||||
t.string :device_type
|
||||
|
||||
# location
|
||||
t.string :country
|
||||
t.string :region
|
||||
t.string :city
|
||||
t.float :latitude
|
||||
t.float :longitude
|
||||
|
||||
# utm parameters
|
||||
t.string :utm_source
|
||||
t.string :utm_medium
|
||||
t.string :utm_term
|
||||
t.string :utm_content
|
||||
t.string :utm_campaign
|
||||
|
||||
# native apps
|
||||
t.string :app_version
|
||||
t.string :os_version
|
||||
t.string :platform
|
||||
|
||||
t.datetime :started_at
|
||||
end
|
||||
|
||||
add_index :ahoy_visits, :visit_token, unique: true
|
||||
add_index :ahoy_visits, [ :visitor_token, :started_at ]
|
||||
|
||||
create_table :ahoy_events do |t|
|
||||
t.references :visit
|
||||
t.references :user
|
||||
|
||||
t.string :name
|
||||
t.text :properties
|
||||
t.datetime :time
|
||||
end
|
||||
|
||||
add_index :ahoy_events, [ :name, :time ]
|
||||
end
|
||||
end
|
47
db/schema.rb
generated
47
db/schema.rb
generated
@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_01_22_053220) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_01_26_155239) do
|
||||
create_table "active_admin_comments", force: :cascade do |t|
|
||||
t.string "namespace"
|
||||
t.text "body"
|
||||
@ -65,6 +65,48 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_22_053220) do
|
||||
t.index ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true
|
||||
end
|
||||
|
||||
create_table "ahoy_events", force: :cascade do |t|
|
||||
t.integer "visit_id"
|
||||
t.integer "user_id"
|
||||
t.string "name"
|
||||
t.text "properties"
|
||||
t.datetime "time"
|
||||
t.index ["name", "time"], name: "index_ahoy_events_on_name_and_time"
|
||||
t.index ["user_id"], name: "index_ahoy_events_on_user_id"
|
||||
t.index ["visit_id"], name: "index_ahoy_events_on_visit_id"
|
||||
end
|
||||
|
||||
create_table "ahoy_visits", force: :cascade do |t|
|
||||
t.string "visit_token"
|
||||
t.string "visitor_token"
|
||||
t.integer "user_id"
|
||||
t.string "ip"
|
||||
t.text "user_agent"
|
||||
t.text "referrer"
|
||||
t.string "referring_domain"
|
||||
t.text "landing_page"
|
||||
t.string "browser"
|
||||
t.string "os"
|
||||
t.string "device_type"
|
||||
t.string "country"
|
||||
t.string "region"
|
||||
t.string "city"
|
||||
t.float "latitude"
|
||||
t.float "longitude"
|
||||
t.string "utm_source"
|
||||
t.string "utm_medium"
|
||||
t.string "utm_term"
|
||||
t.string "utm_content"
|
||||
t.string "utm_campaign"
|
||||
t.string "app_version"
|
||||
t.string "os_version"
|
||||
t.string "platform"
|
||||
t.datetime "started_at"
|
||||
t.index ["user_id"], name: "index_ahoy_visits_on_user_id"
|
||||
t.index ["visit_token"], name: "index_ahoy_visits_on_visit_token", unique: true
|
||||
t.index ["visitor_token", "started_at"], name: "index_ahoy_visits_on_visitor_token_and_started_at"
|
||||
end
|
||||
|
||||
create_table "cities", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.float "latitude"
|
||||
@ -72,8 +114,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_22_053220) do
|
||||
t.boolean "active"
|
||||
t.integer "priority"
|
||||
t.string "timezone"
|
||||
t.datetime "last_weather_fetch"
|
||||
t.datetime "last_image_generation"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "slug"
|
||||
@ -132,6 +172,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_22_053220) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "slug"
|
||||
t.index ["city_id", "weather_date"], name: "index_weather_arts_on_city_id_and_weather_date"
|
||||
t.index ["city_id"], name: "index_weather_arts_on_city_id"
|
||||
t.index ["slug"], name: "index_weather_arts_on_slug", unique: true
|
||||
end
|
||||
|
@ -8,9 +8,7 @@ City.create!([
|
||||
country: australia,
|
||||
timezone: 'Australia/Sydney',
|
||||
active: true,
|
||||
priority: 80,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 80
|
||||
},
|
||||
{
|
||||
name: 'Melbourne',
|
||||
@ -19,8 +17,6 @@ City.create!([
|
||||
country: australia,
|
||||
timezone: 'Australia/Melbourne',
|
||||
active: true,
|
||||
priority: 75,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 75
|
||||
}
|
||||
])
|
||||
|
@ -8,8 +8,6 @@ City.create!([
|
||||
country: bangladesh,
|
||||
timezone: 'Asia/Dhaka',
|
||||
active: true,
|
||||
priority: 85,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 85
|
||||
}
|
||||
])
|
||||
|
@ -8,8 +8,6 @@ City.create!([
|
||||
country: brazil,
|
||||
timezone: 'America/Sao_Paulo',
|
||||
active: true,
|
||||
priority: 80,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 80
|
||||
}
|
||||
])
|
||||
|
@ -6,7 +6,5 @@ City.create!(
|
||||
priority: 50,
|
||||
country: canada,
|
||||
timezone: 'America/Toronto',
|
||||
active: true,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
active: true
|
||||
)
|
||||
|
@ -8,9 +8,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Beijing',
|
||||
@ -19,9 +17,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Shenzhen',
|
||||
@ -30,9 +26,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Guangzhou',
|
||||
@ -41,9 +35,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Chengdu',
|
||||
@ -52,9 +44,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Tianjin',
|
||||
@ -63,9 +53,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Wuhan',
|
||||
@ -74,9 +62,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Dongguan',
|
||||
@ -85,9 +71,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Chongqing',
|
||||
@ -96,9 +80,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: "Xi'an",
|
||||
@ -107,9 +89,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Hangzhou',
|
||||
@ -118,9 +98,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Foshan',
|
||||
@ -129,9 +107,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Nanjing',
|
||||
@ -140,9 +116,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Hong Kong',
|
||||
@ -151,9 +125,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Hong_Kong',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Shenyang',
|
||||
@ -162,9 +134,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Zhengzhou',
|
||||
@ -173,9 +143,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Qingdao',
|
||||
@ -184,9 +152,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Suzhou',
|
||||
@ -195,9 +161,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Changsha',
|
||||
@ -206,9 +170,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Jinan',
|
||||
@ -217,9 +179,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Kunming',
|
||||
@ -228,9 +188,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Harbin',
|
||||
@ -239,9 +197,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Shijiazhuang',
|
||||
@ -250,9 +206,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Hefei',
|
||||
@ -261,9 +215,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Dalian',
|
||||
@ -272,9 +224,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Xiamen',
|
||||
@ -283,9 +233,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Nanning',
|
||||
@ -294,9 +242,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Changchun',
|
||||
@ -305,9 +251,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Taiyuan',
|
||||
@ -316,9 +260,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'New Taipei City',
|
||||
@ -327,9 +269,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Taipei',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Guiyang',
|
||||
@ -338,9 +278,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Wuxi',
|
||||
@ -349,9 +287,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Shantou',
|
||||
@ -360,9 +296,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Ürümqi',
|
||||
@ -371,9 +305,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Zhongshan',
|
||||
@ -382,9 +314,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Ningbo',
|
||||
@ -393,9 +323,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Fuzhou',
|
||||
@ -404,9 +332,7 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Nanchang',
|
||||
@ -415,8 +341,6 @@ City.create!([
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,8 +8,6 @@ City.create!([
|
||||
country: egypt,
|
||||
timezone: 'Africa/Cairo',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,8 +8,6 @@ City.create!([
|
||||
country: france,
|
||||
timezone: 'Europe/Paris',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,9 +8,7 @@ City.create!([
|
||||
country: germany,
|
||||
timezone: 'Europe/Berlin',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Berlin',
|
||||
@ -19,8 +17,6 @@ City.create!([
|
||||
country: germany,
|
||||
timezone: 'Europe/Berlin',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,9 +8,7 @@ City.create!([
|
||||
country: india,
|
||||
timezone: 'Asia/Kolkata',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Bengaluru',
|
||||
@ -19,8 +17,6 @@ City.create!([
|
||||
country: india,
|
||||
timezone: 'Asia/Kolkata',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,9 +8,7 @@ City.create!([
|
||||
country: japan,
|
||||
timezone: 'Asia/Tokyo',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Yokohama',
|
||||
@ -19,8 +17,6 @@ City.create!([
|
||||
country: japan,
|
||||
timezone: 'Asia/Tokyo',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,8 +8,6 @@ City.create!([
|
||||
country: mexico,
|
||||
timezone: 'America/Mexico_City',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,8 +8,6 @@ City.create!([
|
||||
country: nigeria,
|
||||
timezone: 'Africa/Lagos',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,8 +8,6 @@ City.create!([
|
||||
country: pakistan,
|
||||
timezone: 'Asia/Karachi',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,9 +8,7 @@ City.create!([
|
||||
country: russia,
|
||||
timezone: 'Europe/Moscow',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Sankt Petersburg',
|
||||
@ -19,8 +17,6 @@ City.create!([
|
||||
country: russia,
|
||||
timezone: 'Europe/Moscow',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,8 +8,6 @@ City.create!([
|
||||
country: saudi_arabia,
|
||||
timezone: 'Asia/Riyadh',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,8 +8,6 @@ City.create!([
|
||||
country: singapore,
|
||||
timezone: 'Asia/Singapore',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,8 +8,6 @@ City.create!([
|
||||
country: south_korea,
|
||||
timezone: 'Asia/Seoul',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,8 +8,6 @@ City.create!([
|
||||
country: thailand,
|
||||
timezone: 'Asia/Bangkok',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,9 +8,7 @@ City.create!([
|
||||
country: turkey,
|
||||
timezone: 'Europe/Istanbul',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Ankara',
|
||||
@ -19,8 +17,6 @@ City.create!([
|
||||
country: turkey,
|
||||
timezone: 'Europe/Istanbul',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,8 +8,6 @@ City.create!([
|
||||
country: uk,
|
||||
timezone: 'Europe/London',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,9 +8,7 @@ City.create!([
|
||||
country: usa,
|
||||
timezone: 'America/Los_Angeles',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Chicago',
|
||||
@ -19,9 +17,7 @@ City.create!([
|
||||
country: usa,
|
||||
timezone: 'America/Chicago',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'New York City',
|
||||
@ -30,9 +26,7 @@ City.create!([
|
||||
country: usa,
|
||||
timezone: 'America/New_York',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Los Angeles',
|
||||
@ -41,8 +35,6 @@ City.create!([
|
||||
country: usa,
|
||||
timezone: 'America/Los_Angeles',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -8,9 +8,7 @@ City.create!([
|
||||
country: vietnam,
|
||||
timezone: 'Asia/Ho_Chi_Minh',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Hanoi',
|
||||
@ -19,8 +17,6 @@ City.create!([
|
||||
country: vietnam,
|
||||
timezone: 'Asia/Ho_Chi_Minh',
|
||||
active: true,
|
||||
priority: 100,
|
||||
last_weather_fetch: 10.days.ago,
|
||||
last_image_generation: 10.days.ago
|
||||
priority: 100
|
||||
}
|
||||
])
|
||||
|
@ -16,6 +16,8 @@
|
||||
"@hotwired/stimulus": "^3.2.2",
|
||||
"@hotwired/turbo-rails": "^8.0.12",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"jquery": "^3.7.1",
|
||||
"jquery-ui": "^1.14.1",
|
||||
"postcss": "^8.5.1",
|
||||
"sass": "^1.83.4",
|
||||
"tailwindcss": "^3.4.17"
|
||||
|
1
public/ads.txt
Normal file
1
public/ads.txt
Normal file
@ -0,0 +1 @@
|
||||
google.com, pub-7296634171837358, DIRECT, f08c47fec0942fa0
|
@ -1 +1,7 @@
|
||||
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /admin/
|
||||
Disallow: /admin
|
||||
|
||||
Sitemap: https://todayaiweather.com/sitemap.xml.gz
|
||||
|
7
test/controllers/sitemaps_controller_test.rb
Normal file
7
test/controllers/sitemaps_controller_test.rb
Normal file
@ -0,0 +1,7 @@
|
||||
require "test_helper"
|
||||
|
||||
class SitemapsControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
@ -729,12 +729,19 @@ jquery-ui@^1.13.3:
|
||||
dependencies:
|
||||
jquery ">=1.12.0 <5.0.0"
|
||||
|
||||
jquery-ui@^1.14.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.14.1.tgz#ba342ea3ffff662b787595391f607d923313e040"
|
||||
integrity sha512-DhzsYH8VeIvOaxwi+B/2BCsFFT5EGjShdzOcm5DssWjtcpGWIMsn66rJciDA6jBruzNiLf1q0KvwMoX1uGNvnQ==
|
||||
dependencies:
|
||||
jquery ">=1.12.0 <5.0.0"
|
||||
|
||||
jquery-ujs@^1.2.2:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.npmjs.org/jquery-ujs/-/jquery-ujs-1.2.3.tgz"
|
||||
integrity sha512-59wvfx5vcCTHMeQT1/OwFiAj+UffLIwjRIoXdpO7Z7BCFGepzq9T9oLVeoItjTqjoXfUrHJvV7QU6pUR+UzOoA==
|
||||
|
||||
"jquery@>=1.12.0 <5.0.0", jquery@^3.4.1:
|
||||
"jquery@>=1.12.0 <5.0.0", jquery@^3.4.1, jquery@^3.7.1:
|
||||
version "3.7.1"
|
||||
resolved "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz"
|
||||
integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==
|
||||
|
Loading…
Reference in New Issue
Block a user