chore: init commit
in worker.../repo/GITFOLDER.zip is the .git folder.
@@ -0,0 +1,3 @@
|
||||
---
|
||||
allow_single_quoted_attributes: true
|
||||
allow_unquoted_attributes: false
|
||||
37
worker-toolkit-stocks-in-the-future/repo/.dockerignore
Normal file
@@ -0,0 +1,37 @@
|
||||
# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files.
|
||||
|
||||
# Ignore git directory.
|
||||
/.git/
|
||||
|
||||
# Ignore bundler config.
|
||||
/.bundle
|
||||
|
||||
# Ignore all environment files (except templates).
|
||||
/.env*
|
||||
!/.env*.erb
|
||||
|
||||
# Ignore all default key files.
|
||||
/config/master.key
|
||||
/config/credentials/*.key
|
||||
|
||||
# Ignore all logfiles and tempfiles.
|
||||
/log/*
|
||||
/tmp/*
|
||||
!/log/.keep
|
||||
!/tmp/.keep
|
||||
|
||||
# Ignore pidfiles, but keep the directory.
|
||||
/tmp/pids/*
|
||||
!/tmp/pids/.keep
|
||||
|
||||
# Ignore storage (uploaded files in development and any SQLite databases).
|
||||
/storage/*
|
||||
!/storage/.keep
|
||||
/tmp/storage/*
|
||||
!/tmp/storage/.keep
|
||||
|
||||
# Ignore assets.
|
||||
/node_modules/
|
||||
/app/assets/builds/*
|
||||
!/app/assets/builds/.keep
|
||||
/public/assets
|
||||
33
worker-toolkit-stocks-in-the-future/repo/.erb_lint.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
EnableDefaultLinters: true
|
||||
|
||||
glob: "**/*.{html,text,js}{+*,}.erb"
|
||||
exclude:
|
||||
- '**/vendor/**/*'
|
||||
- '**/node_modules/**/*'
|
||||
linters:
|
||||
ErbSafety:
|
||||
enabled: true
|
||||
better_html_config: .better-html.yml
|
||||
|
||||
Rubocop:
|
||||
enabled: true
|
||||
rubocop_config:
|
||||
inherit_from:
|
||||
- .rubocop.yml
|
||||
Layout/InitialIndentation:
|
||||
Enabled: false
|
||||
Layout/LineLength:
|
||||
Enabled: false
|
||||
Layout/TrailingEmptyLines:
|
||||
Enabled: false
|
||||
Naming/FileName:
|
||||
Enabled: false
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
Style/StringLiterals:
|
||||
Enabled: false
|
||||
Lint/UselessAssignment:
|
||||
Enabled: false
|
||||
Rails/OutputSafety:
|
||||
Enabled: false
|
||||
12
worker-toolkit-stocks-in-the-future/repo/.gitattributes
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# See https://git-scm.com/docs/gitattributes for more about git attribute files.
|
||||
|
||||
# Mark the database schema as having been generated.
|
||||
db/schema.rb linguist-generated
|
||||
|
||||
# Mark any vendored files as having been vendored.
|
||||
vendor/* linguist-vendored
|
||||
config/credentials/*.yml.enc diff=rails_credentials
|
||||
config/credentials.yml.enc diff=rails_credentials
|
||||
|
||||
# See https://bun.sh/docs/install/lockfile
|
||||
*.lockb diff=lockb
|
||||
18
worker-toolkit-stocks-in-the-future/repo/.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
# Summary
|
||||
|
||||
Describe an overview of the problem. Provide a general description of the problem it's solving or why this issue should be addressed.
|
||||
|
||||
## Things out of Scope
|
||||
> **OPTIONAL** List here work that is related and shouldn't be done as part of this issue. This can be work we've decided to never do, or it can be work we've decided to put in a separate issue.
|
||||
|
||||
## Things to Consider
|
||||
> **OPTIONAL** If you happen to know that this particular issue will touch other parts of the application in non-obvious ways (maybe there are side effects, or there are other parts of the application that depend on the thing that needs to be changed), you can list them out here. This is an opportunity to share *domain knowledge* that you might have about the problemset.
|
||||
|
||||
# Criteria for Completion
|
||||
> This should be a bulleted list of items that, if satisfied, would result in an acceptably complete Pull Request. Be as specific as you need to be. For example:
|
||||
|
||||
- [ ] When a user clicks on {this} it should now do {that}
|
||||
- [ ] This feature should only be available to admins
|
||||
- [ ] Add a test proving that it works
|
||||
|
||||
19
worker-toolkit-stocks-in-the-future/repo/.github/ISSUE_TEMPLATE/issue_template.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Issue Template
|
||||
description: Report an issue or request a feature
|
||||
title: "Issue: "
|
||||
labels: ["needs-triage"]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What's the issue?
|
||||
placeholder: Describe the problem or feature you'd like
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional details
|
||||
placeholder: Steps to reproduce (for bugs) or more context (for features)
|
||||
validations:
|
||||
required: true
|
||||
11
worker-toolkit-stocks-in-the-future/repo/.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "bundler" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
50
worker-toolkit-stocks-in-the-future/repo/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: "Continuous Integration"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
types: ['opened', 'reopened', 'synchronize', 'unlocked']
|
||||
|
||||
jobs:
|
||||
rails-test:
|
||||
name: Rails Test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
ports:
|
||||
- "5432:5432"
|
||||
env:
|
||||
POSTGRES_DB: sif
|
||||
POSTGRES_USER: sif
|
||||
POSTGRES_PASSWORD: password
|
||||
redis:
|
||||
image: redis:7.0
|
||||
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
DATABASE_URL: "postgres://sif:password@localhost:5432/sif"
|
||||
REDIS_URL: "redis://localhost:6379/1"
|
||||
BROWSERSLIST_IGNORE_OLD_DATA: true
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.4.4'
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- name: Set up database
|
||||
run: bin/rails db:setup
|
||||
|
||||
- name: Run tests
|
||||
run: bin/rails test
|
||||
|
||||
- name: Run system tests
|
||||
run: bin/rails test:system
|
||||
75
worker-toolkit-stocks-in-the-future/repo/.github/workflows/deploy-staging.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
name: "Deploy to Staging"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
# Prevent concurrent deploys - queue rather than cancel
|
||||
concurrency:
|
||||
group: deploy-staging
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Rails Test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
ports:
|
||||
- "5432:5432"
|
||||
env:
|
||||
POSTGRES_DB: sif
|
||||
POSTGRES_USER: sif
|
||||
POSTGRES_PASSWORD: password
|
||||
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
DATABASE_URL: "postgres://sif:password@localhost:5432/sif"
|
||||
BROWSERSLIST_IGNORE_OLD_DATA: true
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.4.4'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Set up database
|
||||
run: bin/rails db:setup
|
||||
|
||||
- name: Run tests
|
||||
run: bin/rails test
|
||||
|
||||
deploy:
|
||||
name: Deploy to Staging
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.4.4'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Set up SSH agent
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Add staging server to known_hosts
|
||||
run: ssh-keyscan -H ${{ secrets.STAGING_SERVER_IP }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy to staging
|
||||
env:
|
||||
STAGING_SERVER_IP: ${{ secrets.STAGING_SERVER_IP }}
|
||||
run: bundle exec cap staging deploy
|
||||
29
worker-toolkit-stocks-in-the-future/repo/.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.4
|
||||
bundler-cache: true # runs bundle install and caches gems
|
||||
|
||||
- name: Install dependencies
|
||||
run: bundle install
|
||||
|
||||
- name: Run Lint
|
||||
run: bin/lint
|
||||
28
worker-toolkit-stocks-in-the-future/repo/.github/workflows/unassign-stale-issues.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
on:
|
||||
schedule:
|
||||
# * is a special character in YAML so you have to quote this string
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
unassign_issues:
|
||||
runs-on: ubuntu-latest
|
||||
name: Unassign issues
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Mark issues as stale
|
||||
uses: actions/stale@v8
|
||||
with:
|
||||
include-only-assigned: true
|
||||
stale-issue-message: 'This issue is marked as stale due to no activity within 20 days. If no further activity is detected within 7 days, it will be unassigned.'
|
||||
days-before-stale: 20
|
||||
days-before-close: -1
|
||||
days-before-pr-close: -1
|
||||
- name: Unassign stale issues
|
||||
uses: boundfoxstudios/action-unassign-contributor-after-days-of-inactivity@main
|
||||
with:
|
||||
last-activity: 7
|
||||
labels: 'Stale'
|
||||
labels-to-remove: 'Stale'
|
||||
message: 'Automatically unassigned after 7 days of inactivity.'
|
||||
76
worker-toolkit-stocks-in-the-future/repo/.gitignore
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
||||
#
|
||||
# If you find yourself ignoring temporary files generated by your text editor
|
||||
# or operating system, you probably want to add a global ignore instead:
|
||||
# git config --global core.excludesfile '~/.gitignore_global'
|
||||
|
||||
# Ignore bundler config.
|
||||
/.bundle
|
||||
|
||||
# Ignore all environment files (except templates).
|
||||
/.env*
|
||||
!/.env*.erb
|
||||
|
||||
# Ignore all logfiles and tempfiles.
|
||||
/log/*
|
||||
/tmp/*
|
||||
!/log/.keep
|
||||
!/tmp/.keep
|
||||
|
||||
# Ignore pidfiles, but keep the directory.
|
||||
/tmp/pids/*
|
||||
!/tmp/pids/
|
||||
!/tmp/pids/.keep
|
||||
|
||||
# Ignore storage (uploaded files in development and any SQLite databases).
|
||||
/storage/*
|
||||
!/storage/.keep
|
||||
/tmp/storage/*
|
||||
!/tmp/storage/
|
||||
!/tmp/storage/.keep
|
||||
|
||||
/public/assets
|
||||
|
||||
# Ignore master key for decrypting credentials and more.
|
||||
/config/master.key
|
||||
/config/api_keys.rb
|
||||
|
||||
# ignore local database.yml
|
||||
/config/database.yml
|
||||
|
||||
# ignore simplecov
|
||||
/coverage
|
||||
|
||||
/app/assets/builds/*
|
||||
!/app/assets/builds/.keep
|
||||
|
||||
/node_modules
|
||||
.vscode/
|
||||
.tool-versions
|
||||
|
||||
# ignore vscode workspace settings
|
||||
*.code-workspace
|
||||
|
||||
#ignore kilocodemodes config files
|
||||
.kilocodemodes
|
||||
|
||||
# Ignore AI assistant configuration files
|
||||
.claude/
|
||||
|
||||
# Ignore VSCode global storage (contains extension settings)
|
||||
Library/
|
||||
|
||||
.idea
|
||||
|
||||
# Ignore macOS metadata files
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# Terraform
|
||||
**/.terraform/
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
*.tfstate.lock.info
|
||||
*.tfvars
|
||||
!*.tfvars.example
|
||||
.terraform.lock.hcl
|
||||
85
worker-toolkit-stocks-in-the-future/repo/.rubocop.yml
Normal file
@@ -0,0 +1,85 @@
|
||||
# .rubocop.yml
|
||||
plugins:
|
||||
- rubocop-rails
|
||||
|
||||
inherit_from:
|
||||
- .rubocop_rails_default.yml
|
||||
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Exclude:
|
||||
- 'db/**/*'
|
||||
- 'bin/**/*'
|
||||
- 'node_modules/**/*'
|
||||
- 'vendor/**/*'
|
||||
- 'config/initializers/devise.rb'
|
||||
- 'config/initializers/permissions_policy.rb'
|
||||
- 'config/initializers/filter_parameter_logging.rb'
|
||||
- 'config/initializers/inflections.rb'
|
||||
- 'config/initializers/assets.rb'
|
||||
- 'config/initializers/content_security_policy.rb'
|
||||
- 'config/initializers/api_keys.rb'
|
||||
SuggestExtensions: false
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
Style/StringLiterals:
|
||||
EnforcedStyle: double_quotes
|
||||
|
||||
Layout/LineLength:
|
||||
Max: 120
|
||||
|
||||
Metrics/BlockLength:
|
||||
Max: 50
|
||||
Exclude:
|
||||
- 'config/routes.rb'
|
||||
- 'spec/**/*'
|
||||
- 'test/**/*'
|
||||
|
||||
Metrics/ClassLength:
|
||||
Exclude:
|
||||
- 'app/models/order.rb'
|
||||
- 'spec/**/*'
|
||||
- 'test/**/*'
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 20
|
||||
Exclude:
|
||||
- 'app/helpers/components/**/*'
|
||||
- 'spec/**/*'
|
||||
- 'test/**/*'
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 25
|
||||
Exclude:
|
||||
- 'spec/**/*'
|
||||
- 'test/**/*'
|
||||
|
||||
Metrics/CyclomaticComplexity:
|
||||
Exclude:
|
||||
- 'app/helpers/components/**/*'
|
||||
- 'spec/**/*'
|
||||
- 'test/**/*'
|
||||
|
||||
Metrics/PerceivedComplexity:
|
||||
Exclude:
|
||||
- 'app/helpers/components/**/*'
|
||||
- 'spec/**/*'
|
||||
- 'test/**/*'
|
||||
|
||||
Metrics/ParameterLists:
|
||||
Exclude:
|
||||
- 'app/helpers/components/**/*'
|
||||
|
||||
Naming/MethodParameterName:
|
||||
MinNameLength: 2
|
||||
|
||||
Layout/FirstMethodArgumentLineBreak:
|
||||
Enabled: true
|
||||
|
||||
Layout/MultilineMethodCallIndentation:
|
||||
EnforcedStyle: indented
|
||||
|
||||
Layout/FirstArgumentIndentation:
|
||||
EnforcedStyle: consistent
|
||||
1305
worker-toolkit-stocks-in-the-future/repo/.rubocop_rails_default.yml
Normal file
1
worker-toolkit-stocks-in-the-future/repo/.ruby-version
Normal file
@@ -0,0 +1 @@
|
||||
3.4.4
|
||||
5
worker-toolkit-stocks-in-the-future/repo/.standard.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
parallel: true
|
||||
ruby_version: 3.4
|
||||
|
||||
plugins:
|
||||
- standard-rails
|
||||
106
worker-toolkit-stocks-in-the-future/repo/CONTRIBUTING.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Contributing
|
||||
|
||||
## Workflow for Contributors
|
||||
|
||||
1. **Find an issue** to work on from our existing issues. If you've identified a bug or potential improvement, please discuss it in the #stocks-in-the-future Slack channel before starting work.
|
||||
|
||||
2. **Create a branch** from `main` with a descriptive name including the issue number
|
||||
|
||||
```console
|
||||
$ git checkout main
|
||||
$ git pull
|
||||
$ git checkout -b feature-description-123
|
||||
```
|
||||
|
||||
3. **Make your changes** and commit them with descriptive messages
|
||||
|
||||
```console
|
||||
$ git add .
|
||||
$ git commit -m "Add feature X that solves problem Y"
|
||||
```
|
||||
|
||||
4. **Run linting** to ensure code quality
|
||||
|
||||
```console
|
||||
$ bin/lint
|
||||
```
|
||||
|
||||
If issues are found, fix them or use auto-fix when appropriate:
|
||||
|
||||
```console
|
||||
$ bundle exec rubocop -A
|
||||
```
|
||||
|
||||
5. **Run tests** to verify your changes don't break existing functionality
|
||||
|
||||
```console
|
||||
$ bin/rails test
|
||||
```
|
||||
|
||||
To check test coverage, run:
|
||||
|
||||
```console
|
||||
$ bin/coverage
|
||||
```
|
||||
|
||||
This generates an HTML coverage report at `coverage/index.html`
|
||||
|
||||
6. **Push your branch** to GitHub
|
||||
|
||||
```console
|
||||
# For the first push to a new branch
|
||||
$ git push --set-upstream origin feature-description-123
|
||||
|
||||
# For all subsequent pushes
|
||||
$ git push
|
||||
```
|
||||
|
||||
7. **Open a Pull Request** against the `main` branch
|
||||
- Include a clear description of the changes
|
||||
- Linking the issue number in the PR's description (e.g., `Resolves #123`)
|
||||
- [More about linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue)
|
||||
- Wait for CI checks to pass
|
||||
- Request review from team members
|
||||
|
||||
## Linting
|
||||
|
||||
This project uses [erb_lint](https://github.com/Shopify/erb-lint) to catch ERB template errors during development.
|
||||
|
||||
To check all ERB templates:
|
||||
|
||||
```console
|
||||
$ bin/dc bundle exec erb_lint --lint-all
|
||||
```
|
||||
|
||||
To see autocorrectable issues:
|
||||
|
||||
```console
|
||||
$ bin/dc bundle exec erb_lint --lint-all --autocorrect
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you need help at any point, comment on the issue you're working on or ask in the #stocks-in-the-future channel on the Ruby for Good Slack.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### ASDF
|
||||
|
||||
If you find yourself in a cycle of encountering errors such as:
|
||||
|
||||
```
|
||||
in stocks-in-the-future/ on main
|
||||
› asdf install ruby 3.4.4
|
||||
|
||||
in stocks-in-the-future/ on main
|
||||
› asdf set local ruby 3.4.4
|
||||
|
||||
in stocks-in-the-future/ on main
|
||||
› ruby --version
|
||||
No version is set for command ruby
|
||||
Consider adding one of the following versions in your config file at /Users/america/dev/stocks-in-the-future/.tool-versions
|
||||
ruby 3.3.6
|
||||
ruby 3.1.6
|
||||
```
|
||||
|
||||
Try upgrading asdf and trying again.
|
||||
12
worker-toolkit-stocks-in-the-future/repo/Capfile
Normal file
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "capistrano/setup"
|
||||
require "capistrano/deploy"
|
||||
require "capistrano/scm/git"
|
||||
install_plugin Capistrano::SCM::Git
|
||||
require "capistrano/rbenv"
|
||||
require "capistrano/bundler"
|
||||
require "capistrano/rails/assets"
|
||||
require "capistrano/rails/migrations"
|
||||
|
||||
Dir.glob("lib/capistrano/tasks/*.cap").each { |r| import r }
|
||||
71
worker-toolkit-stocks-in-the-future/repo/Dockerfile
Normal file
@@ -0,0 +1,71 @@
|
||||
# syntax = docker/dockerfile:1
|
||||
|
||||
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
|
||||
ARG RUBY_VERSION=3.4.4
|
||||
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
|
||||
|
||||
# Rails app lives here
|
||||
WORKDIR /rails
|
||||
|
||||
# Set production environment
|
||||
ENV RAILS_ENV="production" \
|
||||
BUNDLE_DEPLOYMENT="1" \
|
||||
BUNDLE_PATH="/usr/local/bundle" \
|
||||
BUNDLE_WITHOUT="development"
|
||||
|
||||
|
||||
# Throw-away build stage to reduce size of final image
|
||||
FROM base as build
|
||||
|
||||
# Install packages needed to build gems
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y build-essential curl git libpq-dev libvips pkg-config unzip
|
||||
|
||||
ENV BUN_INSTALL=/usr/local/bun
|
||||
ENV PATH=/usr/local/bun/bin:$PATH
|
||||
ARG BUN_VERSION=1.0.26
|
||||
RUN curl -fsSL https://bun.sh/install | bash -s -- "bun-v${BUN_VERSION}"
|
||||
|
||||
# Install application gems
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
RUN bundle install && \
|
||||
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
|
||||
bundle exec bootsnap precompile --gemfile
|
||||
|
||||
# Install node modules
|
||||
COPY package.json bun.lockb ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Precompile bootsnap code for faster boot times
|
||||
RUN bundle exec bootsnap precompile app/ lib/
|
||||
|
||||
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
|
||||
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
|
||||
|
||||
|
||||
# Final stage for app image
|
||||
FROM base
|
||||
|
||||
# Install packages needed for deployment
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y curl libvips postgresql-client && \
|
||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||
|
||||
# Copy built artifacts: gems, application
|
||||
COPY --from=build /usr/local/bundle /usr/local/bundle
|
||||
COPY --from=build /rails /rails
|
||||
|
||||
# Run and own only the runtime files as a non-root user for security
|
||||
RUN useradd rails --create-home --shell /bin/bash && \
|
||||
chown -R rails:rails db log storage tmp
|
||||
USER rails:rails
|
||||
|
||||
# Entrypoint prepares the database.
|
||||
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
|
||||
|
||||
# Start the server by default, this can be overwritten at runtime
|
||||
EXPOSE 3000
|
||||
CMD ["./bin/rails", "server"]
|
||||
35
worker-toolkit-stocks-in-the-future/repo/Dockerfile.dev
Normal file
@@ -0,0 +1,35 @@
|
||||
# syntax = docker/dockerfile:1
|
||||
|
||||
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
|
||||
ARG RUBY_VERSION=3.4.4
|
||||
FROM ruby:$RUBY_VERSION-slim as base
|
||||
|
||||
# Rails app lives here
|
||||
WORKDIR /rails
|
||||
|
||||
# Install packages needed to build gems
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
build-essential libpq-dev curl \
|
||||
nodejs npm libyaml-dev git \
|
||||
chromium chromium-driver # Add these packages \
|
||||
build-essential libpq-dev curl \
|
||||
nodejs npm libyaml-dev git && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN npm install -g yarn
|
||||
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
|
||||
# Install gems - ensure we install all groups
|
||||
RUN gem update --system && \
|
||||
gem install bundler && \
|
||||
bundle config set --local without '' && \
|
||||
bundle install --jobs 20 --retry 5
|
||||
|
||||
COPY . .
|
||||
RUN SECRET_KEY_BASE=placeholder bundle exec rails assets:precompile
|
||||
|
||||
# Start the server by default, this can be overwritten at runtime
|
||||
EXPOSE 3000
|
||||
CMD ["./bin/rails", "server", "-b", "0.0.0.0"]
|
||||
BIN
worker-toolkit-stocks-in-the-future/repo/GITFOLDER.zip
Normal file
62
worker-toolkit-stocks-in-the-future/repo/Gemfile
Normal file
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
ruby "3.4.4"
|
||||
|
||||
gem "rails", "~> 8.1.2", ">= 8.1.2.1"
|
||||
|
||||
gem "bootsnap", require: false
|
||||
gem "csv"
|
||||
gem "daemons"
|
||||
gem "devise", "~> 5.0"
|
||||
gem "faker", "~> 3.8.0"
|
||||
gem "font-awesome-rails"
|
||||
gem "image_processing", "~> 1.14"
|
||||
gem "importmap-rails"
|
||||
gem "jbuilder"
|
||||
gem "lucide-rails"
|
||||
gem "pg", "~> 1.6"
|
||||
gem "propshaft"
|
||||
gem "puma", ">= 5.0"
|
||||
gem "pundit", "~> 2.5"
|
||||
gem "shadcn-ui", "~> 0.0.15"
|
||||
gem "solid_queue", "~> 1.4"
|
||||
gem "stimulus-rails"
|
||||
gem "strong_migrations", "~> 2.8"
|
||||
gem "tailwindcss-rails"
|
||||
gem "turbo-rails"
|
||||
|
||||
gem "discard", "~> 2.0"
|
||||
gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby]
|
||||
|
||||
group :development, :test do
|
||||
gem "brakeman", require: false
|
||||
gem "bundler-audit", require: false
|
||||
gem "debug", platforms: %i[mri mingw mswin x64_mingw]
|
||||
gem "erb_lint", require: false
|
||||
gem "pry", "~> 0.16.0"
|
||||
gem "pry-byebug"
|
||||
gem "rubocop"
|
||||
gem "rubocop-rails"
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem "bcrypt_pbkdf", "~> 1.0", require: false
|
||||
gem "capistrano", "~> 3.20", require: false
|
||||
gem "capistrano-bundler", "~> 2.1", require: false
|
||||
gem "capistrano-rails", "~> 1.6", require: false
|
||||
gem "capistrano-rbenv", "~> 2.2", require: false
|
||||
gem "ed25519", "~> 1.3", require: false
|
||||
gem "i18n-tasks"
|
||||
gem "web-console"
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem "capybara"
|
||||
gem "factory_bot_rails"
|
||||
gem "mocha"
|
||||
gem "selenium-webdriver"
|
||||
gem "simplecov", require: false
|
||||
gem "webmock"
|
||||
end
|
||||
533
worker-toolkit-stocks-in-the-future/repo/Gemfile.lock
Normal file
@@ -0,0 +1,533 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
action_text-trix (2.1.18)
|
||||
railties
|
||||
actioncable (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
activejob (= 8.1.3)
|
||||
activerecord (= 8.1.3)
|
||||
activestorage (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
actionview (= 8.1.3)
|
||||
activejob (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.1.3)
|
||||
actionview (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.1.3)
|
||||
action_text-trix (~> 2.1.15)
|
||||
actionpack (= 8.1.3)
|
||||
activerecord (= 8.1.3)
|
||||
activestorage (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
activerecord (8.1.3)
|
||||
activemodel (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
activejob (= 8.1.3)
|
||||
activerecord (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.1.3)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
json
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.9.0)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
airbrussh (1.6.1)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
ast (2.4.3)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.22)
|
||||
bcrypt_pbkdf (1.1.2)
|
||||
better_html (2.2.0)
|
||||
actionview (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
ast (~> 2.0)
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (4.1.2)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.24.6)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (8.0.5)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bundler-audit (0.9.3)
|
||||
bundler (>= 1.2.0)
|
||||
thor (~> 1.0)
|
||||
byebug (13.0.0)
|
||||
reline (>= 0.6.0)
|
||||
capistrano (3.20.1)
|
||||
airbrussh (>= 1.0.0)
|
||||
i18n
|
||||
rake (>= 10.0.0)
|
||||
sshkit (>= 1.9.0)
|
||||
capistrano-bundler (2.2.0)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-rails (1.7.0)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-bundler (>= 1.1, < 3)
|
||||
capistrano-rbenv (2.2.0)
|
||||
capistrano (~> 3.1)
|
||||
sshkit (~> 1.3)
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
matrix
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.11)
|
||||
rack (>= 1.6.0)
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.3.7)
|
||||
connection_pool (3.0.2)
|
||||
crack (1.0.1)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.7)
|
||||
csv (3.3.5)
|
||||
daemons (1.4.1)
|
||||
date (3.5.1)
|
||||
debug (1.11.1)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
devise (5.0.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 7.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
discard (2.0.0)
|
||||
activerecord (>= 7.0, < 9.0)
|
||||
docile (1.4.1)
|
||||
drb (2.2.3)
|
||||
ed25519 (1.4.0)
|
||||
erb (6.0.4)
|
||||
erb_lint (0.9.0)
|
||||
activesupport
|
||||
better_html (>= 2.0.1)
|
||||
parser (>= 2.7.1.4)
|
||||
rainbow
|
||||
rubocop (>= 1)
|
||||
smart_properties
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.4.0)
|
||||
tzinfo
|
||||
factory_bot (6.6.0)
|
||||
activesupport (>= 6.1.0)
|
||||
factory_bot_rails (6.5.1)
|
||||
factory_bot (~> 6.5)
|
||||
railties (>= 6.1.0)
|
||||
faker (3.8.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
ffi (1.17.4-aarch64-linux-gnu)
|
||||
ffi (1.17.4-aarch64-linux-musl)
|
||||
ffi (1.17.4-arm-linux-gnu)
|
||||
ffi (1.17.4-arm-linux-musl)
|
||||
ffi (1.17.4-x86_64-darwin)
|
||||
ffi (1.17.4-x86_64-linux-gnu)
|
||||
ffi (1.17.4-x86_64-linux-musl)
|
||||
font-awesome-rails (4.7.0.9)
|
||||
railties (>= 3.2, < 9.0)
|
||||
fugit (1.12.1)
|
||||
et-orbi (~> 1.4)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
hashdiff (1.2.1)
|
||||
highline (3.1.2)
|
||||
reline
|
||||
i18n (1.15.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (1.1.2)
|
||||
activesupport (>= 4.0.2)
|
||||
ast (>= 2.1.0)
|
||||
erubi
|
||||
highline (>= 3.0.0)
|
||||
i18n
|
||||
parser (>= 3.2.2.1)
|
||||
prism
|
||||
rails-i18n
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
ruby-progressbar (~> 1.8, >= 1.8.1)
|
||||
terminal-table (>= 1.5.1)
|
||||
image_processing (1.14.0)
|
||||
mini_magick (>= 4.9.5, < 6)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
importmap-rails (2.2.3)
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
io-console (0.8.2)
|
||||
irb (1.18.0)
|
||||
pp (>= 0.6.0)
|
||||
prism (>= 1.3.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jbuilder (2.15.1)
|
||||
actionview (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
json (2.20.0)
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.7.0)
|
||||
loofah (2.25.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
lucide-rails (0.7.4)
|
||||
railties (>= 4.1.0)
|
||||
mail (2.9.0)
|
||||
logger
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.1.0)
|
||||
matrix (0.4.3)
|
||||
method_source (1.1.0)
|
||||
mini_magick (5.3.1)
|
||||
logger
|
||||
mini_mime (1.1.5)
|
||||
minitest (6.0.6)
|
||||
drb (~> 2.0)
|
||||
prism (~> 1.5)
|
||||
mocha (3.1.0)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.8.3)
|
||||
net-imap (0.6.4.1)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-scp (4.1.0)
|
||||
net-ssh (>= 2.6.5, < 8.0.0)
|
||||
net-sftp (4.0.0)
|
||||
net-ssh (>= 5.0.0, < 8.0.0)
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
net-ssh (7.3.2)
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.4-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.4-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.4-arm-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.4-arm-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.4-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.4-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.4-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
orm_adapter (0.5.0)
|
||||
ostruct (0.6.3)
|
||||
parallel (2.1.0)
|
||||
parser (3.3.11.1)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.6.3)
|
||||
pg (1.6.3-aarch64-linux)
|
||||
pg (1.6.3-aarch64-linux-musl)
|
||||
pg (1.6.3-x86_64-darwin)
|
||||
pg (1.6.3-x86_64-linux)
|
||||
pg (1.6.3-x86_64-linux-musl)
|
||||
pp (0.6.4)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.9.0)
|
||||
propshaft (1.3.2)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
rack
|
||||
pry (0.16.0)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
reline (>= 0.6.0)
|
||||
pry-byebug (3.12.0)
|
||||
byebug (~> 13.0)
|
||||
pry (>= 0.13, < 0.17)
|
||||
public_suffix (7.0.5)
|
||||
puma (8.0.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.5.2)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.6)
|
||||
rack-session (2.1.2)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rackup (2.3.1)
|
||||
rack (>= 3)
|
||||
rails (8.1.3)
|
||||
actioncable (= 8.1.3)
|
||||
actionmailbox (= 8.1.3)
|
||||
actionmailer (= 8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
actiontext (= 8.1.3)
|
||||
actionview (= 8.1.3)
|
||||
activejob (= 8.1.3)
|
||||
activemodel (= 8.1.3)
|
||||
activerecord (= 8.1.3)
|
||||
activestorage (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.1.3)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.7.0)
|
||||
loofah (~> 2.25)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rails-i18n (8.1.0)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 8.0.0, < 9)
|
||||
railties (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.4.2)
|
||||
rbs (4.0.3)
|
||||
logger
|
||||
prism (>= 1.6.0)
|
||||
tsort
|
||||
rdoc (8.0.0)
|
||||
erb
|
||||
prism (>= 1.6.0)
|
||||
rbs (>= 4.0.0)
|
||||
tsort
|
||||
regexp_parser (2.12.0)
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
responders (3.2.0)
|
||||
actionpack (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
rexml (3.4.4)
|
||||
rubocop (1.88.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
parallel (>= 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.49.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.49.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.7)
|
||||
rubocop-rails (2.35.5)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.3.0)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (3.4.0)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.45.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 4.0)
|
||||
websocket (~> 1.0)
|
||||
shadcn-ui (0.0.15)
|
||||
tailwind_merge (~> 0.12)
|
||||
simplecov (0.22.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
simplecov_json_formatter (~> 0.1)
|
||||
simplecov-html (0.13.2)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
sin_lru_redux (2.5.2)
|
||||
smart_properties (1.17.0)
|
||||
solid_queue (1.4.0)
|
||||
activejob (>= 7.1)
|
||||
activerecord (>= 7.1)
|
||||
concurrent-ruby (>= 1.3.1)
|
||||
fugit (~> 1.11)
|
||||
railties (>= 7.1)
|
||||
thor (>= 1.3.1)
|
||||
sshkit (1.25.0)
|
||||
base64
|
||||
logger
|
||||
net-scp (>= 1.1.2)
|
||||
net-sftp (>= 2.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
ostruct
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
strong_migrations (2.8.0)
|
||||
activerecord (>= 7.2)
|
||||
tailwind_merge (0.16.0)
|
||||
sin_lru_redux (~> 2.5)
|
||||
tailwindcss-rails (4.6.0)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-ruby (~> 4.0)
|
||||
tailwindcss-ruby (4.3.1)
|
||||
tailwindcss-ruby (4.3.1-aarch64-linux-gnu)
|
||||
tailwindcss-ruby (4.3.1-aarch64-linux-musl)
|
||||
tailwindcss-ruby (4.3.1-x86_64-darwin)
|
||||
tailwindcss-ruby (4.3.1-x86_64-linux-gnu)
|
||||
tailwindcss-ruby (4.3.1-x86_64-linux-musl)
|
||||
terminal-table (4.0.0)
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
thor (1.5.0)
|
||||
timeout (0.6.1)
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.23)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.2.0)
|
||||
uri (1.1.1)
|
||||
useragent (0.16.11)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.3.0)
|
||||
actionview (>= 8.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 8.0.0)
|
||||
webmock (3.26.2)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
websocket (1.2.11)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.8.2)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
aarch64-linux-gnu
|
||||
aarch64-linux-musl
|
||||
arm-linux-gnu
|
||||
arm-linux-musl
|
||||
x86_64-darwin-24
|
||||
x86_64-linux
|
||||
x86_64-linux-gnu
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
bcrypt_pbkdf (~> 1.0)
|
||||
bootsnap
|
||||
brakeman
|
||||
bundler-audit
|
||||
capistrano (~> 3.20)
|
||||
capistrano-bundler (~> 2.1)
|
||||
capistrano-rails (~> 1.6)
|
||||
capistrano-rbenv (~> 2.2)
|
||||
capybara
|
||||
csv
|
||||
daemons
|
||||
debug
|
||||
devise (~> 5.0)
|
||||
discard (~> 2.0)
|
||||
ed25519 (~> 1.3)
|
||||
erb_lint
|
||||
factory_bot_rails
|
||||
faker (~> 3.8.0)
|
||||
font-awesome-rails
|
||||
i18n-tasks
|
||||
image_processing (~> 1.14)
|
||||
importmap-rails
|
||||
jbuilder
|
||||
lucide-rails
|
||||
mocha
|
||||
pg (~> 1.6)
|
||||
propshaft
|
||||
pry (~> 0.16.0)
|
||||
pry-byebug
|
||||
puma (>= 5.0)
|
||||
pundit (~> 2.5)
|
||||
rails (~> 8.1.2, >= 8.1.2.1)
|
||||
rubocop
|
||||
rubocop-rails
|
||||
selenium-webdriver
|
||||
shadcn-ui (~> 0.0.15)
|
||||
simplecov
|
||||
solid_queue (~> 1.4)
|
||||
stimulus-rails
|
||||
strong_migrations (~> 2.8)
|
||||
tailwindcss-rails
|
||||
turbo-rails
|
||||
tzinfo-data
|
||||
web-console
|
||||
webmock
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.4p34
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.7
|
||||
9
worker-toolkit-stocks-in-the-future/repo/LICENSE.md
Normal file
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2023 Ruby for Good
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
3
worker-toolkit-stocks-in-the-future/repo/Procfile
Normal file
@@ -0,0 +1,3 @@
|
||||
web: bundle exec puma -C config/puma.rb
|
||||
scheduler: bin/rake solid_queue:start
|
||||
release: bundle exec rails db:migrate
|
||||
3
worker-toolkit-stocks-in-the-future/repo/Procfile.dev
Normal file
@@ -0,0 +1,3 @@
|
||||
web: env RUBY_DEBUG_OPEN=true bin/rails server
|
||||
css: bin/rails tailwindcss:watch
|
||||
job: bin/rake solid_queue:start
|
||||
198
worker-toolkit-stocks-in-the-future/repo/README.md
Normal file
@@ -0,0 +1,198 @@
|
||||
[](https://github.com/rubyforgood/stocks-in-the-future/actions/workflows/ci.yml)
|
||||
[](https://github.com/rubyforgood/stocks-in-the-future/actions/workflows/lint.yml)
|
||||
[](https://github.com/rubyforgood/stocks-in-the-future/actions/workflows/dependabot/dependabot-updates)
|
||||
|
||||
[](https://github.com/rubyforgood/stocks-in-the-future/labels/help%20wanted)
|
||||
[](https://github.com/rubyforgood/stocks-in-the-future/pulls)
|
||||
[](https://github.com/rubyforgood/stocks-in-the-future/commits)
|
||||
|
||||
# About Stocks in the Future
|
||||
|
||||
## Mission
|
||||
|
||||
[Stocks in the Future](https://sifonline.org/) is a program with the mission of developing highly motivated middle school students who are eager to learn and dedicated to attending class through the use of incentives coupled with a financial literacy curriculum focused on investing that reinforces Math, Language Arts and Social Studies. Stocks in the Future pushes to educate, encourage, and empower the next generation of financially-literate individuals.
|
||||
This application is used by the students to manage their portfolios by trading and selling their stocks. It is also used by the teachers and administrator to manage the classrooms, enter student's grades, attendance, and post announcements.
|
||||
|
||||
## Ruby for Good
|
||||
|
||||
If you have any questions about an issue, comment on the issue, open a new issue, or ask in [the RubyForGood slack](https://join.slack.com/t/rubyforgood/shared_invite/zt-2k5ezv241-Ia2Iac3amxDS8CuhOr69ZA). Stocks-in-the-future has a #stocks-in-the-future channel in the Slack. Feel free to join the community!
|
||||
|
||||
## Contributing
|
||||
|
||||
To understand the project better, read the [project documentation](docs/README.md).
|
||||
|
||||
Then follow our [contributing guide](CONTRIBUTING.md) to get started.
|
||||
|
||||
# Local Development
|
||||
## Requirements
|
||||
|
||||
- A ruby version manager such as [rvm](https://rvm.io/rvm/install), [rbenv](https://formulae.brew.sh/formula/rbenv) or [asdf](https://asdf-vm.com/guide/getting-started.html)
|
||||
- Ruby 3.4.4 (Installed via ruby manager ^)
|
||||
- [PostgreSQL](https://www.postgresql.org/), if you're not using Docker.
|
||||
- [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) and yarn (`npm install -g yarn`)
|
||||
|
||||
## Easy Docker Scripts
|
||||
Go to the [Easy Docker Scripts](docker/README.md) page for an easy way to set up this app, test this app, seed data, run RuboCop, and execute other routine tasks.
|
||||
|
||||
## Installation
|
||||
|
||||
Create `config/database.yml`. A copy of `config/database.yml.sample` should be adequate.
|
||||
|
||||
### With Docker
|
||||
|
||||
Build and start the application with `docker compose up`. Once the application has successfully started, you should be able to visit it at http://localhost:3000/
|
||||
|
||||
Run commands in docker with the `bin/dc` helper script on macos or Linux.
|
||||
|
||||
```console
|
||||
$ bin/dc rails db:setup
|
||||
$ bin/dc rails test
|
||||
```
|
||||
|
||||
Or by prefacing `rails` commands with `docker compose run stocks`.
|
||||
|
||||
```console
|
||||
$ docker compose run stocks rails db:setup
|
||||
$ docker compose run stocks rails test
|
||||
```
|
||||
|
||||
## Mac & Linux Users
|
||||
|
||||
- Run setup: `bin/setup`
|
||||
- Run the Rails server locally: `bin/dev`
|
||||
|
||||
## Windows
|
||||
|
||||
It is **strongly** recommended to use Docker. See instructions above.
|
||||
|
||||
## Seed Data
|
||||
|
||||
After running `bin/rails db:setup`, the database will automatically be seeded with three default users.
|
||||
|
||||
| Role | Username | Password |
|
||||
|---------|----------|----------|
|
||||
| Teacher | Teacher | password |
|
||||
| Student | Student | password |
|
||||
| Admin | Admin | password |
|
||||
|
||||
Use the **username** and **password** to log in and test the application locally.
|
||||
|
||||
## URL
|
||||
|
||||
Access the app via `localhost:3000`
|
||||
|
||||
# Operations
|
||||
|
||||
## Production And Staging Email
|
||||
|
||||
Password reset and account setup emails are sent through Devise using Rails Action Mailer. In staging and production, Action Mailer is configured to use Amazon SES SMTP in `us-east-1`.
|
||||
|
||||
The SES domain identity is `sifonline.org`. DKIM is verified with three CNAME records in GoDaddy DNS:
|
||||
|
||||
```text
|
||||
xqnfudzzscsqz2vbneqh75ojqwduiphb._domainkey.sifonline.org -> xqnfudzzscsqz2vbneqh75ojqwduiphb.dkim.amazonses.com
|
||||
4zk3q7oy5kz7xubi2gxqkn2kpqglwgms._domainkey.sifonline.org -> 4zk3q7oy5kz7xubi2gxqkn2kpqglwgms.dkim.amazonses.com
|
||||
qsq7nni46gdkvr6vu7gn6cm4rexzo22u._domainkey.sifonline.org -> qsq7nni46gdkvr6vu7gn6cm4rexzo22u.dkim.amazonses.com
|
||||
```
|
||||
|
||||
The SES SMTP credentials are stored in AWS Secrets Manager:
|
||||
|
||||
```text
|
||||
stocks-in-the-future/ses-smtp
|
||||
```
|
||||
|
||||
The app reads these runtime environment variables from `/etc/stocks/env` on each Lightsail instance:
|
||||
|
||||
```text
|
||||
APP_HOST
|
||||
MAILER_SENDER
|
||||
SES_SMTP_ADDRESS
|
||||
SES_SMTP_PORT
|
||||
SES_SMTP_USERNAME
|
||||
SES_SMTP_PASSWORD
|
||||
```
|
||||
|
||||
Expected values by environment:
|
||||
|
||||
```text
|
||||
production APP_HOST=app.sifonline.org
|
||||
staging APP_HOST=staging.sifonline.org
|
||||
MAILER_SENDER=no-reply@sifonline.org
|
||||
SES_SMTP_ADDRESS=email-smtp.us-east-1.amazonaws.com
|
||||
SES_SMTP_PORT=587
|
||||
```
|
||||
|
||||
SES may still be sandboxed in a new AWS account or region. If `ProductionAccessEnabled` is `false`, SES can only send to verified recipient email addresses even if the `sifonline.org` sending domain is verified.
|
||||
|
||||
Check SES status:
|
||||
|
||||
```console
|
||||
AWS_PROFILE=713141626029_AdministratorAccess AWS_REGION=us-east-1 aws sesv2 get-email-identity --email-identity sifonline.org
|
||||
AWS_PROFILE=713141626029_AdministratorAccess AWS_REGION=us-east-1 aws sesv2 get-account
|
||||
```
|
||||
|
||||
## SSM Access To Lightsail
|
||||
|
||||
The Lightsail instances are registered with AWS Systems Manager as hybrid managed instances so routine ops do not require SSH keys.
|
||||
|
||||
Current managed instances:
|
||||
|
||||
```text
|
||||
production_web -> mi-059a7bcb37754c44d
|
||||
staging_web -> mi-0c65ce3a1a596c81c
|
||||
```
|
||||
|
||||
Check SSM connectivity:
|
||||
|
||||
```console
|
||||
AWS_PROFILE=713141626029_AdministratorAccess AWS_REGION=us-east-1 aws ssm describe-instance-information
|
||||
```
|
||||
|
||||
Run a safe health check through SSM:
|
||||
|
||||
```console
|
||||
AWS_PROFILE=713141626029_AdministratorAccess AWS_REGION=us-east-1 aws ssm send-command \
|
||||
--instance-ids mi-059a7bcb37754c44d mi-0c65ce3a1a596c81c \
|
||||
--document-name AWS-RunShellScript \
|
||||
--parameters '{"commands":["systemctl is-active stocks && grep -E \"^(APP_HOST|MAILER_SENDER|SES_SMTP_ADDRESS|SES_SMTP_PORT|SES_SMTP_USERNAME|SES_SMTP_PASSWORD)=\" /etc/stocks/env | sed -E \"s/=.*/=present/\""]}'
|
||||
```
|
||||
|
||||
Do not print `SES_SMTP_PASSWORD` or other secrets in terminal output. Check for key presence only.
|
||||
|
||||
## Email Troubleshooting
|
||||
|
||||
If reset password emails do not send:
|
||||
|
||||
1. Confirm the deployed code includes SES Action Mailer configuration in `config/environments/production.rb` or `config/environments/staging.rb`.
|
||||
2. Confirm the app was restarted after updating `/etc/stocks/env`: `sudo systemctl restart stocks`.
|
||||
3. Confirm required env keys exist on the server using SSM. Do not print secret values.
|
||||
4. Confirm SES identity status is `SUCCESS` and DKIM status is `SUCCESS`.
|
||||
5. Confirm SES production access is enabled, or verify the recipient email address while SES is still sandboxed.
|
||||
6. Check recent app logs:
|
||||
|
||||
```console
|
||||
AWS_PROFILE=713141626029_AdministratorAccess AWS_REGION=us-east-1 aws ssm send-command \
|
||||
--instance-ids mi-059a7bcb37754c44d \
|
||||
--document-name AWS-RunShellScript \
|
||||
--parameters '{"commands":["sudo journalctl -u stocks -n 200 --no-pager"]}'
|
||||
```
|
||||
|
||||
If SSM stops working, first confirm the instance is listed as `Online` in `aws ssm describe-instance-information`. If it is missing or offline after a rebuild, install/register the SSM agent again with a new SSM hybrid activation, then enable the agent service so it persists across reboot.
|
||||
|
||||
## DNS And Load Balancers
|
||||
|
||||
The application hostname is `app.sifonline.org`. It should point to the Terraform-managed production Lightsail load balancer:
|
||||
|
||||
```text
|
||||
stocks-production-lb
|
||||
1c0d863aa670cdbc48e7167de9d300ef-1607315586.us-east-1.elb.amazonaws.com
|
||||
```
|
||||
|
||||
Staging uses:
|
||||
|
||||
```text
|
||||
staging-lb
|
||||
d74e239060df7f70047c7878e32ed0c8-1040760270.us-east-1.elb.amazonaws.com
|
||||
```
|
||||
|
||||
If the app returns an AWS ELB `503`, check whether DNS points to an old load balancer or whether the target instance is detached/unhealthy. If HTTPS fails after an HTTP redirect, check that the Lightsail load balancer has a valid attached TLS certificate and exposes port `443`.
|
||||
8
worker-toolkit-stocks-in-the-future/repo/Rakefile
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
||||
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
||||
|
||||
require_relative "config/application"
|
||||
|
||||
Rails.application.load_tasks
|
||||
15
worker-toolkit-stocks-in-the-future/repo/SECURITY.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the latest version [main branch](https://github.com/rubyforgood/stocks-in-the-future) and currently deployed version of this project is in scope for security issues.
|
||||
|
||||
Also, only the production environment is in scope, although, it's ok and normal to test the staging environment.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report a vulnerability by emailing [casa@rubyforgood.org](mailto:polly@rubyforgood.org)
|
||||
|
||||
We will reply to all reported issues.
|
||||
|
||||
We do not have any bug bounty program but we will be happy to list your name in our contributors list! :)
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 154 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 16V21M16 14V21M20 10V21M22 3L13.354 11.646C13.3076 11.6926 13.2524 11.7295 13.1916 11.7547C13.1309 11.7799 13.0658 11.7929 13 11.7929C12.9342 11.7929 12.8691 11.7799 12.8084 11.7547C12.7476 11.7295 12.6924 11.6926 12.646 11.646L9.354 8.354C9.26024 8.26026 9.13308 8.20761 9.0005 8.20761C8.86792 8.20761 8.74076 8.26026 8.647 8.354L2 15M4 18V21M8 14V21" stroke="#EEEFBE" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 555 B |
|
After Width: | Height: | Size: 500 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 21V13C15 12.7348 14.8946 12.4804 14.7071 12.2929C14.5196 12.1054 14.2652 12 14 12H10C9.73478 12 9.48043 12.1054 9.29289 12.2929C9.10536 12.4804 9 12.7348 9 13V21M3 9.99997C2.99993 9.70904 3.06333 9.42159 3.18579 9.15768C3.30824 8.89378 3.4868 8.65976 3.709 8.47197L10.709 2.47297C11.07 2.16788 11.5274 2.00049 12 2.00049C12.4726 2.00049 12.93 2.16788 13.291 2.47297L20.291 8.47197C20.5132 8.65976 20.6918 8.89378 20.8142 9.15768C20.9367 9.42159 21.0001 9.70904 21 9.99997V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V9.99997Z" stroke="#EEEFBE" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 839 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 10H18M16 14H18M6.17004 15.0001C6.37618 14.4141 6.75915 13.9065 7.26606 13.5475C7.77298 13.1884 8.37885 12.9956 9.00004 12.9956C9.62123 12.9956 10.2271 13.1884 10.734 13.5475C11.2409 13.9065 11.6239 14.4141 11.83 15.0001M11 11C11 12.1046 10.1046 13 9 13C7.89543 13 7 12.1046 7 11C7 9.89543 7.89543 9 9 9C10.1046 9 11 9.89543 11 11ZM4 5H20C21.1046 5 22 5.89543 22 7V17C22 18.1046 21.1046 19 20 19H4C2.89543 19 2 18.1046 2 17V7C2 5.89543 2.89543 5 4 5Z" stroke="black" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 651 B |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 152 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 8H10C9.46957 8 8.96086 8.21071 8.58579 8.58579C8.21071 8.96086 8 9.46957 8 10C8 10.5304 8.21071 11.0391 8.58579 11.4142C8.96086 11.7893 9.46957 12 10 12H14C14.5304 12 15.0391 12.2107 15.4142 12.5858C15.7893 12.9609 16 13.4696 16 14C16 14.5304 15.7893 15.0391 15.4142 15.4142C15.0391 15.7893 14.5304 16 14 16H8M12 17.5V6.5M4 2V22L6 21L8 22L10 21L12 22L14 21L16 22L18 21L20 22V2L18 3L16 2L14 3L12 2L10 3L8 2L6 3L4 2Z" stroke="#EEEFBE" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 618 B |
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 500 500">
|
||||
<!-- Generator: Adobe Illustrator 29.8.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #e31837;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
fill: #012169;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path class="st0" d="M251,498.5c-66.3,0-128.7-25.8-175.6-72.7S2.7,316.6,2.7,250.2,28.6,121.6,75.5,74.7,184.7,2,251,2s128.7,25.8,175.6,72.7,72.7,109.2,72.7,175.6-25.8,128.7-72.7,175.6-109.2,72.7-175.6,72.7Z"/>
|
||||
<path d="M251,2.5c33.5,0,65.9,6.6,96.4,19.5,29.5,12.5,56,30.3,78.8,53.1,22.8,22.8,40.6,49.3,53.1,78.8,12.9,30.5,19.5,63,19.5,96.4s-6.6,65.9-19.5,96.4c-12.5,29.5-30.3,56-53.1,78.8-22.8,22.8-49.3,40.6-78.8,53.1-30.5,12.9-63,19.5-96.4,19.5s-65.9-6.6-96.4-19.5c-29.5-12.5-56-30.3-78.8-53.1-22.8-22.8-40.6-49.3-53.1-78.8-12.9-30.5-19.5-63-19.5-96.4s6.6-65.9,19.5-96.4c12.5-29.5,30.3-56,53.1-78.8,22.8-22.8,49.3-40.6,78.8-53.1,30.5-12.9,63-19.5,96.4-19.5M251,1.5C113.6,1.5,2.2,112.9,2.2,250.2s111.4,248.8,248.8,248.8,248.8-111.4,248.8-248.8S388.4,1.5,251,1.5h0Z"/>
|
||||
</g>
|
||||
<path class="st1" d="M251.6,381.1c73.7-63.1,177.1-124.5,234-146.6-8.8-5.6-22.4-13.7-37.9-22.4-73.3,24.2-158.9,77.9-235.4,138.6,13,9.8,26.7,20,39.3,30.5h0Z"/>
|
||||
<path class="st2" d="M217.9,209.5c-10.9-5.3-22.8-10.2-31.9-14.4-27.7,13.7-63.8,34.4-109.4,67,9.8,5.3,20.3,11.6,31.6,17.9,35.1-26,70.5-50.5,109.8-70.5Z"/>
|
||||
<path class="st1" d="M283.5,179.4c-13.3-7.4-59.6-24.6-91.6-31.9-9.5,3.5-22.8,8.8-31.9,12.6,11.6,3.2,54.7,14.7,91.6,33.3,9.5-4.6,22.8-10.2,31.9-14Z"/>
|
||||
<path class="st2" d="M126.7,174.8c-41,18.9-84.2,44.9-107.7,60,8.4,3.9,16.8,7,28.4,12.6,51.9-35.1,92.6-56.5,108.7-63.5-11.6-4.2-22.4-7-29.5-9.1h0Z"/>
|
||||
<path class="st1" d="M314.3,168.5c9.5-3.2,20.3-6,29.8-8.8-27.4-11.6-61.7-23.9-92.6-31.6-4.9,1.4-19.6,5.3-29.8,8.4,10.5,3.2,45.3,11.2,92.6,31.9ZM141.4,300.4c11.2,6.7,23.2,15.8,34.7,23.5,76.8-59.6,152.6-105.6,235.7-131.5-11.6-6-21.7-11.2-34.7-17.5-49.8,12.6-135,46.7-235.7,125.6Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 500 500">
|
||||
<!-- Generator: Adobe Illustrator 29.8.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff5f1;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #dc2329;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
fill: #1670a9;
|
||||
}
|
||||
|
||||
.st3 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st4 {
|
||||
fill: #f5c6b5;
|
||||
}
|
||||
|
||||
.st5 {
|
||||
fill: #7592b9;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path class="st3" d="M251,498.5c-66.3,0-128.7-25.8-175.6-72.7S2.7,316.6,2.7,250.2,28.6,121.6,75.5,74.7,184.7,2,251,2s128.7,25.8,175.6,72.7,72.7,109.2,72.7,175.6-25.8,128.7-72.7,175.6-109.2,72.7-175.6,72.7Z"/>
|
||||
<path d="M251,2.5c33.5,0,65.9,6.6,96.4,19.5,29.5,12.5,56,30.3,78.8,53.1,22.8,22.8,40.6,49.3,53.1,78.8,12.9,30.5,19.5,63,19.5,96.4s-6.6,65.9-19.5,96.4c-12.5,29.5-30.3,56-53.1,78.8-22.8,22.8-49.3,40.6-78.8,53.1-30.5,12.9-63,19.5-96.4,19.5s-65.9-6.6-96.4-19.5c-29.5-12.5-56-30.3-78.8-53.1-22.8-22.8-40.6-49.3-53.1-78.8-12.9-30.5-19.5-63-19.5-96.4s6.6-65.9,19.5-96.4c12.5-29.5,30.3-56,53.1-78.8,22.8-22.8,49.3-40.6,78.8-53.1,30.5-12.9,63-19.5,96.4-19.5M251,1.5C113.6,1.5,2.2,112.9,2.2,250.2s111.4,248.8,248.8,248.8,248.8-111.4,248.8-248.8S388.4,1.5,251,1.5h0Z"/>
|
||||
</g>
|
||||
<path class="st3" d="M438.5,132.7c-40.5,9.6-79.3,24.1-116.9,41.9-16.5,7.8-31.8,13.7-39.1,32-13.7,34.2-.3,63.1,8.3,96.1,5.5,21,9.4,42.6,11.3,64.3.2,1.8-.2,3.5.5,5.3l.5-4.8,10.7-141.7c.6-1.7,16.6-7.4,19.2-9.4s-.3-4.1-1.8-5.5c-3.1-3-7.5-5.4-10.6-8.5l4.8-4.8c37.2-22.1,75.2-43.3,113-64.4"/>
|
||||
<path class="st3" d="M355.2,86.6l-93.1,77.4c-3.9,3-6.9,5.5-11.8,6.8-14.5,3.6-30-2.7-44.9-3.6s-11.2-.4-12.8.5c-7.6,4.3-7.5,30.8.6,34.9,3.5,1.8,8.6-.4,12.1,3.8,5.7,7-1.4,13.4-6.3,18-49.2,52.4-97.7,105.6-145.9,159-1,1.1-2.2,2.3-3.5,3.2h-.5"/>
|
||||
<path class="st1" d="M451.2,125.8c-48.3,10.5-97,23.3-142.7,42.2-24.6,10.2-55.7,22.5-63.5,50.5-16.9,59.9,33.2,115.7,52.4,168.7H51.5s-1.8-.5-1.8-.5c1.3-.9,2.4-2,3.5-3.2,48.2-53.4,96.7-106.6,145.9-159,4.9-4.6,12-11,6.3-18-3.5-4.2-8.5-2-12.1-3.8-8.1-4.1-8.2-30.6-.6-34.9s10.4-.6,12.8-.5c14.9.9,30.5,7.2,44.9,3.6s7.9-3.7,11.8-6.8l93.1-77.4"/>
|
||||
<path class="st1" d="M199.1,174.1c-6.7,1.9-5.7,22.1.4,22.1,4.4,0,5.3-9.8,5.1-13-.2-2.7-1.8-9.7-5.5-9.2Z"/>
|
||||
<polygon class="st4" points="297.4 387.1 297.4 387.7 49.2 387.7 49.2 386.6 49.7 386.6 51.5 387.2 297.4 387.1"/>
|
||||
<path class="st2" d="M438.5,133.2c-37.8,21.1-75.8,42.3-113,64.4l-4.8,4.8c3.2,3.1,7.5,5.5,10.6,8.5s4.2,3.6,1.8,5.5-18.7,7.7-19.2,9.4l-10.7,141.7-1.1-.5c-1.9-21.7-5.8-43.2-11.3-64.3-8.7-33-22-61.9-8.3-96.1,7.3-18.3,22.6-24.1,39.1-32,37.6-17.9,76.4-32.4,116.9-41.9"/>
|
||||
<path class="st5" d="M303.2,367.5l-.5,4.8c-.8-1.8-.4-3.5-.5-5.3l1.1.5Z"/>
|
||||
<path class="st0" d="M199.1,174.1c3.7-.6,5.3,6.5,5.5,9.2.2,3.2-.7,13-5.1,13-6.1,0-7.1-20.2-.4-22.1Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 500 500">
|
||||
<!-- Generator: Adobe Illustrator 29.8.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path class="st0" d="M251,498.5c-66.3,0-128.7-25.8-175.6-72.7S2.7,316.6,2.7,250.2,28.6,121.6,75.5,74.7,184.7,2,251,2s128.7,25.8,175.6,72.7,72.7,109.2,72.7,175.6-25.8,128.7-72.7,175.6-109.2,72.7-175.6,72.7Z"/>
|
||||
<path d="M251,2.5c33.5,0,65.9,6.6,96.4,19.5,29.5,12.5,56,30.3,78.8,53.1,22.8,22.8,40.6,49.3,53.1,78.8,12.9,30.5,19.5,63,19.5,96.4s-6.6,65.9-19.5,96.4c-12.5,29.5-30.3,56-53.1,78.8-22.8,22.8-49.3,40.6-78.8,53.1-30.5,12.9-63,19.5-96.4,19.5s-65.9-6.6-96.4-19.5c-29.5-12.5-56-30.3-78.8-53.1-22.8-22.8-40.6-49.3-53.1-78.8-12.9-30.5-19.5-63-19.5-96.4s6.6-65.9,19.5-96.4c12.5-29.5,30.3-56,53.1-78.8,22.8-22.8,49.3-40.6,78.8-53.1,30.5-12.9,63-19.5,96.4-19.5M251,1.5C113.6,1.5,2.2,112.9,2.2,250.2s111.4,248.8,248.8,248.8,248.8-111.4,248.8-248.8S388.4,1.5,251,1.5h0Z"/>
|
||||
</g>
|
||||
<path id="path3534" d="M151.7,147.7c-5.3,0-10.6.2-16,.7-35.6,3-69.5,22.8-69.6,53,0,15.3,10.9,27.7,27.9,27.1,20-.7,36.4-16.5,45-36.5,3.3-7.6-2.8-10.7-5.6-6.4-5.1,7.9-12.4,14-20.2,17.8-9.4,4.6-19.5,3.3-22.5-4.2-4.3-10.7,4.6-26.6,24.9-34.1,29.3-10.8,60.1-3.6,89.6,1.9.4,0,2,.9.6,1.6-5.3,2.9-10.5,5.2-18.6,12.1-5.8,4.9-13.4,11.5-19.3,18.7-5.8,7.1-10,13.3-15.5,20.3-.8,1-1.7,1-1.7,1-13.4,2.2-26.5,3.7-38.4,12-2.5,1.7-3.6,4.7-2,7,1.4,2,4.4,2.3,6.6.7,7.4-5.5,15.1-8.1,23.9-8.3.4,0,.6,0,.7.3,0,.1,0,.4-.1.6-13.2,18.1-16.5,22.2-26.4,33.4-5.1,5.8-10.1,10.6-15.6,15.7-22.5,20.7-46.7,20.3-55.4,7.4-5.2-7.8-4.5-16-1.1-22.7,4.2-8.3,12.6-14.4,19.7-17.9,2.6-1.3,3.4-4.6.7-7.6-1.7-2-5.7-2.3-8.7-1.8-9.2,1.7-20.1,8.6-26.5,16.9-7.2,9.3-10.9,20.6-9.7,33.3,2.2,22.9,21.2,36.1,42.8,36,17.2-.1,33.4-5.3,52.9-24.9,18.2-18.3,34.7-45.6,50.2-70.9.5-.8.8-1.2,3.2-1.5,9.7-1.2,24.1-2.6,33.3-3.1,2-.1,2.5,0,3.3.9,2.4,3,6,5.5,8.9,7.1,1.4.8,2.1,1.1,3.2,1.2,1.4,0,2.4-.7,3.1-1.5,1-1.1,1.2-2.4.7-3.3-.5-.9-7-4.5-7.2-5.9-.1-.9.8-1.1.8-1.1,4.7-1.5,9.6-4.5,12.6-10.1,2.9-5.6,3.4-13-1.9-17.1-4.7-3.6-11.8-3-17.4,2-5.5,4.9-7.4,12-6.6,18.2.1,1.1,0,1.4-1.4,1.6-8.5.7-16.7.9-25.8,1.4-.6,0-.8-.4-.4-.8,10.5-18,26.2-30.9,43.5-43.6,1.1-.9.8-2.5.8-3.8,31.3,6.7,61.5,19.3,94,19,13.4-.1,26.3-2.1,38.6-8.8,10.8-5.8,15.2-11.2,15.5-16.8.3-3.9-2.5-6.3-6.3-5.5-32.8,7.5-64.1,7.4-96.7,3.5-37.3-4.5-73-13.7-110.1-13.6ZM217.2,200c.8,0,1.5.3,2.1.8,1.7,1.5.9,5.1-.8,7.6-1.9,2.8-5.1,5.2-8.1,5.8-.6.1-1.1-.1-1.3-1.4-.5-3.4.7-8,4.4-11.3,1.2-1,2.6-1.5,3.9-1.5ZM480.4,200.6c-6,0-13.8,0-20,0s-2.2.2-2.9,1.5c-2.3,4.4-30.2,45.8-35.4,53.9-.9,1.2-1.9,1.1-2.1-.3-.5-4.8-4.7-10.3-11.3-12.9-5-2-9.9-2.4-15-1.7-9.2,1.2-17.4,6-24.6,11.4-10.9,8.2-20.2,18.8-32.3,26.6-6.6,4.3-15.6,8-21.7,2.4-5.5-5-4.9-16.2,3.5-24.8.9-.9,1.9-.1,1.8.7-.8,3.9.2,7.8,3.2,10.5,3.7,3.2,8.9,3.6,13.3,1.4,5.3-2.6,8.4-7.6,9.3-13.3,1.4-8.8-5.4-16.6-14-17.2-7-.5-13.6,1.9-20.1,7.2-3.3,2.7-5.1,4.7-7.9,8.6-.7,1-1.7,1.1-1.7-.4.3-11.3-4.4-17.6-13.8-17.8-7.3-.2-15.1,3.8-21.2,8.6-6.7,5.3-12.5,12.3-18.7,18.9-.8.8-1.5.8-1.7-.8-.2-7.5-2.1-14.7-5.6-19.8-1.2-1.8-3.8-2.7-6-1.5-1,.5-4.5,2.1-7.1,4.2-1.3,1-1.8,2.5-1.2,4.4,3.4,11.2,2.6,23.8-1.9,34.6-4.2,9.8-12.4,18.7-21.9,21.7-6.3,2-12.9,1-16.9-4.5-5.6-7.7-3.3-21,5-32,7.3-9.7,18-15.9,28.6-19.9,1.2-.5,1.5-1.3,1.2-2.3-.6-1.7-1.5-4.1-1.9-5.1-1-2.6-3.7-3-7-2.6-7.7.9-14.6,3.5-21.5,7.1-18,9.5-27,28-30.9,37.8-1.9,4.7-3.5,7.7-5.6,10.3-2.8,3.5-6.4,6.7-12.9,12.4-.6.5-1,1.6-.5,2.6.7,1.4,4,6.1,5,6.5,1.1.5,2.3-.2,2.8-.6,4.5-3.1,10-7.9,12.7-11.2.9-1.1,1.9-.7,2.4,1.1,2.5,8.8,9,15.9,18,18.9,16.5,5.5,33.5-2.5,46.4-16.3,8.2-8.8,11.2-14.5,14.3-18.3,5.3-6.5,15.8-21.1,28.3-30.5,4.6-3.5,10-6.1,12.7-4.3,2.2,1.4,2.9,4.7-.6,11-12.7,23-31.5,50.1-34.9,56.7-.6,1.1,0,2,1.1,2,6.7,0,13.3,0,19.3,0,1,0,1.5-.5,2-1.2,9.8-15.2,19-29.6,28.9-44.5.6-.9,1.1-.2,1.1.2.2,3.5,1.1,8.2,3.1,11.3,3.7,5.7,9,8.2,14.9,8.3,4.7,0,7-.6,12.1-2.5,4.1-1.5,7.4-3.5,9.9-5.3,1.5-1,1.7.7,1.7.9-2.3,12,.5,26.3,12.6,32.2,14.5,7.1,30.3-2.9,39.5-11.8.9-.9,1.9-.8,1.9,1.1.2,3.4,1.8,7.5,4.6,10.3,7.7,7.6,23.1,4.6,38.6-7.3,10-7.6,20.4-18.4,29.8-29.8.3-.4.6-1.2,0-1.9-1.4-1.5-3.5-3.2-4.9-4.5-.6-.5-1.6-.4-2.1,0-9.6,9.1-18.3,19.4-30.9,28-4.2,2.9-11,5.3-14,1.2-1.2-1.6-1-3.8.1-6.6,3.7-9,61.6-96.4,64.6-101.6.5-.9,0-1.6-1.2-1.6h0ZM399.2,255.2c1.5,0,2.9.3,4.2.9,7.1,3.4,4.9,11.6,1.7,18-.2.5-.3.9-.1,1.3.1.7,1,1.3,1.9,1.6.3.1.5.3.1,1-2.5,5.2-4.7,8.2-7.8,12.8-2.8,4.2-6,7.5-9.9,10.6-5.8,4.8-14,10-20.7,6.5-3-1.5-4.3-5.7-4.2-9,.2-9.4,4.4-19.1,12.2-28.5,7.7-9.4,16.1-15.2,22.7-15.2Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 500 500">
|
||||
<!-- Generator: Adobe Illustrator 29.8.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path class="st0" d="M251,498.5c-66.3,0-128.7-25.8-175.6-72.7S2.7,316.6,2.7,250.2,28.6,121.6,75.5,74.7,184.7,2,251,2s128.7,25.8,175.6,72.7,72.7,109.2,72.7,175.6-25.8,128.7-72.7,175.6-109.2,72.7-175.6,72.7Z"/>
|
||||
<path d="M251,2.5c33.5,0,65.9,6.6,96.4,19.5,29.5,12.5,56,30.3,78.8,53.1,22.8,22.8,40.6,49.3,53.1,78.8,12.9,30.5,19.5,63,19.5,96.4s-6.6,65.9-19.5,96.4c-12.5,29.5-30.3,56-53.1,78.8-22.8,22.8-49.3,40.6-78.8,53.1-30.5,12.9-63,19.5-96.4,19.5s-65.9-6.6-96.4-19.5c-29.5-12.5-56-30.3-78.8-53.1-22.8-22.8-40.6-49.3-53.1-78.8-12.9-30.5-19.5-63-19.5-96.4s6.6-65.9,19.5-96.4c12.5-29.5,30.3-56,53.1-78.8,22.8-22.8,49.3-40.6,78.8-53.1,30.5-12.9,63-19.5,96.4-19.5M251,1.5C113.6,1.5,2.2,112.9,2.2,250.2s111.4,248.8,248.8,248.8,248.8-111.4,248.8-248.8S388.4,1.5,251,1.5h0Z"/>
|
||||
</g>
|
||||
<polygon class="st0" points="297 56.9 206.6 56.9 205.1 58.5 205.1 128.4 206.6 130 297 130 297 56.9"/>
|
||||
<polygon class="st0" points="297 161.3 205.1 161.3 205.1 455 206.6 456.6 297 456.6 297 161.3"/>
|
||||
<polygon points="297 161.3 297 456.6 206.6 456.6 205.1 455 205.1 161.3 297 161.3"/>
|
||||
<polygon points="297 56.9 297 130 206.6 130 205.1 128.4 205.1 58.5 206.6 56.9 297 56.9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 500 500">
|
||||
<!-- Generator: Adobe Illustrator 29.8.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #e32726;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
fill: #1d3c6d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path class="st0" d="M250,496.4c-66.3,0-128.7-25.8-175.6-72.7S1.7,314.5,1.7,248.1,27.6,119.5,74.5,72.6C121.4,25.7,183.7,0,250,0s128.7,25.8,175.6,72.7c46.9,46.9,72.7,109.2,72.7,175.6s-25.8,128.7-72.7,175.6-109.2,72.7-175.6,72.7h0Z"/>
|
||||
<path d="M250,.4c33.5,0,65.9,6.6,96.4,19.5,29.5,12.5,56,30.3,78.8,53.1,22.8,22.8,40.6,49.3,53.1,78.8,12.9,30.5,19.5,63,19.5,96.4s-6.6,65.9-19.5,96.4c-12.5,29.5-30.3,56-53.1,78.8s-49.3,40.6-78.8,53.1c-30.5,12.9-63,19.5-96.4,19.5s-65.9-6.6-96.4-19.5c-29.5-12.5-56-30.3-78.8-53.1-22.8-22.8-40.6-49.3-53.1-78.8-12.9-30.5-19.5-63-19.5-96.4s6.6-65.9,19.5-96.4c12.5-29.5,30.3-56,53.1-78.8,22.8-22.8,49.3-40.6,78.8-53.1C184.1,7,216.6.4,250,.4M250-.6C112.6-.6,1.2,110.8,1.2,248.1s111.4,248.8,248.8,248.8,248.8-111.4,248.8-248.8S387.4-.6,250-.6h0Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g id="g30">
|
||||
<path id="path32" class="st2" d="M411.5,186.9c0-6.8,0-13.6,0-20.4-5.8,0-11.7,0-17.5,0,0-7.4,0-14.8,0-22.2-7.7,0-15.5,0-23.2,0v22.2h-9.6c0,6.8,0,13.6,0,20.4h9.6s0,0,0,0c0,6.2,0,12.4,0,18.6h0c0,.5,0,1,0,1.5s0,.8,0,1.2h0c0,3.6,0,7.2,0,10.9.2,9.8,3.8,20.3,12,26,7.3,5.2,16.3,6.2,25,6-7.1-7.4-11.4-18.3-13.1-27v-.3c0,0,0-.3,0-.3-.2-1.9-.5-5-.5-8.2,0-.7,0-1.4-.1-2.1,0-1.8,0-3.5,0-5.3,0-7.1,0-14.1,0-21.2,5.8,0,11.7,0,17.5,0"/>
|
||||
</g>
|
||||
<g id="g34">
|
||||
<path id="path36" class="st2" d="M356.9,150.6c0-6.8,0-13.6,0-20.4-11.4-1.9-24.6-1.2-33.8,6.7-8.2,7.4-9.8,19.1-9.5,29.5h-10c0,6.8,0,13.6,0,20.4h8.5c.5,0,1.1,0,1.6,0h0s0,0,0,0v11.2h0c0,16.7,0,33.4,0,50.1h10.8c4.3,0,8.5,0,12.8,0,0-17,0-33.9,0-50.8h0v-10.4h15.7c0-6.8,0-13.6,0-20.4h-15.8c.2-4.6-.3-10.1,3.5-13.5,4.6-3.8,10.9-2.6,16.4-2.3"/>
|
||||
</g>
|
||||
<g id="g38">
|
||||
<path id="path40" class="st2" d="M271.6,248.2h10.5c2,0,4,0,5.9,0h6.6c-.1-17.5.1-35-.1-52.5-.2-10.3-4.1-21.7-13.9-26.5-14-7-30.4-5.4-45.3-3,0,7.4,0,14.9,0,22.3,9.6-2.1,20.1-4.3,29.6-.7,4.4,1.5,6.1,6.2,6.8,10.4-6.7-.8-13.4-2.4-20.1-1.1-8,1-15.8,4.9-20.3,11.7-7.3,10.9-5.2,27.3,5.6,35.1,10.1,7.7,24.7,6.9,35,0,0,1.1,0,3.2,0,4.3M270.6,230.5c-6,1.8-13.2,3.9-18.7-.3-5-3.5-4.3-12,1.1-14.8,5.5-2.9,12-1.9,17.8-.7.5,5.3.5,10.5-.2,15.8"/>
|
||||
</g>
|
||||
<g id="g42">
|
||||
<path id="path44" class="st2" d="M151.3,137.4c9.4,0,18.7,0,28.1,0-13.7,18.5-27.2,37.2-41,55.6,12.3,18.4,24.4,36.9,36.8,55.2-9.7,0-19.4,0-29.2,0-10.8-16.4-21.7-32.9-32.5-49.3,0,16.4,0,32.9,0,49.3-8.4,0-16.8,0-25.1,0,0-36.9,0-73.8,0-110.8,8.4,0,16.7,0,25.1,0,0,17.4,0,34.9,0,52.3,12.5-17.5,25.1-34.8,37.6-52.3"/>
|
||||
</g>
|
||||
<g id="g46">
|
||||
<path id="path48" class="st2" d="M181.1,166.7c7.9,0,15.8,0,23.7,0,0,5.2,0,10.4-.1,15.6,3.8-8.6,12.1-14.2,21.3-15.7,0,7.7.1,15.5,0,23.2-10.1,1.2-20.4,8.7-20.8,19.6-.3,12.9,0,25.9,0,38.8-8,0-15.9,0-23.9,0,0-27.2,0-54.3,0-81.4"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g id="g50">
|
||||
<path id="path52" class="st1" d="M240.8,364.1c-3.3,1.3-4,2-7.9,3.3-4,1.3-8.6,1.3-12.6,0-12.6-5.3-4.6-24.5-4.6-24.5,51.5-12.6,44.3-40.3,30.4-41.6-8.6,0-39.7,1.3-54.2,37-2,5.3-15.9,43,15.2,45.6,6.6,0,13.9-.7,20.5-4,5.9-3.3,10.6-7.3,14.5-12.6,3.3-5.9-1.3-3.3-1.3-3.3M233.5,311.3c4,1.3-5.9,21.1-5.9,21.1-4,1.3-7.9,2.6-10.6,2.6,1.3-2.6,8.6-27.8,16.5-23.8"/>
|
||||
</g>
|
||||
<g id="g54">
|
||||
<path id="path56" class="st1" d="M361,371.4c-.7-.7-2-.7-2.6-1.3-4-2.6-.7-9.9,0-13.9l11.9-42.3c.7-2.6,5.3-16.5-13.2-13.9-15.9,2.6-27.1,12.6-27.1,12.6,1.3-4,2.6-9.9,0-11.2-3.3-1.3-22.5,4.6-27.1,8.6-1.3,0-3.3,2-1.3,4,2,2.6,2.6,4.6,1.3,9.2-2.6,7.9-10.6,35.7-12.6,41-2,5.3-4.6,15.2,0,18.5,3.3,2.6,17.2-3.3,19.2-4.6,2-.7,4-2,4.6-2.6,2.6-2.6-1.3-4,2.6-17.8,0-.7,4.6-14.5,7.9-26.4,2.6-9.9,11.9-14.5,15.2-15.2,5.3-.7,4,4,3.3,7.3-4.6,14.5-9.9,36.3-11.9,41.6-2,5.3-5.3,15.9,0,17.8,4.6,3.3,25.8-5.9,27.8-7.3,4-2.6,4-4,2-4"/>
|
||||
</g>
|
||||
<g id="g58">
|
||||
<path id="path60" class="st1" d="M280.4,371.4c-1.3-.7-2-.7-2.6-1.3-4-2.6-1.3-9.9,0-13.9l12.6-42.3c.7-4.6,3.3-10.6,0-12.6-4-1.3-23.1,4.6-27.8,8.6-.7,0-2.6,2-.7,4,2,2.6,2,4.6.7,9.2-2,7.9-11.2,36.3-13.2,41.6-2,5.3-4.6,15.9,1.3,17.8,4.6,3.3,24.5-5.3,27.1-7.3,4.6-2,4.6-4,2.6-4"/>
|
||||
</g>
|
||||
<g id="g62">
|
||||
<path id="path64" class="st1" d="M276.5,291.4c1.3,2.6,13.2-2.6,19.2-5.9.7-.7,3.3-2.6,4-4.6,1.3-4.6,2-6.6,2.6-9.9.7-1.3.7-3.3-1.3-4-2.6-.7-18.5,1.3-22.5,7.9-1.3,2.6-5.9,15.2-2,16.5"/>
|
||||
</g>
|
||||
<g id="g66">
|
||||
<path id="path68" class="st1" d="M453.6,337.7c-2-7.3-13.9-9.3-18.5-5.3-2.6,2.6-4,7.9-3.3,9.3,11.2-3.3,13.2,6.6,11.2,10.6-4,6-5.3,7.9-10.6,7.9h-35c-.7-.7-1.3-1.3,0-2.6,2-2.6,36.3-49.6,37.7-51.5.7-1.3,2.6-4.6-2-4.6h-38.3c-4,0-5.9,1.3-9.2,4.6-2.6,3.3-5.9,13.2-7.9,19.8,0,0,0,2.6,4-.7.7-.7,2-3.3,4-3.3h23.1c7.3,0-31.1,36.3-43,55.5-2,3.3,1.3,3.3,3.3,3.3h50.2c12.6,0,10.6,2,17.2-7.3,0,0,20.5-26.4,17.2-35.7"/>
|
||||
</g>
|
||||
<g id="g70">
|
||||
<path id="path72" class="st1" d="M177.3,360.2l13.9-53.5c9.2-32.4,14.5-33.7,17.8-37h-21.8c-7.9,0-13.9,11.2-15.2,18.5l-9.3,33.7h-19.8s11.9-43,13.2-47.6c.7-2,.7-4.6-3.3-4.6s-42.3-1.3-55.5,3.3c-5.9,2-14.5,7.3-17.2,15.2-2,4.6-4.6,13.2,1.3,29.1.7,2,2,3.3,2.6,4,2,1.3,19.2-1.3,18.5-1.3,1.3,0,2-.7.7-3.3-1.3-3.3-5.3-9.3-5.9-15.2-2.6-23.1,13.9-26.4,16.5-27.1,4-.7,7.3-.7,11.2,0,1.3,0,4.6.7,5.3,2,1.3,2.6-.7,7.3-2,12.6-1.3,4-12.6,44.9-15.9,56.2-2,8.6-10.6,27.1-19.2,29.7-2,.7-4.6.7-7.3,0-2.6-.7-5.3-2-7.3-3.3-5.3-5.3-8.6-15.9-9.3-23.8-.7-9.9,1.3-17.2-.7-17.2-3.4,0-15,4.6-19.6,7.9,0,0,0,0,0,0-1.9,1.4-2,1.6-2,1.6,0,0-.2.2-.2.3-1.2,2.1-1.1,9.5-.6,14,2,9.9,7.3,21.1,15.2,26.4,21.1,14.5,58.2-11.9,62.1-14.5,5.9-5.3,9.9-12.6,11.2-17.8,2-6.6,5.3-17.8,5.3-17.8h19.8s-7.9,29.7-9.9,37c-2,5.9-3.3,15.9,2,15.2,19.8,0,27.8-7.3,28.4-7.9.7-.7.7-2,0-2.6-1.3-.7-5.9-5.3-3.3-11.9"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 500 500">
|
||||
<!-- Generator: Adobe Illustrator 29.8.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #f40000;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<circle class="st1" cx="251" cy="250.2" r="248.8"/>
|
||||
<g id="Page-1">
|
||||
<g id="Asset-1KO">
|
||||
<path id="Path" class="st1" d="M494.2,251.1c0-134.3-108.9-243.1-243.1-243.1S7.9,116.8,7.9,251.1s108.9,243.1,243.1,243.1,243.1-108.9,243.1-243.1h0"/>
|
||||
<path id="Shape" class="st0" d="M340.6,189.3c-6.8,3.3-12.8,8-17.7,13.8-9.6,10.5-15.1,22.3-11,25.4,1.6,1.2,5.2,1.4,11.7-4.5,6.1-5.3,10.9-12,14-19.5,4.2-10.4,2.9-15.2,2.9-15.3M392.6,208.1c-10.6-5.5-18.3-12.1-26.4-16.2s-12.5-5.5-18.7-4.8c-.9.1-1.7.3-2.6.6,0,0,3.2,10.9-9.5,29.3-12.7,18.4-24.6,18.7-27.8,13.5s1-16.4,8.5-26.2c6.2-7.3,13.5-13.6,21.7-18.8,0,0-4.4-2.6-15.2,3.5-18.2,11.3-33.8,26.4-45.7,44.2-16.8,23.9-20.2,37.2-22.6,47.5-2.4,10.4-.2,21.5,11.3,21.5s24.9-17.6,28.7-23.7c7-11.4,11.7-28.8,2.1-26.4-4.8,1.2-8.6,6.4-10.9,11.4-1.5,3.2-2.7,6.6-3.5,10.1-3.3,1.4-6.5,3.2-9.3,5.5-3.2,2.3-6.1,5-8.8,7.8.7-7.6,3-15,6.7-21.7,4.8-10.4,14.3-17.9,25.5-20.1,6.2-.5,14.3,4.8,7.4,24.2s-26.9,43-48.1,43.3c-19.5.3-32.4-24.7-5.2-68.3,30.3-48.1,65.3-62.9,77-64.6s15.2,3.4,15.2,3.4c6.9-2.8,14.4-4.1,21.8-3.6,14,.9,24.8,10.9,39.1,17.8-3.9,3.6-7.2,7.4-10.5,10.9M455.7,191.9c-9.7,6.3-23.1,11.9-36,10.7-3.3,3.4-6.8,7.3-10.3,11.7,22.4,4.4,42-9.5,52.4-21.3,3.8-4.2,7-8.9,9.7-13.9-4.7,4.9-10,9.2-15.8,12.9M426,255c.2-.3.3-.6.4-.9-.1-1.1-1-1.9-2-1.9-2.1,0-5.1.3-12.7,8.7-6.3,7-11.8,14.7-16.2,23.1-4.4,7.8-6.1,13.9-3.6,15.6.8.4,1.8.5,2.6.3,1.7-.4,3.6-2,6.5-5.4,1.4-1.6,2.9-3.9,4.7-6.3,5-6.9,17.7-28.5,20.2-32.9M346.7,264.2c1.3-3.3,2.9-9.9-.5-12.4s-12.8,3.7-12.9,10.2c.3,4.8,3.5,8.8,8.1,10.2.9.3,1.2.2,1.6-.5,1.4-2.4,2.6-4.9,3.7-7.4M339.8,276.6c-2.3-1-4.4-2.3-6.3-3.9-1.9-1.6-3.5-3.6-4.8-5.8,0-.2-.2,0-.4,0-2,2.5-3.9,5.1-5.6,7.8-4.9,7.2-12.5,22.2-5.6,24.7,4.4,1.6,11.7-6,15.6-11.1,2.7-3.5,5.2-7.3,7.4-11.2,0-.4,0-.5-.2-.6M417.3,293.9c-1.9,3-3.9,8.6,2.2,5.1,7.5-4.3,18.1-20.5,18.1-20.5h4.8c-4.4,6.8-9.2,13.2-14.6,19.2-7.6,8.4-16.8,16-23.4,15.3-7.8-.9-5.2-10.9-5.2-10.9,0,0-10.9,11.7-18.4,10.7-10-1-6.9-13.7-6.9-13.7,0,0-11.7,14.1-19.5,13.7-12.3-.7-9.2-15.6-5.8-26.1,1.8-5.6,3.5-10,3.5-10,0,0-1.2.3-3.9.7-1.3,0-2.6.2-3.9.4,0,0-5.1,9-9.2,14.4s-21.3,24.1-31.4,20.4c-9.5-3.5-6.3-18-.8-29.4,8-16.7,30.6-40.9,43.6-37.7s.3,27.9.3,27.9h0c.8.2,1.7.2,2.6,0,1.7-.3,3.3-.8,4.9-1.4,8-16.6,17.5-32.3,28.5-47.1,15-18.6,44.9-50.6,56.7-42.8,2.8,1.9,1.6,8.5-2.3,16.4-1.7.5-3.5,1-5.3,1.4,2.1-3.6,3.8-7.4,4.8-11.4,1.8-9.6-20.7,10.7-36.7,32.5-9.6,13.1-18.2,27-25.6,41.5,2.9-1.7,5.7-3.7,8.3-5.9,7.1-5.9,13.9-12.3,20.3-19,6.2-6.3,12.1-13,17.5-20,1.7,0,3.4-.2,5.1-.5-5.8,7.9-12.1,15.5-18.9,22.5-6.4,6.5-13.3,13.4-19.5,18.5-5.4,4.6-11,8.8-16.9,12.6,0,0-14.1,28.2-6.4,30.1,4.5,1.1,13.3-12.6,13.3-12.6,0,0,11.7-17.6,18.5-25.6,9.5-10.8,17.7-17,25.9-17.2,3.2.3,6,2.2,7.6,5l2.2-3.5h13.9s-25.8,43.7-27.8,47M455.9,192c-9.7,6.3-23.1,11.9-36,10.7-3.3,3.4-6.8,7.3-10.3,11.7,22.4,4.4,42-9.5,52.4-21.3,3.8-4.2,7-8.9,9.7-13.9-4.7,4.9-10,9.2-15.8,12.9M432.1,304.7h1.9c.6,0,1.1,0,1.7,0,.6.3,1,.9,1,1.6,0,.5-.2.9-.6,1.2-.3.2-.6.3-.9.4.4,0,.8.2,1.1.6.2.3.3.6.4.9v1.2h-1v-1c0-.7,0-1.2-.6-1.4-.3-.2-.7-.3-1.1-.2h-.9v2.4h-1.1l.2-5.6ZM435.8,306.6c0-.5,0-.8-.4-.9-.4-.2-.8-.3-1.3-.2h-.9v2.2h.9c.3,0,.7,0,1,0,.4-.2.6-.5.6-.9M439.6,307.8c0,3.1-2.4,5.5-5.5,5.6-3.1,0-5.5-2.4-5.6-5.5,0-1.5.6-2.9,1.6-4,2.2-2.1,5.6-2.1,7.8,0,1.1,1,1.6,2.4,1.6,3.9M434.1,303c-1.3,0-2.5.5-3.3,1.4-.9.9-1.4,2.1-1.4,3.3,0,1.3.5,2.5,1.4,3.4.9.9,2.1,1.4,3.3,1.4,2.6.2,4.8-1.8,5-4.4,0-.1,0-.2,0-.4,0-1.3-.5-2.5-1.4-3.3-.9-.9-2.1-1.4-3.4-1.4M262.2,213.5h-13.1l-7.4,12h13.1l7.5-12ZM199.9,255c5.6-7.8,21-34.5,21-34.5-.1-1.1-1-1.9-2.1-1.9-2.1,0-5.6.6-12.7,9.5-6.2,7.3-11.7,15.1-16.5,23.4-4.4,7.8-6.3,13.6-3.9,15.1.8.4,1.8.5,2.6.3,2.8-1.1,5.3-3.1,7-5.6,1.4-1.6,2.9-3.9,4.8-6.2M129.7,218.9c-3.6-2.7-12.9,3.7-12.9,10.2,0,6.6,8.3,8.8,9.4,8.8.5.2,1.1,0,1.3-.5.2-.5,1.4-2.6,2.6-5.9s2.9-9.9-.5-12.5M124.3,242.1c-2.5-.8-5-1.9-7.2-3.3-2.1-1.4-3.9-3.3-5.1-5.6,0-.2-.3,0-.4,0s-.9,1.2-5.8,8.4c-4.9,7.2-12.1,22.2-5.3,24.7,4.4,1.6,11.7-6.1,15.6-11.1,3-4,5.8-8.1,8.3-12.4.2-.4,0-.5-.2-.6M231.3,246.5h4.8s-24.1,34.8-37.7,33.3c-7.6-.9-5.3-10.3-5.3-10.3,0,0-10.7,12.5-20.4,10-8.9-2.3-3.2-17.1-3.2-17.1-1,.4-20.2,22.1-33.8,16.9s-6.8-23.6-3.9-29c2.2-4.6,4.7-9.1,4.7-9.1,0,0-2.2.6-3.5.9l-3.4.7s-6.7,10.9-10.8,16.3-21.2,24.1-31.6,20.4-7.8-18-1.9-29.4c9.3-18.5,31.4-41.2,44.7-37.7,13.3,3.4,2.3,25.2,2.3,25.2,0,0,2.6.4,9.1-3.9,9.3-6.3,21.5-23.4,34.7-21.3,6.2.9,12,7.3,3.9,19.5-2.6,3.9-6.8,7.4-10,3.9-2-2.1-.3-6,1.9-8.7,1.7-2.2,4.4-3.4,7.1-3.3,0,0,2.2-5-3.3-4.9-4.5,0-15.8,10.6-22.3,21.1-5.9,9.7-14.8,26.3-5.8,29.9,8.2,3.3,23.8-15.2,31.2-25,7.4-9.7,23.7-31.8,37.5-32.7,3,0,5.9,1.4,7.6,3.9l1.6-2.5h14.1s-26.1,43.5-28.1,46.7c-1.6,2.6-3.9,8.4,2.2,5.1,6.1-3.2,18.1-19,18.1-19M223.8,316.2c-10.2-7-22.4-10.6-34.8-10.1-6.8.2-13.1,3.6-17,9.2-5.3-9.2-15.6-14.8-31.8-14.7-26.1.3-51.9,12.3-72.4,12.3s-34-12.3-33.5-34.2c.8-38.4,33.3-73.1,57.6-88.7,13.9-9,25.6-11.7,33.3-10.7,6.1.9,10.3,6.6,9.4,12.7-.2,1.3-.7,2.6-1.3,3.8-6.2,13.9-14.8,12.7-14.5,6.1.3-3.5,2.2-6.7,5.2-8.5,1.5-.8,3.3-1.2,5-1.2,1.2-1,2.2-8.6-7.8-6.1s-22,12.3-33.5,25c-11.5,12.7-29.2,36.6-33.8,62-2.1,11.7-.7,32.9,27,32.7,23.4,0,58-15.6,89.8-15.3,11.1,0,21.9,2.9,31.4,8.6,8.1,4.7,15.4,10.6,21.6,17.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 500 500">
|
||||
<!-- Generator: Adobe Illustrator 29.8.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #e3000b;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<circle class="st0" cx="251" cy="250.2" r="248.8"/>
|
||||
<g>
|
||||
<polygon class="st0" points="41.6 309.3 27.4 323.5 46.7 329.8 41.6 309.3"/>
|
||||
<polygon class="st0" points="48.7 337 37.7 352.5 53.4 353.6 48.7 337"/>
|
||||
<polygon class="st0" points="34.4 282.9 20 295.5 39.8 302.9 34.4 282.9"/>
|
||||
<polygon class="st0" points="64.4 354.5 32 273.1 17.9 265.7 39.4 252.1 18.6 236.1 41.5 224.8 22.2 207.4 47.4 199.5 30.2 179.8 55.7 174.1 41.8 152.5 67.3 149.9 55.9 126.2 82.7 126.9 75 103.5 101.2 105.6 95.7 81.6 120.4 88.9 118.5 63.1 143 73.5 144.8 48.7 166.9 62.5 170.9 37 191.2 53 199 29.1 217 47.5 227.1 25 243.2 44.7 256 23.4 269.4 44.9 284.5 25.6 294.8 48.6 313.4 30.7 320.6 55.7 340.9 40.4 345.3 65.6 366.8 53.5 367.7 79.6 391.3 69.5 389.1 96.1 414.7 89.5 408 114.1 432.8 111.8 424.1 135 450.6 136 438.1 158 463.1 159.9 447.6 180.1 472.4 187.4 455.5 205.8 478.5 216.7 459.6 232.8 481.3 247.1 461.2 258.8 481.3 275 458.8 285.6 477.4 304.9 453.4 312.6 468.7 332.9 444.4 338.4 458.6 362.4 431.6 362.3 442.6 385.7 417 384.8 424.9 409.2 400.3 403.8 405.8 429.3 380.4 422 382.3 446.4 358.8 436.8 358.3 463.3 336.5 449.7 331.8 473.8 311.5 458.8 303.5 482.7 284.6 464.8 275.4 487.5 258.3 467.9 245.6 488.9 231.8 467.1 216.3 486.4 206 462.4 188.1 481.1 180.4 455.9 159.7 472.4 156.2 446.4 135.2 459.6 135 433.7 110.7 443.5 113.8 418 87.8 424.5 94.8 400.3 69.2 403.4 77.7 379.5 51.7 378.2 64.4 354.5"/>
|
||||
<polygon class="st1" points="29.6 265.5 51.6 259.6 95.3 324.1 95.6 324 100.4 246.4 122 240.6 146.5 331.5 131.7 335.5 112.8 265.3 112.5 265.4 106.6 342.3 93.9 345.7 50.3 282.1 50 282.1 68.9 352.4 54 356.4 29.6 265.5"/>
|
||||
<path class="st1" d="M161.4,230l16.6-4.4,58.7,81.6-16.7,4.5-14.9-21.8-35.6,9.5-1.9,26.4-16.1,4.3,9.8-100.2ZM170.7,286.2l27.2-7.3-23.9-35.5h-.4c0,0-2.9,42.8-2.9,42.8Z"/>
|
||||
<polygon class="st1" points="189.5 222.5 261.7 203 265.4 216.8 237.1 224.4 257.8 301.5 242.2 305.8 221.4 228.6 193.3 236.2 189.5 222.5"/>
|
||||
<polygon class="st1" points="248.7 206.5 321 187.2 324.6 200.9 296.3 208.5 317 285.7 301.4 289.8 280.7 212.7 252.5 220.3 248.7 206.5"/>
|
||||
<polygon class="st1" points="326.7 185.7 388.7 168.9 392.5 182.6 346 195.2 352.4 219 395.3 207.4 398.7 220.4 355.9 232 363 258.5 410.3 245.8 414.1 259.6 351.2 276.5 326.7 185.7"/>
|
||||
<polygon class="st1" points="396.7 166.8 412.3 162.6 433.1 239.8 478.4 227.6 482.1 241.3 421.1 257.7 396.7 166.8"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 500 500">
|
||||
<!-- Generator: Adobe Illustrator 29.8.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path class="st0" d="M251,498.5c-66.3,0-128.7-25.8-175.6-72.7S2.7,316.6,2.7,250.2,28.6,121.6,75.5,74.7,184.7,2,251,2s128.7,25.8,175.6,72.7,72.7,109.2,72.7,175.6-25.8,128.7-72.7,175.6-109.2,72.7-175.6,72.7Z"/>
|
||||
<path d="M251,2.5c33.5,0,65.9,6.6,96.4,19.5,29.5,12.5,56,30.3,78.8,53.1,22.8,22.8,40.6,49.3,53.1,78.8,12.9,30.5,19.5,63,19.5,96.4s-6.6,65.9-19.5,96.4c-12.5,29.5-30.3,56-53.1,78.8-22.8,22.8-49.3,40.6-78.8,53.1-30.5,12.9-63,19.5-96.4,19.5s-65.9-6.6-96.4-19.5c-29.5-12.5-56-30.3-78.8-53.1-22.8-22.8-40.6-49.3-53.1-78.8-12.9-30.5-19.5-63-19.5-96.4s6.6-65.9,19.5-96.4c12.5-29.5,30.3-56,53.1-78.8,22.8-22.8,49.3-40.6,78.8-53.1,30.5-12.9,63-19.5,96.4-19.5M251,1.5C113.6,1.5,2.2,112.9,2.2,250.2s111.4,248.8,248.8,248.8,248.8-111.4,248.8-248.8S388.4,1.5,251,1.5h0Z"/>
|
||||
</g>
|
||||
<path d="M77,347.7c-13.6-.5-24.7-4.3-33.3-11.1-1.7-1.3-5.6-5.3-6.9-6.9-3.5-4.4-5.9-8.7-7.5-13.5-4.9-14.7-2.4-34,7.2-55.2,8.2-18.1,20.9-36.1,43.1-61,3.3-3.7,13-14.4,13-14.4s-.5.9-1.2,2c-5.8,9.6-10.7,21-13.4,30.9-4.3,15.8-3.8,29.3,1.5,39.8,3.7,7.2,10,13.5,17,17,12.4,6.1,30.5,6.6,52.6,1.5,1.5-.4,77-20.4,167.8-44.5,90.8-24.1,165-43.9,165.1-43.9,0,0-210.9,90.3-320.4,137.1-17.3,7.4-22,9.3-30.1,12.1-20.8,7.3-39.5,10.8-54.6,10.2h0Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 500 500">
|
||||
<!-- Generator: Adobe Illustrator 29.8.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path d="M251,498.5c-66.3,0-128.7-25.8-175.6-72.7S2.7,316.6,2.7,250.2,28.6,121.6,75.5,74.7,184.7,2,251,2s128.7,25.8,175.6,72.7,72.7,109.2,72.7,175.6-25.8,128.7-72.7,175.6-109.2,72.7-175.6,72.7Z"/>
|
||||
<path class="st0" d="M251,2.5c33.5,0,65.9,6.6,96.4,19.5,29.5,12.5,56,30.3,78.8,53.1,22.8,22.8,40.6,49.3,53.1,78.8,12.9,30.5,19.5,63,19.5,96.4s-6.6,65.9-19.5,96.4c-12.5,29.5-30.3,56-53.1,78.8-22.8,22.8-49.3,40.6-78.8,53.1-30.5,12.9-63,19.5-96.4,19.5s-65.9-6.6-96.4-19.5c-29.5-12.5-56-30.3-78.8-53.1-22.8-22.8-40.6-49.3-53.1-78.8-12.9-30.5-19.5-63-19.5-96.4s6.6-65.9,19.5-96.4c12.5-29.5,30.3-56,53.1-78.8,22.8-22.8,49.3-40.6,78.8-53.1,30.5-12.9,63-19.5,96.4-19.5M251,1.5C113.6,1.5,2.2,112.9,2.2,250.2s111.4,248.8,248.8,248.8,248.8-111.4,248.8-248.8S388.4,1.5,251,1.5h0Z"/>
|
||||
</g>
|
||||
<g id="g4">
|
||||
<path id="path1" class="st0" d="M231.8,78c-38.2,0-81.6,7.2-118.3,23.7-34.2,15.3-62.1,39.9-62.1,80.6,0,21.3,8.1,41.9,22.7,57.4,9.9,9.1,25.9,24.7,67.6,33.8,18.7,3.9,58.5,10.3,98.2,14.5,39.7,4.2,78.2,7.9,94,12.1,12.5,3.2,33.6,7.5,33.6,31.2s-22.2,30.8-26.1,32.3c-3.9,1.5-30.5,13.8-78.4,13.8-31.7-.9-63.2-6.3-93.3-16.1-17.9-6.4-36.6-14.8-54.1-36.1-7.1-10-11-22-11.2-34.2h-43.4v121.5h48.2v-16.4c0-3.8,3-6.9,6.8-6.9,1.3,0,2.5.3,3.6,1,22.7,9.9,46.3,17.5,70.5,22.8,25.3,5.3,41.7,9.1,73.2,9.1,33.2.6,66.3-4,98-13.8,21.1-5.8,40.8-15.5,58.2-28.7,19.8-15.2,31.4-38.9,31.2-63.9.2-23.4-8.9-46-25.2-62.9-9.1-8.8-19.6-16-31.1-21.3-12.4-5.6-25.2-10.1-38.3-13.4-25-6.1-81.2-13.8-108-16.4-28.2-2.9-77-7-96.5-13-5.9-1.9-18-7.6-18-21.6s5.5-18.4,16.4-25.2c17.3-10.8,52.3-17.6,88.7-17.6,35.3-.4,70.3,6.4,102.8,20.1,8.7,3.8,17,8.7,24.5,14.6,12.2,10.4,20.7,24.6,24.1,40.3h38.9v-105.7h-43.4v12.3c0,3.9-3.9,9.1-11.8,4.9-19.6-10.2-74.5-32.2-141.9-32.5Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 500 500">
|
||||
<!-- Generator: Adobe Illustrator 29.8.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #f50a23;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: url(#radial-gradient);
|
||||
}
|
||||
|
||||
.st2 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
<radialGradient id="radial-gradient" cx="-216" cy="349.6" fx="-216" fy="349.6" r="15.4" gradientTransform="translate(3634.5 -1969.1) rotate(-90) scale(10.7 -9.7)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#f5ff1e"/>
|
||||
<stop offset="1" stop-color="#f50a23"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g>
|
||||
<path class="st2" d="M251,498.5c-66.3,0-128.7-25.8-175.6-72.7S2.7,316.6,2.7,250.2,28.6,121.6,75.5,74.7,184.7,2,251,2s128.7,25.8,175.6,72.7,72.7,109.2,72.7,175.6-25.8,128.7-72.7,175.6-109.2,72.7-175.6,72.7Z"/>
|
||||
<path d="M251,2.5c33.5,0,65.9,6.6,96.4,19.5,29.5,12.5,56,30.3,78.8,53.1,22.8,22.8,40.6,49.3,53.1,78.8,12.9,30.5,19.5,63,19.5,96.4s-6.6,65.9-19.5,96.4c-12.5,29.5-30.3,56-53.1,78.8-22.8,22.8-49.3,40.6-78.8,53.1-30.5,12.9-63,19.5-96.4,19.5s-65.9-6.6-96.4-19.5c-29.5-12.5-56-30.3-78.8-53.1-22.8-22.8-40.6-49.3-53.1-78.8-12.9-30.5-19.5-63-19.5-96.4s6.6-65.9,19.5-96.4c12.5-29.5,30.3-56,53.1-78.8,22.8-22.8,49.3-40.6,78.8-53.1,30.5-12.9,63-19.5,96.4-19.5M251,1.5C113.6,1.5,2.2,112.9,2.2,250.2s111.4,248.8,248.8,248.8,248.8-111.4,248.8-248.8S388.4,1.5,251,1.5h0Z"/>
|
||||
</g>
|
||||
<g id="layer1">
|
||||
<path id="path27" class="st1" d="M60.7,109.5l131,341.1c1,2.6,3.5,4.3,6.3,4.3h106.2c-2.8,0-5.3-1.7-6.3-4.3L166.9,109.5H60.7Z"/>
|
||||
<path id="path29" class="st0" d="M441.4,109.5l-131,341.1c-1,2.6-3.5,4.3-6.3,4.3h-106.2c2.8,0,5.3-1.7,6.3-4.3L335.1,109.5h106.2Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,476 @@
|
||||
/*
|
||||
* Default Trix editor styles. See Action Text overwrites below.
|
||||
*/
|
||||
|
||||
trix-editor {
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 3px;
|
||||
margin: 0;
|
||||
padding: 0.4em 0.6em;
|
||||
min-height: 5em;
|
||||
outline: none; }
|
||||
|
||||
trix-toolbar * {
|
||||
box-sizing: border-box; }
|
||||
|
||||
trix-toolbar .trix-button-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
overflow-x: auto; }
|
||||
|
||||
trix-toolbar .trix-button-group {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #bbb;
|
||||
border-top-color: #ccc;
|
||||
border-bottom-color: #888;
|
||||
border-radius: 3px; }
|
||||
trix-toolbar .trix-button-group:not(:first-child) {
|
||||
margin-left: 1.5vw; }
|
||||
@media (max-width: 768px) {
|
||||
trix-toolbar .trix-button-group:not(:first-child) {
|
||||
margin-left: 0; } }
|
||||
|
||||
trix-toolbar .trix-button-group-spacer {
|
||||
flex-grow: 1; }
|
||||
@media (max-width: 768px) {
|
||||
trix-toolbar .trix-button-group-spacer {
|
||||
display: none; } }
|
||||
|
||||
trix-toolbar .trix-button {
|
||||
position: relative;
|
||||
float: left;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
font-size: 0.75em;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
padding: 0 0.5em;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-radius: 0;
|
||||
background: transparent; }
|
||||
trix-toolbar .trix-button:not(:first-child) {
|
||||
border-left: 1px solid #ccc; }
|
||||
trix-toolbar .trix-button.trix-active {
|
||||
background: #cbeefa;
|
||||
color: black; }
|
||||
trix-toolbar .trix-button:not(:disabled) {
|
||||
cursor: pointer; }
|
||||
trix-toolbar .trix-button:disabled {
|
||||
color: rgba(0, 0, 0, 0.125); }
|
||||
@media (max-width: 768px) {
|
||||
trix-toolbar .trix-button {
|
||||
letter-spacing: -0.01em;
|
||||
padding: 0 0.3em; } }
|
||||
|
||||
trix-toolbar .trix-button--icon {
|
||||
font-size: inherit;
|
||||
width: 2.6em;
|
||||
height: 1.6em;
|
||||
max-width: calc(0.8em + 4vw);
|
||||
text-indent: -9999px; }
|
||||
@media (max-width: 768px) {
|
||||
trix-toolbar .trix-button--icon {
|
||||
height: 2em;
|
||||
max-width: calc(0.8em + 3.5vw); } }
|
||||
trix-toolbar .trix-button--icon::before {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0.6;
|
||||
content: "";
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain; }
|
||||
@media (max-width: 768px) {
|
||||
trix-toolbar .trix-button--icon::before {
|
||||
right: 6%;
|
||||
left: 6%; } }
|
||||
trix-toolbar .trix-button--icon.trix-active::before {
|
||||
opacity: 1; }
|
||||
trix-toolbar .trix-button--icon:disabled::before {
|
||||
opacity: 0.125; }
|
||||
|
||||
/* Show the attach button in Trix toolbar */
|
||||
.trix-button-row > span.trix-button-group {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
[data-trix-button-group="file-tools"] {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.trix-button-row > .trix-button-group-spacer {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.trix-button--icon-attach {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
trix-toolbar .trix-button-group--file-tools {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
trix-toolbar .trix-button-group-spacer {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
trix-toolbar .trix-button--icon-attach,
|
||||
trix-editor .trix-button--icon-attach {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
trix-toolbar .trix-button--icon-attach::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M10.5%2018V7.5c0-2.25%203-2.25%203%200V18c0%204.125-6%204.125-6%200V7.5c0-6.375%209-6.375%209%200V18%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-miterlimit%3D%2210%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
|
||||
top: 8%;
|
||||
bottom: 4%; }
|
||||
|
||||
trix-toolbar .trix-button--icon-bold::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M6.522%2019.242a.5.5%200%200%201-.5-.5V5.35a.5.5%200%200%201%20.5-.5h5.783c1.347%200%202.46.345%203.24.982.783.64%201.216%201.562%201.216%202.683%200%201.13-.587%202.129-1.476%202.71a.35.35%200%200%200%20.049.613c1.259.56%202.101%201.742%202.101%203.22%200%201.282-.483%202.334-1.363%203.063-.876.726-2.132%201.12-3.66%201.12h-5.89ZM9.27%207.347v3.362h1.97c.766%200%201.347-.17%201.733-.464.38-.291.587-.716.587-1.27%200-.53-.183-.928-.513-1.198-.334-.273-.838-.43-1.505-.43H9.27Zm0%205.606v3.791h2.389c.832%200%201.448-.177%201.853-.497.399-.315.614-.786.614-1.423%200-.62-.22-1.077-.63-1.385-.418-.313-1.053-.486-1.905-.486H9.27Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); }
|
||||
|
||||
trix-toolbar .trix-button--icon-italic::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M9%205h6.5v2h-2.23l-2.31%2010H13v2H6v-2h2.461l2.306-10H9V5Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); }
|
||||
|
||||
trix-toolbar .trix-button--icon-link::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M18.948%205.258a4.337%204.337%200%200%200-6.108%200L11.217%206.87a.993.993%200%200%200%200%201.41c.392.39%201.027.39%201.418%200l1.623-1.613a2.323%202.323%200%200%201%203.271%200%202.29%202.29%200%200%201%200%203.251l-2.393%202.38a3.021%203.021%200%200%201-4.255%200l-.05-.049a1.007%201.007%200%200%200-1.418%200%20.993.993%200%200%200%200%201.41l.05.049a5.036%205.036%200%200%200%207.091%200l2.394-2.38a4.275%204.275%200%200%200%200-6.072Zm-13.683%2013.6a4.337%204.337%200%200%200%206.108%200l1.262-1.255a.993.993%200%200%200%200-1.41%201.007%201.007%200%200%200-1.418%200L9.954%2017.45a2.323%202.323%200%200%201-3.27%200%202.29%202.29%200%200%201%200-3.251l2.344-2.331a2.579%202.579%200%200%201%203.631%200c.392.39%201.027.39%201.419%200a.993.993%200%200%200%200-1.41%204.593%204.593%200%200%200-6.468%200l-2.345%202.33a4.275%204.275%200%200%200%200%206.072Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); }
|
||||
|
||||
trix-toolbar .trix-button--icon-strike::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M6%2014.986c.088%202.647%202.246%204.258%205.635%204.258%203.496%200%205.713-1.728%205.713-4.463%200-.275-.02-.536-.062-.781h-3.461c.398.293.573.654.573%201.123%200%201.035-1.074%201.787-2.646%201.787-1.563%200-2.773-.762-2.91-1.924H6ZM6.432%2010h3.763c-.632-.314-.914-.715-.914-1.273%200-1.045.977-1.739%202.432-1.739%201.475%200%202.52.723%202.617%201.914h2.764c-.05-2.548-2.11-4.238-5.39-4.238-3.145%200-5.392%201.719-5.392%204.316%200%20.363.04.703.12%201.02ZM4%2011a1%201%200%201%200%200%202h15a1%201%200%201%200%200-2H4Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); }
|
||||
|
||||
trix-toolbar .trix-button--icon-quote::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M4.581%208.471c.44-.5%201.056-.834%201.758-.995C8.074%207.17%209.201%207.822%2010%208.752c1.354%201.578%201.33%203.555.394%205.277-.941%201.731-2.788%203.163-4.988%203.56a.622.622%200%200%201-.653-.317c-.113-.205-.121-.49.16-.764.294-.286.567-.566.791-.835.222-.266.413-.54.524-.815.113-.28.156-.597.026-.908-.128-.303-.39-.524-.72-.69a3.02%203.02%200%200%201-1.674-2.7c0-.905.283-1.59.72-2.088Zm9.419%200c.44-.5%201.055-.834%201.758-.995%201.734-.306%202.862.346%203.66%201.276%201.355%201.578%201.33%203.555.395%205.277-.941%201.731-2.789%203.163-4.988%203.56a.622.622%200%200%201-.653-.317c-.113-.205-.122-.49.16-.764.294-.286.567-.566.791-.835.222-.266.412-.54.523-.815.114-.28.157-.597.026-.908-.127-.303-.39-.524-.72-.69a3.02%203.02%200%200%201-1.672-2.701c0-.905.283-1.59.72-2.088Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); }
|
||||
|
||||
trix-toolbar .trix-button--icon-heading-1::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21.5%207.5v-3h-12v3H14v13h3v-13h4.5ZM9%2013.5h3.5v-3h-10v3H6v7h3v-7Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); }
|
||||
|
||||
trix-toolbar .trix-button--icon-code::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3.293%2011.293a1%201%200%200%200%200%201.414l4%204a1%201%200%201%200%201.414-1.414L5.414%2012l3.293-3.293a1%201%200%200%200-1.414-1.414l-4%204Zm13.414%205.414%204-4a1%201%200%200%200%200-1.414l-4-4a1%201%200%201%200-1.414%201.414L18.586%2012l-3.293%203.293a1%201%200%200%200%201.414%201.414Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); }
|
||||
|
||||
trix-toolbar .trix-button--icon-bullet-list::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%207.5a1.5%201.5%200%201%200%200-3%201.5%201.5%200%200%200%200%203ZM8%206a1%201%200%200%201%201-1h11a1%201%200%201%201%200%202H9a1%201%200%200%201-1-1Zm1%205a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm0%206a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm-2.5-5a1.5%201.5%200%201%201-3%200%201.5%201.5%200%200%201%203%200ZM5%2019.5a1.5%201.5%200%201%200%200-3%201.5%201.5%200%200%200%200%203Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); }
|
||||
|
||||
trix-toolbar .trix-button--icon-number-list::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3%204h2v4H4V5H3V4Zm5%202a1%201%200%200%201%201-1h11a1%201%200%201%201%200%202H9a1%201%200%200%201-1-1Zm1%205a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm0%206a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm-3.5-7H6v1l-1.5%202H6v1H3v-1l1.667-2H3v-1h2.5ZM3%2017v-1h3v4H3v-1h2v-.5H4v-1h1V17H3Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); }
|
||||
|
||||
trix-toolbar .trix-button--icon-undo::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3%2014a1%201%200%200%200%201%201h6a1%201%200%201%200%200-2H6.257c2.247-2.764%205.151-3.668%207.579-3.264%202.589.432%204.739%202.356%205.174%205.405a1%201%200%200%200%201.98-.283c-.564-3.95-3.415-6.526-6.825-7.095C11.084%207.25%207.63%208.377%205%2011.39V8a1%201%200%200%200-2%200v6Zm2-1Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); }
|
||||
|
||||
trix-toolbar .trix-button--icon-redo::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21%2014a1%201%200%200%201-1%201h-6a1%201%200%201%201%200-2h3.743c-2.247-2.764-5.151-3.668-7.579-3.264-2.589.432-4.739%202.356-5.174%205.405a1%201%200%200%201-1.98-.283c.564-3.95%203.415-6.526%206.826-7.095%203.08-.513%206.534.614%209.164%203.626V8a1%201%200%201%201%202%200v6Zm-2-1Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); }
|
||||
|
||||
trix-toolbar .trix-button--icon-decrease-nesting-level::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%206a1%201%200%200%201%201-1h12a1%201%200%201%201%200%202H6a1%201%200%200%201-1-1Zm4%205a1%201%200%201%200%200%202h9a1%201%200%201%200%200-2H9Zm-3%206a1%201%200%201%200%200%202h12a1%201%200%201%200%200-2H6Zm-3.707-5.707a1%201%200%200%200%200%201.414l2%202a1%201%200%201%200%201.414-1.414L4.414%2012l1.293-1.293a1%201%200%200%200-1.414-1.414l-2%202Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); }
|
||||
|
||||
trix-toolbar .trix-button--icon-increase-nesting-level::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%206a1%201%200%200%201%201-1h12a1%201%200%201%201%200%202H6a1%201%200%200%201-1-1Zm4%205a1%201%200%201%200%200%202h9a1%201%200%201%200%200-2H9Zm-3%206a1%201%200%201%200%200%202h12a1%201%200%201%200%200-2H6Zm-2.293-2.293%202-2a1%201%200%200%200%200-1.414l-2-2a1%201%200%201%200-1.414%201.414L3.586%2012l-1.293%201.293a1%201%200%201%200%201.414%201.414Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); }
|
||||
|
||||
trix-toolbar .trix-dialogs {
|
||||
position: relative; }
|
||||
|
||||
trix-toolbar .trix-dialog {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
font-size: 0.75em;
|
||||
padding: 15px 10px;
|
||||
background: #fff;
|
||||
box-shadow: 0 0.3em 1em #ccc;
|
||||
border-top: 2px solid #888;
|
||||
border-radius: 5px;
|
||||
z-index: 5; }
|
||||
|
||||
trix-toolbar .trix-input--dialog {
|
||||
font-size: inherit;
|
||||
font-weight: normal;
|
||||
padding: 0.5em 0.8em;
|
||||
margin: 0 10px 0 0;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #bbb;
|
||||
background-color: #fff;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none; }
|
||||
trix-toolbar .trix-input--dialog.validate:invalid {
|
||||
box-shadow: #F00 0px 0px 1.5px 1px; }
|
||||
|
||||
trix-toolbar .trix-button--dialog {
|
||||
font-size: inherit;
|
||||
padding: 0.5em;
|
||||
border-bottom: none; }
|
||||
|
||||
trix-toolbar .trix-dialog--link {
|
||||
max-width: 600px; }
|
||||
|
||||
trix-toolbar .trix-dialog__link-fields {
|
||||
display: flex;
|
||||
align-items: baseline; }
|
||||
trix-toolbar .trix-dialog__link-fields .trix-input {
|
||||
flex: 1; }
|
||||
trix-toolbar .trix-dialog__link-fields .trix-button-group {
|
||||
flex: 0 0 content;
|
||||
margin: 0; }
|
||||
|
||||
trix-editor [data-trix-mutable]:not(.attachment__caption-editor) {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none; }
|
||||
|
||||
trix-editor [data-trix-mutable]::-moz-selection,
|
||||
trix-editor [data-trix-cursor-target]::-moz-selection, trix-editor [data-trix-mutable] ::-moz-selection {
|
||||
background: none; }
|
||||
|
||||
trix-editor [data-trix-mutable]::selection,
|
||||
trix-editor [data-trix-cursor-target]::selection, trix-editor [data-trix-mutable] ::selection {
|
||||
background: none; }
|
||||
|
||||
trix-editor .attachment__caption-editor:focus[data-trix-mutable]::-moz-selection {
|
||||
background: highlight; }
|
||||
|
||||
trix-editor .attachment__caption-editor:focus[data-trix-mutable]::selection {
|
||||
background: highlight; }
|
||||
|
||||
trix-editor [data-trix-mutable].attachment.attachment--file {
|
||||
box-shadow: 0 0 0 2px highlight;
|
||||
border-color: transparent; }
|
||||
|
||||
trix-editor [data-trix-mutable].attachment img {
|
||||
box-shadow: 0 0 0 2px highlight; }
|
||||
|
||||
trix-editor .attachment {
|
||||
position: relative; }
|
||||
trix-editor .attachment:hover {
|
||||
cursor: default; }
|
||||
|
||||
trix-editor .attachment--preview .attachment__caption:hover {
|
||||
cursor: text; }
|
||||
|
||||
trix-editor .attachment__progress {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
height: 20px;
|
||||
top: calc(50% - 10px);
|
||||
left: 5%;
|
||||
width: 90%;
|
||||
opacity: 0.9;
|
||||
transition: opacity 200ms ease-in; }
|
||||
trix-editor .attachment__progress[value="100"] {
|
||||
opacity: 0; }
|
||||
|
||||
trix-editor .attachment__caption-editor {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
border: none;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none; }
|
||||
|
||||
trix-editor .attachment__toolbar {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -0.9em;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center; }
|
||||
|
||||
trix-editor .trix-button-group {
|
||||
display: inline-flex; }
|
||||
|
||||
trix-editor .trix-button {
|
||||
position: relative;
|
||||
float: left;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
font-size: 80%;
|
||||
padding: 0 0.8em;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent; }
|
||||
trix-editor .trix-button:not(:first-child) {
|
||||
border-left: 1px solid #ccc; }
|
||||
trix-editor .trix-button.trix-active {
|
||||
background: #cbeefa; }
|
||||
trix-editor .trix-button:not(:disabled) {
|
||||
cursor: pointer; }
|
||||
|
||||
trix-editor .trix-button--remove {
|
||||
text-indent: -9999px;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
line-height: 1.8em;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
border: 2px solid highlight;
|
||||
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.25); }
|
||||
trix-editor .trix-button--remove::before {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0.7;
|
||||
content: "";
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20height%3D%2224%22%20width%3D%2224%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M19%206.41%2017.59%205%2012%2010.59%206.41%205%205%206.41%2010.59%2012%205%2017.59%206.41%2019%2012%2013.41%2017.59%2019%2019%2017.59%2013.41%2012z%22%2F%3E%3Cpath%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 90%; }
|
||||
trix-editor .trix-button--remove:hover {
|
||||
border-color: #333; }
|
||||
trix-editor .trix-button--remove:hover::before {
|
||||
opacity: 1; }
|
||||
|
||||
trix-editor .attachment__metadata-container {
|
||||
position: relative; }
|
||||
|
||||
trix-editor .attachment__metadata {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 2em;
|
||||
transform: translate(-50%, 0);
|
||||
max-width: 90%;
|
||||
padding: 0.1em 0.6em;
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 3px; }
|
||||
trix-editor .attachment__metadata .attachment__name {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
vertical-align: bottom;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
trix-editor .attachment__metadata .attachment__size {
|
||||
margin-left: 0.2em;
|
||||
white-space: nowrap; }
|
||||
|
||||
.trix-content {
|
||||
line-height: 1.5;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word; }
|
||||
.trix-content * {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
.trix-content h1 {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.2; }
|
||||
.trix-content blockquote {
|
||||
border: 0 solid #ccc;
|
||||
border-left-width: 0.3em;
|
||||
margin-left: 0.3em;
|
||||
padding-left: 0.6em; }
|
||||
.trix-content [dir=rtl] blockquote,
|
||||
.trix-content blockquote[dir=rtl] {
|
||||
border-width: 0;
|
||||
border-right-width: 0.3em;
|
||||
margin-right: 0.3em;
|
||||
padding-right: 0.6em; }
|
||||
.trix-content ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.5em; }
|
||||
.trix-content ol {
|
||||
list-style-type: decimal;
|
||||
padding-left: 1.5em; }
|
||||
.trix-content li {
|
||||
margin-left: 1em; }
|
||||
.trix-content [dir=rtl] li {
|
||||
margin-right: 1em; }
|
||||
.trix-content pre {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: top;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
padding: 0.5em;
|
||||
white-space: pre;
|
||||
background-color: #eee;
|
||||
overflow-x: auto; }
|
||||
.trix-content img {
|
||||
max-width: 100%;
|
||||
height: auto; }
|
||||
.trix-content .attachment {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
max-width: 100%; }
|
||||
.trix-content .attachment a {
|
||||
color: inherit;
|
||||
text-decoration: none; }
|
||||
.trix-content .attachment a:hover, .trix-content .attachment a:visited:hover {
|
||||
color: inherit; }
|
||||
.trix-content .attachment__caption {
|
||||
text-align: center; }
|
||||
.trix-content .attachment__caption .attachment__name + .attachment__size::before {
|
||||
content: ' \2022 '; }
|
||||
.trix-content .attachment--preview {
|
||||
width: 100%;
|
||||
text-align: center; }
|
||||
.trix-content .attachment--preview .attachment__caption {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.2; }
|
||||
.trix-content .attachment--file {
|
||||
color: #333;
|
||||
line-height: 1;
|
||||
margin: 0 2px 2px 2px;
|
||||
padding: 0.4em 1em;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 5px; }
|
||||
.trix-content .attachment-gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: relative; }
|
||||
.trix-content .attachment-gallery .attachment {
|
||||
flex: 1 0 33%;
|
||||
padding: 0 0.5em;
|
||||
max-width: 33%; }
|
||||
.trix-content .attachment-gallery.attachment-gallery--2 .attachment, .trix-content .attachment-gallery.attachment-gallery--4 .attachment {
|
||||
flex-basis: 50%;
|
||||
max-width: 50%; }
|
||||
|
||||
/*
|
||||
* We need to override trix.css’s image gallery styles to accommodate the
|
||||
* <action-text-attachment> element we wrap around attachments. Otherwise,
|
||||
* images in galleries will be squished by the max-width: 33%; rule.
|
||||
*/
|
||||
.trix-content .attachment-gallery > action-text-attachment,
|
||||
.trix-content .attachment-gallery > .attachment {
|
||||
flex: 1 0 33%;
|
||||
padding: 0 0.5em;
|
||||
max-width: 33%;
|
||||
}
|
||||
|
||||
.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment,
|
||||
.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment,
|
||||
.trix-content .attachment-gallery.attachment-gallery--4 > .attachment {
|
||||
flex-basis: 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.trix-content action-text-attachment .attachment {
|
||||
padding: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
|
||||
|
||||
/* Global color utilities */
|
||||
.text-green-up {
|
||||
color: #1EB500;
|
||||
}
|
||||
|
||||
.text-destructive {
|
||||
color: #EE4447;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
@layer components {
|
||||
.filter-tabs { @apply flex border-b border-gray-200 mr-8; }
|
||||
|
||||
.filter-tab,
|
||||
.filter-tab:link,
|
||||
.filter-tab:visited {
|
||||
@apply px-4 py-2 text-sm font-medium no-underline border-b-2 border-transparent transition;
|
||||
color: #4b5563 !important;
|
||||
}
|
||||
|
||||
.filter-tab:hover { background-color: #f9fafb !important; color: #374151 !important; }
|
||||
|
||||
.filter-tab:focus { outline: none !important; background-color: #f3f4f6 !important; color: #4b5563 !important; }
|
||||
|
||||
.filter-tab.active { border-bottom-color: #2563eb !important; color: #2563eb !important; }
|
||||
|
||||
.header-controls { @apply flex items-center justify-between mb-4; }
|
||||
.action-buttons { @apply flex gap-2; }
|
||||
|
||||
.form-select { @apply w-full rounded-md border border-input bg-transparent p-2 text-sm; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
@import "./shadcn.css";
|
||||
@import "./tailwind.config.css";
|
||||
@import "./navbar.css";
|
||||
@import "./tables.css";
|
||||
@import "./buttons.css";
|
||||
@import "./forms.css";
|
||||
@import "./admin.css";
|
||||
@@ -0,0 +1,41 @@
|
||||
.tw-btn-sell {
|
||||
@apply box-border content-stretch flex gap-2 h-9 items-center justify-center px-4 py-2 relative rounded-[16px] shrink-0 inline-flex bg-[var(--sitf-primary-blue)] text-white;
|
||||
}
|
||||
|
||||
.tw-btn-buy {
|
||||
@apply box-border content-stretch flex gap-2 h-9 items-center justify-center px-4 py-2 relative rounded-[16px] shrink-0 inline-flex bg-[var(--sitf-4)] text-white;
|
||||
}
|
||||
|
||||
.tw-btn-primary {
|
||||
@apply rounded-lg py-3 px-5 bg-[var(--sitf-primary-blue)] text-white block font-medium cursor-pointer hover:bg-blue-600 transition-colors
|
||||
}
|
||||
|
||||
.tw-btn-primary-disabled {
|
||||
@apply rounded-lg py-3 px-5 bg-gray-300 text-white block font-medium cursor-not-allowed
|
||||
}
|
||||
|
||||
.tw-btn-secondary {
|
||||
@apply rounded-md bg-[#BOEAE5] px-4 py-2 text-sm font-semibold text-white ring-1 ring-gray-300 ring-inset hover:bg-gray-50
|
||||
}
|
||||
|
||||
.tw-btn-tertiary {
|
||||
@apply ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium
|
||||
}
|
||||
|
||||
.tw-btn-danger {
|
||||
@apply rounded-lg py-3 ml-2 px-5 bg-red-100 text-red-500 inline-block font-medium hover:text-red-900
|
||||
}
|
||||
|
||||
/* Font Awesome icon styles */
|
||||
.fa-pencil, .fa-ban {
|
||||
font-size: 1.5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.fa-pencil {
|
||||
color: #3b82f6; /* Blue color for edit */
|
||||
}
|
||||
|
||||
.fa-ban {
|
||||
color: #ef4444; /* Red color for cancel */
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.tw-label-primary {
|
||||
@apply block text-sm/6 font-medium text-gray-900
|
||||
}
|
||||
|
||||
.required-indicator {
|
||||
@apply text-red-500 ml-1
|
||||
}
|
||||
|
||||
.tw-input-primary {
|
||||
@apply block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 border border-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
.navbar {
|
||||
@apply bg-white shadow-xs;
|
||||
}
|
||||
.navbar-container {
|
||||
@apply mx-auto max-w-7xl px-2 sm:px-6 lg:px-8;
|
||||
}
|
||||
.navbar-layout {
|
||||
@apply relative flex h-16 justify-between;
|
||||
}
|
||||
.navbar-mobile-button-container {
|
||||
@apply absolute inset-y-0 left-0 flex items-center sm:hidden;
|
||||
}
|
||||
.navbar-mobile-button {
|
||||
@apply relative inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:ring-2 focus:ring-indigo-500 focus:outline-hidden focus:ring-inset;
|
||||
}
|
||||
.navbar-svg-icon {
|
||||
@apply size-6;
|
||||
}
|
||||
.navbar-left-section {
|
||||
@apply flex flex-1 items-center justify-start sm:items-stretch sm:justify-start;
|
||||
}
|
||||
.navbar-logo-container {
|
||||
@apply flex shrink-0 items-center;
|
||||
}
|
||||
.navbar-logo {
|
||||
@apply h-8 w-auto;
|
||||
}
|
||||
.nav-user-link {
|
||||
@apply text-gray-900 font-medium;
|
||||
}
|
||||
.navbar-main-links-container {
|
||||
@apply hidden sm:ml-6 sm:flex sm:space-x-8;
|
||||
}
|
||||
.navbar-main-link {
|
||||
@apply inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium;
|
||||
}
|
||||
.navbar-main-link-active {
|
||||
@apply border-[#00B8B0] text-gray-900;
|
||||
}
|
||||
.navbar-main-link-default {
|
||||
@apply border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700;
|
||||
}
|
||||
.navbar-right-section {
|
||||
@apply absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0;
|
||||
}
|
||||
.navbar-user-links-container {
|
||||
@apply flex space-x-4;
|
||||
}
|
||||
.navbar-notification-button {
|
||||
@apply relative rounded-full bg-white p-1 text-gray-400 hover:text-gray-500 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-hidden;
|
||||
}
|
||||
|
||||
/* Stock ticker animation styles */
|
||||
.ticker-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticker-content {
|
||||
display: flex;
|
||||
animation: scroll 20s linear infinite;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.ticker-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
margin-right: 2rem;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
@keyframes scroll {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-scroll {
|
||||
animation: scroll 20s linear infinite;
|
||||
}
|
||||
|
||||
/* Trading Floor collapse chevron toggle */
|
||||
details .icon-up {
|
||||
display: none;
|
||||
}
|
||||
|
||||
details[open] .icon-down {
|
||||
display: none;
|
||||
}
|
||||
|
||||
details[open] .icon-up {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 100% 50%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--success: 132, 95.3%, 33.3%, 0.74;
|
||||
--success-foreground: 109 55% 28%;
|
||||
|
||||
--info: 223 78% 42%;
|
||||
--info-foreground: 225 100% 50%;
|
||||
|
||||
--attention: 45 90% 45%;
|
||||
--attention-foreground: 60 98.4% 48.8% 0.82;
|
||||
|
||||
--ring: 215 20.2% 65.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* SITF Color Palette */
|
||||
--sitf-background: #f7f9f3;
|
||||
--sitf-foreground: #000000;
|
||||
--sitf-primary-chart1: #00698c;
|
||||
--sitf-primary-secondary-foreground-card: #ffffff;
|
||||
--sitf-secondary-chart2: #1db8a6;
|
||||
--sitf-accent1-chart3: #D3DF44;
|
||||
--sitf-accent2-chart5: #F59E0B;
|
||||
--sitf-chart4: #EB4A97;
|
||||
--sitf-muted: #f0f0f0;
|
||||
--sitf-status-destructive: #ef4444;
|
||||
--sitf-input: #737373;
|
||||
--sitf-ring: #a5b4fc;
|
||||
|
||||
/* TODO: Legacy variables for backward compatibility - update to use standardized naming above */
|
||||
--sitf-primary-blue: #00698c;
|
||||
--sitf-secondary-teal: #00b8b0;
|
||||
--sitf-6: #161616;
|
||||
--sitf-7: #5c636a;
|
||||
--sitf-8: #a7adaf;
|
||||
--sitf-9: #dee0de;
|
||||
--sitf-dark-blue: #004f6b;
|
||||
--sitf-4: #80cec8;
|
||||
--sitf-5: #d3df44;
|
||||
--sitf-10: #eeefbe;
|
||||
--white: #ffffff;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71% 4%;
|
||||
--foreground: 213 31% 91%;
|
||||
|
||||
--muted: 223 47% 11%;
|
||||
--muted-foreground: 215.4 16.3% 56.9%;
|
||||
|
||||
--accent: 216 34% 17%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 224 71% 4%;
|
||||
--popover-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--border: 216 34% 17%;
|
||||
--input: 216 34% 17%;
|
||||
|
||||
--card: 224 71% 4%;
|
||||
--card-foreground: 213 31% 91%;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/* Table Styles */
|
||||
.table-container {
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
@apply bg-white border-2 border-black rounded-[20px];
|
||||
}
|
||||
|
||||
.table-base {
|
||||
@apply w-full border-collapse font-sans bg-white rounded-[20px];
|
||||
}
|
||||
|
||||
.table-header-row {
|
||||
@apply border-b border-black bg-[#00B8B0];
|
||||
}
|
||||
|
||||
.table-header-cell {
|
||||
@apply h-10 px-7 py-3 font-medium text-base text-black border-b border-black;
|
||||
}
|
||||
|
||||
.table-header-cell-left {
|
||||
@apply text-left;
|
||||
}
|
||||
|
||||
.table-header-cell-left:nth-child(3) {
|
||||
@apply text-left px-4 lg:px-7;
|
||||
}
|
||||
|
||||
.table-header-cell-right {
|
||||
@apply text-right;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.table-header-cell-center {
|
||||
@apply text-center;
|
||||
}
|
||||
|
||||
.table-body-row {
|
||||
@apply h-14 border-b border-black;
|
||||
}
|
||||
|
||||
.table-body-cell {
|
||||
@apply px-7 py-2 border-b border-black;
|
||||
}
|
||||
|
||||
.table-body-cell:nth-child(3) {
|
||||
@apply px-4 lg:px-7;
|
||||
}
|
||||
|
||||
.table-body-cell-right {
|
||||
@apply text-right;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.table-body-cell-center {
|
||||
@apply text-center;
|
||||
}
|
||||
|
||||
.table-cell-content {
|
||||
@apply text-base text-black;
|
||||
}
|
||||
|
||||
.table-empty-cell {
|
||||
@apply px-7 py-4 text-center text-gray-500 border-b border-black;
|
||||
}
|
||||
|
||||
.table-no-permission {
|
||||
@apply text-gray-400;
|
||||
}
|
||||
|
||||
.table-action-link {
|
||||
@apply text-blue-600 hover:text-blue-800;
|
||||
}
|
||||
|
||||
.table-action-button {
|
||||
@apply text-red-600 hover:text-red-800 cursor-pointer;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
@theme {
|
||||
--default-dark-mode: class;
|
||||
|
||||
--container-center: true;
|
||||
--container-padding: 2rem;
|
||||
--breakpoint-2xl: 1400px;
|
||||
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
|
||||
--color-success: hsl(var(--success));
|
||||
--color-success-foreground: hsl(var(--success-foreground));
|
||||
|
||||
--color-info: hsl(var(--info));
|
||||
--color-info-foreground: hsl(var(--info-foreground));
|
||||
|
||||
--color-attention: var(--yellow-50);
|
||||
--color-attention-foreground: hsl(var(--attention-foreground));
|
||||
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
|
||||
/* Border radius */
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
|
||||
/* Font family */
|
||||
--font-sans:
|
||||
Inter, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
|
||||
/* Keyframes */
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes accordion-down {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes accordion-up {
|
||||
from {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ApplicationCable
|
||||
class Connection < ActionCable::Connection::Base
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Shadcn
|
||||
class FormBuilder < ActionView::Helpers::FormBuilder
|
||||
def label(method, options = {})
|
||||
error_class = @object.errors[method].any? ? "error" : ""
|
||||
options[:class] = @template.tw("#{options[:class]} #{error_class}")
|
||||
@template.render_label(name: "#{object_name}[#{method}]", label: label_for(@object, method), **options)
|
||||
end
|
||||
|
||||
def text_field(method, options = {})
|
||||
error_class = @object.errors[method].any? ? "error" : ""
|
||||
options[:class] = @template.tw("#{options[:class]} #{error_class}")
|
||||
@template.render_input(
|
||||
name: "#{object_name}[#{method}]",
|
||||
id: "#{object_name}_#{method}",
|
||||
value: @object.send(method),
|
||||
type: "text", **options
|
||||
)
|
||||
end
|
||||
|
||||
def password_field(method, options = {})
|
||||
error_class = @object.errors[method].any? ? "error" : ""
|
||||
options[:class] = @template.tw("#{options[:class]} #{error_class}")
|
||||
@template.render_input(
|
||||
name: "#{object_name}[#{method}]",
|
||||
id: "#{object_name}_#{method}",
|
||||
value: @object.send(method),
|
||||
type: "password", **options
|
||||
)
|
||||
end
|
||||
|
||||
def email_field(method, options = {})
|
||||
error_class = @object.errors[method].any? ? "error" : ""
|
||||
options[:class] = @template.tw("#{options[:class]} #{error_class}")
|
||||
@template.render_input(
|
||||
name: "#{object_name}[#{method}]",
|
||||
id: "#{object_name}_#{method}",
|
||||
value: @object.send(method),
|
||||
type: "email", **options
|
||||
)
|
||||
end
|
||||
|
||||
def submit(value = nil, options = {})
|
||||
@template.render_button(value, **options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def label_for(object, method)
|
||||
return method.capitalize if object.nil?
|
||||
|
||||
object.class.human_attribute_name(method)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,83 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class AnnouncementsController < BaseController
|
||||
before_action :set_announcement, only: %i[show edit update destroy]
|
||||
|
||||
def index
|
||||
sort_column = params[:sort].presence || "created_at"
|
||||
sort_direction = params[:direction] == "desc" ? :desc : :asc
|
||||
|
||||
@announcements = Announcement.reorder(sort_column => sort_direction)
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Announcements" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "Announcements", path: admin_announcements_path },
|
||||
{ label: @announcement.title }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@announcement = Announcement.new
|
||||
@breadcrumbs = [
|
||||
{ label: "Announcements", path: admin_announcements_path },
|
||||
{ label: "New Announcement" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
@breadcrumbs = [
|
||||
{ label: "Announcements", path: admin_announcements_path },
|
||||
{ label: @announcement.title, path: admin_announcement_path(@announcement) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
@announcement = Announcement.new(announcement_params)
|
||||
|
||||
if @announcement.save
|
||||
redirect_to admin_announcement_path(@announcement), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Announcements", path: admin_announcements_path },
|
||||
{ label: "New Announcement" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @announcement.update(announcement_params)
|
||||
redirect_to admin_announcement_path(@announcement), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Announcements", path: admin_announcements_path },
|
||||
{ label: @announcement.title, path: admin_announcement_path(@announcement) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@announcement.destroy
|
||||
redirect_to admin_announcements_path, notice: t(".notice")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_announcement
|
||||
@announcement = Announcement.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def announcement_params
|
||||
params.expect(announcement: %i[title content featured])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Base controller for in-house admin.
|
||||
# All Admin controllers inherit from this controller.
|
||||
module Admin
|
||||
class BaseController < ApplicationController
|
||||
layout "admin"
|
||||
|
||||
before_action :authenticate_admin
|
||||
|
||||
private
|
||||
|
||||
def authenticate_admin
|
||||
redirect_to root_path, alert: t("application.access_denied.admin_required") unless current_user&.admin?
|
||||
end
|
||||
|
||||
# Apply sorting to a collection based on params
|
||||
# @param collection [ActiveRecord::Relation] The base collection to sort
|
||||
# @param default [String] The default column to sort by
|
||||
# @return [ActiveRecord::Relation] The sorted collection
|
||||
def apply_sorting(collection, default:)
|
||||
sort_column = params[:sort].presence || default
|
||||
sort_direction = params[:direction] == "desc" ? :desc : :asc
|
||||
collection.reorder(sort_column => sort_direction)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,83 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class ClassroomsController < BaseController
|
||||
before_action :set_classroom, only: %i[show edit update toggle_archive]
|
||||
|
||||
def index
|
||||
@classrooms = apply_sorting(Classroom.all, default: "name")
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Classrooms" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "Classrooms", path: admin_classrooms_path },
|
||||
{ label: @classroom.name }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@classroom = Classroom.new
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Classrooms", path: admin_classrooms_path },
|
||||
{ label: "New Classroom" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
@breadcrumbs = [
|
||||
{ label: "Classrooms", path: admin_classrooms_path },
|
||||
{ label: @classroom.name, path: admin_classroom_path(@classroom) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
@classroom = Classroom.new(classroom_params)
|
||||
|
||||
if @classroom.save
|
||||
redirect_to admin_classroom_path(@classroom), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Classrooms", path: admin_classrooms_path },
|
||||
{ label: "New Classroom" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @classroom.update(classroom_params)
|
||||
redirect_to admin_classroom_path(@classroom), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Classrooms", path: admin_classrooms_path },
|
||||
{ label: @classroom.name, path: admin_classroom_path(@classroom) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_archive
|
||||
authorize @classroom, :toggle_archive?
|
||||
@classroom.update!(archived: !@classroom.archived)
|
||||
flash[:notice] = @classroom.archived? ? "Classroom has been archived." : "Classroom has been activated."
|
||||
redirect_to admin_classroom_path(@classroom)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_classroom
|
||||
@classroom = Classroom.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def classroom_params
|
||||
params.expect(classroom: [:name, :trading_enabled, :school_year_id, { grade_ids: [] }])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class ComponentDemoController < BaseController
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def index
|
||||
# Demo data for table component - apply sorting before limit
|
||||
@users = User.all
|
||||
|
||||
# Apply sorting if params present
|
||||
if params[:sort].present?
|
||||
direction = params[:direction] == "desc" ? :desc : :asc
|
||||
@users = @users.order(params[:sort] => direction)
|
||||
end
|
||||
|
||||
@users = @users.limit(10)
|
||||
|
||||
# Demo data for show component
|
||||
@sample_user = User.first || User.new(
|
||||
email: "demo@example.com",
|
||||
name: "Demo User",
|
||||
admin: true,
|
||||
created_at: Time.current
|
||||
)
|
||||
|
||||
# Demo breadcrumbs
|
||||
@breadcrumbs = [
|
||||
{ label: "Components", path: admin_component_demo_index_path },
|
||||
{ label: "Demo" }
|
||||
]
|
||||
|
||||
# Demo table columns
|
||||
@columns = [
|
||||
{ attribute: :id, label: "ID", sortable: true },
|
||||
{ attribute: :email, label: "Email", sortable: true },
|
||||
{ attribute: :name, label: "Name", sortable: true },
|
||||
{ attribute: :admin, label: "Admin", sortable: true },
|
||||
{ attribute: :created_at, label: "Created", sortable: true }
|
||||
]
|
||||
|
||||
# Demo filters
|
||||
@filters = [
|
||||
{
|
||||
name: :type,
|
||||
label: "User Type",
|
||||
options: [["All", ""], ["Admin", "admin"], ["Teacher", "teacher"], ["Student", "student"]]
|
||||
},
|
||||
{
|
||||
name: :status,
|
||||
label: "Status",
|
||||
options: [["All", ""], ["Active", "active"], ["Inactive", "inactive"]]
|
||||
}
|
||||
]
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
def show
|
||||
@user = User.find(params.expect(:id))
|
||||
@breadcrumbs = [
|
||||
{ label: "Components", path: admin_component_demo_index_path },
|
||||
{ label: "User Details" }
|
||||
]
|
||||
end
|
||||
|
||||
def form
|
||||
@user = User.first || User.new(email: "", name: "", admin: false)
|
||||
|
||||
# Add validation errors for demo purposes if requested
|
||||
if params[:show_errors] == "true"
|
||||
@user.errors.add(:email, "can't be blank")
|
||||
@user.errors.add(:name, "is too short (minimum is 3 characters)")
|
||||
end
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Components", path: admin_component_demo_index_path },
|
||||
{ label: "Form Builder Demo" }
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class DashboardController < BaseController
|
||||
def index
|
||||
# Dashboard will show overview of admin resources
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,90 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class PortfolioTransactionsController < BaseController
|
||||
before_action :set_portfolio_transaction, only: %i[show edit update destroy]
|
||||
|
||||
def show
|
||||
# TODO: FIX
|
||||
# authorize @portfolio_transaction
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Portfolio Transaction ##{@portfolio_transaction.id}" }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@portfolio_transaction = PortfolioTransaction.new
|
||||
# TODO: FIX
|
||||
# authorize @portfolio_transaction
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Portfolio Transactions", path: "#" },
|
||||
{ label: "New" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
# TODO: Determine if explicit authorization is needed since BaseController already restricts to admins
|
||||
# authorize @portfolio_transaction
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Portfolio Transaction ##{@portfolio_transaction.id}",
|
||||
path: admin_portfolio_transaction_path(@portfolio_transaction) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
@portfolio_transaction = PortfolioTransaction.new(portfolio_transaction_params)
|
||||
# TODO: Determine if explicit authorization is needed since BaseController already restricts to admins
|
||||
# authorize @portfolio_transaction
|
||||
|
||||
if @portfolio_transaction.save
|
||||
redirect_to admin_portfolio_transaction_path(@portfolio_transaction),
|
||||
notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Portfolio Transactions", path: "#" },
|
||||
{ label: "New" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
# TODO: Determine if explicit authorization is needed since BaseController already restricts to admins
|
||||
# authorize @portfolio_transaction
|
||||
|
||||
if @portfolio_transaction.update(portfolio_transaction_params)
|
||||
redirect_to admin_portfolio_transaction_path(@portfolio_transaction),
|
||||
notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Portfolio Transaction ##{@portfolio_transaction.id}",
|
||||
path: admin_portfolio_transaction_path(@portfolio_transaction) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
# TODO: Determine if explicit authorization is needed since BaseController already restricts to admins
|
||||
# authorize @portfolio_transaction
|
||||
@portfolio_transaction.destroy
|
||||
|
||||
redirect_to admin_root_path, notice: t(".notice")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_portfolio_transaction
|
||||
@portfolio_transaction = PortfolioTransaction.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def portfolio_transaction_params
|
||||
params.expect(portfolio_transaction: %i[portfolio_id transaction_type reason description amount_cents])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,111 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class SchoolYearsController < BaseController
|
||||
before_action :set_school_year, only: %i[show edit update destroy]
|
||||
|
||||
def index
|
||||
@school_years = apply_sorting(SchoolYear.includes(:school, :year), default: "id")
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "School Years" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "School Years", path: admin_school_years_path },
|
||||
{ label: @school_year.to_s }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@school_year = SchoolYear.new
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "School Years", path: admin_school_years_path },
|
||||
{ label: "New School Year" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
@breadcrumbs = [
|
||||
{ label: "School Years", path: admin_school_years_path },
|
||||
{ label: @school_year.to_s, path: admin_school_year_path(@school_year) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
school = School.find_by(id: school_year_params[:school_id])
|
||||
year = Year.find_by(id: school_year_params[:year_id])
|
||||
|
||||
unless school && year
|
||||
@school_year = SchoolYear.new(school_year_params)
|
||||
@school_year.valid?
|
||||
return render_new_with_errors
|
||||
end
|
||||
|
||||
@school_year = SchoolYear.create!(school:, year:)
|
||||
|
||||
if @school_year.persisted?
|
||||
redirect_to admin_school_year_path(@school_year), notice: t(".notice")
|
||||
else
|
||||
render_new_with_errors
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
save_error(school_year_params)
|
||||
render_new_with_errors
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@school_year = e.record
|
||||
render_new_with_errors
|
||||
end
|
||||
|
||||
def update
|
||||
if @school_year.update(school_year_params)
|
||||
redirect_to admin_school_year_path(@school_year), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "School Years", path: admin_school_years_path },
|
||||
{ label: @school_year.to_s, path: admin_school_year_path(@school_year) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @school_year.destroy
|
||||
redirect_to admin_school_years_path, notice: t(".notice")
|
||||
else
|
||||
redirect_to admin_school_years_path, alert: t(".delete_restricted")
|
||||
end
|
||||
rescue ActiveRecord::DeleteRestrictionError
|
||||
redirect_to admin_school_years_path, alert: t(".delete_restricted")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_school_year
|
||||
@school_year = SchoolYear.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def school_year_params
|
||||
params.expect(school_year: %i[school_id year_id])
|
||||
end
|
||||
|
||||
def render_new_with_errors
|
||||
@breadcrumbs = [
|
||||
{ label: "School Years", path: admin_school_years_path },
|
||||
{ label: "New School Year" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def save_error(params)
|
||||
@school_year = SchoolYear.new(params)
|
||||
@school_year.valid?
|
||||
@school_year.errors.add(:base, "A School year with this school and year already exists.")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class SchoolsController < BaseController
|
||||
before_action :set_school, only: %i[show edit update destroy]
|
||||
|
||||
def index
|
||||
sort_column = params[:sort].presence || "name"
|
||||
sort_direction = params[:direction] == "desc" ? :desc : :asc
|
||||
|
||||
@schools = School.reorder(sort_column => sort_direction)
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Schools" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "Schools", path: admin_schools_path },
|
||||
{ label: @school.name }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@school = School.new
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Schools", path: admin_schools_path },
|
||||
{ label: "New School" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Schools", path: admin_schools_path },
|
||||
{ label: @school.name, path: admin_school_path(@school) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
year_ids = school_params[:year_ids]&.reject(&:blank?)
|
||||
@school = School.new(school_params.except(:year_ids))
|
||||
@school.year_ids = year_ids if year_ids.present?
|
||||
|
||||
if @school.save
|
||||
redirect_to admin_school_path(@school), notice: t(".notice")
|
||||
else
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Schools", path: admin_schools_path },
|
||||
{ label: "New School" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
year_ids = school_params[:year_ids]&.reject(&:blank?)
|
||||
update_params = school_params.except(:year_ids)
|
||||
update_params[:year_ids] = year_ids || []
|
||||
|
||||
if @school.update(update_params)
|
||||
redirect_to admin_school_path(@school), notice: t(".notice")
|
||||
else
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Schools", path: admin_schools_path },
|
||||
{ label: @school.name, path: admin_school_path(@school) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@school.destroy
|
||||
redirect_to admin_schools_path, notice: t(".notice")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_school
|
||||
@school = School.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def set_form_data
|
||||
@years = Year.ordered_by_start_year
|
||||
end
|
||||
|
||||
def school_params
|
||||
params.expect(school: [:name, { year_ids: [] }])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,108 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class StocksController < BaseController
|
||||
before_action :set_stock, only: %i[show edit update destroy]
|
||||
|
||||
def index
|
||||
@stocks = apply_sorting(Stock.all, default: "ticker")
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Stocks" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "Stocks", path: admin_stocks_path },
|
||||
{ label: @stock.ticker }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@stock = Stock.new
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Stocks", path: admin_stocks_path },
|
||||
{ label: "New Stock" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
@breadcrumbs = [
|
||||
{ label: "Stocks", path: admin_stocks_path },
|
||||
{ label: @stock.ticker, path: admin_stock_path(@stock) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
@stock = Stock.new(stock_params)
|
||||
|
||||
if @stock.save
|
||||
redirect_to admin_stock_path(@stock), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Stocks", path: admin_stocks_path },
|
||||
{ label: "New Stock" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @stock.update(stock_params)
|
||||
redirect_to admin_stock_path(@stock), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Stocks", path: admin_stocks_path },
|
||||
{ label: @stock.ticker, path: admin_stock_path(@stock) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@stock.destroy
|
||||
redirect_to admin_stocks_path, notice: t(".notice")
|
||||
rescue ActiveRecord::DeleteRestrictionError => e
|
||||
redirect_to admin_stocks_path, alert: "Cannot delete stock: #{e.message}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_stock
|
||||
@stock = Stock.find(params.expect(:id))
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def stock_params
|
||||
params.expect(
|
||||
stock: %i[
|
||||
ticker
|
||||
company_name
|
||||
company_website
|
||||
stock_exchange
|
||||
description
|
||||
price_cents
|
||||
yesterday_price_cents
|
||||
employees
|
||||
industry
|
||||
cash_flow
|
||||
debt
|
||||
debt_to_equity
|
||||
profit_margin
|
||||
sales_growth
|
||||
industry_avg_debt_to_equity
|
||||
industry_avg_profit_margin
|
||||
industry_avg_sales_growth
|
||||
management
|
||||
competitor_names
|
||||
archived
|
||||
]
|
||||
)
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,235 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
# rubocop:disable Metrics/ClassLength
|
||||
class StudentsController < BaseController
|
||||
include SoftDeletableFiltering
|
||||
|
||||
before_action :set_student, only: %i[show edit update destroy add_transaction]
|
||||
before_action :set_discarded_student, only: %i[restore]
|
||||
|
||||
def index
|
||||
@students = apply_sorting(scoped_by_discard_status(Student), default: "username")
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Students" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@attendance_entries = AttendanceEntryPresenter.for_student(@student)
|
||||
@earnings_summary = EarningsSummary.new(@student.portfolio) if @student.portfolio.present?
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Students", path: admin_students_path },
|
||||
{ label: @student.username }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@student = Student.new
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Students", path: admin_students_path },
|
||||
{ label: "New Student" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
@breadcrumbs = [
|
||||
{ label: "Students", path: admin_students_path },
|
||||
{ label: @student.username, path: admin_student_path(@student) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
@student = Student.new(student_params)
|
||||
|
||||
# Generate a memorable password if not provided
|
||||
if @student.password.blank?
|
||||
generated_password = MemorablePasswordGenerator.generate
|
||||
@student.password = generated_password
|
||||
@student.password_confirmation = generated_password
|
||||
else
|
||||
generated_password = @student.password
|
||||
end
|
||||
|
||||
if @student.save
|
||||
redirect_to admin_student_path(@student),
|
||||
notice: t(".notice", username: @student.username, password: generated_password)
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Students", path: admin_students_path },
|
||||
{ label: "New Student" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @student.update(student_params)
|
||||
redirect_to admin_student_path(@student), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Students", path: admin_students_path },
|
||||
{ label: @student.username, path: admin_student_path(@student) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
username = @student.username
|
||||
@student.discard
|
||||
redirect_to admin_students_path, notice: t(".notice", username: username)
|
||||
end
|
||||
|
||||
def restore
|
||||
username = @student.username
|
||||
@student.undiscard
|
||||
redirect_to admin_students_path(discarded: true), notice: t(".notice", username: username)
|
||||
end
|
||||
|
||||
def add_transaction
|
||||
errors = validate_transaction_params
|
||||
|
||||
if errors.present?
|
||||
redirect_to edit_admin_student_path(@student), alert: errors.join(", ")
|
||||
else
|
||||
transaction = PortfolioTransaction.new(
|
||||
portfolio: @student.portfolio,
|
||||
amount_cents: transaction_amount_cents,
|
||||
transaction_type: transaction_type,
|
||||
reason: transaction_reason,
|
||||
description: transaction_description
|
||||
)
|
||||
|
||||
if transaction.save
|
||||
redirect_to admin_student_path(@student), notice: t(".notice")
|
||||
else
|
||||
redirect_to edit_admin_student_path(@student), alert: transaction.errors.full_messages.join(", ")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def import
|
||||
return redirect_with_missing_file_error if params[:csv_file].blank?
|
||||
|
||||
begin
|
||||
results = BulkStudentImportService.import_from_csv(params.expect(:csv_file).path)
|
||||
redirect_with_import_results(results)
|
||||
rescue CSV::MalformedCSVError => e
|
||||
redirect_to admin_students_path, alert: "Invalid CSV format: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
def template
|
||||
send_data BulkStudentImportService.generate_csv_template,
|
||||
filename: "student_import_template.csv",
|
||||
type: "text/csv",
|
||||
disposition: "attachment"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_discarded_student
|
||||
@student = Student.with_discarded.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def set_student
|
||||
@student = Student.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def student_params
|
||||
params.expect(student: %i[username classroom_id password password_confirmation])
|
||||
end
|
||||
|
||||
def transaction_params
|
||||
params.permit(:add_fund_amount, :transaction_type, :transaction_reason, :transaction_description)
|
||||
end
|
||||
|
||||
def transaction_amount_cents
|
||||
amount = transaction_params[:add_fund_amount]
|
||||
amount.present? ? (amount.to_f * 100).to_i : nil
|
||||
end
|
||||
|
||||
def transaction_type
|
||||
transaction_params[:transaction_type]
|
||||
end
|
||||
|
||||
def transaction_reason
|
||||
transaction_params[:transaction_reason]
|
||||
end
|
||||
|
||||
def transaction_description
|
||||
transaction_params[:transaction_description]
|
||||
end
|
||||
|
||||
def validate_transaction_params
|
||||
errors = []
|
||||
errors << t("admin.students.add_transaction.errors.transaction_type_blank") if transaction_type.blank?
|
||||
errors << t("admin.students.add_transaction.errors.amount_blank") if transaction_amount_cents.blank?
|
||||
errors << t("admin.students.add_transaction.errors.reason_blank") if transaction_reason.blank?
|
||||
errors
|
||||
end
|
||||
|
||||
def redirect_with_missing_file_error
|
||||
redirect_to admin_students_path, alert: t(".errors.no_file")
|
||||
end
|
||||
|
||||
def redirect_with_import_results(results)
|
||||
return redirect_with_no_results_error if results.empty?
|
||||
|
||||
created, skipped, failed = partition_results(results)
|
||||
success_messages = build_success_messages(created, skipped)
|
||||
|
||||
if failed.any?
|
||||
redirect_with_mixed_results(success_messages, failed)
|
||||
else
|
||||
redirect_to admin_students_path, notice: success_messages.join(". ")
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_with_no_results_error
|
||||
redirect_to admin_students_path, alert: t(".errors.no_students")
|
||||
end
|
||||
|
||||
def partition_results(results)
|
||||
[
|
||||
results.select(&:created?),
|
||||
results.select(&:skipped?),
|
||||
results.select(&:failed?)
|
||||
]
|
||||
end
|
||||
|
||||
def build_success_messages(created, skipped)
|
||||
messages = []
|
||||
messages << build_created_message(created) if created.any?
|
||||
messages << build_skipped_message(skipped) if skipped.any?
|
||||
messages
|
||||
end
|
||||
|
||||
def build_created_message(created)
|
||||
usernames = created.map { |item| item.student.username }
|
||||
"Successfully created #{created.count} students: #{usernames.join(', ')}"
|
||||
end
|
||||
|
||||
def build_skipped_message(skipped)
|
||||
"Skipped #{skipped.count} existing usernames"
|
||||
end
|
||||
|
||||
def redirect_with_mixed_results(success_messages, failed)
|
||||
error_messages = failed.map { |item| "Row #{item.line_number}: #{item.error_message}" }
|
||||
alert_message = "#{failed.count} errors occurred: #{error_messages.join(', ')}"
|
||||
|
||||
if success_messages.any?
|
||||
redirect_to admin_students_path, notice: success_messages.join(". "), alert: alert_message
|
||||
else
|
||||
redirect_to admin_students_path, alert: alert_message
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/ClassLength
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
module Teachers
|
||||
class DeactivationsController < BaseController
|
||||
before_action :set_teacher
|
||||
|
||||
def create
|
||||
username = @teacher.username
|
||||
@teacher.discard
|
||||
redirect_back_or_to admin_teachers_path, notice: t(".notice", username: username)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_teacher
|
||||
@teacher = Teacher.find(params.expect(:teacher_id))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
module Teachers
|
||||
class ReactivationsController < BaseController
|
||||
before_action :set_teacher
|
||||
|
||||
def create
|
||||
username = @teacher.username
|
||||
@teacher.undiscard
|
||||
redirect_back_or_to admin_teachers_path, notice: t(".notice", username: username)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_teacher
|
||||
@teacher = Teacher.with_discarded.find(params.expect(:teacher_id))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,115 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class TeachersController < BaseController
|
||||
include SoftDeletableFiltering
|
||||
|
||||
before_action :set_teacher, only: %i[show edit update destroy]
|
||||
before_action :require_deactivated, only: %i[destroy]
|
||||
|
||||
def index
|
||||
@teachers = apply_sorting(scoped_by_discard_status(Teacher), default: "username")
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Teachers" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "Teachers", path: admin_teachers_path },
|
||||
{ label: @teacher.username }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@teacher = Teacher.new
|
||||
set_form_data
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Teachers", path: admin_teachers_path },
|
||||
{ label: "New Teacher" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Teachers", path: admin_teachers_path },
|
||||
{ label: @teacher.username, path: admin_teacher_path(@teacher) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
temp_password = Devise.friendly_token.first(20)
|
||||
classroom_ids = teacher_params[:classroom_ids]&.reject(&:blank?)
|
||||
|
||||
@teacher = Teacher.new(teacher_params.except(:classroom_ids).merge(password: temp_password))
|
||||
@teacher.classroom_ids = classroom_ids if classroom_ids.present?
|
||||
|
||||
if @teacher.save
|
||||
@teacher.send_reset_password_instructions
|
||||
redirect_to admin_teacher_path(@teacher), notice: t(".notice")
|
||||
else
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Teachers", path: admin_teachers_path },
|
||||
{ label: "New Teacher" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
classroom_ids = teacher_params[:classroom_ids]&.reject(&:blank?)
|
||||
update_params = teacher_params.except(:classroom_ids)
|
||||
update_params[:classroom_ids] = classroom_ids if classroom_ids.present?
|
||||
|
||||
if @teacher.update(update_params)
|
||||
redirect_to admin_teacher_path(@teacher), notice: t(".notice")
|
||||
else
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Teachers", path: admin_teachers_path },
|
||||
{ label: @teacher.username, path: admin_teacher_path(@teacher) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
username = @teacher.username
|
||||
@teacher.really_destroy!
|
||||
redirect_to admin_teachers_path, notice: t(".notice", username: username)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_teacher
|
||||
@teacher = Teacher.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def require_deactivated
|
||||
return if @teacher.discarded?
|
||||
|
||||
redirect_to edit_admin_teacher_path(@teacher), alert: t("admin.teachers.destroy.must_be_deactivated")
|
||||
end
|
||||
|
||||
def teacher_params
|
||||
params.expect(teacher: [:email, :name, { classroom_ids: [] }])
|
||||
end
|
||||
|
||||
def set_form_data
|
||||
active_years = Year.current_school_year(Date.current)
|
||||
@schools = School.joins(:school_years).where(school_years: { year_id: active_years.ids }).distinct.order(:name)
|
||||
|
||||
@selected_school_id = params[:school_id] || @teacher.classrooms.first&.school&.id
|
||||
|
||||
@classrooms = Classroom.active.joins(:school_year).where(school_years: { year_id: active_years.ids })
|
||||
@classrooms = @classrooms.where(school_years: { school_id: @selected_school_id }) if @selected_school_id.present?
|
||||
@classrooms = @classrooms.order(:name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class UsersController < BaseController
|
||||
before_action :set_user, only: %i[show edit update destroy]
|
||||
|
||||
def index
|
||||
@users = apply_sorting(User.all, default: "username")
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Users" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "Users", path: admin_users_path },
|
||||
{ label: @user.username }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@user = User.new
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Users", path: admin_users_path },
|
||||
{ label: "New User" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
@breadcrumbs = [
|
||||
{ label: "Users", path: admin_users_path },
|
||||
{ label: @user.username, path: admin_user_path(@user) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.new(user_params)
|
||||
|
||||
if @user.save
|
||||
redirect_to admin_user_path(@user), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Users", path: admin_users_path },
|
||||
{ label: "New User" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @user.update(user_params)
|
||||
redirect_to admin_user_path(@user), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Users", path: admin_users_path },
|
||||
{ label: @user.username, path: admin_user_path(@user) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@user.destroy
|
||||
redirect_to admin_users_path, notice: t(".notice")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = User.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.expect(user: %i[username email type admin classroom_id password password_confirmation])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AnnouncementsController < ApplicationController
|
||||
before_action :set_announcement, only: %i[show]
|
||||
|
||||
def show; end
|
||||
|
||||
private
|
||||
|
||||
def set_announcement
|
||||
@announcement = Announcement.find(params.expect(:id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to root_path, alert: t("announcements.not_found")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
include Pundit::Authorization
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||
before_action :set_navbar_stocks
|
||||
|
||||
protected
|
||||
|
||||
def ensure_teacher_or_admin
|
||||
authorize :application, :teacher_or_admin_required?
|
||||
end
|
||||
|
||||
def ensure_admin
|
||||
authorize :application, :admin_required?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_navbar_stocks
|
||||
@navbar_stocks = policy_scope(Stock).active
|
||||
end
|
||||
|
||||
def configure_permitted_parameters
|
||||
devise_parameter_sanitizer.permit(:sign_up, keys: %i[username type classroom_id])
|
||||
devise_parameter_sanitizer.permit(:account_update, keys: %i[username email])
|
||||
end
|
||||
|
||||
rescue_from Pundit::NotAuthorizedError do
|
||||
if current_user.nil?
|
||||
# go to the login page
|
||||
redirect_to new_user_session_path, alert: t("devise.failure.unauthenticated")
|
||||
elsif current_user.student?
|
||||
redirect_to current_user&.portfolio_path, alert: t("application.access_denied.no_access")
|
||||
else
|
||||
redirect_to root_url, alert: t("application.access_denied.no_access")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Manages student enrollments in classrooms
|
||||
class ClassroomEnrollmentsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_teacher_or_admin
|
||||
before_action :set_classroom
|
||||
before_action :set_enrollment, only: %i[destroy unenroll]
|
||||
|
||||
# POST /classrooms/:classroom_id/classroom_enrollments
|
||||
def create
|
||||
student = Student.find(enrollment_params[:student_id])
|
||||
|
||||
enrollment = student.enroll_in!(
|
||||
@classroom,
|
||||
primary: enrollment_params[:primary] == "true"
|
||||
)
|
||||
|
||||
if enrollment.persisted?
|
||||
redirect_to classroom_path(@classroom),
|
||||
notice: "#{student.username} enrolled in #{@classroom.name}"
|
||||
else
|
||||
redirect_to classroom_path(@classroom),
|
||||
alert: "Failed to enroll student: #{enrollment.errors.full_messages.join(', ')}"
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /classrooms/:classroom_id/classroom_enrollments/:id
|
||||
def destroy
|
||||
student = @enrollment.student
|
||||
@enrollment.destroy
|
||||
|
||||
redirect_to classroom_path(@classroom),
|
||||
notice: "Removed #{student.username}'s enrollment from #{@classroom.name}"
|
||||
end
|
||||
|
||||
# PATCH /classrooms/:classroom_id/classroom_enrollments/:id/unenroll
|
||||
def unenroll
|
||||
student = @enrollment.student
|
||||
@enrollment.unenroll!
|
||||
|
||||
redirect_to classroom_path(@classroom),
|
||||
notice: "#{student.username} unenrolled from #{@classroom.name}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_classroom
|
||||
@classroom = Classroom.find(params.expect(:classroom_id))
|
||||
end
|
||||
|
||||
def set_enrollment
|
||||
@enrollment = @classroom.classroom_enrollments.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def enrollment_params
|
||||
params.expect(classroom_enrollment: %i[student_id primary])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,110 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ClassroomsController < ApplicationController
|
||||
before_action :set_classroom, only: %i[show edit update toggle_trading]
|
||||
before_action :authorize_classroom, except: %i[edit update toggle_trading]
|
||||
before_action :authorize_classroom_instance, only: %i[edit update toggle_trading]
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_teacher_or_admin, except: %i[index show]
|
||||
before_action :check_classroom_eligibility, only: :show
|
||||
|
||||
def index
|
||||
@classrooms = policy_scope(Classroom).includes(:teachers, students: :portfolio)
|
||||
@classrooms = Classroom.apply_sorting(@classrooms, params[:sort], params[:direction])
|
||||
end
|
||||
|
||||
def show
|
||||
facade = ClassroomFacade.new(@classroom)
|
||||
@students = facade.students
|
||||
@can_manage_students = current_user.teacher_or_admin?
|
||||
@classroom_stats = facade.stats if @can_manage_students
|
||||
end
|
||||
|
||||
def new
|
||||
@classroom = Classroom.new
|
||||
dropdown_data
|
||||
end
|
||||
|
||||
def edit
|
||||
dropdown_data
|
||||
end
|
||||
|
||||
def create
|
||||
@classroom = Classroom.new(classroom_params.except(:school_id, :year_id))
|
||||
assign_school_year_to_classroom
|
||||
|
||||
if @classroom.save
|
||||
redirect_to classroom_url(@classroom), notice: t(".notice")
|
||||
else
|
||||
dropdown_data
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
assign_school_year_to_classroom
|
||||
|
||||
if @classroom.update(classroom_params.except(:school_id, :year_id))
|
||||
redirect_to classroom_url(@classroom), notice: t(".notice")
|
||||
else
|
||||
dropdown_data
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_trading
|
||||
if @classroom.update(trading_enabled: !@classroom.trading_enabled)
|
||||
notice_key = @classroom.trading_enabled? ? :enabled : :disabled
|
||||
redirect_to classroom_url(@classroom), notice: t(".#{notice_key}")
|
||||
else
|
||||
redirect_to classroom_url(@classroom), alert: t(".alert")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_classroom
|
||||
@classroom = Classroom.includes(users: :portfolio).find(params.expect(:id).to_i)
|
||||
end
|
||||
|
||||
def authorize_classroom
|
||||
authorize Classroom
|
||||
end
|
||||
|
||||
def authorize_classroom_instance
|
||||
authorize @classroom
|
||||
end
|
||||
|
||||
def classroom_params
|
||||
params.expect(classroom: [:name, :trading_enabled, :school_id, :year_id, { grade_ids: [] }, { teacher_ids: [] }])
|
||||
end
|
||||
|
||||
def dropdown_data
|
||||
@schools = School.order(:name)
|
||||
@years = Year.order(:name)
|
||||
@teachers = Teacher.all.sort_by(&:display_name)
|
||||
end
|
||||
|
||||
def assign_school_year_to_classroom
|
||||
return unless classroom_params[:school_id].present? && classroom_params[:year_id].present?
|
||||
|
||||
school = School.find(classroom_params[:school_id])
|
||||
year = Year.find(classroom_params[:year_id])
|
||||
school_year = SchoolYear.find_or_create_by!(school: school, year: year)
|
||||
@classroom.school_year = school_year
|
||||
end
|
||||
|
||||
def check_classroom_eligibility
|
||||
# Redirect if classroom is archived and user is not an admin
|
||||
if @classroom.archived? && !current_user.admin?
|
||||
redirect_to root_path, alert: t("classrooms.archived.alert")
|
||||
return
|
||||
end
|
||||
|
||||
return if current_user.admin? ||
|
||||
(current_user.teacher? &&
|
||||
@classroom.teachers.include?(current_user))
|
||||
|
||||
redirect_to root_path, alert: t("application.access_denied.no_access")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SoftDeletableFiltering
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
private
|
||||
|
||||
# Scopes a resource class based on discard status query parameters
|
||||
#
|
||||
# @param resource_class [ActiveRecord::Base] The model class to scope
|
||||
# @return [ActiveRecord::Relation] Scoped collection based on params
|
||||
#
|
||||
# Query parameters:
|
||||
# - discarded (any value): Returns only discarded records
|
||||
# - all (any value): Returns all records (including discarded)
|
||||
# - (none): Returns only kept (non-discarded) records
|
||||
def scoped_by_discard_status(resource_class)
|
||||
if params[:discarded].present?
|
||||
resource_class.discarded
|
||||
elsif params[:all].present?
|
||||
resource_class.with_discarded
|
||||
else
|
||||
resource_class.kept
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# app/controllers/grade_books_controller.rb
|
||||
class GradeBooksController < ApplicationController
|
||||
before_action :set_classroom_and_grade_book
|
||||
before_action :authorize_grade_book
|
||||
def show; end
|
||||
|
||||
def update
|
||||
GradeEntry.transaction do
|
||||
grade_entry_params.each do |id, attrs|
|
||||
entry = @grade_book.grade_entries.find(id)
|
||||
entry.update!(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
@grade_book.grade_entries.reload
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to classroom_grade_book_path(@classroom, @grade_book), notice: t(".notice") }
|
||||
format.turbo_stream
|
||||
end
|
||||
end
|
||||
|
||||
def finalize
|
||||
if @grade_book.completed?
|
||||
redirect_to classroom_grade_book_path(@classroom, @grade_book),
|
||||
alert: t(".already_completed")
|
||||
else
|
||||
@grade_book.verified!
|
||||
DistributeEarnings.execute(@grade_book)
|
||||
redirect_to classroom_grade_book_path(@classroom, @grade_book),
|
||||
notice: t(".notice")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_grade_book
|
||||
authorize @grade_book
|
||||
end
|
||||
|
||||
def set_classroom_and_grade_book
|
||||
@classroom = Classroom.find(params.expect(:classroom_id))
|
||||
@grade_book = @classroom.grade_books.includes(grade_entries: :user).find(params.expect(:id))
|
||||
|
||||
# Redirect if classroom is archived and user is not an admin
|
||||
return unless @classroom.archived? && !current_user.admin?
|
||||
|
||||
redirect_to root_path, alert: t("classrooms.archived.alert")
|
||||
end
|
||||
|
||||
def grade_entry_params
|
||||
params.require(:grade_entries).transform_values do |entry|
|
||||
entry.permit(:math_grade, :reading_grade, :attendance_days, :is_perfect_attendance)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class HomeController < ApplicationController
|
||||
def index
|
||||
@current_announcement = Announcement.current
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OrdersController < ApplicationController
|
||||
before_action :set_order, only: %i[edit update cancel]
|
||||
before_action :set_stock, only: %i[new]
|
||||
before_action :set_shared_owned, only: %i[new edit]
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@orders = policy_scope(Order)
|
||||
@orders = Order.apply_sorting(@orders, params[:sort], params[:direction])
|
||||
end
|
||||
|
||||
def new
|
||||
@order = Order.new(action: params[:transaction_type], stock: @stock)
|
||||
end
|
||||
|
||||
def edit; end
|
||||
|
||||
def create
|
||||
@order = Order.new(order_params.merge(user: current_user))
|
||||
|
||||
respond_to do |format|
|
||||
if @order.save
|
||||
format.html { redirect_to orders_url, notice: t(".notice") }
|
||||
format.turbo_stream { redirect_to orders_url, notice: t(".notice") }
|
||||
format.json { render :show, status: :created, location: @order }
|
||||
else
|
||||
setup_error_data
|
||||
format.html { render :new, status: :unprocessable_content }
|
||||
format.turbo_stream { render :new, status: :unprocessable_content }
|
||||
format.json { render json: @order.errors, status: :unprocessable_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @order.update(order_params)
|
||||
format.html { redirect_to orders_url, notice: t(".notice") }
|
||||
format.json { render :show, status: :ok, location: @order }
|
||||
else
|
||||
format.html { render :edit, status: :unprocessable_content }
|
||||
format.json { render json: @order.errors, status: :unprocessable_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cancel
|
||||
authorize @order
|
||||
|
||||
if @order.pending?
|
||||
cancel_order
|
||||
else
|
||||
invalid_order_response
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Strong parameters
|
||||
def order_params
|
||||
params.expect(order: %i[stock_id shares action])
|
||||
end
|
||||
|
||||
def set_order
|
||||
@order = Order.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def set_stock
|
||||
@stock = Stock.find(params.expect(:stock_id))
|
||||
end
|
||||
|
||||
def set_shared_owned
|
||||
@shares_owned = current_user.portfolio&.shares_owned(@stock&.id)
|
||||
end
|
||||
|
||||
def setup_error_data
|
||||
@stock = @order.stock
|
||||
@shares_owned = current_user.portfolio&.shares_owned(@stock&.id)
|
||||
end
|
||||
|
||||
def unauthorized_response
|
||||
respond_to do |format|
|
||||
format.html { redirect_to orders_url, alert: t(".unauthorized") }
|
||||
format.json { render json: { error: t(".unauthorized") }, status: :forbidden }
|
||||
end
|
||||
end
|
||||
|
||||
def cancel_order
|
||||
@order.cancel!
|
||||
respond_to do |format|
|
||||
format.html { redirect_to orders_url, notice: t(".success") }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_order_response
|
||||
respond_to do |format|
|
||||
format.html { redirect_to orders_url, alert: t(".pending_only") }
|
||||
format.json { render json: { error: t(".pending_only") }, status: :unprocessable_content }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@order.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to orders_url, notice: t(".notice") }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PortfoliosController < ApplicationController
|
||||
before_action :set_portfolio
|
||||
before_action :authenticate_user!
|
||||
|
||||
def show
|
||||
authorize @portfolio
|
||||
@stocks = @portfolio.stocks
|
||||
@earnings_summary = EarningsSummary.new(@portfolio)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_portfolio
|
||||
@portfolio = Portfolio.find(params.expect(:id))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,72 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SchoolsController < ApplicationController
|
||||
before_action :set_school, only: %i[show edit update destroy]
|
||||
before_action :authenticate_user!
|
||||
|
||||
# GET /schools or /schools.json
|
||||
def index
|
||||
@schools = School.all
|
||||
end
|
||||
|
||||
# GET /schools/1 or /schools/1.json
|
||||
def show; end
|
||||
|
||||
# GET /schools/new
|
||||
def new
|
||||
@school = School.new
|
||||
end
|
||||
|
||||
# GET /schools/1/edit
|
||||
def edit; end
|
||||
|
||||
# POST /schools or /schools.json
|
||||
def create
|
||||
@school = School.new(school_params)
|
||||
|
||||
respond_to do |format|
|
||||
if @school.save
|
||||
format.html { redirect_to school_url(@school), notice: t(".notice") }
|
||||
format.json { render :show, status: :created, location: @school }
|
||||
else
|
||||
format.html { render :new, status: :unprocessable_content }
|
||||
format.json { render json: @school.errors, status: :unprocessable_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /schools/1 or /schools/1.json
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @school.update(school_params)
|
||||
format.html { redirect_to school_url(@school), notice: t(".notice") }
|
||||
format.json { render :show, status: :ok, location: @school }
|
||||
else
|
||||
format.html { render :edit, status: :unprocessable_content }
|
||||
format.json { render json: @school.errors, status: :unprocessable_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /schools/1 or /schools/1.json
|
||||
def destroy
|
||||
@school.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to schools_url, notice: t(".notice") }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_school
|
||||
@school = School.find(params.expect(:id))
|
||||
end
|
||||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def school_params
|
||||
params.expect(school: [:name])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class StocksController < ApplicationController
|
||||
before_action :set_stock, only: %i[show]
|
||||
before_action :authenticate_user!
|
||||
before_action :set_portfolio
|
||||
|
||||
def index
|
||||
@stocks = policy_scope(Stock).includes(portfolio_stocks: :portfolio)
|
||||
@portfolio = current_user.portfolio if current_user.student?
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @stock
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_portfolio
|
||||
@portfolio = current_user.portfolio
|
||||
end
|
||||
|
||||
def set_stock
|
||||
@stock = Stock.find(params.expect(:id))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class StudentsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_teacher_or_admin
|
||||
before_action :set_classroom
|
||||
before_action :set_student, except: %i[new create]
|
||||
|
||||
def new
|
||||
@student = Student.new(classroom: @classroom)
|
||||
end
|
||||
|
||||
def edit; end
|
||||
|
||||
def create
|
||||
@student = Student.new(student_params)
|
||||
@student.classroom = @classroom
|
||||
@student.password = generate_memorable_password
|
||||
|
||||
if @student.save
|
||||
redirect_to classroom_path(@classroom),
|
||||
notice: t(".notice", username: @student.username, password: @student.password)
|
||||
else
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @student.update(student_params)
|
||||
redirect_to classroom_path(@classroom), notice: t(".notice")
|
||||
else
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
username = @student.username
|
||||
@student.discard
|
||||
redirect_to classroom_path(@classroom), notice: t(".notice", username: username)
|
||||
end
|
||||
|
||||
def reset_password
|
||||
new_password = generate_memorable_password
|
||||
@student.update!(password: new_password)
|
||||
redirect_to classroom_path(@classroom),
|
||||
notice: t(".notice", username: @student.username, password: new_password)
|
||||
end
|
||||
|
||||
def generate_password
|
||||
new_password = generate_memorable_password
|
||||
@student.update!(password: new_password)
|
||||
redirect_to classroom_path(@classroom),
|
||||
notice: t(".notice", username: @student.username, password: new_password)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_classroom
|
||||
@classroom = Classroom.find(params.expect(:classroom_id))
|
||||
|
||||
# Redirect if classroom is archived and user is not an admin
|
||||
return unless @classroom.archived? && !current_user.admin?
|
||||
|
||||
redirect_to root_path, alert: t("classrooms.archived.alert")
|
||||
end
|
||||
|
||||
def set_student
|
||||
@student = @classroom.users.students.kept.find(params.expect(:id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to classroom_path(@classroom), alert: t("students.not_found")
|
||||
end
|
||||
|
||||
def student_params
|
||||
params.expect(student: %i[username email])
|
||||
end
|
||||
|
||||
def generate_memorable_password
|
||||
MemorablePasswordGenerator.generate
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Facade for classroom presentation logic
|
||||
class ClassroomFacade
|
||||
attr_reader :classroom
|
||||
|
||||
def initialize(classroom)
|
||||
@classroom = classroom
|
||||
end
|
||||
|
||||
# Get all students for this classroom (both via enrollments and legacy classroom_id)
|
||||
#
|
||||
# @return [ActiveRecord::Relation<Student>]
|
||||
def students
|
||||
enrolled_student_ids = classroom.current_students.pluck(:id)
|
||||
legacy_student_ids = classroom.users.students.kept.pluck(:id)
|
||||
all_student_ids = (enrolled_student_ids + legacy_student_ids).uniq
|
||||
|
||||
Student.kept.where(id: all_student_ids).includes(
|
||||
:portfolio,
|
||||
:orders,
|
||||
portfolio: :portfolio_transactions
|
||||
)
|
||||
end
|
||||
|
||||
def stats
|
||||
return {} unless classroom
|
||||
|
||||
students = classroom.users.students.kept
|
||||
{
|
||||
total_students: students.count,
|
||||
active_students: students.joins(:orders).distinct.count,
|
||||
total_portfolio_value: students.includes(:portfolio).sum do |student|
|
||||
student.portfolio&.calculate_total_value || 0
|
||||
end,
|
||||
recent_orders_count: Order.joins(:user).where(users: { classroom: classroom }).where(
|
||||
"orders.created_at > ?",
|
||||
1.week.ago
|
||||
).count
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,409 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
# rubocop:disable Metrics/ClassLength
|
||||
class FormBuilder < ActionView::Helpers::FormBuilder
|
||||
# Tailwind CSS classes for form elements
|
||||
INPUT_CLASSES = "block w-full rounded-md border-0 py-2 px-3 text-gray-900 ring-1 ring-inset ring-gray-300 " \
|
||||
"placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 " \
|
||||
"sm:text-sm sm:leading-6"
|
||||
|
||||
INPUT_ERROR_CLASSES = "block w-full rounded-md border-0 py-2 px-3 text-red-900 ring-1 ring-inset " \
|
||||
"ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset " \
|
||||
"focus:ring-red-500 sm:text-sm sm:leading-6"
|
||||
|
||||
LABEL_CLASSES = "block text-sm font-medium leading-6 text-gray-900"
|
||||
|
||||
ERROR_CLASSES = "mt-2 text-sm text-red-600"
|
||||
|
||||
HINT_CLASSES = "mt-1 text-sm text-gray-500"
|
||||
|
||||
# Override text_field to include Tailwind styling and error handling
|
||||
def text_field(attribute, options = {})
|
||||
field_wrapper(attribute, options) do
|
||||
super(attribute, input_options(attribute, options))
|
||||
end
|
||||
end
|
||||
|
||||
# Email field with proper type and styling
|
||||
def email_field(attribute, options = {})
|
||||
field_wrapper(attribute, options) do
|
||||
super(attribute, input_options(attribute, options))
|
||||
end
|
||||
end
|
||||
|
||||
# Password field
|
||||
def password_field(attribute, options = {})
|
||||
field_wrapper(attribute, options) do
|
||||
super(attribute, input_options(attribute, options))
|
||||
end
|
||||
end
|
||||
|
||||
# Number field with optional step
|
||||
def number_field(attribute, options = {})
|
||||
field_wrapper(attribute, options) do
|
||||
options[:step] ||= "any" unless options.key?(:step)
|
||||
super(attribute, input_options(attribute, options))
|
||||
end
|
||||
end
|
||||
|
||||
# Currency field with proper formatting
|
||||
# Usage: f.currency_field :price_cents, multiplier: 0.01, decimals: 2
|
||||
def currency_field(attribute, options = {})
|
||||
multiplier = options.delete(:multiplier) || 0.01
|
||||
decimals = options.delete(:decimals) || 2
|
||||
|
||||
field_wrapper(attribute, options) do
|
||||
value = object.public_send(attribute)
|
||||
display_value = value ? (value * multiplier).round(decimals) : nil
|
||||
|
||||
number_field(
|
||||
attribute,
|
||||
options.merge(
|
||||
value: display_value,
|
||||
step: (1.0 / (10**decimals)),
|
||||
class: input_class(attribute),
|
||||
data: { currency_multiplier: (1.0 / multiplier).to_i }
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Text area with proper styling
|
||||
def text_area(attribute, options = {})
|
||||
field_wrapper(attribute, options) do
|
||||
options[:rows] ||= 4
|
||||
options[:class] = "#{input_class(attribute)} #{options[:class] || ''}"
|
||||
super(attribute, options)
|
||||
end
|
||||
end
|
||||
|
||||
# Rich text area for ActionText fields
|
||||
def rich_text_area(attribute, options = {})
|
||||
field_wrapper(attribute, options) do
|
||||
options[:class] = "trix-content #{options[:class] || ''}"
|
||||
super(attribute, options)
|
||||
end
|
||||
end
|
||||
|
||||
# Override select to support label and hint options
|
||||
def select(attribute, choices = nil, options = {}, html_options = {}, &)
|
||||
# Extract custom options if provided as hash (when called with label/hint)
|
||||
return super if choices.is_a?(Hash) && options.empty? && html_options.empty?
|
||||
return super unless options.is_a?(Hash) && (options.key?(:label) || options.key?(:hint))
|
||||
|
||||
# Called as: select(:attr, choices, label: "...", hint: "...", include_blank: ...)
|
||||
render_select_with_wrapper(attribute, choices, options, html_options, &)
|
||||
end
|
||||
|
||||
# Select field with collection
|
||||
# Usage: f.select_field :status, collection: [['Active', 'active'], ['Inactive', 'inactive']]
|
||||
# Usage: f.select_field :grade, collection: (1..12).map { |g| [g.ordinalize, g] }
|
||||
def select_field(attribute, options = {})
|
||||
collection = options.delete(:collection) || []
|
||||
include_blank = options.delete(:include_blank)
|
||||
|
||||
field_wrapper(attribute, options) do
|
||||
select(
|
||||
attribute, collection,
|
||||
{ include_blank: include_blank },
|
||||
input_options(attribute, options)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Boolean field (checkbox) with proper styling
|
||||
def boolean_field(attribute, options = {})
|
||||
hint = options.delete(:hint)
|
||||
label_text = options.delete(:label) || attribute.to_s.humanize
|
||||
|
||||
@template.content_tag(:div, class: "relative flex items-start py-4") do
|
||||
@template.content_tag(:div, class: "flex h-6 items-center") do
|
||||
check_box(
|
||||
attribute,
|
||||
class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600"
|
||||
)
|
||||
end +
|
||||
@template.content_tag(:div, class: "ml-3 text-sm leading-6") do
|
||||
label(attribute, label_text, class: "font-medium text-gray-900") +
|
||||
(hint ? @template.content_tag(:p, hint, class: "text-gray-500") : "".html_safe)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Collection checkboxes with proper styling
|
||||
def collection_check_boxes(attribute, collection, value_method, text_method, options = {})
|
||||
label_text = options.delete(:label) || attribute.to_s.humanize
|
||||
hint = options.delete(:hint)
|
||||
wrapper_class = options.delete(:wrapper_class) || ""
|
||||
|
||||
@template.content_tag(:div, class: "mb-6 #{wrapper_class}") do
|
||||
build_checkbox_collection_label(label_text, hint) +
|
||||
build_checkbox_collection_items(attribute, collection, value_method, text_method) +
|
||||
error_message(attribute)
|
||||
end
|
||||
end
|
||||
|
||||
# Date field
|
||||
def date_field(attribute, options = {})
|
||||
field_wrapper(attribute, options) do
|
||||
super(attribute, input_options(attribute, options))
|
||||
end
|
||||
end
|
||||
|
||||
# DateTime field
|
||||
def datetime_field(attribute, options = {})
|
||||
field_wrapper(attribute, options) do
|
||||
options[:class] = "#{input_class(attribute)} #{options[:class] || ''}"
|
||||
super(attribute, options)
|
||||
end
|
||||
end
|
||||
|
||||
# Association select field (for BelongsTo associations)
|
||||
# Usage: f.association_select :classroom_id, collection: Classroom.all
|
||||
# Usage: f.association_select :school_year_id, collection: SchoolYear.all, label_method: :display_name
|
||||
def association_select(attribute, options = {})
|
||||
collection = options.delete(:collection)
|
||||
label_method = options.delete(:label_method) || :to_s
|
||||
value_method = options.delete(:value_method) || :id
|
||||
include_blank = options.delete(:include_blank) || "Select..."
|
||||
|
||||
raise ArgumentError, "collection is required for association_select" unless collection
|
||||
|
||||
choices = collection.map do |item|
|
||||
build_select_choice(item, label_method, value_method)
|
||||
end
|
||||
|
||||
field_wrapper(attribute, options) do
|
||||
select(
|
||||
attribute, choices,
|
||||
{ include_blank: include_blank },
|
||||
input_options(attribute, options)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Read-only field (displays value, not an input)
|
||||
# Usage: f.read_only_field :created_at
|
||||
# Usage: f.read_only_field :total_amount, value: "$#{object.total_amount}"
|
||||
def read_only_field(attribute, options = {})
|
||||
label_text = options.delete(:label) || attribute.to_s.humanize
|
||||
value = options.delete(:value) || format_value(object.public_send(attribute))
|
||||
hint = options.delete(:hint)
|
||||
|
||||
@template.content_tag(:div, class: "py-4") do
|
||||
@template.content_tag(:dt, label_text, class: "text-sm font-medium text-gray-500") +
|
||||
@template.content_tag(:dd, class: "mt-1 text-sm text-gray-900") do
|
||||
value.to_s.html_safe # rubocop:disable Rails/OutputSafety
|
||||
end +
|
||||
(hint ? @template.content_tag(:p, hint, class: HINT_CLASSES) : "".html_safe)
|
||||
end
|
||||
end
|
||||
|
||||
# Submit button with Tailwind styling
|
||||
def submit_button(text = "Save", options = {})
|
||||
options[:class] = "inline-flex justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold " \
|
||||
"text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 " \
|
||||
"focus-visible:outline-offset-2 focus-visible:outline-blue-600 " \
|
||||
"#{options[:class]}"
|
||||
|
||||
submit(text, options)
|
||||
end
|
||||
|
||||
# Cancel button (link styled as button)
|
||||
def cancel_button(text = "Cancel", url:, options: {})
|
||||
options[:class] = "inline-flex justify-center rounded-md bg-white px-4 py-2 text-sm font-semibold " \
|
||||
"text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 " \
|
||||
"#{options[:class]}"
|
||||
|
||||
@template.link_to(text, url, options)
|
||||
end
|
||||
|
||||
# Displays base errors (errors not tied to a specific attribute)
|
||||
def base_errors
|
||||
return "".html_safe unless object&.errors && object.errors[:base].any?
|
||||
|
||||
@template.content_tag(:div, class: "mb-6 p-4 bg-red-50 border border-red-200 rounded-lg") do
|
||||
@template.content_tag(:p, class: "text-sm text-red-600") do
|
||||
@template.content_tag(:i, "", class: "fas fa-exclamation-circle mr-1") +
|
||||
object.errors[:base].join(", ")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_select_choice(item, label_method, value_method)
|
||||
label = if label_method.respond_to?(:call)
|
||||
label_method.call(item)
|
||||
else
|
||||
item.public_send(label_method)
|
||||
end
|
||||
|
||||
value = if value_method.respond_to?(:call)
|
||||
value_method.call(item)
|
||||
else
|
||||
item.public_send(value_method)
|
||||
end
|
||||
|
||||
[label, value]
|
||||
end
|
||||
|
||||
# Build checkbox collection label and hint
|
||||
def build_checkbox_collection_label(label_text, hint)
|
||||
@template.content_tag(:label, label_text, class: LABEL_CLASSES) +
|
||||
(hint ? @template.content_tag(:p, hint, class: HINT_CLASSES) : "".html_safe)
|
||||
end
|
||||
|
||||
# Build checkbox collection items
|
||||
def build_checkbox_collection_items(attribute, collection, value_method, text_method)
|
||||
@template.content_tag(:div, class: "mt-2 space-y-2") do
|
||||
collection.map do |item|
|
||||
build_single_checkbox(attribute, item, value_method, text_method)
|
||||
end.join.html_safe # rubocop:disable Rails/OutputSafety
|
||||
end
|
||||
end
|
||||
|
||||
# Build a single checkbox item
|
||||
def build_single_checkbox(attribute, item, value_method, text_method)
|
||||
value = item.send(value_method)
|
||||
text = item.send(text_method)
|
||||
checkbox_id = "#{object_name}_#{attribute}_#{value}"
|
||||
checked = Array(object.send(attribute)).include?(value)
|
||||
|
||||
@template.content_tag(:div, class: "relative flex items-start") do
|
||||
build_checkbox_input(attribute, value, checked, checkbox_id) +
|
||||
build_checkbox_label(checkbox_id, text)
|
||||
end
|
||||
end
|
||||
|
||||
# Build checkbox input element
|
||||
def build_checkbox_input(attribute, value, checked, checkbox_id)
|
||||
@template.content_tag(:div, class: "flex h-6 items-center") do
|
||||
@template.check_box_tag(
|
||||
"#{object_name}[#{attribute}][]",
|
||||
value,
|
||||
checked,
|
||||
id: checkbox_id,
|
||||
class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Build checkbox label element
|
||||
def build_checkbox_label(checkbox_id, text)
|
||||
@template.content_tag(:div, class: "ml-3 text-sm leading-6") do
|
||||
@template.label_tag(checkbox_id, text, class: "font-medium text-gray-900")
|
||||
end
|
||||
end
|
||||
|
||||
# Renders select field with label and hint wrapper
|
||||
def render_select_with_wrapper(attribute, choices, options, html_options, &)
|
||||
label_text = options.delete(:label)
|
||||
hint = options.delete(:hint)
|
||||
wrapper_class = options.delete(:wrapper_class)
|
||||
|
||||
# Separate standard select options from html options
|
||||
select_options = extract_select_options(options)
|
||||
remaining_html_options = html_options.merge(options)
|
||||
|
||||
@template.content_tag(:div, class: "mb-6 #{wrapper_class}") do
|
||||
build_label(attribute, label_text) +
|
||||
build_hint(hint) +
|
||||
build_select_field(attribute, choices, select_options, remaining_html_options, &) +
|
||||
error_message(attribute)
|
||||
end
|
||||
end
|
||||
|
||||
# Build label element
|
||||
def build_label(attribute, label_text)
|
||||
return "".html_safe unless label_text
|
||||
|
||||
label(attribute, label_text, class: LABEL_CLASSES)
|
||||
end
|
||||
|
||||
# Build hint element
|
||||
def build_hint(hint)
|
||||
return "".html_safe unless hint
|
||||
|
||||
@template.content_tag(:p, hint, class: HINT_CLASSES)
|
||||
end
|
||||
|
||||
# Build select field element
|
||||
def build_select_field(attribute, choices, select_options, html_options, &)
|
||||
@template.content_tag(:div, class: "mt-2") do
|
||||
ActionView::Helpers::FormBuilder.instance_method(:select).bind(self).call(
|
||||
attribute, choices, select_options, html_options.merge(class: input_class(attribute)), &
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Extract standard select options from the options hash
|
||||
def extract_select_options(options)
|
||||
select_options = {}
|
||||
select_options[:include_blank] = options.delete(:include_blank) if options.key?(:include_blank)
|
||||
select_options[:prompt] = options.delete(:prompt) if options.key?(:prompt)
|
||||
select_options[:disabled] = options.delete(:disabled) if options.key?(:disabled)
|
||||
select_options[:selected] = options.delete(:selected) if options.key?(:selected)
|
||||
select_options
|
||||
end
|
||||
|
||||
# Wraps a field with label, input, and error message
|
||||
def field_wrapper(attribute, options = {}, &)
|
||||
label_text = options.delete(:label) || attribute.to_s.humanize
|
||||
hint = options.delete(:hint)
|
||||
wrapper_class = options.delete(:wrapper_class) || ""
|
||||
|
||||
@template.content_tag(:div, class: "mb-6 #{wrapper_class}") do
|
||||
label(attribute, label_text, class: LABEL_CLASSES) +
|
||||
(hint ? @template.content_tag(:p, hint, class: HINT_CLASSES) : "".html_safe) +
|
||||
@template.content_tag(:div, class: "mt-2", &) +
|
||||
error_message(attribute)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the appropriate CSS class for an input based on validation state
|
||||
def input_class(attribute)
|
||||
if object&.errors && object.errors[attribute].any?
|
||||
INPUT_ERROR_CLASSES
|
||||
else
|
||||
INPUT_CLASSES
|
||||
end
|
||||
end
|
||||
|
||||
# Merges input options with default classes
|
||||
def input_options(attribute, options = {})
|
||||
options[:class] = "#{input_class(attribute)} #{options[:class] || ''}"
|
||||
options
|
||||
end
|
||||
|
||||
# Displays validation error for an attribute
|
||||
def error_message(attribute)
|
||||
return "".html_safe unless object&.errors && object.errors[attribute].any?
|
||||
|
||||
@template.content_tag(:p, class: ERROR_CLASSES, id: "#{attribute}-error") do
|
||||
@template.content_tag(:i, "", class: "fas fa-exclamation-circle mr-1") +
|
||||
object.errors[attribute].first
|
||||
end
|
||||
end
|
||||
|
||||
# Formats a value for display in read-only fields
|
||||
def format_value(value)
|
||||
case value
|
||||
when TrueClass
|
||||
'<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ' \
|
||||
'bg-green-100 text-green-800">Yes</span>'
|
||||
when FalseClass
|
||||
'<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ' \
|
||||
'bg-gray-100 text-gray-800">No</span>'
|
||||
when Time, DateTime, Date
|
||||
value.strftime("%B %d, %Y")
|
||||
when nil
|
||||
'<span class="text-gray-400">—</span>'
|
||||
else
|
||||
value.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/ClassLength
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
module FormHelper
|
||||
# Creates a form using the Admin::FormBuilder
|
||||
# Usage: admin_form_for @resource do |f|
|
||||
# f.text_field :name
|
||||
# f.email_field :email
|
||||
# f.submit_button
|
||||
# end
|
||||
def admin_form_for(record, options = {}, &)
|
||||
options[:builder] = Admin::FormBuilder
|
||||
options[:html] ||= {}
|
||||
options[:html][:class] = "space-y-6 #{options[:html][:class]}"
|
||||
|
||||
form_with(model: record, scope: model_scope(record), url: form_url(record), **options, &)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Determines the model scope for the form
|
||||
def model_scope(record)
|
||||
return record.first.model_name.param_key if record.is_a?(Array)
|
||||
|
||||
record.model_name.param_key
|
||||
end
|
||||
|
||||
# Determines the form URL based on the record (handles both new and existing records)
|
||||
def form_url(record)
|
||||
if record.is_a?(Array)
|
||||
resource = record.last
|
||||
if resource.persisted?
|
||||
[:admin, resource]
|
||||
else
|
||||
record.first.is_a?(Symbol) ? [record.first, :admin, resource] : [:admin, resource]
|
||||
end
|
||||
else
|
||||
resource = record
|
||||
resource.persisted? ? [:admin, resource] : [:admin, resource.class.model_name.collection]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||