From 4eae05788b04e770a19a122b18be2c564fbd5ec3 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 26 Aug 2026 19:55:32 +0800 Subject: [PATCH 01/53] feat: started refactoring courses/show --- ProPro Design/course_show.html.erb | 258 ++++ ProPro Design/grouping_settings.html.erb | 404 +++++++ ProPro Design/progress_updates.html.erb | 422 +++++++ ProPro Design/projects_form.html.erb | 223 ++++ ProPro Design/projects_show.html.erb | 362 ++++++ .../projects_show_responsive.html.erb | 204 ++++ .../projects_version_comparison.html.erb | 315 +++++ ProPro Design/style_guide.html.erb | 1036 +++++++++++++++++ app/controllers/courses_controller.rb | 15 + app/helpers/sidebar_helper.rb | 7 - app/javascript/controllers/tabs_controller.js | 39 + app/models/project.rb | 10 + .../courses/_coordinator_section.html.erb | 256 ---- app/views/courses/_lecturer_section.html.erb | 168 --- app/views/courses/_people_tab.html.erb | 60 + .../courses/_project_details_tab.html.erb | 34 + app/views/courses/_sidebar.html.erb | 67 -- app/views/courses/_student_section.html.erb | 61 - .../courses/_supervised_projects_tab.html.erb | 7 + app/views/courses/_to_review_tab.html.erb | 44 + .../courses/_topic_directory_tab.html.erb | 96 ++ app/views/courses/show.html.erb | 194 ++- app/views/layouts/application.html.erb | 50 +- app/views/participants/index.html.erb | 13 - .../projects/_proposal_list_item.html.erb | 36 + app/views/shared/_header.html.erb | 27 + app/views/shared/_sidebar.html.erb | 54 + app/views/shared/_topbar.html.erb | 11 - app/views/topics/index.html.erb | 2 - config/breadcrumbs.rb | 11 - test/controllers/courses_controller_test.rb | 78 ++ test/system/courses/course_tabs_test.rb | 103 ++ 32 files changed, 3907 insertions(+), 760 deletions(-) create mode 100644 ProPro Design/course_show.html.erb create mode 100644 ProPro Design/grouping_settings.html.erb create mode 100644 ProPro Design/progress_updates.html.erb create mode 100644 ProPro Design/projects_form.html.erb create mode 100644 ProPro Design/projects_show.html.erb create mode 100644 ProPro Design/projects_show_responsive.html.erb create mode 100644 ProPro Design/projects_version_comparison.html.erb create mode 100644 ProPro Design/style_guide.html.erb create mode 100644 app/javascript/controllers/tabs_controller.js delete mode 100644 app/views/courses/_coordinator_section.html.erb delete mode 100644 app/views/courses/_lecturer_section.html.erb create mode 100644 app/views/courses/_people_tab.html.erb create mode 100644 app/views/courses/_project_details_tab.html.erb delete mode 100644 app/views/courses/_sidebar.html.erb delete mode 100644 app/views/courses/_student_section.html.erb create mode 100644 app/views/courses/_supervised_projects_tab.html.erb create mode 100644 app/views/courses/_to_review_tab.html.erb create mode 100644 app/views/courses/_topic_directory_tab.html.erb create mode 100644 app/views/projects/_proposal_list_item.html.erb create mode 100644 app/views/shared/_header.html.erb create mode 100644 app/views/shared/_sidebar.html.erb delete mode 100644 app/views/shared/_topbar.html.erb create mode 100644 test/controllers/courses_controller_test.rb create mode 100644 test/system/courses/course_tabs_test.rb diff --git a/ProPro Design/course_show.html.erb b/ProPro Design/course_show.html.erb new file mode 100644 index 00000000..9e221c4e --- /dev/null +++ b/ProPro Design/course_show.html.erb @@ -0,0 +1,258 @@ +<% content_for :hide_toggler, true %> +<% content_for :no_sidebar, true %> +<% content_for :hide_breadcrumbs, true %> +<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> + + + + +
+ + +
+
+ + ProPro + chevron_right + Grouped Topic Approval Enabled +
+ +
+ +
+ + + + + +
+ + + + +
+ + +
+
+

Pending Proposals

+ 4 awaiting action +
+ +
+ +
+
+
+ description +
+
+

EduPulse

+

Group 1 • Submitted 2 hours ago

+
+
+
+ Pending + +
+
+ +
+
+
+ description +
+
+

CLOCK 101

+

Group 4 • Submitted 5 hours ago

+
+
+
+ Pending + +
+
+ +
+
+
+ description +
+
+

MMU Study Buddy

+

Group 7 • Submitted 1 day ago

+
+
+
+ Pending + +
+
+ +
+
+
+ description +
+
+

Farmer Sim

+

Group 11 • Submitted 2 days ago

+
+
+
+ Pending + +
+
+ +
+
+ + +
+
+

Reviewed Proposals

+ 4 proposals +
+ +
+ +
+
+
+ description +
+
+

MMU Foodie

+

Group 2 • Returned for revision yesterday

+
+
+
+ Redo + +
+
+ +
+
+
+ description +
+
+

Chameloeon Hunt

+

Group 5 • Returned for revision 2 days ago

+
+
+
+ Redo + +
+
+ +
+
+
+ description +
+
+

IMprove

+

Group 9 • Rejected last week

+
+
+
+ Rejected + +
+
+ +
+
+
+ description +
+
+

Save the MEow

+

Group 14 • Rejected last month

+
+
+
+ Rejected + +
+
+ +
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/ProPro Design/grouping_settings.html.erb b/ProPro Design/grouping_settings.html.erb new file mode 100644 index 00000000..081575f1 --- /dev/null +++ b/ProPro Design/grouping_settings.html.erb @@ -0,0 +1,404 @@ +<% content_for :stylesheets do %> + <%= stylesheet_link_tag "tailwind", "new_tailwind" %> +<% end %> +<% breadcrumb :course_settings, @course %> + +<% content_for :title, "Course Settings - #{@course.course_name.truncate(TITLE_NAME_LIMIT)} | ProPro" %> + + +
+ + <%= form_with url: handle_settings_course_path(@course), model: @course, method: :post, id: "course-settings-form", class: "flex flex-col min-h-screen" do |form| %> + + +
+
+ <%= link_to course_path(@course), class: "p-2 rounded-full hover:bg-gray-100 text-gray-600 transition-colors" do %> + + + + <% end %> +

Class settings

+
+
+ <%= form.submit "Save", class: "bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium text-sm px-6 py-2 rounded-full cursor-pointer transition-colors" %> +
+
+ + +
+ +
+ <%= render "courses/flash" %> +
+ + <% if @course.errors.any? %> +
+
+
+

+ <%= pluralize(@course.errors.count, "error") %> prohibited saving: +

+
    + <% @course.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+
+
+ <% end %> + + <%= render "copy_course_overlay", course: @course, mode: "settings" %> + + +
+
+

Class Details

+ +
+ <%= form.label :course_name, "Class name", class: "block text-xs font-medium text-gray-600" %> + <%= form.text_field :course_name, required: true, class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm mt-1" %> +
+ +
+ <%= form.label :course_description, "Project Details", class: "block text-xs font-medium text-gray-600" %> + <%= form.text_area :course_description, rows: 2, class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm mt-1 resize-none overflow-hidden", data: { controller: "textarea-resize", action: "input->textarea-resize#resize" } %> +
+ +
+ <%= form.label :file_link, "Project File Link (URL)", class: "block text-xs font-medium text-gray-600" %> + <%= form.text_field :file_link, class: "w-full bg-transparent border-none p-0 focus:ring-0 text-blue-600 text-sm mt-1 underline" %> +
+ +
+ <%= form.label :starting_week, "Starting Week", class: "block text-xs font-medium text-gray-600" %> + <%= form.text_field :starting_week, required: true, class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm mt-1" %> +
+
+
+ + +
+
+

General

+ + +
+

Coursecode

+ <%= render "course_code_form", course: @course %> +
+ + +
+

Supervisor Capacity

+ +
+
+ <%= form.label :supervisor_projects_limit, "Maximum Projects per Supervisor", class: "block text-xs font-medium text-gray-600" %> + <%= form.hidden_field :supervisor_projects_limit %> + <%= form.number_field :supervisor_projects_limit, id: "supervisor_projects_limit_visible", required: true, disabled: @course.supervisor_auto_calculate_enabled?, class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm mt-1" %> +
+ + + <%= render "supervisor_capacity_settings", form: form %> +
+
+
+
+ + +
+
+

Permissions and Rules

+ +
+ +
+ Allow lecturers to add topics for students to base their proposals on. +
+ + +
+
+ +
+ Lecturer topics require approval? +
+ + +
+
+ +
+ Auto-approve unchanged copied topics? +
+ + +
+
+ +
+ Use progress updates? +
+ + +
+
+ +
+ Lecturer views public? +
+ + +
+
+ +
+ Number of progress updates +
+ <%= form.number_field :number_of_updates, class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm text-center" %> +
+
+ +
+ +
+ Student can see projects from: +
+ <% Course.student_accesses.keys.map { |key| [key, key.to_s.humanize] }.each_with_index do |(value, label_text), idx| %> +
+ <%= form.radio_button :student_access, value, id: "student_access_#{idx}", class: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" %> + <%= form.label :student_access, label_text, value: value, for: "student_access_#{idx}", class: "ml-3 block text-sm text-gray-800 cursor-pointer" %> +
+ <% end %> +
+
+
+
+ + +
+
+ +
+
+

Student Self-Grouping Beta

+

Group formation in ProPro is still in progress!

+
+
+ + +
+ Allow students to form their own groups? +
+ + +
+
+ + +
+ + +
+ + Group Configuration + + + +
+ Student list mode +

+ Choose whether the coordinator knows the exact final count of students. +

+ +
+ + + + + <% if @course.persisted? && @course.grouping_enabled? && !@course.student_list_finalised? %> +
+
+
+ Final student list + + Fixed list mode cannot be enabled once default mode has been saved. + +
+
+ <% else %> + + <% end %> +
+
+ + +
+
+ <%= form.label :group_min, "Minimum group size", class: "block text-xs font-medium text-gray-600" %> + <%= form.number_field :group_min, min: 1, placeholder: "e.g. 3", + class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm mt-1", + data: { grouping_settings_target: "minMax", action: "input->grouping-settings#clearPreview" } %> +
+ +
+ <%= form.label :group_max, "Maximum group size", class: "block text-xs font-medium text-gray-600" %> + <%= form.number_field :group_max, min: 1, placeholder: "e.g. 4", + class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm mt-1", + data: { grouping_settings_target: "minMax", action: "input->grouping-settings#clearPreview" } %> +
+
+ + +
+
+

Preview group distribution

+

+ Enter an expected student count to see how legal groups would form. Actual numbers will come from enrolled students. +

+
+
+
+ +
+ students +
+
+
+
+ + +
+ + Student Access + + + +
+
+ Allow student grouping now + + Students can join, leave, and form groups only when this is on. + +
+
+ + +
+
+ + +
+
+
+ Auto-close window (optional) + + Optionally set specific dates to open and close student access automatically. + +
+ +
+ +
+
+ <%= form.label :grouping_opens_at, "Open from", class: "block text-xs font-medium text-gray-600" %> + <%= form.datetime_local_field :grouping_opens_at, + class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm mt-1", + value: @course.grouping_opens_at&.strftime('%Y-%m-%dT%H:%M') %> +
+
+ <%= form.label :grouping_closes_at, "Close at", class: "block text-xs font-medium text-gray-600" %> + <%= form.datetime_local_field :grouping_closes_at, + class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm mt-1", + value: @course.grouping_closes_at&.strftime('%Y-%m-%dT%H:%M') %> +
+
+
+ +
+ +
+
+
+ + +
+
+ <%= link_to "Add Students", add_students_course_path(@course), class: "w-full sm:w-auto inline-flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %> + <%= link_to "Add Lecturers", add_lecturers_course_path(@course), class: "w-full sm:w-auto inline-flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %> +
+ + <%= link_to "Delete Course", course_path(@course), class: "w-full sm:w-auto inline-flex justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors", data: { turbo_method: :delete, turbo_confirm: "Are you sure? This cannot be undone." } %> +
+ +
+ <% end %> +
\ No newline at end of file diff --git a/ProPro Design/progress_updates.html.erb b/ProPro Design/progress_updates.html.erb new file mode 100644 index 00000000..cd498bcf --- /dev/null +++ b/ProPro Design/progress_updates.html.erb @@ -0,0 +1,422 @@ +<% content_for :hide_toggler, true %> +<% content_for :no_sidebar, true %> +<% content_for :hide_breadcrumbs, true %> +<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> + + + + + + + + + + +
+ + +
+
+ + ProPro + chevron_right + Grouped Topic Approval Enabled +
+
+ +
+
+ + +
+ + + + + +
+ + +
+ + +
+
+
+
+ Approved + 02:57 PM, 25 Aug 2026 +
+

Pineapple das

+
+
+
+ + + + + +
+ + + +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ L1 +
+ + +
+ + +
+
+

+ Extra 1 +

+

+ calendar_today + 2026-09-01 +

+
+ +
+ + + Excellent + + + +
+ + + + +
+
+
+ + +
+
Feedback
+
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. This is a very robust update on the database architecture.

+
+ + +
+ Last updated: 25-08-2026 +
+
+ +
+
+ + +
+ + +
+ L1 +
+ + +
+ + +
+
+

+ Progress 2 +

+

+ calendar_today + 2026-08-20 +

+
+ +
+ + + Satisfactory + + + +
+ + + + +
+
+
+ + +
+
Feedback
+
+

Vestibulum at neque quam, sit amet mattis tellus sollicitudin vitae. Nam maximus mauris in ligula condimentum, tincidunt viverra dolor posuere. Duis lectus mi, egestas a dictum.

+
+ + +
+ Last updated: 20-08-2026 +
+
+ +
+
+ + +
+ + +
+ L1 +
+ + +
+ + +
+
+

+ Progress 1 +

+

+ calendar_today + 2026-08-10 +

+
+ +
+ + + Unsatisfactory + + + +
+ + + + +
+
+
+ + +
+
Feedback
+
+

The wireframes provided lack basic navigation flows. Please revise these before moving to high fidelity.

+
+ + +
+ Last updated: 10-08-2026 +
+
+ +
+
+ +
+
+
+
+ + +
+ +
+

Comments

+ 1 +
+ +
+
+ Comments for V1 +
+ + +
+
L1
+
+
+ Test Lecturer 1 + 01:15 AM +
+

This comment was deleted

+
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+ + + + + + + +
\ No newline at end of file diff --git a/ProPro Design/projects_form.html.erb b/ProPro Design/projects_form.html.erb new file mode 100644 index 00000000..74b0ea39 --- /dev/null +++ b/ProPro Design/projects_form.html.erb @@ -0,0 +1,223 @@ +<% content_for :hide_toggler, true %> +<% content_for :no_sidebar, true %> +<% content_for :hide_breadcrumbs, true %> +<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> + + + + + + + + +
+ + +
+ + +
+ + close + + +
+ +
+

Edit Proposal — Pineapple das

+ Pending +
+
+ + +
+ + +
+
+ + +
+ +
+ + +
+
+

Proposal Method

+

Select how your proposal is submitted for approval.

+
+ + +
+ + +
+
+
+
+ person +
+ +
+ check +
+
+

Propose to Lecturer

+

Submit your original project proposal directly to a supervisor.

+
+ + +
+
Assigned Supervisor
+
+
+
L1
+
+
Test Lecturer 1
+
2/3 approved · 1 pending
+
+
+ +
+
+
+ + +
+
+
+
+ article +
+ +
+
+

Base on a Topic

+

Select from a pre-defined course topic template curated by lecturers.

+
+ +
+ +
+
+ +
+
+ + +
+
+

Project Details

+

Fill out the template fields required for this project category.

+
+ + +
+ + +
*Required
+
+ + +
+ +
+ +
+ expand_more +
+
+
*Required
+
+ + +
+ +
+ + + +
+
*Required
+
+ + +
+
+ + Markdown supported +
+ + +
+
+ + + +
+ + +
+ + +
+ +
+
*Required
+
+ +
+ + +
+ Cancel + +
+ +
+
+ +
\ No newline at end of file diff --git a/ProPro Design/projects_show.html.erb b/ProPro Design/projects_show.html.erb new file mode 100644 index 00000000..a544d4c7 --- /dev/null +++ b/ProPro Design/projects_show.html.erb @@ -0,0 +1,362 @@ +<% content_for :hide_toggler, true %> +<% content_for :no_sidebar, true %> +<% content_for :hide_breadcrumbs, true %> +<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> + + + + +
+ + +
+
+ + ProPro + chevron_right + Grouped Topic Approval Enabled +
+
+ +
+
+ + +
+ + + + + +
+ + +
+ + +
+
+
+
+ Pending + Submitted 2 hours ago +
+

Difficult Group Project Group 4

+
+ +
+ + +
+ Version: + +
+ + + +
+ + +
+ + + + +
+ + + +
+ + + + + +
+
+
+
+ + + + + +
+ + +
+

Project Overview

+ +
+ +
+
Group Name
+
Group 4
+
+ +
+
Type
+
Grouped Project
+
+ +
+
Owners
+
+
+
K
+ Koh Wei Keat +
+
+
R
+ Raymond Lee Wei Kang +
+
+
K
+ Kevin Chong Wei Keong +
+
+
+ +
+
Supervisor
+
+
L1
+ Lecturer 1 +
+
+ +
+
+ + +
+ + +
+
Project Title
+
+ Difficult Group Project Group 4 +
+
+ + +
+
Target Platform
+
+ Web App +
+
+ + +
+
Proposed Deadline
+
+ November 24, 2026 +
+
+ + +
+
Group Project Description
+ +
+

1. User Account Management — Main Feature

+

Users can create and manage their accounts.

+

User Registration

+
  • Username
  • Email
  • Password
+

Login / Logout

+
  • Users can securely log in to their account.
  • Users can log out of their account.
+

Profile Management

+
  • View profile
  • Edit profile information
  • Edit profile image
  • View listed items
  • View transaction history
+ +

2. Collectible Item Management — Main Feature

+

Users can upload and manage collectible items.

+

Item Management

+
  • Add item
  • Edit item
  • Delete item
  • Mark item as unavailable
+

Item Information

+
  • Item Name: Name of the collectible
  • Category: Type/category of the collectible
  • Description: Detailed description
  • Image: Image of the collectible
  • Condition: Physical condition
  • Price: Required for Sell listings
  • Listing Type: Sell / Trade
  • Status: Available / Pending / Sold / Traded
+

Example: Pikachu Rare Card — Pokemon Card — Excellent — Trade — Available

+ +

3. Browse & Search System — Main Feature

+
  • View all available items
  • Search by keyword
  • Filter by category, listing type, price, and condition
+

Categories: Pokemon Card, Pop Mart, Figure, Others

+ +

4. Buying System — Main Feature

+
  • Purchase record
  • Seller notification
  • Transaction history
+

Purchase Flow

+
  1. Buyer views item
  2. Clicks Buy
  3. Confirms purchase
  4. Transaction is completed
  5. Item is removed from the marketplace or marked as Sold
+ +

5. Trading / Exchange System — Main Feature

+
  • Send trade request
  • View received requests
  • Accept trade
  • Reject trade
  • Trade history
+

Trade Flow

+
  1. User A owns Item A
  2. User B sends a trade request
  3. User A accepts or rejects
  4. If accepted, the trade is completed
+ +

6. Favourite / Wishlist System — Main Feature

+
  • Add favourite
  • Remove favourite
  • View wishlist
+ +

7. Auction System — Extra Feature

+

If enough time is available.

+
  • Set starting price
  • Place bids
  • View highest bid
  • Auction countdown
  • Determine winner
+

Example: Charizard Card — Starting Price: RM50 — Current Bid: RM120 — Ends: 10 PM

+ +

8. Rating & Review System — Extra Feature

+

After a successful transaction, users can rate each other.

+
  • 5-star rating
  • Review
+
+
+ + +
+
+ info + Based on Topic: Own Proposal +
+
+ +
+ +
+
+ + + +
+ +
+

Comments

+ 7 +
+ +
+ +
+ Comments for V1 +
+ + +
+
L1
+
+
+ lecturer1 + 1 day ago +
+

Hey everyone, please expand on the historical reporting aspect in your description.

+
+ +
+ + +
+
L1
+
+
+ lecturer1 + 1 day ago +
+

Also make sure you attach the mockups before the deadline.

+
+ +
+ +
+ + +
+
+ + +
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/ProPro Design/projects_show_responsive.html.erb b/ProPro Design/projects_show_responsive.html.erb new file mode 100644 index 00000000..4c365034 --- /dev/null +++ b/ProPro Design/projects_show_responsive.html.erb @@ -0,0 +1,204 @@ +<% content_for :hide_toggler, true %> +<% content_for :no_sidebar, true %> +<% content_for :hide_breadcrumbs, true %> +<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> + + + + + + + +
+ + +
+
+ + + ProPro +
+ +
+
WK
+
+
+ + + + + +
+ + +
+
+
+ Pending + Submitted 2h ago +
+

+ Difficult Group Project Group 4 +

+
+
+ + + + + + +
+ + +
+

Project Overview

+ + +
+
+
Group Name
+
Group 4
+
+
+
Type
+
Grouped Project
+
+
+
Owners
+
+
+
K
+ Koh Wei Keat +
+
+
R
+ Raymond Lee Wei Kang +
+
+
+
+
Supervisor
+
+
L1
+ Lecturer 1 +
+
+
+
+ + +
+ + +
+
Project Title
+
Difficult Group Project Group 4
+
+ + +
+
Target Platform
+
+ Web App +
+
+ + +
+
Proposed Deadline
+
📅 November 24, 2026
+
+ + +
+
Group Project Description
+
+

1. User Account Management

+

Users can create and manage their accounts securely.

+
    +
  • Username & Email
  • +
  • View profile
  • +
+
+
+ + +
+
+ info + Based on Topic: Own Proposal +
+
+
+ +
+
+ + +
+ + +
+ +
+ + +
+ + + + + + + + +
+
+ +
\ No newline at end of file diff --git a/ProPro Design/projects_version_comparison.html.erb b/ProPro Design/projects_version_comparison.html.erb new file mode 100644 index 00000000..9bd7669c --- /dev/null +++ b/ProPro Design/projects_version_comparison.html.erb @@ -0,0 +1,315 @@ +<% content_for :hide_toggler, true %> +<% content_for :no_sidebar, true %> +<% content_for :hide_breadcrumbs, true %> +<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> + + + + + + + + + +
+ + +
+
+ + ProPro + chevron_right + Grouped Topic Approval Enabled +
+
+ +
+
+ + +
+ + + + + +
+ + +
+ + +
+
+
+
+ Redo + Returned for revision yesterday +
+

Difficult Group Project Group 4

+
+ +
+ +
+ Version: + +
+ + +
+ +
+ +
+ +
+ + + + +
+
+
+
+ + + + + +
+ + +
+
+ + +
+
+ +
+ + +
+ sync_alt +
+ + +
+
+ +
+ +
+
+ + +
+ + + + +
+ + +
+
Project Title
+
+ Group Project Group 4 +
+
+ + +
+
Target Platform
+
+ Mobile App +
+
+ + +
+
Group Project Description
+
+

