Compare commits
32 Commits
799dfc18ed
...
7612dd6bd9
Author | SHA1 | Date | |
---|---|---|---|
7612dd6bd9 | |||
b4af78aa77 | |||
b05cf10017 | |||
06a861c639 | |||
2cd23a6047 | |||
80a75d3fbb | |||
f477f205ab | |||
1f47ba59c9 | |||
6544f0247c | |||
a0516f731c | |||
18f751938f | |||
2759646145 | |||
665f6f29b6 | |||
bafb90f5fb | |||
f33fb4d2ba | |||
a4de04874d | |||
3a6d247451 | |||
b3089856c2 | |||
ccb48a387b | |||
e1f9118ead | |||
af95c2e55f | |||
91e62234b4 | |||
6eb8d10965 | |||
97d7930daa | |||
a15bc349a2 | |||
5fa49d97ca | |||
dffac6c665 | |||
c5101fb822 | |||
fd6292a81e | |||
c529f5fd7b | |||
c1fa16c690 | |||
2d5521c3dc |
44
.github/workflows/docker.yml
vendored
44
.github/workflows/docker.yml
vendored
@ -8,6 +8,7 @@ on:
|
|||||||
- v*
|
- v*
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
# Use docker.io for Docker Hub if empty
|
||||||
REGISTRY: docker.io
|
REGISTRY: docker.io
|
||||||
IMAGE_NAME: ${{ github.event.repository.name }}
|
IMAGE_NAME: ${{ github.event.repository.name }}
|
||||||
|
|
||||||
@ -15,51 +16,36 @@ jobs:
|
|||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
-
|
||||||
|
name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0 # 获取完整的 git history 以便生成正确的 tag
|
||||||
|
-
|
||||||
- name: Get Version
|
name: Get Version
|
||||||
id: get_version
|
id: get_version
|
||||||
run: |
|
run: |
|
||||||
echo "VERSION=$(git describe --dirty --always --tags --abbrev=7)" >> $GITHUB_OUTPUT
|
echo "VERSION=$(git describe --dirty --always --tags --abbrev=7)" >> $GITHUB_OUTPUT
|
||||||
|
-
|
||||||
- name: Login to ${{ env.REGISTRY }}
|
name: Login to ${{ env.REGISTRY }}
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{github.actor}}
|
username: ${{github.actor}}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
-
|
||||||
- name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
-
|
||||||
- name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
-
|
||||||
driver-opts: |
|
name: Build and push
|
||||||
network=host
|
|
||||||
|
|
||||||
- name: Cache Docker layers
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache
|
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-buildx-
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:latest
|
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:latest
|
||||||
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:${{ steps.get_version.outputs.VERSION }}
|
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:${{ steps.get_version.outputs.VERSION }}
|
||||||
cache-from: |
|
|
||||||
type=local,src=/tmp/.buildx-cache
|
|
||||||
type=registry,ref=${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:latest
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -38,3 +38,5 @@
|
|||||||
|
|
||||||
/node_modules
|
/node_modules
|
||||||
.idea
|
.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 and own only the runtime files as a non-root user for security
|
||||||
RUN groupadd --system --gid 1000 rails && \
|
RUN groupadd --system --gid 1000 rails && \
|
||||||
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
|
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
|
USER 1000:1000
|
||||||
|
|
||||||
# Entrypoint prepares the database.
|
# Entrypoint prepares the database.
|
||||||
|
5
Gemfile
5
Gemfile
@ -45,6 +45,11 @@ gem "devise", "~> 4.9"
|
|||||||
gem "activeadmin", "~> 3.2"
|
gem "activeadmin", "~> 3.2"
|
||||||
gem "friendly_id", "~> 5.5"
|
gem "friendly_id", "~> 5.5"
|
||||||
|
|
||||||
|
gem "kaminari", "~> 1.2"
|
||||||
|
|
||||||
|
gem "meta-tags", "~> 2.22"
|
||||||
|
gem "sitemap_generator", "~> 6.3"
|
||||||
|
|
||||||
# gem "whenever", "~> 1.0"
|
# gem "whenever", "~> 1.0"
|
||||||
gem "ruby-openai", "~> 7.3"
|
gem "ruby-openai", "~> 7.3"
|
||||||
gem "httparty", "~> 0.22.0"
|
gem "httparty", "~> 0.22.0"
|
||||||
|
@ -233,6 +233,8 @@ GEM
|
|||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.4)
|
marcel (1.0.4)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
|
meta-tags (2.22.1)
|
||||||
|
actionpack (>= 6.0.0, < 8.1)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
minitest (5.25.4)
|
minitest (5.25.4)
|
||||||
msgpack (1.7.5)
|
msgpack (1.7.5)
|
||||||
@ -400,6 +402,8 @@ GEM
|
|||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 6, < 8)
|
sidekiq (>= 6, < 8)
|
||||||
tilt (>= 1.4.0, < 3)
|
tilt (>= 1.4.0, < 3)
|
||||||
|
sitemap_generator (6.3.0)
|
||||||
|
builder (~> 3.0)
|
||||||
solid_cable (3.0.5)
|
solid_cable (3.0.5)
|
||||||
actioncable (>= 7.2)
|
actioncable (>= 7.2)
|
||||||
activejob (>= 7.2)
|
activejob (>= 7.2)
|
||||||
@ -494,6 +498,8 @@ DEPENDENCIES
|
|||||||
jbuilder
|
jbuilder
|
||||||
jsbundling-rails
|
jsbundling-rails
|
||||||
kamal
|
kamal
|
||||||
|
kaminari (~> 1.2)
|
||||||
|
meta-tags (~> 2.22)
|
||||||
pg (~> 1.5)
|
pg (~> 1.5)
|
||||||
propshaft
|
propshaft
|
||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
@ -503,6 +509,7 @@ DEPENDENCIES
|
|||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
sidekiq (~> 7.3)
|
sidekiq (~> 7.3)
|
||||||
sidekiq-scheduler (~> 5.0)
|
sidekiq-scheduler (~> 5.0)
|
||||||
|
sitemap_generator (~> 6.3)
|
||||||
solid_cable
|
solid_cable
|
||||||
solid_cache
|
solid_cache
|
||||||
solid_queue
|
solid_queue
|
||||||
|
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,4 +1,5 @@
|
|||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
|
include SeoConcern
|
||||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||||
allow_browser versions: :modern
|
allow_browser versions: :modern
|
||||||
before_action :set_locale
|
before_action :set_locale
|
||||||
|
21
app/controllers/arts_controller.rb
Normal file
21
app/controllers/arts_controller.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
class ArtsController < ApplicationController
|
||||||
|
def index
|
||||||
|
@regions = Region.all
|
||||||
|
@current_region = Region.find(params[:region]) if params[:region].present?
|
||||||
|
|
||||||
|
@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.order(created_at: :asc)
|
||||||
|
else
|
||||||
|
@weather_arts.order(created_at: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
@weather_arts = @weather_arts.page(params[:page]).per(10)
|
||||||
|
end
|
||||||
|
end
|
@ -1,6 +1,5 @@
|
|||||||
class CitiesController < ApplicationController
|
class CitiesController < ApplicationController
|
||||||
def index
|
def index
|
||||||
@cities = City.all.order(:name)
|
|
||||||
@regions = Region.includes(:countries).order(:name)
|
@regions = Region.includes(:countries).order(:name)
|
||||||
@cities = City.includes(:country, country: :region).active.order(:name)
|
@cities = City.includes(:country, country: :region).active.order(:name)
|
||||||
|
|
||||||
@ -13,9 +12,26 @@ class CitiesController < ApplicationController
|
|||||||
@current_country = Country.friendly.find(params[:country])
|
@current_country = Country.friendly.find(params[:country])
|
||||||
@cities = @cities.by_country(@current_country.id)
|
@cities = @cities.by_country(@current_country.id)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@city = City.friendly.find(params[:id])
|
@city = City.friendly.find(params[:id])
|
||||||
|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
@ -2,5 +2,10 @@ class HomeController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(6)
|
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(6)
|
||||||
@featured_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(5)
|
@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
|
||||||
end
|
end
|
||||||
|
@ -2,5 +2,13 @@ class WeatherArtsController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
@city = City.friendly.find(params[:city_id])
|
@city = City.friendly.find(params[:city_id])
|
||||||
@weather_art = @city.weather_arts.friendly.find(params[:slug])
|
@weather_art = @city.weather_arts.friendly.find(params[:slug])
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
@ -1,2 +1,24 @@
|
|||||||
module ApplicationHelper
|
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
|
end
|
||||||
|
2
app/helpers/arts_helper.rb
Normal file
2
app/helpers/arts_helper.rb
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
module ArtsHelper
|
||||||
|
end
|
@ -5,6 +5,8 @@ class City < ApplicationRecord
|
|||||||
|
|
||||||
has_many :weather_arts, dependent: :destroy
|
has_many :weather_arts, dependent: :destroy
|
||||||
|
|
||||||
|
delegate :region, to: :country
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
validates :latitude, presence: true
|
validates :latitude, presence: true
|
||||||
validates :longitude, presence: true
|
validates :longitude, presence: true
|
||||||
@ -46,6 +48,20 @@ class City < ApplicationRecord
|
|||||||
[ "active", "country", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at" ]
|
[ "active", "country", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at" ]
|
||||||
end
|
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
|
def latest_weather_art
|
||||||
weather_arts.order(weather_date: :desc).first
|
weather_arts.order(weather_date: :desc).first
|
||||||
end
|
end
|
||||||
|
172
app/views/arts/index.html.erb
Normal file
172
app/views/arts/index.html.erb
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<!-- app/views/arts/index.html.erb -->
|
||||||
|
<div class="min-h-screen">
|
||||||
|
<!-- 页面标题和背景 -->
|
||||||
|
<% featured_art = @weather_arts.first %>
|
||||||
|
<div class="relative">
|
||||||
|
<!-- 背景图像 -->
|
||||||
|
<% if featured_art&.image&.attached? %>
|
||||||
|
<div class="absolute inset-0 h-[40vh] overflow-hidden">
|
||||||
|
<%= image_tag featured_art.image,
|
||||||
|
class: "w-full h-full object-cover" %>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-b from-base-100/30 via-base-100/60 to-base-100"></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<!-- 标题内容 -->
|
||||||
|
<div class="relative pt-20 pb-32">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="max-w-3xl mx-auto text-center space-y-6">
|
||||||
|
<h1 class="text-4xl md:text-5xl font-display font-bold">
|
||||||
|
Weather Arts Gallery
|
||||||
|
</h1>
|
||||||
|
<p class="text-xl text-base-content/70">
|
||||||
|
Discover AI-generated weather art from cities around the world
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 如果有特色图片,显示其信息 -->
|
||||||
|
<% if featured_art %>
|
||||||
|
<div class="text-sm text-base-content/60 pt-4">
|
||||||
|
Latest from <%= featured_art.city.name %>, <%= featured_art.city.country.name %>
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<%= featured_art.weather_date.strftime("%B %d, %Y") %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选导航 -->
|
||||||
|
<div class="container mx-auto px-4 -mt-8">
|
||||||
|
<div class="bg-base-100 shadow-xl rounded-box p-6 mb-12">
|
||||||
|
<!-- 筛选选项 -->
|
||||||
|
<div class="flex flex-wrap gap-4 justify-center items-center">
|
||||||
|
<!-- 时间排序 -->
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
<%= params[:sort] == 'oldest' ? 'Oldest First' : 'Newest First' %>
|
||||||
|
<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="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
|
||||||
|
<li>
|
||||||
|
<%= link_to "Newest First", arts_path(sort: 'newest', region: params[:region]),
|
||||||
|
class: "#{'active' if params[:sort] != 'oldest'}" %>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<%= link_to "Oldest First", arts_path(sort: 'oldest', region: params[:region]),
|
||||||
|
class: "#{'active' if params[:sort] == 'oldest'}" %>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 区域筛选 -->
|
||||||
|
<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">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<%= @current_region&.name || 'All Regions' %>
|
||||||
|
<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="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
|
||||||
|
<li>
|
||||||
|
<%= link_to "All Regions", arts_path(sort: params[:sort]),
|
||||||
|
class: "#{'active' unless @current_region}" %>
|
||||||
|
</li>
|
||||||
|
<div class="divider my-1"></div>
|
||||||
|
<% @regions.each do |region| %>
|
||||||
|
<li>
|
||||||
|
<%= link_to region.name, arts_path(region: region.id, sort: params[:sort]),
|
||||||
|
class: "#{'active' if @current_region == region}" %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 结果统计 -->
|
||||||
|
<div class="text-center text-sm text-base-content/70 mt-4">
|
||||||
|
Showing <%= @weather_arts.total_count %> weather arts
|
||||||
|
<% if @current_region %>
|
||||||
|
from <%= @current_region.name %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 pb-16">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 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">
|
||||||
|
<%= art.city.name %>
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-white/80">
|
||||||
|
<%= art.city.country.name %>
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-2 text-white/90">
|
||||||
|
<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>
|
||||||
|
<%= art.description %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<!-- 信息部分 -->
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex justify-between items-start mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-display font-bold leading-tight">
|
||||||
|
<%= art.city.name %>
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
<%= art.weather_date.strftime("%B %d, %Y") %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="text-2xl font-bold text-primary">
|
||||||
|
<%= art.temperature %>°C
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-base-content/70">
|
||||||
|
<%= art.humidity %>% humidity
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= link_to city_weather_art_path(art.city, art),
|
||||||
|
class: "btn btn-primary btn-sm w-full" do %>
|
||||||
|
View Details
|
||||||
|
<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="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||||
|
</svg>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= render 'shared/pagination',
|
||||||
|
collection: @weather_arts,
|
||||||
|
collection_name: 'weather arts' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -12,7 +12,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<!-- 标题内容 -->
|
|
||||||
<div class="relative pt-24 pb-32">
|
<div class="relative pt-24 pb-32">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="max-w-3xl mx-auto text-center space-y-6">
|
<div class="max-w-3xl mx-auto text-center space-y-6">
|
||||||
@ -38,13 +37,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 筛选导航 - 使用下拉菜单 -->
|
|
||||||
<div class="sticky top-16 z-20 bg-base-100/95 backdrop-blur-sm border-b border-base-200">
|
<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="container mx-auto px-4">
|
||||||
<div class="py-3 flex items-center justify-between gap-4">
|
<div class="py-3 flex items-center justify-between gap-4">
|
||||||
<!-- 左侧筛选器 -->
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<!-- 区域选择下拉框 -->
|
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-ghost gap-2">
|
<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">
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 国家选择下拉框 (如果选择了区域) -->
|
|
||||||
<% if @current_region %>
|
<% if @current_region %>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-ghost gap-2">
|
<button class="btn btn-ghost gap-2">
|
||||||
@ -104,7 +99,6 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧结果统计 -->
|
|
||||||
<div class="text-sm text-base-content/70">
|
<div class="text-sm text-base-content/70">
|
||||||
<%= @cities.count %> <%= 'city'.pluralize(@cities.count) %>
|
<%= @cities.count %> <%= 'city'.pluralize(@cities.count) %>
|
||||||
<% if @current_country %>
|
<% if @current_country %>
|
||||||
@ -117,10 +111,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 城市网格 -->
|
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div class="container mx-auto px-4 pb-16">
|
||||||
<%= render partial: 'city', collection: @cities %>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
@ -57,4 +57,12 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-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">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||||
|
</svg>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
11
app/views/kaminari/_first_page.html.erb
Normal file
11
app/views/kaminari/_first_page.html.erb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<%# Link to the "First" page
|
||||||
|
- available local variables
|
||||||
|
url: url to the first page
|
||||||
|
current_page: a page object for the currently displayed page
|
||||||
|
total_pages: total number of pages
|
||||||
|
per_page: number of items to fetch per page
|
||||||
|
remote: data-remote
|
||||||
|
-%>
|
||||||
|
<span class="first">
|
||||||
|
<%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote %>
|
||||||
|
</span>
|
8
app/views/kaminari/_gap.html.erb
Normal file
8
app/views/kaminari/_gap.html.erb
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<%# Non-link tag that stands for skipped pages...
|
||||||
|
- available local variables
|
||||||
|
current_page: a page object for the currently displayed page
|
||||||
|
total_pages: total number of pages
|
||||||
|
per_page: number of items to fetch per page
|
||||||
|
remote: data-remote
|
||||||
|
-%>
|
||||||
|
<span class="page gap"><%= t('views.pagination.truncate').html_safe %></span>
|
11
app/views/kaminari/_last_page.html.erb
Normal file
11
app/views/kaminari/_last_page.html.erb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<%# Link to the "Last" page
|
||||||
|
- available local variables
|
||||||
|
url: url to the last page
|
||||||
|
current_page: a page object for the currently displayed page
|
||||||
|
total_pages: total number of pages
|
||||||
|
per_page: number of items to fetch per page
|
||||||
|
remote: data-remote
|
||||||
|
-%>
|
||||||
|
<span class="last">
|
||||||
|
<%= link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, remote: remote %>
|
||||||
|
</span>
|
11
app/views/kaminari/_next_page.html.erb
Normal file
11
app/views/kaminari/_next_page.html.erb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<%# Link to the "Next" page
|
||||||
|
- available local variables
|
||||||
|
url: url to the next page
|
||||||
|
current_page: a page object for the currently displayed page
|
||||||
|
total_pages: total number of pages
|
||||||
|
per_page: number of items to fetch per page
|
||||||
|
remote: data-remote
|
||||||
|
-%>
|
||||||
|
<span class="next">
|
||||||
|
<%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, rel: 'next', remote: remote %>
|
||||||
|
</span>
|
12
app/views/kaminari/_page.html.erb
Normal file
12
app/views/kaminari/_page.html.erb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<%# Link showing page number
|
||||||
|
- available local variables
|
||||||
|
page: a page object for "this" page
|
||||||
|
url: url to this page
|
||||||
|
current_page: a page object for the currently displayed page
|
||||||
|
total_pages: total number of pages
|
||||||
|
per_page: number of items to fetch per page
|
||||||
|
remote: data-remote
|
||||||
|
-%>
|
||||||
|
<span class="page<%= ' current' if page.current? %>">
|
||||||
|
<%= link_to_unless page.current?, page, url, {remote: remote, rel: page.rel} %>
|
||||||
|
</span>
|
25
app/views/kaminari/_paginator.html.erb
Normal file
25
app/views/kaminari/_paginator.html.erb
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<%# The container tag
|
||||||
|
- available local variables
|
||||||
|
current_page: a page object for the currently displayed page
|
||||||
|
total_pages: total number of pages
|
||||||
|
per_page: number of items to fetch per page
|
||||||
|
remote: data-remote
|
||||||
|
paginator: the paginator that renders the pagination tags inside
|
||||||
|
-%>
|
||||||
|
<%= paginator.render do -%>
|
||||||
|
<nav class="pagination" role="navigation" aria-label="pager">
|
||||||
|
<%= first_page_tag unless current_page.first? %>
|
||||||
|
<%= prev_page_tag unless current_page.first? %>
|
||||||
|
<% each_page do |page| -%>
|
||||||
|
<% if page.display_tag? -%>
|
||||||
|
<%= page_tag page %>
|
||||||
|
<% elsif !page.was_truncated? -%>
|
||||||
|
<%= gap_tag %>
|
||||||
|
<% end -%>
|
||||||
|
<% end -%>
|
||||||
|
<% unless current_page.out_of_range? %>
|
||||||
|
<%= next_page_tag unless current_page.last? %>
|
||||||
|
<%= last_page_tag unless current_page.last? %>
|
||||||
|
<% end %>
|
||||||
|
</nav>
|
||||||
|
<% end -%>
|
11
app/views/kaminari/_prev_page.html.erb
Normal file
11
app/views/kaminari/_prev_page.html.erb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<%# Link to the "Previous" page
|
||||||
|
- available local variables
|
||||||
|
url: url to the previous page
|
||||||
|
current_page: a page object for the currently displayed page
|
||||||
|
total_pages: total number of pages
|
||||||
|
per_page: number of items to fetch per page
|
||||||
|
remote: data-remote
|
||||||
|
-%>
|
||||||
|
<span class="prev">
|
||||||
|
<%= link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, rel: 'prev', remote: remote %>
|
||||||
|
</span>
|
@ -1,10 +1,23 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title><%= content_for(:title) || "Today Ai Weather" %></title>
|
<title><%= content_for(:title) || "Today Ai Weather" %></title>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="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 %>
|
<%= csrf_meta_tags %>
|
||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
@ -20,6 +33,18 @@
|
|||||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||||
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||||
|
<script defer data-domain="todayaiweather.com" src="https://plausible.frytea.com/js/script.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>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="min-h-screen bg-base-100 font-sans">
|
<body class="min-h-screen bg-base-100 font-sans">
|
||||||
@ -33,6 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-none">
|
<div class="flex-none">
|
||||||
<%= link_to "Cities", cities_path, class: "btn btn-ghost font-sans" %>
|
<%= link_to "Cities", cities_path, class: "btn btn-ghost font-sans" %>
|
||||||
|
<%= link_to "Arts", arts_path, class: "btn btn-ghost font-sans" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
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 %>
|
@ -1,3 +1,9 @@
|
|||||||
|
<% content_for :head do %>
|
||||||
|
<script type="application/ld+json">
|
||||||
|
<%= weather_art_schema(@weather_art) %>
|
||||||
|
</script>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="min-h-screen">
|
<div class="min-h-screen">
|
||||||
<!-- 返回导航 -->
|
<!-- 返回导航 -->
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
@ -1,33 +1,36 @@
|
|||||||
class BatchGenerateWeatherArtsWorker
|
class BatchGenerateWeatherArtsWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
GENERATION_INTERVAL = 6.hours
|
||||||
|
MAX_DURATION = 50.minutes
|
||||||
|
SLEEP_DURATION = 3.seconds
|
||||||
|
|
||||||
def perform(*args)
|
def perform(*args)
|
||||||
start_time = Time.current
|
start_time = Time.current
|
||||||
max_duration = 50.minutes
|
|
||||||
|
|
||||||
cities_to_process = get_eligible_cities
|
cities_to_process = get_eligible_cities
|
||||||
|
|
||||||
cities_to_process.each do |city|
|
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 1.minute # 确保不超过API限制
|
sleep SLEEP_DURATION
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def get_eligible_cities
|
def get_eligible_cities
|
||||||
|
cutoff_time = Time.current - GENERATION_INTERVAL
|
||||||
|
|
||||||
City.active
|
City.active
|
||||||
.where(active: true)
|
.joins("LEFT JOIN (
|
||||||
.where("last_weather_fetch IS NULL OR last_weather_fetch < ?", Date.today)
|
SELECT city_id, MAX(created_at) as last_generation_time
|
||||||
# .select { |city| early_morning_in_timezone?(city.timezone) }
|
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
|
end
|
||||||
|
|
||||||
# def early_morning_in_timezone?(timezone)
|
|
||||||
# return false if timezone.blank?
|
|
||||||
|
|
||||||
# time = Time.current.in_time_zone(timezone)
|
|
||||||
# time.hour == 2
|
|
||||||
# end
|
|
||||||
end
|
end
|
||||||
|
@ -1,43 +1,68 @@
|
|||||||
class GenerateWeatherArtWorker
|
class GenerateWeatherArtWorker
|
||||||
def perform(*args)
|
include Sidekiq::Worker
|
||||||
city = args[0]
|
|
||||||
return if city.last_weather_fetch&.today?
|
|
||||||
|
|
||||||
weather_service = WeatherService.new
|
def perform(city_id)
|
||||||
ai_service = AiService.new
|
@city = City.find(city_id)
|
||||||
|
|
||||||
# 获取天气数据
|
weather_data = fetch_weather_data
|
||||||
weather_data = weather_service.get_weather(city.latitude, city.longitude)
|
|
||||||
return unless weather_data
|
return unless weather_data
|
||||||
|
|
||||||
# 生成提示词
|
prompt = generate_prompt(weather_data)
|
||||||
prompt = ai_service.generate_prompt(city, weather_data)
|
|
||||||
return unless prompt
|
return unless prompt
|
||||||
|
|
||||||
# 生成图像
|
image_url = generate_image(prompt)
|
||||||
image_url = ai_service.generate_image(prompt)
|
|
||||||
return unless image_url
|
return unless image_url
|
||||||
|
|
||||||
# 创建天气艺术记录
|
create_weather_art(weather_data, prompt, image_url)
|
||||||
weather_art = city.weather_arts.create!(
|
rescue StandardError => e
|
||||||
weather_date: Date.today,
|
Rails.logger.error "Error generating weather art for city #{city_id}: #{e.message}"
|
||||||
**weather_data,
|
Rails.logger.error e.backtrace.join("\n")
|
||||||
prompt: prompt
|
end
|
||||||
)
|
|
||||||
|
|
||||||
# 下载并附加图像
|
private
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 更新城市状态
|
attr_reader :city
|
||||||
city.update!(
|
|
||||||
last_weather_fetch: Time.current,
|
def fetch_weather_data
|
||||||
last_image_generation: Time.current
|
WeatherService.new.get_weather(city.latitude, city.longitude)
|
||||||
)
|
end
|
||||||
rescue => e
|
|
||||||
Rails.logger.error "Error generating weather art for #{city.name}: #{e.message}"
|
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,
|
||||||
|
prompt: prompt,
|
||||||
|
**weather_data
|
||||||
|
)
|
||||||
|
|
||||||
|
tempfile = Down.download(image_url)
|
||||||
|
|
||||||
|
weather_art.image.attach(
|
||||||
|
io: File.open(tempfile.path),
|
||||||
|
filename: generate_filename,
|
||||||
|
content_type: "image/png"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
39
app/workers/refresh_sitemap_worker.rb
Normal file
39
app/workers/refresh_sitemap_worker.rb
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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
|
||||||
|
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 successfully"
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Error refreshing sitemap: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
35
compose.yaml
35
compose.yaml
@ -1,15 +1,16 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
image: songtianlun/today_ai_weather:latest
|
image: songtianlun/today_ai_weather:latest
|
||||||
ports:
|
#ports:
|
||||||
- "2222:3000"
|
# - "3000:3000"
|
||||||
|
pull_policy: always
|
||||||
environment:
|
environment:
|
||||||
- RAILS_ENV=production
|
- RAILS_ENV=production
|
||||||
- DATABASE_URL=postgresql://postgres:xxx@db:5432/db
|
- DATABASE_URL=postgresql://postgres:${PG_PASSWORD}@db:5432/db
|
||||||
- RAILS_MASTER_KEY=xxx
|
- RAILS_MASTER_KEY=${RAILS_MASTER_KEY}
|
||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- redis
|
- redis
|
||||||
@ -19,9 +20,11 @@ services:
|
|||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
environment:
|
environment:
|
||||||
- RAILS_ENV=production
|
- RAILS_ENV=production
|
||||||
- DATABASE_URL=postgresql://postgres:xxx@db:5432/db
|
- DATABASE_URL=postgresql://postgres:${PG_PASSWORD}@db:5432/db
|
||||||
- RAILS_MASTER_KEY=xxx
|
- RAILS_MASTER_KEY=${RAILS_MASTER_KEY}
|
||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- redis
|
- redis
|
||||||
@ -29,13 +32,21 @@ services:
|
|||||||
db:
|
db:
|
||||||
image: postgres:16
|
image: postgres:16
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/pg:/var/lib/postgresql/data
|
- ../taw_data/pg:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_PASSWORD=xxx
|
- POSTGRES_PASSWORD=${PG_PASSWORD}
|
||||||
- POSTGRES_DB=db
|
- POSTGRES_DB=db
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/redis:/data
|
- ../taw_data/redis:/data
|
||||||
command: redis-server --appendonly yes
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dokploy-network:
|
||||||
|
external: true
|
14
config/initializers/kaminari_config.rb
Normal file
14
config/initializers/kaminari_config.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Kaminari.configure do |config|
|
||||||
|
# config.default_per_page = 25
|
||||||
|
# config.max_per_page = nil
|
||||||
|
# config.window = 4
|
||||||
|
# config.outer_window = 0
|
||||||
|
# config.left = 0
|
||||||
|
# config.right = 0
|
||||||
|
# config.page_method_name = :page
|
||||||
|
# config.param_name = :page
|
||||||
|
# config.max_pages = nil
|
||||||
|
# config.params_on_first_page = false
|
||||||
|
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
|
@ -6,6 +6,7 @@ Rails.application.routes.draw do
|
|||||||
resources :cities, only: [ :index, :show ] do
|
resources :cities, only: [ :index, :show ] do
|
||||||
resources :weather_arts, path: "weather", only: [ :show ], param: :slug
|
resources :weather_arts, path: "weather", only: [ :show ], param: :slug
|
||||||
end
|
end
|
||||||
|
resources :arts, only: [ :index ]
|
||||||
|
|
||||||
# namespace :admin do
|
# namespace :admin do
|
||||||
# resources :cities
|
# resources :cities
|
||||||
@ -22,7 +23,7 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
# mount Sidekiq::Web => '/sidekiq'
|
# mount Sidekiq::Web => '/sidekiq'
|
||||||
authenticate :admin_user do
|
authenticate :admin_user do
|
||||||
mount Sidekiq::Web => "/sidekiq"
|
mount Sidekiq::Web => "/admin/tasks"
|
||||||
end
|
end
|
||||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||||
|
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
batch_generate_weather:
|
batch_generate_weather:
|
||||||
cron: '0 */2 * * *'
|
cron: '0 */1 * * *'
|
||||||
class: BatchGenerateWeatherArtsWorker
|
class: BatchGenerateWeatherArtsWorker
|
||||||
description: "Generate weather arts every 2 hours"
|
description: "Generate weather arts every 2 hours"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
refresh_sitemap:
|
||||||
|
cron: '0 5 * * *'
|
||||||
|
class: RefreshSitemapWorker
|
||||||
|
queue: default
|
||||||
|
description: "Refresh sitemap daily"
|
||||||
|
enabled: true
|
||||||
|
50
config/sitemap.rb
Normal file
50
config/sitemap.rb
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Set the host name for URL creation
|
||||||
|
host = Rails.env.production? ? "https://todayaiweather.com" : "http://127.0.0.1:3000"
|
||||||
|
|
||||||
|
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
|
@ -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
|
5
db/schema.rb
generated
5
db/schema.rb
generated
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_01_22_053220) do
|
ActiveRecord::Schema[8.0].define(version: 2025_01_23_155234) do
|
||||||
create_table "active_admin_comments", force: :cascade do |t|
|
create_table "active_admin_comments", force: :cascade do |t|
|
||||||
t.string "namespace"
|
t.string "namespace"
|
||||||
t.text "body"
|
t.text "body"
|
||||||
@ -72,8 +72,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_22_053220) do
|
|||||||
t.boolean "active"
|
t.boolean "active"
|
||||||
t.integer "priority"
|
t.integer "priority"
|
||||||
t.string "timezone"
|
t.string "timezone"
|
||||||
t.datetime "last_weather_fetch"
|
|
||||||
t.datetime "last_image_generation"
|
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "slug"
|
t.string "slug"
|
||||||
@ -132,6 +130,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_22_053220) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "slug"
|
t.string "slug"
|
||||||
|
t.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 ["city_id"], name: "index_weather_arts_on_city_id"
|
||||||
t.index ["slug"], name: "index_weather_arts_on_slug", unique: true
|
t.index ["slug"], name: "index_weather_arts_on_slug", unique: true
|
||||||
end
|
end
|
||||||
|
@ -8,9 +8,7 @@ City.create!([
|
|||||||
country: australia,
|
country: australia,
|
||||||
timezone: 'Australia/Sydney',
|
timezone: 'Australia/Sydney',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 80,
|
priority: 80
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Melbourne',
|
name: 'Melbourne',
|
||||||
@ -19,8 +17,6 @@ City.create!([
|
|||||||
country: australia,
|
country: australia,
|
||||||
timezone: 'Australia/Melbourne',
|
timezone: 'Australia/Melbourne',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 75,
|
priority: 75
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,8 +8,6 @@ City.create!([
|
|||||||
country: bangladesh,
|
country: bangladesh,
|
||||||
timezone: 'Asia/Dhaka',
|
timezone: 'Asia/Dhaka',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 85,
|
priority: 85
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,8 +8,6 @@ City.create!([
|
|||||||
country: brazil,
|
country: brazil,
|
||||||
timezone: 'America/Sao_Paulo',
|
timezone: 'America/Sao_Paulo',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 80,
|
priority: 80
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -6,7 +6,5 @@ City.create!(
|
|||||||
priority: 50,
|
priority: 50,
|
||||||
country: canada,
|
country: canada,
|
||||||
timezone: 'America/Toronto',
|
timezone: 'America/Toronto',
|
||||||
active: true,
|
active: true
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
)
|
)
|
||||||
|
@ -8,9 +8,7 @@ City.create!([
|
|||||||
country: china,
|
country: china,
|
||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Beijing',
|
name: 'Beijing',
|
||||||
@ -19,9 +17,7 @@ City.create!([
|
|||||||
country: china,
|
country: china,
|
||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Shenzhen',
|
name: 'Shenzhen',
|
||||||
@ -30,9 +26,7 @@ City.create!([
|
|||||||
country: china,
|
country: china,
|
||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Guangzhou',
|
name: 'Guangzhou',
|
||||||
@ -41,9 +35,7 @@ City.create!([
|
|||||||
country: china,
|
country: china,
|
||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Chengdu',
|
name: 'Chengdu',
|
||||||
@ -52,9 +44,7 @@ City.create!([
|
|||||||
country: china,
|
country: china,
|
||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Tianjin',
|
name: 'Tianjin',
|
||||||
@ -63,9 +53,7 @@ City.create!([
|
|||||||
country: china,
|
country: china,
|
||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Wuhan',
|
name: 'Wuhan',
|
||||||
@ -74,9 +62,7 @@ City.create!([
|
|||||||
country: china,
|
country: china,
|
||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Dongguan',
|
name: 'Dongguan',
|
||||||
@ -85,9 +71,7 @@ City.create!([
|
|||||||
country: china,
|
country: china,
|
||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Chongqing',
|
name: 'Chongqing',
|
||||||
@ -97,8 +81,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Xi'an",
|
name: "Xi'an",
|
||||||
@ -108,8 +90,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Hangzhou',
|
name: 'Hangzhou',
|
||||||
@ -119,8 +99,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Foshan',
|
name: 'Foshan',
|
||||||
@ -130,8 +108,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Nanjing',
|
name: 'Nanjing',
|
||||||
@ -141,8 +117,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Hong Kong',
|
name: 'Hong Kong',
|
||||||
@ -152,8 +126,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Hong_Kong',
|
timezone: 'Asia/Hong_Kong',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Shenyang',
|
name: 'Shenyang',
|
||||||
@ -163,8 +135,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Zhengzhou',
|
name: 'Zhengzhou',
|
||||||
@ -174,8 +144,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Qingdao',
|
name: 'Qingdao',
|
||||||
@ -185,8 +153,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Suzhou',
|
name: 'Suzhou',
|
||||||
@ -196,8 +162,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Changsha',
|
name: 'Changsha',
|
||||||
@ -207,8 +171,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Jinan',
|
name: 'Jinan',
|
||||||
@ -218,8 +180,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Kunming',
|
name: 'Kunming',
|
||||||
@ -229,8 +189,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Harbin',
|
name: 'Harbin',
|
||||||
@ -240,8 +198,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Shijiazhuang',
|
name: 'Shijiazhuang',
|
||||||
@ -251,8 +207,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Hefei',
|
name: 'Hefei',
|
||||||
@ -262,8 +216,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Dalian',
|
name: 'Dalian',
|
||||||
@ -273,8 +225,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Xiamen',
|
name: 'Xiamen',
|
||||||
@ -284,8 +234,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Nanning',
|
name: 'Nanning',
|
||||||
@ -295,8 +243,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Changchun',
|
name: 'Changchun',
|
||||||
@ -306,8 +252,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Taiyuan',
|
name: 'Taiyuan',
|
||||||
@ -317,8 +261,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'New Taipei City',
|
name: 'New Taipei City',
|
||||||
@ -328,8 +270,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Taipei',
|
timezone: 'Asia/Taipei',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Guiyang',
|
name: 'Guiyang',
|
||||||
@ -339,8 +279,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Wuxi',
|
name: 'Wuxi',
|
||||||
@ -350,8 +288,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Shantou',
|
name: 'Shantou',
|
||||||
@ -361,8 +297,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Ürümqi',
|
name: 'Ürümqi',
|
||||||
@ -372,8 +306,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Zhongshan',
|
name: 'Zhongshan',
|
||||||
@ -383,8 +315,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Ningbo',
|
name: 'Ningbo',
|
||||||
@ -394,8 +324,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Fuzhou',
|
name: 'Fuzhou',
|
||||||
@ -405,8 +333,6 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Nanchang',
|
name: 'Nanchang',
|
||||||
@ -416,7 +342,5 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,8 +8,6 @@ City.create!([
|
|||||||
country: egypt,
|
country: egypt,
|
||||||
timezone: 'Africa/Cairo',
|
timezone: 'Africa/Cairo',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,8 +8,6 @@ City.create!([
|
|||||||
country: france,
|
country: france,
|
||||||
timezone: 'Europe/Paris',
|
timezone: 'Europe/Paris',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,9 +8,7 @@ City.create!([
|
|||||||
country: germany,
|
country: germany,
|
||||||
timezone: 'Europe/Berlin',
|
timezone: 'Europe/Berlin',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Berlin',
|
name: 'Berlin',
|
||||||
@ -19,8 +17,6 @@ City.create!([
|
|||||||
country: germany,
|
country: germany,
|
||||||
timezone: 'Europe/Berlin',
|
timezone: 'Europe/Berlin',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,9 +8,7 @@ City.create!([
|
|||||||
country: india,
|
country: india,
|
||||||
timezone: 'Asia/Kolkata',
|
timezone: 'Asia/Kolkata',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Bengaluru',
|
name: 'Bengaluru',
|
||||||
@ -19,8 +17,6 @@ City.create!([
|
|||||||
country: india,
|
country: india,
|
||||||
timezone: 'Asia/Kolkata',
|
timezone: 'Asia/Kolkata',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,9 +8,7 @@ City.create!([
|
|||||||
country: japan,
|
country: japan,
|
||||||
timezone: 'Asia/Tokyo',
|
timezone: 'Asia/Tokyo',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Yokohama',
|
name: 'Yokohama',
|
||||||
@ -19,8 +17,6 @@ City.create!([
|
|||||||
country: japan,
|
country: japan,
|
||||||
timezone: 'Asia/Tokyo',
|
timezone: 'Asia/Tokyo',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,8 +8,6 @@ City.create!([
|
|||||||
country: mexico,
|
country: mexico,
|
||||||
timezone: 'America/Mexico_City',
|
timezone: 'America/Mexico_City',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,8 +8,6 @@ City.create!([
|
|||||||
country: nigeria,
|
country: nigeria,
|
||||||
timezone: 'Africa/Lagos',
|
timezone: 'Africa/Lagos',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,8 +8,6 @@ City.create!([
|
|||||||
country: pakistan,
|
country: pakistan,
|
||||||
timezone: 'Asia/Karachi',
|
timezone: 'Asia/Karachi',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,9 +8,7 @@ City.create!([
|
|||||||
country: russia,
|
country: russia,
|
||||||
timezone: 'Europe/Moscow',
|
timezone: 'Europe/Moscow',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Sankt Petersburg',
|
name: 'Sankt Petersburg',
|
||||||
@ -19,8 +17,6 @@ City.create!([
|
|||||||
country: russia,
|
country: russia,
|
||||||
timezone: 'Europe/Moscow',
|
timezone: 'Europe/Moscow',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,8 +8,6 @@ City.create!([
|
|||||||
country: saudi_arabia,
|
country: saudi_arabia,
|
||||||
timezone: 'Asia/Riyadh',
|
timezone: 'Asia/Riyadh',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,8 +8,6 @@ City.create!([
|
|||||||
country: singapore,
|
country: singapore,
|
||||||
timezone: 'Asia/Singapore',
|
timezone: 'Asia/Singapore',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,8 +8,6 @@ City.create!([
|
|||||||
country: south_korea,
|
country: south_korea,
|
||||||
timezone: 'Asia/Seoul',
|
timezone: 'Asia/Seoul',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,8 +8,6 @@ City.create!([
|
|||||||
country: thailand,
|
country: thailand,
|
||||||
timezone: 'Asia/Bangkok',
|
timezone: 'Asia/Bangkok',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,9 +8,7 @@ City.create!([
|
|||||||
country: turkey,
|
country: turkey,
|
||||||
timezone: 'Europe/Istanbul',
|
timezone: 'Europe/Istanbul',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Ankara',
|
name: 'Ankara',
|
||||||
@ -19,8 +17,6 @@ City.create!([
|
|||||||
country: turkey,
|
country: turkey,
|
||||||
timezone: 'Europe/Istanbul',
|
timezone: 'Europe/Istanbul',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,8 +8,6 @@ City.create!([
|
|||||||
country: uk,
|
country: uk,
|
||||||
timezone: 'Europe/London',
|
timezone: 'Europe/London',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,9 +8,7 @@ City.create!([
|
|||||||
country: usa,
|
country: usa,
|
||||||
timezone: 'America/Los_Angeles',
|
timezone: 'America/Los_Angeles',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Chicago',
|
name: 'Chicago',
|
||||||
@ -19,9 +17,7 @@ City.create!([
|
|||||||
country: usa,
|
country: usa,
|
||||||
timezone: 'America/Chicago',
|
timezone: 'America/Chicago',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'New York City',
|
name: 'New York City',
|
||||||
@ -30,9 +26,7 @@ City.create!([
|
|||||||
country: usa,
|
country: usa,
|
||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Los Angeles',
|
name: 'Los Angeles',
|
||||||
@ -41,8 +35,6 @@ City.create!([
|
|||||||
country: usa,
|
country: usa,
|
||||||
timezone: 'America/Los_Angeles',
|
timezone: 'America/Los_Angeles',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -8,9 +8,7 @@ City.create!([
|
|||||||
country: vietnam,
|
country: vietnam,
|
||||||
timezone: 'Asia/Ho_Chi_Minh',
|
timezone: 'Asia/Ho_Chi_Minh',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Hanoi',
|
name: 'Hanoi',
|
||||||
@ -19,8 +17,6 @@ City.create!([
|
|||||||
country: vietnam,
|
country: vietnam,
|
||||||
timezone: 'Asia/Ho_Chi_Minh',
|
timezone: 'Asia/Ho_Chi_Minh',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100
|
||||||
last_weather_fetch: 10.days.ago,
|
|
||||||
last_image_generation: 10.days.ago
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -1 +1,7 @@
|
|||||||
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
# 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/arts_controller_test.rb
Normal file
7
test/controllers/arts_controller_test.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ArtsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user