Compare commits

...

101 Commits

Author SHA1 Message Date
fe5c0d5113 fix: update sitemap retrieval logic
Some checks failed
Docker Dev / docker (push) Has been cancelled
CI / scan_ruby (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
Docker Main / docker (push) Has been cancelled
- Change from using `get_object` to `head_object` to check if the
  sitemap file exists before attempting to retrieve it.
- Implement a presigned URL for accessing the sitemap, which is valid
  for 15 minutes.
- Set cache headers to allow for 1 hour of caching.
- Improved logging for better error tracking and debugging.

This change enhances the efficiency of sitemap retrieval by reducing
unnecessary data transfer and provides a more secure way to access
the sitemaps through presigned URLs. It also improves error handling
by logging specific errors related to missing sitemaps.
2025-02-24 17:42:55 +08:00
3ae870047a feat: add sitemap management feature
- Implement index action to list sitemaps
- Create view for displaying sitemaps with details
- Add helper method for generating sitemap URLs
- Enhance error handling for S3 service errors

This commit introduces a new feature for managing sitemaps in the application. It includes an index view that lists all available sitemaps with their last modified date and size, along with a link to view each sitemap. The error handling for S3 interactions has also been improved to log errors and return appropriate responses.
2025-02-24 17:28:21 +08:00
2a360a6875 style: fix formatting in Arabic locale file
- Ensure consistent formatting in the long date string
- No functional changes were made, only a formatting adjustment

This commit addresses a minor formatting issue in the Arabic locale file, ensuring that the long date format is consistent with the expected output. No changes to functionality or behavior were introduced.
2025-02-24 17:09:21 +08:00
bd04bb63a1 feat: add support for multiple languages
- Update available locales to include Bengali, Hindi, Urdu, and Arabic
- Add new locale files for each language with translations for UI elements
- Ensure the application can now support a wider audience by providing
  localized content

This change enhances the application's accessibility and usability for
users who speak these languages, allowing for a more inclusive user
experience. The new translations cover key UI components and messages,
ensuring that users can interact with the application in their native
languages.
2025-02-24 17:04:23 +08:00
5f98d9ebfd feat: enhance language switcher dropdown
- Update dropdown to support dynamic locales from I18n
- Limit dropdown height with overflow handling for better UX

This change improves the language switcher by dynamically
loading available locales from the I18n configuration. It
also enhances the user experience by limiting the height
of the dropdown and enabling scrolling, making it easier
to navigate through multiple language options.
2025-02-24 15:55:01 +08:00
9ef2a92d60 feat: add multiple language support for locales
- Extend available locales in the application to include:
  - Portuguese (Brazil)
  - Croatian
  - Persian
  - German
  - Spanish
  - French
  - Italian
  - Turkish
  - Russian
  - Ukrainian
  - Polish

- Create new locale files for each language with appropriate translations.
- Update existing locale files to include new languages.

This update enhances the application's accessibility by supporting a wider range of languages, allowing users from different regions to interact with the application in their native language.
2025-02-24 15:46:29 +08:00
03c957e654 feat: add date formats to localization
- Introduce default, short, and long date formats
- Enhance date representation for better user experience

This update allows the application to display dates in multiple formats,
including a default format of 'YYYY-MM-DD', a short format of
'"MMM DD"', and a long format of '"Month DD, YYYY"'. This
improves localization support and user interface flexibility.
2025-02-24 14:20:49 +08:00
b2cc7e7016 chore: update keywords for SEO optimization
- Refactor keywords in cities_controller.rb
- Refactor keywords in weather_arts_controller.rb
- Refactor keywords in application.html.erb

This change improves the SEO of the application by updating
keywords to include 'ai' and 'ai web', which are more relevant
and likely to enhance search visibility. The previous keywords
were less optimized for current trends in AI-related searches.
2025-02-24 14:12:51 +08:00
da2f4f6c86 fix: update keywords for better SEO
- Modify keywords in cities_controller.rb to include 'ai weather'
- Update keywords in weather_arts_controller.rb to include 'ai weather'
- Change keywords in application.html.erb to include 'ai weather'

These changes enhance the search engine optimization (SEO) of the
application by ensuring that relevant keywords are included in
meta tags, improving visibility for users searching for AI
weather-related content.
2025-02-24 14:10:32 +08:00
3661d2b008 chore: update meta keywords for SEO
- Modify keywords in CitiesController for better
  categorization of AI art and weather art.
- Update keywords in WeatherArtsController to include
  city country name and description.
- Add keywords in application layout for overall site
  SEO improvement.

These changes aim to improve search engine visibility
and better describe the content related to AI-generated
weather art.
2025-02-24 14:08:25 +08:00
0e476b546d feat: add locking mechanism to batch task worker
Some checks failed
Docker Dev / docker (push) Has been cancelled
CI / scan_ruby (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
Docker Main / docker (push) Has been cancelled
- Introduce a Redis-based lock to prevent concurrent execution of
  batch generation tasks.
- Set a TTL of 300 seconds for the lock to ensure it is released
  after a timeout.
- Add logging for situations where a task is already in progress.

This enhancement ensures that batch tasks do not overlap, which can
lead to data inconsistencies and resource contention. The locking
mechanism improves the reliability of the batch processing system.
2025-02-22 15:41:42 +08:00
d331a73a85 fix: update html lang attribute for I18n
- Change the lang attribute of the HTML tag to use the current I18n locale
- This change ensures that the application correctly reflects the user's language preferences

Updating this attribute improves accessibility and SEO by helping search engines and assistive technologies better understand the language of the content.
2025-02-22 15:19:40 +08:00
5a82fc9a10 style: correct string split style
- Change string split method from single quotes to double quotes.

This commit improves code consistency by aligning the string
split syntax with the rest of the codebase. No functional
changes were made as a result of this update.
2025-02-22 15:00:37 +08:00
926ba18e85 refactor: change locale handling to around_action
- Update set_locale method to use around_action instead of before_action.
- Modify locale extraction logic to handle more cases, ensuring better fallback handling.
- Improve overall method clarity and maintainability by restructuring code.

This change enhances the localization process by providing a clearer
way to manage locale settings and ensures that it correctly falls back
to the default locale when necessary. It also resolves some edge cases
in locale extraction based on the HTTP_ACCEPT_LANGUAGE header.
2025-02-22 15:00:22 +08:00
09fa1ceea9 feat: update sitemap generation for multiple locales
- Refactor generate_sitemap method to support locale
- Add setup_sitemap_config method for configuration
- Implement sitemap generation for default and localized paths

This update enhances the sitemap generation process by supporting
multiple languages. Users can now access a sitemap with language
prefixes, improving SEO and usability for different locales.
Additionally, error handling has been improved to log specific
errors related to locale generation.
2025-02-22 12:08:59 +08:00
5b996bb64a style: update layout and improve accessibility
Some checks are pending
Docker Dev / docker (push) Waiting to run
- Adjust footer components for better spacing
- Move language switcher to navbar for easier access
- Simplify language switcher links using iteration
- Enhance copyright text to dynamically reflect the current year
2025-02-22 01:03:40 +08:00
80ceac5d94 feat: add locale extraction and sanitization methods
- Implemented `extract_locale_from_accept_language_header` to
  retrieve the user's preferred language from the request.
- Added `sanitize_locale` for validating and sanitizing locale
  inputs against available locales.
- Updated `set_locale` method to prepare for incorporating
  browser language preference handling.

These changes enhance the application's ability to set the locale
based on the user's browser settings, paving the way for better
internationalization support.
2025-02-22 00:13:35 +08:00
bd42833953 feat: add translatable name module for countries and regions
- Introduced `TranslatableName` module to allow for
  localized names for `Country` and `Region` models.
- Updated views to display `localized_name` instead of
  `name` for improved internationalization.
- Refactored JSON serialization for `translations` attribute.
- Enhanced localization support by adding new languages:
  Japanese and Korean, with updated locale files.
- Removed outdated English and Chinese locales for countries
  and regions to clean up the codebase.
2025-02-21 23:46:25 +08:00
f6b9dcf187 feat: add internationalization support
- Implement locale extraction and fallback mechanism
- Add translation files for English and Chinese
- Update views to use translated strings for various UI elements

This commit introduces support for multiple languages in the application, enhancing accessibility for users. It includes a fallback mechanism for locales and updates to the user interface to display translated content.
2025-02-21 17:51:25 +08:00
517e3038cc chore: rename docker workflows and clean up city seeds
- Rename docker workflow files for clarity: 'docker-dev' and 'docker-main'
- Remove unused city seed files to streamline the database seeding process

These changes improve the organization of the workflow files and reduce clutter in the seed data, making it easier to manage and maintain the project.
2025-02-21 10:02:36 +08:00
9fe92b1fc4 feat: add RSS feed functionality
Some checks failed
Docker / docker (push) Has been cancelled
- Introduce RssController to handle RSS feed requests
- Add a new route for the RSS feed
- Implement RSS feed view to display weather art
- Update application layout to include RSS feed link
- Set content type for RSS responses

This commit adds an RSS feed feature that allows users to
subscribe to updates on daily AI-generated weather art.
The feed includes the latest weather art and relevant
metadata, enhancing user engagement and accessibility.
2025-02-19 17:38:49 +08:00
c35f09660a fix: handle nil safely for latest arts image
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
Docker / docker (push) Has been cancelled
- Update image tag to safely access the first latest art
- Use safe navigation operator to prevent potential nil errors

This change ensures that if there are no latest arts or if the
image is not attached, the application will not raise an error.
It improves the robustness of the view by handling edge cases.
2025-02-19 14:18:14 +08:00
789e9f8d23 feat: migrate AWS configuration to MinIO
- Update SitemapsController to use MinIO credentials
- Modify RefreshSitemapWorker to adapt to MinIO settings
- Change AWS configuration in initializers and storage files
- Add MinIO credentials to credentials.yml.enc

This commit transitions the application from using AWS S3 to MinIO for
storage. It updates all relevant configurations and ensures that the
application can now interact with MinIO seamlessly, including
support for both production and development environments.
2025-02-19 11:31:57 +08:00
468a665354 style: update badge classes for weather art display
Some checks are pending
Docker / docker (push) Waiting to run
CI / scan_ruby (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
- Change badge class from 'badge-neutral' to 'badge-ghost' for
  better visual consistency.
- This update improves the UI by aligning the badge styles with
  the overall design language of the application.
2025-02-18 17:00:49 +08:00
9a02115562 feat: enhance weather art display layout
- Update background color for better visibility
- Adjust padding and margins for improved spacing
- Refactor card components for a cleaner design
- Add AI Prompt section for better user engagement

These changes improve the overall user experience by making the
weather art display more visually appealing and easier to
navigate. The layout adjustments also enhance the mobile
view, ensuring a consistent experience across devices.
2025-02-18 16:54:17 +08:00
fc721ada9f Refactor: Limit images generation
Some checks are pending
Docker / docker (push) Waiting to run
CI / scan_ruby (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
- Modify the `PER_RUN_ION` constant of `BatchGenerateWeatherArtsWorker` from
  3 to 2, reducing the number of images generated on each run.

- This change reduces the limit and will not affect other parts
  of the application.
- It is about reducing the number of images generated per run and
  will impact the performance of the application.
- It will not affect the other parts.
</commit_message>
2025-02-17 23:21:04 +08:00
888dc7f22d feat: add Redis integration for sitemap refresh
- Include Redis gem in the Gemfile and update Gemfile.lock
- Implement locking mechanism in the RefreshSitemapWorker to prevent concurrent executions
- Use Redis to manage distributed locks for the sitemap generation process

This change introduces Redis for managing access controls in the sitemap
refresh workflow. It prevents multiple simultaneous jobs from running,
improving stability and performance when generating sitemaps.
2025-02-17 15:07:34 +08:00
f79299d707 feat: update art listings in home controller
- Modify the popular arts query to include city and country relationships
- Ensure the latest arts query also includes necessary associations
- Removed deprecated commented code for clarity

These changes improve data retrieval by establishing better relationships to
cities and their countries. This enhances the availability of data for
the views rendering the arts and keeps the code clean by removing
unnecessary comments.
2025-02-17 14:38:29 +08:00
81116a2f3e chore: update sitemap default host to use ENV variable
- Change default_host assignment to use ENV.fetch for
  RAILS_SITEMAP_DEFAULT_HOST
- This allows for easier configuration based on the environment,
  particularly useful for production and development setups.
2025-02-17 14:08:31 +08:00
df86a10f03 fix: correct logic for weather arts selection
- Update query to select weather arts with last generation time
  greater than the cutoff time.
- Adjust logging for clarity, including brackets around limits
  and counts for better readability.
- Change from appending to selected cities to concatenating selected
  cities, ensuring the full list is preserved.

This commit resolves an issue where the logic for filtering
recent weather arts was incorrect, potentially leading to missing
relevant entries. Additionally, it improves log output formatting
for better debugging.
2025-02-17 13:29:31 +08:00
4ea7f6c03c fix: update sitemap default host
- Change default host for SitemapGenerator from local server
  to the new public URL. This allows the sitemap to be
  generated correctly in the production environment.
- Ensures that the sitemap points to the correct domain, improving
  search engine visibility and indexing.
2025-02-17 13:18:57 +08:00
269e5ef553 refactor: improve logging in weather arts worker
- Added logging for skipped recent cities.
- Enhanced logging for selected cities after filtering.

This refactor improves the observability of the city selection
process in the BatchGenerateWeatherArtsWorker. The added logs
provide better insight during debugging and monitoring, allowing
developers to track recent selections and identify any issues
related to city filtering more effectively.
2025-02-17 13:15:16 +08:00
104597e3ba feat: improve city selection for weather arts generation
- Introduce `get_recent_cities` to filter out cities that have been recently generated weather arts.
- Update `select_cities` method to exclude recent cities from the selection process, ensuring diversity in the generated arts.
- Modify the `select_countries` method to maintain its functionality while integrating recent city filtering.

This update enhances the system's ability to select active cities efficiently, reducing redundancy and improving the variety of generated weather arts by avoiding recent candidates.
2025-02-17 12:58:16 +08:00
f1815afc41 feat: improve city selection logic in weather worker
- Refactor city selection to better optimize the process
- Implement debugging logs for clearer tracking of city selection
- Modify logic to fairly distribute remaining city slots across countries

These changes enhance the efficiency of the city selection process
while providing useful debug information to track operations. The new
logic ensures that cities are selected based on activity status and
spreads the selection across provided countries if additional slots
are available. This improves the probability of including diverse
cities in the generation process, reducing potential bias in
selection.
2025-02-17 11:51:45 +08:00
9a35dc5563 feat: Update sitemap host configuration
Some checks are pending
Docker / docker (push) Waiting to run
CI / scan_ruby (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
The commit updates the sitemap host to point to the production environment.
This is needed so that the sitemap links will point to the correct domain, and the
production environment needs to point to a specific host. The sitemap now uses
the correct host url, and this is the correct config for the current state.
2025-02-17 00:35:46 +08:00
31a2ec373d refactor: Update sitemap configuration to support a new host name
- Modified the host name in the sitemap configuration to support the new domain.
- This commit changes the hostname, and updates the sitemap to reflect the correct host.
- This is done to reflect the correct host on the production server.
- Added the new `public/sitemaps` directory to .gitignore to avoid commits related to this
 directory, and updates the .gitignore.
2025-02-17 00:27:44 +08:00
f02587da57 feat: improve weather art display and generation
- Display the latest weather art on the cities index page.
- Update the weather art preview image size to be big.
- Improve image display by using webp format.
- Adjust the daily and per-run generation limits.

These changes enhance the user experience by providing more
up-to-date and visually appealing content, while also controlling
the resource usage during image generation.
2025-02-17 00:15:32 +08:00
fa1fc7c21a refactor: improve weather art generation process
- Update city selection to consider countries.
- Use daytime instead of sunrise for time checks.
- Add per-run generation limit

This commit refactors the weather art generation process to
increase the efficiency. The city selection has been updated to
consider country. Daytime check has been updated to determine
if the city is within the correct local time. A limit is added
to restrict the number of cities processed in a single run.
2025-02-16 22:59:53 +08:00
afc871deb1 refactor: improve: changes in admin panel
The commit message implements a new feature
in the admin panel for the Sidekiq tasks
allowing users to input the city ID. The
previous implementation uses a selection approach
where users can input the city ID.
The new feature allows for a more streamlined,
direct data input method for the users.
</commit_ message>
2025-02-16 13:42:13 +08:00
0ef979e5c4 feat: refactor: change image generation quality
- Modify the code to use the `hd` quality setting
- Update the code to use the `hd` quality setting.
- Ensure there are no other side effects of this change.
- These changes will improve the user experience.
</commit_message>
2025-02-16 13:27:25 +08:00
e5930c666b feat: rename social media website, including X/Twitter
The commit renames the social sharing feature on
the website. Before the renaming the text was labeled as
'Twitter' and now it has been renamed to 'X/Twitter'.

Motivation:
- To change the presentation of the page.
- To improve the experience of the user.
- To show the correct name of the platform.
</commit_message>
</git_diff>
</commit_message>
2025-02-16 12:12:29 +08:00
3f8b0dd231 feat: implement social sharing functionality
- Added `share_controller.js` to handle sharing logic.
- Created `_share_social.html.erb` partial for social sharing buttons.
- Integrated share buttons into city and weather art show pages.
- Added sharer.js dependency.

This feature allows users to share city and weather art pages on
various social media platforms such as Facebook, Twitter, LinkedIn,
Pinterest, Telegram, and WhatsApp, increasing content visibility.
2025-02-16 11:58:25 +08:00
d4deddbb8c feat: fetch city state information
Some checks are pending
Docker / docker (push) Waiting to run
CI / scan_ruby (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
- Added state association to City model
- Modified City view to display state information

This change includes proper input validation and error handling.
Added city state information to views and models to improve data accuracy and user experience.
2025-02-16 01:35:36 +08:00
6f2a42b92b refactor: prioritize latest arts over featured
- Comment out featured arts in home controller
- Update index.html.erb to display latest arts instead of featured

This change aims to keep the displayed arts up-to-date and fresh by showing the latest ones, enhancing user experience by providing more current content.
2025-02-16 01:14:25 +08:00
0af41e24a8 feat: fix: generate weather art
Add logging for the daily generated counts

The change log adds a log statement to display the
count of slots generated.
The code change includes two parts:
*   a debug log of
    number of slots generated and
    the number of
    generation slots
*   a change that generates the
    number of
    art per day
    to the
    user's log
    informing
    the art generated.
The logs are added for monitoring.
The log
statement outputs the
number of generated art and the limit.
</commit_message>
2025-02-16 01:08:49 +08:00
2f84dde40f refactor: remove random arts from homepage
The code refactoring is done by commenting out the random art functionality from the index page and removing the corresponding code from the home_controller. This eliminates the need for a random art display, simplifying the application's behavior.

This also fixes a bug where it was showing random arts and makes it so the application runs more efficiently since it's not calling the unnecessary code anymore.
2025-02-15 17:47:44 +08:00
abdb40e4bf refactor: fix: timezone format issue
The commit fixes issues in the `Country` model to properly handle
timezones when it's a string. The change processes the content and
parses it as JSON data, and fixes an issue with JSON format.

- The `format_timezones` method now attempts to parse and reformat
  timezones to prevent potential JSON parsing errors.
- If the conversion to JSON fails, logs the error.
- The code ensures data integrity
  by parsing and reformatting the JSON for timezone data to solve a
  bug.
</commit_message>
</commit_message>
</commit_message>
  This ensures the data is correctly formatted for easier handling
  and prevents potential runtime errors when reading timezone data.
</commit_message>
2025-02-15 17:32:43 +08:00
eb9bfc7972 feat: add auto ad to city page and weather art
The auto ad is added to the city page and the weather art page. It improves user experience by showing relevant ads to the user. This change should not affect performance as it only modifies the view.

- Add auto ad in city page and weather art page.
- Refactor shadow effects to improve user experience.
- No side effects or any other consequences.
2025-02-15 17:30:29 +08:00
5e716a46d9 fix: parse timezones as JSON
- Updated timezone parsing to handle JSON format.
- Modified how timezones are fetched in multiple models.
- Added a migration to convert existing timezones data.
- Modified the country model to format timezones.

This commit fixes an issue where the timezone data was not
being correctly parsed, leading to potential errors in time
calculations.  The changes ensure the application correctly
handles and displays timezone information, improving the
accuracy of time-related features.
2025-02-15 17:21:02 +08:00
2ffb1a4248 refactor: Convert country timezones to JSON and handle errors
This commit refactors the database migration to convert the
timezones for each country in the database to a JSON format.
It addresses an issue where the timezone data was not properly
formatted. It provides a default value if there's an error to avoid
data loss. It also includes error handling to catch and log exceptions
during the migration process, preventing the entire migration from failing.
2025-02-15 16:54:59 +08:00
ca3691004f feat: Convert country timezones to JSONB format
- Changed `timezones` attribute from text to JSONB in `Country`.
- Updated related model methods to handle JSONB data.
- Added a migration to convert existing timezone data.
- Used safe navigation operators to prevent errors.

This change improves the storage and management of timezone
information by using the JSONB data type.  It includes data
migration to handle existing timezone data.
2025-02-15 16:51:36 +08:00
983564d534 feat: add auto ad integration
- Add a new file to render the ad.
- Modify `app/views/cities/show.html.erb` to render
 the ad.
- The change is to add a new feature to the ad.
2025-02-15 16:39:17 +08:00
98a335100b refactor: move map partial to shared directory
- Renamed `app/views/cities/_map.html.erb` to
  `app/views/shared/_map.html.erb`
- Updated references to the map partial in `cities/show.html.erb` and
  `weather_arts/show.html.erb`
- Adjusted the layout in `cities/show.html.erb` and
  `weather_arts/show.html.erb` for visual consistency.

This refactoring improves code reusability and maintainability by
centralizing the map component. It also improves the visual
presentation of the application.
2025-02-15 16:32:58 +08:00
67dcaf7a9d Merge branch 'dev' 2025-02-15 16:15:14 +08:00
d3faea06a1 fix: update the weather art url
- Change the method of checking for attached images from `.
- attached` to `.attached?` for consistency

This commit addresses a bug where the weather art image was not
displayed correctly on the map. It fixes the issue by using the
`.attached?` method, which ensures that the correct image URL is
used.
2025-02-15 16:15:00 +08:00
73dcd4df6a refactor: update weather art display logic
This commit updates the map functionality to ensure the weather art's
display is correctly rendered. This includes modifications to the
weather art display. It ensures the correct image is shown and improves
the overall application experience.
2025-02-15 16:14:06 +08:00
a2992d998b Merge branch 'dev' 2025-02-15 13:14:34 +08:00
299107b988 feat: modify logging level of batch job worker
The logging level of the batch job worker has been changed from info to debug. The change affects how the batch job worker outputs information about the cities it is processing.

- Original behavior: The batch job worker logs cities' information at the info level.
- New behavior: The batch job worker logs cities' information at the debug level.

The motivation behind this change is to reduce the noise in the logs. The new behavior will only log cities' information that is relevant for debugging purposes.
2025-02-15 12:32:04 +08:00
2042732787 feat: improve worker logging
- Added current time logging to the worker to help debug and
  analyze processing times
- Cleaned up assets initializer file
2025-02-15 12:30:48 +08:00
936db76437 refactor: Improve weather art generation process
- Add formatted_current_time method to City model.
- Modify BatchGenerateWeatherArtsWorker to check local time.
- Update Sidekiq scheduler to run every hour.

This commit improves the weather art generation by considering
local time for image generation and increasing the frequency
of batch processing for testing purposes. The new method
provides formatted time information for each city. This change
ensures that images are generated during daylight hours.
2025-02-15 12:20:24 +08:00
df074a81a8 fix: access city attribute correctly in WeatherArt model
- Modified the formatted_time method to use 'self' when accessing the city attribute
- This change fixes a bug where the city attribute was not being accessed correctly, ensuring proper timezone information retrieval.
2025-02-15 12:01:19 +08:00
968efb5492 fix: update UI of the show page
- Refactor to remove the link's padding-top.
- The navigation back button should now work as expected.
- No known side effects.
2025-02-15 11:50:36 +08:00
b6635e5a51 fix: fix map marker click and handle null values
-   Comment out marker click listener to fix a bug
-   Update _map.html.erb to handle null values in the city
-   Improve error handling

This commit addresses a bug where clicking the map marker
caused unexpected behavior.  The changes involve commenting out
the click listener to fix the issue, and updating the view
template to handle potential null values in the city object to
prevent unexpected behavior.
2025-02-15 11:48:40 +08:00
daa0ceac3e refactor: replace OpenLayers with Mapbox GL
- Replace OpenLayers with Mapbox GL for map rendering.
- Update CSS for map styling and control visibility.
- Integrate weather art into map popups.
- Add Mapbox token to credentials.yml.enc

This commit replaces the existing OpenLayers map implementation
with Mapbox GL. It also adjusts the styling and adds a
weather art display to the map popup, which enhances the
user experience. The necessary changes include modifying
stylesheets, JavaScript controllers, view templates, and
updating the credentials file. The motivation is to enhance
map rendering performance and user experience.
2025-02-15 11:21:12 +08:00
9ac7dd46af feat: migrate to OpenLayers for map display
Some checks are pending
Docker / docker (push) Waiting to run
- Replaced Leaflet with OpenLayers for improved map rendering
- Added OpenLayers CSS and removed Leaflet CSS
- Updated map controller to use OpenLayers API
- Added marker icon in public directory
- Added scopes and associations for weather art in City model

This change migrates the map display from Leaflet to
OpenLayers, providing better performance and more features.
It also introduces new model associations for weather arts,
allowing to sort cities by latest weather updates.
2025-02-15 00:19:04 +08:00
df456d1031 feat: add map feature to city show page
- Implement MapController for displaying city maps
- Add map rendering in the city show view
- Include weather arts in the city show controller
- Update asset pipeline to include Leaflet CSS and JS

This commit introduces a map feature that allows users to view
geographical information related to cities. The map is integrated
with weather arts data, enhancing the overall functionality of
the city show page.
2025-02-14 18:05:03 +08:00
95f94cb73b feat: add statistics card component and refactor views
- Introduce a new partial for displaying weather statistics in
  a consistent card format.
- Refactor the city show page to utilize the new statistics
  card partial, simplifying the code structure.
- Update layout and styling for improved UX and maintainability.

These changes enhance the maintainability of the codebase by
promoting reuse of the statistics card component and improving
the overall presentation of weather data for cities.
2025-02-14 14:56:51 +08:00
f43a6b4698 feat: add formatted time method for weather_art
- Introduce a new method `formatted_time` in the `WeatherArt` model
- Update various views to use this new method for date and time display
- Support formatting in local time zones or UTC

This update enhances the time representation for weather data, ensuring
that displayed times can reflect the user's local timezone or remain
fixed at UTC. This improves the usability of the application for
users in different regions.
2025-02-14 13:42:42 +08:00
681ad5320f style: update time display format
- Replace local timezone display with UTC in the weather
  display components
- Adjust time formatting in both city and weather art views

This change standardizes the time format across the application,
  making it clear that the displayed times are in UTC, aiding user
  understanding and consistency in time representation.
2025-02-14 11:58:16 +08:00
6f655ee792 Merge branch 'dev'
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
2025-02-14 11:09:59 +08:00
b01cbe960a fix: round load time to the nearest whole number
- Update load time display to show the rounded value instead of the
  raw measurement.
- This change improves the readability of the load time for end users
  by providing a more user-friendly format.

The adjustment enhances the user experience by presenting the load time
in a cleaner and more comprehensible manner.
2025-02-14 11:09:33 +08:00
ec8f89e07a feat: add rack-mini-profiler for performance monitoring
- Include 'rack-mini-profiler' gem in Gemfile
- Update Gemfile.lock with the new dependency
- Create initializer for rack-mini-profiler in development

This addition allows for performance monitoring within the
application during development, which can help identify
and address performance issues more effectively.
2025-02-14 10:47:16 +08:00
de4823cfc9 Merge branch 'dev' 2025-02-14 10:34:44 +08:00
1c13b89854 style: improve footer layout and text labels
- Simplified page views and visitors display in footer
- Improved readability by using labels and abbreviations
- Updated Tailwind CSS configuration for better font fallback

These changes enhance the user interface in the footer by making the
presentation of page views and visitors more concise and visually
appealing. The update also ensures a better font fallback
experience across different platforms.
2025-02-14 10:27:26 +08:00
95afdf1096 refactor: optimize includes for weather arts
- Changed includes for WeatherArt to optimize query
- Removed redundant includes of country in WeatherArt
- Simplified region fetching by directly ordering

These modifications improve the efficiency of the queries
by reducing unnecessary joins and utilizing ActiveRecord’s
query capabilities more effectively. The code change does not
affect the overall functionality but improves maintainability.
2025-02-14 10:01:24 +08:00
18977a9d42 feat: improve retrieval of weather arts
- Include associated city and image attachment in popular,
  random, latest, and featured arts queries to enhance data
  accessibility and performance.
- This modification reduces N+1 query issues by eager-loading
  associations, leading to improved query performance when
  accessing related data.
2025-02-14 09:55:04 +08:00
496dcf83a9 feat: add page load time tracking and footer
- Introduced PageLoadTimeController to measure and display
  page load times.
- Moved the footer content into a partial for better code
  organization.
- Added 'bullet' gem for performance monitoring.

This commit enhances the application by providing useful
information about load time directly in the UI and
refactoring the layout code for maintainability.
2025-02-14 09:47:01 +08:00
c5f8140aa6 Merge branch 'dev'
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
2025-02-13 21:51:08 +08:00
c808b72c63 refactor: use preview image for featured arts
Some checks are pending
Docker / docker (push) Waiting to run
- Use `preview_image(:large).processed` instead of `image`
  for displaying featured arts.

This change improves the performance of the homepage by
displaying a processed preview image instead of the original,
potentially large, image. This reduces the initial load time
and improves user experience.
2025-02-13 21:50:57 +08:00
bebb22079b Merge branch 'dev' 2025-02-13 21:48:58 +08:00
50183539f5 fix: use processed preview image for art display
- Update image tag to display the processed preview image
- Ensures better performance and visual consistency

This change improves the rendering of art images by using
processed versions, which may reduce load time and improve
appearance on the front end.
2025-02-13 21:36:18 +08:00
4844c8b983 Merge branch 'dev' 2025-02-13 18:01:26 +08:00
ec96914067 refactor: improve string representation for countries
- Update links in cities index to include country emoji
- Modify display in cities show view to use emoji and country name
- Clean up random scope method in weather_art model for consistency

These changes enhance the user interface presentation by making
country names visually enriched with emojis, improving the overall
user experience with clearer visual cues.
2025-02-13 17:58:41 +08:00
eb16f5886d feat: enhance weather art display and functionality
- Update to include a new 'random' scope in WeatherArt model.
- Modify HomeController to fetch random and popular arts along with latest arts.
- Refactor index.html.erb to utilize a partial for rendering arts.

This update improves the display of weather art by adding a new
random selection of arts alongside popular ones, increasing user
engagement and variety. The code structure is also cleaner due
to the use of a partial view for rendering arts, promoting
the DRY principle.
2025-02-13 17:48:39 +08:00
ee7ff023df Merge branch 'dev' 2025-02-13 17:22:11 +08:00
be88aebac2 fix: update AI service logging and weather services 2025-02-13 17:21:41 +08:00
caf22a00ca style: update logging format in weather arts worker
- Change the order of displayed city attributes in logs
- Adjust tabulation for clearer output

This commit modifies the logging format used in the
BatchGenerateWeatherArtsWorker class. The attribute order for
city information in the log output has been changed to ensure
a more intuitive display, with regions now appearing before
countries and states. This will enhance readability during
logging without altering any functionality.
2025-02-13 16:54:15 +08:00
80d623b0c2 style: update description in sidekiq scheduler
- Changed description for the 'batch_generate_weather' job from
  'Generate weather arts every 2 hours' to 'Batch Generate weather
  arts'.
- This change provides a more concise description of the job's
  purpose without altering its functionality.
2025-02-13 16:19:33 +08:00
15c1fc654d feat: refine city selection process for weather art
- Implement daily generation limit for weather arts
- Improve city selection by prioritizing active cities
- Introduce logging for city generation start time and list

This update enhances the efficiency of the `BatchGenerateWeatherArtsWorker`. It ensures that the system respects a daily limit on the number of generated images and improves how cities are selected for processing by prioritizing active cities while also conditionally selecting others if slots remain. Additionally, the added logging provides better insights into the generation process.
2025-02-13 16:18:40 +08:00
5ae0367525 feat: enhance image handling for weather arts
- Remove deprecated `image_with_watermark` attachment.
- Introduce `webp_image`, `preview_image`, and `watermarked_image` methods in `WeatherArt` model for optimized image formats.
- Update views to use new image variants, including webp and previews, improving loading times and visual quality.
- Ensure images are processed with relevant attributes such as quality and dimensions for better performance and user experience.

These changes enhance the image handling capabilities of the application by ensuring images are served in a more efficient format (WebP) and with improved resizing options, leading to better performance overall.
2025-02-13 15:25:12 +08:00
eda20ecca5 style: improve whitespace in SQL join syntax
- Adjusted join statement in SQLite and PostgreSQL
- Ensure consistent use of whitespace for better readability

This change improves code clarity without changing any
query behavior, making it easier for future developers
to maintain and read the SQL join conditions.
2025-02-13 13:16:06 +08:00
5efe441fa8 style: improve sign out button layout
- Add padding and flex layout to the sign out button
- Wrap the icon and text in a div for better structure

These changes update the sign out button in the navbar for improved
accessibility and aesthetics, aligning it better with the overall UI
style.
2025-02-12 18:05:23 +08:00
7ebf20aa7b feat: enhance navbar responsiveness and styling
- Adjust main navigation layout for better spacing
- Improve mobile menu accessibility and usability
- Introduce a separate user menu partial for cleaner structure
- Update button styles and sizes for consistency

These changes aim to enhance the user experience by ensuring that the navbar is visually appealing and functional across different screen sizes, making navigation easier.
2025-02-12 18:03:40 +08:00
afcb9c6cd8 refactor: simplify scope limit parameters
- Change `limit:` to `limit =` in the scopes for
  `latest` and `by_popularity`.
- This allows for a default value of 100 to be used
  if no argument is provided, making the code more
  user-friendly and consistent.

This refactor does not alter the behavior of the
existing functionality but streamlines the syntax.
2025-02-12 18:00:30 +08:00
0a6926421f refactor: update popularity scope and controller logic
- Change `by_popularity` method in `City` model to use `start_time` for filtering views.
- Update `by_popularity` in `WeatherArt` model to limit results and incorporate the correct popularity calculation.
- Adjust `HomeController` to define `@popular_shuffle_arts` and `@latest_arts` with appropriate scopes.

These changes improve the accuracy of popularity retrieval for both cities and weather arts, simplifying the logic in the controller and leveraging updated model methods for performance.
2025-02-12 17:54:57 +08:00
940f1a8f76 feat: add country filter to cities controller
- Introduce filtering of cities by selected country
- Retrieve current country using friendly find method
- Apply city filtering if country is provided in parameters

This change allows users to efficiently filter cities based on the
selected country, providing a more tailored experience and
encouraging better navigation through the list of cities.
2025-02-12 15:12:23 +08:00
a1f1f2b728 fix: add turbo_frame data attribute to buttons
- Update buttons to include data attribute for Turbo frames
- Ensure proper behavior of navigation with turbo-enabled requests

This change enhances the functionality of the buttons by allowing them
 to interact with Turbo frames, improving performance and user
 experience when transitioning between different views on the application.
2025-02-12 14:59:19 +08:00
80c2f9a1df feat: add loading spinner and search updates
- Implement loading state in the search input with spinner.
- Optimize the search request to handle pending requests and cancels.
- Add dynamic response handling for Turbo frames to load search results.
- Create a new partial for city search results.
- Update the cities controller to support Turbo stream responses.

These enhancements improve user experience during searches by showing a loading
spinner and addressing potential issues with overlapping requests, ensuring
that the application remains responsive and functional when fetching city
search results.
2025-02-12 14:47:30 +08:00
799f3222a9 feat: add city search functionality
- Implement search_by_name scope in City model
- Add SearchController for handling search input
- Include _search_city partial in cities index view
- Update cities_controller to filter cities based on search query

This commit introduces a new feature that allows users to search for
cities by name using an input field. The search is implemented as a
scope in the City model, and it is integrated into the existing
CitiesController. A dedicated SearchController manages the input
submission with a debouncing mechanism for better performance. The
search field is rendered in the cities index view, enhancing user
interactivity and experience.
2025-02-12 14:00:03 +08:00
51d626a67f feat: add city popularity displays by time period
- Implement new scope in City model to fetch popular cities
- Query modified to consider different time periods: today, week, month, and year
- Update Ahoy Dashboard to show top cities for each time period

This change enhances the dashboard, providing insights into city popularity over varying durations, facilitating better data analysis.
2025-02-12 13:28:47 +08:00
34342a9678 refactor: make country and state associations optional
- Change 'belongs_to :country' association to be optional
- Change 'belongs_to :state' association to be optional

This refactoring allows a City record to exist without having
an associated Country or State, thus increasing the flexibility
of the data model for various use cases.
2025-02-12 13:08:12 +08:00
122 changed files with 3818 additions and 1460 deletions

View File

@ -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

View File

@ -1,4 +1,4 @@
name: Docker
name: Docker Main
on:
push:

1
.gitignore vendored
View File

@ -40,3 +40,4 @@
.idea
public/sitemap.xml.gz
public/sitemaps

View File

@ -1 +1 @@
ruby-3.3.5
3.3.5

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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.",

View 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

View File

@ -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

View File

@ -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
}

View File

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

View File

@ -1,2 +1,5 @@
module SitemapsHelper
def sitemap_url(filename)
"/sitemaps/#{filename}"
end
end

View File

@ -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)

View 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);
}
}

View 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 );
});
}
}

