diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8693b440..237c2da0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,6 @@ --- name: main -on: +on: push: branches: [ "master" ] pull_request: @@ -35,6 +35,12 @@ jobs: - name: Run rubocop run: bin/rubocop + - name: Run slim linter + run: bin/slim-lint app/views + + - name: Run scss linter + run: bin/scss-lint app/assets/stylesheets + - name: Run tests run: bin/rspec ... diff --git a/.scss-lint.yml b/.scss-lint.yml new file mode 100644 index 00000000..7c4dc324 --- /dev/null +++ b/.scss-lint.yml @@ -0,0 +1,12 @@ +scss_files: 'app/assets/stylesheets/**/*.scss' + +linters: + BorderZero: + enabled: false + + Indentation: + severity: warning + width: 2 + + PropertySortOrder: + enabled: false diff --git a/.slim-lint.yml b/.slim-lint.yml new file mode 100644 index 00000000..c07642fa --- /dev/null +++ b/.slim-lint.yml @@ -0,0 +1,36 @@ +linters: + LineLength: + max: 120 + + RedundantDiv: + enabled: false + + RuboCop: + enabled: true + # These cops are incredibly noisy since the Ruby we extract from Slim + # templates isn't well-formatted, so we ignore them. + ignored_cops: + - Layout/ArgumentAlignment + - Layout/HashAlignment + - Layout/IndentationWidth + - Layout/LineLength + - Layout/TrailingEmptyLines + - Lint/BlockAlignment + - Lint/EndAlignment + - Lint/Void + - Metrics/BlockLength + - Style/AlignParameters + - Style/BlockNesting + - Style/FileName + - Style/FirstParameterIndentation + - Style/FrozenStringLiteralComment + - Style/IfUnlessModifier + - Style/IndentationConsistency + - Style/IndentationWidth + - Style/Next + - Style/TrailingBlankLines + - Style/TrailingWhitespace + - Style/WhileUntilModifier + - Style/NestedTernaryOperator + - Style/StringLiterals + - Style/QuotedSymbols diff --git a/Gemfile b/Gemfile index 8a834768..77dbeb04 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,9 @@ gem "pg", "~> 1.1" gem "puma", "~> 5.0" # Use SCSS for stylesheets gem "sass-rails", ">= 6" +gem "scss_lint" +gem "slim_lint" +gem "slim-rails" # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker gem "webpacker", "~> 5.0" # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks diff --git a/Gemfile.lock b/Gemfile.lock index f1e1f7a8..88266a8a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -278,6 +278,11 @@ GEM ruby-next-core (0.15.3) ruby-progressbar (1.13.0) rubyzip (2.3.2) + sass (3.7.4) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) @@ -288,6 +293,8 @@ GEM sprockets (> 3.0) sprockets-rails tilt + scss_lint (0.60.0) + sass (~> 3.5, >= 3.5.5) selenium-webdriver (4.9.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) @@ -298,6 +305,16 @@ GEM connection_pool (>= 2.3.0) rack (>= 2.2.4) redis-client (>= 0.14.0) + slim (5.2.0) + temple (~> 0.10.0) + tilt (>= 2.1.0) + slim-rails (3.6.3) + actionpack (>= 3.1) + railties (>= 3.1) + slim (>= 3.0, < 6.0, != 5.0.0) + slim_lint (0.24.0) + rubocop (>= 1.0, < 2.0) + slim (>= 3.0, < 6.0) spring (4.1.1) sprockets (4.2.1) concurrent-ruby (~> 1.0) @@ -306,6 +323,7 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) + temple (0.10.3) thor (1.2.2) tilt (2.3.0) timeout (0.4.0) @@ -373,8 +391,11 @@ DEPENDENCIES rubocop-rake rubocop-thread_safety sass-rails (>= 6) + scss_lint selenium-webdriver (>= 4.0.0.rc1) sidekiq + slim-rails + slim_lint spring turbolinks (~> 5) tzinfo-data diff --git a/app/assets/stylesheets/_variables.scss b/app/assets/stylesheets/_variables.scss new file mode 100644 index 00000000..0febcab2 --- /dev/null +++ b/app/assets/stylesheets/_variables.scss @@ -0,0 +1,7 @@ +$black: #222; +$white: #fff; +$red: #f00; +$green: #008000; +$grey: #ccc; + +$header-bg: #f1f1f1; diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index b81139eb..3216ebd4 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1,30 +1,12 @@ -.center { - text-align: center; -} -.record { - border: 1px solid #000000; - padding: 10px; - margin-bottom: 10px; -} -.flash-alert { - color: red; -} +// Bootstrap +@import 'bootstrap/scss/bootstrap'; -.flash-notice { - color: green; -} +@import 'variables'; +@import 'reset'; -.sort_links { - place-items: center; - display: flex; - margin-left: auto; - margin-right: auto; -} - -.link { - font-family: "Courier New"; - margin-left: auto; - margin-right: auto; -} +// Custom styles +@import 'base'; +@import 'header'; +@import 'form'; diff --git a/app/assets/stylesheets/base.scss b/app/assets/stylesheets/base.scss new file mode 100644 index 00000000..3114d113 --- /dev/null +++ b/app/assets/stylesheets/base.scss @@ -0,0 +1,45 @@ +.flash-alert { + color: $red; +} + +.flash-notice { + color: $green; +} + +.sort-links { + display: flex; + gap: 1rem; + justify-content: flex-end; + margin: 1rem 0; + + font-size: .75rem; +} + +.link { + margin-left: auto; + margin-right: auto; +} + +.content { + width: 100%; + padding: 1rem 2rem; +} + +.title { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.table-actions { + display: flex; + justify-content: end; + flex-wrap: wrap; + gap: 1rem; + width: 100%; +} + +.cursor-pointer { + cursor: pointer; +} diff --git a/app/assets/stylesheets/form.scss b/app/assets/stylesheets/form.scss new file mode 100644 index 00000000..8b451e36 --- /dev/null +++ b/app/assets/stylesheets/form.scss @@ -0,0 +1,15 @@ +.form { + display: flex; + flex-direction: column; + gap: 1rem; + max-width: 40rem; + + &__field { + width: 100%; + } + + &__actions { + display: flex; + justify-content: end; + } +} diff --git a/app/assets/stylesheets/header.scss b/app/assets/stylesheets/header.scss new file mode 100644 index 00000000..a9957c3d --- /dev/null +++ b/app/assets/stylesheets/header.scss @@ -0,0 +1,10 @@ +.header { + display: flex; + gap: 1rem; + justify-content: flex-end; + align-items: center; + + padding: 2rem; + background: $header-bg; + border-bottom: 1px solid $grey; +} diff --git a/app/assets/stylesheets/reset.scss b/app/assets/stylesheets/reset.scss new file mode 100644 index 00000000..d04e713a --- /dev/null +++ b/app/assets/stylesheets/reset.scss @@ -0,0 +1,40 @@ +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font-family: Trebuchet MS,Lucida Grande,Lucida Sans Unicode,Lucida Sans,Tahoma,sans-serif; + vertical-align: baseline; +} + +footer, +header, +menu, +nav, +section { + display: block; +} + +body { + line-height: 1; +} + +ol, +ul { + list-style: none; +} + +blockquote, +q { + quotes: none; + + &::before, + &::after { + content: ''; + } +} diff --git a/app/javascript/components/script.js b/app/javascript/components/script.js new file mode 100644 index 00000000..228047d6 --- /dev/null +++ b/app/javascript/components/script.js @@ -0,0 +1,44 @@ +import _ from 'lodash'; + +class MyFunc { + constructor($el) { + this.$el = $el; + this.list = $el.querySelector('.js-projects-list'); + this.items = this.$el.querySelectorAll('.js-item'); + + this.sortAscButton = this.$el.querySelector('.js-sort-by-title-asc'); + this.sortDescButton = this.$el.querySelector('.js-sort-by-title-desc'); + + this.formattedItems = Array.from(this.items).map((item) => ({ + id: JSON.parse(item.getAttribute('data-id')), + name: item.getAttribute('data-name'), + node: item, + })); + + this.bindEvents(this.formattedItems); + } + + bindEvents() { + this.sortAscButton.addEventListener('click', this.onSortByTitleASC); + this.sortDescButton.addEventListener('click', this.onSortByTitleDESC); + } + + onSortByTitleASC = () => { + const sorteItemsByTitle = _.orderBy(this.formattedItems, ['name'], ['asc']); + + sorteItemsByTitle.forEach((item) => this.list.appendChild(item.node)); + this.sortAscButton.classList.add('d-none'); + this.sortDescButton.classList.remove('d-none'); + } + + onSortByTitleDESC = () => { + const sorteItemsByTitle = _.orderBy(this.formattedItems, ['name'], ['desc']); + + sorteItemsByTitle.forEach((item) => this.list.appendChild(item.node)); + + this.sortDescButton.classList.add('d-none'); + this.sortAscButton.classList.remove('d-none'); + } +} + +export default MyFunc; diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index f710851a..6c92f38b 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -8,6 +8,16 @@ import Turbolinks from "turbolinks" import * as ActiveStorage from "@rails/activestorage" import "channels" +import "bootstrap" + +import MyFunc from 'components/script' + Rails.start() Turbolinks.start() ActiveStorage.start() + +window.addEventListener("load", (event) => { + Array.from(document.querySelectorAll('.js-projects')).forEach(($el) => { + new MyFunc($el); + }); +}); diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb deleted file mode 100644 index 7f62610a..00000000 --- a/app/views/layouts/application.html.erb +++ /dev/null @@ -1,29 +0,0 @@ - - - - TaskTrackerItis - - <%= csrf_meta_tags %> - <%= csp_meta_tag %> - - <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> - <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> - -
- <% if current_user %> - <%= link_to "#{current_user.first_name} #{current_user.last_name || current_user.email}", sessions_path %> - <%= button_to "Log out", logout_path, method: :delete %> - <% else %> - <%= link_to "Log in", login_path %> - <%= link_to "Sign up", register_path %> - <% end %> -
- - <% flash.each do |type, msg| %> -
-

