diff --git a/.babelrc b/.babelrc deleted file mode 100644 index ded31c0..0000000 --- a/.babelrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "presets": [ - ["env", { - "modules": false, - "targets": { - "browsers": "> 1%", - "uglify": true - }, - "useBuiltIns": true - }] - ], - - "plugins": [ - "syntax-dynamic-import", - "transform-object-rest-spread", - ["transform-class-properties", { "spec": true }] - ] -} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..83610cf --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1a34dc2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,130 @@ +name: CI + +on: + pull_request: + push: + branches: [ main ] + +jobs: + scan_ruby: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Scan for common Rails security vulnerabilities using static analysis + run: bin/brakeman --no-pager + + - name: Scan for known security vulnerabilities in gems used + run: bin/bundler-audit + + scan_js: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Scan for security vulnerabilities in JavaScript dependencies + run: bin/importmap audit + + lint: + runs-on: ubuntu-latest + env: + RUBOCOP_CACHE_ROOT: tmp/rubocop + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Prepare RuboCop cache + uses: actions/cache@v4 + env: + DEPENDENCIES_HASH: ${{ hashFiles('.ruby-version', '**/.rubocop.yml', '**/.rubocop_todo.yml', 'Gemfile.lock') }} + with: + path: ${{ env.RUBOCOP_CACHE_ROOT }} + key: rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }} + restore-keys: | + rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}- + + - name: Lint code for consistent style + run: bin/rubocop -f github + + test: + runs-on: ubuntu-latest + + # services: + # redis: + # image: valkey/valkey:8 + # ports: + # - 6379:6379 + # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Install packages + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libvips + + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Run tests + env: + RAILS_ENV: test + # RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + # REDIS_URL: redis://localhost:6379/0 + run: bin/rails db:test:prepare test + + system-test: + runs-on: ubuntu-latest + + # services: + # redis: + # image: valkey/valkey:8 + # ports: + # - 6379:6379 + # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Install packages + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libvips + + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Run System Tests + env: + RAILS_ENV: test + # RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + # REDIS_URL: redis://localhost:6379/0 + run: bin/rails db:test:prepare test:system + + - name: Keep screenshots from failed system tests + uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots + path: ${{ github.workspace }}/tmp/screenshots + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 381f433..0000000 --- a/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -.bundle -log/*.log -tmp/**/* -tmp/* -!log/.keep -!tmp/.keep -*.swp -.DS_Store -public/assets -public/packs -public/packs-test -node_modules -yarn-error.log -.byebug_history -.env* -.env* -.env* diff --git a/.postcssrc.yml b/.postcssrc.yml deleted file mode 100644 index 150dac3..0000000 --- a/.postcssrc.yml +++ /dev/null @@ -1,3 +0,0 @@ -plugins: - postcss-import: {} - postcss-cssnext: {} diff --git a/.rubocop.yml b/.rubocop.yml index 90a56fe..f9d86d4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,41 +1,8 @@ -AllCops: - Exclude: - - 'bin/**/*' - - 'db/**/*' - - 'config/**/*' - - 'node_modules/**/*' - - 'script/**/*' - - 'support/**/*' - - 'tmp/**/*' - - 'test/**/*' +# Omakase Ruby styling for Rails +inherit_gem: { rubocop-rails-omakase: rubocop.yml } -ConditionalAssignment: - Enabled: false -StringLiterals: - Enabled: false -RedundantReturn: - Enabled: false -Documentation: - Enabled: false -WordArray: - Enabled: false -AbcSize: - Enabled: false -MutableConstant: - Enabled: false -SignalException: - Enabled: false -Casecmp: - Enabled: false -CyclomaticComplexity: - Enabled: false -MethodMissing: - Enabled: false -Style/FrozenStringLiteralComment: - Enabled: false -LineLength: - Max: 120 -Style/EmptyMethod: - Enabled: false -Bundler/OrderedGems: - Enabled: false +# Overwrite or add rules to create your own house style +# +# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` +# Layout/SpaceInsideArrayLiteralBrackets: +# Enabled: false diff --git a/.ruby-version b/.ruby-version index ab6d278..e391e18 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.4.4 \ No newline at end of file +ruby-3.3.6 diff --git a/Gemfile b/Gemfile index 7158da4..1eceec5 100644 --- a/Gemfile +++ b/Gemfile @@ -1,36 +1,68 @@ -source 'https://rubygems.org' -ruby '2.4.4' - -gem 'bootsnap', require: false -gem 'devise' -gem 'jbuilder', '~> 2.0' -gem 'pg', '~> 0.21' -gem 'puma' -gem 'rails', '5.2.1' -gem 'redis' - - -gem 'autoprefixer-rails' -gem 'bootstrap-sass', '~> 3.3' -gem 'font-awesome-sass', '~> 5.0.9' -gem 'sass-rails' -gem 'simple_form' -gem 'uglifier' -gem 'webpacker' -gem 'cloudinary' -gem 'carrierwave', '~> 1.2' -gem 'money-rails' -gem 'stripe' +source "https://rubygems.org" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 8.1.3" +# The modern asset pipeline for Rails [https://github.com/rails/propshaft] +gem "propshaft" +# Use sqlite3 as the database for Active Record +gem "sqlite3", ">= 2.1" +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", ">= 5.0" +# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] +gem "importmap-rails" +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem "turbo-rails" +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem "stimulus-rails" +# Use Tailwind CSS [https://github.com/rails/tailwindcss-rails] +gem "tailwindcss-rails" +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +gem "bcrypt", "~> 3.1.7" + +# Claude API for AI quiz generation [https://github.com/anthropics/anthropic-sdk-ruby] +gem "anthropic" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[ windows jruby ] + +# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable +gem "solid_cache" +gem "solid_queue" +gem "solid_cable" + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] +gem "thruster", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +gem "image_processing", "~> 1.2" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + + # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues) + gem "bundler-audit", require: false + + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem "brakeman", require: false + + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem "rubocop-rails-omakase", require: false +end group :development do - gem 'web-console', '>= 3.3.0' + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" end -group :development, :test do - gem 'pry-byebug' - gem 'pry-rails' - gem 'listen', '~> 3.0.5' - gem 'spring' - gem 'spring-watcher-listen', '~> 2.0.0' - gem 'dotenv-rails', groups: [:development, :test] +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" end diff --git a/Gemfile.lock b/Gemfile.lock index 1b4acb6..71f0b28 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,272 +1,569 @@ GEM remote: https://rubygems.org/ specs: - actioncable (5.2.1) - actionpack (= 5.2.1) + action_text-trix (2.1.19) + railties + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.1) - actionpack (= 5.2.1) - actionview (= 5.2.1) - activejob (= 5.2.1) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (5.2.1) - actionview (= 5.2.1) - activesupport (= 5.2.1) - rack (~> 2.0) + 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.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.1) - activesupport (= 5.2.1) + 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.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.1) - activesupport (= 5.2.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 (5.2.1) - activesupport (= 5.2.1) - activerecord (5.2.1) - activemodel (= 5.2.1) - activesupport (= 5.2.1) - arel (>= 9.0) - activestorage (5.2.1) - actionpack (= 5.2.1) - activerecord (= 5.2.1) - marcel (~> 0.3.1) - activesupport (5.2.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - arel (9.0.0) - autoprefixer-rails (9.1.3) - execjs - aws_cf_signer (0.1.3) - bcrypt (3.1.12) - bindex (0.5.0) - bootsnap (1.3.1) - msgpack (~> 1.0) - bootstrap-sass (3.3.7) - autoprefixer-rails (>= 5.2.1) - sass (>= 3.3.4) - builder (3.2.3) - byebug (10.0.2) - carrierwave (1.2.3) - activemodel (>= 4.0.0) - activesupport (>= 4.0.0) - mime-types (>= 1.16) - cloudinary (1.9.1) - aws_cf_signer - rest-client - coderay (1.1.2) - concurrent-ruby (1.0.5) - crass (1.0.4) - devise (4.5.0) - bcrypt (~> 3.0) - orm_adapter (~> 0.1) - railties (>= 4.1.0, < 6.0) - responders - warden (~> 1.2.3) - domain_name (0.5.20180417) - unf (>= 0.0.5, < 1.0.0) - dotenv (2.5.0) - dotenv-rails (2.5.0) - dotenv (= 2.5.0) - railties (>= 3.2, < 6.0) - erubi (1.7.1) - execjs (2.7.0) - faraday (0.15.2) - multipart-post (>= 1.2, < 3) - ffi (1.9.25) - font-awesome-sass (5.0.13) - sassc (>= 1.11) - globalid (0.4.1) - activesupport (>= 4.2.0) - http-cookie (1.0.3) - domain_name (~> 0.5) - i18n (1.0.1) + 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) + anthropic (1.59.0) + cgi + connection_pool + standardwebhooks + ast (2.4.3) + base64 (0.3.0) + bcrypt (3.1.22) + 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) + 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) + cgi (0.5.2) + concurrent-ruby (1.3.8) + connection_pool (3.0.2) + crass (1.0.7) + date (3.5.1) + debug (1.11.1) + irb (~> 1.10) + reline (>= 0.3.8) + drb (2.2.3) + erb (6.0.6) + erubi (1.13.1) + et-orbi (1.4.0) + tzinfo + 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-arm64-darwin) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + fugit (1.13.0) + et-orbi (~> 1.4) + raabro (~> 1.4) + globalid (1.4.0) + activesupport (>= 6.1) + i18n (1.15.2) concurrent-ruby (~> 1.0) - jbuilder (2.7.0) - activesupport (>= 4.2.0) - multi_json (>= 1.2) - listen (3.0.8) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - loofah (2.2.2) + 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.21.1) + language_server-protocol (3.17.0.6) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.25.2) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.0) + nokogiri (>= 1.12.0) + mail (2.9.1) + logger mini_mime (>= 0.1.1) - marcel (0.3.2) - mimemagic (~> 0.3.2) - method_source (0.9.0) - mime-types (3.2.2) - mime-types-data (~> 3.2015) - mime-types-data (3.2018.0812) - mimemagic (0.3.2) - mini_mime (1.0.1) - mini_portile2 (2.3.0) - minitest (5.11.3) - monetize (1.7.0) - money (~> 6.9) - money (6.11.3) - i18n (>= 0.6.4, < 1.1) - money-rails (1.11.0) - activesupport (>= 3.0) - monetize (~> 1.7.0) - money (~> 6.11.0) - railties (>= 3.0) - msgpack (1.2.4) - multi_json (1.13.1) - multipart-post (2.0.0) - netrc (0.11.0) - nio4r (2.3.1) - nokogiri (1.8.4) - mini_portile2 (~> 2.3.0) - orm_adapter (0.5.0) - pg (0.21.0) - pry (0.11.3) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry-byebug (3.6.0) - byebug (~> 10.0) - pry (~> 0.10) - pry-rails (0.3.6) - pry (>= 0.10.4) - puma (3.12.0) - rack (2.0.5) - rack-proxy (0.6.4) + net-imap + net-pop + net-smtp + marcel (1.2.1) + matrix (0.4.3) + mini_magick (5.3.2) + logger + mini_mime (1.1.5) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) + msgpack (1.8.3) + net-imap (0.6.6) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + 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-arm64-darwin) + 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) + parallel (2.1.0) + parser (3.3.12.0) + ast (~> 2.4.1) + racc + 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 - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (5.2.1) - actioncable (= 5.2.1) - actionmailer (= 5.2.1) - actionpack (= 5.2.1) - actionview (= 5.2.1) - activejob (= 5.2.1) - activemodel (= 5.2.1) - activerecord (= 5.2.1) - activestorage (= 5.2.1) - activesupport (= 5.2.1) - bundler (>= 1.3.0) - railties (= 5.2.1) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + public_suffix (7.0.5) + puma (8.0.2) + nio4r (~> 2.0) + raabro (1.5.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.0.4) - loofah (~> 2.2, >= 2.2.2) - railties (5.2.1) - actionpack (= 5.2.1) - activesupport (= 5.2.1) - method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) - rake (12.3.1) - rb-fsevent (0.10.3) - rb-inotify (0.9.10) - ffi (>= 0.5.0, < 2) - redis (4.0.2) - responders (2.4.0) - actionpack (>= 4.2.0, < 5.3) - railties (>= 4.2.0, < 5.3) - rest-client (2.0.2) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 4.0) - netrc (~> 0.8) - sass (3.5.7) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.0.7) - railties (>= 4.0.0, < 6) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) - sassc (1.12.1) - ffi (~> 1.9.6) - sass (>= 3.3.0) - simple_form (4.0.1) - actionpack (>= 5.0) - activemodel (>= 5.0) - spring (2.0.2) - activesupport (>= 4.2) - spring-watcher-listen (2.0.1) - listen (>= 2.7, < 4.0) - spring (>= 1.2, < 3.0) - sprockets (3.7.2) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) + 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) + 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.1.0) + 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) + rexml (3.4.4) + rubocop (1.88.2) + 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.50.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.36.0) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rails-omakase (1.1.0) + rubocop (>= 1.72) + rubocop-performance (>= 1.24) + rubocop-rails (>= 2.30) + ruby-progressbar (1.13.0) + ruby-vips (2.3.0) + ffi (~> 1.12) + logger + rubyzip (3.4.1) + securerandom (0.4.1) + selenium-webdriver (4.46.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) + solid_cable (4.0.2) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.10) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.5.0) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) + sqlite3 (2.9.5-aarch64-linux-gnu) + sqlite3 (2.9.5-aarch64-linux-musl) + sqlite3 (2.9.5-arm-linux-gnu) + sqlite3 (2.9.5-arm-linux-musl) + sqlite3 (2.9.5-arm64-darwin) + sqlite3 (2.9.5-x86_64-darwin) + sqlite3 (2.9.5-x86_64-linux-gnu) + sqlite3 (2.9.5-x86_64-linux-musl) + standardwebhooks (1.1.0) + stimulus-rails (1.3.4) + railties (>= 6.0.0) + tailwindcss-rails (4.6.0) + railties (>= 7.0.0) + tailwindcss-ruby (~> 4.0) + tailwindcss-ruby (4.3.3) + tailwindcss-ruby (4.3.3-aarch64-linux-gnu) + tailwindcss-ruby (4.3.3-aarch64-linux-musl) + tailwindcss-ruby (4.3.3-arm64-darwin) + tailwindcss-ruby (4.3.3-x86_64-darwin) + tailwindcss-ruby (4.3.3-x86_64-linux-gnu) + tailwindcss-ruby (4.3.3-x86_64-linux-musl) + thor (1.5.0) + thruster (0.1.23) + thruster (0.1.23-aarch64-linux) + thruster (0.1.23-arm64-darwin) + thruster (0.1.23-x86_64-darwin) + thruster (0.1.23-x86_64-linux) + 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) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - stripe (3.23.0) - faraday (~> 0.10) - thor (0.20.0) - thread_safe (0.3.6) - tilt (2.0.8) - tzinfo (1.2.5) - thread_safe (~> 0.1) - uglifier (4.1.18) - execjs (>= 0.3.0, < 3) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.5) - warden (1.2.7) - rack (>= 1.0) - web-console (3.6.2) - actionview (>= 5.0) - activemodel (>= 5.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + useragent (0.16.11) + web-console (4.3.0) + actionview (>= 8.0.0) bindex (>= 0.4.0) - railties (>= 5.0) - webpacker (3.5.5) - activesupport (>= 4.2) - rack-proxy (>= 0.6.1) - railties (>= 4.2) - websocket-driver (0.7.0) + railties (>= 8.0.0) + websocket (1.2.11) + websocket-driver (0.8.2) + base64 websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.3) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.8.2) PLATFORMS - ruby + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES - autoprefixer-rails + anthropic + bcrypt (~> 3.1.7) bootsnap - bootstrap-sass (~> 3.3) - carrierwave (~> 1.2) - cloudinary - devise - dotenv-rails - font-awesome-sass (~> 5.0.9) - jbuilder (~> 2.0) - listen (~> 3.0.5) - money-rails - pg (~> 0.21) - pry-byebug - pry-rails - puma - rails (= 5.2.1) - redis - sass-rails - simple_form - spring - spring-watcher-listen (~> 2.0.0) - stripe - uglifier - web-console (>= 3.3.0) - webpacker + brakeman + bundler-audit + capybara + debug + image_processing (~> 1.2) + importmap-rails + jbuilder + propshaft + puma (>= 5.0) + rails (~> 8.1.3) + rubocop-rails-omakase + selenium-webdriver + solid_cable + solid_cache + solid_queue + sqlite3 (>= 2.1) + stimulus-rails + tailwindcss-rails + thruster + turbo-rails + tzinfo-data + web-console -RUBY VERSION - ruby 2.4.4p296 +CHECKSUMS + action_text-trix (2.1.19) sha256=7012f59421009cf284aa651294896414d653a61a2417c9b8714c8476d2f74009 + actioncable (8.1.3) sha256=e5bc7f75e44e6a22de29c4f43176927c3a9ce4824464b74ed18d8226e75a80f0 + actionmailbox (8.1.3) sha256=df7da474eaa0e70df4ed5a6fef66eb3b3b0f2dbf7f14518deee8d77f1b4aae59 + actionmailer (8.1.3) sha256=831f724891bb70d0aaa4d76581a6321124b6a752cb655c9346aae5479318448d + actionpack (8.1.3) sha256=af998cae4d47c5d581a2cc363b5c77eb718b7c4b45748d81b1887b25621c29a3 + actiontext (8.1.3) sha256=d291019c00e1ea9e6463011fa214f6081a56d7b9a1d224e7d3f6384c1dafc7d2 + actionview (8.1.3) sha256=1347c88c7f3edb38100c5ce0e9fb5e62d7755f3edc1b61cce2eb0b2c6ea2fd5d + activejob (8.1.3) sha256=a149b1766aa8204c3c3da7309e4becd40fcd5529c348cffbf6c9b16b565fe8d3 + activemodel (8.1.3) sha256=90c05cbe4cef3649b8f79f13016191ea94c4525ce4a5c0fb7ef909c4b91c8219 + activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab + activestorage (8.1.3) sha256=0564ce9309143951a67615e1bb4e090ee54b8befed417133cae614479b46384d + activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + anthropic (1.59.0) sha256=fe987f143226d78544bddc5d4a32bdcc0e9a70a996dfbcdbbaf2e4c296335cda + ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032 + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e + bootsnap (1.24.6) sha256=c60bab88c70332290f0a2636a288f675299eb4f804a02a3c085b42eca9da164a + brakeman (8.0.5) sha256=03735f9690d3fd4b32d66aacbf0a6d15a84266bdd06b32c05c8ecc8f6021d2be + builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f + bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9 + capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef + cgi (0.5.2) sha256=61ca30298171190fd4fa0d8018e57ada456eae9b7a2b78526debf7f0a0e6f8bb + concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1 + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295 + date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 + debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6 + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + erb (6.0.6) sha256=a9b24986700f5bf127c4f297c5403c3ca41b83b0a316c0cd09a096b56e644ae5 + erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 + et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 + ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b + ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + fugit (1.13.0) sha256=a4f093fce740da52f216740a5041e2a594ea763cdb89e8b2754ca4399634ab18 + globalid (1.4.0) sha256=037f12fbf1d9d7a014d501c2d5c77356fd4ddd96d7a7991d6700bba96706f427 + i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5 + image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb + importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a + io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 + jbuilder (2.15.1) sha256=2430bec28fb0cebacb5875b1009cf9d8bc3c303ccb810c4c8b062a4b51457637 + json (2.21.1) sha256=13a43df75d95641443f5702dff350f237164a9d811ff0f2c2800d4d980220583 + language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0 + lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + loofah (2.25.2) sha256=2007f746959ac65552456e04b433e83deb22759ab38c838b4445c70e43425918 + mail (2.9.1) sha256=06574eca475253d6c18145dd70af80d0eb970182d55053497c5f4d797ea160e8 + marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f + matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b + mini_magick (5.3.2) sha256=a7ef54b480b15283c396eb8343d9e088471f54b31d9974cb7e523cc57598609f + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1 + msgpack (1.8.3) sha256=8bda4a6428d3244e50d6bd55854d354edbada88a4e1f4f5731a39a0f86bee6a1 + net-imap (0.6.6) sha256=96aa4ee50df3060203e649efc341f53480b791d49e150f2fdebf68beb141a8df + net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 + net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 + net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f + nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af + nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca + nokogiri (1.19.4-arm-linux-musl) sha256=588923c101bcfa78869734d247d25b598674323e7f22474fc468f6e5647311eb + nokogiri (1.19.4-arm64-darwin) sha256=a46db9853286e6597b36ebc6953817d15acf3a299583eb3f89fdc6f91dd63527 + nokogiri (1.19.4-x86_64-darwin) sha256=7fd17057d3e1f00e9954a74b3cd76595d3d4a5ef233b7ed9599047c204f70551 + nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a + nokogiri (1.19.4-x86_64-linux-musl) sha256=17dfb7c1fa194ae02fbf7c51a7afc8d278045ab3fdacfd86f91d02d7b274470b + parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356 + parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828 + pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + propshaft (1.3.2) sha256=1d56a3e56a92c21bfc29caf07406b5386b00d4c47ddf357cf989a5a234b1389e + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb + raabro (1.5.0) sha256=3f998a7bc84f9c84df3ab580634d2e0a5bda4f0841168d56035f529c9877440a + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2 + rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 + rails (8.1.3) sha256=6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3 + rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d + rails-html-sanitizer (1.7.1) sha256=e797a7c9b01e567307e317c576b49ab4168017e63eea4dba9ce3cb587e2f22c2 + railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22 + rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rbs (4.1.0) sha256=8baba59008b0643b4ba2090e9b1d0149655b0bbd42eb2ffe42b1d0eb6923fd72 + rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469 + regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb + reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rubocop (1.88.2) sha256=8def251c90cd955feb4daa3edc0ab56893250c4ce90ef81e6c80c03f9a939bbf + rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db + rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 + rubocop-rails (2.36.0) sha256=105a5f5b0001ff2ef28a3af90da50862464e0775e8d0016d599079c0ca408bdb + rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d + ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 + ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374 + rubyzip (3.4.1) sha256=0a79e745b5c25872ebd148457df5665da8530ed8626c993b01457128f173ca02 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + selenium-webdriver (4.46.0) sha256=cb6cfa6f79eac041749df0b14cdcb0e9fe5df3715a64c77bfaa56b345b99f957 + solid_cable (4.0.2) sha256=084636a67679ad00d23088b33c84047e614bcf41ee559db24b414d83cdc42d03 + solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41 + solid_queue (1.5.0) sha256=329f29caab9cc68eefe489013eb5a38ea4a66708ed2bee301384dbed91890f7f + sqlite3 (2.9.5-aarch64-linux-gnu) sha256=78075b6337d3d182c6d2b4691049ed45cd220826160c9ea18946bf6a1de200dc + sqlite3 (2.9.5-aarch64-linux-musl) sha256=18c801185deb4adc01ddb281e8f672a39e3d1729979ca91e39439cd3eac0402d + sqlite3 (2.9.5-arm-linux-gnu) sha256=1bdfca0c7d63998c60b0f4a8e3c8df2d33800ccc4abd2d612eddbbbc92a4c48b + sqlite3 (2.9.5-arm-linux-musl) sha256=bae1109d12b2e9f588455967729b008e1ff4feb7761749df695019c9079913c6 + sqlite3 (2.9.5-arm64-darwin) sha256=d0cf444a70fc9395d513cfbcc1e6719e224aa645314e3824cb0474c721425aa2 + sqlite3 (2.9.5-x86_64-darwin) sha256=8e9caae38bd7ebb29cbeee3e7ab1d12dc2327d9a1b92c7fcf0dda05589627a81 + sqlite3 (2.9.5-x86_64-linux-gnu) sha256=233dbcb6714148dd23bc5aeb33e8efd6eac974969564ddd5794c23d5f52b231e + sqlite3 (2.9.5-x86_64-linux-musl) sha256=e7d3a7474e8af0f96150c21abc203fbab5437206bfcdf11deab7741c0ca516f2 + standardwebhooks (1.1.0) sha256=356ba711c14def4fb27310e4163b26bd5cccf325212c3440cabadd825e4be11f + stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 + tailwindcss-rails (4.6.0) sha256=d99512867173d55c5ef8890427682299d8539f550cec1408b3d8667a538bd365 + tailwindcss-ruby (4.3.3) sha256=ee0a64030749862deb501acab4c4aaf5adbee13865746a33299d46d7b5d0952a + tailwindcss-ruby (4.3.3-aarch64-linux-gnu) sha256=c86d6dd3eccc85fe0d792a832b06f2bf3c0a7a83b399308aeb9d8f5725f42a6a + tailwindcss-ruby (4.3.3-aarch64-linux-musl) sha256=72b77ca9edea82383dd09510ab520a122e3cb9f9864ca5b38e27698098d2b899 + tailwindcss-ruby (4.3.3-arm64-darwin) sha256=776c51fc734aac64bde0c253eadd2ed2e610b2ba0d8e760501fe7cbec55fd6cf + tailwindcss-ruby (4.3.3-x86_64-darwin) sha256=483011e5d3792cf5ef4afdfb04c1d95daccf42c30cdb6fe53b77520954e7b922 + tailwindcss-ruby (4.3.3-x86_64-linux-gnu) sha256=2337017ff8b02698480eae1e9637cf01faa0e4824db89d067a13c5a5ee38c9b2 + tailwindcss-ruby (4.3.3-x86_64-linux-musl) sha256=27d478c417bcf73828e5b544744c5bdfd5b5cb54f1a266fc4f185281c32efe6c + thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 + thruster (0.1.23) sha256=ad3081e8ff3b5cb413768292c367f1e015c423bb6b35919b9b9f533af50d639f + thruster (0.1.23-aarch64-linux) sha256=042a05581dd1d750d1583d83817460b1f7fde254ed0b2bcd7ec56f9bed278c7a + thruster (0.1.23-arm64-darwin) sha256=2f1b73119ffec813cdb104d64dd39bfc3771b5308eb0f4dc2428077141140022 + thruster (0.1.23-x86_64-darwin) sha256=2b22a951f6ac1218c9f461bfc6004b61854147308649475a07bc05fae2e69869 + thruster (0.1.23-x86_64-linux) sha256=9cd7265bf54e954575c8442e42d050ed8eb55df4f9d7da8fd6f94fcc0339baa5 + timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 + web-console (4.3.0) sha256=e13b71301cdfc2093f155b5aa3a622db80b4672d1f2f713119cc7ec7ac6a6da4 + websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737 + websocket-driver (0.8.2) sha256=97c556b019bf3410b4961002ac501621e9322d3f8a7bc02161a09301cc4c4146 + websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 + xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e + zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12 BUNDLED WITH - 1.16.4 + 4.0.9 diff --git a/Procfile b/Procfile deleted file mode 100644 index c2c566e..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: bundle exec puma -C config/puma.rb diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 0000000..da151fe --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,2 @@ +web: bin/rails server +css: bin/rails tailwindcss:watch diff --git a/README.md b/README.md index f3d3178..396f936 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,42 @@ -READCOIN -Read. Earn. Spend. +# ๐Ÿ“š ReadCoin -What is ReadCoin? -ReadCoin is the first incentive driven coin for childern of the world. +**Read. Earn. Spend.** โ€” kids read real books, pass a comprehension quiz, and earn coins +they spend on rewards their parents define (movie night, a zoo trip, a LEGO set). -Why ReadCoin? -If you want your children to be smart, successful, technologicly advanced, happy and ready for the way that is ahead. +This is the 2026 full rewrite of the original 2018 Rails 5.2 app: Rails 8, Hotwire, +Tailwind, SQLite, and AI-generated quizzes via the Claude API. No payment processing โ€” +rewards are real-world promises from parents, not purchases. +## How it works -https://readcoin.herokuapp.com/ +- **Parents** sign up, add their kids (each kid gets an avatar and a 4-digit PIN), curate + a bookshelf (link to free books on Project Gutenberg / StoryWeaver), and define rewards. +- **Kids** log in on a tablet at `/play` with their PIN โ€” no email or password. They read + a book, take a 5-question quiz, and earn coins for scoring 80%+. Daily reading builds a + streak. Coins are spent in the prize shop; parents approve redemptions. +- **Quizzes** are graded server-side (answers never reach the browser) and can be + generated with Claude: set `ANTHROPIC_API_KEY` and hit โ€œGenerate quiz with AIโ€ on any book. + +## Stack + +Ruby 3.3 ยท Rails 8.1 ยท SQLite (Solid Queue/Cache/Cable) ยท Hotwire ยท Tailwind CSS 4 ยท +Propshaft + import maps ยท `anthropic` gem for quiz generation. + +## Getting started + +```sh +bundle install +bin/rails db:prepare db:seed +bin/dev +``` + +Seeds create a demo family: parent `demo@readcoin.app` / `password123`, +kids **Leo** (PIN `1234`) and **Maya** (PIN `5678`), four public-domain books, and three rewards. + +Optional: `export ANTHROPIC_API_KEY=sk-ant-...` to enable AI quiz generation. + +## Tests + +```sh +bin/rails test +``` diff --git a/Rakefile b/Rakefile index e85f913..9a5ea73 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,6 @@ # 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' +require_relative "config/application" Rails.application.load_tasks diff --git a/app/assets/javascripts/channels/.keep b/app/assets/builds/.keep similarity index 100% rename from app/assets/javascripts/channels/.keep rename to app/assets/builds/.keep diff --git a/app/assets/builds/tailwind.css b/app/assets/builds/tailwind.css new file mode 100644 index 0000000..9f26638 --- /dev/null +++ b/app/assets/builds/tailwind.css @@ -0,0 +1,2 @@ +/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-orange-500:oklch(70.5% .213 47.604);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-100:oklch(95.1% .026 236.824);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-600:oklch(58.8% .158 241.966);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-violet-500:oklch(60.6% .25 292.717);--color-violet-600:oklch(54.1% .281 293.009);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-700:oklch(37.3% .034 259.733);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-500:oklch(55.3% .013 58.071);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-stone-800:oklch(26.8% .007 34.298);--color-white:#fff;--spacing:.25rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-4xl:56rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.collapse{visibility:collapse}.collapse\!{visibility:collapse!important}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.static{position:static}.sticky{position:sticky}.isolate{isolation:isolate}.col-span-3{grid-column:span 3/span 3}.col-span-6{grid-column:span 6/span 6}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-4{margin-block:calc(var(--spacing) * 4)}.my-5{margin-block:calc(var(--spacing) * 5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-28{margin-top:calc(var(--spacing) * 28)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.table{display:table}.h-24{height:calc(var(--spacing) * 24)}.h-\[75vh\]{height:75vh}.min-h-screen{min-height:100vh}.w-16{width:calc(var(--spacing) * 16)}.w-24{width:calc(var(--spacing) * 24)}.w-32{width:calc(var(--spacing) * 32)}.w-36{width:calc(var(--spacing) * 36)}.w-44{width:calc(var(--spacing) * 44)}.w-72{width:calc(var(--spacing) * 72)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-lg{max-width:var(--container-lg)}.max-w-xl{max-width:var(--container-xl)}.flex-1{flex:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.transform\!{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)!important}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-stone-100>:not(:last-child)){border-color:var(--color-stone-100)}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-amber-100{border-color:var(--color-amber-100)}.border-amber-200{border-color:var(--color-amber-200)}.border-amber-300{border-color:var(--color-amber-300)}.border-gray-400{border-color:var(--color-gray-400)}.border-stone-200{border-color:var(--color-stone-200)}.border-stone-300{border-color:var(--color-stone-300)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-sky-50{background-color:var(--color-sky-50)}.bg-sky-500{background-color:var(--color-sky-500)}.bg-stone-50{background-color:var(--color-stone-50)}.bg-stone-800{background-color:var(--color-stone-800)}.bg-violet-600{background-color:var(--color-violet-600)}.bg-white{background-color:var(--color-white)}.bg-white\/80{background-color:#fffc}@supports (color:color-mix(in lab, red, red)){.bg-white\/80{background-color:color-mix(in oklab, var(--color-white) 80%, transparent)}}.bg-gradient-to-b{--tw-gradient-position:to bottom in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-sky-100{--tw-gradient-from:var(--color-sky-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-amber-50{--tw-gradient-to:var(--color-amber-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.object-cover{object-fit:cover}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-16{padding-block:calc(var(--spacing) * 16)}.text-center{text-align:center}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.5em\]{--tw-tracking:.5em;letter-spacing:.5em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-amber-500{color:var(--color-amber-500)}.text-amber-600{color:var(--color-amber-600)}.text-gray-500{color:var(--color-gray-500)}.text-gray-700{color:var(--color-gray-700)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-orange-500{color:var(--color-orange-500)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-sky-500{color:var(--color-sky-500)}.text-sky-600{color:var(--color-sky-600)}.text-stone-400{color:var(--color-stone-400)}.text-stone-500{color:var(--color-stone-500)}.text-stone-600{color:var(--color-stone-600)}.text-stone-800{color:var(--color-stone-800)}.text-white{color:var(--color-white)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.shadow,.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.invert{--tw-invert:invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:scale-\[1\.02\]:hover{scale:1.02}.hover\:bg-amber-50:hover{background-color:var(--color-amber-50)}.hover\:bg-amber-400:hover{background-color:var(--color-amber-400)}.hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}.hover\:bg-sky-400:hover{background-color:var(--color-sky-400)}.hover\:bg-stone-700:hover{background-color:var(--color-stone-700)}.hover\:bg-violet-500:hover{background-color:var(--color-violet-500)}.hover\:text-amber-600:hover{color:var(--color-amber-600)}.hover\:text-stone-600:hover{color:var(--color-stone-600)}.hover\:no-underline:hover{text-decoration-line:none}}.focus\:outline-blue-600:focus{outline-color:var(--color-blue-600)}.focus\:outline-solid:focus{--tw-outline-style:solid;outline-style:solid}.has-checked\:border-amber-400:has(:checked){border-color:var(--color-amber-400)}.has-checked\:bg-amber-100:has(:checked){background-color:var(--color-amber-100)}@media (min-width:40rem){.sm\:mt-0{margin-top:0}.sm\:flex{display:flex}.sm\:w-auto{width:auto}.sm\:items-center{align-items:center}.sm\:gap-4{gap:calc(var(--spacing) * 4)}}@media (min-width:48rem){.md\:col-span-2{grid-column:span 2/span 2}.md\:w-1\/2{width:50%}.md\:w-2\/3{width:66.6667%}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1} \ No newline at end of file diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js deleted file mode 100644 index 9a66180..0000000 --- a/app/assets/config/manifest.js +++ /dev/null @@ -1,4 +0,0 @@ -//= link_tree ../images -//= link_tree ../ebooks -//= link_directory ../javascripts .js -//= link_directory ../stylesheets .css diff --git a/app/assets/images/Transparency.png b/app/assets/images/Transparency.png deleted file mode 100644 index 128795f..0000000 Binary files a/app/assets/images/Transparency.png and /dev/null differ diff --git a/app/assets/images/check.png b/app/assets/images/check.png deleted file mode 100644 index 12aec55..0000000 Binary files a/app/assets/images/check.png and /dev/null differ diff --git a/app/assets/images/coins.png b/app/assets/images/coins.png deleted file mode 100644 index 43ff847..0000000 Binary files a/app/assets/images/coins.png and /dev/null differ diff --git a/app/assets/images/icons8-buy-96.png b/app/assets/images/icons8-buy-96.png deleted file mode 100644 index 9d3f2e7..0000000 Binary files a/app/assets/images/icons8-buy-96.png and /dev/null differ diff --git a/app/assets/images/icons8-checked-checkbox-64.png b/app/assets/images/icons8-checked-checkbox-64.png deleted file mode 100644 index a9d19a6..0000000 Binary files a/app/assets/images/icons8-checked-checkbox-64.png and /dev/null differ diff --git a/app/assets/images/icons8-pass-fail-96.png b/app/assets/images/icons8-pass-fail-96.png deleted file mode 100644 index 70675ba..0000000 Binary files a/app/assets/images/icons8-pass-fail-96.png and /dev/null differ diff --git a/app/assets/images/icons8-read-online-96.png b/app/assets/images/icons8-read-online-96.png deleted file mode 100644 index 80f1761..0000000 Binary files a/app/assets/images/icons8-read-online-96.png and /dev/null differ diff --git a/app/assets/images/kid-reading-with-parents.jpg b/app/assets/images/kid-reading-with-parents.jpg deleted file mode 100644 index c54ca07..0000000 Binary files a/app/assets/images/kid-reading-with-parents.jpg and /dev/null differ diff --git a/app/assets/images/kids_on_roof_reading_book.jpg b/app/assets/images/kids_on_roof_reading_book.jpg deleted file mode 100644 index b532610..0000000 Binary files a/app/assets/images/kids_on_roof_reading_book.jpg and /dev/null differ diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png deleted file mode 100644 index c96cd0b..0000000 Binary files a/app/assets/images/logo.png and /dev/null differ diff --git a/app/assets/images/logo_white.png b/app/assets/images/logo_white.png deleted file mode 100644 index 7f2c813..0000000 Binary files a/app/assets/images/logo_white.png and /dev/null differ diff --git a/app/assets/images/qr code.png b/app/assets/images/qr code.png deleted file mode 100644 index e68b130..0000000 Binary files a/app/assets/images/qr code.png and /dev/null differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js deleted file mode 100644 index 525cd52..0000000 --- a/app/assets/javascripts/application.js +++ /dev/null @@ -1,3 +0,0 @@ -//= require rails-ujs -//= require jquery -//= require_tree . diff --git a/app/assets/javascripts/cable.js b/app/assets/javascripts/cable.js deleted file mode 100644 index 739aa5f..0000000 --- a/app/assets/javascripts/cable.js +++ /dev/null @@ -1,13 +0,0 @@ -// Action Cable provides the framework to deal with WebSockets in Rails. -// You can generate new channels where WebSocket features live using the `rails generate channel` command. -// -//= require action_cable -//= require_self -//= require_tree ./channels - -(function() { - this.App || (this.App = {}); - - App.cable = ActionCable.createConsumer(); - -}).call(this); diff --git a/app/assets/javascripts/i.js b/app/assets/javascripts/i.js deleted file mode 100644 index 9a1e353..0000000 --- a/app/assets/javascripts/i.js +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * (โ„ ) - * # Pipi: BiB/i Putter - * - * - "Putting EPUBs in a Web Page with BiB/i." - * - (c) Satoru MATSUSHIMA - http://bibi.epub.link or https://github.com/satorumurmur/bibi - * - Licensed under the MIT license. - http://www.opensource.org/licenses/mit-license.php - */ -!function(){if(!window["bibi:pipi"]){var e=window["bibi:pipi"]={version:"0.999.9-r8",build:201804031815,Status:"",Bibis:[],Anchors:[],Holders:[],Frames:[],TrustworthyOrigins:[location.origin],Loaded:0};e.Path=function(){if(document.currentScript)return document.currentScript.src;var e=document.getElementsByTagName("script");return e[e.length-1].src}(),e.embed=function(){e.Status="Started";for(var t=document.body.querySelectorAll("a[data-bibi]"),i=t.length,n=0;n1?"s":"")+".")},!1),document.addEventListener("bibi:loaded",function(e){console.log("BiB/i: Loaded. - "+e.detail.Bibis.length+" Bibi"+(e.detail.Bibis.length>1?"s":"")+".")},!1),document.addEventListener("bibi:timed-out",function(e){console.log("BiB/i: Timed Out.")},!1),document.addEventListener("DOMContentLoaded",e.embed,!1)}}(); \ No newline at end of file diff --git a/app/assets/stylesheets/README.md b/app/assets/stylesheets/README.md deleted file mode 100644 index 265da48..0000000 --- a/app/assets/stylesheets/README.md +++ /dev/null @@ -1,101 +0,0 @@ -## Setup - -Ensure you have the following gems in your Rails `Gemfile` - -```ruby -# Gemfile -gem 'bootstrap-sass' -gem 'font-awesome-sass', '~> 5.0.9' -gem 'simple_form' -gem 'autoprefixer-rails' -gem 'jquery-rails' # Add this line if you use Rails 5.1 or higher -``` - -In your terminal, generate SimpleForm Bootstrap config. - -```bash -bundle install -rails generate simple_form:install --bootstrap -``` - -Then replace Rails' stylesheets by Le Wagon's stylesheets: - -``` -rm -rf app/assets/stylesheets -curl -L https://github.com/lewagon/stylesheets/archive/master.zip > stylesheets.zip -unzip stylesheets.zip -d app/assets && rm stylesheets.zip && mv app/assets/rails-stylesheets-master app/assets/stylesheets -``` - -Don't forget the sprockets directives in `assets/javascripts/application.js` - -```javascript -// app/assets/javascripts/application.js - -//= require jquery -//= require jquery_ujs -//= require bootstrap-sprockets -//= require_tree . -``` - -And the viewport in the layout - -```html - - - - - - - - -``` - -## Adding new `.scss` files - -Look at your main `application.scss` file to see how SCSS files are imported. There should **not** be a `*= require_tree .` line in the file. - -```scss -// app/assets/stylesheets/application.scss - -// Graphical variables -@import "config/fonts"; -@import "config/colors"; -@import "config/bootstrap_variables"; - -// External libraries -@import "bootstrap-sprockets"; -@import "bootstrap"; -@import "font-awesome-sprockets"; -@import "font-awesome"; - -// Your CSS partials -@import "layouts/index"; -@import "components/index"; -@import "pages/index"; -``` - -For every folder (**`components`**, **`layouts`**, **`pages`**), there is one `_index.scss` partial which is responsible for importing all the other partials of its folder. - -**Example 1**: Let's say you add a new `_contact.scss` file in **`pages`** then modify `pages/_index.scss` as: - -```scss -// pages/_index.scss -@import "home"; -@import "contact"; -``` - -**Example 2**: Let's say you add a new `_sidebar.scss` file in **`layouts`** then modify `layouts/_index.scss` as: - -```scss -// layouts/_index.scss -@import "sidebar"; -``` - -## Navbar template - -Our `layouts/_navbar.scss` code works well with our home-made ERB template which you can find here: - -- [version without login](https://github.com/lewagon/awesome-navbars/blob/master/templates/_navbar_wagon_without_login.html.erb). -- [version with login](https://github.com/lewagon/awesome-navbars/blob/master/templates/_navbar_wagon.html.erb). - -Don't forget that `*.html.erb` files go in the `app/views` folder, and `*.scss` files go in the `app/assets/stylesheets` folder. Also, our navbar have a link to the `root_path`, so make sure that you have a `root to: "controller#action"` route in your `config/routes.rb` file. diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000..fe93333 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,10 @@ +/* + * This is a manifest file that'll be compiled into application.css. + * + * With Propshaft, assets are served efficiently without preprocessing steps. You can still include + * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard + * cascading order, meaning styles declared later in the document or manifest will override earlier ones, + * depending on specificity. + * + * Consider organizing styles into separate files for maintainability. + */ diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss deleted file mode 100755 index 0fc3512..0000000 --- a/app/assets/stylesheets/application.scss +++ /dev/null @@ -1,22 +0,0 @@ -// Graphical variables -@import "config/fonts"; -@import "config/colors"; -@import "config/bootstrap_variables"; - -// External libraries -@import "bootstrap-sprockets"; -@import "bootstrap"; -@import "font-awesome-sprockets"; -@import "font-awesome"; - -// Your CSS partials -@import "layouts/index"; -@import "components/index"; -@import "pages/index"; -@import "devise/index"; -@import "kids/index"; -@import "books/index"; -@import "prizes/index"; -@import "readings/index"; -@import "purchases/index"; -@import "payments/index"; diff --git a/app/assets/stylesheets/books/_book.scss b/app/assets/stylesheets/books/_book.scss deleted file mode 100644 index a6d6507..0000000 --- a/app/assets/stylesheets/books/_book.scss +++ /dev/null @@ -1,29 +0,0 @@ -.book-show-container { - padding: 60px !important; -} -.book-image { - width: 30vw; - height: 100vh; - padding-top: 25px; - background-size: cover; -} -.book-details { - width: 70vw; -} -.book-show-btn { - margin: 20px; - text-align: center; -} -#book-img { - width: 30%; - margin-right: 20px; - margin-bottom: 20px; - border-radius: 5px; - float: left; -} -.book-show-links { - display: flex; - justify-content: center; - margin-top: 10vh; -} - diff --git a/app/assets/stylesheets/books/_books.scss b/app/assets/stylesheets/books/_books.scss deleted file mode 100644 index bc6f049..0000000 --- a/app/assets/stylesheets/books/_books.scss +++ /dev/null @@ -1,69 +0,0 @@ -.kid-dashboard-flexbox { - display: flex; - height: 170px; - margin-bottom: 15px; - border-bottom: 1px solid lightgrey; -} -.books-pics { - width: 25vw; - background-size: cover; - position: relative; - margin-right: 25px; - margin-bottom: 15px; -} -.book-category { - position: absolute; - top: 8px; - left: 0px; - text-transform: uppercase; - color: $turquiz; - padding: 3px 7px; - background-color: white; - font-size: 14px; -} -.books-details { - flex-grow: 1; - position: relative; - margin-bottom: 15px; -} -.books-details h3 { - margin-top: 0px; - margin-bottom: 6.5px; -} -.books-details p { - margin-bottom: 25px; -} -.book-author { - color: #ababab; -} -.bk-summary { - white-space: nowrap; - height: 20px; - overflow: hidden; - text-overflow: ellipsis; -} -.book-reward { - display: flex; - justify-content: space-around; - align-items: center; - width: 15vw; -} -.book-reward-ctn { - text-align: center; -} -.show-more-btn i { - color: $turquiz; - margin-right: 5px; -} -.show-more-btn { - display: flex; - align-items: center; - width: 150px; - position: absolute; - bottom: 0; - left: 0; -} -.show-more-btn p { - margin: 0; -} - diff --git a/app/assets/stylesheets/books/_index.scss b/app/assets/stylesheets/books/_index.scss deleted file mode 100644 index ce87e3e..0000000 --- a/app/assets/stylesheets/books/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import "book"; -@import "books"; diff --git a/app/assets/stylesheets/components/_alert.scss b/app/assets/stylesheets/components/_alert.scss deleted file mode 100644 index db8a7ed..0000000 --- a/app/assets/stylesheets/components/_alert.scss +++ /dev/null @@ -1,15 +0,0 @@ -/* ------------------------------------- - * Your CSS code for flash notices and alerts -* ------------------------------------- */ - -.alert { - margin: 0; - text-align: center; - color: white; -} -.alert-info { - background: $green; -} -.alert-warning { - background: $red; -} diff --git a/app/assets/stylesheets/components/_avatar.scss b/app/assets/stylesheets/components/_avatar.scss deleted file mode 100644 index 11c1978..0000000 --- a/app/assets/stylesheets/components/_avatar.scss +++ /dev/null @@ -1,21 +0,0 @@ -.kid-avatar { - border: 2px solid white; - border-radius: 50%; - width: 50px; - height: 50px; -} -.parent-avatar { - border: 2px solid $turquiz !important; - border-radius: 50%; - width: 50px; - height: 50px; -} -.login-avatar { - border: 2px solid white; - border-radius: 50%; - width: 100px; - height: 100px; -} -.create-avatar-view { - margin-top: 7vh; -} diff --git a/app/assets/stylesheets/components/_index.scss b/app/assets/stylesheets/components/_index.scss deleted file mode 100644 index e256fc8..0000000 --- a/app/assets/stylesheets/components/_index.scss +++ /dev/null @@ -1,6 +0,0 @@ -// Import your components CSS files here. -@import "alert"; -@import "navbar"; -@import "parentButton"; -@import "kidButton"; -@import "avatar"; diff --git a/app/assets/stylesheets/components/_kidButton.scss b/app/assets/stylesheets/components/_kidButton.scss deleted file mode 100644 index db5e437..0000000 --- a/app/assets/stylesheets/components/_kidButton.scss +++ /dev/null @@ -1,28 +0,0 @@ -.kid-button { - color: rgba(84, 198, 235, 0.8); - padding: 5px 15px; - border-radius: 15px; - border: 2px solid rgba(84, 198, 235, 0.8) -} -.kid-button:hover { - background-color: rgba(84, 198, 235, 1); -} - -a.kid-button:hover { - text-decoration: none; - color: rgba(247, 247, 255, 1); -} -.kid-focus-button { - background-color: rgba(84, 198, 235, 1); - color: white; - padding: 5px 15px; - border-radius: 15px; - border: 2px solid rgba(84, 198, 235, 1) -} -.kid-focus-button:hover { - background-color: white; -} -a.kid-focus-button:hover { - text-decoration: none; - color: rgba(84, 198, 235, 1); -} diff --git a/app/assets/stylesheets/components/_navbar.scss b/app/assets/stylesheets/components/_navbar.scss deleted file mode 100644 index 7d67812..0000000 --- a/app/assets/stylesheets/components/_navbar.scss +++ /dev/null @@ -1,82 +0,0 @@ -/* Main navbar */ -.navbar-wagon { - transition: all 0.3s ease; - height: 70px; - padding: 0px 30px; - display: flex; - align-items: center; - justify-content: space-between; - position: absolute; - top: 0; - width: 100%; -} - -/* Logo */ -.navbar-wagon-brand img { - height: 50px; -} - -/* User Avatar */ -.navbar-wagon .avatar { - height: 35px; - width: 35px; - border-radius: 50%; -} - -/* Right section */ -.navbar-wagon-right { - display: flex; - align-items: center; - justify-content: space-between; -} - -/* Basic navbar item */ -.navbar-wagon-item { - cursor: pointer; - padding: 0 20px; -} - -/* Navbar link */ -.navbar-wagon-link { - color: #616668; - font-size: 18px; - font-family: 'Indie Flower', cursive; -} -.navbar-wagon-link:hover { - color: #da552f; - text-decoration: none; -} - -/* Dropdown menu */ -.navbar-wagon-dropdown-menu { - margin-top: 15px; - box-shadow: 1px 1px 4px #E6E6E6; - border-color: #E6E6E6; -} -.navbar-wagon-dropdown-menu li > a { - transition: color 0.3s ease; - font-weight: lighter !important; - color: #999999 !important; - font-size: 15px !important; - line-height: 22px !important; - padding: 10px 20px; -} -.navbar-wagon-dropdown-menu li > a:hover { - background: transparent !important; - color: black !important; -} -.navbar-wagon-dropdown-menu:before { - content: ' '; - height: 10px; - width: 10px; - position: absolute; - right: 10px; - top: -6px; - background-color: white; - transform: rotate(45deg); - border-left: 1px solid #E6E6E6; - border-top: 1px solid #E6E6E6; -} -.navbar-kid { - color: white !important; -} diff --git a/app/assets/stylesheets/components/_parentButton.scss b/app/assets/stylesheets/components/_parentButton.scss deleted file mode 100644 index 95ade34..0000000 --- a/app/assets/stylesheets/components/_parentButton.scss +++ /dev/null @@ -1,15 +0,0 @@ -.parent-button { - color: rgba(247, 247, 255, 0.8); - background-color: rgba(84, 198, 235, 1); - padding: 5px 15px; - border-radius: 15px; - border: 2px solid rgba(247, 247, 255, 0.8); -} -.parent-button:hover { - background-color: rgba(247, 247, 255, 0.8); - color: rgba(84, 198, 235, 1); -} -a.parent-button:hover { - text-decoration: none; - color: rgba(84, 198, 235, 1); -} diff --git a/app/assets/stylesheets/config/_bootstrap_variables.scss b/app/assets/stylesheets/config/_bootstrap_variables.scss deleted file mode 100755 index c2f55eb..0000000 --- a/app/assets/stylesheets/config/_bootstrap_variables.scss +++ /dev/null @@ -1,53 +0,0 @@ -// This is where you override default Bootstrap variables -// 1. All Bootstrap variables are here => https://github.com/twbs/bootstrap-sass/blob/master/assets/stylesheets/bootstrap/_variables.scss -// 2. These variables are defined with default value (see https://robots.thoughtbot.com/sass-default) -// 3. You can override them below! - -// General style -$font-family-sans-serif: $body-font; -$headings-font-family: $headers-font; -$body-bg: $light-gray; -$font-size-base: 18px; - -// Colors -$gray-base: $gray; -$brand-primary: $blue; -$brand-success: $green; -$brand-info: $yellow; -$brand-danger: $turquiz; -$brand-warning: $orange; - -// Buttons & inputs' radius -$border-radius-base: 2px; -$border-radius-large: 2px; -$border-radius-small: 2px; - - -// Patch to make simple_form compatible with bootstrap 3 -.invalid-feedback { - display: none; - width: 100%; - margin-top: 0.25rem; - font-size: 80%; - color: $red; -} - -.was-validated .form-control:invalid, -.form-control.is-invalid, -.was-validated .custom-select:invalid, -.custom-select.is-invalid { - border-color: $red; -} - -.was-validated .form-control:invalid ~ .invalid-feedback, -.was-validated .form-control:invalid ~ .invalid-tooltip, -.form-control.is-invalid ~ .invalid-feedback, -.form-control.is-invalid ~ .invalid-tooltip, -.was-validated .custom-select:invalid ~ .invalid-feedback, -.was-validated .custom-select:invalid ~ .invalid-tooltip, -.custom-select.is-invalid ~ .invalid-feedback, -.custom-select.is-invalid ~ .invalid-tooltip { - display: block; -} - -// Override other variables below! diff --git a/app/assets/stylesheets/config/_colors.scss b/app/assets/stylesheets/config/_colors.scss deleted file mode 100644 index c598a6d..0000000 --- a/app/assets/stylesheets/config/_colors.scss +++ /dev/null @@ -1,11 +0,0 @@ -// Define variables for your color scheme - -// For example: -$red: #EF798A; -$blue: #469AE0; -$yellow: #FDB631; -$orange: #E67E22; -$green: #32B796; -$gray: #2A2D34; -$light-gray: #F9F9F9; -$turquiz: #54c6eb; diff --git a/app/assets/stylesheets/config/_fonts.scss b/app/assets/stylesheets/config/_fonts.scss deleted file mode 100755 index 5cfbbf3..0000000 --- a/app/assets/stylesheets/config/_fonts.scss +++ /dev/null @@ -1,18 +0,0 @@ -// Import Google fonts -@import url('https://fonts.googleapis.com/css?family=Kurale'); -@import url('https://fonts.googleapis.com/css?family=Indie+Flower'); -@import url('https://fonts.googleapis.com/css?family=Dosis:300,400,700'); -// Define fonts for body and headers -$body-font:'Dosis', serif; -$headers-font: 'Indie Flower', cursive; - - -// To use a font file (.woff) uncomment following lines -// @font-face { -// font-family: "Font Name"; -// src: font-url('FontFile.eot'); -// src: font-url('FontFile.eot?#iefix') format('embedded-opentype'), -// font-url('FontFile.woff') format('woff'), -// font-url('FontFile.ttf') format('truetype') -// } -// $my-font: "Font Name"; diff --git a/app/assets/stylesheets/devise/_index.scss b/app/assets/stylesheets/devise/_index.scss deleted file mode 100644 index cf6ecb6..0000000 --- a/app/assets/stylesheets/devise/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import "login"; -@import "signup"; -@import "newkid"; diff --git a/app/assets/stylesheets/devise/_login.scss b/app/assets/stylesheets/devise/_login.scss deleted file mode 100644 index 8989be7..0000000 --- a/app/assets/stylesheets/devise/_login.scss +++ /dev/null @@ -1,79 +0,0 @@ -.two-sides { - display: flex; - p { - margin-bottom: 20px; - font-size: 22px; - } -} -.right-side { - background-color: $turquiz; - width: 50vw; - height: 100vh; - display: flex; - justify-content: space-around; -} -.right-side-container { - width: 30vw; - margin: 10vh auto; - color: white; - position: relative; - margin-top: 18vh; - h2 { - background: #F7F7FF; - color: #54c6eb; - right: -102px; - border-radius: 15px 0 0 15px; - top: -105px; - width: 90px; - } -} -.kids-description { - margin-top: 72px; - min-height: 75px; -} -.left-side { - background-color: #F7F7FF; - width: 50vw; - height: 100vh; - display: flex; - justify-content: space-around; -} -.login-title { - padding: 10px 15px; - position: absolute; - border-radius: 15px; -} -.left-side-container { - width: 30vw; - margin: 10vh auto; - color: $turquiz; - margin-top: 18vh; - position: relative; - h2 { - background: #54c6eb; - color: white; - text-align: right; - left: -130px; - top: -105px; - width: 190px; - } -} -.login-avatar { - margin-left: 10vw; -} -#login-go-btn { - margin-left: 12vw; -} -.login-links { - margin-top: 25px; - text-align: center; - p { - margin-top: 10px; - } -} -.user-login-links { - color: $turquiz; -} -.form-for-kid { - margin-top: 31px; -} diff --git a/app/assets/stylesheets/devise/_newkid.scss b/app/assets/stylesheets/devise/_newkid.scss deleted file mode 100644 index 5ba07f8..0000000 --- a/app/assets/stylesheets/devise/_newkid.scss +++ /dev/null @@ -1,28 +0,0 @@ -#kid_name { - border: none; - box-shadow: none; - border-bottom-style: solid; - border-bottom-width: 1px; -} - -#kid_password { - border: none; - box-shadow: none; - border-bottom-style: solid; - border-bottom-width: 1px; -} - -#kid_age { - border: none; - box-shadow: none; - border-bottom-style: solid; - border-bottom-width: 1px; -} -.form-control:focus { - border-color: none; - box-shadow: none; -} - -input { - background-color: white; -} diff --git a/app/assets/stylesheets/devise/_signup.scss b/app/assets/stylesheets/devise/_signup.scss deleted file mode 100644 index 4b7d4f2..0000000 --- a/app/assets/stylesheets/devise/_signup.scss +++ /dev/null @@ -1,34 +0,0 @@ -abbr[title="required"] { - display: none; -} - -.form-background { - background-color: white; - margin-top: 130px; -} - -.form-control:focus { - border-color: none; - box-shadow: none; -} - -#user_email { - border: none; - box-shadow: none; - border-bottom-style: solid; - border-bottom-width: 1px; -} - -#user_password { - border: none; - box-shadow: none; - border-bottom-style: solid; - border-bottom-width: 1px; -} - -#user_password_confirmation { - border: none; - box-shadow: none; - border-bottom-style: solid; - border-bottom-width: 1px; -} diff --git a/app/assets/stylesheets/kids/_dashboard.scss b/app/assets/stylesheets/kids/_dashboard.scss deleted file mode 100644 index 131861e..0000000 --- a/app/assets/stylesheets/kids/_dashboard.scss +++ /dev/null @@ -1,34 +0,0 @@ -.kid-dashboard-wrapper{ - display: flex; - justify-content: space-around; - background-color: $turquiz; - overflow: scroll; -} -.kid-dashboard-container{ - width: 85vw; - height: 100vh; - background-color: white; - padding: 20px; - margin-top: 70px; - border-radius: 5px; - overflow: scroll; -} -.kid-dashboard-container h1 { - margin-top: 15px; - margin-bottom: 30px; - text-transform: uppercase; - letter-spacing: 1px; -} -.parent-dashboard-wrapper{ - display: flex; - justify-content: space-around; -} -.parent-dashboard-container{ - width: 85vw; - height: 100vw; - padding: 20px; -} - -#create-avatar { - margin-top: 45px; -} diff --git a/app/assets/stylesheets/kids/_index.scss b/app/assets/stylesheets/kids/_index.scss deleted file mode 100644 index b67b69f..0000000 --- a/app/assets/stylesheets/kids/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import "dashboard"; -@import "kids"; -@import "quizz"; diff --git a/app/assets/stylesheets/kids/_kids.scss b/app/assets/stylesheets/kids/_kids.scss deleted file mode 100644 index c888b56..0000000 --- a/app/assets/stylesheets/kids/_kids.scss +++ /dev/null @@ -1,157 +0,0 @@ -.parents-dashboard-container { - height: 100vw; - margin-top: 10vh; - background-color: #EEEEEE; - overflow: scroll; - width: 85vw; - h1 { - font-family: 'Dosis'; - } -} -.parents-dashboard-wrapper { - display: flex; - justify-content: center; - overflow: scroll; -} -#amount { - width: 167px; -} -.call-to-action { - display:flex; - justify-content:center; -} -.purchase-action { - display: flex; - justify-content:center; - margin-top: 4vh; -} -.buy-btn { - color: rgba(247, 247, 255, 0.8); - background-color: rgba(84, 198, 235, 1); - padding: 5px 15px; - border-radius: 15px; - border: 2px solid rgba(247, 247, 255, 0.8); - } -.buy-btn:hover { - background-color: rgba(247, 247, 255, 0.8); - color: rgba(84, 198, 235, 1); - } -a.buy-btn:hover { - text-decoration: none; - color: rgba(84, 198, 235, 1); - } -.purchase-card { - border: 0.8px solid $turquiz; - border-radius: 12px; - padding: 25px; -} -.balance-card-content { - border: 1px solid $turquiz; - height: 300px; - width: 300px; - border-radius: 50%; - margin: 0; - display:flex; - justify-content:center; - align-items:center; - background-color: $turquiz; -} - -.kid-card { - border: 1.5px solid $turquiz; - border-radius: 12px; - padding: 0; - width: 85%; - margin-bottom: 40px; - height: 35vh; - display: flex; - margin-left: 7vw; -} -.card-left { - width:30%; - border-right: 1px solid $turquiz; - margin: 0; - display:flex; - align-items:center; - justify-content:center; -} -.card-right { - width:70%; - margin: 0; - font-size: 24px; -} -.card-avatar { - height:150px; - width:150px; -} -.kid-name { - margin-top: 30px; -} -.kid-params { - display: flex; - justify-content:space-around; - height: 20vh; - align-items: center; -} -.nav-buttons { - display: flex; - justify-content: space-around; -} -.btn-add-kid-margin { - margin-top: 70px; - display: flex; - justify-content: center; - margin-left: 0 !important; - margin-right: 0 !important; -} -.log-out-btn { - border: none !important; - text-decoration: underline !important; - color: rgba(84, 198, 235, 0.8); - padding: 5px 15px; -} -.add-child-button { - color: rgba(84, 198, 235, 0.8); - padding: 5px 15px; - border-radius: 15px; - border: 2px solid rgba(84, 198, 235, 0.8); - margin-bottom: 30px; - width: 15%; -} -.add-child-button:hover { - background-color: rgba(84, 198, 235, 1); -} -a.add-child-button:hover { - text-decoration: none; - color: rgba(247, 247, 255, 1); -} -.btn-add-kid { - border-radius: 50%; -} -.edit-container-form { - margin-top: 15vh; -} -.num-field, .call-action-content { - color: white; -} -.cards-display { - margin-left: 0 !important; - margin-right: 0 !important; -} - - - - - - - - - - - - - - - - - diff --git a/app/assets/stylesheets/kids/_quizz.scss b/app/assets/stylesheets/kids/_quizz.scss deleted file mode 100644 index 78fe498..0000000 --- a/app/assets/stylesheets/kids/_quizz.scss +++ /dev/null @@ -1,10 +0,0 @@ -.quiz-container { - text-align: center; -} -.score-result { - text-align: center; - margin-top: 30vh; -} -.quiz-links { - margin-top: 5vh; -} diff --git a/app/assets/stylesheets/layouts/_index.scss b/app/assets/stylesheets/layouts/_index.scss deleted file mode 100644 index 5a4ca4b..0000000 --- a/app/assets/stylesheets/layouts/_index.scss +++ /dev/null @@ -1,5 +0,0 @@ -// Import your layouts CSS files here. -// Examples: -// @import "dashboard"; -// @import "profile"; -// @import "map"; diff --git a/app/assets/stylesheets/pages/_home.scss b/app/assets/stylesheets/pages/_home.scss deleted file mode 100755 index 4118b65..0000000 --- a/app/assets/stylesheets/pages/_home.scss +++ /dev/null @@ -1,123 +0,0 @@ -//banner -.body { - position: relative; -} -.banner-img { - background-image: linear-gradient(rgba(0,0,0,0.3), rgba(0,0,0,0.3)), url("http://www.webanywhere.co.uk/blog/wp-content/uploads/2012/09/rsz_children_tablet.jpg"); - background-repeat: no-repeat; - background-size: cover; - background-position: center; - height: 100vh; - width: 100vw; - position: relative; -} - -.img-circle { - width: 250px; - height: 250px; -} - -.banner-content { - position: absolute; - top: 50%; - left:50%; - transform: translate(-50%,-50%); -} - -.banner-title { - font-size: 100px; - font-weight: bolder; - color:white; - filter: brightness(0.9) -} - -.banner-btn { - margin-top: 35px; - font-size: 40px; - width: 200px; - -} - -.banner-text { - font-weight: bolder; -} - -//problem-solution -.problem-solution-background { - background-color: #EEEEEE; - margin-bottom: 0; - height: 500px; -} - -.cards-align { - display: flex; - flex-direction: row; - justify-content: center; - margin-top: 65px; -} - -.card-home-left { - margin-right: 38px; -} - -.card-home-right { - margin-left: 38px; -} - -.lgog-image { - margin-top: 0; - -} - -.path-header { - font-size: 69px; - font-weight:bolder; -} - - - -.the-path-background-color { - background-color: white; - margin: 0; -} - -.get-paid-background { - margin-bottom: 0; - background-color: #39393A; - color: white; - text-align: center; -} - -.how-uses-background { - background-color: #EEEEEE; - padding-top: 40px; - padding-bottom: 40px; - text-align: center; - p { - margin-top: 20px; - } -} - -.image-inline { - display: flex; - justify-content: space-between; - -} -#qr { - margin-bottom: 20px; -} -.landing-page-content p { - font-size: 24px; -} -.btn-banner-home { - background-color: $turquiz; - color: white; - border-radius: 5px; - padding: 10px 25px; - font-size: 35px; -} -.btn-banner-home:hover { - background-color: white; - color: $turquiz; - text-decoration: none; -} diff --git a/app/assets/stylesheets/pages/_index.scss b/app/assets/stylesheets/pages/_index.scss deleted file mode 100644 index 7d880e3..0000000 --- a/app/assets/stylesheets/pages/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -// Import page-specific CSS files here. -@import "home"; diff --git a/app/assets/stylesheets/payments/_confirmation.scss b/app/assets/stylesheets/payments/_confirmation.scss deleted file mode 100644 index 74afbdc..0000000 --- a/app/assets/stylesheets/payments/_confirmation.scss +++ /dev/null @@ -1,13 +0,0 @@ - - -.back-dashbord-btn { - margin-top:45px; -} - -.order-amount-font-size { - font-size: 80px; -} - -.readcoins-font-size { - font-size: 24px; -} diff --git a/app/assets/stylesheets/payments/_index.scss b/app/assets/stylesheets/payments/_index.scss deleted file mode 100644 index 3a19cf0..0000000 --- a/app/assets/stylesheets/payments/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import "new"; -@import "confirmation"; diff --git a/app/assets/stylesheets/payments/_new.scss b/app/assets/stylesheets/payments/_new.scss deleted file mode 100644 index b3510e6..0000000 --- a/app/assets/stylesheets/payments/_new.scss +++ /dev/null @@ -1,43 +0,0 @@ -.page-size { - height: 100vh; - display:flex; - justify-content:center; - align-items:center; - -} - -.payment-card { - height:65vh; - padding: 30px; - border: 0.5px solid #D9D9D9; - box-shadow: 2px 1px #C3C3C3; - border-radius: 2px; - background-color: #EEEEEE; - h1 { - font-family: $body-font; - } -} - -.amount-container { - display:flex; - justify-content:center; - margin-top:25px; -} -.amount-circle { - border: 1px solid $turquiz; - height: 250px; - width: 250px; - border-radius: 50%; - margin: 0; - display:flex; - flex-direction: column; - justify-content:center; - align-items:center; - background-color: $turquiz; - color: white; -} - -.stripe-payment-btn { - margin-top: 45px; -} - diff --git a/app/assets/stylesheets/prizes/_index.scss b/app/assets/stylesheets/prizes/_index.scss deleted file mode 100644 index cdb0129..0000000 --- a/app/assets/stylesheets/prizes/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import "prizes"; -@import "prize"; diff --git a/app/assets/stylesheets/prizes/_prize.scss b/app/assets/stylesheets/prizes/_prize.scss deleted file mode 100644 index 707f129..0000000 --- a/app/assets/stylesheets/prizes/_prize.scss +++ /dev/null @@ -1,21 +0,0 @@ -.prize-show-container { - display: flex; - justify-content: space-around; -} -.prize-show-card { - width: 40vw; - height: 90vh; - border: 4px solid $turquiz; - border-radius: 10px; - box-shadow: 5px 5px 1px #C0EAF7; - text-align: center; - margin-top: 4vh; - h2 { - margin-bottom: 7vh; - } -} -.prize-title h3 { - font-size: 20px; - font-family: 'Dosis'; -} - diff --git a/app/assets/stylesheets/prizes/_prizes.scss b/app/assets/stylesheets/prizes/_prizes.scss deleted file mode 100644 index df94a2e..0000000 --- a/app/assets/stylesheets/prizes/_prizes.scss +++ /dev/null @@ -1,28 +0,0 @@ -.prizes-container { - border-radius: 5px; -} -.prize-card { - background-color: white; - border-radius: 5px; - border: 1px solid $turquiz; - box-shadow: 2px 2px 1px #C0EAF7; - height: 350px; - text-align: center; - margin-bottom: 20px; - padding-top: 10px; - h3 { - margin-top: 0px; - color: $turquiz; - } - p { - margin-bottom: 20px; - } -} -.prize-title { - height: 70px; -} -.eshop-top-flexbox { - display: flex; - justify-content: space-between; - margin-bottom: 4vh; -} diff --git a/app/assets/stylesheets/purchases/_index.scss b/app/assets/stylesheets/purchases/_index.scss deleted file mode 100644 index 843f08b..0000000 --- a/app/assets/stylesheets/purchases/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "purchase"; diff --git a/app/assets/stylesheets/purchases/_purchase.scss b/app/assets/stylesheets/purchases/_purchase.scss deleted file mode 100644 index bcf4426..0000000 --- a/app/assets/stylesheets/purchases/_purchase.scss +++ /dev/null @@ -1,26 +0,0 @@ -.purchase-show-flexbox { - display: flex; - align-items: center; - justify-content: space-around; -} -#purchased-btn { - width: 100px; - height: 100px; -} -.purchase-link { - display: flex; - justify-content: center; -} -.purchase-top-flexbox { - display: flex; - justify-content: space-between; -} -#purchase-back-shop { - margin-right: 10px; -} -.purchase-details { - text-align: center; - h3 { - margin-bottom: 50px; - } -} diff --git a/app/assets/stylesheets/readings/_index.scss b/app/assets/stylesheets/readings/_index.scss deleted file mode 100644 index 552b5cf..0000000 --- a/app/assets/stylesheets/readings/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "reading"; diff --git a/app/assets/stylesheets/readings/_reading.scss b/app/assets/stylesheets/readings/_reading.scss deleted file mode 100644 index aa6ea98..0000000 --- a/app/assets/stylesheets/readings/_reading.scss +++ /dev/null @@ -1,7 +0,0 @@ -.link-to-ebook { - margin-top: 70px; -} -.to-book-btn { - margin-left: 70px; - margin-bottom: 20px; -} diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/app/assets/tailwind/application.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb deleted file mode 100644 index d672697..0000000 --- a/app/channels/application_cable/channel.rb +++ /dev/null @@ -1,4 +0,0 @@ -module ApplicationCable - class Channel < ActionCable::Channel::Base - end -end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 0ff5442..4264c74 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,4 +1,16 @@ module ApplicationCable class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + set_current_user || reject_unauthorized_connection + end + + private + def set_current_user + if session = Session.find_by(id: cookies.signed[:session_id]) + self.current_user = session.user + end + end end end diff --git a/app/controllers/admin/books_controller.rb b/app/controllers/admin/books_controller.rb deleted file mode 100644 index f73d0dc..0000000 --- a/app/controllers/admin/books_controller.rb +++ /dev/null @@ -1,131 +0,0 @@ -class Admin::BooksController < ApplicationController - def index - @books = Book.all - end - def new - @book = Book.new - @categories = ["Adventure", - "Animals", - "Comedy", - "Comic Books", - "Culture", - "Drama", - "Ecology", - "Education", - "Entertainment", - "Environment", - "Fairy Tale", - "Family", - "Fantasy", - "Fiction", - "Food", - "Friendship", - "Futuristic", - "Game Books", - "Holiday", - "Horror", - "Humor", - "Literature", - "Magic", - "Manga", - "Movies", - "Music", - "Mystery", - "Mythology", - "Pets", - "Photography", - "Picture", - "Poetry", - "Romance", - "Science Fiction", - "Sports", - "Superhero", - "Supernatural", - "Suspense", - "Technology", - "Thriller", - "Travel", - "Western", - ] - end - - def create - @book = Book.new(book_params) - if @book.save! - redirect_to admin_books_path - else - render :new - end - end - - def edit - @book = Book.find(params[:id]) - @categories = ["Adventure", - "Animals", - "Comedy", - "Comic Books", - "Culture", - "Drama", - "Ecology", - "Education", - "Entertainment", - "Environment", - "Fairy Tale", - "Family", - "Fantasy", - "Fiction", - "Food", - "Friendship", - "Futuristic", - "Game Books", - "Holiday", - "Horror", - "Humor", - "Literature", - "Magic", - "Manga", - "Movies", - "Music", - "Mystery", - "Mythology", - "Pets", - "Photography", - "Picture", - "Poetry", - "Romance", - "Science Fiction", - "Sports", - "Superhero", - "Supernatural", - "Suspense", - "Technology", - "Thriller", - "Travel", - "Western", - ] - end - - def update - @book = Book.find(params[:id]) - @book.update(book_params) - if @book.save! - redirect_to admin_books_path - else - render :update - end - end - - def destroy - @book = Book.find(params[:id]) - @book.destroy - redirect_to admin_books_path - end - - private - - def book_params - ps = params.require(:book).permit(:title, :url, :quiz, :reward, :min_age, :max_age, :author, :summary, :photo, :genre) - ps[:quiz] = JSON.load(ps[:quiz]) - ps - end -end diff --git a/app/controllers/admin/prizes_controller.rb b/app/controllers/admin/prizes_controller.rb deleted file mode 100644 index 76976a0..0000000 --- a/app/controllers/admin/prizes_controller.rb +++ /dev/null @@ -1,44 +0,0 @@ -class Admin::PrizesController < ApplicationController - def index - @prizes = Prize.all - end - - def new - @prize = Prize.new - end - - def create - @prize = Prize.new(prize_params) - if @prize.save - redirect_to admin_prizes_path - else - render :new - end - end - - def edit - @prize = Prize.find(params[:id]) - end - - def update - @prize = Prize.find(params[:id]) - @prize.update(prize_params) - if @prize.save! - redirect_to admin_prizes_path - else - render :edit - end - end - - def destroy - @prize = Prize.find(params[:id]) - @prize.destroy - redirect_to admin_prizes_path - end - - private - - def prize_params - params.require(:prize).permit(:title, :price, :picture) - end -end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 087f517..5f38f02 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,14 +1,8 @@ - class ApplicationController < ActionController::Base - protect_from_forgery with: :exception - before_action :authenticate_user! - before_action :configure_permitted_parameters, if: :devise_controller? - protected - def after_sign_in_path_for(resource) - request.env['omniauth.origin'] || kids_login_path - end + include Authentication + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern - def configure_permitted_parameters - devise_parameter_sanitizer.permit(:sign_up, keys: [:username]) - end + # Changes to the importmap will invalidate the etag for HTML responses + stale_when_importmap_changes end diff --git a/app/controllers/books_controller.rb b/app/controllers/books_controller.rb deleted file mode 100644 index b8a6834..0000000 --- a/app/controllers/books_controller.rb +++ /dev/null @@ -1,36 +0,0 @@ -class BooksController < ApplicationController - def show - @kid = Kid.find(params[:kid_id]) - @book = Book.find(params[:id]) - end - - def quiz - @kid = Kid.find(params[:kid_id]) - @book = Book.find(params[:book_id]) - end - - def check_quiz - @user = current_user - @kid = Kid.find(params[:kid_id]) - @book = Book.find(params[:book_id]) - @answered_quiz = params[:quiz] - @score = 0 - @answered_quiz.each do |key, value| - @score += 20 if value == "true" - end - if @score == 100 - @kid.wallet += @book.reward - @kid.save! - @user.wallet -= Money.new((@book.reward * 100), 'USD') - @user.save! - end - redirect_to kid_book_answers_quiz_path(@kid, @book, @score) - end - - def answers_quiz - @kid = Kid.find(params[:kid_id]) - @book = Book.find(params[:book_id]) - @score = params[:score] - end - -end diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb new file mode 100644 index 0000000..3538f48 --- /dev/null +++ b/app/controllers/concerns/authentication.rb @@ -0,0 +1,52 @@ +module Authentication + extend ActiveSupport::Concern + + included do + before_action :require_authentication + helper_method :authenticated? + end + + class_methods do + def allow_unauthenticated_access(**options) + skip_before_action :require_authentication, **options + end + end + + private + def authenticated? + resume_session + end + + def require_authentication + resume_session || request_authentication + end + + def resume_session + Current.session ||= find_session_by_cookie + end + + def find_session_by_cookie + Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id] + end + + def request_authentication + session[:return_to_after_authenticating] = request.url + redirect_to new_session_path + end + + def after_authentication_url + session.delete(:return_to_after_authenticating) || root_url + end + + def start_new_session_for(user) + user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session| + Current.session = session + cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax } + end + end + + def terminate_session + Current.session.destroy + cookies.delete(:session_id) + end +end diff --git a/app/controllers/family/base_controller.rb b/app/controllers/family/base_controller.rb new file mode 100644 index 0000000..55fe6c8 --- /dev/null +++ b/app/controllers/family/base_controller.rb @@ -0,0 +1,10 @@ +module Family + class BaseController < ApplicationController + layout "family" + + private + + def current_parent = Current.session.user + helper_method :current_parent + end +end diff --git a/app/controllers/family/books_controller.rb b/app/controllers/family/books_controller.rb new file mode 100644 index 0000000..0a7b3ca --- /dev/null +++ b/app/controllers/family/books_controller.rb @@ -0,0 +1,51 @@ +module Family + class BooksController < BaseController + before_action :set_book, only: %i[show edit update destroy] + + def index + @books = Book.order(:title).includes(:quiz) + end + + def show + end + + def new + @book = Book.new(reward: 10) + end + + def create + @book = Book.new(book_params) + if @book.save + redirect_to edit_family_book_quiz_path(@book), notice: "Book added โ€” now give it a quiz." + else + render :new, status: :unprocessable_entity + end + end + + def edit + end + + def update + if @book.update(book_params) + redirect_to family_books_path, notice: "Saved." + else + render :edit, status: :unprocessable_entity + end + end + + def destroy + @book.destroy + redirect_to family_books_path, notice: "Removed." + end + + private + + def set_book + @book = Book.find(params[:id]) + end + + def book_params + params.require(:book).permit(:title, :author, :genre, :summary, :cover_url, :source_url, :min_age, :max_age, :reward) + end + end +end diff --git a/app/controllers/family/dashboards_controller.rb b/app/controllers/family/dashboards_controller.rb new file mode 100644 index 0000000..6477de3 --- /dev/null +++ b/app/controllers/family/dashboards_controller.rb @@ -0,0 +1,9 @@ +module Family + class DashboardsController < BaseController + def show + @kids = current_parent.kids.includes(:readings) + @pending_redemptions = Redemption.pending.joins(:reward).where(rewards: { parent_id: current_parent.id }).includes(:kid, :reward) + @recent_readings = Reading.finished.where(kid: @kids).order(updated_at: :desc).limit(10).includes(:kid, :book) + end + end +end diff --git a/app/controllers/family/kids_controller.rb b/app/controllers/family/kids_controller.rb new file mode 100644 index 0000000..27d9741 --- /dev/null +++ b/app/controllers/family/kids_controller.rb @@ -0,0 +1,50 @@ +module Family + class KidsController < BaseController + before_action :set_kid, only: %i[edit update destroy] + + def index + @kids = current_parent.kids + end + + def new + @kid = current_parent.kids.new + end + + def create + @kid = current_parent.kids.new(kid_params) + if @kid.save + redirect_to family_kids_path, notice: "#{@kid.name} is ready to read!" + else + render :new, status: :unprocessable_entity + end + end + + def edit + end + + def update + attrs = kid_params + attrs = attrs.except(:pin) if attrs[:pin].blank? + if @kid.update(attrs) + redirect_to family_kids_path, notice: "Saved." + else + render :edit, status: :unprocessable_entity + end + end + + def destroy + @kid.destroy + redirect_to family_kids_path, notice: "Removed." + end + + private + + def set_kid + @kid = current_parent.kids.find(params[:id]) + end + + def kid_params + params.require(:kid).permit(:name, :age, :avatar, :pin) + end + end +end diff --git a/app/controllers/family/quizzes_controller.rb b/app/controllers/family/quizzes_controller.rb new file mode 100644 index 0000000..c9709a6 --- /dev/null +++ b/app/controllers/family/quizzes_controller.rb @@ -0,0 +1,46 @@ +module Family + class QuizzesController < BaseController + before_action :set_book + + def edit + @quiz = @book.quiz || @book.build_quiz(questions: []) + end + + def update + @quiz = @book.quiz || @book.build_quiz + @quiz.questions = parse_questions + if @quiz.save + redirect_to family_books_path, notice: "Quiz saved for #{@book.title}." + else + flash.now[:alert] = @quiz.errors.full_messages.to_sentence + render :edit, status: :unprocessable_entity + end + end + + def generate + @quiz = @book.quiz || @book.build_quiz + @quiz.questions = QuizGenerator.generate(@book) + @quiz.save! + redirect_to edit_family_book_quiz_path(@book), notice: "Quiz generated โ€” review it below." + rescue QuizGenerator::Error => e + redirect_to edit_family_book_quiz_path(@book), alert: e.message + end + + private + + def set_book + @book = Book.find(params[:book_id]) + end + + # Quiz form posts questions as nested fields: + # questions[0][question], questions[0][choices][], questions[0][answer] + def parse_questions + (params[:questions] || {}).values.filter_map do |q| + choices = Array(q[:choices]).map(&:strip).reject(&:blank?) + next if q[:question].blank? && choices.empty? + + { "question" => q[:question].to_s.strip, "choices" => choices, "answer" => q[:answer].to_i } + end + end + end +end diff --git a/app/controllers/family/redemptions_controller.rb b/app/controllers/family/redemptions_controller.rb new file mode 100644 index 0000000..259d8ad --- /dev/null +++ b/app/controllers/family/redemptions_controller.rb @@ -0,0 +1,14 @@ +module Family + class RedemptionsController < BaseController + def index + @redemptions = Redemption.joins(:reward).where(rewards: { parent_id: current_parent.id }) + .order(created_at: :desc).includes(:kid, :reward) + end + + def update + redemption = Redemption.joins(:reward).where(rewards: { parent_id: current_parent.id }).find(params[:id]) + redemption.approve! + redirect_back fallback_location: family_redemptions_path, notice: "Approved โ€” don't forget to deliver it!" + end + end +end diff --git a/app/controllers/family/rewards_controller.rb b/app/controllers/family/rewards_controller.rb new file mode 100644 index 0000000..578552b --- /dev/null +++ b/app/controllers/family/rewards_controller.rb @@ -0,0 +1,48 @@ +module Family + class RewardsController < BaseController + before_action :set_reward, only: %i[edit update destroy] + + def index + @rewards = current_parent.rewards.order(:cost) + end + + def new + @reward = current_parent.rewards.new + end + + def create + @reward = current_parent.rewards.new(reward_params) + if @reward.save + redirect_to family_rewards_path, notice: "Reward added." + else + render :new, status: :unprocessable_entity + end + end + + def edit + end + + def update + if @reward.update(reward_params) + redirect_to family_rewards_path, notice: "Saved." + else + render :edit, status: :unprocessable_entity + end + end + + def destroy + @reward.destroy + redirect_to family_rewards_path, notice: "Removed." + end + + private + + def set_reward + @reward = current_parent.rewards.find(params[:id]) + end + + def reward_params + params.require(:reward).permit(:title, :emoji, :cost) + end + end +end diff --git a/app/controllers/kids_controller.rb b/app/controllers/kids_controller.rb deleted file mode 100644 index 63b032a..0000000 --- a/app/controllers/kids_controller.rb +++ /dev/null @@ -1,195 +0,0 @@ -class KidsController < ApplicationController - - def login - @user = current_user - @show_navbar = true - end - def create_login - @user = current_user - @kids = @user.kids - password_check = false - @kids.each do |kid| - if login_params[:password] == kid.password - password_check = true - @kid = kid - end - end - if password_check - redirect_to kid_dashboard_path(@kid) - else - render :login - end - end - - def kid_dashboard - @kid = Kid.find(params[:id]) - @books = Book.all - @chosen_books = [] - unless @kid.interests.nil? - @kid.interests.each do |interest| - @chosen_books = Book.where(genre: interest).where("min_age < ? AND max_age > ?", @kid.age, @kid.age) - end - end - end - - def index - @user = current_user - @kids = @user.kids - end - - def show - @kid = Kid.find(params[:id]) - end - - def new - @new_kid = Kid.new - @categories = ["Adventure", - "Animals", - "Comedy", - "Comic Books", - "Culture", - "Drama", - "Ecology", - "Education", - "Entertainment", - "Environment", - "Fairy Tale", - "Family", - "Fantasy", - "Fiction", - "Food", - "Friendship", - "Futuristic", - "Game Books", - "Holiday", - "Horror", - "Humor", - "Literature", - "Magic", - "Manga", - "Movies", - "Music", - "Mystery", - "Mythology", - "Pets", - "Photography", - "Picture", - "Poetry", - "Romance", - "Science Fiction", - "Sports", - "Superhero", - "Supernatural", - "Suspense", - "Technology", - "Thriller", - "Travel", - "Western", - ] - end - - def create - @user = current_user - @kid = Kid.new(kid_params) - @kid.parent_id = @user[:id] - if @kid.save - redirect_to kids_path - else - render :new - end - end - -def edit - @kid = Kid.find(params[:id]) - @categories = ["Adventure", - "Animals", - "Comedy", - "Comic Books", - "Culture", - "Drama", - "Ecology", - "Education", - "Entertainment", - "Environment", - "Fairy Tale", - "Family", - "Fantasy", - "Fiction", - "Food", - "Friendship", - "Futuristic", - "Game Books", - "Holiday", - "Horror", - "Humor", - "Literature", - "Magic", - "Manga", - "Movies", - "Music", - "Mystery", - "Mythology", - "Pets", - "Photography", - "Picture", - "Poetry", - "Romance", - "Science Fiction", - "Sports", - "Superhero", - "Supernatural", - "Suspense", - "Technology", - "Thriller", - "Travel", - "Western", - ] -end - -def update - @kid = Kid.find(params[:id]) - @kid.update(kid_params) - if @kid.save! - redirect_to kids_path - else - render :edit - end -end - -def destroy - @kid = Kid.find(params[:id]) - @kid.destroy - redirect_to kids_path -end - -def create_avatar - @kid = Kid.find(params[:kid_id]) -end - -# def add_RDC -# @kid = Kid.find(params[:kid_id]) -# @user = current_user -# if @user.wallet.to_i >= params[:amount].to_i -# @user.wallet -= Money.new((params[:amount].to_i * 100), 'USD') -# @kid.wallet += params[:amount].to_i -# @user.save! -# @kid.save! -# flash[:notice] = "Just added #{params[:amount].to_i} RDC" -# redirect_to @kid -# else -# flash[:notice] = "Not enough RDC please deposit..." -# redirect_to @kid -# end -# end - -private - - def kid_params - params.require(:kid).permit(:name, :parent_id, :password, :age, :avatar, interests: []) - end - - def login_params - params.require('/login').permit(:password) - end - -end diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb deleted file mode 100644 index b3f2c95..0000000 --- a/app/controllers/orders_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -class OrdersController < ApplicationController - def show - @order = current_user.orders.where(state: 'paid').find(params[:id]) - end - def create - @order = Order.create!( amount: params[:amount], state: 'pending', user: current_user) - - redirect_to new_order_payment_path(@order) - end -end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index a86b51c..6a68192 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -1,9 +1,6 @@ class PagesController < ApplicationController - skip_before_action :authenticate_user!, only: [:home] + allow_unauthenticated_access def home - @show_navbar = true - end - def login end end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 0000000..f95ec78 --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,35 @@ +class PasswordsController < ApplicationController + allow_unauthenticated_access + before_action :set_user_by_token, only: %i[ edit update ] + rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." } + + def new + end + + def create + if user = User.find_by(email_address: params[:email_address]) + PasswordsMailer.reset(user).deliver_later + end + + redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)." + end + + def edit + end + + def update + if @user.update(params.permit(:password, :password_confirmation)) + @user.sessions.destroy_all + redirect_to new_session_path, notice: "Password has been reset." + else + redirect_to edit_password_path(params[:token]), alert: "Passwords did not match." + end + end + + private + def set_user_by_token + @user = User.find_by_password_reset_token!(params[:token]) + rescue ActiveSupport::MessageVerifier::InvalidSignature + redirect_to new_password_path, alert: "Password reset link is invalid or has expired." + end +end diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb deleted file mode 100644 index 29975a1..0000000 --- a/app/controllers/payments_controller.rb +++ /dev/null @@ -1,40 +0,0 @@ -class PaymentsController < ApplicationController - before_action :set_order - - def new - end - - def create - customer = Stripe::Customer.create( - source: params[:stripeToken], - email: params[:stripeEmail] - ) - - charge = Stripe::Charge.create( - customer: customer.id, - amount: @order.amount_cents, - description: "Payment of #{@order.amount} in RDC for order #{@order.id}", - currency: @order.amount.currency - ) - - @order.update(payment: charge.to_json, state: 'paid') - @user = current_user - @user.wallet += @order.amount - @user.save! - redirect_to order_path(@order) - - rescue Stripe::CardError => e - flash[:alert] = e.message - redirect_to new_order_payment_path(@order) - - raise - - - end - -private - - def set_order - @order = current_user.orders.where(state: 'pending').find(params[:order_id]) - end -end diff --git a/app/controllers/play/base_controller.rb b/app/controllers/play/base_controller.rb new file mode 100644 index 0000000..6367e3a --- /dev/null +++ b/app/controllers/play/base_controller.rb @@ -0,0 +1,19 @@ +module Play + class BaseController < ApplicationController + allow_unauthenticated_access + layout "play" + + before_action :require_kid! + + private + + def current_kid + @current_kid ||= Kid.find_by(id: session[:kid_id]) + end + helper_method :current_kid + + def require_kid! + redirect_to play_root_path unless current_kid + end + end +end diff --git a/app/controllers/play/books_controller.rb b/app/controllers/play/books_controller.rb new file mode 100644 index 0000000..446e9ab --- /dev/null +++ b/app/controllers/play/books_controller.rb @@ -0,0 +1,51 @@ +module Play + class BooksController < BaseController + before_action :set_book, except: :index + + def index + @books = Book.for_age(current_kid.age || 8).order(:title).includes(:quiz) + @readings = current_kid.readings.index_by(&:book_id) + end + + def show + @reading = current_kid.readings.find_by(book: @book) + end + + def start + current_kid.readings.find_or_create_by!(book: @book) + redirect_to read_play_book_path(@book) + end + + def read + @reading = current_kid.readings.find_by(book: @book) + redirect_to play_book_path(@book) unless @reading + end + + def quiz + @reading = current_kid.readings.find_by(book: @book) + @quiz = @book.quiz + if @reading.nil? + redirect_to play_book_path(@book) + elsif @quiz.nil? || @quiz.questions.blank? + redirect_to play_book_path(@book), alert: "This book has no quiz yet โ€” ask a grown-up to add one!" + end + end + + def submit_quiz + reading = current_kid.readings.find_by(book: @book) + quiz = @book.quiz + return redirect_to(play_book_path(@book)) unless reading && quiz + + already_finished = reading.finished? + score = quiz.grade(params[:answers]&.to_unsafe_h) + reading.finish_with_score!(score) + redirect_to play_book_path(@book, score:, earned: (!already_finished && reading.finished?) ? reading.coins_earned : nil) + end + + private + + def set_book + @book = Book.find(params[:id]) + end + end +end diff --git a/app/controllers/play/dashboards_controller.rb b/app/controllers/play/dashboards_controller.rb new file mode 100644 index 0000000..6900e38 --- /dev/null +++ b/app/controllers/play/dashboards_controller.rb @@ -0,0 +1,9 @@ +module Play + class DashboardsController < BaseController + def show + @in_progress = current_kid.readings.in_progress.includes(:book) + @finished_count = current_kid.finished_count + @next_books = Book.for_age(current_kid.age || 8).where.not(id: current_kid.book_ids).limit(4) + end + end +end diff --git a/app/controllers/play/sessions_controller.rb b/app/controllers/play/sessions_controller.rb new file mode 100644 index 0000000..5a654d4 --- /dev/null +++ b/app/controllers/play/sessions_controller.rb @@ -0,0 +1,55 @@ +module Play + class SessionsController < BaseController + skip_before_action :require_kid! + rate_limit to: 10, within: 1.minute, only: :create, with: -> { redirect_to play_root_path, alert: "Too many tries โ€” take a break!" } + + # Step 1: pick the family (or reuse the signed-in parent / remembered family) + def new + @family = current_family + @kids = @family&.kids + end + + def pick_family + family = User.find_by(email_address: params[:email_address].to_s.strip.downcase) + if family + session[:play_family_id] = family.id + redirect_to play_root_path + else + redirect_to play_root_path, alert: "We couldn't find that family email." + end + end + + # Step 2: PIN pad for the chosen kid + def pin + @kid = current_family&.kids&.find(params[:id]) + redirect_to play_root_path unless @kid + end + + def create + kid = current_family&.kids&.find(params[:id]) + if kid&.authenticate_pin(params[:pin].to_s) + session[:kid_id] = kid.id + redirect_to play_dashboard_path + else + redirect_to play_pin_path(params[:id]), alert: "Wrong PIN โ€” try again!" + end + end + + def destroy + session.delete(:kid_id) + redirect_to play_root_path + end + + private + + def current_family + @current_family ||= + if authenticated? + Current.session.user + elsif session[:play_family_id] + User.find_by(id: session[:play_family_id]) + end + end + helper_method :current_family + end +end diff --git a/app/controllers/play/shop_controller.rb b/app/controllers/play/shop_controller.rb new file mode 100644 index 0000000..806133b --- /dev/null +++ b/app/controllers/play/shop_controller.rb @@ -0,0 +1,16 @@ +module Play + class ShopController < BaseController + def index + @rewards = current_kid.parent.rewards.order(:cost) + @redemptions = current_kid.redemptions.order(created_at: :desc).includes(:reward) + end + + def redeem + reward = current_kid.parent.rewards.find(params[:reward_id]) + Redemption.redeem!(kid: current_kid, reward:) + redirect_to play_shop_path, notice: "You got it! A grown-up will make it happen. ๐ŸŽ‰" + rescue Redemption::InsufficientCoins + redirect_to play_shop_path, alert: "Not enough coins yet โ€” keep reading!" + end + end +end diff --git a/app/controllers/prizes_controller.rb b/app/controllers/prizes_controller.rb deleted file mode 100644 index fe1a409..0000000 --- a/app/controllers/prizes_controller.rb +++ /dev/null @@ -1,13 +0,0 @@ -class PrizesController < ApplicationController - def index - @prizes = Prize.all - @kid = Kid.find(params[:kid_id]) - @kid_balance = Kid.find(params[:kid_id]).wallet - end - - def show - @prize = Prize.find(params[:id]) - @kid = Kid.find(params[:kid_id]) - end - -end diff --git a/app/controllers/purchases_controller.rb b/app/controllers/purchases_controller.rb deleted file mode 100644 index 37b588d..0000000 --- a/app/controllers/purchases_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -class PurchasesController < ApplicationController - - def show - @purchase = Purchase.find(params[:id]) - @kid = Kid.find(params[:kid_id]) - end - - def create - @purchase = Purchase.new - @kid = Kid.find(params[:kid_id]) - @prize = Prize.find(params[:prize_id]) - @kid.wallet -= @prize.price - @kid.save! - @purchase.kid = @kid - @purchase.prize = @prize - if @purchase.save! - redirect_to kid_prize_purchase_path(@kid, @prize, @purchase) - else - redirect_to kid_prizes_path(@kid, @prize) - end - end -end diff --git a/app/controllers/readings_controller.rb b/app/controllers/readings_controller.rb deleted file mode 100644 index 00e5c94..0000000 --- a/app/controllers/readings_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -class ReadingsController < ApplicationController - def show - @reading = Reading.find(params[:id]) - @book = Book.find(params[:book_id]) - @kid = Kid.find(params[:kid_id]) - end - - def reader - end - - def create - @reading = Reading.new - @kid = Kid.find(params[:kid_id]) - @book = Book.find(params[:book_id]) - @reading.kid = @kid - @reading.book = @book - if @reading.save - redirect_to kid_book_reading_path(@kid, @book, @reading) - else - redirect_to kid_books_path(@kid) - end - end -end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb new file mode 100644 index 0000000..0804994 --- /dev/null +++ b/app/controllers/registrations_controller.rb @@ -0,0 +1,23 @@ +class RegistrationsController < ApplicationController + allow_unauthenticated_access + + def new + @user = User.new + end + + def create + @user = User.new(user_params) + if @user.save + start_new_session_for @user + redirect_to family_root_path, notice: "Welcome to ReadCoin!" + else + render :new, status: :unprocessable_entity + end + end + + private + + def user_params + params.require(:user).permit(:email_address, :password, :password_confirmation) + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..cf7fccd --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,21 @@ +class SessionsController < ApplicationController + allow_unauthenticated_access only: %i[ new create ] + rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." } + + def new + end + + def create + if user = User.authenticate_by(params.permit(:email_address, :password)) + start_new_session_for user + redirect_to after_authentication_url + else + redirect_to new_session_path, alert: "Try another email address or password." + end + end + + def destroy + terminate_session + redirect_to new_session_path, status: :see_other + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000..0d7b494 --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1,3 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "@hotwired/turbo-rails" +import "controllers" diff --git a/app/javascript/components/select2.js b/app/javascript/components/select2.js deleted file mode 100644 index 1233817..0000000 --- a/app/javascript/components/select2.js +++ /dev/null @@ -1,6 +0,0 @@ -import $ from 'jquery'; -import 'select2'; - -$('.select2').select2(); - -import 'select2/dist/css/select2.css'; diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 0000000..1213e85 --- /dev/null +++ b/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/app/javascript/controllers/hello_controller.js b/app/javascript/controllers/hello_controller.js new file mode 100644 index 0000000..5975c07 --- /dev/null +++ b/app/javascript/controllers/hello_controller.js @@ -0,0 +1,7 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + this.element.textContent = "Hello World!" + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 0000000..1156bf8 --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,4 @@ +// Import and register all your controllers from the importmap via controllers/**/*_controller +import { application } from "controllers/application" +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" +eagerLoadControllersFrom("controllers", application) diff --git a/app/javascript/packs/a.js b/app/javascript/packs/a.js deleted file mode 100644 index 260a8bc..0000000 --- a/app/javascript/packs/a.js +++ /dev/null @@ -1,16 +0,0 @@ - -import swal from 'sweetalert2'; - -function showSwal(){ - const iframe = document.querySelector('.bibi-frame').contentWindow.document - const current = iframe.getElementById("bibi-nombre-percent").innerHTML - - if (current.includes("100")) { - swal({ - title: 'Good job!', - text: 'You finished the book, go and answer the quiz!', - type: 'success', - }) -} -} -setInterval(showSwal, 5000); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js deleted file mode 100644 index cb36420..0000000 --- a/app/javascript/packs/application.js +++ /dev/null @@ -1,2 +0,0 @@ -import "bootstrap"; -import '../components/select2'; diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index a009ace..d394c3d 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,2 +1,7 @@ class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 286b223..3c34c81 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,4 @@ class ApplicationMailer < ActionMailer::Base - default from: 'from@example.com' - layout 'mailer' + default from: "from@example.com" + layout "mailer" end diff --git a/app/mailers/passwords_mailer.rb b/app/mailers/passwords_mailer.rb new file mode 100644 index 0000000..4f0ac7f --- /dev/null +++ b/app/mailers/passwords_mailer.rb @@ -0,0 +1,6 @@ +class PasswordsMailer < ApplicationMailer + def reset(user) + @user = user + mail subject: "Reset your password", to: user.email_address + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 10a4cba..b63caeb 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,3 +1,3 @@ class ApplicationRecord < ActiveRecord::Base - self.abstract_class = true + primary_abstract_class end diff --git a/app/models/book.rb b/app/models/book.rb index a1d4c38..5ac1822 100644 --- a/app/models/book.rb +++ b/app/models/book.rb @@ -1,10 +1,10 @@ class Book < ApplicationRecord + has_one :quiz, dependent: :destroy has_many :readings, dependent: :destroy has_many :kids, through: :readings - validates :title, presence: true - validates :url, presence: true - validates :genre, presence: true - validates :reward, presence: true - serialize :quiz - mount_uploader :photo, PhotoUploader + + validates :title, :author, :genre, :source_url, presence: true + validates :reward, numericality: { greater_than: 0 } + + scope :for_age, ->(age) { where("min_age IS NULL OR min_age <= ?", age).where("max_age IS NULL OR max_age >= ?", age) } end diff --git a/app/models/current.rb b/app/models/current.rb new file mode 100644 index 0000000..2bef56d --- /dev/null +++ b/app/models/current.rb @@ -0,0 +1,4 @@ +class Current < ActiveSupport::CurrentAttributes + attribute :session + delegate :user, to: :session, allow_nil: true +end diff --git a/app/models/kid.rb b/app/models/kid.rb index 00c45fc..33c663d 100644 --- a/app/models/kid.rb +++ b/app/models/kid.rb @@ -1,12 +1,34 @@ class Kid < ApplicationRecord - belongs_to :parent, class_name: 'User' + AVATARS = %w[๐Ÿฆ ๐Ÿฏ ๐ŸฆŠ ๐Ÿผ ๐Ÿธ ๐Ÿฆ„ ๐Ÿ™ ๐Ÿฆ– ๐Ÿง ๐Ÿฆ‰ ๐Ÿณ ๐Ÿš€].freeze + + belongs_to :parent, class_name: "User", inverse_of: :kids has_many :readings, dependent: :destroy has_many :books, through: :readings - has_many :purchases, dependent: :destroy - has_many :prizes, through: :purchases - validates :parent_id, presence: true + has_many :redemptions, dependent: :destroy + + has_secure_password :pin, validations: false + validates :name, presence: true - validates :password, presence: true - mount_uploader :avatar, PhotoUploader - serialize :interests + validates :avatar, inclusion: { in: AVATARS } + validates :pin, format: { with: /\A\d{4}\z/, message: "must be 4 digits" }, allow_nil: true + + def finished_readings = readings.finished + def finished_count = finished_readings.count + + # Called when a kid finishes a book: extends the streak if they also read + # yesterday, keeps it if they already read today, otherwise restarts at 1. + def record_reading_day!(date = Date.current) + self.streak = + if last_read_on == date then [ streak, 1 ].max + elsif last_read_on == date - 1 then streak + 1 + else 1 + end + self.last_read_on = date + save! + end + + def current_streak + return 0 if last_read_on.nil? || last_read_on < Date.current - 1 + streak + end end diff --git a/app/models/order.rb b/app/models/order.rb deleted file mode 100644 index 07b6930..0000000 --- a/app/models/order.rb +++ /dev/null @@ -1,4 +0,0 @@ -class Order < ApplicationRecord - belongs_to :user - monetize :amount_cents -end diff --git a/app/models/prize.rb b/app/models/prize.rb deleted file mode 100644 index 4ca9354..0000000 --- a/app/models/prize.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Prize < ApplicationRecord - has_many :purchases, dependent: :destroy - has_many :kids, through: :purchases - validates :title, presence: true - validates :price, presence: true - mount_uploader :picture, PhotoUploader -end diff --git a/app/models/purchase.rb b/app/models/purchase.rb deleted file mode 100644 index 3ce664b..0000000 --- a/app/models/purchase.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Purchase < ApplicationRecord - belongs_to :prize - belongs_to :kid - validates :kid_id, presence: true - validates :prize_id, presence: true -end diff --git a/app/models/quiz.rb b/app/models/quiz.rb new file mode 100644 index 0000000..95827e3 --- /dev/null +++ b/app/models/quiz.rb @@ -0,0 +1,26 @@ +# Questions are stored as JSON: [{ "question" => "...", "choices" => ["a", "b", "c", "d"], "answer" => 2 }, ...] +# The answer index never leaves the server โ€” grading happens in #grade. +class Quiz < ApplicationRecord + belongs_to :book + + validate :questions_are_well_formed + + def grade(submitted) + submitted ||= {} + correct = questions.each_with_index.count do |q, i| + submitted[i.to_s].to_i == q["answer"].to_i + end + (correct * 100.0 / questions.size).round + end + + private + + def questions_are_well_formed + unless questions.is_a?(Array) && questions.any? && questions.all? { |q| + q.is_a?(Hash) && q["question"].present? && q["choices"].is_a?(Array) && q["choices"].size >= 2 && + q["answer"].to_i.between?(0, q["choices"].size - 1) + } + errors.add(:questions, "must be a non-empty list of {question, choices, answer}") + end + end +end diff --git a/app/models/reading.rb b/app/models/reading.rb index c0c8b0d..e353f6f 100644 --- a/app/models/reading.rb +++ b/app/models/reading.rb @@ -1,6 +1,29 @@ class Reading < ApplicationRecord + PASS_SCORE = 80 + belongs_to :kid belongs_to :book - validates :kid_id, presence: true - validates :book_id, presence: true + + scope :in_progress, -> { where(status: "reading") } + scope :finished, -> { where(status: "finished") } + + validates :kid_id, uniqueness: { scope: :book_id } + + # Grades a quiz submission exactly once. Re-submitting a passed quiz never re-credits coins. + def finish_with_score!(score) + with_lock do + return if finished? + + self.quiz_score = [ score, quiz_score.to_i ].max + if score >= PASS_SCORE + self.status = "finished" + self.coins_earned = book.reward + kid.coins += book.reward + kid.record_reading_day! + end + save! + end + end + + def finished? = status == "finished" end diff --git a/app/models/redemption.rb b/app/models/redemption.rb new file mode 100644 index 0000000..97562ee --- /dev/null +++ b/app/models/redemption.rb @@ -0,0 +1,20 @@ +class Redemption < ApplicationRecord + belongs_to :kid + belongs_to :reward + + scope :pending, -> { where(status: "pending") } + + class InsufficientCoins < StandardError; end + + # Deducts coins and creates the redemption atomically; raises if the kid can't afford it. + def self.redeem!(kid:, reward:) + kid.with_lock do + raise InsufficientCoins if kid.coins < reward.cost + + kid.decrement!(:coins, reward.cost) + create!(kid:, reward:) + end + end + + def approve! = update!(status: "approved") +end diff --git a/app/models/reward.rb b/app/models/reward.rb new file mode 100644 index 0000000..0c2a7c1 --- /dev/null +++ b/app/models/reward.rb @@ -0,0 +1,7 @@ +class Reward < ApplicationRecord + belongs_to :parent, class_name: "User", inverse_of: :rewards + has_many :redemptions, dependent: :destroy + + validates :title, presence: true + validates :cost, numericality: { greater_than: 0 } +end diff --git a/app/models/session.rb b/app/models/session.rb new file mode 100644 index 0000000..cf376fb --- /dev/null +++ b/app/models/session.rb @@ -0,0 +1,3 @@ +class Session < ApplicationRecord + belongs_to :user +end diff --git a/app/models/user.rb b/app/models/user.rb index a379cd6..82731b7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,9 +1,8 @@ class User < ApplicationRecord - # Include default devise modules. Others available are: - # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable - has_many :orders - devise :database_authenticatable, :registerable, - :recoverable, :rememberable, :validatable - has_many :kids, foreign_key: :parent_id, dependent: :destroy - monetize :wallet_cents + has_secure_password + has_many :sessions, dependent: :destroy + has_many :kids, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy + has_many :rewards, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy + + normalizes :email_address, with: ->(e) { e.strip.downcase } end diff --git a/app/services/quiz_generator.rb b/app/services/quiz_generator.rb new file mode 100644 index 0000000..8e95dfb --- /dev/null +++ b/app/services/quiz_generator.rb @@ -0,0 +1,73 @@ +# Generates an age-appropriate comprehension quiz for a book via the Claude API. +# Returns an array of question hashes matching Quiz#questions, or raises +# QuizGenerator::Error with a human-readable message. +class QuizGenerator + MODEL = :"claude-opus-5" + QUESTION_COUNT = 5 + + SCHEMA = { + type: "object", + additionalProperties: false, + required: [ "questions" ], + properties: { + questions: { + type: "array", + items: { + type: "object", + additionalProperties: false, + required: [ "question", "choices", "answer" ], + properties: { + question: { type: "string" }, + choices: { type: "array", items: { type: "string" } }, + answer: { type: "integer", description: "0-based index of the correct choice" } + } + } + } + } + }.freeze + + class Error < StandardError; end + + def self.available? = ENV["ANTHROPIC_API_KEY"].present? + + def self.generate(book) + raise Error, "Set ANTHROPIC_API_KEY to enable AI quiz generation." unless available? + + client = Anthropic::Client.new + message = client.messages.create( + model: MODEL, + max_tokens: 4096, + output_config: { format: { type: "json_schema", schema: SCHEMA } }, + messages: [ { role: "user", content: prompt(book) } ] + ) + raise Error, "The model declined to generate a quiz for this book." if message.stop_reason == :refusal + + text = message.content.find { |block| block.type == :text }&.text + raise Error, "Empty response from the model." if text.blank? + + JSON.parse(text).fetch("questions") + rescue Anthropic::Errors::APIStatusError => e + raise Error, "Claude API error (#{e.type}): #{e.message.to_s.truncate(200)}" + rescue JSON::ParserError, KeyError + raise Error, "The model returned an unexpected format โ€” try again." + end + + def self.prompt(book) + ages = [ book.min_age, book.max_age ].compact.join("โ€“") + <<~PROMPT + Write a reading-comprehension quiz for a child who just finished this book: + + Title: #{book.title} + Author: #{book.author} + Genre: #{book.genre} + #{"Reader age: #{ages}" if ages.present?} + #{"Summary: #{book.summary}" if book.summary.present?} + + Create exactly #{QUESTION_COUNT} multiple-choice questions with 4 choices each. + The questions must test whether the child actually read and understood the story โ€” + plot events, characters, and cause-and-effect โ€” not trivia about the author. + Use simple, friendly language appropriate for the reader's age. Wrong choices should + be plausible to someone who didn't read the book, but clearly wrong to someone who did. + PROMPT + end +end diff --git a/app/uploaders/photo_uploader.rb b/app/uploaders/photo_uploader.rb deleted file mode 100644 index a700b37..0000000 --- a/app/uploaders/photo_uploader.rb +++ /dev/null @@ -1,3 +0,0 @@ -class PhotoUploader < CarrierWave::Uploader::Base - include Cloudinary::CarrierWave -end diff --git a/app/views/admin/books/edit.html.erb b/app/views/admin/books/edit.html.erb deleted file mode 100644 index 20fbcf4..0000000 --- a/app/views/admin/books/edit.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -

Edit a Book

-<%= simple_form_for (@book), url: admin_book_path do |f| %> - <%= f.input :title %> - <%= f.input :author %> - <%= f.input :summary %> - <%= f.input :url %> - <%= f.input :quiz %> - <%= f.input :reward %> - <%= f.input :genre, collection: @categories %> - <%= f.input :min_age %> - <%= f.input :max_age %> - <%= f.input :photo %> - <%= f.input :photo_cache, as: :hidden %> - <%= f.submit %> -<% end %> diff --git a/app/views/admin/books/index.html.erb b/app/views/admin/books/index.html.erb deleted file mode 100644 index 34f2a28..0000000 --- a/app/views/admin/books/index.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -

admin books index

- -
- <% @books.each do |book| %> -
-

<%= cl_image_tag book.photo, height: 100, width: 80, crop: :fill %>

-

<%= book.title %>

-

<%= book.author %>

-

<%= book.reward %> $rcn

-

<%= link_to "delete", admin_book_path(book), - method: :delete, - data: { confirm: "are you sure?" } %>

-

<%= link_to "update", edit_admin_book_path(book), - data: { confirm: "are you sure?" } %>

-
- <% end %> -
-โ€‹ -<%= link_to "Add a new Book", new_admin_book_path, class:"parent-button" %> diff --git a/app/views/admin/books/new.html.erb b/app/views/admin/books/new.html.erb deleted file mode 100644 index 8840f76..0000000 --- a/app/views/admin/books/new.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -

Add a new Book

-<%= simple_form_for (@book), url: admin_books_path do |f| %> - <%= f.input :title %> - <%= f.input :author %> - <%= f.input :summary %> - <%= f.input :url %> - <%= f.input :quiz %> - <%= f.input :reward %> - <%= f.input :genre, collection: @categories %> - <%= f.input :min_age %> - <%= f.input :max_age %> - <%= f.input :photo %> - <%= f.input :photo_cache, as: :hidden %> - <%= f.submit %> -<% end %> diff --git a/app/views/admin/prizes/edit.html.erb b/app/views/admin/prizes/edit.html.erb deleted file mode 100644 index 9966da8..0000000 --- a/app/views/admin/prizes/edit.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<%= simple_form_for @prize, url: admin_prize_path do |f| %> - <%= f.hidden_field :prize, { :value => @prize } %> - <%= f.input :title %> - <%= f.input :price %> - <%= f.input :picture %> - <%= f.input :picture_cache, as: :hidden %> - <%= f.submit 'update prize', class: 'btn btn-primary form-group' %> -<% end %> diff --git a/app/views/admin/prizes/index.html.erb b/app/views/admin/prizes/index.html.erb deleted file mode 100644 index c18370c..0000000 --- a/app/views/admin/prizes/index.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -

admin prize index

-
- <% @prizes.each do |prize| %> -
-

<%= cl_image_tag prize.picture, height: 100, width: 80, crop: :fill %>

-

<%= prize.title %>

-

<%= prize.price %> $rcn

-

<%= link_to "delete", admin_prize_path(prize), - method: :delete, - data: { confirm: "are you sure?" } %> <%= link_to "update", edit_admin_prize_path(prize) %>

-
- <% end %> -
- -<%= link_to "add a prize", new_admin_prize_path %> - diff --git a/app/views/admin/prizes/new.html.erb b/app/views/admin/prizes/new.html.erb deleted file mode 100644 index ecac5d9..0000000 --- a/app/views/admin/prizes/new.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<%= simple_form_for @prize, url: admin_prizes_path do |f| %> - <%= f.input :title %> - <%= f.input :price %> - <%= f.input :picture %> - <%= f.input :picture_cache, as: :hidden %> - <%= f.submit 'add prize', class: 'btn btn-primary form-group' %> -<% end %> diff --git a/app/views/books/answers_quiz.html.erb b/app/views/books/answers_quiz.html.erb deleted file mode 100644 index f1f73ad..0000000 --- a/app/views/books/answers_quiz.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -
-
-
- <% if @score == "100"%> -

Congratulations!

