Compare commits
No commits in common. "main" and "new" have entirely different histories.
48
.github/workflows/docker-dev.yml
vendored
48
.github/workflows/docker-dev.yml
vendored
@ -1,48 +0,0 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: docker.io
|
||||
IMAGE_NAME: ${{ github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # 获取完整的 git history 以便生成正确的 tag
|
||||
-
|
||||
name: Get Version
|
||||
id: get_version
|
||||
run: |
|
||||
echo "VERSION=$(git describe --dirty --always --tags --abbrev=7)" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Login to ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{github.actor}}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:dev
|
51
.github/workflows/docker-main.yml
vendored
51
.github/workflows/docker-main.yml
vendored
@ -1,51 +0,0 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: docker.io
|
||||
IMAGE_NAME: ${{ github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # 获取完整的 git history 以便生成正确的 tag
|
||||
-
|
||||
name: Get Version
|
||||
id: get_version
|
||||
run: |
|
||||
echo "VERSION=$(git describe --dirty --always --tags --abbrev=7)" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Login to ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{github.actor}}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:main
|
||||
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:${{ steps.get_version.outputs.VERSION }}
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -37,6 +37,3 @@
|
||||
!/app/assets/builds/.keep
|
||||
|
||||
/node_modules
|
||||
.idea
|
||||
|
||||
public/sitemap.xml.gz
|
||||
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="mise: 3.3.5" project-jdk-type="RUBY_SDK" />
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/today_ai_weather.iml" filepath="$PROJECT_DIR$/.idea/today_ai_weather.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
367
.idea/today_ai_weather.iml
generated
Normal file
367
.idea/today_ai_weather.iml
generated
Normal file
@ -0,0 +1,367 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="RUBY_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="RailsFacetType" name="Ruby on Rails">
|
||||
<configuration>
|
||||
<RAILS_FACET_CONFIG_ID NAME="RAILS_FACET_SUPPORT_REMOVED" VALUE="false" />
|
||||
<RAILS_FACET_CONFIG_ID NAME="RAILS_TESTS_SOURCES_PATCHED" VALUE="true" />
|
||||
<RAILS_FACET_CONFIG_ID NAME="RAILS_FACET_APPLICATION_ROOT" VALUE="$MODULE_DIR$" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="ModuleRunConfigurationManager">
|
||||
<shared />
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/components" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/log" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/public/packs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/public/system" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/cache" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="actioncable (v8.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="actionmailbox (v8.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="actionmailer (v8.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="actionpack (v8.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="actiontext (v8.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="actionview (v8.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="activeadmin (v3.2.5, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="activejob (v8.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="activemodel (v8.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="activerecord (v8.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="activestorage (v8.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="activesupport (v8.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="addressable (v2.8.7, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="arbre (v1.7.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="ast (v2.4.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="base64 (v0.2.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="bcrypt (v3.1.20, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="bcrypt_pbkdf (v1.1.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="benchmark (v0.4.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="bigdecimal (v3.1.9, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="bindex (v0.8.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="bootsnap (v1.18.4, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="brakeman (v7.0.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="builder (v3.3.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="bundler (v2.6.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="capybara (v3.40.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.5, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="connection_pool (v2.5.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="cssbundling-rails (v1.4.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="csv (v3.3.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="date (v3.4.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="debug (v1.10.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="devise (v4.9.4, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="dotenv (v3.1.7, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="drb (v2.2.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="ed25519 (v1.3.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="erubi (v1.13.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="et-orbi (v1.2.11, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="formtastic (v5.0.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="formtastic_i18n (v0.7.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="friendly_id (v5.5.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="fugit (v1.11.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="globalid (v1.2.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="has_scope (v0.8.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="i18n (v1.14.6, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="inherited_resources (v1.14.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="io-console (v0.8.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="irb (v1.14.3, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="jbuilder (v2.13.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="jquery-rails (v4.6.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="jsbundling-rails (v1.3.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="json (v2.9.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="kamal (v2.4.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="kaminari (v1.2.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="kaminari-actionview (v1.2.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="kaminari-activerecord (v1.2.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="kaminari-core (v1.2.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.3, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="logger (v1.6.5, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="loofah (v2.24.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="mail (v2.8.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="marcel (v1.0.4, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="matrix (v0.4.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="mini_mime (v1.1.5, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="minitest (v5.25.4, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="msgpack (v1.7.5, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="net-imap (v0.5.5, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="net-pop (v0.1.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="net-protocol (v0.2.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="net-scp (v4.0.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="net-sftp (v4.0.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="net-smtp (v0.5.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="net-ssh (v7.3.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="nio4r (v2.7.4, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="nokogiri (v1.18.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="orm_adapter (v0.5.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="ostruct (v0.6.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="parallel (v1.26.3, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="parser (v3.3.7.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="propshaft (v1.1.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="psych (v5.2.3, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="public_suffix (v6.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="puma (v6.5.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="raabro (v1.4.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="racc (v1.8.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rack (v3.1.8, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rack-session (v2.1.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rack-test (v2.2.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rackup (v2.2.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rails (v8.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rails-dom-testing (v2.2.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rails-html-sanitizer (v1.6.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="railties (v8.0.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rake (v13.2.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="ransack (v4.2.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rdoc (v6.11.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="regexp_parser (v2.10.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="reline (v0.6.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="responders (v3.1.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rexml (v3.4.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rubocop (v1.70.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rubocop-ast (v1.37.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rubocop-minitest (v0.36.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rubocop-performance (v1.23.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rubocop-rails (v2.29.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rubocop-rails-omakase (v1.0.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="ruby2_keywords (v0.0.5, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rubyzip (v2.4.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="securerandom (v0.4.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="selenium-webdriver (v4.27.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="solid_cable (v3.0.5, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="solid_cache (v1.0.6, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="solid_queue (v1.1.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="sqlite3 (v2.5.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="sshkit (v1.23.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="stimulus-rails (v1.3.4, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="stringio (v3.1.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="thor (v1.3.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="thruster (v0.1.10, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="timeout (v0.4.3, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="turbo-rails (v2.0.11, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="tzinfo (v2.0.6, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="unicode-display_width (v3.1.4, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="unicode-emoji (v4.0.4, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="uri (v1.0.2, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="useragent (v0.16.11, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="warden (v1.2.9, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="web-console (v4.2.1, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="websocket (v1.2.11, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="websocket-driver (v0.7.7, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="websocket-extensions (v0.1.5, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="xpath (v3.2.0, mise: 3.3.5) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="zeitwerk (v2.7.1, mise: 3.3.5) [gem]" level="application" />
|
||||
</component>
|
||||
<component name="RModuleSettingsStorage">
|
||||
<LOAD_PATH number="0" />
|
||||
<I18N_FOLDERS number="1" string0="$MODULE_DIR$/config/locales" />
|
||||
</component>
|
||||
<component name="RailsGeneratorsCache">
|
||||
<option name="generators">
|
||||
<list>
|
||||
<option value="active_record:application_record" />
|
||||
<option value="active_record:multi_db" />
|
||||
<option value="application_record" />
|
||||
<option value="authentication" />
|
||||
<option value="benchmark" />
|
||||
<option value="channel" />
|
||||
<option value="controller" />
|
||||
<option value="erb:authentication" />
|
||||
<option value="erb:controller" />
|
||||
<option value="erb:mailer" />
|
||||
<option value="erb:scaffold" />
|
||||
<option value="generator" />
|
||||
<option value="helper" />
|
||||
<option value="integration_test" />
|
||||
<option value="jbuilder" />
|
||||
<option value="job" />
|
||||
<option value="kaminari:config" />
|
||||
<option value="kaminari:views" />
|
||||
<option value="mailbox" />
|
||||
<option value="mailer" />
|
||||
<option value="migration" />
|
||||
<option value="model" />
|
||||
<option value="resource" />
|
||||
<option value="scaffold" />
|
||||
<option value="scaffold_controller" />
|
||||
<option value="script" />
|
||||
<option value="solid_cable:install" />
|
||||
<option value="solid_cable:update" />
|
||||
<option value="solid_cache:install" />
|
||||
<option value="solid_queue:install" />
|
||||
<option value="stimulus" />
|
||||
<option value="system_test" />
|
||||
<option value="tailwindcss:authentication" />
|
||||
<option value="task" />
|
||||
<option value="test_unit:authentication" />
|
||||
<option value="test_unit:channel" />
|
||||
<option value="test_unit:generator" />
|
||||
<option value="test_unit:install" />
|
||||
<option value="test_unit:mailbox" />
|
||||
<option value="test_unit:plugin" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="myGenerators">
|
||||
<list>
|
||||
<option value="active_record:application_record" />
|
||||
<option value="active_record:multi_db" />
|
||||
<option value="application_record" />
|
||||
<option value="authentication" />
|
||||
<option value="benchmark" />
|
||||
<option value="channel" />
|
||||
<option value="controller" />
|
||||
<option value="erb:authentication" />
|
||||
<option value="erb:controller" />
|
||||
<option value="erb:mailer" />
|
||||
<option value="erb:scaffold" />
|
||||
<option value="generator" />
|
||||
<option value="helper" />
|
||||
<option value="integration_test" />
|
||||
<option value="jbuilder" />
|
||||
<option value="job" />
|
||||
<option value="kaminari:config" />
|
||||
<option value="kaminari:views" />
|
||||
<option value="mailbox" />
|
||||
<option value="mailer" />
|
||||
<option value="migration" />
|
||||
<option value="model" />
|
||||
<option value="resource" />
|
||||
<option value="scaffold" />
|
||||
<option value="scaffold_controller" />
|
||||
<option value="script" />
|
||||
<option value="solid_cable:install" />
|
||||
<option value="solid_cable:update" />
|
||||
<option value="solid_cache:install" />
|
||||
<option value="solid_queue:install" />
|
||||
<option value="stimulus" />
|
||||
<option value="system_test" />
|
||||
<option value="tailwindcss:authentication" />
|
||||
<option value="task" />
|
||||
<option value="test_unit:authentication" />
|
||||
<option value="test_unit:channel" />
|
||||
<option value="test_unit:generator" />
|
||||
<option value="test_unit:install" />
|
||||
<option value="test_unit:mailbox" />
|
||||
<option value="test_unit:plugin" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="RailsPaths" isManagedAutomatically="true">
|
||||
<entry key="app">
|
||||
<value>file://$MODULE_DIR$/app</value>
|
||||
</entry>
|
||||
<entry key="app/assets">
|
||||
<value>file://$MODULE_DIR$/app/assets</value>
|
||||
</entry>
|
||||
<entry key="app/channels">
|
||||
<value>file://$MODULE_DIR$/app/channels</value>
|
||||
</entry>
|
||||
<entry key="app/controllers">
|
||||
<value>file://$MODULE_DIR$/app/controllers</value>
|
||||
</entry>
|
||||
<entry key="app/helpers">
|
||||
<value>file://$MODULE_DIR$/app/helpers</value>
|
||||
</entry>
|
||||
<entry key="app/mailers">
|
||||
<value>file://$MODULE_DIR$/app/mailers</value>
|
||||
</entry>
|
||||
<entry key="app/models">
|
||||
<value>file://$MODULE_DIR$/app/models</value>
|
||||
</entry>
|
||||
<entry key="app/views">
|
||||
<value>file://$MODULE_DIR$/app/views</value>
|
||||
</entry>
|
||||
<entry key="config">
|
||||
<value>file://$MODULE_DIR$/config</value>
|
||||
</entry>
|
||||
<entry key="config/cable">
|
||||
<value>file://$MODULE_DIR$/config/cable.yml</value>
|
||||
</entry>
|
||||
<entry key="config/cache">
|
||||
<value>file://$MODULE_DIR$/config/cache.yml</value>
|
||||
</entry>
|
||||
<entry key="config/database">
|
||||
<value>file://$MODULE_DIR$/config/database.yml</value>
|
||||
</entry>
|
||||
<entry key="config/environment">
|
||||
<value>file://$MODULE_DIR$/config/environment.rb</value>
|
||||
</entry>
|
||||
<entry key="config/environments">
|
||||
<value>file://$MODULE_DIR$/config/environments</value>
|
||||
</entry>
|
||||
<entry key="config/initializers">
|
||||
<value>file://$MODULE_DIR$/config/initializers</value>
|
||||
</entry>
|
||||
<entry key="config/locales">
|
||||
<value>file://$MODULE_DIR$/config/locales</value>
|
||||
</entry>
|
||||
<entry key="config/routes">
|
||||
<value>file://$MODULE_DIR$/config/routes</value>
|
||||
</entry>
|
||||
<entry key="config/routes.rb">
|
||||
<value>file://$MODULE_DIR$/config/routes.rb</value>
|
||||
</entry>
|
||||
<entry key="config/solid_cache">
|
||||
<value>file://$MODULE_DIR$/config/solid_cache.yml</value>
|
||||
</entry>
|
||||
<entry key="db">
|
||||
<value>file://$MODULE_DIR$/db</value>
|
||||
</entry>
|
||||
<entry key="db/migrate">
|
||||
<value>file://$MODULE_DIR$/db/migrate</value>
|
||||
</entry>
|
||||
<entry key="db/seeds.rb">
|
||||
<value>file://$MODULE_DIR$/db/seeds.rb</value>
|
||||
</entry>
|
||||
<entry key="lib">
|
||||
<value>file://$MODULE_DIR$/lib</value>
|
||||
</entry>
|
||||
<entry key="lib/assets">
|
||||
<value>file://$MODULE_DIR$/lib/assets</value>
|
||||
</entry>
|
||||
<entry key="lib/tasks">
|
||||
<value>file://$MODULE_DIR$/lib/tasks</value>
|
||||
</entry>
|
||||
<entry key="lib/templates">
|
||||
<value>file://$MODULE_DIR$/lib/templates</value>
|
||||
</entry>
|
||||
<entry key="log">
|
||||
<value>file://$MODULE_DIR$/log/development.log</value>
|
||||
</entry>
|
||||
<entry key="public">
|
||||
<value>file://$MODULE_DIR$/public</value>
|
||||
</entry>
|
||||
<entry key="public/javascripts">
|
||||
<value>file://$MODULE_DIR$/public/javascripts</value>
|
||||
</entry>
|
||||
<entry key="public/stylesheets">
|
||||
<value>file://$MODULE_DIR$/public/stylesheets</value>
|
||||
</entry>
|
||||
<entry key="test/mailers/previews">
|
||||
<value>file://$MODULE_DIR$/test/mailers/previews</value>
|
||||
<value>file://$MODULE_DIR$/test/mailers/previews</value>
|
||||
</entry>
|
||||
<entry key="tmp">
|
||||
<value>file://$MODULE_DIR$/tmp</value>
|
||||
</entry>
|
||||
<entry key="vendor">
|
||||
<value>file://$MODULE_DIR$/vendor</value>
|
||||
</entry>
|
||||
<entry key="vendor/assets">
|
||||
<value>file://$MODULE_DIR$/vendor/assets</value>
|
||||
</entry>
|
||||
</component>
|
||||
</module>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -16,7 +16,7 @@ WORKDIR /rails
|
||||
|
||||
# Install base packages
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 libpq5 redis-tools && \
|
||||
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
|
||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||
|
||||
# Set production environment
|
||||
@ -30,7 +30,7 @@ FROM base AS build
|
||||
|
||||
# Install packages needed to build gems and node modules
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y build-essential git node-gyp pkg-config python-is-python3 libpq-dev && \
|
||||
apt-get install --no-install-recommends -y build-essential git node-gyp pkg-config python-is-python3 && \
|
||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||
|
||||
# Install JavaScript dependencies
|
||||
@ -59,7 +59,7 @@ COPY . .
|
||||
RUN bundle exec bootsnap precompile app/ lib/
|
||||
|
||||
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
|
||||
RUN RAILS_BUILD=1 SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
|
||||
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
|
||||
|
||||
|
||||
RUN rm -rf node_modules
|
||||
@ -75,7 +75,7 @@ COPY --from=build /rails /rails
|
||||
# Run and own only the runtime files as a non-root user for security
|
||||
RUN groupadd --system --gid 1000 rails && \
|
||||
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
|
||||
chown -R rails:rails db log storage tmp public
|
||||
chown -R rails:rails db log storage tmp
|
||||
USER 1000:1000
|
||||
|
||||
# Entrypoint prepares the database.
|
||||
|
29
Gemfile
29
Gemfile
@ -41,28 +41,9 @@ gem "thruster", require: false
|
||||
|
||||
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
|
||||
# gem "image_processing", "~> 1.2"
|
||||
gem "devise", "~> 4.9"
|
||||
gem "activeadmin", "~> 3.2"
|
||||
gem "friendly_id", "~> 5.5"
|
||||
|
||||
gem "kaminari", "~> 1.2"
|
||||
|
||||
gem "meta-tags", "~> 2.22"
|
||||
gem "sitemap_generator", "~> 6.3"
|
||||
|
||||
gem "ahoy_matey", "~> 5.2"
|
||||
|
||||
# gem "whenever", "~> 1.0"
|
||||
gem "ruby-openai", "~> 7.3"
|
||||
gem "httparty", "~> 0.22.0"
|
||||
gem "down", "~> 5.4"
|
||||
gem "aws-sdk-s3", "~> 1.177"
|
||||
gem "sidekiq", "~> 7.3"
|
||||
gem "sidekiq-scheduler", "~> 5.0"
|
||||
|
||||
gem "image_processing", "~> 1.13"
|
||||
gem "ruby-vips", "~> 2.2"
|
||||
gem "mini_magick", "~> 4.13.2"
|
||||
gem 'devise', '~> 4.9'
|
||||
gem 'activeadmin', '~> 3.2'
|
||||
gem 'friendly_id', '~> 5.5'
|
||||
|
||||
group :development, :test do
|
||||
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
||||
@ -80,10 +61,6 @@ group :development do
|
||||
gem "web-console"
|
||||
end
|
||||
|
||||
group :production do
|
||||
gem "pg", "~> 1.5"
|
||||
end
|
||||
|
||||
group :test do
|
||||
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
|
||||
gem "capybara"
|
||||
|
142
Gemfile.lock
142
Gemfile.lock
@ -84,30 +84,10 @@ GEM
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ahoy_matey (5.3.0)
|
||||
activesupport (>= 7)
|
||||
device_detector (>= 1)
|
||||
safely_block (>= 0.4)
|
||||
arbre (1.7.0)
|
||||
activesupport (>= 3.0.0)
|
||||
ruby2_keywords (>= 0.0.2)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1044.0)
|
||||
aws-sdk-core (3.217.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.97.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.179.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.11.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt_pbkdf (1.1.1)
|
||||
@ -140,7 +120,6 @@ GEM
|
||||
debug (1.10.0)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
device_detector (1.1.3)
|
||||
devise (4.9.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
@ -148,30 +127,11 @@ GEM
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
dotenv (3.1.7)
|
||||
down (5.4.2)
|
||||
addressable (~> 2.8)
|
||||
drb (2.2.1)
|
||||
ed25519 (1.3.0)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
event_stream_parser (1.0.0)
|
||||
faraday (2.12.2)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (3.4.0)
|
||||
net-http (>= 0.5.0)
|
||||
ffi (1.17.1-aarch64-linux-gnu)
|
||||
ffi (1.17.1-aarch64-linux-musl)
|
||||
ffi (1.17.1-arm-linux-gnu)
|
||||
ffi (1.17.1-arm-linux-musl)
|
||||
ffi (1.17.1-arm64-darwin)
|
||||
ffi (1.17.1-x86_64-darwin)
|
||||
ffi (1.17.1-x86_64-linux-gnu)
|
||||
ffi (1.17.1-x86_64-linux-musl)
|
||||
formtastic (5.0.0)
|
||||
actionpack (>= 6.0.0)
|
||||
formtastic_i18n (0.7.0)
|
||||
@ -185,29 +145,20 @@ GEM
|
||||
has_scope (0.8.2)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
httparty (0.22.0)
|
||||
csv
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (1.14.7)
|
||||
i18n (1.14.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.13.0)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
inherited_resources (1.14.0)
|
||||
actionpack (>= 6.0)
|
||||
has_scope (>= 0.6)
|
||||
railties (>= 6.0)
|
||||
responders (>= 2)
|
||||
io-console (0.8.0)
|
||||
irb (1.15.1)
|
||||
pp (>= 0.6.0)
|
||||
irb (1.14.3)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jbuilder (2.13.0)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.6.2)
|
||||
jquery-rails (4.6.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
@ -238,7 +189,7 @@ GEM
|
||||
activerecord
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-core (1.2.2)
|
||||
language_server-protocol (3.17.0.4)
|
||||
language_server-protocol (3.17.0.3)
|
||||
logger (1.6.5)
|
||||
loofah (2.24.0)
|
||||
crass (~> 1.0.2)
|
||||
@ -250,17 +201,9 @@ GEM
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
meta-tags (2.22.1)
|
||||
actionpack (>= 6.0.0, < 8.1)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.4)
|
||||
msgpack (1.7.5)
|
||||
multi_xml (0.7.1)
|
||||
bigdecimal (~> 3.1)
|
||||
multipart-post (2.4.1)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-imap (0.5.5)
|
||||
date
|
||||
net-protocol
|
||||
@ -268,7 +211,7 @@ GEM
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-scp (4.1.0)
|
||||
net-scp (4.0.0)
|
||||
net-ssh (>= 2.6.5, < 8.0.0)
|
||||
net-sftp (4.0.0)
|
||||
net-ssh (>= 5.0.0, < 8.0.0)
|
||||
@ -276,21 +219,21 @@ GEM
|
||||
net-protocol
|
||||
net-ssh (7.3.0)
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.2-aarch64-linux-gnu)
|
||||
nokogiri (1.18.1-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.2-aarch64-linux-musl)
|
||||
nokogiri (1.18.1-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.2-arm-linux-gnu)
|
||||
nokogiri (1.18.1-arm-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.2-arm-linux-musl)
|
||||
nokogiri (1.18.1-arm-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.2-arm64-darwin)
|
||||
nokogiri (1.18.1-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.2-x86_64-darwin)
|
||||
nokogiri (1.18.1-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.2-x86_64-linux-gnu)
|
||||
nokogiri (1.18.1-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.2-x86_64-linux-musl)
|
||||
nokogiri (1.18.1-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
orm_adapter (0.5.0)
|
||||
ostruct (0.6.1)
|
||||
@ -298,10 +241,6 @@ GEM
|
||||
parser (3.3.7.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.9)
|
||||
pp (0.6.2)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
propshaft (1.1.0)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
@ -311,11 +250,11 @@ GEM
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.6.0)
|
||||
puma (6.5.0)
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.9)
|
||||
rack (3.1.8)
|
||||
rack-session (2.1.0)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
@ -360,8 +299,6 @@ GEM
|
||||
i18n
|
||||
rdoc (6.11.0)
|
||||
psych (>= 4.0.0)
|
||||
redis-client (0.23.2)
|
||||
connection_pool
|
||||
regexp_parser (2.10.0)
|
||||
reline (0.6.0)
|
||||
io-console (~> 0.5)
|
||||
@ -369,17 +306,17 @@ GEM
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.4.0)
|
||||
rubocop (1.71.1)
|
||||
rubocop (1.70.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-ast (>= 1.36.2, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.38.0)
|
||||
rubocop-ast (1.37.0)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-minitest (0.36.0)
|
||||
rubocop (>= 1.61, < 2.0)
|
||||
@ -387,7 +324,7 @@ GEM
|
||||
rubocop-performance (1.23.1)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rails (2.29.1)
|
||||
rubocop-rails (2.29.0)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.52.0, < 2.0)
|
||||
@ -397,39 +334,17 @@ GEM
|
||||
rubocop-minitest
|
||||
rubocop-performance
|
||||
rubocop-rails
|
||||
ruby-openai (7.3.1)
|
||||
event_stream_parser (>= 0.3.0, < 2.0.0)
|
||||
faraday (>= 1)
|
||||
faraday-multipart (>= 1)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.2)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
rufus-scheduler (3.9.2)
|
||||
fugit (~> 1.1, >= 1.11.1)
|
||||
safely_block (0.4.1)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.28.0)
|
||||
selenium-webdriver (4.27.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
sidekiq (7.3.8)
|
||||
base64
|
||||
connection_pool (>= 2.3.0)
|
||||
logger
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.22.2)
|
||||
sidekiq-scheduler (5.0.6)
|
||||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 6, < 8)
|
||||
tilt (>= 1.4.0, < 3)
|
||||
sitemap_generator (6.3.0)
|
||||
builder (~> 3.0)
|
||||
solid_cable (3.0.7)
|
||||
solid_cable (3.0.5)
|
||||
actioncable (>= 7.2)
|
||||
activejob (>= 7.2)
|
||||
activerecord (>= 7.2)
|
||||
@ -438,7 +353,7 @@ GEM
|
||||
activejob (>= 7.2)
|
||||
activerecord (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
solid_queue (1.1.3)
|
||||
solid_queue (1.1.2)
|
||||
activejob (>= 7.1)
|
||||
activerecord (>= 7.1)
|
||||
concurrent-ruby (>= 1.3.1)
|
||||
@ -468,7 +383,6 @@ GEM
|
||||
thruster (0.1.10-arm64-darwin)
|
||||
thruster (0.1.10-x86_64-darwin)
|
||||
thruster (0.1.10-x86_64-linux)
|
||||
tilt (2.6.0)
|
||||
timeout (0.4.3)
|
||||
turbo-rails (2.0.11)
|
||||
actionpack (>= 6.0.0)
|
||||
@ -510,35 +424,21 @@ PLATFORMS
|
||||
|
||||
DEPENDENCIES
|
||||
activeadmin (~> 3.2)
|
||||
ahoy_matey (~> 5.2)
|
||||
aws-sdk-s3 (~> 1.177)
|
||||
bootsnap
|
||||
brakeman
|
||||
capybara
|
||||
cssbundling-rails
|
||||
debug
|
||||
devise (~> 4.9)
|
||||
down (~> 5.4)
|
||||
friendly_id (~> 5.5)
|
||||
httparty (~> 0.22.0)
|
||||
image_processing (~> 1.13)
|
||||
jbuilder
|
||||
jsbundling-rails
|
||||
kamal
|
||||
kaminari (~> 1.2)
|
||||
meta-tags (~> 2.22)
|
||||
mini_magick (~> 4.13.2)
|
||||
pg (~> 1.5)
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
rails (~> 8.0.1)
|
||||
rubocop-rails-omakase
|
||||
ruby-openai (~> 7.3)
|
||||
ruby-vips (~> 2.2)
|
||||
selenium-webdriver
|
||||
sidekiq (~> 7.3)
|
||||
sidekiq-scheduler (~> 5.0)
|
||||
sitemap_generator (~> 6.3)
|
||||
solid_cable
|
||||
solid_cache
|
||||
solid_queue
|
||||
|
@ -1,5 +1,4 @@
|
||||
ActiveAdmin.register AdminUser do
|
||||
menu label: "AdminUser Manager", parent: "系统管理"
|
||||
permit_params :email, :password, :password_confirmation
|
||||
|
||||
index do
|
||||
@ -25,4 +24,5 @@ ActiveAdmin.register AdminUser do
|
||||
end
|
||||
f.actions
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -1,83 +0,0 @@
|
||||
ActiveAdmin.register_page "Ahoy Dashboard" do
|
||||
menu label: "总览", parent: "数据统计", priority: 1
|
||||
page_action :toggle_city_status, method: :post do
|
||||
city = City.find(params[:city_id])
|
||||
city.update(active: !city.active)
|
||||
redirect_back(fallback_location: admin_dashboard_path, notice: "城市状态已更新")
|
||||
end
|
||||
|
||||
content title: "总览" do
|
||||
columns do
|
||||
column do
|
||||
panel "访问统计" do
|
||||
para "总访问量: #{Ahoy::Visit.count}"
|
||||
para "总事件数: #{Ahoy::Event.count}"
|
||||
para "独立访客数: #{Ahoy::Visit.distinct.count(:visitor_token)}"
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "热门城市" do
|
||||
table_for City.by_popularity.limit(10) do
|
||||
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
|
||||
column("访问量") { |city| city.view_count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "热门天气艺术" do
|
||||
table_for WeatherArt.by_popularity.limit(10) do
|
||||
column("作品") { |art| link_to(art.to_s, admin_weather_art_path(art)) }
|
||||
column("访问量") { |art| art.view_count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
columns do
|
||||
column do
|
||||
panel "最冷门活跃城市" do
|
||||
table_for City.least_popular_active.limit(10) do
|
||||
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
|
||||
column("访问量") { |city| city.view_count }
|
||||
column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
|
||||
# column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
|
||||
column("操作") { |city|
|
||||
button_to "停用",
|
||||
admin_ahoy_dashboard_toggle_city_status_path(city_id: city.id),
|
||||
method: :post,
|
||||
data: { confirm: "确定要停用 #{city.name} 吗?" }
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "热门未活跃城市" do
|
||||
table_for City.most_popular_inactive.limit(10) do
|
||||
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
|
||||
column("访问量") { |city| city.view_count }
|
||||
column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
|
||||
column("所属区域") { |city| city.country.region.name }
|
||||
column("操作") { |city|
|
||||
button_to "激活",
|
||||
admin_ahoy_dashboard_toggle_city_status_path(city_id: city.id),
|
||||
method: :post,
|
||||
data: { confirm: "确定要激活 #{city.name} 吗?" }
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# 添加一个事件列表面板
|
||||
panel "最近事件" do
|
||||
table_for Ahoy::Event.order(time: :desc).limit(10) do
|
||||
column :time
|
||||
column :name
|
||||
column :properties
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,32 +0,0 @@
|
||||
ActiveAdmin.register Ahoy::Event do
|
||||
menu label: "事件统计", parent: "数据统计"
|
||||
# See permitted parameters documentation:
|
||||
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
||||
#
|
||||
# Uncomment all parameters which should be permitted for assignment
|
||||
#
|
||||
# permit_params :visit_id, :user_id, :name, :properties, :time
|
||||
#
|
||||
# or
|
||||
#
|
||||
# permit_params do
|
||||
# permitted = [:visit_id, :user_id, :name, :properties, :time]
|
||||
# permitted << :other if params[:action] == 'create' && current_user.admin?
|
||||
# permitted
|
||||
# end
|
||||
# menu priority: 101, label: "事件统计"
|
||||
|
||||
actions :index
|
||||
|
||||
index do
|
||||
column :id
|
||||
column :name
|
||||
column :time
|
||||
column :properties
|
||||
column :user_id
|
||||
end
|
||||
|
||||
filter :name
|
||||
filter :time
|
||||
filter :properties
|
||||
end
|
@ -1,34 +0,0 @@
|
||||
# app/admin/ahoy_management.rb
|
||||
ActiveAdmin.register_page "Ahoy Management" do
|
||||
menu label: "访问数据管理", parent: "系统管理"
|
||||
|
||||
content title: "访问数据管理" do
|
||||
columns do
|
||||
column do
|
||||
panel "数据统计" do
|
||||
attributes_table_for :ahoy do
|
||||
row("总事件数") { Ahoy::Event.count }
|
||||
row("总访问数") { Ahoy::Visit.count }
|
||||
row("最早事件") { Ahoy::Event.minimum(:time)&.strftime("%Y-%m-%d %H:%M:%S") }
|
||||
row("最早访问") { Ahoy::Visit.minimum(:started_at)&.strftime("%Y-%m-%d %H:%M:%S") }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "操作" do
|
||||
div class: "buttons" do
|
||||
button_to "立即清理旧数据", admin_ahoy_management_cleanup_path, method: :post,
|
||||
data: { confirm: "确定要清理3个月前的数据吗?" },
|
||||
class: "button"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
page_action :cleanup, method: :post do
|
||||
CleanAhoyDataWorker.perform_async
|
||||
redirect_to admin_ahoy_management_path, notice: "清理任务已加入队列"
|
||||
end
|
||||
end
|
@ -1,36 +0,0 @@
|
||||
ActiveAdmin.register Ahoy::Visit do
|
||||
menu label: "访客统计", parent: "数据统计"
|
||||
# See permitted parameters documentation:
|
||||
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
||||
#
|
||||
# Uncomment all parameters which should be permitted for assignment
|
||||
#
|
||||
# permit_params :visit_token, :visitor_token, :user_id, :ip, :user_agent, :referrer, :referring_domain, :landing_page, :browser, :os, :device_type, :country, :region, :city, :latitude, :longitude, :utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign, :app_version, :os_version, :platform, :started_at
|
||||
#
|
||||
# or
|
||||
#
|
||||
# permit_params do
|
||||
# permitted = [:visit_token, :visitor_token, :user_id, :ip, :user_agent, :referrer, :referring_domain, :landing_page, :browser, :os, :device_type, :country, :region, :city, :latitude, :longitude, :utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign, :app_version, :os_version, :platform, :started_at]
|
||||
# permitted << :other if params[:action] == 'create' && current_user.admin?
|
||||
# permitted
|
||||
# end
|
||||
|
||||
# menu priority: 100, label: "访问统计"
|
||||
|
||||
actions :index
|
||||
|
||||
index do
|
||||
column :id
|
||||
column :visitor_token
|
||||
column :ip
|
||||
column :user_agent
|
||||
column :started_at
|
||||
column :city
|
||||
column :country
|
||||
column :region
|
||||
end
|
||||
|
||||
filter :started_at
|
||||
filter :city
|
||||
filter :country
|
||||
end
|
@ -1,5 +1,4 @@
|
||||
ActiveAdmin.register City do
|
||||
menu label: "City Manager", parent: "系统管理"
|
||||
controller do
|
||||
def find_resource
|
||||
scoped_collection.friendly.find(params[:id])
|
||||
@ -11,7 +10,7 @@ ActiveAdmin.register City do
|
||||
#
|
||||
# Uncomment all parameters which should be permitted for assignment
|
||||
#
|
||||
permit_params :name, :country_id, :latitude, :longitude, :active, :priority, :timezone, :last_weather_fetch, :last_image_generation, :slug
|
||||
permit_params :name, :country, :latitude, :longitude, :active, :priority, :timezone, :region, :last_weather_fetch, :last_image_generation, :slug
|
||||
#
|
||||
# or
|
||||
#
|
||||
@ -25,30 +24,31 @@ ActiveAdmin.register City do
|
||||
selectable_column
|
||||
id_column
|
||||
column :name
|
||||
column :country
|
||||
column :region do |city|
|
||||
city.region
|
||||
end
|
||||
column :slug
|
||||
column :latitude
|
||||
column :longitude
|
||||
column :active
|
||||
column :created_at
|
||||
actions
|
||||
end
|
||||
|
||||
filter :name
|
||||
filter :active
|
||||
filter :country, as: :select
|
||||
|
||||
form do |f|
|
||||
f.inputs do
|
||||
f.input :active
|
||||
f.input :name
|
||||
f.input :country
|
||||
f.input :country, as: :String
|
||||
f.input :latitude
|
||||
f.input :longitude
|
||||
f.input :priority
|
||||
f.input :timezone
|
||||
f.input :region
|
||||
f.input :last_weather_fetch
|
||||
f.input :last_image_generation
|
||||
end
|
||||
f.actions
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -1,42 +0,0 @@
|
||||
ActiveAdmin.register Country do
|
||||
menu label: "Country Manager", parent: "系统管理"
|
||||
controller do
|
||||
def find_resource
|
||||
scoped_collection.friendly.find(params[:id])
|
||||
end
|
||||
end
|
||||
# See permitted parameters documentation:
|
||||
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
||||
#
|
||||
# Uncomment all parameters which should be permitted for assignment
|
||||
#
|
||||
permit_params :name, :code, :region_id
|
||||
#
|
||||
# or
|
||||
#
|
||||
# permit_params do
|
||||
# permitted = [:name, :code, :slug, :region_id]
|
||||
# permitted << :other if params[:action] == 'create' && current_user.admin?
|
||||
# permitted
|
||||
# end
|
||||
index do
|
||||
selectable_column
|
||||
id_column
|
||||
column :name
|
||||
column :code
|
||||
column :region
|
||||
column :cities_count do |country|
|
||||
country.cities.count
|
||||
end
|
||||
actions
|
||||
end
|
||||
|
||||
form do |f|
|
||||
f.inputs do
|
||||
f.input :region
|
||||
f.input :name
|
||||
f.input :code
|
||||
end
|
||||
f.actions
|
||||
end
|
||||
end
|
@ -1,5 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ActiveAdmin.register_page "Dashboard" do
|
||||
menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }
|
||||
|
||||
@ -11,66 +10,6 @@ ActiveAdmin.register_page "Dashboard" do
|
||||
end
|
||||
end
|
||||
|
||||
columns do
|
||||
column do
|
||||
panel "访问统计" do
|
||||
para "总访问量: #{Ahoy::Visit.count}"
|
||||
para "总事件数: #{Ahoy::Event.count}"
|
||||
para "独立访客数: #{Ahoy::Visit.distinct.count(:visitor_token)}"
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "热门城市" do
|
||||
table_for City.by_popularity.limit(10) do
|
||||
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
|
||||
column("访问量") { |city| city.view_count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "热门天气艺术" do
|
||||
table_for WeatherArt.by_popularity.limit(10) do
|
||||
column("作品") { |art| link_to(art.to_s, admin_weather_art_path(art)) }
|
||||
column("访问量") { |art| art.view_count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
columns do
|
||||
column do
|
||||
panel "最冷门活跃城市" do
|
||||
table_for City.least_popular_active.limit(10) do
|
||||
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
|
||||
column("访问量") { |city| city.view_count }
|
||||
# column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "热门未活跃城市" do
|
||||
table_for City.most_popular_inactive.limit(10) do
|
||||
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
|
||||
column("访问量") { |city| city.view_count }
|
||||
column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
|
||||
column("所属区域") { |city| city.country.region.name }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# 添加一个事件列表面板
|
||||
panel "最近事件" do
|
||||
table_for Ahoy::Event.order(time: :desc).limit(10) do
|
||||
column :time
|
||||
column :name
|
||||
column :properties
|
||||
end
|
||||
end
|
||||
|
||||
# Here is an example of a simple dashboard with columns and panels.
|
||||
#
|
||||
# columns do
|
||||
|
@ -1,30 +0,0 @@
|
||||
ActiveAdmin.register Region do
|
||||
menu label: "Region Manager", parent: "系统管理"
|
||||
# See permitted parameters documentation:
|
||||
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
||||
#
|
||||
# Uncomment all parameters which should be permitted for assignment
|
||||
#
|
||||
permit_params :name, :code
|
||||
#
|
||||
# or
|
||||
#
|
||||
# permit_params do
|
||||
# permitted = [:name, :code, :slug]
|
||||
# permitted << :other if params[:action] == 'create' && current_user.admin?
|
||||
# permitted
|
||||
# end
|
||||
index do
|
||||
selectable_column
|
||||
id_column
|
||||
column :name
|
||||
column :code
|
||||
column :countries_count do |region|
|
||||
region.countries.count
|
||||
end
|
||||
column :cities_count do |region|
|
||||
region.cities.count
|
||||
end
|
||||
actions
|
||||
end
|
||||
end
|
@ -1,98 +0,0 @@
|
||||
# app/admin/sidekiq_tasks.rb
|
||||
ActiveAdmin.register_page "Sidekiq Tasks" do
|
||||
# menu label: "Sidekiq Tasks", priority: 99
|
||||
menu label: "Sidekiq Tasks Manager", parent: "系统管理"
|
||||
|
||||
content title: "Sidekiq Tasks Management" do
|
||||
div class: "sidekiq-tasks" do
|
||||
panel "Manual Task Execution" do
|
||||
div class: "task-buttons" do
|
||||
div class: "task-button" do
|
||||
h3 "Generate Weather Arts"
|
||||
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
|
||||
end
|
||||
input type: "submit", value: "Run Task", class: "button"
|
||||
end
|
||||
end
|
||||
|
||||
div class: "task-button" do
|
||||
h3 "Batch Generate Weather Arts"
|
||||
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: "BatchGenerateWeatherArts"
|
||||
input type: "submit", value: "Run Task", class: "button"
|
||||
end
|
||||
end
|
||||
|
||||
div class: "task-button" do
|
||||
h3 "Refresh Sitemap"
|
||||
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: "RefreshSitemapWorker"
|
||||
input type: "submit", value: "Run Task", class: "button"
|
||||
end
|
||||
end
|
||||
|
||||
div class: "task-button" do
|
||||
h3 "Clean Ahoy Data"
|
||||
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: "CleanAhoyDataWorker"
|
||||
input type: "submit", value: "Run Task", class: "button"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
panel "Sidekiq Statistics" do
|
||||
stats = Sidekiq::Stats.new
|
||||
table class: "sidekiq-stats" do
|
||||
tr do
|
||||
th "Processed Jobs"
|
||||
td stats.processed
|
||||
end
|
||||
tr do
|
||||
th "Failed Jobs"
|
||||
td stats.failed
|
||||
end
|
||||
tr do
|
||||
th "Enqueued Jobs"
|
||||
td stats.enqueued
|
||||
end
|
||||
tr do
|
||||
th "Scheduled Jobs"
|
||||
td stats.scheduled_size
|
||||
end
|
||||
tr do
|
||||
th "Retry Set Size"
|
||||
td stats.retry_size
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
page_action :run_task, method: :post do
|
||||
task_name = params[:task]
|
||||
city_id = params[:city_id]
|
||||
|
||||
case task_name
|
||||
when "BatchGenerateWeatherArts"
|
||||
BatchGenerateWeatherArtsWorker.perform_async
|
||||
when "GenerateWeatherArtsWorker"
|
||||
GenerateWeatherArtWorker.perform_async(city_id)
|
||||
when "RefreshSitemapWorker"
|
||||
RefreshSitemapWorker.perform_async
|
||||
when "CleanAhoyDataWorker"
|
||||
CleanAhoyDataWorker.perform_async
|
||||
end
|
||||
|
||||
redirect_to admin_sidekiq_tasks_path, notice: "Task #{task_name} has been queued"
|
||||
end
|
||||
end
|
@ -1,10 +1,5 @@
|
||||
ActiveAdmin.register WeatherArt do
|
||||
menu label: "WeatherArt Manager", parent: "系统管理"
|
||||
controller do
|
||||
def find_resource
|
||||
scoped_collection.friendly.find(params[:id])
|
||||
end
|
||||
end
|
||||
|
||||
# See permitted parameters documentation:
|
||||
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
||||
#
|
||||
@ -22,11 +17,9 @@ ActiveAdmin.register WeatherArt do
|
||||
permit_params :city_id, :weather_date, :description, :temperature,
|
||||
:feeling_temp, :humidity, :wind_scale, :wind_speed,
|
||||
:precipitation, :pressure, :visibility, :cloud,
|
||||
:prompt, :image, :slug
|
||||
:prompt, :image
|
||||
|
||||
remove_filter :image_attachment, :image_blob
|
||||
filter :city_id
|
||||
filter :weather_data
|
||||
|
||||
index do
|
||||
selectable_column
|
||||
@ -36,7 +29,7 @@ ActiveAdmin.register WeatherArt do
|
||||
column :description
|
||||
column :temperature
|
||||
column :image do |weather_art|
|
||||
image_tag(weather_art.image, size: "100x100") if weather_art.image.attached?
|
||||
image_tag(weather_art.image, size: '100x100') if weather_art.image.attached?
|
||||
end
|
||||
actions
|
||||
end
|
||||
@ -81,4 +74,5 @@ ActiveAdmin.register WeatherArt do
|
||||
end
|
||||
f.actions
|
||||
end
|
||||
|
||||
end
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 5.9 KiB |
2
app/assets/javascripts/active_admin.js
Normal file
2
app/assets/javascripts/active_admin.js
Normal file
@ -0,0 +1,2 @@
|
||||
//= require active_admin/base
|
||||
import "@activeadmin/activeadmin";
|
@ -1,4 +1,3 @@
|
||||
@import "photoswipe/dist/photoswipe.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
@ -1,24 +0,0 @@
|
||||
# app/concerns/seo_concern.rb
|
||||
module SeoConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :prepare_meta_tags
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prepare_meta_tags
|
||||
set_meta_tags(
|
||||
site: "TodayAIWeather",
|
||||
description: "Discover AI-generated weather art from cities around the world. Real-time weather visualization through artificial intelligence.",
|
||||
keywords: "weather, AI art, weather visualization, city weather, artificial intelligence",
|
||||
og: {
|
||||
title: :title,
|
||||
description: :description,
|
||||
type: "website",
|
||||
url: request.original_url
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
@ -1,74 +1,4 @@
|
||||
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
|
||||
# allow_browser versions: :modern
|
||||
# allow_browser versions: :modern,
|
||||
# patterns: [
|
||||
# # 鸿蒙系统相关
|
||||
# /OpenHarmony/, # 鸿蒙系统标识
|
||||
# /ArkWeb\/[\d.]+/, # 鸿蒙浏览器内核
|
||||
# /Mobile HuaweiBrowser/, # 华为浏览器(新格式)
|
||||
# /HuaweiBrowser\/[\d.]+/, # 华为浏览器(旧格式)
|
||||
#
|
||||
# # 夸克浏览器(更宽松的匹配)
|
||||
# /Quark[\s\/][\d.]+/, # 匹配 "Quark/7.4.6.681" 或 "Quark 7.4.6.681"
|
||||
#
|
||||
# /Mobile Safari/,
|
||||
# /Chrome\/[\d.]+/,
|
||||
# /Quark\/[\d.]+/,
|
||||
# /HuaweiBrowser\/[\d.]+/,
|
||||
# /MiuiBrowser\/[\d.]+/,
|
||||
# /VivoBrowser\/[\d.]+/,
|
||||
# /OppoBrowser\/[\d.]+/,
|
||||
# /UCBrowser\/[\d.]+/,
|
||||
# /QQBrowser\/[\d.]+/,
|
||||
# /MicroMessenger\/[\d.]+/,
|
||||
# /Alipay/,
|
||||
# /BaiduBoxApp/,
|
||||
# /baiduboxapp/i,
|
||||
# /SogouMobile/,
|
||||
# /Weibo/,
|
||||
# /DingTalk/,
|
||||
# /ToutiaoMicroApp/,
|
||||
# /BytedanceWebview/,
|
||||
# /ArkWeb/
|
||||
# ],
|
||||
# on_failure: ->(browser) {
|
||||
# Rails.logger.warn <<~BROWSER_INFO
|
||||
# Browser Blocked:
|
||||
# User Agent: #{browser.ua}
|
||||
# Name: #{browser.name}
|
||||
# Version: #{browser.version}
|
||||
# Platform: #{browser.platform.name}
|
||||
# Device: #{browser.device.name}
|
||||
# Mobile: #{browser.mobile?}
|
||||
# Modern: #{browser.modern?}
|
||||
# Bot: #{browser.bot?}
|
||||
# BROWSER_INFO
|
||||
# }
|
||||
before_action :set_locale
|
||||
after_action :track_action
|
||||
|
||||
def log_browser_info
|
||||
# 构建详细的浏览器信息
|
||||
Rails.logger.debug "User Agent: #{request.user_agent}"
|
||||
|
||||
# 如果是被拦截的浏览器,记录额外信息
|
||||
# unless browser_allowed?
|
||||
# Rails.logger.info "User Agent: #{request.user_agent}"
|
||||
# end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def track_action
|
||||
ahoy.track "Viewed Application", request.path_parameters
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_locale
|
||||
I18n.locale = params[:locale] || I18n.default_locale
|
||||
end
|
||||
allow_browser versions: :modern
|
||||
end
|
||||
|
@ -1,21 +0,0 @@
|
||||
class ArtsController < ApplicationController
|
||||
def index
|
||||
@regions = Region.all
|
||||
@current_region = Region.find(params[:region]) if params[:region].present?
|
||||
|
||||
@weather_arts = WeatherArt.includes(city: [ :country, { country: :region } ])
|
||||
|
||||
if @current_region
|
||||
@weather_arts = @weather_arts.joins(city: :country)
|
||||
.where(countries: { region_id: @current_region.id })
|
||||
end
|
||||
|
||||
@weather_arts = if params[:sort] == "oldest"
|
||||
@weather_arts.order(created_at: :asc)
|
||||
else
|
||||
@weather_arts.order(created_at: :desc)
|
||||
end
|
||||
|
||||
@weather_arts = @weather_arts.page(params[:page]).per(10)
|
||||
end
|
||||
end
|
@ -1,42 +1,9 @@
|
||||
class CitiesController < ApplicationController
|
||||
def index
|
||||
@regions = Region.includes(:countries).order(:name)
|
||||
@cities = City.includes(:country, country: :region).order(:name)
|
||||
|
||||
if params[:region]
|
||||
@current_region = Region.friendly.find(params[:region])
|
||||
@cities = @cities.by_region(@current_region.id)
|
||||
end
|
||||
|
||||
if params[:country]
|
||||
@current_country = Country.friendly.find(params[:country])
|
||||
@cities = @cities.by_country(@current_country.id)
|
||||
end
|
||||
|
||||
@cities = @cities.page(params[:page]).per(10)
|
||||
|
||||
set_meta_tags(
|
||||
title: @current_region ? "Cities in #{@current_region.name}" : "Explore Cities",
|
||||
description: "Discover weather art for cities #{@current_region ? "in #{@current_region.name}" : 'worldwide'}. Real-time AI-generated weather visualization.",
|
||||
keywords: "#{@current_region&.name}, cities, weather art, AI visualization"
|
||||
)
|
||||
@cities = City.all.order(:name)
|
||||
end
|
||||
|
||||
def show
|
||||
@city = City.friendly.find(params[:id])
|
||||
ahoy.track "View City", {
|
||||
city_id: @city.id,
|
||||
name: @city.name,
|
||||
event_type: "city_view"
|
||||
}
|
||||
|
||||
set_meta_tags(
|
||||
title: @city.name,
|
||||
description: "Experience #{@city.name}'s weather through AI-generated art. Daily updates of weather conditions visualized through artificial intelligence.",
|
||||
keywords: "#{@city.name}, #{@city.country.name}, weather art, AI visualization",
|
||||
og: {
|
||||
image: @city.latest_weather_art&.image&.attached? ? url_for(@city.latest_weather_art.image) : nil
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -1,11 +1,6 @@
|
||||
class HomeController < ApplicationController
|
||||
def index
|
||||
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(20).shuffle.last(10)
|
||||
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(6)
|
||||
@featured_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(5)
|
||||
set_meta_tags(
|
||||
title: "AI-Generated Weather Art",
|
||||
description: "Experience weather through artistic AI visualization. Daily updated weather art for cities worldwide.",
|
||||
keywords: "AI weather art, weather visualization, city weather, artificial intelligence"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -1,33 +0,0 @@
|
||||
class SitemapsController < ApplicationController
|
||||
def show
|
||||
path = params[:path]
|
||||
bucket_name =
|
||||
Rails.env.production? ?
|
||||
ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:aws, :bucket)) :
|
||||
ENV.fetch("AWS_DEV_BUCKET", Rails.application.credentials.dig(:aws_dev, :bucket))
|
||||
Rails.logger.info "Sitemap: #{path}"
|
||||
|
||||
begin
|
||||
s3_client = Aws::S3::Client.new
|
||||
response = s3_client.get_object(
|
||||
bucket: bucket_name,
|
||||
key: "sitemaps/#{path}"
|
||||
)
|
||||
|
||||
expires_in 12.hours, public: true
|
||||
content_type = response.content_type || "application/xml"
|
||||
|
||||
send_data(
|
||||
response.body.read,
|
||||
filename: path,
|
||||
type: content_type,
|
||||
disposition: "inline"
|
||||
)
|
||||
rescue Aws::S3::Errors::NoSuchKey
|
||||
render status: :not_found
|
||||
rescue Aws::S3::Errors::ServiceError => e
|
||||
Rails.logger.error "S3 Error: #{e.message}"
|
||||
render status: :internal_server_error
|
||||
end
|
||||
end
|
||||
end
|
@ -1,36 +1,6 @@
|
||||
class WeatherArtsController < ApplicationController
|
||||
def show
|
||||
@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,
|
||||
event_type: "weather_art_view"
|
||||
}
|
||||
ahoy.track "View City", {
|
||||
city_id: @city.id,
|
||||
name: @city.name,
|
||||
event_type: "city_view"
|
||||
}
|
||||
|
||||
set_meta_tags(
|
||||
title: "#{@city.name} Weather Art - #{@weather_art.weather_date.strftime('%B %d, %Y')}",
|
||||
description: "#{@city.name}'s weather visualized through AI art. #{@weather_art.description} at #{@weather_art.temperature}°C.",
|
||||
keywords: "#{@city.name}, weather art, #{@weather_art.description}, AI visualization",
|
||||
og: {
|
||||
image: @weather_art.image.attached? ? url_for(@weather_art.image) : nil
|
||||
}
|
||||
)
|
||||
@weather_art = @city.weather_arts.find(params[:id])
|
||||
end
|
||||
end
|
||||
|
@ -1,24 +1,2 @@
|
||||
module ApplicationHelper
|
||||
def weather_art_schema(weather_art)
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ImageObject",
|
||||
"name": "#{weather_art.city.name} Weather Art",
|
||||
"description": weather_art.description,
|
||||
"datePublished": weather_art.created_at.iso8601,
|
||||
"contentUrl": url_for(weather_art.image),
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "TodayAIWeather"
|
||||
},
|
||||
"locationCreated": {
|
||||
"@type": "Place",
|
||||
"name": weather_art.city.name,
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressCountry": weather_art.city.country.name
|
||||
}
|
||||
}
|
||||
}.to_json.html_safe if weather_art.image.attached?
|
||||
end
|
||||
end
|
||||
|
@ -1,2 +0,0 @@
|
||||
module ArtsHelper
|
||||
end
|
@ -1,2 +0,0 @@
|
||||
module SitemapsHelper
|
||||
end
|
@ -1,51 +1,2 @@
|
||||
module WeatherArtsHelper
|
||||
def weather_description_icon(description)
|
||||
case description&.downcase
|
||||
when /rain/
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>'.html_safe
|
||||
when /cloud/
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||
</svg>'.html_safe
|
||||
when /sun|clear/
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>'.html_safe
|
||||
else
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>'.html_safe
|
||||
end
|
||||
end
|
||||
def weather_stat_icon(type)
|
||||
case type
|
||||
when "temperature"
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>'.html_safe
|
||||
when "wind"
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>'.html_safe
|
||||
when "humidity"
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
||||
</svg>'.html_safe
|
||||
when "visibility"
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>'.html_safe
|
||||
when "pressure"
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>'.html_safe
|
||||
when "cloud"
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||
</svg>'.html_safe
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,6 +0,0 @@
|
||||
|
||||
import './add_jquery'
|
||||
import "jquery/dist/jquery"
|
||||
import "jquery-ui/dist/jquery-ui"
|
||||
import "jquery-ujs"
|
||||
import "@activeadmin/activeadmin"
|
@ -1,4 +0,0 @@
|
||||
import jquery from 'jquery'
|
||||
import $ from 'jquery'
|
||||
window.jQuery = jquery
|
||||
window.$ = $
|
@ -1,10 +1,3 @@
|
||||
// Entry point for the build script in your package.json
|
||||
import "@hotwired/turbo-rails"
|
||||
import "@hotwired/stimulus"
|
||||
import "@fontsource/playfair-display/400.css";
|
||||
import "@fontsource/playfair-display/700.css";
|
||||
import "@fontsource/raleway/400.css";
|
||||
import "@fontsource/raleway/600.css";
|
||||
|
||||
import "./controllers"
|
||||
import "./active_admin"
|
||||
|
@ -6,9 +6,3 @@ import { application } from "./application"
|
||||
|
||||
import HelloController from "./hello_controller"
|
||||
application.register("hello", HelloController)
|
||||
|
||||
import PhotoSwipeLightBoxController from "./photo_swipe_lightbox_controller"
|
||||
|
||||
console.log("ready to register photo-swipe")
|
||||
application.register("photo-swipe-lightbox", PhotoSwipeLightBoxController)
|
||||
console.log("successful to register photo-swipe")
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import PhotoSwipeLightbox from 'photoswipe/lightbox'
|
||||
import PhotoSwipe from 'photoswipe'
|
||||
import 'photoswipe/dist/photoswipe.css'
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ['image', 'gallery']
|
||||
|
||||
connect() {
|
||||
this.initPhotoSwipeLightbox()
|
||||
}
|
||||
|
||||
initPhotoSwipeLightbox() {
|
||||
const lightbox = new PhotoSwipeLightbox({
|
||||
gallery: this.galleryTarget,
|
||||
children: 'a',
|
||||
pswpModule: PhotoSwipe,
|
||||
initialZoomInEndEvent: 'mousedown',
|
||||
dataSource: (items) => {
|
||||
return items.map((item) => ({
|
||||
src: item.dataset.pswpSrc,
|
||||
w: parseInt(item.dataset.pswpWidth, 10),
|
||||
h: parseInt(item.dataset.pswpHeight, 10),
|
||||
title: item.dataset.pswpCaption,
|
||||
}))
|
||||
},
|
||||
padding: { top: 0, bottom: 0, left: 0, right: 0 }, // 自定义图片与页面边界的填充
|
||||
closeOnScroll: false,
|
||||
zoom: true, // 启用缩放功能
|
||||
bgOpacity: 0.9, // 背景透明度
|
||||
pswpUIOptions: {
|
||||
arrowPrev: true,
|
||||
arrowNext: true,
|
||||
zoom: true, // 添加缩放按钮
|
||||
fullscreen: true, // 添加全屏按钮
|
||||
counter: true, // 显示当前图片编号
|
||||
}
|
||||
})
|
||||
lightbox.init()
|
||||
// console.log('PhotoSwipeLightbox instance:', lightbox);
|
||||
}
|
||||
}
|
7
app/jobs/application_job.rb
Normal file
7
app/jobs/application_job.rb
Normal file
@ -0,0 +1,7 @@
|
||||
class ApplicationJob < ActiveJob::Base
|
||||
# Automatically retry jobs that encountered a deadlock
|
||||
# retry_on ActiveRecord::Deadlocked
|
||||
|
||||
# Most jobs are safe to ignore if the underlying records are no longer available
|
||||
# discard_on ActiveJob::DeserializationError
|
||||
end
|
@ -1,14 +0,0 @@
|
||||
class Ahoy::Event < ApplicationRecord
|
||||
# include Ahoy::QueryMethods
|
||||
|
||||
self.table_name = "ahoy_events"
|
||||
|
||||
belongs_to :visit
|
||||
belongs_to :user, optional: true
|
||||
|
||||
serialize :properties, coder: JSON
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
[ "id", "id_value", "name", "properties", "time", "user_id", "visit_id" ]
|
||||
end
|
||||
end
|
@ -1,10 +0,0 @@
|
||||
class Ahoy::Visit < ApplicationRecord
|
||||
self.table_name = "ahoy_visits"
|
||||
|
||||
has_many :events, class_name: "Ahoy::Event"
|
||||
belongs_to :user, optional: true
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
[ "app_version", "browser", "city", "country", "device_type", "id", "ip", "landing_page", "latitude", "longitude", "os", "os_version", "platform", "referrer", "referring_domain", "region", "started_at", "user_agent", "user_id", "utm_campaign", "utm_content", "utm_medium", "utm_source", "utm_term", "visit_token", "visitor_token" ]
|
||||
end
|
||||
end
|
@ -1,96 +1,13 @@
|
||||
class City < ApplicationRecord
|
||||
extend FriendlyId
|
||||
friendly_id :slug_candidates, use: :slugged
|
||||
belongs_to :country
|
||||
friendly_id :name, use: :slugged
|
||||
|
||||
has_many :weather_arts, dependent: :destroy
|
||||
|
||||
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :city_id
|
||||
has_many :events, class_name: "Ahoy::Event", foreign_key: :city_id
|
||||
|
||||
delegate :region, to: :country
|
||||
|
||||
validates :name, presence: true
|
||||
validates :latitude, presence: true
|
||||
validates :longitude, presence: true
|
||||
|
||||
delegate :region, to: :country
|
||||
|
||||
scope :by_region, ->(region_id) { joins(:country).where(countries: { region_id: region_id }) }
|
||||
scope :by_country, ->(country_id) { where(country_id: country_id) }
|
||||
scope :active, -> { where(active: true) }
|
||||
|
||||
scope :by_popularity, -> {
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
|
||||
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
|
||||
.group("cities.id")
|
||||
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count DESC")
|
||||
else
|
||||
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::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")
|
||||
end
|
||||
}
|
||||
|
||||
scope :least_popular_active, -> {
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
active
|
||||
.joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
|
||||
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
|
||||
.group("cities.id")
|
||||
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count ASC, cities.name ASC")
|
||||
else
|
||||
active
|
||||
.joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::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 ASC, cities.name ASC")
|
||||
end
|
||||
}
|
||||
scope :most_popular_inactive, -> {
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
where(active: false)
|
||||
.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("COUNT(ahoy_events.id) DESC, cities.name ASC")
|
||||
else
|
||||
where(active: false)
|
||||
.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("COUNT(ahoy_events.id) DESC, cities.name ASC")
|
||||
end
|
||||
}
|
||||
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def slug_candidates
|
||||
[
|
||||
:name,
|
||||
[ :country, :name ]
|
||||
]
|
||||
end
|
||||
|
||||
def localized_name
|
||||
I18n.t("cities.#{name.parameterize.underscore}", default: name)
|
||||
end
|
||||
|
||||
def full_name
|
||||
"#{name}, #{country}"
|
||||
end
|
||||
|
||||
def should_generate_new_friendly_id?
|
||||
name_changed? || super
|
||||
end
|
||||
@ -100,32 +17,7 @@ class City < ApplicationRecord
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
[ "active", "country_id", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at" ]
|
||||
["active", "country", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at"]
|
||||
end
|
||||
|
||||
def last_weather_fetch
|
||||
# latest_weather_art&.created_at
|
||||
Rails.cache.fetch("city/#{id}/last_weather_fetch", expires_in: 1.hour) do
|
||||
latest_weather_art&.created_at
|
||||
end
|
||||
end
|
||||
|
||||
def last_image_generation
|
||||
# latest_weather_art&.image&.created_at
|
||||
Rails.cache.fetch("city/#{id}/last_image_generation", expires_in: 1.hour) do
|
||||
latest_weather_art&.image&.created_at
|
||||
end
|
||||
end
|
||||
|
||||
def latest_weather_art
|
||||
weather_arts.order(weather_date: :desc).first
|
||||
end
|
||||
|
||||
def view_count
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
Ahoy::Event.where("json_extract(properties, '$.event_type') = 'city_view' AND json_extract(properties, '$.city_id') = ?", self.id).count
|
||||
else
|
||||
Ahoy::Event.where("properties::jsonb->>'event_type' = 'city_view' AND (properties::jsonb->>'city_id')::integer = ?", self.id).count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,26 +0,0 @@
|
||||
class Country < ApplicationRecord
|
||||
extend FriendlyId
|
||||
friendly_id :name, use: :slugged
|
||||
|
||||
belongs_to :region
|
||||
has_many :cities, dependent: :restrict_with_error
|
||||
|
||||
validates :name, presence: true
|
||||
validates :code, presence: true, uniqueness: true
|
||||
|
||||
def to_s
|
||||
name
|
||||
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" ]
|
||||
end
|
||||
|
||||
def self.ransackable_associations(auth_object = nil)
|
||||
[ "cities", "region" ]
|
||||
end
|
||||
end
|
@ -1,28 +0,0 @@
|
||||
class Region < ApplicationRecord
|
||||
extend FriendlyId
|
||||
friendly_id :name, use: :slugged
|
||||
|
||||
has_many :countries, dependent: :restrict_with_error
|
||||
has_many :cities, through: :countries
|
||||
|
||||
validates :name, presence: true
|
||||
validates :code, presence: true, uniqueness: true
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def localized_name
|
||||
I18n.t("regions.#{code}")
|
||||
end
|
||||
|
||||
# 模型中允许被搜索的关联
|
||||
def self.ransackable_associations(auth_object = nil)
|
||||
[ "countries", "cities" ]
|
||||
end
|
||||
|
||||
# 允许被搜索的属性列表
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
[ "code", "created_at", "id", "id_value", "name", "slug", "updated_at" ]
|
||||
end
|
||||
end
|
@ -1,58 +1,16 @@
|
||||
class WeatherArt < ApplicationRecord
|
||||
extend FriendlyId
|
||||
friendly_id :weather_date, use: :slugged
|
||||
|
||||
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
|
||||
has_one_attached :image
|
||||
|
||||
validates :weather_date, presence: true
|
||||
validates :city_id, presence: true
|
||||
|
||||
scope :by_popularity, -> {
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.weather_art_id') = weather_arts.id
|
||||
AND json_extract(ahoy_events.properties, '$.event_type') = 'weather_art_view'")
|
||||
.group("weather_arts.id")
|
||||
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count DESC")
|
||||
else
|
||||
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::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")
|
||||
end
|
||||
}
|
||||
|
||||
def should_generate_new_friendly_id?
|
||||
weather_date_changed? || city_id_changed? || super
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{city.name} - #{weather_date.strftime('%Y-%m-%d')}"
|
||||
end
|
||||
|
||||
def self.ransackable_associations(auth_object = nil)
|
||||
[ "city", "image_attachment", "image_blob" ]
|
||||
["city", "image_attachment", "image_blob"]
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
[ "city_id", "cloud", "created_at", "description", "feeling_temp", "humidity", "id", "id_value", "precipitation", "pressure", "prompt", "temperature", "updated_at", "visibility", "weather_date", "wind_scale", "wind_speed" ]
|
||||
end
|
||||
|
||||
def view_count
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
Ahoy::Event.where("json_extract(properties, '$.event_type') = 'weather_art_view' AND json_extract(properties, '$.weather_art_id') = ?", self.id).count
|
||||
else
|
||||
Ahoy::Event.where("properties::jsonb->>'event_type' = 'weather_art_view' AND (properties::jsonb->>'weather_art_id')::integer = ?", self.id).count
|
||||
end
|
||||
end
|
||||
|
||||
def image_url
|
||||
image.attached? ? image.blob : nil
|
||||
end
|
||||
end
|
||||
|
@ -1,64 +0,0 @@
|
||||
class AiService
|
||||
def initialize
|
||||
@client = OpenAI::Client.new(
|
||||
access_token: Rails.application.credentials.openai.token,
|
||||
uri_base: Rails.application.credentials.openai.uri,
|
||||
request_timeout: 240
|
||||
)
|
||||
end
|
||||
|
||||
def generate_prompt(city, weather_data)
|
||||
response = @client.chat(
|
||||
parameters: {
|
||||
model: "gpt-4",
|
||||
messages: [ {
|
||||
role: "system",
|
||||
content: "You are a professional artist creating prompts for DALL-E 3. Create realistic, artistic weather scenes featuring iconic landmarks."
|
||||
}, {
|
||||
role: "user",
|
||||
content: generate_prompt_request(city, weather_data)
|
||||
} ],
|
||||
temperature: 0.7,
|
||||
max_tokens: 300
|
||||
}
|
||||
)
|
||||
|
||||
response.dig("choices", 0, "message", "content")
|
||||
end
|
||||
|
||||
def generate_image(prompt)
|
||||
response = @client.images.generate(
|
||||
parameters: {
|
||||
model: "dall-e-3",
|
||||
prompt: prompt,
|
||||
size: "1792x1024",
|
||||
quality: "standard",
|
||||
n: 1
|
||||
}
|
||||
)
|
||||
|
||||
response.dig("data", 0, "url")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_prompt_request(city, weather_data)
|
||||
<<~PROMPT
|
||||
Create a DALL-E 3 prompt for a weather scene in #{city.name}, #{city.country.name}.
|
||||
|
||||
Weather conditions:
|
||||
- Temperature: #{weather_data[:temperature]}°C
|
||||
- Weather: #{weather_data[:description]}
|
||||
- Cloud cover: #{weather_data[:cloud]}%
|
||||
- Time: #{weather_data[:time]}
|
||||
|
||||
Requirements:
|
||||
- Feature iconic landmarks or architecture from #{city.name}
|
||||
- Realistic style
|
||||
- Weather conditions should be clearly visible
|
||||
- Atmospheric and artistic composition
|
||||
|
||||
Generate a detailed, creative prompt that will produce a beautiful and realistic image.
|
||||
PROMPT
|
||||
end
|
||||
end
|
@ -1,38 +0,0 @@
|
||||
# app/services/weather_service.rb
|
||||
class WeatherService
|
||||
include HTTParty
|
||||
base_uri Rails.application.credentials.dig(:qweather, :uri)
|
||||
|
||||
def initialize
|
||||
@api_key = Rails.application.credentials.qweather.token
|
||||
end
|
||||
|
||||
def get_weather(latitude, longitude)
|
||||
Rails.logger.debug "Get Weather for #{latitude},#{longitude}"
|
||||
response = self.class.get(
|
||||
"/weather/now",
|
||||
headers: {
|
||||
"X-QW-Api-Key" => "#{@api_key}"
|
||||
},
|
||||
query: {
|
||||
location: "#{longitude},#{latitude}"
|
||||
})
|
||||
|
||||
return nil unless response.success?
|
||||
|
||||
data = response["now"]
|
||||
{
|
||||
temperature: data["temp"].to_f,
|
||||
feeling_temp: data["feelsLike"].to_f,
|
||||
humidity: data["humidity"].to_f,
|
||||
wind_scale: "#{data['windScale']}级",
|
||||
wind_speed: data["windSpeed"].to_f,
|
||||
precipitation: data["precip"].to_f,
|
||||
pressure: data["pressure"].to_f,
|
||||
visibility: data["vis"].to_f,
|
||||
cloud: data["cloud"].to_f,
|
||||
description: data["text"],
|
||||
time: response["updateTime"]
|
||||
}
|
||||
end
|
||||
end
|
@ -1,172 +0,0 @@
|
||||
<!-- app/views/arts/index.html.erb -->
|
||||
<div class="min-h-screen">
|
||||
<!-- 页面标题和背景 -->
|
||||
<% featured_art = @weather_arts.first %>
|
||||
<div class="relative">
|
||||
<!-- 背景图像 -->
|
||||
<% if featured_art&.image&.attached? %>
|
||||
<div class="absolute inset-0 h-[40vh] overflow-hidden">
|
||||
<%= image_tag featured_art.image,
|
||||
class: "w-full h-full object-cover" %>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-base-100/30 via-base-100/60 to-base-100"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- 标题内容 -->
|
||||
<div class="relative pt-20 pb-32">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-3xl mx-auto text-center space-y-6">
|
||||
<h1 class="text-4xl md:text-5xl font-display font-bold">
|
||||
Weather Arts Gallery
|
||||
</h1>
|
||||
<p class="text-xl text-base-content/70">
|
||||
Discover AI-generated weather art from cities around the world
|
||||
</p>
|
||||
|
||||
<!-- 如果有特色图片,显示其信息 -->
|
||||
<% if featured_art %>
|
||||
<div class="text-sm text-base-content/60 pt-4">
|
||||
Latest from <%= featured_art.city.name %>, <%= featured_art.city.country.name %>
|
||||
<span class="mx-2">•</span>
|
||||
<%= featured_art.weather_date.strftime("%B %d, %Y") %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选导航 -->
|
||||
<div class="container mx-auto px-4 -mt-8">
|
||||
<div class="bg-base-100 shadow-xl rounded-box p-6 mb-12">
|
||||
<!-- 筛选选项 -->
|
||||
<div class="flex flex-wrap gap-4 justify-center items-center">
|
||||
<!-- 时间排序 -->
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-ghost gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<%= params[:sort] == 'oldest' ? 'Oldest First' : 'Newest First' %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
|
||||
<li>
|
||||
<%= link_to "Newest First", arts_path(sort: 'newest', region: params[:region]),
|
||||
class: "#{'active' if params[:sort] != 'oldest'}" %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to "Oldest First", arts_path(sort: 'oldest', region: params[:region]),
|
||||
class: "#{'active' if params[:sort] == 'oldest'}" %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 区域筛选 -->
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-ghost gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<%= @current_region&.name || 'All Regions' %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
|
||||
<li>
|
||||
<%= link_to "All Regions", arts_path(sort: params[:sort]),
|
||||
class: "#{'active' unless @current_region}" %>
|
||||
</li>
|
||||
<div class="divider my-1"></div>
|
||||
<% @regions.each do |region| %>
|
||||
<li>
|
||||
<%= link_to region.name, arts_path(region: region.id, sort: params[:sort]),
|
||||
class: "#{'active' if @current_region == region}" %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果统计 -->
|
||||
<div class="text-center text-sm text-base-content/70 mt-4">
|
||||
Showing <%= @weather_arts.total_count %> weather arts
|
||||
<% if @current_region %>
|
||||
from <%= @current_region.name %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto px-4 pb-16">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
<% @weather_arts.each do |art| %>
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 group overflow-hidden">
|
||||
<figure class="relative aspect-square overflow-hidden">
|
||||
<% if art.image.attached? %>
|
||||
<%= image_tag art.image,
|
||||
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
|
||||
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
|
||||
<div class="absolute inset-0 p-6 flex flex-col justify-end translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300">
|
||||
<div class="text-white space-y-2">
|
||||
<h3 class="text-xl font-display font-bold">
|
||||
<%= art.city.name %>
|
||||
</h3>
|
||||
<p class="text-sm text-white/80">
|
||||
<%= art.city.country.name %>
|
||||
</p>
|
||||
<div class="flex items-center gap-2 text-white/90">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||
</svg>
|
||||
<%= art.description %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</figure>
|
||||
|
||||
<!-- 信息部分 -->
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<h3 class="font-display font-bold leading-tight">
|
||||
<%= art.city.name %>
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/70">
|
||||
<%= art.weather_date.strftime("%B %d, %Y") %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-2xl font-bold text-primary">
|
||||
<%= art.temperature %>°C
|
||||
</div>
|
||||
<div class="text-sm text-base-content/70">
|
||||
<%= art.humidity %>% humidity
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= link_to city_weather_art_path(art.city, art),
|
||||
class: "btn btn-primary btn-sm w-full" do %>
|
||||
View Details
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= render 'shared/pagination',
|
||||
collection: @weather_arts,
|
||||
collection_name: 'weather arts' %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,126 +0,0 @@
|
||||
<!-- app/views/cities/_city.html.erb -->
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 group overflow-hidden">
|
||||
<% if city.latest_weather_art&.image&.attached? %>
|
||||
<!-- 图片和主要信息区域 -->
|
||||
<div class="relative">
|
||||
<!-- 图片 -->
|
||||
<figure class="aspect-[16/9] overflow-hidden">
|
||||
<%= image_tag city.latest_weather_art.image,
|
||||
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
|
||||
</figure>
|
||||
|
||||
<!-- 渐变遮罩 - 使用多层渐变提供更好的文字可读性 -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-transparent"></div>
|
||||
|
||||
<!-- 温度显示 - 放在右上角 -->
|
||||
<div class="absolute top-4 right-4 bg-black/30 backdrop-blur-sm rounded-xl px-3 py-2 text-white">
|
||||
<div class="text-2xl font-bold leading-none">
|
||||
<%= city.latest_weather_art.temperature %>°C
|
||||
</div>
|
||||
<div class="text-xs text-white/80">
|
||||
Feels <%= city.latest_weather_art.feeling_temp %>°C
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 城市名称和位置 - 放在底部 -->
|
||||
<div class="absolute bottom-0 left-0 right-0 p-6">
|
||||
<h3 class="text-2xl font-display text-white font-bold mb-2 drop-shadow-lg">
|
||||
<%= city.name %>
|
||||
</h3>
|
||||
<div class="flex items-center gap-2 text-white/90 text-sm drop-shadow-lg">
|
||||
<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 %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部信息区域 -->
|
||||
<div class="card-body bg-base-100">
|
||||
<!-- 天气信息和更新时间 -->
|
||||
<div class="flex items-center justify-between text-sm text-base-content/70 mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary/70" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||
</svg>
|
||||
<%= city.latest_weather_art.description %>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<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") %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 天气详情 -->
|
||||
<div class="grid grid-cols-3 gap-2 mb-4">
|
||||
<div class="stat bg-base-200/50 rounded-lg p-2">
|
||||
<div class="stat-title text-xs">Humidity</div>
|
||||
<div class="stat-value text-lg"><%= city.latest_weather_art.humidity %>%</div>
|
||||
</div>
|
||||
<div class="stat bg-base-200/50 rounded-lg p-2">
|
||||
<div class="stat-title text-xs">Wind</div>
|
||||
<div class="stat-value text-lg"><%= city.latest_weather_art.wind_scale %></div>
|
||||
</div>
|
||||
<div class="stat bg-base-200/50 rounded-lg p-2">
|
||||
<div class="stat-title text-xs">Visibility</div>
|
||||
<div class="stat-value text-lg"><%= city.latest_weather_art.visibility %>km</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<div class="card-actions justify-end">
|
||||
<%= link_to city_path(city),
|
||||
class: "btn btn-primary btn-sm gap-2" do %>
|
||||
View Details
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% else %>
|
||||
<!-- 无图片时的备用显示 -->
|
||||
<div class="card-body">
|
||||
<h3 class="card-title font-display text-2xl mb-3"><%= city.name %></h3>
|
||||
|
||||
<div class="flex items-center gap-2 text-base-content/70 mb-4">
|
||||
<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 %>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-200 rounded-lg p-4 mb-4">
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary/70" 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>
|
||||
<span>Lat: <%= city.latitude %></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary/70" 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>
|
||||
<span>Long: <%= city.longitude %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end">
|
||||
<%= link_to city_path(city),
|
||||
class: "btn btn-primary btn-sm gap-2" do %>
|
||||
View Details
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
@ -1,126 +1,18 @@
|
||||
<!-- app/views/cities/index.html.erb -->
|
||||
<div class="min-h-screen">
|
||||
<!-- 页面标题和背景 -->
|
||||
<% featured_art = WeatherArt.includes(:city).joins(:image_attachment).order(created_at: :desc).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,
|
||||
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 class="space-y-8">
|
||||
<h1 class="text-3xl font-bold">Cities</h1>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<% @cities.each do |city| %>
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title"><%= city.name %></h2>
|
||||
<p>Latitude: <%= city.latitude %></p>
|
||||
<p>Longitude: <%= city.longitude %></p>
|
||||
<div class="card-actions justify-end">
|
||||
<%= link_to "View Weather Art", city_path(city), class: "btn btn-primary" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="relative pt-24 pb-32">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-3xl mx-auto text-center space-y-6">
|
||||
<h1 class="text-5xl md:text-6xl font-display font-bold leading-tight">
|
||||
Explore Cities
|
||||
</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
|
||||
</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
|
||||
<span class="font-semibold"><%= featured_art.city.name %></span>,
|
||||
<%= featured_art.city.country.name %>
|
||||
<span class="mx-2">•</span>
|
||||
<%= featured_art.weather_date.strftime("%B %d, %Y") %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sticky top-16 z-20 bg-base-100/95 backdrop-blur-sm border-b border-base-200">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="py-3 flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-ghost gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<%= @current_region&.name || 'All Regions' %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
|
||||
<li>
|
||||
<%= link_to cities_path,
|
||||
class: "#{@current_region ? '' : 'active'}" do %>
|
||||
All Regions
|
||||
<% end %>
|
||||
</li>
|
||||
<div class="divider my-1"></div>
|
||||
<% @regions.each do |region| %>
|
||||
<li>
|
||||
<%= link_to region.name,
|
||||
cities_path(region: region.slug),
|
||||
class: "#{@current_region == region ? 'active' : ''}" %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<% if @current_region %>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-ghost gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 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" %>
|
||||
<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 in #{@current_region.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,
|
||||
cities_path(region: @current_region.slug, country: country.slug),
|
||||
class: "#{@current_country == country ? 'active' : ''}" %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-base-content/70">
|
||||
<%= @cities.count %> <%= 'city'.pluralize(@cities.count) %>
|
||||
<% if @current_country %>
|
||||
in <%= @current_country.name %>
|
||||
<% elsif @current_region %>
|
||||
in <%= @current_region.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>
|
||||
|
||||
<%= render 'shared/pagination',
|
||||
collection: @cities,
|
||||
collection_name: 'cities' %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
@ -1,149 +1,49 @@
|
||||
<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="relative z-10">
|
||||
<!-- 返回导航 -->
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
<%= 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 %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Cities
|
||||
<% end %>
|
||||
<div class="space-y-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-3xl font-bold"><%= @city.name %></h1>
|
||||
<%= link_to "Back to Cities", cities_path, class: "btn btn-ghost" %>
|
||||
</div>
|
||||
|
||||
<!-- 城市信息头部 -->
|
||||
<div class="container mx-auto px-4 mb-12">
|
||||
<div class="max-w-4xl mx-auto text-center space-y-6">
|
||||
<h1 class="text-4xl md:text-6xl font-display font-bold">
|
||||
<span class="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
<%= @city.localized_name %>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-wrap justify-center items-center gap-3">
|
||||
<div class="badge badge-lg badge-primary gap-2">
|
||||
<%= @city.country.name %>, <%= @city.region %>
|
||||
<div class="stats shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Latitude</div>
|
||||
<div class="stat-value"><%= @city.latitude %></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="stat">
|
||||
<div class="stat-title">Longitude</div>
|
||||
<div class="stat-value"><%= @city.longitude %></div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 天气艺术历史记录 -->
|
||||
<div class="container mx-auto px-4 pb-16">
|
||||
<div class="max-w-7xl mx-auto space-y-8">
|
||||
<!-- 标题和更新时间 -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<h2 class="text-3xl font-display font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Weather Art History
|
||||
</h2>
|
||||
<div class="card bg-base-100/80 backdrop-blur-sm shadow-lg p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="font-medium">Last Updated:</span>
|
||||
<span class="text-base-content/70">
|
||||
<%= time_ago_in_words(@city.last_weather_fetch) if @city.last_weather_fetch %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 天气艺术卡片网格 -->
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-2xl font-bold">Weather Art History</h2>
|
||||
<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">
|
||||
<div class="card bg-base-200">
|
||||
<figure>
|
||||
<% 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.weather_date.strftime("%H:%M") %></div>
|
||||
<div class="text-sm opacity-80"><%= art.weather_date.strftime("%B %d, %Y") %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%= image_tag art.image, class: "w-full h-48 object-cover" %>
|
||||
<% 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>
|
||||
<h3 class="card-title"><%= art.weather_date.strftime("%Y-%m-%d") %></h3>
|
||||
<p><%= art.description %></p>
|
||||
<div class="stats stats-vertical shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Temperature</div>
|
||||
<div class="stat-value"><%= art.temperature %>°C</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<%= weather_stat_icon("wind") %>
|
||||
<span>Wind: <%= art.wind_scale %></span>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Humidity</div>
|
||||
<div class="stat-value"><%= art.humidity %>%</div>
|
||||
</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 class="card-actions justify-end">
|
||||
<%= link_to "View Details", city_weather_art_path(@city, art), class: "btn btn-primary" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,68 +1,46 @@
|
||||
<div>
|
||||
<!-- 首屏展示区 -->
|
||||
<section class="h-screen-90 relative overflow-hidden">
|
||||
<% if @featured_arts.first&.image&.attached? %>
|
||||
<div class="absolute inset-0">
|
||||
<%= image_tag @featured_arts.first.image, 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 class="space-y-8">
|
||||
<!-- 头部标题 -->
|
||||
<div class="text-center space-y-4">
|
||||
<h1 class="text-4xl font-bold">AI Weather Art</h1>
|
||||
<p class="text-xl text-base-content/70">Discover the beauty of weather through AI-generated art</p>
|
||||
</div>
|
||||
|
||||
<!-- 轮播图 -->
|
||||
<div class="carousel w-full rounded-box">
|
||||
<% WeatherArt.last(5).each_with_index do |art, index| %>
|
||||
<div id="slide<%= index %>" class="carousel-item relative w-full">
|
||||
<% if art.image.attached? %>
|
||||
<%= image_tag art.image, class: "w-full aspect-video object-cover" %>
|
||||
<% end %>
|
||||
<div class="absolute flex justify-between transform -translate-y-1/2 left-5 right-5 top-1/2">
|
||||
<a href="#slide<%= index == 0 ? 4 : index - 1 %>" class="btn btn-circle">❮</a>
|
||||
<a href="#slide<%= index == 4 ? 0 : index + 1 %>" class="btn btn-circle">❯</a>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<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
|
||||
</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.
|
||||
</p>
|
||||
<%= link_to "Explore Cities", cities_path,
|
||||
class: "btn btn-primary btn-lg mt-8 font-sans" %>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 最新天气艺术 -->
|
||||
<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">
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-2xl font-bold">Latest Weather Art</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<% WeatherArt.last(6).each do |art| %>
|
||||
<div class="card bg-base-200">
|
||||
<figure>
|
||||
<% if art.image.attached? %>
|
||||
<%= image_tag art.image, class: "w-full h-full object-cover transform hover:scale-105 transition-transform duration-500" %>
|
||||
<%= image_tag art.image, class: "w-full h-48 object-cover" %>
|
||||
<% 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" %>
|
||||
<h3 class="card-title"><%= art.city.name %></h3>
|
||||
<p><%= art.weather_date.strftime("%Y-%m-%d") %></p>
|
||||
<p><%= art.description %></p>
|
||||
<div class="card-actions justify-end">
|
||||
<%= link_to "View Details", city_weather_art_path(art.city, art), class: "btn btn-primary" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</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
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
@ -1,11 +0,0 @@
|
||||
<%# Link to the "First" page
|
||||
- available local variables
|
||||
url: url to the first page
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
-%>
|
||||
<span class="first">
|
||||
<%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote %>
|
||||
</span>
|
@ -1,8 +0,0 @@
|
||||
<%# Non-link tag that stands for skipped pages...
|
||||
- available local variables
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
-%>
|
||||
<span class="page gap"><%= t('views.pagination.truncate').html_safe %></span>
|
@ -1,11 +0,0 @@
|
||||
<%# Link to the "Last" page
|
||||
- available local variables
|
||||
url: url to the last page
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
-%>
|
||||
<span class="last">
|
||||
<%= link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, remote: remote %>
|
||||
</span>
|
@ -1,11 +0,0 @@
|
||||
<%# Link to the "Next" page
|
||||
- available local variables
|
||||
url: url to the next page
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
-%>
|
||||
<span class="next">
|
||||
<%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, rel: 'next', remote: remote %>
|
||||
</span>
|
@ -1,12 +0,0 @@
|
||||
<%# Link showing page number
|
||||
- available local variables
|
||||
page: a page object for "this" page
|
||||
url: url to this page
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
-%>
|
||||
<span class="page<%= ' current' if page.current? %>">
|
||||
<%= link_to_unless page.current?, page, url, {remote: remote, rel: page.rel} %>
|
||||
</span>
|
@ -1,25 +0,0 @@
|
||||
<%# The container tag
|
||||
- available local variables
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
paginator: the paginator that renders the pagination tags inside
|
||||
-%>
|
||||
<%= paginator.render do -%>
|
||||
<nav class="pagination" role="navigation" aria-label="pager">
|
||||
<%= first_page_tag unless current_page.first? %>
|
||||
<%= prev_page_tag unless current_page.first? %>
|
||||
<% each_page do |page| -%>
|
||||
<% if page.display_tag? -%>
|
||||
<%= page_tag page %>
|
||||
<% elsif !page.was_truncated? -%>
|
||||
<%= gap_tag %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% unless current_page.out_of_range? %>
|
||||
<%= next_page_tag unless current_page.last? %>
|
||||
<%= last_page_tag unless current_page.last? %>
|
||||
<% end %>
|
||||
</nav>
|
||||
<% end -%>
|
@ -1,11 +0,0 @@
|
||||
<%# Link to the "Previous" page
|
||||
- available local variables
|
||||
url: url to the previous page
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
-%>
|
||||
<span class="prev">
|
||||
<%= link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, rel: 'prev', remote: remote %>
|
||||
</span>
|
@ -1,23 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<title><%= content_for(:title) || "Today Ai Weather" %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<%= display_meta_tags(
|
||||
site: 'TodayAIWeather',
|
||||
reverse: true,
|
||||
og: {
|
||||
site_name: 'TodayAIWeather',
|
||||
type: 'website',
|
||||
url: request.original_url
|
||||
},
|
||||
alternate: {
|
||||
"zh-CN" => url_for(locale: 'zh-CN'),
|
||||
"en" => url_for(locale: 'en')
|
||||
}
|
||||
) %>
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
@ -33,66 +20,28 @@
|
||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
<script defer data-domain="todayaiweather.com" src="https://plausible.frytea.com/js/script.js"></script>
|
||||
|
||||
<script defer src="https://busuanzi.frytea.com/js"></script>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-PX1C92V5L7"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-PX1C92V5L7');
|
||||
</script>
|
||||
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7296634171837358"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen bg-base-100 font-sans">
|
||||
<!-- 导航栏 -->
|
||||
<div class="navbar bg-base-100/80 backdrop-blur-sm fixed top-0 z-50">
|
||||
<body class="min-h-screen bg-base-100">
|
||||
<div class="navbar bg-base-100">
|
||||
<div class="container mx-auto">
|
||||
<div class="flex-1">
|
||||
<%= link_to root_path, class: "text-2xl font-display font-bold hover:text-primary transition-colors" do %>
|
||||
Today AI Weather
|
||||
<% end %>
|
||||
<%= link_to "AI Weather Art", root_path, class: "btn btn-ghost normal-case text-xl" %>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<%= link_to "Cities", cities_path, class: "btn btn-ghost font-sans" %>
|
||||
<%= link_to "Arts", arts_path, class: "btn btn-ghost font-sans" %>
|
||||
<%= link_to "Cities", cities_path, class: "btn btn-ghost normal-case" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="pt-16">
|
||||
<main class="container mx-auto px-4 py-8">
|
||||
<%= 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>
|
||||
<footer class="footer footer-center p-4 bg-base-200 text-base-content">
|
||||
<div>
|
||||
<p>Copyright © 2024 - All rights reserved by AI Weather Art</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,86 +0,0 @@
|
||||
<%# app/views/shared/_pagination.html.erb %>
|
||||
<% if collection.total_pages > 1 %>
|
||||
<div class="flex flex-col items-center mt-16 gap-6">
|
||||
<!-- 页码信息 -->
|
||||
<div class="text-base-content/70 font-light">
|
||||
<span class="px-4 py-2 bg-base-200/50 rounded-full">
|
||||
Page <%= collection.current_page %> of <%= collection.total_pages %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div class="join shadow-lg">
|
||||
<!-- 首页 -->
|
||||
<%= link_to url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn btn-xs #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
||||
</svg>
|
||||
<% end %>
|
||||
|
||||
<!-- 上一页 -->
|
||||
<%= link_to url_for(page: collection.prev_page || 1, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn btn-xs #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<% end %>
|
||||
|
||||
<!-- 页码 -->
|
||||
<% page_window = 2 # 当前页面前后显示的页码数 %>
|
||||
<% start_page = [1, collection.current_page - page_window].max %>
|
||||
<% end_page = [collection.total_pages, collection.current_page + page_window].min %>
|
||||
|
||||
<% if start_page > 1 %>
|
||||
<%= link_to 1, url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn btn-xs btn-ghost hover:bg-primary/5" %>
|
||||
<% if start_page > 2 %>
|
||||
<button class="join-item btn btn-xs btn-ghost btn-disabled">...</button>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% (start_page..end_page).each do |page| %>
|
||||
<% if page == collection.current_page %>
|
||||
<button class="join-item btn btn-xs btn-ghost bg-primary/10 font-medium">
|
||||
<%= page %>
|
||||
</button>
|
||||
<% else %>
|
||||
<%= link_to page, url_for(page: page, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn btn-xs btn-ghost hover:bg-primary/5" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if end_page < collection.total_pages %>
|
||||
<% if end_page < collection.total_pages - 1 %>
|
||||
<button class="join-item btn btn-xs btn-ghost btn-disabled">...</button>
|
||||
<% end %>
|
||||
<%= link_to collection.total_pages,
|
||||
url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn btn-xs btn-ghost hover:bg-primary/5" %>
|
||||
<% end %>
|
||||
|
||||
<!-- 下一页 -->
|
||||
<%= link_to url_for(page: collection.next_page || collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn btn-xs #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<% end %>
|
||||
|
||||
<!-- 末页 -->
|
||||
<%= link_to url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
|
||||
class: "join-item btn btn-xs #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- 结果统计 -->
|
||||
<div class="text-sm text-base-content/60 font-light">
|
||||
Showing <%= collection.offset_value + 1 %> to
|
||||
<%= collection.last_page? ? collection.total_count : collection.offset_value + collection.limit_value %>
|
||||
of <%= collection.total_count %> <%= collection_name || 'items' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
@ -1,37 +0,0 @@
|
||||
<%# 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-value text-3xl"><%= weather_art.temperature %>°C</div>
|
||||
<div class="stat-desc">Feels 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-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-value text-3xl"><%= weather_art.humidity %>%</div>
|
||||
<div class="stat-desc">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-value text-3xl"><%= weather_art.visibility %> km</div>
|
||||
<div class="stat-desc">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-value text-3xl"><%= weather_art.pressure %> hPa</div>
|
||||
<div class="stat-desc">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-value text-3xl"><%= weather_art.cloud %>%</div>
|
||||
<div class="stat-desc">Sky coverage</div>
|
||||
</div>
|
@ -1,121 +1,46 @@
|
||||
<% content_for :head do %>
|
||||
<script type="application/ld+json">
|
||||
<%= weather_art_schema(@weather_art) %>
|
||||
</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="space-y-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-3xl font-bold"><%= @weather_art.city.name %> - <%= @weather_art.weather_date.strftime("%Y-%m-%d") %></h1>
|
||||
<%= link_to "Back to City", city_path(@weather_art.city), class: "btn btn-ghost" %>
|
||||
</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">
|
||||
|
||||
<!-- 图片区域 -->
|
||||
<div class="card lg:card-side bg-base-200 shadow-xl">
|
||||
<figure class="lg:w-1/2">
|
||||
<% 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" %>
|
||||
<%= image_tag @weather_art.image, class: "w-full h-full object-cover" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
<% end %>
|
||||
<div class="card-body lg:w-1/2">
|
||||
<h2 class="card-title"><%= @weather_art.description %></h2>
|
||||
|
||||
<!-- 信息区域 -->
|
||||
<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 class="stats stats-vertical shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Temperature</div>
|
||||
<div class="stat-value"><%= @weather_art.temperature %>°C</div>
|
||||
<div class="stat-desc">Feels like <%= @weather_art.feeling_temp %>°C</div>
|
||||
</div>
|
||||
<div class="badge badge-lg badge-secondary">
|
||||
<%= @weather_art.weather_date.strftime("%H:%M") %>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title">Wind</div>
|
||||
<div class="stat-value"><%= @weather_art.wind_scale %></div>
|
||||
<div class="stat-desc"><%= @weather_art.wind_speed %> km/h</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title">Humidity</div>
|
||||
<div class="stat-value"><%= @weather_art.humidity %>%</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title">Visibility</div>
|
||||
<div class="stat-value"><%= @weather_art.visibility %> km</div>
|
||||
</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 class="mt-4">
|
||||
<h3 class="font-bold">AI Prompt:</h3>
|
||||
<p class="text-sm"><%= @weather_art.prompt %></p>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- 上一个和下一个导航 -->
|
||||
<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>
|
||||
|
||||
<% 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>
|
||||
|
||||
</div>
|
||||
</div>
|
@ -1,53 +0,0 @@
|
||||
class AddWatermarkToWeatherArtWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(weather_art_id)
|
||||
@weather_art = WeatherArt.find_by(id: weather_art_id)
|
||||
return unless @weather_art
|
||||
|
||||
add_watermark
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error adding watermark to WeatherArt #{weather_art_id}: #{e.message}"
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :weather_art
|
||||
|
||||
def add_watermark
|
||||
return if weather_art.image_with_watermark.attached?
|
||||
|
||||
watermark_path = Rails.root.join("app/assets/images/today_ai_weather_copyright_watermark1.png")
|
||||
return unless File.exist?(watermark_path)
|
||||
|
||||
image_tempfile = nil
|
||||
watermark_tempfile = nil
|
||||
begin
|
||||
image_tempfile = weather_art.image.download
|
||||
return unless image_dimensions_are_sufficient?(image_tempfile.path)
|
||||
|
||||
image = ImageProcessing::Vips.source(image_tempfile.path)
|
||||
watermark = ImageProcessing::Vips.source(watermark_path)
|
||||
watermarked_image = image.composite(watermark, "overlay")
|
||||
watermark_tempfile = Tempfile.new([ "watermarked_image", ".png" ])
|
||||
watermarked_image.write_to_file(watermark_tempfile.path)
|
||||
weather_art.image_with_watermark.attach(
|
||||
io: File.open(watermark_tempfile.path),
|
||||
filename: "#{generate_filename("watermarked")}",
|
||||
content_type: "image/png"
|
||||
)
|
||||
ensure
|
||||
watermark_tempfile.unlink if watermark_tempfile
|
||||
end
|
||||
end
|
||||
|
||||
def image_dimensions_are_sufficient?(image_path)
|
||||
dimensions = ImageProcessing::Vips.source(image_path).sizes
|
||||
dimensions.width >= 200 && dimensions.height >= 200
|
||||
end
|
||||
|
||||
def generate_filename(prefix)
|
||||
"#{prefix}_#{weather_art.image.filename.base}"
|
||||
end
|
||||
end
|
@ -1,32 +0,0 @@
|
||||
class BatchGenerateWeatherArtsWorker
|
||||
include Sidekiq::Worker
|
||||
GENERATION_INTERVAL = 24.hours
|
||||
MAX_DURATION = 50.minutes
|
||||
SLEEP_DURATION = 120.seconds
|
||||
BATCH_SIZE = 20
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_eligible_cities
|
||||
cutoff_time = Time.current - GENERATION_INTERVAL
|
||||
City.active
|
||||
.joins("LEFT JOIN (
|
||||
SELECT city_id, MAX(created_at) as last_generation_time
|
||||
FROM weather_arts
|
||||
GROUP BY city_id
|
||||
) latest_arts ON cities.id = latest_arts.city_id")
|
||||
.where("latest_arts.last_generation_time IS NULL OR latest_arts.last_generation_time < ?", cutoff_time)
|
||||
.order(:priority)
|
||||
end
|
||||
end
|
@ -1,32 +0,0 @@
|
||||
# app/workers/clean_ahoy_data_worker.rb
|
||||
class CleanAhoyDataWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: :default, retry: false
|
||||
|
||||
def perform
|
||||
cleanup_old_events
|
||||
cleanup_old_visits
|
||||
log_cleanup_results
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cleanup_old_events
|
||||
cutoff_date = 3.months.ago
|
||||
deleted_events_count = Ahoy::Event.where("time < ?", cutoff_date).delete_all
|
||||
Rails.logger.info "Deleted #{deleted_events_count} old Ahoy events"
|
||||
end
|
||||
|
||||
def cleanup_old_visits
|
||||
cutoff_date = 3.months.ago
|
||||
deleted_visits_count = Ahoy::Visit.where("started_at < ?", cutoff_date).delete_all
|
||||
Rails.logger.info "Deleted #{deleted_visits_count} old Ahoy visits"
|
||||
end
|
||||
|
||||
def log_cleanup_results
|
||||
Rails.logger.info "Ahoy cleanup completed at #{Time.current}"
|
||||
Rails.logger.info "Remaining events: #{Ahoy::Event.count}"
|
||||
Rails.logger.info "Remaining visits: #{Ahoy::Visit.count}"
|
||||
end
|
||||
end
|
@ -1,68 +0,0 @@
|
||||
class GenerateWeatherArtWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(city_id)
|
||||
@city = City.find(city_id)
|
||||
|
||||
weather_data = fetch_weather_data
|
||||
return unless weather_data
|
||||
|
||||
prompt = generate_prompt(weather_data)
|
||||
return unless prompt
|
||||
|
||||
image_url = generate_image(prompt)
|
||||
return unless image_url
|
||||
|
||||
create_weather_art(weather_data, prompt, image_url)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error generating weather art for city #{city_id}: #{e.message}"
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :city
|
||||
|
||||
def fetch_weather_data
|
||||
WeatherService.new.get_weather(city.latitude, city.longitude)
|
||||
end
|
||||
|
||||
def generate_prompt(weather_data)
|
||||
AiService.new.generate_prompt(city, weather_data)
|
||||
end
|
||||
|
||||
def generate_image(prompt)
|
||||
AiService.new.generate_image(prompt)
|
||||
end
|
||||
|
||||
def create_weather_art(weather_data, prompt, image_url)
|
||||
tempfile = nil
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
weather_art = city.weather_arts.create!(
|
||||
weather_date: Date.today,
|
||||
prompt: prompt,
|
||||
**weather_data
|
||||
)
|
||||
|
||||
tempfile = Down.download(image_url)
|
||||
|
||||
weather_art.image.attach(
|
||||
io: File.open(tempfile.path),
|
||||
filename: generate_filename,
|
||||
content_type: "image/png"
|
||||
)
|
||||
|
||||
weather_art
|
||||
end
|
||||
ensure
|
||||
if tempfile
|
||||
tempfile.close
|
||||
tempfile.unlink
|
||||
end
|
||||
end
|
||||
|
||||
def generate_filename
|
||||
"#{city.country.name}-#{city.name.parameterize}-#{Time.current.strftime('%Y%m%d-%H%M%S')}.png"
|
||||
end
|
||||
end
|
@ -1,58 +0,0 @@
|
||||
class RefreshSitemapWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform
|
||||
host = Rails.env.production? ? "https://todayaiweather.com" : "http://127.0.0.1:3000"
|
||||
Rails.application.routes.default_url_options[:host] = host
|
||||
SitemapGenerator::Sitemap.default_host = host
|
||||
if Rails.env.production?
|
||||
SitemapGenerator::Sitemap.adapter = SitemapGenerator::AwsSdkAdapter.new(
|
||||
ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:aws, :bucket)),
|
||||
aws_access_key_id: ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :access_key_id)),
|
||||
aws_secret_access_key: ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :secret_access_key)),
|
||||
aws_region: ENV.fetch("AWS_REGION", "wnam"),
|
||||
endpoint: ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:aws, :endpoint)),
|
||||
)
|
||||
else
|
||||
SitemapGenerator::Sitemap.adapter = SitemapGenerator::AwsSdkAdapter.new(
|
||||
ENV.fetch("AWS_DEV_BUCKET", Rails.application.credentials.dig(:aws_dev, :bucket)),
|
||||
aws_access_key_id: ENV.fetch("AWS_DEV_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :access_key_id)),
|
||||
aws_secret_access_key: ENV.fetch("AWS_DEV_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :secret_access_key)),
|
||||
aws_region: ENV.fetch("AWS_DEV_REGION", "wnam"),
|
||||
endpoint: ENV.fetch("AWS_DEV_ENDPOINT", Rails.application.credentials.dig(:aws_dev, :endpoint)),
|
||||
)
|
||||
end
|
||||
SitemapGenerator::Sitemap.sitemaps_path = "sitemaps/"
|
||||
|
||||
SitemapGenerator::Sitemap.create do
|
||||
add root_path, changefreq: "daily", priority: 1.0
|
||||
add cities_path, changefreq: "daily", priority: 0.9
|
||||
add arts_path, changefreq: "daily", priority: 0.9
|
||||
|
||||
City.find_each do |city|
|
||||
add city_path(city),
|
||||
changefreq: "daily",
|
||||
priority: 0.8,
|
||||
lastmod: city.updated_at
|
||||
end
|
||||
|
||||
WeatherArt.includes(:city).find_each do |art|
|
||||
if art.image.attached?
|
||||
add city_weather_art_path(art.city, art),
|
||||
changefreq: "daily",
|
||||
priority: 0.7,
|
||||
lastmod: art.updated_at,
|
||||
images: [ {
|
||||
loc: url_for(art.image),
|
||||
title: "#{art.city.name} Weather Art - #{art.weather_date.strftime('%B %d, %Y')}"
|
||||
} ]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# SitemapGenerator::Sitemap.ping_search_engines if Rails.env.production?
|
||||
Rails.logger.info "Sitemap has been generated and uploaded to S3 successfully"
|
||||
rescue => e
|
||||
Rails.logger.error "Error refreshing sitemap: #{e.message}"
|
||||
end
|
||||
end
|
52
compose.yaml
52
compose.yaml
@ -1,52 +0,0 @@
|
||||
services:
|
||||
web:
|
||||
image: songtianlun/today_ai_weather:latest
|
||||
#ports:
|
||||
# - "3000:3000"
|
||||
pull_policy: always
|
||||
environment:
|
||||
- RAILS_ENV=production
|
||||
- DATABASE_URL=postgresql://postgres:${PG_PASSWORD}@db:5432/db
|
||||
- RAILS_MASTER_KEY=${RAILS_MASTER_KEY}
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
networks:
|
||||
- dokploy-network
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
sidekiq:
|
||||
image: songtianlun/today_ai_weather:latest
|
||||
command: bundle exec sidekiq
|
||||
environment:
|
||||
- RAILS_ENV=production
|
||||
- DATABASE_URL=postgresql://postgres:${PG_PASSWORD}@db:5432/db
|
||||
- RAILS_MASTER_KEY=${RAILS_MASTER_KEY}
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
networks:
|
||||
- dokploy-network
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
db:
|
||||
image: postgres:16
|
||||
volumes:
|
||||
- ../taw_data/pg:/var/lib/postgresql/data
|
||||
networks:
|
||||
- dokploy-network
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=${PG_PASSWORD}
|
||||
- POSTGRES_DB=db
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
volumes:
|
||||
- ../taw_data/redis:/data
|
||||
networks:
|
||||
- dokploy-network
|
||||
command: redis-server --appendonly yes
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
@ -1 +1 @@
|
||||
zSnch4RlzPEpqZOE+TyBVBnHu+wyiNfNZoe1zi+cGNcgN/0BMw3xVQNIdu5ZgZzklukyimLlvkic7/G3PZE5aooqKIPeaGIgVKwdBvIGZYkTbrLTPtiA2Mx2iay4u6XI3eLDZZdy9g446GTngz9beQFR9s7dBbRBewOdIuCjA17seywsv722x97wHCv1xoGBeknK0DrEaYQcnECcy3G3esAsGeoKCQRMNHIf5/IbB6J+ZsoyOpnwtjOXdfEUnmtieLqvyw1xjgOxQTp2GMJk8zq+dGUBR3nIzplzccJbotppWfUbd9nxuT0stxWdFtKRPceynUfJ4awQNOmx+qGXi2/sYZH9lEB0nnyuKUpte6/bf1kcxcXyqTcsjaw0f9DXC3z9jx0pPbf2+O1MLZSFmo7Sr18mXjHVWjSCXnuYmWWkkuKESl4DwW5KScguWdnsf+/Y1eHds2MpE9dRLynlf5ONvFMY495iXuN4bR7UE5utAmU3imAg8vhErDqiro2L0GKMY5boMg+9NIObSlxwI6uAer8HaIKBVioiXGHFoFofR0LpmIKTbHiRF9ZZM3KCunsrQXAvRB4X7jL18JR08cDGGML2m03u9fcn9VuCYgcr+TLnsBozoG+ATP03kXc6lxkxjDvOc2z/AKqPmHUejCvsTLibFuEgw34E/NnVgcrC/Cggh/pp2Vmc6Yg2aQN7NcUmz0enGpzaTcnwiiB/8U8ig5WUovZP/iG3JH8S4KtBhDVz4hzz3FDOFBZCHd0rDrLtcyPtOcI3/WTZ0BgV5SXxA6L1gmM+aSYXtiyKZ2iUUOkMnNPvmHtimd+IWJZn2qXswX8RuE5QTqL5R5PRIdawdMSf9QrBSmDllNCo3czzClBRSNMYk2+ueWtiyvRuxP/xyNVl5e8nGOhAFWjCA+DVVA1xGWvS+R+3nxMSMtNaqTc2RPHQ3u4hY4k/0UH2TmnnzIJHS8vYScO4EzasTb/9FMeUEDnx369D7gvkXW6OapIKxwV0oQR8a/ZYUpOVyV6rSUIvEKrlpXCG8fh/6LwprYTPrnWe8iIwGqxKcuecrmtRSijAHgI2ZzMpSN91tomBBn9Qs+5svRbcXCO9KUKtjB9V82J7xqT+LwOyf1wa+v9zcO1aQxel2npRTsg7a4eIyIx1iyP7hHLAAuYIeEj1pBjiY/LiGZw+ScrXu1sIFTpt2SIKUOCVSN1y52/zXkwTRHc3qf4IdeHX6DqzWmWUiEHwm9Lva1fJ6poJ3vqWi7OLWJqyDtiN2FHJqybQ9mrgWPb0WBYi3umnfPy6ZYMQ6jQ8YarK8aFHsVdjUL0I4DKigAuer47p2rUcaFvySGCoMaSu5krSBw3VkDR6y1J8wIAm7nKZOSyoKwRjrgwXOl8yTN+zoemiaUMNJI1mTu1JQeg5rX2yUvwLEu5hu/5eOuRCkoSiwcG1+LmcnZbYRgSRvDzRcKYKZ/5hzQkPCLRkwAhh1Zp7mtc=--dyeF3fjnBpbK0tnw--nWdBu1crWT0VX9aHfnoKRw==
|
||||
LCyDSpt/QvabmfGLE8GlLR1UPEz7QPdFj21uutpnQtJgwAbzfdWf+wh45UH97SLpnbfdy9uP0UT08j8t1bnn2danGnXDgdrhlb+NZeAGUjrV3OoNbv2ywktK6ENFNnmGyztKSrvj5Jnc8N3A+WE202cO6EpAmedhwJ1kl4WF/n2XHYbVrU/9+im91l8Meux+fNNheqXQ3MqpjQmtzN6XBidJFIVnvc2hX8c8XPkiWgrnPt0MJnoXgAUc4r4i9Jy3cXRYbidK+tKvYPPqQnI5b90vttM2bbxIKB/4Z2SdlcZNZK6UcEz5TTbVFF6utYTz8vjjsjFp5JpdzVX4q/Gc9zNPe8Wpkdz8rkoAwQ2OpJKB0T+AYNAuM+0XNycpx4N4AkOozJqK1Gd6OMlicmgIWTqUgWqljLMxbI+1WrEcZTAbeUfe9UajEfYuxHE8ksRFAnMUNCFpOJN348vRoxBv3fvan6rAnQMs2F+P9anRb8jPkJAzOFDct3ej--hNhQcVOfe5QH/0IE--Tk/qRKxDaUSCeZMW5hBwaA==
|
@ -29,8 +29,7 @@ Rails.application.configure do
|
||||
config.cache_store = :memory_store
|
||||
|
||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
||||
# config.active_storage.service = :local
|
||||
config.active_storage.service = :amazon_dev
|
||||
config.active_storage.service = :local
|
||||
|
||||
# Don't care if the mailer can't send.
|
||||
config.action_mailer.raise_delivery_errors = false
|
||||
|
@ -22,7 +22,7 @@ Rails.application.configure do
|
||||
# config.asset_host = "http://assets.example.com"
|
||||
|
||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
||||
config.active_storage.service = ENV["RAILS_BUILD"] ? :build : :amazon
|
||||
config.active_storage.service = :local
|
||||
|
||||
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
|
||||
config.assume_ssl = true
|
||||
@ -50,8 +50,7 @@ Rails.application.configure do
|
||||
config.cache_store = :solid_cache_store
|
||||
|
||||
# Replace the default in-process and non-durable queuing backend for Active Job.
|
||||
# config.active_job.queue_adapter = :solid_queue
|
||||
config.active_job.queue_adapter = :sidekiq
|
||||
config.active_job.queue_adapter = :solid_queue
|
||||
config.solid_queue.connects_to = { database: { writing: :queue } }
|
||||
|
||||
# Ignore bad email addresses and do not raise email delivery errors.
|
||||
|
@ -174,7 +174,7 @@ ActiveAdmin.setup do |config|
|
||||
# You can exclude possibly sensitive model attributes from being displayed,
|
||||
# added to forms, or exported by default by ActiveAdmin
|
||||
#
|
||||
config.filter_attributes = [ :encrypted_password, :password, :password_confirmation ]
|
||||
config.filter_attributes = [:encrypted_password, :password, :password_confirmation]
|
||||
|
||||
# == Localize Date/Time Format
|
||||
#
|
||||
|
@ -1,14 +0,0 @@
|
||||
class Ahoy::Store < Ahoy::DatabaseStore
|
||||
end
|
||||
|
||||
# set to true for JavaScript tracking
|
||||
Ahoy.api = true
|
||||
|
||||
# set to true for geocoding (and add the geocoder gem to your Gemfile)
|
||||
# we recommend configuring local geocoding as well
|
||||
# see https://github.com/ankane/ahoy#geocoding
|
||||
Ahoy.geocode = false
|
||||
|
||||
Ahoy.visit_duration = 30.minutes
|
||||
Ahoy.server_side_visits = :when_needed
|
||||
RETENTION_PERIOD = 3.months
|
@ -1,19 +0,0 @@
|
||||
if Rails.env.production?
|
||||
Aws.config.update({
|
||||
region: ENV.fetch("AWS_REGION", "wnam"),
|
||||
credentials: Aws::Credentials.new(
|
||||
ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :access_key_id)),
|
||||
ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :secret_access_key))
|
||||
),
|
||||
endpoint: ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:aws, :endpoint))
|
||||
})
|
||||
else
|
||||
Aws.config.update({
|
||||
region: ENV.fetch("AWS_DEV_REGION", "wnam"),
|
||||
credentials: Aws::Credentials.new(
|
||||
ENV.fetch("AWS_DEV_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :access_key_id)),
|
||||
ENV.fetch("AWS_DEV_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :secret_access_key))
|
||||
),
|
||||
endpoint: ENV.fetch("AWS_DEV_ENDPOINT", Rails.application.credentials.dig(:aws_dev, :endpoint))
|
||||
})
|
||||
end
|
@ -24,7 +24,7 @@ Devise.setup do |config|
|
||||
# Configure the e-mail address which will be shown in Devise::Mailer,
|
||||
# note that it will be overwritten if you use your own mailer class
|
||||
# with default "from" parameter.
|
||||
config.mailer_sender = "please-change-me-at-config-initializers-devise@example.com"
|
||||
config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
|
||||
|
||||
# Configure the class responsible to send e-mails.
|
||||
# config.mailer = 'Devise::Mailer'
|
||||
@ -36,7 +36,7 @@ Devise.setup do |config|
|
||||
# Load and configure the ORM. Supports :active_record (default) and
|
||||
# :mongoid (bson_ext recommended) by default. Other ORMs may be
|
||||
# available as additional gems.
|
||||
require "devise/orm/active_record"
|
||||
require 'devise/orm/active_record'
|
||||
|
||||
# ==> Configuration for any authentication mechanism
|
||||
# Configure which keys are used when authenticating a user. The default is
|
||||
@ -58,12 +58,12 @@ Devise.setup do |config|
|
||||
# Configure which authentication keys should be case-insensitive.
|
||||
# These keys will be downcased upon creating or modifying a user and when used
|
||||
# to authenticate or find a user. Default is :email.
|
||||
config.case_insensitive_keys = [ :email ]
|
||||
config.case_insensitive_keys = [:email]
|
||||
|
||||
# Configure which authentication keys should have whitespace stripped.
|
||||
# These keys will have whitespace before and after removed upon creating or
|
||||
# modifying a user and when used to authenticate or find a user. Default is :email.
|
||||
config.strip_whitespace_keys = [ :email ]
|
||||
config.strip_whitespace_keys = [:email]
|
||||
|
||||
# Tell if authentication through request.params is enabled. True by default.
|
||||
# It can be set to an array that will enable params authentication only for the
|
||||
@ -97,7 +97,7 @@ Devise.setup do |config|
|
||||
# Notice that if you are skipping storage for all authentication paths, you
|
||||
# may want to disable generating routes to Devise's sessions controller by
|
||||
# passing skip: :sessions to `devise_for` in your config/routes.rb
|
||||
config.skip_session_storage = [ :http_auth ]
|
||||
config.skip_session_storage = [:http_auth]
|
||||
|
||||
# By default, Devise cleans up the CSRF token on authentication to
|
||||
# avoid CSRF token fixation attacks. This means that, when using AJAX
|
||||
|
@ -1,14 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Kaminari.configure do |config|
|
||||
# config.default_per_page = 25
|
||||
# config.max_per_page = nil
|
||||
# config.window = 4
|
||||
# config.outer_window = 0
|
||||
# config.left = 0
|
||||
# config.right = 0
|
||||
# config.page_method_name = :page
|
||||
# config.param_name = :page
|
||||
# config.max_pages = nil
|
||||
# config.params_on_first_page = false
|
||||
end
|
@ -1,52 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Use this setup block to configure all options available in MetaTags.
|
||||
MetaTags.configure do |config|
|
||||
config.title_limit = 70
|
||||
config.description_limit = 160
|
||||
config.keywords_limit = 255
|
||||
# How many characters should the title meta tag have at most. Default is 70.
|
||||
# Set to nil or 0 to remove limits.
|
||||
# config.title_limit = 70
|
||||
|
||||
# When true, site title will be truncated instead of title. Default is false.
|
||||
# config.truncate_site_title_first = false
|
||||
|
||||
# Add HTML attributes to the <title> HTML tag. Default is {}.
|
||||
# config.title_tag_attributes = {}
|
||||
|
||||
# Natural separator when truncating. Default is " " (space character).
|
||||
# Set to nil to disable natural separator.
|
||||
# This also allows you to use a whitespace regular expression (/\s/) or
|
||||
# a Unicode space (/\p{Space}/).
|
||||
# config.truncate_on_natural_separator = " "
|
||||
|
||||
# Maximum length of the page description. Default is 300.
|
||||
# Set to nil or 0 to remove limits.
|
||||
# config.description_limit = 300
|
||||
|
||||
# Maximum length of the keywords meta tag. Default is 255.
|
||||
# config.keywords_limit = 255
|
||||
|
||||
# Default separator for keywords meta tag (used when an Array passed with
|
||||
# the list of keywords). Default is ", ".
|
||||
# config.keywords_separator = ', '
|
||||
|
||||
# When true, keywords will be converted to lowercase, otherwise they will
|
||||
# appear on the page as is. Default is true.
|
||||
# config.keywords_lowercase = true
|
||||
|
||||
# When true, the output will not include new line characters between meta tags.
|
||||
# Default is false.
|
||||
# config.minify_output = false
|
||||
|
||||
# When false, generated meta tags will be self-closing (<meta ... />) instead
|
||||
# of open (`<meta ...>`). Default is true.
|
||||
# config.open_meta_tags = true
|
||||
|
||||
# List of additional meta tags that should use "property" attribute instead
|
||||
# of "name" attribute in <meta> tags.
|
||||
# config.property_tags.push(
|
||||
# 'x-hearthstone:deck',
|
||||
# )
|
||||
end
|
@ -1,9 +0,0 @@
|
||||
# config/initializers/schedule_tasks.rb
|
||||
Rails.application.config.after_initialize do
|
||||
begin
|
||||
RefreshSitemapWorker.perform_async
|
||||
Rails.logger.info "Startup task (RefreshSitemapWorker) scheduled successfully"
|
||||
rescue => e
|
||||
Rails.logger.error "Error scheduling startup task: #{e.message}"
|
||||
end
|
||||
end
|
@ -1,20 +0,0 @@
|
||||
require "sidekiq"
|
||||
require "sidekiq-scheduler"
|
||||
|
||||
Sidekiq.configure_server do |config|
|
||||
config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1") }
|
||||
config.logger.level = Logger::INFO
|
||||
config.on(:startup) do
|
||||
schedule_file = "config/sidekiq_scheduler.yml"
|
||||
if File.exist?(schedule_file)
|
||||
Sidekiq::Scheduler.enabled = true
|
||||
Sidekiq::Scheduler.dynamic = true
|
||||
Sidekiq.schedule = YAML.load_file(schedule_file)
|
||||
Sidekiq::Scheduler.reload_schedule!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Sidekiq.configure_client do |config|
|
||||
config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1") }
|
||||
end
|
@ -1,118 +0,0 @@
|
||||
en:
|
||||
cities:
|
||||
# Australia
|
||||
sydney: 'Sydney'
|
||||
melbourne: 'Melbourne'
|
||||
|
||||
# Bangladesh
|
||||
dhaka: 'Dhaka'
|
||||
|
||||
# Brazil
|
||||
rio_de_janeiro: 'Rio de Janeiro'
|
||||
|
||||
# Canada
|
||||
toronto: 'Toronto'
|
||||
|
||||
# China
|
||||
shanghai: 'Shanghai'
|
||||
beijing: 'Beijing'
|
||||
shenzhen: 'Shenzhen'
|
||||
guangzhou: 'Guangzhou'
|
||||
chengdu: 'Chengdu'
|
||||
tianjin: 'Tianjin'
|
||||
wuhan: 'Wuhan'
|
||||
dongguan: 'Dongguan'
|
||||
chongqing: 'Chongqing'
|
||||
xian: "Xi'an"
|
||||
hangzhou: 'Hangzhou'
|
||||
foshan: 'Foshan'
|
||||
nanjing: 'Nanjing'
|
||||
hong_kong: 'Hong Kong'
|
||||
shenyang: 'Shenyang'
|
||||
zhengzhou: 'Zhengzhou'
|
||||
qingdao: 'Qingdao'
|
||||
suzhou: 'Suzhou'
|
||||
changsha: 'Changsha'
|
||||
jinan: 'Jinan'
|
||||
kunming: 'Kunming'
|
||||
harbin: 'Harbin'
|
||||
shijiazhuang: 'Shijiazhuang'
|
||||
hefei: 'Hefei'
|
||||
dalian: 'Dalian'
|
||||
xiamen: 'Xiamen'
|
||||
nanning: 'Nanning'
|
||||
changchun: 'Changchun'
|
||||
taiyuan: 'Taiyuan'
|
||||
new_taipei_city: 'New Taipei City'
|
||||
guiyang: 'Guiyang'
|
||||
wuxi: 'Wuxi'
|
||||
shantou: 'Shantou'
|
||||
urumqi: 'Ürümqi'
|
||||
zhongshan: 'Zhongshan'
|
||||
ningbo: 'Ningbo'
|
||||
fuzhou: 'Fuzhou'
|
||||
nanchang: 'Nanchang'
|
||||
|
||||
# Democratic Republic of the Congo
|
||||
kinshasa: 'Kinshasa'
|
||||
|
||||
# Egypt
|
||||
alexandria: 'Alexandria'
|
||||
|
||||
# France
|
||||
paris: 'Paris'
|
||||
|
||||
# Germany
|
||||
frankfurt: 'Frankfurt'
|
||||
berlin: 'Berlin'
|
||||
|
||||
# India
|
||||
mumbai: 'Mumbai'
|
||||
bengaluru: 'Bengaluru'
|
||||
|
||||
# Japan
|
||||
tokyo: 'Tokyo'
|
||||
yokohama: 'Yokohama'
|
||||
|
||||
# Mexico
|
||||
mexico_city: 'Mexico City'
|
||||
|
||||
# Nigeria
|
||||
lagos: 'Lagos'
|
||||
|
||||
# Pakistan
|
||||
lahore: 'Lahore'
|
||||
|
||||
# Russia
|
||||
moscow: 'Moscow'
|
||||
sankt_petersburg: 'Sankt Petersburg'
|
||||
|
||||
# Saudi Arabia
|
||||
riyadh: 'Riyadh'
|
||||
|
||||
# Singapore
|
||||
singapore: 'Singapore'
|
||||
|
||||
# South Korea
|
||||
seoul: 'Seoul'
|
||||
|
||||
# Thailand
|
||||
bangkok: 'Bangkok'
|
||||
|
||||
# Turkey
|
||||
istanbul: 'İstanbul'
|
||||
ankara: 'Ankara'
|
||||
|
||||
# United Kingdom
|
||||
london: 'London'
|
||||
|
||||
# United States
|
||||
san_francisco: 'San Francisco'
|
||||
chicago: 'Chicago'
|
||||
new_york_city: 'New York City'
|
||||
los_angeles: 'Los Angeles'
|
||||
|
||||
# Vietnam
|
||||
ho_chi_minh_city: 'Ho Chi Minh City'
|
||||
hanoi: 'Hanoi'
|
||||
|
@ -1,118 +0,0 @@
|
||||
|
||||
zh-CN:
|
||||
cities:
|
||||
# Australia
|
||||
sydney: '悉尼'
|
||||
melbourne: '墨尔本'
|
||||
|
||||
# Bangladesh
|
||||
dhaka: '达卡'
|
||||
|
||||
# Brazil
|
||||
rio_de_janeiro: '里约热内卢'
|
||||
|
||||
# Canada
|
||||
toronto: '多伦多'
|
||||
|
||||
# China
|
||||
shanghai: '上海'
|
||||
beijing: '北京'
|
||||
shenzhen: '深圳'
|
||||
guangzhou: '广州'
|
||||
chengdu: '成都'
|
||||
tianjin: '天津'
|
||||
wuhan: '武汉'
|
||||
dongguan: '东莞'
|
||||
chongqing: '重庆'
|
||||
xian: '西安'
|
||||
hangzhou: '杭州'
|
||||
foshan: '佛山'
|
||||
nanjing: '南京'
|
||||
hong_kong: '香港'
|
||||
shenyang: '沈阳'
|
||||
zhengzhou: '郑州'
|
||||
qingdao: '青岛'
|
||||
suzhou: '苏州'
|
||||
changsha: '长沙'
|
||||
jinan: '济南'
|
||||
kunming: '昆明'
|
||||
harbin: '哈尔滨'
|
||||
shijiazhuang: '石家庄'
|
||||
hefei: '合肥'
|
||||
dalian: '大连'
|
||||
xiamen: '厦门'
|
||||
nanning: '南宁'
|
||||
changchun: '长春'
|
||||
taiyuan: '太原'
|
||||
new_taipei_city: '新北市'
|
||||
guiyang: '贵阳'
|
||||
wuxi: '无锡'
|
||||
shantou: '汕头'
|
||||
urumqi: '乌鲁木齐'
|
||||
zhongshan: '中山'
|
||||
ningbo: '宁波'
|
||||
fuzhou: '福州'
|
||||
nanchang: '南昌'
|
||||
|
||||
# Democratic Republic of the Congo
|
||||
kinshasa: '金沙萨'
|
||||
|
||||
# Egypt
|
||||
alexandria: '亚历山大'
|
||||
|
||||
# France
|
||||
paris: '巴黎'
|
||||
|
||||
# Germany
|
||||
frankfurt: '法兰克福'
|
||||
berlin: '柏林'
|
||||
|
||||
# India
|
||||
mumbai: '孟买'
|
||||
bengaluru: '班加罗尔'
|
||||
|
||||
# Japan
|
||||
tokyo: '东京'
|
||||
yokohama: '横滨'
|
||||
|
||||
# Mexico
|
||||
mexico_city: '墨西哥城'
|
||||
|
||||
# Nigeria
|
||||
lagos: '拉各斯'
|
||||
|
||||
# Pakistan
|
||||
lahore: '拉合尔'
|
||||
|
||||
# Russia
|
||||
moscow: '莫斯科'
|
||||
sankt_petersburg: '圣彼得堡'
|
||||
|
||||
# Saudi Arabia
|
||||
riyadh: '利雅得'
|
||||
|
||||
# Singapore
|
||||
singapore: '新加坡'
|
||||
|
||||
# South Korea
|
||||
seoul: '首尔'
|
||||
|
||||
# Thailand
|
||||
bangkok: '曼谷'
|
||||
|
||||
# Turkey
|
||||
istanbul: '伊斯坦布尔'
|
||||
ankara: '安卡拉'
|
||||
|
||||
# United Kingdom
|
||||
london: '伦敦'
|
||||
|
||||
# United States
|
||||
san_francisco: '旧金山'
|
||||
chicago: '芝加哥'
|
||||
new_york_city: '纽约'
|
||||
los_angeles: '洛杉矶'
|
||||
|
||||
# Vietnam
|
||||
ho_chi_minh_city: '胡志明市'
|
||||
hanoi: '河内'
|
@ -1,55 +0,0 @@
|
||||
en:
|
||||
countries:
|
||||
# East Asia
|
||||
CN: 'China'
|
||||
JP: 'Japan'
|
||||
KR: 'South Korea'
|
||||
TW: 'Taiwan'
|
||||
HK: 'Hong Kong'
|
||||
|
||||
# South Asia
|
||||
IN: 'India'
|
||||
PK: 'Pakistan'
|
||||
BD: 'Bangladesh'
|
||||
|
||||
# Southeast Asia
|
||||
ID: 'Indonesia'
|
||||
VN: 'Vietnam'
|
||||
TH: 'Thailand'
|
||||
MM: 'Myanmar'
|
||||
SG: 'Singapore'
|
||||
|
||||
# Middle East
|
||||
TR: 'Turkey'
|
||||
IR: 'Iran'
|
||||
SA: 'Saudi Arabia'
|
||||
IQ: 'Iraq'
|
||||
|
||||
# Africa
|
||||
NG: 'Nigeria'
|
||||
EG: 'Egypt'
|
||||
CD: 'Democratic Republic of the Congo'
|
||||
TZ: 'Tanzania'
|
||||
ZA: 'South Africa'
|
||||
KE: 'Kenya'
|
||||
AO: 'Angola'
|
||||
ML: 'Mali'
|
||||
CI: 'Ivory Coast'
|
||||
|
||||
# Europe
|
||||
RU: 'Russia'
|
||||
GB: 'United Kingdom'
|
||||
DE: 'Germany'
|
||||
|
||||
# North America
|
||||
US: 'United States'
|
||||
MX: 'Mexico'
|
||||
|
||||
# South America
|
||||
BR: 'Brazil'
|
||||
PE: 'Peru'
|
||||
CO: 'Colombia'
|
||||
CL: 'Chile'
|
||||
|
||||
# Oceania
|
||||
AU: 'Australia'
|
@ -1,55 +0,0 @@
|
||||
zh-CN:
|
||||
countries:
|
||||
# East Asia
|
||||
CN: '中国'
|
||||
JP: '日本'
|
||||
KR: '韩国'
|
||||
TW: '台湾'
|
||||
HK: '香港'
|
||||
|
||||
# South Asia
|
||||
IN: '印度'
|
||||
PK: '巴基斯坦'
|
||||
BD: '孟加拉国'
|
||||
|
||||
# Southeast Asia
|
||||
ID: '印度尼西亚'
|
||||
VN: '越南'
|
||||
TH: '泰国'
|
||||
MM: '缅甸'
|
||||
SG: '新加坡'
|
||||
|
||||
# Middle East
|
||||
TR: '土耳其'
|
||||
IR: '伊朗'
|
||||
SA: '沙特阿拉伯'
|
||||
IQ: '伊拉克'
|
||||
|
||||
# Africa
|
||||
NG: '尼日利亚'
|
||||
EG: '埃及'
|
||||
CD: '刚果民主共和国'
|
||||
TZ: '坦桑尼亚'
|
||||
ZA: '南非'
|
||||
KE: '肯尼亚'
|
||||
AO: '安哥拉'
|
||||
ML: '马里'
|
||||
CI: '科特迪瓦'
|
||||
|
||||
# Europe
|
||||
RU: '俄罗斯'
|
||||
GB: '英国'
|
||||
DE: '德国'
|
||||
|
||||
# North America
|
||||
US: '美国'
|
||||
MX: '墨西哥'
|
||||
|
||||
# South America
|
||||
BR: '巴西'
|
||||
PE: '秘鲁'
|
||||
CO: '哥伦比亚'
|
||||
CL: '智利'
|
||||
|
||||
# Oceania
|
||||
AU: '澳大利亚'
|
@ -1,15 +0,0 @@
|
||||
en:
|
||||
regions:
|
||||
AS: 'Asia'
|
||||
SA: 'South Asia'
|
||||
SEA: 'Southeast Asia'
|
||||
EA: 'East Asia'
|
||||
ME: 'Middle East'
|
||||
AF: 'Africa'
|
||||
NA: 'North Africa'
|
||||
SSA: 'Sub-Saharan Africa'
|
||||
EU: 'Europe'
|
||||
NAM: 'North America'
|
||||
SAM: 'South America'
|
||||
CAM: 'Central America'
|
||||
OC: 'Oceania'
|
@ -1,15 +0,0 @@
|
||||
zh-CN:
|
||||
regions:
|
||||
AS: '亚洲'
|
||||
SA: '南亚'
|
||||
SEA: '东南亚'
|
||||
EA: '东亚'
|
||||
ME: '中东'
|
||||
AF: '非洲'
|
||||
NA: '北非'
|
||||
SSA: '撒哈拉以南非洲'
|
||||
EU: '欧洲'
|
||||
NAM: '北美洲'
|
||||
SAM: '南美洲'
|
||||
CAM: '中美洲'
|
||||
OC: '大洋洲'
|
@ -1,12 +1,9 @@
|
||||
require "sidekiq/web"
|
||||
|
||||
Rails.application.routes.draw do
|
||||
root "home#index"
|
||||
|
||||
resources :cities, only: [ :index, :show ] do
|
||||
resources :weather_arts, path: "weather", only: [ :show ], param: :slug
|
||||
resources :weather_arts, only: [ :show ]
|
||||
end
|
||||
resources :arts, only: [ :index ]
|
||||
|
||||
# namespace :admin do
|
||||
# resources :cities
|
||||
@ -18,15 +15,8 @@ Rails.application.routes.draw do
|
||||
get "cities/index"
|
||||
get "cities/show"
|
||||
get "home/index"
|
||||
get "sitemaps/*path", to: "sitemaps#show", format: false
|
||||
|
||||
devise_for :admin_users, ActiveAdmin::Devise.config
|
||||
ActiveAdmin.routes(self)
|
||||
|
||||
# mount Sidekiq::Web => '/sidekiq'
|
||||
authenticate :admin_user do
|
||||
mount Sidekiq::Web => "/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.
|
||||
|
@ -1,23 +0,0 @@
|
||||
# Use this file to easily define all of your cron jobs.
|
||||
#
|
||||
# It's helpful, but not entirely necessary to understand cron before proceeding.
|
||||
# http://en.wikipedia.org/wiki/Cron
|
||||
|
||||
# Example:
|
||||
#
|
||||
# set :output, "/path/to/my/cron_log.log"
|
||||
#
|
||||
# every 2.hours do
|
||||
# command "/usr/bin/some_great_command"
|
||||
# runner "MyModel.some_method"
|
||||
# rake "some:great:rake:task"
|
||||
# end
|
||||
#
|
||||
# every 4.days do
|
||||
# runner "AnotherModel.prune_old_records"
|
||||
# end
|
||||
# every 2.hour do
|
||||
# runner "BatchGenerateWeatherArtsJob.perform_later"
|
||||
# end
|
||||
|
||||
# Learn more: http://github.com/javan/whenever
|
@ -1,18 +0,0 @@
|
||||
batch_generate_weather:
|
||||
cron: '0 */1 * * *'
|
||||
class: BatchGenerateWeatherArtsWorker
|
||||
description: "Generate weather arts every 2 hours"
|
||||
enabled: true
|
||||
|
||||
refresh_sitemap:
|
||||
cron: '0 5 * * *'
|
||||
class: RefreshSitemapWorker
|
||||
queue: default
|
||||
description: "Refresh sitemap daily"
|
||||
enabled: true
|
||||
|
||||
clean_ahoy_data:
|
||||
cron: '0 0 * * 0'
|
||||
class: CleanAhoyDataWorker
|
||||
queue: default
|
||||
enabled: true
|
@ -1,58 +0,0 @@
|
||||
# Set the host name for URL creation
|
||||
host = Rails.env.production? ? "https://todayaiweather.com" : "http://127.0.0.1:3000"
|
||||
Rails.application.routes.default_url_options[:host] = host
|
||||
SitemapGenerator::Sitemap.adapter = SitemapGenerator::AwsSdkAdapter.new(
|
||||
Rails.application.credentials.dig(:aws, :bucket),
|
||||
aws_access_key_id: Rails.application.credentials.dig(:aws, :access_key_id),
|
||||
aws_secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key),
|
||||
aws_region: Rails.application.credentials.dig(:aws, :region)
|
||||
)
|
||||
SitemapGenerator::Sitemap.sitemaps_path = "sitemaps/"
|
||||
|
||||
SitemapGenerator::Sitemap.default_host = host
|
||||
|
||||
SitemapGenerator::Sitemap.create do
|
||||
add root_path, changefreq: "daily", priority: 1.0
|
||||
add cities_path, changefreq: "daily", priority: 0.9
|
||||
add arts_path, changefreq: "daily", priority: 0.9
|
||||
|
||||
City.find_each do |city|
|
||||
add city_path(city),
|
||||
changefreq: "daily",
|
||||
priority: 0.8,
|
||||
lastmod: city.updated_at
|
||||
end
|
||||
|
||||
WeatherArt.includes(:city).find_each do |art|
|
||||
add city_weather_art_path(art.city, art),
|
||||
changefreq: "daily",
|
||||
priority: 0.7,
|
||||
lastmod: art.updated_at,
|
||||
images: [ {
|
||||
loc: url_for(art.image),
|
||||
title: "#{art.city.name} Weather Art - #{art.weather_date.strftime('%B %d, %Y')}"
|
||||
} ] if art.image.attached?
|
||||
end
|
||||
# Put links creation logic here.
|
||||
#
|
||||
# The root path '/' and sitemap index file are added automatically for you.
|
||||
# Links are added to the Sitemap in the order they are specified.
|
||||
#
|
||||
# Usage: add(path, options={})
|
||||
# (default options are used if you don't specify)
|
||||
#
|
||||
# Defaults: :priority => 0.5, :changefreq => 'weekly',
|
||||
# :lastmod => Time.now, :host => default_host
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# Add '/articles'
|
||||
#
|
||||
# add articles_path, :priority => 0.7, :changefreq => 'daily'
|
||||
#
|
||||
# Add all articles:
|
||||
#
|
||||
# Article.find_each do |article|
|
||||
# add article_path(article), :lastmod => article.updated_at
|
||||
# end
|
||||
end
|
@ -6,10 +6,6 @@ local:
|
||||
service: Disk
|
||||
root: <%= Rails.root.join("storage") %>
|
||||
|
||||
build:
|
||||
service: Disk
|
||||
root: <%= Rails.root.join("tmp/storage") %>
|
||||
|
||||
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
|
||||
# amazon:
|
||||
# service: S3
|
||||
@ -17,21 +13,6 @@ build:
|
||||
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
|
||||
# region: us-east-1
|
||||
# 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)) %>
|
||||
|
||||
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)) %>
|
||||
|
||||
# Remember not to checkin your GCS keyfile to a repository
|
||||
# google:
|
||||
|
@ -7,7 +7,7 @@ class CreateActiveAdminComments < ActiveRecord::Migration[8.0]
|
||||
t.references :author, polymorphic: true
|
||||
t.timestamps
|
||||
end
|
||||
add_index :active_admin_comments, [ :namespace ]
|
||||
add_index :active_admin_comments, [:namespace]
|
||||
end
|
||||
|
||||
def self.down
|
||||
|
@ -14,8 +14,8 @@ class CreateFriendlyIdSlugs < MIGRATION_CLASS
|
||||
t.string :scope
|
||||
t.datetime :created_at
|
||||
end
|
||||
add_index :friendly_id_slugs, [ :sluggable_type, :sluggable_id ]
|
||||
add_index :friendly_id_slugs, [ :slug, :sluggable_type ], length: { slug: 140, sluggable_type: 50 }
|
||||
add_index :friendly_id_slugs, [ :slug, :sluggable_type, :scope ], length: { slug: 70, sluggable_type: 50, scope: 70 }, unique: true
|
||||
add_index :friendly_id_slugs, [:sluggable_type, :sluggable_id]
|
||||
add_index :friendly_id_slugs, [:slug, :sluggable_type], length: {slug: 140, sluggable_type: 50}
|
||||
add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], length: {slug: 70, sluggable_type: 50, scope: 70}, unique: true
|
||||
end
|
||||
end
|
||||
|
@ -1,13 +0,0 @@
|
||||
class CreateRegions < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :regions do |t|
|
||||
t.string :name
|
||||
t.string :code
|
||||
t.string :slug
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :regions, :code, unique: true
|
||||
add_index :regions, :slug, unique: true
|
||||
end
|
||||
end
|
@ -1,14 +0,0 @@
|
||||
class CreateCountries < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :countries do |t|
|
||||
t.string :name
|
||||
t.string :code
|
||||
t.string :slug
|
||||
t.references :region, null: false, foreign_key: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :countries, :code, unique: true
|
||||
add_index :countries, :slug, unique: true
|
||||
end
|
||||
end
|
@ -1,5 +0,0 @@
|
||||
class AddCountryToCities < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_reference :cities, :country, null: false, foreign_key: true
|
||||
end
|
||||
end
|
@ -1,6 +0,0 @@
|
||||
class RemoveGeographicFieldsFromCities < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
remove_column :cities, :region, :string
|
||||
remove_column :cities, :country, :string
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user