View 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')
}
}
}

View 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()
}
}

View File

@ -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
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("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count DESC")
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")
.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
@ -151,5 +216,36 @@ class City < ApplicationRecord
self.id)
.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

View 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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -1,151 +1,124 @@
<div class="relative min-h-screen bg-base-200">
<!-- 背景效果 -->
<% if @city.latest_weather_art&.image&.attached? %>
<div class="fixed inset-0 -z-10">
<%= image_tag @city.latest_weather_art.image,
class: "absolute w-full h-full object-cover scale-110 filter blur-2xl opacity-25" %>
<div class="absolute inset-0 bg-gradient-to-b from-base-200/90 to-base-200/70 backdrop-blur-md"></div>
</div>
<% end %>
<div class="min-h-screen bg-base-100">
<!-- 页面标题和背景 -->
<div class="relative">
<!-- 背景图像和渐变 -->
<% if @city.latest_weather_art&.image&.attached? %>
<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>
</h1>
<div class="flex flex-wrap justify-center items-center gap-3">
<div class="badge badge-lg badge-primary gap-2">
<%= @city.country.name %>, <%= @city.region %>
<div class="flex flex-wrap justify-center items-center gap-3">
<div class="badge badge-lg badge-primary gap-2">
<%= "#{@city&.country&.emoji + " " || ""}#{@city&.country&.name}" %>
</div>
<div class="badge badge-lg badge-secondary gap-2">
<%= @city&.state&.name %>
</div>
</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" %>
<!-- 主要统计信息 -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-8">
<%= 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="card bg-base-100 backdrop-blur-md shadow-lg border border-primary/20 overflow-hidden">
<%= render 'shared/map', city: @city %>
</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 class="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-8">
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("temperature") %>
<div class="stat-title font-medium">Latest Weather</div>
</div>
<div class="stat-value text-2xl"><%= @city.latest_weather_art&.temperature %>°C</div>
<div class="stat-desc mt-1"><%= @city.latest_weather_art&.description %></div>
</div>
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("location") %>
<div class="stat-title font-medium">Coordinates</div>
</div>
<div class="stat-value text-xl">
<%= @city.latitude %>°N,
<%= @city.longitude %>°E
</div>
<div class="stat-desc mt-1">Geographical Location</div>
</div>
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("history") %>
<div class="stat-title font-medium">Records</div>
</div>
<div class="stat-value text-2xl"><%= @city.weather_arts.count %></div>
<div class="stat-desc mt-1">Total Weather Arts</div>
</div>
</div>
<%= render 'cities/admin_panel' %>
</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>
</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 %>
<%= render partial: 'weather_arts/card', collection: @arts, as: :weather_art %>
</div>
</div>
</div>
</div>
</div>
</div>

