chore: init commit

in worker.../repo/GITFOLDER.zip is the .git folder.
This commit is contained in:
2026-08-11 14:44:09 -04:00
parent 0012380fd3
commit 392781f7aa
781 changed files with 65944 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
---
allow_single_quoted_attributes: true
allow_unquoted_attributes: false

View 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

View 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

View 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

View 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

View 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

View 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"

View 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

View 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

View 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

View 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.'

View 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

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
3.4.4

View File

@@ -0,0 +1,5 @@
parallel: true
ruby_version: 3.4
plugins:
- standard-rails

View 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.

View 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 }

View 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"]

View 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"]

Binary file not shown.

View 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

View 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

View 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.

View 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

View 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

View File

@@ -0,0 +1,198 @@
[![Continuous Integration](https://github.com/rubyforgood/stocks-in-the-future/actions/workflows/ci.yml/badge.svg)](https://github.com/rubyforgood/stocks-in-the-future/actions/workflows/ci.yml)
[![Lint](https://github.com/rubyforgood/stocks-in-the-future/actions/workflows/lint.yml/badge.svg)](https://github.com/rubyforgood/stocks-in-the-future/actions/workflows/lint.yml)
[![Dependabot Updates](https://github.com/rubyforgood/stocks-in-the-future/actions/workflows/dependabot/dependabot-updates/badge.svg)](https://github.com/rubyforgood/stocks-in-the-future/actions/workflows/dependabot/dependabot-updates)
[![Help wanted](https://badgen.net/github/label-issues/rubyforgood/stocks-in-the-future/help%20wanted/open??color=green&icon=github&cache=3600)](https://github.com/rubyforgood/stocks-in-the-future/labels/help%20wanted)
[![Open PRs](https://badgen.net/github/open-prs/rubyforgood/stocks-in-the-future??color=green&icon=github&cache=3600)](https://github.com/rubyforgood/stocks-in-the-future/pulls)
[![Last commit](https://badgen.net/github/last-commit/rubyforgood/stocks-in-the-future??icon=github&cache=3600)](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`.

View 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

View 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! :)

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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; }
}

View File

@@ -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";

View File

@@ -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 */
}

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -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%;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,6 @@
# frozen_string_literal: true
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@@ -0,0 +1,6 @@
# frozen_string_literal: true
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
class HomeController < ApplicationController
def index
@current_announcement = Announcement.current
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

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