diff --git a/app/controllers/candidates_controller.rb b/app/controllers/candidates_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..61f1c3c9a6280949cfe56e0892f796902fbf0689 --- /dev/null +++ b/app/controllers/candidates_controller.rb @@ -0,0 +1,59 @@ +require 'icalendar/tzinfo' + +class CandidatesController < ApplicationController + def create + @session = Session.find_by(ref_id: params[:session_ref_id]) + @conference = Conference.find_by(slug: params[:conference_slug]) + @user = User.find(session[:user_id]) + @candidate = Candidate.find_or_initialize_by(user: @user, session: @session).tap do |candidate_| + candidate_.note = params[:note] + candidate_.save! + end + + if @candidate + Rails.logger.debug("Saved candidate #{@candidate.inspect}") + #@session = Session.find_by(ref_id: params[:session_ref_id]) + Turbo::StreamsChannel.broadcast_replace_to( + @session.conference, + target: helpers.dom_id(@session), + partial: "sessions/session", + locals: { session: @session } + ) + flash.now[:success] = 'User assigned successfully.' + respond_to do |format| + format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }) } + format.html { redirect_to conference_session_path(@session.conference, @session), success: 'User assigned successfully.' } + end + else + flash.now[:alert] = 'Failed to record candidate.' + respond_to do |format| + format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }), status: :unprocessable_entity } + format.html { render :show, status: :unprocessable_entity } + end + end + end + + def destroy + @candidate = Candidate.find(params[:id]) + @session = @candidate.session + + if @candidate&.destroy + Rails.logger.debug("destroyed candidate entry") + Turbo::StreamsChannel.broadcast_replace_later_to( + @session.conference, + target: helpers.dom_id(@session), + partial: "sessions/session", + locals: { session: @session } + ) + respond_to do |format| + format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }) } + format.html { redirect_to conference_session_path(@session.conference, @session), notice: 'Candidate removed successfully.' } + end + else + respond_to do |format| + format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }), status: :unprocessable_entity } + format.html { redirect_to conference_session_path(@session.conference, @session), alert: 'Failed to remove candidate.' } + end + end + end +end diff --git a/app/javascript/controllers/bothhands_controller.js b/app/javascript/controllers/bothhands_controller.js new file mode 100644 index 0000000000000000000000000000000000000000..e818f5719abb856472b4b1a568d5f9edd7caacaf --- /dev/null +++ b/app/javascript/controllers/bothhands_controller.js @@ -0,0 +1,19 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["form", "link"]; + + submitWithPrompt(event) { + event.preventDefault(); + + const reason = prompt("Why are you especially qualified? (Please keep it short, thanks)"); + if (reason !== null && reason.trim() !== "") { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = "note"; + input.value = reason; + this.formTarget.appendChild(input); + this.formTarget.requestSubmit(); + } + } +} diff --git a/app/models/candidate.rb b/app/models/candidate.rb new file mode 100644 index 0000000000000000000000000000000000000000..4d402d1310cc2c965531f9b5356b743ce3b8ad90 --- /dev/null +++ b/app/models/candidate.rb @@ -0,0 +1,6 @@ +class Candidate < ApplicationRecord + belongs_to :user + belongs_to :session + + validates :user_id, uniqueness: { scope: :session_id, message: "has already recorded interest for this session" } +end diff --git a/app/models/session.rb b/app/models/session.rb index b45ecce6d6f1845c0e29335b6cce5b54dcdea680..28e5bbf5ef4465881b47f0cac2deb180ef976ebd 100644 --- a/app/models/session.rb +++ b/app/models/session.rb @@ -3,6 +3,7 @@ class Session < ApplicationRecord belongs_to :stage has_many :assignments has_many :users, through: :assignments + has_many :candidates has_many :session_speakers, dependent: :destroy has_many :speakers, through: :session_speakers diff --git a/app/models/user.rb b/app/models/user.rb index 5918786c2c51182d70ea3d5bd067bf8636805dc3..c6a2a7a9166ba36f79cc3963f677daa6ac477f30 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,6 @@ class User < ApplicationRecord has_many :assignments + has_many :candidates has_secure_password diff --git a/app/views/candidates/_user_avatar.html.erb b/app/views/candidates/_user_avatar.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..04f1a3b84a30641888bca5a58c117b40c7c96968 --- /dev/null +++ b/app/views/candidates/_user_avatar.html.erb @@ -0,0 +1,17 @@ +<% user = candidate.user %> +<% session = candidate.session %> +<span class="gap-x-0.5 rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10" style="background-color: <%= user.avatar_color %>" title="<%= user.name %>"> + <span style="color: <%= user.text_color %>"><%= user.name %></span> + <button type="button" class="group relative -mr-1 size-3.5 rounded-sm hover:bg-gray-500/20"> + <%= link_to conference_session_assignments_path(session.conference, session, user_id: user.id), data: { turbo_method: :post, turbo_frame: dom_id(session) } do %> + <span class="sr-only">Add</span> + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16" stroke-width="1" class="size-3.5 stroke-gray-600/50 group-hover:stroke-gray-600/75 fill-gray-600/50" style="stroke: <%= user.text_color %>; fill: <%= user.text_color %>"> + <path d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" /> + </svg> + <span class="absolute -inset-1"></span> + <% end %> + </button> +</span> +<% if candidate.note %> + <span class="relative ml-1.5"><span class="max-w-60 max-h-14 overflow-scroll bg-gray-600 text-white text-xs font-medium px-2 py-1 rounded-md relative inline-block"><%= candidate.note %></span><span class="w-0 h-0 border-t-[6px] border-t-transparent border-b-[6px] border-b-transparent border-r-[6px] border-r-gray-600 absolute left-[-5px] top-[20%] transform -translate-y-1/2"></span> </span> +<% end %> diff --git a/app/views/sessions/_session.html.erb b/app/views/sessions/_session.html.erb index a0eeadbbfc72290d647e13f116e4df3cd33dfee8..e34e709a7e39a3f4e567f6dbb92b586dcfad729f 100644 --- a/app/views/sessions/_session.html.erb +++ b/app/views/sessions/_session.html.erb @@ -5,7 +5,7 @@ <small class="text-2xs uppercase font-light bg-black/10 rounded-sm p-1 mr-1 lang-<%= session.language %>"><%= session.language %></small> <%= link_to session.title, session.url, target: "_top" %> </h4> - <div> + <div class="relative"> <span class="session-time text-xs mr-1"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="inline-block size-4 stroke-slate-500"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /> @@ -20,6 +20,24 @@ </svg> <%= session.stage.name %> </span> + <% if logged_in? %> + <span class="absolute top-0 right-0 text-3xl"> + <% if candidate = session.candidates.find_by(user_id: current_user.id) %> + <%= link_to conference_session_candidate_path(session.conference, session, candidate), class: "pr-1", title: "Withdraw", aria_label: "Withdraw", data: { turbo_method: :delete, turbo_frame: dom_id(session) }, method: :delete do %> + 🙅 + <% end %> + <% else %> + <%= link_to conference_session_candidates_path(session.conference, session), class: "pr-1", title: "Volunteer Myself", aria_label: "Volunteer Myself", data: { turbo_method: :post, turbo_frame: dom_id(session) } do %> + 🙋 + <% end %> + <%= form_with url: conference_session_candidates_path(session.conference, session), method: :post, class: "inline", data: { turbo_frame: dom_id(session), controller: "bothhands", bothhands_target: "form" } do |form| %> + <%= link_to conference_session_candidates_path(session.conference, session), class: "pr-1", title: "Volunteer (with special skills)", aria_label: "Volunteer (with special skills)", data: { action: "bothhands#submitWithPrompt", turbo_prefetch:"false" } do %> + 🙌 + <% end %> + <% end %> + <% end %> + </span> + <% end %> </div> <div> <ul class="flex flex-wrap text-xs gap-1"> @@ -57,6 +75,15 @@ <%= @assignment.errors.full_messages.join(", ") %> </div> <% end %> + <small>candidates (<%= session.candidates.length %>)</small> + <ul class="inline-flex flex-wrap gap-1 my-1"> + <% session.candidates.sort_by { |cand| cand.note.nil? ? 1:0 }.each do |candidate| %> + <li class="inline-flex items-end"> + <span class="assigned-user"><%= render partial: 'candidates/user_avatar', locals: { candidate: } %></span> + </li> + <% end %> + </ul> + <hr> <small>unassigned (<%= unassigned_users.length %>)</small> <%= render partial: 'assignments/filteredlist', locals: { session: session, users: unassigned_users } %> </div> diff --git a/config/routes.rb b/config/routes.rb index cca06578e2f8b6c1dca1eb4c35e5c13a66128064..0483a6e75f73813647670ac1453ad5caa500bf1b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -25,6 +25,7 @@ Rails.application.routes.draw do get ':date', action: :show, on: :member, as: :date, date: /\d{4}-\d{2}-\d{2}/ resources :sessions, param: :ref_id do resources :assignments, only: [:create, :destroy] + resources :candidates, only: [:create, :destroy] end resources :speakers, param: :ref_id end diff --git a/db/migrate/20241118161733_create_candidates.rb b/db/migrate/20241118161733_create_candidates.rb new file mode 100644 index 0000000000000000000000000000000000000000..38b6177f7b5a9de140fcc5fd66c59c9618c0e7a6 --- /dev/null +++ b/db/migrate/20241118161733_create_candidates.rb @@ -0,0 +1,11 @@ +class CreateCandidates < ActiveRecord::Migration[7.1] + def change + create_table :candidates do |t| + t.references :user, null: false, foreign_key: true + t.references :session, null: false, foreign_key: true + t.string :note, null: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 801a3e85cef60567962eb992fb73a36fcb49f166..bcd53dd3b0425adb96a3c101aa24aed72370f16f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_05_27_191332) do +ActiveRecord::Schema[7.1].define(version: 2024_11_18_161733) do create_table "assignments", force: :cascade do |t| t.integer "user_id", null: false t.integer "session_id", null: false @@ -20,6 +20,16 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_27_191332) do t.index ["user_id"], name: "index_assignments_on_user_id" end + create_table "candidates", force: :cascade do |t| + t.integer "user_id", null: false + t.integer "session_id", null: false + t.string "note" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["session_id"], name: "index_candidates_on_session_id" + t.index ["user_id"], name: "index_candidates_on_user_id" + end + create_table "conferences", force: :cascade do |t| t.string "name" t.datetime "starts_at" @@ -270,6 +280,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_27_191332) do add_foreign_key "assignments", "sessions" add_foreign_key "assignments", "users" + add_foreign_key "candidates", "sessions" + add_foreign_key "candidates", "users" add_foreign_key "relevant_stages", "conferences" add_foreign_key "relevant_stages", "stages" add_foreign_key "revision_sets", "conferences" diff --git a/test/fixtures/candidates.yml b/test/fixtures/candidates.yml new file mode 100644 index 0000000000000000000000000000000000000000..2d3d603f0f9fde8565a15521f05c21708739bebe --- /dev/null +++ b/test/fixtures/candidates.yml @@ -0,0 +1,7 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + note: MyString + +two: + note: MyString diff --git a/test/models/candidate_test.rb b/test/models/candidate_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..f8edf6f9d492ebdb3fb3e4909c441478fe08ef2c --- /dev/null +++ b/test/models/candidate_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class CandidateTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end