Skip to content

Commit

Permalink
Feature: Sortable Solutions
Browse files Browse the repository at this point in the history
Because:
- Being able to sort solutions will give users the ability to view solutions in different ways. Currently we only display solutions in order of most liked, which has resulted in the most liked solutions being all that a majority of our users ever see.

This commit:
- Adds a reusable sort component
- Add the ability for users to sort solutions by newest, oldest and most liked
- Includes redesigned UI for project and lesson pages
  - The user solution is now the only solution visible on project pages and moved into the lessons two column layout
  - The lesson complete/navigation buttons have also been moved into the lessons two column layout.
- Adds a dedicated user solution component instead of using the project submission item component for both the current user and other user submissions.

---

-
  • Loading branch information
KevinMulhern committed Jan 13, 2024
1 parent 76ec533 commit b84f48e
Show file tree
Hide file tree
Showing 49 changed files with 484 additions and 367 deletions.
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ Layout/EmptyLinesAroundAttributeAccessor:
Layout/SpaceAroundMethodCallOperator:
Enabled: true

Layout/MultilineMethodCallIndentation:
Enabled: true
EnforcedStyle: indented

Lint/AmbiguousBlockAssociation:
Enabled: false

Expand Down
3 changes: 3 additions & 0 deletions app/assets/images/icons/arrows-up-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions app/assets/images/icons/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions app/assets/images/icons/plus-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 6 additions & 18 deletions app/components/project_submissions/item_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<div id="<%= dom_id(project_submission) %>" data-id="<%= project_submission.id %>" data-test-id="submission-item" data-sort-target="item" data-sort-code="<%= sort_code %>">
<div class="relative py-6 border-solid border-t border-gray-300 flex flex-col md:flex-row justify-between md:items-center">
<div id="<%= dom_id(project_submission) %>" data-test-id="submission-item">
<div class="relative py-6 flex flex-col md:flex-row justify-between md:items-center">

<div class="flex items-center mb-4 md:mb-0">
<%= render ProjectSubmissions::LikeComponent.new(project_submission:, current_users_submission: current_users_submission?) %>
<%= render ProjectSubmissions::LikeComponent.new(project_submission:, current_users_submission: false) %>
<%= title %>
</div>

Expand All @@ -29,21 +29,9 @@
data-transition-leave-end="transform opacity-10 scale-95"
class="hidden absolute right-0 z-10 mt-2 w-32 origin-top-right rounded-md bg-white dark:bg-gray-700 py-2 shadow-lg ring-1 ring-gray-900/5 dark:ring-gray-300/5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="options-menu-0-button" tabindex="-1">

<% if project_submission.user == current_user %>
<%= link_to edit_path, class: 'text-gray-700 dark:text-gray-300 group flex items-center px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-gray-200', role: 'menuitem', tabindex: '-1', data: { turbo_frame: 'modal', test_id: 'edit-submission', action: 'click->visibility#off'} do %>
<%= inline_svg_tag 'icons/pencil-square.svg', class: 'mr-3 h-4 w-4 text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300', aria: true, title: 'edit', desc: 'edit icon' %>
Edit
<% end %>

<%= link_to lesson_project_submission_path(project_submission.lesson, project_submission), class: 'text-gray-700 dark:text-gray-300 group flex items-center px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-gray-200', role: 'menuitem', tabindex: '-1', data: { turbo_method: :delete, turbo_confirm: 'Are you sure? this cannot be undone.', test_id: 'delete-submission' } do %>
<%= inline_svg_tag 'icons/trash.svg', class: 'mr-3 h-4 w-4 text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300', aria: true, title: 'edit', desc: 'edit icon' %>
Delete
<% end %>
<% else %>
<%= link_to new_project_submission_flag_path(project_submission), class: 'text-gray-700 dark:text-gray-300 group flex items-center px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-gray-200', role: 'menuitem', tabindex: '-1', data: { turbo_frame: 'modal', test_id: 'report-submission', action: 'click->visibility#off' } do %>
<%= inline_svg_tag 'icons/flag.svg', class: 'mr-3 h-4 w-4 text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300', aria: true, title: 'edit', desc: 'edit icon' %>
Report
<% end %>
<%= link_to new_project_submission_flag_path(project_submission), class: 'text-gray-700 dark:text-gray-300 group flex items-center px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-gray-200', role: 'menuitem', tabindex: '-1', data: { turbo_frame: 'modal', test_id: 'report-submission', action: 'click->visibility#off' } do %>
<%= inline_svg_tag 'icons/flag.svg', class: 'mr-3 h-4 w-4 text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300', aria: true, title: 'edit', desc: 'edit icon' %>
Report
<% end %>
</div>
</div>
Expand Down
24 changes: 2 additions & 22 deletions app/components/project_submissions/item_component.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
module ProjectSubmissions
class ItemComponent < ApplicationComponent
CURRENT_USER_SORT_CODE = 10_000_000 # current user's submission should always be first

with_collection_parameter :project_submission
renders_one :title, ProjectSubmissions::TitleComponent

