Compare commits

...

No commits in common. "main" and "rm" have entirely different histories.
main ... rm

240 changed files with 2785 additions and 6479 deletions

View File

@ -22,6 +22,22 @@ jobs:
- name: Scan for common Rails security vulnerabilities using static analysis - name: Scan for common Rails security vulnerabilities using static analysis
run: bin/brakeman --no-pager run: bin/brakeman --no-pager
scan_js:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Scan for security vulnerabilities in JavaScript dependencies
run: bin/importmap audit
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -48,7 +64,7 @@ jobs:
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
steps: steps:
- name: Install packages - name: Install packages
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git node-gyp pkg-config python-is-python3 google-chrome-stable run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git pkg-config google-chrome-stable
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@ -1,48 +0,0 @@
name: Docker
on:
push:
branches:
- dev
env:
# Use docker.io for Docker Hub if empty
REGISTRY: docker.io
IMAGE_NAME: ${{ github.event.repository.name }}
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # 获取完整的 git history 以便生成正确的 tag
-
name: Get Version
id: get_version
run: |
echo "VERSION=$(git describe --dirty --always --tags --abbrev=7)" >> $GITHUB_OUTPUT
-
name: Login to ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{github.actor}}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:dev

View File

@ -1,51 +0,0 @@
name: Docker
on:
push:
branches:
- main
tags:
- v*
env:
# Use docker.io for Docker Hub if empty
REGISTRY: docker.io
IMAGE_NAME: ${{ github.event.repository.name }}
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # 获取完整的 git history 以便生成正确的 tag
-
name: Get Version
id: get_version
run: |
echo "VERSION=$(git describe --dirty --always --tags --abbrev=7)" >> $GITHUB_OUTPUT
-
name: Login to ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{github.actor}}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:main
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:${{ steps.get_version.outputs.VERSION }}

3
.gitignore vendored
View File

@ -37,6 +37,5 @@
!/app/assets/builds/.keep !/app/assets/builds/.keep
/node_modules /node_modules
.idea
public/sitemap.xml.gz .idea

View File

@ -1 +0,0 @@
20.17.0

View File

@ -1 +1 @@
ruby-3.3.5 3.3.5

View File

@ -2,8 +2,8 @@
# check=error=true # check=error=true
# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: # This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t today_ai_weather . # docker build -t sample_app .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name today_ai_weather today_ai_weather # docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name sample_app sample_app
# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html # For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html
@ -16,7 +16,7 @@ WORKDIR /rails
# Install base packages # Install base packages
RUN apt-get update -qq && \ RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 libpq5 redis-tools && \ apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Set production environment # Set production environment
@ -28,19 +28,28 @@ ENV RAILS_ENV="production" \
# Throw-away build stage to reduce size of final image # Throw-away build stage to reduce size of final image
FROM base AS build FROM base AS build
# Install packages needed to build gems and node modules # Install packages needed to build gems
RUN apt-get update -qq && \ RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential git node-gyp pkg-config python-is-python3 libpq-dev && \ apt-get install --no-install-recommends -y build-essential git pkg-config && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Install JavaScript dependencies # 安装 NVM
ARG NODE_VERSION=20.17.0 ENV NVM_DIR="/usr/local/nvm"
ARG YARN_VERSION=1.22.22 ENV NODE_VERSION="18.20.4"
ENV PATH=/usr/local/node/bin:$PATH
RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \ RUN mkdir -p $NVM_DIR && \
/tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash && \
npm install -g yarn@$YARN_VERSION && \ . $NVM_DIR/nvm.sh && \
rm -rf /tmp/node-build-master nvm install $NODE_VERSION && \
nvm alias default $NODE_VERSION && \
nvm use default
# 设置环境变量
ENV NODE_PATH="$NVM_DIR/v$NODE_VERSION/lib/node_modules"
ENV PATH="$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH"
# 安装 yarn
RUN npm install -g yarn
# Install application gems # Install application gems
COPY Gemfile Gemfile.lock ./ COPY Gemfile Gemfile.lock ./
@ -48,10 +57,6 @@ RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile --gemfile bundle exec bootsnap precompile --gemfile
# Install node modules
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
# Copy application code # Copy application code
COPY . . COPY . .
@ -59,10 +64,9 @@ COPY . .
RUN bundle exec bootsnap precompile app/ lib/ RUN bundle exec bootsnap precompile app/ lib/
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY # Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN RAILS_BUILD=1 SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
RUN rm -rf node_modules
# Final stage for app image # Final stage for app image
@ -75,7 +79,7 @@ COPY --from=build /rails /rails
# Run and own only the runtime files as a non-root user for security # Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \ RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp public chown -R rails:rails db log storage tmp
USER 1000:1000 USER 1000:1000
# Entrypoint prepares the database. # Entrypoint prepares the database.

48
Gemfile
View File

@ -8,17 +8,20 @@ gem "propshaft"
gem "sqlite3", ">= 2.1" gem "sqlite3", ">= 2.1"
# Use the Puma web server [https://github.com/puma/puma] # Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0" gem "puma", ">= 5.0"
# Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails] # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "jsbundling-rails" gem "importmap-rails"
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails" gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails" gem "stimulus-rails"
# Bundle and process CSS [https://github.com/rails/cssbundling-rails]
gem "cssbundling-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder] # Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder" gem "jbuilder"
gem "bcrypt", "~> 3.1"
gem "faker", "~> 3.5"
gem "kaminari", "~> 1.2"
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7" # gem "bcrypt", "~> 3.1.7"
@ -41,28 +44,6 @@ 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 "activeadmin", "~> 3.2"
gem "friendly_id", "~> 5.5"
gem "kaminari", "~> 1.2"
gem "meta-tags", "~> 2.22"
gem "sitemap_generator", "~> 6.3"
gem "ahoy_matey", "~> 5.2"
# gem "whenever", "~> 1.0"
gem "ruby-openai", "~> 7.3"
gem "httparty", "~> 0.22.0"
gem "down", "~> 5.4"
gem "aws-sdk-s3", "~> 1.177"
gem "sidekiq", "~> 7.3"
gem "sidekiq-scheduler", "~> 5.0"
gem "image_processing", "~> 1.13"
gem "ruby-vips", "~> 2.2"
gem "mini_magick", "~> 4.13.2"
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
@ -78,14 +59,21 @@ end
group :development do group :development do
# Use console on exceptions pages [https://github.com/rails/web-console] # Use console on exceptions pages [https://github.com/rails/web-console]
gem "web-console" gem "web-console"
end gem "guard", "~> 2.19"
group :production do
gem "pg", "~> 1.5"
end 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"
gem "selenium-webdriver" gem "selenium-webdriver"
gem "minitest-reporters"
gem "rails-controller-testing"
end end
gem "jsbundling-rails", "~> 1.3"
group :production do
gem "pg", "~> 1.5"
end
gem "tailwindcss-rails", "~> 3.2"

View File

@ -44,16 +44,6 @@ GEM
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activeadmin (3.2.5)
arbre (~> 1.2, >= 1.2.1)
csv
formtastic (>= 3.1)
formtastic_i18n (>= 0.4)
inherited_resources (~> 1.7)
jquery-rails (>= 4.2)
kaminari (>= 1.2.1)
railties (>= 6.1)
ransack (>= 4.0)
activejob (8.0.1) activejob (8.0.1)
activesupport (= 8.0.1) activesupport (= 8.0.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
@ -84,30 +74,8 @@ 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) ansi (1.5.0)
activesupport (>= 7)
device_detector (>= 1)
safely_block (>= 0.4)
arbre (1.7.0)
activesupport (>= 3.0.0)
ruby2_keywords (>= 0.0.2)
ast (2.4.2) 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)
@ -130,40 +98,22 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
concurrent-ruby (1.3.5) coderay (1.1.3)
connection_pool (2.5.0) concurrent-ruby (1.3.4)
connection_pool (2.4.1)
crass (1.0.6) crass (1.0.6)
cssbundling-rails (1.4.1)
railties (>= 6.0.0)
csv (3.3.2)
date (3.4.1) date (3.4.1)
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)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
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) faker (3.5.1)
faraday (2.12.2) i18n (>= 1.8.11, < 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-gnu)
ffi (1.17.1-aarch64-linux-musl) ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm-linux-gnu) ffi (1.17.1-arm-linux-gnu)
@ -172,46 +122,36 @@ GEM
ffi (1.17.1-x86_64-darwin) ffi (1.17.1-x86_64-darwin)
ffi (1.17.1-x86_64-linux-gnu) ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl) ffi (1.17.1-x86_64-linux-musl)
formtastic (5.0.0) formatador (1.1.0)
actionpack (>= 6.0.0)
formtastic_i18n (0.7.0)
friendly_id (5.5.1)
activerecord (>= 4.0.0)
fugit (1.11.1) fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11) et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4) raabro (~> 1.4)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
has_scope (0.8.2) guard (2.19.1)
actionpack (>= 5.2) formatador (>= 0.2.4)
activesupport (>= 5.2) listen (>= 2.7, < 4.0)
httparty (0.22.0) logger (~> 1.6)
csv lumberjack (>= 1.0.12, < 2.0)
mini_mime (>= 1.0.0) nenv (~> 0.1)
multi_xml (>= 0.5.2) notiffany (~> 0.0)
i18n (1.14.7) ostruct (~> 0.6)
pry (>= 0.13.0)
shellany (~> 0.0)
thor (>= 0.18.1)
i18n (1.14.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.13.0) importmap-rails (2.1.0)
mini_magick (>= 4.9.5, < 5) actionpack (>= 6.0.0)
ruby-vips (>= 2.0.17, < 3) activesupport (>= 6.0.0)
inherited_resources (1.14.0) railties (>= 6.0.0)
actionpack (>= 6.0)
has_scope (>= 0.6)
railties (>= 6.0)
responders (>= 2)
io-console (0.8.0) io-console (0.8.0)
irb (1.15.1) irb (1.14.3)
pp (>= 0.6.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jbuilder (2.13.0) jbuilder (2.13.0)
actionview (>= 5.0.0) actionview (>= 5.0.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
jmespath (1.6.2)
jquery-rails (4.6.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
jsbundling-rails (1.3.1) jsbundling-rails (1.3.1)
railties (>= 6.0.0) railties (>= 6.0.0)
json (2.9.1) json (2.9.1)
@ -238,11 +178,15 @@ GEM
activerecord activerecord
kaminari-core (= 1.2.2) kaminari-core (= 1.2.2)
kaminari-core (1.2.2) kaminari-core (1.2.2)
language_server-protocol (3.17.0.4) language_server-protocol (3.17.0.3)
logger (1.6.5) listen (3.9.0)
loofah (2.24.0) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.6.4)
loofah (2.23.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
lumberjack (1.2.10)
mail (2.8.1) mail (2.8.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap net-imap
@ -250,25 +194,24 @@ 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) method_source (1.1.0)
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)
minitest-reporters (1.7.1)
ansi
builder
minitest (>= 5.0)
ruby-progressbar
msgpack (1.7.5) msgpack (1.7.5)
multi_xml (0.7.1) nenv (0.3.0)
bigdecimal (~> 3.1) net-imap (0.5.4)
multipart-post (2.4.1)
net-http (0.6.0)
uri
net-imap (0.5.5)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.2.2)
timeout timeout
net-scp (4.1.0) net-scp (4.0.0)
net-ssh (>= 2.6.5, < 8.0.0) net-ssh (>= 2.6.5, < 8.0.0)
net-sftp (4.0.0) net-sftp (4.0.0)
net-ssh (>= 5.0.0, < 8.0.0) net-ssh (>= 5.0.0, < 8.0.0)
@ -276,53 +219,55 @@ GEM
net-protocol net-protocol
net-ssh (7.3.0) net-ssh (7.3.0)
nio4r (2.7.4) nio4r (2.7.4)
nokogiri (1.18.2-aarch64-linux-gnu) nokogiri (1.18.1-aarch64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.2-aarch64-linux-musl) nokogiri (1.18.1-aarch64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.2-arm-linux-gnu) nokogiri (1.18.1-arm-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.2-arm-linux-musl) nokogiri (1.18.1-arm-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.2-arm64-darwin) nokogiri (1.18.1-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.2-x86_64-darwin) nokogiri (1.18.1-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.2-x86_64-linux-gnu) nokogiri (1.18.1-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.2-x86_64-linux-musl) nokogiri (1.18.1-x86_64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
orm_adapter (0.5.0) notiffany (0.1.3)
nenv (~> 0.1)
shellany (~> 0.0)
ostruct (0.6.1) ostruct (0.6.1)
parallel (1.26.3) parallel (1.26.3)
parser (3.3.7.0) parser (3.3.7.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.9) 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)
rack rack
railties (>= 7.0.0) railties (>= 7.0.0)
psych (5.2.3) pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
psych (5.2.2)
date date
stringio stringio
public_suffix (6.0.1) public_suffix (6.0.1)
puma (6.6.0) puma (6.5.0)
nio4r (~> 2.0) nio4r (~> 2.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.9) rack (2.2.10)
rack-session (2.1.0) rack-session (1.0.2)
base64 (>= 0.1.0) rack (< 3)
rack (>= 3.0.0)
rack-test (2.2.0) rack-test (2.2.0)
rack (>= 1.3) rack (>= 1.3)
rackup (2.2.1) rackup (1.0.1)
rack (>= 3) rack (< 3)
webrick
rails (8.0.1) rails (8.0.1)
actioncable (= 8.0.1) actioncable (= 8.0.1)
actionmailbox (= 8.0.1) actionmailbox (= 8.0.1)
@ -337,6 +282,10 @@ GEM
activesupport (= 8.0.1) activesupport (= 8.0.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.0.1) railties (= 8.0.1)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@ -354,40 +303,34 @@ GEM
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.2.1) rake (13.2.1)
ransack (4.2.1) rb-fsevent (0.11.2)
activerecord (>= 6.1.5) rb-inotify (0.11.1)
activesupport (>= 6.1.5) ffi (~> 1.0)
i18n rdoc (6.10.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)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.4.0) rexml (3.4.0)
rubocop (1.71.1) rubocop (1.70.0)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0) regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.38.0, < 2.0) rubocop-ast (>= 1.36.2, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.38.0) rubocop-ast (1.37.0)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
rubocop-minitest (0.36.0) rubocop-minitest (0.36.0)
rubocop (>= 1.61, < 2.0) rubocop (>= 1.61, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-performance (1.23.1) rubocop-performance (1.23.0)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.29.1) rubocop-rails (2.28.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0) rubocop (>= 1.52.0, < 2.0)
@ -397,39 +340,17 @@ GEM
rubocop-minitest rubocop-minitest
rubocop-performance rubocop-performance
rubocop-rails rubocop-rails
ruby-openai (7.3.1)
event_stream_parser (>= 0.3.0, < 2.0.0)
faraday (>= 1)
faraday-multipart (>= 1)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.2.2) rubyzip (2.3.2)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
safely_block (0.4.1)
securerandom (0.4.1) securerandom (0.4.1)
selenium-webdriver (4.28.0) selenium-webdriver (4.27.0)
base64 (~> 0.2) base64 (~> 0.2)
logger (~> 1.4) logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0) rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0) websocket (~> 1.0)
sidekiq (7.3.8) shellany (0.0.1)
base64 solid_cable (3.0.5)
connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4)
redis-client (>= 0.22.2)
sidekiq-scheduler (5.0.6)
rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8)
tilt (>= 1.4.0, < 3)
sitemap_generator (6.3.0)
builder (~> 3.0)
solid_cable (3.0.7)
actioncable (>= 7.2) actioncable (>= 7.2)
activejob (>= 7.2) activejob (>= 7.2)
activerecord (>= 7.2) activerecord (>= 7.2)
@ -438,7 +359,7 @@ GEM
activejob (>= 7.2) activejob (>= 7.2)
activerecord (>= 7.2) activerecord (>= 7.2)
railties (>= 7.2) railties (>= 7.2)
solid_queue (1.1.3) solid_queue (1.1.2)
activejob (>= 7.1) activejob (>= 7.1)
activerecord (>= 7.1) activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1) concurrent-ruby (>= 1.3.1)
@ -462,13 +383,20 @@ GEM
stimulus-rails (1.3.4) stimulus-rails (1.3.4)
railties (>= 6.0.0) railties (>= 6.0.0)
stringio (3.1.2) stringio (3.1.2)
tailwindcss-rails (3.2.0)
railties (>= 7.0.0)
tailwindcss-ruby
tailwindcss-ruby (3.4.17-aarch64-linux)
tailwindcss-ruby (3.4.17-arm-linux)
tailwindcss-ruby (3.4.17-arm64-darwin)
tailwindcss-ruby (3.4.17-x86_64-darwin)
tailwindcss-ruby (3.4.17-x86_64-linux)
thor (1.3.2) thor (1.3.2)
thruster (0.1.10) thruster (0.1.10)
thruster (0.1.10-aarch64-linux) thruster (0.1.10-aarch64-linux)
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)
@ -480,16 +408,14 @@ GEM
unicode-emoji (4.0.4) unicode-emoji (4.0.4)
uri (1.0.2) uri (1.0.2)
useragent (0.16.11) useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1) web-console (4.2.1)
actionview (>= 6.0.0) actionview (>= 6.0.0)
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webrick (1.9.1)
websocket (1.2.11) websocket (1.2.11)
websocket-driver (0.7.7) websocket-driver (0.7.6)
base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
@ -509,41 +435,32 @@ PLATFORMS
x86_64-linux-musl x86_64-linux-musl
DEPENDENCIES DEPENDENCIES
activeadmin (~> 3.2) bcrypt (~> 3.1)
ahoy_matey (~> 5.2)
aws-sdk-s3 (~> 1.177)
bootsnap bootsnap
brakeman brakeman
capybara capybara
cssbundling-rails
debug debug
devise (~> 4.9) faker (~> 3.5)
down (~> 5.4) guard (~> 2.19)
friendly_id (~> 5.5) importmap-rails
httparty (~> 0.22.0)
image_processing (~> 1.13)
jbuilder jbuilder
jsbundling-rails jsbundling-rails (~> 1.3)
kamal kamal
kaminari (~> 1.2) kaminari (~> 1.2)
meta-tags (~> 2.22) minitest-reporters
mini_magick (~> 4.13.2)
pg (~> 1.5) pg (~> 1.5)
propshaft propshaft
puma (>= 5.0) puma (>= 5.0)
rails (~> 8.0.1) rails (~> 8.0.1)
rails-controller-testing
rubocop-rails-omakase rubocop-rails-omakase
ruby-openai (~> 7.3)
ruby-vips (~> 2.2)
selenium-webdriver selenium-webdriver
sidekiq (~> 7.3)
sidekiq-scheduler (~> 5.0)
sitemap_generator (~> 6.3)
solid_cable solid_cable
solid_cache solid_cache
solid_queue solid_queue
sqlite3 (>= 2.1) sqlite3 (>= 2.1)
stimulus-rails stimulus-rails
tailwindcss-rails (~> 3.2)
thruster thruster
turbo-rails turbo-rails
tzinfo-data tzinfo-data