View 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>

View File

@ -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>

View 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>

View File

@ -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>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<%= 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 %>
<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>
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><% 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">
<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>
<% end %>
<% end %>
<% else %>
<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>
<%= t("title.sign_in") %>
<% end %>
</li>
<% end %>
</ul>
</div>
</div>
</div>
</div>

View 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>

View File

@ -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>

View 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

View 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>

View 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>

View 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>

View File

@ -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 %>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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">
<%= 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 %>
<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 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 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>
<%= "#{t("button.back_to")} #{@weather_art.city.name}" %>
<% end %>
<!-- 标题区域 -->
<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&.city&.country&.emoji + " " || ""}#{@city&.country&.localized_name}" %>
</div>
<div class="badge badge-lg badge-secondary">
<%= @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>
<!-- 主要内容 -->
<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>
<% 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">
<%= @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") %>
</div>
<div class="badge badge-lg badge-secondary">
<%= @weather_art.created_at.strftime("%H:%M") %>
</div>
</div>
<h2 class="text-2xl font-semibold mb-4">
<%= 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 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>
</div>
<!-- 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"><%= t("title.ai_prompt") %></h3>
</div>
<p class="text-base-content/80 leading-relaxed">
<%= @weather_art.prompt %>
</p>
</div>
</div>
</div>
<!-- AI Prompt -->
<div class="bg-primary/10 backdrop-blur-md p-6 rounded-lg border border-primary/20">
<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/80 leading-relaxed">
<%= @weather_art.prompt %>
</p>
<!-- 地图区域 -->
<div class="card bg-base-100 shadow-lg mb-8">
<%= render 'shared/map', city: @weather_art.city %>
</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 class="card bg-base-100 shadow-lg mb-8">
<%= render 'shared/auto_ad' %>
</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>
<!-- 社交分享 -->
<%
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>
</div>