def initialize(project_submission:, current_user:, edit_path: nil)
def initialize(project_submission:)
@project_submission = project_submission
@current_user = current_user
@edit_path = edit_path
end

def render?
Expand All @@ -17,22 +13,6 @@ def render?

private

attr_reader :project_submission, :current_user

def current_users_submission?
project_submission.user == current_user
end

def sort_code
return CURRENT_USER_SORT_CODE if current_users_submission?

project_submission.likes_count
end

def edit_path
return @edit_path if @edit_path.present?

edit_lesson_project_submission_path(project_submission.lesson, project_submission)
end
attr_reader :project_submission
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<div id="<%= dom_id(project_submission) %>" data-test-id="submission-item">
<div class="relative py-6 flex flex-col md:flex-row justify-between md:items-center">

<div class="flex items-center mb-4 md:mb-0">
<%= render ProjectSubmissions::LikeComponent.new(project_submission:, current_users_submission: true) %>
<%= title %>
</div>

<div class="flex flex-row md:items-center">
<%= link_to 'View code', project_submission.repo_url, target: '_blank', rel: 'noreferrer', class: 'button button--gray font-semibold mr-4', data: { test_id: 'view-code-btn' } %>

<% if project_submission.lesson.previewable? && project_submission.live_preview_url? %>
<%= link_to 'Live preview', project_submission.live_preview_url, target: '_blank', rel: 'noreferrer', class: 'button button--gray font-semibold mr-4', data: { test_id: 'live-preview-btn' } %>
<% end %>

<div class="flex-none absolute top-7 right-0 md:relative md:top-auto md:right-auto" data-controller="visibility" data-action="visibility:click:outside->visibility#off" data-visibility-visible-value="false">
<button type="button" data-action="click->visibility#toggle" data-test-id="submission-action-menu-btn" class="-m-2.5 block p-2.5 text-gray-500 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100" id="options-menu-0-button" aria-expanded="false" aria-haspopup="true">
<span class="sr-only">Open options</span>
<%= inline_svg_tag 'icons/ellipsis-vertical.svg', aria: true, title: 'open menu', desc: 'open menu icon' %>
</button>

<div
data-visibility-target="content"
data-transition-enter="transition ease-out duration-200"
data-transition-enter-start="transform opacity-0 scale-95"
data-transition-enter-end="transform opacity-100 scale-100"
data-transition-leave="transition ease-in duration-75"
data-transition-leave-start="transform opacity-100 scale-100"
data-transition-leave-end="transform opacity-10 scale-95"
class="hidden absolute right-0 z-10 mt-2 w-32 origin-top-right rounded-md bg-white dark:bg-gray-700 py-2 shadow-lg ring-1 ring-gray-900/5 dark:ring-gray-300/5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="options-menu-0-button" tabindex="-1">

<%= link_to edit_lesson_project_submission_path(project_submission.lesson, project_submission), class: 'text-gray-700 dark:text-gray-300 group flex items-center px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-gray-200', role: 'menuitem', tabindex: '-1', data: { turbo_frame: 'modal', test_id: 'edit-submission', action: 'click->visibility#off'} do %>
<%= inline_svg_tag 'icons/pencil-square.svg', class: 'mr-3 h-4 w-4 text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300', aria: true, title: 'edit', desc: 'edit icon' %>
Edit
<% end %>

<%= link_to lesson_project_submission_path(project_submission.lesson, project_submission), class: 'text-gray-700 dark:text-gray-300 group flex items-center px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-gray-200', role: 'menuitem', tabindex: '-1', data: { turbo_method: :delete, turbo_confirm: 'Are you sure? this cannot be undone.', test_id: 'delete-submission' } do %>
<%= inline_svg_tag 'icons/trash.svg', class: 'mr-3 h-4 w-4 text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300', aria: true, title: 'edit', desc: 'edit icon' %>
Delete
<% end %>
</div>
</div>
</div>
</div>
</div>
18 changes: 18 additions & 0 deletions app/components/project_submissions/user_solution_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module ProjectSubmissions
class UserSolutionComponent < ApplicationComponent
with_collection_parameter :project_submission
renders_one :title, ProjectSubmissions::TitleComponent

def initialize(project_submission:)
@project_submission = project_submission
end

def render?
project_submission.present?
end

private

attr_reader :project_submission
end
end
35 changes: 35 additions & 0 deletions app/components/sort_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<div data-controller="visibility" data-action="visibility:click:outside->visibility#off" class="flex items-center space-x-2 w-full">
<label id="listbox-label" class="block w-full text-sm font-medium leading-6 text-gray-900 dark:text-gray-200 text-right">Sort by</label>
<div class="relative w-full">
<button type="button" data-action="click->visibility#toggle" data-test-id="sort-select" class="relative w-full cursor-default rounded-md bg-white dark:bg-gray-700 py-1.5 pl-3 pr-10 text-left text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-600 dark:focus:ring-gray-500 sm:text-sm sm:leading-6" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label">
<span class="block text-sm"><%= selected_option.fetch(:label) %></span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<%= inline_svg_tag 'icons/arrows-up-down.svg', class: 'h-5 w-5 text-gray-400 dark:text-gray-300', aria: true, title: 'Sort' %>
</span>
</button>

