Compare commits
175 Commits
Author | SHA1 | Date | |
---|---|---|---|
4dca074c6d | |||
b97ac8b91d | |||
ba8957e444 | |||
cd26313808 | |||
1790b2d50c | |||
a60f1bfe38 | |||
76a8275af6 | |||
31c2913ea6 | |||
884e1dfc9f | |||
5d1846e0a0 | |||
130a8983a1 | |||
d69a193e6d | |||
2c72e96977 | |||
378530cc1b | |||
536b97a7da | |||
f2951e2741 | |||
9417358625 | |||
|
31bd6fd74e | ||
|
e36448e6d7 | ||
|
11df3a31d1 | ||
|
2c3f8e1b8e | ||
9a745dcee0 | |||
09b3f06ad4 | |||
05449d1e7f | |||
905ec35fd8 | |||
0352923c5b | |||
bbf8dfc2e6 | |||
15d64b94b0 | |||
62bfe8888e | |||
42efca67c7 | |||
138d610c3a | |||
c332230709 | |||
f0f94de528 | |||
069b6d4a4f | |||
2a0226eb68 | |||
8cd4c50024 | |||
4020f89271 | |||
29de36f5fb | |||
2e438166ee | |||
bf2ff282bb | |||
ce5d09b621 | |||
c68fecf3fa | |||
16ee512b0c | |||
111fd85ebb | |||
59a3f792c6 | |||
9e666310cf | |||
cc74145033 | |||
3e713a9b26 | |||
263c85486c | |||
a895216bda | |||
75cdd69b9b | |||
70f977cae1 | |||
444c3e67bc | |||
ffbd201d62 | |||
eb06398308 | |||
93543c6db3 | |||
5bfa94bc82 | |||
a16b14c7ca | |||
f59955ca6e | |||
978cec359e | |||
6eca78da8d | |||
f918a42619 | |||
adb671e668 | |||
50321533f7 | |||
dd6cd0451d | |||
5f30e08a6e | |||
bf10e41c1e | |||
9d1ff31c53 | |||
155669866a | |||
8cacf2a9ff | |||
b0bdb72f8e | |||
cea07ccfea | |||
5c8308a991 | |||
5feaee4922 | |||
ead795266e | |||
1ca468f9af | |||
1f35664590 | |||
40631fe95b | |||
f7c5ae4ee7 | |||
742c94ced1 | |||
c37a93bcdf | |||
7ebf9cefae | |||
dd37e2835b | |||
4e1fb58abf | |||
5bc06007b2 | |||
84c224cf8d | |||
032ff0552a | |||
3203face6b | |||
8364d42759 | |||
9ce473dddb | |||
fe55437c96 | |||
ec3669249f | |||
f6270b1ad4 | |||
9dd7044a77 | |||
dd6bb9972c | |||
23fc14af59 | |||
fedb954d34 | |||
7612dd6bd9 | |||
b4af78aa77 | |||
b05cf10017 | |||
06a861c639 | |||
2cd23a6047 | |||
80a75d3fbb | |||
f477f205ab | |||
1f47ba59c9 | |||
6544f0247c | |||
a0516f731c | |||
18f751938f | |||
2759646145 | |||
665f6f29b6 | |||
bafb90f5fb | |||
f33fb4d2ba | |||
a4de04874d | |||
3a6d247451 | |||
b3089856c2 | |||
ccb48a387b | |||
e1f9118ead | |||
af95c2e55f | |||
91e62234b4 | |||
6eb8d10965 | |||
97d7930daa | |||
a15bc349a2 | |||
5fa49d97ca | |||
dffac6c665 | |||
c5101fb822 | |||
fd6292a81e | |||
c529f5fd7b | |||
c1fa16c690 | |||
2d5521c3dc | |||
799dfc18ed | |||
a2c75ba3c2 | |||
e9095ece6e | |||
c20ff296eb | |||
f74f34ce82 | |||
a6cea6b80d | |||
08c584b85b | |||
78722caeb9 | |||
2bcfea30ee | |||
607fc9e8b8 | |||
494ae40088 | |||
3748ea5215 | |||
fd910fb469 | |||
b5c40f2e13 | |||
2d81dd91e7 | |||
2ab495897d | |||
853a1d03ce | |||
d728d7f50e | |||
6e387d1a8c | |||
f7d295b41b | |||
da7fca139c | |||
9cb1467301 | |||
be7856935f | |||
e70763dbe0 | |||
f83600412b | |||
93b24ff50c | |||
c9aef3ddfe | |||
48f3da8913 | |||
a988c49ae0 | |||
1ab860c739 | |||
24cbf13a26 | |||
5bb91f16fd | |||
04595824e6 | |||
2b4275055c | |||
0f9b4efc80 | |||
ec2c92e6c9 | |||
54133b2f87 | |||
c11d10c86a | |||
ebaf7a3f34 | |||
|
e2e5e38a7d | ||
ac199af963 | |||
5fd9573cf2 | |||
34c05232ee | |||
d570f43f95 | |||
a533390356 | |||
38884bdaa7 |
48
.github/workflows/docker-dev.yml
vendored
Normal file
48
.github/workflows/docker-dev.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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
Normal file
51
.github/workflows/docker-main.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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,3 +37,6 @@
|
|||||||
!/app/assets/builds/.keep
|
!/app/assets/builds/.keep
|
||||||
|
|
||||||
/node_modules
|
/node_modules
|
||||||
|
.idea
|
||||||
|
|
||||||
|
public/sitemap.xml.gz
|
||||||
|
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@ -1,8 +0,0 @@
|
|||||||
# 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
4
.idea/misc.xml
generated
@ -1,4 +0,0 @@
|
|||||||
<?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
8
.idea/modules.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<?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
367
.idea/today_ai_weather.iml
generated
@ -1,367 +0,0 @@
|
|||||||
<?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
6
.idea/vcs.xml
generated
@ -1,6 +0,0 @@
|
|||||||
<?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 && \
|
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 libpq5 redis-tools && \
|
||||||
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 && \
|
apt-get install --no-install-recommends -y build-essential git node-gyp pkg-config python-is-python3 libpq-dev && \
|
||||||
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 SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
|
RUN RAILS_BUILD=1 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
|
chown -R rails:rails db log storage tmp public
|
||||||
USER 1000:1000
|
USER 1000:1000
|
||||||
|
|
||||||
# Entrypoint prepares the database.
|
# Entrypoint prepares the database.
|
||||||
|
29
Gemfile
29
Gemfile
@ -41,9 +41,28 @@ 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
|
||||||
@ -61,6 +80,10 @@ 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"
|
||||||
|
141
Gemfile.lock
141
Gemfile.lock
@ -84,10 +84,30 @@ 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)
|
||||||
@ -120,6 +140,7 @@ 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)
|
||||||
@ -127,11 +148,30 @@ 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)
|
||||||
@ -145,20 +185,29 @@ GEM
|
|||||||
has_scope (0.8.2)
|
has_scope (0.8.2)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
i18n (1.14.6)
|
httparty (0.22.0)
|
||||||
|
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.14.3)
|
irb (1.15.1)
|
||||||
|
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)
|
||||||
@ -189,7 +238,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.3)
|
language_server-protocol (3.17.0.4)
|
||||||
logger (1.6.5)
|
logger (1.6.5)
|
||||||
loofah (2.24.0)
|
loofah (2.24.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
@ -201,9 +250,17 @@ 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
|
||||||
@ -211,7 +268,7 @@ GEM
|
|||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.2)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-scp (4.0.0)
|
net-scp (4.1.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)
|
||||||
@ -219,21 +276,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.1-aarch64-linux-gnu)
|
nokogiri (1.18.2-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-aarch64-linux-musl)
|
nokogiri (1.18.2-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-arm-linux-gnu)
|
nokogiri (1.18.2-arm-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-arm-linux-musl)
|
nokogiri (1.18.2-arm-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-arm64-darwin)
|
nokogiri (1.18.2-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-x86_64-darwin)
|
nokogiri (1.18.2-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-x86_64-linux-gnu)
|
nokogiri (1.18.2-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-x86_64-linux-musl)
|
nokogiri (1.18.2-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)
|
||||||
@ -241,6 +298,10 @@ 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)
|
||||||
@ -250,11 +311,11 @@ GEM
|
|||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (6.0.1)
|
public_suffix (6.0.1)
|
||||||
puma (6.5.0)
|
puma (6.6.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.8)
|
rack (3.1.9)
|
||||||
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)
|
||||||
@ -299,6 +360,8 @@ 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)
|
||||||
@ -306,17 +369,17 @@ GEM
|
|||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.4.0)
|
rexml (3.4.0)
|
||||||
rubocop (1.70.0)
|
rubocop (1.71.1)
|
||||||
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.36.2, < 2.0)
|
rubocop-ast (>= 1.38.0, < 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.37.0)
|
rubocop-ast (1.38.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)
|
||||||
@ -324,7 +387,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.0)
|
rubocop-rails (2.29.1)
|
||||||
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)
|
||||||
@ -334,17 +397,39 @@ 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.27.0)
|
selenium-webdriver (4.28.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)
|
||||||
solid_cable (3.0.5)
|
sidekiq (7.3.8)
|
||||||
|
base64
|
||||||
|
connection_pool (>= 2.3.0)
|
||||||
|
logger
|
||||||
|
rack (>= 2.2.4)
|
||||||
|
redis-client (>= 0.22.2)
|
||||||
|
sidekiq-scheduler (5.0.6)
|
||||||
|
rufus-scheduler (~> 3.2)
|
||||||
|
sidekiq (>= 6, < 8)
|
||||||
|
tilt (>= 1.4.0, < 3)
|
||||||
|
sitemap_generator (6.3.0)
|
||||||
|
builder (~> 3.0)
|
||||||
|
solid_cable (3.0.7)
|
||||||
actioncable (>= 7.2)
|
actioncable (>= 7.2)
|
||||||
activejob (>= 7.2)
|
activejob (>= 7.2)
|
||||||
activerecord (>= 7.2)
|
activerecord (>= 7.2)
|
||||||
@ -353,7 +438,7 @@ GEM
|
|||||||
activejob (>= 7.2)
|
activejob (>= 7.2)
|
||||||
activerecord (>= 7.2)
|
activerecord (>= 7.2)
|
||||||
railties (>= 7.2)
|
railties (>= 7.2)
|
||||||
solid_queue (1.1.2)
|
solid_queue (1.1.3)
|
||||||
activejob (>= 7.1)
|
activejob (>= 7.1)
|
||||||
activerecord (>= 7.1)
|
activerecord (>= 7.1)
|
||||||
concurrent-ruby (>= 1.3.1)
|
concurrent-ruby (>= 1.3.1)
|
||||||
@ -383,6 +468,7 @@ 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)
|
||||||
@ -424,21 +510,34 @@ 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)
|
||||||
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,4 +1,5 @@
|
|||||||
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
|
||||||
@ -24,5 +25,4 @@ ActiveAdmin.register AdminUser do
|
|||||||
end
|
end
|
||||||
f.actions
|
f.actions
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
114
app/admin/ahoy_dashboard.rb
Normal file
114
app/admin/ahoy_dashboard.rb
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
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
|
||||||
|
table_for City.least_popular_active do
|
||||||
|
column("ID") { |city| city.id }
|
||||||
|
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.name+"/"+city.country.region.name }
|
||||||
|
# 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
|
||||||
|
table_for City.most_popular_inactive do
|
||||||
|
column("ID") { |city| city.id }
|
||||||
|
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.name+"/"+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
|
||||||
|
|
||||||
|
# columns do
|
||||||
|
# column do
|
||||||
|
# panel "城市排名" do
|
||||||
|
# table_for City.by_popularity 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|
|
||||||
|
# if city.active?
|
||||||
|
# button_to "停用",
|
||||||
|
# admin_ahoy_dashboard_toggle_city_status_path(city_id: city.id),
|
||||||
|
# method: :post,
|
||||||
|
# data: { confirm: "确定要停用 #{city.name} 吗?" }
|
||||||
|
# else
|
||||||
|
# button_to "激活",
|
||||||
|
# admin_ahoy_dashboard_toggle_city_status_path(city_id: city.id),
|
||||||
|
# method: :post,
|
||||||
|
# data: { confirm: "确定要激活 #{city.name} 吗?" }
|
||||||
|
# end
|
||||||
|
# }
|
||||||
|
# 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
|
32
app/admin/ahoy_events.rb
Normal file
32
app/admin/ahoy_events.rb
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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
|
34
app/admin/ahoy_management.rb
Normal file
34
app/admin/ahoy_management.rb
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# 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
|
36
app/admin/ahoy_visits.rb
Normal file
36
app/admin/ahoy_visits.rb
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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,4 +1,5 @@
|
|||||||
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])
|
||||||
@ -10,7 +11,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, :latitude, :longitude, :active, :priority, :timezone, :region, :last_weather_fetch, :last_image_generation, :slug
|
permit_params :name, :country_id, :latitude, :longitude, :active, :priority, :timezone, :last_weather_fetch, :last_image_generation, :slug
|
||||||
#
|
#
|
||||||
# or
|
# or
|
||||||
#
|
#
|
||||||
@ -24,31 +25,30 @@ ActiveAdmin.register City do
|
|||||||
selectable_column
|
selectable_column
|
||||||
id_column
|
id_column
|
||||||
column :name
|
column :name
|
||||||
column :slug
|
column :country
|
||||||
|
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, as: :String
|
f.input :country
|
||||||
f.input :latitude
|
f.input :latitude
|
||||||
f.input :longitude
|
f.input :longitude
|
||||||
f.input :priority
|
f.input :priority
|
||||||
f.input :timezone
|
f.input :timezone
|
||||||
f.input :region
|
|
||||||
f.input :last_weather_fetch
|
|
||||||
f.input :last_image_generation
|
|
||||||
end
|
end
|
||||||
f.actions
|
f.actions
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
42
app/admin/countries.rb
Normal file
42
app/admin/countries.rb
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
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,4 +1,5 @@
|
|||||||
# 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") }
|
||||||
|
|
||||||
@ -10,6 +11,43 @@ 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
|
||||||
|
|
||||||
|
# 一个事件列表面板
|
||||||
|
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
|
||||||
|
30
app/admin/regions.rb
Normal file
30
app/admin/regions.rb
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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
|
98
app/admin/sidekiq_jobs.rb
Normal file
98
app/admin/sidekiq_jobs.rb
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# 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,5 +1,10 @@
|
|||||||
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
|
||||||
#
|
#
|
||||||
@ -17,9 +22,11 @@ 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
|
:prompt, :image, :slug
|
||||||
|
|
||||||
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
|
||||||
@ -29,7 +36,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
|
||||||
@ -74,5 +81,4 @@ ActiveAdmin.register WeatherArt do
|
|||||||
end
|
end
|
||||||
f.actions
|
f.actions
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
BIN
app/assets/images/today_ai_weather_copyright_watermark1.png
Normal file
BIN
app/assets/images/today_ai_weather_copyright_watermark1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
@ -1,2 +0,0 @@
|
|||||||
//= require active_admin/base
|
|
||||||
import "@activeadmin/activeadmin";
|
|
@ -1,3 +1,4 @@
|
|||||||
|
@import "photoswipe/dist/photoswipe.css";
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
|
24
app/concerns/seo_concern.rb
Normal file
24
app/concerns/seo_concern.rb
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# 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,4 +1,74 @@
|
|||||||
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.
|
||||||
allow_browser versions: :modern
|
before_action :log_browser_info
|
||||||
|
# allow_browser versions: :modern
|
||||||
|
# allow_browser versions: :modern,
|
||||||
|
# patterns: [
|
||||||
|
# # 鸿蒙系统相关
|
||||||
|
# /OpenHarmony/, # 鸿蒙系统标识
|
||||||
|
# /ArkWeb\/[\d.]+/, # 鸿蒙浏览器内核
|
||||||
|
# /Mobile HuaweiBrowser/, # 华为浏览器(新格式)
|
||||||
|
# /HuaweiBrowser\/[\d.]+/, # 华为浏览器(旧格式)
|
||||||
|
#
|
||||||
|
# # 夸克浏览器(更宽松的匹配)
|
||||||
|
# /Quark[\s\/][\d.]+/, # 匹配 "Quark/7.4.6.681" 或 "Quark 7.4.6.681"
|
||||||
|
#
|
||||||
|
# /Mobile Safari/,
|
||||||
|
# /Chrome\/[\d.]+/,
|
||||||
|
# /Quark\/[\d.]+/,
|
||||||
|
# /HuaweiBrowser\/[\d.]+/,
|
||||||
|
# /MiuiBrowser\/[\d.]+/,
|
||||||
|
# /VivoBrowser\/[\d.]+/,
|
||||||
|
# /OppoBrowser\/[\d.]+/,
|
||||||
|
# /UCBrowser\/[\d.]+/,
|
||||||
|
# /QQBrowser\/[\d.]+/,
|
||||||
|
# /MicroMessenger\/[\d.]+/,
|
||||||
|
# /Alipay/,
|
||||||
|
# /BaiduBoxApp/,
|
||||||
|
# /baiduboxapp/i,
|
||||||
|
# /SogouMobile/,
|
||||||
|
# /Weibo/,
|
||||||
|
# /DingTalk/,
|
||||||
|
# /ToutiaoMicroApp/,
|
||||||
|
# /BytedanceWebview/,
|
||||||
|
# /ArkWeb/
|
||||||
|
# ],
|
||||||
|
# on_failure: ->(browser) {
|
||||||
|
# Rails.logger.warn <<~BROWSER_INFO
|
||||||
|
# Browser Blocked:
|
||||||
|
# User Agent: #{browser.ua}
|
||||||
|
# Name: #{browser.name}
|
||||||
|
# Version: #{browser.version}
|
||||||
|
# Platform: #{browser.platform.name}
|
||||||
|
# Device: #{browser.device.name}
|
||||||
|
# Mobile: #{browser.mobile?}
|
||||||
|
# Modern: #{browser.modern?}
|
||||||
|
# Bot: #{browser.bot?}
|
||||||
|
# BROWSER_INFO
|
||||||
|
# }
|
||||||
|
before_action :set_locale
|
||||||
|
after_action :track_action
|
||||||
|
|
||||||
|
def log_browser_info
|
||||||
|
# 构建详细的浏览器信息
|
||||||
|
Rails.logger.debug "User Agent: #{request.user_agent}"
|
||||||
|
|
||||||
|
# 如果是被拦截的浏览器,记录额外信息
|
||||||
|
# unless browser_allowed?
|
||||||
|
# Rails.logger.info "User Agent: #{request.user_agent}"
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def track_action
|
||||||
|
ahoy.track "Viewed Application", request.path_parameters
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_locale
|
||||||
|
I18n.locale = params[:locale] || I18n.default_locale
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
21
app/controllers/arts_controller.rb
Normal file
21
app/controllers/arts_controller.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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,9 +1,42 @@
|
|||||||
class CitiesController < ApplicationController
|
class CitiesController < ApplicationController
|
||||||
def index
|
def index
|
||||||
@cities = City.all.order(:name)
|
@regions = Region.includes(:countries).order(:name)
|
||||||
|
@cities = City.includes(:country, country: :region).order(:name)
|
||||||
|
|
||||||
|
if params[:region]
|
||||||
|
@current_region = Region.friendly.find(params[:region])
|
||||||
|
@cities = @cities.by_region(@current_region.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:country]
|
||||||
|
@current_country = Country.friendly.find(params[:country])
|
||||||
|
@cities = @cities.by_country(@current_country.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@cities = @cities.page(params[:page]).per(10)
|
||||||
|
|
||||||
|
set_meta_tags(
|
||||||
|
title: @current_region ? "Cities in #{@current_region.name}" : "Explore Cities",
|
||||||
|
description: "Discover weather art for cities #{@current_region ? "in #{@current_region.name}" : 'worldwide'}. Real-time AI-generated weather visualization.",
|
||||||
|
keywords: "#{@current_region&.name}, cities, weather art, AI visualization"
|
||||||
|
)
|
||||||
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,6 +1,11 @@
|
|||||||
class HomeController < ApplicationController
|
class HomeController < ApplicationController
|
||||||
def index
|
def index
|
||||||
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(6)
|
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(20).shuffle.last(10)
|
||||||
@featured_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(5)
|
@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
|
||||||
|
33
app/controllers/sitemaps_controller.rb
Normal file
33
app/controllers/sitemaps_controller.rb
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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,6 +1,36 @@
|
|||||||
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.find(params[:id])
|
@weather_art = @city.weather_arts.friendly.find(params[:slug])
|
||||||
|
|
||||||
|
@previous_weather_art = @city.weather_arts
|
||||||
|
.where("id < ?", @weather_art.id)
|
||||||
|
.order(id: :desc)
|
||||||
|
.first
|
||||||
|
|
||||||
|
@next_weather_art = @city.weather_arts
|
||||||
|
.where("id > ?", @weather_art.id)
|
||||||
|
.order(id: :asc)
|
||||||
|
.first
|
||||||
|
|
||||||
|
ahoy.track "View Weather Art", {
|
||||||
|
weather_art_id: @weather_art.id,
|
||||||
|
city_id: @weather_art.city_id,
|
||||||
|
event_type: "weather_art_view"
|
||||||
|
}
|
||||||
|
ahoy.track "View City", {
|
||||||
|
city_id: @city.id,
|
||||||
|
name: @city.name,
|
||||||
|
event_type: "city_view"
|
||||||
|
}
|
||||||
|
|
||||||
|
set_meta_tags(
|
||||||
|
title: "#{@city.name} Weather Art - #{@weather_art.weather_date.strftime('%B %d, %Y')}",
|
||||||
|
description: "#{@city.name}'s weather visualized through AI art. #{@weather_art.description} at #{@weather_art.temperature}°C.",
|
||||||
|
keywords: "#{@city.name}, weather art, #{@weather_art.description}, AI visualization",
|
||||||
|
og: {
|
||||||
|
image: @weather_art.image.attached? ? url_for(@weather_art.image) : nil
|
||||||
|
}
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,2 +1,24 @@
|
|||||||
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
|
||||||
|
2
app/helpers/arts_helper.rb
Normal file
2
app/helpers/arts_helper.rb
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
module ArtsHelper
|
||||||
|
end
|
2
app/helpers/sitemaps_helper.rb
Normal file
2
app/helpers/sitemaps_helper.rb
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
module SitemapsHelper
|
||||||
|
end
|
@ -1,2 +1,51 @@
|
|||||||
module WeatherArtsHelper
|
module WeatherArtsHelper
|
||||||
|
def weather_description_icon(description)
|
||||||
|
case description&.downcase
|
||||||
|
when /rain/
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||||
|
</svg>'.html_safe
|
||||||
|
when /cloud/
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||||
|
</svg>'.html_safe
|
||||||
|
when /sun|clear/
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||||
|
</svg>'.html_safe
|
||||||
|
else
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>'.html_safe
|
||||||
|
end
|
||||||
|
end
|
||||||
|
def weather_stat_icon(type)
|
||||||
|
case type
|
||||||
|
when "temperature"
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>'.html_safe
|
||||||
|
when "wind"
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||||
|
</svg>'.html_safe
|
||||||
|
when "humidity"
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
||||||
|
</svg>'.html_safe
|
||||||
|
when "visibility"
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>'.html_safe
|
||||||
|
when "pressure"
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>'.html_safe
|
||||||
|
when "cloud"
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||||
|
</svg>'.html_safe
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
6
app/javascript/active_admin.js
Normal file
6
app/javascript/active_admin.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
import './add_jquery'
|
||||||
|
import "jquery/dist/jquery"
|
||||||
|
import "jquery-ui/dist/jquery-ui"
|
||||||
|
import "jquery-ujs"
|
||||||
|
import "@activeadmin/activeadmin"
|
4
app/javascript/add_jquery.js
Normal file
4
app/javascript/add_jquery.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import jquery from 'jquery'
|
||||||
|
import $ from 'jquery'
|
||||||
|
window.jQuery = jquery
|
||||||
|
window.$ = $
|
@ -1,3 +1,10 @@
|
|||||||
// 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,3 +6,9 @@ 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")
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
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
|
|
14
app/models/ahoy/event.rb
Normal file
14
app/models/ahoy/event.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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
|
10
app/models/ahoy/visit.rb
Normal file
10
app/models/ahoy/visit.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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,13 +1,96 @@
|
|||||||
class City < ApplicationRecord
|
class City < ApplicationRecord
|
||||||
extend FriendlyId
|
extend FriendlyId
|
||||||
friendly_id :name, use: :slugged
|
friendly_id :slug_candidates, 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
|
||||||
@ -17,7 +100,32 @@ class City < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.ransackable_attributes(auth_object = nil)
|
def self.ransackable_attributes(auth_object = nil)
|
||||||
["active", "country", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at"]
|
[ "active", "country_id", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at" ]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def last_weather_fetch
|
||||||
|
# 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
|
||||||
|
26
app/models/country.rb
Normal file
26
app/models/country.rb
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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
|
28
app/models/region.rb
Normal file
28
app/models/region.rb
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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,11 +1,41 @@
|
|||||||
class WeatherArt < ApplicationRecord
|
class WeatherArt < ApplicationRecord
|
||||||
belongs_to :city
|
extend FriendlyId
|
||||||
|
friendly_id :weather_date, use: :slugged
|
||||||
|
|
||||||
|
belongs_to :city
|
||||||
has_one_attached :image
|
has_one_attached :image
|
||||||
|
has_one_attached :image_with_watermark
|
||||||
|
|
||||||
|
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id
|
||||||
|
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
|
||||||
|
|
||||||
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
|
||||||
@ -13,4 +43,16 @@ class WeatherArt < ApplicationRecord
|
|||||||
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
|
||||||
|
64
app/services/ai_service.rb
Normal file
64
app/services/ai_service.rb
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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
|
38
app/services/weather_service.rb
Normal file
38
app/services/weather_service.rb
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# 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
|
172
app/views/arts/index.html.erb
Normal file
172
app/views/arts/index.html.erb
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<!-- 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>
|
126
app/views/cities/_city.html.erb
Normal file
126
app/views/cities/_city.html.erb
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<!-- 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,18 +1,126 @@
|
|||||||
<div class="space-y-8">
|
<!-- app/views/cities/index.html.erb -->
|
||||||
<h1 class="text-3xl font-bold">Cities</h1>
|
<div class="min-h-screen">
|
||||||
|
<!-- 页面标题和背景 -->
|
||||||
|
<% featured_art = WeatherArt.includes(:city).joins(:image_attachment).order(created_at: :desc).first %>
|
||||||
|
<div class="relative bg-base-100">
|
||||||
|
<!-- 背景图像和渐变 -->
|
||||||
|
<% if featured_art&.image&.attached? %>
|
||||||
|
<div class="absolute inset-0 h-[60vh] overflow-hidden">
|
||||||
|
<%= image_tag featured_art.image,
|
||||||
|
class: "w-full h-full object-cover object-center" %>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-b from-base-100/40 via-base-100/80 to-base-100"></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="relative pt-24 pb-32">
|
||||||
<% @cities.each do |city| %>
|
<div class="container mx-auto px-4">
|
||||||
<div class="card bg-base-200">
|
<div class="max-w-3xl mx-auto text-center space-y-6">
|
||||||
<div class="card-body">
|
<h1 class="text-5xl md:text-6xl font-display font-bold leading-tight">
|
||||||
<h2 class="card-title"><%= city.name %></h2>
|
Explore Cities
|
||||||
<p>Latitude: <%= city.latitude %></p>
|
</h1>
|
||||||
<p>Longitude: <%= city.longitude %></p>
|
<p class="text-xl md:text-2xl text-base-content/70 font-light max-w-2xl mx-auto">
|
||||||
<div class="card-actions justify-end">
|
Discover AI-generated weather art from cities around the world
|
||||||
<%= link_to "View Weather Art", city_path(city), class: "btn btn-primary" %>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
<!-- 特色图片信息 -->
|
||||||
|
<% 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>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sticky top-16 z-20 bg-base-100/95 backdrop-blur-sm border-b border-base-200">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="py-3 flex items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-ghost gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<%= @current_region&.name || 'All Regions' %>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
|
||||||
|
<li>
|
||||||
|
<%= link_to cities_path,
|
||||||
|
class: "#{@current_region ? '' : 'active'}" do %>
|
||||||
|
All Regions
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<div class="divider my-1"></div>
|
||||||
|
<% @regions.each do |region| %>
|
||||||
|
<li>
|
||||||
|
<%= link_to region.name,
|
||||||
|
cities_path(region: region.slug),
|
||||||
|
class: "#{@current_region == region ? 'active' : ''}" %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if @current_region %>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-ghost gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
|
||||||
|
</svg>
|
||||||
|
<%= @current_country&.name || "All Countries" %>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
|
||||||
|
<li>
|
||||||
|
<%= link_to "All in #{@current_region.name}",
|
||||||
|
cities_path(region: @current_region.slug),
|
||||||
|
class: "#{@current_country ? '' : 'active'}" %>
|
||||||
|
</li>
|
||||||
|
<div class="divider my-1"></div>
|
||||||
|
<% @current_region.countries.order(:name).each do |country| %>
|
||||||
|
<li>
|
||||||
|
<%= link_to country.name,
|
||||||
|
cities_path(region: @current_region.slug, country: country.slug),
|
||||||
|
class: "#{@current_country == country ? 'active' : ''}" %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-base-content/70">
|
||||||
|
<%= @cities.count %> <%= 'city'.pluralize(@cities.count) %>
|
||||||
|
<% if @current_country %>
|
||||||
|
in <%= @current_country.name %>
|
||||||
|
<% elsif @current_region %>
|
||||||
|
in <%= @current_region.name %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="container mx-auto px-4 pb-16">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
<%= render partial: 'city', collection: @cities %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= render 'shared/pagination',
|
||||||
|
collection: @cities,
|
||||||
|
collection_name: 'cities' %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,49 +1,149 @@
|
|||||||
<div class="space-y-8">
|
<div class="relative min-h-screen bg-base-200">
|
||||||
<div class="flex justify-between items-center">
|
<!-- 背景效果 -->
|
||||||
<h1 class="text-3xl font-bold"><%= @city.name %></h1>
|
<% if @city.latest_weather_art&.image&.attached? %>
|
||||||
<%= link_to "Back to Cities", cities_path, class: "btn btn-ghost" %>
|
<div class="fixed inset-0 -z-10">
|
||||||
|
<%= image_tag @city.latest_weather_art.image,
|
||||||
|
class: "absolute w-full h-full object-cover scale-110 filter blur-2xl opacity-25" %>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-b from-base-200/90 to-base-200/70 backdrop-blur-md"></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<!-- 主要内容 -->
|
||||||
|
<div class="relative z-10">
|
||||||
|
<!-- 返回导航 -->
|
||||||
|
<div class="container mx-auto px-4 py-6">
|
||||||
|
<%= link_to cities_path,
|
||||||
|
class: "btn btn-ghost btn-lg gap-2 bg-base-100/50 backdrop-blur-sm hover:bg-base-100/70 transition-all duration-300" do %>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
Back to Cities
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats shadow">
|
<!-- 城市信息头部 -->
|
||||||
<div class="stat">
|
<div class="container mx-auto px-4 mb-12">
|
||||||
<div class="stat-title">Latitude</div>
|
<div class="max-w-4xl mx-auto text-center space-y-6">
|
||||||
<div class="stat-value"><%= @city.latitude %></div>
|
<h1 class="text-4xl md:text-6xl font-display font-bold">
|
||||||
|
<span class="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||||
|
<%= @city.localized_name %>
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap justify-center items-center gap-3">
|
||||||
|
<div class="badge badge-lg badge-primary gap-2">
|
||||||
|
<%= @city.country.name %>, <%= @city.region %>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="badge badge-lg badge-secondary gap-2">
|
||||||
<div class="stat-title">Longitude</div>
|
<%= @city.timezone.present? ? Time.current.in_time_zone(@city.timezone).strftime("%Y-%m-%d %H:%M") : "Timezone undefined" %>
|
||||||
<div class="stat-value"><%= @city.longitude %></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<!-- 主要统计信息 -->
|
||||||
<h2 class="text-2xl font-bold">Weather Art History</h2>
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-8">
|
||||||
|
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<%= weather_stat_icon("temperature") %>
|
||||||
|
<div class="stat-title font-medium">Latest Weather</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-value text-2xl"><%= @city.latest_weather_art&.temperature %>°C</div>
|
||||||
|
<div class="stat-desc mt-1"><%= @city.latest_weather_art&.description %></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<%= weather_stat_icon("location") %>
|
||||||
|
<div class="stat-title font-medium">Coordinates</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-value text-xl">
|
||||||
|
<%= @city.latitude %>°N,
|
||||||
|
<%= @city.longitude %>°E
|
||||||
|
</div>
|
||||||
|
<div class="stat-desc mt-1">Geographical Location</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<%= weather_stat_icon("history") %>
|
||||||
|
<div class="stat-title font-medium">Records</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-value text-2xl"><%= @city.weather_arts.count %></div>
|
||||||
|
<div class="stat-desc mt-1">Total Weather Arts</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 天气艺术历史记录 -->
|
||||||
|
<div class="container mx-auto px-4 pb-16">
|
||||||
|
<div class="max-w-7xl mx-auto space-y-8">
|
||||||
|
<!-- 标题和更新时间 -->
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||||
|
<h2 class="text-3xl font-display font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||||
|
Weather Art History
|
||||||
|
</h2>
|
||||||
|
<div class="card bg-base-100/80 backdrop-blur-sm shadow-lg p-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span class="font-medium">Last Updated:</span>
|
||||||
|
<span class="text-base-content/70">
|
||||||
|
<%= time_ago_in_words(@city.last_weather_fetch) if @city.last_weather_fetch %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 天气艺术卡片网格 -->
|
||||||
<div class="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-6">
|
||||||
<% @city.weather_arts.order(weather_date: :desc).each do |art| %>
|
<% @city.weather_arts.order(weather_date: :desc).each do |art| %>
|
||||||
<div class="card bg-base-200">
|
<div class="card bg-base-100/80 backdrop-blur-sm shadow-xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1">
|
||||||
<figure>
|
<figure class="relative aspect-video overflow-hidden">
|
||||||
<% if art.image.attached? %>
|
<% if art.image.attached? %>
|
||||||
<%= image_tag art.image, class: "w-full h-48 object-cover" %>
|
<%= 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 %>
|
<% end %>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="card-title"><%= art.weather_date.strftime("%Y-%m-%d") %></h3>
|
<h3 class="card-title font-display">
|
||||||
<p><%= art.description %></p>
|
<%= weather_description_icon(art.description) %>
|
||||||
<div class="stats stats-vertical shadow">
|
<%= art.description %>
|
||||||
<div class="stat">
|
</h3>
|
||||||
<div class="stat-title">Temperature</div>
|
|
||||||
<div class="stat-value"><%= art.temperature %>°C</div>
|
<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>
|
||||||
<div class="stat">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<div class="stat-title">Humidity</div>
|
<%= weather_stat_icon("wind") %>
|
||||||
<div class="stat-value"><%= art.humidity %>%</div>
|
<span>Wind: <%= art.wind_scale %></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions justify-end">
|
|
||||||
<%= link_to "View Details", city_weather_art_path(@city, art), class: "btn btn-primary" %>
|
<%= link_to city_weather_art_path(@city, art),
|
||||||
</div>
|
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 %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,46 +1,68 @@
|
|||||||
<div class="space-y-8">
|
<div>
|
||||||
<!-- 头部标题 -->
|
<!-- 首屏展示区 -->
|
||||||
<div class="text-center space-y-4">
|
<section class="h-screen-90 relative overflow-hidden">
|
||||||
<h1 class="text-4xl font-bold">AI Weather Art</h1>
|
<% if @featured_arts.first&.image&.attached? %>
|
||||||
<p class="text-xl text-base-content/70">Discover the beauty of weather through AI-generated art</p>
|
<div class="absolute inset-0">
|
||||||
|
<%= image_tag @featured_arts.first.image, class: "w-full h-full object-cover" %>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-r from-base-100/90 to-base-100/50"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<!-- 轮播图 -->
|
<div class="container mx-auto px-4 h-full flex items-center relative">
|
||||||
<div class="carousel w-full rounded-box">
|
<div class="max-w-2xl space-y-6">
|
||||||
<% WeatherArt.last(5).each_with_index do |art, index| %>
|
<h1 class="text-5xl md:text-6xl font-display font-bold leading-tight">
|
||||||
<div id="slide<%= index %>" class="carousel-item relative w-full">
|
Where Weather Meets<br>Artificial Intelligence
|
||||||
<% if art.image.attached? %>
|
</h1>
|
||||||
<%= image_tag art.image, class: "w-full aspect-video object-cover" %>
|
<p class="text-xl text-base-content/70 font-sans">
|
||||||
<% end %>
|
Experience weather through the lens of AI-generated art,
|
||||||
<div class="absolute flex justify-between transform -translate-y-1/2 left-5 right-5 top-1/2">
|
bringing a new perspective to daily meteorological phenomena.
|
||||||
<a href="#slide<%= index == 0 ? 4 : index - 1 %>" class="btn btn-circle">❮</a>
|
</p>
|
||||||
<a href="#slide<%= index == 4 ? 0 : index + 1 %>" class="btn btn-circle">❯</a>
|
<%= link_to "Explore Cities", cities_path,
|
||||||
|
class: "btn btn-primary btn-lg mt-8 font-sans" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
</section>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 最新天气艺术 -->
|
<!-- 最新天气艺术 -->
|
||||||
<div class="space-y-4">
|
<section class="container mx-auto px-4 py-16 space-y-12">
|
||||||
<h2 class="text-2xl font-bold">Latest Weather Art</h2>
|
<h2 class="text-3xl font-display font-bold text-center">Shuffle Latest Weather Art</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
<% WeatherArt.last(6).each do |art| %>
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
<div class="card bg-base-200">
|
<% @latest_arts.each do |art| %>
|
||||||
<figure>
|
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow duration-300">
|
||||||
|
<figure class="relative aspect-[4/3] overflow-hidden">
|
||||||
<% if art.image.attached? %>
|
<% if art.image.attached? %>
|
||||||
<%= image_tag art.image, class: "w-full h-48 object-cover" %>
|
<%= image_tag art.image, class: "w-full h-full object-cover transform hover:scale-105 transition-transform duration-500" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</figure>
|
</figure>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="card-title"><%= art.city.name %></h3>
|
<div class="flex justify-between items-start">
|
||||||
<p><%= art.weather_date.strftime("%Y-%m-%d") %></p>
|
<div>
|
||||||
<p><%= art.description %></p>
|
<h3 class="card-title font-display"><%= art.city.name %></h3>
|
||||||
<div class="card-actions justify-end">
|
<p class="text-base-content/70">
|
||||||
<%= link_to "View Details", city_weather_art_path(art.city, art), class: "btn btn-primary" %>
|
<%= art.weather_date.strftime("%B %d, %Y") %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="text-2xl font-bold"><%= art.temperature %>°C</div>
|
||||||
|
<div class="text-sm text-base-content/70"><%= art.description %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions justify-end mt-4">
|
||||||
|
<%= link_to "View Details", city_weather_art_path(art.city, art),
|
||||||
|
class: "btn btn-primary btn-outline" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
11
app/views/kaminari/_first_page.html.erb
Normal file
11
app/views/kaminari/_first_page.html.erb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<%# 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>
|
8
app/views/kaminari/_gap.html.erb
Normal file
8
app/views/kaminari/_gap.html.erb
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<%# 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>
|
11
app/views/kaminari/_last_page.html.erb
Normal file
11
app/views/kaminari/_last_page.html.erb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<%# 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>
|
11
app/views/kaminari/_next_page.html.erb
Normal file
11
app/views/kaminari/_next_page.html.erb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<%# 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>
|
12
app/views/kaminari/_page.html.erb
Normal file
12
app/views/kaminari/_page.html.erb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<%# 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>
|
25
app/views/kaminari/_paginator.html.erb
Normal file
25
app/views/kaminari/_paginator.html.erb
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<%# 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 -%>
|
11
app/views/kaminari/_prev_page.html.erb
Normal file
11
app/views/kaminari/_prev_page.html.erb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<%# 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,10 +1,23 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<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 %>
|
||||||
|
|
||||||
@ -20,28 +33,66 @@
|
|||||||
<%# 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">
|
<body class="min-h-screen bg-base-100 font-sans">
|
||||||
<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 "AI Weather Art", root_path, class: "btn btn-ghost normal-case text-xl" %>
|
<%= link_to root_path, class: "text-2xl font-display font-bold hover:text-primary transition-colors" do %>
|
||||||
|
Today AI Weather
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-none">
|
<div class="flex-none">
|
||||||
<%= link_to "Cities", cities_path, class: "btn btn-ghost normal-case" %>
|
<%= link_to "Cities", cities_path, class: "btn btn-ghost font-sans" %>
|
||||||
|
<%= 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">
|
<!-- 页脚 -->
|
||||||
<div>
|
<footer class="footer footer-center p-8 bg-base-200 text-base-content">
|
||||||
<p>Copyright © 2024 - All rights reserved by AI Weather Art</p>
|
<div class="container mx-auto flex flex-col gap-2">
|
||||||
|
<div id="busuanzi_container" class="text-xs opacity-70">
|
||||||
|
<div class="space-x-2">
|
||||||
|
<span>Page Views: <span id="busuanzi_page_pv"></span></span>
|
||||||
|
<span>|</span>
|
||||||
|
<span>Page Visitors: <span id="busuanzi_page_uv"></span></span>
|
||||||
|
<span>|</span>
|
||||||
|
<span>Total Views: <span id="busuanzi_site_pv"></span></span>
|
||||||
|
<span>|</span>
|
||||||
|
<span>Total Visitors: <span id="busuanzi_site_uv"></span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="font-display opacity-80">
|
||||||
|
Copyright © 2024 - All rights reserved by AI Weather Art
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
86
app/views/shared/_pagination.html.erb
Normal file
86
app/views/shared/_pagination.html.erb
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<%# 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 %>
|
37
app/views/weather_arts/_weather_stats.html.erb
Normal file
37
app/views/weather_arts/_weather_stats.html.erb
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<%# Partial _weather_stats.html.erb %>
|
||||||
|
|
||||||
|
<div class="stat bg-gradient-to-br from-primary/10 to-primary/20 hover:from-primary hover:to-primary/30 p-4 rounded-lg">
|
||||||
|
<div class="stat-title font-medium text-base">Temperature</div>
|
||||||
|
<div class="stat-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,46 +1,121 @@
|
|||||||
<div class="space-y-8">
|
<% content_for :head do %>
|
||||||
<div class="flex justify-between items-center">
|
<script type="application/ld+json">
|
||||||
<h1 class="text-3xl font-bold"><%= @weather_art.city.name %> - <%= @weather_art.weather_date.strftime("%Y-%m-%d") %></h1>
|
<%= weather_art_schema(@weather_art) %>
|
||||||
<%= link_to "Back to City", city_path(@weather_art.city), class: "btn btn-ghost" %>
|
</script>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card lg:card-side bg-base-200 shadow-xl">
|
|
||||||
<figure class="lg:w-1/2">
|
|
||||||
<% if @weather_art.image.attached? %>
|
|
||||||
<%= image_tag @weather_art.image, class: "w-full h-full object-cover" %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<div class="relative min-h-screen bg-white"> <!-- 使用更明快的背景颜色 -->
|
||||||
|
<div class="container mx-auto px-4 pt-12 pb-16">
|
||||||
|
<div class="max-w-6xl mx-auto space-y-6">
|
||||||
|
|
||||||
|
<!-- 返回导航 -->
|
||||||
|
<div class="flex items-center">
|
||||||
|
<%= link_to city_path(@weather_art.city),
|
||||||
|
class: "btn btn-ghost btn-md gap-2 bg-base-200 hover:bg-base-300 transition-all duration-300" do %>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
Back to <%= @weather_art.city.name %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主要内容 -->
|
||||||
|
<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>
|
</figure>
|
||||||
<div class="card-body lg:w-1/2">
|
<% end %>
|
||||||
<h2 class="card-title"><%= @weather_art.description %></h2>
|
|
||||||
|
|
||||||
<div class="stats stats-vertical shadow">
|
<!-- 信息区域 -->
|
||||||
<div class="stat">
|
<div class="card-body p-8 lg:py-10 lg:px-12">
|
||||||
<div class="stat-title">Temperature</div>
|
<div class="prose max-w-none">
|
||||||
<div class="stat-value"><%= @weather_art.temperature %>°C</div>
|
<h1 class="font-display text-4xl md:text-5xl font-bold text-gradient mb-6">
|
||||||
<div class="stat-desc">Feels like <%= @weather_art.feeling_temp %>°C</div>
|
<%= @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>
|
||||||
|
<div class="badge badge-lg badge-secondary">
|
||||||
<div class="stat">
|
<%= @weather_art.weather_date.strftime("%H:%M") %>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<h2 class="text-2xl font-semibold mb-4">
|
||||||
<h3 class="font-bold">AI Prompt:</h3>
|
<%= weather_description_icon(@weather_art.description) %>
|
||||||
<p class="text-sm"><%= @weather_art.prompt %></p>
|
<%= @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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI Prompt -->
|
||||||
|
<div class="bg-primary/10 backdrop-blur-md p-6 rounded-lg border border-primary/20">
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
<h3 class="font-display font-bold text-lg">AI Prompt</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-base-content/80 leading-relaxed">
|
||||||
|
<%= @weather_art.prompt %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上一个和下一个导航 -->
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between items-center gap-4 mt-8">
|
||||||
|
<% if @previous_weather_art %>
|
||||||
|
<%= link_to city_weather_art_path(@city, @previous_weather_art),
|
||||||
|
class: "btn btn-outline btn-primary w-full sm:w-auto flex items-center justify-center gap-2" do %>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
Previous Weather Art
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if @next_weather_art %>
|
||||||
|
<%= link_to city_weather_art_path(@city, @next_weather_art),
|
||||||
|
class: "btn btn-outline btn-primary w-full sm:w-auto flex items-center justify-center gap-2" do %>
|
||||||
|
Next Weather Art
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if @previous_weather_art.nil? && @next_weather_art.nil? %>
|
||||||
|
<div class="text-center text-base-content/70 py-4">
|
||||||
|
No more Weather Arts available
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
53
app/workers/add_watermark_to_weather_art_worker.rb
Normal file
53
app/workers/add_watermark_to_weather_art_worker.rb
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
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
|
||||||
|
nil 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
|
32
app/workers/batch_generate_weather_arts_worker.rb
Normal file
32
app/workers/batch_generate_weather_arts_worker.rb
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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
|
32
app/workers/clean_ahoy_data_worker.rb
Normal file
32
app/workers/clean_ahoy_data_worker.rb
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# 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
|
68
app/workers/generate_weather_art_worker.rb
Normal file
68
app/workers/generate_weather_art_worker.rb
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
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
|
58
app/workers/refresh_sitemap_worker.rb
Normal file
58
app/workers/refresh_sitemap_worker.rb
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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
Normal file
52
compose.yaml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
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 @@
|
|||||||
LCyDSpt/QvabmfGLE8GlLR1UPEz7QPdFj21uutpnQtJgwAbzfdWf+wh45UH97SLpnbfdy9uP0UT08j8t1bnn2danGnXDgdrhlb+NZeAGUjrV3OoNbv2ywktK6ENFNnmGyztKSrvj5Jnc8N3A+WE202cO6EpAmedhwJ1kl4WF/n2XHYbVrU/9+im91l8Meux+fNNheqXQ3MqpjQmtzN6XBidJFIVnvc2hX8c8XPkiWgrnPt0MJnoXgAUc4r4i9Jy3cXRYbidK+tKvYPPqQnI5b90vttM2bbxIKB/4Z2SdlcZNZK6UcEz5TTbVFF6utYTz8vjjsjFp5JpdzVX4q/Gc9zNPe8Wpkdz8rkoAwQ2OpJKB0T+AYNAuM+0XNycpx4N4AkOozJqK1Gd6OMlicmgIWTqUgWqljLMxbI+1WrEcZTAbeUfe9UajEfYuxHE8ksRFAnMUNCFpOJN348vRoxBv3fvan6rAnQMs2F+P9anRb8jPkJAzOFDct3ej--hNhQcVOfe5QH/0IE--Tk/qRKxDaUSCeZMW5hBwaA==
|
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==
|
@ -29,7 +29,8 @@ 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 = :local
|
config.active_storage.service = ENV["RAILS_BUILD"] ? :build : :amazon
|
||||||
|
|
||||||
# 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,7 +50,8 @@ 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.
|
||||||
|
14
config/initializers/ahoy.rb
Normal file
14
config/initializers/ahoy.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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
|
19
config/initializers/aws.rb
Normal file
19
config/initializers/aws.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
|
14
config/initializers/kaminari_config.rb
Normal file
14
config/initializers/kaminari_config.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# 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
|
52
config/initializers/meta_tags.rb
Normal file
52
config/initializers/meta_tags.rb
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# 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
|
9
config/initializers/schedule_tasks.rb
Normal file
9
config/initializers/schedule_tasks.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# 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
|
20
config/initializers/sidekiq.rb
Normal file
20
config/initializers/sidekiq.rb
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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
|
118
config/locales/cities.en.yml
Normal file
118
config/locales/cities.en.yml
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
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'
|
||||||
|
|
118
config/locales/cities.zh-CN.yml
Normal file
118
config/locales/cities.zh-CN.yml
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
|
||||||
|
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: '河内'
|
55
config/locales/countries.en.yml
Normal file
55
config/locales/countries.en.yml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
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'
|
55
config/locales/countries.zh-CN.yml
Normal file
55
config/locales/countries.zh-CN.yml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
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: '澳大利亚'
|
15
config/locales/regions.en.yml
Normal file
15
config/locales/regions.en.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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'
|
15
config/locales/regions.zh-CN.yml
Normal file
15
config/locales/regions.zh-CN.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
zh-CN:
|
||||||
|
regions:
|
||||||
|
AS: '亚洲'
|
||||||
|
SA: '南亚'
|
||||||
|
SEA: '东南亚'
|
||||||
|
EA: '东亚'
|
||||||
|
ME: '中东'
|
||||||
|
AF: '非洲'
|
||||||
|
NA: '北非'
|
||||||
|
SSA: '撒哈拉以南非洲'
|
||||||
|
EU: '欧洲'
|
||||||
|
NAM: '北美洲'
|
||||||
|
SAM: '南美洲'
|
||||||
|
CAM: '中美洲'
|
||||||
|
OC: '大洋洲'
|
@ -1,9 +1,12 @@
|
|||||||
|
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, only: [ :show ]
|
resources :weather_arts, path: "weather", only: [ :show ], param: :slug
|
||||||
end
|
end
|
||||||
|
resources :arts, only: [ :index ]
|
||||||
|
|
||||||
# namespace :admin do
|
# namespace :admin do
|
||||||
# resources :cities
|
# resources :cities
|
||||||
@ -15,8 +18,15 @@ 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.
|
||||||
|
23
config/schedule.rb
Normal file
23
config/schedule.rb
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# 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
|
18
config/sidekiq_scheduler.yml
Normal file
18
config/sidekiq_scheduler.yml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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
|
58
config/sitemap.rb
Normal file
58
config/sitemap.rb
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# 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,6 +6,10 @@ 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
|
||||||
@ -13,6 +17,21 @@ local:
|
|||||||
# 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:
|
||||||
|
13
db/migrate/20250121015832_create_regions.rb
Normal file
13
db/migrate/20250121015832_create_regions.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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
|
14
db/migrate/20250121015841_create_countries.rb
Normal file
14
db/migrate/20250121015841_create_countries.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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
|
5
db/migrate/20250121015849_add_country_to_cities.rb
Normal file
5
db/migrate/20250121015849_add_country_to_cities.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
class AddCountryToCities < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_reference :cities, :country, null: false, foreign_key: true
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,6 @@
|
|||||||
|
class RemoveGeographicFieldsFromCities < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
remove_column :cities, :region, :string
|
||||||
|
remove_column :cities, :country, :string
|
||||||
|
end
|
||||||
|
end
|
6
db/migrate/20250122053220_add_slug_to_weather_arts.rb
Normal file
6
db/migrate/20250122053220_add_slug_to_weather_arts.rb
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
class AddSlugToWeatherArts < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :weather_arts, :slug, :string
|
||||||
|
add_index :weather_arts, :slug, unique: true
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,8 @@
|
|||||||
|
class RemoveLastFetchFieldsFromCities < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
remove_column :cities, :last_weather_fetch
|
||||||
|
remove_column :cities, :last_image_generation
|
||||||
|
|
||||||
|
add_index :weather_arts, [ :city_id, :weather_date ]
|
||||||
|
end
|
||||||
|
end
|
61
db/migrate/20250126155239_create_ahoy_visits_and_events.rb
Normal file
61
db/migrate/20250126155239_create_ahoy_visits_and_events.rb
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
class CreateAhoyVisitsAndEvents < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
create_table :ahoy_visits do |t|
|
||||||
|
t.string :visit_token
|
||||||
|
t.string :visitor_token
|
||||||
|
|
||||||
|
# the rest are recommended but optional
|
||||||
|
# simply remove any you don't want
|
||||||
|
|
||||||
|
# user
|
||||||
|
t.references :user
|
||||||
|
|
||||||
|
# standard
|
||||||
|
t.string :ip
|
||||||
|
t.text :user_agent
|
||||||
|
t.text :referrer
|
||||||
|
t.string :referring_domain
|
||||||
|
t.text :landing_page
|
||||||
|
|
||||||
|
# technology
|
||||||
|
t.string :browser
|
||||||
|
t.string :os
|
||||||
|
t.string :device_type
|
||||||
|
|
||||||
|
# location
|
||||||
|
t.string :country
|
||||||
|
t.string :region
|
||||||
|
t.string :city
|
||||||
|
t.float :latitude
|
||||||
|
t.float :longitude
|
||||||
|
|
||||||
|
# utm parameters
|
||||||
|
t.string :utm_source
|
||||||
|
t.string :utm_medium
|
||||||
|
t.string :utm_term
|
||||||
|
t.string :utm_content
|
||||||
|
t.string :utm_campaign
|
||||||
|
|
||||||
|
# native apps
|
||||||
|
t.string :app_version
|
||||||
|
t.string :os_version
|
||||||
|
t.string :platform
|
||||||
|
|
||||||
|
t.datetime :started_at
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :ahoy_visits, :visit_token, unique: true
|
||||||
|
add_index :ahoy_visits, [ :visitor_token, :started_at ]
|
||||||
|
|
||||||
|
create_table :ahoy_events do |t|
|
||||||
|
t.references :visit
|
||||||
|
t.references :user
|
||||||
|
|
||||||
|
t.string :name
|
||||||
|
t.text :properties
|
||||||
|
t.datetime :time
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :ahoy_events, [ :name, :time ]
|
||||||
|
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