diff --git a/app/assets/javascripts/feedback.js b/app/assets/javascripts/feedback.js new file mode 100644 index 000000000..7d9ab3772 --- /dev/null +++ b/app/assets/javascripts/feedback.js @@ -0,0 +1,74 @@ +$(document).on("turbolinks:load", () => { + if (!shouldRegisterFeedback()) { + return; + } + registerToasts(); + registerSubmitButtonHandler(); + registerFeedbackBodyValidator(); +}); + +var SUBMIT_FEEDBACK_ID = "#submit-feedback"; + +var TOAST_OPTIONS = { + animation: true, + autohide: true, + delay: 6000, // autohide after ... milliseconds +}; + +function shouldRegisterFeedback() { + return $(SUBMIT_FEEDBACK_ID).length > 0; +} + +function registerToasts() { + const toastElements = document.querySelectorAll(".toast"); + [...toastElements].map((toast) => { + new bootstrap.Toast(toast, TOAST_OPTIONS); + }); +} + +function registerSubmitButtonHandler() { + // Invoke the hidden submit button inside the actual Rails form + $("#submit-feedback-form-btn-outside").click(() => { + submitFeedback(); + }); + + // Submit form by pressing Ctrl + Enter + document.addEventListener("keydown", (event) => { + const isModalOpen = $(SUBMIT_FEEDBACK_ID).is(":visible"); + if (isModalOpen && event.ctrlKey && event.key == "Enter") { + submitFeedback(); + } + }); +} + +function registerFeedbackBodyValidator() { + const feedbackBody = document.getElementById("feedback_feedback"); + feedbackBody.addEventListener("input", () => { + validateFeedback(); + }); +} + +function validateFeedback() { + const feedbackBody = document.getElementById("feedback_feedback"); + const validityState = feedbackBody.validity; + if (validityState.tooShort) { + const tooShortMessage = feedbackBody.dataset.tooShortMessage; + feedbackBody.setCustomValidity(tooShortMessage); + } + else if (validityState.valueMissing) { + const valueMissingMessage = feedbackBody.dataset.valueMissingMessage; + feedbackBody.setCustomValidity(valueMissingMessage); + } + else { + // render input valid, so that form will submit + feedbackBody.setCustomValidity(""); + } + + feedbackBody.reportValidity(); +} + +function submitFeedback() { + const submitButton = $("#submit-feedback-form-btn"); + validateFeedback(); + submitButton.click(); +} diff --git a/app/assets/javascripts/lectures.coffee b/app/assets/javascripts/lectures.coffee index 15dfacca0..5e47d4fea 100644 --- a/app/assets/javascripts/lectures.coffee +++ b/app/assets/javascripts/lectures.coffee @@ -214,6 +214,7 @@ $(document).on 'turbolinks:load', -> $('#secondnav').show() $('#lecturesDropdown').appendTo($('#secondnav')) $('#notificationDropdown').appendTo($('#secondnav')) + $('#feedback-btn').appendTo($('#secondnav')) $('#searchField').appendTo($('#secondnav')) $('#second-admin-nav').show() $('#adminDetails').appendTo($('#second-admin-nav')) @@ -236,6 +237,7 @@ $(document).on 'turbolinks:load', -> $('#secondnav').hide() $('#lecturesDropdown').appendTo($('#firstnav')) $('#notificationDropdown').appendTo($('#firstnav')) + $('#feedback-btn').appendTo($('#firstnav')) $('#searchField').appendTo($('#firstnav')) $('#second-admin-nav').hide() $('#teachableDrop').appendTo($('#first-admin-nav')) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 35f8c0d78..4fed1ddea 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -302,4 +302,8 @@ a { &:hover { text-decoration: underline; } +} + +.toast { + background-color: white; } \ No newline at end of file diff --git a/app/assets/stylesheets/feedback.scss b/app/assets/stylesheets/feedback.scss new file mode 100644 index 000000000..7f14ad18c --- /dev/null +++ b/app/assets/stylesheets/feedback.scss @@ -0,0 +1,11 @@ +#feedback-btn { + color: white; + + &:focus { + box-shadow: none; + } + + &:hover { + color: #ffc107; + } +} \ No newline at end of file diff --git a/app/controllers/feedbacks_controller.rb b/app/controllers/feedbacks_controller.rb new file mode 100644 index 000000000..9ae31a957 --- /dev/null +++ b/app/controllers/feedbacks_controller.rb @@ -0,0 +1,21 @@ +class FeedbacksController < ApplicationController + authorize_resource except: [:create] + + def create + feedback = Feedback.new(feedback_params) + feedback.user_id = current_user.id + @feedback_success = feedback.save + + if @feedback_success + FeedbackMailer.with(feedback: feedback).new_user_feedback_email.deliver_later + end + + respond_to(&:js) + end + + private + + def feedback_params + params.require(:feedback).permit(:title, :feedback, :can_contact) + end +end diff --git a/app/mailers/feedback_mailer.rb b/app/mailers/feedback_mailer.rb new file mode 100644 index 000000000..152b1378b --- /dev/null +++ b/app/mailers/feedback_mailer.rb @@ -0,0 +1,15 @@ +class FeedbackMailer < ApplicationMailer + default from: DefaultSetting::FEEDBACK_EMAIL + layout false + + # Mail to the MaMpf developers including the new feedback of a user. + def new_user_feedback_email + @feedback = params[:feedback] + reply_to_mail = @feedback.can_contact ? @feedback.user.email : "" + subject = "Feedback: #{@feedback.title}" + mail(to: DefaultSetting::FEEDBACK_EMAIL, + subject: subject, + content_type: "text/plain", + reply_to: reply_to_mail) + end +end diff --git a/app/models/feedback.rb b/app/models/feedback.rb new file mode 100644 index 000000000..2cc0a0c6b --- /dev/null +++ b/app/models/feedback.rb @@ -0,0 +1,10 @@ +# Feedback from users regarding MaMpf itself. +class Feedback < ApplicationRecord + belongs_to :user + + BODY_MIN_LENGTH = 10 + BODY_MAX_LENGTH = 10_000 + validates :feedback, length: { minimum: BODY_MIN_LENGTH, + maximum: BODY_MAX_LENGTH }, + allow_blank: false +end diff --git a/app/models/user.rb b/app/models/user.rb index 40feb6f60..4b612f20e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -78,6 +78,9 @@ class User < ApplicationRecord # a user has a watchlist with watchlist_entries has_many :watchlists, dependent: :destroy + + has_many :feedbacks, dependent: :destroy + include ScreenshotUploader[:image] # if a homepage is given it should at leat be a valid address diff --git a/app/views/feedback_mailer/new_user_feedback_email.text.erb b/app/views/feedback_mailer/new_user_feedback_email.text.erb new file mode 100644 index 000000000..9b580a4a4 --- /dev/null +++ b/app/views/feedback_mailer/new_user_feedback_email.text.erb @@ -0,0 +1,13 @@ +# Title (optional) +<%= @feedback.title %> + +# Feedback +<%= @feedback.feedback %> + + +----- +<% if @feedback.can_contact %> +Reply to this mail to contact the user. +<% else %> +User did not give permission to contact them regarding their feedback, so we cannot reply to this mail. +<% end %> diff --git a/app/views/feedbacks/_feedback.html.erb b/app/views/feedbacks/_feedback.html.erb new file mode 100644 index 000000000..b98f5c8bc --- /dev/null +++ b/app/views/feedbacks/_feedback.html.erb @@ -0,0 +1,44 @@ +<%= stylesheet_link_tag 'feedback' %> + +<%# Main Modal %> + + +<%# Messages toasts %> +
+ <%# Error %> + + + <%# Success %> + + +
diff --git a/app/views/feedbacks/_feedback_button.html.erb b/app/views/feedbacks/_feedback_button.html.erb new file mode 100644 index 000000000..c3d9c02fa --- /dev/null +++ b/app/views/feedbacks/_feedback_button.html.erb @@ -0,0 +1,8 @@ +<%= stylesheet_link_tag 'feedback' %> + +<%# Feedback button %> + diff --git a/app/views/feedbacks/_feedback_form.html.erb b/app/views/feedbacks/_feedback_form.html.erb new file mode 100644 index 000000000..2e0833e3e --- /dev/null +++ b/app/views/feedbacks/_feedback_form.html.erb @@ -0,0 +1,63 @@ +<%= javascript_include_tag :feedback %> + + + + + + diff --git a/app/views/feedbacks/create.js.erb b/app/views/feedbacks/create.js.erb new file mode 100644 index 000000000..1538b24bf --- /dev/null +++ b/app/views/feedbacks/create.js.erb @@ -0,0 +1,7 @@ +<% if @feedback_success %> +$("#submit-feedback").modal("hide"); +$("#submit-feedback").find("form").trigger("reset"); // clear form +$("#toast-successfully-sent").toast("show"); +<% else %> +$("#toast-could-not-send").toast("show"); +<% end %> diff --git a/app/views/shared/_dropdown_notifications.html.erb b/app/views/shared/_dropdown_notifications.html.erb index 2117400a9..5405d3bc0 100644 --- a/app/views/shared/_dropdown_notifications.html.erb +++ b/app/views/shared/_dropdown_notifications.html.erb @@ -1,7 +1,7 @@ <% relevant_notifications = current_user.notifications .sort_by(&:created_at).reverse %> <% if relevant_notifications.present? %> - -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/shared/_footer.html.erb b/app/views/shared/_footer.html.erb index efb373378..501a4b168 100644 --- a/app/views/shared/_footer.html.erb +++ b/app/views/shared/_footer.html.erb @@ -46,7 +46,7 @@ bugs: link_to(t('here'), 'https://github.com/MaMpf-HD/mampf/issues', target: :_blank), - feedback: mail_to(DefaultSetting::PROJECT_EMAIL, nil), + feedback: mail_to(DefaultSetting::FEEDBACK_EMAIL, nil), background_photo_credit: link_to('Craig S. Kaplan', 'https://github.com/isohedral/hatviz', target: :_blank)) %> diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index 18cc82648..73e98ebf1 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -1,5 +1,8 @@ <% I18n.with_locale(current_user.try(:locale)) do %> <%= stylesheet_link_tag 'navbar' %> + +<%= render partial: 'feedbacks/feedback' %> +