diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..43dc58cf9 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,87 @@ +version: 2.1 + +workflows: + version: 2.1 + build: + jobs: + - build_and_test +orbs: + browser-tools: circleci/browser-tools@1.1 +jobs: + build_and_test: + working_directory: ~/cfp_app + docker: + - image: cimg/ruby:3.1.2-browsers + environment: + PGHOST: localhost + PGUSER: cfp_app + RAILS_ENV: test + - image: postgres:12.5 + environment: + POSTGRES_USER: cfp_app + POSTGRES_DB: cfp_app_test + POSTGRES_PASSWORD: "" + POSTGRES_HOST_AUTH_METHOD: trust + steps: + - browser-tools/install-browser-tools + - checkout + - run: | + ruby --version + node --version + java --version + google-chrome --version + + - run: gem install bundler + - run: yarn install + + # Restore Cached gem Dependencies + - restore_cache: + keys: + - cfp_app-gems-{{ checksum "Gemfile.lock" }} + - cfp_app-gems- + # Restore Cached node module Dependencies + - restore_cache: + keys: + - cfp_app-packages-{{ checksum "yarn.lock" }} + - cfp_app-packages- + + # Bundle install dependencies + - run: bundle config set path 'vendor/bundle' + - run: bundle install + + ## Cache Dependencies + + # Generate this fallback cache first because the most recent match + # will be used even if there is a more precise match + - save_cache: + key: cfp_app-gems- + paths: + - vendor/bundle + # Generate a cache for this exact Gemfile.lock + - save_cache: + key: cfp_app-gems-{{ checksum "Gemfile.lock" }} + paths: + - vendor/bundle + # Generate a fallback cache of node packages + - save_cache: + key: cfp_app-packages- + paths: + - node_modules + # Generate a cache for this exact yarn.lock + - save_cache: + key: cfp_app-packages-{{ checksum "yarn.lock" }} + paths: + - node_modules + + # Wait for DB + - run: dockerize -wait tcp://localhost:5432 -timeout 1m + + # Setup the database + - run: bundle exec rake db:setup + + # Run the tests + - run: bin/rake + + # Store failed js spec artifacts + - store_artifacts: + path: tmp/screenshots diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..0b1d04054 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "bundler" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..752cfef56 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +Title + +Reason for Change +================= +* + +Changes +======= +* + +Minor +===== +* + +https://link.to/task_tracker_card diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..be8e3fd99 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,17 @@ +name: Deploy + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: akhileshns/heroku-deploy@v3.12.12 # This is the action + with: + heroku_api_key: ${{secrets.HEROKU_API_KEY}} + heroku_app_name: "cfp-next-staging" #Must be unique in Heroku + heroku_email: "adarsh@cylinder.work" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..935c4594a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,45 @@ +name: Test + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres + env: + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - 5432:5432 + options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libvips postgresql-client libpq-dev + + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - uses: actions/setup-node@v4 + with: + node-version: '15.14.0' + + - name: Run tests + env: + RAILS_ENV: test + DATABASE_URL: postgres://postgres@localhost:5432/cfp_app_test + run: bin/rails db:setup assets:precompile spec + + - name: Keep screenshots from failed system tests + uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots + path: ${{ github.workspace }}/tmp/capybara + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 4ab5f6475..dcdb4ffe1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,249 @@ -/.bundle +scratch.md +.ruby-version +.ruby-gemset +.rvmrc +.nvmrc +.vscode +.idea + +latest.dump* +dump.rdb + +### Rails ### +*.rbc +capybara-*.html +.rspec +/db/*.sqlite3 +/db/*.sqlite3-journal +/db/*.sqlite3-[0-9]* +/public/system +/coverage/ +/spec/tmp +*.orig +rerun.txt +pickle-email-*.html # Ignore all logfiles and tempfiles. -/log/*.log -/tmp +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep +# TODO Comment out this rule if you are OK with secrets being uploaded to the repo +config/initializers/secret_token.rb +config/master.key + +# Only include if you have production secrets in this file, which is no longer a Rails default +# config/secrets.yml + +# dotenv, dotenv-rails +# TODO Comment out these rules if environment variables can be committed .env -scratch.md -.ruby-version -.ruby-gemset +.env*.local + +## Environment normalization: +/.bundle +/vendor/bundle + +# these should all be checked in to normalize the environment: +# Gemfile.lock, .ruby-version, .ruby-gemset + +# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc +# if using bower-rails ignore default bower_components path bower.json files +/vendor/assets/bower_components +*.bowerrc +bower.json + +# Ignore pow environment settings +.powenv + +# Ignore Byebug command history file. +.byebug_history + +# Ignore node_modules +node_modules/ + +# Ignore precompiled javascript packs /public/packs /public/packs-test -/node_modules +/public/assets + +# Ignore yarn files /yarn-error.log yarn-debug.log* .yarn-integrity -.vscode + +# Ignore uploaded files in development +/storage/* +!/storage/.keep +/public/uploads + +### Ruby ### +*.gem +/.config +/InstalledFiles +/pkg/ +/spec/reports/ +/spec/examples.txt +/test/tmp/ +/test/version_tmp/ +/tmp/ + +# Used by dotenv library to load environment variables. +# .env + +# Ignore Byebug command history file. + +## Specific to RubyMotion: +.dat* +.repl_history +build/ +*.bridgesupport +build-iPhoneOS/ +build-iPhoneSimulator/ + +## Specific to RubyMotion (use of CocoaPods): +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# vendor/Pods/ + +## Documentation cache and generated files: +/.yardoc/ +/_yardoc/ +/doc/ +/rdoc/ + +/.bundle/ +/lib/bundler/man/ + +# for a library or gem, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# Gemfile.lock +# .ruby-version +# .ruby-gemset + +# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: + +# Used by RuboCop. Remote config files pulled in from inherit_from directive. +# .rubocop-https?--* + +# End of https://www.toptal.com/developers/gitignore/api/ruby,rails + +### RubyMine ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### RubyMine Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +# End of https://www.toptal.com/developers/gitignore/api/rubymine diff --git a/.irbrc b/.irbrc new file mode 100644 index 000000000..a6bf9ae7d --- /dev/null +++ b/.irbrc @@ -0,0 +1,2 @@ +require "amazing_print" +AmazingPrint.irb! diff --git a/.node-version b/.node-version new file mode 100644 index 000000000..fc2cbe502 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +15.14.0 diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 000000000..fdf9fc483 --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +cfp-app diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 000000000..ef538c281 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.1.2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 95fd8c94b..000000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: ruby -rvm: - - 2.6.5 -env: - global: - - NOKOGIRI_USE_SYSTEM_LIBRARIES=true - - QMAKE=/usr/lib/x86_64-linux-gnu/qt4/bin/qmake -addons: - apt: - sources: - - ubuntu-sdk-team - packages: - - libqt5webkit5-dev - - qtdeclarative5-dev - chrome: stable -bundler_args: --jobs=3 --retry=3 --without development -script: xvfb-run bundle exec rake -services: - - postgresql -before_script: - - psql -c 'create database cfp_app_test;' -U postgres diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d45751fc4..5c3d92e68 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ Guidelines for bug reports: reported. 2. **Check if the issue has been fixed** — try to reproduce it using the - latest `master` or development branch in the repository. + latest `main` or development branch in the repository. 3. **Isolate the problem** — create a [reduced test case](http://css-tricks.com/6263-reduced-test-cases/) and a live example. @@ -136,7 +136,7 @@ project: ``` 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) - with a clear title and description. + with a clear title and description. NOTE: please avoid opening Pull Requests for Work-in-Progress or draft code. Opening a PR sends many notifications to followers and we want to conserve their attention span. **IMPORTANT**: By submitting a patch, you agree to allow the project owner to license your work under the same license as that used by the project. \ No newline at end of file diff --git a/Gemfile b/Gemfile index aaa3f035b..bc6c0fb4e 100644 --- a/Gemfile +++ b/Gemfile @@ -1,54 +1,73 @@ +# frozen_string_literal: true + source 'https://rubygems.org' -ruby '2.6.5' -gem 'rails', '~> 5.2' +# https://andycroll.com/ruby/read-ruby-version-in-your-gemfile/ +ruby File.read(".ruby-version").strip + gem 'puma' +gem 'rails', '~> 6.1.6' +gem 'mimemagic', github: 'mimemagicrb/mimemagic', ref: '01f92d86d15d85cfd0f20dabd025dcbd36a8a60f' +gem 'mime-types-data' +gem 'mime-types' +gem 'rexml' +gem 'matrix' +gem 'honeybadger', '~> 5.20' + +# Required until Rails 7 - https://github.com/mikel/mail/pull/1472#issuecomment-1165161541 +gem 'net-smtp', require: false +gem 'net-imap', require: false +gem 'net-pop', require: false gem 'pg' +gem 'bootstrap-sass', '~> 3.4.1' +gem 'haml', '~> 5.0' +gem 'jbuilder' +gem 'jquery-datatables-rails' gem 'jquery-rails' gem 'jquery-ui-rails' -gem 'jquery-datatables-rails' -gem 'underscore-rails' -gem 'uglifier', '>= 1.3.0' -gem 'sassc-rails' -gem 'haml', '~> 5.0.0' -gem 'bootstrap-sass', '~> 3.4.1' gem 'rails-assets-momentjs', source: 'https://rails-assets.org' +gem 'sassc-rails' gem 'selectize-rails' -gem 'jbuilder' +gem 'uglifier', '>= 1.3.0' +gem 'underscore-rails' -gem 'devise', '~> 4.7' +gem 'devise', '~> 4.8' gem 'omniauth-github' gem 'omniauth-twitter' +gem "omniauth-rails_csrf_protection", "~> 1.0" +gem 'actionview-encoded_mail_to' +gem 'active_model_serializers', '~> 0.10.0' +gem 'bootsnap', '~> 1.13', require: false +gem 'bootstrap-multiselect-rails', '~> 0.9.9' gem 'chartkick' -gem 'groupdate' -gem 'country_select', '~> 1.3' -gem 'redcarpet', '~> 3.4' gem 'coderay', '~> 1.0' -gem 'bootstrap-multiselect-rails', '~> 0.9.9' -gem 'active_model_serializers', '~> 0.10.0' -gem 'draper', '~> 3.0.1' -gem 'simple_form' -gem 'responders', '~> 2.4.0' -gem 'pundit' +gem 'country_select', '~> 8.0' +gem 'draper', '~> 4.0' gem 'faker' -gem 'actionview-encoded_mail_to' +gem 'fastly' +gem 'groupdate' gem 'nokogiri' -gem 'bootsnap', require: false - -gem 'webpacker' +gem 'pundit' +gem 'redcarpet', '~> 3.5' +gem 'simple_form' +gem 'tinymce-rails' +gem 'image_processing', '~> 1.2' gem 'react-rails' +gem 'webpacker' gem 'sidekiq' gem 'diffy' gem 'paper_trail' +gem 'sendgrid-ruby' + group :production do - gem 'rails_12factor' - gem 'rack-timeout', '~> 0.5' + gem 'aws-sdk-s3' + gem 'rack-timeout', '~> 0.6' end group :development do @@ -56,31 +75,31 @@ group :development do gem 'better_errors' gem 'binding_of_caller' gem 'foreman' + gem 'haml-rails' + gem 'html2haml', '~> 2.2' gem 'launchy' gem 'rack-mini-profiler' - gem 'html2haml', '~> 2.2' - gem 'haml-rails' gem 'spring-commands-rspec', require: false gem 'web-console' end group :development, :test do - gem 'capybara', '~> 3.33' - gem "selenium-webdriver" - gem 'webdrivers', '~> 4.0' - gem 'database_cleaner', '~> 1.6' + gem 'amazing_print', require: false + gem 'capybara', '~> 3.37' + gem 'database_cleaner', '~> 2.0' gem 'dotenv-rails' - gem "factory_bot_rails" + gem 'factory_bot_rails' gem 'growl' gem 'guard' - gem 'guard-rspec' gem 'guard-livereload', '~> 2.1' + gem 'guard-rspec' + gem 'pry-rails' + gem 'pry-remote' + gem 'pry-rescue' + gem 'rails-controller-testing' gem 'rspec' gem 'rspec-rails' - gem 'rails-controller-testing' - gem 'timecop' + gem 'selenium-webdriver' gem 'spring' - gem 'pry-rails' - gem 'pry-rescue' - gem 'pry-remote' + gem 'timecop' end diff --git a/Gemfile.lock b/Gemfile.lock index c3096711b..08340fc4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,159 +1,216 @@ +GIT + remote: https://github.com/mimemagicrb/mimemagic.git + revision: 01f92d86d15d85cfd0f20dabd025dcbd36a8a60f + ref: 01f92d86d15d85cfd0f20dabd025dcbd36a8a60f + specs: + mimemagic (0.3.5) + GEM - remote: https://rubygems.org/ remote: https://rails-assets.org/ specs: - actioncable (5.2.4.3) - actionpack (= 5.2.4.3) + rails-assets-momentjs (2.29.4) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (6.1.7.2) + actionpack (= 6.1.7.2) + activesupport (= 6.1.7.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.4.3) - actionpack (= 5.2.4.3) - actionview (= 5.2.4.3) - activejob (= 5.2.4.3) + actionmailbox (6.1.7.2) + actionpack (= 6.1.7.2) + activejob (= 6.1.7.2) + activerecord (= 6.1.7.2) + activestorage (= 6.1.7.2) + activesupport (= 6.1.7.2) + mail (>= 2.7.1) + actionmailer (6.1.7.2) + actionpack (= 6.1.7.2) + actionview (= 6.1.7.2) + activejob (= 6.1.7.2) + activesupport (= 6.1.7.2) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.4.3) - actionview (= 5.2.4.3) - activesupport (= 5.2.4.3) - rack (~> 2.0, >= 2.0.8) + actionpack (6.1.7.2) + actionview (= 6.1.7.2) + activesupport (= 6.1.7.2) + rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.4.3) - activesupport (= 5.2.4.3) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.1.7.2) + actionpack (= 6.1.7.2) + activerecord (= 6.1.7.2) + activestorage (= 6.1.7.2) + activesupport (= 6.1.7.2) + nokogiri (>= 1.8.5) + actionview (6.1.7.2) + activesupport (= 6.1.7.2) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) + rails-html-sanitizer (~> 1.1, >= 1.2.0) actionview-encoded_mail_to (1.0.9) rails - active_model_serializers (0.10.10) - actionpack (>= 4.1, < 6.1) - activemodel (>= 4.1, < 6.1) + active_model_serializers (0.10.13) + actionpack (>= 4.1, < 7.1) + activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (5.2.4.3) - activesupport (= 5.2.4.3) + activejob (6.1.7.2) + activesupport (= 6.1.7.2) globalid (>= 0.3.6) - activemodel (5.2.4.3) - activesupport (= 5.2.4.3) + activemodel (6.1.7.2) + activesupport (= 6.1.7.2) activemodel-serializers-xml (1.0.2) activemodel (> 5.x) activesupport (> 5.x) builder (~> 3.1) - activerecord (5.2.4.3) - activemodel (= 5.2.4.3) - activesupport (= 5.2.4.3) - arel (>= 9.0) - activestorage (5.2.4.3) - actionpack (= 5.2.4.3) - activerecord (= 5.2.4.3) - marcel (~> 0.3.1) - activesupport (5.2.4.3) + activerecord (6.1.7.2) + activemodel (= 6.1.7.2) + activesupport (= 6.1.7.2) + activestorage (6.1.7.2) + actionpack (= 6.1.7.2) + activejob (= 6.1.7.2) + activerecord (= 6.1.7.2) + activesupport (= 6.1.7.2) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (6.1.7.2) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - addressable (2.7.0) - public_suffix (>= 2.0.2, < 5.0) - annotate (3.1.1) - activerecord (>= 3.2, < 7.0) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + amazing_print (1.4.0) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) - arel (9.0.0) - autoprefixer-rails (9.8.5) - execjs + autoprefixer-rails (10.4.7.0) + execjs (~> 2) + aws-eventstream (1.2.0) + aws-partitions (1.710.0) + aws-sdk-core (3.170.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.62.0) + aws-sdk-core (~> 3, >= 3.165.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.119.0) + aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.5.2) + aws-eventstream (~> 1, >= 1.0.2) babel-source (5.8.35) babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) execjs (~> 2.0) - bcrypt (3.1.13) - better_errors (2.7.1) + bcrypt (3.1.18) + better_errors (2.9.1) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) bindex (0.8.1) - binding_of_caller (0.8.0) + binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - bootsnap (1.4.6) - msgpack (~> 1.0) + bootsnap (1.16.0) + msgpack (~> 1.2) bootstrap-multiselect-rails (0.9.9) rails (>= 4.0.0) bootstrap-sass (3.4.1) autoprefixer-rails (>= 5.2.1) sassc (>= 2.0.0) - builder (3.2.4) - capybara (3.33.0) + builder (3.3.0) + capybara (3.38.0) addressable + matrix mini_mime (>= 0.1.3) nokogiri (~> 1.8) rack (>= 1.6.0) rack-test (>= 0.6.3) - regexp_parser (~> 1.5) + regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) case_transform (0.2) activesupport - chartkick (3.3.1) - childprocess (3.0.0) + chartkick (5.0.1) coderay (1.1.3) - concurrent-ruby (1.1.6) - connection_pool (2.2.3) - countries (0.9.3) - currencies (~> 0.4.2) - country_select (1.3.1) - countries (= 0.9.3) + concurrent-ruby (1.3.4) + connection_pool (2.3.0) + countries (5.3.1) + unaccent (~> 0.3) + country_select (8.0.1) + countries (~> 5.0) crass (1.0.6) - currencies (0.4.2) - database_cleaner (1.8.5) - debug_inspector (0.0.3) - devise (4.7.2) + database_cleaner (2.0.1) + database_cleaner-active_record (~> 2.0.0) + database_cleaner-active_record (2.0.1) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0.0) + database_cleaner-core (2.0.1) + date (3.3.3) + debug_inspector (1.1.0) + devise (4.8.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - diff-lcs (1.4.4) - diffy (3.3.0) - dotenv (2.7.6) - dotenv-rails (2.7.6) - dotenv (= 2.7.6) + diff-lcs (1.5.1) + diffy (3.4.3) + dotenv (2.8.1) + dotenv-rails (2.8.1) + dotenv (= 2.8.1) railties (>= 3.2) - draper (3.0.1) - actionpack (~> 5.0) - activemodel (~> 5.0) - activemodel-serializers-xml (~> 1.0) - activesupport (~> 5.0) - request_store (~> 1.0) - em-websocket (0.5.1) + draper (4.0.2) + actionpack (>= 5.0) + activemodel (>= 5.0) + activemodel-serializers-xml (>= 1.0) + activesupport (>= 5.0) + request_store (>= 1.0) + ruby2_keywords + em-websocket (0.5.3) eventmachine (>= 0.12.9) - http_parser.rb (~> 0.6.0) - erubi (1.9.0) + http_parser.rb (~> 0) + erubi (1.13.0) erubis (2.7.0) + ethon (0.16.0) + ffi (>= 1.15.0) eventmachine (1.2.7) - execjs (2.7.0) - factory_bot (6.1.0) + execjs (2.8.1) + factory_bot (6.2.1) activesupport (>= 5.0.0) - factory_bot_rails (6.1.0) - factory_bot (~> 6.1.0) + factory_bot_rails (6.2.0) + factory_bot (~> 6.2.0) railties (>= 5.0.0) - faker (2.13.0) - i18n (>= 1.6, < 2) - faraday (1.0.1) - multipart-post (>= 1.2, < 3) - ffi (1.13.1) - foreman (0.87.1) - formatador (0.2.5) - globalid (0.4.2) - activesupport (>= 4.2.0) - groupdate (5.0.0) - activesupport (>= 5) + faker (3.1.1) + i18n (>= 1.8.11, < 2) + faraday (2.7.4) + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-net_http (3.0.2) + fastly (4.1.0) + typhoeus (~> 1.0, >= 1.0.1) + ffi (1.15.5) + foreman (0.87.2) + formatador (1.1.0) + globalid (1.1.0) + activesupport (>= 5.0) + groupdate (6.2.0) + activesupport (>= 5.2) growl (1.0.3) - guard (2.16.2) + guard (2.18.0) formatador (>= 0.2.4) listen (>= 2.7, < 4.0) lumberjack (>= 1.0.12, < 2.0) nenv (~> 0.1) notiffany (~> 0.0) - pry (>= 0.9.12) + pry (>= 0.13.0) shellany (~> 0.0) thor (>= 0.18.1) guard-compat (1.2.1) @@ -166,97 +223,129 @@ GEM guard (~> 2.1) guard-compat (~> 1.1) rspec (>= 2.99.0, < 4.0) - haml (5.0.4) + haml (5.2.2) temple (>= 0.8.0) tilt - haml-rails (2.0.1) + haml-rails (2.1.0) actionpack (>= 5.1) activesupport (>= 5.1) - haml (>= 4.0.6, < 6.0) - html2haml (>= 1.0.1) + haml (>= 4.0.6) railties (>= 5.1) - hashie (4.1.0) - html2haml (2.2.0) + hashie (5.0.0) + honeybadger (5.20.1) + logger + html2haml (2.3.0) erubis (~> 2.7.0) - haml (>= 4.0, < 6) + haml (>= 4.0) nokogiri (>= 1.6.0) ruby_parser (~> 3.5) - http_parser.rb (0.6.0) - i18n (1.8.3) + http_parser.rb (0.8.0) + i18n (1.14.6) concurrent-ruby (~> 1.0) + image_processing (1.12.2) + mini_magick (>= 4.9.5, < 5) + ruby-vips (>= 2.0.17, < 3) interception (0.5) - jbuilder (2.10.0) + jbuilder (2.11.5) + actionview (>= 5.0.0) activesupport (>= 5.0.0) + jmespath (1.6.2) jquery-datatables-rails (3.4.0) actionpack (>= 3.1) jquery-rails railties (>= 3.1) sass-rails - jquery-rails (4.4.0) + jquery-rails (4.5.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) jquery-ui-rails (6.0.1) railties (>= 3.2.16) jsonapi-renderer (0.2.2) - jwt (2.2.1) - launchy (2.5.0) - addressable (~> 2.7) - listen (3.2.1) + jwt (2.7.0) + launchy (2.5.2) + addressable (~> 2.8) + listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.6.0) + logger (1.6.1) + loofah (2.23.1) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - lumberjack (1.2.6) - mail (2.7.1) + nokogiri (>= 1.12.0) + lumberjack (1.2.8) + mail (2.8.1) mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (1.0.0) - mimemagic (0.3.5) - mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.14.1) - msgpack (1.3.3) + net-imap + net-pop + net-smtp + marcel (1.0.2) + matrix (0.4.2) + method_source (1.1.0) + mime-types (3.4.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2022.0105) + mini_magick (4.12.0) + mini_mime (1.1.2) + mini_portile2 (2.8.7) + minitest (5.25.1) + msgpack (1.6.0) multi_json (1.15.0) multi_xml (0.6.0) - multipart-post (2.1.1) nenv (0.3.0) - nio4r (2.5.2) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) + net-imap (0.3.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout + net-smtp (0.3.3) + net-protocol + nio4r (2.5.8) + nokogiri (1.14.1) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) notiffany (0.1.3) nenv (~> 0.1) shellany (~> 0.0) - oauth (0.5.4) - oauth2 (1.4.4) - faraday (>= 0.8, < 2.0) + oauth (1.1.0) + oauth-tty (~> 1.0, >= 1.0.1) + snaky_hash (~> 2.0) + version_gem (~> 1.1) + oauth-tty (1.0.5) + version_gem (~> 1.1, >= 1.1.1) + oauth2 (2.0.9) + faraday (>= 0.17.3, < 3.0) jwt (>= 1.0, < 3.0) - multi_json (~> 1.3) multi_xml (~> 0.5) - rack (>= 1.2, < 3) - omniauth (1.9.1) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0) + version_gem (~> 1.1) + omniauth (2.1.1) hashie (>= 3.4.6) - rack (>= 1.6.2, < 3) - omniauth-github (1.4.0) - omniauth (~> 1.5) - omniauth-oauth2 (>= 1.4.0, < 2.0) - omniauth-oauth (1.1.0) + rack (>= 2.2.3) + rack-protection + omniauth-github (2.0.1) + omniauth (~> 2.0) + omniauth-oauth2 (~> 1.8) + omniauth-oauth (1.2.0) oauth - omniauth (~> 1.0) - omniauth-oauth2 (1.6.0) - oauth2 (~> 1.1) - omniauth (~> 1.9) + omniauth (>= 1.0, < 3) + omniauth-oauth2 (1.8.0) + oauth2 (>= 1.4, < 3) + omniauth (~> 2.0) + omniauth-rails_csrf_protection (1.0.1) + actionpack (>= 4.2) + omniauth (~> 2.0) omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack orm_adapter (0.5.0) - paper_trail (10.3.1) - activerecord (>= 4.2) - request_store (~> 1.1) - pg (1.2.3) - pry (0.13.1) + paper_trail (14.0.0) + activerecord (>= 6.0) + request_store (~> 1.4) + pg (1.4.5) + pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) pry-rails (0.3.9) @@ -267,95 +356,102 @@ GEM pry-rescue (1.5.2) interception (>= 0.5) pry (>= 0.12.0) - public_suffix (4.0.5) - puma (4.3.5) + public_suffix (5.0.1) + puma (6.0.2) nio4r (~> 2.0) - pundit (2.1.0) + pundit (2.3.0) activesupport (>= 3.0.0) - rack (2.2.3) - rack-mini-profiler (2.0.2) + racc (1.8.1) + rack (2.2.10) + rack-mini-profiler (3.0.0) rack (>= 1.2.0) - rack-proxy (0.6.5) + rack-protection (3.0.5) + rack + rack-proxy (0.7.6) rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rack-timeout (0.6.0) - rails (5.2.4.3) - actioncable (= 5.2.4.3) - actionmailer (= 5.2.4.3) - actionpack (= 5.2.4.3) - actionview (= 5.2.4.3) - activejob (= 5.2.4.3) - activemodel (= 5.2.4.3) - activerecord (= 5.2.4.3) - activestorage (= 5.2.4.3) - activesupport (= 5.2.4.3) - bundler (>= 1.3.0) - railties (= 5.2.4.3) + rack-test (2.1.0) + rack (>= 1.3) + rack-timeout (0.6.3) + rails (6.1.7.2) + actioncable (= 6.1.7.2) + actionmailbox (= 6.1.7.2) + actionmailer (= 6.1.7.2) + actionpack (= 6.1.7.2) + actiontext (= 6.1.7.2) + actionview (= 6.1.7.2) + activejob (= 6.1.7.2) + activemodel (= 6.1.7.2) + activerecord (= 6.1.7.2) + activestorage (= 6.1.7.2) + activesupport (= 6.1.7.2) + bundler (>= 1.15.0) + railties (= 6.1.7.2) sprockets-rails (>= 2.0.0) - rails-assets-momentjs (2.22.2) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - rails_12factor (0.0.3) - rails_serve_static_assets - rails_stdout_logging - rails_serve_static_assets (0.0.5) - rails_stdout_logging (0.0.5) - railties (5.2.4.3) - actionpack (= 5.2.4.3) - activesupport (= 5.2.4.3) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (6.1.7.2) + actionpack (= 6.1.7.2) + activesupport (= 6.1.7.2) method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) - rake (13.0.1) - rb-fsevent (0.10.4) + rake (>= 12.2) + thor (~> 1.0) + rake (13.2.1) + rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - react-rails (2.6.1) + react-rails (2.6.2) babel-transpiler (>= 0.7.0) connection_pool execjs railties (>= 3.2) tilt - redcarpet (3.5.0) - redis (4.2.1) - regexp_parser (1.7.1) - request_store (1.5.0) + redcarpet (3.6.0) + redis-client (0.12.1) + connection_pool + regexp_parser (2.7.0) + request_store (1.5.1) rack (>= 1.4) - responders (2.4.1) - actionpack (>= 4.2.0, < 6.0) - railties (>= 4.2.0, < 6.0) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.2) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) + rexml (3.2.5) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-rails (4.0.1) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.9) - rspec-expectations (~> 3.9) - rspec-mocks (~> 3.9) - rspec-support (~> 3.9) - rspec-support (3.9.3) - ruby_parser (3.14.2) - sexp_processor (~> 4.9) - rubyzip (2.3.0) + rspec-support (~> 3.13.0) + rspec-rails (6.1.5) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.1) + ruby-vips (2.1.4) + ffi (~> 1.12) + ruby2_keywords (0.0.5) + ruby_http_client (3.5.5) + ruby_parser (3.19.2) + sexp_processor (~> 4.16) + rubyzip (2.3.2) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) @@ -367,61 +463,72 @@ GEM sprockets-rails tilt selectize-rails (0.12.6) - selenium-webdriver (3.142.7) - childprocess (>= 0.5, < 4.0) - rubyzip (>= 1.2.2) - semantic_range (2.3.0) - sexp_processor (4.15.0) + selenium-webdriver (4.8.0) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + semantic_range (3.0.0) + sendgrid-ruby (6.6.2) + ruby_http_client (~> 3.4) + sexp_processor (4.16.1) shellany (0.0.1) - sidekiq (6.1.0) - connection_pool (>= 2.2.2) - rack (~> 2.0) - redis (>= 4.2.0) - simple_form (5.0.2) - actionpack (>= 5.0) - activemodel (>= 5.0) + sidekiq (7.0.3) + concurrent-ruby (< 2) + connection_pool (>= 2.3.0) + rack (>= 2.2.4) + redis-client (>= 0.11.0) + simple_form (5.2.0) + actionpack (>= 5.2) + activemodel (>= 5.2) slop (3.6.0) - spring (2.1.0) + snaky_hash (2.0.1) + hashie + version_gem (~> 1.1, >= 1.1.1) + spring (4.1.1) spring-commands-rspec (1.0.4) spring (>= 0.9.1) - sprockets (4.0.2) + sprockets (4.2.0) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) sprockets (>= 3.0.0) - temple (0.8.2) - thor (1.0.1) - thread_safe (0.3.6) - tilt (2.0.10) - timecop (0.9.1) - tzinfo (1.2.7) - thread_safe (~> 0.1) + temple (0.10.0) + thor (1.3.2) + tilt (2.0.11) + timecop (0.9.6) + timeout (0.3.1) + tinymce-rails (6.3.1) + railties (>= 3.1.1) + typhoeus (1.4.0) + ethon (>= 0.9.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) + unaccent (0.4.0) underscore-rails (1.8.3) - warden (1.2.8) - rack (>= 2.0.6) - web-console (3.7.0) - actionview (>= 5.0) - activemodel (>= 5.0) + version_gem (1.1.1) + warden (1.2.9) + rack (>= 2.0.9) + web-console (4.2.0) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) bindex (>= 0.4.0) - railties (>= 5.0) - webdrivers (4.4.1) - nokogiri (~> 1.6) - rubyzip (>= 1.3.0) - selenium-webdriver (>= 3.0, < 4.0) - webpacker (5.1.1) + railties (>= 6.0.0) + webpacker (5.4.4) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) - websocket-driver (0.7.3) + websocket (1.2.9) + websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) + zeitwerk (2.6.18) PLATFORMS ruby @@ -429,39 +536,52 @@ PLATFORMS DEPENDENCIES actionview-encoded_mail_to active_model_serializers (~> 0.10.0) + amazing_print annotate + aws-sdk-s3 better_errors binding_of_caller - bootsnap + bootsnap (~> 1.13) bootstrap-multiselect-rails (~> 0.9.9) bootstrap-sass (~> 3.4.1) - capybara (~> 3.33) + capybara (~> 3.37) chartkick coderay (~> 1.0) - country_select (~> 1.3) - database_cleaner (~> 1.6) - devise (~> 4.7) + country_select (~> 8.0) + database_cleaner (~> 2.0) + devise (~> 4.8) diffy dotenv-rails - draper (~> 3.0.1) + draper (~> 4.0) factory_bot_rails faker + fastly foreman groupdate growl guard guard-livereload (~> 2.1) guard-rspec - haml (~> 5.0.0) + haml (~> 5.0) haml-rails + honeybadger (~> 5.20) html2haml (~> 2.2) + image_processing (~> 1.2) jbuilder jquery-datatables-rails jquery-rails jquery-ui-rails launchy + matrix + mime-types + mime-types-data + mimemagic! + net-imap + net-pop + net-smtp nokogiri omniauth-github + omniauth-rails_csrf_protection (~> 1.0) omniauth-twitter paper_trail pg @@ -471,32 +591,32 @@ DEPENDENCIES puma pundit rack-mini-profiler - rack-timeout (~> 0.5) - rails (~> 5.2) + rack-timeout (~> 0.6) + rails (~> 6.1.6) rails-assets-momentjs! rails-controller-testing - rails_12factor react-rails - redcarpet (~> 3.4) - responders (~> 2.4.0) + redcarpet (~> 3.5) + rexml rspec rspec-rails sassc-rails selectize-rails selenium-webdriver + sendgrid-ruby sidekiq simple_form spring spring-commands-rspec timecop + tinymce-rails uglifier (>= 1.3.0) underscore-rails web-console - webdrivers (~> 4.0) webpacker RUBY VERSION - ruby 2.6.5p114 + ruby 3.1.2p20 BUNDLED WITH - 1.17.3 + 2.3.17 diff --git a/Procfile b/Procfile index 9002b5942..a00c59c52 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,3 @@ +release: bundle exec rake db:migrate web: bundle exec puma -C config/puma.rb worker: bundle exec sidekiq -t 25 -q default diff --git a/README.md b/README.md index b01fa9a2f..a3c99cc4d 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,12 @@ The CFP App does not provide a public facing website for your conference, though ### Prerequisite Requirements -* Ruby 2.6.5 -* Bundler (was installed with 1.17.3) -* PostgreSQL +* Ruby 3.1.2 (set in `.ruby-version`) +* Bundler (was installed with 2.3.11) +* PostgreSQL 14.1 +* Google Chrome browser must be installed to run tests -Make sure you have Ruby and Postgres installed in your environment. Double check in the [Gemfile](../blob/master/Gemfile) for the exact supported version. This is a Rails 5 app and uses bundler to install all required gems. We are also making the assumption that you're familiar with how Rails apps are setup and deployed. If this is not the case then you'll want to refer to documentation that will bridge any gaps in the instructions below. +Make sure you have Ruby and Postgres installed in your environment. Double check in the [Gemfile](../blob/main/Gemfile) for the exact supported version. This is a Rails 6 app and uses bundler to install all required gems. We are also making the assumption that you're familiar with how Rails apps are setup and deployed. If this is not the case then you'll want to refer to documentation that will bridge any gaps in the instructions below. Run [bin/setup](bin/setup) script to install gem dependencies and setup database for development. @@ -30,29 +31,28 @@ Run [bin/setup](bin/setup) script to install gem dependencies and setup database bin/setup ``` -This will create `.env` and a development database with seed data. Seeds will make an admin user with an email of `an@admin.com` and password of `userpass` to get you started. +This script will: -You will also need to install the JavaScript packages. To do that run: +- Install Ruby dependencies +- Install Javascript dependencies +- Setup the database +- Clear old logs and tempfiles +- Create `.env` and a development database with seed data. Seeds will make an admin user with an email of `an@admin.com` and password of `userpass` to get you started +- Run the test suite -```bash -yarn install --check-files -``` - -Start the server: +To start the server on port 3000: ```bash bin/rails server ``` -Runs on port 3000. - If you have the heroku toolbelt installed you can also use: ```bash heroku local ``` -This will boot up using Foreman and allow the .env file to be read / set for use locally. Runs on port 5000. +This will boot up using Foreman and allow the `.env` file to be read / set for use locally. Runs on port 5000. ### Environment variables @@ -62,6 +62,10 @@ This will boot up using Foreman and allow the .env file to be read / set for use POSTGRES_USER (dev/test only) MAIL_HOST (production only - from host) MAIL_FROM (production only - from address) + SMTP_ADDRESS (production only - address of SMTP server, defaults to 'smtp.sendgrid.net') + SMTP_PORT (production only - port of SMTP server, defaults to 587) + SMTP_USERNAME (production only - SMTP account username, defaults to 'apikey' for use with SendGrid) + SMTP_PASSWORD (production only - SMTP account password, defaults to feching environment variable 'SENDGRID_API_KEY') SECRET_TOKEN (production only) GITHUB_KEY GITHUB_SECRET @@ -89,7 +93,7 @@ There are five user roles in the CFP App. To log in as a user type in developmen ## Deployment on Heroku -The app was written with a Heroku deployment stack in mind. You can easily deploy the application using the button below, or you can deploy it anywhere assuming you can run Ruby 2.6.5 and Rails 5.2.2 with a PostgreSQL database and an SMTP listener. +The app was written with a Heroku deployment stack in mind. You can easily deploy the application using the button below, or you can deploy it anywhere assuming you can run Ruby and Rails with a PostgreSQL database and an SMTP listener. The Heroku stack will use the following free addons: @@ -210,6 +214,11 @@ One thing that will happen is you will get notifications showing you what talks ...Coming Soon... +### Website Hosting beta + +CFP App can now host a website for your actual conference including managing the +static content pages and dynamic schedule program pages. See [Website +Documentation](/docs/website_documentation.md) for more details. ## Customizing and Contributing diff --git a/app.json b/app.json index 90ce052f0..997e7aa02 100644 --- a/app.json +++ b/app.json @@ -1,4 +1,5 @@ { + "stack": "heroku-22", "name": "CFP-App", "description": "Conference call for proposal management application by Ruby Central", "keywords": [ @@ -8,10 +9,7 @@ ], "addons": [ "heroku-postgresql", - "heroku-redis", - "newrelic:wayne", - "papertrail:choklad", - "sendgrid:starter" + "heroku-redis" ], "scripts": { "postdeploy": "bundle exec rake db:schema:load db:seed" diff --git a/app/assets/images/themes/default/arrow-white.png b/app/assets/images/themes/default/arrow-white.png new file mode 100644 index 000000000..de53891a0 Binary files /dev/null and b/app/assets/images/themes/default/arrow-white.png differ diff --git a/app/assets/images/themes/default/caret-2.png b/app/assets/images/themes/default/caret-2.png new file mode 100644 index 000000000..68faef6d4 Binary files /dev/null and b/app/assets/images/themes/default/caret-2.png differ diff --git a/app/assets/images/themes/default/caret.png b/app/assets/images/themes/default/caret.png new file mode 100644 index 000000000..5880c829c Binary files /dev/null and b/app/assets/images/themes/default/caret.png differ diff --git a/app/assets/images/themes/default/close.png b/app/assets/images/themes/default/close.png new file mode 100644 index 000000000..22bc205dd Binary files /dev/null and b/app/assets/images/themes/default/close.png differ diff --git a/app/assets/images/themes/default/closed-filter.png b/app/assets/images/themes/default/closed-filter.png new file mode 100644 index 000000000..a141d7b36 Binary files /dev/null and b/app/assets/images/themes/default/closed-filter.png differ diff --git a/app/assets/images/themes/default/fb-icon.svg b/app/assets/images/themes/default/fb-icon.svg new file mode 100644 index 000000000..1aae1b650 --- /dev/null +++ b/app/assets/images/themes/default/fb-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/themes/default/image-placeholder.png b/app/assets/images/themes/default/image-placeholder.png new file mode 100644 index 000000000..12029b9c3 Binary files /dev/null and b/app/assets/images/themes/default/image-placeholder.png differ diff --git a/app/assets/images/themes/default/instagram-icon.svg b/app/assets/images/themes/default/instagram-icon.svg new file mode 100644 index 000000000..1db4ec832 --- /dev/null +++ b/app/assets/images/themes/default/instagram-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/themes/default/menu-close.svg b/app/assets/images/themes/default/menu-close.svg new file mode 100644 index 000000000..c26d35ae3 --- /dev/null +++ b/app/assets/images/themes/default/menu-close.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/assets/images/themes/default/menu.svg b/app/assets/images/themes/default/menu.svg new file mode 100644 index 000000000..0a08570a0 --- /dev/null +++ b/app/assets/images/themes/default/menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/themes/default/open-filter.png b/app/assets/images/themes/default/open-filter.png new file mode 100644 index 000000000..3bea49dba Binary files /dev/null and b/app/assets/images/themes/default/open-filter.png differ diff --git a/app/assets/images/themes/default/pin-icon.svg b/app/assets/images/themes/default/pin-icon.svg new file mode 100644 index 000000000..eb52edcac --- /dev/null +++ b/app/assets/images/themes/default/pin-icon.svg @@ -0,0 +1,11 @@ + + + + Combined Shape + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/app/assets/images/themes/default/room-icon.png b/app/assets/images/themes/default/room-icon.png new file mode 100644 index 000000000..e705dbf08 Binary files /dev/null and b/app/assets/images/themes/default/room-icon.png differ diff --git a/app/assets/images/themes/default/sponsor-star.png b/app/assets/images/themes/default/sponsor-star.png new file mode 100644 index 000000000..24a56b33c Binary files /dev/null and b/app/assets/images/themes/default/sponsor-star.png differ diff --git a/app/assets/images/themes/default/sponsor-tier.svg b/app/assets/images/themes/default/sponsor-tier.svg new file mode 100644 index 000000000..deaa5f61b --- /dev/null +++ b/app/assets/images/themes/default/sponsor-tier.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/assets/images/themes/default/twitter-icon.svg b/app/assets/images/themes/default/twitter-icon.svg new file mode 100644 index 000000000..3d3e76606 --- /dev/null +++ b/app/assets/images/themes/default/twitter-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 6f1a6a699..046d6f668 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -26,4 +26,5 @@ //= require momentjs //= require selectize //= require palette.js +//= require tinymce //= require_tree . diff --git a/app/assets/javascripts/base.js b/app/assets/javascripts/base.js index b0e026a8b..0c40d169b 100644 --- a/app/assets/javascripts/base.js +++ b/app/assets/javascripts/base.js @@ -5,6 +5,21 @@ $(document).ready(function() { setTimeout(function() { $(".alert").not('.alert-confirm, .scheduling-error').alert('close'); }, 5000); + + $(".selectize-sortable").selectize({ + plugins: ["drag_drop"], + }); + + $(".selectize-create").selectize({ + plugins: ["drag_drop"], + persist: false, + create: function (input) { + return { + value: input, + text: input, + }; + }, + }); }); // Datatable extension for reseting sort order diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index def1833d7..797910bc1 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -47,6 +47,7 @@ @import "modules/buttons"; @import "modules/calendar"; @import "modules/coderay"; +@import "modules/code-mirror"; @import "modules/dashboard"; @import "modules/data_tables"; @import "modules/discussions"; @@ -73,3 +74,4 @@ @import "modules/tooltips"; @import "modules/widgets"; @import "modules/teammates"; +@import "modules/staff-website-page"; diff --git a/app/assets/stylesheets/default.css.scss b/app/assets/stylesheets/default.css.scss new file mode 100644 index 000000000..017a897f2 --- /dev/null +++ b/app/assets/stylesheets/default.css.scss @@ -0,0 +1 @@ +@import 'themes/default/application'; diff --git a/app/assets/stylesheets/modules/_code-mirror.scss b/app/assets/stylesheets/modules/_code-mirror.scss new file mode 100644 index 000000000..9a893255d --- /dev/null +++ b/app/assets/stylesheets/modules/_code-mirror.scss @@ -0,0 +1,46 @@ +.CodeMirror { + /* Bootstrap Settings */ + min-height: 60vh; + box-sizing: border-box; + margin: 0; + font: inherit; + overflow: auto; + font-family: inherit; + display: block; + width: 100%; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); /* Bootstrap 3 */ + box-shadow: none; /* Bootstrap 4 */ + transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out; + /* Code Mirror Settings */ + font-family: monospace; + position: relative; + overflow: hidden; +} + +.CodeMirror-focused { + /* Bootstrap Settings */ + border-color: #66afe9; + outline: 0; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); /* Bootstrap 3 */ + box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25); /* Bootstrap 4 */ + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} + +#staff_websites_edit, #staff_websites_new { + .CodeMirror { + height: 15vh; + min-height: 15vh; + } + .CodeMirror-focused { + height: 60vh; + min-height: 60vh; + } +} diff --git a/app/assets/stylesheets/modules/_forms.scss b/app/assets/stylesheets/modules/_forms.scss index 929aa0432..597871526 100644 --- a/app/assets/stylesheets/modules/_forms.scss +++ b/app/assets/stylesheets/modules/_forms.scss @@ -49,8 +49,19 @@ span[title="required"] { // e.g., the show page for proposal .fieldset-legend { @extend legend; + padding-top: 20px; } .cfp-date-input { width: 50%; } + +input.boolean { + margin: 4px; +} + +.inline-form { + display: flex; + gap: 10px; + align-items: center; +} diff --git a/app/assets/stylesheets/modules/_navbar.scss b/app/assets/stylesheets/modules/_navbar.scss index d952aab01..aaa8faec6 100644 --- a/app/assets/stylesheets/modules/_navbar.scss +++ b/app/assets/stylesheets/modules/_navbar.scss @@ -174,7 +174,9 @@ } } -.navbar-fixed-top.program-subnav, .navbar-fixed-top.schedule-subnav { +.navbar-fixed-top.program-subnav, +.navbar-fixed-top.schedule-subnav, +.navbar-fixed-top.website-subnav { border-radius: 0; background-color: $dark-blue; border-color: $dark-blue; diff --git a/app/assets/stylesheets/modules/_staff-website-page.scss b/app/assets/stylesheets/modules/_staff-website-page.scss new file mode 100644 index 000000000..19b20a0ed --- /dev/null +++ b/app/assets/stylesheets/modules/_staff-website-page.scss @@ -0,0 +1,35 @@ +#page-preview-wrapper { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} + +.preview-flex { + display: flex; + width: 100%; + + .resize { + flex-grow: 1; + display: flex; + margin: 10px; + padding: 10px; + overflow: scroll; + resize: both; + border: 1px solid black; + height: 70vh; + max-width: 70vw; + .inner { + flex-grow: 1; + margin: 0; + padding: 0; + border: 0; + display: block; + } + } + iframe { + border: 0; + width: 100%; + height: 100vh; + } +} diff --git a/app/assets/stylesheets/themes/default/application.scss b/app/assets/stylesheets/themes/default/application.scss new file mode 100644 index 000000000..a0eab5eee --- /dev/null +++ b/app/assets/stylesheets/themes/default/application.scss @@ -0,0 +1,96 @@ +@import 'colors'; +@import 'shared'; +@import 'nav'; +@import 'footer'; +@import 'program'; +@import 'schedule'; +@import 'sponsors'; + +:root { + --sans-serif-font: 'Rubik'; + --secondary-body-font: 'Rubik' +} + +html { + font-family: var(--sans-serif-font), sans-serif !important; +} + +@supports (font-variation-settings: normal) { + html { + font-family: var(--sans-serif-font), sans-serif; + } +} + +body { + color: var(--text); + padding: 0; + margin: 0; + background: var(--body_background_color); + overflow: scroll; +} + +.hidden { + display: none!important; +} + +header + #content { + background-color: var(--main_content_background); + margin-top: 110px; + min-height: 95vh; +} + +p { + font-family: var(--secondary-body-font); + font-size: 14px; + line-height: 21px; + margin-bottom: 20px; + &:last-child { + margin: 0; + } +} + +a { + display: inline-block; + transition: all 0.25s ease-in-out; + text-decoration: underline; + color: var(--text); +} + +em { + font-style: italic; +} + +ul li { + list-style-type: none; + padding: 0 0 12px 12px; +} + +.btn { + display: inline-block; + padding: 10px 15px; + border-radius: 5px; + text-decoration: none; + margin: 0 0 12px 0; +} + +h3.section-title { + font-weight: 700; + font-size: 24px; + text-align: center; +} + +.page-title { + padding-top: 100px; + text-align: center; + font-size: 32px; + font-weight: 700; + line-height: 38px; + margin-bottom: 35px +} + +@media screen and (max-width: 900px) and (orientation: portrait), + (max-width: 823px) and (orientation: landscape) { + header + #content { + margin-top: 90px; + } +} diff --git a/app/assets/stylesheets/themes/default/colors.scss b/app/assets/stylesheets/themes/default/colors.scss new file mode 100644 index 000000000..5cc14f586 --- /dev/null +++ b/app/assets/stylesheets/themes/default/colors.scss @@ -0,0 +1,68 @@ +:root { + --beige: #F4EEE6; + --black: #000000; + --blue: #3444DA; + --blue-dark: #417D7A; + --blue-light: #A1E3D8; + --divider: #E0D2C7; + --divider-light: #EBEBEB; + --bronze: #B77D39; + --brown: #5A5655; + --gold: #FFB62C; + --green: #829757; + --green-dark: #417D7A; + --grey: #494644; + --grey-light: #C4C4C4; + --grey-lighter: #F5F5F5; + --grey-lightest: #E7E7E7; + --grey-secondary: #E6E6E6; + --orange: #FFBC90; + --orange-burnt: #EA652C; + --pastel-pink: #F94975; + --pastel-purple: #CD8DF4; + --pastel-peach: #FFBC80; + --pastel-blue-light: #A1E3D8; + --pastel-blue: #3444DA; + --pink: #FF88ab; + --platinum: #C7D7D7; + --purple: #7445C0; + --red: #C04F45; + --red-dark: #E14943; + --red-light: #E14943; + --silver: #E0E0E0; + --silver-border: #EBEBEB; + --sponsor-tag: #F8B400; + --teal: #48c892; + --text: var(--grey); + --text-secondary: #5E5E5E; + --yellow: #FFD801; + --white: #FFFFFF; + + // Layout + --body_background_color: var(--white); + --nav_background_color: var(--black); + --nav_text_color: var(--white); + --nav_link_hover: var(--white); + --main_content_background: var(--white); + + // Session Formats + --session-format-tag-1: var(--pastel-pink); + --session-format-tag-2: var(--pastel-purple); + --session-format-tag-3: var(--pastel-peach); + --session-format-tag-4: var(--pastel-blue-light); + --session-format-tag-5: var(--pastel-blue); + + // Track Tags + --bg-track-1: var(--yellow); + --bg-track-2: var(--orange-burnt); + --bg-track-3: var(--pink); + --bg-track-4: var(--purple); + --bg-track-5: var(--blue-dark); + --bg-track-6: var(--teal); + --bg-track-7: var(--red-dark); + --bg-track-8: var(--brown); + --bg-track-9: var(--silver); + --bg-track-10: var(--blue-light); + --bg-track-11: var(--bronze); + --bg-track-12: var(--orange); +} diff --git a/app/assets/stylesheets/themes/default/footer.scss b/app/assets/stylesheets/themes/default/footer.scss new file mode 100644 index 000000000..2bc78afac --- /dev/null +++ b/app/assets/stylesheets/themes/default/footer.scss @@ -0,0 +1,131 @@ +footer { + color: white; + position: relative; + z-index: 11; + background-color: black; + + .overlay { + background-color: black; + opacity: 0.9; + padding: 20px 30px 35px; + } + .footer-header { + .logo-container { + display: flex; + align-items: center; + margin-bottom: 20px; + .website-info { + margin-left: 10px; + a { + text-decoration: none; + font-weight: 700; + font-size: 18px; + } + } + } + } + + .footer-content { + display: grid; + width: 100%; + grid-template-columns: 1fr 1fr; + gap: 40px; + border-bottom: 1px solid white; + @media screen and (max-width: 900px) and (orientation: portrait), + (max-width: 823px) and (orientation: landscape) { + grid-template-columns: 1fr; + } + + .footer-event-details { + max-width: 470px; + padding-bottom: 20px; + .location-section { + display: flex; + div { + flex-grow: 1 + } + .location-details { + padding-bottom: 20px; + .date { + font-weight: 700; + font-size: 16px; + padding-bottom: 5px; + } + } + .register-button { + color: #E14943; + background-color: white; + padding: 10px 20px; + border-radius: 4px; + &:hover { + transform: translateX(2px) translateY(-2px); + } + } + } + + .footer-about { + padding-bottom: 20px; + p { + font-weight: 400; + } + p:first-child { + font-weight: 700; + } + } + + .footer-social-links { + display: flex; + flex-direction: row; + align-items: center; + a { + width: 30px; + height: 30px; + display: inline-block; + margin: 0 6px 0 0; + &:hover { + transform: translateX(2px); + } + } + .twitter-link { + background: asset-url('themes/default/twitter-icon.svg') center center no-repeat; + } + .facebook-link { + background: asset-url('themes/default/fb-icon.svg') center center no-repeat; + } + .instagram-link { + background: asset-url('themes/default/instagram-icon.svg') center center no-repeat; + } + } + } + + .footer-event-links { + display: flex; + flex-wrap: wrap; + .footer-category-links { + flex-grow: 1; + margin-right: 40px; + margin-bottom: 40px; + + .footer-category-name { + font-weight: 700; + } + + a { + display: inline-block; + margin-right: 10px; + text-decoration: none; + padding-top: 20px; + &:hover { + transform: translateX(2px) translateY(-2px); + } + } + } + } + } + + .footer-copyright { + text-align: center; + font-size: 14px; + margin-top: 20px; + } +} diff --git a/app/assets/stylesheets/themes/default/nav.scss b/app/assets/stylesheets/themes/default/nav.scss new file mode 100644 index 000000000..9811008ca --- /dev/null +++ b/app/assets/stylesheets/themes/default/nav.scss @@ -0,0 +1,99 @@ +#main-header { + position: fixed; + top: 0; + width: 100%; + color: var(--nav_text_color); + z-index: 11; + + .overlay { + padding: 30px; + background: var(--nav_background_color); + opacity: 0.9; + display: flex; + justify-content: space-between; + align-items: center; + } + + #header-logo-container { + display: flex; + align-items: center; + margin-left: 10px; + + .website-info { + margin-left: 10px; + a { + text-decoration: none; + font-weight: 700; + font-size: 18px; + } + } + } + + #main-nav { + margin-right: 15px; + display: flex; + gap: 25px; + + a { + display: block; + color: var(--nav_text_color); + &:hover { + color: var(--nav_link_hover); + transform: translateX(2px); + } + } + } + + a { + text-decoration: none; + width: auto; + &#menu-toggle { + display: none; + } + } +} + +@media screen and (max-width: 900px) and (orientation: portrait), + (max-width: 823px) and (orientation: landscape) { + #main-header .overlay{ + padding: 20px; + z-index: 20; + flex-direction: column; + align-items: start; + + #header-logo-container { + white-space: nowrap; + } + + a { + &#menu-toggle { + display: block; + width: 64px; + height: 64px; + position: absolute; + right: 0; + background: image-url('themes/default/menu.svg') center center no-repeat; + transition: none; + &.menu-toggle--opened { + background: image-url('themes/default/menu-close.svg') center center no-repeat; + } + } + } + } + #main-nav { + margin-left: 10px; + display: none!important; + transition: transform .35s ease-in-out; + a { + padding: 0; + height: 36px; + line-height: 36px; + display: block; + text-indent: 8px; + } + &.menu-mobile--opened { + display: block!important; + transform: translate(0, 0)!important; + } + } + } diff --git a/app/assets/stylesheets/themes/default/program.scss b/app/assets/stylesheets/themes/default/program.scss new file mode 100644 index 000000000..a48a432f7 --- /dev/null +++ b/app/assets/stylesheets/themes/default/program.scss @@ -0,0 +1,177 @@ +.program-sessions-wrapper { + display: flex; + margin: auto; + max-width: 1600px; + justify-content: center; + flex-direction: row; + flex-wrap: wrap; + gap: 14px; + background-color: var(--main_content_background); + padding: 20px 0 0 0; + margin-bottom: 40px; + + &.empty .empty-placeholder { + display: block !important; + text-align: center; + margin-top: 30px; + + h3 { + font-size: 18px; + font-weight: 800; + margin-bottom: 8px; + } + + p { + font-family: var(--secondary-body-font); + line-height: 130%; + } + } +} + +.session-card { + display: flex; + position: relative; + justify-content: space-between; + flex-direction: column; + width: 30%; + padding: 1.875rem 1.25rem .625rem 1.25rem; + border-radius: 10px; + border: 1px solid var(--grey-light); + box-shadow: 2px 4px 16px 4px rgba(0, 0, 0, 0.1); + + .seesion-title-abstract-container { + height: 13.75rem; + } + + .session-speaker-track-container { + display: flex; + flex-direction: column; + justify-content: end; + margin-bottom: .75rem; + } + + .tag-wrapper { + .session-format-tag, + .track-tag { + margin-bottom: .25rem; + } + } + + .session-title { + font-size: 21px; + font-weight: 700; + margin-bottom: .6rem; + text-decoration: #F8B400 underline solid 2px; + text-underline-offset: 0.1em; + } + + .session-abstract { + font-family: var(--secondary-body-font); + height: 6.25rem; + font-size: 16px; + font-weight: 400; + color: var(--text-secondary); + white-space: no-wrap; + overflow: hidden; + } + + .session-speaker-details { + font-size: 16px; + font-weight: 700; + margin-bottom: 12px; + } + + .session-accent { + width: 100%; + height: .5rem; + position: absolute; + bottom: 0; + left: 0; + border-radius: 0 0 100px 100px; + } +} + +.fly-out-wrapper { + .program-session-detail { + margin: 0 40px 50px 30px; + overflow: scroll; + + .program-session-title { + font-size: 21px; + font-weight: 700; + margin-bottom: 15px; + } + + .program-session-tags { + display: flex; + flex-wrap: wrap; + margin-bottom: 25px; + } + + .program-session-about { + border-top: 1px solid #D1D1D1; + font-family: var(--secondary-body-font); + padding-top: 25px; + width: 100%; + font-weight: 400; + font-size: 16px; + line-height: 25px; + margin-bottom: 25px; + + + span, a { + font-family: var(--sans-serif-font); + font-weight: 700; + font-size: 16px; + margin-bottom: 6px; + display: block; + } + + a { + margin-top: 0.6rem; + text-decoration: #F8B400 underline solid 2px; + text-underline-offset: 0.1em; + } + } + + .program-session-authors { + .speaker-name { + font-weight: 700; + font-size: 16px; + margin-bottom: 6px; + } + + .speaker-bio { + font-family: var(--secondary-body-font); + margin-bottom: 15px; + font-weight: 400; + font-size: 16px; + line-height: 25px; + } + } + } +} + +@media screen and (max-width: 1200px) and (orientation: portrait), + (max-width: 1123px) and (orientation: landscape) { + .program-sessions-wrapper { + padding: 15px 10px; + } + .session-card { + width: 40%; + } + } + + + + +@media screen and (max-width: 900px) and (orientation: portrait), + (max-width: 823px) and (orientation: landscape) { + .program-sessions-wrapper { + padding: 25px; + } + + .session-card { + width: 100%; + } + } diff --git a/app/assets/stylesheets/themes/default/schedule.scss b/app/assets/stylesheets/themes/default/schedule.scss new file mode 100644 index 000000000..54fff4738 --- /dev/null +++ b/app/assets/stylesheets/themes/default/schedule.scss @@ -0,0 +1,360 @@ +.schedule-block-container.empty { display: none !important; } +.schedule-page-wrapper { + .page-title { + padding-top: 100px; + text-align: center; + font-size: 36px; + font-weight: 700; + line-height: 38px; + margin-bottom: 15px; + } + + .schedule-session-title { + font-size: 21px; + font-weight: 700; + margin-bottom: .3rem; + text-decoration: #F8B400 underline solid 2px; + text-underline-offset: 0.1em; + } + + .schedule-room { + display: flex; + align-items: center; + + .room-icon { + margin-right: 7px; + } + } + + .schedule-tags { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + } + + .schedule-block-container { + display: grid; + grid-template-columns: 250px 1fr; + grid-template-areas: "time-duration program-sessions"; + border-bottom: 3px solid var(--black); + + &:last-child { + border-bottom: 1px solid var(--divider-light); + margin-bottom: 50px; + } + + .schedule-duration { + grid-area: time-duration; + border-right: 1px solid var(--grey-light); + text-align: center; + padding-top: 40px; + font-weight: 700; + font-size: 16px; + + .session-end-time::before { + content: "-"; + } + } + + .schedule-grid { + display: flex; + grid-area: program-sessions; + flex-wrap: wrap; + } + + .time-slot-card { + border-right: 1px solid var(--divider-light); + border-bottom: 1px solid var(--divider-light); + width: 25%; + padding: 40px 25px 0 20px; + display: flex; + flex-direction: column; + justify-content: space-between; + + .schedule-title-room-wrapper { + margin-bottom: 35px; + } + + .schedule-speaker-tag-wrapper { + margin-bottom: 30px; + } + + &:only-child, + &:last-child { + border-right: none !important; + } + + &:nth-child(4n+4) { + border-right: none; + } + + .schedule-presenter { + font-size: 16px; + font-weight: 700; + margin-bottom: 15px; + + .sponsor-presents { + margin-right: 7px; + + &::after { + padding-left: 5px; + content: "presents" + } + } + } + } + } + + .schedule-day-wrapper { + margin-bottom: 50px; + + &.empty { + .empty-placeholder { + display: block !important; + text-align: center; + margin-top: 30px; + + h3 { + font-size: 18px; + font-weight: 800; + margin-bottom: 8px; + } + + p { + font-family: var(--secondary-body-font); + line-height: 130%; + } + } + } + } + + .schedule-block-container.empty { + display: none !important; + } + + + .fly-out-wrapper { + .schedule-detail-wrapper { + margin: 0 40px 50px 50px; + padding-left: 5px; + overflow: scroll; + + .schedule-date { + display: block; + color: var(--red-light); + font-size: 22px; + font-weight: 600; + margin-bottom: 10px; + } + + .schedule-time { + display: flex; + font-size: 18px; + padding-bottom: 25px; + margin-bottom: 30px; + border-bottom: 1px solid var(--divider-light); + + span { display: inline-block; } + .start-time { + &::after { + content: " – "; + margin-right: 4px; + } + } + } + + .schedule-session-title { + text-decoration: none; + } + + .schedule-room { + margin-bottom: 35px; + } + + .sponsored-by { + font-weight: 700; + margin-bottom: 10px; + display: block; + + &::before { + content: "Sponsored by " + } + } + + .schedule-tags { + padding-bottom: 25px; + margin-bottom: 30px; + border-bottom: 1px solid var(--divider-light); + } + + .schedule-time-slot-details { + border-bottom: 1px solid var(--divider-light); + margin-bottom: 30px; + } + + .speaker-description, + .schedule-description { + margin-bottom: 20px; + line-height: 150%; + + span { + display: block; + font-weight: 800; + font-size: 20; + margin-bottom: 6px; + } + } + + .schedule-sponsor-content { + .sponsor-logo { + max-width: 300px; + margin-bottom: 5px; + } + + .sponsor-tier-tag { + padding: 5px 4px; + border: 1px solid var(--silver-border); + font-size: 12px; + font-weight: 600; + border-radius: 4px; + display: flex; + align-items: center; + width: min-content; + text-transform: capitalize; + margin-bottom: 15px; + + .sponsor-tier-star { + margin-right: 3px; + display: inline-block; + -webkit-mask-image: asset_url('themes/default/sponsor-tier.svg'); + -webkit-mask-repeat: no-repeat; + -webkit-mask-size: 15px 15px; + mask-image: asset_url('themes/default/sponsor-tier.svg'); + mask-repeat: no-repeat; + mask-size: 15px 15px; + height: 15px; + width: 15px; + + &.platinum { background-color: var(--platinum); } + &.gold { background-color: var(--gold); } + &.silver { background-color: var(--silver); } + &.bronze { background-color: var(--bronze); } + &.other { background-color: var(--grey-light) }; + &.supporter { background-color: var(--grey-light) }; + } + } + + p { + line-height: 160%; + margin-bottom: 30px; + } + + .sponsor-offer-wrapper { + background-color: var(--beige); + border-radius: 10px; + padding: 35px; + + span { + display: block; + + &.offer-headline { + font-weight: 700; + font-size: 16px; + margin-bottom: 3px; + } + + &.offer_text { + margin-bottom: 40px; + } + } + + .offer-url { + color: var(--white); + background-color: var(--red-light); + padding: 14px 24px; + border-radius: 4px; + + &::after { + content: ""; + margin-left: 9px; + background-image: asset_url('themes/default/arrow-white.png'); + display: inline-block; + height: 15px; + width: 15px; + } + } + } + } + } + } +} + +@media screen and (max-width: 1200px) and (orientation: portrait), + (max-width: 1123px) and (orientation: landscape) { + .schedule-page-wrapper { + .time-slot-card { + width: 50% !important; + + &:nth-child(4n+4) { + border-right: 1px solid var(--divider-light); + } + &:nth-child(2n+2) { + border-right: none; + } + } + } + } + + +@media screen and (max-width: 900px) and (orientation: portrait), + (max-width: 823px) and (orientation: landscape) { + .schedule-page-wrapper { + .sub-nav { + margin-bottom: 30px; + } + + .schedule-block-container { + display: block; + + border-top: 3px solid var(--black); + + .schedule-duration { + float: right; + position: relative; + background-color: var(--body_background_color); + top: -13px; + white-space: nowrap; + flex-direction: row; + padding: 0 20px 0 15px; + } + } + + .schedule-grid { + flex-direction: column; + width: 100% + } + + .time-slot-card { + border-right: none !important; + border-bottom: 1px solid var(--divider-light); + width: 100% !important; + display: flex; + flex-direction: column; + padding: 0 20px; + } + } + } + + @media screen and (max-width: 700px) and (orientation: portrait), + (max-width: 523px) and (orientation: landscape) { + .schedule-page-wrapper { + nav.sub-nav { + ul { + display: flex; + flex-direction: column; + justify-content: center; + } + } + } + } diff --git a/app/assets/stylesheets/themes/default/shared.scss b/app/assets/stylesheets/themes/default/shared.scss new file mode 100644 index 000000000..a9e18b065 --- /dev/null +++ b/app/assets/stylesheets/themes/default/shared.scss @@ -0,0 +1,258 @@ +.bg-track-1 { background-color: var(--bg-track-1); } +.bg-track-2 { background-color: var(--bg-track-2); } +.bg-track-3 { background-color: var(--bg-track-3); } +.bg-track-4 { background-color: var(--bg-track-4); } +.bg-track-5 { background-color: var(--bg-track-5); } +.bg-track-6 { background-color: var(--bg-track-6); } +.bg-track-7 { background-color: var(--bg-track-7); } +.bg-track-8 { background-color: var(--bg-track-8); } +.bg-track-9 { background-color: var(--bg-track-9); } +.bg-track-10 { background-color: var(--bg-track-10); } +.bg-track-11 { background-color: var(--bg-track-11); } +.bg-track-12 { background-color: var(--bg-track-12); } + +.session-format-bg-1 { background-color: var(--session-format-tag-1); } +.session-format-bg-2 { background-color: var(--session-format-tag-2); } +.session-format-bg-3 { background-color: var(--session-format-tag-3); } +.session-format-bg-4 { background-color: var(--session-format-tag-4); } +.session-format-bg-5 { background-color: var(--session-format-tag-5); } + +.session-format-tag { + display: inline-block; + width: min-content; + white-space: nowrap; + padding: 8px 10px 8px 50px; + border: 1px solid var(--divider-light); + border-radius: 100px; + font-size: 12px; + margin-right: 8px; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 26px; + transform: translate(-50%, -50%); + height: 12px; + width: 24px; + border-radius: 150px 150px 0 0; + } + &.session-format-tag-1::before { background-color: var(--session-format-tag-1); } + &.session-format-tag-2::before { background-color: var(--session-format-tag-2); } + &.session-format-tag-3::before { background-color: var(--session-format-tag-3); } + &.session-format-tag-4::before { background-color: var(--session-format-tag-4); } + &.session-format-tag-5::before { background-color: var(--session-format-tag-5); } +} + +.track-tag { + font-size: 12px; + padding: 7px 10px; + border-radius: 20px; + border: 1px solid var(--divider-light); + display: inline-block; + width: min-content; + white-space: nowrap; + + .track-dot{ + height: 16px; + width: 16px; + display: inline-block; + position: relative; + top: 3px; + border-radius: 50%; + margin-right: 4px; + } +} + +.sponsored-tag { + display: flex; + align-items: center; + width: min-content; + font-size: 13px; + font-weight: 700; + padding: 8px 11px; + margin-right: 6px; + border-radius: 20px; + background-color: var(--sponsor-tag); + + &::before { + content: ""; + margin-right: 5px; + display: inline-block; + background-image: asset_url('themes/default/sponsor-star.png'); + height: 16px; + width: 16px; + } +} + +.fly-out-wrapper { + position: fixed; + top: 0; + bottom: 0; + padding-top: 200px; + width: 40%; + height: 100%; + z-index: 10; + background-color: var(--white); + border: 1px solid var(--black); + transition: all 1s; + overflow: scroll; + + + &.open { + right: 0; + } + + &.closed { + right: -40%; + } + + .close-icon { + position: absolute; + right: 40px; + top: 164px; + } + + .program-session-detail { + margin: 0 40px 50px 30px; + overflow: scroll; + } +} + +.filter-toggle-button { + text-align: center; + margin: 0 auto 15px; + display: block; + + span { pointer-events: none; } + img { + margin: 0 auto 1px; + display: block; + padding: 6px 6px; + border-radius: 50%; + border: 2px solid var(--red-light); + pointer-events: none; + } + + &:hover img { + transform: scale(110%); + box-shadow: 0 2px 9px rgba(239, 70, 70, .5); + } +} + +.filter-wrapper { + margin: 0 40px 50px 30px; + + h3 { + color: var(--red-light); + font-size: 21px; + font-weight: 700; + } + + button { + color: var(--text-secondary) + } + + .form-group { + display: flex; + flex-direction: column; + margin: 15px 0; + padding: 15px 0; + border-top: 1px solid var(--divider-light); + + h5 { + font-weight: 700; + text-transform: uppercase; + font-size: 16px; + margin-bottom: 7px; + } + + input { + margin-right: 4px; + } + } + + .speaker-group { + border-bottom: 1px solid var(--divider-light); + padding: 5px 0 5px 5px; + + .speaker-group-label { + width: 100%; + margin-bottom: 6px 0; + * { pointer-events: none;} + + span { + font-weight: 700; + text-transform: uppercase; + font-size: 16px; + min-width: 20px; + display: inline-block; + } + + img { + display: inline-block; + margin-right: 8px; + transform: rotate(-90deg); + } + } + } +} + +.sub-nav { + display: flex; + align-items: center; + justify-content: center; + padding-bottom: 6px; + border-bottom: 1px solid var(--divider-light); + + ul { + margin: 0; + padding: 0; + display: flex; + gap: 30px; + li { + margin: 0; + padding: 0; + a { + font-size: 24px; + font-weight: 700; + display: block; + color: var(--grey-light); + width: 100%; + margin: auto; + text-align: center; + text-decoration: none; + &:hover, &.selected { + color: var(--red-light); + box-shadow: 0 4px var(--main_content_background), + 0 7px var(--red-light); + transition: all 300ms; + } + + .visible-count { + pointer-events: none; + } + } + } + } +} + +@media screen and (max-width: 1200px) and (orientation: portrait), + (max-width: 1123px) and (orientation: landscape) { + .fly-out-wrapper { + width: 70%; + &.open { right: 0 } + &.closed { right: -70%; } + } + } + + @media screen and (max-width: 900px) and (orientation: portrait), + (max-width: 823px) and (orientation: landscape) { + .fly-out-wrapper { + width: 100% !important; + &.open { right: 0; } + &.closed { right: -100%; } + } + } + diff --git a/app/assets/stylesheets/themes/default/sponsors.scss b/app/assets/stylesheets/themes/default/sponsors.scss new file mode 100644 index 000000000..8464abc17 --- /dev/null +++ b/app/assets/stylesheets/themes/default/sponsors.scss @@ -0,0 +1,195 @@ +.sponsor-tier-wrapper { + margin-bottom: 20px; + + .tier-title { + margin-bottom: 15px; + font-size: 18px; + font-weight: 600; + text-transform: capitalize; + text-align: center; + &::after { + content: "Sponsors"; + } + } + + .sponsors-wrapper { + column-count: 3; + margin: auto; + padding: 0 20px; + max-width: 1600px; + + .sponsor-wrapper { + position: relative; + margin-bottom: 10px; + -webkit-column-break-inside: avoid; /* Chrome, Safari, Opera */ + page-break-inside: avoid; /* Firefox */ + break-inside: avoid; /* IE 10+ */ + border: 1px solid var(--grey-light); + padding: 22px 15px 15px 15px; + border-radius: 10px; + + .sponsor-primary-logo { + max-height: 50px; + max-width: 170px; + display: block; + margin-bottom: 15px; + } + } + } +} + +.sponsor-tier-badge { + position: absolute; + top: 0; + right: 0; + padding: 3px 9px; + display: flex; + align-items: center; + border-radius: 0 10px 0 10px; + font-weight: 700; + text-transform: capitalize; + + &::before { + height: 15px; + width: 15px; + background-image: url('images/themes/default/sponsor-star.png'); + } + + img { + margin-right: 4px; + } + + &.platinum { background-color: var(--platinum); } + &.gold { background-color: var(--gold); } + &.silver { background-color: var(--silver); } + &.bronze { background-color: var(--bronze); } + &.other { background-color: var(--grey-light); } + &.supporter { background-color: var(--grey-light); } +} + +.sponsors-footer-wrapper { + width: 100%; + max-width: 1600px; + margin: auto; + column-count: 3; + margin-bottom: 30px; + + .sponsor-footer-card { + width: 100%; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-bottom: 10px; + border: 1px solid var(--grey-lightest); + border-radius: 10px; + box-shadow: 2px 4px 16px 4px rgba(0, 0, 0, 0.05); + -webkit-column-break-inside: avoid; /* Chrome, Safari, Opera */ + page-break-inside: avoid; /* Firefox */ + break-inside: avoid; /* IE 10+ */ + + .sponsor-footer-logo-wrapper { + height: 85px; + display: flex; + align-items: center; + + .sponsor-footer-logo { + max-height: 70px; + max-width: 130px; + display: block; + } + } + + .sponsor-offer-wrapper { + background-color: var(--grey-lighter); + padding: 20px; + overflow: hidden; + transition: all 1s linear; + + .flex-row-container { + display: flex; + margin: 6px 0 8px 0; + justify-content: space-between; + + .offer-headline { + display: block; + font-weight: 600; + font-size: 14px; + margin-right: 4px; + } + + .offer-url { + display: block; + align-self: flex-start; + padding: 10px 11px; + background-color: var(--black); + color: var(--white); + border-radius: 10px; + white-space: nowrap; + text-decoration: none; + } + } + + &.hidden { + height: 0px + } + } + .sponsor-offer-reveal-button { + height: 25px; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + + img { + display: block; + pointer-events: none; + } + + &:hover { + img { + transform: scale(1.2) + } + } + } + } +} + +.banner-ad-wrapper { + position: relative; + height: 170px; + padding-bottom: 30px; + max-height: 170px; + + .banner-ad-item { + position: absolute; + top: 0; + right: 0; + width: 100%; + transition: all 200ms ease-in-out; + + img { + margin: auto; + max-height: 130px; + } + } +} + +@media screen and (max-width: 1200px) and (orientation: portrait), + (max-width: 1123px) and (orientation: landscape) { + .sponsors-wrapper { + column-count: 2 !important; + } + } + + +@media screen and (max-width: 900px) and (orientation: portrait), + (max-width: 823px) and (orientation: landscape) { + .sponsors-wrapper { + column-count: 1 !important; + } + .sponsors-footer-wrapper { + column-count: 2 !important; + } + } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c97f32b2c..2995e3b64 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,5 @@ class ApplicationController < ActionController::Base - include Pundit + include Pundit::Authorization include ActivateNavigation rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized @@ -9,9 +9,11 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :current_event + helper_method :current_website helper_method :display_staff_event_subnav? helper_method :display_staff_selection_subnav? helper_method :display_staff_program_subnav? + helper_method :display_website_subnav? helper_method :program_mode? helper_method :schedule_mode? helper_method :program_tracks @@ -51,6 +53,31 @@ def current_event @current_event ||= set_current_event(session[:current_event_id]) if session[:current_event_id] end + def current_website + @current_website ||= begin + if current_event + current_event.website + elsif params[:slug] + Website.joins(:event).find_by(events: { slug: params[:slug] }) + else + older_domain_website || latest_domain_website + end + end&.decorate + end + + def older_domain_website + @older_domain_website ||= + domain_websites.find_by(events: { slug: params[:domain_page_or_slug] }) + end + + def latest_domain_website + @latest_domain_website ||= domain_websites.first + end + + def domain_websites + Website.domain_match(request.domain).joins(:event).order(created_at: :desc) + end + def set_current_event(event_id) @current_event = Event.find_by(id: event_id).try(:decorate) session[:current_event_id] = @current_event.try(:id) @@ -89,10 +116,18 @@ def require_event end end + def require_website + redirect_to not_found_path and return unless current_website + end + def require_proposal @proposal = @event.proposals.find_by!(uuid: params[:proposal_uuid] || params[:uuid]) end + def require_website + redirect_to not_found_path and return unless current_website + end + def user_not_authorized flash[:alert] = "You are not authorized to perform this action." redirect_to(request.referrer || root_path) @@ -142,6 +177,14 @@ def enable_staff_schedule_subnav @display_schedule_subnav = true end + def display_website_subnav? + @display_website_subnav + end + + def enable_website_subnav + @display_website_subnav = true + end + def program_mode? @display_program_subnav || @display_selection_subnav end @@ -153,4 +196,22 @@ def schedule_mode? def program_tracks @program_tracks ||= current_event && current_event.tracks.any? ? current_event.tracks : [] end + + def set_cache_headers + return unless Rails.configuration.action_controller.perform_caching + + server_cache_age = + current_website.caching_off? ? 0 : ENV.fetch('CACHE_CONTROL_S_MAXAGE', 1.week) + + expires_in( + ENV.fetch('CACHE_CONTROL_MAX_AGE', 0).to_i, + public: !current_website.caching_off?, + 's-maxage': server_cache_age.to_i + ) + response.headers['Surrogate-Key'] = current_website.event.slug if FastlyService.service + fresh_when( + current_website, + last_modified: current_website.purged_at || current_website.updated_at + ) unless current_website.caching_off? + end end diff --git a/app/controllers/concerns/activate_navigation.rb b/app/controllers/concerns/activate_navigation.rb index 3644a9daa..d761bbdec 100644 --- a/app/controllers/concerns/activate_navigation.rb +++ b/app/controllers/concerns/activate_navigation.rb @@ -56,6 +56,7 @@ def nav_item_map starts_with_path(:proposals), starts_with_path(:event_event_proposals, current_event) ], + 'event-website-link' => website_subnav_item_map, 'event-review-proposals-link' => starts_with_path(:event_staff_proposals, current_event), 'event-selection-link' => selection_subnav_item_map, 'event-program-link' => program_subnav_item_map, @@ -78,6 +79,13 @@ def event_subnav_item_map } end + def website_subnav_item_map + @website_subnav_item_map ||= { + 'event-website-configuration-link' => starts_with_path(:event_staff_website, current_event), + 'event-pages-link' => exact_path(:event_staff_pages, current_event) + } + end + def selection_subnav_item_map @selection_subnav_item_map ||= { 'event-program-proposals-selection-link' => [ diff --git a/app/controllers/image_uploads_controller.rb b/app/controllers/image_uploads_controller.rb new file mode 100644 index 000000000..692c7cf1e --- /dev/null +++ b/app/controllers/image_uploads_controller.rb @@ -0,0 +1,14 @@ +class ImageUploadsController < ApplicationController + skip_forgery_protection + before_action :require_user + + def create + blob = ActiveStorage::Blob.create_after_upload!( + io: params[:file], + filename: params[:file].original_filename, + content_type: params[:file].content_type + ) + + render json: {location: url_for(blob)}, content_type: "text/html" + end +end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index d848e83ec..3caf11039 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -1,6 +1,36 @@ class PagesController < ApplicationController + before_action :require_website, only: :show + before_action :require_page, only: :show + + after_action :set_cache_headers, only: :show def current_styleguide end + def show + @body = @page.published_body + render layout: "themes/#{current_website.theme}" + end + + private + + def require_page + @page = current_website.pages.published.find_by(page_conditions) + unless @page + @body = "Page Not Found" + render layout: "themes/#{current_website.theme}" and return + end + end + + def page_conditions + landing_page_request? ? { landing: true } : { slug: page_param } + end + + def page_param + params[:domain_page_or_slug] || params[:page] + end + + def landing_page_request? + page_param.nil? || @older_domain_website + end end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 2c0da26b6..137ee562e 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -7,7 +7,7 @@ def edit end def update - if current_user.update_attributes(user_params) + if current_user.update(user_params) if current_user.unconfirmed_email.present? flash[:danger] = I18n.t("devise.registrations.update_needs_confirmation") diff --git a/app/controllers/programs_controller.rb b/app/controllers/programs_controller.rb new file mode 100644 index 000000000..25988061e --- /dev/null +++ b/app/controllers/programs_controller.rb @@ -0,0 +1,10 @@ +class ProgramsController < ApplicationController + before_action :require_website + + after_action :set_cache_headers, only: :show + + def show + @program_sessions = current_website.program_sessions.live + render layout: "themes/#{current_website.theme}" + end +end diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index 16be2bc77..03918abe8 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -143,7 +143,7 @@ def parse_edit_field def proposal_params params.require(:proposal).permit(:title, {tags: []}, :session_format_id, :track_id, :abstract, :details, :pitch, custom_fields: @event.custom_fields, comments_attributes: [:body, :proposal_id, :user_id], - speakers_attributes: [:bio, :id]) + speakers_attributes: [:bio, :id, :age_range, :pronouns, :ethnicity, :first_time_speaker]) end def notes_params diff --git a/app/controllers/schedule_controller.rb b/app/controllers/schedule_controller.rb new file mode 100644 index 000000000..aa15c5534 --- /dev/null +++ b/app/controllers/schedule_controller.rb @@ -0,0 +1,12 @@ +class ScheduleController < ApplicationController + include WebsiteScheduleHelper + after_action :set_cache_headers, only: :show + + decorates_assigned :schedule, with: Staff::TimeSlotDecorator + + def show + @schedule = current_website.time_slots.grid_order + .includes(:room, program_session: { proposal: {speakers: :user}}) + render layout: "themes/#{current_website.theme}" + end +end diff --git a/app/controllers/sponsors_controller.rb b/app/controllers/sponsors_controller.rb new file mode 100644 index 000000000..503a17542 --- /dev/null +++ b/app/controllers/sponsors_controller.rb @@ -0,0 +1,10 @@ +class SponsorsController < ApplicationController + before_action :require_website + + after_action :set_cache_headers, only: :show + + def show + @sponsors_by_tier = current_website.sponsors.published.order_by_tier.group_by(&:tier) + render layout: "themes/#{current_website.theme}" + end +end diff --git a/app/controllers/staff/events_controller.rb b/app/controllers/staff/events_controller.rb index ce845ebbb..b5f5230cd 100644 --- a/app/controllers/staff/events_controller.rb +++ b/app/controllers/staff/events_controller.rb @@ -71,7 +71,7 @@ def configuration def update_custom_fields authorize_update - @event.update_attributes(event_params) + @event.update(event_params) respond_to do |format| format.js do render locals: { event: @event } @@ -101,7 +101,7 @@ def update_proposal_tags def update authorize_update - if @event.update_attributes(event_params) + if @event.update(event_params) if session[:target] redirect_to session[:target] session[:target].clear @@ -116,7 +116,7 @@ def update def open_cfp authorize_update - if @event.update_attributes(state: Event::STATUSES[:open]) + if @event.update(state: Event::STATUSES[:open]) flash[:info] = "Your CFP was successfully opened." else flash['danger alert-confirm'] = "There was a problem opening your CFP: #{@event.errors.full_messages.to_sentence}" diff --git a/app/controllers/staff/grids/time_slots_controller.rb b/app/controllers/staff/grids/time_slots_controller.rb index b8bad42c4..ec6e23584 100644 --- a/app/controllers/staff/grids/time_slots_controller.rb +++ b/app/controllers/staff/grids/time_slots_controller.rb @@ -10,7 +10,7 @@ def edit def update respond_to do |format| - if @time_slot.update_attributes(time_slot_params) + if @time_slot.update(time_slot_params) format.json { render json: update_response.merge(status: :ok) } format.html { flash.now[:info] = "Time slot updated." } else diff --git a/app/controllers/staff/pages_controller.rb b/app/controllers/staff/pages_controller.rb new file mode 100644 index 000000000..8a463009d --- /dev/null +++ b/app/controllers/staff/pages_controller.rb @@ -0,0 +1,110 @@ +class Staff::PagesController < Staff::ApplicationController + before_action :require_website + before_action :enable_website_subnav + before_action :set_page, except: :index + before_action :authorize_page, except: :index + + def index + @pages = current_website.pages + authorize(@pages) + end + + def show + @body = params[:preview] || @page.unpublished_body || "" + render template: 'pages/show', layout: "themes/#{current_website.theme}" + end + + def new; end + + def create + if @page.update(page_params) + flash[:success] = "#{@page.name} Page was successfully created." + redirect_to event_staff_pages_path(current_event) + else + render :new + end + end + + def edit; end + + def update + if @page.update(page_params) + flash[:success] = "#{@page.name} Page was successfully updated." + redirect_to event_staff_pages_path(current_event) + else + render :edit + end + end + + def preview; end + + def publish + @page.update(published_body: @page.unpublished_body, + body_published_at: Time.current) + flash[:success] = "#{@page.name} Page was successfully published." + redirect_to event_staff_pages_path(current_event) + end + + def promote + Page.promote(@page) + flash[:success] = "#{@page.name} Page was successfully promoted." + redirect_to event_staff_pages_path(current_event) + end + + def destroy + @page.destroy + flash[:success] = "#{@page.name} Page was successfully destroyed." + redirect_to event_staff_pages_path(current_event) + end + + private + + def set_page + @page = if params[:id] && (params[:id] != Page::BLANK_SLUG) + current_website.pages.find_by(slug: params[:id]) + else + build_page + end + end + + def build_page + if template = params[:page] && page_params[:template].presence + Page.from_template( + template, + unpublished_body: render_to_string( + "staff/pages/themes/#{current_website.theme}/#{template}", + layout: false + ), + website: current_website + ) + else + current_website.pages.build + end + end + + def authorize_page + authorize(@page) + end + + def page_params + params + .require(:page) + .permit( + :template, + :name, + :slug, + :hide_page, + :hide_header, + :hide_footer, + :footer_category, + :unpublished_body + ) + end + + def require_website + return if current_website + + redirect_to new_event_staff_website_path(current_event), + alert: "Please configure your website before attempting to create pages" + end +end diff --git a/app/controllers/staff/proposal_reviews_controller.rb b/app/controllers/staff/proposal_reviews_controller.rb index b9d6b22c9..c8696643b 100644 --- a/app/controllers/staff/proposal_reviews_controller.rb +++ b/app/controllers/staff/proposal_reviews_controller.rb @@ -51,7 +51,7 @@ def update tags = params[:proposal][:review_tags].downcase params[:proposal][:review_tags] = Tagging.tags_string_to_array(tags) - unless @proposal.update_attributes(proposal_review_tags_params) + unless @proposal.update(proposal_review_tags_params) flash[:danger] = 'There was a problem saving the proposal.' else @proposal.reload diff --git a/app/controllers/staff/ratings_controller.rb b/app/controllers/staff/ratings_controller.rb index 145a78642..9dad57e22 100644 --- a/app/controllers/staff/ratings_controller.rb +++ b/app/controllers/staff/ratings_controller.rb @@ -10,9 +10,11 @@ class Staff::RatingsController < Staff::ApplicationController def create authorize @proposal, :rate? @rating = Rating.find_or_create_by(proposal: @proposal, user: current_user) - @rating.update_attributes(rating_params) + @rating.update(rating_params) if @rating.save - respond_with @rating, locals: {rating: @rating} + respond_to do |format| + format.js + end else logger.warn("Error creating rating for proposal [#{@proposal.id}] for user [#{current_user.id}]: #{@rating.errors.full_messages}") render json: @rating.to_json, status: :bad_request @@ -26,11 +28,15 @@ def update if rating_params[:score].blank? @rating.destroy @rating = current_user.ratings.build(proposal: @proposal) - respond_with :reviewer, locals: {rating: @rating} + respond_to do |format| + format.js + end return end - if @rating.update_attributes(rating_params) - respond_with :reviewer, locals: {rating: @rating} + if @rating.update(rating_params) + respond_to do |format| + format.js + end else logger.warn("Error updating rating for proposal [#{@proposal.id}] for user [#{current_user.id}]: #{@rating.errors.full_messages}") render json: @rating.to_json, status: :bad_request diff --git a/app/controllers/staff/rooms_controller.rb b/app/controllers/staff/rooms_controller.rb index b6f30dd75..50b3cb019 100644 --- a/app/controllers/staff/rooms_controller.rb +++ b/app/controllers/staff/rooms_controller.rb @@ -20,7 +20,7 @@ def create end def update - unless @room.update_attributes(room_params) + unless @room.update(room_params) flash.now[:danger] = "There was a problem updating your room, #{@room.errors.full_messages.join(", ")}." end respond_to do |format| diff --git a/app/controllers/staff/session_formats_controller.rb b/app/controllers/staff/session_formats_controller.rb index 4ddc68039..0e529bf8f 100644 --- a/app/controllers/staff/session_formats_controller.rb +++ b/app/controllers/staff/session_formats_controller.rb @@ -30,7 +30,7 @@ def create end def update - unless @session_format.update_attributes(session_format_params) + unless @session_format.update(session_format_params) flash.now[:danger] = "There was a problem updating your session format, #{@session_format.errors.full_messages.join(", ")}." end respond_to do |format| diff --git a/app/controllers/staff/sponsors_controller.rb b/app/controllers/staff/sponsors_controller.rb new file mode 100644 index 000000000..6d4e8de58 --- /dev/null +++ b/app/controllers/staff/sponsors_controller.rb @@ -0,0 +1,56 @@ +class Staff::SponsorsController < Staff::ApplicationController + before_action :enable_website_subnav + + def index + @sponsors = current_event.sponsors.order_by_tier + end + + def new + @sponsor = current_event.sponsors.build + end + + def create + @sponsor = current_event.sponsors.build(sponsor_params) + if @sponsor.save + redirect_to event_staff_sponsors_path + flash[:success] = "Sponsor was successfully created." + else + render :new + end + end + + def edit + @sponsor = current_event.sponsors.find_by(id: params[:id]) + end + + def update + @sponsor = current_event.sponsors.find_by(id: params[:id]) + @sponsor.update(sponsor_params) + flash[:success] = "#{@sponsor.name} was successfully updated." + redirect_to event_staff_sponsors_path + end + + def destroy + @sponsor = current_event.sponsors.find(params[:id]) + @sponsor.destroy + redirect_to event_staff_sponsors_path + flash[:info] = "Sponsor was successfully removed." + end + + private + def sponsor_params + params.require(:sponsor).permit(:name, + :tier, + :published, + :url, + :other_title, + :primary_logo, + :footer_logo, + :banner_ad, + :description, + :offer_headline, + :offer_text, + :offer_url + ) + end +end diff --git a/app/controllers/staff/time_slots_controller.rb b/app/controllers/staff/time_slots_controller.rb index 0b9d1bdf5..4cfdaec27 100644 --- a/app/controllers/staff/time_slots_controller.rb +++ b/app/controllers/staff/time_slots_controller.rb @@ -58,7 +58,7 @@ def edit end def update - if @time_slot.update_attributes(time_slot_params) + if @time_slot.update(time_slot_params) flash.now[:info] = "Time slot updated." else flash.now[:danger] = "There was a problem saving this time slot." @@ -82,7 +82,7 @@ def destroy private def time_slot_params - params.require(:time_slot).permit(:conference_day, :room_id, :start_time, :end_time, :program_session_id, :title, :track_id, :presenter, :description) + params.require(:time_slot).permit(:conference_day, :room_id, :start_time, :end_time, :program_session_id, :sponsor_id, :title, :track_id, :presenter, :description) end def set_time_slot diff --git a/app/controllers/staff/tracks_controller.rb b/app/controllers/staff/tracks_controller.rb index 755400391..c2f65061d 100644 --- a/app/controllers/staff/tracks_controller.rb +++ b/app/controllers/staff/tracks_controller.rb @@ -31,7 +31,7 @@ def create end def update - if @track.update_attributes(track_params) + if @track.update(track_params) flash.now[:success] = "#{@track.name} has been updated." # changes to guildlines are invisible else flash.now[:danger] = "There was a problem updating your track, #{@track.errors.full_messages.join(", ")}." diff --git a/app/controllers/staff/websites_controller.rb b/app/controllers/staff/websites_controller.rb new file mode 100644 index 000000000..e32fa5a09 --- /dev/null +++ b/app/controllers/staff/websites_controller.rb @@ -0,0 +1,82 @@ +class Staff::WebsitesController < Staff::ApplicationController + before_action :set_website + before_action :authorize_website + before_action :enable_website_subnav + + def new; end + + def create + if @website.update(website_params) + flash[:success] = "Website was successfully created." + redirect_to edit_event_staff_website_path(current_event) + else + flash[:warning] = "There were errors creating your website configuration" + render :new + end + end + + def edit; end + + def update + if @website.update(website_params) + flash[:success] = "Website was successfully updated." + redirect_to edit_event_staff_website_path(current_event) + else + flash[:warning] = "There were errors updating your website configuration" + render :edit + end + end + + def purge + @website.manual_purge + + flash[:success] = "Website was successfully purged." + redirect_to edit_event_staff_website_path(current_event) + end + + private + + def set_website + @website = (current_event.website || current_event.build_website).decorate + end + + def authorize_website + authorize(@website) + end + + def website_params + params + .require(:website) + .permit( + :logo, + :background, + :favicon, + :city, + :location, + :directions, + :prospectus_link, + :domains, + :footer_about_content, + :footer_copyright, + :twitter_handle, + :facebook_url, + :instagram_url, + :head_content, + :caching, + footer_categories: [], + navigation_links: [], + session_format_configs_attributes: [ + :id, :name, :display, :position, :session_format_id + ], + fonts_attributes: [ + :id, :name, :file, :primary, :secondary, :_destroy + ], + contents_attributes: [ + :id, :name, :html, :placement, :_destroy + ], + meta_data_attributes: [ + :id, :title, :author, :description, :image + ] + ) + end +end diff --git a/app/decorators/event_decorator.rb b/app/decorators/event_decorator.rb index 11a50cf61..031b1df2c 100644 --- a/app/decorators/event_decorator.rb +++ b/app/decorators/event_decorator.rb @@ -106,7 +106,7 @@ def waitlisted_percent end def line_chart - h.line_chart object.proposals.group_by_day(:created_at, Time.zone, proposal_date_range).count, + h.line_chart object.proposals.group_by_day(:created_at, range: proposal_date_range).count, library: {pointSize: 0, lineWidth: 2, series: [{color: '#9ACFEA'}]} end diff --git a/app/decorators/staff/time_slot_decorator.rb b/app/decorators/staff/time_slot_decorator.rb index 7456964eb..dc60eda27 100644 --- a/app/decorators/staff/time_slot_decorator.rb +++ b/app/decorators/staff/time_slot_decorator.rb @@ -20,14 +20,14 @@ def time_slot_id def row_data_time_sortable(buttons: false) row = [object.conference_day, object.start_time, object.end_time, linked_title, - display_presenter, object.room_name, display_track_name] + display_presenter, object.room_name, display_sponsor_star, display_track_name] row << action_links if buttons row end def row_data(buttons: false) row = [object.conference_day, start_time, end_time, linked_title, - display_presenter, object.room_name, display_track_name] + display_presenter, object.room_name, display_sponsor_star, display_track_name] row << action_links if buttons row @@ -122,6 +122,10 @@ def display_presenter object.session_presenter || object.presenter end + def presenters_with_bios + object.program_session.speakers.pluck(:speaker_name, :bio) + end + def display_track_name object.session_track_name || object.track_name end @@ -130,6 +134,18 @@ def display_description object.session_description || object.description end + def room + object.room_name + end + + def sponsored? + object.sponsor.present? + end + + def display_sponsor_star + h.content_tag(:span, "", class: "glyphicon glyphicon-star") if sponsored? + end + def preview_css 'preview' unless object.persisted? end @@ -138,6 +154,14 @@ def filled_with_session? object.program_session.present? end + def session_format + object.program_session.session_format + end + + def track + object.program_session.track + end + def configured? object.persisted? && (display_title || display_presenter || display_track_name || display_description) end diff --git a/app/decorators/website_decorator.rb b/app/decorators/website_decorator.rb new file mode 100644 index 000000000..d1d71631a --- /dev/null +++ b/app/decorators/website_decorator.rb @@ -0,0 +1,219 @@ +class WebsiteDecorator < ApplicationDecorator + delegate_all + delegate :title, :author, :description, to: :meta_data + + DEFAULT_LINKS = { + 'Schedule' => 'schedule', + 'Program' => 'program', + 'Sponsors' => 'sponsors', + }.freeze + + def name + event.name + end + + def date_range + event.date_range + end + + def event + @event ||= object.event.decorate + end + + def formatted_location + h.simple_format(object.location) + end + + def contact_email + event.contact_email + end + + def closes_at + event.closes_at(:month_day_year) + end + + def banner_sponsors + event.sponsors.published.with_banner_ad + end + + def sponsors_in_footer + event.sponsors.published.with_footer_image.order_by_tier + end + + def categorized_footer_pages + pages.in_footer + .select(:footer_category, :name, :slug) + .group_by(&:footer_category) + .sort_by { |category, _| footer_categories.index(category) || 1_000 } + end + + def twitter_url + "https://twitter.com/#{object.twitter_handle}" + end + + def register_page + pages.published.find_by(slug: 'register') + end + + def background_style + return {} unless background.attached? + + { style: "background-image: url('#{h.url_for(background)}');" } + end + + def background_style_html + background_style.map {|key, value| "#{key} = \"#{value}\""}.join(' ').html_safe + end + + def session_format_configs + event.session_formats.map.with_index do |session_format, index| + SessionFormatConfig.find_or_initialize_by(session_format: session_format) do |config| + config.name = session_format.name + config.position = index + 1 + end + end.sort_by(&:position) + end + + def displayed_session_format_configs + object.session_format_configs.displayed + end + + def default_session_slug + object.session_format_configs.displayed.in_order.first.slug + end + + def link_options + @link_options ||= pages.published.pluck(:name, :slug) + .each_with_object(DEFAULT_LINKS.dup) do |(name, slug), memo| + memo[name] = slug + end.sort_by { |_key, value| navigation_links.index(value) || 0 }.to_h + end + + def tracks + event.tracks + end + + def track_num(track) + tracks.index(track) + 1 + end + + def track_background(track) + "bg-track-#{track_num(track)}" + end + + def track_class_data(program_session) + program_session.track ? h.dom_id(program_session.track) : "" + end + + def time_slot_track_class_data(time_slot) + time_slot.program_session&.track ? h.dom_id(time_slot.program_session.track) : "" + end + + def time_slot_speaker_class_data(time_slot) + return '' unless time_slot.program_session&.speakers + + time_slot.program_session&.speakers.map { |speaker| h.dom_id(speaker) }.join(" ") + end + + def tracks_in_use + event.tracks.distinct.joins(:program_sessions) + end + + def speakers_in_order + event.speakers.in_program.a_to_z + end + + def speaker_class_data(program_session) + program_session.speakers.map { |speaker| h.dom_id(speaker) }.join(" ") + end + + def program_filter_classes(program_session) + [track_class_data(program_session), speaker_class_data(program_session)].join(' ') + end + + def session_format_class_data(time_slot) + time_slot.program_session ? h.dom_id(time_slot.program_session.session_format) : "" + end + + def schedule_filter_classes(time_slot) + [session_format_class_data(time_slot), + time_slot_track_class_data(time_slot), + time_slot_speaker_class_data(time_slot) + ].join(' ') + end + + def session_format_num(session_format) + session_formats.index(session_format) + 1 + end + + def session_format_tag_class(session_format) + "session-format-tag-#{session_format_num(session_format)}" + end + + def session_format_background_class(session_format) + "session-format-bg-#{session_format_num(session_format)}" + end + + def session_format_name(session_format) + object.session_format_configs.find_by(session_format: session_format).name + end + + def font_faces_css + fonts.map do |font| + <<~CSS + @font-face { + font-family: "#{font.name}"; + src: url('#{h.rails_storage_proxy_path(font.file)}'); + } + CSS + end.join("\n").html_safe + end + + def font_root_css + font_primary = fonts.primary.first + font_secondary = fonts.secondary.first + + return "" unless font_primary || font_secondary + <<~CSS.html_safe + :root { + #{"--sans-serif-font: '#{font_primary.name}' !important;" if font_primary} + #{"--secondary-body-font: '#{font_secondary.name}' !important;" if font_secondary} + } + CSS + end + + def head_content + object.contents.for(Website::Content::HEAD).pluck(:html).join.html_safe + end + + def footer_content + object.contents.for(Website::Content::FOOTER).pluck(:html).join.html_safe + end + + def meta_data + @meta_data ||= object.meta_data || object.build_meta_data + end + + def meta_image_url + attachment = meta_data.image.attached? ? meta_data.image : logo + h.polymorphic_url(attachment) if attachment.attached? + end + + def website_title(page_title) + [page_title, title].reject(&:blank?).join(" | ") + end + + def favicon_url + h.polymorphic_url(favicon) if favicon.attached? + end + + def event_day + today = Time.current + event_day = ((today - event.start_date) / 1.day.seconds).ceil + event_day >= 1 && event_day <= event.days ? event_day : 1 + end + + def schedule_id_for_event_day + "schedule-day-#{event_day}" + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5c025a063..e1bc4eb1b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -8,13 +8,8 @@ def title end end - def demographic_label(demographic) - case demographic - when :gender then - "Gender Identity" - else - demographic.to_s.titleize - end + def boolean_to_words(value) + value ? "Yes" : "No" end class MarkdownRenderer < Redcarpet::Render::HTML @@ -118,7 +113,19 @@ def staff_nav? current_user.staff_for?(current_event) end + def website_nav? + policy(Website).show? + end + def admin_nav? current_user.admin? end + + def new_or_edit_website_path + if current_website + edit_event_staff_website_path(current_event) + else + new_event_staff_website_path(current_event) + end + end end diff --git a/app/helpers/image_helper.rb b/app/helpers/image_helper.rb new file mode 100644 index 000000000..8a99ddb71 --- /dev/null +++ b/app/helpers/image_helper.rb @@ -0,0 +1,24 @@ +module ImageHelper + def resize_image_tag(image, width:, height: width) + width ||= 100 + return unless image.attached? + if image.content_type.match('svg') + image_tag(image, width: width) + else + image_tag(image.variant(resize_to_fit: [width, height])) + end + end + + def image_input(form, field, width: 100) + image = form.object.send(field) + title = field.to_s.titleize + attached = image.attached? + input = "" + if attached + input << form.label(field, "Current #{title}", class: "control-label") + input << content_tag(:div, resize_image_tag(image, width: width)) + end + input << form.input(field, label: attached ? "Replace #{title}" : "#{title}") + input.html_safe + end +end diff --git a/app/helpers/page_helper.rb b/app/helpers/page_helper.rb new file mode 100644 index 000000000..bdbf126c3 --- /dev/null +++ b/app/helpers/page_helper.rb @@ -0,0 +1,39 @@ +module PageHelper + TAGS = { + "" => "sponsors/banner_ads", #DEPRECATED spelling mistake + "" => "sponsors/banner_ads", + "" => "sponsors/sponsors_footer", + /(<\/logo-image>)/ => :logo_image, + "background-image-style-url" => :background_style, + } + + def embed(body) + body.tap do |body| + TAGS.each do |tag, template| + body.gsub!(tag) do + args = tag.is_a?(Regexp) ? extract($1) : {} + case template + when String + render(**args.merge(template: template, layout: false)) + when Symbol + send(template, **args) + end + end + end + end.html_safe + end + + def extract(tag) + fragment = Nokogiri::HTML.fragment(tag) + tag_name = fragment.children.first.name + fragment.at(tag_name).to_h.symbolize_keys + end + + def background_style + current_website.background_style_html + end + + def logo_image(args) + resize_image_tag(current_website.logo, **args) + end +end diff --git a/app/helpers/proposal_helper.rb b/app/helpers/proposal_helper.rb index 05ab74d3a..24ea072c2 100644 --- a/app/helpers/proposal_helper.rb +++ b/app/helpers/proposal_helper.rb @@ -22,7 +22,7 @@ def session_format_tooltip concat(content_tag(:p) do content_tag(:strong, "Session Format Guide") end) - event.session_formats.each do |format| + event.session_formats.publicly_viewable.each do |format| concat(content_tag(:p, "#{format.name} - #{format.description}")) end end diff --git a/app/helpers/website_helper.rb b/app/helpers/website_helper.rb new file mode 100644 index 000000000..6b48bf5ad --- /dev/null +++ b/app/helpers/website_helper.rb @@ -0,0 +1,25 @@ +module WebsiteHelper + DOCS_PAGE = "https://github.com/rubycentral/cfp-app/blob/main/docs/website_documentation.md".freeze + def website_event_slug + params[:slug] || (@older_domain_website && @older_domain_website.event.slug) + end + + def font_file_label(font) + "File".tap do |label| + label.concat(" (Current File: #{font.file.filename.to_s})") if font.file.attached? + end + end + + def legend_with_docs(title) + content_tag("legend", class: "fieldset-legend") do + concat(title) + concat(link_to_docs(title.parameterize)) + end + end + + def link_to_docs(anchor) + link_to(DOCS_PAGE + "##{anchor}", target: "_blank") do + content_tag("i", nil, class: "fa fa-fw fa-question-circle") + end + end +end diff --git a/app/helpers/website_schedule_helper.rb b/app/helpers/website_schedule_helper.rb new file mode 100644 index 000000000..acef25bba --- /dev/null +++ b/app/helpers/website_schedule_helper.rb @@ -0,0 +1,10 @@ +module WebsiteScheduleHelper + + def schedule_for_day(schedule, day_number) + schedule.select { |time_slot| time_slot[:conference_day] == day_number } + .filter { |time_slot| !time_slot[:title].blank? || time_slot.program_session } + .sort_by {|time_slot| time_slot[:start_time] } + .group_by {|time_slot| time_slot[:start_time] } + .values + end +end diff --git a/app/javascript/components/Schedule.js b/app/javascript/components/Schedule.js index 0636aae8c..87f851b58 100644 --- a/app/javascript/components/Schedule.js +++ b/app/javascript/components/Schedule.js @@ -1,20 +1,20 @@ -import React, { Component } from "react" -import PropTypes from "prop-types" +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; -import Nav from "./Schedule/Nav" -import Ruler from "./Schedule/Ruler" -import DayView from "./Schedule/DayView" -import UnscheduledArea from "./Schedule/UnscheduledArea" -import GenerateGridButton from "./Schedule/GenerateGridButton" -import BulkCreateModal from "./Schedule/BulkCreateModal" -import BulkGenerateConfirm from "./Schedule/BulkGenerateConfirm" -import Alert from './Schedule/Alert' +import { Nav } from './Schedule/Nav'; +import { Ruler } from './Schedule/Ruler'; +import { DayView } from './Schedule/DayView'; +import { UnscheduledArea } from './Schedule/UnscheduledArea'; +import { GenerateGridButton } from './Schedule/GenerateGridButton'; +import { BulkCreateModal } from './Schedule/BulkCreateModal'; +import { BulkGenerateConfirm } from './Schedule/BulkGenerateConfirm'; +import { Alert } from './Schedule/Alert'; -import { postBulkTimeSlots } from "../apiCalls" +import { postBulkTimeSlots } from '../apiCalls'; class Schedule extends Component { constructor(props) { - super(props) + super(props); this.state = { dayViewing: 1, startTime: 10, @@ -29,221 +29,237 @@ class Schedule extends Component { bulkTimeSlotModalEditState: null, errorMessages: [], bulkErrorMessages: [], - } + }; } - changeDayView = day => { - this.setState({ dayViewing: day }) - } + changeDayView = (day) => { + this.setState({ dayViewing: day }); + }; - ripTime = time => { - const hours = parseInt(time.split("T")[1].split(":")[0]) - const minutes = parseInt(time.split(':')[1]) / 60 - return hours + minutes - } + ripTime = (time) => { + const hours = parseInt(time.split('T')[1].split(':')[0]); + const minutes = parseInt(time.split(':')[1]) / 60; + return hours + minutes; + }; - determineHours = slots => { + determineHours = (slots) => { let hours = { startTime: 12, - endTime: 12 - } + endTime: 12, + }; - slots.forEach(slot => { + slots.forEach((slot) => { if (this.ripTime(slot.start_time) < hours.startTime) { - hours.startTime = this.ripTime(slot.start_time) + hours.startTime = this.ripTime(slot.start_time); } if (this.ripTime(slot.end_time) > hours.endTime) { - hours.endTime = this.ripTime(slot.end_time) + hours.endTime = this.ripTime(slot.end_time); } - }) - return hours - } + }); + return hours; + }; - changeDragged = programSession => { - this.setState({ draggedSession: programSession }) - } + changeDragged = (programSession) => { + this.setState({ draggedSession: programSession }); + }; - handleMoveSessionResponse = ( sessions, unscheduledSessions, slots, session ) => { + handleMoveSessionResponse = (sessions, unscheduledSessions, slots, session) => { if (session) { - slots.forEach(slot => { + slots.forEach((slot) => { if (slot.id === session.slot.id) { - slot.program_session_id = null + slot.program_session_id = null; } - }) + }); } - this.setState({ sessions, unscheduledSessions, slots }) - } + this.setState({ sessions, unscheduledSessions, slots }); + }; openBulkTimeSlotModal = () => { this.setState({ bulkTimeSlotModalOpen: true, - previewSlots: [] - }) - } + previewSlots: [], + }); + }; closeBulkTimeSlotModal = (e) => { - e.preventDefault() + e.preventDefault(); this.setState({ - previewSlots: [], + previewSlots: [], bulkTimeSlotModalEditState: null, - bulkTimeSlotModalOpen: false - }) - } + bulkTimeSlotModalOpen: false, + }); + }; cancelBulkPreview = () => { - let hours = this.determineHours(this.state.slots) + let hours = this.determineHours(this.state.slots); this.setState({ previewSlots: [], bulkTimeSlotModalEditState: null, - ...hours - }) - } + ...hours, + }); + }; findTimeSlotConflicts = (previewSlots) => { - const { slots } = this.state - let conflicts = [] + const { slots } = this.state; + let conflicts = []; - previewSlots.forEach(ps => { - slots.forEach(s => { - let sameDaySameRoom = s.room_id === parseInt(ps.room) && s.conference_day === ps.day + previewSlots.forEach((ps) => { + slots.forEach((s) => { + let sameDaySameRoom = s.room_id === parseInt(ps.room) && s.conference_day === ps.day; if (sameDaySameRoom) { - let timeConflict = this.determineTimeConflict(ps, this.ripTime(s.start_time), this.ripTime(s.end_time)) + let timeConflict = this.determineTimeConflict(ps, this.ripTime(s.start_time), this.ripTime(s.end_time)); if (timeConflict) { - conflicts.push(s) + conflicts.push(s); } } - }) + }); - previewSlots.forEach(preview => { - if (Object.is(ps, preview)) { return } - let sameDaySameRoom = parseInt(preview.room) === parseInt(ps.room) && preview.day === ps.day + previewSlots.forEach((preview) => { + if (Object.is(ps, preview)) { + return; + } + let sameDaySameRoom = parseInt(preview.room) === parseInt(ps.room) && preview.day === ps.day; if (sameDaySameRoom) { - let timeConflict = this.determineTimeConflict(ps, preview.startTime, preview.endTime) - + let timeConflict = this.determineTimeConflict(ps, preview.startTime, preview.endTime); + if (timeConflict) { - conflicts.push(Object.assign(preview, { previewConflict: true })) + conflicts.push(Object.assign(preview, { previewConflict: true })); } } - }) - - }) + }); + }); - return conflicts - } + return conflicts; + }; determineTimeConflict = (previewSlot, compareStartTime, compareEndTime) => { - return previewSlot.startTime > compareStartTime && previewSlot.startTime < compareEndTime || previewSlot.endTime > compareStartTime && previewSlot.startTime < compareEndTime - } + return ( + (previewSlot.startTime > compareStartTime && previewSlot.startTime < compareEndTime) || + (previewSlot.endTime > compareStartTime && previewSlot.startTime < compareEndTime) + ); + }; handleConflicts = (conflicts, bulkTimeSlotModalEditState) => { - const { rooms } = this.state + const { rooms } = this.state; - let errorMessages = [] + let errorMessages = []; - conflicts.forEach(c => { - let message + conflicts.forEach((c) => { + let message; if (c.previewConflict) { - message = `You attempted to make two new time slots that overlap. The overlap occurs on Day ${c.day} at the ${rooms.find(r => r.id == parseInt(c.room)).name} location.` + message = `You attempted to make two new time slots that overlap. The overlap occurs on Day ${c.day} at the ${ + rooms.find((r) => r.id == parseInt(c.room)).name + } location.`; } else { - message = `You attempted to preview a slot which overlaps an existing slot. The overlap involves a previously existing slot on Day ${c.conference_day} at the ${rooms.find(r => r.id == c.room_id).name} location, between ${c.start_time.split('T')[1].split('.')[0]} and ${c.end_time.split('T')[1].split('.')[0]}` + message = `You attempted to preview a slot which overlaps an existing slot. The overlap involves a previously existing slot on Day ${ + c.conference_day + } at the ${rooms.find((r) => r.id == c.room_id).name} location, between ${ + c.start_time.split('T')[1].split('.')[0] + } and ${c.end_time.split('T')[1].split('.')[0]}`; } - - errorMessages.push(message) - }) - errorMessages = [...new Set(errorMessages)]; + errorMessages.push(message); + }); + + errorMessages = [...new Set(errorMessages)]; this.setState({ errorMessages, bulkTimeSlotModalEditState, bulkTimeSlotModalOpen: false, - }) - } + }); + }; createTimeSlotPreviews = (previewSlots, bulkTimeSlotModalEditState) => { - let { startTime, endTime } = this.state + let { startTime, endTime } = this.state; - previewSlots.forEach(preview => { + previewSlots.forEach((preview) => { if (preview.startTime < startTime) { - startTime = preview.startTime + startTime = preview.startTime; } if (preview.endTime > endTime) { - endTime = preview.endTime + endTime = preview.endTime; } - }) + }); - let conflicts = this.findTimeSlotConflicts(previewSlots) + let conflicts = this.findTimeSlotConflicts(previewSlots); if (conflicts.length > 0) { - this.handleConflicts(conflicts, bulkTimeSlotModalEditState) + this.handleConflicts(conflicts, bulkTimeSlotModalEditState); } else { this.setState({ - previewSlots, - bulkTimeSlotModalEditState, + previewSlots, + bulkTimeSlotModalEditState, bulkTimeSlotModalOpen: false, dayViewing: parseInt(bulkTimeSlotModalEditState.day), startTime, - endTime - }) + endTime, + }); } - - } + }; requestBulkTimeSlotCreate = () => { - const {bulkTimeSlotModalEditState, bulkPath} = this.state - const {day, duration, rooms, startTimes} = bulkTimeSlotModalEditState - - // the API expects time strings to have a minutes declaration, this following code adds a minute decaration to each time in a string, if needed. - const formattedTimes = startTimes.replace(/\s/g, '').split(',').map(time => { - if (time.split(':').length > 1) { - return time - } else { - return time + ':00' - } - }).join(', ') + const { bulkTimeSlotModalEditState, bulkPath } = this.state; + const { day, duration, rooms, startTimes } = bulkTimeSlotModalEditState; + + // the API expects time strings to have a minutes declaration, this following code adds a minute decaration to each time in a string, if needed. + const formattedTimes = startTimes + .replace(/\s/g, '') + .split(',') + .map((time) => { + if (time.split(':').length > 1) { + return time; + } else { + return time + ':00'; + } + }) + .join(', '); postBulkTimeSlots(bulkPath, day, rooms, duration, formattedTimes) - .then(response => response.json()) - .then(data => { - const { errors } = data + .then((response) => response.json()) + .then((data) => { + const { errors } = data; if (errors) { - this.setState({ bulkErrorMessages: errors }) - return + this.setState({ bulkErrorMessages: errors }); + return; } - this.setState({ - slots: data.slots, - previewSlots: [], - bulkTimeSlotModalEditState: null - }, () => { - let hours = this.determineHours(this.state.slots) - this.setState({...hours}) - }) + this.setState( + { + slots: data.slots, + previewSlots: [], + bulkTimeSlotModalEditState: null, + }, + () => { + let hours = this.determineHours(this.state.slots); + this.setState({ ...hours }); + } + ); }) - .catch(err => console.log('Error: ', err)) - } + .catch((err) => console.log('Error: ', err)); + }; componentDidMount() { - let hours = this.determineHours(this.props.slots) - const trackColors = palette("tol-rainbow", this.props.tracks.length) + let hours = this.determineHours(this.props.slots); + const trackColors = palette('tol-rainbow', this.props.tracks.length); this.props.tracks.forEach((track, i) => { - track.color = "#" + trackColors[i] - }) - - this.setState(Object.assign(this.state, this.props, hours)) - } + track.color = '#' + trackColors[i]; + }); - showErrors = messages => { - this.setState({ errorMessages: messages }) + this.setState(Object.assign(this.state, this.props, hours)); } + showErrors = (messages) => { + this.setState({ errorMessages: messages }); + }; + removeErrors = () => { - this.setState({ errorMessages: [], bulkErrorMessages: [] }) - } + this.setState({ errorMessages: [], bulkErrorMessages: [] }); + }; render() { const { @@ -263,61 +279,52 @@ class Schedule extends Component { sessionFormats, errorMessages, bulkErrorMessages, - } = this.state + } = this.state; - const headers = rooms.map(room => ( + const headers = rooms.map((room) => (
{room.name}
- )) - - const headersMinWidth = (180 * rooms.length) + 'px' - - const bulkTimeSlotModal = bulkTimeSlotModalOpen && + )); + + const headersMinWidth = 180 * rooms.length + 'px'; + + const bulkTimeSlotModal = bulkTimeSlotModalOpen && ( + + ); return (
- {errorMessages.length > 0 && ( - - )} - {bulkErrorMessages.length > 0 && ( - - )} + {errorMessages.length > 0 && } + {bulkErrorMessages.length > 0 && } {bulkTimeSlotModal} -
-
- ) + ); } } @@ -355,7 +362,7 @@ Schedule.propTypes = { sessions: PropTypes.array, counts: PropTypes.object, unscheduledSessions: PropTypes.array, - tracks: PropTypes.array -} + tracks: PropTypes.array, +}; -export default Schedule +export default Schedule; diff --git a/app/javascript/components/Schedule/Alert.js b/app/javascript/components/Schedule/Alert.js index ae21665e6..e5d855a4d 100644 --- a/app/javascript/components/Schedule/Alert.js +++ b/app/javascript/components/Schedule/Alert.js @@ -1,5 +1,5 @@ -import React, { Component, Fragment } from "react" -import PropTypes from "prop-types" +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; class Alert extends Component { render() { @@ -7,18 +7,21 @@ class Alert extends Component { return (
- - { messages.map(message => { - return

{message}

- }) } + + {messages.map((message) => { + return

{message}

; + })}
- ) + ); } } Alert.propTypes = { onClose: PropTypes.func, messages: PropTypes.array, -} +}; -export default Alert +export { Alert }; diff --git a/app/javascript/components/Schedule/BulkCreateModal.js b/app/javascript/components/Schedule/BulkCreateModal.js index eba155d6d..a617f83dd 100644 --- a/app/javascript/components/Schedule/BulkCreateModal.js +++ b/app/javascript/components/Schedule/BulkCreateModal.js @@ -1,170 +1,168 @@ -import React, { Component } from "react" -import PropTypes from "prop-types" +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; class BulkCreateModal extends Component { constructor(props) { - super(props) + super(props); this.state = { day: 1, rooms: [], startTimes: '', duration: '', - timeError: false - } + timeError: false, + }; } changeDay = (e) => { - this.setState({day: e.target.value}) - } + this.setState({ day: e.target.value }); + }; changeRooms = (e) => { - let rooms = this.state.rooms.slice() - const room = e.target.name - + let rooms = this.state.rooms.slice(); + const room = e.target.name; + if (rooms.includes(room)) { - rooms = rooms.filter(selectedRoom => selectedRoom !== room) + rooms = rooms.filter((selectedRoom) => selectedRoom !== room); } else { - rooms.push(room) + rooms.push(room); } - this.setState({rooms}) - } + this.setState({ rooms }); + }; changeInput = (e) => { - const name = e.target.name - this.setState({[name]: e.target.value}, () => { - name === 'startTimes' && this.validateTimes() - }) - } + const name = e.target.name; + this.setState({ [name]: e.target.value }, () => { + name === 'startTimes' && this.validateTimes(); + }); + }; validateTimes = () => { - const { startTimes } = this.state - let valid = true - startTimes.split(',').forEach(time => { - const cleanedTime = time.replace(/\s/g, '') + const { startTimes } = this.state; + let valid = true; + startTimes.split(',').forEach((time) => { + const cleanedTime = time.replace(/\s/g, ''); if (time.includes(':')) { - const validTime = /^([0-1]?[0-9]|2[0-3]):([0-5][0-9])(:[0-5][0-9])?$/.test(cleanedTime) - + const validTime = /^([0-1]?[0-9]|2[0-3]):([0-5][0-9])(:[0-5][0-9])?$/.test(cleanedTime); + if (!validTime) { - valid = false - } + valid = false; + } } else { - if ( parseInt(cleanedTime) > 24 || parseInt(cleanedTime) < 0 ) { - valid = false + if (parseInt(cleanedTime) > 24 || parseInt(cleanedTime) < 0) { + valid = false; } } - }) + }); if (!valid) { - this.setState({timeError: true}) + this.setState({ timeError: true }); } else { - this.setState({timeError: false}) + this.setState({ timeError: false }); } - } + }; previewSlots = () => { - const { day, rooms, startTimes } = this.state - const { createTimeSlotPreviews } = this.props - let duration = parseInt(this.state.duration) - let slots = [] - - startTimes.split(',').forEach(time => { - let cleanedStartTime = time.replace(/\s/g, '').split(':') + const { day, rooms, startTimes } = this.state; + const { createTimeSlotPreviews } = this.props; + let duration = parseInt(this.state.duration); + let slots = []; + + startTimes.split(',').forEach((time) => { + let cleanedStartTime = time.replace(/\s/g, '').split(':'); if (cleanedStartTime.length > 1) { - cleanedStartTime = parseInt(cleanedStartTime[0]) + (parseInt(cleanedStartTime[1]) / 60) + cleanedStartTime = parseInt(cleanedStartTime[0]) + parseInt(cleanedStartTime[1]) / 60; } else { - cleanedStartTime = parseInt(cleanedStartTime[0]) + cleanedStartTime = parseInt(cleanedStartTime[0]); } - rooms.forEach(room => { - let slot = {} - slot.startTime = cleanedStartTime - slot.endTime = cleanedStartTime + (duration / 60) - slot.day = day - slot.room = room - slots.push(slot) - }) - }) - - createTimeSlotPreviews(slots, this.state) - } + rooms.forEach((room) => { + let slot = {}; + slot.startTime = cleanedStartTime; + slot.endTime = cleanedStartTime + duration / 60; + slot.day = day; + slot.room = room; + slots.push(slot); + }); + }); + + createTimeSlotPreviews(slots, this.state); + }; componentDidMount() { - const { editState, dayViewing } = this.props + const { editState, dayViewing } = this.props; if (editState) { - this.setState(editState) + this.setState(editState); } else { this.setState({ - day: dayViewing - }) + day: dayViewing, + }); } } render() { - let { sessionFormats, closeBulkTimeSlotModal, counts } = this.props - let { timeError, day, startTimes, duration } = this.state - - const days = Object.keys(counts) - const dayOptions = days.map(day => ( - - )) - - const rooms = this.props.rooms.map(room => { - const checked = this.state.rooms.includes(room.id.toString()) + let { sessionFormats, closeBulkTimeSlotModal, counts } = this.props; + let { timeError, day, startTimes, duration } = this.state; + + const days = Object.keys(counts); + const dayOptions = days.map((day) => ( + + )); + + const rooms = this.props.rooms.map((room) => { + const checked = this.state.rooms.includes(room.id.toString()); return (
- {room.name}
- ) - }) + ); + }); - const denoteRequired = stateKey => { + const denoteRequired = (stateKey) => { if (this.state[stateKey].length < 1) { - return * + return *; } - } + }; - sessionFormats = sessionFormats.slice() - sessionFormats.unshift({}) - const formats = sessionFormats.map(format => { - const text = format.name ? format.name + ' (' + format.duration + ' minutes)' : '' + sessionFormats = sessionFormats.slice(); + sessionFormats.unshift({}); + const formats = sessionFormats.map((format) => { + const text = format.name ? format.name + ' (' + format.duration + ' minutes)' : ''; return ( - - ) - }) + + ); + }); const previewDisabled = () => { - const { rooms, startTimes, duration, timeError } = this.state + const { rooms, startTimes, duration, timeError } = this.state; - return rooms.length < 1 || startTimes.length < 1 || duration.length < 1 || timeError - } + return rooms.length < 1 || startTimes.length < 1 || duration.length < 1 || timeError; + }; return ( -
-
-
+
+
+

Bulk Generate Time Slots

-
+
-
- - +
+ +
- ) + ); } } @@ -228,11 +222,14 @@ BulkCreateModal.propTypes = { counts: PropTypes.object, rooms: PropTypes.array, createTimeSlotPreview: PropTypes.func, - editState: PropTypes.oneOfType([PropTypes.null, PropTypes.object]) -} + editState: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.object), + PropTypes.object + ]), +}; BulkCreateModal.defaultProps = { - rooms: [] -} + rooms: [], +}; -export default BulkCreateModal +export { BulkCreateModal }; diff --git a/app/javascript/components/Schedule/BulkGenerateConfirm.js b/app/javascript/components/Schedule/BulkGenerateConfirm.js index 113c22bf1..c944d6a98 100644 --- a/app/javascript/components/Schedule/BulkGenerateConfirm.js +++ b/app/javascript/components/Schedule/BulkGenerateConfirm.js @@ -1,31 +1,33 @@ -import React, { Component } from "react" -import PropTypes from "prop-types" +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; class BulkGenerateConfirm extends Component { render() { - const { - cancelBulkPreview, - openBulkTimeSlotModal, - requestBulkTimeSlotCreate - } = this.props + const { cancelBulkPreview, openBulkTimeSlotModal, requestBulkTimeSlotCreate } = this.props; return ( -
+
Previewing Grid changes -
- - - +
+ + +
- ) + ); } } BulkGenerateConfirm.propTypes = { cancelBulkPreview: PropTypes.func, openBulkTimeSlotModal: PropTypes.func, - requestBulkTimeSlotCreate: PropTypes.func -} + requestBulkTimeSlotCreate: PropTypes.func, +}; -export default BulkGenerateConfirm +export { BulkGenerateConfirm }; diff --git a/app/javascript/components/Schedule/DayView.js b/app/javascript/components/Schedule/DayView.js index 2275eac8e..f466b04ea 100644 --- a/app/javascript/components/Schedule/DayView.js +++ b/app/javascript/components/Schedule/DayView.js @@ -1,7 +1,7 @@ -import React, { Component } from "react" -import PropTypes from "prop-types" +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; -import ScheduleColumn from './ScheduleColumn' +import { ScheduleColumn } from './ScheduleColumn'; class DayView extends Component { render() { @@ -21,13 +21,13 @@ class DayView extends Component { unscheduledSessions, sessionFormats, showErrors, - } = this.props + } = this.props; - let rows = rooms.map(room => { - const height = (( endTime - startTime + 1) * 90) + 25 + 'px' + let rows = rooms.map((room) => { + const height = (endTime - startTime + 1) * 90 + 25 + 'px'; return ( - ) - }) + ); + }); - return ( - - {rows} - - ) + return {rows}; } } @@ -71,7 +67,7 @@ DayView.propTypes = { unscheduledSessions: PropTypes.array, sessionFormats: PropTypes.array, showErrors: PropTypes.func, -} -DayView.defaultProps = {sessions: []} +}; +DayView.defaultProps = { sessions: [] }; -export default DayView +export { DayView }; diff --git a/app/javascript/components/Schedule/GenerateGridButton.js b/app/javascript/components/Schedule/GenerateGridButton.js index 2a8a06319..09b178f2a 100644 --- a/app/javascript/components/Schedule/GenerateGridButton.js +++ b/app/javascript/components/Schedule/GenerateGridButton.js @@ -1,21 +1,21 @@ -import React, { Component } from "react" -import PropTypes from "prop-types" +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; class GenerateGridButton extends Component { render() { - const { openBulkTimeSlotModal } = this.props + const { openBulkTimeSlotModal } = this.props; return ( - - ) + + ); } } GenerateGridButton.propTypes = { dayViewing: PropTypes.number, - generateGridPath: PropTypes.string -} + generateGridPath: PropTypes.string, +}; -export default GenerateGridButton +export { GenerateGridButton }; diff --git a/app/javascript/components/Schedule/Nav.js b/app/javascript/components/Schedule/Nav.js index 5777e1d75..05186b296 100644 --- a/app/javascript/components/Schedule/Nav.js +++ b/app/javascript/components/Schedule/Nav.js @@ -1,29 +1,29 @@ -import React, { Component } from "react" -import PropTypes from "prop-types" +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; class Nav extends Component { render() { - const { changeDayView, counts, dayViewing, slots} = this.props - - const navTabs = Object.keys(counts).map(dayNumber => { - let allSlots = slots.filter(slot => slot.conference_day == dayNumber) - let bookedSlots = allSlots.filter(slot => slot.program_session_id) + const { changeDayView, counts, dayViewing, slots } = this.props; - return
  • changeDayView(parseInt(dayNumber))} - key={'day-tab ' + dayNumber} - className={dayNumber == dayViewing ? 'active' : ''} - > - Day {dayNumber} - {bookedSlots.length}/{allSlots.length} -
  • - }) + const navTabs = Object.keys(counts).map((dayNumber) => { + let allSlots = slots.filter((slot) => slot.conference_day == dayNumber); + let bookedSlots = allSlots.filter((slot) => slot.program_session_id); - return ( -
      - {navTabs} -
    - ) + return ( +
  • changeDayView(parseInt(dayNumber))} + key={'day-tab ' + dayNumber} + className={dayNumber == dayViewing ? 'active' : ''} + > + Day {dayNumber} + + {bookedSlots.length}/{allSlots.length}{' '} + +
  • + ); + }); + + return
      {navTabs}
    ; } } @@ -31,7 +31,7 @@ Nav.propTypes = { changeDayView: PropTypes.func, counts: PropTypes.object, dayViewing: PropTypes.number, - schedule: PropTypes.object -} + schedule: PropTypes.object, +}; -export default Nav +export { Nav }; diff --git a/app/javascript/components/Schedule/Preview.js b/app/javascript/components/Schedule/Preview.js index f250dfcae..7e0d66eeb 100644 --- a/app/javascript/components/Schedule/Preview.js +++ b/app/javascript/components/Schedule/Preview.js @@ -1,25 +1,22 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; class Preview extends Component { render() { - const { preview, startTime } = this.props + const { preview, startTime } = this.props; const style = { - top: (preview.startTime - Math.floor(startTime)) * 90 +"px", - height: (preview.endTime - preview.startTime) * 90 +"px" - } + top: (preview.startTime - Math.floor(startTime)) * 90 + 'px', + height: (preview.endTime - preview.startTime) * 90 + 'px', + }; - return ( -
    -
    - ) + return
    ; } } Preview.propTypes = { preview: PropTypes.object, - startTime: PropTypes.number -} + startTime: PropTypes.number, +}; -export default Preview +export { Preview }; diff --git a/app/javascript/components/Schedule/ProgramSession.js b/app/javascript/components/Schedule/ProgramSession.js index 2686f3ba3..c484998fe 100644 --- a/app/javascript/components/Schedule/ProgramSession.js +++ b/app/javascript/components/Schedule/ProgramSession.js @@ -1,45 +1,52 @@ -import React, { Component } from "react" -import PropTypes from "prop-types" +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; class ProgramSession extends Component { constructor(props) { - super(props) + super(props); } drag = (session, e) => { - this.props.onDrag(session) - e.dataTransfer.setData('text', 'anything'); - } - - dragEnd = e => { - e.preventDefault() - } - - render() { - const {session, tracks} = this.props + this.props.onDrag(session); + e.dataTransfer.setData('text', 'anything'); + }; - const sessionTrack = tracks.find(track => track.id === session.track_id) + dragEnd = (e) => { + e.preventDefault(); + }; - const bkgColor = sessionTrack ? sessionTrack.color : '#fff' - const trackName = sessionTrack ? sessionTrack.name : '' - - return( -
    this.drag(session, e)} onDragEnd={(e) => this.dragEnd(e)}> -
    {trackName}
    -
    + render() { + const { session, tracks } = this.props; + + const sessionTrack = tracks.find((track) => track.id === session.track_id); + + const bkgColor = sessionTrack ? sessionTrack.color : '#fff'; + const trackName = sessionTrack ? sessionTrack.name : ''; + + return ( +
    this.drag(session, e)} + onDragEnd={(e) => this.dragEnd(e)} + > +
    + {trackName} +
    +

    {session.title}

    - ) + ); } } -export default ProgramSession +export { ProgramSession }; ProgramSession.propTypes = { session: PropTypes.object, onDrag: PropTypes.func, - tracks: PropTypes.array -} + tracks: PropTypes.array, +}; -ProgramSession.defaultProps = {tracks: []} +ProgramSession.defaultProps = { tracks: [] }; diff --git a/app/javascript/components/Schedule/Ruler.js b/app/javascript/components/Schedule/Ruler.js index 0648091b2..af98795b5 100644 --- a/app/javascript/components/Schedule/Ruler.js +++ b/app/javascript/components/Schedule/Ruler.js @@ -1,34 +1,32 @@ -import React, { Component } from "react" -import PropTypes from "prop-types" +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; class Ruler extends Component { render() { - const {startTime, endTime} = this.props - let hours = [] + const { startTime, endTime } = this.props; + let hours = []; for (let i = Math.floor(startTime); i <= Math.floor(endTime); i++) { - let time + let time; if (i > 12) { - time = i-12 + ":00 pm" + time = i - 12 + ':00 pm'; } else { - time = i + ":00 am" + time = i + ':00 am'; } - hours.push(time) + hours.push(time); } - let ticks = hours.map(hour => ( -
  • {hour}
  • - )) + let ticks = hours.map((hour) => ( +
  • + {hour} +
  • + )); - return( -
      - {ticks} -
    - ) + return
      {ticks}
    ; } } Ruler.propTypes = { startTime: PropTypes.number, - endTime: PropTypes.number -} + endTime: PropTypes.number, +}; -export default Ruler +export { Ruler }; diff --git a/app/javascript/components/Schedule/ScheduleColumn.js b/app/javascript/components/Schedule/ScheduleColumn.js index 7962972bf..9516595df 100644 --- a/app/javascript/components/Schedule/ScheduleColumn.js +++ b/app/javascript/components/Schedule/ScheduleColumn.js @@ -1,8 +1,8 @@ -import React, { Component, Fragment } from "react" -import PropTypes from "prop-types" +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; -import ScheduleSlot from './ScheduleSlot' -import Preview from './Preview' +import { ScheduleSlot } from './ScheduleSlot'; +import { Preview } from './Preview'; class ScheduleColumn extends Component { render() { @@ -22,27 +22,29 @@ class ScheduleColumn extends Component { unscheduledSessions, sessionFormats, showErrors, - } = this.props + } = this.props; - const roomID = room.id + const roomID = room.id; - const previews = previewSlots.filter(preview => { - return parseInt(preview.room) === roomID && parseInt(preview.day) === dayViewing - }).map((preview, index) => { - return - }) + const previews = previewSlots + .filter((preview) => { + return parseInt(preview.room) === roomID && parseInt(preview.day) === dayViewing; + }) + .map((preview, index) => { + return ; + }); - const thisRoomThisDaySlots = slots.filter(slot => slot.room_id == roomID && slot.conference_day == dayViewing) + const thisRoomThisDaySlots = slots.filter((slot) => slot.room_id == roomID && slot.conference_day == dayViewing); - let rowSlots = + let rowSlots = ; if (thisRoomThisDaySlots) { - rowSlots = thisRoomThisDaySlots.map(slot => { + rowSlots = thisRoomThisDaySlots.map((slot) => { return ( - - ) - }) + ); + }); } - + return ( -
    +
    {previews} {rowSlots}
    - ) + ); } } @@ -89,8 +87,8 @@ ScheduleColumn.propTypes = { handleMoveSessionResponse: PropTypes.func, unscheduledSessions: PropTypes.array, showErrors: PropTypes.func, -} +}; -ScheduleColumn.defaultProps = {sessions: []} +ScheduleColumn.defaultProps = { sessions: [] }; -export default ScheduleColumn +export { ScheduleColumn }; diff --git a/app/javascript/components/Schedule/ScheduleSlot.js b/app/javascript/components/Schedule/ScheduleSlot.js index eafc4835f..f32f4a7cd 100644 --- a/app/javascript/components/Schedule/ScheduleSlot.js +++ b/app/javascript/components/Schedule/ScheduleSlot.js @@ -1,149 +1,168 @@ -import React, { Component, Fragment } from "react" -import PropTypes from "prop-types" +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; -import ProgramSession from './ProgramSession' -import TimeSlotInfo from './TimeSlotInfo' -import TimeSlotModal from './TimeSlotModal' -import { patchTimeSlot } from "../../apiCalls" +import { ProgramSession } from './ProgramSession'; +import { TimeSlotInfo } from './TimeSlotInfo'; +import { TimeSlotModal } from './TimeSlotModal'; +import { patchTimeSlot } from '../../apiCalls'; class ScheduleSlot extends Component { constructor(props) { - super(props) + super(props); this.state = { hoverDrag: false, modalShowing: false, title: this.props.slot.title || '', track: this.props.slot.track_id || '', presenter: this.props.slot.presenter || '', - description: this.props.slot.description || '' - } - } - - onDragOver = e => { - e.preventDefault() - this.setState({ hoverDrag: true }) + description: this.props.slot.description || '', + }; } - onDragLeave = e => { - e.preventDefault() - this.setState({ hoverDrag: false}) - } + onDragOver = (e) => { + e.preventDefault(); + this.setState({ hoverDrag: true }); + }; + + onDragLeave = (e) => { + e.preventDefault(); + this.setState({ hoverDrag: false }); + }; onDrop = (slot, e) => { - e.preventDefault() - const session = this.props.draggedSession - const { handleMoveSessionResponse, changeDragged } = this.props - + e.preventDefault(); + const session = this.props.draggedSession; + const { handleMoveSessionResponse, changeDragged } = this.props; + if (session.slot) { if (session.slot.program_session_id === slot.program_session_id) { - changeDragged(null) - return + changeDragged(null); + return; } } patchTimeSlot(slot, session) .then((response) => response.json()) - .then(data => { - const { errors } = data + .then((data) => { + const { errors } = data; if (errors) { - this.props.showErrors(errors) - return + this.props.showErrors(errors); + return; } if (session.slot) { patchTimeSlot(session.slot, null) .then((response) => response.json()) - .then(data => { - const { sessions, slots, unscheduled_sessions } = data - handleMoveSessionResponse(sessions, unscheduled_sessions, slots, session) - }) + .then((data) => { + const { sessions, slots, unscheduled_sessions } = data; + handleMoveSessionResponse(sessions, unscheduled_sessions, slots, session); + }); } else { - const { sessions, slots, unscheduled_sessions } = data - handleMoveSessionResponse(sessions, unscheduled_sessions, slots) + const { sessions, slots, unscheduled_sessions } = data; + handleMoveSessionResponse(sessions, unscheduled_sessions, slots); } - changeDragged(null) - this.setState({ hoverDrag: false }) + changeDragged(null); + this.setState({ hoverDrag: false }); }) - .catch(error => console.error("Error:", error)) - } + .catch((error) => console.error('Error:', error)); + }; onDrag = (programSession) => { - const { title, description, track, presenter } = this.state - this.props.changeDragged(Object.assign(programSession, {slot: Object.assign(this.props.slot, {title, description, track_id: track, presenter})})) - } + const { title, description, track, presenter } = this.state; + this.props.changeDragged( + Object.assign(programSession, { + slot: Object.assign(this.props.slot, { + title, + description, + track_id: track, + presenter, + }), + }) + ); + }; showModal = () => { if (!this.state.modalShowing) { - this.setState({modalShowing: true}) + this.setState({ modalShowing: true }); } - } + }; closeModal = () => { - this.setState({modalShowing: false}) - } + this.setState({ modalShowing: false }); + }; updateSlot = (e) => { - const { name, value } = e.target + const { name, value } = e.target; this.setState({ - [name]: value - }) - } + [name]: value, + }); + }; render() { - const { slot, ripTime, startTime, sessions, tracks, unscheduledSessions, handleMoveSessionResponse, sessionFormats, roomName } = this.props - const { title, track, presenter, description } = this.state - - const slotStartTime = ripTime(slot.start_time) - const slotEndTime = ripTime(slot.end_time) - let background = this.state.hoverDrag ? '#f9f6f1' : '#fff' - + const { + slot, + ripTime, + startTime, + sessions, + tracks, + unscheduledSessions, + handleMoveSessionResponse, + sessionFormats, + roomName, + } = this.props; + const { title, track, presenter, description } = this.state; + + const slotStartTime = ripTime(slot.start_time); + const slotEndTime = ripTime(slot.end_time); + let background = this.state.hoverDrag ? '#f9f6f1' : '#fff'; + const style = { - top: (slotStartTime - startTime) * 90 + "px", - height: (slotEndTime - slotStartTime) * 90 + "px", - background - } + top: (slotStartTime - startTime) * 90 + 'px', + height: (slotEndTime - slotStartTime) * 90 + 'px', + background, + }; - let matchedSession - let session + let matchedSession; + let session; if (slot.program_session_id) { - matchedSession = sessions.find( - session => session.id === slot.program_session_id - ) - session = + matchedSession = sessions.find((session) => session.id === slot.program_session_id); + session = ; } - let timeSlotInfo = + let timeSlotInfo = ; return (
    this.onDragOver(e)} - onDragLeave={e => this.onDragLeave(e)} + onDragOver={(e) => this.onDragOver(e)} + onDragLeave={(e) => this.onDragLeave(e)} onDrop={(e) => this.onDrop(slot, e)} onClick={this.showModal} > {session || timeSlotInfo} - {this.state.modalShowing === true && } + {this.state.modalShowing === true && ( + + )}
    - ) + ); } } @@ -157,7 +176,7 @@ ScheduleSlot.propTypes = { tracks: PropTypes.array, unscheduledSessions: PropTypes.array, showErrors: PropTypes.func, - roomName: PropTypes.string -} + roomName: PropTypes.string, +}; -export default ScheduleSlot +export { ScheduleSlot }; diff --git a/app/javascript/components/Schedule/TimeSlotInfo.js b/app/javascript/components/Schedule/TimeSlotInfo.js index dd2030176..5b5169650 100644 --- a/app/javascript/components/Schedule/TimeSlotInfo.js +++ b/app/javascript/components/Schedule/TimeSlotInfo.js @@ -1,35 +1,37 @@ -import React, { Component } from "react" -import PropTypes from "prop-types" +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; class TimeSlotInfo extends Component { constructor(props) { - super(props) + super(props); } - + render() { - const {slot, tracks} = this.props + const { slot, tracks } = this.props; - const slotTrack = tracks.find(track => track.id === slot.track_id) + const slotTrack = tracks.find((track) => track.id === slot.track_id); - const bkgColor = slotTrack ? slotTrack.color : 'unset' - const trackName = slotTrack ? slotTrack.name : '' + const bkgColor = slotTrack ? slotTrack.color : 'unset'; + const trackName = slotTrack ? slotTrack.name : ''; - return( -
    -
    {trackName}
    -
    + return ( +
    +
    + {trackName} +
    +

    {slot.title}

    - ) + ); } } -export default TimeSlotInfo +export { TimeSlotInfo }; TimeSlotInfo.propTypes = { slot: PropTypes.object, - tracks: PropTypes.array -} + tracks: PropTypes.array, +}; -TimeSlotInfo.defaultProps = {tracks: []} +TimeSlotInfo.defaultProps = { tracks: [] }; diff --git a/app/javascript/components/Schedule/TimeSlotModal.js b/app/javascript/components/Schedule/TimeSlotModal.js index 76033db5c..cc497671d 100644 --- a/app/javascript/components/Schedule/TimeSlotModal.js +++ b/app/javascript/components/Schedule/TimeSlotModal.js @@ -1,191 +1,165 @@ -import React, { Component } from "react" -import PropTypes from "prop-types" +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import { patchTimeSlot } from '../../apiCalls'; class TimeSlotModal extends Component { constructor(props) { - super(props) + super(props); this.state = { - sessionSelected: this.props.matchedSession - ? this.props.matchedSession - : '' - } + sessionSelected: this.props.matchedSession ? this.props.matchedSession : '', + }; } componentDidMount() { - const { slot, matchedSession } = this.props + const { slot, matchedSession } = this.props; } changeSession = (e) => { - let session = this.props.sessions.find(s => s.title === e.target.value ) + let session = this.props.sessions.find((s) => s.title === e.target.value); if (!session) { - this.setState({ sessionSelected: '' }) + this.setState({ sessionSelected: '' }); } else { - this.setState({ sessionSelected: session }) + this.setState({ sessionSelected: session }); } - } + }; close = () => { - this.props.closeModal() - } + this.props.closeModal(); + }; saveChanges = () => { - const { sessionSelected } = this.state - const { closeModal, handleMoveSessionResponse, title, track, presenter, description } = this.props - - let slot + const { sessionSelected } = this.state; + const { closeModal, handleMoveSessionResponse, title, track, presenter, description } = this.props; + + let slot; if (!sessionSelected) { - slot = Object.assign(this.props.slot, {title, track_id: track, presenter, description}) + slot = Object.assign(this.props.slot, { + title, + track_id: track, + presenter, + description, + }); } else { - slot = this.props.slot + slot = this.props.slot; } - - patchTimeSlot(slot, (sessionSelected || null)) - .then(response => response.json()) - .then(data => { - const { sessions, slots, unscheduled_sessions } = data - handleMoveSessionResponse(sessions, unscheduled_sessions, slots) - closeModal() - }) - } + + patchTimeSlot(slot, sessionSelected || null) + .then((response) => response.json()) + .then((data) => { + const { sessions, slots, unscheduled_sessions } = data; + handleMoveSessionResponse(sessions, unscheduled_sessions, slots); + closeModal(); + }); + }; formatTime = (time) => { - let hours = time.split("T")[1].split(":")[0] - let amPm = hours < 12 ? ' am' : ' pm' + let hours = time.split('T')[1].split(':')[0]; + let amPm = hours < 12 ? ' am' : ' pm'; if (parseInt(hours) > 12) { - hours = (parseInt(hours) - 12).toString() + hours = (parseInt(hours) - 12).toString(); } if (hours.charAt(0) === '0') { - hours = hours.substr(1) + hours = hours.substr(1); } - const minutes = time.split(":")[1] - return hours + ':' + minutes + amPm - } + const minutes = time.split(':')[1]; + return hours + ':' + minutes + amPm; + }; render() { - const { - slot, - matchedSession, - unscheduledSessions, - tracks, - sessionFormats, - title, - track, - presenter, - description, - updateSlot, - roomName - } = this.props - - const { sessionSelected } = this.state - - let sessionOptions + const { slot, matchedSession, unscheduledSessions, tracks, sessionFormats, roomName } = this.props; + + const { sessionSelected } = this.state; + + let sessionOptions; if (sessionSelected) { - sessionOptions = matchedSession + sessionOptions = matchedSession ? [matchedSession, ...unscheduledSessions] - : [{title: ''}, sessionSelected, ...unscheduledSessions.filter(s => s.id !== sessionSelected.id)] + : [{ title: '' }, sessionSelected, ...unscheduledSessions.filter((s) => s.id !== sessionSelected.id)]; } else { - sessionOptions = [{title: ''}, ...unscheduledSessions] + sessionOptions = [{ title: '' }, ...unscheduledSessions]; } - sessionOptions = sessionOptions.map(session => ( - - )) + sessionOptions = sessionOptions.map((session) => ( + + )); - let sessionInfo + let sessionInfo; if (sessionSelected) { - sessionInfo = <> + sessionInfo = ( + <> + + + + + + ); + } + + let timeSlotForm = ( + <>