<ul
data-visibility-target="content"
data-transition-enter=""
data-transition-enter-start=""
data-transition-enter-end=""
data-transition-leave="transition ease-in duration-100"
data-transition-leave-start="opacity-100"
data-transition-leave-end="opacity-0"
class="absolute hidden z-10 mt-1 max-h-60 w-full min-w-max rounded-md bg-white dark:bg-gray-700 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" tabindex="-1" role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-option-3">

<% options.each do |option| %>
<li class="text-gray-900 dark:text-gray-200 relative cursor-default select-none py-2 pl-8 pr-4" id="listbox-option-0" role="option">
<%= link_to request.params.merge(sort: option.fetch(:value), direction: option.fetch(:direction)), role: 'menuitem', tabindex: '-1', data: tag.attributes(data: data_attributes) do %>
<span class="font-normal text-sm block <%= 'font-semibold' if selected?(option) %>"><%= option.fetch(:label) %> </span>
<% end %>
<% if selected?(option) %>
<span class="text-gray-800 dark:text-gray-300 absolute inset-y-0 left-0 flex items-center pl-2">
<%= inline_svg_tag 'icons/check.svg', class: 'h-4 w-4', aria: true, title: 'Selected option' %>
</span>
<% end %>
</li>
<% end %>
</ul>
</div>
</div>
25 changes: 25 additions & 0 deletions app/components/sort_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class SortComponent < ApplicationComponent
def initialize(options:, selected: {}, data_attributes: {})
@options = options
@selected = selected
@data_attributes = data_attributes
end

def selected_option
options.find { |option| selected?(option) } || default_option
end

def default_option
options.find { |option| option[:default] }
end

private

attr_reader :options, :selected, :data_attributes

def selected?(option)
return option[:default] if selected.compact.empty?

option[:value] == selected[:value] && option[:direction] == selected[:direction]
end
end
2 changes: 1 addition & 1 deletion app/components/theme/switcher_component.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
link:
base: 'text-gray-600 group flex items-center dark:text-gray-300'
base: 'text-gray-700 group flex items-center text-sm dark:text-gray-300'
default: 'py-2 px-3'
mobile: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 text-base font-medium py-2 px-2 dark:hover:bg-gray-700/60 dark:hover:text-gray-200'
icon_only: 'py-2 px-3'
Expand Down
17 changes: 17 additions & 0 deletions app/controllers/concerns/sortable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Sortable
extend ActiveSupport::Concern

included do
helper_method :sort_column, :sort_direction
end

private

def sort_column(klass)
params[:sort].presence_in(klass.sortable_columns) || 'created_at'
end

def sort_direction(default: 'asc')
params[:direction].presence_in(%w[asc desc]) || default
end
end
21 changes: 12 additions & 9 deletions app/controllers/lessons/project_submissions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
module Lessons
class ProjectSubmissionsController < ApplicationController
include Sortable

before_action :authenticate_user!
before_action :set_lesson
before_action :can_add_solution

def index
@current_user_submission = current_user.project_submissions.find_by(lesson: @lesson)
@pagy, @project_submissions = pagy_array(project_submissions_query, items: params.fetch(:limit, 15))
project_submissions = @lesson
.project_submissions
.only_public
.includes(:user)
.sort_by_params(params[:sort], params[:direction] || 'desc')

@pagy, @project_submissions = pagy_array(project_submissions, items: params.fetch(:limit, 15))
mark_liked_project_submissions
end

def new
Expand Down Expand Up @@ -56,15 +64,10 @@ def destroy

private

def project_submissions_query
@project_submissions_query ||= ::LessonProjectSubmissionsQuery.new(
lesson: @lesson,
current_user:
)

def mark_liked_project_submissions
ProjectSubmissions::MarkLiked.call(
user: current_user,
project_submissions: @project_submissions_query.public_submissions
project_submissions: @project_submissions
)
end

Expand Down
1 change: 1 addition & 0 deletions app/controllers/lessons_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ def show
@lesson = Lesson.find(params[:id])

if user_signed_in?
@project_submission = current_user.project_submissions.find_by(lesson: @lesson)
Courses::MarkCompletedLessons.call(user: current_user, lessons: Array(@lesson))
end
end
Expand Down
27 changes: 0 additions & 27 deletions app/controllers/users/project_submissions_controller.rb

This file was deleted.

43 changes: 0 additions & 43 deletions app/javascript/controllers/sort_controller.js

This file was deleted.

9 changes: 9 additions & 0 deletions app/models/project_submission.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ class ProjectSubmission < ApplicationRecord
scope :created_today, -> { where('created_at >= ?', Time.zone.now.beginning_of_day) }
scope :discardable, -> { not_removed_by_admin.where(discard_at: ..Time.zone.now) }

def self.sort_by_params(column, direction = 'desc')
sortable_column = column.presence_in(sortable_columns) || 'created_at'
order(sortable_column => direction)
end

def self.sortable_columns
%w[created_at likes_count]
end

private

def live_preview_allowed
Expand Down
Loading

0 comments on commit b84f48e

Please sign in to comment.