diff --git a/.rubocop.yml b/.rubocop.yml index c68a2f1..3c01f3f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,11 +1,9 @@ -Rails: - Enabled: true - AllCops: Exclude: - 'bin/*' - 'node_modules/**/*' - 'db/schema.rb' + - 'app/models/achievements/**/*' Metrics/MethodLength: Exclude: @@ -14,13 +12,18 @@ Metrics/AbcSize: Exclude: - 'db/migrate/*' -Style/ClassAndModuleChildren: - Enabled: false -Style/RegexpLiteral: - EnforcedStyle: slashes +Rails: + Enabled: true +Rails/InverseOf: + Enabled: true + Style/Documentation: Exclude: - 'db/migrate/*' - -Rails/InverseOf: - Enabled: true \ No newline at end of file + - 'app/controllers/*' +Style/RegexpLiteral: + Enabled: true + EnforcedStyle: slashes +Style/ClassAndModuleChildren: + Enabled: true + EnforcedStyle: compact diff --git a/.travis.yml b/.travis.yml index 10960c0..b5df232 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,6 @@ language: ruby rvm: - 2.4.4 -# We need Ubuntu 14 (Trusty) to use Postgres 9.6 -dist: trusty - -addons: - postgresql: 9.6 - cache: directories: - vendor/bundle diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..afe3509 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contributing to riverrats.com.au + +Thanks for taking the time to contribute to the River Rats official website! Help is always appreciated. + + + +## Branches + +We follow the branching model outlined in the article [A successful Git branching model](https://nvie.com/posts/a-successful-git-branching-model/). If you can't be bothered to read all that, here's the important bits. + +There are two main branches: + + 1. `master`: The current production version of the website. + 2. `develop`: Where future releases are under construction. Unless you're writing a bug fix you'll likely want to start here. + +Then there are three types of supporting branches: + + * `feature-*`: A branch which introduces a new feature. e.g. `feature-player-nicknames`. + * `release-*`: A branch devoted to polishing up the project for a new release. e.g. `release-1.2.1`. Note that only project maintainers will create this kind of branch. + * `hotfix-*`: For fixing bugs in production. + + + +## Redis + +To run the development server you'll need to have an instance of Redis up and running. Jobs are managed by `sidekiq`, so you'll also need to start `sidekiq`. Once Redis is running you can achieve this with the command (from the root project directory): + +``` +$ bundle exec sidekiq -C config/sidekiq.yml +``` + + +## Elasticsearch + +If you want to use searching functionality you'll also need to install elasticsearch on your machine. diff --git a/Gemfile b/Gemfile index 531b6f4..e36a987 100644 --- a/Gemfile +++ b/Gemfile @@ -35,8 +35,6 @@ gem 'jbuilder', '~> 2.8' gem 'bcrypt', platform: :ruby group :development, :test do - # Call 'byebug' anywhere in the code to stop execution and get a debugger - # console gem 'byebug', platforms: %i[mri mingw x64_mingw] # Adds support for Capybara system testing and selenium driver gem 'capybara', '~> 3.12' @@ -45,7 +43,7 @@ end group :development do gem 'capistrano', '~> 3.11.0', require: false - gem 'capistrano-bundler', '~> 1.3', require: false + gem 'capistrano-bundler', '~> 1.4', require: false gem 'capistrano-passenger', require: false gem 'capistrano-rails', '~> 1.3', require: false gem 'capistrano-rvm', require: false @@ -68,9 +66,10 @@ gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] gem 'autoprefixer-rails' gem 'bootsnap', require: false gem 'connection_pool' +gem 'coveralls', require: false gem 'devise' gem 'elasticsearch' -gem 'friendly_id', '~> 5.1.0' +gem 'friendly_id', '~> 5.2.4' gem 'ice_cube' gem 'js-routes' gem 'kaminari' diff --git a/Gemfile.lock b/Gemfile.lock index aefd4a2..0815d33 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -48,7 +48,7 @@ GEM sshkit (>= 1.6.1, != 1.7.0) arel (9.0.0) ast (2.4.0) - autoprefixer-rails (9.1.4) + autoprefixer-rails (9.3.1) execjs bcrypt (3.1.12) bindex (0.5.0) @@ -61,7 +61,7 @@ GEM i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (1.3.0) + capistrano-bundler (1.4.0) capistrano (~> 3.1) sshkit (~> 1.2) capistrano-passenger (0.2.0) @@ -92,6 +92,12 @@ GEM coffee-script-source (1.12.2) concurrent-ruby (1.1.3) connection_pool (2.2.2) + coveralls (0.7.1) + multi_json (~> 1.3) + rest-client + simplecov (>= 0.7) + term-ansicolor + thor crass (1.0.4) devise (4.5.0) bcrypt (~> 3.0) @@ -99,6 +105,9 @@ GEM railties (>= 4.1.0, < 6.0) responders warden (~> 1.2.3) + docile (1.3.1) + domain_name (0.5.20180417) + unf (>= 0.0.5, < 1.0.0) elasticsearch (6.1.0) elasticsearch-api (= 6.1.0) elasticsearch-transport (= 6.1.0) @@ -112,11 +121,13 @@ GEM faraday (0.15.3) multipart-post (>= 1.2, < 3) ffi (1.9.25) - friendly_id (5.1.0) + friendly_id (5.2.4) activerecord (>= 4.0.0) globalid (0.4.1) activesupport (>= 4.2.0) hashie (3.6.0) + http-cookie (1.0.3) + domain_name (~> 0.5) i18n (1.1.1) concurrent-ruby (~> 1.0) ice_cube (0.16.3) @@ -127,6 +138,7 @@ GEM js-routes (1.4.4) railties (>= 3.2) sprockets-rails + json (2.1.0) kaminari (1.1.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.1.1) @@ -170,6 +182,7 @@ GEM net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (5.0.2) + netrc (0.11.0) nio4r (2.3.1) nokogiri (1.8.5) mini_portile2 (~> 2.3.0) @@ -181,7 +194,7 @@ GEM mimemagic (~> 0.3.0) terrapin (~> 0.6.0) parallel (1.12.1) - parser (2.5.1.2) + parser (2.5.3.0) ast (~> 2.4.0) pg (1.1.3) powerpack (0.1.2) @@ -229,14 +242,18 @@ GEM responders (2.4.0) actionpack (>= 4.2.0, < 5.3) railties (>= 4.2.0, < 5.3) - rubocop (0.59.2) + rest-client (2.0.2) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + rubocop (0.60.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.5, != 2.5.1.1) powerpack (~> 0.1) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) + unicode-display_width (~> 1.4.0) ruby-progressbar (1.10.0) ruby_dep (1.5.0) rubyzip (1.2.2) @@ -258,13 +275,18 @@ GEM activemodel (>= 4.2) elasticsearch (>= 5) hashie - selenium-webdriver (3.14.0) + selenium-webdriver (3.141.0) childprocess (~> 0.5) - rubyzip (~> 1.2) + rubyzip (~> 1.2, >= 1.2.2) sidekiq (5.2.3) connection_pool (~> 2.2, >= 2.2.2) rack-protection (>= 1.5.0) redis (>= 3.3.5, < 5) + simplecov (0.16.1) + docile (~> 1.1) + json (>= 1.8, < 3) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.2) spring (2.0.2) activesupport (>= 4.2) spring-watcher-listen (2.0.1) @@ -277,14 +299,17 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sshkit (1.17.0) + sshkit (1.18.0) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) + term-ansicolor (1.6.0) + tins (~> 1.0) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) thor (0.20.3) thread_safe (0.3.6) tilt (2.0.8) + tins (1.16.3) turbolinks (5.2.0) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) @@ -292,6 +317,9 @@ GEM thread_safe (~> 0.1) uglifier (4.1.20) execjs (>= 0.3.0, < 3) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.5) unicode-display_width (1.4.0) unicorn (5.4.1) kgio (~> 2.6) @@ -322,16 +350,17 @@ DEPENDENCIES bootsnap byebug capistrano (~> 3.11.0) - capistrano-bundler (~> 1.3) + capistrano-bundler (~> 1.4) capistrano-passenger capistrano-rails (~> 1.3) capistrano-rvm capybara (~> 3.12) coffee-rails (~> 4.2) connection_pool + coveralls devise elasticsearch - friendly_id (~> 5.1.0) + friendly_id (~> 5.2.4) ice_cube jbuilder (~> 2.8) js-routes diff --git a/README.md b/README.md index e21e637..7c620d2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,22 @@ -# riverrats.com.au +# River Rats Poker League [![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=sean0x42/riverrats.com.au&identifier=126777895)](https://dependabot.com) +[![Coverage Status](https://coveralls.io/repos/github/sean0x42/riverrats.com.au/badge.svg?branch=master)](https://coveralls.io/github/sean0x42/riverrats.com.au?branch=master) [![Build Status](https://travis-ci.com/sean0x42/riverrats.com.au.svg?token=y4PzktMpXpMzBmaZHNGq&branch=develop)](https://travis-ci.com/sean0x42/riverrats.com.au) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/a5bd9fd7908c4e838dd9824ed347b559)](https://www.codacy.com/app/sean_19/riverrats.com.au?utm_source=github.com&utm_medium=referral&utm_content=sean0x42/riverrats.com.au&utm_campaign=Badge_Grade) +[![GitLicense](https://gitlicense.com/badge/sean0x42/riverrats.com.au)](https://gitlicense.com/license/sean0x42/riverrats.com.au) -This is a commissioned Rails webapp for the River Rats Poker League, operating throughout the Mid-North Coast (Australia). +This is a commissioned Rails webapp for the River Rats Poker League, operating +throughout the Mid-North Coast (Australia). + +## Contributing + +Contributions are welcome, and encouraged! + + * If you have a feature request, or bug report, please [create an issue](https://github.com/sean0x42/riverrats.com.au/issues/new). + * If you would like to make a suggestion, you may fork the repository, or use inline suggestions on GitHub. + * For more complex contributions, please check out our [contribution guide](https://github.com/sean0x42/riverrats.com.au/blob/master/CONTRIBUTING.md). This will explain our approach to branching, as well as some helpful advice to get your development environment up and running. + +## Author + +This website is designed, developed, and maintained by +[Sean Bailey](https://www.seanbailey.io). diff --git a/Rakefile b/Rakefile index 488c551..2d79e51 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,8 @@ # frozen_string_literal: true # Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. +# for example lib/tasks/capistrano.rake, and they will automatically be +# available to Rake. require_relative 'config/application' diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 6839d7f..c1c91da 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -13,4 +13,4 @@ //= require rails-ujs //= require turbolinks //= require local-time -//= require js-routes \ No newline at end of file +//= require js-routes diff --git a/app/assets/stylesheets/components/_achievements.scss b/app/assets/stylesheets/components/_achievements.scss index 3e64218..7e8250e 100644 --- a/app/assets/stylesheets/components/_achievements.scss +++ b/app/assets/stylesheets/components/_achievements.scss @@ -22,7 +22,7 @@ } .title { - font-weight: bold; + font-weight: 500; } } } diff --git a/app/assets/stylesheets/components/_button.scss b/app/assets/stylesheets/components/_button.scss index 7257c15..b581915 100644 --- a/app/assets/stylesheets/components/_button.scss +++ b/app/assets/stylesheets/components/_button.scss @@ -98,9 +98,9 @@ } .button-tertiary { - background: $grey-light; - color: lighten($text, 20%); - fill: lighten($text, 20%); + background: $grey; + color: lighten($text, 15%); + fill: lighten($text, 15%); margin: 0.5rem -0.25rem; &:hover { diff --git a/app/assets/stylesheets/components/_comments.scss b/app/assets/stylesheets/components/_comments.scss new file mode 100644 index 0000000..250ed82 --- /dev/null +++ b/app/assets/stylesheets/components/_comments.scss @@ -0,0 +1,129 @@ +.comment-form { + background: $grey-lighter; + border: 1px solid $grey; + border-radius: $border-radius; + max-width: 700px; + padding: 0.75rem; + + textarea { + margin: 0 0 0.5rem; + resize: vertical; + } + + .button { + padding: 12px 18px; + } + + .comment-form--footer { + align-items: flex-start; + display: flex; + justify-content: space-between; + + label { + color: lighten($text, 20%); + line-height: 1; + } + } +} + +.game-comments { + list-style-type: none; + margin: 1.5rem 0 0; + max-width: 80ch; + padding: 0; + position: relative; + + &::before { + background: $grey-dark; + bottom: 0; + content: ""; + left: 38.5px; + position: absolute; + top: 0; + width: 2px; + z-index: 1; + } + + .comment { + background: $white; + border: 1px solid $grey-dark; + border-radius: $border-radius; + margin: 2rem 0; + position: relative; + z-index: 2; + + &.deleted { + background: $red-lighter; + border-color: $red; + color: $red-lighter-inverted; + } + } + + .comment-header { + align-items: center; + background: $grey-lighter; + border-bottom: 1px solid $grey; + border-radius: $border-radius $border-radius 0 0; + color: lighten($text, 25%); + display: flex; + padding: 0.75rem 1rem; + + a { + color: inherit; + } + + .highlight { + color: $text; + } + + .flair { + background: $zurple-light; + color: $zurple-lighter-inverted; + } + } + + .deleted .comment-header { + background: $red-light; + border-color: $red; + color: $red-light-inverted; + + .highlight { + color: $red-inverted; + font-weight: 500; + } + + .flair { + background: $red-light-inverted; + color: $red-light; + } + } + + .comment-body { + padding: 0.75rem 1rem; + + p { + margin: 0; + } + } + + .comment-footer { + align-items: center; + display: flex; + list-style-type: none; + padding: 0.25rem 1rem 0.75rem; + + a, + span { + color: lighten($text, 25%); + margin-right: 0.5rem; + } + } + + .deleted .comment-footer { + a, + span { + color: $red-lighter-inverted; + text-decoration: line-through; + } + } +} diff --git a/app/assets/stylesheets/components/_flash-messages.scss b/app/assets/stylesheets/components/_flash-messages.scss index 0597f47..9a4e794 100644 --- a/app/assets/stylesheets/components/_flash-messages.scss +++ b/app/assets/stylesheets/components/_flash-messages.scss @@ -88,7 +88,7 @@ } .flash { - margin-top: 0; margin-bottom: 1rem; + margin-top: 0; } } diff --git a/app/assets/stylesheets/components/_header-menu.scss b/app/assets/stylesheets/components/_header-menu.scss index e76e662..f258825 100755 --- a/app/assets/stylesheets/components/_header-menu.scss +++ b/app/assets/stylesheets/components/_header-menu.scss @@ -26,7 +26,7 @@ left: 0; line-height: $body-line-height; margin-top: 0.8rem; - min-width: 275px; + min-width: 240px; position: absolute; right: 0; } @@ -40,7 +40,7 @@ width: 100%; } - a { + a:not(.notification-link) { align-items: center; color: $text; cursor: pointer; @@ -61,8 +61,8 @@ } .spacer { - border-bottom: 1px solid $grey; - margin: 0.75rem 1.25rem; + border-bottom: 2px solid $grey; + margin: 0.8rem 0; width: unset; } } @@ -82,6 +82,30 @@ flex-direction: column; } +// Define triangle +.header-menu::after, +.header-menu::before { + border-style: solid; + bottom: 100%; + content: ""; + display: block; + height: 0; + position: absolute; + width: 0; +} + +.header-menu::after { + border-color: $transparent $transparent $white; + border-width: 8px; + right: 25px; +} + +.header-menu::before { + border-color: $transparent $transparent $grey; + border-width: 9px; + right: 24px; +} + @media (min-width: 980px) { .header-link, .header-menu-wrapper .header-menu-trigger { diff --git a/app/assets/stylesheets/components/_header.scss b/app/assets/stylesheets/components/_header.scss index e49cc97..678dd06 100644 --- a/app/assets/stylesheets/components/_header.scss +++ b/app/assets/stylesheets/components/_header.scss @@ -86,9 +86,10 @@ fill: $darcula-inverted; } - svg { + svg.material-icons { fill: darken($darcula-inverted, 8%); height: 22px; + margin-bottom: 0; width: 22px; } diff --git a/app/assets/stylesheets/components/_modal-form.scss b/app/assets/stylesheets/components/_modal-form.scss index 63d1de0..24112f5 100644 --- a/app/assets/stylesheets/components/_modal-form.scss +++ b/app/assets/stylesheets/components/_modal-form.scss @@ -1,13 +1,14 @@ -.admin .modal-form { +.modal-wrapper .modal-form { + padding: 0; +} + +.modal-form { background: $white; border-radius: $border-radius; + margin-top: 2rem; max-width: $modal-width; padding: 2rem; width: 100%; -} - -.modal-form { - margin-top: 2rem; hr { border: 0; @@ -50,7 +51,17 @@ margin: 0; } - input { + input:not([type="number"]) { max-width: unset; } + + textarea { + min-height: 156px; + resize: vertical; + } + + span.highlight { + color: $text-bold; + font-weight: 500; + } } diff --git a/app/assets/stylesheets/components/_model-list.scss b/app/assets/stylesheets/components/_model-list.scss index 39de0d5..96b7475 100644 --- a/app/assets/stylesheets/components/_model-list.scss +++ b/app/assets/stylesheets/components/_model-list.scss @@ -61,6 +61,14 @@ .numeric { text-align: right; } + + .icon svg { + fill: lighten($text, 25%); + height: 22px; + line-height: 1; + vertical-align: middle; + width: 22px; + } } .model-list-footer { diff --git a/app/assets/stylesheets/components/_notifications.scss b/app/assets/stylesheets/components/_notifications.scss new file mode 100644 index 0000000..204e93f --- /dev/null +++ b/app/assets/stylesheets/components/_notifications.scss @@ -0,0 +1,107 @@ +$width: 350px; + +.header-menu.notification-area { + max-width: $width; + padding: 0.75rem 1rem 1rem; + width: $width; + + .notifications-header { + align-items: center; + border-bottom: 1px solid $grey; + display: flex; + justify-content: space-between; + padding: 0 0 0.5rem; + + p { + color: $text-bold; + font-weight: 500; + margin: 0; + } + } + + .clear-all { + color: lighten($text, 15%); + display: inline-block; + margin-left: auto; + + &:hover { + text-decoration: underline; + } + } + + .button-tertiary { + margin: 0; + white-space: nowrap; + } + + .empty { + align-items: center; + display: flex; + flex-direction: column; + padding: 2.25rem 0; + + svg { + $size: 75px; + fill: darken($grey-dark, 15%); + height: $size; + margin-bottom: 0.5rem; + width: $size; + } + + p { + color: lighten($text, 20%); + margin: 0; + } + } +} + +.notification-list { + max-height: 300px; + overflow-y: auto; + + li { + margin-bottom: 1rem; + } + + .notification-body, + a.notification-link { + border-left: 2px solid $zurple-light; + color: $text; + display: inline-block; + margin-left: 9px; + padding: 0; + padding-left: 15px; + } + + .notification-header { + align-items: center; + color: $text-bold; + display: flex; + + svg { + height: 20px; + margin-right: 0.4rem; + width: 20px; + } + } +} + +.model-list.not-list { + .read { + align-items: center; + display: flex; + + svg { + fill: lighten($text, 15%); + height: 22px; + line-height: 1; + margin-right: 0.4rem; + vertical-align: middle; + width: 22px; + } + + &:hover svg { + fill: $zurple-light; + } + } +} diff --git a/app/assets/stylesheets/components/_password-banner.scss b/app/assets/stylesheets/components/_password-banner.scss new file mode 100644 index 0000000..ea991e5 --- /dev/null +++ b/app/assets/stylesheets/components/_password-banner.scss @@ -0,0 +1,21 @@ +.password-banner { + align-items: center; + background: $white; + border: 2px solid $grey; + border-radius: $border-radius; + display: flex; + justify-content: space-between; + margin: 0 0 4rem; + padding: 1.5rem; + + h2 { + line-height: 1; + margin: 0 0 0.5rem; + } + + p { + line-height: 1.5; + margin: 0; + max-width: 70ch; + } +} diff --git a/app/assets/stylesheets/components/_player.scss b/app/assets/stylesheets/components/_player.scss index 0d5b049..2194735 100644 --- a/app/assets/stylesheets/components/_player.scss +++ b/app/assets/stylesheets/components/_player.scss @@ -7,7 +7,7 @@ border: 1px solid $grey; border-radius: $border-radius; margin-top: 1.5rem; - padding: 1.5rem; + padding: 1.25rem; > *:first-child { margin-top: 0; @@ -17,9 +17,10 @@ margin-bottom: 0; } + ul { list-style-type: none; - margin: 1.5rem 0; + margin: 0.25rem 0; padding: 0; } @@ -33,16 +34,18 @@ .achievement { align-items: center; display: flex; - margin-top: 0.75rem; + margin: 1rem 0; width: 100%; .image { flex-shrink: 0; - margin-right: 1rem; + margin-right: 0.75rem; } p { + color: $text; font-size: 15px; + line-height: 1.5; margin: 0; overflow: hidden; text-overflow: ellipsis; @@ -61,6 +64,33 @@ margin: 0; text-align: center; } + + .tickets { + align-items: center; + background: $grey-lighter; + border-top: 1px solid $grey; + margin: 1.5rem -1.25rem -1.25rem; + padding: 1.25rem; + + p { + align-items: center; + display: flex; + line-height: 1; + margin: 0; + } + } + + svg { + fill: $grey-darker; + height: 22px; + margin-right: 0.5rem; + width: 22px; + } + + .highlight { + color: $text; + font-weight: 500; + } } @media (min-width: 980px) { diff --git a/app/assets/stylesheets/components/_wrapper.scss b/app/assets/stylesheets/components/_wrapper.scss index af395cf..2a7c5f6 100644 --- a/app/assets/stylesheets/components/_wrapper.scss +++ b/app/assets/stylesheets/components/_wrapper.scss @@ -19,6 +19,10 @@ &.split-header { margin-bottom: 2.8rem; } + + &.with-modal { + max-width: $modal-width; + } } @media (min-width: 980px) { diff --git a/app/assets/stylesheets/components/admin/_game-form.scss b/app/assets/stylesheets/components/admin/_game-form.scss index 741e92b..5b809f4 100644 --- a/app/assets/stylesheets/components/admin/_game-form.scss +++ b/app/assets/stylesheets/components/admin/_game-form.scss @@ -1,5 +1,5 @@ .admin form.game-form { - max-width: $modal-width + 50px; + max-width: $modal-width + 80px; .js-sortable { outline: none; diff --git a/app/assets/stylesheets/components/admin/_navigation.scss b/app/assets/stylesheets/components/admin/_navigation.scss index 5692b82..0bb7bb8 100644 --- a/app/assets/stylesheets/components/admin/_navigation.scss +++ b/app/assets/stylesheets/components/admin/_navigation.scss @@ -98,9 +98,9 @@ bottom: unset; display: block; left: unset; - position: sticky; + position: relative; right: unset; - top: 100px; + top: unset; width: 270px; z-index: 1; @@ -113,7 +113,7 @@ } a { - padding: 6px 1rem; + padding: 5px 1rem; } } diff --git a/app/assets/stylesheets/elements/_inputs.scss b/app/assets/stylesheets/elements/_inputs.scss index 927348c..a756600 100644 --- a/app/assets/stylesheets/elements/_inputs.scss +++ b/app/assets/stylesheets/elements/_inputs.scss @@ -32,10 +32,10 @@ textarea { border: 1px solid $grey-dark; border-radius: 2px; color: $grey-light-inverted; - line-height: 1.8; + line-height: 1.6; margin-top: 0.25rem; - min-height: 120px; + min-height: 96px; min-width: 200px; - padding: 10px 14px; + padding: 8px 12px; width: 100%; } diff --git a/app/assets/stylesheets/settings/_colours.scss b/app/assets/stylesheets/settings/_colours.scss index f33a992..53972a1 100644 --- a/app/assets/stylesheets/settings/_colours.scss +++ b/app/assets/stylesheets/settings/_colours.scss @@ -6,7 +6,7 @@ $zurple-inverted: hsl(278, 14%, 99%); $zurple-light: hsl(278, 51%, 61%); $zurple-light-inverted: hsl(278, 14%, 95%); $zurple-lighter: hsl(278, 51%, 71%); -$zurple-lighter-inverted: hsl(278, 14%, 95%); +$zurple-lighter-inverted: hsl(278, 14%, 98%); // Darcula $darcula-dark: hsl(270, 21%, 13%); @@ -51,5 +51,15 @@ $grey-light: hsl(278, 20%, 97%); $grey-light-inverted: hsl(278, 21%, 40%); $grey-lighter: hsl(278, 18%, 98%); +// Red +$red-dark: hsl(11, 57%, 52%); +$red-dark-inverted: hsl(11, 15%, 99%); +$red: hsl(11, 70%, 57%); +$red-inverted: hsl(11, 15%, 97%); +$red-light: hsl(11, 70%, 65%); +$red-light-inverted: hsl(11, 15%, 97%); +$red-lighter: hsl(11, 70%, 73%); +$red-lighter-inverted: hsl(11, 15%, 99%); + $transparent: rgba(0, 0, 0, 0); $white: #ffffff; diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 730dadd..9d0ed5d 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -# Application wide connection module ApplicationCable + # Application wide connection class Connection < ActionCable::Connection::Base end end diff --git a/app/controllers/achievements_controller.rb b/app/controllers/achievements_controller.rb index e0b57f0..0994fbc 100644 --- a/app/controllers/achievements_controller.rb +++ b/app/controllers/achievements_controller.rb @@ -4,7 +4,8 @@ class AchievementsController < ApplicationController # GET /player/:username/achievements def index - @player = Player.find_by! username: params[:player_username] + @player = Player.find_by!(username: params[:player_username]) + @achievements = @player.achievements.page(params[:page]) end # GET /player/:username/achievements/:id diff --git a/app/controllers/admin/achievements_controller.rb b/app/controllers/admin/achievements_controller.rb index 29d6a56..23698f0 100644 --- a/app/controllers/admin/achievements_controller.rb +++ b/app/controllers/admin/achievements_controller.rb @@ -5,30 +5,26 @@ # Controller for achievements in the admin scope class Admin::AchievementsController < ApplicationController layout 'admin' - - # noinspection RailsParamDefResolve before_action :authenticate_player! before_action :require_admin - # GET /admin/achievements/new + # GET /admin/players/:player_username/achievements/new def new - @achievement = Achievement.new + @player = Player.find_by!(username: params[:player_username]) + @achievement = @player.achievements.build end - # POST /admin/achievements + # POST /admin/players/:player_username/achievements def create - @achievement = Achievement.new achievement_params + @player = Player.find_by!(username: params[:player_username]) + @achievement = @player.achievements.build(achievement_params) if @achievement.save - flash[:success] = Struct::Flash.new( - t('admin.achievements.create.title'), - t('admin.achievements.create.body') - ) - redirect_to admin_players_path + record_action(:achievement, 'achievements.create', + achievement: @achievement.type, player: @player.username) + redirect_to admin_players_path, + notice: t('admin.achievements.create.flash') else - if params.key?(:achievement) && params[:achievement].key?(:player_id) - @player_name = Player.find(params[:achievement][:player_id]).full_name - end render 'new' end end @@ -36,6 +32,6 @@ def create private def achievement_params - params.require(:achievement).permit(:type, :player_id, :proof) + params.require(:achievement).permit(:type, :proof) end end diff --git a/app/controllers/admin/actions_controller.rb b/app/controllers/admin/actions_controller.rb new file mode 100644 index 0000000..0af4f48 --- /dev/null +++ b/app/controllers/admin/actions_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# A controller for viewing actions history +class Admin::ActionsController < ApplicationController + layout 'admin' + before_action :authenticate_player! + before_action :require_admin + + # GET /admin/actions + def index + @actions = Action.order(created_at: :desc).page(params[:page]) + end +end diff --git a/app/controllers/admin/events_controller.rb b/app/controllers/admin/events_controller.rb index 6e15c94..483b322 100644 --- a/app/controllers/admin/events_controller.rb +++ b/app/controllers/admin/events_controller.rb @@ -5,8 +5,6 @@ # A controller for events in the admin scope class Admin::EventsController < ApplicationController layout 'admin' - - # noinspection RailsParamDefResolve before_action :authenticate_player! before_action :require_admin @@ -33,6 +31,7 @@ def create end if @event.save + record_action(:event, 'events.create', event: @event.title) redirect_to admin_events_path, notice: t('admin.events.create.flash') else respond_to do |format| @@ -52,6 +51,7 @@ def update @event = Event.find params[:id] if @event.update(edit_event_params) + record_action(:event, 'events.update', event: @event.title) redirect_to admin_events_path, notice: t('admin.events.update.flash') else render 'edit' @@ -63,6 +63,7 @@ def destroy @event = Event.find params[:id] @event.destroy_from_date(params[:from]) + record_action(:event, 'events.destroy', event: @event.title) redirect_to admin_events_path, notice: t('admin.events.destroy.flash') end diff --git a/app/controllers/admin/games_controller.rb b/app/controllers/admin/games_controller.rb index bff7d05..b40eaf9 100644 --- a/app/controllers/admin/games_controller.rb +++ b/app/controllers/admin/games_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'flash_message' - # A controller for games in the admin scope class Admin::GamesController < ApplicationController layout 'admin' @@ -21,10 +19,13 @@ def new end # POST /admin/games + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize def create - @game = Game.new games_params + @game = Game.new(games_params) if @game.save + @game.award_tickets(params[:game][:tickets]) + record_action(:game, 'games.create', game: @game.id) redirect_to admin_games_path, notice: t('admin.games.create.flash') else respond_to do |format| @@ -33,17 +34,20 @@ def create end end end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize # GET /admin/games/:id/edit def edit - @game = Game.find params[:id] + @game = Game.find(params[:id]) end # POST /admin/games/:id + # rubocop:disable Metrics/AbcSize def update - @game = Game.find params[:id] + @game = Game.find(params[:id]) - if @game.update games_params + if @game.update(games_params) + record_action(:game, 'games.update', game: @game.id) redirect_to admin_games_path, notice: t('admin.games.update.flash') else respond_to do |format| @@ -52,12 +56,14 @@ def update end end end + # rubocop:enable Metrics/AbcSize # DELETE /admin/games/:id def destroy - @game = Game.find params[:id] + @game = Game.find(params[:id]) @game.destroy + record_action(:game, 'games.destroy', game: @game.id) redirect_to admin_games_path, notice: t('admin.games.destroy.flash') end diff --git a/app/controllers/admin/mail_controller.rb b/app/controllers/admin/mail_controller.rb index ff99bdb..e909e04 100644 --- a/app/controllers/admin/mail_controller.rb +++ b/app/controllers/admin/mail_controller.rb @@ -5,16 +5,16 @@ # A controller for mail in the admins cope class Admin::MailController < ApplicationController layout 'admin' - - # noinspection RailsParamDefResolve before_action :authenticate_player! before_action :require_admin # GET /admin/mail - def index; end + def index + respond_to :js + end # POST /admin/mail/players.csv - def show + def generate target = params.key?(:target) ? params[:target] : 'promotional' options = {} diff --git a/app/controllers/admin/players_controller.rb b/app/controllers/admin/players_controller.rb index 77e2d08..e535a72 100644 --- a/app/controllers/admin/players_controller.rb +++ b/app/controllers/admin/players_controller.rb @@ -5,14 +5,12 @@ # A controller for players in the admin scope class Admin::PlayersController < ApplicationController layout 'admin' - - # noinspection RailsParamDefResolve before_action :authenticate_player! before_action :require_admin # GET /admin/players def index - @players = Player.order(score: :desc).page params[:page] + @players = Player.order(score: :desc).page(params[:page]) end # GET /admin/players/:username @@ -26,13 +24,16 @@ def new end # POST /admin/players + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize def create player_params = new_params player_params[:password] = Devise.friendly_token(8) @player = Player.new(player_params) + @player.password_changed = false # noinspection RailsChecklist01 if @player.save + record_action(:player, 'players.create', player: @player.username) PlayerMailer.welcome(@player.id, player_params[:password]).deliver_later redirect_to admin_players_path, notice: t('admin.players.create.flash') else @@ -42,6 +43,7 @@ def create end end end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize # GET /admin/players/:username/edit def edit @@ -49,35 +51,41 @@ def edit end # PATCH /admin/players/:username + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize def update @player = Player.find_by!(username: params[:username]) if @player.update edit_params + record_action(:player, 'players.update', player: @player.username) redirect_to admin_players_path, notice: t('admin.players.update.flash') else # noinspection RubyResolve @player.username = @player.username_was if @player.username_changed? - render 'edit' + respond_to do |format| + format.html { render 'edit' } + format.js { render 'failure' } + end end end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize # DELETE /admin/players/:username def destroy @player = Player.find_by!(username: params[:username]) @player.destroy + record_action(:player, 'players.destroy', player: @player.username) redirect_to admin_players_path, notice: t('admin.players.destroy.flash') end private def new_params - params[:email] = nil if params.key?(:email) && params[:email].blank? - params.require(:player).permit(:first_name, :last_name, :email) + params.require(:player).permit(:first_name, :nickname, :last_name, :email) end def edit_params - params[:email] = nil if params.key?(:email) && params[:email].blank? - params.require(:player).permit(:username, :first_name, :last_name, :email) + params.require(:player).permit(:username, :nickname, :first_name, + :last_name, :email) end end diff --git a/app/controllers/admin/regions_controller.rb b/app/controllers/admin/regions_controller.rb index a63891d..0776dca 100644 --- a/app/controllers/admin/regions_controller.rb +++ b/app/controllers/admin/regions_controller.rb @@ -5,8 +5,6 @@ # A controller for regions in the admin scope class Admin::RegionsController < ApplicationController layout 'admin' - - # noinspection RailsParamDefResolve before_action :authenticate_player! before_action :require_admin @@ -25,6 +23,7 @@ def create @region = Region.new region_params if @region.save + record_action(:region, 'regions.create', region: @region.name) redirect_to admin_regions_path, notice: t('admin.regions.create.flash') else respond_to do |format| @@ -44,6 +43,7 @@ def update @region = Region.friendly.find params[:id] if @region.update region_params + record_action(:region, 'regions.update', region: @region.name) redirect_to admin_regions_path, notice: t('admin.regions.update.flash') else render 'edit' @@ -55,6 +55,7 @@ def destroy @region = Region.friendly.find params[:id] @region.destroy + record_action(:region, 'regions.destroy', region: @region.name) redirect_to admin_regions_path, notice: t('admin.regions.destroy.flash') end diff --git a/app/controllers/admin/tickets_controller.rb b/app/controllers/admin/tickets_controller.rb new file mode 100644 index 0000000..ac88e89 --- /dev/null +++ b/app/controllers/admin/tickets_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# A controller for managing tickets in the admin panel +class Admin::TicketsController < ApplicationController + layout 'admin' + respond_to :js + before_action :authenticate_player! + before_action :require_admin + + # GET /admin/players/:player_username/tickets + def edit + @player = Player.find_by!(username: params[:player_username]) + end + + # PATCH|PUT /admin/players/:player_username/tickets + def update + tickets = params[:tickets].to_i + @player = Player.find_by!(username: params[:player_username]) + @player.tickets += tickets + @player.save + record_action(:ticket, 'tickets.update', + tickets: tickets, player: @player.username) + redirect_to admin_players_path, notice: t('admin.tickets.update.flash') + end +end diff --git a/app/controllers/admin/venues_controller.rb b/app/controllers/admin/venues_controller.rb index f15cc17..c113c01 100644 --- a/app/controllers/admin/venues_controller.rb +++ b/app/controllers/admin/venues_controller.rb @@ -25,6 +25,7 @@ def create @venue = Venue.new venue_params if @venue.save + record_action(:venue, 'venues.create', venue: @venue.name) redirect_to admin_venues_path, notice: t('admin.venues.create.flash') else respond_to do |format| @@ -44,6 +45,7 @@ def update @venue = Venue.friendly.find params[:id] if @venue.update venue_params + record_action(:venue, 'venues.update', venue: @venue.name) redirect_to admin_venues_path, notice: t('admin.venues.update.flash') else render 'edit' @@ -55,6 +57,7 @@ def destroy @venue = Venue.friendly.find params[:id] @venue.destroy + record_action(:venue, 'venues.destroy', venue: @venue.name) redirect_to admin_venues_path, notice: t('admin.venues.destroy.flash') end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c8e8d28..18f2fa0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -19,4 +19,11 @@ def require_admin def after_sign_in_path_for(_resource) root_path end + + def record_action(action, translation, value_hash = {}) + Action.create( + player: current_player, action: action, + description: format(t("admin.#{translation}.action"), value_hash) + ) + end end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb new file mode 100644 index 0000000..59c1124 --- /dev/null +++ b/app/controllers/comments_controller.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# A controller for managing comments +class CommentsController < ApplicationController + before_action :authenticate_player! + respond_to :js + + # POST /games/:id/comments + def create + @game = Game.find(params[:game_id]) + @comment = Comment.new(comment_params) + + # Add relations + @comment.player = current_player + @comment.game = @game + + if @comment.save + @comment = @game.comments.build + render 'success' + else + render 'failure' + end + end + + # GET /games/:id/comments/:id + def edit + @game = Game.find(params[:game_id]) + @comment = @game.comments.where(deleted: false).find(params[:id]) + end + + # PATCH|PUT /games/:id/comments/:id + def update + @game = Game.find(params[:game_id]) + @comment = @game.comments.build + comment = current_player.comments.where(deleted: false).find(params[:id]) + + if comment.update(comment_params) + render 'edit_success' + else + render 'edit' + end + end + + # DELETE /games/:id/comments/:id + def destroy + @game = Game.find(params[:game_id]) + @comment = @game.comments.build + comment = @game.comments.where(deleted: false).find(params[:id]) + + if comment.player_id == current_player.id || current_player.admin + comment.update(deleted: true) + render 'destroy' + else + head :forbidden + end + end + + private + + def comment_params + params.require(:comment).permit(:body) + end +end diff --git a/app/controllers/games_controller.rb b/app/controllers/games_controller.rb index 13be1e5..f47b9e3 100644 --- a/app/controllers/games_controller.rb +++ b/app/controllers/games_controller.rb @@ -10,5 +10,10 @@ def index # GET /games/:id def show @game = Game.includes(:venue).find(params[:id]) + @comment = @game.comments.build + + respond_to do |format| + format.html + end end end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb new file mode 100644 index 0000000..b07c4d0 --- /dev/null +++ b/app/controllers/notifications_controller.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# A controller for notifications +class NotificationsController < ApplicationController + # GET /notifications + def index + @notifications = if request.format.html? + current_player.notifications.page(params[:page]) + else + current_player.unread_notifications.limit(15) + end + end + + # DELETE /notifications/:id + def destroy + @notification = current_player.notifications.find(params[:id]) + @notification.destroy + + respond_to do |format| + format.html { redirect_to notifications_path } + format.js + end + end + + # PATCH/PUT /notifications/:notification_id/mark-read + def mark_read + @notification = current_player.notifications.find(params[:notification_id]) + @notification.update(read: !@notification.read) + + respond_to do |format| + format.html { redirect_to notifications_path } + format.js { render 'success' } + end + end + + # PATCH/PUT /notifications + def clear + # rubocop:disable Rails/SkipsModelValidations + current_player.notifications.update_all(read: true) + # rubocop:enable Rails/SkipsModelValidations + head :ok, content_type: 'text/html' + end +end diff --git a/app/controllers/players/registrations_controller.rb b/app/controllers/players/registrations_controller.rb index fd09d60..33e4278 100644 --- a/app/controllers/players/registrations_controller.rb +++ b/app/controllers/players/registrations_controller.rb @@ -22,9 +22,12 @@ def create # end # PUT /resource - # def update - # super - # end + def update + super + return if current_player.password_changed + + current_player.update(password_changed: true) + end # DELETE /resource # def destroy diff --git a/app/helpers/admin/actions_helper.rb b/app/helpers/admin/actions_helper.rb new file mode 100644 index 0000000..e7a1524 --- /dev/null +++ b/app/helpers/admin/actions_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# A helper for the admin actions section +module Admin::ActionsHelper +end diff --git a/app/helpers/admin/tickets_helper.rb b/app/helpers/admin/tickets_helper.rb new file mode 100644 index 0000000..d80b394 --- /dev/null +++ b/app/helpers/admin/tickets_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# A helper for tickets in the admin panel +module Admin::TicketsHelper +end diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb new file mode 100644 index 0000000..a289024 --- /dev/null +++ b/app/helpers/comments_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# A helper for comments +module CommentsHelper +end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb new file mode 100644 index 0000000..d18f631 --- /dev/null +++ b/app/helpers/notifications_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# A helper for notifications +module NotificationsHelper +end diff --git a/app/javascript/packs/components/better_select.js b/app/javascript/packs/components/better_select.js index 818d71c..aa7e073 100644 --- a/app/javascript/packs/components/better_select.js +++ b/app/javascript/packs/components/better_select.js @@ -21,6 +21,31 @@ const getValueAsString = (select) => { return defaultValue; }; +/** + * An event handler, that should be fired whenever the + * trigger for a better select element is clicked. + * @param event Click event. + */ +const onSelectTriggerClick = (event) => { + // Buttons inside a form will submit their parent form when clicked => prevent default + event.preventDefault(); + const wrapper = event.target.parentNode; + + // Disable all selects + document.querySelectorAll(".better-select-wrapper[active]").forEach((select) => { + if (select !== wrapper) { + select.removeAttribute("active"); + } + }); + + // Toggle wrapper + if (wrapper.hasAttribute("active")) { + wrapper.removeAttribute("active"); + } else { + wrapper.setAttribute("active", ""); + } +}; + /** * Builds a custom select wrapper around an existing select element. * @param select Existing select element. @@ -60,31 +85,6 @@ const buildSelectWrapper = (select) => { select.parentNode.insertBefore(wrapper, select); }; -/** - * An event handler, that should be fired whenever the - * trigger for a better select element is clicked. - * @param event Click event. - */ -const onSelectTriggerClick = (event) => { - // Buttons inside a form will submit their parent form when clicked => prevent default - event.preventDefault(); - const wrapper = event.target.parentNode; - - // Disable all selects - document.querySelectorAll(".better-select-wrapper[active]").forEach((select) => { - if (select !== wrapper) { - select.removeAttribute("active"); - } - }); - - // Toggle wrapper - if (wrapper.hasAttribute("active")) { - wrapper.removeAttribute("active"); - } else { - wrapper.setAttribute("active", ""); - } -}; - /** * An event handler, that should be fired whenever an * option for a better select element is clicked. diff --git a/app/javascript/packs/game_editor.js b/app/javascript/packs/game_editor.js index 2dd7af8..896f1b9 100644 --- a/app/javascript/packs/game_editor.js +++ b/app/javascript/packs/game_editor.js @@ -30,7 +30,7 @@ function constructPlayerElement(player) { const element = document.createElement("li"); element.setAttribute("data-player-id", player.id); element.innerHTML = ``; - element.appendChild(player.asElement()); + element.appendChild(player.asElement(1)); // Construct remove button const button = document.createElement("button"); @@ -254,7 +254,6 @@ function onSubmitForm(event) { // Get fields const positionField = player.querySelector(".js-position-field"); const destroyField = player.querySelector(".js-destroy-field"); - console.log(player); // Set if (destroyField.value === true || destroyField.value === "true") { diff --git a/app/javascript/packs/navigation.js b/app/javascript/packs/navigation.js index 881fac5..80cf4f4 100644 --- a/app/javascript/packs/navigation.js +++ b/app/javascript/packs/navigation.js @@ -92,6 +92,7 @@ const init = () => { menuOverlay = document.querySelector(".header-menu-overlay"); document.querySelectorAll(".header-menu-wrapper").forEach(bindToWrapperEvents); + // Register events for movile admin navigation trigger const adminNavTrigger = document.querySelector(".mobile-admin-navigation-trigger"); if (adminNavTrigger !== null) { adminNavTrigger.addEventListener("click", () => { @@ -99,6 +100,14 @@ const init = () => { nav.setAttribute("active", ""); }); } + + // Register events for notification clear button + const clearButton = document.querySelector(".notification-area .clear-all"); + if (clearButton !== null) { + clearButton.addEventListener("click", () => { + disableMenu(); + }); + } }; -addEventListener("turbolinks:load", init); \ No newline at end of file +addEventListener("turbolinks:load", init); diff --git a/app/javascript/packs/player/player.js b/app/javascript/packs/player/player.js index 7c44967..0051c9a 100644 --- a/app/javascript/packs/player/player.js +++ b/app/javascript/packs/player/player.js @@ -27,9 +27,10 @@ export class Player { /** * An element representation of this player. + * @param position Player position (optional) * @returns {HTMLElement} Element containing information about this player. */ - asElement() { + asElement(positon = null) { // Init const player = document.createElement("div"); const nameSpan = document.createElement("span"); diff --git a/app/javascript/packs/polyfill/includes.js b/app/javascript/packs/polyfill/includes.js index 96e8366..a147278 100644 --- a/app/javascript/packs/polyfill/includes.js +++ b/app/javascript/packs/polyfill/includes.js @@ -2,7 +2,7 @@ if (!Array.prototype.includes) { Object.defineProperty(Array.prototype, "includes", { - value: function(searchElement, n = 0) { + value: (searchElement, n = 0) => { if (this === null) { throw new TypeError("\"this\" is null or undefined."); } diff --git a/app/models/action.rb b/app/models/action.rb new file mode 100644 index 0000000..68446e0 --- /dev/null +++ b/app/models/action.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Represents a single administrative action in the admin panel +class Action < ApplicationRecord + belongs_to :player + + enum action: %i[player game event venue region achievement ticket comment] + + with_options presence: true do + validates :action + validates :description, length: { minimum: 3 } + end +end diff --git a/app/models/comment.rb b/app/models/comment.rb new file mode 100644 index 0000000..015357a --- /dev/null +++ b/app/models/comment.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# A comment on a game +class Comment < ApplicationRecord + default_scope { order(created_at: :asc) } + + after_create :send_notifications + + belongs_to :game + belongs_to :player + + validates :body, presence: true, length: { within: 3..440 } + validates :deleted, inclusion: { in: [true, false] } + + protected + + def send_notifications + CommentNotificationsWorker.perform_async(id) + end +end diff --git a/app/models/game.rb b/app/models/game.rb index 48552b3..aeab74b 100644 --- a/app/models/game.rb +++ b/app/models/game.rb @@ -2,9 +2,6 @@ # Represents a single game recorded by tournament directors class Game < ApplicationRecord - belongs_to :venue - belongs_to :season - default_scope { order(id: :desc) } searchkick callbacks: :async @@ -12,20 +9,22 @@ class Game < ApplicationRecord after_save :update_stats, :update_ranks after_destroy :update_stats, :update_ranks - has_many :games_players, - class_name: 'GamesPlayer', - dependent: :delete_all, - inverse_of: :game + belongs_to :venue + belongs_to :season + has_many :players, through: :games_players - has_many :referees, dependent: :delete_all, inverse_of: :game has_many :players, through: :referees - accepts_nested_attributes_for :games_players, - reject_if: :all_blank, - allow_destroy: true - accepts_nested_attributes_for :referees, - reject_if: :all_blank, - allow_destroy: true + with_options dependent: :delete_all, inverse_of: :game do + has_many :games_players, class_name: 'GamesPlayer' + has_many :referees + has_many :comments + end + + with_options reject_if: :all_blank, allow_destroy: true do + accepts_nested_attributes_for :games_players + accepts_nested_attributes_for :referees + end validates :venue, :season, :played_on, presence: true validate :player_count, :referee_count, :no_duplicate_players, @@ -51,6 +50,10 @@ def game_played_by Player.joins(:games_players).where(games_players: { game_id: id }) end + def award_tickets(tickets) + GameTicketsWorker.perform_async(id, tickets) unless tickets.nil? + end + private def player_count diff --git a/app/models/games_player.rb b/app/models/games_player.rb index 6b7977c..0e86d51 100644 --- a/app/models/games_player.rb +++ b/app/models/games_player.rb @@ -15,6 +15,7 @@ class GamesPlayer < ApplicationRecord before_save :calc_score after_save :update_stats after_destroy :update_stats + after_create :send_create_notification with_options presence: true do validates :game, :player @@ -46,4 +47,8 @@ def calc_score def update_stats CalculatePlayerStatsWorker.perform_async(player.id) end + + def send_create_notification + GameNotificationWorker.perform_in(10.seconds, id, player.id) + end end diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 0000000..04cd339 --- /dev/null +++ b/app/models/notification.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# A single notification item. +class Notification < ApplicationRecord + default_scope { order(created_at: :desc) } + + enum icon: %i[game comment] + belongs_to :player + + validates :message, presence: true, length: { within: 5..200 } +end diff --git a/app/models/player.rb b/app/models/player.rb index 678ec5e..5ce9936 100644 --- a/app/models/player.rb +++ b/app/models/player.rb @@ -4,6 +4,7 @@ require 'username_lib' # Represents a single player +# rubocop:disable Metrics/ClassLength class Player < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable @@ -12,81 +13,103 @@ class Player < ApplicationRecord searchkick callbacks: :async, word_start: %i[full_name username] + # Active record callbacks before_validation :gen_username, on: :create + # Relationships with_options dependent: :nullify, inverse_of: :player do - has_many :games_players, class_name: 'GamesPlayer' - has_many :players_venues, class_name: 'PlayersVenue' + has_many :games_players, class_name: 'GamesPlayer' + has_many :players_venues, class_name: 'PlayersVenue' has_many :players_regions, class_name: 'PlayersRegion' has_many :players_seasons, class_name: 'PlayersSeason' + has_many :comments + has_many :actions end - has_many :games, through: :games_players has_many :referees, dependent: :nullify - has_many :games, through: :referees - has_many :venues, through: :players_venues - has_many :regions, through: :players_regions - has_many :seasons, through: :players_seasons - has_many :achievements, dependent: :destroy + has_many :games, through: :games_players + has_many :games, through: :referees + has_many :venues, through: :players_venues + has_many :regions, through: :players_regions + has_many :seasons, through: :players_seasons + + with_options dependent: :destroy do + has_many :achievements + has_many :notifications + end attr_writer :login + # Validation with_options presence: true do validates :username, uniqueness: { case_sensitive: false }, length: { minimum: 2 }, format: { with: /\A[a-z0-9-]*\z/, - message: 'may use numbers, letters, underscores (_), and hyphens (-)' - } - validates :first_name, :last_name, - length: { maximum: 64 }, - format: { - with: /\A[a-zA-Z][a-zA-Z-]*[a-zA-Z]\z/, - message: 'may use letters and hyphens (-)' + message: 'may use numbers, letters, underscores (_), and '\ + 'hyphens (-)' } validates :notify_promotional, :notify_events end - validates :score, :games_played, :games_won, - numericality: { - only_integer: true, - greater_than_or_equal_to: 0 - } + with_options length: { maximum: 64 }, + format: { + with: /\A[A-Z][a-zA-Z-]*[a-z]\z/, + message: 'may use letters and hyphens (-), must start with an'\ + ' uppercase letter' + } do + validates :first_name, :last_name, presence: true + validates :nickname, allow_nil: true, allow_blank: true + end + + validates :score, :games_played, :games_won, :tickets, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } - validates :email, - format: { with: URI::MailTo::EMAIL_REGEXP }, - allow_nil: true, - allow_blank: true, - uniqueness: true + validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, + allow_nil: true, allow_blank: true, uniqueness: true + + # Since validation by presence doesn't work for booleans + validates :password_changed, :developer, :admin, + inclusion: { in: [true, false] } def to_param username end + # Defines searchkick data def search_data { full_name: full_name, username: "@#{username}", - is_admin: admin?, - is_developer: developer? + is_admin: admin, + is_developer: developer } end + # Returns a human readable form of the players full name def full_name - "#{first_name} #{last_name}" + if nickname.nil? + "#{first_name} #{last_name}" + else + "#{first_name} '#{nickname}' #{last_name}" + end end def login @login || username || email end + # noinspection RubyClassMethodNamingConvention def self.find_for_database_authentication(warden_conditions) conditions = warden_conditions.dup if (login = conditions.delete(:login)) - where(conditions.to_h).where(['lower(username) = :value OR lower(email) = :value', { value: login.downcase }]).first + where(conditions.to_h).find_by([ + 'lower(username) = :value OR lower(email) = :value', + { value: login.downcase } + ]) elsif conditions.key?(:username) || conditions.key?(:email) - where(conditions.to_h).first + find_by(conditions.to_h) end end @@ -130,7 +153,7 @@ def self.to_csv def recent_games GamesPlayer.includes(game: [:venue]) .where(player: self) - .reorder(created_at: :desc).limit(25) + .reorder(created_at: :desc) end def season_player @@ -144,4 +167,34 @@ def self.recent(days = 30) def self.admins Player.where(admin: true).or(Player.where(developer: true)) end + + def unread_notifications + notifications.where(read: false) + end + + # Define custom setters which make blank attributes nil + %w[nickname email].each do |attribute| + define_method "#{attribute}=" do |value| + value = value.presence unless value.nil? + super(value) + end + end + + # Define custom setters which automatically titleize + %w[first_name last_name].each do |attribute| + define_method "#{attribute}=" do |value| + # We need to be sure we only capitalize the first char + if value.instance_of?(String) && value.present? + value = value[0].capitalize + value.slice(1..-1) + end + + super(value) + end + end + + def tickets=(value) + value = 0 if value.negative? + super(value) + end end +# rubocop:enable Metrics/ClassLength diff --git a/app/models/venue.rb b/app/models/venue.rb index 28b5600..af8ed12 100644 --- a/app/models/venue.rb +++ b/app/models/venue.rb @@ -14,6 +14,7 @@ class Venue < ApplicationRecord inverse_of: :venue has_many :players, through: :players_venues belongs_to :region + has_many :games, dependent: :nullify enum state: %i[ACT NSW NT QLD SA TAS VIC WA] diff --git a/app/views/achievements/_achievement.html.erb b/app/views/achievements/_achievement.html.erb new file mode 100644 index 0000000..094f920 --- /dev/null +++ b/app/views/achievements/_achievement.html.erb @@ -0,0 +1,12 @@ +
A banner prompting players to change their password if it was automatically generated. See <%= link_to '#139', 'https://github.com/sean0x42/riverrats.com.au/pull/139', class: 'anchor' %>.
+Tickets! Players can now be awarded tickets by administrators. See <%= link_to '#126', 'https://github.com/sean0x42/riverrats.com.au/pull/126', class: 'anchor' %>.
+Admin actions are now recorded, and can be reviewed by other admins. See <%= link_to '#126', 'https://github.com/sean0x42/riverrats.com.au/pull/126', class: 'anchor' %>.
+Players can now comment on games. See <%= link_to '#112', 'https://github.com/sean0x42/riverrats.com.au/pull/112', class: 'anchor' %>.
+You'll now receive notifications for various things, such as playing in a game. See <%= link_to '#101', 'https://github.com/sean0x42/riverrats.com.au/pull/101', class: 'anchor' %>.
+Admins can now give players nicknames. See <%= link_to '#100', 'https://github.com/sean0x42/riverrats.com.au/pull/100', class: 'anchor' %>.
+Improved achievement views. See <%= link_to '#137', 'https://github.com/sean0x42/riverrats.com.au/pull/137', class: 'anchor' %>.
+Error pages (<%= link_to '404', '/404', class: 'anchor', target: '_blank' %>, <%= link_to '422', '/422', class: 'anchor', target: '_blank' %>, <%= link_to '500', '/500', class: 'anchor', target: '_blank' %>) have had a facelift. They should be more meaningful now. See <%= link_to '#138', 'https://github.com/sean0x42/riverrats.com.au/pull/138', class: 'anchor' %>.
+Some emails have had an aesthetic rework. See <%= link_to '#136', 'https://github.com/sean0x42/riverrats.com.au/pull/136', class: 'anchor' %>.
+Fixed a bug with logging in, signing up, and editing account settings. See <%= link_to '#133', 'https://github.com/sean0x42/riverrats.com.au/pull/133', class: 'anchor' %> and <%= link_to '#139', 'https://github.com/sean0x42/riverrats.com.au/pull/139', class: 'anchor' %>.
+Player's names are now capitalised appropriately. See <%= link_to '#15', 'https://github.com/sean0x42/riverrats.com.au/issues/15', class: 'anchor' %>.
+Fixed an issue preventing players from resetting their passwords #105.
+Fixed an issue preventing players from resetting their passwords <%= link_to '#105', 'https://github.com/sean0x42/riverrats.com.au/issues/105', class: 'anchor' %>.
Nice! You're all caught up.
+Welcome <%= @player.first_name %>,
-We've just created an account for you over - at <%= link_to 'riverrats.com.au', root_url, style: 'color: #9154ab;' %>. Logging in is easy. simply use the - following username and password:
+Welcome <%= @player.first_name %>,
+Someone just created an account for you at <%= link_to 'riverrats.com.au', root_url %>.
-Your username: - <%= @player.username %> -
-Your password: - <%= @password %> -
+Your username: + <%= @player.username %> +
- <%= link_to 'Click here to log in', new_player_session_url, style: 'color: #fdfcfd; background: linear-gradient(90deg, #9154ab, #ab54a8);line-height: 1.1; border-radius: 3px;text-decoration: none;padding: 16px 26px;margin: 0;display: inline-block;' %> +Your password: + <%= @password %> +
-We strongly recommend you <%= link_to 'update your password, by clicking this link', edit_player_registration_url, style: 'color: #ab7dbf;' %>.
-<%= page_entries_info @players %>
Warning! This action cannot be undone. Unhappy? Cancel your account here. We'll be sad to see - you go.
+ you go.Sorry, this section is under construction. Please check back later.
+ <%= link_to 'View all achievements', player_achievements_path(@player), class: 'button-tertiary' %> + ++ + <%= number_format @player.tickets %> <%= 'ticket'.pluralize(@player.tickets) %> +
+Sorry, but we couldn't find that page. You may have mistyped the address, or the page may have been moved.
- -