1. User Account Management

+

Users can create and manage their accounts. Basic authentication is included.

+ +

2. Item Management

+

Users can upload items for display.

+
+
+
+ + +
+ + +
+
Project Title
+
+ Difficult Group Project Group 4 +
+
+ + +
+
Target Platform
+
+ Mobile App Web App +
+
+ + +
+
Group Project Description
+
+

1. User Account Management

+

Users can create and manage their accounts. OAuth2 authentication and role-based permissions are included.

+ +

2. Item Management and Trading

+

Users can upload items for display, request trades, and process transactions securely via the marketplace flow.

+
+
+
+
+
+
+ + +
+ +
+

Comments

+ 7 +
+ +
+ +
+ Comments for V1 +
+ + +
+
L1
+
+
+ lecturer1 + 1 day ago +
+

Hey everyone, please expand on the historical reporting aspect in your description.

+
+ +
+ + +
+
L1
+
+
+ lecturer1 + 1 day ago +
+

Also make sure you attach the mockups before the deadline.

+
+ +
+ +
+ + +
+
+ + +
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/ProPro Design/style_guide.html.erb b/ProPro Design/style_guide.html.erb new file mode 100644 index 00000000..ceae9d67 --- /dev/null +++ b/ProPro Design/style_guide.html.erb @@ -0,0 +1,1036 @@ +<% content_for :hide_toggler, true %> +<% content_for :no_sidebar, true %> +<% content_for :hide_breadcrumbs, true %> +<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> + + + + + + + +
+
+ +
+

ProPro Material Style Guide

+

Exact Google Classroom visual language. Soft shadows, pill buttons, pure white surfaces, standard Google Material Colors, and a Google Sans / Roboto type pairing for UI chrome vs. reading copy.

+
+ + +
+

+ warning + Audit: components added, drift found +

+

+ Sections 1–12 below are unchanged from the original guide. Sections 13–24 document components pulled from + progress_updates, projects_form, projects_show, projects_show_responsive, + projects_version_comparison, and course_show that weren't captured yet. Section 25 lists exact + color/shape/icon drift found in the live .erb files (mostly in grouping_settings_html.erb and projects_form_html.erb) + that should be corrected in those files directly — the guide below always shows the canonical version, not the drifted one. +

+
+ + +
+
+

Colors