-

All the answer are corrects, you just won:

-

<%= image_tag 'coins.png', width: 50, height: 50 %> <%= @book.reward %> RDC

- - <% else %> -

Try Again!

-

You have <%= @score %>% of correct answers

- <%= link_to "Back to book", kid_book_path(@kid, @book), class: "kid-button" %> - <%= link_to "Back to quiz", kid_book_quiz_path(@kid, @book), class: "kid-button" %> - <% end %> -
-
-
diff --git a/app/views/books/quiz.html.erb b/app/views/books/quiz.html.erb deleted file mode 100644 index 44394d5..0000000 --- a/app/views/books/quiz.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -
-
- -

QUIZ on <%= @book.title %>

-
-
- <%= simple_form_for "quiz", url: kid_book_check_quiz_path(@kid, @book) do |f| %> - <% @book.quiz.each do |q , a| %> - <%= f.input q, label: q, collection: a, as: :select, prompt: 'Choose your answer'%> - <% end %> - - <%= link_to "back to book", kid_book_path(@kid, @book), class: "kid-button" %> <%= f.submit "Submit", class: "kid-focus-button" %> -
-
- <% end %> -
-
diff --git a/app/views/books/show.html.erb b/app/views/books/show.html.erb deleted file mode 100644 index 82bbbb2..0000000 --- a/app/views/books/show.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -
-
- <%= cl_image_tag @book.photo, crop: :fill, id: "book-img" %> - -
-