73
Guardfile Normal file
View File

@ -0,0 +1,73 @@
# A sample Guardfile
# More info at https://github.com/guard/guard#readme
## Uncomment and set this to only include directories you want to watch
# directories %w(app lib config test spec features) \
# .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")}
## Note: if you are using the `directories` clause above and you are not
## watching the project directory ('.'), then you will want to move
## the Guardfile to a watched dir and symlink it back, e.g.
#
# $ mkdir config
# $ mv Guardfile config/
# $ ln -s config/Guardfile .
#
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
require 'active_support/core_ext/string' # Defines the matching rules for Guard.
guard :minitest, spring: "bin/rails test", all_on_start: false do
watch(%r{^test/(.*)/?(.*)_test\.rb$})
watch('test/test_helper.rb') { 'test' }
watch('config/routes.rb') { interface_tests }
watch(%r{app/views/layouts/*}) { interface_tests }
watch(%r{âpp/models/(.*?)\.rb$}) do |matches|
[ "test/models/#{matches[1]}_test.rb", "test/integration/microposts_interface_test.rb" ]
end
watch(%r{^test/fixtures/(.*?)\.yml$}) do |matches|
"test/models/#{matches[1].singularize}_test.rb"
end
watch(%r{âpp/mailers/(.*?)\.rb$}) do |matches|
"test/mailers/#{matches[1]}_test.rb"
end
watch(%r{âpp/views/(.*)_mailer/.*$}) do |matches|
"test/mailers/#{matches[1]}_mailer_test.rb"
end
watch(%r{âpp/controllers/(.*?)_controller\.rb$}) do |matches|
resource_tests(matches[1])
end
watch(%r{âpp/views/([^/]*?)/.*\.html\.erb$}) do |matches|
[ "test/controllers/#{matches[1]}_controller_test.rb" ] + integration_tests(matches[1])
end
watch(%r{âpp/helpers/(.*?)_helper\.rb$}) do |matches| integration_tests(matches[1])
end
watch('app/views/layouts/application.html.erb') do 'test/integration/site_layout_test.rb'
end
watch('app/helpers/sessions_helper.rb') do integration_tests << 'test/helpers/sessions_helper_test.rb'
end
watch('app/controllers/sessions_controller.rb') do [ 'test/controllers/sessions_controller_test.rb', 'test/integration/users_login_test.rb' ]
end
watch('app/controllers/account_activations_controller.rb') do 'test/integration/users_signup_test.rb'
end
watch(%r{app/views/users/*}) do resource_tests('users') + [ 'test/integration/microposts_interface_test.rb' ]
end
end # Returns the integration tests corresponding to the given resource.
def integration_tests(resource = :all)
if resource == :all
Dir["test/integration/*"]
else
Dir["test/integration/#{resource}_*.rb"]
end
end # Returns all tests that hit the interface.
def
interface_tests
integration_tests << "test/controllers"
end # Returns the controller tests corresponding to the given resource.
def
controller_test(resource) "test/controllers/#{resource}_controller_test.rb"
end # Returns all tests for the given resource.
def
resource_tests(resource) integration_tests(resource) << controller_test(resource)
end

View File

@ -1,3 +1,3 @@
web: env RUBY_DEBUG_OPEN=true bin/rails server web: env RUBY_DEBUG_OPEN=true bin/rails server
js: yarn build --watch js: yarn build --watch
css: yarn build:css --watch css: bin/rails tailwindcss:watch

View File

@ -1,28 +0,0 @@
ActiveAdmin.register AdminUser do
menu label: "AdminUser Manager", parent: "系统管理"
permit_params :email, :password, :password_confirmation
index do
selectable_column
id_column
column :email
column :current_sign_in_at
column :sign_in_count
column :created_at
actions
end
filter :email
filter :current_sign_in_at
filter :sign_in_count
filter :created_at
form do |f|
f.inputs do
f.input :email
f.input :password
f.input :password_confirmation
end
f.actions
end
end

View File

@ -1,83 +0,0 @@
ActiveAdmin.register_page "Ahoy Dashboard" do
menu label: "总览", parent: "数据统计", priority: 1
page_action :toggle_city_status, method: :post do
city = City.find(params[:city_id])
city.update(active: !city.active)
redirect_back(fallback_location: admin_dashboard_path, notice: "城市状态已更新")
end
content title: "总览" do
columns do
column do
panel "访问统计" do
para "总访问量: #{Ahoy::Visit.count}"
para "总事件数: #{Ahoy::Event.count}"
para "独立访客数: #{Ahoy::Visit.distinct.count(:visitor_token)}"
end
end
column do
panel "热门城市" do
table_for City.by_popularity.limit(10) do
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
column("访问量") { |city| city.view_count }
end
end
end
column do
panel "热门天气艺术" do
table_for WeatherArt.by_popularity.limit(10) do
column("作品") { |art| link_to(art.to_s, admin_weather_art_path(art)) }
column("访问量") { |art| art.view_count }
end
end
end
end
columns do
column do
panel "最冷门活跃城市" do
table_for City.least_popular_active.limit(10) do
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
column("访问量") { |city| city.view_count }
column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
# column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
column("操作") { |city|
button_to "停用",
admin_ahoy_dashboard_toggle_city_status_path(city_id: city.id),
method: :post,
data: { confirm: "确定要停用 #{city.name} 吗?" }
}
end
end
end
column do
panel "热门未活跃城市" do
table_for City.most_popular_inactive.limit(10) do
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
column("访问量") { |city| city.view_count }
column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
column("所属区域") { |city| city.country.region.name }
column("操作") { |city|
button_to "激活",
admin_ahoy_dashboard_toggle_city_status_path(city_id: city.id),
method: :post,
data: { confirm: "确定要激活 #{city.name} 吗?" }
}
end
end
end
end
# 添加一个事件列表面板
panel "最近事件" do
table_for Ahoy::Event.order(time: :desc).limit(10) do
column :time
column :name
column :properties
end
end
end
end

View File

@ -1,32 +0,0 @@
ActiveAdmin.register Ahoy::Event do
menu label: "事件统计", parent: "数据统计"
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
# permit_params :visit_id, :user_id, :name, :properties, :time
#
# or
#
# permit_params do
# permitted = [:visit_id, :user_id, :name, :properties, :time]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
# menu priority: 101, label: "事件统计"
actions :index
index do
column :id
column :name
column :time
column :properties
column :user_id
end
filter :name
filter :time
filter :properties
end

View File

@ -1,34 +0,0 @@
# app/admin/ahoy_management.rb
ActiveAdmin.register_page "Ahoy Management" do
menu label: "访问数据管理", parent: "系统管理"
content title: "访问数据管理" do
columns do
column do
panel "数据统计" do
attributes_table_for :ahoy do
row("总事件数") { Ahoy::Event.count }
row("总访问数") { Ahoy::Visit.count }
row("最早事件") { Ahoy::Event.minimum(:time)&.strftime("%Y-%m-%d %H:%M:%S") }
row("最早访问") { Ahoy::Visit.minimum(:started_at)&.strftime("%Y-%m-%d %H:%M:%S") }
end
end
end
column do
panel "操作" do
div class: "buttons" do
button_to "立即清理旧数据", admin_ahoy_management_cleanup_path, method: :post,
data: { confirm: "确定要清理3个月前的数据吗" },
class: "button"
end
end
end
end
end
page_action :cleanup, method: :post do
CleanAhoyDataWorker.perform_async
redirect_to admin_ahoy_management_path, notice: "清理任务已加入队列"
end
end

View File

@ -1,36 +0,0 @@
ActiveAdmin.register Ahoy::Visit do
menu label: "访客统计", parent: "数据统计"
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
# permit_params :visit_token, :visitor_token, :user_id, :ip, :user_agent, :referrer, :referring_domain, :landing_page, :browser, :os, :device_type, :country, :region, :city, :latitude, :longitude, :utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign, :app_version, :os_version, :platform, :started_at
#
# or
#
# permit_params do
# permitted = [:visit_token, :visitor_token, :user_id, :ip, :user_agent, :referrer, :referring_domain, :landing_page, :browser, :os, :device_type, :country, :region, :city, :latitude, :longitude, :utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign, :app_version, :os_version, :platform, :started_at]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
# menu priority: 100, label: "访问统计"
actions :index
index do
column :id
column :visitor_token
column :ip
column :user_agent
column :started_at
column :city
column :country
column :region
end
filter :started_at
filter :city
filter :country
end

View File

@ -1,54 +0,0 @@
ActiveAdmin.register City do
menu label: "City 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, :country_id, :latitude, :longitude, :active, :priority, :timezone, :last_weather_fetch, :last_image_generation, :slug
#
# or
#
# permit_params do
# permitted = [:name, :country, :latitude, :longitude, :active, :priority, :timezone, :region, :last_weather_fetch, :last_image_generation, :slug]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
index do
selectable_column
id_column
column :name
column :country
column :region do |city|
city.region
end
column :latitude
column :longitude
column :active
actions
end
filter :name
filter :active
filter :country, as: :select
form do |f|
f.inputs do
f.input :active
f.input :name
f.input :country
f.input :latitude
f.input :longitude
f.input :priority
f.input :timezone
end
f.actions
end
end

View File

@ -1,42 +0,0 @@
ActiveAdmin.register Country do
menu label: "Country Manager", parent: "系统管理"
controller do
def find_resource
scoped_collection.friendly.find(params[:id])
end
end
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
permit_params :name, :code, :region_id
#
# or
#
# permit_params do
# permitted = [:name, :code, :slug, :region_id]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
index do
selectable_column
id_column
column :name
column :code
column :region
column :cities_count do |country|
country.cities.count
end
actions
end
form do |f|
f.inputs do
f.input :region
f.input :name
f.input :code
end
f.actions
end
end

View File

@ -1,94 +0,0 @@
# frozen_string_literal: true
ActiveAdmin.register_page "Dashboard" do
menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }
content title: proc { I18n.t("active_admin.dashboard") } do
div class: "blank_slate_container", id: "dashboard_default_message" do
span class: "blank_slate" do
span I18n.t("active_admin.dashboard_welcome.welcome")
small I18n.t("active_admin.dashboard_welcome.call_to_action")
end
end
columns do
column do
panel "访问统计" do
para "总访问量: #{Ahoy::Visit.count}"
para "总事件数: #{Ahoy::Event.count}"
para "独立访客数: #{Ahoy::Visit.distinct.count(:visitor_token)}"
end
end
column do
panel "热门城市" do
table_for City.by_popularity.limit(10) do
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
column("访问量") { |city| city.view_count }
end
end
end
column do
panel "热门天气艺术" do
table_for WeatherArt.by_popularity.limit(10) do
column("作品") { |art| link_to(art.to_s, admin_weather_art_path(art)) }
column("访问量") { |art| art.view_count }
end
end
end
end
columns do
column do
panel "最冷门活跃城市" do
table_for City.least_popular_active.limit(10) do
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
column("访问量") { |city| city.view_count }
# column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
end
end
end
column do
panel "热门未活跃城市" do
table_for City.most_popular_inactive.limit(10) do
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
column("访问量") { |city| city.view_count }
column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
column("所属区域") { |city| city.country.region.name }
end
end
end
end
# 添加一个事件列表面板
panel "最近事件" do
table_for Ahoy::Event.order(time: :desc).limit(10) do
column :time
column :name
column :properties
end
end
# Here is an example of a simple dashboard with columns and panels.
#
# columns do
# column do
# panel "Recent Posts" do
# ul do
# Post.recent(5).map do |post|
# li link_to(post.title, admin_post_path(post))
# end
# end
# end
# end
# column do
# panel "Info" do
# para "Welcome to ActiveAdmin."
# end
# end
# end
end # content
end

View File

@ -1,30 +0,0 @@
ActiveAdmin.register Region do
menu label: "Region Manager", parent: "系统管理"
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
permit_params :name, :code
#
# or
#
# permit_params do
# permitted = [:name, :code, :slug]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
index do
selectable_column
id_column
column :name
column :code
column :countries_count do |region|
region.countries.count
end
column :cities_count do |region|
region.cities.count
end
actions
end
end

View File

@ -1,98 +0,0 @@
# app/admin/sidekiq_tasks.rb
ActiveAdmin.register_page "Sidekiq Tasks" do
# menu label: "Sidekiq Tasks", priority: 99
menu label: "Sidekiq Tasks Manager", parent: "系统管理"
content title: "Sidekiq Tasks Management" do
div class: "sidekiq-tasks" do
panel "Manual Task Execution" do
div class: "task-buttons" do
div class: "task-button" do
h3 "Generate Weather Arts"
form action: admin_sidekiq_tasks_run_task_path, method: :post do
input type: "hidden", name: "authenticity_token", value: form_authenticity_token
input type: "hidden", name: "task", value: "GenerateWeatherArtsWorker"
select name: "city_id" do
City.all.map do |city|
option city.name, value: city.id
end
end
input type: "submit", value: "Run Task", class: "button"
end
end
div class: "task-button" do
h3 "Batch Generate Weather Arts"
form action: admin_sidekiq_tasks_run_task_path, method: :post do
input type: "hidden", name: "authenticity_token", value: form_authenticity_token
input type: "hidden", name: "task", value: "BatchGenerateWeatherArts"
input type: "submit", value: "Run Task", class: "button"
end
end
div class: "task-button" do
h3 "Refresh Sitemap"
form action: admin_sidekiq_tasks_run_task_path, method: :post do
input type: "hidden", name: "authenticity_token", value: form_authenticity_token
input type: "hidden", name: "task", value: "RefreshSitemapWorker"
input type: "submit", value: "Run Task", class: "button"
end
end
div class: "task-button" do
h3 "Clean Ahoy Data"
form action: admin_sidekiq_tasks_run_task_path, method: :post do
input type: "hidden", name: "authenticity_token", value: form_authenticity_token
input type: "hidden", name: "task", value: "CleanAhoyDataWorker"
input type: "submit", value: "Run Task", class: "button"
end
end
end
end
panel "Sidekiq Statistics" do
stats = Sidekiq::Stats.new
table class: "sidekiq-stats" do
tr do
th "Processed Jobs"
td stats.processed
end
tr do
th "Failed Jobs"
td stats.failed
end
tr do
th "Enqueued Jobs"
td stats.enqueued
end
tr do
th "Scheduled Jobs"
td stats.scheduled_size
end
tr do
th "Retry Set Size"
td stats.retry_size
end
end
end
end
end
page_action :run_task, method: :post do
task_name = params[:task]
city_id = params[:city_id]
case task_name
when "BatchGenerateWeatherArts"
BatchGenerateWeatherArtsWorker.perform_async
when "GenerateWeatherArtsWorker"
GenerateWeatherArtWorker.perform_async(city_id)
when "RefreshSitemapWorker"
RefreshSitemapWorker.perform_async
when "CleanAhoyDataWorker"
CleanAhoyDataWorker.perform_async
end
redirect_to admin_sidekiq_tasks_path, notice: "Task #{task_name} has been queued"
end
end

View File

@ -1,84 +0,0 @@
ActiveAdmin.register WeatherArt do
menu label: "WeatherArt Manager", parent: "系统管理"
controller do
def find_resource
scoped_collection.friendly.find(params[:id])
end
end
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
# permit_params :city_id, :weather_date, :description, :temperature, :feeling_temp, :humidity, :wind_scale, :wind_speed, :precipitation, :pressure, :visibility, :cloud, :prompt
#
# or
#
# permit_params do
# permitted = [:city_id, :weather_date, :description, :temperature, :feeling_temp, :humidity, :wind_scale, :wind_speed, :precipitation, :pressure, :visibility, :cloud, :prompt]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
permit_params :city_id, :weather_date, :description, :temperature,
:feeling_temp, :humidity, :wind_scale, :wind_speed,
:precipitation, :pressure, :visibility, :cloud,
:prompt, :image, :slug
remove_filter :image_attachment, :image_blob
filter :city_id
filter :weather_data
index do
selectable_column
id_column
column :city
column :weather_date
column :description
column :temperature
column :image do |weather_art|
image_tag(weather_art.image, size: "100x100") if weather_art.image.attached?
end
actions
end
show do
attributes_table do
row :city
row :weather_date
row :description
row :temperature
row :feeling_temp
row :humidity
row :wind_scale
row :wind_speed
row :precipitation
row :pressure
row :visibility
row :cloud
row :prompt
row :image do |weather_art|
image_tag(weather_art.image) if weather_art.image.attached?
end
end
end
form do |f|
f.inputs do
f.input :city
f.input :weather_date
f.input :description
f.input :temperature
f.input :feeling_temp
f.input :humidity
f.input :wind_scale
f.input :wind_speed
f.input :precipitation
f.input :pressure
f.input :visibility
f.input :cloud
f.input :prompt
f.input :image, as: :file
end
f.actions
end
end

View File

@ -0,0 +1,3 @@
//= link_tree ../images
// = link_tree ../builds
//= link_tree ../builds

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

View File

@ -0,0 +1 @@
<svg height='100px' width='100px' fill="#000000" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve"><g><path d="M15,29c-3.309,0-6,2.691-6,6c0,2.206,1.794,4,4,4s4-1.794,4-4h-2c0,1.103-0.897,2-2,2s-2-0.897-2-2c0-2.206,1.794-4,4-4 c3.309,0,6,2.691,6,6c0,4.411-3.589,8-8,8C7.486,45,3,40.514,3,35c0-4.057,1.984-7.869,5.308-10.195l10.9-7.63 C21.583,15.513,23,12.79,23,9.891C23,4.989,19.011,1,14.109,1C12.395,1,11,2.395,11,4.109v16.371l-3.839,2.687 C3.303,25.867,1,30.291,1,35c0,5.934,4.334,10.863,10,11.819V58c0,1.654-1.346,3-3,3s-3-1.346-3-3c0-0.551,0.449-1,1-1s1,0.449,1,1 v1h2v-1c0-1.654-1.346-3-3-3s-3,1.346-3,3c0,2.757,2.243,5,5,5s5-2.243,5-5V47c5.514,0,10-4.486,10-10C23,32.589,19.411,29,15,29z M13,4.109C13,3.498,13.498,3,14.109,3C17.909,3,21,6.091,21,9.891c0,2.247-1.099,4.357-2.939,5.646L13,19.079V4.109z"></path><path d="M31,45.22l10,2v-8.04l-12-2.4V49h-2c-2.206,0-4,1.794-4,4s1.794,4,4,4s4-1.794,4-4V45.22z M31,39.22l8,1.6v3.96l-8-1.6 V39.22z M29,53c0,1.103-0.897,2-2,2s-2-0.897-2-2s0.897-2,2-2h2V53z"></path><path d="M51,18.895V29h-2c-2.206,0-4,1.794-4,4s1.794,4,4,4s4-1.794,4-4v-5.895l8,0.8V31h-2c-2.206,0-4,1.794-4,4s1.794,4,4,4 s4-1.794,4-4V20.095L51,18.895z M51,33c0,1.103-0.897,2-2,2s-2-0.897-2-2s0.897-2,2-2h2V33z M61,35c0,1.103-0.897,2-2,2 s-2-0.897-2-2s0.897-2,2-2h2V35z M53,25.095v-3.99l8,0.8v3.99L53,25.095z"></path><path d="M43,9h2v12h2V5c0-2.206-1.794-4-4-4s-4,1.794-4,4S40.794,9,43,9z M43,3c1.103,0,2,0.897,2,2v2h-2c-1.103,0-2-0.897-2-2 S41.897,3,43,3z"></path><path d="M29,23c2.206,0,4-1.794,4-4V3h-2v12h-2c-2.206,0-4,1.794-4,4S26.794,23,29,23z M29,17h2v2c0,1.103-0.897,2-2,2 s-2-0.897-2-2S27.897,17,29,17z"></path><path d="M36.792,61.086l0.813,1.828c8.887-3.949,15.894-11.447,19.224-20.572l-1.879-0.686 C51.804,50.275,45.186,57.357,36.792,61.086z"></path><path d="M56.434,16.628c-1.505-3.76-3.624-7.221-6.298-10.286l-1.507,1.314c2.526,2.896,4.527,6.165,5.949,9.715L56.434,16.628z"></path><path d="M53.532,39.309l-1.902-0.617c-3.047,9.39-11.047,16.797-20.879,19.332l0.499,1.937 C41.737,57.256,50.275,49.343,53.532,39.309z"></path><path d="M51.841,16.554c-0.713-1.432-1.547-2.823-2.481-4.134l-1.629,1.16c0.873,1.226,1.653,2.526,2.32,3.866L51.841,16.554z"></path><path d="M34.081,34.615l1.846,0.771C36.639,33.677,37,31.866,37,30c0-2.683-0.761-5.29-2.201-7.539l-1.685,1.078 C34.348,25.466,35,27.7,35,30C35,31.6,34.69,33.152,34.081,34.615z"></path><path d="M41,30c0,2.291-0.424,4.522-1.261,6.631l1.859,0.737C42.528,35.023,43,32.544,43,30c0-6.212-2.815-11.967-7.723-15.79 l-1.229,1.578C38.466,19.229,41,24.409,41,30z"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 140" enable-background="new 0 0 400 140" xml:space="preserve">
<style>.a{fill:#c00;}</style>
<title>rails-logo</title>
<g>
<path class="a" d="M346.6,121.5v18.1c0,0,23.4,0,32.7,0c6.7,0,18.2-4.9,18.6-18.6c0-0.6,0-6.5,0-7c0-11.7-9.6-18.6-18.6-18.6
c-4.2,0-16.3,0-16.3,0V87l32.3,0V68.8c0,0-22.2,0-31,0c-8,0-18.7,6.6-18.7,18.9c0,1.2,0,5.2,0,6.3c0,12.3,10.6,18.6,18.7,18.6
c22.5,0.1-5.4,0,15.4,0c0,8.8,0,8.8,0,8.8"/>
<path class="a" d="M171.4,117.1c0,0,17.5-1.5,17.5-24.1s-21.2-24.7-21.2-24.7h-38.2v71.3h19.2v-17.2l16.6,17.2h28.4
L171.4,117.1z M164,102.5h-15.3V86.2h15.4c0,0,4.3,1.6,4.3,8.1S164,102.5,164,102.5z"/>
<path class="a" d="M236.3,68.8c-4.9,0-5.6,0-19.5,0c-13.9,0-18.6,12.6-18.6,18.6c0,13,0,52.2,0,52.2h19.5v-12.5H236v12.5h18.9
c0,0,0-38.5,0-52.2C254.9,72.2,241.1,68.8,236.3,68.8z M236,106.9h-18.4V89.6c0,0,0-3.9,6.1-3.9c5.6,0,1,0,6.7,0
c5.4,0,5.5,3.9,5.5,3.9V106.9z"/>
<rect x="263.8" y="68.8" class="a" width="20.3" height="70.8"/>
<polygon class="a" points="312.6,121.3 312.6,68.8 292.4,68.8 292.4,121.3 292.4,139.6 312.6,139.6 339.9,139.6 339.9,121.3
"/>
<path class="a" d="M9,139.6h79c0,0-15.1-68.9,34.9-96.8c10.9-5.3,45.6-25.1,102.4,16.9c1.8-1.5,3.5-2.7,3.5-2.7
S176.8,5.1,118.9,10.9C89.8,13.5,54,40,33,75S9,139.6,9,139.6z"/>
<path class="a" d="M9,139.6h79c0,0-15.1-68.9,34.9-96.8c10.9-5.3,45.6-25.1,102.4,16.9c1.8-1.5,3.5-2.7,3.5-2.7
S176.8,5.1,118.9,10.9C89.8,13.5,54,40,33,75S9,139.6,9,139.6z"/>
<path class="a" d="M9,139.6h79c0,0-15.1-68.9,34.9-96.8c10.9-5.3,45.6-25.1,102.4,16.9c1.8-1.5,3.5-2.7,3.5-2.7
S176.8,5.1,118.9,10.9C89.7,13.5,53.9,40,32.9,75S9,139.6,9,139.6z"/>
<path class="a" d="M173.6,16.5l0.4-6.7c-0.9-0.5-3.4-1.7-9.7-3.5l-0.4,6.6C167.2,14,170.4,15.2,173.6,16.5z"/>
<path class="a" d="M164.1,37.7l-0.4,6.3c3.3,0.1,6.6,0.5,9.9,1.2l0.4-6.2C170.6,38.3,167.3,37.9,164.1,37.7z"/>
<path class="a" d="M127.1,6.5c0.3,0,0.7,0,1,0l-2-6.1c-3.1,0-6.3,0.2-9.6,0.6l1.9,5.9C121.3,6.6,124.2,6.5,127.1,6.5z"/>
<path class="a" d="M131.9,43.3l2.3,6.9c2.9-1.4,5.8-2.6,8.7-3.5l-2.2-6.6C137.3,41.1,134.4,42.2,131.9,43.3z"/>
<path class="a" d="M86.5,17L82,10.1c-2.5,1.3-5.1,2.7-7.8,4.3l4.6,7C81.4,19.8,83.9,18.3,86.5,17z"/>
<path class="a" d="M107,62l4.8,7.2c1.7-2.5,3.7-4.8,5.9-7.1l-4.5-6.8C110.9,57.4,108.8,59.7,107,62z"/>
<path class="a" d="M92.5,94.2l8.1,6.4c0.4-3.9,1.1-7.8,2.1-11.7l-7.2-5.7C94.2,86.9,93.3,90.6,92.5,94.2z"/>
<path class="a" d="M48.7,46.7l-7.1-6.2c-2.6,2.5-5.1,5-7.4,7.5l7.7,6.6C44,51.9,46.3,49.2,48.7,46.7z"/>
<path class="a" d="M18.5,91.4L7,87.2c-1.9,4.3-4,9.3-5,12l11.5,4.2C14.8,100,16.9,95.1,18.5,91.4z"/>
<path class="a" d="M91,119.6c0.2,5.3,0.7,9.6,1.2,12.6l12,4.3c-0.9-3.9-1.8-8.3-2.4-13L91,119.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -1,20 +0,0 @@
// Sass variable overrides must be declared before loading up Active Admin's styles.
//
// To view the variables that Active Admin provides, take a look at
// `app/assets/stylesheets/active_admin/mixins/_variables.scss` in the
// Active Admin source.
//
// For example, to change the sidebar width:
// $sidebar-width: 242px;
// Active Admin's got SASS!
//@import "active_admin/mixins";
//@import "active_admin/base";
@import "@activeadmin/activeadmin/src/scss/mixins";
@import "@activeadmin/activeadmin/src/scss/base";
// Overriding any non-variable Sass must be done after the fact.
// For example, to change the default status-tag color:
//
// .status_tag { background: #6090DB; }

View File

@ -1 +0,0 @@
// Entry point for your Sass build

View File

@ -1,6 +1,31 @@
@import "photoswipe/dist/photoswipe.css";
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/*
@layer components {
.btn-primary {
@apply py-2 px-4 bg-blue-200;
}
}
*/
@layer base {
h1, h2, h3, h4, h5, h6 {
@apply leading-none;
}
h1 {
@apply text-[3em] tracking-[-2px] mb-8 mt-8 text-center;
}
h2 {
@apply text-[1.2em] tracking-[-1px] mb-[30px] text-center font-normal text-gray-400;
}
p {
@apply text-[1.1em] leading-[1.7];
}
}

View File

@ -1,24 +0,0 @@
# app/concerns/seo_concern.rb
module SeoConcern
extend ActiveSupport::Concern
included do
before_action :prepare_meta_tags
end
private
def prepare_meta_tags
set_meta_tags(
site: "TodayAIWeather",
description: "Discover AI-generated weather art from cities around the world. Real-time weather visualization through artificial intelligence.",
keywords: "weather, AI art, weather visualization, city weather, artificial intelligence",
og: {
title: :title,
description: :description,
type: "website",
url: request.original_url
}
)
end
end

View File

@ -0,0 +1,15 @@
class AccountActivationsController < ApplicationController
include SessionsHelper
def edit
user = User.find_by(email: params[:email])
if user && !user.activated? && user.authenticated?(:activation, params[:id])
user.activate
log_in user
flash[:success] = "Account activated!"
redirect_to user
else
flash[:danger] = "Invalid activation link"
redirect_to root_url
end
end
end

View File

@ -1,74 +1,8 @@
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include SeoConcern
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
before_action :log_browser_info allow_browser versions: :modern
# allow_browser versions: :modern
# allow_browser versions: :modern,
# patterns: [
# # 鸿蒙系统相关
# /OpenHarmony/, # 鸿蒙系统标识
# /ArkWeb\/[\d.]+/, # 鸿蒙浏览器内核
# /Mobile HuaweiBrowser/, # 华为浏览器(新格式)
# /HuaweiBrowser\/[\d.]+/, # 华为浏览器(旧格式)
#
# # 夸克浏览器(更宽松的匹配)
# /Quark[\s\/][\d.]+/, # 匹配 "Quark/7.4.6.681" 或 "Quark 7.4.6.681"
#
# /Mobile Safari/,
# /Chrome\/[\d.]+/,
# /Quark\/[\d.]+/,
# /HuaweiBrowser\/[\d.]+/,
# /MiuiBrowser\/[\d.]+/,
# /VivoBrowser\/[\d.]+/,
# /OppoBrowser\/[\d.]+/,
# /UCBrowser\/[\d.]+/,
# /QQBrowser\/[\d.]+/,
# /MicroMessenger\/[\d.]+/,
# /Alipay/,
# /BaiduBoxApp/,
# /baiduboxapp/i,
# /SogouMobile/,
# /Weibo/,
# /DingTalk/,
# /ToutiaoMicroApp/,
# /BytedanceWebview/,
# /ArkWeb/
# ],
# on_failure: ->(browser) {
# Rails.logger.warn <<~BROWSER_INFO
# Browser Blocked:
# User Agent: #{browser.ua}
# Name: #{browser.name}
# Version: #{browser.version}
# Platform: #{browser.platform.name}
# Device: #{browser.device.name}
# Mobile: #{browser.mobile?}
# Modern: #{browser.modern?}
# Bot: #{browser.bot?}
# BROWSER_INFO
# }
before_action :set_locale
after_action :track_action
def log_browser_info def hello
# 构建详细的浏览器信息 render html: "Hello World!"
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 end

View File

@ -1,21 +0,0 @@
class ArtsController < ApplicationController
def index
@regions = Region.all
@current_region = Region.find(params[:region]) if params[:region].present?
@weather_arts = WeatherArt.includes(city: [ :country, { country: :region } ])
if @current_region
@weather_arts = @weather_arts.joins(city: :country)
.where(countries: { region_id: @current_region.id })
end
@weather_arts = if params[:sort] == "oldest"
@weather_arts.order(created_at: :asc)
else
@weather_arts.order(created_at: :desc)
end
@weather_arts = @weather_arts.page(params[:page]).per(10)
end
end

View File

@ -1,42 +1,11 @@
class CitiesController < ApplicationController class CitiesController < ApplicationController
def index def index
@regions = Region.includes(:countries).order(:name) @cities = City.all
@cities = City.includes(:country, country: :region).order(:name) @cities = City.all.includes(:weather_arts).page(params[:page])
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.find(params[:id])
ahoy.track "View City", { @weather_arts = @city.weather_arts.latest.page(params[:page])
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

View File

@ -1,11 +0,0 @@
class HomeController < ApplicationController
def index
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(20).shuffle.last(10)
@featured_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(5)
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

View File

@ -0,0 +1,65 @@
class PasswordResetsController < ApplicationController
before_action :get_user, only: [ :edit, :update ]
before_action :valid_user, only: [ :edit, :update ]
before_action :check_expiration, only: [ :edit, :update ]
include SessionsHelper
def new
end
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@user.create_reset_digest
@user.send_password_reset_email
flash[:info] = "Email send with password reset instructions"
redirect_to root_url
else
flash.now[:danger] = "Email not found"
render "new", status: :unprocessable_entity
end
end
def edit
end
def update
if params[:user][:password].empty?
@user.errors.add(:password, "can't be empty")
render "edit", status: :unprocessable_entity
elsif @user.update(user_params)
forget(@user)
reset_session
@user.update_attribute(:reset_digest, nil)
log_in @user
flash[:success] = "Password has been reset"
redirect_to @user
else
render "edit", status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:password, :password_confirmation)
end
def get_user
@user = User.find_by(email: params[:email])
end
def valid_user
unless @user && @user.activated? &&
@user.authenticated?(:reset, params[:id])
redirect_to root_url
end
end
def check_expiration
if @user.password_reset_expired?
flash[:danger] = "Password reset has expired"
redirect_to new_password_reset_url
end
end
end

View File

@ -0,0 +1,32 @@
class SessionsController < ApplicationController
include SessionsHelper
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
# if user && user.authenticate(params[:session][:password])
if user&.authenticate(params[:session][:password])
if user.activated?
forwarding_url = session[:forwarding_url]
reset_session
params[:session][:remember_me] == "1" ? remember(user) : forget(user)
log_in user
redirect_to forwarding_url || user
else
message = "Account not activated. "
message += "Check your email for the activation link."
flash[:danger] = message
redirect_to root_url
end
else
flash.now[:danger] = "Invalid email/password combination"
render "new", status: :unprocessable_entity
end
end
def destroy
log_out if logged_in?
redirect_to root_url
end
end

View File

@ -1,33 +0,0 @@
class SitemapsController < ApplicationController
def show
path = params[:path]
bucket_name =
Rails.env.production? ?
ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:aws, :bucket)) :
ENV.fetch("AWS_DEV_BUCKET", Rails.application.credentials.dig(:aws_dev, :bucket))
Rails.logger.info "Sitemap: #{path}"
begin
s3_client = Aws::S3::Client.new
response = s3_client.get_object(
bucket: bucket_name,
key: "sitemaps/#{path}"
)
expires_in 12.hours, public: true
content_type = response.content_type || "application/xml"
send_data(
response.body.read,
filename: path,
type: content_type,
disposition: "inline"
)
rescue Aws::S3::Errors::NoSuchKey
render status: :not_found
rescue Aws::S3::Errors::ServiceError => e
Rails.logger.error "S3 Error: #{e.message}"
render status: :internal_server_error
end
end
end

