Compare commits

..

17 Commits

Author SHA1 Message Date
c37a93bcdf feat: remove trailing newline at end of file
Some checks are pending
CI / scan_ruby (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
Docker / docker (push) Waiting to run
Remove trailing newline at end of file in `schedule_tasks.rb`.

- Modify file to remove unnecessary newline at end of file.
2025-01-25 01:55:00 +08:00
7ebf9cefae feat: switch startup task to Sidekiq with Redis cache
- Update schedule tasks to use Sidekiq and Redis
- Improve task scheduling with caching
- Remove unnecessary code and error handling

This change enhances the reliability and performance of the startup task, leveraging the power of Sidekiq and Redis for background job processing and caching. The improvements ensure a smoother and more efficient application startup experience.
2025-01-25 01:49:28 +08:00
dd37e2835b style: add newline at end of schedule_tasks.rb
This commit adds a newline at the end of the 'schedule_tasks.rb' file to comply with the standard coding practices. Proper file formatting helps in maintaining code consistency across the project.
2025-01-25 01:38:35 +08:00
4e1fb58abf feat: prevent startup task from running in development environment
- Updated schedule_tasks.rb to include RAILS_BUILD check
- Improved application startup behavior in development mode

This change ensures the startup task does not run unnecessarily in development environment, reducing application startup time and resources usage.
2025-01-25 01:38:12 +08:00
5bc06007b2 refactor: improve countries find_resource method
- Update `find_resource` method to use `scoped_collection.friendly.find(params[:id])`

This refactoring improves the find_resource method to work with friendly URLs, making it more robust and user-friendly.
2025-01-25 01:29:50 +08:00
84c224cf8d feat: add startup scheduling for production
- Schedule RefreshSitemapWorker after initialization
- Implement error handling for scheduling task
- Use Rails cache to prevent multiple tasks running simultaneously

This commit introduces a mechanism that schedules the
RefreshSitemapWorker to run once after the application starts
in production. It ensures that the task does not run
multiple times concurrently by using a cache key.
Error handling is included to log any failures
in scheduling the task, improving overall reliability.
2025-01-25 01:23:04 +08:00
032ff0552a feat: add jQuery and UI support
- Introduce jQuery and jQuery UI libraries
- Create an add_jquery.js for global availability
- Import active_admin.js to initialize the setup

These changes integrate jQuery and jQuery UI into the project, which
will facilitate more interactive user interface features. The jQuery
object is made globally accessible to use within other scripts.
2025-01-25 01:11:15 +08:00
3203face6b feat: add country filter in city admin
- Introduce a new filter for selecting cities by country in the
  ActiveAdmin interface.
- Update the ransackable attributes to include country_id for
  searchable functionality.

This change enhances the Admin UI by allowing easier access to
city data based on country, improving the user experience for
administrators.
2025-01-25 00:54:23 +08:00
8364d42759 feat: add country input to city form
- Added country input field to the city form for better user experience.
- Removed unused region and weather-related fields to declutter the form.

This change improves the data captured for cities and enhances the form's usability by focusing on relevant information.
2025-01-25 00:50:43 +08:00
9ce473dddb fix: increase sleep duration in weather arts worker
- Change SLEEP_DURATION from 60 seconds to 120 seconds

This change is made to improve resource management
and allow for a more efficient operation of the batch
processing tasks performed by the worker. It helps in
avoiding potential overload on system resources.
2025-01-25 00:41:29 +08:00
fe55437c96 refactor: update generation and sleep intervals
- Change generation interval from 6 hours to 24 hours
- Increase sleep duration from 3 seconds to 60 seconds

These adjustments aim to optimize the worker's performance and reduce resource consumption. This change reflects a shift towards longer generation cycles, potentially improving throughput.
2025-01-25 00:40:51 +08:00
ec3669249f style: update site title in application layout
- Change the displayed title from 'Today AI Weather Art' to 'Today AI Weather'.

This change refines the branding by removing the word 'Art', making the title more concise and focused. This is a minor modification that does not impact functionality.
2025-01-25 00:34:20 +08:00
f6270b1ad4 feat: add weather icons and update city view
- Introduce `weather_description_icon` and `weather_stat_icon`
  helper methods for displaying SVG icons based on weather
  conditions and statistics.
- Enhance the city show view by using these icons to display
  visual weather information such as temperature, wind, humidity,
  visibility, pressure, and cloud cover.
- Optimize the visual styling and layout of the weather stats
  and cards for better user experience.
2025-01-24 10:01:43 +08:00
9dd7044a77 style: update title in application layout
- Change title from 'AI Weather Art' to 'Today AI Weather Art'

This change enhances the clarity of the application by specifying that the art relates to today's weather. It does not affect any functionality but improves the user interface.
2025-01-24 09:15:51 +08:00
dd6bb9972c feat: implement ads.txt configuration
- Add ads.txt configuration file
- Update public/ads.txt with Google AdMob publisher ID

This feature allows us to set up Google AdMob in our app and earn revenue from ads.
It includes proper configuration of the ads.txt file for future ad networks.
2025-01-24 09:14:13 +08:00
23fc14af59 style: format priority values for consistency
- Clean up the seed file for cities in China
- Ensure all priority values have a consistent formatting style

This change enhances the readability of the data seeding code without altering the underlying logic or functionality of the application.
2025-01-24 09:09:57 +08:00
fedb954d34 chore: add Google AdSense script to layout
- Include async script for Google AdSense in the application layout
- Ensure script loads with crossorigin attribute

This change will allow the application to serve ads from Google, potentially generating revenue. The implementation is non-intrusive and maintains existing functionality.
2025-01-24 09:04:41 +08:00
17 changed files with 349 additions and 134 deletions

View File

@ -36,18 +36,17 @@ ActiveAdmin.register City do
filter :name filter :name
filter :active filter :active
filter :country, as: :select
form do |f| form do |f|
f.inputs do f.inputs do
f.input :active f.input :active
f.input :name f.input :name
f.input :country
f.input :latitude f.input :latitude
f.input :longitude f.input :longitude
f.input :priority f.input :priority
f.input :timezone f.input :timezone
f.input :region
f.input :last_weather_fetch
f.input :last_image_generation
end end
f.actions f.actions
end end

View File

@ -1,4 +1,9 @@
ActiveAdmin.register Country do ActiveAdmin.register Country do
controller do
def find_resource
scoped_collection.friendly.find(params[:id])
end
end
# See permitted parameters documentation: # See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters # https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
# #

View File

@ -1,2 +1,51 @@
module WeatherArtsHelper module WeatherArtsHelper
def weather_description_icon(description)
case description&.downcase
when /rain/
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>'.html_safe
when /cloud/
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
</svg>'.html_safe
when /sun|clear/
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>'.html_safe
else
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>'.html_safe
end
end
def weather_stat_icon(type)
case type
when "temperature"
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>'.html_safe
when "wind"
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>'.html_safe
when "humidity"
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>'.html_safe
when "visibility"
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>'.html_safe
when "pressure"
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>'.html_safe
when "cloud"
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
</svg>'.html_safe
end
end
end end

View File

@ -0,0 +1,6 @@
import './add_jquery'
import "jquery/dist/jquery"
import "jquery-ui/dist/jquery-ui"
import "jquery-ujs"
import "@activeadmin/activeadmin"

View File

@ -0,0 +1,4 @@
import jquery from 'jquery'
import $ from 'jquery'
window.jQuery = jquery
window.$ = $

View File

@ -6,3 +6,4 @@ import "@fontsource/raleway/400.css";
import "@fontsource/raleway/600.css"; import "@fontsource/raleway/600.css";
import "./controllers" import "./controllers"
import "./active_admin"

View File

@ -45,7 +45,7 @@ class City < ApplicationRecord
end end
def self.ransackable_attributes(auth_object = nil) def self.ransackable_attributes(auth_object = nil)
[ "active", "country", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at" ] [ "active", "country_id", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at" ]
end end
def last_weather_fetch def last_weather_fetch

View File

@ -1,37 +1,53 @@
<div class="min-h-screen"> <div class="min-h-screen bg-base-200">
<!-- 城市头部信息 --> <!-- 城市头部信息 -->
<section class="relative h-[40vh] overflow-hidden"> <section class="relative h-[50vh] overflow-hidden">
<% if @city.latest_weather_art&.image&.attached? %> <% if @city.latest_weather_art&.image&.attached? %>
<%= image_tag @city.latest_weather_art.image, <%= image_tag @city.latest_weather_art.image,
class: "w-full h-full object-cover" %> class: "w-full h-full object-cover" %>
<div class="absolute inset-0 bg-gradient-to-t from-base-100 via-base-100/50 to-transparent"></div> <div class="absolute inset-0 bg-gradient-to-t from-base-100 via-base-100/60 to-transparent"></div>
<% end %> <% end %>
<div class="absolute inset-0 flex items-center"> <div class="absolute inset-0 flex items-center">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="max-w-4xl"> <div class="max-w-4xl">
<div class="flex items-center space-x-4 mb-4"> <div class="flex items-center space-x-4 mb-6">
<%= link_to cities_path, <%= link_to cities_path,
class: "btn btn-ghost btn-circle" do %> class: "btn btn-ghost btn-circle bg-base-100/50 backdrop-blur-sm hover:bg-base-100/70" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg> </svg>
<% end %> <% end %>
<h1 class="text-4xl md:text-5xl font-display font-bold"><%= @city.localized_name %></h1> <div>
<h1 class="text-4xl md:text-5xl font-display font-bold mb-2"><%= @city.localized_name %></h1>
<p class="text-lg opacity-80">
<%= @city.country.name %>, <%= @city.region %>
<span class="mx-2">•</span>
<%= Time.current.in_time_zone(@city.timezone).strftime("%Y-%m-%d %H:%M") %>
</p>
</div>
</div> </div>
<div class="stats bg-base-100/80 backdrop-blur-sm shadow"> <div class="stats bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box">
<div class="stat"> <div class="stat">
<div class="stat-title">Latitude</div> <div class="stat-title">Latest Weather</div>
<div class="stat-value text-2xl"><%= @city.latitude %></div> <div class="stat-value text-2xl">
<%= @city.latest_weather_art&.temperature %>°C
</div>
<div class="stat-desc">
<%= @city.latest_weather_art&.description %>
</div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-title">Longitude</div> <div class="stat-title">Coordinates</div>
<div class="stat-value text-2xl"><%= @city.longitude %></div> <div class="stat-value text-xl">
<%= @city.latitude %>°N, <%= @city.longitude %>°E
</div>
<div class="stat-desc">Geographical Location</div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-title">Weather Arts</div> <div class="stat-title">Weather Records</div>
<div class="stat-value text-2xl"><%= @city.weather_arts.count %></div> <div class="stat-value text-2xl"><%= @city.weather_arts.count %></div>
<div class="stat-desc">Total Entries</div>
</div> </div>
</div> </div>
</div> </div>
@ -42,20 +58,39 @@
<!-- 天气艺术历史记录 --> <!-- 天气艺术历史记录 -->
<section class="container mx-auto px-4 py-16"> <section class="container mx-auto px-4 py-16">
<div class="space-y-8"> <div class="space-y-8">
<h2 class="text-3xl font-display font-bold">Weather Art History</h2> <div class="flex justify-between items-center">
<h2 class="text-3xl font-display font-bold">Weather Art History</h2>
<div class="stats shadow inline-flex">
<div class="stat">
<div class="stat-title">Last Updated</div>
<div class="stat-value text-primary text-lg">
<%= time_ago_in_words(@city.last_weather_fetch) if @city.last_weather_fetch %>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<% @city.weather_arts.order(weather_date: :desc).each do |art| %> <% @city.weather_arts.order(weather_date: :desc).each do |art| %>
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300"> <div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300">
<figure class="relative aspect-[4/3] overflow-hidden"> <figure class="relative aspect-video overflow-hidden">
<% if art.image.attached? %> <% if art.image.attached? %>
<%= image_tag art.image, <%= image_tag art.image,
class: "w-full h-full object-cover transform hover:scale-105 transition-transform duration-500" %> class: "w-full h-full object-cover transform hover:scale-105 transition-transform duration-500" %>
<% end %> <% end %>
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/60 to-transparent"> <div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/70 to-transparent">
<div class="text-white"> <div class="text-white">
<div class="text-2xl font-bold"><%= art.temperature %>°C</div> <div class="flex items-center justify-between">
<div class="text-sm opacity-90"><%= art.weather_date.strftime("%B %d, %Y") %></div> <div class="text-3xl font-bold"><%= art.temperature %>°C</div>
<div class="text-right">
<div class="text-sm font-semibold">
<%= art.weather_date.strftime("%H:%M") %>
</div>
<div class="text-xs opacity-80">
<%= art.weather_date.strftime("%B %d, %Y") %>
</div>
</div>
</div>
</div> </div>
</div> </div>
</figure> </figure>
@ -66,7 +101,7 @@
<div class="grid grid-cols-2 gap-4 my-4 text-sm"> <div class="grid grid-cols-2 gap-4 my-4 text-sm">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.5 14.7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg> </svg>
<span>Humidity: <%= art.humidity %>%</span> <span>Humidity: <%= art.humidity %>%</span>
</div> </div>
@ -78,10 +113,13 @@
</div> </div>
</div> </div>
<div class="card-actions justify-end"> <%= link_to city_weather_art_path(@city, art),
<%= link_to "View Details", city_weather_art_path(@city, art), class: "btn btn-primary btn-block" do %>
class: "btn btn-primary btn-outline" %> View Details
</div> <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>
</div> </div>
<% end %> <% end %>

View File

@ -45,6 +45,9 @@
gtag('config', 'G-PX1C92V5L7'); gtag('config', 'G-PX1C92V5L7');
</script> </script>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7296634171837358"
crossorigin="anonymous"></script>
</head> </head>
<body class="min-h-screen bg-base-100 font-sans"> <body class="min-h-screen bg-base-100 font-sans">
@ -53,7 +56,7 @@
<div class="container mx-auto"> <div class="container mx-auto">
<div class="flex-1"> <div class="flex-1">
<%= link_to root_path, class: "text-2xl font-display font-bold hover:text-primary transition-colors" do %> <%= link_to root_path, class: "text-2xl font-display font-bold hover:text-primary transition-colors" do %>
AI Weather Art Today AI Weather
<% end %> <% end %>
</div> </div>
<div class="flex-none"> <div class="flex-none">

View File

@ -0,0 +1,11 @@
<!-- app/views/weather_arts/_weather_stat.html.erb -->
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="<%= icon %>" />
</svg>
<div class="stat-title font-medium"><%= title %></div>
</div>
<div class="stat-value text-2xl"><%= value %></div>
<div class="stat-desc mt-1"><%= desc %></div>
</div>

View File

@ -4,84 +4,152 @@
</script> </script>
<% end %> <% end %>
<div class="min-h-screen"> <div class="relative min-h-screen bg-base-200">
<!-- 返回导航 --> <!-- 背景图片 -->
<div class="container mx-auto px-4 py-8"> <% if @weather_art.image.attached? %>
<%= link_to city_path(@weather_art.city), <div class="fixed inset-0 -z-10">
class: "btn btn-ghost gap-2" do %> <%= image_tag @weather_art.image,
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> class: "absolute w-full h-full object-cover scale-110 filter blur-2xl opacity-25" %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /> <div class="absolute inset-0 bg-gradient-to-b from-base-200/90 to-base-200/70 backdrop-blur-md"></div>
</svg> </div>
Back to <%= @weather_art.city.name %> <% end %>
<% end %>
</div>
<!-- 主要内容 --> <!-- 主要内容 -->
<div class="container mx-auto px-4 pb-16"> <div class="relative z-10">
<div class="max-w-6xl mx-auto"> <!-- 返回导航 -->
<!-- 头部信息 --> <div class="container mx-auto px-4 py-6">
<div class="text-center space-y-4 mb-12"> <%= link_to city_path(@weather_art.city),
<h1 class="text-4xl md:text-5xl font-display font-bold"> 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 %>
<%= @weather_art.city.name %> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
</h1> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
<p class="text-xl text-base-content/70"> </svg>
<%= @weather_art.weather_date.strftime("%B %d, %Y") %> Back to <%= @weather_art.city.name %>
</p> <% end %>
</div> </div>
<!-- 主要卡片 --> <div class="container mx-auto px-4 pb-16">
<div class="card lg:card-side bg-base-100 shadow-2xl"> <div class="max-w-6xl mx-auto">
<figure class="lg:w-1/2 relative aspect-square lg:aspect-auto"> <!-- 头部信息 -->
<% if @weather_art.image.attached? %> <div class="text-center space-y-4 mb-12">
<%= image_tag @weather_art.image, <div class="inline-flex items-center gap-2 text-sm font-medium px-4 py-2 rounded-full bg-base-100/50 backdrop-blur-sm">
class: "w-full h-full object-cover" %> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<% end %> <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" />
</figure> </svg>
<%= @weather_art.city.full_name %>
<div class="card-body lg:w-1/2">
<h2 class="card-title font-display text-2xl mb-6">
<%= @weather_art.description %>
</h2>
<!-- 天气数据网格 -->
<div class="grid grid-cols-2 gap-6">
<div class="stat bg-base-200 rounded-box">
<div class="stat-title">Temperature</div>
<div class="stat-value"><%= @weather_art.temperature %>°C</div>
<div class="stat-desc">Feels like <%= @weather_art.feeling_temp %>°C</div>
</div>
<div class="stat bg-base-200 rounded-box">
<div class="stat-title">Wind</div>
<div class="stat-value"><%= @weather_art.wind_scale %></div>
<div class="stat-desc"><%= @weather_art.wind_speed %> km/h</div>
</div>
<div class="stat bg-base-200 rounded-box">
<div class="stat-title">Humidity</div>
<div class="stat-value"><%= @weather_art.humidity %>%</div>
</div>
<div class="stat bg-base-200 rounded-box">
<div class="stat-title">Visibility</div>
<div class="stat-value"><%= @weather_art.visibility %> km</div>
</div>
<div class="stat bg-base-200 rounded-box">
<div class="stat-title">Pressure</div>
<div class="stat-value"><%= @weather_art.pressure %> hPa</div>
</div>
<div class="stat bg-base-200 rounded-box">
<div class="stat-title">Cloud Cover</div>
<div class="stat-value"><%= @weather_art.cloud %>%</div>
</div>
</div> </div>
<!-- AI Prompt --> <h1 class="text-4xl md:text-6xl font-display font-bold">
<div class="mt-8 bg-base-200 p-6 rounded-box"> <span class="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
<h3 class="font-display font-bold text-lg mb-3">AI Prompt</h3> Weather Art
<p class="text-base-content/70"><%= @weather_art.prompt %></p> </span>
</h1>
<div class="flex flex-wrap justify-center items-center gap-3">
<div class="badge badge-lg badge-primary gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<%= @weather_art.weather_date.strftime("%B %d, %Y") %>
</div>
<div class="badge badge-lg badge-secondary gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<%= @weather_art.weather_date.strftime("%H:%M") %>
</div>
</div>
</div>
<!-- 主要卡片 -->
<div class="card lg:card-side bg-base-100/80 backdrop-blur-md shadow-2xl">
<figure class="lg:w-1/2 relative aspect-square lg:aspect-auto group">
<% if @weather_art.image.attached? %>
<%= image_tag @weather_art.image,
class: "w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" %>
<div class="absolute inset-0 bg-gradient-to-t from-base-100/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<% end %>
</figure>
<div class="card-body lg:w-1/2">
<div class="prose max-w-none mb-8">
<h2 class="card-title font-display text-3xl mb-4 flex items-center gap-3">
<%= weather_description_icon(@weather_art.description) %>
<%= @weather_art.description %>
</h2>
<div class="divider"></div>
</div>
<!-- 天气数据网格 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("temperature") %>
<div class="stat-title font-medium">Temperature</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.temperature %>°C</div>
<div class="stat-desc mt-1">Feels like <%= @weather_art.feeling_temp %>°C</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("wind") %>
<div class="stat-title font-medium">Wind</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.wind_scale %></div>
<div class="stat-desc mt-1"><%= @weather_art.wind_speed %> km/h</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("humidity") %>
<div class="stat-title font-medium">Humidity</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.humidity %>%</div>
<div class="stat-desc mt-1">Relative humidity</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("visibility") %>
<div class="stat-title font-medium">Visibility</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.visibility %> km</div>
<div class="stat-desc mt-1">Clear view distance</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("pressure") %>
<div class="stat-title font-medium">Pressure</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.pressure %> hPa</div>
<div class="stat-desc mt-1">Atmospheric pressure</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("cloud") %>
<div class="stat-title font-medium">Cloud Cover</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.cloud %>%</div>
<div class="stat-desc mt-1">Sky coverage</div>
</div>
</div>
<!-- AI Prompt -->
<div class="mt-8">
<div class="bg-base-200/50 backdrop-blur-sm p-6 rounded-box border border-base-300">
<div class="flex items-center gap-3 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<h3 class="font-display font-bold text-lg">AI Prompt</h3>
</div>
<p class="text-base-content/70 leading-relaxed">
<%= @weather_art.prompt %>
</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,9 +1,9 @@
class BatchGenerateWeatherArtsWorker class BatchGenerateWeatherArtsWorker
include Sidekiq::Worker include Sidekiq::Worker
GENERATION_INTERVAL = 6.hours GENERATION_INTERVAL = 24.hours
MAX_DURATION = 50.minutes MAX_DURATION = 50.minutes
SLEEP_DURATION = 3.seconds SLEEP_DURATION = 120.seconds
def perform(*args) def perform(*args)
start_time = Time.current start_time = Time.current

View File

@ -0,0 +1,21 @@
# config/initializers/schedule_tasks.rb
Rails.application.config.after_initialize do
if Rails.env.production? && !ENV["RAILS_BUILD"]
begin
redis_key = "startup_task_running"
unless Sidekiq.redis { |conn| conn.get(redis_key) }
Sidekiq.redis do |conn|
conn.setex(redis_key, 1.hour.to_i, "1")
end
RefreshSitemapWorker.perform_async
Rails.logger.info "Startup task (RefreshSitemapWorker) scheduled successfully"
end
rescue => e
Rails.logger.error "Error scheduling startup task: #{e.message}"
ensure
Sidekiq.redis { |conn| conn.del(redis_key) }
end
end
end

View File

@ -80,7 +80,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: "Xi'an", name: "Xi'an",
@ -89,7 +89,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Hangzhou', name: 'Hangzhou',
@ -98,7 +98,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Foshan', name: 'Foshan',
@ -107,7 +107,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Nanjing', name: 'Nanjing',
@ -116,7 +116,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Hong Kong', name: 'Hong Kong',
@ -125,7 +125,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Hong_Kong', timezone: 'Asia/Hong_Kong',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Shenyang', name: 'Shenyang',
@ -134,7 +134,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Zhengzhou', name: 'Zhengzhou',
@ -143,7 +143,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Qingdao', name: 'Qingdao',
@ -152,7 +152,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Suzhou', name: 'Suzhou',
@ -161,7 +161,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Changsha', name: 'Changsha',
@ -170,7 +170,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Jinan', name: 'Jinan',
@ -179,7 +179,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Kunming', name: 'Kunming',
@ -188,7 +188,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Harbin', name: 'Harbin',
@ -197,7 +197,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Shijiazhuang', name: 'Shijiazhuang',
@ -206,7 +206,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Hefei', name: 'Hefei',
@ -215,7 +215,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Dalian', name: 'Dalian',
@ -224,7 +224,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Xiamen', name: 'Xiamen',
@ -233,7 +233,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Nanning', name: 'Nanning',
@ -242,7 +242,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Changchun', name: 'Changchun',
@ -251,7 +251,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Taiyuan', name: 'Taiyuan',
@ -260,7 +260,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'New Taipei City', name: 'New Taipei City',
@ -269,7 +269,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Taipei', timezone: 'Asia/Taipei',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Guiyang', name: 'Guiyang',
@ -278,7 +278,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Wuxi', name: 'Wuxi',
@ -287,7 +287,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Shantou', name: 'Shantou',
@ -296,7 +296,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Ürümqi', name: 'Ürümqi',
@ -305,7 +305,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Zhongshan', name: 'Zhongshan',
@ -314,7 +314,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Ningbo', name: 'Ningbo',
@ -323,7 +323,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Fuzhou', name: 'Fuzhou',
@ -332,7 +332,7 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
}, },
{ {
name: 'Nanchang', name: 'Nanchang',
@ -341,6 +341,6 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100
} }
]) ])