<%= @book.title %>

-

Author: <%= @book.author%>

-

<%= @book.summary %>

- -
-
-
diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb deleted file mode 100644 index f7b4a65..0000000 --- a/app/views/devise/confirmations/new.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -

Resend confirmation instructions

- -<%= simple_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> - <%= f.error_notification %> - <%= f.full_error :confirmation_token %> - -
- <%= f.input :email, - required: true, - autofocus: true, - value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email), - input_html: { autocomplete: "email" } %> -
- -
- <%= f.button :submit, "Resend confirmation instructions" %> -
-<% end %> - -<%= render "devise/shared/links" %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb deleted file mode 100644 index dc55f64..0000000 --- a/app/views/devise/mailer/confirmation_instructions.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -

Welcome <%= @email %>!

- -

You can confirm your account email through the link below:

- -

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/app/views/devise/mailer/email_changed.html.erb b/app/views/devise/mailer/email_changed.html.erb deleted file mode 100644 index 32f4ba8..0000000 --- a/app/views/devise/mailer/email_changed.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -

Hello <%= @email %>!

- -<% if @resource.try(:unconfirmed_email?) %> -

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

-<% else %> -

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

-<% end %> diff --git a/app/views/devise/mailer/password_change.html.erb b/app/views/devise/mailer/password_change.html.erb deleted file mode 100644 index b41daf4..0000000 --- a/app/views/devise/mailer/password_change.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -

Hello <%= @resource.email %>!

- -

We're contacting you to notify you that your password has been changed.

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb deleted file mode 100644 index f667dc1..0000000 --- a/app/views/devise/mailer/reset_password_instructions.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -

Hello <%= @resource.email %>!

- -

Someone has requested a link to change your password. You can do this through the link below.

- -

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

- -

If you didn't request this, please ignore this email.

-

Your password won't change until you access the link above and create a new one.

diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb deleted file mode 100644 index 41e148b..0000000 --- a/app/views/devise/mailer/unlock_instructions.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -

Hello <%= @resource.email %>!

- -

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

- -

Click the link below to unlock your account:

- -

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb deleted file mode 100644 index b43dc15..0000000 --- a/app/views/devise/passwords/edit.html.erb +++ /dev/null @@ -1,24 +0,0 @@ -

Change your password

- -<%= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> - <%= f.error_notification %> - - <%= f.input :reset_password_token, as: :hidden %> - <%= f.full_error :reset_password_token %> - -
- <%= f.input :password, - label: "New password", - required: true, - autofocus: true, - hint: ("#{@minimum_password_length} characters minimum" if @minimum_password_length), - input_html: { autocomplete: "new-password" } %> - <%= f.input :password_confirmation, label: "Confirm your new password", required: true %> -
- -
- <%= f.button :submit, "Change my password" %> -
-<% end %> - -<%= render "devise/shared/links" %> diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb deleted file mode 100644 index 01ce0b8..0000000 --- a/app/views/devise/passwords/new.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -

Forgot your password?

- -<%= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> - <%= f.error_notification %> - -
- <%= f.input :email, - required: true, - autofocus: true, - input_html: { autocomplete: "email" } %> -
- -
- <%= f.button :submit, "Send me reset password instructions" %> -
-<% end %> - -<%= render "devise/shared/links" %> diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb deleted file mode 100644 index 54fbc6c..0000000 --- a/app/views/devise/registrations/edit.html.erb +++ /dev/null @@ -1,35 +0,0 @@ -

Edit <%= resource_name.to_s.humanize %>

- -<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> - <%= f.error_notification %> - -
- <%= f.input :email, required: true, autofocus: true %> - - <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> -

Currently waiting confirmation for: <%= resource.unconfirmed_email %>

- <% end %> - - <%= f.input :password, - hint: "leave it blank if you don't want to change it", - required: false - input_html: { autocomplete: "new-password" } %> - <%= f.input :password_confirmation, - required: false, - input_html: { autocomplete: "new-password" } %> - <%= f.input :current_password, - hint: "we need your current password to confirm your changes", - required: true, - input_html: { autocomplete: "current-password" } %> -
- -
- <%= f.button :submit, "Update" %> -
-<% end %> - -

Cancel my account

- -

Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>

- -<%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb deleted file mode 100644 index 4d52691..0000000 --- a/app/views/devise/registrations/new.html.erb +++ /dev/null @@ -1,38 +0,0 @@ -
-
-
-

Sign up

- - <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> - <%= f.error_notification %> - -
- <%= f.input :username, - required: true, - autofocus: true , - input_html: { autocomplete: "email" }%> - <%= f.input :email, - required: true, - autofocus: true , - input_html: { autocomplete: "email" }%> - <%= f.input :password, - required: true, - hint: ("#{@minimum_password_length} characters minimum" if @minimum_password_length), - input_html: { autocomplete: "new-password" } %> - <%= f.input :password_confirmation, - required: true, - input_html: { autocomplete: "new-password" } %> -
- -
- <%= f.button :submit, "Sign up", class:"btn btn-lg btn-primary" %> -
- <% end %> -
-
-
- - - - - diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb deleted file mode 100644 index f2348f0..0000000 --- a/app/views/devise/sessions/new.html.erb +++ /dev/null @@ -1,37 +0,0 @@ -
-
-
- - -

Parents, enter your login below to access your dashboard and unlock your kids login!

- - <%= image_tag 'kid-reading-with-parents.jpg', class: "login-avatar parent-avatar" %> - - <%= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> -

- <%= f.input :email, - required: false, - autofocus: true, - input_html: { autocomplete: "email" } %> - <%= f.input :password, - required: false, - input_html: { autocomplete: "current-password" } %> - <%= f.input :remember_me, as: :boolean if devise_mapping.rememberable? %> -
- -
- <%= f.button :submit, "Log in", class: "kid-button" %> -
- <% end %> - - <%= render "devise/shared/links" %> -
-
-
-
- -

Ask your parents to login, then you can access your ebooks and discover your eshop!

- <%= image_tag 'kids_on_roof_reading_book.jpg', class: "login-avatar" %> -
-
-
diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb deleted file mode 100644 index df399ca..0000000 --- a/app/views/devise/shared/_links.html.erb +++ /dev/null @@ -1,25 +0,0 @@ -<%- if controller_name != 'sessions' %> - <%= link_to "Log in", new_session_path(resource_name) %>
-<% end -%> - -<%- if devise_mapping.registerable? && controller_name != 'registrations' %> - <%= link_to "Sign up", new_registration_path(resource_name), class: "user-login-links" %>
-<% end -%> - -<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> - <%= link_to "Forgot your password?", new_password_path(resource_name), class: "user-login-links" %>
-<% end -%> - -<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> - <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
-<% end -%> - -<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> - <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
-<% end -%> - -<%- if devise_mapping.omniauthable? %> - <%- resource_class.omniauth_providers.each do |provider| %> - <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %>
- <% end -%> -<% end -%> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb deleted file mode 100644 index c42de17..0000000 --- a/app/views/devise/unlocks/new.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -

Resend unlock instructions

- -<%= simple_form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> - <%= f.error_notification %> - <%= f.full_error :unlock_token %> - -
- <%= f.input :email, - required: true, - autofocus: true, - input_html: { autocomplete: "email" } %> -
- -
- <%= f.button :submit, "Resend unlock instructions" %> -
-<% end %> - -<%= render "devise/shared/links" %> diff --git a/app/views/family/books/_form.html.erb b/app/views/family/books/_form.html.erb new file mode 100644 index 0000000..05497ed --- /dev/null +++ b/app/views/family/books/_form.html.erb @@ -0,0 +1,46 @@ +<%= form_with model: [ :family, book ], class: "bg-white rounded-2xl border border-stone-200 p-6 max-w-2xl" do |form| %> + <% if book.errors.any? %> +
<%= book.errors.full_messages.to_sentence %>
+ <% end %> + +
+
+ + <%= form.text_field :title, required: true, class: "block w-full rounded-md border border-stone-300 px-3 py-2" %> +
+
+ + <%= form.text_field :author, required: true, class: "block w-full rounded-md border border-stone-300 px-3 py-2" %> +
+
+ + <%= form.text_field :genre, required: true, placeholder: "Adventure", class: "block w-full rounded-md border border-stone-300 px-3 py-2" %> +
+
+ + <%= form.number_field :reward, min: 1, required: true, class: "block w-full rounded-md border border-stone-300 px-3 py-2" %> +
+
+ + <%= form.number_field :min_age, in: 3..16, class: "block w-full rounded-md border border-stone-300 px-3 py-2" %> +
+
+ + <%= form.number_field :max_age, in: 3..16, class: "block w-full rounded-md border border-stone-300 px-3 py-2" %> +
+
+ + <%= form.url_field :source_url, required: true, class: "block w-full rounded-md border border-stone-300 px-3 py-2" %> +
+
+ + <%= form.url_field :cover_url, class: "block w-full rounded-md border border-stone-300 px-3 py-2" %> +
+
+ + <%= form.text_area :summary, rows: 4, class: "block w-full rounded-md border border-stone-300 px-3 py-2" %> +
+
+ + <%= form.submit class: "mt-6 px-4 py-2 rounded-xl bg-amber-500 text-white font-bold cursor-pointer" %> +<% end %> diff --git a/app/views/family/books/edit.html.erb b/app/views/family/books/edit.html.erb new file mode 100644 index 0000000..ebf117d --- /dev/null +++ b/app/views/family/books/edit.html.erb @@ -0,0 +1,2 @@ +

Edit book

+<%= render "form", book: @book %> diff --git a/app/views/family/books/index.html.erb b/app/views/family/books/index.html.erb new file mode 100644 index 0000000..b834263 --- /dev/null +++ b/app/views/family/books/index.html.erb @@ -0,0 +1,28 @@ +
+

Books

+ + <%= link_to "+ Add book", new_family_book_path, class: "px-4 py-2 rounded-xl bg-amber-500 text-white font-bold" %> +
+ +
+ <% @books.each do |book| %> +
+
+ <%= book.title %> by <%= book.author %> +
<%= book.genre %> ยท ages <%= book.min_age %>โ€“<%= book.max_age %> ยท ๐Ÿช™ <%= book.reward %>
+
+ + <% if book.quiz&.questions.present? %> + โœ“ quiz + <% else %> + no quiz + <% end %> + <%= link_to "Quiz", edit_family_book_quiz_path(book), class: "underline" %> + <%= link_to "Edit", edit_family_book_path(book), class: "underline" %> + <%= button_to "Remove", family_book_path(book), method: :delete, data: { turbo_confirm: "Remove this book?" }, class: "text-red-500 underline cursor-pointer" %> +
+ <% end %> + <% if @books.empty? %> +

No books yet. Tip: Project Gutenberg and StoryWeaver have thousands of free children's books to link to.

+ <% end %> +
diff --git a/app/views/family/books/new.html.erb b/app/views/family/books/new.html.erb new file mode 100644 index 0000000..76a84c0 --- /dev/null +++ b/app/views/family/books/new.html.erb @@ -0,0 +1,2 @@ +

Add a book

+<%= render "form", book: @book %> diff --git a/app/views/family/books/show.html.erb b/app/views/family/books/show.html.erb new file mode 100644 index 0000000..77f3542 --- /dev/null +++ b/app/views/family/books/show.html.erb @@ -0,0 +1,7 @@ +

<%= @book.title %>

+

by <%= @book.author %> ยท <%= @book.genre %>

+

<%= @book.summary %>

+
+ <%= link_to "Edit", edit_family_book_path(@book), class: "underline" %> + <%= link_to "Quiz", edit_family_book_quiz_path(@book), class: "underline" %> +
diff --git a/app/views/family/dashboards/show.html.erb b/app/views/family/dashboards/show.html.erb new file mode 100644 index 0000000..ac42272 --- /dev/null +++ b/app/views/family/dashboards/show.html.erb @@ -0,0 +1,42 @@ +

Family dashboard

+ +
+ <% @kids.each do |kid| %> +
+
<%= kid.avatar %>
+

<%= kid.name %>

+

