Compare commits
101 Commits
dependabot
...
main
Author | SHA1 | Date | |
---|---|---|---|
fe5c0d5113 | |||
3ae870047a | |||
2a360a6875 | |||
bd04bb63a1 | |||
5f98d9ebfd | |||
9ef2a92d60 | |||
03c957e654 | |||
b2cc7e7016 | |||
da2f4f6c86 | |||
3661d2b008 | |||
0e476b546d | |||
d331a73a85 | |||
5a82fc9a10 | |||
926ba18e85 | |||
09fa1ceea9 | |||
5b996bb64a | |||
80ceac5d94 | |||
bd42833953 | |||
f6b9dcf187 | |||
517e3038cc | |||
9fe92b1fc4 | |||
c35f09660a | |||
789e9f8d23 | |||
468a665354 | |||
9a02115562 | |||
fc721ada9f | |||
888dc7f22d | |||
f79299d707 | |||
81116a2f3e | |||
df86a10f03 | |||
4ea7f6c03c | |||
269e5ef553 | |||
104597e3ba | |||
f1815afc41 | |||
9a35dc5563 | |||
31a2ec373d | |||
f02587da57 | |||
fa1fc7c21a | |||
afc871deb1 | |||
0ef979e5c4 | |||
e5930c666b | |||
3f8b0dd231 | |||
d4deddbb8c | |||
6f2a42b92b | |||
0af41e24a8 | |||
2f84dde40f | |||
abdb40e4bf | |||
eb9bfc7972 | |||
5e716a46d9 | |||
2ffb1a4248 | |||
ca3691004f | |||
983564d534 | |||
98a335100b | |||
67dcaf7a9d | |||
d3faea06a1 | |||
73dcd4df6a | |||
a2992d998b | |||
299107b988 | |||
2042732787 | |||
936db76437 | |||
df074a81a8 | |||
968efb5492 | |||
b6635e5a51 | |||
daa0ceac3e | |||
9ac7dd46af | |||
df456d1031 | |||
95f94cb73b | |||
f43a6b4698 | |||
681ad5320f | |||
6f655ee792 | |||
b01cbe960a | |||
ec8f89e07a | |||
de4823cfc9 | |||
1c13b89854 | |||
95afdf1096 | |||
18977a9d42 | |||
496dcf83a9 | |||
c5f8140aa6 | |||
c808b72c63 | |||
bebb22079b | |||
50183539f5 | |||
4844c8b983 | |||
ec96914067 | |||
eb16f5886d | |||
ee7ff023df | |||
be88aebac2 | |||
caf22a00ca | |||
80d623b0c2 | |||
15c1fc654d | |||
5ae0367525 | |||
eda20ecca5 | |||
5efe441fa8 | |||
7ebf20aa7b | |||
afcb9c6cd8 | |||
0a6926421f | |||
940f1a8f76 | |||
a1f1f2b728 | |||
80c2f9a1df | |||
799f3222a9 | |||
51d626a67f | |||
34342a9678 |
4
.github/workflows/docker-dev.yml
vendored
4
.github/workflows/docker-dev.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Docker
|
||||
name: Docker Dev
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -45,4 +45,4 @@ jobs:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:dev
|
||||
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}_dev:dev
|
2
.github/workflows/docker-main.yml
vendored
2
.github/workflows/docker-main.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Docker
|
||||
name: Docker Main
|
||||
|
||||
on:
|
||||
push:
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -40,3 +40,4 @@
|
||||
.idea
|
||||
|
||||
public/sitemap.xml.gz
|
||||
public/sitemaps
|
||||
|
@ -1 +1 @@
|
||||
ruby-3.3.5
|
||||
3.3.5
|
||||
|
7
Gemfile
7
Gemfile
@ -65,6 +65,10 @@ gem "image_processing", "~> 1.13"
|
||||
# gem "ruby-vips", "~> 2.2"
|
||||
gem "mini_magick", "~> 4.13.2"
|
||||
|
||||
gem "redis", "~> 5.3"
|
||||
|
||||
gem "builder", "~> 3.3"
|
||||
|
||||
group :development, :test do
|
||||
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
||||
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
|
||||
@ -79,6 +83,9 @@ end
|
||||
group :development do
|
||||
# Use console on exceptions pages [https://github.com/rails/web-console]
|
||||
gem "web-console"
|
||||
|
||||
gem "bullet", "~> 8.0"
|
||||
gem "rack-mini-profiler", "~> 3.3"
|
||||
end
|
||||
|
||||
group :production do
|
||||
|
12
Gemfile.lock
12
Gemfile.lock
@ -121,6 +121,9 @@ GEM
|
||||
brakeman (7.0.0)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bullet (8.0.1)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
matrix
|
||||
@ -316,6 +319,8 @@ GEM
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.9)
|
||||
rack-mini-profiler (3.3.1)
|
||||
rack (>= 1.2.0)
|
||||
rack-session (2.1.0)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
@ -360,6 +365,8 @@ GEM
|
||||
i18n
|
||||
rdoc (6.11.0)
|
||||
psych (>= 4.0.0)
|
||||
redis (5.3.0)
|
||||
redis-client (>= 0.22.0)
|
||||
redis-client (0.23.2)
|
||||
connection_pool
|
||||
regexp_parser (2.10.0)
|
||||
@ -478,6 +485,7 @@ GEM
|
||||
unicode-display_width (3.1.4)
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
uniform_notifier (1.16.0)
|
||||
uri (1.0.2)
|
||||
useragent (0.16.11)
|
||||
warden (1.2.9)
|
||||
@ -515,6 +523,8 @@ DEPENDENCIES
|
||||
aws-sdk-s3 (~> 1.170)
|
||||
bootsnap
|
||||
brakeman
|
||||
builder (~> 3.3)
|
||||
bullet (~> 8.0)
|
||||
capybara
|
||||
cssbundling-rails
|
||||
debug
|
||||
@ -532,7 +542,9 @@ DEPENDENCIES
|
||||
pg (~> 1.5)
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
rack-mini-profiler (~> 3.3)
|
||||
rails (~> 8.0.1)
|
||||
redis (~> 5.3)
|
||||
rubocop-rails-omakase
|
||||
ruby-openai (~> 7.3)
|
||||
selenium-webdriver
|
||||
|
@ -37,6 +37,41 @@ ActiveAdmin.register_page "Ahoy Dashboard" do
|
||||
end
|
||||
end
|
||||
|
||||
columns do
|
||||
column do
|
||||
panel "今日热门城市" do
|
||||
table_for City.by_popularity(:day, 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 City.by_popularity(:week, 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 City.by_popularity(:month, 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 City.by_popularity(:year, 10) do
|
||||
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
|
||||
column("访问量") { |city| city.view_count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
columns do
|
||||
column do
|
||||
panel "最冷门活跃城市" do
|
||||
|
@ -12,10 +12,9 @@ ActiveAdmin.register_page "Sidekiq Tasks" do
|
||||
form action: admin_sidekiq_tasks_run_task_path, method: :post do
|
||||
input type: "hidden", name: "authenticity_token", value: form_authenticity_token
|
||||
input type: "hidden", name: "task", value: "GenerateWeatherArtsWorker"
|
||||
select name: "city_id" do
|
||||
City.all.map do |city|
|
||||
option city.name, value: city.id
|
||||
end
|
||||
div class: "input-field" do
|
||||
label "City ID"
|
||||
input type: "number", name: "city_id", placeholder: "Enter city ID", required: true
|
||||
end
|
||||
input type: "submit", value: "Run Task", class: "button"
|
||||
end
|
||||
|
@ -1,6 +1,61 @@
|
||||
@import "photoswipe/dist/photoswipe.css";
|
||||
@import "mapbox-gl/dist/mapbox-gl.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.loading {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 1rem;
|
||||
transform: translateY(-50%);
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-right-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: translateY(-50%) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.mapboxgl-ctrl-logo,
|
||||
.mapboxgl-ctrl-attrib {
|
||||
@apply hidden !important; /* 隐藏版权信息 */
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-zoom-in,
|
||||
.mapboxgl-ctrl-zoom-out {
|
||||
@apply bg-base-200 text-base-content hover:bg-base-300 p-2;
|
||||
}
|
||||
|
||||
.mapboxgl-marker path {
|
||||
@apply fill-current text-primary;
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ class ApplicationController < ActionController::Base
|
||||
include SeoConcern
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
before_action :log_browser_info
|
||||
before_action :set_content_type_for_rss, if: -> { request.format.rss? }
|
||||
# allow_browser versions: :modern
|
||||
# allow_browser versions: :modern,
|
||||
# patterns: [
|
||||
@ -47,7 +48,7 @@ class ApplicationController < ActionController::Base
|
||||
# Bot: #{browser.bot?}
|
||||
# BROWSER_INFO
|
||||
# }
|
||||
before_action :set_locale
|
||||
around_action :set_locale
|
||||
after_action :track_action
|
||||
|
||||
def log_browser_info
|
||||
@ -75,7 +76,55 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
private
|
||||
|
||||
def set_locale
|
||||
I18n.locale = params[:locale] || I18n.default_locale
|
||||
def set_locale(&action)
|
||||
I18n.locale = extract_locale || I18n.default_locale
|
||||
I18n.fallbacks[I18n.locale] = [ I18n.locale, I18n.default_locale ].uniq
|
||||
locale = params[:locale] || extract_locale_from_accept_language_header || I18n.default_locale
|
||||
I18n.with_locale(locale, &action)
|
||||
# 重定向到带有语言前缀的相同路径
|
||||
# redirect_to "/#{locale}#{request.fullpath}"
|
||||
end
|
||||
|
||||
def extract_locale_from_accept_language_header
|
||||
return I18n.default_locale.to_s unless request.env["HTTP_ACCEPT_LANGUAGE"]
|
||||
|
||||
available_locales = I18n.available_locales.map(&:to_s)
|
||||
|
||||
accept_language = request.env["HTTP_ACCEPT_LANGUAGE"].to_s
|
||||
# 修改正则表达式以匹配 'zh-CN' 这样的格式
|
||||
if (full_locale = accept_language.scan(/^[a-z]{2}-[A-Z]{2}/).first)
|
||||
locale = full_locale
|
||||
else
|
||||
# 否则只匹配语言代码 (例如 'en')
|
||||
locale = accept_language.scan(/^[a-z]{2}/).first || I18n.default_locale.to_s
|
||||
end
|
||||
|
||||
return locale if available_locales.include?(locale)
|
||||
|
||||
# 尝试基础语言匹配(例如:当请求 'zh' 时匹配 'zh-CN')
|
||||
base_language = locale.split("-").first
|
||||
matching_locale = available_locales.find do |available_locale|
|
||||
available_locale.start_with?(base_language)
|
||||
end
|
||||
matching_locale ? matching_locale : I18n.default_locale.to_s
|
||||
end
|
||||
|
||||
def sanitize_locale(locale)
|
||||
# 直接使用 I18n.available_locales
|
||||
locale = locale.to_sym
|
||||
I18n.available_locales.include?(locale) ? locale : I18n.default_locale
|
||||
end
|
||||
|
||||
def extract_locale
|
||||
parsed_locale = params[:locale]
|
||||
I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
|
||||
end
|
||||
|
||||
def default_url_options
|
||||
{ locale: I18n.locale }
|
||||
end
|
||||
|
||||
def set_content_type_for_rss
|
||||
response.headers["Content-Type"] = "application/rss+xml; charset=utf-8"
|
||||
end
|
||||
end
|
||||
|
@ -3,7 +3,7 @@ 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 ]).includes(:image_attachment)
|
||||
|
||||
if @current_region
|
||||
@weather_arts = @weather_arts.joins(city: :country)
|
||||
|
@ -3,8 +3,13 @@ class CitiesController < ApplicationController
|
||||
before_action :require_admin, only: [ :generate_weather_art ]
|
||||
|
||||
def index
|
||||
@regions = Region.includes(:countries).order(:name)
|
||||
@regions = Region.order(:name)
|
||||
@cities = City.includes(:country, country: :region).order(:name)
|
||||
@latest_arts = WeatherArt.includes(:city, :image_attachment).latest(1)
|
||||
|
||||
if params[:query].present?
|
||||
@cities = @cities.search_by_name(params[:query])
|
||||
end
|
||||
|
||||
if params[:region]
|
||||
@current_region = Region.friendly.find(params[:region])
|
||||
@ -13,20 +18,25 @@ class CitiesController < ApplicationController
|
||||
|
||||
if params[:country]
|
||||
@current_country = Country.friendly.find(params[:country])
|
||||
@cities = @cities.by_country(@current_country.id)
|
||||
@cities = @cities.by_country(@current_country.id) if @current_country
|
||||
end
|
||||
|
||||
@cities = @cities.page(params[:page]).per(12)
|
||||
|
||||
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"
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.turbo_stream {
|
||||
render turbo_stream: turbo_stream.update("cities_results",
|
||||
partial: "cities/results",
|
||||
locals: { cities: @cities }
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@city = City.friendly.find(params[:id])
|
||||
@arts = @city.weather_arts.order(weather_date: :desc).includes([ :image_attachment ])
|
||||
ahoy.track "View City", {
|
||||
city_id: @city.id,
|
||||
name: @city.name,
|
||||
@ -36,7 +46,7 @@ class CitiesController < ApplicationController
|
||||
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",
|
||||
keywords: "#{@city.name}, #{@city.country.name}, ai, ai web, ai art, ai weather, weather art, AI visualization",
|
||||
og: {
|
||||
image: @city.latest_weather_art&.image&.attached? ? url_for(@city.latest_weather_art.image) : nil
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
class HomeController < ApplicationController
|
||||
def index
|
||||
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(20).shuffle.last(10)
|
||||
@featured_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(5)
|
||||
@popular_arts = WeatherArt.includes(:image_attachment, city: :country).by_popularity(3)
|
||||
@latest_arts = WeatherArt.includes(:image_attachment, city: :country).latest(6)
|
||||
# @random_arts = WeatherArt.includes(:city, :image_attachment).random(3)
|
||||
# @featured_arts = WeatherArt.includes(:city, :image_attachment).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.",
|
||||
|
9
app/controllers/rss_controller.rb
Normal file
9
app/controllers/rss_controller.rb
Normal file
@ -0,0 +1,9 @@
|
||||
class RssController < ApplicationController
|
||||
def feed
|
||||
@weather_arts = WeatherArt.order(created_at: :desc).limit(20)
|
||||
|
||||
respond_to do |format|
|
||||
format.rss { render layout: false }
|
||||
end
|
||||
end
|
||||
end
|
@ -1,33 +1,126 @@
|
||||
class SitemapsController < ApplicationController
|
||||
include SitemapsHelper
|
||||
before_action :set_bucket_name
|
||||
|
||||
def index
|
||||
@sitemaps = list_sitemaps
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.xml { render_sitemap_index }
|
||||
end
|
||||
rescue Aws::S3::Errors::ServiceError => e
|
||||
Rails.logger.error "S3 Error: #{e.message}"
|
||||
render status: :internal_server_error
|
||||
end
|
||||
|
||||
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}"
|
||||
key = "sitemaps/#{path}"
|
||||
|
||||
Rails.logger.info "Requesting sitemap: #{path}"
|
||||
|
||||
begin
|
||||
s3_client = Aws::S3::Client.new
|
||||
response = s3_client.get_object(
|
||||
bucket: bucket_name,
|
||||
key: "sitemaps/#{path}"
|
||||
# 检查文件是否存在
|
||||
s3_client.head_object(
|
||||
bucket: @bucket_name,
|
||||
key: key
|
||||
)
|
||||
|
||||
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"
|
||||
# 生成预签名URL,设置15分钟有效期
|
||||
signer = Aws::S3::Presigner.new(client: s3_client)
|
||||
url = signer.presigned_url(
|
||||
:get_object,
|
||||
bucket: @bucket_name,
|
||||
key: key,
|
||||
expires_in: 15 * 60, # 15 minutes
|
||||
# response_content_type: 'application/xml', # 确保正确的内容类型
|
||||
response_content_disposition: "inline" # 在浏览器中直接显示
|
||||
)
|
||||
rescue Aws::S3::Errors::NoSuchKey
|
||||
|
||||
# 设置缓存头
|
||||
response.headers["Cache-Control"] = "public, max-age=3600" # 1小时缓存
|
||||
|
||||
# 重定向到预签名URL
|
||||
redirect_to url, allow_other_host: true, status: :found
|
||||
|
||||
rescue Aws::S3::Errors::NotFound
|
||||
Rails.logger.error "Sitemap not found: #{path}"
|
||||
render status: :not_found
|
||||
rescue Aws::S3::Errors::ServiceError => e
|
||||
Rails.logger.error "S3 Error: #{e.message}"
|
||||
render status: :internal_server_error
|
||||
end
|
||||
end
|
||||
|
||||
# def show
|
||||
# path = params[:path]
|
||||
# Rails.logger.info "Sitemap: #{path}"
|
||||
|
||||
# begin
|
||||
# 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
|
||||
|
||||
private
|
||||
|
||||
def set_bucket_name
|
||||
@bucket_name = Rails.env.production? ?
|
||||
ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:minio, :bucket)) :
|
||||
ENV.fetch("AWS_DEV_BUCKET", Rails.application.credentials.dig(:minio_dev, :bucket))
|
||||
end
|
||||
|
||||
def s3_client
|
||||
@s3_client ||= Aws::S3::Client.new
|
||||
end
|
||||
|
||||
def list_sitemaps
|
||||
response = s3_client.list_objects_v2(
|
||||
bucket: @bucket_name,
|
||||
prefix: "sitemaps/"
|
||||
)
|
||||
|
||||
response.contents.map do |object|
|
||||
{
|
||||
key: object.key.sub("sitemaps/", ""),
|
||||
last_modified: object.last_modified,
|
||||
size: object.size,
|
||||
url: sitemap_url(object.key.sub("sitemaps/", ""))
|
||||
}
|
||||
end.reject { |obj| obj[:key].empty? }
|
||||
end
|
||||
|
||||
def render_sitemap_index
|
||||
base_url = "#{request.protocol}#{request.host_with_port}"
|
||||
|
||||
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
||||
xml.sitemapindex(xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9") do
|
||||
@sitemaps.each do |sitemap|
|
||||
xml.sitemap do
|
||||
xml.loc "#{base_url}/sitemaps/#{sitemap[:key]}"
|
||||
xml.lastmod sitemap[:last_modified].iso8601
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
render xml: builder.to_xml
|
||||
end
|
||||
end
|
||||
|
@ -3,16 +3,6 @@ class WeatherArtsController < ApplicationController
|
||||
@city = City.friendly.find(params[:city_id])
|
||||
@weather_art = @city.weather_arts.friendly.find(params[:slug])
|
||||
|
||||
@previous_weather_art = @city.weather_arts
|
||||
.where("id < ?", @weather_art.id)
|
||||
.order(id: :desc)
|
||||
.first
|
||||
|
||||
@next_weather_art = @city.weather_arts
|
||||
.where("id > ?", @weather_art.id)
|
||||
.order(id: :asc)
|
||||
.first
|
||||
|
||||
ahoy.track "View Weather Art", {
|
||||
weather_art_id: @weather_art.id,
|
||||
city_id: @weather_art.city_id,
|
||||
@ -27,7 +17,7 @@ class WeatherArtsController < ApplicationController
|
||||
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",
|
||||
keywords: "#{@city.name}, #{@city.country.name}, ai, ai web, ai art, ai weather, weather art, AI visualization, #{@weather_art.description}",
|
||||
og: {
|
||||
image: @weather_art.image.attached? ? url_for(@weather_art.image) : nil
|
||||
}
|
||||
|
2
app/helpers/rss_helper.rb
Normal file
2
app/helpers/rss_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module RssHelper
|
||||
end
|
@ -1,2 +1,5 @@
|
||||
module SitemapsHelper
|
||||
def sitemap_url(filename)
|
||||
"/sitemaps/#{filename}"
|
||||
end
|
||||
end
|
||||
|
@ -6,7 +6,16 @@ import { application } from "./application"
|
||||
import HelloController from "./hello_controller"
|
||||
import PhotoSwipeLightBoxController from "./photo_swipe_lightbox_controller"
|
||||
import FlashMessageController from "./flash_controller"
|
||||
import SearchController from "./search_controller"
|
||||
import PageLoadTimeController from "./page_load_time_controller"
|
||||
import MapController from "./map_controller"
|
||||
import ShareController from "./share_controller"
|
||||
|
||||
application.register("hello", HelloController)
|
||||
application.register("photo-swipe-lightbox", PhotoSwipeLightBoxController)
|
||||
application.register("flash", FlashMessageController)
|
||||
application.register("search", SearchController)
|
||||
application.register("page-load-time", PageLoadTimeController)
|
||||
application.register("map", MapController)
|
||||
application.register("share", ShareController)
|
||||
|
||||
|
72
app/javascript/controllers/map_controller.js
Normal file
72
app/javascript/controllers/map_controller.js
Normal file
@ -0,0 +1,72 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import mapboxgl from 'mapbox-gl'
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
latitude: Number,
|
||||
longitude: Number,
|
||||
zoom: { type: Number, default: 6 },
|
||||
weatherArt: Object,
|
||||
weatherArtUrl: String,
|
||||
token: String
|
||||
}
|
||||
|
||||
connect() {
|
||||
mapboxgl.accessToken = this.tokenValue
|
||||
|
||||
this.map = new mapboxgl.Map({
|
||||
container: this.element,
|
||||
style: 'mapbox://styles/mapbox/satellite-streets-v12',
|
||||
// projection: 'globe', // 启用 3D 地球模式
|
||||
center: [this.longitudeValue, this.latitudeValue],
|
||||
zoom: this.zoomValue
|
||||
});
|
||||
|
||||
this.map.on('style.load', () => {
|
||||
// 设置地球效果
|
||||
this.map.setFog({
|
||||
'color': 'rgb(186, 210, 235)',
|
||||
'high-color': 'rgb(36, 92, 223)',
|
||||
'horizon-blend': 0.02
|
||||
});
|
||||
});
|
||||
|
||||
// 添加标记
|
||||
// const marker = new mapboxgl.Marker({
|
||||
// color: "#FF6B6B",
|
||||
// scale: 1.2
|
||||
// })
|
||||
// .setLngLat([this.longitudeValue, this.latitudeValue])
|
||||
// .addTo(this.map);
|
||||
|
||||
const marker = new mapboxgl.Marker()
|
||||
.setLngLat([this.longitudeValue, this.latitudeValue])
|
||||
.addTo(this.map);
|
||||
// marker.getElement().addEventListener('click', () => {
|
||||
// this.showPopup();
|
||||
// });
|
||||
|
||||
// 默认弹出窗口
|
||||
// this.showPopup();
|
||||
|
||||
// 添加缩放控件
|
||||
this.map.addControl(new mapboxgl.NavigationControl());
|
||||
}
|
||||
|
||||
showPopup() {
|
||||
console.log("weatherArtValue: ", this.weatherArtValue)
|
||||
const popupContent = `
|
||||
<div class="p-4 bg-white rounded-lg shadow-lg">
|
||||
<img src="${this.weatherArtUrlValue}" alt="${this.weatherArtValue.description}" class="w-full h-auto rounded-md mb-2" />
|
||||
<p class="text-sm text-gray-600">${this.weatherArtValue.description}</p>
|
||||
<a href="/weather_arts/${this.weatherArtValue.id}" class="btn btn-primary mt-2">查看详情</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const popup = new mapboxgl.Popup()
|
||||
.setLngLat([this.longitudeValue, this.latitudeValue])
|
||||
.setHTML(popupContent)
|
||||
.addTo(this.map);
|
||||
}
|
||||
}
|
18
app/javascript/controllers/page_load_time_controller.js
Normal file
18
app/javascript/controllers/page_load_time_controller.js
Normal file
@ -0,0 +1,18 @@
|
||||
import {Controller} from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["timer"]
|
||||
|
||||
connect() {
|
||||
// 记录页面加载开始的时间
|
||||
const startTime = performance.now();
|
||||
|
||||
// 监听页面加载完成事件
|
||||
window.addEventListener('load', () => {
|
||||
const endTime = performance.now();
|
||||
const loadTime = endTime - startTime;
|
||||
// 更新显示
|
||||
this.timerTarget.textContent = Math.ceil( loadTime );
|
||||
});
|
||||
}
|
||||
}
|
72
app/javascript/controllers/search_controller.js
Normal file
72
app/javascript/controllers/search_controller.js
Normal file
@ -0,0 +1,72 @@
|
||||
// app/javascript/controllers/search_controller.js
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["input", "clearButton", "spinner", "statusIcon"]
|
||||
|
||||
connect() {
|
||||
this.pendingRequest = null
|
||||
}
|
||||
|
||||
submit() {
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => {
|
||||
// 如果有待处理的请求,则中止它
|
||||
if (this.pendingRequest) {
|
||||
this.pendingRequest.abort()
|
||||
}
|
||||
|
||||
const form = this.element
|
||||
const searchInput = this.inputTarget
|
||||
const encodedValue = encodeURIComponent(searchInput.value)
|
||||
|
||||
// 更新 URL
|
||||
const url = new URL(window.location)
|
||||
url.searchParams.set('query', encodedValue)
|
||||
window.history.pushState({}, '', url)
|
||||
|
||||
// 显示加载状态
|
||||
this.showLoadingState()
|
||||
|
||||
// 发送请求
|
||||
this.pendingRequest = new AbortController()
|
||||
fetch(form.action + '?' + new URLSearchParams(new FormData(form)), {
|
||||
headers: {
|
||||
'Accept': 'text/vnd.turbo-stream.html',
|
||||
'Turbo-Frame': 'cities_results'
|
||||
},
|
||||
signal: this.pendingRequest.signal
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
Turbo.renderStreamMessage(html)
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.name === 'AbortError') return
|
||||
console.error('Search error:', error)
|
||||
})
|
||||
.finally(() => {
|
||||
this.hideLoadingState()
|
||||
this.pendingRequest = null
|
||||
})
|
||||
}, 300)
|
||||
}
|
||||
|
||||
showLoadingState() {
|
||||
if (this.hasClearButtonTarget) {
|
||||
this.clearButtonTarget.classList.add('hidden')
|
||||
}
|
||||
if (this.hasSpinnerTarget) {
|
||||
this.spinnerTarget.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
hideLoadingState() {
|
||||
if (this.hasClearButtonTarget) {
|
||||
this.clearButtonTarget.classList.remove('hidden')
|
||||
}
|
||||
if (this.hasSpinnerTarget) {
|
||||
this.spinnerTarget.classList.add('hidden')
|
||||
}
|
||||
}
|
||||
}
|
12
app/javascript/controllers/share_controller.js
Normal file
12
app/javascript/controllers/share_controller.js
Normal file
@ -0,0 +1,12 @@
|
||||
// app/javascript/controllers/share_controller.js
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import Sharer from "sharer.js"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["button"]
|
||||
|
||||
connect() {
|
||||
// 初始化 sharer.js
|
||||
window.Sharer.init()
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
class City < ApplicationRecord
|
||||
extend FriendlyId
|
||||
friendly_id :slug_candidates, use: :slugged
|
||||
belongs_to :country
|
||||
belongs_to :country, optional: true
|
||||
belongs_to :state, optional: true
|
||||
|
||||
has_many :weather_arts, dependent: :destroy
|
||||
@ -22,20 +22,45 @@ class City < ApplicationRecord
|
||||
scope :active, -> { where(active: true) }
|
||||
scope :inactive, -> { where(active: false) }
|
||||
|
||||
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")
|
||||
# 在 City 模型中
|
||||
scope :by_popularity, ->(period = :year, limit = 100) {
|
||||
# 根据时间周期确定时间范围
|
||||
start_time =
|
||||
case period.to_sym
|
||||
when :day
|
||||
1.day.ago
|
||||
when :week
|
||||
1.week.ago
|
||||
when :month
|
||||
1.month.ago
|
||||
when :year
|
||||
1.year.ago
|
||||
else
|
||||
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
|
||||
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
|
||||
1.year.ago
|
||||
end
|
||||
|
||||
# 根据数据库类型构建不同的查询
|
||||
base_query = if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
joins(<<-SQL.squish)
|
||||
LEFT JOIN ahoy_events ON#{' '}
|
||||
json_extract(ahoy_events.properties, '$.city_id') = cities.id
|
||||
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'
|
||||
AND ahoy_events.time > '#{start_time}'
|
||||
SQL
|
||||
else
|
||||
joins(<<-SQL.squish)
|
||||
LEFT JOIN ahoy_events ON#{' '}
|
||||
(ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
|
||||
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'
|
||||
AND ahoy_events.time > '#{start_time}'
|
||||
SQL
|
||||
end
|
||||
|
||||
base_query
|
||||
.group("cities.id")
|
||||
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count DESC")
|
||||
end
|
||||
.limit(limit)
|
||||
}
|
||||
|
||||
scope :least_popular_active, ->(limit = 100) {
|
||||
@ -72,6 +97,46 @@ class City < ApplicationRecord
|
||||
.order("COUNT(ahoy_events.id) DESC, cities.name ASC").limit(limit)
|
||||
end
|
||||
}
|
||||
scope :search_by_name, ->(query) {
|
||||
return all if query.blank?
|
||||
|
||||
decoded_query = URI.decode_www_form_component(query).downcase
|
||||
|
||||
left_joins(:state)
|
||||
.where(
|
||||
"LOWER(cities.name) LIKE :query OR LOWER(states.name) LIKE :query",
|
||||
query: "%#{decoded_query}%"
|
||||
)
|
||||
.distinct # 避免重复结果
|
||||
}
|
||||
|
||||
# 定义 latest_weather_art 关联
|
||||
has_one :latest_weather_art, -> { order(weather_date: :desc) },
|
||||
class_name: "WeatherArt"
|
||||
|
||||
# 包含最新天气艺术的 scope
|
||||
scope :with_latest_weather_art, -> {
|
||||
includes(:latest_weather_art)
|
||||
}
|
||||
|
||||
# 只获取有最新天气艺术的城市
|
||||
scope :has_weather_art, -> {
|
||||
joins(:weather_arts).distinct
|
||||
}
|
||||
|
||||
# 按最新天气更新时间排序
|
||||
scope :order_by_latest_weather, -> {
|
||||
joins(:weather_arts)
|
||||
.group("cities.id")
|
||||
.order("MAX(weather_arts.weather_date) DESC")
|
||||
}
|
||||
|
||||
# 获取最近24小时内更新过天气的城市
|
||||
scope :recently_updated, -> {
|
||||
joins(:weather_arts)
|
||||
.where("weather_arts.weather_date > ?", 24.hours.ago)
|
||||
.distinct
|
||||
}
|
||||
|
||||
|
||||
def to_s
|
||||
@ -152,4 +217,35 @@ class City < ApplicationRecord
|
||||
.count
|
||||
end
|
||||
end
|
||||
|
||||
def formatted_current_time(type = :date, use_local_timezone = true)
|
||||
# 获取时区
|
||||
timezone_info = self.country&.timezones.present? ?
|
||||
JSON.parse(country.timezones)&.first :
|
||||
{ "zoneName" => "UTC", "gmtOffsetName" => "UTC+00:00" }
|
||||
|
||||
# 设置时区对象
|
||||
time_zone = ActiveSupport::TimeZone[timezone_info["zoneName"]] ||
|
||||
ActiveSupport::TimeZone["UTC"]
|
||||
time = Time.current
|
||||
|
||||
case type
|
||||
when :date
|
||||
# 格式化日期
|
||||
time.strftime("%B %d, %Y")
|
||||
when :time
|
||||
use_local_timezone ?
|
||||
"#{time.in_time_zone(time_zone).strftime('%H:%M')} #{timezone_info['gmtOffsetName']}" :
|
||||
"#{time.utc.strftime('%H:%M')} UTC"
|
||||
when :all
|
||||
# 返回日期 + 时间 + UTC 信息
|
||||
date = time.strftime("%B %d, %Y")
|
||||
time = use_local_timezone ?
|
||||
updated_at.in_time_zone(time_zone).strftime("%H:%M") + " #{timezone_info['gmtOffsetName']}" :
|
||||
updated_at.utc.strftime("%H:%M") + " UTC"
|
||||
"#{date} #{time}"
|
||||
else
|
||||
"Unknown #{type}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
22
app/models/concerns/translatable_name.rb
Normal file
22
app/models/concerns/translatable_name.rb
Normal file
@ -0,0 +1,22 @@
|
||||
# app/models/concerns/translatable_name.rb
|
||||
module TranslatableName
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def localized_name(default_locale = "en")
|
||||
return name unless translations.present?
|
||||
|
||||
translations_hash = translations.is_a?(String) ? JSON.parse(translations) : translations
|
||||
|
||||
# 尝试完全匹配当前语言设置
|
||||
current_locale = I18n.locale.to_s
|
||||
return translations_hash[current_locale] if translations_hash[current_locale].present?
|
||||
|
||||
# 尝试匹配语言的基础部分(例如 'zh-CN' => 'zh')
|
||||
base_locale = current_locale.split("-").first
|
||||
matching_key = translations_hash.keys.find { |k| k.start_with?(base_locale) }
|
||||
return translations_hash[matching_key] if matching_key.present?
|
||||
|
||||
# 如果没有匹配,返回默认语言的翻译或原始名称
|
||||
translations_hash[default_locale] || name
|
||||
end
|
||||
end
|
@ -1,7 +1,10 @@
|
||||
class Country < ApplicationRecord
|
||||
include TranslatableName
|
||||
extend FriendlyId
|
||||
friendly_id :name, use: :slugged
|
||||
|
||||
# before_save :format_json_attributes, :timezones, :translations
|
||||
|
||||
belongs_to :region, optional: true
|
||||
belongs_to :subregion, optional: true
|
||||
has_many :cities, dependent: :restrict_with_error
|
||||
@ -11,13 +14,15 @@ class Country < ApplicationRecord
|
||||
validates :code, presence: true, uniqueness: true
|
||||
validates :iso2, uniqueness: true, allow_blank: true
|
||||
|
||||
serialize :translations, coder: JSON
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def localized_name
|
||||
I18n.t("countries.#{code}")
|
||||
end
|
||||
# def localized_name
|
||||
# I18n.t("countries.#{code}")
|
||||
# end
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
[ "code", "created_at", "id", "id_value", "name", "region_id", "slug", "updated_at" ]
|
||||
@ -26,4 +31,29 @@ class Country < ApplicationRecord
|
||||
def self.ransackable_associations(auth_object = nil)
|
||||
[ "cities", "region" ]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# def format_timezones
|
||||
# return unless timezones.is_a?(String)
|
||||
#
|
||||
# # 使用正则替换 => 为 :
|
||||
# json_string = timezones.gsub(/=>/, ":")
|
||||
#
|
||||
# # 清理多余的空格
|
||||
# json_string = json_string.gsub(/\s+/, " ").strip
|
||||
#
|
||||
# begin
|
||||
# # 验证是否为有效的 JSON
|
||||
# parsed_json = JSON.parse(json_string)
|
||||
# self.timezones = parsed_json.to_json
|
||||
# rescue JSON::ParserError
|
||||
# # 如果转换失败,可以选择:
|
||||
# # 1. 保持原值
|
||||
# # 2. 设置为空数组
|
||||
# # 3. 记录错误日志
|
||||
# Rails.logger.error("Invalid JSON format for country #{id}: #{timezones}")
|
||||
# self.timezones = "[]"
|
||||
# end
|
||||
# end
|
||||
end
|
||||
|
@ -1,4 +1,6 @@
|
||||
class Region < ApplicationRecord
|
||||
include TranslatableName
|
||||
|
||||
extend FriendlyId
|
||||
friendly_id :name, use: :slugged
|
||||
|
||||
@ -9,13 +11,15 @@ class Region < ApplicationRecord
|
||||
validates :name, presence: true, uniqueness: true
|
||||
validates :code, presence: true, uniqueness: true
|
||||
|
||||
serialize :translations, coder: JSON
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def localized_name
|
||||
I18n.t("regions.#{code}")
|
||||
end
|
||||
# def localized_name
|
||||
# I18n.t("regions.#{code}")
|
||||
# end
|
||||
|
||||
# 模型中允许被搜索的关联
|
||||
def self.ransackable_associations(auth_object = nil)
|
||||
|
@ -4,7 +4,6 @@ class WeatherArt < ApplicationRecord
|
||||
|
||||
belongs_to :city
|
||||
has_one_attached :image
|
||||
has_one_attached :image_with_watermark
|
||||
|
||||
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id
|
||||
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
|
||||
@ -12,19 +11,36 @@ class WeatherArt < ApplicationRecord
|
||||
validates :weather_date, presence: true
|
||||
validates :city_id, presence: true
|
||||
|
||||
scope :by_popularity, -> {
|
||||
scope :latest, ->(limit = 100) {
|
||||
order(created_at: :desc).limit(limit)
|
||||
}
|
||||
|
||||
scope :by_popularity, ->(limit = 100) {
|
||||
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")
|
||||
.order("visit_count DESC").limit(limit)
|
||||
else
|
||||
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'weather_art_id')::integer = weather_arts.id
|
||||
AND ahoy_events.properties::jsonb->>'event_type' = 'weather_art_view'")
|
||||
.group("weather_arts.id")
|
||||
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count DESC")
|
||||
.order("visit_count DESC").limit(limit)
|
||||
end
|
||||
}
|
||||
|
||||
scope :random, ->(limit = 3) {
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "postgresql"
|
||||
# PostgreSQL 优化版本
|
||||
order(Arel.sql("RANDOM()")).limit(limit)
|
||||
elsif ActiveRecord::Base.connection.adapter_name.downcase == "mysql2"
|
||||
# MySQL 优化版本
|
||||
order(Arel.sql("RAND()")).limit(limit)
|
||||
else
|
||||
# SQLite 或其他数据库的通用版本
|
||||
order(Arel.sql("RANDOM()")).limit(limit)
|
||||
end
|
||||
}
|
||||
|
||||
@ -52,7 +68,195 @@ class WeatherArt < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def formatted_time(type = :date, use_local_timezone = false)
|
||||
# 获取时区
|
||||
timezone_info = self.city&.country&.timezones.present? ?
|
||||
JSON.parse(self.city.country.timezones)&.first :
|
||||
{ "zoneName" => "UTC", "gmtOffsetName" => "UTC+00:00" }
|
||||
|
||||
# 设置时区对象
|
||||
time_zone = ActiveSupport::TimeZone[timezone_info["zoneName"]] ||
|
||||
ActiveSupport::TimeZone["UTC"]
|
||||
time = self.updated_at
|
||||
|
||||
# 使用 I18n 本地化格式化日期
|
||||
date_string = I18n.l(self.weather_date, format: :long)
|
||||
|
||||
# 格式化时间
|
||||
time_format = use_local_timezone ? time.in_time_zone(time_zone) : time.utc
|
||||
time_string =
|
||||
if use_local_timezone
|
||||
I18n.t("time.formats.with_zone",
|
||||
time: I18n.l(time_format, format: :time_only),
|
||||
zone: timezone_info["gmtOffsetName"]
|
||||
)
|
||||
else
|
||||
I18n.t("time.formats.with_zone",
|
||||
time: I18n.l(time_format, format: :time_only),
|
||||
zone: "UTC"
|
||||
)
|
||||
end
|
||||
|
||||
case type
|
||||
when :date
|
||||
date_string
|
||||
when :time
|
||||
time_string
|
||||
when :all
|
||||
I18n.t("time.formats.date_and_time",
|
||||
date: date_string,
|
||||
time: time_string
|
||||
)
|
||||
else
|
||||
I18n.t("time.formats.date_and_time",
|
||||
date: date_string,
|
||||
time: time_string
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def image_url
|
||||
image.attached? ? image.blob : nil
|
||||
end
|
||||
|
||||
def webp_image
|
||||
return nil unless image.attached?
|
||||
|
||||
image.variant(
|
||||
format: "webp",
|
||||
saver: {
|
||||
quality: 100,
|
||||
strip: true, # 移除元数据以减小文件大小
|
||||
interlace: "plane" # 渐进式加载
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
# 添加图片变体处理
|
||||
PREVIEW_DIMENSIONS = {
|
||||
big: [ 1792, 1024 ],
|
||||
medium: [ 896, 512 ],
|
||||
small: [ 448, 256 ]
|
||||
}.freeze
|
||||
def preview_image(size = :big)
|
||||
return nil unless image.attached?
|
||||
|
||||
width, height = PREVIEW_DIMENSIONS[size] || PREVIEW_DIMENSIONS[:medium]
|
||||
|
||||
image.variant(
|
||||
resize_to_limit: [ width, height ],
|
||||
format: "webp",
|
||||
saver: {
|
||||
quality: 75,
|
||||
strip: true, # 移除元数据以减小文件大小
|
||||
interlace: "plane" # 渐进式加载
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def watermarked_image
|
||||
return nil unless image.attached?
|
||||
|
||||
overlay_text = create_overlay_text
|
||||
|
||||
image.variant(
|
||||
composite: [ {
|
||||
input: overlay_text,
|
||||
gravity: "southeast"
|
||||
} ]
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_overlay_text
|
||||
{
|
||||
create: {
|
||||
width: 400,
|
||||
height: 100,
|
||||
background: [ 0, 0, 0, 0.5 ] # 半透明黑色背景
|
||||
},
|
||||
"svg-overlay": %(
|
||||
<svg width="400" height="100">
|
||||
<text x="20" y="40"
|
||||
style="fill: white; font-family: Arial; font-size: 20px;">
|
||||
#{city.name} - #{weather_date.strftime('%Y-%m-%d')}
|
||||
</text>
|
||||
<text x="20" y="70"
|
||||
style="fill: white; font-family: Arial; font-size: 20px;">
|
||||
© todayaiweather.com
|
||||
</text>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
def create_text_layer(font_size)
|
||||
text = [
|
||||
weather_date.strftime("%Y-%m-%d"),
|
||||
"#{temperature}°C, #{description}",
|
||||
"#{city.name}, #{city.country.name}, #{city.country.region.name}",
|
||||
"© todayaiweather.com"
|
||||
].join("\n")
|
||||
|
||||
{
|
||||
create: {
|
||||
width: 600,
|
||||
height: 200,
|
||||
background: [ 0, 0, 0, 0 ]
|
||||
},
|
||||
"svg-overlay": %(
|
||||
<svg width="600" height="200">
|
||||
<style>
|
||||
.text {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: #{font_size}px;
|
||||
}
|
||||
.shadow {
|
||||
fill: white;
|
||||
stroke: black;
|
||||
stroke-width: 2px;
|
||||
paint-order: stroke fill;
|
||||
}
|
||||
</style>
|
||||
<text x="20" y="#{font_size + 10}" class="text shadow">#{weather_date.strftime('%Y-%m-%d')}</text>
|
||||
<text x="20" y="#{font_size * 2 + 20}" class="text shadow">#{temperature}°C, #{description}</text>
|
||||
<text x="20" y="#{font_size * 3 + 30}" class="text shadow">#{city.name}, #{city.country.name}</text>
|
||||
<text x="20" y="#{font_size * 4 + 40}" class="text shadow">© todayaiweather.com</text>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
def watermark_command(font_size:, stroke_width:, spacing:)
|
||||
date_str = weather_date.strftime("%Y-%m-%d")
|
||||
weather_info = "#{temperature}°C, #{description}"
|
||||
location_info = "#{city.name}, #{city.country.name}, #{city.country.region.name}"
|
||||
copyright = "© todayaiweather.com"
|
||||
|
||||
"gravity southeast " \
|
||||
"fill white " \
|
||||
"font Arial " \
|
||||
"pointsize #{font_size} " \
|
||||
"stroke black " \
|
||||
"strokewidth #{stroke_width} " \
|
||||
"text 30,#{spacing * 12} '#{copyright}' " \
|
||||
"text 30,#{spacing * 8} '#{location_info}' " \
|
||||
"text 30,#{spacing * 4} '#{weather_info}' " \
|
||||
"text 30,#{spacing} '#{date_str}'"
|
||||
end
|
||||
|
||||
def watermark_text
|
||||
date_str = weather_date.strftime("%Y-%m-%d")
|
||||
weather_info = "#{temperature}°C, #{description}"
|
||||
location_info = "#{city.name}, #{city.country.name}, #{city.country.region.name}"
|
||||
copyright = "© todayaiweather.com"
|
||||
|
||||
[
|
||||
"text 30,120 '#{copyright}'",
|
||||
"text 30,80 '#{location_info}'",
|
||||
"text 30,40 '#{weather_info}'",
|
||||
"text 30,0 '#{date_str}'"
|
||||
].join(" ")
|
||||
end
|
||||
end
|
||||
|
@ -3,6 +3,7 @@ class AiService
|
||||
@client = OpenAI::Client.new(
|
||||
access_token: Rails.application.credentials.openai.token,
|
||||
uri_base: Rails.application.credentials.openai.uri,
|
||||
log_errors: Rails.env.development?, # 只在开发环境下启用
|
||||
request_timeout: 240
|
||||
)
|
||||
end
|
||||
@ -34,7 +35,8 @@ class AiService
|
||||
model: "dall-e-3",
|
||||
prompt: prompt,
|
||||
size: "1792x1024",
|
||||
quality: "standard",
|
||||
# quality: "standard",
|
||||
quality: "hd",
|
||||
n: 1
|
||||
}
|
||||
)
|
||||
@ -47,8 +49,8 @@ class AiService
|
||||
def ask_ai(system_message, user_message)
|
||||
response = @client.chat(
|
||||
parameters: {
|
||||
model: "gpt-4",
|
||||
message:
|
||||
model: "gpt-4o",
|
||||
messages:
|
||||
[ {
|
||||
role: "System",
|
||||
content: system_message
|
||||
|
@ -31,8 +31,8 @@ class WeatherService
|
||||
pressure: data["pressure"].to_f,
|
||||
visibility: data["vis"].to_f,
|
||||
cloud: data["cloud"].to_f,
|
||||
description: data["text"],
|
||||
time: response["updateTime"]
|
||||
description: data["text"]
|
||||
# time: response["updateTime"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
@ -6,7 +6,7 @@
|
||||
<!-- 背景图像 -->
|
||||
<% if featured_art&.image&.attached? %>
|
||||
<div class="absolute inset-0 h-[40vh] overflow-hidden">
|
||||
<%= image_tag featured_art.image,
|
||||
<%= image_tag featured_art.webp_image.processed,
|
||||
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>
|
||||
@ -17,18 +17,18 @@
|
||||
<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
|
||||
<%= t("arts.title") %>
|
||||
</h1>
|
||||
<p class="text-xl text-base-content/70">
|
||||
Discover AI-generated weather art from cities around the world
|
||||
<%= t("arts.subtitle") %>
|
||||
</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 %>
|
||||
<%= "#{t("text.latest_from")} #{featured_art.city.full_name}" %>
|
||||
<span class="mx-2">•</span>
|
||||
<%= featured_art.weather_date.strftime("%B %d, %Y") %>
|
||||
<%= featured_art.formatted_time(:date) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@ -47,18 +47,18 @@
|
||||
<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' %>
|
||||
<%= params[:sort] == 'oldest' ? t("text.oldest_first") : t("text.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]),
|
||||
<%= link_to t("text.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]),
|
||||
<%= link_to t("text.oldest_first"), arts_path(sort: 'oldest', region: params[:region]),
|
||||
class: "#{'active' if params[:sort] == 'oldest'}" %>
|
||||
</li>
|
||||
</ul>
|
||||
@ -70,20 +70,20 @@
|
||||
<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' %>
|
||||
<%= @current_region&.localized_name || t("text.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]),
|
||||
<%= link_to t("text.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]),
|
||||
<%= link_to region.localized_name, arts_path(region: region.id, sort: params[:sort]),
|
||||
class: "#{'active' if @current_region == region}" %>
|
||||
</li>
|
||||
<% end %>
|
||||
@ -93,9 +93,9 @@
|
||||
|
||||
<!-- 结果统计 -->
|
||||
<div class="text-center text-sm text-base-content/70 mt-4">
|
||||
Showing <%= @weather_arts.total_count %> weather arts
|
||||
<%= "#{t("text.showing")} #{@weather_arts.total_count} #{t("text.weather_arts")} " %>
|
||||
<% if @current_region %>
|
||||
from <%= @current_region.name %>
|
||||
from <%= @current_region.localized_name %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@ -107,7 +107,7 @@
|
||||
<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,
|
||||
<%= image_tag art.preview_image.processed,
|
||||
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>
|
||||
@ -118,7 +118,7 @@
|
||||
<%= art.city.name %>
|
||||
</h3>
|
||||
<p class="text-sm text-white/80">
|
||||
<%= art.city.country.name %>
|
||||
<%= "#{art.city&.country&.emoji + " " || ""}#{art.city&.country&.localized_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">
|
||||
@ -139,7 +139,7 @@
|
||||
<%= art.city.name %>
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/70">
|
||||
<%= art.weather_date.strftime("%B %d, %Y") %>
|
||||
<%= art.formatted_time(:date, true) %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
@ -154,7 +154,7 @@
|
||||
|
||||
<%= link_to city_weather_art_path(art.city, art),
|
||||
class: "btn btn-primary btn-sm w-full" do %>
|
||||
View Details
|
||||
<%= t("button.view_detail") %>
|
||||
<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>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="relative">
|
||||
<!-- 图片 -->
|
||||
<figure class="aspect-[16/9] overflow-hidden">
|
||||
<%= image_tag city.latest_weather_art.image,
|
||||
<%= image_tag city.latest_weather_art.preview_image.processed,
|
||||
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
|
||||
</figure>
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
<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>
|
||||
<%= city&.country&.name %>, <%= city&.region&.name %>
|
||||
<%= "#{city&.state&.name}, " if city&.state&.name %><%= city&.country&.name %>, <%= city&.region&.name %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -51,7 +51,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<%= city.latest_weather_art.weather_date.strftime("%b %d, %Y") %>
|
||||
<%= city.latest_weather_art.formatted_time(:date) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -74,8 +74,8 @@
|
||||
<!-- 按钮 -->
|
||||
<div class="card-actions justify-end">
|
||||
<%= link_to city_path(city),
|
||||
class: "btn btn-primary btn-sm gap-2" do %>
|
||||
View Details
|
||||
class: "btn btn-primary btn-sm gap-2", data: { turbo_frame: "_top" } do %>
|
||||
<%= t("button.view_detail") %>
|
||||
<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>
|
||||
@ -92,7 +92,7 @@
|
||||
<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>
|
||||
<%= city&.country&.name %>, <%= city&.region&.name %>
|
||||
<%= "#{city&.state&.name}, " if city&.state&.name %><%= city&.country&.name %>, <%= city&.region&.name %>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-200 rounded-lg p-4 mb-4">
|
||||
@ -114,8 +114,8 @@
|
||||
|
||||
<div class="card-actions justify-end">
|
||||
<%= link_to city_path(city),
|
||||
class: "btn btn-primary btn-sm gap-2" do %>
|
||||
View Details
|
||||
class: "btn btn-primary btn-sm gap-2", data: { turbo_frame: "_top" } do %>
|
||||
<%= t("button.view_detail") %>
|
||||
<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>
|
||||
|
24
app/views/cities/_results.html.erb
Normal file
24
app/views/cities/_results.html.erb
Normal file
@ -0,0 +1,24 @@
|
||||
<!-- app/views/cities/_results.html.erb -->
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<% if cities.empty? %>
|
||||
<div class="text-center py-16">
|
||||
<div class="text-base-content/50">
|
||||
<svg class="w-16 h-16 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
|
||||
</svg>
|
||||
<h3 class="text-xl font-semibold mb-2"><%= t('.no_results_title') %></h3>
|
||||
<p class="text-base-content/70">
|
||||
<%= t('.no_results_message') %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<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' %>
|
||||
<% end %>
|
||||
</div>
|
52
app/views/cities/_search_city.html.erb
Normal file
52
app/views/cities/_search_city.html.erb
Normal file
@ -0,0 +1,52 @@
|
||||
<div class="mt-8 mb-2 max-w-2xl mx-auto">
|
||||
<%= form_with url: cities_path, method: :get,
|
||||
class: "relative",
|
||||
data: {
|
||||
controller: "search",
|
||||
turbo_frame: "cities_results",
|
||||
turbo_action: "advance"
|
||||
} do |f| %>
|
||||
<div class="relative">
|
||||
<!-- 搜索图标 -->
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-4 z-10">
|
||||
<svg class="w-5 h-5 text-base-content/70" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 搜索输入框 -->
|
||||
<%= f.text_field :query,
|
||||
value: params[:query] ? URI.decode_www_form_component(params[:query]) : nil,
|
||||
class: "w-full pl-12 pr-12 py-3 rounded-full bg-base-200/80 backdrop-blur border border-base-300 focus:outline-none focus:ring-2 focus:ring-primary/50 transition",
|
||||
placeholder: t("text.search_cities"),
|
||||
autocomplete: "off",
|
||||
data: {
|
||||
action: "input->search#submit",
|
||||
search_target: "input"
|
||||
} %>
|
||||
|
||||
<!-- 右侧按钮区域(清除按钮或加载动画) -->
|
||||
<!-- 更简单的环形 loading 图标版本 -->
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-4 z-10"
|
||||
data-search-target="statusIcon">
|
||||
<% if params[:query].present? %>
|
||||
<%= link_to cities_path,
|
||||
class: "text-base-content/50 hover:text-base-content transition",
|
||||
data: { search_target: "clearButton" } do %>
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<!-- 简单的环形 loading -->
|
||||
<div class="hidden" data-search-target="spinner">
|
||||
<div class="w-5 h-5 border-2 border-base-content/20 border-t-base-content/70 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<%= f.hidden_field :region, value: params[:region] if params[:region] %>
|
||||
<%= f.hidden_field :country, value: params[:country] if params[:country] %>
|
||||
<% end %>
|
||||
</div>
|
13
app/views/cities/_stat_card.html.erb
Normal file
13
app/views/cities/_stat_card.html.erb
Normal file
@ -0,0 +1,13 @@
|
||||
<!-- app/views/cities/_stat_card.html.erb -->
|
||||
<div class="stat bg-base-100 shadow-lg rounded-box hover:bg-base-300 transition-all duration-300">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<%= weather_stat_icon(stat[:icon]) %>
|
||||
<div class="stat-title font-medium"><%= stat[:title] %></div>
|
||||
</div>
|
||||
<div class="stat-value text-base overflow-x-auto whitespace-nowrap scrollbar-thin scrollbar-thumb-base-300 scrollbar-track-base-100">
|
||||
<div class="min-w-min pr-2">
|
||||
<%= stat[:value] %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-desc mt-1"><%= stat[:desc] %></div>
|
||||
</div>
|
@ -1,12 +1,12 @@
|
||||
<!-- app/views/cities/index.html.erb -->
|
||||
<div class="min-h-screen">
|
||||
<!-- 页面标题和背景 -->
|
||||
<% featured_art = WeatherArt.includes(:city).joins(:image_attachment).order(created_at: :desc).first %>
|
||||
<% featured_art = @latest_arts.first %>
|
||||
<div class="relative bg-base-100">
|
||||
<!-- 背景图像和渐变 -->
|
||||
<% if featured_art&.image&.attached? %>
|
||||
<div class="absolute inset-0 h-[60vh] overflow-hidden">
|
||||
<%= image_tag featured_art.image,
|
||||
<%= image_tag featured_art.webp_image.processed,
|
||||
class: "w-full h-full object-cover object-center" %>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-base-100/40 via-base-100/80 to-base-100"></div>
|
||||
</div>
|
||||
@ -16,27 +16,31 @@
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-3xl mx-auto text-center space-y-6">
|
||||
<h1 class="text-5xl md:text-6xl font-display font-bold leading-tight">
|
||||
Explore Cities
|
||||
<%= t("cities.title") %>
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl text-base-content/70 font-light max-w-2xl mx-auto">
|
||||
Discover AI-generated weather art from cities around the world
|
||||
<%= t("arts.subtitle") %>
|
||||
</p>
|
||||
|
||||
<!-- 特色图片信息 -->
|
||||
<% if featured_art %>
|
||||
<div class="inline-block mt-6 px-4 py-2 bg-base-100/80 backdrop-blur-sm rounded-full text-sm">
|
||||
Latest from
|
||||
<%= t("text.latest_from") %>
|
||||
<span class="font-semibold"><%= featured_art.city.name %></span>,
|
||||
<%= featured_art.city.country.name %>
|
||||
<%= featured_art.city.country.localized_name %>
|
||||
<span class="mx-2">•</span>
|
||||
<%= featured_art.weather_date.strftime("%B %d, %Y") %>
|
||||
<%= featured_art.formatted_time(:date) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render 'cities/search_city' %>
|
||||
|
||||
</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="container mx-auto px-4">
|
||||
<div class="py-3 flex items-center justify-between gap-4">
|
||||
@ -46,7 +50,7 @@
|
||||
<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' %>
|
||||
<%= @current_region&.localized_name || t("text.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>
|
||||
@ -55,13 +59,13 @@
|
||||
<li>
|
||||
<%= link_to cities_path,
|
||||
class: "#{@current_region ? '' : 'active'}" do %>
|
||||
All Regions
|
||||
<%= t("text.all_regions") %>
|
||||
<% end %>
|
||||
</li>
|
||||
<div class="divider my-1"></div>
|
||||
<% @regions.each do |region| %>
|
||||
<li>
|
||||
<%= link_to region.name,
|
||||
<%= link_to region.localized_name,
|
||||
cities_path(region: region.slug),
|
||||
class: "#{@current_region == region ? 'active' : ''}" %>
|
||||
</li>
|
||||
@ -75,21 +79,21 @@
|
||||
<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 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
|
||||
</svg>
|
||||
<%= @current_country&.name || "All Countries" %>
|
||||
<%= @current_country&.localized_name || t("text.all_countries") %>
|
||||
<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 max-h-80 overflow-y-auto flex-nowrap">
|
||||
<li>
|
||||
<%= link_to "All in #{@current_region.name}",
|
||||
<%= link_to "#{t("text.all_in")} #{@current_region.localized_name}",
|
||||
cities_path(region: @current_region.slug),
|
||||
class: "#{@current_country ? '' : 'active'}" %>
|
||||
</li>
|
||||
<div class="divider my-1"></div>
|
||||
<% @current_region.countries.order(:name).each do |country| %>
|
||||
<li>
|
||||
<%= link_to country.name,
|
||||
<%= link_to "#{country&.emoji + " " || ""}#{country.localized_name}",
|
||||
cities_path(region: @current_region.slug, country: country.slug),
|
||||
class: "#{@current_country == country ? 'active' : ''}" %>
|
||||
</li>
|
||||
@ -99,28 +103,20 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-base-content/70">
|
||||
<div class="text-sm text-base-content/70 hidden">
|
||||
<%= @cities.count %> <%= 'city'.pluralize(@cities.count) %>
|
||||
<% if @current_country %>
|
||||
in <%= @current_country.name %>
|
||||
in <%= @current_country.localized_name %>
|
||||
<% elsif @current_region %>
|
||||
in <%= @current_region.name %>
|
||||
in <%= @current_region.localized_name %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<%= turbo_frame_tag "cities_results" do %>
|
||||
<%= render "cities/results", cities: @cities %>
|
||||
<% end %>
|
||||
|
||||
<%= render 'shared/pagination',
|
||||
collection: @cities,
|
||||
collection_name: 'cities' %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
@ -1,151 +1,124 @@
|
||||
<div class="relative min-h-screen bg-base-200">
|
||||
<!-- 背景效果 -->
|
||||
<div class="min-h-screen bg-base-100">
|
||||
<!-- 页面标题和背景 -->
|
||||
<div class="relative">
|
||||
<!-- 背景图像和渐变 -->
|
||||
<% if @city.latest_weather_art&.image&.attached? %>
|
||||
<div class="fixed inset-0 -z-10">
|
||||
<%= image_tag @city.latest_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 class="absolute inset-0 h-[60vh] overflow-hidden">
|
||||
<%= image_tag @city.latest_weather_art.webp_image.processed,
|
||||
class: "w-full h-full object-cover object-center" %>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-base-100/40 via-base-100/80 to-base-100"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="relative z-10">
|
||||
<!-- 返回导航 -->
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
<div class="relative max-w-6xl mx-auto px-4 pt-4 ">
|
||||
<%= link_to cities_path,
|
||||
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 %>
|
||||
class: "inline-flex items-center btn btn-ghost gap-2 " 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
|
||||
<%= t("button.back_to_cities") %>
|
||||
<% end %>
|
||||
</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">
|
||||
<div class="relative pt-8 pb-8">
|
||||
<div class="container mx-auto px-4">
|
||||
|
||||
<div class="max-w-6xl mx-auto text-center space-y-6">
|
||||
<h1 class="text-5xl md:text-6xl font-display font-bold leading-tight">
|
||||
<%= @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 %>
|
||||
<%= "#{@city&.country&.emoji + " " || ""}#{@city&.country&.name}" %>
|
||||
</div>
|
||||
<div class="badge badge-lg badge-secondary gap-2">
|
||||
<%= @city.timezone.present? ? Time.current.in_time_zone(@city.timezone).strftime("%Y-%m-%d %H:%M") : "Timezone undefined" %>
|
||||
<%= @city&.state&.name %>
|
||||
</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>
|
||||
<%= render partial: 'cities/stat_card', collection: [
|
||||
{
|
||||
icon: 'temperature',
|
||||
title: 'Latest Weather',
|
||||
value: "#{@city.latest_weather_art&.temperature}°C",
|
||||
desc: @city.latest_weather_art&.description
|
||||
},
|
||||
{
|
||||
icon: 'location',
|
||||
title: 'Coordinates',
|
||||
value: "#{@city.latitude}°N, #{@city.longitude}°E",
|
||||
desc: 'Geographical Location'
|
||||
},
|
||||
{
|
||||
icon: 'history',
|
||||
title: 'Records',
|
||||
value: @city.weather_arts.count,
|
||||
desc: 'Total Weather Arts'
|
||||
}
|
||||
], as: :stat %>
|
||||
</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 class="card bg-base-100 backdrop-blur-md shadow-lg border border-primary/20 overflow-hidden">
|
||||
<%= render 'shared/map', city: @city %>
|
||||
</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-value text-2xl"><%= @city.weather_arts.count %></div>
|
||||
<div class="stat-desc mt-1">Total Weather Arts</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 backdrop-blur-md shadow-lg overflow-hidden">
|
||||
<%= render 'shared/auto_ad' %>
|
||||
</div>
|
||||
|
||||
<%= render 'cities/admin_panel' %>
|
||||
|
||||
<div class="card bg-base-100 backdrop-blur-md shadow-lg border border-primary/20 overflow-hidden">
|
||||
<%
|
||||
# 构建更吸引人的分享标题
|
||||
share_title =
|
||||
"🎨 #{@city.full_name}'s Weather Transformed into Art"
|
||||
|
||||
# 构建更有描述性的分享描述
|
||||
share_description = [
|
||||
"Discover this stunning AI-generated weather art!",
|
||||
"in #{@city.full_name}.",
|
||||
"Visit TodayAIWeather to see more amazing weather art."
|
||||
].join(" ")
|
||||
%>
|
||||
<%= render "shared/share_social",
|
||||
title: share_title,
|
||||
description: share_description,
|
||||
tags: "AIWeather,Art,AIart,Weather,#{@city&.name},#{@city&.country&.name}",
|
||||
image: url_for(@city&.latest_weather_art&.webp_image&.processed)
|
||||
%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 天气艺术历史记录 -->
|
||||
<div class="container mx-auto px-4 pb-16">
|
||||
<div class="max-w-7xl mx-auto space-y-8">
|
||||
<div class="bg-base-100">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-6xl 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">
|
||||
<h2 class="text-3xl font-display font-bold">Weather Art History</h2>
|
||||
<div class="card bg-base-100 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>
|
||||
<span>Last Updated: <%= 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-6">
|
||||
<% @city.weather_arts.order(weather_date: :desc).each do |art| %>
|
||||
<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" %>
|
||||
<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-right">
|
||||
<div class="font-medium"><%= art.created_at.strftime("%H:%M") %></div>
|
||||
<div class="text-sm opacity-80"><%= art.weather_date.strftime("%B %d, %Y") %></div>
|
||||
<%= render partial: 'weather_arts/card', collection: @arts, as: :weather_art %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</figure>
|
||||
|
||||
<div class="card-body">
|
||||
<h3 class="card-title font-display">
|
||||
<%= weather_description_icon(art.description) %>
|
||||
<%= art.description %>
|
||||
</h3>
|
||||
|
||||
<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 text-sm">
|
||||
<%= weather_stat_icon("wind") %>
|
||||
<span>Wind: <%= art.wind_scale %></span>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
29
app/views/home/_arts.html.erb
Normal file
29
app/views/home/_arts.html.erb
Normal file
@ -0,0 +1,29 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<% arts.each do |art| %>
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow duration-300">
|
||||
<figure class="relative aspect-[4/3] overflow-hidden">
|
||||
<% if art.image.attached? %>
|
||||
<%= image_tag art.preview_image.processed, class: "w-full h-full object-cover transform hover:scale-105 transition-transform duration-500" %>
|
||||
<% end %>
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="card-title font-display"><%= art.city.name %></h3>
|
||||
<p class="text-base-content/70">
|
||||
<%= art.formatted_time(:date) %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-2xl font-bold"><%= art.temperature %>°C</div>
|
||||
<div class="text-sm text-base-content/70"><%= art.description %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<%= link_to t("button.view_detail"), city_weather_art_path(art.city, art),
|
||||
class: "btn btn-primary btn-outline" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
@ -1,9 +1,9 @@
|
||||
<div>
|
||||
<!-- 首屏展示区 -->
|
||||
<section class="h-screen-90 relative overflow-hidden">
|
||||
<% if @featured_arts.first&.image&.attached? %>
|
||||
<% if @latest_arts.first&.image&.attached? %>
|
||||
<div class="absolute inset-0">
|
||||
<%= image_tag @featured_arts.first.image, class: "w-full h-full object-cover" %>
|
||||
<%= image_tag @latest_arts&.first&.webp_image&.processed, class: "w-full h-full object-cover" %>
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-base-100/90 to-base-100/50"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
@ -11,13 +11,12 @@
|
||||
<div class="container mx-auto px-4 h-full flex items-center relative">
|
||||
<div class="max-w-2xl space-y-6">
|
||||
<h1 class="text-5xl md:text-6xl font-display font-bold leading-tight">
|
||||
Where Weather Meets<br>Artificial Intelligence
|
||||
<%= t("home.headline_html") %>
|
||||
</h1>
|
||||
<p class="text-xl text-base-content/70 font-sans">
|
||||
Experience weather through the lens of AI-generated art,
|
||||
bringing a new perspective to daily meteorological phenomena.
|
||||
<%= t("home.subtitle") %>
|
||||
</p>
|
||||
<%= link_to "Explore Cities", cities_path,
|
||||
<%= link_to t("button.explore_cities"), cities_path,
|
||||
class: "btn btn-primary btn-lg mt-8 font-sans" %>
|
||||
</div>
|
||||
</div>
|
||||
@ -25,42 +24,17 @@
|
||||
|
||||
<!-- 最新天气艺术 -->
|
||||
<section class="container mx-auto px-4 py-16 space-y-12">
|
||||
<h2 class="text-3xl font-display font-bold text-center">Shuffle Latest Weather Art</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<% @latest_arts.each do |art| %>
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow duration-300">
|
||||
<figure class="relative aspect-[4/3] 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 %>
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="card-title font-display"><%= art.city.name %></h3>
|
||||
<p class="text-base-content/70">
|
||||
<%= art.weather_date.strftime("%B %d, %Y") %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-2xl font-bold"><%= art.temperature %>°C</div>
|
||||
<div class="text-sm text-base-content/70"><%= art.description %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<%= link_to "View Details", city_weather_art_path(art.city, art),
|
||||
class: "btn btn-primary btn-outline" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<h2 class="text-3xl font-display font-bold text-center"><%= t("title.latest_weather_art") %></h2>
|
||||
<%= render 'home/arts', arts: @latest_arts %>
|
||||
<h2 class="text-3xl font-display font-bold text-center"><%= t("title.popular_weather_art") %></h2>
|
||||
<%= render 'home/arts', arts: @popular_arts %>
|
||||
<!-- <h2 class="text-3xl font-display font-bold text-center">Random Weather Art</h2>-->
|
||||
<%#= render 'home/arts', arts: @random_arts %>
|
||||
</section>
|
||||
</div>
|
||||
<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
|
||||
<%= t("button.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>
|
||||
|
35
app/views/layouts/_footer.html.erb
Normal file
35
app/views/layouts/_footer.html.erb
Normal file
@ -0,0 +1,35 @@
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer footer-center p-8 bg-base-200 text-base-content">
|
||||
<div class="container mx-auto flex flex-col gap-4">
|
||||
<!-- 第一行:功能按钮 -->
|
||||
<div class="flex items-center justify-center space-x-4">
|
||||
<%= link_to rss_feed_path(format: :rss), class: "btn btn-ghost btn-sm", title: "RSS Feed" 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="M6 5c7.18 0 13 5.82 13 13M6 11a7 7 0 017 7m-6 0a1 1 0 11-2 0 1 1 0 012 0z" />
|
||||
</svg>
|
||||
<% end %>
|
||||
|
||||
<%= render 'shared/language_switcher' %>
|
||||
</div>
|
||||
|
||||
<!-- 第二行:统计信息 -->
|
||||
<div id="busuanzi_container" class="text-xs opacity-70">
|
||||
<div class="space-x-2">
|
||||
<span>Page: </span>
|
||||
<span>PV <span id="busuanzi_page_pv"></span></span>
|
||||
<span>UV <span id="busuanzi_page_uv"></span></span>
|
||||
<span>Site: </span>
|
||||
<span>PV <span id="busuanzi_site_pv"></span></span>
|
||||
<span>UV <span id="busuanzi_site_uv"></span></span>
|
||||
<span data-controller="page-load-time">
|
||||
Load Time: <span data-page-load-time-target="timer">x</span> ms
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第三行:版权信息 -->
|
||||
<p class="font-display opacity-80 text-sm">
|
||||
Copyright © <%= Time.current.year %> - All rights reserved by AI Weather Art
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
@ -1,58 +1,90 @@
|
||||
<div class="navbar bg-base-100/80 backdrop-blur-sm fixed top-0 z-50">
|
||||
<div class="container mx-auto">
|
||||
<div class="container mx-auto px-4">
|
||||
<!-- Logo -->
|
||||
<div class="flex-1">
|
||||
<%= link_to root_path, class: "text-2xl font-display font-bold hover:text-primary transition-colors" do %>
|
||||
Today AI Weather
|
||||
<%= link_to root_path, class: "text-xl md:text-2xl font-display font-bold hover:text-primary transition-colors" do %>
|
||||
<%= t('brand.name') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex-none gap-2">
|
||||
<%= link_to "Cities", cities_path, class: "btn btn-ghost font-sans" %>
|
||||
<%= link_to "Arts", arts_path, class: "btn btn-ghost font-sans" %>
|
||||
|
||||
<!-- Desktop Menu -->
|
||||
<div class="hidden md:flex flex-none gap-2 items-center">
|
||||
<%= link_to t("title.cities"), cities_path, class: "btn btn-ghost btn-sm font-sans" %>
|
||||
<%= link_to t("title.arts"), arts_path, class: "btn btn-ghost btn-sm font-sans" %>
|
||||
|
||||
<% if user_signed_in? %>
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-ghost">
|
||||
<label tabindex="0" class="btn btn-ghost btn-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span><%= current_user.email %></span>
|
||||
<span class="hidden lg:inline"><%= current_user.email %></span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
<%= render 'layouts/user_menu' %>
|
||||
</div>
|
||||
|
||||
<%= render 'shared/language_switcher' %>
|
||||
<% else %>
|
||||
<%= link_to new_user_session_path, class: "btn btn-primary btn-sm" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
|
||||
</svg>
|
||||
<span><%= t("title.sign_in") %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div class="flex md:hidden">
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-ghost btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<li><%= link_to t("title.cities"), cities_path %></li>
|
||||
<li><%= link_to t("title.arts"), arts_path %></li>
|
||||
<div class="divider my-1"></div>
|
||||
<% if user_signed_in? %>
|
||||
<li>
|
||||
<%= link_to edit_user_registration_path, class: "flex items-center gap-2 p-2" do %>
|
||||
<%= link_to edit_user_registration_path do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span>Settings</span>
|
||||
<%= t("title.settings") %>
|
||||
<% end %>
|
||||
</li>
|
||||
<div class="divider my-1"></div>
|
||||
<li>
|
||||
<%= button_to destroy_user_session_path,
|
||||
method: :delete,
|
||||
class: "flex items-center gap-2 p-2 w-full text-left hover:bg-base-200 rounded-lg" do %>
|
||||
class: "flex items-center gap-2 w-full p-2" do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
||||
</svg>
|
||||
<span>Sign out</span>
|
||||
<span><% t("title.sign_out") %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= link_to new_user_session_path, class: "btn btn-primary" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<li>
|
||||
<%= link_to new_user_session_path do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
|
||||
</svg>
|
||||
<span>Sign in</span>
|
||||
<%= t("title.sign_in") %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
35
app/views/layouts/_user_menu.html.erb
Normal file
35
app/views/layouts/_user_menu.html.erb
Normal file
@ -0,0 +1,35 @@
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<li>
|
||||
<%= link_to edit_user_registration_path, class: "flex items-center gap-2 p-2" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span><%= t("title.settings") %></span>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
<% if current_user.admin? %>
|
||||
<li>
|
||||
<%= link_to admin_root_path, class: "flex items-center gap-2 p-2" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V19.5a2.25 2.25 0 002.25 2.25h.75m0-3H21m-3.75 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z" />
|
||||
</svg>
|
||||
<span><%= t("title.admin_dashboard") %></span>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
<div class="divider my-1"></div>
|
||||
|
||||
<li>
|
||||
<%= button_to destroy_user_session_path,
|
||||
method: :delete,
|
||||
class: "flex items-center gap-2 p-2 w-full text-left hover:bg-base-200 rounded-lg" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
||||
</svg>
|
||||
<span><%= t("title.sign_out") %></span>
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="<%= I18n.locale %>">
|
||||
<head>
|
||||
<title><%= content_for(:title) || "Today Ai Weather" %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
@ -11,11 +11,14 @@
|
||||
og: {
|
||||
site_name: 'TodayAIWeather',
|
||||
type: 'website',
|
||||
keywords: "ai, ai web, ai art, ai weather, weather art, AI visualization, today ai weather",
|
||||
url: request.original_url
|
||||
},
|
||||
alternate: {
|
||||
"en" => url_for(locale: 'en'),
|
||||
"zh-CN" => url_for(locale: 'zh-CN'),
|
||||
"en" => url_for(locale: 'en')
|
||||
"ja" => url_for(locale: 'ja'),
|
||||
"ko" => url_for(locale: 'ko')
|
||||
}
|
||||
) %>
|
||||
<%= csrf_meta_tags %>
|
||||
@ -33,6 +36,8 @@
|
||||
<%# 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" %>
|
||||
<%= auto_discovery_link_tag :rss, rss_feed_url(format: :rss), title: 'RSS Feed' %>
|
||||
|
||||
<script defer data-domain="todayaiweather.com" src="https://plausible.frytea.com/js/script.js"></script>
|
||||
|
||||
<script defer src="https://busuanzi.frytea.com/js"></script>
|
||||
@ -64,26 +69,7 @@
|
||||
<%= yield %>
|
||||
</main>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<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>
|
||||
<%= render 'layouts/footer' %>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
24
app/views/rss/feed.rss.builder
Normal file
24
app/views/rss/feed.rss.builder
Normal file
@ -0,0 +1,24 @@
|
||||
# app/views/rss/feed.rss.builder
|
||||
xml.instruct! :xml, version: "1.0"
|
||||
xml.rss version: "2.0",
|
||||
"xmlns:atom" => "http://www.w3.org/2005/Atom" do
|
||||
xml.channel do
|
||||
xml.title "Today AI Weather Art"
|
||||
xml.description "Daily AI-generated weather art and forecasts"
|
||||
xml.link root_url
|
||||
xml.language "en"
|
||||
xml.atom :link, href: rss_feed_url(format: :rss), rel: "self", type: "application/rss+xml"
|
||||
|
||||
@weather_arts.each do |art|
|
||||
xml.item do
|
||||
xml.title "#{art.city.full_name} Weather Art"
|
||||
xml.description art.description
|
||||
xml.pubDate art.created_at.to_fs(:rfc822)
|
||||
xml.link city_weather_art_url(art.city, art)
|
||||
xml.guid city_weather_art_url(art.city, art)
|
||||
# 如果有图片,添加图片链接
|
||||
xml.enclosure url: rails_blob_url(art.webp_image.processed), type: "image/jpeg" if art.image.attached?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
10
app/views/shared/_auto_ad.html.erb
Normal file
10
app/views/shared/_auto_ad.html.erb
Normal file
@ -0,0 +1,10 @@
|
||||
<!-- today_ai_weather -->
|
||||
<ins class="adsbygoogle"
|
||||
style="display:block"
|
||||
data-ad-client="ca-pub-7296634171837358"
|
||||
data-ad-slot="7447936130"
|
||||
data-ad-format="auto"
|
||||
data-full-width-responsive="true"></ins>
|
||||
<script>
|
||||
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||
</script>
|
17
app/views/shared/_language_switcher.html.erb
Normal file
17
app/views/shared/_language_switcher.html.erb
Normal file
@ -0,0 +1,17 @@
|
||||
<%# app/views/shared/_language_switcher.html.erb %>
|
||||
<div class="dropdown dropdown-top">
|
||||
<label tabindex="0" class="btn btn-ghost btn-sm">
|
||||
<%= t("language.#{I18n.locale}") %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 max-h-80 overflow-y-auto flex-nowrap shadow bg-base-100 rounded-box w-32">
|
||||
<% I18n.available_locales.each do |locale| %>
|
||||
<%= link_to url_for(locale: locale),
|
||||
class: "px-4 py-2 hover:bg-base-200 rounded-lg #{I18n.locale == locale ? 'bg-base-200' : ''}" do %>
|
||||
<%= t("language.#{locale}") %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
16
app/views/shared/_map.html.erb
Normal file
16
app/views/shared/_map.html.erb
Normal file
@ -0,0 +1,16 @@
|
||||
<!-- 插入到 "主要统计信息" 的下方 -->
|
||||
<div class="container mx-auto px-2 py-2">
|
||||
<div class="max-w-7xl mx-auto bg-base-100 rounded-2xl shadow-xl overflow-hidden">
|
||||
<!-- <h3 class="text-2xl font-display font-bold p-6 bg-base-200"><%#= t('city.location_on_globe') %></h3>-->
|
||||
<div
|
||||
data-controller="map"
|
||||
data-map-latitude-value="<%= @city.latitude %>"
|
||||
data-map-longitude-value="<%= @city.longitude %>"
|
||||
data-map-token-value="<%= Rails.application.credentials.dig(:mapbox, :token) %>"
|
||||
data-map-weather-art-value="<%= @city.latest_weather_art.to_json %>"
|
||||
data-map-weather-art-url-value="<%= rails_blob_url(@city&.latest_weather_art&.webp_image&.processed ) if @city&.latest_weather_art&.image&.attached? %>"
|
||||
class="h-[500px] w-full rounded-b-2xl z-10"
|
||||
style="touch-action: none"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
@ -78,9 +78,12 @@
|
||||
|
||||
<!-- 结果统计 -->
|
||||
<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' %>
|
||||
<%= t('pagination.showing_items',
|
||||
from: collection.offset_value + 1,
|
||||
to: collection.last_page? ? collection.total_count : collection.offset_value + collection.limit_value,
|
||||
total: collection.total_count,
|
||||
items: t("pagination.items.#{collection_name}", default: t('pagination.items.default'))
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
74
app/views/shared/_share_social.html.erb
Normal file
74
app/views/shared/_share_social.html.erb
Normal file
@ -0,0 +1,74 @@
|
||||
<!-- app/views/shared/_share_social.html.erb -->
|
||||
<div class="card bg-base-100 p-4 rounded-2xl shadow-xl overflow-hidden"
|
||||
data-controller="share">
|
||||
<h3 class="font-display text-base font-medium center mb-4">Share This Page</h3>
|
||||
|
||||
<div class="flex flex-wrap gap-4 justify-center">
|
||||
<!-- Facebook -->
|
||||
<button class="btn btn-primary"
|
||||
data-sharer="facebook"
|
||||
data-title="<%= title %>"
|
||||
data-url="<%= request.original_url %>">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z"/>
|
||||
</svg>
|
||||
Facebook
|
||||
</button>
|
||||
|
||||
<!-- Twitter/X -->
|
||||
<button class="btn btn-info"
|
||||
data-sharer="x"
|
||||
data-title="<%= title %>"
|
||||
data-hashtags=<%= tags %>
|
||||
data-url="<%= request.original_url %>">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||
</svg>
|
||||
X/Twitter
|
||||
</button>
|
||||
|
||||
<!-- LinkedIn -->
|
||||
<button class="btn btn-secondary"
|
||||
data-sharer="linkedin"
|
||||
data-url="<%= request.original_url %>">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
LinkedIn
|
||||
</button>
|
||||
|
||||
<!-- Pinterest -->
|
||||
<button class="btn btn-accent"
|
||||
data-sharer="pinterest"
|
||||
data-url="<%= request.original_url %>"
|
||||
data-image="<%= image %>"
|
||||
data-description="<%= description %>">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0c-6.627 0-12 5.372-12 12 0 5.084 3.163 9.426 7.627 11.174-.105-.949-.2-2.405.042-3.441.218-.937 1.407-5.965 1.407-5.965s-.359-.719-.359-1.782c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738.098.119.112.224.083.345l-.333 1.36c-.053.22-.174.267-.402.161-1.499-.698-2.436-2.889-2.436-4.649 0-3.785 2.75-7.262 7.929-7.262 4.163 0 7.398 2.967 7.398 6.931 0 4.136-2.607 7.464-6.227 7.464-1.216 0-2.359-.631-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146 1.124.347 2.317.535 3.554.535 6.627 0 12-5.373 12-12 0-6.628-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
Pinterest
|
||||
</button>
|
||||
|
||||
<!-- WhatsApp -->
|
||||
<button class="btn btn-success"
|
||||
data-sharer="whatsapp"
|
||||
data-title="<%= title %>"
|
||||
data-url="<%= request.original_url %>">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"/>
|
||||
</svg>
|
||||
WhatsApp
|
||||
</button>
|
||||
|
||||
<!-- Telegram -->
|
||||
<button class="btn btn-info"
|
||||
data-sharer="telegram"
|
||||
data-title="<%= title %>"
|
||||
data-url="<%= request.original_url %>">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
|
||||
</svg>
|
||||
Telegram
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
43
app/views/sitemaps/index.html.erb
Normal file
43
app/views/sitemaps/index.html.erb
Normal file
@ -0,0 +1,43 @@
|
||||
<%# app/views/sitemaps/index.html.erb %>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h1 class="text-3xl font-bold mb-6">Sitemaps Index</h1>
|
||||
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<th>Last Modified</th>
|
||||
<th>Size</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @sitemaps.each do |sitemap| %>
|
||||
<tr class="hover">
|
||||
<td><%= sitemap[:key] %></td>
|
||||
<td><%= sitemap[:last_modified].strftime("%Y-%m-%d %H:%M:%S") %></td>
|
||||
<td><%= number_to_human_size(sitemap[:size]) %></td>
|
||||
<td>
|
||||
<%= link_to "View", sitemap[:url],
|
||||
class: "btn btn-sm btn-primary",
|
||||
target: "_blank" %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 bg-base-200 p-4 rounded-lg">
|
||||
<h2 class="text-xl font-semibold mb-2">For Search Engines</h2>
|
||||
<p class="mb-2">Sitemap Index URL:</p>
|
||||
<code class="block bg-base-300 p-2 rounded">
|
||||
<%= sitemaps_url(format: :xml) %>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
44
app/views/weather_arts/_card.html.erb
Normal file
44
app/views/weather_arts/_card.html.erb
Normal file
@ -0,0 +1,44 @@
|
||||
<!-- app/views/weather_arts/_card.html.erb -->
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1">
|
||||
<figure class="relative aspect-video overflow-hidden">
|
||||
<% if weather_art.image.attached? %>
|
||||
<%= image_tag weather_art.preview_image.processed,
|
||||
class: "w-full h-full object-cover transform hover:scale-105 transition-transform duration-500" %>
|
||||
<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"><%= weather_art.temperature %>°C</div>
|
||||
<div class="text-right">
|
||||
<div class="font-medium"><%= weather_art.formatted_time(:date) %></div>
|
||||
<div class="text-sm opacity-80"><%= weather_art.formatted_time(:time, true) %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</figure>
|
||||
|
||||
<div class="card-body">
|
||||
<h3 class="card-title font-display">
|
||||
<%= weather_description_icon(weather_art.description) %>
|
||||
<%= weather_art.description %>
|
||||
</h3>
|
||||
|
||||
<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: <%= weather_art.humidity %>%</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<%= weather_stat_icon("wind") %>
|
||||
<span>Wind: <%= weather_art.wind_scale %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= link_to city_weather_art_path(weather_art.city, weather_art),
|
||||
class: "btn btn-primary btn-block" do %>
|
||||
<%= t("button.view_detail") %>
|
||||
<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>
|
@ -1,37 +1,37 @@
|
||||
<%# Partial _weather_stats.html.erb %>
|
||||
|
||||
<div class="stat bg-gradient-to-br from-primary/10 to-primary/20 hover:from-primary hover:to-primary/30 p-4 rounded-lg">
|
||||
<div class="stat-title font-medium text-base">Temperature</div>
|
||||
<div class="stat-title font-medium text-base"><%= t("card.temperature") %></div>
|
||||
<div class="stat-value text-3xl"><%= weather_art.temperature %>°C</div>
|
||||
<div class="stat-desc">Feels like <%= weather_art.feeling_temp %>°C</div>
|
||||
<div class="stat-desc"><%= t("card.feel_like") %> <%= weather_art.feeling_temp %>°C</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-gradient-to-br from-secondary/10 to-secondary/20 hover:from-secondary hover:to-secondary/30 p-4 rounded-lg">
|
||||
<div class="stat-title font-medium text-base">Wind</div>
|
||||
<div class="stat-title font-medium text-base"><%= t("card.wind") %></div>
|
||||
<div class="stat-value text-3xl"><%= weather_art.wind_scale %></div>
|
||||
<div class="stat-desc"><%= weather_art.wind_speed %> km/h</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-300 hover:bg-base-400 p-4 rounded-lg">
|
||||
<div class="stat-title font-medium text-base">Humidity</div>
|
||||
<div class="stat-title font-medium text-base"><%= t("card.humidity") %></div>
|
||||
<div class="stat-value text-3xl"><%= weather_art.humidity %>%</div>
|
||||
<div class="stat-desc">Relative humidity</div>
|
||||
<div class="stat-desc"><%= t("card.relative_humidity") %></div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-300 hover:bg-base-400 p-4 rounded-lg">
|
||||
<div class="stat-title font-medium text-base">Visibility</div>
|
||||
<div class="stat-title font-medium text-base"><%= t("card.visibility") %></div>
|
||||
<div class="stat-value text-3xl"><%= weather_art.visibility %> km</div>
|
||||
<div class="stat-desc">Clear view distance</div>
|
||||
<div class="stat-desc"><%= t("card.clear_view_distance") %></div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-accent/10 hover:bg-accent p-4 rounded-lg">
|
||||
<div class="stat-title font-medium text-base">Pressure</div>
|
||||
<div class="stat-title font-medium text-base"><%= t("card.pressure") %></div>
|
||||
<div class="stat-value text-3xl"><%= weather_art.pressure %> hPa</div>
|
||||
<div class="stat-desc">Atmospheric pressure</div>
|
||||
<div class="stat-desc"><%= t("card.atmospheric_pressure") %></div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-200 hover:bg-base-100 p-4 rounded-lg">
|
||||
<div class="stat-title font-medium text-base">Cloud Cover</div>
|
||||
<div class="stat-title font-medium text-base"><%= t("card.cloud_cover") %></div>
|
||||
<div class="stat-value text-3xl"><%= weather_art.cloud %>%</div>
|
||||
<div class="stat-desc">Sky coverage</div>
|
||||
<div class="stat-desc"><%= t("card.sky_coverage") %></div>
|
||||
</div>
|
@ -4,117 +4,124 @@
|
||||
</script>
|
||||
<% end %>
|
||||
|
||||
<div class="relative min-h-screen bg-white"> <!-- 使用更明快的背景颜色 -->
|
||||
<div class="container mx-auto px-4 pt-12 pb-16">
|
||||
<div class="max-w-6xl mx-auto space-y-6">
|
||||
<!-- 返回导航 -->
|
||||
<div class="flex items-center">
|
||||
<div class="min-h-screen bg-base-100">
|
||||
<div class="container mx-auto px-4 md:px-6 pt-8 pb-16">
|
||||
<!-- 返回按钮 -->
|
||||
<%= link_to city_path(@weather_art.city),
|
||||
class: "btn btn-ghost btn-md gap-2 bg-base-200 hover:bg-base-300 transition-all duration-300" do %>
|
||||
class: "btn btn-ghost btn-md gap-2 bg-base-200 hover:bg-base-300 transition-all duration-300 mb-4" 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 <%= @weather_art.city.name %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="card bg-base-200/80 backdrop-blur-md shadow-lg overflow-hidden"> <!-- 调整透明度和阴影 -->
|
||||
<div class="grid lg:grid-cols-2 gap-6 items-center">
|
||||
|
||||
<!-- 图片区域 -->
|
||||
<% if @weather_art.image.attached? %>
|
||||
<figure class="relative lg:h-[30rem] h-auto overflow-hidden rounded-lg"> <!-- 添加圆角 -->
|
||||
<div class="gallery" data-controller="photo-swipe-lightbox">
|
||||
<div data-photo-swipe-lightbox-target="gallery" class="h-full">
|
||||
<% blob = @weather_art.image_blob %>
|
||||
<%= link_to rails_blob_path(blob),
|
||||
data: {
|
||||
pswp_src: rails_blob_url(blob),
|
||||
pswp_caption: 'Weather Art',
|
||||
pswp_width: 1792,
|
||||
pswp_height: 1024
|
||||
} do %>
|
||||
<%= image_tag @weather_art.image, class: "object-cover w-full h-full transition-transform transform hover:scale-105 ease-in-out" %>
|
||||
<%#= image_tag @weather_art.watermarked_variant.processed , class: "object-cover w-full h-full transition-transform transform hover:scale-105 ease-in-out" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
<%= "#{t("button.back_to")} #{@weather_art.city.name}" %>
|
||||
<% end %>
|
||||
|
||||
<!-- 信息区域 -->
|
||||
<div class="card-body p-8 lg:py-10 lg:px-12">
|
||||
<div class="prose max-w-none">
|
||||
<h1 class="font-display text-4xl md:text-5xl font-bold text-gradient mb-6">
|
||||
<!-- 标题区域 -->
|
||||
<div class="max-w-6xl mx-auto mb-8">
|
||||
<h1 class="font-display text-4xl md:text-5xl font-bold text-gradient mb-4">
|
||||
<%= @weather_art.city.full_name %> Weather Art
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-wrap gap-4 mb-6">
|
||||
<div class="badge badge-lg badge-primary">
|
||||
<%= @weather_art.weather_date.strftime("%B %d, %Y") %>
|
||||
<%= "#{@weather_art&.city&.country&.emoji + " " || ""}#{@city&.country&.localized_name}" %>
|
||||
</div>
|
||||
<div class="badge badge-lg badge-secondary">
|
||||
<%= @weather_art.created_at.strftime("%H:%M") %>
|
||||
<%= @weather_art&.city&.state&.name %>
|
||||
</div>
|
||||
<div class="badge badge-lg badge-ghost">
|
||||
<%= @weather_art.formatted_time(:date) %>
|
||||
</div>
|
||||
<div class="badge badge-lg badge-ghost">
|
||||
<%= @weather_art.formatted_time(:time, true) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="text-2xl font-semibold mb-4">
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 艺术图像区域 - 移动端全宽显示 -->
|
||||
<% if @weather_art.image.attached? %>
|
||||
<div class="md:rounded-xl overflow-hidden mb-8 -mx-4 md:mx-0"> <!-- 负margin实现移动端全宽 -->
|
||||
<div class="gallery" data-controller="photo-swipe-lightbox">
|
||||
<div data-photo-swipe-lightbox-target="gallery">
|
||||
<% watermarked = @weather_art.webp_image.processed %>
|
||||
<%= link_to rails_blob_path(watermarked),
|
||||
data: {
|
||||
pswp_src: rails_blob_url(watermarked),
|
||||
pswp_caption: 'Weather Art',
|
||||
pswp_width: 1792,
|
||||
pswp_height: 1024
|
||||
} do %>
|
||||
<%= image_tag @weather_art.preview_image(:big).processed,
|
||||
class: "w-full h-auto object-cover transition-transform hover:scale-105 duration-300 ease-in-out" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- 天气信息卡片 -->
|
||||
<div class="grid md:grid-cols-2 gap-6 mb-8">
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body">
|
||||
<h2 class="text-2xl font-semibold mb-4 flex items-center gap-2">
|
||||
<%= weather_description_icon(@weather_art.description) %>
|
||||
<%= @weather_art.description %>
|
||||
</h2>
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<%= render 'weather_stats', weather_art: @weather_art %> <!-- 使用局部渲染 -->
|
||||
</div>
|
||||
</div>
|
||||
<%= render 'weather_stats', weather_art: @weather_art %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Prompt -->
|
||||
<div class="bg-primary/10 backdrop-blur-md p-6 rounded-lg border border-primary/20">
|
||||
<!-- AI Prompt 卡片 -->
|
||||
<div class="card bg-primary/10 shadow-lg">
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<h3 class="font-display font-bold text-lg"><%= t("title.ai_prompt") %></h3>
|
||||
</div>
|
||||
<p class="text-base-content/80 leading-relaxed">
|
||||
<%= @weather_art.prompt %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 上一个和下一个导航 -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center gap-4 mt-8">
|
||||
<% if @previous_weather_art %>
|
||||
<%= link_to city_weather_art_path(@city, @previous_weather_art),
|
||||
class: "btn btn-outline btn-primary w-full sm:w-auto flex items-center justify-center gap-2" 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>
|
||||
Previous Weather Art
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if @next_weather_art %>
|
||||
<%= link_to city_weather_art_path(@city, @next_weather_art),
|
||||
class: "btn btn-outline btn-primary w-full sm:w-auto flex items-center justify-center gap-2" do %>
|
||||
Next Weather Art
|
||||
<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 %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @previous_weather_art.nil? && @next_weather_art.nil? %>
|
||||
<div class="text-center text-base-content/70 py-4">
|
||||
No more Weather Arts available
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- 地图区域 -->
|
||||
<div class="card bg-base-100 shadow-lg mb-8">
|
||||
<%= render 'shared/map', city: @weather_art.city %>
|
||||
</div>
|
||||
|
||||
<!-- 广告区域 -->
|
||||
<div class="card bg-base-100 shadow-lg mb-8">
|
||||
<%= render 'shared/auto_ad' %>
|
||||
</div>
|
||||
|
||||
<!-- 社交分享 -->
|
||||
<%
|
||||
share_title = [
|
||||
"🎨 Amazing AI Weather Art: #{@weather_art.city.full_name}",
|
||||
"#{@weather_art.description} at #{@weather_art.temperature}°C",
|
||||
"#{@weather_art.formatted_time(:all, true)}"
|
||||
].join("\n")
|
||||
|
||||
share_description = [
|
||||
"Discover this stunning AI-generated weather art!",
|
||||
"#{@weather_art.description} in #{@weather_art.city.full_name}.",
|
||||
"Created at #{@weather_art.formatted_time(:time, true)}",
|
||||
"Visit TodayAIWeather to see more amazing weather art."
|
||||
].join(" ")
|
||||
%>
|
||||
<%= render "shared/share_social",
|
||||
title: share_title,
|
||||
description: share_description,
|
||||
tags: "AIWeather,Art,AIart,Weather,#{@weather_art.city&.name},#{@weather_art&.city&.country&.name}",
|
||||
image: url_for(@weather_art.webp_image.processed)
|
||||
%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,32 +1,154 @@
|
||||
class BatchGenerateWeatherArtsWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
GENERATION_INTERVAL = 36.hours
|
||||
MAX_DURATION = 50.minutes
|
||||
SLEEP_DURATION = 120.seconds
|
||||
BATCH_SIZE = 20
|
||||
MAX_DURATION = 5.minutes
|
||||
SLEEP_DURATION = 10.seconds
|
||||
DAILY_GENERATION_LIMIT = 60 # 每日生成图片上限
|
||||
PER_RUN_GENERATION_LIMIT = 2 # 每次运行生成图片上限
|
||||
|
||||
def perform(*args)
|
||||
start_time = Time.current
|
||||
cities_to_process = get_eligible_cities.shuffle.take(BATCH_SIZE)
|
||||
cities_to_process.each do |city|
|
||||
break if Time.current - start_time > MAX_DURATION
|
||||
Rails.logger.info "Generating weather art for #{city.name}"
|
||||
GenerateWeatherArtWorker.perform_async(city.id)
|
||||
sleep SLEEP_DURATION
|
||||
lock_key = "batch_generate_weather_lock"
|
||||
lock_ttl = 300 # 锁的生存时间,单位为秒
|
||||
|
||||
redis = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1"))
|
||||
|
||||
if redis.set(lock_key, Time.current.to_s, nx: true, ex: lock_ttl)
|
||||
begin
|
||||
batch_tasks
|
||||
ensure
|
||||
redis.del(lock_key)
|
||||
end
|
||||
else
|
||||
Rails.logger.info "Sitemap refresh is already in progress"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_eligible_cities
|
||||
def batch_tasks
|
||||
start_time = Time.current
|
||||
remaining_slots = calculate_remaining_slots
|
||||
return if remaining_slots <= 0
|
||||
|
||||
remaining_slots = [ remaining_slots, PER_RUN_GENERATION_LIMIT ].min
|
||||
|
||||
countries_to_process = select_countries
|
||||
recent_cities = get_recent_cities
|
||||
cities_to_process = select_cities(recent_cities, countries_to_process, remaining_slots)&.shuffle
|
||||
print_cities_list(cities_to_process, start_time)
|
||||
|
||||
skipped_cities = []
|
||||
processed_cities = []
|
||||
|
||||
cities_to_process.each do |city|
|
||||
if within_daytime?(city)
|
||||
Rails.logger.info "Generating weather art for #{city.name} (time: [#{city.formatted_current_time(:all)}])"
|
||||
GenerateWeatherArtWorker.perform_async(city.id)
|
||||
processed_cities << city.name
|
||||
else
|
||||
Rails.logger.info "Skipping #{city.name} (time: [#{city.formatted_current_time(:all)}]) due to local time not being within daytime hours."
|
||||
skipped_cities << city.name
|
||||
end
|
||||
sleep SLEEP_DURATION
|
||||
break if Time.current - start_time > MAX_DURATION || processed_cities.size >= PER_RUN_GENERATION_LIMIT
|
||||
end
|
||||
|
||||
print_summary(processed_cities, skipped_cities)
|
||||
end
|
||||
|
||||
def get_recent_cities
|
||||
cutoff_time = Time.current - GENERATION_INTERVAL
|
||||
City.active
|
||||
.joins("LEFT JOIN (
|
||||
City.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)
|
||||
.where("latest_arts.last_generation_time > ?", cutoff_time)
|
||||
end
|
||||
|
||||
def calculate_remaining_slots
|
||||
today_generations = WeatherArt
|
||||
.where("DATE(created_at) = ?", Date.today)
|
||||
.where.not(image_attachment: nil)
|
||||
.count
|
||||
Rails.logger.info "Generating weather art for #{today_generations}(limit: #{DAILY_GENERATION_LIMIT}) generated slots."
|
||||
|
||||
[ DAILY_GENERATION_LIMIT - today_generations, 0 ].max
|
||||
end
|
||||
|
||||
def select_countries
|
||||
Country.all.select do |country|
|
||||
timezone_info = country.timezones.present? ? JSON.parse(country.timezones).first : { "zoneName" => "UTC" }
|
||||
local_time = Time.current.in_time_zone(ActiveSupport::TimeZone[timezone_info["zoneName"]])
|
||||
local_time.hour >= 6 && local_time.hour <= 18
|
||||
end
|
||||
end
|
||||
|
||||
def select_cities(recent_city, countries, limit)
|
||||
Rails.logger.debug "Select Cities with limit: [#{limit}], in [#{countries.size}] countries."
|
||||
Rails.logger.debug "Skip Cities(count: [#{recent_city.count}]) list: [#{recent_city.map(&:name).join(', ')}]."
|
||||
# 第一阶段:筛选 active 城市, 排除最近生成过的城市
|
||||
active_cities = City.where.not(id: recent_city.pluck(:id))
|
||||
.where(active: true, country_id: countries.map(&:id)).limit(limit).to_a
|
||||
selected = active_cities.first(limit)
|
||||
remaining = limit - selected.size
|
||||
Rails.logger.debug "Step1: Selected #{selected.size} cities with active city."
|
||||
Rails.logger.debug "==recent selected city list: #{selected.map(&:name).join(', ')}."
|
||||
|
||||
return selected if remaining <= 0
|
||||
|
||||
# 第二阶段: 将剩余名额平均分配到国家中
|
||||
select_countries = countries.sample(remaining)
|
||||
remaining_every_country_count =
|
||||
remaining > countries.count ?
|
||||
(remaining / countries.count).round :
|
||||
remaining
|
||||
Rails.logger.debug "Step2: Selected #{remaining_every_country_count} cities in every country"
|
||||
|
||||
select_countries.each do |country|
|
||||
c = City.where.not(id: recent_city.pluck(:id))
|
||||
.where.not(id: select_countries.pluck(:id))
|
||||
.where(country_id: country.id)
|
||||
.order(Arel.sql("RANDOM()"))
|
||||
.first(remaining_every_country_count)
|
||||
if c.any? # 检查是否有有效的城市
|
||||
Rails.logger.debug "== Selected city [#{c.first.name}] in country: [#{country.name}]"
|
||||
selected += c
|
||||
else
|
||||
Rails.logger.debug "== No valid cities found in country: [#{country.name}]"
|
||||
end
|
||||
end
|
||||
Rails.logger.debug "==recent selected city list: #{selected.map(&:name).join(', ')}." if selected.any?
|
||||
Rails.logger.debug "Finished selected #{selected.size} cities."
|
||||
selected
|
||||
end
|
||||
|
||||
def within_daytime?(city)
|
||||
local_time = get_local_time(city)
|
||||
local_time.hour >= 6 && local_time.hour <= 18
|
||||
end
|
||||
|
||||
def get_local_time(city)
|
||||
return Time.current unless city
|
||||
timezone_info = city.country&.timezones.present? ? JSON.parse(city.country.timezones).first : { "zoneName" => "UTC" }
|
||||
|
||||
timezone = ActiveSupport::TimeZone[timezone_info["zoneName"]] || ActiveSupport::TimeZone["UTC"]
|
||||
Time.current.in_time_zone(timezone)
|
||||
end
|
||||
|
||||
def print_cities_list(cities, start_time)
|
||||
Rails.logger.info "Generate city task start at: #{start_time}"
|
||||
Rails.logger.info "Generate city list: "
|
||||
Rails.logger.info "======================================"
|
||||
Rails.logger.info "ID\tRegion\tCountry\tState\tName\tLocalTime"
|
||||
cities.each do |city|
|
||||
Rails.logger.info "#{city.id}\t#{city.country&.region&.name}\t#{city.country&.name}\t#{city.state&.name}\t#{city.name}\t#{city.formatted_current_time(:all)}"
|
||||
end
|
||||
end
|
||||
|
||||
def print_summary(processed_cities, skipped_cities)
|
||||
Rails.logger.info "Processed cities: #{processed_cities.join(', ')}"
|
||||
Rails.logger.info "Skipped cities: #{skipped_cities.join(', ')}"
|
||||
end
|
||||
end
|
||||
|
@ -14,6 +14,7 @@ class GenerateWeatherArtWorker
|
||||
return unless image_url
|
||||
|
||||
create_weather_art(weather_data, prompt, image_url)
|
||||
Rails.logger.info "Successful Generate Weather Art In #{city.name}"
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error generating weather art for city #{city_id}: #{e.message}"
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
|
@ -1,58 +1,129 @@
|
||||
class RefreshSitemapWorker
|
||||
include Sidekiq::Worker
|
||||
require "redis"
|
||||
|
||||
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
|
||||
lock_key = "refresh_sitemap_lock"
|
||||
lock_ttl = 60 # 锁的生存时间,单位为秒
|
||||
|
||||
redis = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1"))
|
||||
|
||||
if redis.set(lock_key, Time.current.to_s, nx: true, ex: lock_ttl)
|
||||
begin
|
||||
setup_sitemap_config
|
||||
# 生成默认的不带语言前缀的 sitemap
|
||||
generate_sitemap(nil)
|
||||
|
||||
# 为每个可用语言生成带前缀的 sitemap
|
||||
I18n.available_locales.each do |locale|
|
||||
generate_sitemap(locale)
|
||||
end
|
||||
ensure
|
||||
redis.del(lock_key)
|
||||
end
|
||||
else
|
||||
Rails.logger.info "Sitemap refresh is already in progress"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def setup_sitemap_config
|
||||
@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 = ENV.fetch("RAILS_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)),
|
||||
ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:minio, :bucket)),
|
||||
aws_access_key_id: ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:minio, :access_key_id)),
|
||||
aws_secret_access_key: ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:minio, :secret_access_key)),
|
||||
aws_region: ENV.fetch("AWS_REGION", Rails.application.credentials.dig(:minio, :region)),
|
||||
force_path_style: ENV.fetch("AWS_FORCE_PATH_STYLE", Rails.application.credentials.dig(:minio, :force_path_style)),
|
||||
endpoint: ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:minio, :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)),
|
||||
ENV.fetch("AWS_DEV_BUCKET", Rails.application.credentials.dig(:minio_dev, :bucket)),
|
||||
aws_access_key_id: ENV.fetch("AWS_DEV_ACCESS_KEY_ID", Rails.application.credentials.dig(:minio_dev, :access_key_id)),
|
||||
aws_secret_access_key: ENV.fetch("AWS_DEV_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:minio_dev, :secret_access_key)),
|
||||
aws_region: ENV.fetch("AWS_DEV_REGION", Rails.application.credentials.dig(:minio_dev, :region)),
|
||||
force_path_style: ENV.fetch("AWS_FORCE_PATH_STYLE", Rails.application.credentials.dig(:minio_dev, :force_path_style)),
|
||||
endpoint: ENV.fetch("AWS_DEV_ENDPOINT", Rails.application.credentials.dig(:minio_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
|
||||
|
||||
def generate_sitemap(locale = nil)
|
||||
# 设置当前语言环境
|
||||
I18n.locale = locale || I18n.default_locale
|
||||
Rails.application.routes.default_url_options[:locale] = locale
|
||||
|
||||
# 设置 sitemap 路径
|
||||
# path_prefix = locale ? "sitemaps/#{locale}/" : "sitemaps/"
|
||||
# SitemapGenerator::Sitemap.sitemaps_path = path_prefix
|
||||
|
||||
filename = locale==nil ? "sitemap" : "sitemap_#{locale}"
|
||||
SitemapGenerator::Sitemap.create(filename: filename) do
|
||||
available_locales = I18n.available_locales
|
||||
|
||||
# 首页
|
||||
add root_path(locale: locale),
|
||||
changefreq: "daily",
|
||||
priority: 1.0,
|
||||
alternate: available_locales.map { |al|
|
||||
{ lang: al, href: root_url(locale: al) }
|
||||
}
|
||||
|
||||
# 城市列表页
|
||||
add cities_path(locale: locale),
|
||||
changefreq: "daily",
|
||||
priority: 0.9,
|
||||
alternate: available_locales.map { |al|
|
||||
{ lang: al, href: cities_url(locale: al) }
|
||||
}
|
||||
|
||||
# 艺术作品列表页
|
||||
add arts_path(locale: locale),
|
||||
changefreq: "daily",
|
||||
priority: 0.9,
|
||||
alternate: available_locales.map { |al|
|
||||
{ lang: al, href: arts_url(locale: al) }
|
||||
}
|
||||
|
||||
# 城市详情页
|
||||
City.find_each do |city|
|
||||
add city_path(city, locale: locale),
|
||||
changefreq: "daily",
|
||||
priority: 0.8,
|
||||
lastmod: city.updated_at,
|
||||
alternate: available_locales.map { |al|
|
||||
{ lang: al, href: city_url(city, locale: al) }
|
||||
}
|
||||
end
|
||||
|
||||
# 天气艺术作品页
|
||||
WeatherArt.includes(:city).find_each do |art|
|
||||
if art.image.attached?
|
||||
add city_weather_art_path(art.city, art),
|
||||
add city_weather_art_path(art.city, art, locale: locale),
|
||||
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')}"
|
||||
} ]
|
||||
} ],
|
||||
alternate: available_locales.map { |al|
|
||||
{ lang: al, href: city_weather_art_url(art.city, art, locale: al) }
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# SitemapGenerator::Sitemap.ping_search_engines if Rails.env.production?
|
||||
Rails.logger.info "Sitemap has been generated and uploaded to S3 successfully"
|
||||
Rails.logger.info "Generated sitemap for #{locale || 'default'} version"
|
||||
rescue => e
|
||||
Rails.logger.error "Error refreshing sitemap: #{e.message}"
|
||||
Rails.logger.error "Error generating sitemap for #{locale || 'default'}: #{e.message}"
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
@ -1 +1 @@
|
||||
WIRCFJoA1YUpdSuljSkBqEJvBpftvTRGA73T9Oy9vXNnOIHcnZDHtlhh2VuF4Xtq41d0aPJ2qPI4kUeGzM34I6FITsSY6i1y5ZEW4vUa8F/BRTO0/viXBPiSAS+BkWVEWYrFkNWJAIitVLG0hq8vCBEEs/KctxY8w9h0LwgyRV2udPzn6A7wyoJhR/V5nRamU6USFiHh6cpTlFQdSIZ0sC6v21IIS+G/ZPJXNNTw2+gVaDfL1yuX1nrLsxeMsb30iugksDj/gCVi42kF/hPVOGIZCM1ftM5j4Q+BcTDyEVqIMeXRdtegLKWM5sI0yB070m7gMRK1Oewa3z+NuPLGp1Stuuo977LGuATmP3/GnAMEZe8tgMfeKYeQv2+TerpLmO07KayOzN3j0qSs/OqrcSUP+SByxRBmpmeXNEQ6q+f1ZDreo/Q1Cm+aZe1UWXYpcgd6MVGHjYUna4SgTQqUKHKdzvf7Yx8fOjrzHRqZ6Y8LWq53Vzr8oNJ9IoNAh9TSJMF4tR3M+OQ7+SARgwQLoVsehgK1Z658GMvhVoeLrPTwvdID54+TSFMepMnownDJPZGKZoZK1+NlUyzz3rJ2iH9AxZnvwPMCcmlHH/Fh/FANmtQxd3DIPpjHHyZjxFLsR9tNuIapXPK8SiRhWbtYo+4i0/dgUvD8SfECSOklfx2tVIhvxTjg4AM+WtI9gieDISq+AZe9fjqyv8vLOJuDX8Nk1k69DKIqB2u+OFJW2/PPYFz2Ry283ep+VRKwS2qlsFEkTRkzPJ5y9dVrCXIYl0R7vgC+GDrUkK1IffZmTaJ+rqmzzwLF1OwPdtN/QuJm0aZZQmN9pQgUjt1PEgthQVW9dt/ElDZKgVjIcds3F1kX0+ZWJQRvBldilC9W5lRSea/9x253dw3wj1ERF6sZsIJCwcxTJigTM4rEfcjrMnEIfiW1HWc1LTJ5DUIh5GISBv7NDF52voP2TBamAq2Zg2vVg2v+bU9ydkkIVOm4oLYZWDQyDBeQC4KJscFCSo1ZxvRuqIRtnI3h2KoXJeK63cxGcVxbHcyYO5xTPXmPkk18pFPYoXe46an6xTEzxYXQMfWuIUvM9oOjgZXFvdgPwDWzL/pSZb6qW5MiDqvlt38ddivUtsZLMMtpgZjXOAyKQyIQNSOHyrrYoj6r8PdFiCZMWjn1B0HmXgIl47jrb9o1nrif9rb5DSUY2Pvl0+TeRnUB8r7Wa+t+UdHjrq6PEaXVOQbej78Rzo5xmZ5XknlbKv02Vjpcm8EIkc00nKvMo29f0mr1Wv48CL+tuDxiW3v2yX8NcTnMEoQkTTaw5wJmSb3dmPdVZwjcCdbqs1CbRpc/ngX7nJP9kF2P57BWqqYq6V4fWk3vEgLpTUsTCbJvTlCx7McLKy2/smtz2HQl6Vs5jofpSIXgNC35TBkPP22XLFiBTah0E2IUeF2TyuLphswQsMDzKzADMxkKpzEVNLCzzKfygDfsfsJqZjKBGHJz3RHDbGoqmgP/4u7FEBV9gUXFt5i5k71HxSnafbpcFkLk3AdfNLso2qUNZSj/8EzY33WFjQIEAFJeTqB8WML/LcVvNqQqvjUyUMgvA+rZq+BwS8svqJEvcRFmZhjeqQbsYs+C+jOlwjrohoak/CgfiHofuXWvnNyi0+PT6bPO5CsjbpJLwFxbPi19tWmnMR7ZOqIE3vN4bf8Rjg4T5zhKKbgIU7wEP+i0u9p0zPFEa6EZMxp8/5q50EjXdlY/cHa/Sp05x2h8jhi1Trxgg8yFOdu2P+dStAwW3Sv4x2o=--aZKHys4Q3dCV2P+L--yEOdlrHAYleGIYmdKAB4FQ==
|
||||
geGCObHW9aRt27AEzTpe7YfR9YNZtC1NVigIGs72aWNrwAkXB1DnXeDZH4OMJTPug+nxO4/5trV/XgwpTHPyqY4t1cbB22oX2aYSUijnJKz4rWaVi8K+OQfInc1pQSro9yTkJrT76a1UCYR27yxhR+l/+CxVb8yNfHMhPOLXpjhJKE6D2bppxkFJ7MTQ6FEAMxtgLLL7aDIMkt+KLhep/dfeKhW0IbZsng7MA5ExL8sd+eM3twRgTr9XIThpJ8v6lgo69fsINvaUxYdgZddqOQ+O7EeYaBGasE/RCR2DULreKwEWLA7fLgPuqrxs4EqGxdiD9LeNKZN40sJw360ZbmLtRUsMn7hEbFRcUfito7aatJ97Q/TStvTkhyWqLgt4JsqlRXHtbFayUiixwQI9WJY0iF1by6lBtoXiA8HsifY2t7muv4OaMI2386LptZ3JiIFaQOfaFJRPHmMSfejj13TaPs6J6gDDvpeLv95lhpwbMu79+zdYGeEtFK71kx2rj266YGhe5dKttcMlcNWke1WwS+GIFArjEO5sR057Z//Q/fP+voW+GL/aVgt4qySnX0Fd9kGh0ZkEtSufP8djFRYM/Yl7NjzM4ofoww3ghtzT0+eahktfpEZMqqBdLtxKf4tQSKO5xMAW2puRThunKiQeLrcuDh0PVkVeDbSwH05Wq0GFHwNft3d62Bg9310PIGHGkKU8eWnLfgn3dNy8HsihXfAgZHLzZSDZfy6htqXYsj9Vk4LeV6c359dI2lPuxb8ka0c1jjnKniPiYpcHqSUd46lPYuKEdP49ezxdOLzrkGRpiGiX4iqIV0bUrB1GLvJ2N2kcblkprvoR4daNqCuLl8noUfEczDl+puDOZg79XMWvp1zJDVyHyPnCG58951VI6GpvNg1LpF7e+EuUIBbyjBXbiyisLCujYy2Xi4RGSF+z++FVRUdXDHHATPcFoGo4kOcPiAncde7D/vM3HlaU+SmFA8vSV1BMVsKBvHyuI0SEyD3JxgCz4/Bv9cMjF4Xhtf8xcz3746jZDnUWWk/wDjUNjCm/btI3TEsQ5e2InPohL2qG/86RPhyRvGFPdDyS1ulscSNrEDZd1yuPQ3ZZaXaHpUD4K/M/nWdWvNbsrQ9J/goob+x3VwqtASdf6a4zIdGlgNhOdsYogSqms/GlKaih1jMCKUXsnY5s5phd87Tw5Gtq6I1QKOa0p2Y6grIr0uOvU/DnxlSvKbm5/tpkF7v05hOCdDRwu4//MYGQPXXCOBF3icgQvmlwBZ7kEWFLmpOFheZ4koqMyxK9GrgQnKwBLpsu0lZG4p8wNSboj5gR9MABVxTYlMVkfIPuNXhU5iWH9g2KpagDgyPcN4PHsJgBXWBBmcUvdO4DMWfH9Z7z6Q1V3TImpUabHtRN5dsL9iAepMlt4xrlqUBdWCjhBYKpX5zYukkCy83ccaKB448KupgnjbW12MFy1pLTLEKhtLV8woX3QAFDQJgFfoH8fD4lx3C25lLvfJPYSlp2xFAcNy+B3mdUF306Iff6XmOCzKlSGI39HWjYA3jpv9031T0YwY6eVKGbQyZMzO7QVeK2lSn+0BA+55INJ5hdP6XlwRlS4lidbPIxUAbMpC3yILMjImc36FvHdomuAAGC1s5naFCFtsl8LpVcdlYsX7Jwf56TV7whchV9+45aStNANyfSH6dhkikquA7i8DBPWnSbgHyomu8ihb4qVSqvtCJcw6Oh7oSz9zrQsNZkmZ+PmsGx37yY80ZZNUbdqqpIuNzTrEU5jgHbCpJqGGbEjjysleB6A3cnVLbBXWHlQubZY/NdpRMCsuThyf4YEhez6OTxd+GEwiCuJse7dSHw3N0xfkFaqad0fR+BJAXIItqIAgYMI/AMyyqrF9lIYk5YTTUSML4Ae0wIEo6tteNNxly/Ofz+DFF/GOCkBxQ88yLBGAq2QG/wkQ9upBRBAZ+dFHeazh9fi1W811EjpDEiqELEqNbxgbV+2cWBbbwVb0917+SVEWDgxer0O+iZUyYjJ9Q8rTtikjWdGuikikz8HnMY6v6kl7TC4bP3gPsyxiLk1JnmTJYGtRrJXQYH3ImlHJKVaS0gtCXZ/rXBcbqGZXpEW4b4ZeVQalMFtQW5BT+rqW7h8f+FkEeV6xKaauOsnPg3fqVfCazRMkuvKatdkn/rF/V2RjydsK3rLfAvLwfz89XRpekzI9PGJJsUKqEFO/VSoEKXedqn/tQkdGWok4VqYoplDWarCbi2sQJQztbwTUCcBT87m+a/oxHyqCrXi5hTpomCHXPrTovilF28s12Zl8ivWpFTebjtxvD/QarAjrWgx42KUuISEAAuUoVxga+ICxd0WoZlSrhcwAntWWvdQkzW41t9IUvzlQX0O9AdZEki81lRRASzEMS28ge7pFv7VkLMIplzY0Ltl/GOq1q+flX1zebEzjQjcq0PiXoIWLAj8PNIhHV/aFwH+jAwcH4pYpO8aQNjhoYBM9KZ1W5ZWSphehdrIddZ5oiZsUs9PMwFza28bAdQvCVarcrF2ofOZCNTLOdL6FntoUPovxemuQERnZ0V5xvVYnLMW8KnHjLYJqPmVMdd38gZe1Qf4JeOz+2WyW/E0SSz2dOt0zKp3klyURwr5+dlTQeGy7D1a4OPxvRIwAAr3cGNpPG2NBzGNeDSargSDrQIRBpp23gaGQpJrfnR95MFvNSx9IG9GkK+ifpwAbtsqEjF2iTYJ9BKSR8VIODLx/VoUD/1euTI--w1b7CHBZ4zFPRjgq--DpdTPDeqnUeN8aZTrINdZg==
|
@ -1,6 +1,15 @@
|
||||
require "active_support/core_ext/integer/time"
|
||||
|
||||
Rails.application.configure do
|
||||
config.after_initialize do
|
||||
Bullet.enable = true
|
||||
Bullet.alert = false
|
||||
Bullet.bullet_logger = true
|
||||
Bullet.console = true
|
||||
Bullet.rails_logger = true
|
||||
Bullet.add_footer = true
|
||||
end
|
||||
|
||||
# Settings specified here will take precedence over those in config/application.rb.
|
||||
|
||||
# Make code changes take effect immediately without server restart.
|
||||
@ -70,4 +79,6 @@ Rails.application.configure do
|
||||
|
||||
# Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
|
||||
# config.generators.apply_rubocop_autocorrect_after_generate!
|
||||
|
||||
config.serve_static_assets = true
|
||||
end
|
||||
|
@ -5,3 +5,8 @@ Rails.application.config.assets.version = "1.0"
|
||||
|
||||
# Add additional assets to the asset load path.
|
||||
# Rails.application.config.assets.paths << Emoji.images_path
|
||||
# Rails.application.config.assets.paths << Rails.root.join("app/assets/builds")
|
||||
# Rails.application.config.assets.paths << Rails.root.join("node_modules")
|
||||
# Rails.application.config.assets.precompile += %w( *.png *.jpg *.jpeg *.gif )
|
||||
# Rails.application.config.assets.paths << Rails.root.join("app/assets/builds")
|
||||
# Rails.application.config.assets.precompile += %w( *.png *.jpg *.jpeg *.gif )
|
||||
|
@ -1,19 +1,21 @@
|
||||
if Rails.env.production?
|
||||
Aws.config.update({
|
||||
region: ENV.fetch("AWS_REGION", "wnam"),
|
||||
region: ENV.fetch("AWS_REGION", Rails.application.credentials.dig(:minio, :region)),
|
||||
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))
|
||||
ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:minio, :access_key_id)),
|
||||
ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:minio, :secret_access_key))
|
||||
),
|
||||
endpoint: ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:aws, :endpoint))
|
||||
force_path_style: ENV.fetch("AWS_FORCE_PATH_STYLE", Rails.application.credentials.dig(:minio_dev, :force_path_style)),
|
||||
endpoint: ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:minio, :endpoint))
|
||||
})
|
||||
else
|
||||
Aws.config.update({
|
||||
region: ENV.fetch("AWS_DEV_REGION", "wnam"),
|
||||
region: ENV.fetch("AWS_DEV_REGION", Rails.application.credentials.dig(:minio_dev, :region)),
|
||||
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))
|
||||
ENV.fetch("AWS_DEV_ACCESS_KEY_ID", Rails.application.credentials.dig(:minio_dev, :access_key_id)),
|
||||
ENV.fetch("AWS_DEV_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:minio_dev, :secret_access_key))
|
||||
),
|
||||
endpoint: ENV.fetch("AWS_DEV_ENDPOINT", Rails.application.credentials.dig(:aws_dev, :endpoint))
|
||||
force_path_style: ENV.fetch("AWS_DEV_FORCE_PATH_STYLE", Rails.application.credentials.dig(:minio_dev, :force_path_style)),
|
||||
endpoint: ENV.fetch("AWS_DEV_ENDPOINT", Rails.application.credentials.dig(:minio_dev, :endpoint))
|
||||
})
|
||||
end
|
||||
|
19
config/initializers/locale.rb
Normal file
19
config/initializers/locale.rb
Normal file
@ -0,0 +1,19 @@
|
||||
# config/initializers/locale.rb
|
||||
require "i18n/backend/fallbacks"
|
||||
|
||||
# Where the I18n library should search for translation files
|
||||
I18n.load_path += Dir[Rails.root.join("config", "locales", "*.{rb,yml}")]
|
||||
|
||||
# Permitted locales available for the application
|
||||
I18n.available_locales = [ :en, :"zh-CN", :ja, :ko, :"pt-BR", :hr, :fa, :de, :es, :fr, :it, :tr, :ru, :uk, :pl, :bn, :hi, :ur, :ar ]
|
||||
|
||||
I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
|
||||
# I18n::Backend::Simple.include I18n::Backend::Fallbacks
|
||||
# I18n.fallbacks[:en]
|
||||
I18n.fallbacks = I18n::Locale::Fallbacks.new(
|
||||
en: [ :en ],
|
||||
'zh-CN': [ :zh, :zh_cn, :en ]
|
||||
)
|
||||
|
||||
# Set default locale to something other than :en
|
||||
I18n.default_locale = :en
|
8
config/initializers/rack_mini_profiler.rb
Normal file
8
config/initializers/rack_mini_profiler.rb
Normal file
@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
if Rails.env.development?
|
||||
require "rack-mini-profiler"
|
||||
|
||||
# The initializer was required late, so initialize it manually.
|
||||
# Rack::MiniProfilerRails.initialize!(Rails.application)
|
||||
end
|
67
config/locales/ar.yml
Normal file
67
config/locales/ar.yml
Normal file
@ -0,0 +1,67 @@
|
||||
ar:
|
||||
hello: "مرحباً بالعالم"
|
||||
brand:
|
||||
name: "الطقس اليوم بالذكاء الاصطناعي"
|
||||
title:
|
||||
cities: "المدن"
|
||||
arts: "الفنون"
|
||||
sign_in: "تسجيل الدخول"
|
||||
sign_out: "تسجيل الخروج"
|
||||
settings: "الإعدادات"
|
||||
admin_dashboard: "لوحة تحكم المشرف"
|
||||
latest_weather_art: "أحدث فن الطقس"
|
||||
popular_weather_art: "فن الطقس الشائع"
|
||||
ai_prompt: "موجه الذكاء الاصطناعي"
|
||||
text:
|
||||
latest_from: "أحدث من"
|
||||
search_cities: "البحث عن المدن..."
|
||||
all_regions: "جميع المناطق"
|
||||
all_countries: "جميع البلدان"
|
||||
all_in: "الكل في"
|
||||
showing: "عرض"
|
||||
weather_arts: "فنون الطقس"
|
||||
newest_first: "الأحدث أولاً"
|
||||
oldest_first: "الأقدم أولاً"
|
||||
cities:
|
||||
title: "استكشف المدن"
|
||||
arts:
|
||||
title: "معرض فنون الطقس"
|
||||
subtitle: "اكتشف فن الطقس المُنشأ بالذكاء الاصطناعي من مدن حول العالم"
|
||||
home:
|
||||
headline_html: حيث يلتقي الطقس<br>بالذكاء الاصطناعي
|
||||
subtitle:
|
||||
اختبر الطقس من خلال عدسة الفن المُنشأ بالذكاء الاصطناعي،
|
||||
مما يجلب منظوراً جديداً للظواهر الجوية اليومية.
|
||||
button:
|
||||
explore_cities: "استكشف المدن"
|
||||
view_detail: "عرض التفاصيل"
|
||||
view_all_weather_arts: "عرض كل فنون الطقس"
|
||||
back_to_cities: "العودة إلى المدن"
|
||||
back_to: "العودة إلى"
|
||||
card:
|
||||
temperature: "درجة الحرارة"
|
||||
wind: "الرياح"
|
||||
humidity: "الرطوبة"
|
||||
visibility: "الرؤية"
|
||||
pressure: "الضغط"
|
||||
cloud_cover: "الغطاء السحابي"
|
||||
feel_like: "الشعور كأنها"
|
||||
relative_humidity: "الرطوبة النسبية"
|
||||
clear_view_distance: "مسافة الرؤية الواضحة"
|
||||
atmospheric_pressure: "الضغط الجوي"
|
||||
sky_coverage: "تغطية السماء"
|
||||
pagination:
|
||||
showing_items: "عرض %{from} إلى %{to} من %{total} %{items}"
|
||||
items:
|
||||
weather: "سجلات الطقس"
|
||||
default: "العناصر"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%b %d"
|
||||
long: "%B %d, %Y"
|
67
config/locales/bn.yml
Normal file
67
config/locales/bn.yml
Normal file
@ -0,0 +1,67 @@
|
||||
bn:
|
||||
hello: "ওহে বিশ্ব"
|
||||
brand:
|
||||
name: "টুডে এআই ওয়েদার"
|
||||
title:
|
||||
cities: "শহরগুলি"
|
||||
arts: "শিল্প"
|
||||
sign_in: "সাইন ইন"
|
||||
sign_out: "সাইন আউট"
|
||||
settings: "সেটিংস"
|
||||
admin_dashboard: "অ্যাডমিন ড্যাশবোর্ড"
|
||||
latest_weather_art: "সর্বশেষ আবহাওয়া শিল্প"
|
||||
popular_weather_art: "জনপ্রিয় আবহাওয়া শিল্প"
|
||||
ai_prompt: "এআই প্রম্পট"
|
||||
text:
|
||||
latest_from: "সর্বশেষ"
|
||||
search_cities: "শহর অনুসন্ধান..."
|
||||
all_regions: "সব অঞ্চল"
|
||||
all_countries: "সব দেশ"
|
||||
all_in: "সবগুলি"
|
||||
showing: "দেখাচ্ছে"
|
||||
weather_arts: "আবহাওয়া শিল্প"
|
||||
newest_first: "নতুনগুলি প্রথমে"
|
||||
oldest_first: "পুরানোগুলি প্রথমে"
|
||||
cities:
|
||||
title: "শহরগুলি অন্বেষণ করুন"
|
||||
arts:
|
||||
title: "আবহাওয়া শিল্প গ্যালারি"
|
||||
subtitle: "বিশ্বজুড়ে শহরগুলি থেকে এআই-জেনারেটেড আবহাওয়া শিল্প আবিষ্কার করুন"
|
||||
home:
|
||||
headline_html: যেখানে আবহাওয়া মিলিত হয়<br>কৃত্রিম বুদ্ধিমত্তার সাথে
|
||||
subtitle:
|
||||
এআই-জেনারেটেড শিল্পের মাধ্যমে আবহাওয়া অনুভব করুন,
|
||||
দৈনিক আবহাওয়া ঘটনার একটি নতুন দৃষ্টিভঙ্গি আনয়ন করে।
|
||||
button:
|
||||
explore_cities: "শহরগুলি অন্বেষণ করুন"
|
||||
view_detail: "বিস্তারিত দেখুন"
|
||||
view_all_weather_arts: "সমস্ত আবহাওয়া শিল্প দেখুন"
|
||||
back_to_cities: "শহরগুলিতে ফিরে যান"
|
||||
back_to: "ফিরে যান"
|
||||
card:
|
||||
temperature: "তাপমাত্রা"
|
||||
wind: "বাতাস"
|
||||
humidity: "আর্দ্রতা"
|
||||
visibility: "দৃশ্যমানতা"
|
||||
pressure: "চাপ"
|
||||
cloud_cover: "মেঘাচ্ছন্নতা"
|
||||
feel_like: "অনুভূত হয়"
|
||||
relative_humidity: "আপেক্ষিক আর্দ্রতা"
|
||||
clear_view_distance: "পরিষ্কার দৃষ্টির দূরত্ব"
|
||||
atmospheric_pressure: "বায়ুমণ্ডলীয় চাপ"
|
||||
sky_coverage: "আকাশ আচ্ছাদন"
|
||||
pagination:
|
||||
showing_items: "%{total} %{items}-এর মধ্যে %{from} থেকে %{to} দেখানো হচ্ছে"
|
||||
items:
|
||||
weather: "আবহাওয়া রেকর্ড"
|
||||
default: "আইটেম"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%b %d"
|
||||
long: "%B %d, %Y"
|
@ -1,55 +0,0 @@
|
||||
en:
|
||||
countries:
|
||||
# East Asia
|
||||
CN: 'China'
|
||||
JP: 'Japan'
|
||||
KR: 'South Korea'
|
||||
TW: 'Taiwan'
|
||||
HK: 'Hong Kong'
|
||||
|
||||
# South Asia
|
||||
IN: 'India'
|
||||
PK: 'Pakistan'
|
||||
BD: 'Bangladesh'
|
||||
|
||||
# Southeast Asia
|
||||
ID: 'Indonesia'
|
||||
VN: 'Vietnam'
|
||||
TH: 'Thailand'
|
||||
MM: 'Myanmar'
|
||||
SG: 'Singapore'
|
||||
|
||||
# Middle East
|
||||
TR: 'Turkey'
|
||||
IR: 'Iran'
|
||||
SA: 'Saudi Arabia'
|
||||
IQ: 'Iraq'
|
||||
|
||||
# Africa
|
||||
NG: 'Nigeria'
|
||||
EG: 'Egypt'
|
||||
CD: 'Democratic Republic of the Congo'
|
||||
TZ: 'Tanzania'
|
||||
ZA: 'South Africa'
|
||||
KE: 'Kenya'
|
||||
AO: 'Angola'
|
||||
ML: 'Mali'
|
||||
CI: 'Ivory Coast'
|
||||
|
||||
# Europe
|
||||
RU: 'Russia'
|
||||
GB: 'United Kingdom'
|
||||
DE: 'Germany'
|
||||
|
||||
# North America
|
||||
US: 'United States'
|
||||
MX: 'Mexico'
|
||||
|
||||
# South America
|
||||
BR: 'Brazil'
|
||||
PE: 'Peru'
|
||||
CO: 'Colombia'
|
||||
CL: 'Chile'
|
||||
|
||||
# Oceania
|
||||
AU: 'Australia'
|
@ -1,55 +0,0 @@
|
||||
zh-CN:
|
||||
countries:
|
||||
# East Asia
|
||||
CN: '中国'
|
||||
JP: '日本'
|
||||
KR: '韩国'
|
||||
TW: '台湾'
|
||||
HK: '香港'
|
||||
|
||||
# South Asia
|
||||
IN: '印度'
|
||||
PK: '巴基斯坦'
|
||||
BD: '孟加拉国'
|
||||
|
||||
# Southeast Asia
|
||||
ID: '印度尼西亚'
|
||||
VN: '越南'
|
||||
TH: '泰国'
|
||||
MM: '缅甸'
|
||||
SG: '新加坡'
|
||||
|
||||
# Middle East
|
||||
TR: '土耳其'
|
||||
IR: '伊朗'
|
||||
SA: '沙特阿拉伯'
|
||||
IQ: '伊拉克'
|
||||
|
||||
# Africa
|
||||
NG: '尼日利亚'
|
||||
EG: '埃及'
|
||||
CD: '刚果民主共和国'
|
||||
TZ: '坦桑尼亚'
|
||||
ZA: '南非'
|
||||
KE: '肯尼亚'
|
||||
AO: '安哥拉'
|
||||
ML: '马里'
|
||||
CI: '科特迪瓦'
|
||||
|
||||
# Europe
|
||||
RU: '俄罗斯'
|
||||
GB: '英国'
|
||||
DE: '德国'
|
||||
|
||||
# North America
|
||||
US: '美国'
|
||||
MX: '墨西哥'
|
||||
|
||||
# South America
|
||||
BR: '巴西'
|
||||
PE: '秘鲁'
|
||||
CO: '哥伦比亚'
|
||||
CL: '智利'
|
||||
|
||||
# Oceania
|
||||
AU: '澳大利亚'
|
67
config/locales/de.yml
Normal file
67
config/locales/de.yml
Normal file
@ -0,0 +1,67 @@
|
||||
de:
|
||||
hello: "Hallo Welt"
|
||||
brand:
|
||||
name: "Today AI Weather"
|
||||
title:
|
||||
cities: "Städte"
|
||||
arts: "Kunst"
|
||||
sign_in: "Anmelden"
|
||||
sign_out: "Abmelden"
|
||||
settings: "Einstellungen"
|
||||
admin_dashboard: "Admin-Dashboard"
|
||||
latest_weather_art: "Neueste Wetterkunst"
|
||||
popular_weather_art: "Beliebte Wetterkunst"
|
||||
ai_prompt: "KI-Prompt"
|
||||
text:
|
||||
latest_from: "Neuestes von"
|
||||
search_cities: "Städte suchen..."
|
||||
all_regions: "Alle Regionen"
|
||||
all_countries: "Alle Länder"
|
||||
all_in: "Alles in"
|
||||
showing: "Zeigt"
|
||||
weather_arts: "Wetterkunst"
|
||||
newest_first: "Neueste zuerst"
|
||||
oldest_first: "Älteste zuerst"
|
||||
cities:
|
||||
title: "Städte erkunden"
|
||||
arts:
|
||||
title: "Wetterkunst-Galerie"
|
||||
subtitle: "Entdecken Sie KI-generierte Wetterkunst aus Städten auf der ganzen Welt"
|
||||
home:
|
||||
headline_html: Wo Wetter auf<br>Künstliche Intelligenz trifft
|
||||
subtitle:
|
||||
Erleben Sie Wetter durch die Linse KI-generierter Kunst,
|
||||
die eine neue Perspektive auf tägliche meteorologische Phänomene bietet.
|
||||
button:
|
||||
explore_cities: "Städte erkunden"
|
||||
view_detail: "Details anzeigen"
|
||||
view_all_weather_arts: "Alle Wetterkunst anzeigen"
|
||||
back_to_cities: "Zurück zu Städten"
|
||||
back_to: "Zurück zu"
|
||||
card:
|
||||
temperature: "Temperatur"
|
||||
wind: "Wind"
|
||||
humidity: "Luftfeuchtigkeit"
|
||||
visibility: "Sichtweite"
|
||||
pressure: "Luftdruck"
|
||||
cloud_cover: "Bewölkung"
|
||||
feel_like: "Gefühlt wie"
|
||||
relative_humidity: "Relative Luftfeuchtigkeit"
|
||||
clear_view_distance: "Klare Sichtweite"
|
||||
atmospheric_pressure: "Atmosphärischer Druck"
|
||||
sky_coverage: "Himmelsbedeckung"
|
||||
pagination:
|
||||
showing_items: "Zeigt %{from} bis %{to} von %{total} %{items}"
|
||||
items:
|
||||
weather: "Wetteraufzeichnungen"
|
||||
default: "Einträge"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%d. %b"
|
||||
long: "%d. %B %Y"
|
@ -28,4 +28,90 @@
|
||||
# enabled: "ON"
|
||||
|
||||
en:
|
||||
language:
|
||||
en: "English"
|
||||
zh-CN: "简体中文"
|
||||
ja: "日本語"
|
||||
ko: "한국어" # 韩语
|
||||
pt-BR: "Português (Brasil)" # 巴西葡萄牙语
|
||||
pt: "Português" # 葡萄牙语
|
||||
hr: "Hrvatski" # 克罗地亚语
|
||||
fa: "فارسی" # 波斯语(法尔西语)
|
||||
de: "Deutsch" # 德语
|
||||
es: "Español" # 西班牙语
|
||||
fr: "Français" # 法语
|
||||
it: "Italiano" # 意大利语
|
||||
tr: "Türkçe" # 土耳其语
|
||||
ru: "Русский" # 俄语
|
||||
uk: "Українська" # 乌克兰语
|
||||
pl: "Polski" # 波兰语
|
||||
bn: "বাংলা" # 孟加拉
|
||||
hi: "हिंदी" # 印地语
|
||||
ur: " اردو" # 乌尔都语
|
||||
ar: "العربية" # 阿拉伯
|
||||
hello: "Hello world"
|
||||
brand:
|
||||
name: "Today AI Weather"
|
||||
title:
|
||||
cities: "Cities"
|
||||
arts: "Arts"
|
||||
sign_in: "Sign in"
|
||||
sign_out: "Sign out"
|
||||
settings: "Settings"
|
||||
admin_dashboard: "Admin Dashboard"
|
||||
latest_weather_art: "Latest Weather Art"
|
||||
popular_weather_art: "Popular Weather Art"
|
||||
ai_prompt: "AI Prompt"
|
||||
text:
|
||||
latest_from: "Latest from"
|
||||
search_cities: "Search cities..."
|
||||
all_regions: "All Regions"
|
||||
all_countries: "All Countries"
|
||||
all_in: "All in"
|
||||
showing: "Showing"
|
||||
weather_arts: "Weather Arts"
|
||||
newest_first: "Newest First"
|
||||
oldest_first: "Oldest First"
|
||||
cities:
|
||||
title: "Explore Cities"
|
||||
arts:
|
||||
title: "Weather Arts Gallery"
|
||||
subtitle: "Discover AI-generated weather art from cities around the world"
|
||||
home:
|
||||
headline_html: Where Weather Meets<br>Artificial Intelligence
|
||||
subtitle:
|
||||
Experience weather through the lens of AI-generated art,
|
||||
bringing a new perspective to daily meteorological phenomena.
|
||||
button:
|
||||
explore_cities: "Explore Cities"
|
||||
view_detail: "View Details"
|
||||
view_all_weather_arts: "View All Weather Arts"
|
||||
back_to_cities: "Back to Cities"
|
||||
back_to: "Back to"
|
||||
card:
|
||||
temperature: "Temperature"
|
||||
wind: "Wind"
|
||||
humidity: "Humidity"
|
||||
visibility: "Visibility"
|
||||
pressure: "Pressure"
|
||||
cloud_cover: "Cloud Cover"
|
||||
feel_like: "Feels like"
|
||||
relative_humidity: "Relative humidity"
|
||||
clear_view_distance: "Clear view distance"
|
||||
atmospheric_pressure: "Atmospheric pressure"
|
||||
sky_coverage: "Sky coverage"
|
||||
pagination:
|
||||
showing_items: "Showing %{from} to %{to} of %{total} %{items}"
|
||||
items:
|
||||
weather: "weather records"
|
||||
default: "items"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%b %d"
|
||||
long: "%B %d, %Y"
|
||||
|
67
config/locales/es.yml
Normal file
67
config/locales/es.yml
Normal file
@ -0,0 +1,67 @@
|
||||
es:
|
||||
hello: "Hola mundo"
|
||||
brand:
|
||||
name: "Today AI Weather"
|
||||
title:
|
||||
cities: "Ciudades"
|
||||
arts: "Arte"
|
||||
sign_in: "Iniciar sesión"
|
||||
sign_out: "Cerrar sesión"
|
||||
settings: "Configuración"
|
||||
admin_dashboard: "Panel de administración"
|
||||
latest_weather_art: "Último arte del tiempo"
|
||||
popular_weather_art: "Arte del tiempo popular"
|
||||
ai_prompt: "Prompt de IA"
|
||||
text:
|
||||
latest_from: "Lo último de"
|
||||
search_cities: "Buscar ciudades..."
|
||||
all_regions: "Todas las regiones"
|
||||
all_countries: "Todos los países"
|
||||
all_in: "Todo en"
|
||||
showing: "Mostrando"
|
||||
weather_arts: "Arte del tiempo"
|
||||
newest_first: "Más recientes primero"
|
||||
oldest_first: "Más antiguos primero"
|
||||
cities:
|
||||
title: "Explorar ciudades"
|
||||
arts:
|
||||
title: "Galería de arte del tiempo"
|
||||
subtitle: "Descubre arte generado por IA del tiempo de ciudades de todo el mundo"
|
||||
home:
|
||||
headline_html: Donde el tiempo se encuentra<br>con la Inteligencia Artificial
|
||||
subtitle:
|
||||
Experimenta el tiempo a través del lente del arte generado por IA,
|
||||
brindando una nueva perspectiva a los fenómenos meteorológicos diarios.
|
||||
button:
|
||||
explore_cities: "Explorar ciudades"
|
||||
view_detail: "Ver detalles"
|
||||
view_all_weather_arts: "Ver todo el arte del tiempo"
|
||||
back_to_cities: "Volver a ciudades"
|
||||
back_to: "Volver a"
|
||||
card:
|
||||
temperature: "Temperatura"
|
||||
wind: "Viento"
|
||||
humidity: "Humedad"
|
||||
visibility: "Visibilidad"
|
||||
pressure: "Presión"
|
||||
cloud_cover: "Cobertura de nubes"
|
||||
feel_like: "Sensación térmica"
|
||||
relative_humidity: "Humedad relativa"
|
||||
clear_view_distance: "Distancia de visibilidad clara"
|
||||
atmospheric_pressure: "Presión atmosférica"
|
||||
sky_coverage: "Cobertura del cielo"
|
||||
pagination:
|
||||
showing_items: "Mostrando %{from} a %{to} de %{total} %{items}"
|
||||
items:
|
||||
weather: "registros meteorológicos"
|
||||
default: "elementos"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%b %d"
|
||||
long: "%d de %B de %Y"
|
67
config/locales/fa.yml
Normal file
67
config/locales/fa.yml
Normal file
@ -0,0 +1,67 @@
|
||||
fa:
|
||||
hello: "سلام دنیا"
|
||||
brand:
|
||||
name: "هوای هوش مصنوعی امروز"
|
||||
title:
|
||||
cities: "شهرها"
|
||||
arts: "هنرها"
|
||||
sign_in: "ورود"
|
||||
sign_out: "خروج"
|
||||
settings: "تنظیمات"
|
||||
admin_dashboard: "داشبورد مدیر"
|
||||
latest_weather_art: "آخرین هنر آب و هوا"
|
||||
popular_weather_art: "هنر آب و هوای محبوب"
|
||||
ai_prompt: "پرامپت هوش مصنوعی"
|
||||
text:
|
||||
latest_from: "آخرین از"
|
||||
search_cities: "جستجوی شهرها..."
|
||||
all_regions: "همه مناطق"
|
||||
all_countries: "همه کشورها"
|
||||
all_in: "همه در"
|
||||
showing: "نمایش"
|
||||
weather_arts: "هنرهای آب و هوا"
|
||||
newest_first: "جدیدترین اول"
|
||||
oldest_first: "قدیمیترین اول"
|
||||
cities:
|
||||
title: "کاوش شهرها"
|
||||
arts:
|
||||
title: "گالری هنرهای آب و هوا"
|
||||
subtitle: "کشف هنر آب و هوای تولید شده توسط هوش مصنوعی از شهرهای سراسر جهان"
|
||||
home:
|
||||
headline_html: جایی که آب و هوا با<br>هوش مصنوعی ملاقات میکند
|
||||
subtitle:
|
||||
تجربه آب و هوا از طریق لنز هنر تولید شده توسط هوش مصنوعی،
|
||||
آوردن دیدگاهی جدید به پدیدههای هواشناسی روزانه.
|
||||
button:
|
||||
explore_cities: "کاوش شهرها"
|
||||
view_detail: "مشاهده جزئیات"
|
||||
view_all_weather_arts: "مشاهده همه هنرهای آب و هوا"
|
||||
back_to_cities: "بازگشت به شهرها"
|
||||
back_to: "بازگشت به"
|
||||
card:
|
||||
temperature: "دما"
|
||||
wind: "باد"
|
||||
humidity: "رطوبت"
|
||||
visibility: "دید"
|
||||
pressure: "فشار"
|
||||
cloud_cover: "پوشش ابر"
|
||||
feel_like: "احساس مانند"
|
||||
relative_humidity: "رطوبت نسبی"
|
||||
clear_view_distance: "فاصله دید شفاف"
|
||||
atmospheric_pressure: "فشار جو"
|
||||
sky_coverage: "پوشش آسمان"
|
||||
pagination:
|
||||
showing_items: "نمایش %{from} تا %{to} از %{total} %{items}"
|
||||
items:
|
||||
weather: "رکوردهای آب و هوا"
|
||||
default: "موارد"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%b %d"
|
||||
long: "%B %d, %Y"
|
67
config/locales/fr.yml
Normal file
67
config/locales/fr.yml
Normal file
@ -0,0 +1,67 @@
|
||||
fr:
|
||||
hello: "Bonjour le monde"
|
||||
brand:
|
||||
name: "Today AI Weather"
|
||||
title:
|
||||
cities: "Villes"
|
||||
arts: "Arts"
|
||||
sign_in: "Se connecter"
|
||||
sign_out: "Se déconnecter"
|
||||
settings: "Paramètres"
|
||||
admin_dashboard: "Tableau de bord administrateur"
|
||||
latest_weather_art: "Derniers arts météorologiques"
|
||||
popular_weather_art: "Arts météorologiques populaires"
|
||||
ai_prompt: "Prompt IA"
|
||||
text:
|
||||
latest_from: "Derniers de"
|
||||
search_cities: "Rechercher des villes..."
|
||||
all_regions: "Toutes les régions"
|
||||
all_countries: "Tous les pays"
|
||||
all_in: "Tout dans"
|
||||
showing: "Affichage"
|
||||
weather_arts: "Arts météorologiques"
|
||||
newest_first: "Plus récents d'abord"
|
||||
oldest_first: "Plus anciens d'abord"
|
||||
cities:
|
||||
title: "Explorer les villes"
|
||||
arts:
|
||||
title: "Galerie d'arts météorologiques"
|
||||
subtitle: "Découvrez l'art météorologique généré par l'IA des villes du monde entier"
|
||||
home:
|
||||
headline_html: "Là où la météo rencontre<br>l'Intelligence Artificielle"
|
||||
subtitle:
|
||||
Découvrez la météo à travers le prisme de l'art généré par l'IA,
|
||||
apportant une nouvelle perspective aux phénomènes météorologiques quotidiens.
|
||||
button:
|
||||
explore_cities: "Explorer les villes"
|
||||
view_detail: "Voir les détails"
|
||||
view_all_weather_arts: "Voir tous les arts météorologiques"
|
||||
back_to_cities: "Retour aux villes"
|
||||
back_to: "Retour à"
|
||||
card:
|
||||
temperature: "Température"
|
||||
wind: "Vent"
|
||||
humidity: "Humidité"
|
||||
visibility: "Visibilité"
|
||||
pressure: "Pression"
|
||||
cloud_cover: "Couverture nuageuse"
|
||||
feel_like: "Ressenti"
|
||||
relative_humidity: "Humidité relative"
|
||||
clear_view_distance: "Distance de visibilité claire"
|
||||
atmospheric_pressure: "Pression atmosphérique"
|
||||
sky_coverage: "Couverture du ciel"
|
||||
pagination:
|
||||
showing_items: "Affichage de %{from} à %{to} sur %{total} %{items}"
|
||||
items:
|
||||
weather: "relevés météorologiques"
|
||||
default: "éléments"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%b %d"
|
||||
long: "%d %B %Y"
|
67
config/locales/hi.yml
Normal file
67
config/locales/hi.yml
Normal file
@ -0,0 +1,67 @@
|
||||
hi:
|
||||
hello: "नमस्ते दुनिया"
|
||||
brand:
|
||||
name: "टुडे एआई वेदर"
|
||||
title:
|
||||
cities: "शहर"
|
||||
arts: "कला"
|
||||
sign_in: "साइन इन"
|
||||
sign_out: "साइन आउट"
|
||||
settings: "सेटिंग्स"
|
||||
admin_dashboard: "एडमिन डैशबोर्ड"
|
||||
latest_weather_art: "नवीनतम मौसम कला"
|
||||
popular_weather_art: "लोकप्रिय मौसम कला"
|
||||
ai_prompt: "एआई प्रॉम्प्ट"
|
||||
text:
|
||||
latest_from: "से नवीनतम"
|
||||
search_cities: "शहर खोजें..."
|
||||
all_regions: "सभी क्षेत्र"
|
||||
all_countries: "सभी देश"
|
||||
all_in: "सभी में"
|
||||
showing: "दिखा रहा है"
|
||||
weather_arts: "मौसम कला"
|
||||
newest_first: "नवीनतम पहले"
|
||||
oldest_first: "सबसे पुराना पहले"
|
||||
cities:
|
||||
title: "शहरों की खोज करें"
|
||||
arts:
|
||||
title: "मौसम कला गैलरी"
|
||||
subtitle: "दुनिया भर के शहरों से एआई-जनित मौसम कला की खोज करें"
|
||||
home:
|
||||
headline_html: जहां मौसम मिलता है<br>कृत्रिम बुद्धिमत्ता
|
||||
subtitle:
|
||||
एआई-जनित कला के माध्यम से मौसम का अनुभव करें,
|
||||
दैनिक मौसम संबंधी घटनाओं को एक नया दृष्टिकोण प्रदान करें।
|
||||
button:
|
||||
explore_cities: "शहरों की खोज करें"
|
||||
view_detail: "विवरण देखें"
|
||||
view_all_weather_arts: "सभी मौसम कला देखें"
|
||||
back_to_cities: "शहरों पर वापस जाएं"
|
||||
back_to: "वापस जाएं"
|
||||
card:
|
||||
temperature: "तापमान"
|
||||
wind: "हवा"
|
||||
humidity: "नमी"
|
||||
visibility: "दृश्यता"
|
||||
pressure: "दबाव"
|
||||
cloud_cover: "बादल छाए"
|
||||
feel_like: "महसूस होता है"
|
||||
relative_humidity: "सापेक्ष आर्द्रता"
|
||||
clear_view_distance: "स्पष्ट दृश्य दूरी"
|
||||
atmospheric_pressure: "वायुमंडलीय दबाव"
|
||||
sky_coverage: "आकाश कवरेज"
|
||||
pagination:
|
||||
showing_items: "%{total} %{items} में से %{from} से %{to} तक दिखा रहा है"
|
||||
items:
|
||||
weather: "मौसम रिकॉर्ड"
|
||||
default: "आइटम"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%b %d"
|
||||
long: "%B %d, %Y"
|
67
config/locales/hr.yml
Normal file
67
config/locales/hr.yml
Normal file
@ -0,0 +1,67 @@
|
||||
hr:
|
||||
hello: "Pozdrav svijete"
|
||||
brand:
|
||||
name: "Today AI Weather"
|
||||
title:
|
||||
cities: "Gradovi"
|
||||
arts: "Umjetnost"
|
||||
sign_in: "Prijava"
|
||||
sign_out: "Odjava"
|
||||
settings: "Postavke"
|
||||
admin_dashboard: "Administratorska ploča"
|
||||
latest_weather_art: "Najnovija vremenska umjetnost"
|
||||
popular_weather_art: "Popularna vremenska umjetnost"
|
||||
ai_prompt: "AI upit"
|
||||
text:
|
||||
latest_from: "Najnovije od"
|
||||
search_cities: "Pretraži gradove..."
|
||||
all_regions: "Sve regije"
|
||||
all_countries: "Sve države"
|
||||
all_in: "Sve u"
|
||||
showing: "Prikazuje se"
|
||||
weather_arts: "Vremenska umjetnost"
|
||||
newest_first: "Najnovije prvo"
|
||||
oldest_first: "Najstarije prvo"
|
||||
cities:
|
||||
title: "Istražite gradove"
|
||||
arts:
|
||||
title: "Galerija vremenske umjetnosti"
|
||||
subtitle: "Otkrijte umjetnost vremena generiranu AI-em iz gradova širom svijeta"
|
||||
home:
|
||||
headline_html: Gdje se vrijeme susreće<br>s umjetnom inteligencijom
|
||||
subtitle:
|
||||
Doživite vrijeme kroz objektiv umjetnosti generirane AI-em,
|
||||
donoseći novu perspektivu svakodnevnim meteorološkim pojavama.
|
||||
button:
|
||||
explore_cities: "Istražite gradove"
|
||||
view_detail: "Pogledaj detalje"
|
||||
view_all_weather_arts: "Pogledaj svu vremensku umjetnost"
|
||||
back_to_cities: "Natrag na gradove"
|
||||
back_to: "Natrag na"
|
||||
card:
|
||||
temperature: "Temperatura"
|
||||
wind: "Vjetar"
|
||||
humidity: "Vlažnost"
|
||||
visibility: "Vidljivost"
|
||||
pressure: "Tlak"
|
||||
cloud_cover: "Naoblaka"
|
||||
feel_like: "Osjeća se kao"
|
||||
relative_humidity: "Relativna vlažnost"
|
||||
clear_view_distance: "Udaljenost čistog pogleda"
|
||||
atmospheric_pressure: "Atmosferski tlak"
|
||||
sky_coverage: "Pokrivenost neba"
|
||||
pagination:
|
||||
showing_items: "Prikazuje se %{from} do %{to} od %{total} %{items}"
|
||||
items:
|
||||
weather: "vremenskih zapisa"
|
||||
default: "stavki"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%b %d"
|
||||
long: "%d. %B %Y."
|
67
config/locales/it.yml
Normal file
67
config/locales/it.yml
Normal file
@ -0,0 +1,67 @@
|
||||
it:
|
||||
hello: "Ciao mondo"
|
||||
brand:
|
||||
name: "Today AI Weather"
|
||||
title:
|
||||
cities: "Città"
|
||||
arts: "Arte"
|
||||
sign_in: "Accedi"
|
||||
sign_out: "Esci"
|
||||
settings: "Impostazioni"
|
||||
admin_dashboard: "Dashboard Amministratore"
|
||||
latest_weather_art: "Ultima Arte Meteorologica"
|
||||
popular_weather_art: "Arte Meteorologica Popolare"
|
||||
ai_prompt: "Prompt IA"
|
||||
text:
|
||||
latest_from: "Ultimi da"
|
||||
search_cities: "Cerca città..."
|
||||
all_regions: "Tutte le Regioni"
|
||||
all_countries: "Tutti i Paesi"
|
||||
all_in: "Tutto in"
|
||||
showing: "Mostrando"
|
||||
weather_arts: "Arte Meteorologica"
|
||||
newest_first: "Prima i più Recenti"
|
||||
oldest_first: "Prima i più Vecchi"
|
||||
cities:
|
||||
title: "Esplora Città"
|
||||
arts:
|
||||
title: "Galleria Arte Meteorologica"
|
||||
subtitle: "Scopri l'arte meteorologica generata dall'IA dalle città di tutto il mondo"
|
||||
home:
|
||||
headline_html: Dove il Meteo Incontra<br>l'Intelligenza Artificiale
|
||||
subtitle:
|
||||
Vivi il meteo attraverso la lente dell'arte generata dall'IA,
|
||||
portando una nuova prospettiva ai fenomeni meteorologici quotidiani.
|
||||
button:
|
||||
explore_cities: "Esplora Città"
|
||||
view_detail: "Visualizza Dettagli"
|
||||
view_all_weather_arts: "Visualizza Tutta l'Arte Meteorologica"
|
||||
back_to_cities: "Torna alle Città"
|
||||
back_to: "Torna a"
|
||||
card:
|
||||
temperature: "Temperatura"
|
||||
wind: "Vento"
|
||||
humidity: "Umidità"
|
||||
visibility: "Visibilità"
|
||||
pressure: "Pressione"
|
||||
cloud_cover: "Copertura Nuvolosa"
|
||||
feel_like: "Percepita"
|
||||
relative_humidity: "Umidità relativa"
|
||||
clear_view_distance: "Distanza di visibilità"
|
||||
atmospheric_pressure: "Pressione atmosferica"
|
||||
sky_coverage: "Copertura del cielo"
|
||||
pagination:
|
||||
showing_items: "Mostrando da %{from} a %{to} di %{total} %{items}"
|
||||
items:
|
||||
weather: "registrazioni meteo"
|
||||
default: "elementi"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%b %d"
|
||||
long: "%d %B %Y"
|
66
config/locales/ja.yml
Normal file
66
config/locales/ja.yml
Normal file
@ -0,0 +1,66 @@
|
||||
ja:
|
||||
hello: "こんにちは世界"
|
||||
brand:
|
||||
name: "今日のAI天気"
|
||||
title:
|
||||
cities: "都市"
|
||||
arts: "アート"
|
||||
sign_in: "サインイン"
|
||||
sign_out: "サインアウト"
|
||||
settings: "設定"
|
||||
admin_dashboard: "管理者ダッシュボード"
|
||||
latest_weather_art: "最新の天気アート"
|
||||
popular_weather_art: "人気の天気アート"
|
||||
ai_prompt: "AIプロンプト"
|
||||
text:
|
||||
latest_from: "最新情報"
|
||||
search_cities: "都市を検索..."
|
||||
all_regions: "すべての地域"
|
||||
all_countries: "すべての国"
|
||||
all_in: "すべて含む"
|
||||
showing: "表示中"
|
||||
weather_arts: "天気アート"
|
||||
newest_first: "最新順"
|
||||
oldest_first: "古い順"
|
||||
cities:
|
||||
title: "都市を探る"
|
||||
arts:
|
||||
title: "天気アートギャラリー"
|
||||
subtitle: "世界中の都市から生成されたAI天気アートを発見"
|
||||
home:
|
||||
headline_html: 天気が出会う場所<br>人工知能
|
||||
subtitle:
|
||||
AI生成アートのレンズを通して天気を体験し、
|
||||
日常の気象現象に新しい視点をもたらします。
|
||||
button:
|
||||
explore_cities: "都市を探る"
|
||||
view_detail: "詳細を見る"
|
||||
view_all_weather_arts: "すべての天気アートを見る"
|
||||
back_to_cities: "都市に戻る"
|
||||
back_to: "戻る"
|
||||
card:
|
||||
temperature: "温度"
|
||||
wind: "風"
|
||||
humidity: "湿度"
|
||||
visibility: "視界"
|
||||
pressure: "圧力"
|
||||
cloud_cover: "雲の覆い"
|
||||
feel_like: "体感温度"
|
||||
relative_humidity: "相対湿度"
|
||||
clear_view_distance: "クリアビュー距離"
|
||||
atmospheric_pressure: "大気圧"
|
||||
sky_coverage: "空の覆い"
|
||||
pagination:
|
||||
showing_items: "合計 %{total} %{items} のうち %{from} から %{to} まで表示"
|
||||
items:
|
||||
weather: "天気記録"
|
||||
default: "アイテム"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
short: "%Y-%m-%d"
|
||||
long: "%Y 年 %m 月 %d 日"
|
66
config/locales/ko.yml
Normal file
66
config/locales/ko.yml
Normal file
@ -0,0 +1,66 @@
|
||||
ko:
|
||||
hello: "안녕하세요 세계"
|
||||
brand:
|
||||
name: "오늘의 AI 날씨"
|
||||
title:
|
||||
cities: "도시"
|
||||
arts: "예술"
|
||||
sign_in: "로그인"
|
||||
sign_out: "로그아웃"
|
||||
settings: "설정"
|
||||
admin_dashboard: "관리자 대시보드"
|
||||
latest_weather_art: "최신 날씨 예술"
|
||||
popular_weather_art: "인기 있는 날씨 예술"
|
||||
ai_prompt: "AI 프롬프트"
|
||||
text:
|
||||
latest_from: "최신 소식"
|
||||
search_cities: "도시 검색..."
|
||||
all_regions: "모든 지역"
|
||||
all_countries: "모든 국가"
|
||||
all_in: "모두 포함"
|
||||
showing: "표시 중"
|
||||
weather_arts: "날씨 예술"
|
||||
newest_first: "최신순"
|
||||
oldest_first: "오래된 순"
|
||||
cities:
|
||||
title: "도시 탐험"
|
||||
arts:
|
||||
title: "날씨 예술 갤러리"
|
||||
subtitle: "전 세계 도시에서 생성된 AI 날씨 예술 발견하기"
|
||||
home:
|
||||
headline_html: 날씨가 만나는 곳<br>인공지능
|
||||
subtitle:
|
||||
AI 생성 예술의 렌즈를 통해 날씨를 경험하세요,
|
||||
일상적인 기상 현상에 대한 새로운 관점을 제공합니다.
|
||||
button:
|
||||
explore_cities: "도시 탐험"
|
||||
view_detail: "상세 보기"
|
||||
view_all_weather_arts: "모든 날씨 예술 보기"
|
||||
back_to_cities: "도시로 돌아가기"
|
||||
back_to: "돌아가기"
|
||||
card:
|
||||
temperature: "온도"
|
||||
wind: "바람"
|
||||
humidity: "습도"
|
||||
visibility: "가시성"
|
||||
pressure: "압력"
|
||||
cloud_cover: "구름 덮개"
|
||||
feel_like: "체감 온도"
|
||||
relative_humidity: "상대 습도"
|
||||
clear_view_distance: "맑은 시야 거리"
|
||||
atmospheric_pressure: "대기압"
|
||||
sky_coverage: "하늘 덮개"
|
||||
pagination:
|
||||
showing_items: "총 %{total} %{items} 중 %{from}에서 %{to}까지 표시"
|
||||
items:
|
||||
weather: "날씨 기록"
|
||||
default: "항목"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
short: "%Y-%m-%d"
|
||||
long: "%Y 년 %m 월 %d 일"
|
67
config/locales/pl.yml
Normal file
67
config/locales/pl.yml
Normal file
@ -0,0 +1,67 @@
|
||||
pl:
|
||||
hello: "Witaj świecie"
|
||||
brand:
|
||||
name: "Today AI Weather"
|
||||
title:
|
||||
cities: "Miasta"
|
||||
arts: "Sztuka"
|
||||
sign_in: "Zaloguj się"
|
||||
sign_out: "Wyloguj się"
|
||||
settings: "Ustawienia"
|
||||
admin_dashboard: "Panel administratora"
|
||||
latest_weather_art: "Najnowsza sztuka pogodowa"
|
||||
popular_weather_art: "Popularna sztuka pogodowa"
|
||||
ai_prompt: "Prompt AI"
|
||||
text:
|
||||
latest_from: "Najnowsze z"
|
||||
search_cities: "Szukaj miast..."
|
||||
all_regions: "Wszystkie regiony"
|
||||
all_countries: "Wszystkie kraje"
|
||||
all_in: "Wszystko w"
|
||||
showing: "Wyświetlanie"
|
||||
weather_arts: "Sztuka pogodowa"
|
||||
newest_first: "Od najnowszych"
|
||||
oldest_first: "Od najstarszych"
|
||||
cities:
|
||||
title: "Odkryj miasta"
|
||||
arts:
|
||||
title: "Galeria sztuki pogodowej"
|
||||
subtitle: "Odkryj sztukę pogodową generowaną przez AI z miast na całym świecie"
|
||||
home:
|
||||
headline_html: Gdzie pogoda spotyka się<br>ze sztuczną inteligencją
|
||||
subtitle:
|
||||
Doświadcz pogody przez pryzmat sztuki generowanej przez AI,
|
||||
wprowadzając nową perspektywę do codziennych zjawisk meteorologicznych.
|
||||
button:
|
||||
explore_cities: "Odkryj miasta"
|
||||
view_detail: "Zobacz szczegóły"
|
||||
view_all_weather_arts: "Zobacz całą sztukę pogodową"
|
||||
back_to_cities: "Powrót do miast"
|
||||
back_to: "Powrót do"
|
||||
card:
|
||||
temperature: "Temperatura"
|
||||
wind: "Wiatr"
|
||||
humidity: "Wilgotność"
|
||||
visibility: "Widoczność"
|
||||
pressure: "Ciśnienie"
|
||||
cloud_cover: "Zachmurzenie"
|
||||
feel_like: "Odczuwalna"
|
||||
relative_humidity: "Wilgotność względna"
|
||||
clear_view_distance: "Zasięg widoczności"
|
||||
atmospheric_pressure: "Ciśnienie atmosferyczne"
|
||||
sky_coverage: "Pokrycie nieba"
|
||||
pagination:
|
||||
showing_items: "Wyświetlanie %{from} do %{to} z %{total} %{items}"
|
||||
items:
|
||||
weather: "zapisów pogody"
|
||||
default: "elementów"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%b %d"
|
||||
long: "%d %B %Y"
|
67
config/locales/pt-BR.yml
Normal file
67
config/locales/pt-BR.yml
Normal file
@ -0,0 +1,67 @@
|
||||
pt-BR:
|
||||
hello: "Olá mundo"
|
||||
brand:
|
||||
name: "Today AI Weather"
|
||||
title:
|
||||
cities: "Cidades"
|
||||
arts: "Artes"
|
||||
sign_in: "Entrar"
|
||||
sign_out: "Sair"
|
||||
settings: "Configurações"
|
||||
admin_dashboard: "Painel de Administração"
|
||||
latest_weather_art: "Arte Meteorológica Mais Recente"
|
||||
popular_weather_art: "Arte Meteorológica Popular"
|
||||
ai_prompt: "Prompt de IA"
|
||||
text:
|
||||
latest_from: "Mais recente de"
|
||||
search_cities: "Pesquisar cidades..."
|
||||
all_regions: "Todas as Regiões"
|
||||
all_countries: "Todos os Países"
|
||||
all_in: "Tudo em"
|
||||
showing: "Mostrando"
|
||||
weather_arts: "Artes Meteorológicas"
|
||||
newest_first: "Mais Recentes Primeiro"
|
||||
oldest_first: "Mais Antigos Primeiro"
|
||||
cities:
|
||||
title: "Explorar Cidades"
|
||||
arts:
|
||||
title: "Galeria de Artes Meteorológicas"
|
||||
subtitle: "Descubra arte meteorológica gerada por IA de cidades ao redor do mundo"
|
||||
home:
|
||||
headline_html: Onde o Clima Encontra<br>a Inteligência Artificial
|
||||
subtitle:
|
||||
Experimente o clima através das lentes da arte gerada por IA,
|
||||
trazendo uma nova perspectiva para os fenômenos meteorológicos diários.
|
||||
button:
|
||||
explore_cities: "Explorar Cidades"
|
||||
view_detail: "Ver Detalhes"
|
||||
view_all_weather_arts: "Ver Todas as Artes Meteorológicas"
|
||||
back_to_cities: "Voltar para Cidades"
|
||||
back_to: "Voltar para"
|
||||
card:
|
||||
temperature: "Temperatura"
|
||||
wind: "Vento"
|
||||
humidity: "Umidade"
|
||||
visibility: "Visibilidade"
|
||||
pressure: "Pressão"
|
||||
cloud_cover: "Cobertura de Nuvens"
|
||||
feel_like: "Sensação térmica"
|
||||
relative_humidity: "Umidade relativa"
|
||||
clear_view_distance: "Distância de visão clara"
|
||||
atmospheric_pressure: "Pressão atmosférica"
|
||||
sky_coverage: "Cobertura do céu"
|
||||
pagination:
|
||||
showing_items: "Mostrando %{from} até %{to} de %{total} %{items}"
|
||||
items:
|
||||
weather: "registros meteorológicos"
|
||||
default: "itens"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%d/%m/%Y"
|
||||
short: "%d %b"
|
||||
long: "%d de %B de %Y"
|
67
config/locales/pt.yml
Normal file
67
config/locales/pt.yml
Normal file
@ -0,0 +1,67 @@
|
||||
pt:
|
||||
hello: "Olá mundo"
|
||||
brand:
|
||||
name: "Today AI Weather"
|
||||
title:
|
||||
cities: "Cidades"
|
||||
arts: "Artes"
|
||||
sign_in: "Entrar"
|
||||
sign_out: "Sair"
|
||||
settings: "Configurações"
|
||||
admin_dashboard: "Painel de Administração"
|
||||
latest_weather_art: "Arte Meteorológica Mais Recente"
|
||||
popular_weather_art: "Arte Meteorológica Popular"
|
||||
ai_prompt: "Prompt de IA"
|
||||
text:
|
||||
latest_from: "Mais recente de"
|
||||
search_cities: "Pesquisar cidades..."
|
||||
all_regions: "Todas as Regiões"
|
||||
all_countries: "Todos os Países"
|
||||
all_in: "Tudo em"
|
||||
showing: "Mostrando"
|
||||
weather_arts: "Artes Meteorológicas"
|
||||
newest_first: "Mais Recentes Primeiro"
|
||||
oldest_first: "Mais Antigos Primeiro"
|
||||
cities:
|
||||
title: "Explorar Cidades"
|
||||
arts:
|
||||
title: "Galeria de Artes Meteorológicas"
|
||||
subtitle: "Descubra arte meteorológica gerada por IA de cidades ao redor do mundo"
|
||||
home:
|
||||
headline_html: Onde o Clima Encontra<br>a Inteligência Artificial
|
||||
subtitle:
|
||||
Experimente o clima através das lentes da arte gerada por IA,
|
||||
trazendo uma nova perspectiva para os fenômenos meteorológicos diários.
|
||||
button:
|
||||
explore_cities: "Explorar Cidades"
|
||||
view_detail: "Ver Detalhes"
|
||||
view_all_weather_arts: "Ver Todas as Artes Meteorológicas"
|
||||
back_to_cities: "Voltar para Cidades"
|
||||
back_to: "Voltar para"
|
||||
card:
|
||||
temperature: "Temperatura"
|
||||
wind: "Vento"
|
||||
humidity: "Umidade"
|
||||
visibility: "Visibilidade"
|
||||
pressure: "Pressão"
|
||||
cloud_cover: "Cobertura de Nuvens"
|
||||
feel_like: "Sensação térmica"
|
||||
relative_humidity: "Umidade relativa"
|
||||
clear_view_distance: "Distância de visão clara"
|
||||
atmospheric_pressure: "Pressão atmosférica"
|
||||
sky_coverage: "Cobertura do céu"
|
||||
pagination:
|
||||
showing_items: "Mostrando %{from} a %{to} de %{total} %{items}"
|
||||
items:
|
||||
weather: "registros meteorológicos"
|
||||
default: "itens"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%d de %b"
|
||||
long: "%d de %B de %Y"
|
@ -1,15 +0,0 @@
|
||||
en:
|
||||
regions:
|
||||
AS: 'Asia'
|
||||
SA: 'South Asia'
|
||||
SEA: 'Southeast Asia'
|
||||
EA: 'East Asia'
|
||||
ME: 'Middle East'
|
||||
AF: 'Africa'
|
||||
NA: 'North Africa'
|
||||
SSA: 'Sub-Saharan Africa'
|
||||
EU: 'Europe'
|
||||
NAM: 'North America'
|
||||
SAM: 'South America'
|
||||
CAM: 'Central America'
|
||||
OC: 'Oceania'
|
@ -1,15 +0,0 @@
|
||||
zh-CN:
|
||||
regions:
|
||||
AS: '亚洲'
|
||||
SA: '南亚'
|
||||
SEA: '东南亚'
|
||||
EA: '东亚'
|
||||
ME: '中东'
|
||||
AF: '非洲'
|
||||
NA: '北非'
|
||||
SSA: '撒哈拉以南非洲'
|
||||
EU: '欧洲'
|
||||
NAM: '北美洲'
|
||||
SAM: '南美洲'
|
||||
CAM: '中美洲'
|
||||
OC: '大洋洲'
|
67
config/locales/ru.yml
Normal file
67
config/locales/ru.yml
Normal file
@ -0,0 +1,67 @@
|
||||
ru:
|
||||
hello: "Привет, мир"
|
||||
brand:
|
||||
name: "Today AI Weather"
|
||||
title:
|
||||
cities: "Города"
|
||||
arts: "Искусство"
|
||||
sign_in: "Войти"
|
||||
sign_out: "Выйти"
|
||||
settings: "Настройки"
|
||||
admin_dashboard: "Панель администратора"
|
||||
latest_weather_art: "Последнее погодное искусство"
|
||||
popular_weather_art: "Популярное погодное искусство"
|
||||
ai_prompt: "AI подсказка"
|
||||
text:
|
||||
latest_from: "Последнее от"
|
||||
search_cities: "Поиск городов..."
|
||||
all_regions: "Все регионы"
|
||||
all_countries: "Все страны"
|
||||
all_in: "Все в"
|
||||
showing: "Показано"
|
||||
weather_arts: "Погодное искусство"
|
||||
newest_first: "Сначала новые"
|
||||
oldest_first: "Сначала старые"
|
||||
cities:
|
||||
title: "Исследуйте города"
|
||||
arts:
|
||||
title: "Галерея погодного искусства"
|
||||
subtitle: "Откройте для себя AI-сгенерированное погодное искусство из городов по всему миру"
|
||||
home:
|
||||
headline_html: Где погода встречается<br>с искусственным интеллектом
|
||||
subtitle:
|
||||
Познакомьтесь с погодой через призму искусства, созданного искусственным интеллектом,
|
||||
открывая новый взгляд на ежедневные метеорологические явления.
|
||||
button:
|
||||
explore_cities: "Исследовать города"
|
||||
view_detail: "Посмотреть детали"
|
||||
view_all_weather_arts: "Посмотреть все погодное искусство"
|
||||
back_to_cities: "Вернуться к городам"
|
||||
back_to: "Вернуться к"
|
||||
card:
|
||||
temperature: "Температура"
|
||||
wind: "Ветер"
|
||||
humidity: "Влажность"
|
||||
visibility: "Видимость"
|
||||
pressure: "Давление"
|
||||
cloud_cover: "Облачность"
|
||||
feel_like: "Ощущается как"
|
||||
relative_humidity: "Относительная влажность"
|
||||
clear_view_distance: "Дальность видимости"
|
||||
atmospheric_pressure: "Атмосферное давление"
|
||||
sky_coverage: "Покрытие неба"
|
||||
pagination:
|
||||
showing_items: "Показано с %{from} по %{to} из %{total} %{items}"
|
||||
items:
|
||||
weather: "записей о погоде"
|
||||
default: "элементов"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%b %d"
|
||||
long: "%d %B %Y"
|
67
config/locales/tr.yml
Normal file
67
config/locales/tr.yml
Normal file
@ -0,0 +1,67 @@
|
||||
tr:
|
||||
hello: "Merhaba dünya"
|
||||
brand:
|
||||
name: "Today AI Weather"
|
||||
title:
|
||||
cities: "Şehirler"
|
||||
arts: "Sanat"
|
||||
sign_in: "Giriş yap"
|
||||
sign_out: "Çıkış yap"
|
||||
settings: "Ayarlar"
|
||||
admin_dashboard: "Yönetici Paneli"
|
||||
latest_weather_art: "En Son Hava Durumu Sanatı"
|
||||
popular_weather_art: "Popüler Hava Durumu Sanatı"
|
||||
ai_prompt: "AI Komut"
|
||||
text:
|
||||
latest_from: "En son"
|
||||
search_cities: "Şehirleri ara..."
|
||||
all_regions: "Tüm Bölgeler"
|
||||
all_countries: "Tüm Ülkeler"
|
||||
all_in: "Tümü"
|
||||
showing: "Gösteriliyor"
|
||||
weather_arts: "Hava Durumu Sanatları"
|
||||
newest_first: "En Yeni Önce"
|
||||
oldest_first: "En Eski Önce"
|
||||
cities:
|
||||
title: "Şehirleri Keşfet"
|
||||
arts:
|
||||
title: "Hava Durumu Sanat Galerisi"
|
||||
subtitle: "Dünya genelindeki şehirlerden AI tarafından oluşturulan hava durumu sanatını keşfedin"
|
||||
home:
|
||||
headline_html: "Hava Durumu<br>Yapay Zeka ile Buluşuyor"
|
||||
subtitle:
|
||||
AI tarafından oluşturulan sanat perspektifinden hava durumunu deneyimleyin,
|
||||
günlük meteorolojik olaylara yeni bir bakış açısı getirin.
|
||||
button:
|
||||
explore_cities: "Şehirleri Keşfet"
|
||||
view_detail: "Detayları Görüntüle"
|
||||
view_all_weather_arts: "Tüm Hava Durumu Sanatlarını Görüntüle"
|
||||
back_to_cities: "Şehirlere Geri Dön"
|
||||
back_to: "Geri Dön"
|
||||
card:
|
||||
temperature: "Sıcaklık"
|
||||
wind: "Rüzgar"
|
||||
humidity: "Nem"
|
||||
visibility: "Görüş Mesafesi"
|
||||
pressure: "Basınç"
|
||||
cloud_cover: "Bulut Örtüsü"
|
||||
feel_like: "Hissedilen"
|
||||
relative_humidity: "Bağıl nem"
|
||||
clear_view_distance: "Net görüş mesafesi"
|
||||
atmospheric_pressure: "Atmosfer basıncı"
|
||||
sky_coverage: "Gökyüzü kapsama"
|
||||
pagination:
|
||||
showing_items: "%{total} %{items} içinden %{from} ile %{to} arası gösteriliyor"
|
||||
items:
|
||||
weather: "hava durumu kayıtları"
|
||||
default: "öğe"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%d %b"
|
||||
long: "%d %B %Y"
|
67
config/locales/uk.yml
Normal file
67
config/locales/uk.yml
Normal file
@ -0,0 +1,67 @@
|
||||
uk:
|
||||
hello: "Привіт світ"
|
||||
brand:
|
||||
name: "Today AI Weather"
|
||||
title:
|
||||
cities: "Міста"
|
||||
arts: "Мистецтво"
|
||||
sign_in: "Увійти"
|
||||
sign_out: "Вийти"
|
||||
settings: "Налаштування"
|
||||
admin_dashboard: "Панель адміністратора"
|
||||
latest_weather_art: "Останнє погодне мистецтво"
|
||||
popular_weather_art: "Популярне погодне мистецтво"
|
||||
ai_prompt: "AI підказка"
|
||||
text:
|
||||
latest_from: "Останнє від"
|
||||
search_cities: "Пошук міст..."
|
||||
all_regions: "Всі регіони"
|
||||
all_countries: "Всі країни"
|
||||
all_in: "Все в"
|
||||
showing: "Показано"
|
||||
weather_arts: "Погодне мистецтво"
|
||||
newest_first: "Спочатку нові"
|
||||
oldest_first: "Спочатку старі"
|
||||
cities:
|
||||
title: "Огляд міст"
|
||||
arts:
|
||||
title: "Галерея погодного мистецтва"
|
||||
subtitle: "Відкрийте для себе згенероване ШІ погодне мистецтво з міст по всьому світу"
|
||||
home:
|
||||
headline_html: Де погода зустрічається<br>зі штучним інтелектом
|
||||
subtitle:
|
||||
Відчуйте погоду через призму мистецтва, створеного ШІ,
|
||||
що дає новий погляд на щоденні метеорологічні явища.
|
||||
button:
|
||||
explore_cities: "Огляд міст"
|
||||
view_detail: "Переглянути деталі"
|
||||
view_all_weather_arts: "Переглянути все погодне мистецтво"
|
||||
back_to_cities: "Назад до міст"
|
||||
back_to: "Назад до"
|
||||
card:
|
||||
temperature: "Температура"
|
||||
wind: "Вітер"
|
||||
humidity: "Вологість"
|
||||
visibility: "Видимість"
|
||||
pressure: "Тиск"
|
||||
cloud_cover: "Хмарність"
|
||||
feel_like: "Відчувається як"
|
||||
relative_humidity: "Відносна вологість"
|
||||
clear_view_distance: "Дальність видимості"
|
||||
atmospheric_pressure: "Атмосферний тиск"
|
||||
sky_coverage: "Покриття неба"
|
||||
pagination:
|
||||
showing_items: "Показано %{from} до %{to} з %{total} %{items}"
|
||||
items:
|
||||
weather: "погодних записів"
|
||||
default: "елементів"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%b %d"
|
||||
long: "%d %B %Y"
|
67
config/locales/ur.yml
Normal file
67
config/locales/ur.yml
Normal file
@ -0,0 +1,67 @@
|
||||
ur:
|
||||
hello: "ہیلو دنیا"
|
||||
brand:
|
||||
name: "ٹوڈے اے آئی ویدر"
|
||||
title:
|
||||
cities: "شہر"
|
||||
arts: "فن"
|
||||
sign_in: "سائن ان"
|
||||
sign_out: "سائن آؤٹ"
|
||||
settings: "ترتیبات"
|
||||
admin_dashboard: "ایڈمن ڈیش بورڈ"
|
||||
latest_weather_art: "تازہ ترین موسمی فن"
|
||||
popular_weather_art: "مقبول موسمی فن"
|
||||
ai_prompt: "اے آئی پرامپٹ"
|
||||
text:
|
||||
latest_from: "تازہ ترین"
|
||||
search_cities: "شہروں کی تلاش..."
|
||||
all_regions: "تمام علاقے"
|
||||
all_countries: "تمام ممالک"
|
||||
all_in: "تمام"
|
||||
showing: "دکھا رہا ہے"
|
||||
weather_arts: "موسمی فن"
|
||||
newest_first: "نیا پہلے"
|
||||
oldest_first: "پرانا پہلے"
|
||||
cities:
|
||||
title: "شہروں کی دریافت"
|
||||
arts:
|
||||
title: "موسمی فن گیلری"
|
||||
subtitle: "دنیا بھر کے شہروں سے اے آئی سے تیار کردہ موسمی فن دریافت کریں"
|
||||
home:
|
||||
headline_html: جہاں موسم<br>مصنوعی ذہانت سے ملتا ہے
|
||||
subtitle:
|
||||
اے آئی سے تیار کردہ فن کے ذریعے موسم کا تجربہ کریں،
|
||||
روزمرہ موسمیاتی مظاہر کو ایک نیا نظریہ فراہم کرتا ہے۔
|
||||
button:
|
||||
explore_cities: "شہروں کی دریافت"
|
||||
view_detail: "تفصیلات دیکھیں"
|
||||
view_all_weather_arts: "تمام موسمی فن دیکھیں"
|
||||
back_to_cities: "شہروں کی طرف واپس"
|
||||
back_to: "واپس"
|
||||
card:
|
||||
temperature: "درجہ حرارت"
|
||||
wind: "ہوا"
|
||||
humidity: "نمی"
|
||||
visibility: "دید"
|
||||
pressure: "دباؤ"
|
||||
cloud_cover: "بادل"
|
||||
feel_like: "محسوس ہوتا ہے"
|
||||
relative_humidity: "اضافی نمی"
|
||||
clear_view_distance: "صاف نظر کی دوری"
|
||||
atmospheric_pressure: "ہوائی دباؤ"
|
||||
sky_coverage: "آسمانی احاطہ"
|
||||
pagination:
|
||||
showing_items: "%{items} کے %{total} میں سے %{from} سے %{to} تک دکھا رہا ہے"
|
||||
items:
|
||||
weather: "موسمی ریکارڈز"
|
||||
default: "آئٹمز"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
default: "%Y-%m-%d"
|
||||
short: "%b %d"
|
||||
long: "%B %d, %Y"
|
65
config/locales/zh-CN.yml
Normal file
65
config/locales/zh-CN.yml
Normal file
@ -0,0 +1,65 @@
|
||||
zh-CN:
|
||||
hello: "你好"
|
||||
brand:
|
||||
name: "全球艺术天气"
|
||||
title:
|
||||
cities: "城市探索"
|
||||
arts: "艺术巡览"
|
||||
sign_in: "用户登录"
|
||||
sign_out: "退出登录"
|
||||
settings: "系统设置"
|
||||
admin_dashboard: "管理中枢"
|
||||
latest_weather_art: "气象绘卷·新作"
|
||||
popular_weather_art: "气象绘卷·佳作"
|
||||
ai_prompt: "天气描述"
|
||||
text:
|
||||
latest_from: "新至之城"
|
||||
search_cities: "寻城觅境…"
|
||||
all_regions: "寰宇之境"
|
||||
all_countries: "所有国家"
|
||||
all_in: "全部"
|
||||
showing: "映现"
|
||||
weather_arts: "气象艺境"
|
||||
newest_first: "最新优先"
|
||||
oldest_first: "最早优先"
|
||||
cities:
|
||||
title: "云游四海"
|
||||
arts:
|
||||
title: "天象画廊"
|
||||
subtitle: "邂逅寰宇都市AI气象绘卷"
|
||||
home:
|
||||
headline_html: 当气象邂逅<br>人工智能之美
|
||||
subtitle:
|
||||
通过AI生成的艺术视角感受气象,为日常天气现象带来全新解读。
|
||||
button:
|
||||
explore_cities: "云游四海"
|
||||
view_detail: "详阅此卷"
|
||||
view_all_weather_arts: "尽览天工"
|
||||
back_to_cities: "继续探索城市"
|
||||
back_to: "回到"
|
||||
card:
|
||||
temperature: "温度"
|
||||
wind: "风力"
|
||||
humidity: "湿度"
|
||||
visibility: "能见度"
|
||||
pressure: "气压"
|
||||
cloud_cover: "云量"
|
||||
feel_like: "体感温度"
|
||||
relative_humidity: "相对湿度"
|
||||
clear_view_distance: "清晰视距"
|
||||
atmospheric_pressure: "大气压力"
|
||||
sky_coverage: "天空覆盖"
|
||||
pagination:
|
||||
showing_items: "显示第 %{from} 到第 %{to} 条,共 %{total} 条%{items}"
|
||||
items:
|
||||
weather: "天气记录"
|
||||
default: "记录"
|
||||
time:
|
||||
formats:
|
||||
time_only: "%H:%M"
|
||||
with_zone: "%{time} %{zone}"
|
||||
date_and_time: "%{date} %{time}"
|
||||
date:
|
||||
formats:
|
||||
short: "%Y-%m-%d"
|
||||
long: "%Y 年 %m 月 %d 日"
|
@ -1,6 +1,7 @@
|
||||
require "sidekiq/web"
|
||||
|
||||
Rails.application.routes.draw do
|
||||
scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do
|
||||
devise_for :users
|
||||
root "home#index"
|
||||
|
||||
@ -24,7 +25,9 @@ Rails.application.routes.draw do
|
||||
get "cities/index"
|
||||
get "cities/show"
|
||||
get "home/index"
|
||||
get "sitemaps", to: "sitemaps#index"
|
||||
get "sitemaps/*path", to: "sitemaps#show", format: false
|
||||
get "feed", to: "rss#feed", format: "rss", as: :rss_feed
|
||||
|
||||
devise_for :admin_users, ActiveAdmin::Devise.config
|
||||
ActiveAdmin.routes(self)
|
||||
@ -46,4 +49,5 @@ Rails.application.routes.draw do
|
||||
|
||||
# Defines the root path route ("/")
|
||||
# root "posts#index"
|
||||
end
|
||||
end
|
||||
|
@ -1,7 +1,7 @@
|
||||
batch_generate_weather:
|
||||
cron: '0 8,18 * * *'
|
||||
cron: '0 */1 * * *'
|
||||
class: BatchGenerateWeatherArtsWorker
|
||||
description: "Generate weather arts every 2 hours"
|
||||
description: "Batch Generate weather arts"
|
||||
enabled: true
|
||||
|
||||
refresh_sitemap:
|
||||
|
@ -2,14 +2,14 @@
|
||||
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)
|
||||
Rails.application.credentials.dig(:minio, :bucket),
|
||||
aws_access_key_id: Rails.application.credentials.dig(:minio, :access_key_id),
|
||||
aws_secret_access_key: Rails.application.credentials.dig(:minio, :secret_access_key),
|
||||
aws_region: Rails.application.credentials.dig(:minio, :region)
|
||||
)
|
||||
SitemapGenerator::Sitemap.sitemaps_path = "sitemaps/"
|
||||
|
||||
SitemapGenerator::Sitemap.default_host = host
|
||||
SitemapGenerator::Sitemap.default_host = "https://pub.r2.todayaiweather.com"
|
||||
|
||||
SitemapGenerator::Sitemap.create do
|
||||
add root_path, changefreq: "daily", priority: 1.0
|
||||
|
@ -19,19 +19,21 @@ build:
|
||||
# bucket: your_own_bucket-<%= Rails.env %>
|
||||
amazon_dev:
|
||||
service: S3
|
||||
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)) %>
|
||||
access_key_id: <%= ENV.fetch("AWS_DEV_ACCESS_KEY_ID", Rails.application.credentials.dig(:minio_dev, :access_key_id)) %>
|
||||
secret_access_key: <%= ENV.fetch("AWS_DEV_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:minio_dev, :secret_access_key)) %>
|
||||
region: <%= ENV.fetch("AWS_DEV_REGION", Rails.application.credentials.dig(:minio_dev, :region)) %>
|
||||
bucket: <%= ENV.fetch("AWS_DEV_BUCKET", Rails.application.credentials.dig(:minio_dev, :bucket)) %>
|
||||
endpoint: <%= ENV.fetch("AWS_DEV_ENDPOINT", Rails.application.credentials.dig(:minio_dev, :endpoint)) %>
|
||||
force_path_style: <%= ENV.fetch("AWS_DEV_FORCE_PATH_STYLE", Rails.application.credentials.dig(:minio_dev, :force_path_style)) %>
|
||||
|
||||
amazon:
|
||||
service: S3
|
||||
access_key_id: <%= ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :access_key_id)) %>
|
||||
secret_access_key: <%= ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :secret_access_key)) %>
|
||||
region: <%= ENV.fetch("AWS_REGION", "wnam") %>
|
||||
bucket: <%= ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:aws, :bucket)) %>
|
||||
endpoint: <%= ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:aws, :endpoint)) %>
|
||||
access_key_id: <%= ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:minio, :access_key_id)) %>
|
||||
secret_access_key: <%= ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:minio, :secret_access_key)) %>
|
||||
region: <%= ENV.fetch("AWS_REGION", Rails.application.credentials.dig(:minio, :region)) %>
|
||||
bucket: <%= ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:minio, :bucket)) %>
|
||||
endpoint: <%= ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:minio, :endpoint)) %>
|
||||
force_path_style: <%= ENV.fetch("AWS_DEV_FORCE_PATH_STYLE", Rails.application.credentials.dig(:minio, :force_path_style)) %>
|
||||
|
||||
# Remember not to checkin your GCS keyfile to a repository
|
||||
# google:
|
||||
|
4
db/schema.rb
generated
4
db/schema.rb
generated
@ -120,7 +120,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_11_035423) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "slug"
|
||||
t.integer "country_id", null: false
|
||||
t.integer "country_id"
|
||||
t.string "state_code"
|
||||
t.string "country_code"
|
||||
t.boolean "flag", default: true
|
||||
@ -150,7 +150,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_11_035423) do
|
||||
t.string "native"
|
||||
t.string "subregion"
|
||||
t.string "nationality"
|
||||
t.text "timezones"
|
||||
t.text "translations"
|
||||
t.decimal "latitude", precision: 10, scale: 8
|
||||
t.decimal "longitude", precision: 11, scale: 8
|
||||
@ -159,6 +158,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_11_035423) do
|
||||
t.boolean "flag", default: true
|
||||
t.string "wiki_data_id"
|
||||
t.bigint "subregion_id"
|
||||
t.text "timezones"
|
||||
t.index ["code"], name: "index_countries_on_code", unique: true
|
||||
t.index ["region_id"], name: "index_countries_on_region_id"
|
||||
t.index ["slug"], name: "index_countries_on_slug", unique: true
|
||||
|
40
db/seeds.rb
40
db/seeds.rb
@ -10,11 +10,6 @@
|
||||
# AdminUser.create!(email: 'admin@example.com', password: 'password', password_confirmation: 'password') if Rails.env.development?
|
||||
AdminUser.create!(email: 'admin@example.com', password: 'password', password_confirmation: 'password')
|
||||
|
||||
# WeatherArt.delete_all
|
||||
# City.delete_all
|
||||
# Country.delete_all
|
||||
# Region.delete_all
|
||||
|
||||
# 创建区域
|
||||
regions = Region.create!([
|
||||
{
|
||||
@ -22,46 +17,14 @@ regions = Region.create!([
|
||||
code: 'AS'
|
||||
# },
|
||||
# {
|
||||
# name: 'Southeast Asia',
|
||||
# code: 'SEA'
|
||||
# },
|
||||
# {
|
||||
# name: 'East Asia',
|
||||
# code: 'EA'
|
||||
# },
|
||||
# {
|
||||
# name: 'Middle East',
|
||||
# code: 'ME'
|
||||
# },
|
||||
# {
|
||||
# name: 'Africa',
|
||||
# code: 'AF'
|
||||
# },
|
||||
# {
|
||||
# name: 'North Africa',
|
||||
# code: 'NA'
|
||||
# },
|
||||
# {
|
||||
# name: 'Sub-Saharan Africa',
|
||||
# code: 'SSA'
|
||||
# },
|
||||
# {
|
||||
# name: 'Europe',
|
||||
# code: 'EU'
|
||||
# },
|
||||
# {
|
||||
# name: 'North America',
|
||||
# code: 'NAM'
|
||||
# },
|
||||
# {
|
||||
# name: 'South America',
|
||||
# code: 'SAM'
|
||||
# },
|
||||
# {
|
||||
# name: 'Central America',
|
||||
# code: 'CAM'
|
||||
# },
|
||||
# {
|
||||
# name: 'Oceania',
|
||||
# code: 'OC'
|
||||
}
|
||||
@ -133,9 +96,6 @@ Country.create!([
|
||||
])
|
||||
|
||||
# 创建城市
|
||||
# Dir[Rails.root.join('db/seeds/cities/*.rb')].sort.each do |file|
|
||||
# require file
|
||||
# end
|
||||
china = Country.find_by code: 'CN'
|
||||
City.create!([
|
||||
{
|
||||
|
@ -1,22 +0,0 @@
|
||||
australia = Country.find_by code: 'AU'
|
||||
|
||||
City.create!([
|
||||
{
|
||||
name: 'Sydney',
|
||||
latitude: -33.8688,
|
||||
longitude: 151.2093,
|
||||
country: australia,
|
||||
timezone: 'Australia/Sydney',
|
||||
active: true,
|
||||
priority: 80
|
||||
},
|
||||
{
|
||||
name: 'Melbourne',
|
||||
latitude: -37.8136,
|
||||
longitude: 144.9631,
|
||||
country: australia,
|
||||
timezone: 'Australia/Melbourne',
|
||||
active: true,
|
||||
priority: 75
|
||||
}
|
||||
])
|
@ -1,13 +0,0 @@
|
||||
bangladesh = Country.find_by code: 'BD'
|
||||
|
||||
City.create!([
|
||||
{
|
||||
name: 'Dhaka',
|
||||
latitude: 23.8103,
|
||||
longitude: 90.4125,
|
||||
country: bangladesh,
|
||||
timezone: 'Asia/Dhaka',
|
||||
active: true,
|
||||
priority: 85
|
||||
}
|
||||
])
|
@ -1,13 +0,0 @@
|
||||
brazil = Country.find_by code: 'BR'
|
||||
|
||||
City.create!([
|
||||
{
|
||||
name: 'Rio de Janeiro',
|
||||
latitude: -22.9068,
|
||||
longitude: -43.1729,
|
||||
country: brazil,
|
||||
timezone: 'America/Sao_Paulo',
|
||||
active: true,
|
||||
priority: 80
|
||||
}
|
||||
])
|
@ -1,10 +0,0 @@
|
||||
canada = Country.find_by code: 'CA'
|
||||
City.create!(
|
||||
name: 'Toronto',
|
||||
latitude: 43.6532,
|
||||
longitude: -79.3832,
|
||||
priority: 50,
|
||||
country: canada,
|
||||
timezone: 'America/Toronto',
|
||||
active: true
|
||||
)
|
@ -1,346 +0,0 @@
|
||||
china = Country.find_by code: 'CN'
|
||||
|
||||
City.create!([
|
||||
{
|
||||
name: 'Shanghai',
|
||||
latitude: 31.2304,
|
||||
longitude: 121.4737,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Beijing',
|
||||
latitude: 39.9042,
|
||||
longitude: 116.4074,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Shenzhen',
|
||||
latitude: 22.5431,
|
||||
longitude: 114.0579,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Guangzhou',
|
||||
latitude: 23.1291,
|
||||
longitude: 113.2644,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Chengdu',
|
||||
latitude: 30.5728,
|
||||
longitude: 104.0668,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Tianjin',
|
||||
latitude: 39.3434,
|
||||
longitude: 117.3616,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Wuhan',
|
||||
latitude: 30.5928,
|
||||
longitude: 114.3055,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Dongguan',
|
||||
latitude: 23.0208,
|
||||
longitude: 113.7518,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Chongqing',
|
||||
latitude: 29.4316,
|
||||
longitude: 106.9123,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: "Xi'an",
|
||||
latitude: 34.3416,
|
||||
longitude: 108.9398,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Hangzhou',
|
||||
latitude: 30.2741,
|
||||
longitude: 120.1551,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Foshan',
|
||||
latitude: 23.0219,
|
||||
longitude: 113.1216,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Nanjing',
|
||||
latitude: 32.0603,
|
||||
longitude: 118.7969,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Hong Kong',
|
||||
latitude: 22.3193,
|
||||
longitude: 114.1694,
|
||||
country: china,
|
||||
timezone: 'Asia/Hong_Kong',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Shenyang',
|
||||
latitude: 41.8057,
|
||||
longitude: 123.4315,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Zhengzhou',
|
||||
latitude: 34.7472,
|
||||
longitude: 113.6249,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Qingdao',
|
||||
latitude: 36.0671,
|
||||
longitude: 120.3826,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Suzhou',
|
||||
latitude: 31.2990,
|
||||
longitude: 120.5853,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Changsha',
|
||||
latitude: 28.2282,
|
||||
longitude: 112.9388,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Jinan',
|
||||
latitude: 36.6512,
|
||||
longitude: 117.1201,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Kunming',
|
||||
latitude: 25.0389,
|
||||
longitude: 102.7183,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Harbin',
|
||||
latitude: 45.8038,
|
||||
longitude: 126.5340,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Shijiazhuang',
|
||||
latitude: 38.0428,
|
||||
longitude: 114.5149,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Hefei',
|
||||
latitude: 31.8206,
|
||||
longitude: 117.2272,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Dalian',
|
||||
latitude: 38.9140,
|
||||
longitude: 121.6147,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Xiamen',
|
||||
latitude: 24.4798,
|
||||
longitude: 118.0819,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Nanning',
|
||||
latitude: 22.8170,
|
||||
longitude: 108.3665,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Changchun',
|
||||
latitude: 43.8171,
|
||||
longitude: 125.3235,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Taiyuan',
|
||||
latitude: 37.8706,
|
||||
longitude: 112.5489,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'New Taipei City',
|
||||
latitude: 25.0120,
|
||||
longitude: 121.4657,
|
||||
country: china,
|
||||
timezone: 'Asia/Taipei',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Guiyang',
|
||||
latitude: 26.6470,
|
||||
longitude: 106.6302,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Wuxi',
|
||||
latitude: 31.4914,
|
||||
longitude: 120.3119,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Shantou',
|
||||
latitude: 23.3535,
|
||||
longitude: 116.6822,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Ürümqi',
|
||||
latitude: 43.8256,
|
||||
longitude: 87.6168,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Zhongshan',
|
||||
latitude: 22.5415,
|
||||
longitude: 113.3926,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Ningbo',
|
||||
latitude: 29.8683,
|
||||
longitude: 121.5440,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Fuzhou',
|
||||
latitude: 26.0745,
|
||||
longitude: 119.2965,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Nanchang',
|
||||
latitude: 28.6820,
|
||||
longitude: 115.8579,
|
||||
country: china,
|
||||
timezone: 'Asia/Shanghai',
|
||||
active: true,
|
||||
priority: 100
|
||||
}
|
||||
])
|
@ -1,13 +0,0 @@
|
||||
egypt = Country.find_by code: 'EG'
|
||||
|
||||
City.create!([
|
||||
{
|
||||
name: 'Alexandria',
|
||||
latitude: 31.2001,
|
||||
longitude: 29.9187,
|
||||
country: egypt,
|
||||
timezone: 'Africa/Cairo',
|
||||
active: true,
|
||||
priority: 100
|
||||
}
|
||||
])
|
@ -1,13 +0,0 @@
|
||||
france = Country.find_by code: 'FRA'
|
||||
|
||||
City.create!([
|
||||
{
|
||||
name: 'Paris',
|
||||
latitude: 48.8566,
|
||||
longitude: 2.3522,
|
||||
country: france,
|
||||
timezone: 'Europe/Paris',
|
||||
active: true,
|
||||
priority: 100
|
||||
}
|
||||
])
|
@ -1,22 +0,0 @@
|
||||
germany = Country.find_by code: 'DE'
|
||||
|
||||
City.create!([
|
||||
{
|
||||
name: 'Frankfurt',
|
||||
latitude: 50.1109,
|
||||
longitude: 8.6821,
|
||||
country: germany,
|
||||
timezone: 'Europe/Berlin',
|
||||
active: true,
|
||||
priority: 100
|
||||
},
|
||||
{
|
||||
name: 'Berlin',
|
||||
latitude: 52.5200,
|
||||
longitude: 13.4050,
|
||||
country: germany,
|
||||
timezone: 'Europe/Berlin',
|
||||
active: true,
|
||||
priority: 100
|
||||
}
|
||||
])
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user