View File

@ -16,6 +16,8 @@
"@hotwired/stimulus": "^3.2.2", "@hotwired/stimulus": "^3.2.2",
"@hotwired/turbo-rails": "^8.0.12", "@hotwired/turbo-rails": "^8.0.12",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"jquery": "^3.7.1",
"jquery-ui": "^1.14.1",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"sass": "^1.83.4", "sass": "^1.83.4",
"tailwindcss": "^3.4.17" "tailwindcss": "^3.4.17"

1
public/ads.txt Normal file
View File

@ -0,0 +1 @@
google.com, pub-7296634171837358, DIRECT, f08c47fec0942fa0

View File

@ -729,12 +729,19 @@ jquery-ui@^1.13.3:
dependencies: dependencies:
jquery ">=1.12.0 <5.0.0" jquery ">=1.12.0 <5.0.0"
jquery-ui@^1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.14.1.tgz#ba342ea3ffff662b787595391f607d923313e040"
integrity sha512-DhzsYH8VeIvOaxwi+B/2BCsFFT5EGjShdzOcm5DssWjtcpGWIMsn66rJciDA6jBruzNiLf1q0KvwMoX1uGNvnQ==
dependencies:
jquery ">=1.12.0 <5.0.0"
jquery-ujs@^1.2.2: jquery-ujs@^1.2.2:
version "1.2.3" version "1.2.3"
resolved "https://registry.npmjs.org/jquery-ujs/-/jquery-ujs-1.2.3.tgz" resolved "https://registry.npmjs.org/jquery-ujs/-/jquery-ujs-1.2.3.tgz"
integrity sha512-59wvfx5vcCTHMeQT1/OwFiAj+UffLIwjRIoXdpO7Z7BCFGepzq9T9oLVeoItjTqjoXfUrHJvV7QU6pUR+UzOoA== integrity sha512-59wvfx5vcCTHMeQT1/OwFiAj+UffLIwjRIoXdpO7Z7BCFGepzq9T9oLVeoItjTqjoXfUrHJvV7QU6pUR+UzOoA==
"jquery@>=1.12.0 <5.0.0", jquery@^3.4.1: "jquery@>=1.12.0 <5.0.0", jquery@^3.4.1, jquery@^3.7.1:
version "3.7.1" version "3.7.1"
resolved "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz" resolved "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz"
integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==