<%= msg %>

-
- <% end %> - <%= yield %> - - diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim new file mode 100644 index 00000000..8f63e304 --- /dev/null +++ b/app/views/layouts/application.html.slim @@ -0,0 +1,26 @@ +doctype html +html + head + title Task Tracker Itis + meta name="viewport" content="width=device-width,initial-scale=1" + = csrf_meta_tags + = csp_meta_tag + + = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' + = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload', 'data-turbolinks-eval': 'false' + + body + - flash.each do |type, msg| + div class='flash-#{type}' + p = msg + + header.header + - if current_user + = link_to "#{current_user.first_name} #{current_user.last_name || current_user.email}", sessions_path + = button_to "Log out", logout_path, method: :delete, class: "btn btn-secondary" + - else + = link_to "Log in", login_path + = link_to "Sign up", register_path + + main.content + = yield diff --git a/app/views/projects/_form.html.erb b/app/views/projects/_form.html.erb deleted file mode 100644 index 3142463b..00000000 --- a/app/views/projects/_form.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -<%= form_with(model: project) do |form| %> - <% if project.errors.any? %> -
-

<%= pluralize(project.errors.count, "error") %> prohibited this project from being saved:

- - -
- <% end %> - -
- <%= form.label :name %> - <%= form.text_field :name %> -
- -
- <%= form.label :description %> - <%= form.text_area :description %> -
- -
- <%= form.submit %> -
-<% end %> - -<%= link_to "Back", projects_path %> diff --git a/app/views/projects/_form.html.slim b/app/views/projects/_form.html.slim new file mode 100644 index 00000000..1ddb98fe --- /dev/null +++ b/app/views/projects/_form.html.slim @@ -0,0 +1,23 @@ += form_with(model: project, html: { class: "form" }) do |form| + - if project.errors.any? + #error_explanation + h2 + => pluralize(project.errors.count, "error") + | prohibited this project from being saved: + + ul + - project.errors.each do |error| + li = error.full_message + + .form__field + = form.label :name, class: "form-label" + = form.text_field :name, class: "form-control" + + .form__field + = form.label :description, class: "form-label" + = form.text_area :description, class: "form-control" + + .form__actions + = form.submit "Save", class: "btn btn-primary" + += link_to "Back", projects_path diff --git a/app/views/projects/edit.html.erb b/app/views/projects/edit.html.erb deleted file mode 100644 index 4a5f68bc..00000000 --- a/app/views/projects/edit.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -

Edit project

- -<%= render 'form', project: @project %> diff --git a/app/views/projects/edit.html.slim b/app/views/projects/edit.html.slim new file mode 100644 index 00000000..c3e4cd35 --- /dev/null +++ b/app/views/projects/edit.html.slim @@ -0,0 +1,3 @@ +h1 Edit project + += render 'form', project: @project diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb deleted file mode 100644 index 824df7e1..00000000 --- a/app/views/projects/index.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -

Hello world

- -

<%= link_to "New Project", new_project_path %>

- -<%= link_to "Name", :sort => "name asc"%> -<%= link_to "Name reverse", :sort => "name desc"%> -<%= link_to "Description", :sort => "description asc" %> -<%= link_to "Description reverse", :sort => "description desc" %> -
-
- -<% @projects.each do |project| %> -
-

<%= project.id %>. <%= link_to project.name, project_tasks_path(project) %>

-

<%= project.description %>

-

<%= link_to "Edit", edit_project_path(project) %>

-

<%= button_to "Destroy", project, method: :delete, data: {confirm: "Are you sure?"} %>

-
-<% end %> - -<%= paginate @projects %> diff --git a/app/views/projects/index.html.slim b/app/views/projects/index.html.slim new file mode 100644 index 00000000..fe94be8f --- /dev/null +++ b/app/views/projects/index.html.slim @@ -0,0 +1,37 @@ +.js-projects + h2.title + span Projects Page + = link_to "New Project", new_project_path, class: "btn btn-primary" + + .sort-links + = link_to "Name", sort: "name asc" + = link_to "Name reverse", sort: "name desc" + = link_to "Description", sort: "description asc" + = link_to "Description reverse", sort: "description desc" + + table.table.table-striped.align-middle + thead + tr + th + ' Name + span.cursor-pointer.js-sort-by-title-asc ↑ + span.cursor-pointer.js-sort-by-title-desc.d-none ↓ + th Description + th.text-end Actions + tbody.js-projects-list + - @projects.each do |project| + tr.js-item[data-id=project.id data-name="#{project.name}"] + td + => project.id + ' . + = link_to project.name, project_tasks_path(project) + td + = project.description + + td + .table-actions + = link_to "Edit", edit_project_path(project), class: "btn btn-warning btn-sm" + = button_to "Destroy", project, method: :delete, + data: { confirm: "Are you sure?" }, class: "btn btn-danger btn-sm" + + = paginate @projects diff --git a/app/views/projects/new.html.erb b/app/views/projects/new.html.erb deleted file mode 100644 index 5a94b6ac..00000000 --- a/app/views/projects/new.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -

NEW project

- -<%= render 'form', project: @project %> diff --git a/app/views/projects/new.html.slim b/app/views/projects/new.html.slim new file mode 100644 index 00000000..aa37bab7 --- /dev/null +++ b/app/views/projects/new.html.slim @@ -0,0 +1,3 @@ +h1 New project + += render 'form', project: @project diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb deleted file mode 100644 index 94472545..00000000 --- a/app/views/projects/show.html.erb +++ /dev/null @@ -1,45 +0,0 @@ -

<%= @project.name %>

- -

Description: <%= @project.description %>

- -<%= link_to "New Task", new_project_task_path(@project), class: "btn btn-primary" %> -<%= link_to "Back", projects_path %> - -

Tasks for <%= @project.name %>

- -<%= link_to "Name", :sort => "name asc" %> -<%= link_to "Name reverse", project_path(@project, sort: "name desc") %> -<%= link_to "Created date", project_path(@project, sort: "created_at asc") %> -<%= link_to "Created date reverse", project_path(@project, sort: "created_at desc") %> -<%= link_to "Deadline", project_path(@project, sort: "deadline_at asc") %> -<%= link_to "Deadline reverse", project_path(@project, sort: "deadline_at desc") %> -
-
- - - - - - - - - - - - - <% @project.tasks.each do |task| %> - - - - - - - - <% end %> - -
NameDescriptionStatusDeadlineActions
<%= task.name %><%= task.description %><%= task.status ? "Completed" : "Not Completed" %><%= task.deadline_at %> - <%= link_to "Show", project_task_path(@project, task), class: "btn btn-primary btn-sm" %> - <%= link_to "Edit", edit_project_task_path(@project, task), class: "btn btn-warning btn-sm" %> - <%= link_to "Destroy", project_task_path(@project, task), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-danger btn-sm" %> -
-<%= paginate @tasks %> \ No newline at end of file diff --git a/app/views/projects/show.html.slim b/app/views/projects/show.html.slim new file mode 100644 index 00000000..7191959d --- /dev/null +++ b/app/views/projects/show.html.slim @@ -0,0 +1,47 @@ +.mb-3 + = link_to "Back", projects_path + +h1.title + span = @project.name + = link_to "New Task", new_project_task_path(@project), class: "btn btn-primary" + +p + strong> Description: + = @project.description + +h2 + ' Tasks for + = @project.name + +.sort-links + = link_to "Name", sort: "name asc" + = link_to "Name reverse", project_path(@project, sort: "name desc") + = link_to "Created date", project_path(@project, sort: "created_at asc") + = link_to "Created date reverse", project_path(@project, sort: "created_at desc") + = link_to "Deadline", project_path(@project, sort: "deadline_at asc") + = link_to "Deadline reverse", project_path(@project, sort: "deadline_at desc") + +table.table.table-striped.align-middle + thead + tr + th Name + th Description + th Status + th Deadline + th.text-end Actions + + tbody + - @project.tasks.each do |task| + tr + td = task.name + td = task.description + td = task.status ? "Completed" : "Not Completed" + td = task.deadline_at + td + .table-actions + = link_to "Show", project_task_path(@project, task), class: "btn btn-primary btn-sm" + = link_to "Edit", edit_project_task_path(@project, task), class: "btn btn-warning btn-sm" + = link_to "Destroy", project_task_path(@project, task), + method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-danger btn-sm" + += paginate @tasks diff --git a/app/views/sessions/_form.html.erb b/app/views/sessions/_form.html.erb deleted file mode 100644 index 4a706264..00000000 --- a/app/views/sessions/_form.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -<%= form_with(model: @user, url: login_path) do |form| %> - <% if @user.errors.any? %> -
-