+
+
Primary Blue (#1A73E8) — hover #1B66C9
+
Success Green (#137333) — hover #0f5c29 — Approve buttons, "Excellent"/"Approved" semantics
+
Deep Card Header Tone (#37474F)
+
Active Tab / Highlight (#E8F0FE)
+
Selected Container (#D3E3FD) — inset active-item pill
+
Primary Text (#3C4043)
+
On-Surface / Heading Text (#1f1f1f) — headings & titles, one shade darker than Primary Text
+
Secondary Text (#5F6368) — inactive nav icons & labels
+
Meta Text (#444746) — timestamps, "N awaiting action" counts, comment timestamps
+
Dividers / Borders (#E0E0E0)
+
Input Borders (#DADCE0) — one step darker than divider gray, used on all form controls
+
Surface Container (#F1F3F4)
+
App Canvas (#f8fafd) — page background behind the white content pane
+
Neutral Avatar (#E1E3E1 bg / #3C4043 text) — non-color-coded people (group owners, generic users)
+
Danger (#C5221F) — paired bg tint #FCE8E6
+
+

+ Do not use generic Tailwind palette colors (gray-*, blue-*, red-*, amber-*) anywhere. + Every color in the app should resolve to one of the hex tokens on this page. This is the single biggest deviation found in the audit — see Section 25. +

+
+ +
+

Typography

+ +
Google Sans — UI Chrome
+
+
Display (36px / 400 / 44px)
+
Section Heading (22px / 400 / 28px)
+
Card / Section Title (16px / 500 / 24px)
+
Nav / Tab / Label (14px / 500 / 20px)
+
+

Used for headings, nav items, tabs, and list/card titles — anything that's UI chrome rather than reading copy. Falls back to Roboto when neither Google Sans nor Google Sans Flex loads.

+ +
Roboto — Reading Type
+
+
Body Medium (14px / 500)
+
Body Regular (14px / 400) - Descriptions
+
Meta / Timestamp (14px / 400 / 20px, #444746) - "Group 1 • Submitted 2 hours ago"
+
Eyebrow Label (12px / 500 / uppercase, #5F6368)
+
+

+ The Eyebrow Label style (text-xs font-medium text-[#5F6368] uppercase tracking-wide) is its own reusable pattern — + it's the field label on every definition-list row ("Group Name", "Feedback", "Proposed Deadline") and every settings sub-heading. Always + #5F6368 at font-medium — never font-semibold and never a darker/different gray. projects_form_html.erb + currently renders its field labels as text-xs font-semibold text-[#3C4043] uppercase (bolder, darker) — bring it in line with this token. +

+
+
+ + +
+
+

Corner Radius

+
+
+
+ 4px (rounded-[4px]) - Compact/inline controls: version-picker selects, snackbars +
+
+
+ 6px (rounded-md) - Standard standalone form inputs, textareas, selects, WYSIWYG toolbar +
+
+
+ 8px (rounded-lg) - Project Cards, Dialogs, Feed Items, Data Chips, Timeline content cards +
+
+
+ 10px (rounded-[10px]) - Selection cards (Section 15) +
+
+
+ 12px (rounded-xl) - Top-level form/settings section containers (bigger than a card, holds multiple fields) +
+
+
+ Full (rounded-full) - Buttons, Badges, Avatars, Nav Pills — always, never rounded-md on a button +
+
+
+ 28px (rounded-tl-[28px]) - App shell content pane / modal dialogs +
+
+

grouping_settings_html.erb's footer action buttons ("Add Students", "Delete Course") currently use rounded-md — every button must be a pill regardless of context.

+
+ +
+

Spacing System

+
+
4px (p-1, gap-1) - Icon spacing
+
8px (p-2, gap-2) - Tight component padding
+
16px (p-4, gap-4) - Standard card padding
+
24px (p-6, gap-6) - Section padding, Modal padding
+
32px (p-8, mb-8) - Major section spacing
+
+
+
+ + +
+

Header & Nav Bar

+ +
Global App Header — compact
+
+
+ + ProPro +
+
+ +
+
+

Used on the top-level Home dashboard, where there's no page beneath "ProPro" to break out to.

+ +
Breadcrumb Header — page-level
+
+
+ + ProPro + chevron_right + Course or Project Title +
+ +
+

Appears once you're inside a course or project — "ProPro" stays a clickable root, the second label swaps to whatever the person is currently viewing.

+ +
Side Nav Bar — isolated preview (32px avatars)
+ +

Compact, boxed variant for previewing the nav in isolation — top-level items highlight edge-to-edge (rounded-r-full).

+ +
Side Nav Bar — live page states (36px avatars)
+ +

Real usage inside an expandable group: the active item gets an inset full pill (mx-3 rounded-full, bg #D3E3FD / text #0B57D0) rather than the compact header's edge-to-edge highlight. Its avatar recolors to neutral gray (#E1E3E1 / #3C4043) so it doesn't fight the selection tint. Reserve this pattern for the active item inside an expandable ("Enrolled") group; use the edge-to-edge rounded-r-full highlight for top-level items like Home.

+
+ + +
+

Tabs

+ +

Flush against the content edge and left-aligned — the row fills whatever width is available rather than centering or capping. Active tab: Primary Blue text with a 4px underline. When tabs live inside a scrollable/sticky context (project detail pages), see Section 24's mobile scrollable-tabs variant, which adds a count badge.

+
+ + +
+

Course Cards

+ +
+
+ + +
+
+

Scatting class

+

G11

+
+
+ + +
+ + +
+ + + +
+ +
+
+

Title now carries the Google Sans chrome font, matching the Card / Section Title scale used everywhere else.

+
+ + +
+

List Items — Proposal Rows

+ +
Section header pattern
+
+

Pending Proposals

+ 4 awaiting action +
+ +
Row — status variants
+
+ + +
+
+
+ description +
+
+

EduPulse

+

Group 1 • Submitted 2 hours ago

+
+
+
+ Pending + +
+
+ + +
+
+
+ description +
+
+

MMU Foodie

+

Group 2 • Returned for revision yesterday

+
+
+
+ Redo + +
+
+ + +
+
+
+ description +
+
+

IMprove

+

Group 9 • Rejected last week

+
+
+
+ Rejected + +
+
+ +
+

The icon avatar and status pill always share a color: blue/#1A73E8 = Pending, yellow/#F57F17 = Redo, red/#C5221F = Rejected, green/#137333 = Approved (see Status Colors & Badges). The trailing more_vert button stays hidden until row hover (opacity-0 group-hover:opacity-100) to keep dense lists quiet. Rejected rows also drop the title's hover-highlight — they're closed out, not actionable. This "Pending" pill (bg #E8F0FE / text #1967D2, unbordered) is the only correct Pending treatment — see Section 25 for a bordered gold variant found in projects_form_html.erb that should be replaced with this one.

+
+ + +
+

Status Colors & Badges

+ +
+
+
+
+
+ Pending / Action Required +
+ Pending +
+ +
+
+
+ Redo / In Progress +
+ Redo +
+ +
+
+
+ Danger / Rejected +
+ Rejected +
+ +
+
+
+ Success / Approved +
+ Approved +
+
+
+

Same four colors drive the icon avatars in List Items — Proposal Rows above, the rating pills on Progress Updates (Section 19), and the diff/version-compare toolbar (Section 21). This 4-color system is now confirmed consistent across every page that uses it — no drift found here.

+
+ + +
+

Buttons & Actions

+
+ + + + + + + +
+

+ Primary hover is always #1B66C9 — projects_form_html.erb currently uses #1557B0 on its "Update Proposal" buttons; align it to this token. More button variants (Success, Split/Dropdown, Danger icon-only, small Ghost) are in Section 13. +

+
+ + +
+

Inputs & Forms

+
+ + +
+ + +
+ + +
+ + +
+ +
+

This is the canonical Filled Input. grouping_settings_html.erb reimplements it with generic colors (bg-[#f1f3f4] is fine, but border-gray-400 / focus-within:bg-[#e8eaed] / focus-within:border-blue-600 should become border-[#5F6368] / no bg change on focus / focus-within:border-[#1A73E8]). Standalone standard text inputs (not floating-label) use 6px radius — see Section 24.

+
+ + +
+

Comments & Updates Feed

+ + +
+ +
+
+
L1
+
+
Lecturer 1
+
Oct 15 (Edited Oct 16)
+
+
+ +
+ +
+ Please review the attached grading rubric for your project proposals. Make sure your methodology section clearly outlines your data gathering techniques. +
+ + +
+
WK
+
+ Add class comment... +
+
+
+

This full-width pill trigger opens a composer. The actual persistent side-panel composer (used on project detail pages) is a different, compact variant — see Section 20.

+
+ + +
+

Diff Viewer

+
+
+
-
+
Original proposal scope included 3 features.
+
+
+
+
+
Revised proposal scope includes 5 features and testing.
+
+
+
+
Timeline remains set for 12 weeks.
+
+
+

Line-level diff, for structured field-by-field changes. For inline/word-level diffing inside rich text (used on the Compare Versions tab), see the Inline Diff Highlights in Section 21 — same red/green tokens, different presentation.

+
+ + +
+

Data Tables

+
+ + + + + + + + + + + + + + + +
NameEmailRole
+
K
+ Koh Wei Keat +
weikeat@example.eduCoordinator
+
+
+ + +
+

13. Buttons — Additional Variants

+ +
Success Button (Approve actions)
+ + +
Split Button (primary action + attached dropdown)
+
+
+ +
+ +
+
+

The dropdown reveals a popover menu — see the "Rich" dropdown pattern in Section 14. Any color works for the primary segment (Success shown here); a thin 1px white/30 divider always separates the two segments.

+ +
Danger Icon-Only Button (mobile action bars)
+ + +
Small Ghost Button (compact secondary action inside a card)
+ + +
Text Link Button (inline, no button chrome)
+ + +

Every filled/outlined button variant is still a pill (rounded-full) except the Small Ghost variant, which is a rectangular sub-component nested inside a card, and the plain Text Link, which has no shape at all. grouping_settings_html.erb's footer buttons ("Add Students", "Delete Course", "Save") should map onto Outline / Danger Text / Primary respectively, using the hex tokens and pill shape — not generic Tailwind grays/reds on rounded-md.

+
+ + +
+

14. Dropdown / Context Menus

+ +
Simple Menu (icon + single label — triggered by more_vert)
+
+
+ +
+ +
+
+ +
Rich Menu (icon + title + subtitle — used off the Approve split button)
+
+
+ +
+ +
+
+

Both variants: rounded-lg shadow-lg popover, destructive items hover to the Danger bg tint (#FCE8E6) with red text/icon, a 1px divider separates neutral from destructive actions.

+
+ + +
+

15. Selection Cards

+

For choosing between a small number of mutually-exclusive options (proposal method, student-list mode). 10px radius — a step between the 8px card radius and the 12px section radius.

+
+
+
+
+
+ person +
+
+ check +
+
+

Propose to Lecturer

+

Submit your original project proposal directly to a supervisor.

+
+
+
+
+
+
+ article +
+
+
+

Base on a Topic

+

Select from a pre-defined course topic template curated by lecturers.

+
+
+
+

Selected: 2px Primary Blue border, #F8F9FA bg, filled blue check-circle indicator. Unselected: 1px #DADCE0 border, white bg, hollow gray circle, border turns blue on hover. A larger radio-card variant with description text and a muted/disabled state also appears in Settings (Section 24).

+
+ + +
+

16. Modals & Dialogs

+
+
+

Record Progress Update

+ +
+
+
+ +
+ + arrow_drop_down +
+
+
+
+ + +
+
+

28px radius (Material 3 standard), dark bg-black/50 backdrop, header/body/footer structure, footer actions right-aligned with a text Cancel + filled primary Save. Select fields inside modals use the 4px "compact" radius, not 6px.

+
+ + +
+

17. Definition List / Meta Info Grid

+
+

Project Overview

+
+
+
Group Name
+
Group 4
+
+
+
Owners
+
+
+
K
+ Koh Wei Keat +
+
+
+
+
+ +
Row variant (label/value stripe, used for dynamic template fields)
+
+
+
Project Title
+
Difficult Group Project Group 4
+
+
+
+ info + Based on Topic: Own Proposal +
+
+
+

Both use the Eyebrow Label for field names. The row variant stacks label above value on mobile (drop the md: prefixes) and swaps hover:bg-[#F8F9FA] for active:bg-[#F8F9FA] on touch. A gray info-strip footer (bg #F8F9FA, info icon) closes out a looped field list when it's derived from a topic template.

+
+ + +
+

18. Data Chips vs. Status Pills

+
+
+ Web App +

Data Chip — rounded-lg (8px), gray, bordered. Displays a selected field value (a dropdown answer, a tag).

+
+
+ Pending +

Status Pill — rounded-full, colored, unbordered. Communicates workflow state.

+
+
+

Easy to confuse since both are small colored labels — the shape is the signal: square-ish corners = a value, fully round = a state.

+
+ + +
+

19. Timeline / Activity Feed

+
+
+
+
L1
+
+
+
+

Progress 2

+

+ calendar_today + 2026-08-20 +

+
+ Satisfactory +
+
+
Feedback
+
Vestibulum at neque quam, sit amet mattis tellus sollicitudin vitae.
+
Last updated: 20-08-2026
+
+
+
+
+

A continuous 1px vertical line runs behind circular author-avatar nodes (40px, 3px white ring so the line reads as passing behind, not through). Each entry is an 8px-radius (rounded-xl here specifically, matching the card's own header radius) flat card with a shaded header strip. Rating pill reuses the 4 status colors: green=Excellent, blue=Satisfactory, yellow=Unsatisfactory, (red=No Progress, not yet shown but follows the same system).

+
+ + +
+

20. Comments Panel (persistent side rail)

+
+
+

Comments

+ 7 +
+
+
Comments for V1
+ +
+
L1
+
+
+ lecturer1 + 1 day ago +
+

Please expand on the historical reporting aspect.

+
+ +
+ +
+
L1
+
+
+ Test Lecturer 1 + 01:15 AM +
+

This comment was deleted

+
+
+
+
+
+ + +
+
+
+

+ Author name color is role-coded: lecturer/staff names render in Success Green (#137333), not the default heading color — this is consistent across every page that shows comments. Timestamp is always Meta Text (14px, #444746) — projects_show_html.erb currently sets its comment timestamps to text-xs text-[#5F6368]; bring them in line with the token used everywhere else. Deleted state: italic gray body text (#5F6368), no delete button, no timestamp interaction. Live comment: a red close icon fades in on row hover, top-right, to delete. This compact composer (rounded-lg box, inline send icon) is the persistent-panel variant — different from the full-width pill "Add class comment..." trigger in Section 10. +

+
+ + +
+

21. Version Compare

+ +
Compare toolbar
+
+
+
+
+ +
+ sync_alt +
+
+ +
+
+
+ +
Inline diff highlights (within rich text)
+
+ Mobile App + Web App +
+

Toolbar pills use a small colored dot + the same status colors as everywhere else (yellow=older/base, green=newer/current) wrapping a borderless <select>. Inline diffs use the same red/green tokens as the line-based Diff Viewer (Section 11) but as small inline spans: struck-through red for removed text, bold green for added text — never a whole new color pair.

+
+ + +
+

22. Split-Pane App Layout (70/30)

+

+ Used on every project detail page (Project Details / Compare Versions / Progress Updates tabs). Structure: + main is a flex row inside the white rounded-tl-[28px] canvas. The left pane (flex-1) scrolls independently and holds the sticky context header + sticky tabs + tab content. + The right pane is a fixed w-[380px] Comments Panel (Section 20) on the app-canvas gray (bg-[#f8fafd]), separated by a single border-l border-[#E0E0E0] — no shadow. Both panes independently + overflow-y-auto. On mobile this collapses to a single column with Comments becoming a 4th scrollable tab (Section 24) instead of a persistent rail. +

+
+ + +
+

23. Page Headers — Additional Variants

+ +
Full-Screen Takeover Header (editing a proposal)
+
+
+ + close + +
+
+

Edit Proposal — Pineapple das

+ Pending +
+
+ +
+

Sticky, bordered, white — replaces the global app header entirely while editing. Close (✕) + a vertical divider + title on the left, primary action on the right. Status pill next to the title always uses the standard Status Pill colors (Section 7/18) — not a bordered/uppercase gold variant.

+ +
Full-Screen Settings Overlay Header
+
+
+ + close + +

Class settings

+
+ +
+

grouping_settings_html.erb uses this pattern but with a hand-drawn SVG ✕ (Material Symbols never loads on that page — see Section 25) and a gray Save button instead of Primary Blue.

+
+ + +
+

24. Forms — Additional Patterns

+ +
Standard Text Input (standalone, not floating-label)
+ +
*Required — helper text sits directly under the field, 12px, secondary gray.
+ +
Radio Pills (methodology-style choice group)
+
+ + + +
+ +
WYSIWYG Toolbar
+
+
+ + +
+ + +
+ +
+ +
Settings Row (label + inline Yes/No)
+
+
+ Use progress updates? +
+ + +
+
+
+ +
Radio-Card List (mode selector w/ description + disabled state)
+
+ +
+
+
+ Final student list + Unavailable once Default mode has been saved. +
+
+
+ +
Preview / Result Panel
+
+

Preview group distribution

+

Enter an expected student count to see how legal groups would form.

+
— result renders here —
+
+ +
Alert / Error Banner
+
+

2 errors prohibited saving:

+
    +
  • Minimum group size can't be blank
  • +
+
+ +
Beta / Experimental Tag
+ Beta + +

+ All of the above are pulled from grouping_settings_html.erb, remapped from generic Tailwind grays/blues/reds/ambers onto the standard hex tokens. Radio-card selected state reuses the same blue tint pattern as the Section 15 selection cards; the disabled option drops to a neutral #9AA0A6 muted-gray text with 60% opacity on the whole row. The Beta tag reuses the Redo/yellow token rather than introducing a new amber. +

+ +
Mobile — sticky bottom action bar
+
+ +
+ + + +
+
+ +
Mobile — scrollable tabs with count badge
+
+
+ Project Details + + Comments + 7 + +
+
+

On narrow viewports the persistent Comments rail (Section 22) collapses into a 4th tab carrying the same count-badge used on the panel header. Icon-only Reject/Redo buttons + a full Approve pill fill a fixed bottom bar so primary actions stay reachable while scrolling.

+
+ + +
+

+ rule + 25. Known Inconsistencies To Fix +

+
    +
  • grouping_settings_html.erb — missing the Google Fonts / Material Symbols <link> tags entirely; uses font-sans instead of Roboto/Google Sans; hand-draws an SVG ✕ icon instead of the material-symbols-outlined close span; uses generic Tailwind gray-*/blue-*/red-*/amber-* classes throughout instead of the hex tokens in Section 1; footer buttons ("Add Students", "Add Lecturers", "Delete Course") are rounded-md instead of pill-shaped, and the Danger button uses red-50/red-700 instead of #FCE8E6/#C5221F; the "Save" button is gray instead of Primary Blue.
  • +
  • projects_form_html.erb — header "Pending" badge is a bordered gold chip (#FEF7E0/#B06000, uppercase, bordered) instead of the standard unbordered blue Status Pill; primary-button hover is #1557B0 instead of the documented #1B66C9; field labels are font-semibold text-[#3C4043] instead of the standard Eyebrow Label (font-medium text-[#5F6368]); its select-field caret uses expand_more where every other select on the app uses arrow_drop_down — standardize on arrow_drop_down.
  • +
  • projects_show_html.erb — comment timestamps use text-xs text-[#5F6368] instead of the Meta Text token (text-[14px] text-[#444746]) used on every other page's comment panel.
  • +
+
+ +
+
\ No newline at end of file diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 9a5a57d7..ff2fbda9 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -51,6 +51,21 @@ def show @incoming_proposals = @course.projects.where(supervisor_enrolment: @current_user_enrolment).proposals end + # For To Review tab + @pending_proposals = @incoming_proposals.select { |p| p.current_status == "pending" } + @reviewed_proposals = @incoming_proposals.select { |p| %w[redo rejected].include?(p.current_status) } + @pending_topics = if @current_user_enrolment&.coordinator? + @topic_list.select do |topic| + topic.lecturer? && topic.owner != current_user && + %w[pending redo rejected].include?(topic.status.to_s) + end + else + [] + end + + # For Supervised Projects tab (Ticket 6) + @approved_projects = @my_student_projects.select(&:approved?) + # view instances for participants_table @filtered_group_list = filtered_group_list @filtered_student_list = filtered_student_list diff --git a/app/helpers/sidebar_helper.rb b/app/helpers/sidebar_helper.rb index 49cd8ac4..cf2e5c39 100644 --- a/app/helpers/sidebar_helper.rb +++ b/app/helpers/sidebar_helper.rb @@ -47,11 +47,4 @@ def render_sidebar(&) end end - def sidebar_link(label, path) - link_to( - label, - path, - class: 'block p-0 md:p-1 lg:px-2 lg:py-1.75 w-full rounded-md hover:bg-gray-200 hover:text-gray-900 transition-colors' - ) - end end diff --git a/app/javascript/controllers/tabs_controller.js b/app/javascript/controllers/tabs_controller.js new file mode 100644 index 00000000..56bea9b5 --- /dev/null +++ b/app/javascript/controllers/tabs_controller.js @@ -0,0 +1,39 @@ +import { Controller } from "@hotwired/stimulus" + +// Generic tab controller — deliberately NOT mobile_tabs_controller.js, +// which is hard-coded to 3 named targets for the project/topic show pages +// and out of scope for this work. +// +// Usage: +//
+// +// +//
...
+//
...
+//
+// +// The "Settings" entry in the tab bar is a plain link_to (real navigation +// to a separate page), not a data-tabs-target="tab" — it doesn't participate +// in this controller at all. +export default class extends Controller { + static targets = ["tab", "panel"] + static classes = ["active", "inactive"] + + connect() { + this.show({ params: { index: 0 } }) + } + + show(event) { + const index = Number(event.params.index) + + this.panelTargets.forEach((panel, i) => { + panel.classList.toggle("hidden", i !== index) + }) + + this.tabTargets.forEach((tab, i) => { + const isActive = i === index + tab.classList.toggle(this.hasActiveClass ? this.activeClass : "text-[#1A73E8] border-[#1A73E8]", isActive) + tab.classList.toggle(this.hasInactiveClass ? this.inactiveClass : "text-[#5F6368] border-transparent", !isActive) + }) + } +} diff --git a/app/models/project.rb b/app/models/project.rb index a16ee874..b47e1082 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -58,6 +58,16 @@ def current_title current_instance&.title || title end + def owner_name + if owner.respond_to?(:name) + owner.name + elsif owner.respond_to?(:group_name) + owner.group_name + else + "Unknown" + end + end + def editable? !approved? end diff --git a/app/views/courses/_coordinator_section.html.erb b/app/views/courses/_coordinator_section.html.erb deleted file mode 100644 index 359dbcfd..00000000 --- a/app/views/courses/_coordinator_section.html.erb +++ /dev/null @@ -1,256 +0,0 @@ -<% container_classes = - "border border-gray-200 shadow-xs rounded-[10px] w-full p-4 bg-[#f8f9fa] mb-5" %> - -<% unless @course.solo_supervisor? %> -
-
-
-

My Student Projects

-

Student projects currently under supervision

-
-
-
- <% if my_student_projects.any? %> - <%= render partial: "project_card", - collection: my_student_projects, - as: :project, - locals: { - course: @course, - } %> - <% else %> -

No approved student projects yet.

- <% end %> -
-
-
-
-<% end %> - -
-
-
-

Incoming Proposals

-

Student proposals awaiting approval

-
- <% if incoming_proposals.empty? %> -

No student proposals are pending approval.

- <% else %> - <% %w[pending redo rejected].each do |stat| %> - <% group = incoming_proposals.select { |p| p.status.to_s == stat } %> - <% if group.any? %> - <% is_collapsible = %w[redo rejected].include?(stat) %> - -
-
- - - -
-
-
- <%= image_tag "#{stat}.svg", class: "inline-block w-4 h-4" %> -
- <%= stat.upcase %> -
- <%= group.size %> -
- -
- <%# Add the 'hidden' class conditionally if this group is collapsible, project card grid %> -
- <%= render partial: "project_card", collection: group, as: :project %> -
-
- <% end %> - <% end %> - <% end %> -
-
- - -<% if @course.toggle_topics %> -
-
-
-
-

My Topics

-
- <%= link_to "View All Topics", - course_topics_path(course), - class: "inline-flex items-center px-3 py-1.5 text-xs font-semibold text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors" %> - <% if policy(Topic.new(course: course)).create? %> - <%= link_to "Create Topic", - new_course_topic_path(course), - class: "inline-flex items-center px-3 py-1.5 text-xs font-semibold text-white bg-gray-600 hover:bg-gray-700 rounded-lg transition-colors" %> - <% end %> -
-
-

Your submitted topics. Only approved topics are visible to students in your Lecturer View.

-
- - <% if my_topics.empty? %> -

You haven't created any topics yet.

- <% else %> - <% %w[approved pending redo rejected].each do |stat| %> - <% group = my_topics.select { |p| p.status.to_s == stat } %> - - <% if group.any? %> - <% is_collapsible = stat != "approved" %> -
-
- - -
-
-
- <%= image_tag "#{stat}.svg", class: "inline-block w-4 h-4" %> -
- <%= stat.upcase %> -
- <%= group.size %> -
- -
-
- <%= render partial: "topic_card", collection: group, as: :topic %> -
-
- <% end %> - <% end %> - <% end %> -
-
-<% end %> - -<% unless @course.solo_supervisor? %> -
-
-

Supervisors Available

-
    - <%= render partial: "lecturers", - collection: lecturers, - as: :lecturer, - locals: { course: course } %> -
- <% if @course.supervisor_auto_calculate_enabled? && @capacity_result %> -
-

- Total Projects: <%= @capacity_result.total %> - <% if @capacity_result.base > 0 %> - | - Maximum Projects per Lecturer: <%= @capacity_result.base %> - <% end %> -

- - <% if @capacity_result.remainder > 0 %> -

- - - - You will need to allocate an additional <%= @capacity_result.remainder %> project(s) to supervisors using offsets -

- <% end %> -
- <% end %> -
-
-<% end %> - -<% if @course.toggle_topics %> -
-
-
-

Incoming Topics

-

Lecturer-proposed topics awaiting your approval

-
- <% incoming_topics = - topic_list.select do |project| - project.lecturer? && project.owner != current_user && - %w[pending redo rejected].include?(project.status.to_s) - end %> - <% if incoming_topics.empty? %> -

No Topics submmited to you.

- <% else %> - - <% %w[pending redo rejected].each do |stat| %> - <% group = incoming_topics.select { |p| p.status.to_s == stat } %> - <% if group.any? %> - <% is_collapsible = stat != "pending" %> - -
-
- - - -
-
-
- <%= image_tag "#{stat}.svg", class: "inline-block w-4 h-4" %> -
- <%= stat.upcase %> -
- <%= group.size %> -
-
- -
- <%= render partial: "topic_card", collection: group, as: :topic %> -
-
- <% end %> - <% end %> - <% end %> -
-
-<% end %> diff --git a/app/views/courses/_lecturer_section.html.erb b/app/views/courses/_lecturer_section.html.erb deleted file mode 100644 index 9ec75e45..00000000 --- a/app/views/courses/_lecturer_section.html.erb +++ /dev/null @@ -1,168 +0,0 @@ -<% container_classes = - "border border-gray-200 shadow-xs rounded-[10px] w-full p-4 bg-[#f8f9fa] mb-5" %> - -<% unless @course.solo_supervisor? %> -
-
-
-

My Student Projects

-

Approved student projects currently under your supervision

-
- -
-
- <% if my_student_projects.any? %> - <%= render partial: "project_card", collection: my_student_projects, as: :project %> - <% else %> -

No approved student projects yet.

- <% end %> -
-
-
-
-<% end %> - -
-
- -
-

Incoming Proposals

-

Student proposals awaiting approval

-
- - <% if incoming_proposals.empty? %> -

No student proposals are pending approval.

- <% else %> - <% %w[pending redo rejected].each do |stat| %> - <% group = incoming_proposals.select { |p| p.status.to_s == stat } %> - <% if group.any? %> - <% is_collapsible = %w[redo rejected].include?(stat) %> - -
- -
- - -
-
-
- <%= image_tag "#{stat}.svg", class: "inline-block w-4 h-4" %> -
- <%= stat.upcase %> -
- <%= group.size %> -
-
- -
- <%= render partial: "project_card", collection: group, as: :project %> -
-
- - <% end %> - <% end %> - <% end %> -
-
- -<% unless @course.solo_supervisor? %> -
-
-
-

Supervisors Available

-
-
    - <%= render partial: "lecturers", - collection: lecturers, - as: :lecturer, - locals: { - course: course, - } %> -
-
-
-<% end %> - -<% if @course.toggle_topics %> -
-
- -
-
-

My Topics

-
- <%= link_to "View All Topics", - course_topics_path(course), - class: "inline-flex items-center px-3 py-1.5 text-xs font-semibold text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors" %> - <% if policy(Topic.new(course: course)).create? %> - <%= link_to "Create Topic", - new_course_topic_path(course), - class: "inline-flex items-center px-3 py-1.5 text-xs font-semibold text-white bg-gray-600 hover:bg-gray-700 rounded-lg transition-colors" %> - <% end %> -
-
- <% if course.require_coordinator_approval? %> -

Draft topics here. Approved topics appear in your Lecturer View - (visible to students)

- <% else %> -

Your submitted topics. Only approved topics are visible to students - in your Lecturer View.

- <% end %> -
- - <% if my_topics.empty? %> -

You haven't created any topics yet.

- <% else %> - <% %w[approved pending redo rejected].each do |stat| %> - <% group = my_topics.select { |p| p.status.to_s == stat } %> - <% if group.any? %> - <% is_collapsible = stat != "approved" %> -
-
- -
-
-
- <%= image_tag "#{stat}.svg", class: "inline-block w-4 h-4" %> -
- <%= stat.upcase %> -
- <%= group.size %> -
-
-
- <%= render partial: "topic_card", collection: group, as: :topic %> -
-
- <% end %> - <% end %> - <% end %> -
-
-<% end %> diff --git a/app/views/courses/_people_tab.html.erb b/app/views/courses/_people_tab.html.erb new file mode 100644 index 00000000..6cbe974f --- /dev/null +++ b/app/views/courses/_people_tab.html.erb @@ -0,0 +1,60 @@ +<% container_classes = "border border-gray-200 shadow-xs rounded-[10px] w-full p-4 bg-[#f8f9fa] mb-5" %> + +<% unless @course.solo_supervisor? %> +
+
+
+

Supervisors Available

+
+
    + <%= render partial: "lecturers", collection: @lecturers, as: :lecturer, locals: { course: @course } %> +
+ <% if @course.supervisor_auto_calculate_enabled? && @capacity_result %> +
+

+ Total Projects: <%= @capacity_result.total %> + <% if @capacity_result.base > 0 %> + | + Maximum Projects per Lecturer: <%= @capacity_result.base %> + <% end %> +

+ + <% if @capacity_result.remainder > 0 %> +

+ + + + You will need to allocate an additional <%= @capacity_result.remainder %> project(s) to supervisors using offsets +

+ <% end %> +
+ <% end %> +
+
+<% else %> +
+
+
+

Instructor: <%= @lecturers.first&.name %>

+
+
    + <%= render partial: "lecturers", collection: @lecturers, as: :lecturer, locals: { course: @course } %> +
+
+
+<% end %> + +<%= render "participants", course: @course, use_progress_updates: @course.use_progress_updates %> + +<% if policy(@course).manage_students? || policy(@course).manage_lecturers? %> +
+ <% if policy(@course).manage_students? %> + <%= link_to "Add Students", add_students_course_path(@course), + class: "inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 transition-colors" %> + <% end %> + <% if policy(@course).manage_lecturers? %> + <%= link_to "Add Lecturers", add_lecturers_course_path(@course), + class: "inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 transition-colors" %> + <% end %> +
+<% end %> diff --git a/app/views/courses/_project_details_tab.html.erb b/app/views/courses/_project_details_tab.html.erb new file mode 100644 index 00000000..b852c3c5 --- /dev/null +++ b/app/views/courses/_project_details_tab.html.erb @@ -0,0 +1,34 @@ +
+
+
+

Project Details

+

<%= @description || "No description available for this course." %>

+
+ + <% if @course.file_link.present? %> + +
+
+ <%= image_tag "folder.svg" %> +
+
+ Attachment Link +
+
+
+ <% end %> +
+
+ +<% if @course.solo_supervisor? %> +
+
+
+

Instructor: <%= @lecturers.first&.name %>

+
+
    + <%= render partial: "lecturers", collection: @lecturers, as: :lecturer, locals: { course: @course } %> +
+
+
+<% end %> diff --git a/app/views/courses/_sidebar.html.erb b/app/views/courses/_sidebar.html.erb deleted file mode 100644 index 5ec3e853..00000000 --- a/app/views/courses/_sidebar.html.erb +++ /dev/null @@ -1,67 +0,0 @@ -<% render_sidebar do %> - <%# --- STUDENT VIEW --- %> - <% if @current_user_enrolment&.student? %> - <%= sidebar_link "Proposal Description", "#proposal-guidelines" %> - <%= sidebar_link "Proposal Status", "#proposal-status" %> - - <% if @course.solo_supervisor? %> - <%= sidebar_link "Instructor", "#supervisors-available" %> - <% else %> - <%= sidebar_link "Supervisor Avaible", "#supervisors-available" %> - <% end %> - - <%= sidebar_link "Participants", "#participants" %> - <% end %> - - <%# --- COORDINATOR VIEW --- %> - <% if @current_user_enrolment&.coordinator? %> - <%= sidebar_link "Proposal Description", "#proposal-guidelines" %> - <%= sidebar_link "My Student Projects", "#my-student-projects" %> - <%= sidebar_link "Incoming Proposals", "#incoming-proposals" %> - - <% if @course.toggle_topics %> - <%= sidebar_link "My Topics", "#my-topics" %> - <% end %> - - <% if @course.solo_supervisor? %> - <%= sidebar_link "Instructor", "#supervisors-available" %> - <% else %> - <%= sidebar_link "Supervisor Avaible", "#supervisors-available" %> - <% end %> - - <% if @course.toggle_topics %> - <%= sidebar_link "Incoming Topics", "#incoming-topics" %> - <% end %> - <%= sidebar_link "Participants", "#participants" %> - -
-
-
- - <%= sidebar_link "Add Students", add_students_course_path(@course) %> - <%= sidebar_link "Add Lecturers", add_lecturers_course_path(@course) %> - <%= sidebar_link "Edit Course Settings", settings_course_path(@course) %> - <%= sidebar_link "Edit Templates", edit_course_project_template_path(@course) %> - <%= sidebar_link "Export CSV", export_csv_course_path(@course) %> - <% end %> - - <%# --- LECTURER VIEW --- %> - <% if @current_user_enrolment&.lecturer? %> - <%= sidebar_link "Proposal Description", "#proposal-guidelines" %> - <%= sidebar_link "My Student Projects", "#my-student-projects" %> - <%= sidebar_link "Incoming Proposals", "#incoming-proposals" %> - - <% if @course.solo_supervisor? %> - <%= sidebar_link "Instructor", "#supervisors-available" %> - <% else %> - <%= sidebar_link "Supervisors Available", "#supervisors-available" %> - <% end %> - - - <% if @course.toggle_topics %> - <%= sidebar_link "My Topics", "#my-topics" %> - <% end %> - <%= sidebar_link "Participants", "#participants" %> - <% end %> - -<% end %> diff --git a/app/views/courses/_student_section.html.erb b/app/views/courses/_student_section.html.erb deleted file mode 100644 index d273ea94..00000000 --- a/app/views/courses/_student_section.html.erb +++ /dev/null @@ -1,61 +0,0 @@ -<% container_classes = - "border border-gray-200 shadow-xs rounded-[10px] w-full p-4 bg-[#f8f9fa] mb-5" %> - -
-
- <%= render "project_status_bar", - project: project, - course: course, - enrolment: current_user_enrolment %> -
-
- -<% unless @course.solo_supervisor? %> -
-
-
-

Supervisors Available

-
-
    - <%= render partial: "lecturers", - collection: lecturers, - as: :lecturer, - locals: { - course: course, - } %> -
-
-
-<% end %> - -<% if @course.toggle_topics %> -
-
-
-

Available Topics

-

Topics available for selection

- -
- <%= link_to "View All Topics", - course_topics_path(course), - class: "inline-flex items-center px-3 py-1.5 text-xs font-semibold text-gray-600 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-900 transition-colors" %> -
-
- -
-
- <% available_topics = policy_scope(@topic_list) %> - <% if available_topics.any? %> - <%= render partial: "topic_card", collection: available_topics, as: :topic %> - <% else %> -

No topics are currently available.

- <% end %> -
-
-
-
-<% end %> diff --git a/app/views/courses/_supervised_projects_tab.html.erb b/app/views/courses/_supervised_projects_tab.html.erb new file mode 100644 index 00000000..d6603ef0 --- /dev/null +++ b/app/views/courses/_supervised_projects_tab.html.erb @@ -0,0 +1,7 @@ +
+ <% if @approved_projects.any? %> + <%= render partial: "project_card", collection: @approved_projects, as: :project, locals: { course: @course } %> + <% else %> +

No approved student projects yet.

+ <% end %> +
diff --git a/app/views/courses/_to_review_tab.html.erb b/app/views/courses/_to_review_tab.html.erb new file mode 100644 index 00000000..6030b5f5 --- /dev/null +++ b/app/views/courses/_to_review_tab.html.erb @@ -0,0 +1,44 @@ +
+
+

Pending Proposals

+ <%= pluralize(@pending_proposals.size, "proposal") %> awaiting action +
+ + <% if @pending_proposals.any? %> +
+ <%= render partial: "projects/proposal_list_item", + collection: @pending_proposals, as: :proposal %> +
+ <% else %> +

No proposals awaiting review.

+ <% end %> +
+ +
+
+

Reviewed Proposals

+ <%= pluralize(@reviewed_proposals.size, "proposal") %> +
+ + <% if @reviewed_proposals.any? %> +
+ <%= render partial: "projects/proposal_list_item", + collection: @reviewed_proposals, as: :proposal %> +
+ <% else %> +

No reviewed proposals yet.

+ <% end %> +
+ +<% if @pending_topics.present? %> +
+
+

Pending Topics

+ <%= pluralize(@pending_topics.size, "topic") %> awaiting action +
+ +
+ <%= render partial: "courses/topic_card", collection: @pending_topics, as: :topic %> +
+
+<% end %> diff --git a/app/views/courses/_topic_directory_tab.html.erb b/app/views/courses/_topic_directory_tab.html.erb new file mode 100644 index 00000000..25b48063 --- /dev/null +++ b/app/views/courses/_topic_directory_tab.html.erb @@ -0,0 +1,96 @@ +<% container_classes = "border border-gray-200 shadow-xs rounded-[10px] w-full p-4 bg-[#f8f9fa] mb-5" %> + +<% if @course.toggle_topics %> +
+
+
+

Available Topics

+

Topics available for selection

+ +
+ <%= link_to "View All Topics", + course_topics_path(@course), + class: "inline-flex items-center px-3 py-1.5 text-xs font-semibold text-gray-600 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-900 transition-colors" %> +
+
+ +
+
+ <% if @topic_list.any? %> + <%= render partial: "topic_card", collection: @topic_list, as: :topic %> + <% else %> +

No topics are currently available.

+ <% end %> +
+
+
+
+<% end %> + +<% if @current_user_enrolment&.coordinator? || @current_user_enrolment&.lecturer? %> + <% if @course.toggle_topics %> +
+
+
+
+

My Topics

+
+ <%= link_to "View All Topics", + course_topics_path(@course), + class: "inline-flex items-center px-3 py-1.5 text-xs font-semibold text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors" %> + <% if policy(Topic.new(course: @course)).create? %> + <%= link_to "Create Topic", + new_course_topic_path(@course), + class: "inline-flex items-center px-3 py-1.5 text-xs font-semibold text-white bg-gray-600 hover:bg-gray-700 rounded-lg transition-colors" %> + <% end %> +
+
+ <% if @course.require_coordinator_approval? %> +

Draft topics here. Approved topics appear in your Lecturer View (visible to students)

+ <% else %> +

Your submitted topics. Only approved topics are visible to students in your Lecturer View.

+ <% end %> +
+ + <% if @my_topics.empty? %> +

You haven't created any topics yet.

+ <% else %> + <% %w[approved pending redo rejected].each do |stat| %> + <% group = @my_topics.select { |p| p.status.to_s == stat } %> + <% if group.any? %> + <% is_collapsible = stat != "approved" %> +
+
+ +
+
+
+ <%= image_tag "#{stat}.svg", class: "inline-block w-4 h-4" %> +
+ <%= stat.upcase %> +
+ <%= group.size %> +
+
+
+ <%= render partial: "topic_card", collection: group, as: :topic %> +
+
+ <% end %> + <% end %> + <% end %> +
+
+ <% end %> +<% end %> diff --git a/app/views/courses/show.html.erb b/app/views/courses/show.html.erb index cf5897c7..4f460c86 100644 --- a/app/views/courses/show.html.erb +++ b/app/views/courses/show.html.erb @@ -1,122 +1,80 @@ -<% content_for :stylesheets do %> - <%= stylesheet_link_tag "tailwind", "new_tailwind" %> -<% end %> +<% content_for :hide_toggler, true %> +<% content_for :no_sidebar, true %> +<% content_for :hide_breadcrumbs, true %> + <% breadcrumb :course, @course %> -<%= render "sidebar", -course: @course, -current_user_enrolment: @current_user_enrolment %> - -
-
-

<%= @course.course_name.upcase %>

-
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
-
<%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
-
- -
-
-
-

Project Details

-

<%= @description || "No description available for this course." %>

-
- - <% if @course.file_link.present? %> - -
-
- <%= image_tag "folder.svg" %> -
-
- Attachment Link -
-
-
+ +<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> + + + +
+ +
+ <%= render "shared/sidebar" %> + +
+ +
+
+ + + + + <%# Placeholder — see flag at top of file, not in the mockup. %> + <% if @current_user_enrolment&.coordinator? || @current_user_enrolment&.lecturer? %> + + <% end %> + + + + + + <%# Real navigation, not a tab panel — separate settings page. %> + <%= link_to "Settings", settings_course_path(@course), + style: "font-family: 'Google Sans', Roboto, Arial, sans-serif; font-size: .875rem; font-weight: 500; letter-spacing: 0; line-height: 1.25rem;", + class: "pb-3 text-[#5F6368] hover:text-[#3C4043] border-b-4 border-transparent transition-colors" %> +
+
+ + +
+ <%= render "courses/project_details_tab" %> +
+ + + + + <% if @current_user_enrolment&.coordinator? || @current_user_enrolment&.lecturer? %> + + <% end %> -
-
- - <% if @course.solo_supervisor? %> -
-
-
-

Instructor: <%= @lecturers.first&.name %>

-
-
    - <%= render partial: "lecturers", - collection: @lecturers, - as: :lecturer, - locals: { - course: @course, - } %> -
+ + + -
- <% end %> - - - <% if @current_user_enrolment&.coordinator? %> - - <%= render "coordinator_section", - course: @course, - current_user_enrolment: @current_user_enrolment, - topic_list: @topic_list, - my_topics: @my_topics, - current_user: @current_user, - my_student_projects: @my_student_projects, - incoming_proposals: @incoming_proposals, - lecturers: @lecturers %> - - <% elsif @current_user_enrolment&.lecturer? %> - - <%= render "lecturer_section", - course: @course, - current_user_enrolment: @current_user_enrolment, - topic_list: @topic_list, - my_topics: @my_topics, - current_user: @current_user, - my_student_projects: @my_student_projects, - incoming_proposals: @incoming_proposals, - lecturers: @lecturers %> - - <% else %> - - <%= render "student_section", - course: @course, - current_user_enrolment: @current_user_enrolment, - topic_list: @topic_list, - project: @project, - lecturers: @lecturers %> - <% end %> - - <%= render "participants", - course: @course, - use_progress_updates: @course.use_progress_updates %> + + + + + +
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 1411f823..1db92241 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -25,61 +25,19 @@ class=" min-h-screen flex flex-col font-['Inter'] <%= content_for?(:body_class) ? yield(:body_class) : "bg-[#efefef]" %> " - data-controller="scroll-spy sidebar" + data-controller="scroll-spy" data-scroll-spy-active-class="bg-transparent md:bg-sky-100 text-gray-950 font-semibold" data-scroll-spy-inactive-class="text-gray-600 hover:bg-gray-50 hover:text-gray-900" > - <%= render "shared/topbar", user: current_user, course: @course %> - -
+
<% unless content_for?(:no_sidebar) %> <%= yield :sidebar %> <% end %> -
- - <% unless content_for?(:hide_toggler) && content_for?(:hide_breadcrumbs) %> - -
- - <% unless content_for?(:hide_toggler) %> - - <% end %> +
-
- <% unless content_for?(:hide_breadcrumbs) %> - <% render_custom_breadcrumbs %> - <% if content_for?(:breadcrumbs) %> - <%= yield :breadcrumbs %> - <% else %> - <%= link_to "Home", root_path, class: "hover:text-gray-900 transition-colors" %> - <% end %> - <% end %> -
- -
- - <% end %> + <%= render "shared/header" %> <%= yield %> diff --git a/app/views/participants/index.html.erb b/app/views/participants/index.html.erb index ac30668b..27ccf8ca 100644 --- a/app/views/participants/index.html.erb +++ b/app/views/participants/index.html.erb @@ -2,17 +2,4 @@ <%= stylesheet_link_tag "courses", "data-turbo-track": "reload" %> <% end %> <% breadcrumb :course, @course %> -<% content_for :sidebar do %> - -<% end %> - <%= render 'courses/participants', course: @course, fullpage: true %> diff --git a/app/views/projects/_proposal_list_item.html.erb b/app/views/projects/_proposal_list_item.html.erb new file mode 100644 index 00000000..082a537f --- /dev/null +++ b/app/views/projects/_proposal_list_item.html.erb @@ -0,0 +1,36 @@ +<% icon_bg, icon_fg, badge_bg, badge_fg, verb = + case proposal.current_status + when "pending" + ["bg-[#E8F0FE]", "text-[#1A73E8]", "bg-[#E8F0FE]", "text-[#1967D2]", "Submitted"] + when "redo" + ["bg-[#FFF8E1]", "text-[#F57F17]", "bg-[#FFF8E1]", "text-[#F57F17]", "Returned for revision"] + when "rejected" + ["bg-[#FCE8E6]", "text-[#C5221F]", "bg-[#FCE8E6]", "text-[#C5221F]", "Rejected"] + else + ["bg-[#E8F0FE]", "text-[#1A73E8]", "bg-[#E8F0FE]", "text-[#1967D2]", "Updated"] + end %> + +
+
+
+ description +
+
+

+ <%= proposal.current_title %> +

+

+ <%= proposal.owner_name %> • <%= verb %> <%= time_ago_in_words(proposal.current_instance&.updated_at || proposal.updated_at) %> ago +

+
+
+
+ + <%= proposal.current_status.capitalize %> + + +
+
diff --git a/app/views/shared/_header.html.erb b/app/views/shared/_header.html.erb new file mode 100644 index 00000000..58560c61 --- /dev/null +++ b/app/views/shared/_header.html.erb @@ -0,0 +1,27 @@ +
+
+ <% unless content_for?(:hide_toggler) %> + + <% end %> + + ProPro + + <% unless content_for?(:hide_breadcrumbs) %> + <% render_custom_breadcrumbs %> + <% if content_for?(:breadcrumbs) %> + <%= yield :breadcrumbs %> + <% else %> + <%= link_to "Home", root_path, class: "hover:text-gray-900 transition-colors" %> + <% end %> + <% end %> +
+ + <%= button_to "Log out", session_path, method: :delete, + form: { data: { turbo_confirm: nil } }, + class: "text-[15px] font-medium text-[#5F6368] hover:text-[#3C4043] transition-colors pr-2 bg-transparent border-0 cursor-pointer" %> +
diff --git a/app/views/shared/_sidebar.html.erb b/app/views/shared/_sidebar.html.erb new file mode 100644 index 00000000..1daac054 --- /dev/null +++ b/app/views/shared/_sidebar.html.erb @@ -0,0 +1,54 @@ + + + diff --git a/app/views/shared/_topbar.html.erb b/app/views/shared/_topbar.html.erb deleted file mode 100644 index 4d58e36c..00000000 --- a/app/views/shared/_topbar.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -
- - <%= link_to "ProPro", - root_path, - class: "shrink-0 px-5 py-5 text-[25px] font-bold text-black no-underline" %> - -
diff --git a/app/views/topics/index.html.erb b/app/views/topics/index.html.erb index 1965881f..719ee1a5 100644 --- a/app/views/topics/index.html.erb +++ b/app/views/topics/index.html.erb @@ -7,8 +7,6 @@ <% content_for :title, "Topics List - #{@course.course_name.truncate(TITLE_NAME_LIMIT)} | ProPro" %> -<%= render "shared/topbar" %> -
diff --git a/config/breadcrumbs.rb b/config/breadcrumbs.rb index fcad3fc9..8f00520e 100644 --- a/config/breadcrumbs.rb +++ b/config/breadcrumbs.rb @@ -68,17 +68,6 @@ end end -crumb :topic do |topic| - link topic.topic_instances.last&.title - if params[:lecturer_id] - lecturer = User.find(params[:lecturer_id]) - parent :lecturer, topic.course, lecturer - elsif params[:from_new_project] - parent :topics, topic.course - else - parent :topics, topic.course - end -end crumb :new_topic do |course| link 'New Topic', new_course_topic_path(course) parent :topics, course diff --git a/test/controllers/courses_controller_test.rb b/test/controllers/courses_controller_test.rb new file mode 100644 index 00000000..7880890e --- /dev/null +++ b/test/controllers/courses_controller_test.rb @@ -0,0 +1,78 @@ +require 'test_helper' + +class CoursesControllerTest < ActionDispatch::IntegrationTest + setup do + @course = create(:course) + @coordinator_user = create(:user) + @coordinator_enrolment = create(:enrolment, :coordinator, user: @coordinator_user, course: @course) + + @lecturer_user = create(:user, :staff) + @lecturer_enrolment = create(:enrolment, :lecturer, user: @lecturer_user, course: @course) + + @student_user = create(:user) + @student_enrolment = create(:enrolment, user: @student_user, course: @course) + end + + test "show renders successfully for coordinator" do + sign_in @coordinator_user + get course_path(@course) + assert_response :success + assert_select "button", text: "Project Details" + assert_select "button", text: "To Review" + assert_select "button", text: "Supervised Projects" + assert_select "button", text: "Topic Directory" + assert_select "button", text: "People" + end + + test "show renders successfully for lecturer" do + sign_in @lecturer_user + get course_path(@course) + assert_response :success + assert_select "button", text: "Project Details" + assert_select "button", text: "To Review" + assert_select "button", text: "Supervised Projects" + end + + test "show renders successfully for student" do + sign_in @student_user + get course_path(@course) + assert_response :success + assert_select "button", text: "Project Details" + assert_select "button", text: "To Review" + assert_select "button", text: "Topic Directory" + assert_select "button", text: "People" + assert_select "button", text: "Supervised Projects", count: 0 + end + + test "show displays pending proposals in to review tab" do + sign_in @coordinator_user + supervisor_enrolment = @coordinator_enrolment + pending_project = create(:project, course: @course, supervisor_enrolment: supervisor_enrolment, status: :pending) + create(:project_instance, project: pending_project, supervisor_enrolment: supervisor_enrolment, created_by: @student_user, status: :pending, title: "Test Proposal") + + get course_path(@course) + assert_response :success + assert_select "h2", text: "Pending Proposals" + end + + test "show settings link uses settings_course_path" do + sign_in @coordinator_user + get course_path(@course) + assert_response :success + assert_select "a[href=?]", settings_course_path(@course), text: "Settings" + end + + test "show displays course description in project details" do + @course.update!(course_description: "My test description") + sign_in @student_user + get course_path(@course) + assert_response :success + assert_includes response.body, "My test description" + end + + private + + def sign_in(user) + post session_path, params: { email_address: user.email_address, password: 'password' } + end +end diff --git a/test/system/courses/course_tabs_test.rb b/test/system/courses/course_tabs_test.rb new file mode 100644 index 00000000..4b6ef7ef --- /dev/null +++ b/test/system/courses/course_tabs_test.rb @@ -0,0 +1,103 @@ +require 'application_system_test_case' + +class CourseTabsTest < ApplicationSystemTestCase + setup do + @course = create(:course) + + @coordinator_user = create(:user) + @coordinator_enrolment = create(:enrolment, :coordinator, user: @coordinator_user, course: @course) + + @lecturer_user = create(:user, :staff) + @lecturer_enrolment = create(:enrolment, :lecturer, user: @lecturer_user, course: @course) + + @student_user = create(:user) + create(:enrolment, user: @student_user, course: @course) + end + + test "coordinator sees all five tabs" do + login_as @coordinator_user + visit course_path(@course) + + assert_text "Project Details" + assert_text "To Review" + assert_text "Supervised Projects" + assert_text "Topic Directory" + assert_text "People" + assert_text "Settings" + end + + test "lecturer sees all five tabs" do + login_as @lecturer_user + visit course_path(@course) + + assert_text "Project Details" + assert_text "To Review" + assert_text "Supervised Projects" + assert_text "Topic Directory" + assert_text "People" + assert_text "Settings" + end + + test "student sees four tabs no supervised projects" do + login_as @student_user + visit course_path(@course) + + assert_text "Project Details" + assert_text "To Review" + assert_text "Topic Directory" + assert_text "People" + assert_text "Settings" + assert_no_text "Supervised Projects" + end + + test "to review tab shows pending proposals" do + pending_project = create(:project, course: @course, supervisor_enrolment: @coordinator_enrolment, status: :pending) + create(:project_instance, project: pending_project, supervisor_enrolment: @coordinator_enrolment, created_by: @student_user, status: :pending, title: "Test Proposal") + + login_as @coordinator_user + visit course_path(@course) + + click_button "To Review" + assert_text "Pending Proposals" + assert_text "Test Proposal" + end + + test "to review tab shows reviewed proposals" do + redo_project = create(:project, course: @course, supervisor_enrolment: @coordinator_enrolment, status: :redo) + create(:project_instance, project: redo_project, supervisor_enrolment: @coordinator_enrolment, created_by: @student_user, status: :redo, title: "Redo Proposal") + + login_as @coordinator_user + visit course_path(@course) + + click_button "To Review" + assert_text "Reviewed Proposals" + assert_text "Redo Proposal" + end + + test "supervised projects tab shows only approved projects" do + approved = create(:project, course: @course, supervisor_enrolment: @lecturer_enrolment, status: :approved) + create(:project_instance, project: approved, supervisor_enrolment: @lecturer_enrolment, created_by: @student_user, status: :approved, title: "Approved Project") + + login_as @lecturer_user + visit course_path(@course) + + click_button "Supervised Projects" + assert_text "Approved Project" + end + + test "settings link uses correct path" do + login_as @coordinator_user + visit course_path(@course) + + click_link "Settings" + assert_current_path settings_course_path(@course) + end + + test "project details tab shows course description" do + @course.update!(course_description: "Test course description") + login_as @student_user + visit course_path(@course) + + assert_text "Test course description" + end +end From 88dbc653dc3886c3e2c9b014dd117ed170ec07ba Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 27 Aug 2026 08:27:25 +0800 Subject: [PATCH 02/53] feat: fix js issues + auth issues --- app/javascript/controllers/tabs_controller.js | 13 ++++++++-- app/views/courses/show.html.erb | 7 ++---- app/views/projects/_comment.html.erb | 18 ++++++++++++++ app/views/projects/_comments_panel.html.erb | 24 +++++++++++++++++++ .../projects/_proposal_list_item.html.erb | 5 ++-- 5 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 app/views/projects/_comment.html.erb create mode 100644 app/views/projects/_comments_panel.html.erb diff --git a/app/javascript/controllers/tabs_controller.js b/app/javascript/controllers/tabs_controller.js index 56bea9b5..7e23e10d 100644 --- a/app/javascript/controllers/tabs_controller.js +++ b/app/javascript/controllers/tabs_controller.js @@ -32,8 +32,17 @@ export default class extends Controller { this.tabTargets.forEach((tab, i) => { const isActive = i === index - tab.classList.toggle(this.hasActiveClass ? this.activeClass : "text-[#1A73E8] border-[#1A73E8]", isActive) - tab.classList.toggle(this.hasInactiveClass ? this.inactiveClass : "text-[#5F6368] border-transparent", !isActive) + + const activeClasses = (this.hasActiveClass ? this.activeClass : "text-[#1A73E8] border-[#1A73E8]").split(" ") + const inactiveClasses = (this.hasInactiveClass ? this.inactiveClass : "text-[#5F6368] border-transparent").split(" ") + + if (isActive) { + tab.classList.add(...activeClasses) + tab.classList.remove(...inactiveClasses) + } else { + tab.classList.add(...inactiveClasses) + tab.classList.remove(...activeClasses) + } }) } } diff --git a/app/views/courses/show.html.erb b/app/views/courses/show.html.erb index 4f460c86..f712bc22 100644 --- a/app/views/courses/show.html.erb +++ b/app/views/courses/show.html.erb @@ -1,6 +1,4 @@ -<% content_for :hide_toggler, true %> -<% content_for :no_sidebar, true %> -<% content_for :hide_breadcrumbs, true %> +<% content_for :body_class, "bg-[#f8fafd]" %> <% breadcrumb :course, @course %> @@ -8,11 +6,10 @@ -
+
<%= render "shared/sidebar" %> -
diff --git a/app/views/projects/_comment.html.erb b/app/views/projects/_comment.html.erb new file mode 100644 index 00000000..9151dca0 --- /dev/null +++ b/app/views/projects/_comment.html.erb @@ -0,0 +1,18 @@ +<%# locals: (comment:) %> +
+ <%= render "shared/avatar", label: comment.author_initials, variant: :filled, size: "w-8 h-8", text_size: "text-xs" %> +
+
+ <%= comment.author_name %> + <%= time_ago_in_words(comment.created_at) %> ago +
+

<%= comment.body %>

+
+ <% if comment.author == current_user %> + <%= button_to comment_path(comment), method: :delete, + form: { data: { turbo_confirm: "Delete this comment?" } }, + class: "text-[#5F6368] hover:text-[#C5221F] opacity-0 group-hover:opacity-100 transition-opacity self-start mt-0.5 bg-transparent border-0 p-0" do %> + close + <% end %> + <% end %> +
diff --git a/app/views/projects/_comments_panel.html.erb b/app/views/projects/_comments_panel.html.erb new file mode 100644 index 00000000..33611581 --- /dev/null +++ b/app/views/projects/_comments_panel.html.erb @@ -0,0 +1,24 @@ +<%# locals: (project:, comments:) %> +
+
+

Comments

+ <%= comments.size %> +
+ +
+
+ Comments for V<%= project.current_version_number %> +
+ <%= render partial: "projects/comment", collection: comments %> +
+ +
+ <%= form_with model: [project, Comment.new], + class: "border border-[#DADCE0] rounded-lg focus-within:border-[#1A73E8] focus-within:ring-1 focus-within:ring-[#1A73E8] transition-all bg-white flex items-end" do |f| %> + <%= f.text_area :body, rows: 1, class: "w-full bg-transparent border-none p-3 text-[#3C4043] focus:ring-0 text-[14px] resize-none min-h-[44px] outline-none", placeholder: "Write a comment..." %> + <%= f.button type: "submit", class: "p-2 m-1 text-[#1A73E8] hover:bg-[#F1F3F4] rounded-full transition-colors flex items-center justify-center" do %> + send + <% end %> + <% end %> +
+
diff --git a/app/views/projects/_proposal_list_item.html.erb b/app/views/projects/_proposal_list_item.html.erb index 082a537f..56ed4937 100644 --- a/app/views/projects/_proposal_list_item.html.erb +++ b/app/views/projects/_proposal_list_item.html.erb @@ -10,7 +10,8 @@ ["bg-[#E8F0FE]", "text-[#1A73E8]", "bg-[#E8F0FE]", "text-[#1967D2]", "Updated"] end %> -
+<%= link_to course_project_path(@course, proposal), + class: "group flex items-center justify-between p-4 hover:bg-[#F8F9FA] cursor-pointer transition-colors no-underline text-inherit" do %>
description @@ -33,4 +34,4 @@ more_vert
-
+<% end %> \ No newline at end of file From 9cdb82538dcc437df7f9e9d8d12d2959cbec37ca Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 27 Aug 2026 09:16:54 +0800 Subject: [PATCH 03/53] feat: fixed courses/show coordinator enrolment bug --- app/javascript/controllers/tabs_controller.js | 38 ++- app/views/courses/show.html.erb | 2 +- .../projects/_proposal_list_item.html.erb | 2 +- docs/projects_show_refactor_plan.md | 313 ++++++++++++++++++ 4 files changed, 338 insertions(+), 17 deletions(-) create mode 100644 docs/projects_show_refactor_plan.md diff --git a/app/javascript/controllers/tabs_controller.js b/app/javascript/controllers/tabs_controller.js index 7e23e10d..d46f99ed 100644 --- a/app/javascript/controllers/tabs_controller.js +++ b/app/javascript/controllers/tabs_controller.js @@ -1,4 +1,4 @@ -import { Controller } from "@hotwired/stimulus" +import { Controller } from "@hotwired/stimulus"; // Generic tab controller — deliberately NOT mobile_tabs_controller.js, // which is hard-coded to 3 named targets for the project/topic show pages @@ -16,33 +16,41 @@ import { Controller } from "@hotwired/stimulus" // to a separate page), not a data-tabs-target="tab" — it doesn't participate // in this controller at all. export default class extends Controller { - static targets = ["tab", "panel"] - static classes = ["active", "inactive"] + static targets = ["tab", "panel"]; + static classes = ["active", "inactive"]; connect() { - this.show({ params: { index: 0 } }) + this.show({ params: { index: 0 } }); } show(event) { - const index = Number(event.params.index) + const index = Number(event.params.index); this.panelTargets.forEach((panel, i) => { - panel.classList.toggle("hidden", i !== index) - }) + panel.classList.toggle("hidden", i !== index); + }); this.tabTargets.forEach((tab, i) => { - const isActive = i === index + const isActive = i === index; - const activeClasses = (this.hasActiveClass ? this.activeClass : "text-[#1A73E8] border-[#1A73E8]").split(" ") - const inactiveClasses = (this.hasInactiveClass ? this.inactiveClass : "text-[#5F6368] border-transparent").split(" ") + const activeClasses = ( + this.hasActiveClass + ? this.activeClass + : "text-[#1A73E8] border-[#1A73E8]" + ).split(" "); + const inactiveClasses = ( + this.hasInactiveClass + ? this.inactiveClass + : "text-[#5F6368] border-transparent" + ).split(" "); if (isActive) { - tab.classList.add(...activeClasses) - tab.classList.remove(...inactiveClasses) + tab.classList.add(...activeClasses); + tab.classList.remove(...inactiveClasses); } else { - tab.classList.add(...inactiveClasses) - tab.classList.remove(...activeClasses) + tab.classList.add(...inactiveClasses); + tab.classList.remove(...activeClasses); } - }) + }); } } diff --git a/app/views/courses/show.html.erb b/app/views/courses/show.html.erb index f712bc22..c3f69f00 100644 --- a/app/views/courses/show.html.erb +++ b/app/views/courses/show.html.erb @@ -4,7 +4,7 @@ <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> - +
diff --git a/app/views/projects/_proposal_list_item.html.erb b/app/views/projects/_proposal_list_item.html.erb index 56ed4937..2c48d4d5 100644 --- a/app/views/projects/_proposal_list_item.html.erb +++ b/app/views/projects/_proposal_list_item.html.erb @@ -34,4 +34,4 @@ more_vert
-<% end %> \ No newline at end of file +<% end %> diff --git a/docs/projects_show_refactor_plan.md b/docs/projects_show_refactor_plan.md new file mode 100644 index 00000000..a369c324 --- /dev/null +++ b/docs/projects_show_refactor_plan.md @@ -0,0 +1,313 @@ +# ProPro Redesign — projects/show Refactor & Redesign — Master Plan + +Grounded against `hackerspacemmu/ProPro` @ `main` (`0d9e1e8`). All +identifiers and queries verified against actual controller/model/view code. +File:line references included for re-verification. + +**Prerequisite:** assumes `courses/show` Tickets 1 & 2 (`shared/_header`, +`shared/_sidebar` rendered unconditionally from `layouts/application.html.erb`) +are merged. This doc wires `projects/show` into that layout. + +--- + +## 1. Query / Path Audit + +| Claimed correct form | Status | Evidence | +|---|---|---| +| `current_user.courses` | ✅ | `User` has `has_many :courses, through: :enrolments` (`user.rb:9`). | +| `user_profile_path` | ✅ | `get 'user/profile'` (`routes.rb:9`) auto-names to `user_profile_path`. | +| `settings_course_path(@course)` | ✅ | `get 'settings'` under member block (`routes.rb:37`). | +| `proposal.owner_name` | ✅ **already exists** | `Project#owner_name` (`project.rb:61-69`). The other agent's plan was wrong about this not existing — it does. The card partials just inline-duplicate it. `Topic` does NOT have `owner_name` yet — that's genuinely new work for a follow-up. | +| `proposal.current_status` | ✅ | `Project#current_status` (`project.rb:53-55`). | +| `proposal.current_instance&.updated_at` | ✅ | `Project#current_instance` (`project.rb:49-51`). `projects.updated_at` is stale (nothing `touch:`es the parent). | + +**Net:** all six are available. `owner_name` already exists on `Project` +and just needs to be called from the card partials instead of inlined. +`Topic#owner_name` is new work but out of scope for this ticket. + +--- + +## 2. What's already correct — do not rebuild + +`ProjectsController#show` (`projects_controller.rb:5-48`) computes +everything the new design needs. **No controller changes required.** + +Already correct and reusable as-is: +- Comments pane (`_project_comments.html.erb`) — groups by version, + gates composer behind policy, uses locals only. +- Version diff engine — `HTMLDiff`-based comparison in + `_project_details.html.erb:141-375`. Needs to be **moved**, not written. +- Version navigation — prev/next arrows in `_project_header.html.erb:307-348`. +- Status change endpoint — `change_status_course_project_path` + + `ProjectPolicy#change_status?` already work. + +--- + +## 3. Ticket List + +### Ticket 1 — Shared tab-shell controller + +**Files:** +- Reuse the existing generic `tabs_controller.js` from courses/show. It is + already index-based with `data-tabs-target="tab"` / `"panel"` and + `data-tabs-index-param`. No changes needed to it. +- **Do NOT delete `mobile_tabs_controller.js`** — `topics/show.html.erb:43` + also uses `data-controller="mobile-tabs"`. It stays until topics/show + is migrated. Only `projects/show` stops referencing it. + +### Ticket 2 — Extract "Compare Versions" into its own tab + +**Files:** +- New `app/views/projects/_compare_versions_tab.html.erb` — **move** + (cut/paste) the `VERSION DIFF COMPARISON` block from + `_project_details.html.erb:141-375`. Same locals: `fields`, `next_fields`, + `index`, `instances`. +- Modify `app/views/projects/_project_details.html.erb` — delete the moved + block. What's left is purely the field list + "Based on Topic" footer. +- No controller changes. + +**Decision (open item 1):** When there's nothing to compare (single +version), show the tab with an empty state message: "Only one version +exists — nothing to compare yet." The tab is always visible; the empty +state prevents confusion about why the panel is blank. + +### Ticket 3 — Restyle project header, convert status control to split-button + +**Files:** +- Modify `app/views/projects/_project_header.html.erb` — visual restyle + (Material palette, pill badges). Same locals. +- Replace the inline `` dropdown** per the mockup. + New tiny `app/javascript/controllers/version_select_controller.js` — + navigates to `course_project_path(course, project, version: N)` on + change. Replaces the prev/next arrows. + +### Ticket 4 — Restyle Comments pane + +**Files:** +- Modify `app/views/projects/_project_comments.html.erb` — visual + restyle only. No behavior or local changes. +- On mobile, Comments becomes a 4th tab in the shared tab shell + (confirmed decision, open item 4). + +### Ticket 5 — Restyle Progress Updates tab + +**Files:** +- Modify `app/views/projects/_progress_updates.html.erb` — visual + restyle only. +- No controller change (`@progress`/`@weeks` already only set when + `@course.use_progress_updates`, `projects_controller.rb:44-47`). + +### Ticket 6 — Wire the new shell into `show.html.erb` + +**Files:** +- Modify `app/views/projects/show.html.erb`: + - Replace `data-controller="mobile-tabs"` + two-column layout with + single-column layout using `data-controller="tabs"`. + - Tab "Project Details" → `render "project_header"` + + `render "project_details"` + `render "project_actions"` + - Tab "Compare Versions" → `render "compare_versions_tab"` (Ticket 2) + - Tab "Progress Updates" → `render "progress_updates"` (conditional on + `@course.use_progress_updates`) + - Tab "Comments" → `render "project_comments"` (always present, + becomes a 4th tab on all viewports) + - Drop `content_for :hide_toggler, true` once the header/sidebar + ticket's final shape makes it dead code. +- `_project_actions.html.erb` stays unchanged — just fit into the new + shell's density. + +**No Settings link** — confirmed: the projects mockup has no Settings +tab/link. Projects don't have a separate settings page like courses do. + +### Ticket 7 — `Project#owner_name` cleanup (cross-cutting, independent) + +**Files:** +- `Project#owner_name` already exists (`project.rb:61-69`). No model + change needed. +- Update `app/views/courses/_project_card_contents.html.erb:18-24` to + call `project.owner_name` instead of inlining the ternary. +- Update `app/views/courses/_topic_card_contents.html.erb:52-58` to + call `topic.owner_name` — but `Topic` doesn't have this method yet. + Either add `Topic#owner_name` (same implementation) or leave the + inline logic for now. Recommend adding the method for consistency, + flagged as a small follow-up. +- Not used by `projects/show` itself — this is cleanup while we're + touching the codebase. + +### Ticket 8 — Delete dead partials + +**Files:** +- Delete `app/views/projects/_project_fields.html.erb` — confirmed only + referenced at `show.html.erb:160`. Desktop-only Turbo tab wrapper, + replaced by unified Stimulus tabs. +- Delete `app/views/projects/_comments_panel.html.erb` — dead code. + Legacy Google-Material panel, never rendered from show.html.erb. + References non-existent methods (`project.current_version_number`, + `comment.body`, `comment.author`). +- **Do NOT delete `mobile_tabs_controller.js`** — still used by + `topics/show.html.erb:43`. + +--- + +## 4. File Operations Summary + +### New files (5) +1. `app/views/projects/_compare_versions_tab.html.erb` +2. `app/javascript/controllers/dropdown_controller.js` +3. `app/javascript/controllers/version_select_controller.js` +4. (reuse existing `app/javascript/controllers/tabs_controller.js` — no new file) + +### Modified files (4) +5. `app/views/projects/show.html.erb` — rewrite (unified tab shell) +6. `app/views/projects/_project_header.html.erb` — restyle, split-button +7. `app/views/projects/_project_comments.html.erb` — restyle +8. `app/views/projects/_progress_updates.html.erb` — restyle +9. `app/views/projects/_project_details.html.erb` — remove version diff + block (moved to _compare_versions_tab) + +### Deleted files (2) +10. `app/views/projects/_project_fields.html.erb` +11. `app/views/projects/_comments_panel.html.erb` + +### Untouched files +- `app/views/projects/_project_actions.html.erb` — no changes +- `app/views/projects/_comment.html.erb` — no changes +- `app/controllers/projects_controller.rb` — no changes +- `app/javascript/controllers/tabs_controller.js` — no changes +- `app/javascript/controllers/scroll_to_bottom_controller.js` — no changes +- `app/javascript/controllers/field_expand_modal_controller.js` — no changes +- `app/javascript/controllers/textarea_resize_controller.js` — no changes +- `app/javascript/controllers/mobile_tabs_controller.js` — NOT deleted + (still used by topics/show) +- `app/views/projects/edit.html.erb` — no changes +- `app/views/projects/new.html.erb` — no changes + +--- + +## 5. Target Architecture + +``` +┌──────────────────────────────────────────────────────────┐ +│ Tab bar (Stimulus tabs#show) │ +│ [Project Details] [Compare Versions] [Progress*] [Comments] │ +├──────────────────────────────────────────────────────────┤ +│ Panel 0: Project Details │ +│ ┌──────────────────────────────────────────────────────┐│ +│ │ _project_header (title, props, status split-button, ││ +│ │ version select dropdown) ││ +│ │ _project_details (fields, expand modal, topic footer)││ +│ │ _project_actions (edit, jump-to-latest) ││ +│ └──────────────────────────────────────────────────────┘│ +├──────────────────────────────────────────────────────────┤ +│ Panel 1: Compare Versions │ +│ ┌──────────────────────────────────────────────────────┐│ +│ │ _compare_versions_tab (HTMLDiff table, or empty ││ +│ │ state if single version) ││ +│ └──────────────────────────────────────────────────────┘│ +├──────────────────────────────────────────────────────────┤ +│ Panel 2: Progress Updates* (conditional) │ +│ ┌──────────────────────────────────────────────────────┐│ +│ │ _progress_updates (list with rating badges, edit/del)││ +│ └──────────────────────────────────────────────────────┘│ +├──────────────────────────────────────────────────────────┤ +│ Panel 3: Comments │ +│ ┌──────────────────────────────────────────────────────┐│ +│ │ _project_comments (grouped list + composer) ││ +│ └──────────────────────────────────────────────────────┘│ +└──────────────────────────────────────────────────────────┘ + +* Progress Updates tab shown only if @course.use_progress_updates +``` + +**Key decisions applied:** +- Four tabs, not three — Compare Versions gets its own tab. +- Comments are a tab on all viewports (mobile + desktop), not a sidebar. +- No Settings link — projects don't have a settings page. +- Split-button for status change (Approve / Request Changes / Reject). +- Version `` dropdown per mockup, with + `version_select_controller.js`. +4. **Comments on mobile** → 4th tab in the shared tab shell. +5. **`owner_name` precedence** → Already defined on `Project` at + `project.rb:61-69` as `:name`-first-then-`:group_name`. The card + partials just need to call the model method instead of inlining it. + No behavior change. +6. **Settings link** → Not in the mockup. Projects don't have a settings + page. No link added. +7. **Bottom "Back to Course"** → Removed (top link sufficient on tabbed + page). Confirmed by user. +8. **`_project_fields.html.erb` deletion** → Confirmed only referenced + at `show.html.erb:160`. Safe to delete. +9. **`mobile_tabs_controller.js`** → **NOT deleted** — still used by + `topics/show.html.erb:43`. Both original plans were wrong about this. From b93187da47ac3b7668c9e83e40ebf7071e5a60fe Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 27 Aug 2026 10:23:28 +0800 Subject: [PATCH 04/53] temp commit for fallback --- .../controllers/dropdown_controller.js | 40 ++ .../record_update_modal_controller.js | 26 + .../controllers/version_select_controller.js | 14 + app/models/topic.rb | 10 + .../courses/_project_card_contents.html.erb | 9 +- .../courses/_topic_card_contents.html.erb | 9 +- app/views/projects/_comments_panel.html.erb | 24 - .../projects/_compare_versions_tab.html.erb | 264 +++++++++++ app/views/projects/_context_header.html.erb | 158 ++++++ app/views/projects/_progress_updates.html.erb | 260 +++++----- app/views/projects/_project_comments.html.erb | 141 +++--- app/views/projects/_project_details.html.erb | 236 --------- app/views/projects/_project_fields.html.erb | 70 --- app/views/projects/_project_header.html.erb | 373 --------------- app/views/projects/_project_overview.html.erb | 85 ++++ .../projects/_record_update_modal.html.erb | 90 ++++ app/views/projects/show.html.erb | 374 ++++----------- app/views/shared/_sidebar.html.erb | 2 +- docs/projects_show_refactor_plan.md | 448 +++++++++++------- 19 files changed, 1248 insertions(+), 1385 deletions(-) create mode 100644 app/javascript/controllers/dropdown_controller.js create mode 100644 app/javascript/controllers/record_update_modal_controller.js create mode 100644 app/javascript/controllers/version_select_controller.js delete mode 100644 app/views/projects/_comments_panel.html.erb create mode 100644 app/views/projects/_compare_versions_tab.html.erb create mode 100644 app/views/projects/_context_header.html.erb delete mode 100644 app/views/projects/_project_fields.html.erb delete mode 100644 app/views/projects/_project_header.html.erb create mode 100644 app/views/projects/_project_overview.html.erb create mode 100644 app/views/projects/_record_update_modal.html.erb diff --git a/app/javascript/controllers/dropdown_controller.js b/app/javascript/controllers/dropdown_controller.js new file mode 100644 index 00000000..67cfd02b --- /dev/null +++ b/app/javascript/controllers/dropdown_controller.js @@ -0,0 +1,40 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["menu"] + + connect() { + this.boundClickOutside = this.clickOutside.bind(this) + } + + disconnect() { + document.removeEventListener("click", this.boundClickOutside) + } + + toggle(event) { + event.stopPropagation() + const isVisible = !this.menuTarget.classList.contains("hidden") + + // Close all other open dropdowns first + document.querySelectorAll('[data-dropdown-target="menu"]').forEach((menu) => { + if (menu !== this.menuTarget) { + menu.classList.add("hidden") + } + }) + + if (isVisible) { + this.menuTarget.classList.add("hidden") + document.removeEventListener("click", this.boundClickOutside) + } else { + this.menuTarget.classList.remove("hidden") + document.addEventListener("click", this.boundClickOutside) + } + } + + clickOutside(event) { + if (!this.element.contains(event.target)) { + this.menuTarget.classList.add("hidden") + document.removeEventListener("click", this.boundClickOutside) + } + } +} diff --git a/app/javascript/controllers/record_update_modal_controller.js b/app/javascript/controllers/record_update_modal_controller.js new file mode 100644 index 00000000..eeaf8dd6 --- /dev/null +++ b/app/javascript/controllers/record_update_modal_controller.js @@ -0,0 +1,26 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["dialog"] + + connect() { + this.boundCloseOnBackdrop = this.closeOnBackdrop.bind(this) + this.dialogTarget.addEventListener("click", this.boundCloseOnBackdrop) + } + + disconnect() { + this.dialogTarget.removeEventListener("click", this.boundCloseOnBackdrop) + } + + open() { + this.dialogTarget.showModal() + } + + close() { + this.dialogTarget.close() + } + + closeOnBackdrop(event) { + if (event.target === this.dialogTarget) this.close() + } +} diff --git a/app/javascript/controllers/version_select_controller.js b/app/javascript/controllers/version_select_controller.js new file mode 100644 index 00000000..b6ae11de --- /dev/null +++ b/app/javascript/controllers/version_select_controller.js @@ -0,0 +1,14 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static values = { + course: Number, + project: Number, + } + + navigate(event) { + const version = event.target.value + const url = `/courses/${this.courseValue}/projects/${this.projectValue}?version=${version}` + window.location.href = url + } +} diff --git a/app/models/topic.rb b/app/models/topic.rb index 5039781b..a29c8dfa 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -56,6 +56,16 @@ def current_title current_instance&.title || title end + def owner_name + if owner.respond_to?(:name) + owner.name + elsif owner.respond_to?(:group_name) + owner.group_name + else + 'Unknown' + end + end + def instance_to_edit(created_by:, has_coordinator_comment:, status:) if rejected? || redo? || (pending? && has_coordinator_comment) topic_instances.build( diff --git a/app/views/courses/_project_card_contents.html.erb b/app/views/courses/_project_card_contents.html.erb index e5238eeb..494dba80 100644 --- a/app/views/courses/_project_card_contents.html.erb +++ b/app/views/courses/_project_card_contents.html.erb @@ -15,14 +15,7 @@ found_text = desc_field&.value || fields.first&.value description_text = found_text if found_text.present? # --- Determine Owner Name --- -owner_name = - if project.owner.respond_to?(:name) - project.owner.name - elsif project.owner.respond_to?(:group_name) - project.owner.group_name - else - "Unknown" - end %> +owner_name = project.owner_name %>
+owner_name = topic.owner_name %> <%# --- CARD HEADER (Colored) --- %>
-
-
-

Comments

- <%= comments.size %> -
- -
-
- Comments for V<%= project.current_version_number %> -
- <%= render partial: "projects/comment", collection: comments %> -
- -
- <%= form_with model: [project, Comment.new], - class: "border border-[#DADCE0] rounded-lg focus-within:border-[#1A73E8] focus-within:ring-1 focus-within:ring-[#1A73E8] transition-all bg-white flex items-end" do |f| %> - <%= f.text_area :body, rows: 1, class: "w-full bg-transparent border-none p-3 text-[#3C4043] focus:ring-0 text-[14px] resize-none min-h-[44px] outline-none", placeholder: "Write a comment..." %> - <%= f.button type: "submit", class: "p-2 m-1 text-[#1A73E8] hover:bg-[#F1F3F4] rounded-full transition-colors flex items-center justify-center" do %> - send - <% end %> - <% end %> -
-
diff --git a/app/views/projects/_compare_versions_tab.html.erb b/app/views/projects/_compare_versions_tab.html.erb new file mode 100644 index 00000000..4eabe749 --- /dev/null +++ b/app/views/projects/_compare_versions_tab.html.erb @@ -0,0 +1,264 @@ +<% require "htmldiff" %> + +<% if index < instances.size %> + + <%# --- COMPARE TOOLBAR (deferred — see TODO below) --- %> + <%# TODO: Add two ` dropdown per mockup. - Status change endpoint — `change_status_course_project_path` + `ProjectPolicy#change_status?` already work. --- -## 3. Ticket List +## 3. Target Architecture — matched to mockup + +All three mockup files (`projects_show.html.erb`, `progress_updates.html.erb`, +`projects_version_comparison.html.erb`) share **identical shell**: + +``` +┌────────────────────────────────────────────────────┬──────────────────┐ +│ LEFT PANE (flex-1 flex flex-col overflow-y-auto) │ RIGHT PANE │ +│ │ (w-[380px] │ +│ ┌────────────────────────────────────────────────┐ │ shrink-0 │ +│ │ Context Header (sticky info) │ │ flex flex-col) │ +│ │ Status badge · Timestamp · Title │ │ ┌──────────────┐│ +│ │ [Version: ▾ select] [Approve ▾ split-btn] │ │ │ Comments ││ +│ ├────────────────────────────────────────────────┤ │ │ header + ││ +│ │ Sticky Tabs │ │ │ count badge ││ +│ │ [Project Details] [Compare Versions] │ │ ├──────────────┤│ +│ │ [Progress Updates] │ │ │ scrollable ││ +│ ├────────────────────────────────────────────────┤ │ │ comment list ││ +│ │ Tab Panel Content (swaps per active tab) │ │ │ (grouped by ││ +│ │ │ │ │ version) ││ +│ └────────────────────────────────────────────────┘ │ ├──────────────┤│ +│ │ │ Comment ││ +│ │ │ composer ││ +│ │ └──────────────┘│ +└────────────────────────────────────────────────────┴──────────────────┘ +``` + +**Critical constraint:** Comments are a **persistent sibling panel**, not a +tab. They are visible on every left-pane tab. This matches the current +working code (`w-[380px]`/`w-[450px]` right column in `show.html.erb:214-289`) +and all three mockups. + +### What changes vs. current code + +| Element | Current | Mockup | +|---------|---------|--------| +| Layout | Two-column with mobile/desktop split | Two-column, single rendering path | +| Mobile | `mobile-tabs` Stimulus, tabs hide/show | Same two-pane, comments折叠成4th tab on small screens | +| Context Header | `_project_header.html.erb` (373 lines): title + properties + status card + version card, all in one | Split: context header (status badge, title, version select, split-button) + "Project Overview" card in tab content | +| Status control | `` dropdown in context header | +| Tab shell | Desktop: Turbo `link_to` tabs (`_project_fields.html.erb`). Mobile: `mobile-tabs` Stimulus | Stimulus `tabs_controller.js` (index-based, same as courses/show) | +| Compare Versions | Embedded in `_project_details.html.erb:141-375` | Own tab with compare toolbar + side-by-side diff | +| Progress Updates | Conditional tab (desktop), hidden panel (mobile) | Always-visible tab (content hidden behind `use_progress_updates`) | +| Right sidebar | `w-[450px]`, desktop only | `w-[380px]`, always visible | +| Comments partial | `_project_comments.html.erb` (178 lines) | Restyled to match mockup | +| "Back to Course" link | Inline at top and bottom | Removed (sidebar provides navigation context) | + +--- + +## 4. Ticket List -### Ticket 1 — Shared tab-shell controller +### Ticket 1 — Tab shell controller **Files:** -- Reuse the existing generic `tabs_controller.js` from courses/show. It is - already index-based with `data-tabs-target="tab"` / `"panel"` and - `data-tabs-index-param`. No changes needed to it. +- Reuse existing `app/javascript/controllers/tabs_controller.js`. Already + generic, index-based. No changes needed. - **Do NOT delete `mobile_tabs_controller.js`** — `topics/show.html.erb:43` - also uses `data-controller="mobile-tabs"`. It stays until topics/show - is migrated. Only `projects/show` stops referencing it. + also uses `data-controller="mobile-tabs"`. Stays until topics/show is + migrated. ### Ticket 2 — Extract "Compare Versions" into its own tab @@ -66,125 +112,204 @@ Already correct and reusable as-is: block. What's left is purely the field list + "Based on Topic" footer. - No controller changes. -**Decision (open item 1):** When there's nothing to compare (single -version), show the tab with an empty state message: "Only one version -exists — nothing to compare yet." The tab is always visible; the empty -state prevents confusion about why the panel is blank. +**Behavior:** Tab always visible. When single version (`index >= instances.size`), +show empty state: "Only one version exists — nothing to compare yet." -### Ticket 3 — Restyle project header, convert status control to split-button +**Compare toolbar (new in mockup):** The mockup shows a version-pair selector +above the diff (two ``, split-button. ~50 lines. +2. **"Project Overview" card** (tab panel content, first item in Details tab): + Group Name, Type, Owners (with avatars), Supervisor. ~40 lines. **Files:** -- Modify `app/views/projects/_project_header.html.erb` — visual restyle - (Material palette, pill badges). Same locals. -- Replace the inline `` dropdown + - Split-button (Approve primary, dropdown for Request Changes / Reject) +- New `app/views/projects/_project_overview.html.erb` — extracted from + `_project_header.html.erb`. Contains: + - "Project Overview" card with Group Name, Type, Owners, Supervisor +- Modify `app/views/projects/_project_header.html.erb` — delete everything + extracted into the two new partials. What's left? Nothing — this partial + is fully decomposed. Delete it. - New `app/javascript/controllers/dropdown_controller.js` — generic open/close menu on click, close on outside-click/Escape. Needed for - the split-button caret dropdown. **Not** CSS `group-hover` (breaks on - touch). -- Version selector: **build the `` change. + +**Status badge colors (from mockup):** +- Pending: `bg-[#E8F0FE] text-[#1967D2]` +- Approved: `bg-[#E6F4EA] text-[#137333]` +- Redo: `bg-[#FFF8E1] text-[#F57F17]` +- Rejected: `bg-[#FCE8E6] text-[#C5221F]` + +**Split-button:** The primary button always says "Approve" (green). The +dropdown contains "Request Changes" (redo) and "Reject Proposal" (red hover). +All three hit `change_status_course_project_path` with the appropriate +`status:` param. Gated by `current_user == project.supervisor && current_version == latest_version`. + +**Version `` (No Progress / Unsatisfactory / Satisfactory / Excellent) + - Feedback ` + +
+
+ +
+ +
+ +
+
+
+ + \ No newline at end of file diff --git a/app/javascript/controllers/dropdown_controller.js b/app/javascript/controllers/dropdown_controller.js index 67cfd02b..0c9f4a67 100644 --- a/app/javascript/controllers/dropdown_controller.js +++ b/app/javascript/controllers/dropdown_controller.js @@ -1,40 +1,42 @@ -import { Controller } from "@hotwired/stimulus" +import { Controller } from "@hotwired/stimulus"; export default class extends Controller { - static targets = ["menu"] + static targets = ["menu"]; connect() { - this.boundClickOutside = this.clickOutside.bind(this) + this.boundClickOutside = this.clickOutside.bind(this); } disconnect() { - document.removeEventListener("click", this.boundClickOutside) + document.removeEventListener("click", this.boundClickOutside); } toggle(event) { - event.stopPropagation() - const isVisible = !this.menuTarget.classList.contains("hidden") + event.stopPropagation(); + const isVisible = !this.menuTarget.classList.contains("hidden"); // Close all other open dropdowns first - document.querySelectorAll('[data-dropdown-target="menu"]').forEach((menu) => { - if (menu !== this.menuTarget) { - menu.classList.add("hidden") - } - }) + document + .querySelectorAll('[data-dropdown-target="menu"]') + .forEach((menu) => { + if (menu !== this.menuTarget) { + menu.classList.add("hidden"); + } + }); if (isVisible) { - this.menuTarget.classList.add("hidden") - document.removeEventListener("click", this.boundClickOutside) + this.menuTarget.classList.add("hidden"); + document.removeEventListener("click", this.boundClickOutside); } else { - this.menuTarget.classList.remove("hidden") - document.addEventListener("click", this.boundClickOutside) + this.menuTarget.classList.remove("hidden"); + document.addEventListener("click", this.boundClickOutside); } } clickOutside(event) { if (!this.element.contains(event.target)) { - this.menuTarget.classList.add("hidden") - document.removeEventListener("click", this.boundClickOutside) + this.menuTarget.classList.add("hidden"); + document.removeEventListener("click", this.boundClickOutside); } } } diff --git a/app/javascript/controllers/record_update_modal_controller.js b/app/javascript/controllers/record_update_modal_controller.js index eeaf8dd6..32e958c1 100644 --- a/app/javascript/controllers/record_update_modal_controller.js +++ b/app/javascript/controllers/record_update_modal_controller.js @@ -1,26 +1,26 @@ -import { Controller } from "@hotwired/stimulus" +import { Controller } from "@hotwired/stimulus"; export default class extends Controller { - static targets = ["dialog"] + static targets = ["dialog"]; connect() { - this.boundCloseOnBackdrop = this.closeOnBackdrop.bind(this) - this.dialogTarget.addEventListener("click", this.boundCloseOnBackdrop) + this.boundCloseOnBackdrop = this.closeOnBackdrop.bind(this); + this.dialogTarget.addEventListener("click", this.boundCloseOnBackdrop); } disconnect() { - this.dialogTarget.removeEventListener("click", this.boundCloseOnBackdrop) + this.dialogTarget.removeEventListener("click", this.boundCloseOnBackdrop); } open() { - this.dialogTarget.showModal() + this.dialogTarget.showModal(); } close() { - this.dialogTarget.close() + this.dialogTarget.close(); } closeOnBackdrop(event) { - if (event.target === this.dialogTarget) this.close() + if (event.target === this.dialogTarget) this.close(); } } diff --git a/app/javascript/controllers/version_select_controller.js b/app/javascript/controllers/version_select_controller.js index b6ae11de..836fee62 100644 --- a/app/javascript/controllers/version_select_controller.js +++ b/app/javascript/controllers/version_select_controller.js @@ -1,14 +1,14 @@ -import { Controller } from "@hotwired/stimulus" +import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static values = { course: Number, project: Number, - } + }; navigate(event) { - const version = event.target.value - const url = `/courses/${this.courseValue}/projects/${this.projectValue}?version=${version}` - window.location.href = url + const version = event.target.value; + const url = `/courses/${this.courseValue}/projects/${this.projectValue}?version=${version}`; + window.location.href = url; } } diff --git a/app/views/courses/show.html.erb b/app/views/courses/show.html.erb index c3f69f00..70b90911 100644 --- a/app/views/courses/show.html.erb +++ b/app/views/courses/show.html.erb @@ -3,8 +3,6 @@ <% breadcrumb :course, @course %> <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> - -
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 1db92241..b1fd6cdc 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,6 +11,8 @@ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" > + + - <%# --- COMPARE TOOLBAR (deferred — see TODO below) --- %> - <%# TODO: Add two - + arrow_drop_down
diff --git a/app/views/projects/_project_comments.html.erb b/app/views/projects/_project_comments.html.erb index 6fbd4954..0b82b2a6 100644 --- a/app/views/projects/_project_comments.html.erb +++ b/app/views/projects/_project_comments.html.erb @@ -2,7 +2,7 @@
-
+
Comments for v<%= version %>
@@ -89,11 +89,6 @@
<% end %> - <% if version_comments.any? && version == current_version && comments.group_by { |c| c.location.version }.keys.length > 1 %> -
- Version <%= version %> Context (Current) -
- <% end %> <% end %>
diff --git a/app/views/projects/_record_update_modal.html.erb b/app/views/projects/_record_update_modal.html.erb index 5eb39210..89f63516 100644 --- a/app/views/projects/_record_update_modal.html.erb +++ b/app/views/projects/_record_update_modal.html.erb @@ -41,7 +41,7 @@ ], "" ), - { prompt: "Select an overall status..." }, + { prompt: "Select a rating" }, class: "w-full bg-white border border-[#DADCE0] rounded-[4px] pl-4 pr-10 py-3.5 text-[#3C4043] focus:outline-none focus:ring-2 focus:ring-[#1A73E8] focus:border-transparent appearance-none cursor-pointer text-[14px]", data: { testid: "progress-rating-select" } %> arrow_drop_down diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index bf4b551b..53bc0dbe 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -101,14 +101,23 @@ current_user: current_user, members: @members %> - <%= render "project_comments", - comments: @comments, - new_comment: @new_comment, - course: @course, - project: @project, - current_instance_id: @current_instance.id, - current_version: @current_version, - latest_version: @latest_version %> +
+ <%= render "project_comments", + comments: @comments, + new_comment: @new_comment, + course: @course, + project: @project, + current_instance_id: @current_instance.id, + current_version: @current_version, + latest_version: @latest_version %> +
diff --git a/docs/projects_forms_refactor_plan.md b/docs/projects_forms_refactor_plan.md new file mode 100644 index 00000000..348167fc --- /dev/null +++ b/docs/projects_forms_refactor_plan.md @@ -0,0 +1,153 @@ +# ProPro Redesign — Regression Audit Prompt & Safe Refactor Workflow + +Context: `main` = source of truth for working behavior. `refactor/design` = new Tailwind/Google-Sans redesign, built mockup-first then wired up. `projects/show` is already redesigned; `projects/edit` and `projects/new` are next (the uploaded ERB is the target mockup for edit, not yet wired in). + +I pulled both branches to ground this doc in real diffs instead of writing something generic. Two concrete things worth knowing before you hand this off: + +- **`app/policies/project_policy.rb` already differs between branches** — `refactor/design` adds `return true if coordinator` inside `update?` (or similar) that isn't on `main`. That's a genuine permission change bundled into a "visual" redesign branch, exactly the kind of thing you don't want to discover in prod. +- **`test/system/projects/project_versioning_test.rb` was NOT updated** for the new `show` DOM/copy (tabs went from "Details / Progress Updates / Comments (n)" to "Project Details / Compare Versions / Progress Updates", and comments moved from a tab into a permanent sidebar). The old mobile-only rendering path (`data-controller="mobile-tabs"`, duplicate `_project_fields` render for small screens) was deleted outright — `_project_header.html.erb` (373 lines) and `_project_fields.html.erb` (70 lines) no longer exist in `refactor/design`. Nothing references them anymore (no dead `render` calls), so that part was cleaned up correctly — but it means mobile-specific behavior was either intentionally dropped or needs to be re-solved elsewhere, and nobody has confirmed which. + +Those two are seed examples, not the whole list — use Part 1 to find the rest systematically. + +--- + +## Part 1 — Regression Audit Prompt (paste this to the audit agent) + +Use this on its own agent session/branch checkout, separate from whoever is doing the implementation work. Its only output should be a report — no fixes. + +``` +You are auditing a Rails app (hackerspacemmu/ProPro) for regressions introduced by a +UI/partial redesign. You are NOT implementing anything and you must not modify any +files. Your only deliverable is a written audit report. + +BRANCHES +- Reference (known-good behavior): main +- Candidate (redesign in progress): refactor/design + +Check out both into separate worktrees (or diff via `git diff main...refactor/design`) +so you can read full file contents on each side, not just the diff hunks — partial +renames make line-diffs misleading. + +SCOPE (in this order — stop and report after each pass rather than batching): +1. app/controllers/projects_controller.rb, project_groups_controller.rb, + project_templates_controller.rb +2. app/policies/project_policy.rb, project_group_policy.rb +3. app/views/projects/**, and any partial referenced from it (grep the redesign + branch for every `render "..."` / `render partial:` call, then locate each) +4. config/routes.rb (projects-related resources/nested routes) +5. app/javascript/controllers/** referenced via data-controller attributes in the + above views (e.g. tabs, mobile-tabs, and anything else) +6. test/system/projects/**, test/models tied to Project/ProjectInstance, and any + controller/request specs for the above controllers + +FOR EACH FILE THAT DIFFERS BETWEEN BRANCHES, do the following: + +A. Controller/route parity + - Diff params permitted, before_actions, redirects/flash, instance variables + assigned. Flag ANY difference, even ones that look purely cosmetic — a + dropped before_action or a renamed instance variable is invisible in a + visual review but breaks the view silently. + +B. Policy/authorization parity + - Diff every Pundit (or equivalent) policy method line by line. Do not assume + a change is "obviously fine" because it looks small — a single added + `return true if X` line is a real permission grant. For every such + difference, state explicitly: what capability changed, who gains or loses + it, and whether it appears intentional (tied to a described feature) or + accidental (leftover from an experiment, or a merge artifact). + - Cross-check: does every `policy(@project).something?` call site in the old + view have an equivalent authorization check somewhere in the new view? A + redesign that removes a UI element without removing the underlying + permission check is usually fine; one that removes the permission check + itself (or moves an action outside the guarded block) is not. + +C. Partial contract parity ("locals in vs locals out") + - For every partial that was renamed, split, or merged (e.g. old + `_project_header.html.erb` + `_project_fields.html.erb` → + new `_context_header.html.erb` + `_project_overview.html.erb` + + `_compare_versions_tab.html.erb`, etc. — confirm actual names on your + checkout, this repo's names will drift), build a table: + + old partial | locals it received | where each local's DATA now lives + new partial(s) | locals declared (check for `<%# locals: (...) %>`) + + Flag any local that was used in the old partial but has no home in the + new one — that's either dropped functionality or a live bug waiting to + happen (NameError on a missing local, or a silently blank section). + +D. Feature/interaction inventory + - Walk the OLD view top to bottom and list every distinct user-facing + behavior: every `if`/`unless` branch, every link/button/form, every + conditional class, every empty-state, every count or computed label + (e.g. "Comments (n)", "X/Y approved · Z pending"). For each one, find its + equivalent in the NEW view and mark it: + - PRESERVED (same behavior, possibly restyled) + - RELOCATED (moved to a different tab/panel/modal — note where) + - CHANGED (behavior itself is different — describe precisely) + - REMOVED (no equivalent found — flag as a question, not an assumption) + - Pay special attention to responsive/mobile-only code paths in the old + view (e.g. `data-controller="mobile-tabs"`, `hidden md:block` pairs). + If the new markup has no mobile equivalent, say so explicitly rather than + letting it slide as a styling detail. + +E. Dead code / orphan check + - Any partial, stimulus controller, helper, or route left on either branch + that is no longer rendered/called by anything. + - Any locals declared in a `<%# locals: (...) %>` comment that are never + used in the partial body, or used-but-undeclared. + +F. Test coverage parity + - Flag every system/request test that references DOM text, CSS selectors, + or `data-*` attributes that no longer exist on the redesign branch — + these will pass on main and silently rot or hard-fail on the redesign + branch. List them by file and line. + - Flag features found in step D that have NO test coverage on either + branch — not a blocker, but worth surfacing. + +OUTPUT FORMAT +Produce a single markdown report with sections matching A–F above. Within each +section, group findings by severity: + - BREAKING — will error or silently no-op in production + - BEHAVIOR CHANGE — works, but does something different than main (may be + intentional; needs a human decision either way) + - COSMETIC — visual only, no functional impact + - NEEDS DECISION — ambiguous; you found a difference but can't tell intent +For every finding, cite exact file paths and, where possible, line numbers on +both branches. Do not editorialize about which design is "better" — your job +is to make every functional delta visible, not to judge the redesign. +``` + +--- + +## Part 2 — Workflow for finishing `projects/edit` and `projects/new` + +You already know the risk: the uploaded mockup is a hardcoded snapshot (static "Test Lecturer 1", static "2/3 approved · 1 pending", a placeholder textarea) with no wiring to real data yet, and the old `_project_edit.html.erb` / `_project_details.html.erb` / `_proposal_method.html.erb` almost certainly loop over dynamic template fields, real supervisor-approval state, and policy-gated edit rules that the mockup doesn't represent at all. Mockup-first is fine as a design tool — the risk is entirely in the wiring step. So the goal isn't "don't do mockup-first," it's "don't let the mockup silently define the feature set." + +**Step 1 — Freeze a feature inventory before touching `edit`/`new`, using `main`.** +Do this by hand or with an agent, but do it *before* wiring, not after — otherwise you'll rediscover requirements one bug report at a time. For `projects_controller#edit`/`#update` and `#new`/`#create`, list: +- every param permitted and every validation/callback that fires on save +- every `policy(@project).x?` check gating a field, button, or section +- every piece of *dynamic* data the current view renders (template fields loop, approval counts, topic-catalog data, methodology options) vs what's currently hardcoded in your mockup +- every partial currently involved (`_project_edit`, `_project_details`, `_proposal_method`, `_project_new`) and what locals each one needs today + +This list is your acceptance checklist for the rewired version — not a spec to redesign around, just a "don't forget this exists" list. + +**Step 2 — Map the mockup to that inventory, gap by gap.** +Go section by section through the mockup and mark each piece as: *already dynamic in the mockup's structure and just needs real data*, *currently hardcoded and needs to become a loop/partial*, or *missing entirely from the mockup* (you'll need to design a spot for it, e.g. where does the free-edit-fields policy state show up?). Don't wire anything yet — this pass is just so wiring doesn't turn into archaeology mid-implementation. + +**Step 3 — Wire one section at a time, smallest first.** +Suggested order for the edit form specifically, since it's lower-risk to higher-risk: +1. Proposal Method selector cards (mostly static toggle state + one relationship — supervisor) +2. Simple fields (title, dropdown) — these map straight to template fields +3. Methodology radio / dynamic field types — this is where the field-type loop lives +4. Rich text / description — likely the most custom (toolbar, markdown) and most likely to hide edge cases (empty content, existing saved markdown rendering) + +After each section, run existing tests plus a manual pass against that section's line items from Step 1's inventory before moving to the next section. Small blast radius per commit makes it obvious which change caused which regression. + +**Step 4 — Clean up partials as you wire, using two rules, not vibes.** +This is straight from the reusable-components guidance you shared, and it's worth making a hard rule for this refactor rather than a suggestion: +- **A partial exists only if it's reused, or is a genuinely reusable component** (a field renderer, a card, an item row). Don't extract a partial just to shorten a long view file — that's what the old `_project_header.html.erb` (373 lines) and `_project_details.html.erb` (376 lines) look like they became: grab-bags that grew because "extract to partial" was used as a stand-in for "this method is getting long," rather than because the markup was actually reused elsewhere. If a chunk of the new edit/create view isn't rendered from more than one place, it can usually just stay inline in `edit.html.erb`/`new.html.erb`. +- **Every partial takes locals, never instance variables**, and should declare them explicitly with Rails' strict-locals comment: `<%# locals: (project:, fields:, ...) %>`. The redesign branch already does this correctly in `_context_header.html.erb` — hold every new/changed partial in `edit`/`new` to that same standard. This alone prevents a whole class of "partial silently reads a stale `@instance_variable`" bugs, and it makes Part 1's contract-parity check (section C above) mechanical instead of guesswork, since the locals list is right there in the file. + +**Step 5 — Once edit/create are wired, re-run Part 1's audit prompt scoped to them.** +Same process that should be run against `show` now applies to `edit`/`new`/`create` — controller/policy parity, partial contract parity, feature inventory, dead code, test coverage. Treat it as the exit criteria for calling the redesign "done," not an afterthought. \ No newline at end of file From 9cfd872b25da57c1ba858a934c509fd11fc3eb27 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 28 Aug 2026 23:04:50 +0800 Subject: [PATCH 07/53] linter --- {ProPro Design => ProPro_Design}/course_show.html.erb | 0 {ProPro Design => ProPro_Design}/grouping_settings.html.erb | 0 {ProPro Design => ProPro_Design}/progress_updates.html.erb | 0 {ProPro Design => ProPro_Design}/projects_form.html.erb | 0 {ProPro Design => ProPro_Design}/projects_show.html.erb | 0 .../projects_show_responsive.html.erb | 0 {ProPro Design => ProPro_Design}/projects_show_v2.html.erb | 0 .../projects_version_comparison.html.erb | 0 {ProPro Design => ProPro_Design}/style_guide.html.erb | 0 app/views/projects/edit.html.erb | 2 +- 10 files changed, 1 insertion(+), 1 deletion(-) rename {ProPro Design => ProPro_Design}/course_show.html.erb (100%) rename {ProPro Design => ProPro_Design}/grouping_settings.html.erb (100%) rename {ProPro Design => ProPro_Design}/progress_updates.html.erb (100%) rename {ProPro Design => ProPro_Design}/projects_form.html.erb (100%) rename {ProPro Design => ProPro_Design}/projects_show.html.erb (100%) rename {ProPro Design => ProPro_Design}/projects_show_responsive.html.erb (100%) rename {ProPro Design => ProPro_Design}/projects_show_v2.html.erb (100%) rename {ProPro Design => ProPro_Design}/projects_version_comparison.html.erb (100%) rename {ProPro Design => ProPro_Design}/style_guide.html.erb (100%) diff --git a/ProPro Design/course_show.html.erb b/ProPro_Design/course_show.html.erb similarity index 100% rename from ProPro Design/course_show.html.erb rename to ProPro_Design/course_show.html.erb diff --git a/ProPro Design/grouping_settings.html.erb b/ProPro_Design/grouping_settings.html.erb similarity index 100% rename from ProPro Design/grouping_settings.html.erb rename to ProPro_Design/grouping_settings.html.erb diff --git a/ProPro Design/progress_updates.html.erb b/ProPro_Design/progress_updates.html.erb similarity index 100% rename from ProPro Design/progress_updates.html.erb rename to ProPro_Design/progress_updates.html.erb diff --git a/ProPro Design/projects_form.html.erb b/ProPro_Design/projects_form.html.erb similarity index 100% rename from ProPro Design/projects_form.html.erb rename to ProPro_Design/projects_form.html.erb diff --git a/ProPro Design/projects_show.html.erb b/ProPro_Design/projects_show.html.erb similarity index 100% rename from ProPro Design/projects_show.html.erb rename to ProPro_Design/projects_show.html.erb diff --git a/ProPro Design/projects_show_responsive.html.erb b/ProPro_Design/projects_show_responsive.html.erb similarity index 100% rename from ProPro Design/projects_show_responsive.html.erb rename to ProPro_Design/projects_show_responsive.html.erb diff --git a/ProPro Design/projects_show_v2.html.erb b/ProPro_Design/projects_show_v2.html.erb similarity index 100% rename from ProPro Design/projects_show_v2.html.erb rename to ProPro_Design/projects_show_v2.html.erb diff --git a/ProPro Design/projects_version_comparison.html.erb b/ProPro_Design/projects_version_comparison.html.erb similarity index 100% rename from ProPro Design/projects_version_comparison.html.erb rename to ProPro_Design/projects_version_comparison.html.erb diff --git a/ProPro Design/style_guide.html.erb b/ProPro_Design/style_guide.html.erb similarity index 100% rename from ProPro Design/style_guide.html.erb rename to ProPro_Design/style_guide.html.erb diff --git a/app/views/projects/edit.html.erb b/app/views/projects/edit.html.erb index b5644083..b3a39a91 100644 --- a/app/views/projects/edit.html.erb +++ b/app/views/projects/edit.html.erb @@ -104,4 +104,4 @@
- \ No newline at end of file + From 4f46feb1458e90987863e4a045b053010b0584ab Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 29 Aug 2026 18:08:03 +0800 Subject: [PATCH 08/53] feat: trying to add responsiveness to projects/show --- CONTEXT.md | 43 +++ ProPro_Design/course_show.html.erb | 4 +- ProPro_Design/progress_updates.html.erb | 4 +- ProPro_Design/projects_form.html.erb | 4 +- ProPro_Design/projects_show.html.erb | 4 +- .../projects_show_responsive.html.erb | 4 +- ProPro_Design/projects_show_v2.html.erb | 4 +- .../projects_version_comparison.html.erb | 4 +- ...ng_settings.html.erb => settings.html.erb} | 2 +- ProPro_Design/style_guide.html.erb | 4 +- app/controllers/projects_controller.rb | 33 ++- app/helpers/sidebar_helper.rb | 69 ++--- .../controllers/comments_drawer_controller.js | 36 +++ .../controllers/method_picker_controller.js | 244 ++++++++++++++++ .../controllers/sidebar_controller.js | 126 +++----- app/views/layouts/application.html.erb | 4 +- app/views/projects/_lecturer_picker.html.erb | 55 ++++ app/views/projects/_project_edit.html.erb | 107 ------- app/views/projects/_project_header.html.erb | 44 ++- app/views/projects/_project_new.html.erb | 100 ------- .../projects/_project_review_card.html.erb | 99 +------ app/views/projects/_proposal_method.html.erb | 275 ++++++++---------- .../projects/_review_action_bar.html.erb | 17 ++ app/views/projects/_review_actions.html.erb | 90 ++++++ app/views/projects/_template_fields.html.erb | 96 ++++++ app/views/projects/_topic_picker.html.erb | 47 +++ app/views/projects/edit.html.erb | 223 +++++++------- app/views/projects/new.html.erb | 149 +++++----- app/views/projects/show.html.erb | 36 ++- app/views/shared/_header.html.erb | 9 +- app/views/shared/_sidebar.html.erb | 12 +- docs/adr/0001-in-form-method-pickers.md | 30 ++ docs/adr/0002-overwrite-on-method-switch.md | 30 ++ docs/adr/0003-keep-easymde-restyle-shell.md | 23 ++ docs/adr/0004-policy-is-authoritative.md | 24 ++ .../0005-full-screen-takeover-form-layout.md | 23 ++ .../adr/0006-preserve-course-config-gating.md | 26 ++ ...7-comments-drawer-and-review-action-bar.md | 59 ++++ docs/adr/0008-app-sidebar-drawer.md | 57 ++++ docs/projects_show_refactor_plan.md | 93 +++++- test/system/projects/project_form_test.rb | 160 ++++++++++ .../projects/project_show_responsive_test.rb | 57 ++++ 42 files changed, 1707 insertions(+), 823 deletions(-) create mode 100644 CONTEXT.md rename ProPro_Design/{grouping_settings.html.erb => settings.html.erb} (99%) create mode 100644 app/javascript/controllers/comments_drawer_controller.js create mode 100644 app/javascript/controllers/method_picker_controller.js create mode 100644 app/views/projects/_lecturer_picker.html.erb delete mode 100644 app/views/projects/_project_edit.html.erb delete mode 100644 app/views/projects/_project_new.html.erb create mode 100644 app/views/projects/_review_action_bar.html.erb create mode 100644 app/views/projects/_review_actions.html.erb create mode 100644 app/views/projects/_template_fields.html.erb create mode 100644 app/views/projects/_topic_picker.html.erb create mode 100644 docs/adr/0001-in-form-method-pickers.md create mode 100644 docs/adr/0002-overwrite-on-method-switch.md create mode 100644 docs/adr/0003-keep-easymde-restyle-shell.md create mode 100644 docs/adr/0004-policy-is-authoritative.md create mode 100644 docs/adr/0005-full-screen-takeover-form-layout.md create mode 100644 docs/adr/0006-preserve-course-config-gating.md create mode 100644 docs/adr/0007-comments-drawer-and-review-action-bar.md create mode 100644 docs/adr/0008-app-sidebar-drawer.md create mode 100644 test/system/projects/project_form_test.rb create mode 100644 test/system/projects/project_show_responsive_test.rb diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000..a3a3068f --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,43 @@ +# ProPro Glossary + +Shared vocabulary for ProPro. A term lands here the moment it is agreed. No +implementation detail, no spec — just the project's own words and tight +definitions. + +## Projects & proposals + +- **proposal** — a project before approval ("Pending" status). Called "Project" once approved. +- **project instance / instance / version** — a `project_instance` on a project; a new one is created on each edit. `current_instance` is the latest. Submitted values live on `project_instance_fields`. +- **proposal method** — how a proposal is submitted: **Propose to Lecturer** (own proposal) or **Base on a Topic**. +- **own proposal** — a proposal submitted directly to a lecturer. Encoded in `based_on_topic` as `own_proposal_`. +- **based_on_topic** — the single hidden form field carrying the proposal-method choice: `own_proposal_` or ``. Read by `create`/`update` to derive the supervisor enrolment. +- **method picker** — the in-form modal dialog that lists lecturers/topics and applies a proposal-method selection without navigating away. + +## Courses + +- **solo supervisor course** — a course with fewer than three lecturer+coordinator enrolments. No lecturer choice exists; proposals default to the single lecturer (`Course#solo_supervisor?`). +- **toggle topics** — `Course#toggle_topics`; gates whether "Base on a Topic" exists at all. + +## Template fields + +- **template fields** — `project_template_fields` on a course's template; typed `shorttext`/`textarea`/`dropdown`/`radio`, with `required`, `hint`, `free_edit`, `is_project_title`, and `applicable_to` attributes. +- **free-edit field** — a template field that stays editable after the proposal is approved. +- **supervisor capacity** — `approved_count / effective_cap · pending_count` for a lecturer in a course, computed by `SupervisorCapacityCalculator`. + +## Project show (review + comments) + +- **content tabs** — the Project Details / Compare Versions / Progress Updates tab set in the sticky tab bar. Constant at every width; screensize never adds tabs. +- **review actions** — the shared version switcher + policy-driven actions (`_review_actions`), rendered in either the review card (desktop) or the review action bar (mobile). +- **review card** — the desktop-only Review Project card (title + Active badge) hosting `_review_actions`. +- **review action bar** — the mobile-only pinned bottom bar (version switcher + actions) hosting the same `_review_actions`; thumb-reachable on every tab. +- **comments drawer** — one element, two presentations: the static sticky comments column on desktop; a backdrop-toggled off-canvas slide-in on mobile (`top-0 right-0 bottom-0` + `translate-x-full`, escaped by `min-[1245px]:translate-x-0`), opened by the comments trigger. Controlled by `comments-drawer`. +- **comments trigger** — the mobile-only tab-bar button (chat bubble + count badge) that opens/closes the comments drawer (`aria-expanded` mirrors open state). + +## App chrome + +- **app sidebar** — `shared/_sidebar`: app-wide nav (Home / courses / Edit profile), plus the `render_sidebar` helper widget for legacy pages. One element, two presentations: a static left column at ≥`lg` (1024px), an off-canvas drawer below it opened by the header menu icon. Controlled by `sidebar`. Its breakpoint is chrome-level and deliberately independent of content breakpoints (e.g., the `min-[1245px]` comments drawer). +- **drawer idiom** — the shared pattern for "drawer over content": a single element that is a static in-flow column on desktop (`lg:static lg:translate-x-0` / `min-[1245px]:translate-x-0`) and an off-canvas panel below it; the controller only toggles the translate class, the backdrop `hidden`, body scroll lock, and `aria-expanded`. Used by `sidebar` and `comments-drawer`. + +## Form actions + +- **Discard Changes / Cancel** — the same behavior by either name: leave the form without saving. \ No newline at end of file diff --git a/ProPro_Design/course_show.html.erb b/ProPro_Design/course_show.html.erb index 9e221c4e..39bf6850 100644 --- a/ProPro_Design/course_show.html.erb +++ b/ProPro_Design/course_show.html.erb @@ -4,7 +4,7 @@ <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> - +
@@ -255,4 +255,4 @@
- \ No newline at end of file + diff --git a/ProPro_Design/progress_updates.html.erb b/ProPro_Design/progress_updates.html.erb index cd498bcf..c7dc9f20 100644 --- a/ProPro_Design/progress_updates.html.erb +++ b/ProPro_Design/progress_updates.html.erb @@ -6,7 +6,7 @@ - + @@ -419,4 +419,4 @@ - \ No newline at end of file + diff --git a/ProPro_Design/projects_form.html.erb b/ProPro_Design/projects_form.html.erb index 74b0ea39..e007284f 100644 --- a/ProPro_Design/projects_form.html.erb +++ b/ProPro_Design/projects_form.html.erb @@ -6,7 +6,7 @@ - + + + + + + + +
+
+ + + ProPro + + + +
+ + +
+ + + + +
+ + + + + +
+ + +
+
+ + + + + + + + + +
+
+ + +
+
+ + + + +
+ +
+

Teachers

+ +
+ + +
+
+
+
C
+ CHIAM JUIN HOONG +
+ +
+ +
+
+
T
+ Test Lecturer 1 +
+ +
+
+
+ + + + +
+ +
+ +
+

Groups

+ 4 groups +
+ + +
+
+ search + +
+ + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
GroupMembersProject TitleStatusSupervisor
01 +
+
+
T
+
+ 1 member +
+
+ Pineapple das + + + Pending + + +
+ Test Lecturer 1 + expand_more +
+
03 +
+
+
S
+
N
+
T
+
+ 3 members +
+
+ Test Create Proposal for ... + + + Approved + + +
+ Test Lecturer 1 + expand_more +
+
04 +
+
+
C
+
C
+
+ 2 members +
+
+ IIIIIIIIIIIII + + + Approved + + +
+ Test Lecturer 1 + expand_more +
+
+
+ +
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/ProPro_Design/settings.html.erb b/ProPro_Design/settings.html.erb index 652bb5a1..3452360f 100644 --- a/ProPro_Design/settings.html.erb +++ b/ProPro_Design/settings.html.erb @@ -1,79 +1,98 @@ -<% content_for :stylesheets do %> - <%= stylesheet_link_tag "tailwind", "new_tailwind" %> -<% end %> -<% breadcrumb :course_settings, @course %> - -<% content_for :title, "Course Settings - #{@course.course_name.truncate(TITLE_NAME_LIMIT)} | ProPro" %> + + + + + +Course Settings - Final Year Project I | ProPro + + + + + + +
- - <%= form_with url: handle_settings_course_path(@course), model: @course, method: :post, id: "course-settings-form", class: "flex flex-col min-h-screen" do |form| %> + +
- <%= link_to course_path(@course), class: "p-2 rounded-full hover:bg-gray-100 text-gray-600 transition-colors" do %> + - <% end %> +

Class settings

- <%= form.submit "Save", class: "bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium text-sm px-6 py-2 rounded-full cursor-pointer transition-colors" %> +
- -
- <%= render "courses/flash" %> -
- <% if @course.errors.any? %> -
+
+
-

- <%= pluralize(@course.errors.count, "error") %> prohibited saving: -

-
    - <% @course.errors.full_messages.each do |message| %> -
  • <%= message %>
  • - <% end %> -
+

Success

+
Course successfully updated.
- <% end %> +
- <%= render "copy_course_overlay", course: @course, mode: "settings" %> + +
+
+ +
+

Class Details

- +
- <%= form.label :course_name, "Class name", class: "block text-xs font-medium text-gray-600" %> - <%= form.text_field :course_name, required: true, class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm mt-1" %> + +
- <%= form.label :course_description, "Project Details", class: "block text-xs font-medium text-gray-600" %> - <%= form.text_area :course_description, rows: 2, class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm mt-1 resize-none overflow-hidden", data: { controller: "textarea-resize", action: "input->textarea-resize#resize" } %> + +
- <%= form.label :file_link, "Project File Link (URL)", class: "block text-xs font-medium text-gray-600" %> - <%= form.text_field :file_link, class: "w-full bg-transparent border-none p-0 focus:ring-0 text-blue-600 text-sm mt-1 underline" %> + +
- <%= form.label :starting_week, "Starting Week", class: "block text-xs font-medium text-gray-600" %> - <%= form.text_field :starting_week, required: true, class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm mt-1" %> + +
@@ -82,26 +101,124 @@

General

- +

Coursecode

- <%= render "course_code_form", course: @course %> + +
+ +

+ Use this only for individual projects. For groups, use CSV imports. + Group formation in ProPro coming in a future update. +

+
+ + +
+ +

Supervisor Capacity

- +
- <%= form.label :supervisor_projects_limit, "Maximum Projects per Supervisor", class: "block text-xs font-medium text-gray-600" %> - <%= form.hidden_field :supervisor_projects_limit %> - <%= form.number_field :supervisor_projects_limit, id: "supervisor_projects_limit_visible", required: true, disabled: @course.supervisor_auto_calculate_enabled?, class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm mt-1" %> + +
- <%= render "supervisor_capacity_settings", form: form %> +
+
+ + Manage Project Allocations and Lecturer exclusions + + + + + +
+

+ Maximum projects and variable project allocations affect display numbers in supervisor list only, serving as a guideline. Supervisors are free to accept projects beyond the listed number. +

+ +
+
+ +
+
+ +

Divides total projects by eligible supervisors. Disables the manual base limit input.

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SupervisorExcludeOffsetRelative Capacity
Dr. Wong Mei Ling + + + + + 5 + (5 + 0) +
Dr. Ahmad Faiz Rahman + + + + + 7 + (5 + 2) +
Dr. Priya Nair + + + + + 4 + (5 - 1) +
+
+
+
+
@@ -111,17 +228,17 @@

Permissions and Rules

- +
- +
Allow lecturers to add topics for students to base their proposals on.
@@ -130,10 +247,10 @@ Lecturer topics require approval?
@@ -142,10 +259,10 @@ Auto-approve unchanged copied topics?
@@ -154,10 +271,10 @@ Use progress updates?
@@ -166,10 +283,10 @@ Lecturer views public?
@@ -177,7 +294,7 @@
Number of progress updates
- <%= form.number_field :number_of_updates, class: "w-full bg-transparent border-none p-0 focus:ring-0 text-gray-800 text-sm text-center" %> +
@@ -186,12 +303,18 @@
Student can see projects from:
- <% Course.student_accesses.keys.map { |key| [key, key.to_s.humanize] }.each_with_index do |(value, label_text), idx| %> -
- <%= form.radio_button :student_access, value, id: "student_access_#{idx}", class: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" %> - <%= form.label :student_access, label_text, value: value, for: "student_access_#{idx}", class: "ml-3 block text-sm text-gray-800 cursor-pointer" %> -
- <% end %> +
+ + +
+
+ + +
+
+ + +
@@ -199,11 +322,8 @@
-
- +
+

Student Self-Grouping Beta

@@ -216,40 +336,30 @@ Allow students to form their own groups?
-
+
- - Group Configuration - + Group Configuration
Student list mode -

- Choose whether the coordinator knows the exact final count of students. -

+

Choose whether the coordinator knows the exact final count of students.

-
@@ -131,7 +129,6 @@ 28px (rounded-tl-[28px]) - App shell content pane / modal dialogs -

grouping_settings_html.erb's footer action buttons ("Add Students", "Delete Course") currently use rounded-md — every button must be a pill regardless of context.

@@ -465,7 +462,7 @@
-

This is the canonical Filled Input. grouping_settings_html.erb reimplements it with generic colors (bg-[#f1f3f4] is fine, but border-gray-400 / focus-within:bg-[#e8eaed] / focus-within:border-blue-600 should become border-[#5F6368] / no bg change on focus / focus-within:border-[#1A73E8]). Standalone standard text inputs (not floating-label) use 6px radius — see Section 24.

+

This is the canonical Filled Input. Standalone standard text inputs (not floating-label) use 6px radius — see Section 24.

@@ -588,7 +585,7 @@
Text Link Button (inline, no button chrome)
-

Every filled/outlined button variant is still a pill (rounded-full) except the Small Ghost variant, which is a rectangular sub-component nested inside a card, and the plain Text Link, which has no shape at all. grouping_settings_html.erb's footer buttons ("Add Students", "Delete Course", "Save") should map onto Outline / Danger Text / Primary respectively, using the hex tokens and pill shape — not generic Tailwind grays/reds on rounded-md.

+

Every filled/outlined button variant is still a pill (rounded-full) except the Small Ghost variant, which is a rectangular sub-component nested inside a card, and the plain Text Link, which has no shape at all.

@@ -905,7 +902,6 @@ -

grouping_settings_html.erb uses this pattern but with a hand-drawn SVG ✕ (Material Symbols never loads on that page — see Section 25) and a gray Save button instead of Primary Blue.

@@ -989,7 +985,7 @@ Beta

- All of the above are pulled from grouping_settings_html.erb, remapped from generic Tailwind grays/blues/reds/ambers onto the standard hex tokens. Radio-card selected state reuses the same blue tint pattern as the Section 15 selection cards; the disabled option drops to a neutral #9AA0A6 muted-gray text with 60% opacity on the whole row. The Beta tag reuses the Redo/yellow token rather than introducing a new amber. + Radio-card selected state reuses the same blue tint pattern as the Section 15 selection cards; the disabled option drops to a neutral #9AA0A6 muted-gray text with 60% opacity on the whole row. The Beta tag reuses the Redo/yellow token rather than introducing a new amber.

Mobile — sticky bottom action bar
@@ -1026,7 +1022,6 @@ 25. Known Inconsistencies To Fix
    -
  • grouping_settings_html.erb — missing the Google Fonts / Material Symbols <link> tags entirely; uses font-sans instead of Roboto/Google Sans; hand-draws an SVG ✕ icon instead of the material-symbols-outlined close span; uses generic Tailwind gray-*/blue-*/red-*/amber-* classes throughout instead of the hex tokens in Section 1; footer buttons ("Add Students", "Add Lecturers", "Delete Course") are rounded-md instead of pill-shaped, and the Danger button uses red-50/red-700 instead of #FCE8E6/#C5221F; the "Save" button is gray instead of Primary Blue.
  • projects_form_html.erb — header "Pending" badge is a bordered gold chip (#FEF7E0/#B06000, uppercase, bordered) instead of the standard unbordered blue Status Pill; primary-button hover is #1557B0 instead of the documented #1B66C9; field labels are font-semibold text-[#3C4043] instead of the standard Eyebrow Label (font-medium text-[#5F6368]); its select-field caret uses expand_more where every other select on the app uses arrow_drop_down — standardize on arrow_drop_down.
  • projects_show_html.erb — comment timestamps use text-xs text-[#5F6368] instead of the Meta Text token (text-[14px] text-[#444746]) used on every other page's comment panel.
diff --git a/app/javascript/controllers/coursecode_form_handler_controller.js b/app/javascript/controllers/coursecode_form_handler_controller.js index ee73ba92..4e7b0011 100644 --- a/app/javascript/controllers/coursecode_form_handler_controller.js +++ b/app/javascript/controllers/coursecode_form_handler_controller.js @@ -1,16 +1,28 @@ import { Controller } from "@hotwired/stimulus"; // Connects to data-controller="coursecode-form-handler" +// Fires the coursecode_enabled toggle independently of any
: fetch() the +// update_coursecode endpoint with a form-encoded body, then render its turbo +// stream (replaces the course_code_form frame and the flash). export default class extends Controller { - static targets = ["generateFlag"]; + static values = { url: String }; - submitForm() { - this.element.requestSubmit(); - } + async toggle(event) { + const body = new URLSearchParams({ + "course[coursecode_enabled]": event.target.checked ? "1" : "0" + }); + + const response = await fetch(this.urlValue, { + method: "POST", + headers: { + Accept: "text/vnd.turbo-stream.html", + "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]')?.content + }, + body + }); - generateCode(event) { - event.preventDefault(); - this.generateFlagTarget.value = "true"; - this.element.requestSubmit(); + if (response.ok) { + window.Turbo.renderStreamMessage(await response.text()); + } } -} +} \ No newline at end of file diff --git a/app/views/courses/_copy_course_overlay.html.erb b/app/views/courses/_copy_course_overlay.html.erb index 06294bcf..0847d467 100644 --- a/app/views/courses/_copy_course_overlay.html.erb +++ b/app/views/courses/_copy_course_overlay.html.erb @@ -1,6 +1,8 @@ +<%# locals: (course:, mode:, courses:) %>
-
@@ -8,56 +10,59 @@
- <%= turbo_frame_tag "overlay_content", class: "bg-white rounded-xl shadow-2xl w-9/12 min-h-[85vh] max-h-[85vh] flex flex-col p-10 relative", + <%= turbo_frame_tag "overlay_content", class: "bg-white rounded-[28px] shadow-xl w-9/12 min-h-[85vh] max-h-[85vh] flex flex-col p-10 relative", data: { overlay_target: "container" } do %> -
-

Select a Course to Copy

-

+
+

Select a Course to Copy

+ +
-
- <% if @courses.present? %> -
- <% @courses.each do |course_item| %> +
+ <% if courses.present? %> +
+ <% courses.each do |course_item| %>
+ class="h-full min-h-[240px] bg-white rounded-[10px] border border-[#E0E0E0] p-6 transition-all duration-200 ease-in-out hover:border-[#1A73E8] hover:shadow-md flex flex-col justify-between cursor-pointer">
-

+

<%= course_item.course_name %>

-

+

<%= course_item.course_description.presence || "No description available for this course." %>

-
-
+
+
<%= course_item.coordinators.first&.name&.first&.upcase || "?" %>
- + Coordinator: - <%= course_item.coordinators.first&.name || "None" %> + <%= course_item.coordinators.first&.name || "None" %>
<% end %>
<% else %> -
- <%= image_tag "info.svg", class: "w-6 h-6" %> -
- You have no courses at the moment. Please create one. -
+
+ info +

+ You have no courses at the moment. Please create one. +

<% end %>
-
-
diff --git a/app/views/courses/_course_code_form.html.erb b/app/views/courses/_course_code_form.html.erb index a94d8509..af235f0f 100644 --- a/app/views/courses/_course_code_form.html.erb +++ b/app/views/courses/_course_code_form.html.erb @@ -1,29 +1,35 @@ +<%# locals: (course:) %> +<%# Formless: Generate/Re-Generate is a data-turbo-method link (Turbo synthesizes a throwaway + form at click time) and the toggle fires its own fetch. There is no in the static + DOM, so this can live anywhere inside #course-settings-form. The top-level node of this + partial must stay exactly — Turbo Streams replace() + swaps in the WHOLE template content, so any wrapper would get re-nested on every replace + (see ADR-0010). %> <%= turbo_frame_tag "course_code_form" do %> -
- <%= form_with url: update_coursecode_course_path(@course), model: @course, method: :post, data: { controller: "coursecode-form-handler" } do |form| %> - <%= form.label :coursecode, - 'Course Join Code & Invite Link (Beta)', - class: "block text-sm font-medium text-gray-700" %> -

- Use this only for individual projects. For groups, use CSV imports. - Group formation in ProPro coming in a future update. -

-
- <%= form.text_field :coursecode, - required: true, - readonly: true, - disabled: true, - class: "appearance-none block w-[10em] font-mono px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm" %> - -
- - - <% end %> +
+ +

+ Use this only for individual projects. For groups, use CSV imports. Group formation in ProPro coming in a future update. +

+
+ + <%= link_to (course.coursecode.present? ? 'Re-Generate Join Code' : 'Generate Join Code'), + update_coursecode_course_path(course, generate: true), + id: "regenerate-code-btn", + data: { turbo_method: :post }, + class: "px-4 py-2 bg-blue-200 hover:bg-blue-300 text-gray-800 text-sm font-medium rounded shadow-sm cursor-pointer" %> +
+
<% end %> diff --git a/app/views/courses/_grouping_settings.html.erb b/app/views/courses/_grouping_settings.html.erb deleted file mode 100644 index fbf273b6..00000000 --- a/app/views/courses/_grouping_settings.html.erb +++ /dev/null @@ -1,326 +0,0 @@ -
- -
-
-

Student Self-Grouping (Beta)

- -

- Group formation in ProPro is still in progress! -

-
- - <%# Toggle: enable / disable %> -
- - Allow students to form their own groups? - -
-
- <%= form.radio_button :grouping_enabled, true, - class: "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300", - data: { action: "change->grouping-settings#toggleEnabled" } %> - <%= form.label :grouping_enabled, "Yes", value: true, - class: "ml-2 block text-sm text-gray-700" %> -
-
- <%= form.radio_button :grouping_enabled, false, - class: "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300", - data: { action: "change->grouping-settings#toggleEnabled" } %> - <%= form.label :grouping_enabled, "No", value: false, - class: "ml-2 block text-sm text-gray-700" %> - -
-
-
- - <%# Options section — hidden until student fixed list mode %> -
- - <%# Section: Group configuration %> -
-
- - Group configuration - - - <%# Mode selection %> -
- Student list mode -

- Choose whether the coordinator knows the exact final count of students. -

- -
- <%# Default mode — always rendered %> - - - <%# Fixed list mode — locked out once default mode has been saved %> - <% if @course.persisted? && @course.grouping_enabled? && !@course.student_list_finalised? %> -
-
-
- Final student list - - Fixed list mode cannot be enabled once default mode has been saved. - -
-
- <% else %> - - <% end %> -
-
- - <%# Min / Max group size %> -
-
- <%= form.label :group_min, "Minimum group size", - class: "block text-sm font-medium text-gray-700" %> -
- <%= form.number_field :group_min, - min: 1, - placeholder: "e.g. 3", - class: "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm", - data: { grouping_settings_target: "minMax", action: "input->grouping-settings#clearPreview" } %> -
-
-
- <%= form.label :group_max, "Maximum group size", - class: "block text-sm font-medium text-gray-700" %> -
- <%= form.number_field :group_max, - min: 1, - placeholder: "e.g. 4", - class: "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm", - data: { grouping_settings_target: "minMax", action: "input->grouping-settings#clearPreview" } %> -
-
-
- - <%# Preview section %> -
-
-

Preview group distribution

-

- Enter an expected student count to see how legal groups would form. - Actual numbers will come from enrolled students. -

-
-
- - students -
-
-
-
-
- - <%# Section: Student access %> -
- - Student access - - - <%# Manual open/close toggle %> -
-
- Allow student grouping now - - Students can join, leave, and form groups only when this is on. - -
-
-
- <%= form.radio_button :grouping_open, true, - class: "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300" %> - <%= form.label :grouping_open, "Open", value: true, - class: "ml-1 text-sm text-gray-700" %> -
-
- <%= form.radio_button :grouping_open, false, - class: "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300" %> - <%= form.label :grouping_open, "Closed", value: false, - class: "ml-1 text-sm text-gray-700" %> -
-
-
- - <%# Optional datetime window %> -
- Auto-close window (optional) - -
-

- Optionally set specific dates to open and close student access automatically. -

-
-
- <%= form.label :grouping_opens_at, "Open from", - class: "block text-sm font-medium text-gray-700" %> -
- <%= form.datetime_local_field :grouping_opens_at, - class: "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm", - value: @course.grouping_opens_at&.strftime('%Y-%m-%dT%H:%M') %> -
-
-
- <%= form.label :grouping_closes_at, "Close at", - class: "block text-sm font-medium text-gray-700" %> -
- <%= form.datetime_local_field :grouping_closes_at, - class: "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm", - value: @course.grouping_closes_at&.strftime('%Y-%m-%dT%H:%M') %> -
-
-
-
- -
<%# /groupingOptions %> -
<%# /data-controller %> diff --git a/app/views/courses/_supervisor_capacity_settings.html.erb b/app/views/courses/_supervisor_capacity_settings.html.erb index 34cccc0a..24408083 100644 --- a/app/views/courses/_supervisor_capacity_settings.html.erb +++ b/app/views/courses/_supervisor_capacity_settings.html.erb @@ -1,87 +1,80 @@ +<%# locals: (form:, capacity_result:, lecturer_enrolments:) %>
+ data-supervisor-capacity-base-value="<%= capacity_result.base %>" + class="space-y-6"> -
+
+ class="cursor-pointer px-5 py-4 flex items-center justify-between gap-4 text-left text-[14px] font-medium text-[#3C4043] list-none"> Manage Project Allocations and Lecturer exclusions - - - + expand_more -
-

+

+

Maximum projects and variable project allocations affect display numbers in supervisor list only, serving as a guideline. Supervisors are free to accept projects beyond the listed number.

<%= form.check_box :supervisor_auto_calculate_enabled, - class: "h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500", + class: "h-4 w-4 rounded border-[#DADCE0] text-[#137333] focus:ring-[#137333]", data: { action: "change->supervisor-capacity#toggleAutoCalculate" } %>
<%= form.label :supervisor_auto_calculate_enabled, "Auto-calculate based on number of groups and lecturers", - class: "text-sm font-medium text-gray-700" %> -

+ class: "text-[14px] font-medium text-[#3C4043]" %> +

Divides total projects by eligible supervisors. Disables the manual base limit input.

- <% if @lecturer_enrolments.any? %> - <% lecturer_capacity_map = @capacity_result.lecturer_capacities.index_by { |lc| lc.enrolment.id } %> + <% if lecturer_enrolments.any? %> + <% lecturer_capacity_map = capacity_result.lecturer_capacities.index_by { |lc| lc.enrolment.id } %> -
- - +
+
+ - - - - + + + + - - <% @lecturer_enrolments.each do |enrolment| %> + + <% lecturer_enrolments.each do |enrolment| %> <% lc = lecturer_capacity_map[enrolment.id] %> <% offset = enrolment.supervisor_capacity_offset %> <% effective_cap = lc&.effective_cap || 0 %> <% sign = offset >= 0 ? "+ #{offset}" : "- #{offset.abs}" %> - - + - - <% end %> @@ -89,7 +82,7 @@
SupervisorExcludeOffsetRelative CapacitySupervisorExcludeOffsetRelative Capacity
<%= enrolment.user.name %> + <%= enrolment.user.name %> <%= check_box_tag "supervisor_capacity_excluded[]", enrolment.id, enrolment.supervisor_capacity_excluded, - class: "h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500", + class: "h-4 w-4 rounded border-[#DADCE0] text-[#137333] focus:ring-[#137333]", data: { action: "change->supervisor-capacity#updateRow", excluded: true } %> + <%= number_field_tag "supervisor_capacity_offsets[#{enrolment.id}]", offset, disabled: enrolment.supervisor_capacity_excluded?, - class: "w-20 text-center px-2 py-1 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-green-500 focus:border-green-500", + class: "w-20 text-center px-2 py-1.5 border border-[#DADCE0] rounded-[6px] text-[14px] text-[#3C4043] focus:outline-none focus:border-[#1A73E8] focus:ring-1 focus:ring-[#1A73E8]", data: { action: "input->supervisor-capacity#updateRow", offset: true } %> - <%= effective_cap %> - (<%= @capacity_result.base %> <%= sign %>) + + <%= effective_cap %> + (<%= capacity_result.base %> <%= sign %>)
<% else %> -

No lecturers enrolled yet.

+

No lecturers enrolled yet.

<% end %>
diff --git a/app/views/courses/settings.html.erb b/app/views/courses/settings.html.erb index b068ad7d..8b556dc3 100644 --- a/app/views/courses/settings.html.erb +++ b/app/views/courses/settings.html.erb @@ -1,357 +1,411 @@ <% content_for :stylesheets do %> <%= stylesheet_link_tag "tailwind", "new_tailwind" %> <% end %> +<% content_for :no_sidebar, true %> +<% content_for :hide_toggler, true %> +<% content_for :hide_breadcrumbs, true %> +<% content_for :body_class, "bg-[#f8fafd]" %> <% breadcrumb :course_settings, @course %> <% content_for :title, "Course Settings - #{@course.course_name.truncate(TITLE_NAME_LIMIT)} | ProPro" %> -
- -
-

- Course Settings - - <%= @course.course_name %> - <%= image_tag( - "edit.svg", - class: - "w-6 h-6 opacity-40 hover:opacity-100 transition-opacity cursor-pointer", - ) %> -

-

Update your course configuration below.

-
- -
- <%= render "courses/flash" %> -
- - <%= render "course_code_form", course: @course %> - -
- <%= form_with url: handle_settings_course_path(@course), model: @course, method: :post, class: "space-y-6" do |form| %> - - <% if @course.errors.any? %> -
-
-
-

- <%= pluralize(@course.errors.count, "error") %> - prohibited saving: -

-
    - <% @course.errors.full_messages.each do |message| %> -
  • <%= message %>
  • - <% end %> -
-
-
-
- <% end %> - - <%= render "copy_course_overlay", course: @course, mode: "settings" %> - -
-
- <%= form.label :course_name, - "Course Name", - class: "block text-sm font-medium text-gray-700" %> -
- <%= form.text_field :course_name, - required: true, - class: - "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm" %> -
-
+
-
- <%= form.label :course_description, - "Project Details", - class: "block text-sm font-medium text-gray-700" %> -
- <%= form.text_area :course_description, - rows: 2, - class: - "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm resize-none overflow-hidden", - data: { - controller: "textarea-resize", - action: "input->textarea-resize#resize", - } %> -
+ <%= form_with url: handle_settings_course_path(@course), model: @course, method: :post, id: "course-settings-form", class: "flex flex-col min-h-screen" do |form| %> + + +
+
+ <%= link_to course_path(@course), title: "Close and return to course", class: "p-2 -ml-2 rounded-full hover:bg-[#F1F3F4] text-[#5F6368] hover:text-[#3C4043] transition-colors flex items-center justify-center" do %> + close + <% end %> +

Course Settings

+
+
+ <%= form.submit "Save", class: "bg-[#1A73E8] hover:bg-[#1B66C9] text-white px-6 py-2 rounded-full font-medium text-[14px] transition-all shadow-sm cursor-pointer" %> +
+
+ + +
+
+ +
+ <%= render "courses/flash" %>
-
- <%= form.label :file_link, - "Project File Link (URL)", - class: "block text-sm font-medium text-gray-700" %> -
- <%= form.text_field :file_link, - class: - "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm text-blue-600 underline" %> + <% if @course.errors.any? %> +
+

+ <%= pluralize(@course.errors.count, "error") %> prohibited saving: +

+
    + <% @course.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
-
+ <% end %> -
- -
- <%= form.label :supervisor_projects_limit, - "Maximum Projects per Supervisor", - class: "block text-sm font-medium text-gray-700" %> -
- <%= form.hidden_field :supervisor_projects_limit %> - <%= form.number_field :supervisor_projects_limit, - id: "supervisor_projects_limit_visible", - required: true, - disabled: @course.supervisor_auto_calculate_enabled?, - class: - "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm" %> + <%= render "copy_course_overlay", course: @course, mode: "settings", courses: @courses %> + + +
+

Class Details

+ +
+
+ <%= form.label :course_name, "Course Name", class: "absolute top-2 left-4 text-xs font-medium text-[#5F6368]" %> + <%= form.text_field :course_name, required: true, class: "w-full bg-transparent border-none p-0 text-[#3C4043] focus:ring-0 text-[14px] mt-1" %>
-
-
- <%= form.label :starting_week, - "Starting Week", - class: "block text-sm font-medium text-gray-700" %> -
- <%= form.text_field :starting_week, - required: true, - class: - "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm" %> +
+ <%= form.label :course_description, "Project Details", class: "absolute top-2 left-4 text-xs font-medium text-[#5F6368]" %> + <%= form.text_area :course_description, rows: 2, class: "w-full bg-transparent border-none p-0 text-[#3C4043] focus:ring-0 text-[14px] mt-1 resize-none overflow-hidden", data: { controller: "textarea-resize", action: "input->textarea-resize#resize" } %> +
+ +
+ <%= form.label :file_link, "Project File Link (URL)", class: "absolute top-2 left-4 text-xs font-medium text-[#5F6368]" %> + <%= form.text_field :file_link, class: "w-full bg-transparent border-none p-0 text-[#1A73E8] focus:ring-0 text-[14px] mt-1 underline" %> +
+ +
+ <%= form.label :starting_week, "Starting Week", class: "absolute top-2 left-4 text-xs font-medium text-[#5F6368]" %> + <%= form.text_field :starting_week, required: true, class: "w-full bg-transparent border-none p-0 text-[#3C4043] focus:ring-0 text-[14px] mt-1" %>
-
+
- <%= render "supervisor_capacity_settings", form: form %> + +
+

General

-
+ +
+

Coursecode

-
- -
-

Permissions & Rules

- -
- -
- Allow lecturers to add topics for students to base their proposals on. -
-
- <%= form.radio_button :toggle_topics, - true, - class: - "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300" %> - <%= form.label :toggle_topics, - "Yes", - value: true, - class: "ml-2 block text-sm text-gray-700" %> -
-
- <%= form.radio_button :toggle_topics, - false, - class: - "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300" %> - <%= form.label :toggle_topics, - "No", - value: false, - class: "ml-2 block text-sm text-gray-700" %> -
-
+ <%= render "course_code_form", course: @course %>
+
- Lecturer topics require approval? -
-
- <%= form.radio_button :require_coordinator_approval, - true, - class: - "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300" %> - <%= form.label :require_coordinator_approval, - "Yes", - value: true, - class: "ml-2 block text-sm text-gray-700" %> -
-
- <%= form.radio_button :require_coordinator_approval, - false, - class: - "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300" %> - <%= form.label :require_coordinator_approval, - "No", - value: false, - class: "ml-2 block text-sm text-gray-700" %> +

Supervisor Capacity

+ +
+
+ <%= form.label :supervisor_projects_limit, "Maximum Projects per Supervisor", class: "absolute top-2 left-4 text-xs font-medium text-[#5F6368]" %> + <%= form.hidden_field :supervisor_projects_limit %> + <%= form.number_field :supervisor_projects_limit, id: "supervisor_projects_limit_visible", required: true, disabled: @course.supervisor_auto_calculate_enabled?, class: "w-full bg-transparent border-none p-0 text-[#3C4043] focus:ring-0 text-[14px] mt-1" %>
+ + <%= render "supervisor_capacity_settings", form: form, capacity_result: @capacity_result, lecturer_enrolments: @lecturer_enrolments %>
+ + + +
+

Permissions and Rules

+ +
+
+ +
+ Allow lecturers to add topics for students to base their proposals on. +
+ + +
+
-
- Auto-approve unchanged copied topics? -
-
- <%= form.radio_button :auto_approve_copied_topics_without_changes, - true, - class: "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300" %> - <%= form.label :auto_approve_copied_topics_without_changes, - "Yes", - value: true, - class: "ml-2 block text-sm text-gray-700" %> +
+ Lecturer topics require approval? +
+ + +
-
- <%= form.radio_button :auto_approve_copied_topics_without_changes, - false, - class: "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300" %> - <%= form.label :auto_approve_copied_topics_without_changes, - "No", - value: false, - class: "ml-2 block text-sm text-gray-700" %> + +
+ Auto-approve unchanged copied topics? +
+ + +
-
-
-
- Use progress updates? -
-
- <%= form.radio_button :use_progress_updates, - true, - class: - "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300" %> - <%= form.label :use_progress_updates, - "Yes", - value: true, - class: "ml-2 block text-sm text-gray-700" %> +
+ Use progress updates? +
+ + +
-
- <%= form.radio_button :use_progress_updates, - false, - class: - "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300" %> - <%= form.label :use_progress_updates, - "No", - value: false, - class: "ml-2 block text-sm text-gray-700" %> + +
+ Lecturer views public? +
+ + +
-
-
-
- Lecturer views public? -
-
- <%= form.radio_button :lecturer_access, - true, - class: - "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300" %> - <%= form.label :lecturer_access, - "Yes", - value: true, - class: "ml-2 block text-sm text-gray-700" %> +
+ Number of progress updates +
+ <%= form.number_field :number_of_updates, class: "w-full bg-transparent border-none p-0 text-[#3C4043] focus:ring-0 text-[14px] text-center" %> +
-
- <%= form.radio_button :lecturer_access, - false, - class: - "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300" %> - <%= form.label :lecturer_access, - "No", - value: false, - class: "ml-2 block text-sm text-gray-700" %> + +
+ +
+ Student can see projects from: +
+ <% Course.student_accesses.keys.map { |key| [key, key.to_s.humanize] }.each_with_index do |(value, label_text), idx| %> +
+ <%= form.radio_button :student_access, value, id: "student_access_#{idx}", class: "text-[#1A73E8] border-[#DADCE0]" %> + <%= form.label :student_access, label_text, value: value, for: "student_access_#{idx}", class: "ml-3 block text-sm text-[#3C4043] cursor-pointer" %> +
+ <% end %>
+
+ + +
+
+ +
+
+

Student Self-Grouping Beta

+

Group formation in ProPro is still in progress!

+
+
-
- <%= form.label :number_of_updates, - "Number of progress updates", - class: "block text-sm font-medium text-gray-700" %> -
- <%= form.number_field :number_of_updates, - class: - "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm" %> + +
+ Allow students to form their own groups? +
+ + +
-
-
-
- Student can see projects from: -
- <% Course.student_accesses.keys.map { |key| [key, key.to_s.humanize] }.each_with_index do |(value, label_text), idx| %> -
- <%= form.radio_button :student_access, - value, - id: "student_access_#{idx}", - class: - "focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300" %> - <%= form.label :student_access, - label_text, - value: value, - for: "student_access_#{idx}", - class: "ml-3 block text-sm text-gray-700" %> + +
+ + +
+ + Group Configuration + + + +
+ Student list mode +

+ Choose whether the coordinator knows the exact final count of students. +

+ +
+ + + + + <% if @course.persisted? && @course.grouping_enabled? && !@course.student_list_finalised? %> +
+
+
+ Final student list + + Fixed list mode cannot be enabled once default mode has been saved. + +
+
+ <% else %> + + <% end %> +
+
+ + +
+
+ <%= form.label :group_min, "Minimum group size", class: "absolute top-2 left-4 text-xs font-medium text-[#5F6368]" %> + <%= form.number_field :group_min, min: 1, placeholder: "e.g. 3", + class: "w-full bg-transparent border-none p-0 text-[#3C4043] focus:ring-0 text-[14px] mt-1", + data: { grouping_settings_target: "minMax", action: "input->grouping-settings#clearPreview" } %> +
+ +
+ <%= form.label :group_max, "Maximum group size", class: "absolute top-2 left-4 text-xs font-medium text-[#5F6368]" %> + <%= form.number_field :group_max, min: 1, placeholder: "e.g. 4", + class: "w-full bg-transparent border-none p-0 text-[#3C4043] focus:ring-0 text-[14px] mt-1", + data: { grouping_settings_target: "minMax", action: "input->grouping-settings#clearPreview" } %> +
+
+ + +
+
+

Preview group distribution

+

+ Enter an expected student count to see how legal groups would form. Actual numbers will come from enrolled students. +

+
+
+
+ +
+ students +
+
+
- <% end %> -
-
-
- <%= render "grouping_settings", form: form %> - -
- <%= link_to "Add Students", - add_students_course_path(@course), - class: - "w-full sm:w-auto inline-flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500" %> - <%= link_to "Add Lecturers", - add_lecturers_course_path(@course), - class: - "w-full sm:w-auto inline-flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500" %> -
+ +
+ + Student Access + + + +
+
+ Allow student grouping now + + Students can join, leave, and form groups only when this is on. + +
+
+ + +
+
+ + +
+
+
+ Auto-close window (optional) + + Optionally set specific dates to open and close student access automatically. + +
+ +
+ +
+
+ <%= form.label :grouping_opens_at, "Open from", class: "absolute top-2 left-4 text-xs font-medium text-[#5F6368]" %> + <%= form.datetime_local_field :grouping_opens_at, + class: "w-full bg-transparent border-none p-0 text-[#3C4043] focus:ring-0 text-[14px] mt-1", + value: @course.grouping_opens_at&.strftime('%Y-%m-%dT%H:%M') %> +
+
+ <%= form.label :grouping_closes_at, "Close at", class: "absolute top-2 left-4 text-xs font-medium text-[#5F6368]" %> + <%= form.datetime_local_field :grouping_closes_at, + class: "w-full bg-transparent border-none p-0 text-[#3C4043] focus:ring-0 text-[14px] mt-1", + value: @course.grouping_closes_at&.strftime('%Y-%m-%dT%H:%M') %> +
+
+
-
+
- <%= link_to course_path(@course), class: "order-2 sm:order-1 inline-flex items-center text-sm text-gray-500 hover:text-gray-900 transition-colors" do %> - - Back to Course - <% end %> +
+
+
+ + +
+
+ <%= link_to add_students_course_path(@course), class: "w-full sm:w-auto inline-flex justify-center items-center gap-2 border border-[#DADCE0] text-[#1A73E8] hover:bg-[#F8F9FA] px-6 py-2.5 rounded-full font-medium text-[14px] transition-colors" do %> + person_add + Add Students + <% end %> + <%= link_to add_lecturers_course_path(@course), class: "w-full sm:w-auto inline-flex justify-center items-center gap-2 border border-[#DADCE0] text-[#1A73E8] hover:bg-[#F8F9FA] px-6 py-2.5 rounded-full font-medium text-[14px] transition-colors" do %> + group_add + Add Lecturers + <% end %> +
-
- <%= link_to "Delete Course", - course_path(@course), - class: - "w-full sm:w-auto inline-flex justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500", - data: { - turbo_method: :delete, - turbo_confirm: "Are you sure? This cannot be undone.", - } %> - - <%= form.submit "Save Settings", - class: - "w-full sm:w-auto inline-flex justify-center px-6 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-700 hover:bg-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 cursor-pointer" %> + <%= link_to course_path(@course), class: "w-full sm:w-auto inline-flex justify-center items-center gap-2 text-[#C5221F] hover:bg-[#FCE8E6] px-5 py-2.5 rounded-full font-medium text-[14px] transition-colors", data: { turbo_method: :delete, turbo_confirm: "Are you sure? This cannot be undone." } do %> + delete + Delete Course + <% end %>
-
- <% end %> -
-
+
+
+ <% end %> +
diff --git a/app/views/project_templates/edit.html.erb b/app/views/project_templates/edit.html.erb index 5100141e..6fccf46d 100644 --- a/app/views/project_templates/edit.html.erb +++ b/app/views/project_templates/edit.html.erb @@ -46,7 +46,7 @@
<% end %> - <%= render "courses/copy_course_overlay", course: @course, mode: "template" %> + <%= render "courses/copy_course_overlay", course: @course, mode: "template", courses: @courses %>
diff --git a/docs/adr/0009-sibling-forms-on-course-settings.md b/docs/adr/0009-sibling-forms-on-course-settings.md new file mode 100644 index 00000000..e5b42ebc --- /dev/null +++ b/docs/adr/0009-sibling-forms-on-course-settings.md @@ -0,0 +1,65 @@ +# ADR 009 — Sibling forms on course settings + +Date: 2026-08-29 +Status: Superseded by ADR-0010 + +## Context + +The course settings redesign (Google-Classroom-style mockup) renders the +coursecode subsection inside SECTION 2 (General), which on `main` lives inside +the outer settings ``. But `_course_code_form.html.erb` opens its own +`` (posted to `update_coursecode`, wired to the `coursecode-form-handler` +Stimulus controller and the `course_code_form` turbo frame). A `` nested +inside another `` is invalid HTML5; browsers silently drop the inner +`` open tag, so the Stimulus controller never connects and Generate / +"Allow joining via course code" silently no-op. The page has zero system, +request, or policy test coverage, so nothing in CI can catch it. + +The alternative — merging the coursecode fields into the settings form and +moving persistence into `handle_settings` — was rejected for two reasons: + +- A regenerate-credential control (Join Code, API key, invite code) is a + distinct, idempotent, side-effecting action expected to fire immediately and + independently of unsaved form state. Tying it to the settings Save couples + two unrelated persistence moments in both directions. +- `Course#generate_coursecode!` does its own `save!` and raises for grouped + courses. Called inside `handle_settings`'s transaction, that guard failing + would roll back the entire settings save (course name, permissions, grouping, + supervisor capacity). + +The mockup's "General wraps Coursecode" is a design-sketch consequence of +nesting; it cannot be reproduced with a DOM-sibling form using ordinary flow +CSS, and position tricks are fragile. + +## Decision + +Course settings is a **sibling-forms page**: + +- The settings `` (posted to + `handle_settings`) wraps Class Details, Supervisor Capacity, Permissions and + Rules, Grouping, and the footer actions. +- `_course_code_form` keeps its own ``, the `update_coursecode` endpoint, + the `coursecode-form-handler` controller, and the `course_code_form` turbo + frame. It renders as a peer section card below the settings form, restyled to + match the other section cards, and its Generate/toggle persist immediately. +- The `form="..."` attribute may be used to associate a submit button that + lives outside its form (ADR-0005), but is not required while the sticky + header stays inside the settings form. + +Invariant: **a partial that contains its own `` renders only as a +page-level sibling of any other form, never inside one.** + +## Consequences + +- Generate / Re-Generate and the enable toggle keep immediate, independent + persistence with their own flash messages; no cross-action transaction + coupling. +- The mockup's "General" container is dropped; Coursecode and Supervisor + Capacity are standalone section cards, and there is no valid-HTML violation. +- `update_coursecode`, `coursecode_form_handler_controller.js`, + `_course_code_form.html.erb`, and `update_coursecode_test.rb` are untouched + (aside from restyling and the locals-only `course:` conversion). +- `handle_settings`'s whitelist never gains `coursecode` / `coursecode_enabled` + (unchanged anchor). +- A new system test exercises the coursecode generate/toggle path through the + settings page DOM. \ No newline at end of file diff --git a/docs/adr/0010-formless-coursecode-widget.md b/docs/adr/0010-formless-coursecode-widget.md new file mode 100644 index 00000000..4c22dc1d --- /dev/null +++ b/docs/adr/0010-formless-coursecode-widget.md @@ -0,0 +1,69 @@ +# ADR 010 — Formless coursecode widget on course settings + +Date: 2026-08-30 +Status: Accepted +Supersedes: ADR-0009 + +## Context + +ADR-0009 kept the coursecode control as a standalone sibling `` below the +settings form so it would never nest inside the settings form's HTML. That +worked but drifted from the Google-Classroom mockup on three fronts: + +1. **Placement** — the mockup puts Coursecode inside SECTION 2 (General). A + DOM-sibling form cannot live inside the settings form; position tricks were + rejected in ADR-0009. +2. **Behavior** — Course code widgets (Classroom's "Manage invitation codes", + an API-key rotate button) are not forms; each control fires its own + immediate request, with no submit ceremony. +3. **Bug** — the standalone sibling card with an inner layout wrapper + (`w-full max-w-4xl mx-auto px-4 sm:px-6 pb-8 -mt-8` around the + ``) made the card "keep getting slimmer" + on every Generate click. Turbo Streams `replace()` swaps in the **entire** + `