View File

@ -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

View File

@ -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")

View File

@ -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/"
end
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
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),
add city_path(city, locale: locale),
changefreq: "daily",
priority: 0.8,
lastmod: city.updated_at
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')}"
} ]
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

View File

@ -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==

View File

@ -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

View File

@ -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 )

View File

@ -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

View 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

View 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
View 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
View 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"

View File

@ -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'

View File

@ -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
View 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"

View File

@ -28,4 +28,90 @@
# enabled: "ON"
en:
hello: "Hello world"
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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"

View File

@ -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'

View File

@ -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
View 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
View 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
View 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
View 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
View 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 日"

View File

@ -1,49 +1,53 @@
require "sidekiq/web"
Rails.application.routes.draw do
devise_for :users
root "home#index"
scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do
devise_for :users
root "home#index"
resources :cities, only: [ :index, :show ] do
resources :weather_arts, path: "weather", only: [ :show ], param: :slug
end
resources :cities do
member do
post :generate_weather_art, param: :slug
resources :cities, only: [ :index, :show ] do
resources :weather_arts, path: "weather", only: [ :show ], param: :slug
end
resources :cities do
member do
post :generate_weather_art, param: :slug
end
end
resources :arts, only: [ :index ]
# namespace :admin do
# resources :cities
# resources :weather_arts
# root to: "cities#index"
# end
get "weather_arts/show"
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)
# mount Sidekiq::Web => '/sidekiq'
# authenticate :admin_user do
authenticate :user, lambda { |u| u.admin? } do
mount Sidekiq::Web => "/admin/tasks"
end
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check
# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)
# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
# Defines the root path route ("/")
# root "posts#index"
end
resources :arts, only: [ :index ]
# namespace :admin do
# resources :cities
# resources :weather_arts
# root to: "cities#index"
# end
get "weather_arts/show"
get "cities/index"
get "cities/show"
get "home/index"
get "sitemaps/*path", to: "sitemaps#show", format: false
devise_for :admin_users, ActiveAdmin::Devise.config
ActiveAdmin.routes(self)
# mount Sidekiq::Web => '/sidekiq'
# authenticate :admin_user do
authenticate :user, lambda { |u| u.admin? } do
mount Sidekiq::Web => "/admin/tasks"
end
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check
# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)
# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
# Defines the root path route ("/")
# root "posts#index"
end

View File

@ -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:

View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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!([
{

View File

@ -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
}
])

View File

@ -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
}
])

View File

@ -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
}
])

View File

@ -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
)

View File

@ -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
}
])

View File

@ -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
}
])

View File

@ -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
}
])

View File

@ -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