View File

@ -0,0 +1,18 @@
class StaticPagesController < ApplicationController
def home
@featured_cities = City.featured.limit(5)
@latest_arts = WeatherArt.latest.limit(6)
end
def help
end
def about
end
def contact
end
def demo
end
end

View File

@ -0,0 +1,90 @@
class UsersController < ApplicationController
include SessionsHelper
before_action :logged_in_user, only: [ :index, :edit, :update, :destroy ]
before_action :correct_user, only: [ :edit, :update ]
before_action :admin_user, only: [ :destroy ]
def index
# @users = User.all
# @users = User.order(:name).page(params[:page])
@users = User.where(activated: true).page(params[:page])
end
def show
@user = User.find(params[:id])
redirect_to root_url and return unless @user.activated?
# debugger
end
def new
@user = User.new
# debugger
end
def create
@user = User.new(user_params)
if @user.save
# reset_session
# log_in @user
# flash[:success] = "Welcome to the Sample App!"
# redirect_to @user
# redirect_to user_url(@user)
begin
@user.send_activation_email
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
rescue => e
logger.error "User creation failed: #{e.message}"
flash[:danger] = "Something went wrong. Please try again."
@user.destroy
render "new", status: :unprocessable_entity
end
else
render "new", status: :unprocessable_entity
end
end
def edit
@user = User.find(params[:id])
end
def update
@user = User.find(params[:id])
if @user.update(user_params)
flash[:success] = "Profile updated"
redirect_to @user
# redirect_to user_url(@user)
else
render "edit"
end
end
def destroy
User.find(params[:id]).destroy
flash[:success] = "User deleted"
redirect_to users_url
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
def admin_user
redirect_to(root_url) unless current_user.admin?
end
end