๐Ÿช™ <%= kid.coins %> coins ยท ๐Ÿ“– <%= kid.finished_count %> books ยท ๐Ÿ”ฅ <%= kid.current_streak %>-day streak

+
+ <% end %> + <% if @kids.empty? %> +

No readers yet โ€” <%= link_to "add your first kid", new_family_kid_path, class: "underline" %>.

+ <% end %> +
+ +<% if @pending_redemptions.any? %> +
+

๐ŸŽ Rewards waiting for your approval

+ <% @pending_redemptions.each do |r| %> +
+ <%= r.kid.avatar %> <%= r.kid.name %> wants <%= r.reward.emoji %> <%= r.reward.title %> + + <%= button_to "Approve", family_redemption_path(r), method: :patch, class: "px-3 py-1.5 rounded-lg bg-amber-500 text-white text-sm font-bold cursor-pointer" %> +
+ <% end %> +
+<% end %> + +

Recently finished books

+
+ <% @recent_readings.each do |reading| %> +
+ <%= reading.kid.avatar %> + <%= reading.kid.name %> finished <%= reading.book.title %> + + score <%= reading.quiz_score %>% ยท +<%= reading.coins_earned %> ๐Ÿช™ ยท <%= time_ago_in_words(reading.updated_at) %> ago +
+ <% end %> + <% if @recent_readings.empty? %> +

Nothing yet โ€” once your kids finish books and pass quizzes, they'll show up here.

+ <% end %> +
diff --git a/app/views/family/kids/_form.html.erb b/app/views/family/kids/_form.html.erb new file mode 100644 index 0000000..ea62c1a --- /dev/null +++ b/app/views/family/kids/_form.html.erb @@ -0,0 +1,25 @@ +<%= form_with model: [ :family, kid ], class: "bg-white rounded-2xl border border-stone-200 p-6 max-w-lg" do |form| %> + <% if kid.errors.any? %> +
<%= kid.errors.full_messages.to_sentence %>
+ <% end %> + + + <%= form.text_field :name, required: true, class: "block w-full rounded-md border border-stone-300 px-3 py-2 mb-4" %> + + + <%= form.number_field :age, in: 3..16, class: "block w-24 rounded-md border border-stone-300 px-3 py-2 mb-4" %> + + +
+ <% Kid::AVATARS.each do |a| %> + + <% end %> +
+ + + <%= form.text_field :pin, inputmode: :numeric, pattern: "\\d{4}", maxlength: 4, required: kid.new_record?, class: "block w-32 rounded-md border border-stone-300 px-3 py-2 mb-6 tracking-widest text-center" %> + + <%= form.submit class: "px-4 py-2 rounded-xl bg-amber-500 text-white font-bold cursor-pointer" %> +<% end %> diff --git a/app/views/family/kids/edit.html.erb b/app/views/family/kids/edit.html.erb new file mode 100644 index 0000000..707b89a --- /dev/null +++ b/app/views/family/kids/edit.html.erb @@ -0,0 +1,2 @@ +

Edit <%= @kid.name %>

+<%= render "form", kid: @kid %> diff --git a/app/views/family/kids/index.html.erb b/app/views/family/kids/index.html.erb new file mode 100644 index 0000000..ee049dd --- /dev/null +++ b/app/views/family/kids/index.html.erb @@ -0,0 +1,19 @@ +
+

Kids

+ + <%= link_to "+ Add kid", new_family_kid_path, class: "px-4 py-2 rounded-xl bg-amber-500 text-white font-bold" %> +
+ +
+ <% @kids.each do |kid| %> +
+
<%= kid.avatar %>
+

<%= kid.name %> <%= "ยท age #{kid.age}" if kid.age %>

+

๐Ÿช™ <%= kid.coins %> ยท ๐Ÿ“– <%= kid.finished_count %>

+
+ <%= link_to "Edit", edit_family_kid_path(kid), class: "underline" %> + <%= button_to "Remove", family_kid_path(kid), method: :delete, data: { turbo_confirm: "Remove #{kid.name} and all their progress?" }, class: "text-red-500 underline cursor-pointer" %> +
+
+ <% end %> +
diff --git a/app/views/family/kids/new.html.erb b/app/views/family/kids/new.html.erb new file mode 100644 index 0000000..33f8169 --- /dev/null +++ b/app/views/family/kids/new.html.erb @@ -0,0 +1,2 @@ +

Add a kid

+<%= render "form", kid: @kid %> diff --git a/app/views/family/quizzes/edit.html.erb b/app/views/family/quizzes/edit.html.erb new file mode 100644 index 0000000..5ca58b1 --- /dev/null +++ b/app/views/family/quizzes/edit.html.erb @@ -0,0 +1,27 @@ +

Quiz for โ€œ<%= @book.title %>โ€

+

Answers are only stored on the server โ€” kids can't peek at them in the page source.

+ +
+ <%= button_to "โœจ Generate quiz with AI", generate_family_book_quiz_path(@book), class: "px-4 py-2 rounded-xl bg-violet-600 hover:bg-violet-500 text-white font-bold cursor-pointer" %> + <% unless QuizGenerator.available? %> +

Requires ANTHROPIC_API_KEY to be set on the server.

+ <% end %> +
+ +<%= form_with url: family_book_quiz_path(@book), method: :patch, class: "space-y-6 max-w-2xl" do %> + <% questions = @quiz.questions.presence || Array.new(5) { { "question" => "", "choices" => [ "", "", "", "" ], "answer" => 0 } } %> + <% questions.each_with_index do |q, i| %> +
+ + <%= text_field_tag "questions[#{i}][question]", q["question"], class: "block w-full rounded-md border border-stone-300 px-3 py-2 mb-3" %> + <% Array(q["choices"]).each_with_index do |choice, ci| %> +
+ <%= radio_button_tag "questions[#{i}][answer]", ci, q["answer"].to_i == ci, title: "Correct answer" %> + <%= text_field_tag "questions[#{i}][choices][]", choice, class: "block w-full rounded-md border border-stone-300 px-3 py-1.5 text-sm" %> +
+ <% end %> +

Select the radio button next to the correct answer.

+
+ <% end %> + <%= submit_tag "Save quiz", class: "px-4 py-2 rounded-xl bg-amber-500 text-white font-bold cursor-pointer" %> +<% end %> diff --git a/app/views/family/redemptions/index.html.erb b/app/views/family/redemptions/index.html.erb new file mode 100644 index 0000000..817190c --- /dev/null +++ b/app/views/family/redemptions/index.html.erb @@ -0,0 +1,18 @@ +

Redemptions

+ +
+ <% @redemptions.each do |r| %> +
+ <%= r.kid.avatar %> <%= r.kid.name %> โ†’ <%= r.reward.emoji %> <%= r.reward.title %> (๐Ÿช™ <%= r.reward.cost %>) + + <% if r.status == "pending" %> + <%= button_to "Approve", family_redemption_path(r), method: :patch, class: "px-3 py-1.5 rounded-lg bg-amber-500 text-white font-bold cursor-pointer" %> + <% else %> + โœ“ approved + <% end %> +
+ <% end %> + <% if @redemptions.empty? %> +

No redemptions yet.

+ <% end %> +
diff --git a/app/views/family/rewards/_form.html.erb b/app/views/family/rewards/_form.html.erb new file mode 100644 index 0000000..07a2de5 --- /dev/null +++ b/app/views/family/rewards/_form.html.erb @@ -0,0 +1,16 @@ +<%= form_with model: [ :family, reward ], class: "bg-white rounded-2xl border border-stone-200 p-6 max-w-lg" do |form| %> + <% if reward.errors.any? %> +
<%= reward.errors.full_messages.to_sentence %>
+ <% end %> + + + <%= form.text_field :title, required: true, placeholder: "Trip to the zoo", class: "block w-full rounded-md border border-stone-300 px-3 py-2 mb-4" %> + + + <%= form.text_field :emoji, placeholder: "๐Ÿฆ“", maxlength: 4, class: "block w-24 rounded-md border border-stone-300 px-3 py-2 mb-4 text-center text-xl" %> + + + <%= form.number_field :cost, min: 1, required: true, class: "block w-32 rounded-md border border-stone-300 px-3 py-2 mb-6" %> + + <%= form.submit class: "px-4 py-2 rounded-xl bg-amber-500 text-white font-bold cursor-pointer" %> +<% end %> diff --git a/app/views/family/rewards/edit.html.erb b/app/views/family/rewards/edit.html.erb new file mode 100644 index 0000000..37b930f --- /dev/null +++ b/app/views/family/rewards/edit.html.erb @@ -0,0 +1,2 @@ +

Edit reward

+<%= render "form", reward: @reward %> diff --git a/app/views/family/rewards/index.html.erb b/app/views/family/rewards/index.html.erb new file mode 100644 index 0000000..ab57de4 --- /dev/null +++ b/app/views/family/rewards/index.html.erb @@ -0,0 +1,23 @@ +
+

Rewards

+ + <%= link_to "+ Add reward", new_family_reward_path, class: "px-4 py-2 rounded-xl bg-amber-500 text-white font-bold" %> +
+

These are the prizes your kids can buy with their coins โ€” real-world things you deliver, like a zoo trip or movie night.

+ +
+ <% @rewards.each do |reward| %> +
+
<%= reward.emoji %>
+

<%= reward.title %>

+

๐Ÿช™ <%= reward.cost %>

+
+ <%= link_to "Edit", edit_family_reward_path(reward), class: "underline" %> + <%= button_to "Remove", family_reward_path(reward), method: :delete, data: { turbo_confirm: "Remove this reward?" }, class: "text-red-500 underline cursor-pointer" %> +
+
+ <% end %> + <% if @rewards.empty? %> +

No rewards yet โ€” add the first one!

+ <% end %> +
diff --git a/app/views/family/rewards/new.html.erb b/app/views/family/rewards/new.html.erb new file mode 100644 index 0000000..23c5b2d --- /dev/null +++ b/app/views/family/rewards/new.html.erb @@ -0,0 +1,2 @@ +

Add a reward

+<%= render "form", reward: @reward %> diff --git a/app/views/kids/create_avatar.html.erb b/app/views/kids/create_avatar.html.erb deleted file mode 100644 index a76acd2..0000000 --- a/app/views/kids/create_avatar.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -
- -
-
- <%= simple_form_for(@kid) do |f| %> - <%= f.input :avatar %> - <%= f.input :avatar_cache, as: :hidden %> - <%= f.submit class: 'kid-button' %> - <% end %> -
diff --git a/app/views/kids/edit.html.erb b/app/views/kids/edit.html.erb deleted file mode 100644 index 75f2ef6..0000000 --- a/app/views/kids/edit.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -
-
-
- <%= simple_form_for(@kid) do |f| %> - <%= f.input :name %> - <%= f.input :password %> - <%= f.input :age %> -

Interests

- <%= select_tag "kid[interests][]", options_from_collection_for_select( - @categories, - :to_s, :to_s), - class: 'select2', multiple: 'multiple' %> - <%= f.submit class: 'btn btn-primary form-group' %> - <% end %> -
-
-
diff --git a/app/views/kids/index.html.erb b/app/views/kids/index.html.erb deleted file mode 100644 index c29c582..0000000 --- a/app/views/kids/index.html.erb +++ /dev/null @@ -1,104 +0,0 @@ -
-
-

My account

-

Purchase ReadCoins and allow your children to explore the wonders of the books world

- - - -
-
-
-
-
- <%= humanized_money_with_symbol(current_user.wallet) %> -
-

Balance

-

1 RDC = 1 USD

-
-
- - -
-
- - -
-
- <%= form_tag orders_path do %> - <%= number_field_tag "amount", class:"buy-field" %> -

Enter amount

-
-
- <%= submit_tag 'Convert To ReadCoins', class:"buy-btn" %> -
- <% end %> -
- -
-
- <%= link_to "Add a child", new_kid_path, class: "btn-add-kid", style:"text-decoration:none; color: #54c6eb;" %> -
-
-
- <% @kids.each do |kid| %> -
-
-
- <%= cl_image_tag kid.avatar, class: "parent-avatar card-avatar-img", style:"height:150px; width:150px; " %> -
-
-
- <%= link_to edit_kid_path(kid), style:"position: absolute; right: 22px; top:22px;" do %> - - <% end %> -
-
-

<%= kid.name %>

-
-
-
-
- <%= kid.readings.length %> -
-

Books Read

-
-
-
- <%= kid.purchases.length %> -
-

Gift purchased

-
-
-
- <%= kid.wallet %> -
-

Readcoins

-
-
-
-
-
- <% end %> -
-
-
- - - - - - - - - - - - - - - - - - - - diff --git a/app/views/kids/kid_dashboard.html.erb b/app/views/kids/kid_dashboard.html.erb deleted file mode 100644 index 77ec84b..0000000 --- a/app/views/kids/kid_dashboard.html.erb +++ /dev/null @@ -1,64 +0,0 @@ -
-
-

My ebooks

- <% if @chosen_books.empty? %> - <% @books.each do |book| %> -
-
-
- <%= book.genre %> -
-
- -
-

<%= book.title %>

-

Author: <%= book.author %>

- <%= link_to kid_book_path(@kid, book), - class: "kid-button show-more-btn" do %> - -

Show more

- <% end %> -
- -
-
- <%= image_tag 'coins.png' %> -

<%= book.reward %> rdcn

-
-
- -
- <% end %> - <% else %> - <% @chosen_books.each do |book| %> -
-
-
- <%= book.genre %> -
-
- -
-

<%= book.title %>

-

Author: <%= book.author %>

- <%= link_to kid_book_path(@kid, book), - class: "kid-button show-more-btn" do %> - -

Show more

- <% end %> -
- -
-
- <%= image_tag 'coins.png' %> -

<%= book.reward %> rdcn

-
-
- -
- <% end %> - <% end %> -
-
- - diff --git a/app/views/kids/login.html.erb b/app/views/kids/login.html.erb deleted file mode 100644 index 5909d24..0000000 --- a/app/views/kids/login.html.erb +++ /dev/null @@ -1,31 +0,0 @@ -
-
-
- -

<%= "Welcome #{@user.username}!" %>

-

Access your dashboard below to buy some readcoins, and see your kids activities

- <%= image_tag 'kid-reading-with-parents.jpg', class: "login-avatar parent-avatar" %>

- - <% if current_user.admin? %> -

<%= link_to "admin book dashboard", admin_books_path, class: "kid-button" %>

-

<%= link_to "admin prize dashboard", admin_prizes_path, class: "kid-button" %>

- <% end %> -
-
-
-
- -

Enter your password to access your dashboard and see your books

- <%= image_tag 'kids_on_roof_reading_book.jpg', class: "login-avatar" %> -
- <%= simple_form_for kids_login_path do |f| %> - <%= f.input :password, label: "Magic password" %> - <%= f.submit "GO!", class: 'parent-button', id: "login-go-btn" %> - <% end %> -
-
-
-
diff --git a/app/views/kids/new.html.erb b/app/views/kids/new.html.erb deleted file mode 100644 index 2bb0982..0000000 --- a/app/views/kids/new.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -
-
-
-

Add A Child

- <%= simple_form_for(@new_kid) do |f| %> - <%= f.input :name %> - <%= f.input :password %> - <%= f.input :age %> -

Interests

- <%= select_tag "kid[interests][]", options_from_collection_for_select( - @categories, - :to_s, :to_s), - class: 'select2', multiple: 'multiple' %> - <%= f.submit class: 'btn btn-primary form-group' %> - <% end %> -
-
-
diff --git a/app/views/kids/show.html.erb b/app/views/kids/show.html.erb deleted file mode 100644 index 3ea01cd..0000000 --- a/app/views/kids/show.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<% if flash[:notice] %> -
<%= flash[:notice] %>
-<% end %> -
-
-

Wallet: <%= humanized_money_with_symbol(current_user.wallet) %> USD -

<%= @kid.name %>

-

RDC: <%= @kid.wallet %>

-

Readings:

-
    - <% @kid.readings.each do |reading| %> -
  • <%= reading.book.title %>
  • - <% end %> -
-

Purchases:

-
    - <% @kid.purchases.each do |purchase| %> -
  • <%= purchase.prize.title %>
  • - <% end %> -
-
-
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 1fdc255..0f628b9 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,28 +1,31 @@ - - - Read-Coin - - <%= favicon_link_tag 'logo.png'%> + <%= content_for(:title) || "Readcoin" %> + + + + <%= csrf_meta_tags %> - <%= action_cable_meta_tag %> - <%= stylesheet_link_tag 'application', media: 'all' %> - <%= stylesheet_pack_tag 'application', media: 'all' %> - - - <% if !@show_navbar %> - <%= render 'shared/navbar' unless current_page?(new_user_session_path) %> - <% end %> - <%#= #render 'shared/flashes' %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> + <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> - <%= yield %> + + + - <%= javascript_include_tag 'application' %> - <%= javascript_pack_tag 'application' %> - <% if controller_name == "readings" %> - <%= javascript_pack_tag 'a' %> - <% end %> + <%# Includes all stylesheet files in app/assets/stylesheets %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + +
+ <%= yield %> +
diff --git a/app/views/layouts/family.html.erb b/app/views/layouts/family.html.erb new file mode 100644 index 0000000..df5a377 --- /dev/null +++ b/app/views/layouts/family.html.erb @@ -0,0 +1,30 @@ + + + + <%= content_for(:title) || "ReadCoin โ€” Family" %> + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + +
+ <%= render "shared/flashes" %> + <%= yield %> +
+ + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index cbd34d2..3aac900 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -1,7 +1,7 @@ - + diff --git a/app/views/layouts/play.html.erb b/app/views/layouts/play.html.erb new file mode 100644 index 0000000..371a77b --- /dev/null +++ b/app/views/layouts/play.html.erb @@ -0,0 +1,33 @@ + + + + <%= content_for(:title) || "ReadCoin โ€” Play" %> + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + <% if current_kid %> + + <% end %> +
+ <%= render "shared/flashes" %> + <%= yield %> +
+ + diff --git a/app/views/orders/show.html.erb b/app/views/orders/show.html.erb deleted file mode 100644 index b8fa843..0000000 --- a/app/views/orders/show.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -
-
-

Congratulations! you just purchased

-
-
-
- <%= @order.amount_cents / 100 %> -
-
- ReadCoins -
-
-
-
- <%= link_to "back to dashboard", kids_path, class: "parent-button" %> -
-
-
- - - - diff --git a/app/views/pages/home.html.erb b/app/views/pages/home.html.erb index 474d75b..fc9f90f 100644 --- a/app/views/pages/home.html.erb +++ b/app/views/pages/home.html.erb @@ -1,78 +1,19 @@ -