diff options
150 files changed, 7057 insertions, 0 deletions
diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9612375 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,37 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8dc4323 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b66b15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key + +/app/assets/builds/* +!/app/assets/builds/.keep diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..71e447d --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-3.1.4 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f4d6670 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,62 @@ +# syntax = docker/dockerfile:1 + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile +ARG RUBY_VERSION=3.1.4 +FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base + +# Rails app lives here +WORKDIR /rails + +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + + +# Throw-away build stage to reduce size of final image +FROM base as build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config + +# Install application gems +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + +# Final stage for app image +FROM base + +# Install packages needed for deployment +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libvips postgresql-client && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Copy built artifacts: gems, application +COPY --from=build /usr/local/bundle /usr/local/bundle +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN useradd rails --create-home --shell /bin/bash && \ + chown -R rails:rails db log storage tmp +USER rails:rails + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD ["bundle", "exec", "iodine"] @@ -0,0 +1,77 @@ +source "https://rubygems.org" + +ruby "3.1.4" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 7.1.3" + +# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] +gem "sprockets-rails" + +# Use postgresql as the database for Active Record +gem "pg", "~> 1.1" + +# 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 Dart SASS [https://github.com/rails/dartsass-rails] +gem "dartsass-rails" + +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder" + +# Use Redis adapter to run Action Cable in production +# gem "redis", ">= 4.0.1" + +# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] +# gem "kredis" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +# gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[ windows jruby ] + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +# gem "image_processing", "~> 1.2" + +gem 'ransack' + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[ mri windows ] +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" + + # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] + # gem "rack-mini-profiler" + + # Speed up commands on slow machines / big apps [https://github.com/rails/spring] + # gem "spring" + + gem "error_highlight", ">= 0.4.0", platforms: [:ruby] +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" +end + +gem "iodine", "~> 0.7.57" + +gem "rodauth-rails", "~> 1.13" + +gem "argon2", "~> 2.3" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..7dfe1f9 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,352 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.1.3) + actionpack (= 7.1.3) + activesupport (= 7.1.3) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.1.3) + actionpack (= 7.1.3) + activejob (= 7.1.3) + activerecord (= 7.1.3) + activestorage (= 7.1.3) + activesupport (= 7.1.3) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.1.3) + actionpack (= 7.1.3) + actionview (= 7.1.3) + activejob (= 7.1.3) + activesupport (= 7.1.3) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.2) + actionpack (7.1.3) + actionview (= 7.1.3) + activesupport (= 7.1.3) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.3) + actionpack (= 7.1.3) + activerecord (= 7.1.3) + activestorage (= 7.1.3) + activesupport (= 7.1.3) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.1.3) + activesupport (= 7.1.3) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.3) + activesupport (= 7.1.3) + globalid (>= 0.3.6) + activemodel (7.1.3) + activesupport (= 7.1.3) + activerecord (7.1.3) + activemodel (= 7.1.3) + activesupport (= 7.1.3) + timeout (>= 0.4.0) + activestorage (7.1.3) + actionpack (= 7.1.3) + activejob (= 7.1.3) + activerecord (= 7.1.3) + activesupport (= 7.1.3) + marcel (~> 1.0) + activesupport (7.1.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + after_commit_everywhere (1.3.1) + activerecord (>= 4.2) + activesupport + argon2 (2.3.0) + ffi (~> 1.15) + ffi-compiler (~> 1.0) + base64 (0.2.0) + bcrypt (3.1.20) + bigdecimal (3.1.6) + bindex (0.8.1) + bootsnap (1.17.1) + msgpack (~> 1.2) + builder (3.2.4) + 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) + concurrent-ruby (1.2.3) + connection_pool (2.4.1) + crass (1.0.6) + dartsass-rails (0.5.0) + railties (>= 6.0.0) + sass-embedded (~> 1.63) + date (3.3.4) + debug (1.9.1) + irb (~> 1.10) + reline (>= 0.3.8) + drb (2.2.0) + ruby2_keywords + error_highlight (0.6.0) + erubi (1.12.0) + ffi (1.16.3) + ffi-compiler (1.0.1) + ffi (>= 1.0.0) + rake + globalid (1.2.1) + activesupport (>= 6.1) + google-protobuf (3.25.2) + google-protobuf (3.25.2-aarch64-linux) + google-protobuf (3.25.2-arm64-darwin) + google-protobuf (3.25.2-x86-linux) + google-protobuf (3.25.2-x86_64-darwin) + google-protobuf (3.25.2-x86_64-linux) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + importmap-rails (2.0.1) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.7.2) + iodine (0.7.57) + irb (1.11.1) + rdoc + reline (>= 0.4.2) + jbuilder (2.11.5) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) + loofah (2.22.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.2) + matrix (0.4.2) + mini_mime (1.1.5) + minitest (5.21.2) + msgpack (1.7.2) + mutex_m (0.2.0) + net-imap (0.4.9.1) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.4.0.1) + net-protocol + nio4r (2.7.0) + nokogiri (1.16.0-aarch64-linux) + racc (~> 1.4) + nokogiri (1.16.0-arm-linux) + racc (~> 1.4) + nokogiri (1.16.0-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.0-x86-linux) + racc (~> 1.4) + nokogiri (1.16.0-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.0-x86_64-linux) + racc (~> 1.4) + pg (1.5.4) + psych (5.1.2) + stringio + public_suffix (5.0.4) + racc (1.7.3) + rack (3.0.8) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails (7.1.3) + actioncable (= 7.1.3) + actionmailbox (= 7.1.3) + actionmailer (= 7.1.3) + actionpack (= 7.1.3) + actiontext (= 7.1.3) + actionview (= 7.1.3) + activejob (= 7.1.3) + activemodel (= 7.1.3) + activerecord (= 7.1.3) + activestorage (= 7.1.3) + activesupport (= 7.1.3) + bundler (>= 1.15.0) + railties (= 7.1.3) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.3) + actionpack (= 7.1.3) + activesupport (= 7.1.3) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rake (13.1.0) + ransack (4.1.1) + activerecord (>= 6.1.5) + activesupport (>= 6.1.5) + i18n + rdoc (6.6.2) + psych (>= 4.0.0) + regexp_parser (2.9.0) + reline (0.4.2) + io-console (~> 0.5) + rexml (3.2.6) + roda (3.76.0) + rack + rodauth (2.33.0) + roda (>= 2.6.0) + sequel (>= 4) + rodauth-model (0.2.1) + rodauth (~> 2.0) + rodauth-rails (1.13.0) + bcrypt + railties (>= 5.0, < 8) + roda (~> 3.73) + rodauth (~> 2.30) + rodauth-model (~> 0.2) + sequel-activerecord_connection (~> 1.1) + tilt + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + sass-embedded (1.70.0-aarch64-linux-gnu) + google-protobuf (~> 3.25) + sass-embedded (1.70.0-aarch64-linux-musl) + google-protobuf (~> 3.25) + sass-embedded (1.70.0-arm-linux-gnueabihf) + google-protobuf (~> 3.25) + sass-embedded (1.70.0-arm-linux-musleabihf) + google-protobuf (~> 3.25) + sass-embedded (1.70.0-arm64-darwin) + google-protobuf (~> 3.25) + sass-embedded (1.70.0-x86-linux-gnu) + google-protobuf (~> 3.25) + sass-embedded (1.70.0-x86-linux-musl) + google-protobuf (~> 3.25) + sass-embedded (1.70.0-x86_64-darwin) + google-protobuf (~> 3.25) + sass-embedded (1.70.0-x86_64-linux-gnu) + google-protobuf (~> 3.25) + sass-embedded (1.70.0-x86_64-linux-musl) + google-protobuf (~> 3.25) + selenium-webdriver (4.17.0) + base64 (~> 0.2) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + sequel (5.76.0) + bigdecimal + sequel-activerecord_connection (1.3.1) + activerecord (>= 4.2, < 8) + after_commit_everywhere (~> 1.1) + sequel (~> 5.38) + sprockets (4.2.1) + concurrent-ruby (~> 1.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + sprockets (>= 3.0.0) + stimulus-rails (1.3.3) + railties (>= 6.0.0) + stringio (3.1.0) + thor (1.3.0) + tilt (2.3.0) + timeout (0.4.1) + turbo-rails (1.5.0) + actionpack (>= 6.0.0) + activejob (>= 6.0.0) + railties (>= 6.0.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + webrick (1.8.1) + websocket (1.2.10) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.6.12) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux + arm-linux-gnueabihf + arm-linux-musleabihf + arm64-darwin + x86-linux + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + argon2 (~> 2.3) + bootsnap + capybara + dartsass-rails + debug + error_highlight (>= 0.4.0) + importmap-rails + iodine (~> 0.7.57) + jbuilder + pg (~> 1.1) + rails (~> 7.1.3) + ransack + rodauth-rails (~> 1.13) + selenium-webdriver + sprockets-rails + stimulus-rails + turbo-rails + tzinfo-data + web-console + +RUBY VERSION + ruby 3.1.4p223 + +BUNDLED WITH + 2.5.5 diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 0000000..bbeddd4 --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,2 @@ +web: bundle exec iodine +css: bin/rails dartsass:watch diff --git a/README.md b/README.md new file mode 100644 index 0000000..7db80e4 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# README + +This README would normally document whatever steps are necessary to get the +application up and running. + +Things you may want to cover: + +* Ruby version + +* System dependencies + +* Configuration + +* Database creation + +* Database initialization + +* How to run the test suite + +* Services (job queues, cache servers, search engines, etc.) + +* Deployment instructions + +* ... diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/Rakefile @@ -0,0 +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" + +Rails.application.load_tasks diff --git a/app/assets/builds/.keep b/app/assets/builds/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/assets/builds/.keep diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000..4028c22 --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,4 @@ +//= link_tree ../images +//= link_tree ../../javascript .js +//= link_tree ../../../vendor/javascript .js +//= link_tree ../builds diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/assets/images/.keep diff --git a/app/assets/stylesheets/_functions.scss b/app/assets/stylesheets/_functions.scss new file mode 100644 index 0000000..160870c --- /dev/null +++ b/app/assets/stylesheets/_functions.scss @@ -0,0 +1,4 @@ +// Output color in RGB format +@function to-rgb($color) { + @return unquote("rgb(#{red($color)}, #{green($color)}, #{blue($color)})"); +}
\ No newline at end of file diff --git a/app/assets/stylesheets/_variables.scss b/app/assets/stylesheets/_variables.scss new file mode 100644 index 0000000..e2f653e --- /dev/null +++ b/app/assets/stylesheets/_variables.scss @@ -0,0 +1,69 @@ +// Config +// –––––––––––––––––––– + +// Set the root element for $enable-semantic-container and $enable-responsive-spacings +$semantic-root-element: "body" !default; + +// Enable <header>, <main>, <footer> inside $semantic-root-element as containers +$enable-semantic-container: false !default; + +// Enable .container and .container-fluid +$enable-class-container: true !default; + +// Enable a centered viewport for <header>, <main>, <footer> inside $enable-semantic-container +// Fluid layout if disabled +$enable-viewport: true !default; + +// Enable responsive spacings for <header>, <main>, <footer>, <section>, <article> +// Fixed spacings if disabled +$enable-responsive-spacings: true !default; + +// Enable responsive typography +// Fixed root element size if disabled +$enable-responsive-typography: true !default; + +// Enable .classes +// .classless version if disabled +$enable-classes: true !default; + +// Enable .grid class +$enable-grid: true !default; + +// Enable transitions +$enable-transitions: true !default; + +// Enable overriding with !important +$enable-important: true !default; + +// Responsive +// –––––––––––––––––––– + +// xs: Extra small (portrait phones) +// sm: Small(landscape phones) +// md: Medium(tablets) +// lg: Large(desktops) +// xl: Extra large (large desktops) + +// NOTE: +// To provide an easy and fine styling on each breakpoint +// we didn't use @each, @mixin or @include. +// That means you need to edit each CSS selector file to add a breakpoint + +// Breakpoints +// 'null' disable the breakpoint +$breakpoints: ( + xs: 0, + sm: 576px, + md: 768px, + lg: 992px, + xl: 1200px, +) !default; + +// Viewports +$viewports: ( + // 'null' disable the viewport on a breakpoint + sm: 510px, + md: 700px, + lg: 920px, + xl: 1130px +) !default; diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss new file mode 100644 index 0000000..a3ac6ef --- /dev/null +++ b/app/assets/stylesheets/application.scss @@ -0,0 +1,2 @@ +// Sassy +@import "pico"; diff --git a/app/assets/stylesheets/components/_accordion.scss b/app/assets/stylesheets/components/_accordion.scss new file mode 100644 index 0000000..59b2c65 --- /dev/null +++ b/app/assets/stylesheets/components/_accordion.scss @@ -0,0 +1,116 @@ +/** + * Accordion (<details>) + */ + +details { + display: block; + margin-bottom: var(--spacing); + padding-bottom: var(--spacing); + border-bottom: var(--border-width) solid var(--accordion-border-color); + + summary { + line-height: 1rem; + list-style-type: none; + cursor: pointer; + + &:not([role]) { + color: var(--accordion-close-summary-color); + } + + @if $enable-transitions { + transition: color var(--transition); + } + + // Reset marker + &::-webkit-details-marker { + display: none; + } + + &::marker { + display: none; + } + + &::-moz-list-bullet { + list-style-type: none; + } + + // Marker + &::after { + display: block; + width: 1rem; + height: 1rem; + margin-inline-start: calc(var(--spacing, 1rem) * 0.5); + float: right; + transform: rotate(-90deg); + background-image: var(--icon-chevron); + background-position: right center; + background-size: 1rem auto; + background-repeat: no-repeat; + content: ""; + + @if $enable-transitions { + transition: transform var(--transition); + } + } + + &:focus { + outline: none; + + &:not([role="button"]) { + color: var(--accordion-active-summary-color); + } + } + + // Type button + &[role="button"] { + width: 100%; + text-align: left; + + // Marker + &::after { + height: calc(1rem * var(--line-height, 1.5)); + background-image: var(--icon-chevron-button); + } + + @if $enable-classes { + // .contrast + &:not(.outline).contrast { + // Marker + &::after { + background-image: var(--icon-chevron-button-inverse); + } + } + } + } + } + + // Open + &[open] { + > summary { + margin-bottom: calc(var(--spacing)); + + &:not([role]) { + &:not(:focus) { + color: var(--accordion-open-summary-color); + } + } + + &::after { + transform: rotate(0); + } + } + } +} + +[dir="rtl"] { + details { + summary { + text-align: right; + + &::after { + float: left; + background-position: left center; + } + } + } +} diff --git a/app/assets/stylesheets/components/_card.scss b/app/assets/stylesheets/components/_card.scss new file mode 100644 index 0000000..924415e --- /dev/null +++ b/app/assets/stylesheets/components/_card.scss @@ -0,0 +1,36 @@ +/** + * Card (<article>) + */ + +article { + margin: var(--block-spacing-vertical) 0; + padding: var(--block-spacing-vertical) var(--block-spacing-horizontal); + border-radius: var(--border-radius); + background: var(--card-background-color); + box-shadow: var(--card-box-shadow); + + > header, + > footer { + margin-right: calc(var(--block-spacing-horizontal) * -1); + margin-left: calc(var(--block-spacing-horizontal) * -1); + padding: calc(var(--block-spacing-vertical) * 0.66) + var(--block-spacing-horizontal); + background-color: var(--card-sectionning-background-color); + } + + > header { + margin-top: calc(var(--block-spacing-vertical) * -1); + margin-bottom: var(--block-spacing-vertical); + border-bottom: var(--border-width) solid var(--card-border-color); + border-top-right-radius: var(--border-radius); + border-top-left-radius: var(--border-radius); + } + + > footer { + margin-top: var(--block-spacing-vertical); + margin-bottom: calc(var(--block-spacing-vertical) * -1); + border-top: var(--border-width) solid var(--card-border-color); + border-bottom-right-radius: var(--border-radius); + border-bottom-left-radius: var(--border-radius); + } +} diff --git a/app/assets/stylesheets/components/_dropdown.scss b/app/assets/stylesheets/components/_dropdown.scss new file mode 100644 index 0000000..a16e6d2 --- /dev/null +++ b/app/assets/stylesheets/components/_dropdown.scss @@ -0,0 +1,214 @@ +/** + * Dropdown ([role="list"]) + */ + +// Menu +details[role="list"], +li[role="list"] { + position: relative; +} + +details[role="list"] summary + ul, +li[role="list"] > ul { + display: flex; + z-index: 99; + position: absolute; + top: auto; + right: 0; + left: 0; + flex-direction: column; + margin: 0; + padding: 0; + border: var(--border-width) solid var(--dropdown-border-color); + border-radius: var(--border-radius); + border-top-right-radius: 0; + border-top-left-radius: 0; + background-color: var(--dropdown-background-color); + box-shadow: var(--card-box-shadow); + color: var(--dropdown-color); + white-space: nowrap; + + li { + width: 100%; + margin-bottom: 0; + padding: calc(var(--form-element-spacing-vertical) * 0.5) + var(--form-element-spacing-horizontal); + list-style: none; + + &:first-of-type { + margin-top: calc(var(--form-element-spacing-vertical) * 0.5); + } + + &:last-of-type { + margin-bottom: calc(var(--form-element-spacing-vertical) * 0.5); + } + + a { + display: block; + margin: calc(var(--form-element-spacing-vertical) * -0.5) + calc(var(--form-element-spacing-horizontal) * -1); + padding: calc(var(--form-element-spacing-vertical) * 0.5) + var(--form-element-spacing-horizontal); + overflow: hidden; + color: var(--dropdown-color); + text-decoration: none; + text-overflow: ellipsis; + + &:hover { + background-color: var(--dropdown-hover-background-color); + } + } + } +} + +// Marker +details[role="list"] summary, +li[role="list"] > a { + &::after { + display: block; + width: 1rem; + height: calc(1rem * var(--line-height, 1.5)); + margin-inline-start: 0.5rem; + float: right; + transform: rotate(0deg); + background-image: var(--icon-chevron); + background-position: right center; + background-size: 1rem auto; + background-repeat: no-repeat; + content: ""; + } +} + +// Global dropdown only +details[role="list"] { + padding: 0; + border-bottom: none; + + // Style <summary> as <select> + summary { + margin-bottom: 0; + + &:not([role]) { + height: calc( + 1rem * var(--line-height) + var(--form-element-spacing-vertical) * 2 + + var(--border-width) * 2 + ); + padding: var(--form-element-spacing-vertical) + var(--form-element-spacing-horizontal); + border: var(--border-width) solid var(--form-element-border-color); + border-radius: var(--border-radius); + background-color: var(--form-element-background-color); + color: var(--form-element-placeholder-color); + line-height: inherit; + cursor: pointer; + + @if $enable-transitions { + transition: background-color var(--transition), + border-color var(--transition), color var(--transition), + box-shadow var(--transition); + } + + &:active, + &:focus { + border-color: var(--form-element-active-border-color); + background-color: var(--form-element-active-background-color); + } + + &:focus { + box-shadow: 0 0 0 var(--outline-width) var(--form-element-focus-color); + } + } + } + + // Close for details[role="list"] + &[open] summary { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + + &::before { + display: block; + z-index: 1; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: none; + content: ""; + cursor: default; + } + } +} + +// All Dropdowns inside <nav> +nav details[role="list"] summary, +nav li[role="list"] a { + display: flex; + direction: ltr; +} + +nav details[role="list"] summary + ul, +nav li[role="list"] > ul { + min-width: fit-content; + border-radius: var(--border-radius); + + li a { + border-radius: 0; + } +} + +// Dropdowns inside <nav> as nested <details> +nav details[role="list"] { + summary, + summary:not([role]) { + height: auto; + padding: var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal); + } + + &[open] summary { + border-radius: var(--border-radius); + } + + summary + ul { + margin-top: var(--outline-width); + margin-inline-start: 0; + } + + summary[role="link"] { + margin-bottom: calc(var(--nav-link-spacing-vertical) * -1); + line-height: var(--line-height); + + + ul { + margin-top: calc(var(--nav-link-spacing-vertical) + var(--outline-width)); + margin-inline-start: calc(var(--nav-link-spacing-horizontal) * -1); + } + } +} + +// Dropdowns inside a <nav> without using <details> +li[role="list"] { + // Open on hover (for mobile) + // or on active/focus (for keyboard navigation) + &:hover > ul, + a:active ~ ul, + a:focus ~ ul { + display: flex; + } + + > ul { + display: none; + margin-top: calc(var(--nav-link-spacing-vertical) + var(--outline-width)); + margin-inline-start: calc( + var(--nav-element-spacing-horizontal) - var(--nav-link-spacing-horizontal) + ); + } + + > a::after { + background-image: var(--icon-chevron); + } +} + +label > details[role="list"] { + margin-top: calc(var(--spacing) * .25); + margin-bottom: var(--spacing); +} diff --git a/app/assets/stylesheets/components/_modal.scss b/app/assets/stylesheets/components/_modal.scss new file mode 100644 index 0000000..af5cb16 --- /dev/null +++ b/app/assets/stylesheets/components/_modal.scss @@ -0,0 +1,168 @@ +/** + * Modal (<dialog>) + */ + +:root { + --scrollbar-width: 0px; +} + +dialog { + display: flex; + z-index: 999; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + align-items: center; + justify-content: center; + width: inherit; + min-width: 100%; + height: inherit; + min-height: 100%; + padding: var(--spacing); + border: 0; + backdrop-filter: var(--modal-overlay-backdrop-filter); + background-color: var(--modal-overlay-background-color); + color: var(--color); + + // Content + article { + $close-selector: if($enable-classes, ".close", "a[rel='prev']"); + max-height: calc(100vh - var(--spacing) * 2); + overflow: auto; + + @if map-get($breakpoints, "sm") { + @media (min-width: map-get($breakpoints, "sm")) { + max-width: map-get($viewports, "sm"); + } + } + + @if map-get($breakpoints, "md") { + @media (min-width: map-get($breakpoints, "md")) { + max-width: map-get($viewports, "md"); + } + } + + > header, + > footer { + padding: calc(var(--block-spacing-vertical) * 0.5) + var(--block-spacing-horizontal); + } + + > header { + #{$close-selector} { + margin: 0; + margin-left: var(--spacing); + float: right; + } + } + + > footer { + text-align: right; + + [role="button"] { + margin-bottom: 0; + + &:not(:first-of-type) { + margin-left: calc(var(--spacing) * 0.5); + } + } + } + + p { + &:last-of-type { + margin: 0; + } + } + + // Close icon + #{$close-selector} { + display: block; + width: 1rem; + height: 1rem; + margin-top: calc(var(--block-spacing-vertical) * -0.5); + margin-bottom: var(--typography-spacing-vertical); + margin-left: auto; + background-image: var(--icon-close); + background-position: center; + background-size: auto 1rem; + background-repeat: no-repeat; + opacity: 0.5; + + @if $enable-transitions { + transition: opacity var(--transition); + } + + &:is([aria-current], :hover, :active, :focus) { + opacity: 1; + } + } + } + + // Closed state + &:not([open]), + &[open="false"] { + display: none; + } +} + +// Utilities +@if $enable-classes { + .modal-is-open { + padding-right: var(--scrollbar-width, 0px); + overflow: hidden; + pointer-events: none; + touch-action: none; + + dialog { + pointer-events: auto; + } + } +} + +// Animations +@if ($enable-classes and $enable-transitions) { + $animation-duration: 0.2s; + + :where(.modal-is-opening, .modal-is-closing) { + dialog, + dialog > article { + animation-duration: $animation-duration; + animation-timing-function: ease-in-out; + animation-fill-mode: both; + } + + dialog { + animation-duration: ($animation-duration * 4); + animation-name: modal-overlay ; + + > article { + animation-delay: $animation-duration; + animation-name: modal; + } + } + } + + .modal-is-closing { + dialog, + dialog > article { + animation-delay: 0s; + animation-direction: reverse; + } + } + + @keyframes modal-overlay { + from { + backdrop-filter: none; + background-color: transparent; + } + } + + @keyframes modal { + from { + transform: translateY(-100%); + opacity: 0; + } + } +} diff --git a/app/assets/stylesheets/components/_nav.scss b/app/assets/stylesheets/components/_nav.scss new file mode 100644 index 0000000..06fdd22 --- /dev/null +++ b/app/assets/stylesheets/components/_nav.scss @@ -0,0 +1,141 @@ +/** + * Nav + */ + +// Reboot based on : +// - sanitize.css v13.0.0 | CC0 1.0 Universal | github.com/csstools/sanitize.css + +// Prevent VoiceOver from ignoring list semantics in Safari (opinionated) +:where(nav li)::before { + float: left; + content: "\200B"; +} + +// Pico +// –––––––––––––––––––– + +// Horizontal Nav +nav, +nav ul { + display: flex; +} + +nav { + justify-content: space-between; + + ol, + ul { + align-items: center; + margin-bottom: 0; + padding: 0; + list-style: none; + + &:first-of-type { + margin-left: calc(var(--nav-element-spacing-horizontal) * -1); + } + &:last-of-type { + margin-right: calc(var(--nav-element-spacing-horizontal) * -1); + } + } + + li { + display: inline-block; + margin: 0; + padding: var(--nav-element-spacing-vertical) + var(--nav-element-spacing-horizontal); + + // Minimal support for buttons and forms elements + > * { + --spacing: 0; + } + } + + :where(a, [role="link"]) { + display: inline-block; + margin: calc(var(--nav-link-spacing-vertical) * -1) + calc(var(--nav-link-spacing-horizontal) * -1); + padding: var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal); + border-radius: var(--border-radius); + text-decoration: none; + + &:is([aria-current], :hover, :active, :focus) { + text-decoration: none; + } + } + + // Breadcrumb + &[aria-label="breadcrumb"] { + align-items: center; + justify-content: start; + + & ul li { + &:not(:first-child) { + margin-inline-start: var(--nav-link-spacing-horizontal); + } + + &:not(:last-child) { + ::after { + position: absolute; + width: calc(var(--nav-link-spacing-horizontal) * 2); + margin-inline-start: calc(var(--nav-link-spacing-horizontal) / 2); + content: "/"; + color: var(--muted-color); + text-align: center; + } + } + } + + & a[aria-current] { + background-color: transparent; + color: inherit; + text-decoration: none; + pointer-events: none; + } + } + + // Minimal support for role="button" + [role="button"] { + margin-right: inherit; + margin-left: inherit; + padding: var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal); + } +} + +// Vertical Nav +aside { + nav, + ol, + ul, + li { + display: block; + } + + li { + padding: calc(var(--nav-element-spacing-vertical) * 0.5) + var(--nav-element-spacing-horizontal); + + a { + display: block; + } + + // Minimal support for links as buttons + [role="button"] { + margin: inherit; + } + } +} + +// Breadcrumb RTL +[dir="rtl"] { + nav { + &[aria-label="breadcrumb"] { + & ul li { + &:not(:last-child) { + ::after { + content: "\\"; + } + } + } + } + } +} diff --git a/app/assets/stylesheets/components/_progress.scss b/app/assets/stylesheets/components/_progress.scss new file mode 100644 index 0000000..d62ddc0 --- /dev/null +++ b/app/assets/stylesheets/components/_progress.scss @@ -0,0 +1,89 @@ +/** + * Progress + */ + +// Reboot based on : +// - normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css +// - sanitize.css v13.0.0 | CC0 1.0 Universal | github.com/csstools/sanitize.css +// –––––––––––––––––––– + +// 1. Add the correct display in Edge 18- and IE +// 2. Add the correct vertical alignment in Chrome, Edge, and Firefox +progress { + display: inline-block; // 1 + vertical-align: baseline; // 2 +} + +// Pico +// –––––––––––––––––––– + +progress { + // Reset the default appearance + -webkit-appearance: none; + -moz-appearance: none; + + // Styles + display: inline-block; + appearance: none; + width: 100%; + height: 0.5rem; + margin-bottom: calc(var(--spacing) * 0.5); + overflow: hidden; + + // Remove Firefox and Opera border + border: 0; + border-radius: var(--border-radius); + background-color: var(--progress-background-color); + + // IE10 uses `color` to set the bar background-color + color: var(--progress-color); + + &::-webkit-progress-bar { + border-radius: var(--border-radius); + background: none; + } + &[value]::-webkit-progress-value { + background-color: var(--progress-color); + } + &::-moz-progress-bar { + background-color: var(--progress-color); + } + + // Indeterminate state + @media (prefers-reduced-motion: no-preference) { + &:indeterminate { + background: var(--progress-background-color) + linear-gradient( + to right, + var(--progress-color) 30%, + var(--progress-background-color) 30% + ) + top left / 150% 150% no-repeat; + animation: progress-indeterminate 1s linear infinite; + + &[value]::-webkit-progress-value { + background-color: transparent; + } + &::-moz-progress-bar { + background-color: transparent; + } + } + } +} + +[dir="rtl"] { + @media (prefers-reduced-motion: no-preference) { + progress:indeterminate { + animation-direction: reverse; + } + } +} + +@keyframes progress-indeterminate { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} diff --git a/app/assets/stylesheets/content/_button.scss b/app/assets/stylesheets/content/_button.scss new file mode 100644 index 0000000..075a166 --- /dev/null +++ b/app/assets/stylesheets/content/_button.scss @@ -0,0 +1,183 @@ +/** + * Button + */ + +// Reboot based on : +// - normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css +// - sanitize.css v13.0.0 | CC0 1.0 Universal | github.com/csstools/sanitize.css +// –––––––––––––––––––– + +// 1. Change the font styles in all browsers +// 2. Remove the margin on controls in Safari +// 3. Show the overflow in Edge +button { + margin: 0; // 2 + overflow: visible; // 3 + font-family: inherit; // 1 + text-transform: none; // 1 +} + +// Correct the inability to style buttons in iOS and Safari +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +// Pico +// –––––––––––––––––––– + +button { + display: block; + width: 100%; + margin-bottom: var(--spacing); +} + +[role="button"] { + display: inline-block; + text-decoration: none; +} + +button, +input[type="submit"], +input[type="button"], +input[type="reset"], +[role="button"] { + --background-color: var(--primary); + --border-color: var(--primary); + --color: var(--primary-inverse); + --box-shadow: var(--button-box-shadow, 0 0 0 rgba(0, 0, 0, 0)); + padding: var(--form-element-spacing-vertical) + var(--form-element-spacing-horizontal); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + outline: none; + background-color: var(--background-color); + box-shadow: var(--box-shadow); + color: var(--color); + font-weight: var(--font-weight); + font-size: 1rem; + line-height: var(--line-height); + text-align: center; + cursor: pointer; + + @if $enable-transitions { + transition: background-color var(--transition), + border-color var(--transition), color var(--transition), + box-shadow var(--transition); + } + + &:is([aria-current], :hover, :active, :focus) { + --background-color: var(--primary-hover); + --border-color: var(--primary-hover); + --box-shadow: var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)); + --color: var(--primary-inverse); + } + + &:focus { + --box-shadow: var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), + 0 0 0 var(--outline-width) var(--primary-focus); + } +} + +// .secondary, .contrast & .outline +@if $enable-classes { + + // Secondary + :is(button, input[type="submit"], input[type="button"], [role="button"]).secondary, + input[type="reset"] { + --background-color: var(--secondary); + --border-color: var(--secondary); + --color: var(--secondary-inverse); + cursor: pointer; + + &:is([aria-current], :hover, :active, :focus) { + --background-color: var(--secondary-hover); + --border-color: var(--secondary-hover); + --color: var(--secondary-inverse); + } + + &:focus { + --box-shadow: var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), + 0 0 0 var(--outline-width) var(--secondary-focus); + } + } + + // Contrast + :is(button, input[type="submit"], input[type="button"], [role="button"]).contrast { + --background-color: var(--contrast); + --border-color: var(--contrast); + --color: var(--contrast-inverse); + + &:is([aria-current], :hover, :active, :focus) { + --background-color: var(--contrast-hover); + --border-color: var(--contrast-hover); + --color: var(--contrast-inverse); + } + + &:focus { + --box-shadow: var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), + 0 0 0 var(--outline-width) var(--contrast-focus); + } + } + + // Outline (primary) + :is(button, input[type="submit"], input[type="button"], [role="button"]).outline, + input[type="reset"].outline { + --background-color: transparent; + --color: var(--primary); + + &:is([aria-current], :hover, :active, :focus) { + --background-color: transparent; + --color: var(--primary-hover); + } + } + + // Outline (secondary) + :is(button, input[type="submit"], input[type="button"], [role="button"]).outline.secondary, + input[type="reset"].outline { + --color: var(--secondary); + + &:is([aria-current], :hover, :active, :focus) { + --color: var(--secondary-hover); + } + } + + // Outline (contrast) + :is(button, input[type="submit"], input[type="button"], [role="button"]).outline.contrast { + --color: var(--contrast); + + &:is([aria-current], :hover, :active, :focus) { + --color: var(--contrast-hover); + } + } +} +@else { + // Secondary button without .class + input[type="reset"] { + --background-color: var(--secondary); + --border-color: var(--secondary); + --color: var(--secondary-inverse); + cursor: pointer; + + &:is([aria-current], :hover, :active, :focus) { + --background-color: var(--secondary-hover); + --border-color: var(--secondary-hover); + } + + &:focus { + --box-shadow: var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), + 0 0 0 var(--outline-width) var(--secondary-focus); + } + } +} + +// Button [disabled] +// Links without href are disabled by default +:where(button, [type="submit"], [type="button"], [type="reset"], [role="button"])[disabled], +:where(fieldset[disabled]) :is(button, [type="submit"], [type="button"], [type="reset"], [role="button"]), +a[role="button"]:not([href]) { + opacity: 0.5; + pointer-events: none; +} diff --git a/app/assets/stylesheets/content/_code.scss b/app/assets/stylesheets/content/_code.scss new file mode 100644 index 0000000..e03b191 --- /dev/null +++ b/app/assets/stylesheets/content/_code.scss @@ -0,0 +1,91 @@ +/** + * Code + */ + +// Reboot based on : +// - normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css +// - sanitize.css v13.0.0 | CC0 1.0 Universal | github.com/csstools/sanitize.css +// –––––––––––––––––––– + +// 1. Correct the inheritance and scaling of font size in all browsers +// 2. Correct the odd `em` font sizing in all browsers +pre, +code, +kbd, +samp { + font-size: 0.875em; // 2 + font-family: var(--font-family); // 1 +} + +// Prevent overflow of the container in all browsers (opinionated) +pre { + -ms-overflow-style: scrollbar; + overflow: auto; +} + +// Pico +// –––––––––––––––––––– + +pre, +code, +kbd { + border-radius: var(--border-radius); + background: var(--code-background-color); + color: var(--code-color); + font-weight: var(--font-weight); + line-height: initial; +} + +code, +kbd { + display: inline-block; + padding: 0.375rem 0.5rem; +} + +pre { + display: block; + margin-bottom: var(--spacing); + overflow-x: auto; + + > code { + display: block; + padding: var(--spacing); + background: none; + font-size: 14px; + line-height: var(--line-height); + } +} + +// Code Syntax +code { + // Tags + b { + color: var(--code-tag-color); + font-weight: var(--font-weight); + } + + // Properties + i { + color: var(--code-property-color); + font-style: normal; + } + + // Values + u { + color: var(--code-value-color); + text-decoration: none; + } + + // Comments + em { + color: var(--code-comment-color); + font-style: normal; + } +} + +// kbd +kbd { + background-color: var(--code-kbd-background-color); + color: var(--code-kbd-color); + vertical-align: baseline; +} diff --git a/app/assets/stylesheets/content/_embedded.scss b/app/assets/stylesheets/content/_embedded.scss new file mode 100644 index 0000000..d48293b --- /dev/null +++ b/app/assets/stylesheets/content/_embedded.scss @@ -0,0 +1,48 @@ +/** + * Embedded content + */ + +// Reboot based on : +// - normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css +// - sanitize.css v13.0.0 | CC0 1.0 Universal | github.com/csstools/sanitize.css +// –––––––––––––––––––– + +// Change the alignment on media elements in all browsers (opinionated) +:where(audio, canvas, iframe, img, svg, video) { + vertical-align: middle; +} + +// Add the correct display in IE 9- +audio, +video { + display: inline-block; +} + +// Add the correct display in iOS 4-7 +audio:not([controls]) { + display: none; + height: 0; +} + +// Remove the border on iframes in all browsers (opinionated) +:where(iframe) { + border-style: none; +} + +// 1. Remove the border on images inside links in IE 10. +// 2. Responsive by default +img { + max-width: 100%; // 2 + height: auto; // 2 + border-style: none; // 1 +} + +// Change the fill color to match the text color in all browsers (opinionated) +:where(svg:not([fill])) { + fill: currentColor; +} + +// Hide the overflow in IE +svg:not(:root) { + overflow: hidden; +} diff --git a/app/assets/stylesheets/content/_form-alt-input-types.scss b/app/assets/stylesheets/content/_form-alt-input-types.scss new file mode 100644 index 0000000..f22151e --- /dev/null +++ b/app/assets/stylesheets/content/_form-alt-input-types.scss @@ -0,0 +1,286 @@ +/** + * Form elements + * Alternatives input types (Not Checkboxes & Radios) + */ + +// Color +[type="color"] { + // Wrapper + @mixin color-wrapper { + padding: 0; + } + + &::-webkit-color-swatch-wrapper { + @include color-wrapper; + } + + &::-moz-focus-inner { + @include color-wrapper; + } + + // Swatch + @mixin color-swatch { + border: 0; + border-radius: calc(var(--border-radius) * 0.5); + } + + &::-webkit-color-swatch { + @include color-swatch; + } + + &::-moz-color-swatch { + @include color-swatch; + } +} + +// Date & Time +// :not() are needed to add Specificity and avoid !important on padding +input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"]) { + &:is([type="date"], [type="datetime-local"], [type="month"], [type="time"], [type="week"]) { + --icon-position: 0.75rem; + --icon-width: 1rem; + padding-right: calc(var(--icon-width) + var(--icon-position)); + background-image: var(--icon-date); + background-position: center right var(--icon-position); + background-size: var(--icon-width) auto; + background-repeat: no-repeat; + } + + // Time + &[type="time"] { + background-image: var(--icon-time); + } +} + +// Calendar picker +[type="date"], +[type="datetime-local"], +[type="month"], +[type="time"], +[type="week"] { + &::-webkit-calendar-picker-indicator { + width: var(--icon-width); + margin-right: calc(var(--icon-width) * -1); + margin-left: var(--icon-position); + opacity: 0; + } +} + +[dir="rtl"] + :is([type="date"], [type="datetime-local"], [type="month"], [type="time"], [type="week"]) { + text-align: right; +} + +// Calendar icons are hidden in Firefox +@if $enable-important { + @-moz-document url-prefix() { + [type="date"], + [type="datetime-local"], + [type="month"], + [type="time"], + [type="week"] { + padding-right: var(--form-element-spacing-horizontal) !important; + background-image: none !important; + } + } +} + +// File +[type="file"] { + --color: var(--muted-color); + padding: calc(var(--form-element-spacing-vertical) * 0.5) 0; + border: 0; + border-radius: 0; + background: none; + + @mixin file-selector-button { + --background-color: var(--secondary); + --border-color: var(--secondary); + --color: var(--secondary-inverse); + margin-right: calc(var(--spacing) / 2); + margin-left: 0; + margin-inline-start: 0; + margin-inline-end: calc(var(--spacing) / 2); + padding: calc(var(--form-element-spacing-vertical) * 0.5) + calc(var(--form-element-spacing-horizontal) * 0.5); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + outline: none; + background-color: var(--background-color); + box-shadow: var(--box-shadow); + color: var(--color); + font-weight: var(--font-weight); + font-size: 1rem; + line-height: var(--line-height); + text-align: center; + cursor: pointer; + + @if $enable-transitions { + transition: background-color var(--transition), + border-color var(--transition), color var(--transition), + box-shadow var(--transition); + } + + &:is(:hover, :active, :focus) { + --background-color: var(--secondary-hover); + --border-color: var(--secondary-hover); + } + } + + &::file-selector-button { + @include file-selector-button; + } + + &::-webkit-file-upload-button { + @include file-selector-button; + } + + &::-ms-browse { + @include file-selector-button; + } +} + +// Range +[type="range"] { + // Config + $height-track: 0.25rem; + $height-thumb: 1.25rem; + $border-thumb: 2px; + + // Styles + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 100%; + height: $height-thumb; + background: none; + + // Slider Track + @mixin slider-track { + width: 100%; + height: $height-track; + border-radius: var(--border-radius); + background-color: var(--range-border-color); + + @if $enable-transitions { + transition: background-color var(--transition), + box-shadow var(--transition); + } + } + + &::-webkit-slider-runnable-track { + @include slider-track; + } + + &::-moz-range-track { + @include slider-track; + } + + &::-ms-track { + @include slider-track; + } + + // Slider Thumb + @mixin slider-thumb { + -webkit-appearance: none; + width: $height-thumb; + height: $height-thumb; + margin-top: #{(-($height-thumb * 0.5) + ($height-track * 0.5))}; + border: $border-thumb solid var(--range-thumb-border-color); + border-radius: 50%; + background-color: var(--range-thumb-color); + cursor: pointer; + + @if $enable-transitions { + transition: background-color var(--transition), + transform var(--transition); + } + } + &::-webkit-slider-thumb { + @include slider-thumb; + } + + &::-moz-range-thumb { + @include slider-thumb; + } + + &::-ms-thumb { + @include slider-thumb; + } + + &:hover, + &:focus { + --range-border-color: var(--range-active-border-color); + --range-thumb-color: var(--range-thumb-hover-color); + } + + &:active { + --range-thumb-color: var(--range-thumb-active-color); + + // Slider Thumb + &::-webkit-slider-thumb { + transform: scale(1.25); + } + + &::-moz-range-thumb { + transform: scale(1.25); + } + + &::-ms-thumb { + transform: scale(1.25); + } + } +} + +// Search +// :not() are needed to add Specificity and avoid !important on padding +input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"]) { + &[type="search"] { + padding-inline-start: calc(var(--form-element-spacing-horizontal) + 1.75rem); + border-radius: 5rem; + background-image: var(--icon-search); + background-position: center left 1.125rem; + background-size: 1rem auto; + background-repeat: no-repeat; + + &[aria-invalid] { + @if $enable-important { + padding-inline-start: calc(var(--form-element-spacing-horizontal) + 1.75rem) !important; + } + @else { + padding-inline-start: calc(var(--form-element-spacing-horizontal) + 1.75rem); + } + background-position: center left 1.125rem, center right 0.75rem; + } + + &[aria-invalid="false"] { + background-image: var(--icon-search), var(--icon-valid); + } + + &[aria-invalid="true"] { + background-image: var(--icon-search), var(--icon-invalid); + } + } +} + +// Cancel button +[type="search"] { + &::-webkit-search-cancel-button { + -webkit-appearance: none; + display: none; + } +} + +[dir="rtl"] { + :where(input) { + &:not([type="checkbox"], [type="radio"], [type="range"], [type="file"]) { + &[type="search"] { + background-position: center right 1.125rem; + + &[aria-invalid] { + background-position: center right 1.125rem, center left 0.75rem; + } + } + } + } +} diff --git a/app/assets/stylesheets/content/_form-checkbox-radio.scss b/app/assets/stylesheets/content/_form-checkbox-radio.scss new file mode 100644 index 0000000..c83d35e --- /dev/null +++ b/app/assets/stylesheets/content/_form-checkbox-radio.scss @@ -0,0 +1,138 @@ +/** + * Form elements + * Checkboxes & Radios + */ + +[type="checkbox"], +[type="radio"] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 1.25em; + height: 1.25em; + margin-top: -0.125em; + margin-right: 0.375em; + margin-left: 0; + margin-inline-start: 0; + margin-inline-end: 0.375em; + border-width: var(--border-width); + font-size: inherit; + vertical-align: middle; + cursor: pointer; + + &::-ms-check { + display: none; // unstyle IE checkboxes + } + + &:checked, + &:checked:active, + &:checked:focus { + --background-color: var(--primary); + --border-color: var(--primary); + background-image: var(--icon-checkbox); + background-position: center; + background-size: 0.75em auto; + background-repeat: no-repeat; + } + + & ~ label { + display: inline-block; + margin-right: 0.375em; + margin-bottom: 0; + cursor: pointer; + } +} + +// Checkboxes +[type="checkbox"] { + &:indeterminate { + --background-color: var(--primary); + --border-color: var(--primary); + background-image: var(--icon-minus); + background-position: center; + background-size: 0.75em auto; + background-repeat: no-repeat; + } +} + +// Radios +[type="radio"] { + border-radius: 50%; + + &:checked, + &:checked:active, + &:checked:focus { + --background-color: var(--primary-inverse); + border-width: 0.35em; + background-image: none; + } +} + +// Switchs +[type="checkbox"][role="switch"] { + --background-color: var(--switch-background-color); + --border-color: var(--switch-background-color); + --color: var(--switch-color); + + // Config + $switch-height: 1.25em; + $switch-width: 2.25em; + $switch-transition: 0.1s ease-in-out; + + // Styles + width: $switch-width; + height: $switch-height; + border: var(--border-width) solid var(--border-color); + border-radius: $switch-height; + background-color: var(--background-color); + line-height: $switch-height; + + &:focus { + --background-color: var(--switch-background-color); + --border-color: var(--switch-background-color); + } + + &:checked { + --background-color: var(--switch-checked-background-color); + --border-color: var(--switch-checked-background-color); + } + + &:before { + display: block; + width: calc(#{$switch-height} - (var(--border-width) * 2)); + height: 100%; + border-radius: 50%; + background-color: var(--color); + content: ""; + + @if $enable-transitions { + transition: margin $switch-transition; + } + } + + &:checked { + background-image: none; + + &::before { + margin-left: calc(#{$switch-width * 0.5} - var(--border-width)); + margin-inline-start: calc(#{$switch-width * 0.5} - var(--border-width)); + } + } +} + +// Aria-invalid +[type="checkbox"], +[type="checkbox"]:checked, +[type="radio"], +[type="radio"]:checked, +[type="checkbox"][role="switch"], +[type="checkbox"][role="switch"]:checked { + + &[aria-invalid="false"] { + --border-color: var(--form-element-valid-border-color); + } + + &[aria-invalid="true"] { + --border-color: var(--form-element-invalid-border-color); + } +}
\ No newline at end of file diff --git a/app/assets/stylesheets/content/_form.scss b/app/assets/stylesheets/content/_form.scss new file mode 100644 index 0000000..1dda8f3 --- /dev/null +++ b/app/assets/stylesheets/content/_form.scss @@ -0,0 +1,352 @@ +/** + * Form elements + */ + +// Reboot based on : +// - normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css +// - sanitize.css v13.0.0 | CC0 1.0 Universal | github.com/csstools/sanitize.css +// –––––––––––––––––––– + +// 1. Change the font styles in all browsers +// 2. Remove the margin in Firefox and Safari +input, +optgroup, +select, +textarea { + margin: 0; // 2 + font-size: 1rem; // 1 + line-height: var(--line-height); // 1 + font-family: inherit; // 1 + letter-spacing: inherit; // 2 +} + +// Show the overflow in IE. +input { + overflow: visible; +} + +// Remove the inheritance of text transform in Edge, Firefox, and IE +select { + text-transform: none; +} + +// 1. Correct the text wrapping in Edge and IE +// 2. Correct the color inheritance from `fieldset` elements in IE +// 3. Remove the padding so developers are not caught out when they zero out +// `fieldset` elements in all browsers +legend { + max-width: 100%; // 1 + padding: 0; // 3 + color: inherit; // 2 + white-space: normal; // 1 +} + +// 1. Remove the default vertical scrollbar in IE +textarea { + overflow: auto; // 1 +} + +// Remove the padding in IE 10 +[type="checkbox"], +[type="radio"] { + padding: 0; +} + +// Correct the cursor style of increment and decrement buttons in Safari +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +// 1. Correct the odd appearance in Chrome and Safari +// 2. Correct the outline style in Safari +[type="search"] { + -webkit-appearance: textfield; // 1 + outline-offset: -2px; // 2 +} + +// Remove the inner padding in Chrome and Safari on macOS +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +// 1. Correct the inability to style clickable types in iOS and Safari +// 2. Change font properties to `inherit` in Safari +::-webkit-file-upload-button { + -webkit-appearance: button; // 1 + font: inherit; // 2 +} + +// Remove the inner border and padding of focus outlines in Firefox +::-moz-focus-inner { + padding: 0; + border-style: none; +} + +// Remove the focus outline in Firefox +:-moz-focusring { + outline: none; +} + +// Remove the additional :invalid styles in Firefox +:-moz-ui-invalid { + box-shadow: none; +} + +// Change the inconsistent appearance in IE (opinionated) +::-ms-expand { + display: none; +} + +// Remove the border and padding in all browsers (opinionated) +[type="file"], +[type="range"] { + padding: 0; + border-width: 0; +} + +// Pico +// –––––––––––––––––––– + +// Force height for alternatives input types +input:not([type="checkbox"], [type="radio"], [type="range"]) { + height: calc( + (1rem * var(--line-height)) + (var(--form-element-spacing-vertical) * 2) + + (var(--border-width) * 2) + ); +} + +// Fieldset +fieldset { + margin: 0; + margin-bottom: var(--spacing); + padding: 0; + border: 0; +} + +// Label & legend +label, +fieldset legend { + display: block; + margin-bottom: calc(var(--spacing) * 0.25); + font-weight: var(--form-label-font-weight, var(--font-weight)); +} + +// Blocks, 100% +input:not([type="checkbox"], [type="radio"]), +select, +textarea { + width: 100%; +} + +// Reset appearance (Not Checkboxes, Radios, Range and File) +input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"]), +select, +textarea { + appearance: none; + padding: var(--form-element-spacing-vertical) + var(--form-element-spacing-horizontal); +} + +// Commons styles +input, +select, +textarea { + --background-color: var(--form-element-background-color); + --border-color: var(--form-element-border-color); + --color: var(--form-element-color); + --box-shadow: none; + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + outline: none; + background-color: var(--background-color); + box-shadow: var(--box-shadow); + color: var(--color); + font-weight: var(--font-weight); + + @if $enable-transitions { + transition: background-color var(--transition), + border-color var(--transition), color var(--transition), + box-shadow var(--transition); + } +} + +// Active & Focus +input:not([type="submit"], [type="button"], [type="reset"], [type="checkbox"], [type="radio"], [readonly]), +:where(select, textarea) { + &:is(:active, :focus) { + --background-color: var(--form-element-active-background-color); + } +} + +// Active & Focus +input:not([type="submit"], [type="button"], [type="reset"], [role="switch"], [readonly]), +:where(select, textarea) { + &:is(:active, :focus) { + --border-color: var(--form-element-active-border-color); + } +} + +// Focus +input:not([type="submit"], [type="button"], [type="reset"], [type="range"], [type="file"], [readonly]), +select, +textarea { + &:focus { + --box-shadow: 0 0 0 var(--outline-width) var(--form-element-focus-color); + } +} + +// Disabled +input:not([type="submit"], [type="button"], [type="reset"])[disabled], +select[disabled], +textarea[disabled], +:where(fieldset[disabled]) :is(input:not([type="submit"], [type="button"], [type="reset"]), select, textarea) { + --background-color: var(--form-element-disabled-background-color); + --border-color: var(--form-element-disabled-border-color); + opacity: var(--form-element-disabled-opacity); + pointer-events: none; +} + +// Aria-invalid +:where(input, select, textarea) { + &:not([type="checkbox"], [type="radio"], [type="date"], [type="datetime-local"], [type="month"], [type="time"], [type="week"]) { + &[aria-invalid] { + @if $enable-important { + padding-right: calc( + var(--form-element-spacing-horizontal) + 1.5rem + ) !important; + padding-left: var(--form-element-spacing-horizontal); + padding-inline-start: var(--form-element-spacing-horizontal) !important; + padding-inline-end: calc( + var(--form-element-spacing-horizontal) + 1.5rem + ) !important; + } + @else { + padding-right: calc(var(--form-element-spacing-horizontal) + 1.5rem); + padding-left: var(--form-element-spacing-horizontal); + padding-inline-start: var(--form-element-spacing-horizontal); + padding-inline-end: calc(var(--form-element-spacing-horizontal) + 1.5rem); + } + background-position: center right 0.75rem; + background-size: 1rem auto; + background-repeat: no-repeat; + } + + &[aria-invalid="false"] { + background-image: var(--icon-valid); + } + + &[aria-invalid="true"] { + background-image: var(--icon-invalid); + } + } + + &[aria-invalid="false"] { + --border-color: var(--form-element-valid-border-color); + + &:is(:active, :focus) { + @if $enable-important { + --border-color: var(--form-element-valid-active-border-color) !important; + --box-shadow: 0 0 0 var(--outline-width) var(--form-element-valid-focus-color) !important; + } + @else { + --border-color: var(--form-element-valid-active-border-color); + --box-shadow: 0 0 0 var(--outline-width) var(--form-element-valid-focus-color); + } + } + } + + &[aria-invalid="true"] { + --border-color: var(--form-element-invalid-border-color); + + &:is(:active, :focus) { + @if $enable-important { + --border-color: var(--form-element-invalid-active-border-color) !important; + --box-shadow: 0 0 0 var(--outline-width) var(--form-element-invalid-focus-color) !important; + } + @else { + --border-color: var(--form-element-invalid-active-border-color); + --box-shadow: 0 0 0 var(--outline-width) var(--form-element-invalid-focus-color); + } + } + } +} + +[dir="rtl"] { + :where(input, select, textarea) { + &:not([type="checkbox"], [type="radio"]) { + &:is([aria-invalid], [aria-invalid="true"], [aria-invalid="false"] ){ + background-position: center left 0.75rem; + } + } + } +} + +// Placeholder +input::placeholder, +input::-webkit-input-placeholder, +textarea::placeholder, +textarea::-webkit-input-placeholder, +select:invalid { + color: var(--form-element-placeholder-color); + opacity: 1; +} + +// Margin bottom (Not Checkboxes and Radios) +input:not([type="checkbox"], [type="radio"]), +select, +textarea { + margin-bottom: var(--spacing); +} + +// Select +select { + // Unstyle the caret on `<select>`s in IE10+. + &::-ms-expand { + border: 0; + background-color: transparent; + } + + &:not([multiple], [size]) { + padding-right: calc(var(--form-element-spacing-horizontal) + 1.5rem); + padding-left: var(--form-element-spacing-horizontal); + padding-inline-start: var(--form-element-spacing-horizontal); + padding-inline-end: calc(var(--form-element-spacing-horizontal) + 1.5rem); + background-image: var(--icon-chevron); + background-position: center right 0.75rem; + background-size: 1rem auto; + background-repeat: no-repeat; + } +} + +[dir="rtl"] { + select { + &:not([multiple], [size]) { + background-position: center left 0.75rem; + } + } +} + +// Helper +$inputs: "input, select, textarea"; + +@if ($enable-classes and $enable-grid) { + $inputs: $inputs + ", .grid"; +} + +:where(#{$inputs}) { + + small { + display: block; + width: 100%; + margin-top: calc(var(--spacing) * -0.75); + margin-bottom: var(--spacing); + color: var(--muted-color); + } +} + +// Styles for Input inside a label +label { + > :where(input, select, textarea) { + margin-top: calc(var(--spacing) * 0.25); + } +} diff --git a/app/assets/stylesheets/content/_miscs.scss b/app/assets/stylesheets/content/_miscs.scss new file mode 100644 index 0000000..526baba --- /dev/null +++ b/app/assets/stylesheets/content/_miscs.scss @@ -0,0 +1,33 @@ +/** + * Miscs + */ + +// Reboot based on : +// - normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css +// - sanitize.css v13.0.0 | CC0 1.0 Universal | github.com/csstools/sanitize.css +// –––––––––––––––––––– + +// 1. Correct the inheritance of border color in Firefox +// 2. Add the correct box sizing in Firefox +hr { + height: 0; // 2 + border: 0; + border-top: 1px solid var(--muted-border-color); + color: inherit; // 1 +} + +// Add the correct display in IE 10+ +[hidden], +template { + @if $enable-important { + display: none !important; + } + @else { + display: none; + } +} + +// Add the correct display in IE 9- +canvas { + display: inline-block; +} diff --git a/app/assets/stylesheets/content/_table.scss b/app/assets/stylesheets/content/_table.scss new file mode 100644 index 0000000..d70cfd7 --- /dev/null +++ b/app/assets/stylesheets/content/_table.scss @@ -0,0 +1,50 @@ +/** + * Table + */ + +// Reboot based on : +// - normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css +// - sanitize.css v13.0.0 | CC0 1.0 Universal | github.com/csstools/sanitize.css +// –––––––––––––––––––– + +// 1. Collapse border spacing in all browsers (opinionated) +// 2. Remove text indentation from table contents in Chrome, Edge, and Safari +:where(table) { + width: 100%; + border-collapse: collapse; // 1 + border-spacing: 0; + text-indent: 0; // 2 +} + +// Pico +// –––––––––––––––––––– + +// Cells +th, +td { + padding: calc(var(--spacing) / 2) var(--spacing); + border-bottom: var(--border-width) solid var(--table-border-color); + color: var(--color); + font-weight: var(--font-weight); + font-size: var(--font-size); + text-align: left; + text-align: start; +} + +// Footer +tfoot { + th, + td { + border-top: var(--border-width) solid var(--table-border-color); + border-bottom: 0; + } +} + +// Striped +table { + &[role="grid"] { + tbody tr:nth-child(odd) { + background-color: var(--table-row-stripped-background-color); + } + } +} diff --git a/app/assets/stylesheets/content/_typography.scss b/app/assets/stylesheets/content/_typography.scss new file mode 100644 index 0000000..a9ed71b --- /dev/null +++ b/app/assets/stylesheets/content/_typography.scss @@ -0,0 +1,264 @@ +/** + * Typography + */ + +// Reboot based on : +// - normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css +// - sanitize.css v13.0.0 | CC0 1.0 Universal | github.com/csstools/sanitize.css +// –––––––––––––––––––– + +// Add the correct font weight in Chrome, Edge, and Safari +b, +strong { + font-weight: bolder; +} + +// Prevent `sub` and `sup` elements from affecting the line height in all browsers +sub, +sup { + position: relative; + font-size: 0.75em; + line-height: 0; + vertical-align: baseline; +} +sub { + bottom: -0.25em; +} +sup { + top: -0.5em; +} + +// Pico +// –––––––––––––––––––– + +address, +blockquote, +dl, +figure, +form, +ol, +p, +pre, +table, +ul { + margin-top: 0; + margin-bottom: var(--typography-spacing-vertical); + color: var(--color); + font-style: normal; + font-weight: var(--font-weight); + font-size: var(--font-size); +} + +// Links +// 1. Remove the gray background on active links in IE 10 +a, +[role="link"] { + --color: var(--primary); + --background-color: transparent; + outline: none; + background-color: var(--background-color); // 1 + color: var(--color); + text-decoration: var(--text-decoration); + + @if $enable-transitions { + transition: background-color var(--transition), color var(--transition), + text-decoration var(--transition), box-shadow var(--transition); + } + + &:is([aria-current], :hover, :active, :focus) { + --color: var(--primary-hover); + --text-decoration: underline; + } + + &:focus { + --background-color: var(--primary-focus); + } + + @if $enable-classes { + // Secondary + &.secondary { + --color: var(--secondary); + + &:is([aria-current], :hover, :active, :focus) { + --color: var(--secondary-hover); + } + + &:focus { + --background-color: var(--secondary-focus); + } + } + + // Contrast + &.contrast { + --color: var(--contrast); + + &:is([aria-current], :hover, :active, :focus) { + --color: var(--contrast-hover); + } + + &:focus { + --background-color: var(--contrast-focus); + } + } + } +} + +// Headings +h1, +h2, +h3, +h4, +h5, +h6 { + margin-top: 0; + margin-bottom: var(--typography-spacing-vertical); + color: var(--color); + font-weight: var(--font-weight); + font-size: var(--font-size); + font-family: var(--font-family); +} + +h1 { + --color: var(--h1-color); +} +h2 { + --color: var(--h2-color); +} +h3 { + --color: var(--h3-color); +} +h4 { + --color: var(--h4-color); +} +h5 { + --color: var(--h5-color); +} +h6 { + --color: var(--h6-color); +} + +// Margin-top for headings after a typography block +:where(address, blockquote, dl, figure, form, ol, p, pre, table, ul) { + ~ :is(h1, h2, h3, h4, h5, h6) { + margin-top: var(--typography-spacing-vertical); + } +} + +// Heading group +@if $enable-classes == false { + hgroup { + margin-bottom: var(--typography-spacing-vertical); + + > * { + margin-bottom: 0; + } + + > *:last-child { + --color: var(--muted-color); + --font-weight: unset; + font-size: 1rem; + font-family: unset; + } + } +} + +@if $enable-classes { + hgroup, + .headings { + margin-bottom: var(--typography-spacing-vertical); + + > * { + margin-bottom: 0; + } + + > *:last-child { + --color: var(--muted-color); + --font-weight: unset; + font-size: 1rem; + font-family: unset; + } + } +} + +// Paragraphs +p { + margin-bottom: var(--typography-spacing-vertical); +} + +// Small +small { + font-size: var(--font-size); +} + +// Lists +:where(dl, ol, ul) { + padding-right: 0; + padding-left: var(--spacing); + padding-inline-start: var(--spacing); + padding-inline-end: 0; + + li { + margin-bottom: calc(var(--typography-spacing-vertical) * 0.25); + } +} + +// Margin-top for nested lists +// 1. Remove the margin on nested lists in Chrome, Edge, IE, and Safari +:where(dl, ol, ul) { + :is(dl, ol, ul) { + margin: 0; // 1 + margin-top: calc(var(--typography-spacing-vertical) * 0.25); + } +} + +ul li { + list-style: square; +} + +// Highlighted text +mark { + padding: 0.125rem 0.25rem; + background-color: var(--mark-background-color); + color: var(--mark-color); + vertical-align: baseline; +} + +// Blockquote +blockquote { + display: block; + margin: var(--typography-spacing-vertical) 0; + padding: var(--spacing); + border-right: none; + border-left: 0.25rem solid var(--blockquote-border-color); + border-inline-start: 0.25rem solid var(--blockquote-border-color); + border-inline-end: none; + + footer { + margin-top: calc(var(--typography-spacing-vertical) * 0.5); + color: var(--blockquote-footer-color); + } +} + +// Abbreviations +// 1. Remove underline decoration in Chrome, Edge, IE, Opera, and Safari +abbr[title] { + border-bottom: 1px dotted; + text-decoration: none; // 1 + cursor: help; +} + +// Ins +ins { + color: var(--ins-color); + text-decoration: none; +} + +// del +del { + color: var(--del-color); +} + +// selection +::selection { + background-color: var(--primary-focus); +} diff --git a/app/assets/stylesheets/layout/_container.scss b/app/assets/stylesheets/layout/_container.scss new file mode 100644 index 0000000..6a20bea --- /dev/null +++ b/app/assets/stylesheets/layout/_container.scss @@ -0,0 +1,42 @@ +@if ($enable-class-container and $enable-classes) { + /** + * Container + */ + + .container, + .container-fluid { + width: 100%; + margin-right: auto; + margin-left: auto; + padding-right: var(--spacing); + padding-left: var(--spacing); + } + + .container { + @if map-get($breakpoints, "sm") { + @media (min-width: map-get($breakpoints, "sm")) { + max-width: map-get($viewports, "sm"); + padding-right: 0; + padding-left: 0; + } + } + + @if map-get($breakpoints, "md") { + @media (min-width: map-get($breakpoints, "md")) { + max-width: map-get($viewports, "md"); + } + } + + @if map-get($breakpoints, "lg") { + @media (min-width: map-get($breakpoints, "lg")) { + max-width: map-get($viewports, "lg"); + } + } + + @if map-get($breakpoints, "xl") { + @media (min-width: map-get($breakpoints, "xl")) { + max-width: map-get($viewports, "xl"); + } + } + } +} diff --git a/app/assets/stylesheets/layout/_document.scss b/app/assets/stylesheets/layout/_document.scss new file mode 100644 index 0000000..1607659 --- /dev/null +++ b/app/assets/stylesheets/layout/_document.scss @@ -0,0 +1,48 @@ +/** + * Document + * Content-box & Responsive typography + */ + +// Reboot based on : +// - normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css +// - sanitize.css v13.0.0 | CC0 1.0 Universal | github.com/csstools/sanitize.css +// –––––––––––––––––––– + +// 1. Add border box sizing in all browsers (opinionated) +// 2. Backgrounds do not repeat by default (opinionated) +*, +*::before, +*::after { + box-sizing: border-box; // 1 + background-repeat: no-repeat; // 2 +} + +// 1. Add text decoration inheritance in all browsers (opinionated) +// 2. Add vertical alignment inheritance in all browsers (opinionated) +::before, +::after { + text-decoration: inherit; // 1 + vertical-align: inherit; // 2 +} + +// 1. Use the default cursor in all browsers (opinionated) +// 2. Change the line height in all browsers (opinionated) +// 3. Breaks words to prevent overflow in all browsers (opinionated) +// 4. Use a 4-space tab width in all browsers (opinionated) +// 5. Remove the grey highlight on links in iOS (opinionated) +// 6. Prevent adjustments of font size after orientation changes in iOS +:where(:root) { + -webkit-tap-highlight-color: transparent; // 5 + -webkit-text-size-adjust: 100%; // 6 + text-size-adjust: 100%; // 6 + background-color: var(--background-color); + color: var(--color); + font-weight: var(--font-weight); + font-size: var(--font-size); + line-height: var(--line-height); // 2 + font-family: var(--font-family); + text-rendering: optimizeLegibility; + overflow-wrap: break-word; // 3 + cursor: default; // 1 + tab-size: 4; // 4 +} diff --git a/app/assets/stylesheets/layout/_grid.scss b/app/assets/stylesheets/layout/_grid.scss new file mode 100644 index 0000000..572312b --- /dev/null +++ b/app/assets/stylesheets/layout/_grid.scss @@ -0,0 +1,24 @@ +@if ($enable-classes and $enable-grid) { + /** + * Grid + * Minimal grid system with auto-layout columns + */ + + .grid { + grid-column-gap: var(--grid-spacing-horizontal); + grid-row-gap: var(--grid-spacing-vertical); + display: grid; + grid-template-columns: 1fr; + margin: 0; + + @if map-get($breakpoints, "lg") { + @media (min-width: map-get($breakpoints, "lg")) { + grid-template-columns: repeat(auto-fit, minmax(0%, 1fr)); + } + } + + & > * { + min-width: 0; // HACK for childs in overflow + } + } +} diff --git a/app/assets/stylesheets/layout/_scroller.scss b/app/assets/stylesheets/layout/_scroller.scss new file mode 100644 index 0000000..9b58ef7 --- /dev/null +++ b/app/assets/stylesheets/layout/_scroller.scss @@ -0,0 +1,16 @@ +/** + * Horizontal scroller (<figure>) + */ + +// Wrapper to make any content responsive across all viewports +figure { + display: block; + margin: 0; + padding: 0; + overflow-x: auto; + + figcaption { + padding: calc(var(--spacing) * 0.5) 0; + color: var(--muted-color); + } +} diff --git a/app/assets/stylesheets/layout/_section.scss b/app/assets/stylesheets/layout/_section.scss new file mode 100644 index 0000000..8bc6902 --- /dev/null +++ b/app/assets/stylesheets/layout/_section.scss @@ -0,0 +1,8 @@ +/** + * Section + * Responsive spacings for section + */ + +section { + margin-bottom: var(--block-spacing-vertical); +} diff --git a/app/assets/stylesheets/layout/_sectioning.scss b/app/assets/stylesheets/layout/_sectioning.scss new file mode 100644 index 0000000..00d73c5 --- /dev/null +++ b/app/assets/stylesheets/layout/_sectioning.scss @@ -0,0 +1,70 @@ +/** + * Sectioning + * Container and responsive spacings for header, main, footer + */ + +// Reboot based on : +// - normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css +// - sanitize.css v13.0.0 | CC0 1.0 Universal | github.com/csstools/sanitize.css +// –––––––––––––––––––– + +// Render the `main` element consistently in IE +main { + display: block; +} + +// Pico +// –––––––––––––––––––– + +// 1. Remove the margin in all browsers (opinionated) +#{$semantic-root-element} { + width: 100%; + margin: 0; // 1 + + > header, + > main, + > footer { + width: 100%; + margin-right: auto; + margin-left: auto; + + // Semantic container + @if $enable-semantic-container { + padding: var(--block-spacing-vertical) var(--block-spacing-horizontal); + + // Centered viewport + @if $enable-viewport { + @if map-get($breakpoints, "sm") and $enable-viewport { + @media (min-width: map-get($breakpoints, "sm")) { + max-width: map-get($viewports, "sm"); + padding-right: 0; + padding-left: 0; + } + } + + @if map-get($breakpoints, "md") and $enable-viewport { + @media (min-width: map-get($breakpoints, "md")) { + max-width: map-get($viewports, "md"); + } + } + + @if map-get($breakpoints, "lg") and $enable-viewport { + @media (min-width: map-get($breakpoints, "lg")) { + max-width: map-get($viewports, "lg"); + } + } + + @if map-get($breakpoints, "xl") and $enable-viewport { + @media (min-width: map-get($breakpoints, "xl")) { + max-width: map-get($viewports, "xl"); + } + } + } + } + + // Semantic container + @else { + padding: var(--block-spacing-vertical) 0; + } + } +} diff --git a/app/assets/stylesheets/pico.classless.scss b/app/assets/stylesheets/pico.classless.scss new file mode 100644 index 0000000..c5ea832 --- /dev/null +++ b/app/assets/stylesheets/pico.classless.scss @@ -0,0 +1,13 @@ +// Config +// -------------------- + +// Enable <header>, <main>, <footer> inside $semantic-root-element as containers +$enable-semantic-container: true; + +// Enable .classes +$enable-classes: false; + +// Pico Lib +// -------------------- + +@import "pico"; diff --git a/app/assets/stylesheets/pico.fluid.classless.scss b/app/assets/stylesheets/pico.fluid.classless.scss new file mode 100644 index 0000000..ef6af04 --- /dev/null +++ b/app/assets/stylesheets/pico.fluid.classless.scss @@ -0,0 +1,16 @@ +// Config +// -------------------- + +// Enable <header>, <main>, <footer> inside $semantic-root-element as containers +$enable-semantic-container: true; + +// Enable a centered viewport for <header>, <main>, <footer> inside $enable-semantic-container +$enable-viewport: false; + +// Enable .classes +$enable-classes: false; + +// Pico Lib +// -------------------- + +@import "pico"; diff --git a/app/assets/stylesheets/pico.scss b/app/assets/stylesheets/pico.scss new file mode 100644 index 0000000..b248799 --- /dev/null +++ b/app/assets/stylesheets/pico.scss @@ -0,0 +1,43 @@ +/*! + * Pico CSS v1.5.11 (https://picocss.com) + * Copyright 2019-2023 - Licensed under MIT + */ + +// Config +@import "variables"; + +// Theming +@import "themes/default"; + +// Layout +@import "layout/document"; // html +@import "layout/sectioning"; // body, header, main, footer +@import "layout/container"; // .container, .container-fluid +@import "layout/section"; // section +@import "layout/grid"; // .grid +@import "layout/scroller"; // figure + +// Content +@import "content/typography"; // a, headings, p, ul, blockquote, ... +@import "content/embedded"; // audio, canvas, iframe, img, svg, video +@import "content/button"; // button, a[role=button], type=button, type=submit ... +@import "content/form"; // input, select, textarea, label, fieldset, legend +@import "content/form-checkbox-radio"; // type=checkbox, type=radio, role=switch +@import "content/form-alt-input-types"; // type=color, type=date, type=file, type=search, ... +@import "content/table"; // table, tr, td, ... +@import "content/code"; // pre, code, ... +@import "content/miscs"; // hr, template, [hidden], dialog, canvas + +// Components +@import "components/accordion"; // details, summary +@import "components/card"; // article +@import "components/modal"; // dialog +@import "components/nav"; // nav +@import "components/progress"; // progress +@import "components/dropdown"; // dropdown + +// Utilities +@import "utilities/loading"; // aria-busy=true +@import "utilities/tooltip"; // data-tooltip +@import "utilities/accessibility"; // -ms-touch-action, aria-* +@import "utilities/reduce-motion"; // prefers-reduced-motion diff --git a/app/assets/stylesheets/pico.slim.scss b/app/assets/stylesheets/pico.slim.scss new file mode 100644 index 0000000..cb9aa42 --- /dev/null +++ b/app/assets/stylesheets/pico.slim.scss @@ -0,0 +1,47 @@ +/*! + * Pico CSS v1.5.11 (https://picocss.com) + * Copyright 2019-2023 - Licensed under MIT + * + * Slim version example + * You can export only the modules you need + */ + +// Config +// -------------------- + +// Enable responsive spacings for <header>, <main>, <footer>, <section>, <article> +$enable-responsive-spacings: false; + +// Enable transitions +$enable-transitions: false; + +// Enable overriding with !important +$enable-important: false; + +// Pico Lib +// -------------------- + +// Config +@import "variables"; + +// Theming +@import "themes/default"; + +// Layout +@import "layout/document"; // html +@import "layout/sectioning"; // body, header, main, footer +@import "layout/container"; // .container, .container-fluid +@import "layout/section"; // section +@import "layout/grid"; // .grid +@import "layout/scroller"; // figure + +// Content +@import "content/typography"; // a, headings, p, ul, blockquote, ... +@import "content/embedded"; // audio, canvas, iframe, img, svg, video +@import "content/button"; // button, a[role=button], type=button, type=submit ... +@import "content/form"; // input, select, textarea, label, fieldset, legend +@import "content/table"; // table, tr, td, ... + +// Utilities +@import "utilities/accessibility"; // -ms-touch-action, aria-* +@import "utilities/reduce-motion"; // prefers-reduced-motion diff --git a/app/assets/stylesheets/postcss.config.js b/app/assets/stylesheets/postcss.config.js new file mode 100644 index 0000000..4eaff47 --- /dev/null +++ b/app/assets/stylesheets/postcss.config.js @@ -0,0 +1,9 @@ +module.exports = { + syntax: "postcss-scss", + map: false, + plugins: { + "css-declaration-sorter": { + order: "smacss" + } + } +}
\ No newline at end of file diff --git a/app/assets/stylesheets/themes/default.scss b/app/assets/stylesheets/themes/default.scss new file mode 100644 index 0000000..c96b7e7 --- /dev/null +++ b/app/assets/stylesheets/themes/default.scss @@ -0,0 +1,37 @@ +/** + * Theme: default + */ + +// Variables +@import "../variables"; +@import "default/colors"; + +// Commons styles +@import "default/styles"; + +// Light theme (Default) +// Can be forced with data-theme="light" +@import "default/light"; + +// Dark theme (Auto) +// Automatically enabled if user has Dark mode enabled +@import "default/dark"; +@media only screen and (prefers-color-scheme: dark) { + :root:not([data-theme]) { + @include dark; + } +} + +// Dark theme (Forced) +// Enabled if forced with data-theme="dark" +[data-theme="dark"] { + @include dark; +} + +// Accent-color +progress, +[type="checkbox"], +[type="radio"], +[type="range"] { + accent-color: var(--primary); +} diff --git a/app/assets/stylesheets/themes/default/_colors.scss b/app/assets/stylesheets/themes/default/_colors.scss new file mode 100644 index 0000000..19079ff --- /dev/null +++ b/app/assets/stylesheets/themes/default/_colors.scss @@ -0,0 +1,65 @@ +// Navy-Grey +$grey-hue: 205 !default; +$grey-50: hsl($grey-hue, 20%, 94%) !default; +$grey-100: hsl($grey-hue, 18%, 86%) !default; +$grey-200: hsl($grey-hue, 16%, 77%) !default; +$grey-300: hsl($grey-hue, 14%, 68%) !default; +$grey-400: hsl($grey-hue, 12%, 59%) !default; +$grey-500: hsl($grey-hue, 10%, 50%) !default; +$grey-600: hsl($grey-hue, 15%, 41%) !default; +$grey-700: hsl($grey-hue, 20%, 32%) !default; +$grey-800: hsl($grey-hue, 25%, 23%) !default; +$grey-900: hsl($grey-hue, 30%, 15%) !default; + +// Light Blue +$primary-hue: 195 !default; +$primary-50: hsl($primary-hue, 90%, 94%) !default; +$primary-100: hsl($primary-hue, 88%, 86%) !default; +$primary-200: hsl($primary-hue, 86%, 77%) !default; +$primary-300: hsl($primary-hue, 84%, 68%) !default; +$primary-400: hsl($primary-hue, 82%, 59%) !default; +$primary-500: hsl($primary-hue, 80%, 50%) !default; +$primary-600: hsl($primary-hue, 85%, 41%) !default; +$primary-700: hsl($primary-hue, 90%, 32%) !default; +$primary-800: hsl($primary-hue, 95%, 23%) !default; +$primary-900: hsl($primary-hue, 100%, 15%) !default; + +// Black & White +$black: #000 !default; +$white: #fff !default; + +// Amber +$amber-50: #fff8e1 !default; +$amber-100: #ffecb3 !default; +$amber-200: #ffe082 !default; +$amber-300: #ffd54f !default; +$amber-400: #ffca28 !default; +$amber-500: #ffc107 !default; +$amber-600: #ffb300 !default; +$amber-700: #ffa000 !default; +$amber-800: #ff8f00 !default; +$amber-900: #ff6f00 !default; + +// Green +$green-50: #e8f5e9 !default; +$green-100: #c8e6c9 !default; +$green-200: #a5d6a7 !default; +$green-300: #81c784 !default; +$green-400: #66bb6a !default; +$green-500: #4caf50 !default; +$green-600: #43a047 !default; +$green-700: #388e3c !default; +$green-800: #2e7d32 !default; +$green-900: #1b5e20 !default; + +// Red +$red-50: #ffebee !default; +$red-100: #ffcdd2 !default; +$red-200: #ef9a9a !default; +$red-300: #e57373 !default; +$red-400: #ef5350 !default; +$red-500: #f44336 !default; +$red-600: #e53935 !default; +$red-700: #d32f2f !default; +$red-800: #c62828 !default; +$red-900: #b71c1c !default; diff --git a/app/assets/stylesheets/themes/default/_dark.scss b/app/assets/stylesheets/themes/default/_dark.scss new file mode 100644 index 0000000..2bf12e2 --- /dev/null +++ b/app/assets/stylesheets/themes/default/_dark.scss @@ -0,0 +1,159 @@ +@import "../../functions"; + +// Default: Dark theme +@mixin dark { + --background-color: #{mix($black, $grey-900, 37.5%)}; + + // Texts colors + --color: #{$grey-200}; + --h1-color: #{$grey-50}; + --h2-color: #{mix($grey-100, $grey-50)}; + --h3-color: #{$grey-100}; + --h4-color: #{mix($grey-200, $grey-100)}; + --h5-color: #{$grey-200}; + --h6-color: #{mix($grey-300, $grey-200)}; + + // Muted colors + --muted-color: #{$grey-500}; + --muted-border-color: #{mix($grey-900, $grey-800, 75%)}; + + // Primary colors + --primary: #{$primary-600}; + --primary-hover: #{$primary-500}; + --primary-focus: #{rgba($primary-600, 0.25)}; + --primary-inverse: #{$white}; + + // Secondary colors + --secondary: #{$grey-600}; + --secondary-hover: #{$grey-500}; + --secondary-focus: #{rgba($grey-500, 0.25)}; + --secondary-inverse: #{$white}; + + // Contrast colors + --contrast: #{$grey-50}; + --contrast-hover: #{$white}; + --contrast-focus: #{rgba($grey-500, 0.25)}; + --contrast-inverse: #{$black}; + + // Highlighted text (<mark>) + --mark-background-color: #{mix($grey-300, $amber-300)}; + --mark-color: #{mix($black, $grey-900, 37.5%)}; + + // Inserted (<ins>) & Deleted (<ins>) + --ins-color: #{$green-700}; + --del-color: #{$red-800}; + + // Blockquote + --blockquote-border-color: var(--muted-border-color); + --blockquote-footer-color: var(--muted-color); + + // Button + // To disable box-shadow, remove the var or set to '0 0 0 rgba(0, 0, 0, 0)' + // Don't use, 'none, 'false, 'null', '0', etc. + --button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + + // Form elements + --form-element-background-color: #{mix($black, $grey-900, 37.5%)}; + --form-element-border-color: #{mix($grey-800, $grey-700)}; + --form-element-color: var(--color); + --form-element-placeholder-color: var(--muted-color); + --form-element-active-background-color: var(--form-element-background-color); + --form-element-active-border-color: var(--primary); + --form-element-focus-color: var(--primary-focus); + --form-element-disabled-background-color: #{$grey-800}; + --form-element-disabled-border-color: #{$grey-700}; + --form-element-disabled-opacity: 0.5; + --form-element-invalid-border-color: #{$red-900}; + --form-element-invalid-active-border-color: #{$red-800}; + --form-element-invalid-focus-color: #{rgba($red-800, 0.25)}; + --form-element-valid-border-color: #{$green-800}; + --form-element-valid-active-border-color: #{$green-700}; + --form-element-valid-focus-color: #{rgba($green-700, 0.25)}; + + // Switch (input[type="checkbox"][role="switch"]) + --switch-background-color: #{mix($grey-800, $grey-700)}; + --switch-color: var(--primary-inverse); + --switch-checked-background-color: var(--primary); + + // Range (input[type="range"]) + --range-border-color: #{mix($grey-900, $grey-800)}; + --range-active-border-color: #{$grey-800}; + --range-thumb-border-color: var(--background-color); + --range-thumb-color: var(--secondary); + --range-thumb-hover-color: var(--secondary-hover); + --range-thumb-active-color: var(--primary); + + // Table + --table-border-color: var(--muted-border-color); + --table-row-stripped-background-color: #{rgba($grey-500, 0.05)}; + + // Code + --code-background-color: #{mix($black, $grey-900, 12.5%)}; + --code-color: var(--muted-color); + --code-kbd-background-color: var(--contrast); + --code-kbd-color: var(--contrast-inverse); + --code-tag-color: #{hsl(330, 30%, 50%)}; + --code-property-color: #{hsl(185, 30%, 50%)}; + --code-value-color: #{hsl(40, 10%, 50%)}; + --code-comment-color: #{mix($grey-700, $grey-600)}; + + // Accordion (<details>) + --accordion-border-color: var(--muted-border-color); + --accordion-active-summary-color: var(--primary); + --accordion-close-summary-color: var(--color); + --accordion-open-summary-color: var(--muted-color); + + // Card (<article>) + $box-shadow-elevation: 1rem; + $box-shadow-blur-strengh: 6rem; + $box-shadow-opacity: 0.06; + --card-background-color: #{mix($black, $grey-900, 25%)}; + --card-border-color: var(--card-background-color); + --card-box-shadow: + #{($box-shadow-elevation * 0.5 * 0.029)} #{($box-shadow-elevation * 0.029)} #{($box-shadow-blur-strengh * 0.029)} #{rgba($black, ($box-shadow-opacity * 0.283))}, + #{($box-shadow-elevation * 0.5 * 0.067)} #{($box-shadow-elevation * 0.067)} #{($box-shadow-blur-strengh * 0.067)} #{rgba($black, ($box-shadow-opacity * 0.4))}, + #{($box-shadow-elevation * 0.5 * 0.125)} #{($box-shadow-elevation * 0.125)} #{($box-shadow-blur-strengh * 0.125)} #{rgba($black, ($box-shadow-opacity * 0.5))}, + #{($box-shadow-elevation * 0.5 * 0.225)} #{($box-shadow-elevation * 0.225)} #{($box-shadow-blur-strengh * 0.225)} #{rgba($black, ($box-shadow-opacity * 0.6))}, + #{($box-shadow-elevation * 0.5 * 0.417)} #{($box-shadow-elevation * 0.417)} #{($box-shadow-blur-strengh * 0.417)} #{rgba($black, ($box-shadow-opacity * 0.717))}, + #{($box-shadow-elevation * 0.5)} #{$box-shadow-elevation} #{$box-shadow-blur-strengh} #{rgba($black, $box-shadow-opacity)}, + 0 0 0 0.0625rem #{rgba($black, ($box-shadow-opacity * 0.25) )}; + --card-sectionning-background-color: #{mix($black, $grey-900, 12.5%)}; + + // Dropdown (<details role="list">) + --dropdown-background-color: #{$grey-900}; + --dropdown-border-color: #{mix($grey-900, $grey-800)}; + --dropdown-box-shadow: var(--card-box-shadow); + --dropdown-color: var(--color); + --dropdown-hover-background-color: #{rgba(mix($grey-900, $grey-800), 0.75)}; + + // Modal (<dialog>) + --modal-overlay-background-color: #{rgba(mix($grey-900, $grey-800), 0.8)}; + + // Progress + --progress-background-color: #{mix($grey-900, $grey-800)}; + --progress-color: var(--primary); + + // Loading ([aria-busy=true]) + --loading-spinner-opacity: 0.5; + + // Tooltip ([data-tooltip]) + --tooltip-background-color: var(--contrast); + --tooltip-color: var(--contrast-inverse); + + // Icons + --icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($white)}' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($grey-300)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + --icon-chevron-button: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($white)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + --icon-chevron-button-inverse: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($black)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($grey-500)}' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E"); + --icon-date: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($grey-300)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E"); + --icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($red-900)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); + --icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($white)}' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E"); + --icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($grey-300)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); + --icon-time: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($grey-300)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E"); + --icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($green-800)}' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + + // Document + color-scheme: dark; +} diff --git a/app/assets/stylesheets/themes/default/_light.scss b/app/assets/stylesheets/themes/default/_light.scss new file mode 100644 index 0000000..237fb8f --- /dev/null +++ b/app/assets/stylesheets/themes/default/_light.scss @@ -0,0 +1,159 @@ +@import "../../functions"; + +// Default: Light theme +[data-theme="light"], +:root:not([data-theme="dark"]) { + --background-color: #{$white}; + + // Texts colors + --color: #{$grey-700}; + --h1-color: #{$grey-900}; + --h2-color: #{mix($grey-900, $grey-800)}; + --h3-color: #{$grey-800}; + --h4-color: #{mix($grey-800, $grey-700)}; + --h5-color: #{$grey-700}; + --h6-color: #{mix($grey-700, $grey-600)}; + + // Muted colors + --muted-color: #{$grey-500}; + --muted-border-color: #{$grey-50}; + + // Primary colors + --primary: #{$primary-600}; + --primary-hover: #{$primary-700}; + --primary-focus: #{rgba($primary-600, 0.125)}; + --primary-inverse: #{$white}; + + // Secondary colors + --secondary: #{$grey-600}; + --secondary-hover: #{$grey-700}; + --secondary-focus: #{rgba($grey-600, 0.125)}; + --secondary-inverse: #{$white}; + + // Contrast colors + --contrast: #{$grey-900}; + --contrast-hover: #{$black}; + --contrast-focus: #{rgba($grey-600, 0.125)}; + --contrast-inverse: #{$white}; + + // Highlighted text (<mark>) + --mark-background-color: #{mix($amber-100, $amber-50)}; + --mark-color: #{mix($grey-900, $amber-900, 75%)}; + + // Inserted (<ins>) & Deleted (<ins>) + --ins-color: #{$green-700}; + --del-color: #{$red-800}; + + // Blockquote + --blockquote-border-color: var(--muted-border-color); + --blockquote-footer-color: var(--muted-color); + + // Button + // To disable box-shadow, remove the var or set to '0 0 0 rgba(0, 0, 0, 0)' + // Don't use, 'none, 'false, 'null', '0', etc. + --button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + + // Form elements + --form-element-background-color: transparent; + --form-element-border-color: #{$grey-300}; + --form-element-color: var(--color); + --form-element-placeholder-color: var(--muted-color); + --form-element-active-background-color: transparent; + --form-element-active-border-color: var(--primary); + --form-element-focus-color: var(--primary-focus); + --form-element-disabled-background-color: #{$grey-100}; + --form-element-disabled-border-color: #{$grey-300}; + --form-element-disabled-opacity: 0.5; + --form-element-invalid-border-color: #{$red-800}; + --form-element-invalid-active-border-color: #{$red-700}; + --form-element-invalid-focus-color: #{rgba($red-700, 0.125)}; + --form-element-valid-border-color: #{$green-700}; + --form-element-valid-active-border-color: #{$green-600}; + --form-element-valid-focus-color: #{rgba($green-600, 0.125)}; + + // Switch (input[type="checkbox"][role="switch"]) + --switch-background-color: #{$grey-200}; + --switch-color: var(--primary-inverse); + --switch-checked-background-color: var(--primary); + + // Range (input[type="range"]) + --range-border-color: #{$grey-100}; + --range-active-border-color: #{$grey-200}; + --range-thumb-border-color: var(--background-color); + --range-thumb-color: var(--secondary); + --range-thumb-hover-color: var(--secondary-hover); + --range-thumb-active-color: var(--primary); + + // Table + --table-border-color: var(--muted-border-color); + --table-row-stripped-background-color: #{mix($grey-50, $white)}; + + // Code + --code-background-color: #{$grey-50}; + --code-color: var(--muted-color); + --code-kbd-background-color: var(--contrast); + --code-kbd-color: var(--contrast-inverse); + --code-tag-color: #{hsl(330, 40%, 50%)}; + --code-property-color: #{hsl(185, 40%, 40%)}; + --code-value-color: #{hsl(40, 20%, 50%)}; + --code-comment-color: #{$grey-300}; + + // Accordion (<details>) + --accordion-border-color: var(--muted-border-color); + --accordion-close-summary-color: var(--color); + --accordion-open-summary-color: var(--muted-color); + + // Card (<article>) + $box-shadow-elevation: 1rem; + $box-shadow-blur-strengh: 6rem; + $box-shadow-opacity: 0.06; + --card-background-color: var(--background-color); + --card-border-color: var(--muted-border-color); + --card-box-shadow: + #{($box-shadow-elevation * 0.5 * 0.029)} #{($box-shadow-elevation * 0.029)} #{($box-shadow-blur-strengh * 0.029)} #{rgba($grey-900, ($box-shadow-opacity * 0.283))}, + #{($box-shadow-elevation * 0.5 * 0.067)} #{($box-shadow-elevation * 0.067)} #{($box-shadow-blur-strengh * 0.067)} #{rgba($grey-900, ($box-shadow-opacity * 0.4))}, + #{($box-shadow-elevation * 0.5 * 0.125)} #{($box-shadow-elevation * 0.125)} #{($box-shadow-blur-strengh * 0.125)} #{rgba($grey-900, ($box-shadow-opacity * 0.5))}, + #{($box-shadow-elevation * 0.5 * 0.225)} #{($box-shadow-elevation * 0.225)} #{($box-shadow-blur-strengh * 0.225)} #{rgba($grey-900, ($box-shadow-opacity * 0.6))}, + #{($box-shadow-elevation * 0.5 * 0.417)} #{($box-shadow-elevation * 0.417)} #{($box-shadow-blur-strengh * 0.417)} #{rgba($grey-900, ($box-shadow-opacity * 0.717))}, + #{($box-shadow-elevation * 0.5)} #{$box-shadow-elevation} #{$box-shadow-blur-strengh} #{rgba($grey-900, $box-shadow-opacity)}, + 0 0 0 0.0625rem #{rgba($grey-900, ($box-shadow-opacity * 0.25) )}; + --card-sectionning-background-color: #{mix($grey-50, $white, 25%)}; + + // Dropdown (<details role="list">) + --dropdown-background-color: #{mix($grey-50, $white, 25%)}; + --dropdown-border-color: #{mix($grey-100, $grey-50)}; + --dropdown-box-shadow: var(--card-box-shadow); + --dropdown-color: var(--color); + --dropdown-hover-background-color: #{$grey-50}; + + // Modal (<dialog>) + --modal-overlay-background-color: #{rgba($grey-100, 0.7)}; + + // Progress + --progress-background-color: #{$grey-100}; + --progress-color: var(--primary); + + // Loading ([aria-busy=true]) + --loading-spinner-opacity: 0.5; + + // Tooltip ([data-tooltip]) + --tooltip-background-color: var(--contrast); + --tooltip-color: var(--contrast-inverse); + + // Icons + --icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($white)}' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($grey-700)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + --icon-chevron-button: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($white)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + --icon-chevron-button-inverse: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($white)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($grey-500)}' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E"); + --icon-date: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($grey-700)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E"); + --icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($red-800)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); + --icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($white)}' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E"); + --icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($grey-700)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); + --icon-time: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($grey-700)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E"); + --icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{to-rgb($green-700)}' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + + // Document + color-scheme: light; +} diff --git a/app/assets/stylesheets/themes/default/_styles.scss b/app/assets/stylesheets/themes/default/_styles.scss new file mode 100644 index 0000000..3a0a46d --- /dev/null +++ b/app/assets/stylesheets/themes/default/_styles.scss @@ -0,0 +1,247 @@ +// Commons Styles +:root { + // Typography + --font-family: system-ui, -apple-system, "Segoe UI", "Roboto", "Ubuntu", + "Cantarell", "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; + --line-height: 1.5; + --font-weight: 400; + --font-size: 16px; + + // Responsive typography + @if $enable-responsive-typography { + @if map-get($breakpoints, "sm") { + @media (min-width: map-get($breakpoints, "sm")) { + --font-size: 17px; + } + } + + @if map-get($breakpoints, "md") { + @media (min-width: map-get($breakpoints, "md")) { + --font-size: 18px; + } + } + + @if map-get($breakpoints, "lg") { + @media (min-width: map-get($breakpoints, "lg")) { + --font-size: 19px; + } + } + + @if map-get($breakpoints, "xl") { + @media (min-width: map-get($breakpoints, "xl")) { + --font-size: 20px; + } + } + } + + // Borders + --border-radius: 0.25rem; + --border-width: 1px; + --outline-width: 3px; + + // Spacings + --spacing: 1rem; + + // Spacings for typography elements + --typography-spacing-vertical: 1.5rem; + + // Spacings for body > header, body > main, body > footer, section, article + --block-spacing-vertical: calc(var(--spacing) * 2); + --block-spacing-horizontal: var(--spacing); + + @if ($enable-classes and $enable-grid) { + --grid-spacing-vertical: 0; + --grid-spacing-horizontal: var(--spacing); + } + + // Spacings for form elements and button + --form-element-spacing-vertical: 0.75rem; + --form-element-spacing-horizontal: 1rem; + + // Spacings for nav component + --nav-element-spacing-vertical: 1rem; + --nav-element-spacing-horizontal: 0.5rem; + --nav-link-spacing-vertical: 0.5rem; + --nav-link-spacing-horizontal: 0.5rem; + + // Font weight for form labels & fieldsets legend + --form-label-font-weight: var(--font-weight); + + // Transitions + --transition: 0.2s ease-in-out; + + // Modal (<dialog>) + --modal-overlay-backdrop-filter: blur(0.25rem); +} + +// Responsives spacings +@if $enable-responsive-spacings { + // Sectioning + #{$semantic-root-element} > header, + #{$semantic-root-element} > main, + #{$semantic-root-element} > footer, + section { + @if map-get($breakpoints, "sm") { + @media (min-width: map-get($breakpoints, "sm")) { + --block-spacing-vertical: calc(var(--spacing) * 2.5); + } + } + + @if map-get($breakpoints, "md") { + @media (min-width: map-get($breakpoints, "md")) { + --block-spacing-vertical: calc(var(--spacing) * 3); + } + } + + @if map-get($breakpoints, "lg") { + @media (min-width: map-get($breakpoints, "lg")) { + --block-spacing-vertical: calc(var(--spacing) * 3.5); + } + } + + @if map-get($breakpoints, "xl") { + @media (min-width: map-get($breakpoints, "xl")) { + --block-spacing-vertical: calc(var(--spacing) * 4); + } + } + } + + // Card (<article>) + article { + @if map-get($breakpoints, "sm") { + @media (min-width: map-get($breakpoints, "sm")) { + --block-spacing-horizontal: calc(var(--spacing) * 1.25); + } + } + + @if map-get($breakpoints, "md") { + @media (min-width: map-get($breakpoints, "md")) { + --block-spacing-horizontal: calc(var(--spacing) * 1.5); + } + } + + @if map-get($breakpoints, "lg") { + @media (min-width: map-get($breakpoints, "lg")) { + --block-spacing-horizontal: calc(var(--spacing) * 1.75); + } + } + + @if map-get($breakpoints, "xl") { + @media (min-width: map-get($breakpoints, "xl")) { + --block-spacing-horizontal: calc(var(--spacing) * 2); + } + } + } + + // Modal + dialog > article { + + --block-spacing-vertical: calc(var(--spacing) * 2); + --block-spacing-horizontal: var(--spacing); + + @if map-get($breakpoints, "sm") { + @media (min-width: map-get($breakpoints, "sm")) { + --block-spacing-vertical: calc(var(--spacing) * 2.5); + --block-spacing-horizontal: calc(var(--spacing) * 1.25); + } + } + + @if map-get($breakpoints, "md") { + @media (min-width: map-get($breakpoints, "md")) { + --block-spacing-vertical: calc(var(--spacing) * 3); + --block-spacing-horizontal: calc(var(--spacing) * 1.5); + } + } + } +} + +// Link +a { + --text-decoration: none; + + // Secondary & Contrast + @if $enable-classes { + &.secondary, + &.contrast { + --text-decoration: underline; + } + } +} + +// Small +small { + --font-size: 0.875em; +} + +// Headings +h1, +h2, +h3, +h4, +h5, +h6 { + --font-weight: 700; +} + +h1 { + --font-size: 2rem; + --typography-spacing-vertical: 3rem; +} + +h2 { + --font-size: 1.75rem; + --typography-spacing-vertical: 2.625rem; +} + +h3 { + --font-size: 1.5rem; + --typography-spacing-vertical: 2.25rem; +} + +h4 { + --font-size: 1.25rem; + --typography-spacing-vertical: 1.874rem; +} + +h5 { + --font-size: 1.125rem; + --typography-spacing-vertical: 1.6875rem; +} + +// Forms elements +[type="checkbox"], +[type="radio"] { + --border-width: 2px; +} + +[type="checkbox"][role="switch"] { + --border-width: 3px; +} + +// Table +thead, +tfoot { + th, + td { + --border-width: 3px; + } +} + +:not(thead, tfoot) > * > td { + --font-size: 0.875em; +} + +// Code +pre, +code, +kbd, +samp { + --font-family: "Menlo", "Consolas", "Roboto Mono", "Ubuntu Monospace", + "Noto Mono", "Oxygen Mono", "Liberation Mono", monospace, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +kbd { + --font-weight: bolder; +} diff --git a/app/assets/stylesheets/utilities/_accessibility.scss b/app/assets/stylesheets/utilities/_accessibility.scss new file mode 100644 index 0000000..e97ed7e --- /dev/null +++ b/app/assets/stylesheets/utilities/_accessibility.scss @@ -0,0 +1,52 @@ +/** + * Accessibility & User interaction + */ + +// Based on : +// - normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css +// - sanitize.css v13.0.0 | CC0 1.0 Universal | github.com/csstools/sanitize.css +// –––––––––––––––––––– + +// Accessibility + +// Change the cursor on control elements in all browsers (opinionated) +[aria-controls] { + cursor: pointer; +} + +// Change the cursor on disabled, not-editable, or otherwise inoperable elements in all browsers (opinionated) +[aria-disabled="true"], +[disabled] { + cursor: not-allowed; +} + +// Change the display on visually hidden accessible elements in all browsers (opinionated) +[aria-hidden="false"][hidden] { + display: initial; +} + +[aria-hidden="false"][hidden]:not(:focus) { + clip: rect(0, 0, 0, 0); + position: absolute; +} + +// User interaction +// Remove the tapping delay in IE 10 +a, +area, +button, +input, +label, +select, +summary, +textarea, +[tabindex] { + -ms-touch-action: manipulation; +} + +// Pico +// –––––––––––––––––––– + +[dir="rtl"] { + direction: rtl; +} diff --git a/app/assets/stylesheets/utilities/_loading.scss b/app/assets/stylesheets/utilities/_loading.scss new file mode 100644 index 0000000..c6f3afe --- /dev/null +++ b/app/assets/stylesheets/utilities/_loading.scss @@ -0,0 +1,58 @@ +/** + * Loading ([aria-busy=true]) + */ + + +// Cursor +[aria-busy="true"] { + cursor: progress; +} + +// Everyting except form elements +[aria-busy="true"]:not(input, select, textarea, html) { + + &::before { + display: inline-block; + width: 1em; + height: 1em; + border: 0.1875em solid currentColor; + border-radius: 1em; + border-right-color: transparent; + content: ""; + vertical-align: text-bottom; + vertical-align: -.125em; // Visual alignment + animation: spinner 0.75s linear infinite; + opacity: var(--loading-spinner-opacity); + } + + &:not(:empty) { + &::before { + margin-right: calc(var(--spacing) * 0.5); + margin-left: 0; + margin-inline-start: 0; + margin-inline-end: calc(var(--spacing) * 0.5); + } + } + + &:empty { + text-align: center; + } +} + +// Buttons and links +button, +input[type="submit"], +input[type="button"], +input[type="reset"], +a { + &[aria-busy="true"] { + pointer-events: none; + } +} + +// Animation: rotate +@keyframes spinner { + to { + transform: rotate(360deg); + } +} diff --git a/app/assets/stylesheets/utilities/_reduce-motion.scss b/app/assets/stylesheets/utilities/_reduce-motion.scss new file mode 100644 index 0000000..ecfd6fd --- /dev/null +++ b/app/assets/stylesheets/utilities/_reduce-motion.scss @@ -0,0 +1,27 @@ +@if $enable-transitions and $enable-important { + /** + * Reduce Motion Features + */ + + // Based on : + // - sanitize.css v13.0.0 | CC0 1.0 Universal | github.com/csstools/sanitize.css + // –––––––––––––––––––– + + // 1. Remove animations when motion is reduced (opinionated) + // 2. Remove fixed background attachments when motion is reduced (opinionated) + // 3. Remove timed scrolling behaviors when motion is reduced (opinionated) + // 4. Remove transitions when motion is reduced (opinionated) + @media (prefers-reduced-motion: reduce) { + *:not([aria-busy="true"]), + :not([aria-busy="true"])::before, + :not([aria-busy="true"])::after { + background-attachment: initial !important; // 2 + animation-duration: 1ms !important; // 1 + animation-delay: -1ms !important; // 1 + animation-iteration-count: 1 !important; // 1 + scroll-behavior: auto !important; // 3 + transition-delay: 0s !important; // 4 + transition-duration: 0s !important; // 4 + } + } +} diff --git a/app/assets/stylesheets/utilities/_tooltip.scss b/app/assets/stylesheets/utilities/_tooltip.scss new file mode 100644 index 0000000..d0355a3 --- /dev/null +++ b/app/assets/stylesheets/utilities/_tooltip.scss @@ -0,0 +1,278 @@ +/** + * Tooltip ([data-tooltip]) + */ + +[data-tooltip] { + position: relative; + + &:not(a, button, input) { + border-bottom: 1px dotted; + text-decoration: none; + cursor: help; + } + + &[data-placement="top"]::before, + &[data-placement="top"]::after, + &::before, + &::after { + display: block; + z-index: 99; + position: absolute; + bottom: 100%; + left: 50%; + padding: .25rem .5rem; + overflow: hidden; + transform: translate(-50%, -.25rem); + border-radius: var(--border-radius); + background: var(--tooltip-background-color); + content: attr(data-tooltip); + color: var(--tooltip-color); + font-style: normal; + font-weight: var(--font-weight); + font-size: .875rem; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + opacity: 0; + pointer-events: none; + } + + // Caret + &[data-placement="top"]::after, + &::after { + padding: 0; + transform: translate(-50%, 0rem); + border-top: .3rem solid; + border-right: .3rem solid transparent; + border-left: .3rem solid transparent; + border-radius: 0; + background-color: transparent; + content: ""; + color: var(--tooltip-background-color); + } + + &[data-placement="bottom"] { + &::before, + &::after { + top: 100%; + bottom: auto; + transform: translate(-50%, .25rem); + } + + &:after{ + transform: translate(-50%, -.3rem); + border: .3rem solid transparent; + border-bottom: .3rem solid; + } + } + + &[data-placement="left"] { + &::before, + &::after { + top: 50%; + right: 100%; + bottom: auto; + left: auto; + transform: translate(-.25rem, -50%); + } + + &:after{ + transform: translate(.3rem, -50%); + border: .3rem solid transparent; + border-left: .3rem solid; + } + } + + &[data-placement="right"] { + &::before, + &::after { + top: 50%; + right: auto; + bottom: auto; + left: 100%; + transform: translate(.25rem, -50%); + } + + &:after{ + transform: translate(-.3rem, -50%); + border: .3rem solid transparent; + border-right: .3rem solid; + } + } + + // Display + &:focus, + &:hover { + &::before, + &::after { + opacity: 1; + } + } + + + @if $enable-transitions { + + // Animations, excluding touch devices + @media (hover: hover) and (pointer: fine) { + &[data-placement="bottom"]:focus, + &[data-placement="bottom"]:hover + &:focus, + &:hover { + &::before, + &::after { + animation-duration: .2s; + animation-name: tooltip-slide-top; + } + + &::after { + animation-name: tooltip-caret-slide-top; + } + } + + &[data-placement="bottom"] { + &:focus, + &:hover { + &::before, + &::after { + animation-duration: .2s; + animation-name: tooltip-slide-bottom; + } + + &::after { + animation-name: tooltip-caret-slide-bottom; + } + } + } + + &[data-placement="left"] { + &:focus, + &:hover { + &::before, + &::after { + animation-duration: .2s; + animation-name: tooltip-slide-left; + } + + &::after { + animation-name: tooltip-caret-slide-left; + } + } + } + + &[data-placement="right"] { + &:focus, + &:hover { + &::before, + &::after { + animation-duration: .2s; + animation-name: tooltip-slide-right; + } + + &::after { + animation-name: tooltip-caret-slide-right; + } + } + } + } + + @keyframes tooltip-slide-top { + from { + transform: translate(-50%, .75rem); + opacity: 0; + } + to { + transform: translate(-50%, -.25rem); + opacity: 1; + } + } + + @keyframes tooltip-caret-slide-top { + from { + opacity: 0; + } + 50% { + transform: translate(-50%, -.25rem); + opacity: 0; + } + to { + transform: translate(-50%, 0rem); + opacity: 1; + } + } + + @keyframes tooltip-slide-bottom { + from { + transform: translate(-50%, -.75rem); + opacity: 0; + } + to { + transform: translate(-50%, .25rem); + opacity: 1; + } + } + + @keyframes tooltip-caret-slide-bottom { + from { + opacity: 0; + } + 50% { + transform: translate(-50%, -.5rem); + opacity: 0; + } + to { + transform: translate(-50%, -.3rem); + opacity: 1; + } + } + + @keyframes tooltip-slide-left { + from { + transform: translate(.75rem, -50%); + opacity: 0; + } + to { + transform: translate(-.25rem, -50%); + opacity: 1; + } + } + + @keyframes tooltip-caret-slide-left { + from { + opacity: 0; + } + 50% { + transform: translate(.05rem, -50%); + opacity: 0; + } + to { + transform: translate(.3rem, -50%); + opacity: 1; + } + } + + @keyframes tooltip-slide-right { + from { + transform: translate(-.75rem, -50%); + opacity: 0; + } + to { + transform: translate(.25rem, -50%); + opacity: 1; + } + } + + @keyframes tooltip-caret-slide-right { + from { + opacity: 0; + } + 50% { + transform: translate(-.05rem, -50%); + opacity: 0; + } + to { + transform: translate(-.3rem, -50%); + opacity: 1; + } + } + } +} diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..d672697 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000..0ff5442 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..09705d1 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/controllers/concerns/.keep diff --git a/app/controllers/leagues_controller.rb b/app/controllers/leagues_controller.rb new file mode 100644 index 0000000..102a9bd --- /dev/null +++ b/app/controllers/leagues_controller.rb @@ -0,0 +1,16 @@ +class LeaguesController < ApplicationController + def index + @leagues = League.all + end + + def show + league_id = params[:id] + @league = League.find_by(league_id: league_id) + + @q = LeaguePickScore.where(league_id: league_id).ransack(params[:q]) + @q.sorts = 'total desc' if @q.sorts.empty? + @scores = @q.result(distinct: true) + + @player_scores = PlayerScore.where(league_id: league_id) + end +end diff --git a/app/controllers/rodauth_controller.rb b/app/controllers/rodauth_controller.rb new file mode 100644 index 0000000..469db79 --- /dev/null +++ b/app/controllers/rodauth_controller.rb @@ -0,0 +1,4 @@ +class RodauthController < ApplicationController + # used by Rodauth for rendering views, CSRF protection, and running any + # registered action callbacks and rescue_from handlers +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/helpers/leagues_helper.rb b/app/helpers/leagues_helper.rb new file mode 100644 index 0000000..cf00be6 --- /dev/null +++ b/app/helpers/leagues_helper.rb @@ -0,0 +1,2 @@ +module LeaguesHelper +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/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..54ad4ca --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,11 @@ +// Import and register all your controllers from the importmap under controllers/* + +import { application } from "controllers/application" + +// Eager load all controllers defined in the import map under controllers/**/*_controller +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" +eagerLoadControllersFrom("controllers", application) + +// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) +// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" +// lazyLoadControllersFrom("controllers", application) diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..3c34c81 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/mailers/rodauth_mailer.rb b/app/mailers/rodauth_mailer.rb new file mode 100644 index 0000000..6782c87 --- /dev/null +++ b/app/mailers/rodauth_mailer.rb @@ -0,0 +1,62 @@ +class RodauthMailer < ApplicationMailer + default to: -> { @rodauth.email_to }, from: -> { @rodauth.email_from } + + def verify_account(name, account_id, key) + @rodauth = rodauth(name, account_id) { @verify_account_key_value = key } + @account = @rodauth.rails_account + + mail subject: @rodauth.email_subject_prefix + @rodauth.verify_account_email_subject + end + + def reset_password(name, account_id, key) + @rodauth = rodauth(name, account_id) { @reset_password_key_value = key } + @account = @rodauth.rails_account + + mail subject: @rodauth.email_subject_prefix + @rodauth.reset_password_email_subject + end + + def verify_login_change(name, account_id, key) + @rodauth = rodauth(name, account_id) { @verify_login_change_key_value = key } + @account = @rodauth.rails_account + @new_email = @account.login_change_key.login + + mail to: @new_email, subject: @rodauth.email_subject_prefix + @rodauth.verify_login_change_email_subject + end + + def password_changed(name, account_id) + @rodauth = rodauth(name, account_id) + @account = @rodauth.rails_account + + mail subject: @rodauth.email_subject_prefix + @rodauth.password_changed_email_subject + end + + # def reset_password_notify(name, account_id) + # @rodauth = rodauth(name, account_id) + # @account = @rodauth.rails_account + + # mail subject: @rodauth.email_subject_prefix + @rodauth.reset_password_notify_email_subject + # end + + # def email_auth(name, account_id, key) + # @rodauth = rodauth(name, account_id) { @email_auth_key_value = key } + # @account = @rodauth.rails_account + + # mail subject: @rodauth.email_subject_prefix + @rodauth.email_auth_email_subject + # end + + # def unlock_account(name, account_id, key) + # @rodauth = rodauth(name, account_id) { @unlock_account_key_value = key } + # @account = @rodauth.rails_account + + # mail subject: @rodauth.email_subject_prefix + @rodauth.unlock_account_email_subject + # end + + private + + def rodauth(name, account_id, &block) + instance = RodauthApp.rodauth(name).allocate + instance.instance_eval { @account = account_ds(account_id).first! } + instance.instance_eval(&block) if block + instance + end +end diff --git a/app/misc/rodauth_app.rb b/app/misc/rodauth_app.rb new file mode 100644 index 0000000..6372422 --- /dev/null +++ b/app/misc/rodauth_app.rb @@ -0,0 +1,25 @@ +class RodauthApp < Rodauth::Rails::App + # primary configuration + configure RodauthMain + + # secondary configuration + # configure RodauthAdmin, :admin + + route do |r| + rodauth.load_memory # autologin remembered users + + r.rodauth # route rodauth requests + + # ==> Authenticating requests + # Call `rodauth.require_account` for requests that you want to + # require authentication for. For example: + # + # # authenticate /dashboard/* and /account/* requests + # if r.path.start_with?("/dashboard") || r.path.start_with?("/account") + # rodauth.require_account + # end + + # ==> Secondary configurations + # r.rodauth(:admin) # route admin rodauth requests + end +end diff --git a/app/misc/rodauth_main.rb b/app/misc/rodauth_main.rb new file mode 100644 index 0000000..fb19c9a --- /dev/null +++ b/app/misc/rodauth_main.rb @@ -0,0 +1,182 @@ +require "sequel/core" + +class RodauthMain < Rodauth::Rails::Auth + configure do + # List of authentication features that are loaded. + enable :create_account, :verify_account, :verify_account_grace_period, + :login, :logout, :remember, + :reset_password, :change_password, :change_password_notify, + :change_login, :verify_login_change, :close_account, :argon2 + + # See the Rodauth documentation for the list of available config options: + # http://rodauth.jeremyevans.net/documentation.html + + # ==> General + # Initialize Sequel and have it reuse Active Record's database connection. + db Sequel.postgres(extensions: :activerecord_connection, keep_reference: false) + + # Avoid DB query that checks accounts table schema at boot time. + convert_token_id_to_integer? true + + # Change prefix of table and foreign key column names from default "account" + # accounts_table :users + # verify_account_table :user_verification_keys + # verify_login_change_table :user_login_change_keys + # reset_password_table :user_password_reset_keys + # remember_table :user_remember_keys + + # The secret key used for hashing public-facing tokens for various features. + # Defaults to Rails `secret_key_base`, but you can use your own secret key. + # hmac_secret "43a2cc4e1fbca7754f14061ff7bbcdb56db757e84a89af87e9fe388f0d39aa6faf4ff347f07c50b95e4cff27a4fe15111960aeaf859ba559e4cb6bba94e38ad9" + + # Use a rotatable password pepper when hashing passwords with Argon2. + # argon2_secret { hmac_secret } + + # Since we're using argon2, prevent loading the bcrypt gem to save memory. + require_bcrypt? false + + # Use path prefix for all routes. + # prefix "/auth" + + # Specify the controller used for view rendering, CSRF, and callbacks. + rails_controller { RodauthController } + + # Make built-in page titles accessible in your views via an instance variable. + title_instance_variable :@page_title + + # Store account status in an integer column without foreign key constraint. + account_status_column :status + + # Store password hash in a column instead of a separate table. + account_password_hash_column :password_hash + + # Set password when creating account instead of when verifying. + verify_account_set_password? false + + # Change some default param keys. + login_param "email" + login_confirm_param "email-confirm" + # password_confirm_param "confirm_password" + + # Redirect back to originally requested location after authentication. + # login_return_to_requested_location? true + # two_factor_auth_return_to_requested_location? true # if using MFA + + # Autologin the user after they have reset their password. + # reset_password_autologin? true + + # Delete the account record when the user has closed their account. + # delete_account_on_close? true + + # Redirect to the app from login and registration pages if already logged in. + # already_logged_in { redirect login_redirect } + + # ==> Emails + # Use a custom mailer for delivering authentication emails. + create_reset_password_email do + RodauthMailer.reset_password(self.class.configuration_name, account_id, reset_password_key_value) + end + create_verify_account_email do + RodauthMailer.verify_account(self.class.configuration_name, account_id, verify_account_key_value) + end + create_verify_login_change_email do |_login| + RodauthMailer.verify_login_change(self.class.configuration_name, account_id, verify_login_change_key_value) + end + create_password_changed_email do + RodauthMailer.password_changed(self.class.configuration_name, account_id) + end + # create_reset_password_notify_email do + # RodauthMailer.reset_password_notify(self.class.configuration_name, account_id) + # end + # create_email_auth_email do + # RodauthMailer.email_auth(self.class.configuration_name, account_id, email_auth_key_value) + # end + # create_unlock_account_email do + # RodauthMailer.unlock_account(self.class.configuration_name, account_id, unlock_account_key_value) + # end + send_email do |email| + # queue email delivery on the mailer after the transaction commits + db.after_commit { email.deliver_later } + end + + # ==> Flash + # Match flash keys with ones already used in the Rails app. + # flash_notice_key :success # default is :notice + # flash_error_key :error # default is :alert + + # Override default flash messages. + # create_account_notice_flash "Your account has been created. Please verify your account by visiting the confirmation link sent to your email address." + # require_login_error_flash "Login is required for accessing this page" + # login_notice_flash nil + + # ==> Validation + # Override default validation error messages. + # no_matching_login_message "user with this email address doesn't exist" + # already_an_account_with_this_login_message "user with this email address already exists" + # password_too_short_message { "needs to have at least #{password_minimum_length} characters" } + # login_does_not_meet_requirements_message { "invalid email#{", #{login_requirement_message}" if login_requirement_message}" } + + # Passwords shorter than 8 characters are considered weak according to OWASP. + password_minimum_length 8 + # Having a maximum password length set prevents long password DoS attacks. + password_maximum_length 64 + + # Custom password complexity requirements (alternative to password_complexity feature). + # password_meets_requirements? do |password| + # super(password) && password_complex_enough?(password) + # end + # auth_class_eval do + # def password_complex_enough?(password) + # return true if password.match?(/\d/) && password.match?(/[^a-zA-Z\d]/) + # set_password_requirement_error_message(:password_simple, "requires one number and one special character") + # false + # end + # end + + # ==> Remember Feature + # Remember all logged in users. + after_login { remember_login } + + # Or only remember users that have ticked a "Remember Me" checkbox on login. + # after_login { remember_login if param_or_nil("remember") } + + # Extend user's remember period when remembered via a cookie + extend_remember_deadline? true + + # ==> Hooks + # Validate custom fields in the create account form. + # before_create_account do + # throw_error_status(422, "name", "must be present") if param("name").empty? + # end + + # Perform additional actions after the account is created. + # after_create_account do + # Profile.create!(account_id: account_id, name: param("name")) + # end + + # Do additional cleanup after the account is closed. + # after_close_account do + # Profile.find_by!(account_id: account_id).destroy + # end + + # ==> Redirects + # Redirect to home page after logout. + logout_redirect "/" + + # Redirect to wherever login redirects to after account verification. + verify_account_redirect { login_redirect } + + # Redirect to login page after password reset. + reset_password_redirect { login_path } + + # Ensure requiring login follows login route changes. + require_login_redirect { login_path } + + # ==> Deadlines + # Change default deadlines for some actions. + # verify_account_grace_period 3.days.to_i + # reset_password_deadline_interval Hash[hours: 6] + # verify_login_change_deadline_interval Hash[days: 2] + # remember_deadline_interval Hash[days: 30] + end +end diff --git a/app/models/account.rb b/app/models/account.rb new file mode 100644 index 0000000..4fcee6f --- /dev/null +++ b/app/models/account.rb @@ -0,0 +1,4 @@ +class Account < ApplicationRecord + include Rodauth::Model(RodauthMain) + enum :status, unverified: 1, verified: 2, closed: 3 +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..b63caeb --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/models/concerns/.keep diff --git a/app/models/league.rb b/app/models/league.rb new file mode 100644 index 0000000..ff967fc --- /dev/null +++ b/app/models/league.rb @@ -0,0 +1,2 @@ +class League < ApplicationRecord +end diff --git a/app/models/league_pick_score.rb b/app/models/league_pick_score.rb new file mode 100644 index 0000000..f7c6521 --- /dev/null +++ b/app/models/league_pick_score.rb @@ -0,0 +1,7 @@ +class LeaguePickScore < ApplicationRecord + def self.ransackable_attributes(auth_object = nil) + ["champion", "conference", "divisional", "league_id", "player", "playoffs", "superbowl", "team", "total", "win"] + end + + # private_class_method :ransackable_attributes +end diff --git a/app/models/player_score.rb b/app/models/player_score.rb new file mode 100644 index 0000000..a79e4a7 --- /dev/null +++ b/app/models/player_score.rb @@ -0,0 +1,2 @@ +class PlayerScore < ApplicationRecord +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000..2e2c0cc --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> + <head> + <title>Team Draft</title> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + </head> + + <body> + <nav class="container-fluid"> + <ul> + <li> + <a href="/" class="contrast"><strong>Team Draft</strong></a> + </li> + </ul> + </nav> + <main class="container"> + <%= yield %> + </main> + </body> +</html> diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..3aac900 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <style> + /* Email styles need to be inline */ + </style> + </head> + + <body> + <%= yield %> + </body> +</html> diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/leagues/index.html.erb b/app/views/leagues/index.html.erb new file mode 100644 index 0000000..bcfa1fd --- /dev/null +++ b/app/views/leagues/index.html.erb @@ -0,0 +1,3 @@ +<% @leagues.each do |league| %> + <p><%= link_to league.name, league_path(league) %></p> +<% end %> diff --git a/app/views/leagues/show.html.erb b/app/views/leagues/show.html.erb new file mode 100644 index 0000000..d7a43d3 --- /dev/null +++ b/app/views/leagues/show.html.erb @@ -0,0 +1,33 @@ +<div class="grid"> +<% @player_scores.each do |score| %> + <div><strong><%= score.player %> - <%= score.score %></strong></div> +<% end %> +</div> +<table role="grid"> + <thead> + <tr> + <th scope="col"><%= sort_link(@q, :team) %></th> + <th scope="col"><%= sort_link(@q, :player) %></th> + <th scope="col" style="width: 11em"><%= sort_link(@q, :total) %></th> + </tr> + </thead> + <tbody> + <% @scores.each do |score| %> + <tr> + <td scope="row"><%= score.team %></td> + <td><%= score.player %></td> + <td> + <%= score.total %> + <p> + <small><strong><abbr title="Wins">W</abbr>:</strong> <%= score.win %></small> + <% if score.playoffs > 0 %><small><strong><abbr title="Made the Playoffs">P</abbr>:</strong> <%= score.playoffs %></small><% end %> + <% if score.divisional > 0 %><small><strong><abbr title="Made the Divisional Round">D</abbr>:</strong> <%= score.divisional %></small><% end %> + <% if score.conference > 0 %><small><strong><abbr title="Made the Conference Championship">C</abbr>:</strong> <%= score.conference %></small><% end %> + <% if score.superbowl > 0 %><small><strong><abbr title="Made the Superbowl">S</abbr>:</strong> <%= score.superbowl %></small><% end %> + <% if score.champion > 0 %><small><strong><abbr title="Superbowl Winner">SW</abbr>:</strong> <%= score.champion %></small><% end %> + </p> + </td> + </tr> + <% end %> + </tbody> +</table> diff --git a/app/views/rodauth_mailer/email_auth.text.erb b/app/views/rodauth_mailer/email_auth.text.erb new file mode 100644 index 0000000..9b989fe --- /dev/null +++ b/app/views/rodauth_mailer/email_auth.text.erb @@ -0,0 +1,5 @@ +Someone has requested a login link for the account with this email +address. If you did not request a login link, please ignore this +message. If you requested a login link, please go to +<%= @rodauth.email_auth_email_link %> +to login to this account. diff --git a/app/views/rodauth_mailer/password_changed.text.erb b/app/views/rodauth_mailer/password_changed.text.erb new file mode 100644 index 0000000..2e48708 --- /dev/null +++ b/app/views/rodauth_mailer/password_changed.text.erb @@ -0,0 +1,2 @@ +Someone (hopefully you) has changed the password for the account +associated to this email address. diff --git a/app/views/rodauth_mailer/reset_password.text.erb b/app/views/rodauth_mailer/reset_password.text.erb new file mode 100644 index 0000000..fcfb698 --- /dev/null +++ b/app/views/rodauth_mailer/reset_password.text.erb @@ -0,0 +1,5 @@ +Someone has requested a password reset for the account with this email +address. If you did not request a password reset, please ignore this +message. If you requested a password reset, please go to +<%= @rodauth.reset_password_email_link %> +to reset the password for the account. diff --git a/app/views/rodauth_mailer/unlock_account.text.erb b/app/views/rodauth_mailer/unlock_account.text.erb new file mode 100644 index 0000000..3d24759 --- /dev/null +++ b/app/views/rodauth_mailer/unlock_account.text.erb @@ -0,0 +1,5 @@ +Someone has requested that the account with this email be unlocked. +If you did not request the unlocking of this account, please ignore this +message. If you requested the unlocking of this account, please go to +<%= @rodauth.unlock_account_email_link %> +to unlock this account. diff --git a/app/views/rodauth_mailer/verify_account.text.erb b/app/views/rodauth_mailer/verify_account.text.erb new file mode 100644 index 0000000..78ff6ad --- /dev/null +++ b/app/views/rodauth_mailer/verify_account.text.erb @@ -0,0 +1,4 @@ +Someone has created an account with this email address. If you did not create +this account, please ignore this message. If you created this account, please go to +<%= @rodauth.verify_account_email_link %> +to verify the account. diff --git a/app/views/rodauth_mailer/verify_login_change.text.erb b/app/views/rodauth_mailer/verify_login_change.text.erb new file mode 100644 index 0000000..693680b --- /dev/null +++ b/app/views/rodauth_mailer/verify_login_change.text.erb @@ -0,0 +1,10 @@ +Someone with an account has requested their login be changed to this email address: + +Old email: <%= @account.email %> + +New email: <%= @new_email %> + +If you did not request this login change, please ignore this message. If you +requested this login change, please go to +<%= @rodauth.verify_login_change_email_link %> +to verify the login change. diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000..50da5fd --- /dev/null +++ b/bin/bundle @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +if ! gem list foreman -i --silent; then + echo "Installing foreman..." + gem install foreman +fi + +exec foreman start -f Procfile.dev "$@" diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 0000000..67ef493 --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# If running the rails server then create or migrate existing database +if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/bin/importmap b/bin/importmap new file mode 100755 index 0000000..36502ab --- /dev/null +++ b/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..efc0377 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..4fbf10b --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..3cd5a9d --- /dev/null +++ b/bin/setup @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + puts "\n== Restarting application server ==" + system! "bin/rails restart" +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..4a3c09a --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..0a4d44d --- /dev/null +++ b/config/application.rb @@ -0,0 +1,28 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Teamdraft + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 7.1 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w(assets tasks)) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + config.active_record.schema_format = :sql + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..988a5dd --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000..ef9da8a --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: teamdraft_production diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000..59f0119 --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +dvaGG+w18b3mMPYsFbGZb5sAmAnJ18svQcasHEtQc8sQ/7xwRskiO7qvCyKWCsbN/7KNcZmQVyTalt9Z/I538Mdmh8J0Aw3eo5f5g+a5gKqW1tI3T4ELObOmnag/xlTwJg1eSMJFjyes5fAoSuCqXX1tKM7sgLriOMl/ftirXmd9djpRKurklAH+eT8KU1J9vmFAuCo+tePxGFvo0ELjhF2D046j2juMoAbR0qOYU/RdiFqgx8yYYtqjE3uYzrCSferOHtAdrZn+ICY+9txzeKjxvh8Q6HA+pQEe0iRWWT3oZtK30iChDIYfFoukWA55ChGwcSe6cPe0M6qqkUAH2WPpvtlQXQVxN84wUzhHLX2rp2St/WMCBB9KGeC9nUkxN02tiLGmaPdOsUEvkHncQ9QWXpPr--gEBOGXKSt6idrN+T--sIIV/OpvW93v1wBWYYiWCQ==
\ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..921a8dd --- /dev/null +++ b/config/database.yml @@ -0,0 +1,88 @@ +# PostgreSQL. Versions 9.3 and up are supported. +# +# Install the pg driver: +# gem install pg +# On macOS with Homebrew: +# gem install pg -- --with-pg-config=/usr/local/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +# +# Configure Using Gemfile +# gem "pg" +# +default: &default + adapter: postgresql + encoding: unicode + # For details on connection pooling, see Rails configuration guide + # https://guides.rubyonrails.org/configuring.html#database-pooling + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + username: teamdraft_website + schema_search_path: 'teamdraft,public' + host: <%= ENV["TEAMDRAFT_DATABASE_HOST"] %> + +development: + <<: *default + database: teamdraft + username: teamdraft + + # The specified database role being used to connect to PostgreSQL. + # To create additional roles in PostgreSQL see `$ createuser --help`. + # When left blank, PostgreSQL will use the default role. This is + # the same name as the operating system user running Rails. + #username: teamdraft + + # The password associated with the PostgreSQL role (username). + #password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + + # The TCP port the server listens on. Defaults to 5432. + # If your server runs on a different port number, change accordingly. + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # Defaults to warning. + #min_messages: notice + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: teamdraft_test + +# As with config/credentials.yml, you never want to store sensitive information, +# like your database password, in your source code. If your source code is +# ever seen by anyone, they now have access to your database. +# +# Instead, provide the password or a full connection URL as an environment +# variable when you boot the app. For example: +# +# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" +# +# If the connection URL is provided in the special DATABASE_URL environment +# variable, Rails will automatically merge its configuration values on top of +# the values provided in this file. Alternatively, you can specify a connection +# URL environment variable explicitly: +# +# production: +# url: <%= ENV["MY_APP_DATABASE_URL"] %> +# +# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full overview on how database connection configuration can be specified. +# +production: + <<: *default + database: teamdraft + username: teamdraft_website + password: <%= ENV["TEAMDRAFT_DATABASE_PASSWORD"] %> diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..cac5315 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..ec43c00 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,79 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing + config.server_timing = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Generate source maps + config.assets.debug = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..d1bf876 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,98 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment + # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. + # config.public_file_server.enabled = false + + # Compress CSS using a preprocessor. + # config.assets.css_compressor = :sass + + # Do not fall back to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = "wss://example.com/cable" + # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. + # config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Log to STDOUT by default + config.logger = ActiveSupport::Logger.new(STDOUT) + .tap { |logger| logger.formatter = ::Logger::Formatter.new } + .then { |logger| ActiveSupport::TaggedLogging.new(logger) } + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # "info" includes generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "teamdraft_production" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + config.hosts = ["teamdraft.sadbeast.com"] + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..adbb4a6 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,64 @@ +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 0000000..bc060ed --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,7 @@ +# Pin npm packages by running ./bin/importmap + +pin "application" +pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true +pin "@hotwired/stimulus", to: "stimulus.min.js" +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" +pin_all_from "app/javascript/controllers", under: "controllers" diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000..2eeef96 --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,12 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..b3076b3 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,25 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..c2d89e2 --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000..3860f65 --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb new file mode 100644 index 0000000..7db3b95 --- /dev/null +++ b/config/initializers/permissions_policy.rb @@ -0,0 +1,13 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide HTTP permissions policy. For further +# information see: https://developers.google.com/web/updates/2018/06/feature-policy + +# Rails.application.config.permissions_policy do |policy| +# policy.camera :none +# policy.gyroscope :none +# policy.microphone :none +# policy.usb :none +# policy.fullscreen :self +# policy.payment :self, "https://secure.example.com" +# end diff --git a/config/initializers/rodauth.rb b/config/initializers/rodauth.rb new file mode 100644 index 0000000..a832109 --- /dev/null +++ b/config/initializers/rodauth.rb @@ -0,0 +1,3 @@ +Rodauth::Rails.configure do |config| + config.app = "RodauthApp" +end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..6c349ae --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..8acf3fa --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,12 @@ +Rails.application.routes.draw do + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + get "leagues", to: "leagues#index", as: "leagues" + get "leagues/:id", to: "leagues#show", as: "league" + + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. + # Can be used by load balancers and uptime monitors to verify that the app is live. + get "up" => "rails/health#show", as: :rails_health_check + + # Defines the root path route ("/") + root "leagues#index" +end diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000..4942ab6 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/db/migrate/20240129044207_create_rodauth.rb b/db/migrate/20240129044207_create_rodauth.rb new file mode 100644 index 0000000..9c38cbf --- /dev/null +++ b/db/migrate/20240129044207_create_rodauth.rb @@ -0,0 +1,47 @@ +class CreateRodauth < ActiveRecord::Migration[7.1] + def change + enable_extension "citext" + + create_table :accounts do |t| + t.integer :status, null: false, default: 1 + t.citext :email, null: false + t.index :email, unique: true, where: "status IN (1, 2)" + t.string :password_hash + end + + # Used by the password reset feature + create_table :account_password_reset_keys, id: false do |t| + t.bigint :id, primary_key: true + t.foreign_key :accounts, column: :id + t.string :key, null: false + t.datetime :deadline, null: false + t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" } + end + + # Used by the account verification feature + create_table :account_verification_keys, id: false do |t| + t.bigint :id, primary_key: true + t.foreign_key :accounts, column: :id + t.string :key, null: false + t.datetime :requested_at, null: false, default: -> { "CURRENT_TIMESTAMP" } + t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" } + end + + # Used by the verify login change feature + create_table :account_login_change_keys, id: false do |t| + t.bigint :id, primary_key: true + t.foreign_key :accounts, column: :id + t.string :key, null: false + t.string :login, null: false + t.datetime :deadline, null: false + end + + # Used by the remember me feature + create_table :account_remember_keys, id: false do |t| + t.bigint :id, primary_key: true + t.foreign_key :accounts, column: :id + t.string :key, null: false + t.datetime :deadline, null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..fbe0b4e --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,126 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.1].define(version: 0) do + create_schema "teamdraft" + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + # Custom types defined in this database. + # Note that some types may not work with other database engines. Be careful if changing database. + create_enum "divisions", ["nfcn", "nfce", "nfcs", "nfcw", "afcn", "afce", "afcs", "afcw"] + create_enum "score_category", ["win", "playoffs", "divisional", "conference", "superbowl", "champion"] + + create_table "drafts", primary_key: "draft_id", id: :integer, default: nil, force: :cascade do |t| + t.integer "league_season_id", null: false + t.timestamptz "started_at", null: false + t.timestamptz "created_at", default: -> { "now()" }, null: false + t.timestamptz "modified_at", default: -> { "now()" }, null: false + end + + create_table "league_seasons", primary_key: "league_season_id", id: :integer, default: nil, force: :cascade do |t| + t.integer "season_id", limit: 2, null: false + t.integer "league_id", null: false + t.timestamptz "created_at", default: -> { "now()" }, null: false + t.timestamptz "modified_at", default: -> { "now()" }, null: false + end + + create_table "league_users", primary_key: "league_user_id", id: :bigint, default: nil, force: :cascade do |t| + t.integer "league_season_id", null: false + t.integer "user_id", null: false + t.timestamptz "created_at", default: -> { "now()" }, null: false + t.timestamptz "modified_at", default: -> { "now()" }, null: false + end + + create_table "leagues", primary_key: "league_id", id: :integer, default: nil, force: :cascade do |t| + t.text "name" + t.integer "user_id", null: false + t.timestamptz "created_at", default: -> { "now()" }, null: false + t.timestamptz "modified_at", default: -> { "now()" }, null: false + t.check_constraint "char_length(name) <= 255", name: "leagues_name_ck" + end + + create_table "picks", primary_key: "pick_id", id: :integer, default: nil, force: :cascade do |t| + t.integer "draft_id", null: false + t.integer "league_user_id", null: false + t.integer "team_id", limit: 2, null: false + t.boolean "auto", default: false, null: false + t.timestamptz "created_at", default: -> { "now()" }, null: false + t.timestamptz "modified_at", default: -> { "now()" }, null: false + end + + create_table "rankings", primary_key: "ranking_id", id: :integer, default: nil, force: :cascade do |t| + t.integer "season_id", null: false + t.integer "team_id", null: false + t.integer "rank", limit: 2, null: false + t.timestamptz "created_at", default: -> { "now()" }, null: false + t.timestamptz "modified_at", default: -> { "now()" }, null: false + end + + create_table "rosters", primary_key: "roster_id", id: :integer, default: nil, force: :cascade do |t| + t.bigint "league_user_id", null: false + t.integer "team_id", limit: 2, null: false + t.timestamptz "created_at", default: -> { "now()" }, null: false + t.timestamptz "modified_at", default: -> { "now()" }, null: false + end + + create_table "scores", primary_key: "score_id", id: :integer, default: nil, force: :cascade do |t| + t.integer "season_id", null: false + t.integer "team_id", null: false + t.integer "week", limit: 2, null: false + t.enum "category", default: "win", null: false, enum_type: "score_category" + t.timestamptz "scored_at", default: -> { "now()" }, null: false + t.timestamptz "created_at", default: -> { "now()" }, null: false + t.timestamptz "modified_at", default: -> { "now()" }, null: false + + t.unique_constraint ["season_id", "team_id", "week", "category"], name: "scores_season_id_team_id_week_category_key" + end + + create_table "seasons", primary_key: "season_id", id: { type: :integer, limit: 2, default: nil }, force: :cascade do |t| + t.timestamptz "started_at", null: false + t.timestamptz "ended_at", null: false + t.timestamptz "created_at", default: -> { "now()" }, null: false + t.timestamptz "modified_at", default: -> { "now()" }, null: false + end + + create_table "teams", primary_key: "team_id", id: { type: :integer, limit: 2, default: nil }, force: :cascade do |t| + t.text "name", null: false + t.enum "division", null: false, enum_type: "divisions" + t.text "external_id" + t.timestamptz "created_at", default: -> { "now()" }, null: false + t.timestamptz "modified_at", default: -> { "now()" }, null: false + t.check_constraint "char_length(name) <= 255", name: "teams_name_ck" + end + + create_table "users", primary_key: "user_id", id: :integer, default: nil, force: :cascade do |t| + t.text "name" + t.text "password", null: false + t.timestamptz "created_at", default: -> { "now()" }, null: false + t.timestamptz "modified_at", default: -> { "now()" }, null: false + end + + add_foreign_key "drafts", "league_seasons", primary_key: "league_season_id", name: "drafts_league_seasons_fk" + add_foreign_key "league_seasons", "leagues", primary_key: "league_id", name: "league_seasons_leagues_fk" + add_foreign_key "league_seasons", "seasons", primary_key: "season_id", name: "league_seasons_seasons_fk" + add_foreign_key "league_users", "league_seasons", primary_key: "league_season_id", name: "league_users_league_seasons_fk" + add_foreign_key "league_users", "users", primary_key: "user_id", name: "league_users_users_fk" + add_foreign_key "leagues", "users", primary_key: "user_id", name: "leagues_users_fk" + add_foreign_key "picks", "drafts", primary_key: "draft_id", name: "picks_drafts_fk" + add_foreign_key "picks", "league_users", primary_key: "league_user_id", name: "picks_league_users_fk" + add_foreign_key "picks", "teams", primary_key: "team_id", name: "picks_teams_fk" + add_foreign_key "rankings", "seasons", primary_key: "season_id", name: "rankings_seasons_fk" + add_foreign_key "rosters", "league_users", primary_key: "league_user_id", name: "rosters_league_users_fk" + add_foreign_key "rosters", "teams", primary_key: "team_id", name: "rosters_teams_fk" + add_foreign_key "scores", "seasons", primary_key: "season_id", name: "scores_seasons_fk" + add_foreign_key "scores", "teams", primary_key: "team_id", name: "scores_teams_fk" +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000..4fbd6ed --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,9 @@ +# This file should ensure the existence of records required to run the application in every environment (production, +# development, test). The code here should be idempotent so that it can be executed at any point in every environment. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Example: +# +# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| +# MovieGenre.find_or_create_by!(name: genre_name) +# end diff --git a/db/structure.sql b/db/structure.sql new file mode 100644 index 0000000..a4668f9 --- /dev/null +++ b/db/structure.sql @@ -0,0 +1,1074 @@ +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: public; Type: SCHEMA; Schema: -; Owner: - +-- + +CREATE SCHEMA public; + + +-- +-- Name: SCHEMA public; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON SCHEMA public IS 'standard public schema'; + + +-- +-- Name: teamdraft; Type: SCHEMA; Schema: -; Owner: - +-- + +CREATE SCHEMA teamdraft; + + +-- +-- Name: divisions; Type: TYPE; Schema: teamdraft; Owner: - +-- + +CREATE TYPE teamdraft.divisions AS ENUM ( + 'nfcn', + 'nfce', + 'nfcs', + 'nfcw', + 'afcn', + 'afce', + 'afcs', + 'afcw' +); + + +-- +-- Name: score_category; Type: TYPE; Schema: teamdraft; Owner: - +-- + +CREATE TYPE teamdraft.score_category AS ENUM ( + 'win', + 'playoffs', + 'divisional', + 'conference', + 'superbowl', + 'champion' +); + + +-- +-- Name: category_score(teamdraft.score_category); Type: FUNCTION; Schema: teamdraft; Owner: - +-- + +CREATE FUNCTION teamdraft.category_score(category teamdraft.score_category) RETURNS smallint + LANGUAGE plpgsql + AS $$ +DECLARE +score smallint; +BEGIN + SELECT CASE + WHEN category = 'win' THEN 1 + WHEN category IN ('superbowl', 'conference') THEN 10 + ELSE 5 END + INTO score; + + RETURN score; +END; +$$; + + +-- +-- Name: create_league(text, timestamp with time zone); Type: FUNCTION; Schema: teamdraft; Owner: - +-- + +CREATE FUNCTION teamdraft.create_league(league_name text, start_at timestamp with time zone) RETURNS integer + LANGUAGE plpgsql + AS $$ +DECLARE +new_user_id integer; +new_league_id integer; +new_league_season_id integer; +new_draft_id integer; +BEGIN + INSERT INTO users (password) VALUES ('test') RETURNING user_id INTO new_user_id; + INSERT INTO leagues (name, user_id) VALUES (league_name, new_user_id) RETURNING league_id INTO new_league_id; + INSERT INTO league_seasons (season_id, league_id) VALUES ((SELECT season_id FROM seasons LIMIT 1), new_league_id) RETURNING league_season_id INTO new_league_season_id; + INSERT INTO league_users (league_season_id, user_id) VALUES (new_league_season_id, new_user_id); + INSERT INTO drafts (league_season_id, started_at) VALUES (new_league_season_id, start_at) RETURNING draft_id INTO new_draft_id; + RETURN new_draft_id; +END; +$$; + + +-- +-- Name: current_season(); Type: FUNCTION; Schema: teamdraft; Owner: - +-- + +CREATE FUNCTION teamdraft.current_season() RETURNS integer + LANGUAGE sql + AS $$ + SELECT season_id FROM teamdraft.seasons WHERE started_at <= now() AND ended_at > now() LIMIT 1; +$$; + + +-- +-- Name: current_week(); Type: FUNCTION; Schema: teamdraft; Owner: - +-- + +CREATE FUNCTION teamdraft.current_week() RETURNS smallint + LANGUAGE plpgsql + AS $$ +DECLARE +current_week smallint; +BEGIN + SELECT TRUNC(DATE_PART('day', now() - started_at) / 7)::smallint + 1 AS current_week + FROM teamdraft.seasons WHERE season_id = teamdraft.current_season() INTO current_week; + + RETURN current_week; +END; +$$; + + +-- +-- Name: record_score(integer, smallint, teamdraft.score_category, timestamp with time zone); Type: FUNCTION; Schema: teamdraft; Owner: - +-- + +CREATE FUNCTION teamdraft.record_score(team_id integer, week smallint, category teamdraft.score_category, scored_at timestamp with time zone) RETURNS integer + LANGUAGE plpgsql + AS $$ +DECLARE +new_score_id integer; + +BEGIN + INSERT INTO scores (season_id, team_id, week, category, scored_at) + VALUES (teamdraft.current_season(), team_id, week, category, scored_at) ON CONFLICT ON CONSTRAINT scores_season_id_team_id_week_category_key DO + --UPDATE SET scored_at = EXCLUDED.scored_at, modified_at = now() + NOTHING + RETURNING score_id INTO new_score_id; + RETURN new_score_id; +END; +$$; + + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: account_login_change_keys; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.account_login_change_keys ( + id bigint NOT NULL, + key character varying NOT NULL, + login character varying NOT NULL, + deadline timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: account_login_change_keys_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +CREATE SEQUENCE teamdraft.account_login_change_keys_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: account_login_change_keys_id_seq; Type: SEQUENCE OWNED BY; Schema: teamdraft; Owner: - +-- + +ALTER SEQUENCE teamdraft.account_login_change_keys_id_seq OWNED BY teamdraft.account_login_change_keys.id; + + +-- +-- Name: account_password_reset_keys; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.account_password_reset_keys ( + id bigint NOT NULL, + key character varying NOT NULL, + deadline timestamp(6) without time zone NOT NULL, + email_last_sent timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: account_password_reset_keys_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +CREATE SEQUENCE teamdraft.account_password_reset_keys_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: account_password_reset_keys_id_seq; Type: SEQUENCE OWNED BY; Schema: teamdraft; Owner: - +-- + +ALTER SEQUENCE teamdraft.account_password_reset_keys_id_seq OWNED BY teamdraft.account_password_reset_keys.id; + + +-- +-- Name: account_remember_keys; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.account_remember_keys ( + id bigint NOT NULL, + key character varying NOT NULL, + deadline timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: account_remember_keys_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +CREATE SEQUENCE teamdraft.account_remember_keys_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: account_remember_keys_id_seq; Type: SEQUENCE OWNED BY; Schema: teamdraft; Owner: - +-- + +ALTER SEQUENCE teamdraft.account_remember_keys_id_seq OWNED BY teamdraft.account_remember_keys.id; + + +-- +-- Name: account_verification_keys; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.account_verification_keys ( + id bigint NOT NULL, + key character varying NOT NULL, + requested_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + email_last_sent timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: account_verification_keys_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +CREATE SEQUENCE teamdraft.account_verification_keys_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: account_verification_keys_id_seq; Type: SEQUENCE OWNED BY; Schema: teamdraft; Owner: - +-- + +ALTER SEQUENCE teamdraft.account_verification_keys_id_seq OWNED BY teamdraft.account_verification_keys.id; + + +-- +-- Name: accounts; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.accounts ( + id bigint NOT NULL, + status integer DEFAULT 1 NOT NULL, + email teamdraft.citext NOT NULL, + password_hash character varying +); + + +-- +-- Name: accounts_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +CREATE SEQUENCE teamdraft.accounts_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: accounts_id_seq; Type: SEQUENCE OWNED BY; Schema: teamdraft; Owner: - +-- + +ALTER SEQUENCE teamdraft.accounts_id_seq OWNED BY teamdraft.accounts.id; + + +-- +-- Name: ar_internal_metadata; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.ar_internal_metadata ( + key character varying NOT NULL, + value character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: drafts; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.drafts ( + draft_id integer NOT NULL, + league_season_id integer NOT NULL, + started_at timestamp with time zone NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: drafts_draft_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +ALTER TABLE teamdraft.drafts ALTER COLUMN draft_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME teamdraft.drafts_draft_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: league_seasons; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.league_seasons ( + league_season_id integer NOT NULL, + season_id smallint NOT NULL, + league_id integer NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: league_users; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.league_users ( + league_user_id bigint NOT NULL, + league_season_id integer NOT NULL, + user_id integer NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: picks; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.picks ( + pick_id integer NOT NULL, + draft_id integer NOT NULL, + league_user_id integer NOT NULL, + team_id smallint NOT NULL, + auto boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: teams; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.teams ( + team_id smallint NOT NULL, + name text NOT NULL, + division teamdraft.divisions NOT NULL, + external_id text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT teams_name_ck CHECK ((char_length(name) <= 255)) +); + + +-- +-- Name: users; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.users ( + user_id integer NOT NULL, + name text, + password text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: league_picks; Type: VIEW; Schema: teamdraft; Owner: - +-- + +CREATE VIEW teamdraft.league_picks AS + SELECT league_seasons.league_id, + teams.team_id, + teams.name AS team, + users.name AS player + FROM ((((teamdraft.picks + JOIN teamdraft.teams USING (team_id)) + JOIN teamdraft.league_users USING (league_user_id)) + JOIN teamdraft.users USING (user_id)) + JOIN teamdraft.league_seasons USING (league_season_id)) + ORDER BY picks.created_at; + + +-- +-- Name: scores; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.scores ( + score_id integer NOT NULL, + season_id integer NOT NULL, + team_id integer NOT NULL, + week smallint NOT NULL, + category teamdraft.score_category DEFAULT 'win'::teamdraft.score_category NOT NULL, + scored_at timestamp with time zone DEFAULT now() NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: league_pick_scores; Type: VIEW; Schema: teamdraft; Owner: - +-- + +CREATE VIEW teamdraft.league_pick_scores AS + WITH current_scores AS ( + SELECT scores.team_id, + COALESCE(sum( + CASE + WHEN (scores.category = 'win'::teamdraft.score_category) THEN teamdraft.category_score('win'::teamdraft.score_category) + ELSE NULL::smallint + END), (0)::bigint) AS win, + COALESCE(sum( + CASE + WHEN (scores.category = 'playoffs'::teamdraft.score_category) THEN teamdraft.category_score('playoffs'::teamdraft.score_category) + ELSE NULL::smallint + END), (0)::bigint) AS playoffs, + COALESCE(sum( + CASE + WHEN (scores.category = 'divisional'::teamdraft.score_category) THEN teamdraft.category_score('divisional'::teamdraft.score_category) + ELSE NULL::smallint + END), (0)::bigint) AS divisional, + COALESCE(sum( + CASE + WHEN (scores.category = 'conference'::teamdraft.score_category) THEN teamdraft.category_score('conference'::teamdraft.score_category) + ELSE NULL::smallint + END), (0)::bigint) AS conference, + COALESCE(sum( + CASE + WHEN (scores.category = 'superbowl'::teamdraft.score_category) THEN teamdraft.category_score('superbowl'::teamdraft.score_category) + ELSE NULL::smallint + END), (0)::bigint) AS superbowl, + COALESCE(sum( + CASE + WHEN (scores.category = 'champion'::teamdraft.score_category) THEN teamdraft.category_score('champion'::teamdraft.score_category) + ELSE NULL::smallint + END), (0)::bigint) AS champion, + COALESCE(sum(teamdraft.category_score(scores.category)), (0)::bigint) AS total + FROM teamdraft.scores + WHERE (scores.season_id = teamdraft.current_season()) + GROUP BY scores.team_id + ) + SELECT league_picks.league_id, + league_picks.team, + league_picks.player, + COALESCE(current_scores.win, (0)::bigint) AS win, + COALESCE(current_scores.playoffs, (0)::bigint) AS playoffs, + COALESCE(current_scores.divisional, (0)::bigint) AS divisional, + COALESCE(current_scores.conference, (0)::bigint) AS conference, + COALESCE(current_scores.superbowl, (0)::bigint) AS superbowl, + COALESCE(current_scores.champion, (0)::bigint) AS champion, + COALESCE(current_scores.total, (0)::bigint) AS total + FROM (teamdraft.league_picks + LEFT JOIN current_scores USING (team_id)); + + +-- +-- Name: league_seasons_league_season_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +ALTER TABLE teamdraft.league_seasons ALTER COLUMN league_season_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME teamdraft.league_seasons_league_season_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: league_users_league_user_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +ALTER TABLE teamdraft.league_users ALTER COLUMN league_user_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME teamdraft.league_users_league_user_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: leagues; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.leagues ( + league_id integer NOT NULL, + name text, + user_id integer NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT leagues_name_ck CHECK ((char_length(name) <= 255)) +); + + +-- +-- Name: leagues_league_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +ALTER TABLE teamdraft.leagues ALTER COLUMN league_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME teamdraft.leagues_league_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: picks_pick_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +ALTER TABLE teamdraft.picks ALTER COLUMN pick_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME teamdraft.picks_pick_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: player_scores; Type: VIEW; Schema: teamdraft; Owner: - +-- + +CREATE VIEW teamdraft.player_scores AS + WITH current_scores AS ( + SELECT scores.team_id, + (sum(teamdraft.category_score(scores.category)))::integer AS points + FROM teamdraft.scores + WHERE (scores.season_id = teamdraft.current_season()) + GROUP BY scores.team_id + ) + SELECT league_picks.league_id, + league_picks.player, + (sum(COALESCE(current_scores.points, 0)))::integer AS score + FROM (teamdraft.league_picks + LEFT JOIN current_scores USING (team_id)) + GROUP BY league_picks.league_id, league_picks.player; + + +-- +-- Name: rankings; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.rankings ( + ranking_id integer NOT NULL, + season_id integer NOT NULL, + team_id integer NOT NULL, + rank smallint NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: rankings_ranking_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +ALTER TABLE teamdraft.rankings ALTER COLUMN ranking_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME teamdraft.rankings_ranking_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: rosters; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.rosters ( + roster_id integer NOT NULL, + league_user_id bigint NOT NULL, + team_id smallint NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: rosters_roster_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +ALTER TABLE teamdraft.rosters ALTER COLUMN roster_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME teamdraft.rosters_roster_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: schema_migrations; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.schema_migrations ( + version text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: scores_score_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +ALTER TABLE teamdraft.scores ALTER COLUMN score_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME teamdraft.scores_score_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: seasons; Type: TABLE; Schema: teamdraft; Owner: - +-- + +CREATE TABLE teamdraft.seasons ( + season_id smallint NOT NULL, + started_at timestamp with time zone NOT NULL, + ended_at timestamp with time zone NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: seasons_season_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +ALTER TABLE teamdraft.seasons ALTER COLUMN season_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME teamdraft.seasons_season_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: teams_team_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +ALTER TABLE teamdraft.teams ALTER COLUMN team_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME teamdraft.teams_team_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: users_user_id_seq; Type: SEQUENCE; Schema: teamdraft; Owner: - +-- + +ALTER TABLE teamdraft.users ALTER COLUMN user_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME teamdraft.users_user_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: account_login_change_keys id; Type: DEFAULT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.account_login_change_keys ALTER COLUMN id SET DEFAULT nextval('teamdraft.account_login_change_keys_id_seq'::regclass); + + +-- +-- Name: account_password_reset_keys id; Type: DEFAULT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.account_password_reset_keys ALTER COLUMN id SET DEFAULT nextval('teamdraft.account_password_reset_keys_id_seq'::regclass); + + +-- +-- Name: account_remember_keys id; Type: DEFAULT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.account_remember_keys ALTER COLUMN id SET DEFAULT nextval('teamdraft.account_remember_keys_id_seq'::regclass); + + +-- +-- Name: account_verification_keys id; Type: DEFAULT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.account_verification_keys ALTER COLUMN id SET DEFAULT nextval('teamdraft.account_verification_keys_id_seq'::regclass); + + +-- +-- Name: accounts id; Type: DEFAULT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.accounts ALTER COLUMN id SET DEFAULT nextval('teamdraft.accounts_id_seq'::regclass); + + +-- +-- Name: account_login_change_keys account_login_change_keys_pkey; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.account_login_change_keys + ADD CONSTRAINT account_login_change_keys_pkey PRIMARY KEY (id); + + +-- +-- Name: account_password_reset_keys account_password_reset_keys_pkey; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.account_password_reset_keys + ADD CONSTRAINT account_password_reset_keys_pkey PRIMARY KEY (id); + + +-- +-- Name: account_remember_keys account_remember_keys_pkey; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.account_remember_keys + ADD CONSTRAINT account_remember_keys_pkey PRIMARY KEY (id); + + +-- +-- Name: account_verification_keys account_verification_keys_pkey; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.account_verification_keys + ADD CONSTRAINT account_verification_keys_pkey PRIMARY KEY (id); + + +-- +-- Name: accounts accounts_pkey; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.accounts + ADD CONSTRAINT accounts_pkey PRIMARY KEY (id); + + +-- +-- Name: ar_internal_metadata ar_internal_metadata_pkey; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.ar_internal_metadata + ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key); + + +-- +-- Name: drafts drafts_pk; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.drafts + ADD CONSTRAINT drafts_pk PRIMARY KEY (draft_id); + + +-- +-- Name: league_seasons league_seasons_pk; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.league_seasons + ADD CONSTRAINT league_seasons_pk PRIMARY KEY (league_season_id); + + +-- +-- Name: league_users league_users_pk; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.league_users + ADD CONSTRAINT league_users_pk PRIMARY KEY (league_user_id); + + +-- +-- Name: leagues leagues_pk; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.leagues + ADD CONSTRAINT leagues_pk PRIMARY KEY (league_id); + + +-- +-- Name: picks picks_pk; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.picks + ADD CONSTRAINT picks_pk PRIMARY KEY (pick_id); + + +-- +-- Name: rankings rankings_pk; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.rankings + ADD CONSTRAINT rankings_pk PRIMARY KEY (ranking_id); + + +-- +-- Name: rosters rosters_pk; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.rosters + ADD CONSTRAINT rosters_pk PRIMARY KEY (roster_id); + + +-- +-- Name: scores scores_pk; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.scores + ADD CONSTRAINT scores_pk PRIMARY KEY (score_id); + + +-- +-- Name: scores scores_season_id_team_id_week_category_key; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.scores + ADD CONSTRAINT scores_season_id_team_id_week_category_key UNIQUE (season_id, team_id, week, category); + + +-- +-- Name: seasons seasons_pk; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.seasons + ADD CONSTRAINT seasons_pk PRIMARY KEY (season_id); + + +-- +-- Name: teams teams_pk; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.teams + ADD CONSTRAINT teams_pk PRIMARY KEY (team_id); + + +-- +-- Name: users users_pk; Type: CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.users + ADD CONSTRAINT users_pk PRIMARY KEY (user_id); + + +-- +-- Name: index_accounts_on_email; Type: INDEX; Schema: teamdraft; Owner: - +-- + +CREATE UNIQUE INDEX index_accounts_on_email ON teamdraft.accounts USING btree (email) WHERE (status = ANY (ARRAY[1, 2])); + + +-- +-- Name: drafts drafts_league_seasons_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.drafts + ADD CONSTRAINT drafts_league_seasons_fk FOREIGN KEY (league_season_id) REFERENCES teamdraft.league_seasons(league_season_id); + + +-- +-- Name: account_login_change_keys fk_rails_18962144a4; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.account_login_change_keys + ADD CONSTRAINT fk_rails_18962144a4 FOREIGN KEY (id) REFERENCES teamdraft.accounts(id); + + +-- +-- Name: account_verification_keys fk_rails_2e3b612008; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.account_verification_keys + ADD CONSTRAINT fk_rails_2e3b612008 FOREIGN KEY (id) REFERENCES teamdraft.accounts(id); + + +-- +-- Name: account_remember_keys fk_rails_9b2f6d8501; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.account_remember_keys + ADD CONSTRAINT fk_rails_9b2f6d8501 FOREIGN KEY (id) REFERENCES teamdraft.accounts(id); + + +-- +-- Name: account_password_reset_keys fk_rails_ccaeb37cea; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.account_password_reset_keys + ADD CONSTRAINT fk_rails_ccaeb37cea FOREIGN KEY (id) REFERENCES teamdraft.accounts(id); + + +-- +-- Name: league_seasons league_seasons_leagues_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.league_seasons + ADD CONSTRAINT league_seasons_leagues_fk FOREIGN KEY (league_id) REFERENCES teamdraft.leagues(league_id); + + +-- +-- Name: league_seasons league_seasons_seasons_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.league_seasons + ADD CONSTRAINT league_seasons_seasons_fk FOREIGN KEY (season_id) REFERENCES teamdraft.seasons(season_id); + + +-- +-- Name: league_users league_users_league_seasons_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.league_users + ADD CONSTRAINT league_users_league_seasons_fk FOREIGN KEY (league_season_id) REFERENCES teamdraft.league_seasons(league_season_id); + + +-- +-- Name: league_users league_users_users_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.league_users + ADD CONSTRAINT league_users_users_fk FOREIGN KEY (user_id) REFERENCES teamdraft.users(user_id); + + +-- +-- Name: leagues leagues_users_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.leagues + ADD CONSTRAINT leagues_users_fk FOREIGN KEY (user_id) REFERENCES teamdraft.users(user_id); + + +-- +-- Name: picks picks_drafts_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.picks + ADD CONSTRAINT picks_drafts_fk FOREIGN KEY (draft_id) REFERENCES teamdraft.drafts(draft_id); + + +-- +-- Name: picks picks_league_users_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.picks + ADD CONSTRAINT picks_league_users_fk FOREIGN KEY (league_user_id) REFERENCES teamdraft.league_users(league_user_id); + + +-- +-- Name: picks picks_teams_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.picks + ADD CONSTRAINT picks_teams_fk FOREIGN KEY (team_id) REFERENCES teamdraft.teams(team_id); + + +-- +-- Name: rankings rankings_seasons_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.rankings + ADD CONSTRAINT rankings_seasons_fk FOREIGN KEY (season_id) REFERENCES teamdraft.seasons(season_id); + + +-- +-- Name: rosters rosters_league_users_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.rosters + ADD CONSTRAINT rosters_league_users_fk FOREIGN KEY (league_user_id) REFERENCES teamdraft.league_users(league_user_id); + + +-- +-- Name: rosters rosters_teams_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.rosters + ADD CONSTRAINT rosters_teams_fk FOREIGN KEY (team_id) REFERENCES teamdraft.teams(team_id); + + +-- +-- Name: scores scores_seasons_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.scores + ADD CONSTRAINT scores_seasons_fk FOREIGN KEY (season_id) REFERENCES teamdraft.seasons(season_id); + + +-- +-- Name: scores scores_teams_fk; Type: FK CONSTRAINT; Schema: teamdraft; Owner: - +-- + +ALTER TABLE ONLY teamdraft.scores + ADD CONSTRAINT scores_teams_fk FOREIGN KEY (team_id) REFERENCES teamdraft.teams(team_id); + + +-- +-- PostgreSQL database dump complete +-- + +SET search_path TO teamdraft,public; + +INSERT INTO "schema_migrations" (version) VALUES +('20240129044207'); + diff --git a/lib/assets/.keep b/lib/assets/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/assets/.keep diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/tasks/.keep diff --git a/log/.keep b/log/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/log/.keep diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..2be3af2 --- /dev/null +++ b/public/404.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html> +<head> + <title>The page you were looking for doesn't exist (404)</title> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <style> + .rails-default-error-page { + background-color: #EFEFEF; + color: #2E2F30; + text-align: center; + font-family: arial, sans-serif; + margin: 0; + } + + .rails-default-error-page div.dialog { + width: 95%; + max-width: 33em; + margin: 4em auto 0; + } + + .rails-default-error-page div.dialog > div { + border: 1px solid #CCC; + border-right-color: #999; + border-left-color: #999; + border-bottom-color: #BBB; + border-top: #B00100 solid 4px; + border-top-left-radius: 9px; + border-top-right-radius: 9px; + background-color: white; + padding: 7px 12% 0; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); + } + + .rails-default-error-page h1 { + font-size: 100%; + color: #730E15; + line-height: 1.5em; + } + + .rails-default-error-page div.dialog > p { + margin: 0 0 1em; + padding: 1em; + background-color: #F7F7F7; + border: 1px solid #CCC; + border-right-color: #999; + border-left-color: #999; + border-bottom-color: #999; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-top-color: #DADADA; + color: #666; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); + } + </style> +</head> + +<body class="rails-default-error-page"> + <!-- This file lives in public/404.html --> + <div class="dialog"> + <div> + <h1>The page you were looking for doesn't exist.</h1> + <p>You may have mistyped the address or the page may have moved.</p> + </div> + <p>If you are the application owner check the logs for more information.</p> + </div> +</body> +</html> diff --git a/public/422.html b/public/422.html new file mode 100644 index 0000000..c08eac0 --- /dev/null +++ b/public/422.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html> +<head> + <title>The change you wanted was rejected (422)</title> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <style> + .rails-default-error-page { + background-color: #EFEFEF; + color: #2E2F30; + text-align: center; + font-family: arial, sans-serif; + margin: 0; + } + + .rails-default-error-page div.dialog { + width: 95%; + max-width: 33em; + margin: 4em auto 0; + } + + .rails-default-error-page div.dialog > div { + border: 1px solid #CCC; + border-right-color: #999; + border-left-color: #999; + border-bottom-color: #BBB; + border-top: #B00100 solid 4px; + border-top-left-radius: 9px; + border-top-right-radius: 9px; + background-color: white; + padding: 7px 12% 0; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); + } + + .rails-default-error-page h1 { + font-size: 100%; + color: #730E15; + line-height: 1.5em; + } + + .rails-default-error-page div.dialog > p { + margin: 0 0 1em; + padding: 1em; + background-color: #F7F7F7; + border: 1px solid #CCC; + border-right-color: #999; + border-left-color: #999; + border-bottom-color: #999; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-top-color: #DADADA; + color: #666; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); + } + </style> +</head> + +<body class="rails-default-error-page"> + <!-- This file lives in public/422.html --> + <div class="dialog"> + <div> + <h1>The change you wanted was rejected.</h1> + <p>Maybe you tried to change something you didn't have access to.</p> + </div> + <p>If you are the application owner check the logs for more information.</p> + </div> +</body> +</html> diff --git a/public/500.html b/public/500.html new file mode 100644 index 0000000..78a030a --- /dev/null +++ b/public/500.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<html> +<head> + <title>We're sorry, but something went wrong (500)</title> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <style> + .rails-default-error-page { + background-color: #EFEFEF; + color: #2E2F30; + text-align: center; + font-family: arial, sans-serif; + margin: 0; + } + + .rails-default-error-page div.dialog { + width: 95%; + max-width: 33em; + margin: 4em auto 0; + } + + .rails-default-error-page div.dialog > div { + border: 1px solid #CCC; + border-right-color: #999; + border-left-color: #999; + border-bottom-color: #BBB; + border-top: #B00100 solid 4px; + border-top-left-radius: 9px; + border-top-right-radius: 9px; + background-color: white; + padding: 7px 12% 0; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); + } + + .rails-default-error-page h1 { + font-size: 100%; + color: #730E15; + line-height: 1.5em; + } + + .rails-default-error-page div.dialog > p { + margin: 0 0 1em; + padding: 1em; + background-color: #F7F7F7; + border: 1px solid #CCC; + border-right-color: #999; + border-left-color: #999; + border-bottom-color: #999; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-top-color: #DADADA; + color: #666; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); + } + </style> +</head> + +<body class="rails-default-error-page"> + <!-- This file lives in public/500.html --> + <div class="dialog"> + <div> + <h1>We're sorry, but something went wrong.</h1> + </div> + <p>If you are the application owner check the logs for more information.</p> + </div> +</body> +</html> diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/public/apple-touch-icon-precomposed.png diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/public/apple-touch-icon.png diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/public/favicon.ico diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..c19f78a --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/storage/.keep b/storage/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/storage/.keep diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb new file mode 100644 index 0000000..d19212a --- /dev/null +++ b/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/test/channels/application_cable/connection_test.rb b/test/channels/application_cable/connection_test.rb new file mode 100644 index 0000000..6340bf9 --- /dev/null +++ b/test/channels/application_cable/connection_test.rb @@ -0,0 +1,13 @@ +require "test_helper" + +module ApplicationCable + class ConnectionTest < ActionCable::Connection::TestCase + # test "connects with cookies" do + # cookies.signed[:user_id] = 42 + # + # connect + # + # assert_equal connection.user_id, "42" + # end + end +end diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/controllers/.keep diff --git a/test/controllers/leagues_controller_test.rb b/test/controllers/leagues_controller_test.rb new file mode 100644 index 0000000..c7b24af --- /dev/null +++ b/test/controllers/leagues_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class LeaguesControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/fixtures/accounts.yml b/test/fixtures/accounts.yml new file mode 100644 index 0000000..ea6f893 --- /dev/null +++ b/test/fixtures/accounts.yml @@ -0,0 +1,10 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html +one: + email: freddie@queen.com + password_hash: <%= RodauthMain.allocate.password_hash("password") %> + status: verified + +two: + email: brian@queen.com + password_hash: <%= RodauthMain.allocate.password_hash("password") %> + status: verified diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/fixtures/files/.keep diff --git a/test/fixtures/league_pick_scores.yml b/test/fixtures/league_pick_scores.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/league_pick_scores.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/fixtures/leagues.yml b/test/fixtures/leagues.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/leagues.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/fixtures/player_scores.yml b/test/fixtures/player_scores.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/player_scores.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/helpers/.keep diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/integration/.keep diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/mailers/.keep diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/models/.keep diff --git a/test/models/league_pick_score_test.rb b/test/models/league_pick_score_test.rb new file mode 100644 index 0000000..73a9c8f --- /dev/null +++ b/test/models/league_pick_score_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class LeaguePickScoreTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/league_test.rb b/test/models/league_test.rb new file mode 100644 index 0000000..d3ae350 --- /dev/null +++ b/test/models/league_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class LeagueTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/player_score_test.rb b/test/models/player_score_test.rb new file mode 100644 index 0000000..3aaf475 --- /dev/null +++ b/test/models/player_score_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class PlayerScoreTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/system/.keep b/test/system/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/system/.keep diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..0c22470 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,15 @@ +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +require "rails/test_help" + +module ActiveSupport + class TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... + end +end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tmp/.keep diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tmp/pids/.keep diff --git a/tmp/storage/.keep b/tmp/storage/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tmp/storage/.keep diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/vendor/.keep diff --git a/vendor/javascript/.keep b/vendor/javascript/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/vendor/javascript/.keep |