<%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:

- - -
- <% end %> - -
- <%= form.label :email %> - <%= form.text_field :email %> -
- -
- <%= form.label :password %> - <%= form.text_field :password, type: :password %> -
- -
- <%= form.submit "Log in" %> -
-<% end %> diff --git a/app/views/sessions/_form.html.slim b/app/views/sessions/_form.html.slim new file mode 100644 index 00000000..09726361 --- /dev/null +++ b/app/views/sessions/_form.html.slim @@ -0,0 +1,21 @@ += form_with(model: @user, url: login_path, html: { class: "form" }) do |form| + - if @user.errors.any? + #error_explanation + h2 + => pluralize(@user.errors.count, "error") + | prohibited this user from being saved: + + ul.form__errors + - @user.errors.each do |error| + li = error.full_message + + .form__field + = form.label :email + = form.text_field :email + + .form__field + = form.label :password + = form.text_field :password, type: :password + + .form__actions + = form.submit "Log in", class: "btn" diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb deleted file mode 100644 index 3da971d0..00000000 --- a/app/views/sessions/new.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -

Log in

- -<%= render 'form', user: @user %> - -<%= link_to "Back", root_path %> diff --git a/app/views/sessions/new.html.slim b/app/views/sessions/new.html.slim new file mode 100644 index 00000000..b8f90b13 --- /dev/null +++ b/app/views/sessions/new.html.slim @@ -0,0 +1,6 @@ +h1 + | Log in + += render 'form', user: @user + += link_to 'Back', root_path diff --git a/app/views/sessions/show.html.erb b/app/views/sessions/show.html.erb deleted file mode 100644 index 7c2f81ed..00000000 --- a/app/views/sessions/show.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -

Your profile info

- -

- First Name: - <%= current_user.first_name %> -

- -

- Last Name: - <%= current_user.last_name %> -

- -

- Email: - <%= current_user.email %> -

- -<%= link_to "Go to Projects", projects_path %> diff --git a/app/views/sessions/show.html.slim b/app/views/sessions/show.html.slim new file mode 100644 index 00000000..918762b5 --- /dev/null +++ b/app/views/sessions/show.html.slim @@ -0,0 +1,15 @@ +h1 Your profile info + +p + strong First Name: + span = current_user.first_name + +p + strong Last Name: + span = current_user.last_name + +p + strong Email: + span = current_user.email + += link_to "Go to Projects", projects_path diff --git a/app/views/tasks/index.html.erb b/app/views/tasks/index.html.erb index 4f336b59..eb3784c9 100644 --- a/app/views/tasks/index.html.erb +++ b/app/views/tasks/index.html.erb @@ -2,7 +2,7 @@ <%= link_to "NEW TASK", new_project_task_path(@project) %> -