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
|
!/app/assets/builds/.keep
|
||||||
|
|
||||||
/node_modules
|
/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
|
# Install base packages
|
||||||
RUN apt-get update -qq && \
|
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
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||||
|
|
||||||
# Set production environment
|
# Set production environment
|
||||||
@ -30,7 +30,7 @@ FROM base AS build
|
|||||||
|
|
||||||
# Install packages needed to build gems and node modules
|
# Install packages needed to build gems and node modules
|
||||||
RUN apt-get update -qq && \
|
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
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||||
|
|
||||||
# Install JavaScript dependencies
|
# Install JavaScript dependencies
|
||||||
@ -59,7 +59,7 @@ COPY . .
|
|||||||
RUN bundle exec bootsnap precompile app/ lib/
|
RUN bundle exec bootsnap precompile app/ lib/
|
||||||
|
|
||||||
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
|
# 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
|
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 and own only the runtime files as a non-root user for security
|
||||||
RUN groupadd --system --gid 1000 rails && \
|
RUN groupadd --system --gid 1000 rails && \
|
||||||
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
|
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
|
||||||
chown -R rails:rails db log storage tmp public
|
chown -R rails:rails db log storage tmp
|
||||||
USER 1000:1000
|
USER 1000:1000
|
||||||
|
|
||||||
# Entrypoint prepares the database.
|
# 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]
|
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
|
||||||
# gem "image_processing", "~> 1.2"
|
# gem "image_processing", "~> 1.2"
|
||||||
gem "devise", "~> 4.9"
|
gem 'devise', '~> 4.9'
|
||||||
gem "activeadmin", "~> 3.2"
|
gem 'activeadmin', '~> 3.2'
|
||||||
gem "friendly_id", "~> 5.5"
|
gem 'friendly_id', '~> 5.5'
|
||||||
|
|
||||||
gem "kaminari", "~> 1.2"
|
|
||||||
|
|
||||||
gem "meta-tags", "~> 2.22"
|
|
||||||
gem "sitemap_generator", "~> 6.3"
|
|
||||||
|
|
||||||
gem "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"
|
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
||||||
@ -80,10 +61,6 @@ group :development do
|
|||||||
gem "web-console"
|
gem "web-console"
|
||||||
end
|
end
|
||||||
|
|
||||||
group :production do
|
|
||||||
gem "pg", "~> 1.5"
|
|
||||||
end
|
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
|
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
|
||||||
gem "capybara"
|
gem "capybara"
|
||||||
|
142
Gemfile.lock
142
Gemfile.lock
@ -84,30 +84,10 @@ GEM
|
|||||||
uri (>= 0.13.1)
|
uri (>= 0.13.1)
|
||||||
addressable (2.8.7)
|
addressable (2.8.7)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
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)
|
arbre (1.7.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
ruby2_keywords (>= 0.0.2)
|
ruby2_keywords (>= 0.0.2)
|
||||||
ast (2.4.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)
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
bcrypt_pbkdf (1.1.1)
|
bcrypt_pbkdf (1.1.1)
|
||||||
@ -140,7 +120,6 @@ GEM
|
|||||||
debug (1.10.0)
|
debug (1.10.0)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
device_detector (1.1.3)
|
|
||||||
devise (4.9.4)
|
devise (4.9.4)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
@ -148,30 +127,11 @@ GEM
|
|||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
dotenv (3.1.7)
|
dotenv (3.1.7)
|
||||||
down (5.4.2)
|
|
||||||
addressable (~> 2.8)
|
|
||||||
drb (2.2.1)
|
drb (2.2.1)
|
||||||
ed25519 (1.3.0)
|
ed25519 (1.3.0)
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
et-orbi (1.2.11)
|
et-orbi (1.2.11)
|
||||||
tzinfo
|
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)
|
formtastic (5.0.0)
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
formtastic_i18n (0.7.0)
|
formtastic_i18n (0.7.0)
|
||||||
@ -185,29 +145,20 @@ GEM
|
|||||||
has_scope (0.8.2)
|
has_scope (0.8.2)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
httparty (0.22.0)
|
i18n (1.14.6)
|
||||||
csv
|
|
||||||
mini_mime (>= 1.0.0)
|
|
||||||
multi_xml (>= 0.5.2)
|
|
||||||
i18n (1.14.7)
|
|
||||||
concurrent-ruby (~> 1.0)
|
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)
|
inherited_resources (1.14.0)
|
||||||
actionpack (>= 6.0)
|
actionpack (>= 6.0)
|
||||||
has_scope (>= 0.6)
|
has_scope (>= 0.6)
|
||||||
railties (>= 6.0)
|
railties (>= 6.0)
|
||||||
responders (>= 2)
|
responders (>= 2)
|
||||||
io-console (0.8.0)
|
io-console (0.8.0)
|
||||||
irb (1.15.1)
|
irb (1.14.3)
|
||||||
pp (>= 0.6.0)
|
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
jbuilder (2.13.0)
|
jbuilder (2.13.0)
|
||||||
actionview (>= 5.0.0)
|
actionview (>= 5.0.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
jmespath (1.6.2)
|
|
||||||
jquery-rails (4.6.0)
|
jquery-rails (4.6.0)
|
||||||
rails-dom-testing (>= 1, < 3)
|
rails-dom-testing (>= 1, < 3)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
@ -238,7 +189,7 @@ GEM
|
|||||||
activerecord
|
activerecord
|
||||||
kaminari-core (= 1.2.2)
|
kaminari-core (= 1.2.2)
|
||||||
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)
|
logger (1.6.5)
|
||||||
loofah (2.24.0)
|
loofah (2.24.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
@ -250,17 +201,9 @@ GEM
|
|||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.4)
|
marcel (1.0.4)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
meta-tags (2.22.1)
|
|
||||||
actionpack (>= 6.0.0, < 8.1)
|
|
||||||
mini_magick (4.13.2)
|
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
minitest (5.25.4)
|
minitest (5.25.4)
|
||||||
msgpack (1.7.5)
|
msgpack (1.7.5)
|
||||||
multi_xml (0.7.1)
|
|
||||||
bigdecimal (~> 3.1)
|
|
||||||
multipart-post (2.4.1)
|
|
||||||
net-http (0.6.0)
|
|
||||||
uri
|
|
||||||
net-imap (0.5.5)
|
net-imap (0.5.5)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
@ -268,7 +211,7 @@ GEM
|
|||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.2)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-scp (4.1.0)
|
net-scp (4.0.0)
|
||||||
net-ssh (>= 2.6.5, < 8.0.0)
|
net-ssh (>= 2.6.5, < 8.0.0)
|
||||||
net-sftp (4.0.0)
|
net-sftp (4.0.0)
|
||||||
net-ssh (>= 5.0.0, < 8.0.0)
|
net-ssh (>= 5.0.0, < 8.0.0)
|
||||||
@ -276,21 +219,21 @@ GEM
|
|||||||
net-protocol
|
net-protocol
|
||||||
net-ssh (7.3.0)
|
net-ssh (7.3.0)
|
||||||
nio4r (2.7.4)
|
nio4r (2.7.4)
|
||||||
nokogiri (1.18.2-aarch64-linux-gnu)
|
nokogiri (1.18.1-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.2-aarch64-linux-musl)
|
nokogiri (1.18.1-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.2-arm-linux-gnu)
|
nokogiri (1.18.1-arm-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.2-arm-linux-musl)
|
nokogiri (1.18.1-arm-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.2-arm64-darwin)
|
nokogiri (1.18.1-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.2-x86_64-darwin)
|
nokogiri (1.18.1-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.2-x86_64-linux-gnu)
|
nokogiri (1.18.1-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.2-x86_64-linux-musl)
|
nokogiri (1.18.1-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostruct (0.6.1)
|
ostruct (0.6.1)
|
||||||
@ -298,10 +241,6 @@ GEM
|
|||||||
parser (3.3.7.0)
|
parser (3.3.7.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pg (1.5.9)
|
|
||||||
pp (0.6.2)
|
|
||||||
prettyprint
|
|
||||||
prettyprint (0.2.0)
|
|
||||||
propshaft (1.1.0)
|
propshaft (1.1.0)
|
||||||
actionpack (>= 7.0.0)
|
actionpack (>= 7.0.0)
|
||||||
activesupport (>= 7.0.0)
|
activesupport (>= 7.0.0)
|
||||||
@ -311,11 +250,11 @@ GEM
|
|||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (6.0.1)
|
public_suffix (6.0.1)
|
||||||
puma (6.6.0)
|
puma (6.5.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.9)
|
rack (3.1.8)
|
||||||
rack-session (2.1.0)
|
rack-session (2.1.0)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
@ -360,8 +299,6 @@ GEM
|
|||||||
i18n
|
i18n
|
||||||
rdoc (6.11.0)
|
rdoc (6.11.0)
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
redis-client (0.23.2)
|
|
||||||
connection_pool
|
|
||||||
regexp_parser (2.10.0)
|
regexp_parser (2.10.0)
|
||||||
reline (0.6.0)
|
reline (0.6.0)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
@ -369,17 +306,17 @@ GEM
|
|||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.4.0)
|
rexml (3.4.0)
|
||||||
rubocop (1.71.1)
|
rubocop (1.70.0)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.3.0.2)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 2.9.3, < 3.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)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 4.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.38.0)
|
rubocop-ast (1.37.0)
|
||||||
parser (>= 3.3.1.0)
|
parser (>= 3.3.1.0)
|
||||||
rubocop-minitest (0.36.0)
|
rubocop-minitest (0.36.0)
|
||||||
rubocop (>= 1.61, < 2.0)
|
rubocop (>= 1.61, < 2.0)
|
||||||
@ -387,7 +324,7 @@ GEM
|
|||||||
rubocop-performance (1.23.1)
|
rubocop-performance (1.23.1)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rails (2.29.1)
|
rubocop-rails (2.29.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.52.0, < 2.0)
|
rubocop (>= 1.52.0, < 2.0)
|
||||||
@ -397,39 +334,17 @@ GEM
|
|||||||
rubocop-minitest
|
rubocop-minitest
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
rubocop-rails
|
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-progressbar (1.13.0)
|
||||||
ruby-vips (2.2.2)
|
|
||||||
ffi (~> 1.12)
|
|
||||||
logger
|
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.4.1)
|
rubyzip (2.4.1)
|
||||||
rufus-scheduler (3.9.2)
|
|
||||||
fugit (~> 1.1, >= 1.11.1)
|
|
||||||
safely_block (0.4.1)
|
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
selenium-webdriver (4.28.0)
|
selenium-webdriver (4.27.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
logger (~> 1.4)
|
logger (~> 1.4)
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
rubyzip (>= 1.2.2, < 3.0)
|
rubyzip (>= 1.2.2, < 3.0)
|
||||||
websocket (~> 1.0)
|
websocket (~> 1.0)
|
||||||
sidekiq (7.3.8)
|
solid_cable (3.0.5)
|
||||||
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)
|
|
||||||
actioncable (>= 7.2)
|
actioncable (>= 7.2)
|
||||||
activejob (>= 7.2)
|
activejob (>= 7.2)
|
||||||
activerecord (>= 7.2)
|
activerecord (>= 7.2)
|
||||||
@ -438,7 +353,7 @@ GEM
|
|||||||
activejob (>= 7.2)
|
activejob (>= 7.2)
|
||||||
activerecord (>= 7.2)
|
activerecord (>= 7.2)
|
||||||
railties (>= 7.2)
|
railties (>= 7.2)
|
||||||
solid_queue (1.1.3)
|
solid_queue (1.1.2)
|
||||||
activejob (>= 7.1)
|
activejob (>= 7.1)
|
||||||
activerecord (>= 7.1)
|
activerecord (>= 7.1)
|
||||||
concurrent-ruby (>= 1.3.1)
|
concurrent-ruby (>= 1.3.1)
|
||||||
@ -468,7 +383,6 @@ GEM
|
|||||||
thruster (0.1.10-arm64-darwin)
|
thruster (0.1.10-arm64-darwin)
|
||||||
thruster (0.1.10-x86_64-darwin)
|
thruster (0.1.10-x86_64-darwin)
|
||||||
thruster (0.1.10-x86_64-linux)
|
thruster (0.1.10-x86_64-linux)
|
||||||
tilt (2.6.0)
|
|
||||||
timeout (0.4.3)
|
timeout (0.4.3)
|
||||||
turbo-rails (2.0.11)
|
turbo-rails (2.0.11)
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
@ -510,35 +424,21 @@ PLATFORMS
|
|||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
activeadmin (~> 3.2)
|
activeadmin (~> 3.2)
|
||||||
ahoy_matey (~> 5.2)
|
|
||||||
aws-sdk-s3 (~> 1.177)
|
|
||||||
bootsnap
|
bootsnap
|
||||||
brakeman
|
brakeman
|
||||||
capybara
|
capybara
|
||||||
cssbundling-rails
|
cssbundling-rails
|
||||||
debug
|
debug
|
||||||
devise (~> 4.9)
|
devise (~> 4.9)
|
||||||
down (~> 5.4)
|
|
||||||
friendly_id (~> 5.5)
|
friendly_id (~> 5.5)
|
||||||
httparty (~> 0.22.0)
|
|
||||||
image_processing (~> 1.13)
|
|
||||||
jbuilder
|
jbuilder
|
||||||
jsbundling-rails
|
jsbundling-rails
|
||||||
kamal
|
kamal
|
||||||
kaminari (~> 1.2)
|
|
||||||
meta-tags (~> 2.22)
|
|
||||||
mini_magick (~> 4.13.2)
|
|
||||||
pg (~> 1.5)
|
|
||||||
propshaft
|
propshaft
|
||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
rails (~> 8.0.1)
|
rails (~> 8.0.1)
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
ruby-openai (~> 7.3)
|
|
||||||
ruby-vips (~> 2.2)
|
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
sidekiq (~> 7.3)
|
|
||||||
sidekiq-scheduler (~> 5.0)
|
|
||||||
sitemap_generator (~> 6.3)
|
|
||||||
solid_cable
|
solid_cable
|
||||||
solid_cache
|
solid_cache
|
||||||
solid_queue
|
solid_queue
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
ActiveAdmin.register AdminUser do
|
ActiveAdmin.register AdminUser do
|
||||||
menu label: "AdminUser Manager", parent: "系统管理"
|
|
||||||
permit_params :email, :password, :password_confirmation
|
permit_params :email, :password, :password_confirmation
|
||||||
|
|
||||||
index do
|
index do
|
||||||
@ -25,4 +24,5 @@ ActiveAdmin.register AdminUser do
|
|||||||
end
|
end
|
||||||
f.actions
|
f.actions
|
||||||
end
|
end
|
||||||
|
|
||||||
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
|
ActiveAdmin.register City do
|
||||||
menu label: "City Manager", parent: "系统管理"
|
|
||||||
controller do
|
controller do
|
||||||
def find_resource
|
def find_resource
|
||||||
scoped_collection.friendly.find(params[:id])
|
scoped_collection.friendly.find(params[:id])
|
||||||
@ -11,7 +10,7 @@ ActiveAdmin.register City do
|
|||||||
#
|
#
|
||||||
# Uncomment all parameters which should be permitted for assignment
|
# 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
|
# or
|
||||||
#
|
#
|
||||||
@ -25,30 +24,31 @@ ActiveAdmin.register City do
|
|||||||
selectable_column
|
selectable_column
|
||||||
id_column
|
id_column
|
||||||
column :name
|
column :name
|
||||||
column :country
|
column :slug
|
||||||
column :region do |city|
|
|
||||||
city.region
|
|
||||||
end
|
|
||||||
column :latitude
|
column :latitude
|
||||||
column :longitude
|
column :longitude
|
||||||
column :active
|
column :active
|
||||||
|
column :created_at
|
||||||
actions
|
actions
|
||||||
end
|
end
|
||||||
|
|
||||||
filter :name
|
filter :name
|
||||||
filter :active
|
filter :active
|
||||||
filter :country, as: :select
|
|
||||||
|
|
||||||
form do |f|
|
form do |f|
|
||||||
f.inputs do
|
f.inputs do
|
||||||
f.input :active
|
f.input :active
|
||||||
f.input :name
|
f.input :name
|
||||||
f.input :country
|
f.input :country, as: :String
|
||||||
f.input :latitude
|
f.input :latitude
|
||||||
f.input :longitude
|
f.input :longitude
|
||||||
f.input :priority
|
f.input :priority
|
||||||
f.input :timezone
|
f.input :timezone
|
||||||
|
f.input :region
|
||||||
|
f.input :last_weather_fetch
|
||||||
|
f.input :last_image_generation
|
||||||
end
|
end
|
||||||
f.actions
|
f.actions
|
||||||
end
|
end
|
||||||
|
|
||||||
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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
ActiveAdmin.register_page "Dashboard" do
|
ActiveAdmin.register_page "Dashboard" do
|
||||||
menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }
|
menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }
|
||||||
|
|
||||||
@ -11,66 +10,6 @@ ActiveAdmin.register_page "Dashboard" do
|
|||||||
end
|
end
|
||||||
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.
|
# Here is an example of a simple dashboard with columns and panels.
|
||||||
#
|
#
|
||||||
# columns do
|
# 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
|
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:
|
# See permitted parameters documentation:
|
||||||
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
||||||
#
|
#
|
||||||
@ -22,11 +17,9 @@ ActiveAdmin.register WeatherArt do
|
|||||||
permit_params :city_id, :weather_date, :description, :temperature,
|
permit_params :city_id, :weather_date, :description, :temperature,
|
||||||
:feeling_temp, :humidity, :wind_scale, :wind_speed,
|
:feeling_temp, :humidity, :wind_scale, :wind_speed,
|
||||||
:precipitation, :pressure, :visibility, :cloud,
|
:precipitation, :pressure, :visibility, :cloud,
|
||||||
:prompt, :image, :slug
|
:prompt, :image
|
||||||
|
|
||||||
remove_filter :image_attachment, :image_blob
|
remove_filter :image_attachment, :image_blob
|
||||||
filter :city_id
|
|
||||||
filter :weather_data
|
|
||||||
|
|
||||||
index do
|
index do
|
||||||
selectable_column
|
selectable_column
|
||||||
@ -36,7 +29,7 @@ ActiveAdmin.register WeatherArt do
|
|||||||
column :description
|
column :description
|
||||||
column :temperature
|
column :temperature
|
||||||
column :image do |weather_art|
|
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
|
end
|
||||||
actions
|
actions
|
||||||
end
|
end
|
||||||
@ -81,4 +74,5 @@ ActiveAdmin.register WeatherArt do
|
|||||||
end
|
end
|
||||||
f.actions
|
f.actions
|
||||||
end
|
end
|
||||||
|
|
||||||
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 base;
|
||||||
@tailwind components;
|
@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
|
class ApplicationController < ActionController::Base
|
||||||
include SeoConcern
|
|
||||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||||
before_action :log_browser_info
|
allow_browser versions: :modern
|
||||||
# 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
|
|
||||||
end
|
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
|
class CitiesController < ApplicationController
|
||||||
def index
|
def index
|
||||||
@regions = Region.includes(:countries).order(:name)
|
@cities = City.all.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"
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@city = City.friendly.find(params[:id])
|
@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
|
||||||
end
|
end
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
class HomeController < ApplicationController
|
class HomeController < ApplicationController
|
||||||
def index
|
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)
|
@featured_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(5)
|
||||||
set_meta_tags(
|
|
||||||
title: "AI-Generated Weather Art",
|
|
||||||
description: "Experience weather through artistic AI visualization. Daily updated weather art for cities worldwide.",
|
|
||||||
keywords: "AI weather art, weather visualization, city weather, artificial intelligence"
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -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
|
class WeatherArtsController < ApplicationController
|
||||||
def show
|
def show
|
||||||
@city = City.friendly.find(params[:city_id])
|
@city = City.friendly.find(params[:city_id])
|
||||||
@weather_art = @city.weather_arts.friendly.find(params[:slug])
|
@weather_art = @city.weather_arts.find(params[:id])
|
||||||
|
|
||||||
@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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,24 +1,2 @@
|
|||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
def weather_art_schema(weather_art)
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "ImageObject",
|
|
||||||
"name": "#{weather_art.city.name} Weather Art",
|
|
||||||
"description": weather_art.description,
|
|
||||||
"datePublished": weather_art.created_at.iso8601,
|
|
||||||
"contentUrl": url_for(weather_art.image),
|
|
||||||
"author": {
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": "TodayAIWeather"
|
|
||||||
},
|
|
||||||
"locationCreated": {
|
|
||||||
"@type": "Place",
|
|
||||||
"name": weather_art.city.name,
|
|
||||||
"address": {
|
|
||||||
"@type": "PostalAddress",
|
|
||||||
"addressCountry": weather_art.city.country.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.to_json.html_safe if weather_art.image.attached?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
module ArtsHelper
|
|
||||||
end
|
|
@ -1,2 +0,0 @@
|
|||||||
module SitemapsHelper
|
|
||||||
end
|
|
@ -1,51 +1,2 @@
|
|||||||
module WeatherArtsHelper
|
module WeatherArtsHelper
|
||||||
def weather_description_icon(description)
|
|
||||||
case description&.downcase
|
|
||||||
when /rain/
|
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
|
||||||
</svg>'.html_safe
|
|
||||||
when /cloud/
|
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
|
||||||
</svg>'.html_safe
|
|
||||||
when /sun|clear/
|
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
||||||
</svg>'.html_safe
|
|
||||||
else
|
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>'.html_safe
|
|
||||||
end
|
|
||||||
end
|
|
||||||
def weather_stat_icon(type)
|
|
||||||
case type
|
|
||||||
when "temperature"
|
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>'.html_safe
|
|
||||||
when "wind"
|
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
|
||||||
</svg>'.html_safe
|
|
||||||
when "humidity"
|
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
|
||||||
</svg>'.html_safe
|
|
||||||
when "visibility"
|
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
||||||
</svg>'.html_safe
|
|
||||||
when "pressure"
|
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
||||||
</svg>'.html_safe
|
|
||||||
when "cloud"
|
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
|
||||||
</svg>'.html_safe
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -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
|
// Entry point for the build script in your package.json
|
||||||
import "@hotwired/turbo-rails"
|
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 "./controllers"
|
||||||
import "./active_admin"
|
|
||||||
|
@ -6,9 +6,3 @@ import { application } from "./application"
|
|||||||
|
|
||||||
import HelloController from "./hello_controller"
|
import HelloController from "./hello_controller"
|
||||||
application.register("hello", HelloController)
|
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,7 +1,7 @@
|
|||||||
class AdminUser < ApplicationRecord
|
class AdminUser < ApplicationRecord
|
||||||
# Include default devise modules. Others available are:
|
# Include default devise modules. Others available are:
|
||||||
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
|
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
|
||||||
devise :database_authenticatable,
|
devise :database_authenticatable,
|
||||||
:recoverable, :rememberable, :validatable
|
:recoverable, :rememberable, :validatable
|
||||||
|
|
||||||
def self.ransackable_attributes(auth_object = nil)
|
def self.ransackable_attributes(auth_object = nil)
|
||||||
|
@ -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
|
class City < ApplicationRecord
|
||||||
extend FriendlyId
|
extend FriendlyId
|
||||||
friendly_id :slug_candidates, use: :slugged
|
friendly_id :name, use: :slugged
|
||||||
belongs_to :country
|
|
||||||
|
|
||||||
has_many :weather_arts, dependent: :destroy
|
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 :name, presence: true
|
||||||
validates :latitude, presence: true
|
validates :latitude, presence: true
|
||||||
validates :longitude, 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?
|
def should_generate_new_friendly_id?
|
||||||
name_changed? || super
|
name_changed? || super
|
||||||
end
|
end
|
||||||
@ -100,32 +17,7 @@ class City < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.ransackable_attributes(auth_object = nil)
|
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
|
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
|
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
|
class WeatherArt < ApplicationRecord
|
||||||
extend FriendlyId
|
|
||||||
friendly_id :weather_date, use: :slugged
|
|
||||||
|
|
||||||
belongs_to :city
|
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_one_attached :image
|
||||||
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
|
|
||||||
|
|
||||||
validates :weather_date, presence: true
|
validates :weather_date, presence: true
|
||||||
validates :city_id, 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)
|
def self.ransackable_associations(auth_object = nil)
|
||||||
[ "city", "image_attachment", "image_blob" ]
|
["city", "image_attachment", "image_blob"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.ransackable_attributes(auth_object = nil)
|
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" ]
|
[ "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
|
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
|
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="space-y-8">
|
||||||
<div class="min-h-screen">
|
<h1 class="text-3xl font-bold">Cities</h1>
|
||||||
<!-- 页面标题和背景 -->
|
|
||||||
<% featured_art = WeatherArt.includes(:city).joins(:image_attachment).order(created_at: :desc).first %>
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<div class="relative bg-base-100">
|
<% @cities.each do |city| %>
|
||||||
<!-- 背景图像和渐变 -->
|
<div class="card bg-base-200">
|
||||||
<% if featured_art&.image&.attached? %>
|
<div class="card-body">
|
||||||
<div class="absolute inset-0 h-[60vh] overflow-hidden">
|
<h2 class="card-title"><%= city.name %></h2>
|
||||||
<%= image_tag featured_art.image,
|
<p>Latitude: <%= city.latitude %></p>
|
||||||
class: "w-full h-full object-cover object-center" %>
|
<p>Longitude: <%= city.longitude %></p>
|
||||||
<div class="absolute inset-0 bg-gradient-to-b from-base-100/40 via-base-100/80 to-base-100"></div>
|
<div class="card-actions justify-end">
|
||||||
|
<%= link_to "View Weather Art", city_path(city), class: "btn btn-primary" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% 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>
|
||||||
</div>
|
</div>
|
@ -1,149 +1,49 @@
|
|||||||
<div class="relative min-h-screen bg-base-200">
|
<div class="space-y-8">
|
||||||
<!-- 背景效果 -->
|
<div class="flex justify-between items-center">
|
||||||
<% if @city.latest_weather_art&.image&.attached? %>
|
<h1 class="text-3xl font-bold"><%= @city.name %></h1>
|
||||||
<div class="fixed inset-0 -z-10">
|
<%= link_to "Back to Cities", cities_path, class: "btn btn-ghost" %>
|
||||||
<%= image_tag @city.latest_weather_art.image,
|
</div>
|
||||||
class: "absolute w-full h-full object-cover scale-110 filter blur-2xl opacity-25" %>
|
|
||||||
<div class="absolute inset-0 bg-gradient-to-b from-base-200/90 to-base-200/70 backdrop-blur-md"></div>
|
<div class="stats shadow">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-title">Latitude</div>
|
||||||
|
<div class="stat-value"><%= @city.latitude %></div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<div class="stat">
|
||||||
|
<div class="stat-title">Longitude</div>
|
||||||
<!-- 主要内容 -->
|
<div class="stat-value"><%= @city.longitude %></div>
|
||||||
<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>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 城市信息头部 -->
|
<div class="space-y-4">
|
||||||
<div class="container mx-auto px-4 mb-12">
|
<h2 class="text-2xl font-bold">Weather Art History</h2>
|
||||||
<div class="max-w-4xl mx-auto text-center space-y-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<h1 class="text-4xl md:text-6xl font-display font-bold">
|
<% @city.weather_arts.order(weather_date: :desc).each do |art| %>
|
||||||
<span class="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
<div class="card bg-base-200">
|
||||||
<%= @city.localized_name %>
|
<figure>
|
||||||
</span>
|
<% if art.image.attached? %>
|
||||||
</h1>
|
<%= image_tag art.image, class: "w-full h-48 object-cover" %>
|
||||||
|
<% end %>
|
||||||
<div class="flex flex-wrap justify-center items-center gap-3">
|
</figure>
|
||||||
<div class="badge badge-lg badge-primary gap-2">
|
<div class="card-body">
|
||||||
<%= @city.country.name %>, <%= @city.region %>
|
<h3 class="card-title"><%= art.weather_date.strftime("%Y-%m-%d") %></h3>
|
||||||
</div>
|
<p><%= art.description %></p>
|
||||||
<div class="badge badge-lg badge-secondary gap-2">
|
<div class="stats stats-vertical shadow">
|
||||||
<%= @city.timezone.present? ? Time.current.in_time_zone(@city.timezone).strftime("%Y-%m-%d %H:%M") : "Timezone undefined" %>
|
<div class="stat">
|
||||||
</div>
|
<div class="stat-title">Temperature</div>
|
||||||
</div>
|
<div class="stat-value"><%= art.temperature %>°C</div>
|
||||||
|
</div>
|
||||||
<!-- 主要统计信息 -->
|
<div class="stat">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-8">
|
<div class="stat-title">Humidity</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="stat-value"><%= art.humidity %>%</div>
|
||||||
<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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
<% @city.weather_arts.order(weather_date: :desc).each do |art| %>
|
|
||||||
<div class="card bg-base-100/80 backdrop-blur-sm shadow-xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1">
|
|
||||||
<figure class="relative aspect-video overflow-hidden">
|
|
||||||
<% if art.image.attached? %>
|
|
||||||
<%= image_tag art.image,
|
|
||||||
class: "w-full h-full object-cover transform hover:scale-105 transition-transform duration-500" %>
|
|
||||||
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/70 to-transparent">
|
|
||||||
<div class="flex items-center justify-between text-white">
|
|
||||||
<div class="text-2xl font-bold"><%= art.temperature %>°C</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<div class="font-medium"><%= art.weather_date.strftime("%H:%M") %></div>
|
|
||||||
<div class="text-sm opacity-80"><%= art.weather_date.strftime("%B %d, %Y") %></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title font-display">
|
|
||||||
<%= weather_description_icon(art.description) %>
|
|
||||||
<%= art.description %>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 my-4">
|
|
||||||
<div class="flex items-center gap-2 text-sm">
|
|
||||||
<%= weather_stat_icon("humidity") %>
|
|
||||||
<span>Humidity: <%= art.humidity %>%</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 text-sm">
|
|
||||||
<%= weather_stat_icon("wind") %>
|
|
||||||
<span>Wind: <%= art.wind_scale %></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= link_to city_weather_art_path(@city, art),
|
|
||||||
class: "btn btn-primary btn-block" do %>
|
|
||||||
View Details
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<div class="card-actions justify-end">
|
||||||
|
<%= link_to "View Details", city_weather_art_path(@city, art), class: "btn btn-primary" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -1,68 +1,46 @@
|
|||||||
<div>
|
<div class="space-y-8">
|
||||||
<!-- 首屏展示区 -->
|
<!-- 头部标题 -->
|
||||||
<section class="h-screen-90 relative overflow-hidden">
|
<div class="text-center space-y-4">
|
||||||
<% if @featured_arts.first&.image&.attached? %>
|
<h1 class="text-4xl font-bold">AI Weather Art</h1>
|
||||||
<div class="absolute inset-0">
|
<p class="text-xl text-base-content/70">Discover the beauty of weather through AI-generated art</p>
|
||||||
<%= image_tag @featured_arts.first.image, class: "w-full h-full object-cover" %>
|
</div>
|
||||||
<div class="absolute inset-0 bg-gradient-to-r from-base-100/90 to-base-100/50"></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>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
</div>
|
||||||
<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">
|
<div class="space-y-4">
|
||||||
<h2 class="text-3xl font-display font-bold text-center">Shuffle Latest Weather Art</h2>
|
<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">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<% WeatherArt.last(6).each do |art| %>
|
||||||
<% @latest_arts.each do |art| %>
|
<div class="card bg-base-200">
|
||||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow duration-300">
|
<figure>
|
||||||
<figure class="relative aspect-[4/3] overflow-hidden">
|
|
||||||
<% if art.image.attached? %>
|
<% 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 %>
|
<% end %>
|
||||||
</figure>
|
</figure>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex justify-between items-start">
|
<h3 class="card-title"><%= art.city.name %></h3>
|
||||||
<div>
|
<p><%= art.weather_date.strftime("%Y-%m-%d") %></p>
|
||||||
<h3 class="card-title font-display"><%= art.city.name %></h3>
|
<p><%= art.description %></p>
|
||||||
<p class="text-base-content/70">
|
<div class="card-actions justify-end">
|
||||||
<%= art.weather_date.strftime("%B %d, %Y") %>
|
<%= link_to "View Details", city_weather_art_path(art.city, art), class: "btn btn-primary" %>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<div class="text-2xl font-bold"><%= art.temperature %>°C</div>
|
|
||||||
<div class="text-sm text-base-content/70"><%= art.description %></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-actions justify-end mt-4">
|
|
||||||
<%= link_to "View Details", city_weather_art_path(art.city, art),
|
|
||||||
class: "btn btn-primary btn-outline" %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</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>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title><%= content_for(:title) || "Today Ai Weather" %></title>
|
<title><%= content_for(:title) || "Today Ai Weather" %></title>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<%= display_meta_tags(
|
|
||||||
site: 'TodayAIWeather',
|
|
||||||
reverse: true,
|
|
||||||
og: {
|
|
||||||
site_name: 'TodayAIWeather',
|
|
||||||
type: 'website',
|
|
||||||
url: request.original_url
|
|
||||||
},
|
|
||||||
alternate: {
|
|
||||||
"zh-CN" => url_for(locale: 'zh-CN'),
|
|
||||||
"en" => url_for(locale: 'en')
|
|
||||||
}
|
|
||||||
) %>
|
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
@ -33,66 +20,28 @@
|
|||||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||||
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||||
<script defer data-domain="todayaiweather.com" src="https://plausible.frytea.com/js/script.js"></script>
|
|
||||||
|
|
||||||
<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>
|
</head>
|
||||||
|
|
||||||
<body class="min-h-screen bg-base-100 font-sans">
|
<body class="min-h-screen bg-base-100">
|
||||||
<!-- 导航栏 -->
|
<div class="navbar bg-base-100">
|
||||||
<div class="navbar bg-base-100/80 backdrop-blur-sm fixed top-0 z-50">
|
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<%= link_to root_path, class: "text-2xl font-display font-bold hover:text-primary transition-colors" do %>
|
<%= link_to "AI Weather Art", root_path, class: "btn btn-ghost normal-case text-xl" %>
|
||||||
Today AI Weather
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-none">
|
<div class="flex-none">
|
||||||
<%= link_to "Cities", cities_path, class: "btn btn-ghost font-sans" %>
|
<%= link_to "Cities", cities_path, class: "btn btn-ghost normal-case" %>
|
||||||
<%= link_to "Arts", arts_path, class: "btn btn-ghost font-sans" %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 主要内容 -->
|
<main class="container mx-auto px-4 py-8">
|
||||||
<main class="pt-16">
|
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- 页脚 -->
|
<footer class="footer footer-center p-4 bg-base-200 text-base-content">
|
||||||
<footer class="footer footer-center p-8 bg-base-200 text-base-content">
|
<div>
|
||||||
<div class="container mx-auto flex flex-col gap-2">
|
<p>Copyright © 2024 - All rights reserved by AI Weather Art</p>
|
||||||
<div id="busuanzi_container" class="text-xs opacity-70">
|
|
||||||
<div class="space-x-2">
|
|
||||||
<span>Page Views: <span id="busuanzi_page_pv"></span></span>
|
|
||||||
<span>|</span>
|
|
||||||
<span>Page Visitors: <span id="busuanzi_page_uv"></span></span>
|
|
||||||
<span>|</span>
|
|
||||||
<span>Total Views: <span id="busuanzi_site_pv"></span></span>
|
|
||||||
<span>|</span>
|
|
||||||
<span>Total Visitors: <span id="busuanzi_site_uv"></span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="font-display opacity-80">
|
|
||||||
Copyright © 2024 - All rights reserved by AI Weather Art
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 %>
|
<div class="space-y-8">
|
||||||
<script type="application/ld+json">
|
<div class="flex justify-between items-center">
|
||||||
<%= weather_art_schema(@weather_art) %>
|
<h1 class="text-3xl font-bold"><%= @weather_art.city.name %> - <%= @weather_art.weather_date.strftime("%Y-%m-%d") %></h1>
|
||||||
</script>
|
<%= link_to "Back to City", city_path(@weather_art.city), class: "btn btn-ghost" %>
|
||||||
<% end %>
|
</div>
|
||||||
|
|
||||||
<div class="relative min-h-screen bg-white"> <!-- 使用更明快的背景颜色 -->
|
<div class="card lg:card-side bg-base-200 shadow-xl">
|
||||||
<div class="container mx-auto px-4 pt-12 pb-16">
|
<figure class="lg:w-1/2">
|
||||||
<div class="max-w-6xl mx-auto space-y-6">
|
<% if @weather_art.image.attached? %>
|
||||||
|
<%= image_tag @weather_art.image, class: "w-full h-full object-cover" %>
|
||||||
<!-- 返回导航 -->
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 主要内容 -->
|
|
||||||
<div class="card bg-base-200/80 backdrop-blur-md shadow-lg overflow-hidden"> <!-- 调整透明度和阴影 -->
|
|
||||||
<div class="grid lg:grid-cols-2 gap-6 items-center">
|
|
||||||
|
|
||||||
<!-- 图片区域 -->
|
|
||||||
<% if @weather_art.image.attached? %>
|
|
||||||
<figure class="relative lg:h-[30rem] h-auto overflow-hidden rounded-lg"> <!-- 添加圆角 -->
|
|
||||||
<div class="gallery" data-controller="photo-swipe-lightbox">
|
|
||||||
<div data-photo-swipe-lightbox-target="gallery" class="h-full">
|
|
||||||
<% blob = @weather_art.image_blob %>
|
|
||||||
<%= link_to rails_blob_path(blob),
|
|
||||||
data: {
|
|
||||||
pswp_src: rails_blob_url(blob),
|
|
||||||
pswp_caption: 'Weather Art',
|
|
||||||
pswp_width: 1792,
|
|
||||||
pswp_height: 1024
|
|
||||||
} do %>
|
|
||||||
<%= image_tag @weather_art.image, class: "object-cover w-full h-full transition-transform transform hover:scale-105 ease-in-out" %>
|
|
||||||
<%#= image_tag @weather_art.watermarked_variant.processed , class: "object-cover w-full h-full transition-transform transform hover:scale-105 ease-in-out" %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</figure>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<!-- 信息区域 -->
|
|
||||||
<div class="card-body p-8 lg:py-10 lg:px-12">
|
|
||||||
<div class="prose max-w-none">
|
|
||||||
<h1 class="font-display text-4xl md:text-5xl font-bold text-gradient mb-6">
|
|
||||||
<%= @weather_art.city.full_name %> Weather Art
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4 mb-6">
|
|
||||||
<div class="badge badge-lg badge-primary">
|
|
||||||
<%= @weather_art.weather_date.strftime("%B %d, %Y") %>
|
|
||||||
</div>
|
|
||||||
<div class="badge badge-lg badge-secondary">
|
|
||||||
<%= @weather_art.weather_date.strftime("%H:%M") %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-2xl font-semibold mb-4">
|
|
||||||
<%= weather_description_icon(@weather_art.description) %>
|
|
||||||
<%= @weather_art.description %>
|
|
||||||
</h2>
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<%= render 'weather_stats', weather_art: @weather_art %> <!-- 使用局部渲染 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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 %>
|
<% end %>
|
||||||
</div>
|
</figure>
|
||||||
|
<div class="card-body lg:w-1/2">
|
||||||
|
<h2 class="card-title"><%= @weather_art.description %></h2>
|
||||||
|
|
||||||
|
<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="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>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3 class="font-bold">AI Prompt:</h3>
|
||||||
|
<p class="text-sm"><%= @weather_art.prompt %></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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
|
config.cache_store = :memory_store
|
||||||
|
|
||||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
# Store uploaded files on the local file system (see config/storage.yml for options).
|
||||||
# config.active_storage.service = :local
|
config.active_storage.service = :local
|
||||||
config.active_storage.service = :amazon_dev
|
|
||||||
|
|
||||||
# Don't care if the mailer can't send.
|
# Don't care if the mailer can't send.
|
||||||
config.action_mailer.raise_delivery_errors = false
|
config.action_mailer.raise_delivery_errors = false
|
||||||
|
@ -22,7 +22,7 @@ Rails.application.configure do
|
|||||||
# config.asset_host = "http://assets.example.com"
|
# config.asset_host = "http://assets.example.com"
|
||||||
|
|
||||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
# 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.
|
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
|
||||||
config.assume_ssl = true
|
config.assume_ssl = true
|
||||||
@ -50,8 +50,7 @@ Rails.application.configure do
|
|||||||
config.cache_store = :solid_cache_store
|
config.cache_store = :solid_cache_store
|
||||||
|
|
||||||
# Replace the default in-process and non-durable queuing backend for Active Job.
|
# 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 = :solid_queue
|
||||||
config.active_job.queue_adapter = :sidekiq
|
|
||||||
config.solid_queue.connects_to = { database: { writing: :queue } }
|
config.solid_queue.connects_to = { database: { writing: :queue } }
|
||||||
|
|
||||||
# Ignore bad email addresses and do not raise email delivery errors.
|
# 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,
|
# You can exclude possibly sensitive model attributes from being displayed,
|
||||||
# added to forms, or exported by default by ActiveAdmin
|
# 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
|
# == 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,
|
# 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
|
# note that it will be overwritten if you use your own mailer class
|
||||||
# with default "from" parameter.
|
# 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.
|
# Configure the class responsible to send e-mails.
|
||||||
# config.mailer = 'Devise::Mailer'
|
# config.mailer = 'Devise::Mailer'
|
||||||
@ -36,7 +36,7 @@ Devise.setup do |config|
|
|||||||
# Load and configure the ORM. Supports :active_record (default) and
|
# Load and configure the ORM. Supports :active_record (default) and
|
||||||
# :mongoid (bson_ext recommended) by default. Other ORMs may be
|
# :mongoid (bson_ext recommended) by default. Other ORMs may be
|
||||||
# available as additional gems.
|
# available as additional gems.
|
||||||
require "devise/orm/active_record"
|
require 'devise/orm/active_record'
|
||||||
|
|
||||||
# ==> Configuration for any authentication mechanism
|
# ==> Configuration for any authentication mechanism
|
||||||
# Configure which keys are used when authenticating a user. The default is
|
# 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.
|
# Configure which authentication keys should be case-insensitive.
|
||||||
# These keys will be downcased upon creating or modifying a user and when used
|
# These keys will be downcased upon creating or modifying a user and when used
|
||||||
# to authenticate or find a user. Default is :email.
|
# 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.
|
# Configure which authentication keys should have whitespace stripped.
|
||||||
# These keys will have whitespace before and after removed upon creating or
|
# 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.
|
# 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.
|
# 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
|
# 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
|
# Notice that if you are skipping storage for all authentication paths, you
|
||||||
# may want to disable generating routes to Devise's sessions controller by
|
# may want to disable generating routes to Devise's sessions controller by
|
||||||
# passing skip: :sessions to `devise_for` in your config/routes.rb
|
# 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
|
# By default, Devise cleans up the CSRF token on authentication to
|
||||||
# avoid CSRF token fixation attacks. This means that, when using AJAX
|
# 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: '澳大利亚'
|
|
@ -28,4 +28,4 @@
|
|||||||
# enabled: "ON"
|
# enabled: "ON"
|
||||||
|
|
||||||
en:
|
en:
|
||||||
hello: "Hello world"
|
hello: "Hello world"
|
||||||
|
@ -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
|
Rails.application.routes.draw do
|
||||||
root "home#index"
|
root "home#index"
|
||||||
|
|
||||||
resources :cities, only: [ :index, :show ] do
|
resources :cities, only: [ :index, :show ] do
|
||||||
resources :weather_arts, path: "weather", only: [ :show ], param: :slug
|
resources :weather_arts, only: [ :show ]
|
||||||
end
|
end
|
||||||
resources :arts, only: [ :index ]
|
|
||||||
|
|
||||||
# namespace :admin do
|
# namespace :admin do
|
||||||
# resources :cities
|
# resources :cities
|
||||||
@ -18,15 +15,8 @@ Rails.application.routes.draw do
|
|||||||
get "cities/index"
|
get "cities/index"
|
||||||
get "cities/show"
|
get "cities/show"
|
||||||
get "home/index"
|
get "home/index"
|
||||||
get "sitemaps/*path", to: "sitemaps#show", format: false
|
|
||||||
|
|
||||||
devise_for :admin_users, ActiveAdmin::Devise.config
|
devise_for :admin_users, ActiveAdmin::Devise.config
|
||||||
ActiveAdmin.routes(self)
|
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
|
# 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.
|
# 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
|
service: Disk
|
||||||
root: <%= Rails.root.join("storage") %>
|
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)
|
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
|
||||||
# amazon:
|
# amazon:
|
||||||
# service: S3
|
# service: S3
|
||||||
@ -17,21 +13,6 @@ build:
|
|||||||
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
|
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
|
||||||
# region: us-east-1
|
# region: us-east-1
|
||||||
# bucket: your_own_bucket-<%= Rails.env %>
|
# 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
|
# Remember not to checkin your GCS keyfile to a repository
|
||||||
# google:
|
# google:
|
||||||
|
@ -7,7 +7,7 @@ class CreateActiveAdminComments < ActiveRecord::Migration[8.0]
|
|||||||
t.references :author, polymorphic: true
|
t.references :author, polymorphic: true
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
add_index :active_admin_comments, [ :namespace ]
|
add_index :active_admin_comments, [:namespace]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.down
|
def self.down
|
||||||
|
@ -14,8 +14,8 @@ class CreateFriendlyIdSlugs < MIGRATION_CLASS
|
|||||||
t.string :scope
|
t.string :scope
|
||||||
t.datetime :created_at
|
t.datetime :created_at
|
||||||
end
|
end
|
||||||
add_index :friendly_id_slugs, [ :sluggable_type, :sluggable_id ]
|
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], 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, [:slug, :sluggable_type, :scope], length: {slug: 70, sluggable_type: 50, scope: 70}, unique: true
|
||||||
end
|
end
|
||||||
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
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user