View File

@ -1,36 +0,0 @@
class WeatherArtsController < ApplicationController
def show
@city = City.friendly.find(params[:city_id])
@weather_art = @city.weather_arts.friendly.find(params[:slug])
@previous_weather_art = @city.weather_arts
.where("id < ?", @weather_art.id)
.order(id: :desc)
.first
@next_weather_art = @city.weather_arts
.where("id > ?", @weather_art.id)
.order(id: :asc)
.first
ahoy.track "View Weather Art", {
weather_art_id: @weather_art.id,
city_id: @weather_art.city_id,
event_type: "weather_art_view"
}
ahoy.track "View City", {
city_id: @city.id,
name: @city.name,
event_type: "city_view"
}
set_meta_tags(
title: "#{@city.name} Weather Art - #{@weather_art.weather_date.strftime('%B %d, %Y')}",
description: "#{@city.name}'s weather visualized through AI art. #{@weather_art.description} at #{@weather_art.temperature}°C.",
keywords: "#{@city.name}, weather art, #{@weather_art.description}, AI visualization",
og: {
image: @weather_art.image.attached? ? url_for(@weather_art.image) : nil
}
)
end
end

View File

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

View File

@ -1,24 +1,10 @@
module ApplicationHelper module ApplicationHelper
def weather_art_schema(weather_art) def full_title(page_title = "")
{ base_title = "Ruby on Rails Tutorial Sample App"
"@context": "https://schema.org", if page_title.empty?
"@type": "ImageObject", base_title
"name": "#{weather_art.city.name} Weather Art", else
"description": weather_art.description, page_title + " | " + base_title
"datePublished": weather_art.created_at.iso8601, end
"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 end

View File

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

View File

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

View File

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

View File

@ -0,0 +1,53 @@
module SessionsHelper
def log_in(user)
session[:user_id] = user.id
# 防范会话重放攻击
session[:session_token] = user.session_token
end
def remember(user)
user.remember
cookies.permanent.encrypted[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
def current_user
if (user_id = session[:user_id])
user = User.find_by(id: user_id)
if user && session[:session_token] == user.session_token
@current_user = user
end
elsif (user_id = cookies.encrypted[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(:remember, cookies[:remember_token])
log_in user
@current_user = user
end
end
end
def logged_in?
!current_user.nil?
end
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
def log_out
forget(current_user)
reset_session
@current_user = nil
end
def current_user?(user)
user && user == current_user
end
def store_location
session[:forwarding_url] = request.original_url if
request.get? || request.head?
end
end

View File

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

View File

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

View File

@ -0,0 +1,8 @@
module UsersHelper
def gravatar_for(user, options = { size: 80 })
size = options[:size]
gravatar_id = Digest::MD5.hexdigest(user.email.downcase)
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
end

View File

@ -1,51 +0,0 @@
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

View File

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

View File

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

View File

@ -1,10 +1,4 @@
// Entry point for the build script in your package.json // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
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"

View File

@ -1,14 +1,13 @@
// This file is auto-generated by ./bin/rails stimulus:manifest:update // Import and register all your controllers from the importmap via controllers/**/*_controller
// Run that command whenever you add a new controller or create them with // import { application } from "controllers/application"
// ./bin/rails generate stimulus controllerName // import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
// eagerLoadControllersFrom("controllers", application)
import { application } from "./application" import { application } from "./application"
import ThemeController from "./theme_controller"
import ToastController from "./toast_controller"
import HelloController from "./hello_controller" import HelloController from "./hello_controller"
application.register("hello", HelloController) application.register("hello", HelloController)
application.register("theme", ThemeController)
import PhotoSwipeLightBoxController from "./photo_swipe_lightbox_controller" application.register("toast", ToastController)
console.log("ready to register photo-swipe")
application.register("photo-swipe-lightbox", PhotoSwipeLightBoxController)
console.log("successful to register photo-swipe")

View File

@ -1,42 +0,0 @@
import { Controller } from "@hotwired/stimulus"
import PhotoSwipeLightbox from 'photoswipe/lightbox'
import PhotoSwipe from 'photoswipe'
import 'photoswipe/dist/photoswipe.css'
export default class extends Controller {
static targets = ['image', 'gallery']
connect() {
this.initPhotoSwipeLightbox()
}
initPhotoSwipeLightbox() {
const lightbox = new PhotoSwipeLightbox({
gallery: this.galleryTarget,
children: 'a',
pswpModule: PhotoSwipe,
initialZoomInEndEvent: 'mousedown',
dataSource: (items) => {
return items.map((item) => ({
src: item.dataset.pswpSrc,
w: parseInt(item.dataset.pswpWidth, 10),
h: parseInt(item.dataset.pswpHeight, 10),
title: item.dataset.pswpCaption,
}))
},
padding: { top: 0, bottom: 0, left: 0, right: 0 }, // 自定义图片与页面边界的填充
closeOnScroll: false,
zoom: true, // 启用缩放功能
bgOpacity: 0.9, // 背景透明度
pswpUIOptions: {
arrowPrev: true,
arrowNext: true,
zoom: true, // 添加缩放按钮
fullscreen: true, // 添加全屏按钮
counter: true, // 显示当前图片编号
}
})
lightbox.init()
// console.log('PhotoSwipeLightbox instance:', lightbox);
}
}

View File

@ -0,0 +1,24 @@
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="theme"
export default class extends Controller {
static targets = ["toggle"]
connect() {
this.initTheme()
}
initTheme() {
const savedTheme = localStorage.getItem('theme')
if(savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme)
this.toggleTarget.checked = savedTheme === 'dark'
}
}
toggle(event) {
const newTheme = event.target.checked ? 'dark' : 'light'
document.documentElement.setAttribute('data-theme', newTheme)
localStorage.setItem('theme', newTheme)
}
}

View File

@ -0,0 +1,14 @@
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="toast"
export default class extends Controller {
connect() {
// 3秒后自动隐藏
setTimeout(() => {
this.element.classList.add('animate-fade-out')
setTimeout(() => {
this.element.remove()
}, 500)
}, 3000)
}
}

View File

@ -0,0 +1,22 @@
// 获取切换按钮
const themeToggle = document.querySelector('.theme-controller');
// 监听变化
themeToggle.addEventListener('change', (e) => {
// 切换 HTML data-theme 属性
if(e.target.checked) {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.setAttribute('data-theme', 'light');
}
// 保存主题设置到 localStorage
localStorage.setItem('theme', e.target.checked ? 'dark' : 'light');
});
// 页面加载时检查之前的主题设置
const savedTheme = localStorage.getItem('theme');
if(savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
themeToggle.checked = savedTheme === 'dark';
}

View File

@ -0,0 +1,7 @@
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked
# Most jobs are safe to ignore if the underlying records are no longer available
# discard_on ActiveJob::DeserializationError
end

View File

@ -1,4 +1,4 @@
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
default from: "from@example.com" default from: ENV.fetch("RAILS_SMTP_USERNAME", "noreply@mail.frytea.com")
layout "mailer" layout "mailer"
end end

View File

@ -0,0 +1,21 @@
class UserMailer < ApplicationMailer
# Subject can be set in your I18n file at config/locales/en.yml
# with the following lookup:
#
# en.user_mailer.account_activation.subject
#
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end
# Subject can be set in your I18n file at config/locales/en.yml
# with the following lookup:
#
# en.user_mailer.password_reset.subject
#
def password_reset(user)
@user = user
mail to: user.email, subject: "Password reset"
end
end

View File

@ -1,11 +0,0 @@
class AdminUser < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable,
:recoverable, :rememberable, :validatable
def self.ransackable_attributes(auth_object = nil)
# 列出你想要允许搜索的属性
%w[created_at email id updated_at]
end
end

View File

@ -1,14 +0,0 @@
class Ahoy::Event < ApplicationRecord
# include Ahoy::QueryMethods
self.table_name = "ahoy_events"
belongs_to :visit
belongs_to :user, optional: true
serialize :properties, coder: JSON
def self.ransackable_attributes(auth_object = nil)
[ "id", "id_value", "name", "properties", "time", "user_id", "visit_id" ]
end
end

View File

@ -1,10 +0,0 @@
class Ahoy::Visit < ApplicationRecord
self.table_name = "ahoy_visits"
has_many :events, class_name: "Ahoy::Event"
belongs_to :user, optional: true
def self.ransackable_attributes(auth_object = nil)
[ "app_version", "browser", "city", "country", "device_type", "id", "ip", "landing_page", "latitude", "longitude", "os", "os_version", "platform", "referrer", "referring_domain", "region", "started_at", "user_agent", "user_id", "utm_campaign", "utm_content", "utm_medium", "utm_source", "utm_term", "visit_token", "visitor_token" ]
end
end

View File

@ -1,131 +1,13 @@
class City < ApplicationRecord class City < ApplicationRecord
extend FriendlyId has_many :weather_arts
friendly_id :slug_candidates, use: :slugged
belongs_to :country
has_many :weather_arts, dependent: :destroy scope :featured, -> { where(featured: true) }
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :city_id def current_weather_art
has_many :events, class_name: "Ahoy::Event", foreign_key: :city_id weather_arts.find_by(weather_date: Date.current)
delegate :region, to: :country
validates :name, presence: true
validates :latitude, presence: true
validates :longitude, presence: true
delegate :region, to: :country
scope :by_region, ->(region_id) { joins(:country).where(countries: { region_id: region_id }) }
scope :by_country, ->(country_id) { where(country_id: country_id) }
scope :active, -> { where(active: true) }
scope :by_popularity, -> {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count DESC")
else
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count DESC")
end
}
scope :least_popular_active, -> {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
active
.joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count ASC, cities.name ASC")
else
active
.joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count ASC, cities.name ASC")
end
}
scope :most_popular_inactive, -> {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
where(active: false)
.joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("COUNT(ahoy_events.id) DESC, cities.name ASC")
else
where(active: false)
.joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("COUNT(ahoy_events.id) DESC, cities.name ASC")
end
}
def to_s
name
end end
def slug_candidates def weather_art_for_date(date)
[ weather_arts.find_by(weather_date: date)
:name,
[ :country, :name ]
]
end
def localized_name
I18n.t("cities.#{name.parameterize.underscore}", default: name)
end
def full_name
"#{name}, #{country}"
end
def should_generate_new_friendly_id?
name_changed? || super
end
def self.ransackable_associations(auth_object = nil)
[ "weather_arts" ]
end
def self.ransackable_attributes(auth_object = nil)
[ "active", "country_id", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at" ]
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 end

View File

@ -1,26 +0,0 @@
class Country < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
belongs_to :region
has_many :cities, dependent: :restrict_with_error
validates :name, presence: true
validates :code, presence: true, uniqueness: true
def to_s
name
end
def localized_name
I18n.t("countries.#{code}")
end
def self.ransackable_attributes(auth_object = nil)
[ "code", "created_at", "id", "id_value", "name", "region_id", "slug", "updated_at" ]
end
def self.ransackable_associations(auth_object = nil)
[ "cities", "region" ]
end
end

View File

@ -1,28 +0,0 @@
class Region < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
has_many :countries, dependent: :restrict_with_error
has_many :cities, through: :countries
validates :name, presence: true
validates :code, presence: true, uniqueness: true
def to_s
name
end
def localized_name
I18n.t("regions.#{code}")
end
# 模型中允许被搜索的关联
def self.ransackable_associations(auth_object = nil)
[ "countries", "cities" ]
end
# 允许被搜索的属性列表
def self.ransackable_attributes(auth_object = nil)
[ "code", "created_at", "id", "id_value", "name", "slug", "updated_at" ]
end
end

102
app/models/user.rb Normal file
View File

@ -0,0 +1,102 @@
class User < ApplicationRecord
attr_accessor :remember_token, :activation_token, :reset_token
# before_save { self.email = email.downcase }
# before_save { email.downcase! }
before_save :downcase_email
before_create :create_activation_digest
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
def User.new_token
SecureRandom.urlsafe_base64
end
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
remember_digest
end
# 返回一个会话令牌,防止会话劫持
# 简单起见,直接使用记忆令牌
def session_token
remember_digest || remember
end
class << self
def digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
def new_token
SecureRandom.urlsafe_base64
end
end
# powered by metaprogramming
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
def forget
update_attribute(:remember_digest, nil)
end
def activate
# update_attribute(:activated, true)
# update_attribute(:activated_at, Time.zone.now)
update_columns(activated: true, activated_at: Time.zone.now)
end
def send_activation_email
begin
result = UserMailer.account_activation(self).deliver_now
Rails.logger.info "Email sent successfully: #{result.inspect}"
rescue => e
Rails.logger.error "Failed to send email: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
raise
end
end
def create_reset_digest
self.reset_token = User.new_token
update_columns(reset_digest: User.digest(reset_token),
reset_send_at: Time.zone.now)
end
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
def password_reset_expired?
reset_send_at < 2.hours.ago
end
private
def downcase_email
# self.email = email.downcase
email.downcase!
end
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end

View File

@ -1,58 +1,12 @@
class WeatherArt < ApplicationRecord class WeatherArt < ApplicationRecord
extend FriendlyId
friendly_id :weather_date, use: :slugged
belongs_to :city belongs_to :city
has_one_attached :image has_one_attached :image
has_one_attached :image_with_watermark
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id scope :latest, -> { order(created_at: :desc) }
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
validates :weather_date, presence: true
validates :city_id, presence: true
scope :by_popularity, -> {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.weather_art_id') = weather_arts.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'weather_art_view'")
.group("weather_arts.id")
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count DESC")
else
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'weather_art_id')::integer = weather_arts.id
AND ahoy_events.properties::jsonb->>'event_type' = 'weather_art_view'")
.group("weather_arts.id")
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count DESC")
end
}
def should_generate_new_friendly_id?
weather_date_changed? || city_id_changed? || super
end
def to_s
"#{city.name} - #{weather_date.strftime('%Y-%m-%d')}"
end
def self.ransackable_associations(auth_object = nil)
[ "city", "image_attachment", "image_blob" ]
end
def self.ransackable_attributes(auth_object = nil)
[ "city_id", "cloud", "created_at", "description", "feeling_temp", "humidity", "id", "id_value", "precipitation", "pressure", "prompt", "temperature", "updated_at", "visibility", "weather_date", "wind_scale", "wind_speed" ]
end
def view_count
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
Ahoy::Event.where("json_extract(properties, '$.event_type') = 'weather_art_view' AND json_extract(properties, '$.weather_art_id') = ?", self.id).count
else
Ahoy::Event.where("properties::jsonb->>'event_type' = 'weather_art_view' AND (properties::jsonb->>'weather_art_id')::integer = ?", self.id).count
end
end
def image_url def image_url
image.attached? ? image.blob : nil # 这里实现获取图片URL的逻辑可以是AWS S3或其他存储服务
Rails.application.routes.url_helpers.rails_blob_path(image, only_path: true) if image.attached?
end end
end end

View File

@ -1,64 +0,0 @@
class AiService
def initialize
@client = OpenAI::Client.new(
access_token: Rails.application.credentials.openai.token,
uri_base: Rails.application.credentials.openai.uri,
request_timeout: 240
)
end
def generate_prompt(city, weather_data)
response = @client.chat(
parameters: {
model: "gpt-4",
messages: [ {
role: "system",
content: "You are a professional artist creating prompts for DALL-E 3. Create realistic, artistic weather scenes featuring iconic landmarks."
}, {
role: "user",
content: generate_prompt_request(city, weather_data)
} ],
temperature: 0.7,
max_tokens: 300
}
)
response.dig("choices", 0, "message", "content")
end
def generate_image(prompt)
response = @client.images.generate(
parameters: {
model: "dall-e-3",
prompt: prompt,
size: "1792x1024",
quality: "standard",
n: 1
}
)
response.dig("data", 0, "url")
end
private
def generate_prompt_request(city, weather_data)
<<~PROMPT
Create a DALL-E 3 prompt for a weather scene in #{city.name}, #{city.country.name}.
Weather conditions:
- Temperature: #{weather_data[:temperature]}°C
- Weather: #{weather_data[:description]}
- Cloud cover: #{weather_data[:cloud]}%
- Time: #{weather_data[:time]}
Requirements:
- Feature iconic landmarks or architecture from #{city.name}
- Realistic style
- Weather conditions should be clearly visible
- Atmospheric and artistic composition
Generate a detailed, creative prompt that will produce a beautiful and realistic image.
PROMPT
end
end

View File

@ -1,38 +0,0 @@
# app/services/weather_service.rb
class WeatherService
include HTTParty
base_uri Rails.application.credentials.dig(:qweather, :uri)
def initialize
@api_key = Rails.application.credentials.qweather.token
end
def get_weather(latitude, longitude)
Rails.logger.debug "Get Weather for #{latitude},#{longitude}"
response = self.class.get(
"/weather/now",
headers: {
"X-QW-Api-Key" => "#{@api_key}"
},
query: {
location: "#{longitude},#{latitude}"
})
return nil unless response.success?
data = response["now"]
{
temperature: data["temp"].to_f,
feeling_temp: data["feelsLike"].to_f,
humidity: data["humidity"].to_f,
wind_scale: "#{data['windScale']}",
wind_speed: data["windSpeed"].to_f,
precipitation: data["precip"].to_f,
pressure: data["pressure"].to_f,
visibility: data["vis"].to_f,
cloud: data["cloud"].to_f,
description: data["text"],
time: response["updateTime"]
}
end
end

View File

@ -1,172 +0,0 @@
<!-- app/views/arts/index.html.erb -->
<div class="min-h-screen">
<!-- 页面标题和背景 -->
<% featured_art = @weather_arts.first %>
<div class="relative">
<!-- 背景图像 -->
<% if featured_art&.image&.attached? %>
<div class="absolute inset-0 h-[40vh] overflow-hidden">
<%= image_tag featured_art.image,
class: "w-full h-full object-cover" %>
<div class="absolute inset-0 bg-gradient-to-b from-base-100/30 via-base-100/60 to-base-100"></div>
</div>
<% end %>
<!-- 标题内容 -->
<div class="relative pt-20 pb-32">
<div class="container mx-auto px-4">
<div class="max-w-3xl mx-auto text-center space-y-6">
<h1 class="text-4xl md:text-5xl font-display font-bold">
Weather Arts Gallery
</h1>
<p class="text-xl text-base-content/70">
Discover AI-generated weather art from cities around the world
</p>
<!-- 如果有特色图片,显示其信息 -->
<% if featured_art %>
<div class="text-sm text-base-content/60 pt-4">
Latest from <%= featured_art.city.name %>, <%= featured_art.city.country.name %>
<span class="mx-2">•</span>
<%= featured_art.weather_date.strftime("%B %d, %Y") %>
</div>
<% end %>
</div>
</div>
</div>
</div>
<!-- 筛选导航 -->
<div class="container mx-auto px-4 -mt-8">
<div class="bg-base-100 shadow-xl rounded-box p-6 mb-12">
<!-- 筛选选项 -->
<div class="flex flex-wrap gap-4 justify-center items-center">
<!-- 时间排序 -->
<div class="dropdown">
<button class="btn btn-ghost gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<%= params[:sort] == 'oldest' ? 'Oldest First' : 'Newest First' %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
<li>
<%= link_to "Newest First", arts_path(sort: 'newest', region: params[:region]),
class: "#{'active' if params[:sort] != 'oldest'}" %>
</li>
<li>
<%= link_to "Oldest First", arts_path(sort: 'oldest', region: params[:region]),
class: "#{'active' if params[:sort] == 'oldest'}" %>
</li>
</ul>
</div>
<!-- 区域筛选 -->
<div class="dropdown">
<button class="btn btn-ghost gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<%= @current_region&.name || 'All Regions' %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
<li>
<%= link_to "All Regions", arts_path(sort: params[:sort]),
class: "#{'active' unless @current_region}" %>
</li>
<div class="divider my-1"></div>
<% @regions.each do |region| %>
<li>
<%= link_to region.name, arts_path(region: region.id, sort: params[:sort]),
class: "#{'active' if @current_region == region}" %>
</li>
<% end %>
</ul>
</div>
</div>
<!-- 结果统计 -->
<div class="text-center text-sm text-base-content/70 mt-4">
Showing <%= @weather_arts.total_count %> weather arts
<% if @current_region %>
from <%= @current_region.name %>
<% end %>
</div>
</div>
</div>
<div class="container mx-auto px-4 pb-16">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<% @weather_arts.each do |art| %>
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 group overflow-hidden">
<figure class="relative aspect-square overflow-hidden">
<% if art.image.attached? %>
<%= image_tag art.image,
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div class="absolute inset-0 p-6 flex flex-col justify-end translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300">
<div class="text-white space-y-2">
<h3 class="text-xl font-display font-bold">
<%= art.city.name %>
</h3>
<p class="text-sm text-white/80">
<%= art.city.country.name %>
</p>
<div class="flex items-center gap-2 text-white/90">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
</svg>
<%= art.description %>
</div>
</div>
</div>
<% end %>
</figure>
<!-- 信息部分 -->
<div class="card-body p-4">
<div class="flex justify-between items-start mb-3">
<div>
<h3 class="font-display font-bold leading-tight">
<%= art.city.name %>
</h3>
<p class="text-sm text-base-content/70">
<%= art.weather_date.strftime("%B %d, %Y") %>
</p>
</div>
<div class="text-right">
<div class="text-2xl font-bold text-primary">
<%= art.temperature %>°C
</div>
<div class="text-sm text-base-content/70">
<%= art.humidity %>% humidity
</div>
</div>
</div>
<%= link_to city_weather_art_path(art.city, art),
class: "btn btn-primary btn-sm w-full" do %>
View Details
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
<% end %>
</div>
</div>
<% end %>
</div>
<%= render 'shared/pagination',
collection: @weather_arts,
collection_name: 'weather arts' %>
</div>
</div>
</div>

View File

@ -1,126 +0,0 @@
<!-- app/views/cities/_city.html.erb -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 group overflow-hidden">
<% if city.latest_weather_art&.image&.attached? %>
<!-- 图片和主要信息区域 -->
<div class="relative">
<!-- 图片 -->
<figure class="aspect-[16/9] overflow-hidden">
<%= image_tag city.latest_weather_art.image,
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
</figure>
<!-- 渐变遮罩 - 使用多层渐变提供更好的文字可读性 -->
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent"></div>
<div class="absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-transparent"></div>
<!-- 温度显示 - 放在右上角 -->
<div class="absolute top-4 right-4 bg-black/30 backdrop-blur-sm rounded-xl px-3 py-2 text-white">
<div class="text-2xl font-bold leading-none">
<%= city.latest_weather_art.temperature %>°C
</div>
<div class="text-xs text-white/80">
Feels <%= city.latest_weather_art.feeling_temp %>°C
</div>
</div>
<!-- 城市名称和位置 - 放在底部 -->
<div class="absolute bottom-0 left-0 right-0 p-6">
<h3 class="text-2xl font-display text-white font-bold mb-2 drop-shadow-lg">
<%= city.name %>
</h3>
<div class="flex items-center gap-2 text-white/90 text-sm drop-shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
<%= city.country.name %>, <%= city.region.name %>
</div>
</div>
</div>
<!-- 底部信息区域 -->
<div class="card-body bg-base-100">
<!-- 天气信息和更新时间 -->
<div class="flex items-center justify-between text-sm text-base-content/70 mb-3">
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary/70" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
</svg>
<%= city.latest_weather_art.description %>
</div>
<div class="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<%= city.latest_weather_art.weather_date.strftime("%b %d, %Y") %>
</div>
</div>
<!-- 天气详情 -->
<div class="grid grid-cols-3 gap-2 mb-4">
<div class="stat bg-base-200/50 rounded-lg p-2">
<div class="stat-title text-xs">Humidity</div>
<div class="stat-value text-lg"><%= city.latest_weather_art.humidity %>%</div>
</div>
<div class="stat bg-base-200/50 rounded-lg p-2">
<div class="stat-title text-xs">Wind</div>
<div class="stat-value text-lg"><%= city.latest_weather_art.wind_scale %></div>
</div>
<div class="stat bg-base-200/50 rounded-lg p-2">
<div class="stat-title text-xs">Visibility</div>
<div class="stat-value text-lg"><%= city.latest_weather_art.visibility %>km</div>
</div>
</div>
<!-- 按钮 -->
<div class="card-actions justify-end">
<%= link_to city_path(city),
class: "btn btn-primary btn-sm gap-2" do %>
View Details
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
<% end %>
</div>
</div>
<% else %>
<!-- 无图片时的备用显示 -->
<div class="card-body">
<h3 class="card-title font-display text-2xl mb-3"><%= city.name %></h3>
<div class="flex items-center gap-2 text-base-content/70 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
<%= city.country.name %>, <%= city.region.name %>
</div>
<div class="bg-base-200 rounded-lg p-4 mb-4">
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary/70" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
<span>Lat: <%= city.latitude %></span>
</div>
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary/70" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
<span>Long: <%= city.longitude %></span>
</div>
</div>
</div>
<div class="card-actions justify-end">
<%= link_to city_path(city),
class: "btn btn-primary btn-sm gap-2" do %>
View Details
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
<% end %>
</div>
</div>
<% end %>
</div>

View File

@ -1,126 +1,20 @@
<!-- app/views/cities/index.html.erb --> <div class="container mx-auto px-4 py-8">
<div class="min-h-screen"> <h1 class="text-3xl font-bold mb-8">Cities</h1>
<!-- 页面标题和背景 -->
<% featured_art = WeatherArt.includes(:city).joins(:image_attachment).order(created_at: :desc).first %> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="relative bg-base-100"> <% @cities.each do |city| %>
<!-- 背景图像和渐变 --> <div class="card bg-base-100 shadow-xl">
<% if featured_art&.image&.attached? %> <div class="card-body">
<div class="absolute inset-0 h-[60vh] overflow-hidden"> <h2 class="card-title"><%= city.name %></h2>
<%= image_tag featured_art.image, <p><%= city.country %></p>
class: "w-full h-full object-cover object-center" %> <div class="card-actions justify-end">
<div class="absolute inset-0 bg-gradient-to-b from-base-100/40 via-base-100/80 to-base-100"></div> <%= link_to 'View Weather History', city_path(city),
class: "btn btn-primary" %>
</div>
</div>
</div> </div>
<% end %> <% end %>
<div class="relative pt-24 pb-32">
<div class="container mx-auto px-4">
<div class="max-w-3xl mx-auto text-center space-y-6">
<h1 class="text-5xl md:text-6xl font-display font-bold leading-tight">
Explore Cities
</h1>
<p class="text-xl md:text-2xl text-base-content/70 font-light max-w-2xl mx-auto">
Discover AI-generated weather art from cities around the world
</p>
<!-- 特色图片信息 -->
<% if featured_art %>
<div class="inline-block mt-6 px-4 py-2 bg-base-100/80 backdrop-blur-sm rounded-full text-sm">
Latest from
<span class="font-semibold"><%= featured_art.city.name %></span>,
<%= featured_art.city.country.name %>
<span class="mx-2">•</span>
<%= featured_art.weather_date.strftime("%B %d, %Y") %>
</div>
<% end %>
</div>
</div>
</div>
</div> </div>
<div class="sticky top-16 z-20 bg-base-100/95 backdrop-blur-sm border-b border-base-200"> <%= paginate @cities %>
<div class="container mx-auto px-4">
<div class="py-3 flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<div class="dropdown">
<button class="btn btn-ghost gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<%= @current_region&.name || 'All Regions' %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
<li>
<%= link_to cities_path,
class: "#{@current_region ? '' : 'active'}" do %>
All Regions
<% end %>
</li>
<div class="divider my-1"></div>
<% @regions.each do |region| %>
<li>
<%= link_to region.name,
cities_path(region: region.slug),
class: "#{@current_region == region ? 'active' : ''}" %>
</li>
<% end %>
</ul>
</div>
<% if @current_region %>
<div class="dropdown">
<button class="btn btn-ghost gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
</svg>
<%= @current_country&.name || "All Countries" %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
<li>
<%= link_to "All in #{@current_region.name}",
cities_path(region: @current_region.slug),
class: "#{@current_country ? '' : 'active'}" %>
</li>
<div class="divider my-1"></div>
<% @current_region.countries.order(:name).each do |country| %>
<li>
<%= link_to country.name,
cities_path(region: @current_region.slug, country: country.slug),
class: "#{@current_country == country ? 'active' : ''}" %>
</li>
<% end %>
</ul>
</div>
<% end %>
</div>
<div class="text-sm text-base-content/70">
<%= @cities.count %> <%= 'city'.pluralize(@cities.count) %>
<% if @current_country %>
in <%= @current_country.name %>
<% elsif @current_region %>
in <%= @current_region.name %>
<% end %>
</div>
</div>
</div>
</div>
<div class="container mx-auto px-4 py-8">
<div class="container mx-auto px-4 pb-16">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<%= render partial: 'city', collection: @cities %>
</div>
<%= render 'shared/pagination',
collection: @cities,
collection_name: 'cities' %>
</div>
</div>
</div> </div>

View File

@ -1,149 +1,130 @@
<div class="relative min-h-screen bg-base-200"> <!-- app/views/cities/show.html.erb -->
<!-- 背景效果 --> <div class="container mx-auto px-4 py-8">
<% if @city.latest_weather_art&.image&.attached? %> <!-- 城市信息头部 -->
<div class="fixed inset-0 -z-10"> <div class="bg-base-200 rounded-box p-8 mb-8">
<%= image_tag @city.latest_weather_art.image, <div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
class: "absolute w-full h-full object-cover scale-110 filter blur-2xl opacity-25" %> <div>
<div class="absolute inset-0 bg-gradient-to-b from-base-200/90 to-base-200/70 backdrop-blur-md"></div> <h1 class="text-4xl font-bold mb-2"><%= @city.name %></h1>
</div> <div class="flex items-center gap-2 text-base-content/70">
<% end %> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd" />
</svg>
<span><%= @city.country %></span>
</div>
</div>
<!-- 主要内容 --> <!-- 当前天气信息 -->
<div class="relative z-10"> <% if @current_weather_art = @city.current_weather_art %>
<!-- 返回导航 --> <div class="stats shadow">
<div class="container mx-auto px-4 py-6"> <div class="stat">
<%= link_to cities_path, <div class="stat-title">Temperature</div>
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 %> <div class="stat-value"><%= @current_weather_art.temperature %>°C</div>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> </div>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /> <div class="stat">
</svg> <div class="stat-title">Condition</div>
Back to Cities </div>
</div>
<% end %> <% end %>
</div> </div>
</div>
<!-- 城市信息头部 --> <!-- 日期筛选器 -->
<div class="container mx-auto px-4 mb-12"> <div class="flex flex-col md:flex-row gap-4 items-center justify-between mb-8">
<div class="max-w-4xl mx-auto text-center space-y-6"> <div class="flex items-center gap-4">
<h1 class="text-4xl md:text-6xl font-display font-bold"> <h2 class="text-2xl font-bold">Weather Art History</h2>
<span class="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"> <div class="badge badge-primary"><%= @weather_arts.total_count %> artworks</div>
<%= @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 class="badge badge-lg badge-secondary gap-2">
<%= @city.timezone.present? ? Time.current.in_time_zone(@city.timezone).strftime("%Y-%m-%d %H:%M") : "Timezone undefined" %>
</div>
</div>
<!-- 主要统计信息 -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-8">
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("temperature") %>
<div class="stat-title font-medium">Latest Weather</div>
</div>
<div class="stat-value text-2xl"><%= @city.latest_weather_art&.temperature %>°C</div>
<div class="stat-desc mt-1"><%= @city.latest_weather_art&.description %></div>
</div>
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("location") %>
<div class="stat-title font-medium">Coordinates</div>
</div>
<div class="stat-value text-xl">
<%= @city.latitude %>°N,
<%= @city.longitude %>°E
</div>
<div class="stat-desc mt-1">Geographical Location</div>
</div>
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("history") %>
<div class="stat-title font-medium">Records</div>
</div>
<div class="stat-value text-2xl"><%= @city.weather_arts.count %></div>
<div class="stat-desc mt-1">Total Weather Arts</div>
</div>
</div>
</div>
</div> </div>
<!-- 天气艺术历史记录 --> <%= form_tag city_path(@city), method: :get, class: "join" do %>
<div class="container mx-auto px-4 pb-16"> <%= date_field_tag :start_date, params[:start_date],
<div class="max-w-7xl mx-auto space-y-8"> class: "join-item input input-bordered w-full max-w-xs",
<!-- 标题和更新时间 --> placeholder: "Start Date" %>
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4"> <%= date_field_tag :end_date, params[:end_date],
<h2 class="text-3xl font-display font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"> class: "join-item input input-bordered w-full max-w-xs",
Weather Art History placeholder: "End Date" %>
</h2> <%= submit_tag "Filter", class: "join-item btn btn-primary" %>
<div class="card bg-base-100/80 backdrop-blur-sm shadow-lg p-4"> <% end %>
</div>
<!-- 天气艺术网格 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<% @weather_arts.each do |art| %>
<div class="card bg-base-100 shadow-xl group hover:scale-105 transition-transform duration-200"
data-controller="modal"
data-action="click->modal#open">
<!-- 卡片主体 -->
<figure class="relative">
<%= image_tag art.image_url,
class: "w-full h-64 object-cover" %>
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/70 to-transparent text-white">
<div class="text-lg font-bold"><%= art.weather_date.strftime("%B %d, %Y") %></div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <span><%= art.temperature %>°C</span>
<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" /> <span class="text-xs">|</span>
</svg> <span><%= art.description %></span>
<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>
</div> </figure>
<!-- 天气艺术卡片网格 --> <!-- 模态框内容 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <dialog id="modal_<%= art.id %>" class="modal">
<% @city.weather_arts.order(weather_date: :desc).each do |art| %> <div class="modal-box max-w-4xl">
<div class="card bg-base-100/80 backdrop-blur-sm shadow-xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1"> <h3 class="font-bold text-lg mb-4">
<figure class="relative aspect-video overflow-hidden"> Weather Art - <%= art.weather_date.strftime("%B %d, %Y") %>
<% if art.image.attached? %> </h3>
<%= image_tag art.image, <%= image_tag art.image_url, class: "w-full rounded-lg mb-4" %>
class: "w-full h-full object-cover transform hover:scale-105 transition-transform duration-500" %> <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/70 to-transparent"> <div class="stat bg-base-200 rounded-box">
<div class="flex items-center justify-between text-white"> <div class="stat-title">Temperature</div>
<div class="text-2xl font-bold"><%= art.temperature %>°C</div> <div class="stat-value text-lg"><%= art.temperature %>°C</div>
<div class="text-right"> </div>
<div class="font-medium"><%= art.weather_date.strftime("%H:%M") %></div> <div class="stat bg-base-200 rounded-box">
<div class="text-sm opacity-80"><%= art.weather_date.strftime("%B %d, %Y") %></div> <div class="stat-title">Humidity</div>
</div> <div class="stat-value text-lg"><%= art.humidity %>%</div>
</div> </div>
</div> <div class="stat bg-base-200 rounded-box">
<% end %> <div class="stat-title">Wind Speed</div>
</figure> <div class="stat-value text-lg"><%= art.wind_speed %></div>
</div>
<div class="card-body"> <div class="stat bg-base-200 rounded-box">
<h3 class="card-title font-display"> <div class="stat-title">Condition</div>
<%= weather_description_icon(art.description) %> <div class="stat-value text-lg"><%= art.description %></div>
<%= art.description %>
</h3>
<div class="grid grid-cols-2 gap-4 my-4">
<div class="flex items-center gap-2 text-sm">
<%= weather_stat_icon("humidity") %>
<span>Humidity: <%= art.humidity %>%</span>
</div>
<div class="flex items-center gap-2 text-sm">
<%= weather_stat_icon("wind") %>
<span>Wind: <%= art.wind_scale %></span>
</div>
</div>
<%= link_to city_weather_art_path(@city, art),
class: "btn btn-primary btn-block" do %>
View Details
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<% end %>
</div> </div>
</div> </div>
<% end %> <div class="bg-base-200 p-4 rounded-box mb-4">
</div> <h4 class="font-bold mb-2">AI Prompt</h4>
<p class="text-sm"><%= art.prompt %></p>
</div>
<div class="modal-action">
<form method="dialog">
<button class="btn">Close</button>
</form>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</div> </div>
</div> <% end %>
</div> </div>
</div>
<!-- 分页 -->
<div class="flex justify-center">
<%= paginate @weather_arts %>
</div>
</div>
<!-- Stimulus Controller for Modal -->
<%# app/javascript/controllers/modal_controller.js %>
<script type="module">
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
open(event) {
const modalId = this.element.querySelector('dialog').id
const modal = document.getElementById(modalId)
modal.showModal()
}
}
</script>

View File

@ -1,68 +0,0 @@
<div>
<!-- 首屏展示区 -->
<section class="h-screen-90 relative overflow-hidden">
<% if @featured_arts.first&.image&.attached? %>
<div class="absolute inset-0">
<%= image_tag @featured_arts.first.image, class: "w-full h-full object-cover" %>
<div class="absolute inset-0 bg-gradient-to-r from-base-100/90 to-base-100/50"></div>
</div>
<% end %>
<div class="container mx-auto px-4 h-full flex items-center relative">
<div class="max-w-2xl space-y-6">
<h1 class="text-5xl md:text-6xl font-display font-bold leading-tight">
Where Weather Meets<br>Artificial Intelligence
</h1>
<p class="text-xl text-base-content/70 font-sans">
Experience weather through the lens of AI-generated art,
bringing a new perspective to daily meteorological phenomena.
</p>
<%= link_to "Explore Cities", cities_path,
class: "btn btn-primary btn-lg mt-8 font-sans" %>
</div>
</div>
</section>
<!-- 最新天气艺术 -->
<section class="container mx-auto px-4 py-16 space-y-12">
<h2 class="text-3xl font-display font-bold text-center">Shuffle Latest Weather Art</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<% @latest_arts.each do |art| %>
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow duration-300">
<figure class="relative aspect-[4/3] overflow-hidden">
<% if art.image.attached? %>
<%= image_tag art.image, class: "w-full h-full object-cover transform hover:scale-105 transition-transform duration-500" %>
<% end %>
</figure>
<div class="card-body">
<div class="flex justify-between items-start">
<div>
<h3 class="card-title font-display"><%= art.city.name %></h3>
<p class="text-base-content/70">
<%= art.weather_date.strftime("%B %d, %Y") %>
</p>
</div>
<div class="text-right">
<div class="text-2xl font-bold"><%= art.temperature %>°C</div>
<div class="text-sm text-base-content/70"><%= art.description %></div>
</div>
</div>
<div class="card-actions justify-end mt-4">
<%= link_to "View Details", city_weather_art_path(art.city, art),
class: "btn btn-primary btn-outline" %>
</div>
</div>
</div>
<% end %>
</div>
</section>
</div>
<div class="text-center mt-12 mb-12">
<%= link_to arts_path, class: "btn btn-primary btn-lg gap-2" do %>
View All Weather Arts
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
<% end %>
</div>

View File

@ -1,11 +1,3 @@
<%# Link to the "First" page <button class="join-item btn">
- available local variables <%= link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote %>
url: url to the first page </button>
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>

View File

@ -1,8 +1,3 @@
<%# Non-link tag that stands for skipped pages... <button class="join-item btn btn-disabled">
- available local variables <%= content_tag :a, raw(t 'views.pagination.truncate') %>
current_page: a page object for the currently displayed page </button>
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>

View File

@ -1,11 +1,3 @@
<%# Link to the "Last" page <button class="join-item btn">
- available local variables <%= link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, { remote: remote } %>
url: url to the last page </button>
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>

View File

@ -1,11 +1,3 @@
<%# Link to the "Next" page <button class="join-item btn">
- available local variables <%= link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, rel: 'next', remote: remote %>
url: url to the next page </button>
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>

View File

@ -1,12 +1,9 @@
<%# Link showing page number <% if page.current? %>
- available local variables <button class='join-item btn btn-active'>
page: a page object for "this" page <%= content_tag :a, page, data: { remote: remote }, rel: page.rel %>
url: url to this page </button>
current_page: a page object for the currently displayed page <% else %>
total_pages: total number of pages <button class="join-item btn">
per_page: number of items to fetch per page <%= link_to page, url, remote: remote, rel: page.rel %>
remote: data-remote </button>
-%> <% end %>
<span class="page<%= ' current' if page.current? %>">
<%= link_to_unless page.current?, page, url, {remote: remote, rel: page.rel} %>
</span>

View File

@ -1,25 +1,15 @@
<%# 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 -%> <%= paginator.render do -%>
<nav class="pagination" role="navigation" aria-label="pager"> <div class="join pagination">
<%= first_page_tag unless current_page.first? %> <%= first_page_tag unless current_page.first? %>
<%= prev_page_tag unless current_page.first? %> <%= prev_page_tag unless current_page.first? %>
<% each_page do |page| -%> <% each_page do |page| -%>
<% if page.display_tag? -%> <% if page.left_outer? || page.right_outer? || page.inside_window? -%>
<%= page_tag page %> <%= page_tag page %>
<% elsif !page.was_truncated? -%> <% elsif !page.was_truncated? -%>
<%= gap_tag %> <%= gap_tag %>
<% end -%> <% end -%>
<% end -%> <% end -%>
<% unless current_page.out_of_range? %> <%= next_page_tag unless current_page.last? %>
<%= next_page_tag unless current_page.last? %> <%= last_page_tag unless current_page.last? %>
<%= last_page_tag unless current_page.last? %> </div>
<% end %>
</nav>
<% end -%> <% end -%>

View File

@ -1,11 +1,3 @@
<%# Link to the "Previous" page <button class="join-item btn">
- available local variables <%= link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote %>
url: url to the previous page </button>
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>

View File

@ -0,0 +1,8 @@
<footer class="footer p-10 bg-neutral text-neutral-content">
<div>
<span class="footer-title">AI Weather Art</span>
<p>Daily weather-inspired AI artwork for cities worldwide</p>
<p>Copyright © <%= Date.current.year %> - All right reserved by ACME Industries Ltd</p>
</div>
</footer>

View File

@ -0,0 +1,90 @@
<!-- 固定在顶部容器 -->
<div class="fixed top-0 left-0 right-0 z-50">
<!-- 响应式内边距 -->
<!--<div class="container mx-auto px-3 sm:px-6 lg:px-1 py-4">-->
<!-- 顶部菜单栏 -->
<div class="navbar
backdrop-filter backdrop-blur-lg bg-opacity-30 border-b border-gray-200 border-transparent
shadow-md min-h-0 h-12 px-8 lg:px-12">
<div class="navbar-start">
<%= link_to "Today AI Weather", root_url, id: "logo", class: "btn btn-ghost text-xl" %>
</div>
<div class="navbar-end">
<div class="hidden lg:flex">
<ul class="menu menu-horizontal px-1">
<li><%= link_to "Home", root_url %></li>
<li><%= link_to "Help", help_url %></li>
<li><%= link_to 'Calendar', "#" %></li>
<li><%= link_to 'Cities', "#" %></li>
<% if logged_in? %>
<li><%= link_to "Users", users_path %></li>
<li>
<details>
<summary>
Account <b class="caret"></b>
</summary>
<ul class="bg-base-100 rounded-t-none p-2">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", edit_user_path(current_user) %></li>
<div class="divider"></div>
<li>
<%= link_to "Log out", logout_path, data: { turbo_method: :delete } %>
</li>
</ul>
</details>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</div>
<details class="dropdown dropdown-end">
<summary tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<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="M4 6h16M4 12h8m-8 6h16" />
</svg>
</summary>
<ul
tabindex="0"
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow">
<li><%= link_to "Home", root_url %></li>
<li><%= link_to "Help", help_url %></li>
<li><%= link_to 'Cities', "#" %></li>
<li><%= link_to 'Calendar', "#" %></li>
<% if logged_in? %>
<li><%= link_to "Users", users_path %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", edit_user_path(current_user) %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, data: { turbo_method: :delete } %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</details>
<%= render "layouts/theme_swap" %>
</div>
</div>
<!--</div>-->
</div>
<!-- 添加一个占位 div防止内容被覆盖 -->
<div aria-hidden="true" class="border-none h-12"></div>

View File

@ -0,0 +1,37 @@
<!-- 放在布局文件的 body 标签末尾 -->
<div class="toast toast-end">
<% flash.each do |message_type, message| %>
<% alert_class = case message_type
when "success" then "alert alert-success"
when "danger", "error" then "alert alert-error"
when "warning" then "alert alert-warning"
when "info" then "alert alert-info"
else "alert"
end
%>
<div data-controller="toast" class="<%= alert_class %> animate-slide-in-right">
<div>
<% case message_type %>
<% when "success" %>
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<% when "error", "danger" %>
<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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<% when "warning" %>
<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 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<% 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>
<% end %>
<span><%= message %></span>
</div>
</div>
<% end %>
</div>

View File

@ -0,0 +1,21 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= yield :head %>
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
<link rel="icon" href="/icon.png" type="image/png">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.png">
<%# Includes all stylesheet files in app/assets/stylesheets %>
<%#= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%#= stylesheet_link_tag 'application', media: 'all', 'data-turbo-track': 'reload' %>
<%#= javascript_pack_tag 'application', 'data-turbo-track': 'reload' %>
<%#= javascript_importmap_tags %>

View File

@ -0,0 +1,4 @@
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
</script>
<![endif]-->

View File

@ -0,0 +1,29 @@
<div data-controller="theme">
<label class="swap swap-rotate">
<!-- this hidden checkbox controls the state -->
<input
type="checkbox"
class="theme-controller"
data-theme-target="toggle"
data-action="change->theme#toggle"
/>
<!-- sun icon -->
<svg
class="swap-off h-6 w-6 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
<!-- moon icon -->
<svg
class="swap-on h-6 w-6 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
</svg>
</label>
</div>

View File

@ -1,98 +1,54 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title><%= content_for(:title) || "Today Ai Weather" %></title> <!-- <title><%#= content_for(:title) || "Sample App" %></title>-->
<meta name="viewport" content="width=device-width,initial-scale=1"> <title><%= full_title(yield(:title)) %></title>
<meta name="apple-mobile-web-app-capable" content="yes"> <%= render 'layouts/rails_default' %>
<meta name="mobile-web-app-capable" content="yes"> <%= render 'layouts/shim' %>
<%= display_meta_tags( <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
site: 'TodayAIWeather',
reverse: true,
og: {
site_name: 'TodayAIWeather',
type: 'website',
url: request.original_url
},
alternate: {
"zh-CN" => url_for(locale: 'zh-CN'),
"en" => url_for(locale: 'en')
}
) %>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= yield :head %>
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
<link rel="icon" href="/icon.png" type="image/png">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.png">
<%# 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" %>
<script defer data-domain="todayaiweather.com" src="https://plausible.frytea.com/js/script.js"></script>
<script defer src="https://busuanzi.frytea.com/js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-PX1C92V5L7"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-PX1C92V5L7');
</script>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7296634171837358"
crossorigin="anonymous"></script>
</head> </head>
<body class="min-h-screen bg-base-100 font-sans"> <body>
<!-- 导航栏 --> <%= render 'layouts/header' %>
<div class="navbar bg-base-100/80 backdrop-blur-sm fixed top-0 z-50"> <div class="">
<div class="container mx-auto"> <%= render 'layouts/message' %>
<div class="flex-1"> <%= yield %>
<%= link_to root_path, class: "text-2xl font-display font-bold hover:text-primary transition-colors" do %> <%= render 'layouts/footer' %>
Today AI Weather <%#= debug(params) if Rails.env.development? %>
<% end %> <%#= debug(params.to_yaml) if Rails.env.development? %>
</div> <%#= debug(session) if Rails.env.development? %>
<div class="flex-none"> <%#= debug(Time.now) if Rails.env.development? %>
<%= 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>
<!-- 主要内容 --> <% if Rails.env.development? %>
<main class="pt-16"> <div class="container mx-auto px-4 mb-8 pt-4">
<%= yield %> <div class="max-w-4xl mx-auto border-2 border-gray-300 rounded-lg p-6 bg-gray-100">
</main> <!-- Params -->
<div class="mb-6">
<h3 class="text-gray-700 font-bold mb-2">Params:</h3>
<pre class="bg-white p-4 rounded shadow overflow-x-auto text-sm text-gray-600 border border-gray-200">
<%= params.to_yaml %>
</pre>
</div>
<!-- 页脚 --> <!-- Session -->
<footer class="footer footer-center p-8 bg-base-200 text-base-content"> <div class="mb-6">
<div class="container mx-auto flex flex-col gap-2"> <h3 class="text-gray-700 font-bold mb-2">Session:</h3>
<div id="busuanzi_container" class="text-xs opacity-70"> <pre class="bg-white p-4 rounded shadow overflow-x-auto text-sm text-gray-600 border border-gray-200">
<div class="space-x-2"> <%= session %>
<span>Page Views: <span id="busuanzi_page_pv"></span></span> </pre>
<span>|</span> </div>
<span>Page Visitors: <span id="busuanzi_page_uv"></span></span>
<span>|</span> <!-- Time -->
<span>Total Views: <span id="busuanzi_site_pv"></span></span> <div>
<span>|</span> <h3 class="text-gray-700 font-bold mb-2">Current Time:</h3>
<span>Total Visitors: <span id="busuanzi_site_uv"></span></span> <pre class="bg-white p-4 rounded shadow overflow-x-auto text-sm text-gray-600 border border-gray-200">
<%= Time.now %>
</pre>
</div>
</div>
</div> </div>
</div> <% end %>
<p class="font-display opacity-80">
Copyright © 2024 - All rights reserved by AI Weather Art
</p>
</div> </div>
</footer>
</body> </body>
</html> </html>

View File

@ -0,0 +1,21 @@
<% provide(:title, "Reset password") %>
<h1>Reset password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_with(model: @user, url: password_reset_path(params[:id]),
local: true) do |f| %>
<%= render 'shared/error_messages' %>
<%= hidden_field_tag :email, @user.email %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: "form-control" %>
<%= f.submit "Update password", class: "btn btn-primary" %>
<% end %>
</div>
</div>

View File

@ -0,0 +1,14 @@
<% provide(:title, "Forget password") %>
<h1>Forget password</h1>
<div class="row">
<div class="vol-md-6 col-md-offset-3">
<%= form_with(url: password_resets_path, scope: :password_reset,
local: true) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: "form-control" %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
</div>
</div>

View File

@ -1,5 +1,5 @@
{ {
"name": "TodayAiWeather", "name": "SampleApp",
"icons": [ "icons": [
{ {
"src": "/icon.png", "src": "/icon.png",
@ -16,7 +16,7 @@
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"scope": "/", "scope": "/",
"description": "TodayAiWeather.", "description": "SampleApp.",
"theme_color": "red", "theme_color": "red",
"background_color": "red" "background_color": "red"
} }

View File

@ -0,0 +1,45 @@
<% provide(:title, "Log in") %>
<div class ="container mx-auto px-4">
<h1 class="text-3xl font-bold text-center my-8">Log in</h1>
<div class="max-w-md mx-auto">
<%= form_with(url: login_path, scope: :session, local: true, class: "space-y-4") do |f| %>
<div class="form-control">
<%= f.label :email, class: "label" do %>
<span class="label-text">Email</span>
<% end %>
<%= f.email_field :email, class: "input input-bordered w-full" %>
</div>
<div class="form-control">
<div class="flex justify-between items-center">
<%= f.label :password, class: "label" do %>
<span class="label-text">Password</span>
<% end %>
<%= link_to "(forgot password)", new_password_reset_path,
class: "text-sm text-primary hover:text-primary-focus" %>
</div>
<%= f.password_field :password, class: "input input-bordered w-full" %>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<%= f.check_box :remember_me, class: "checkbox checkbox-primary" %>
<span class="label-text ml-2">Remember me on this computer</span>
</label>
</div>
<div class="form-control mt-6">
<%= f.submit "Log in", class: "btn btn-primary w-full" %>
</div>
<% end %>
<div class="text-center mt-6">
<p>New user?
<%= link_to "Sign up now!", signup_path,
class: "text-primary hover:text-primary-focus" %>
</p>
</div>
</div>
</div>

View File

@ -0,0 +1,12 @@
<% if @user.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(@user.errors.count, "error") %>.
</div>
<ul>
<% @user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>

View File

@ -1,86 +0,0 @@
<%# app/views/shared/_pagination.html.erb %>
<% if collection.total_pages > 1 %>
<div class="flex flex-col items-center mt-16 gap-6">
<!-- 页码信息 -->
<div class="text-base-content/70 font-light">
<span class="px-4 py-2 bg-base-200/50 rounded-full">
Page <%= collection.current_page %> of <%= collection.total_pages %>
</span>
</div>
<!-- 分页控件 -->
<div class="join shadow-lg">
<!-- 首页 -->
<%= link_to url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-xs #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
<% end %>
<!-- 上一页 -->
<%= link_to url_for(page: collection.prev_page || 1, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-xs #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
<% end %>
<!-- 页码 -->
<% page_window = 2 # 当前页面前后显示的页码数 %>
<% start_page = [1, collection.current_page - page_window].max %>
<% end_page = [collection.total_pages, collection.current_page + page_window].min %>
<% if start_page > 1 %>
<%= link_to 1, url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-xs btn-ghost hover:bg-primary/5" %>
<% if start_page > 2 %>
<button class="join-item btn btn-xs btn-ghost btn-disabled">...</button>
<% end %>
<% end %>
<% (start_page..end_page).each do |page| %>
<% if page == collection.current_page %>
<button class="join-item btn btn-xs btn-ghost bg-primary/10 font-medium">
<%= page %>
</button>
<% else %>
<%= link_to page, url_for(page: page, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-xs btn-ghost hover:bg-primary/5" %>
<% end %>
<% end %>
<% if end_page < collection.total_pages %>
<% if end_page < collection.total_pages - 1 %>
<button class="join-item btn btn-xs btn-ghost btn-disabled">...</button>
<% end %>
<%= link_to collection.total_pages,
url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-xs btn-ghost hover:bg-primary/5" %>
<% end %>
<!-- 下一页 -->
<%= link_to url_for(page: collection.next_page || collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-xs #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<% end %>
<!-- 末页 -->
<%= link_to url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-xs #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
<% end %>
</div>
<!-- 结果统计 -->
<div class="text-sm text-base-content/60 font-light">
Showing <%= collection.offset_value + 1 %> to
<%= collection.last_page? ? collection.total_count : collection.offset_value + collection.limit_value %>
of <%= collection.total_count %> <%= collection_name || 'items' %>
</div>
</div>
<% end %>

View File

@ -0,0 +1,11 @@
<% provide(:title, "About") %>
<h1>About</h1>
<p>
The <a href="https://www.railstutorial.org/"><em>Ruby on Rails Tutorial</em></a>
, part of the <a href="https://www.learnenough.com/">Learn Enough</a> family of
tutorials, is a <a href="https://www.railstutorial.org/book">book</a> and
<a href="https://screencasts.railstutorial.org/">screencast series</a>
to teach web development with <a href="https://rubyonrails.org/">Ruby on Rails</a>.
This is the sample app for the tutorial.
</p>

View File

@ -0,0 +1,8 @@
<% provide(:title, "Contact") %>
<h1>Contact</h1>
<p>
Contact the Ruby on Rails Tutorial about the sample app at the
<a href="https://www.railstutorial.org/contact">contact page</a>.
</p>

Some files were not shown because too many files have changed in this diff Show More