From 47d7e5d6c4e7a3319500375e87426560f05e99d8 Mon Sep 17 00:00:00 2001 From: Daniel James Date: Sat, 24 Aug 2024 20:26:28 +0530 Subject: [PATCH] Edit Profile Form --- app/controllers/contacts_controller.rb | 49 ++++++- app/models/contact_edit_profile.rb | 20 +++ app/views/contacts/edit_profile.html.erb | 5 + app/views/mail_form/contact_edit_profile.erb | 18 +++ .../EditProfileForm.jsx | 93 +++++++++++++ .../EditProfileFormWithWcaId.jsx | 126 ++++++++++++++++++ .../ContactEditProfilePage/index.jsx | 53 ++++++++ app/webpacker/lib/requests/routes.js.erb | 2 + config/locales/en.yml | 18 +++ config/routes.rb | 8 +- 10 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 app/models/contact_edit_profile.rb create mode 100644 app/views/contacts/edit_profile.html.erb create mode 100644 app/views/mail_form/contact_edit_profile.erb create mode 100644 app/webpacker/components/ContactEditProfilePage/EditProfileForm.jsx create mode 100644 app/webpacker/components/ContactEditProfilePage/EditProfileFormWithWcaId.jsx create mode 100644 app/webpacker/components/ContactEditProfilePage/index.jsx diff --git a/app/controllers/contacts_controller.rb b/app/controllers/contacts_controller.rb index abda50fadb9..2016e856943 100644 --- a/app/controllers/contacts_controller.rb +++ b/app/controllers/contacts_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ContactsController < ApplicationController + PERSON_FIELDS_EDIT_REQUESTABLE = [:name, :country_iso2, :gender, :dob].freeze + private def maybe_send_contact_email(contact) if !contact.valid? render status: :bad_request, json: { error: "Invalid contact object created" } @@ -37,7 +39,7 @@ class ContactsController < ApplicationController end private def new_profile_data_key_to_value(new_profile_data, profile_data_to_change) - if profile_data_to_change == 'country' + if ['country', 'country_iso2'].include?(profile_data_to_change) Country.find_by(iso2: new_profile_data).name else new_profile_data @@ -98,6 +100,51 @@ def contact end end + def edit_profile_action + formValues = JSON.parse(params.require(:formValues), symbolize_names: true) + edited_profile_details = formValues[:editedProfileDetails] + edit_profile_reason = formValues[:editProfileReason] + contact_email = formValues[:contactEmail] + attachment = params[:attachment] + wca_id = formValues[:wcaId] + if current_user.present? + profile_to_edit = { + name: current_user.person.name, + country_iso2: current_user.person.country_iso2, + gender: current_user.person.gender, + dob: current_user.person.dob, + } + else + person = Person.find_by(wca_id: wca_id) + profile_to_edit = { + name: person.name, + country_iso2: person.country_iso2, + gender: person.gender, + # DOB should not be visible to unauthenticated users. + } + end + changes_requested = PERSON_FIELDS_EDIT_REQUESTABLE + .reject { |field| profile_to_edit[field].to_s == edited_profile_details[field].to_s } + .map { |field| { + field: field.to_s.humanize, + from: (new_profile_data_key_to_value(profile_to_edit[field], field.to_s) || "Unknown").to_s, + to: new_profile_data_key_to_value(edited_profile_details[field], field.to_s), + } + } + + maybe_send_contact_email( + ContactEditProfile.new( + your_email: current_user&.email || contact_email, + name: profile_to_edit[:name], + wca_id: wca_id, + changes_requested: changes_requested, + edit_profile_reason: edit_profile_reason, + document: attachment, + request: request, + ), + ) + end + def dob @contact = DobContact.new(your_email: current_user&.email) end diff --git a/app/models/contact_edit_profile.rb b/app/models/contact_edit_profile.rb new file mode 100644 index 00000000000..2e623a4f5e4 --- /dev/null +++ b/app/models/contact_edit_profile.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class ContactEditProfile < ContactForm + attribute :wca_id + attribute :changes_requested + attribute :edit_profile_reason + attribute :document, attachment: true + + def to_email + UserGroup.teams_committees_group_wrt.metadata.email + end + + def subject + Time.now.strftime("Edit Profile request by #{wca_id} on %d %b %Y at %R") + end + + def headers + super.merge(template_name: "contact_edit_profile") + end +end diff --git a/app/views/contacts/edit_profile.html.erb b/app/views/contacts/edit_profile.html.erb new file mode 100644 index 00000000000..5c54d2b47da --- /dev/null +++ b/app/views/contacts/edit_profile.html.erb @@ -0,0 +1,5 @@ +<% provide(:title, t('page.contact_edit_profile.title')) %> +<%= react_component("ContactEditProfilePage", { + loggedInUserId: current_user&.id, + recaptchaPublicKey: AppSecrets.RECAPTCHA_PUBLIC_KEY, +}) %> diff --git a/app/views/mail_form/contact_edit_profile.erb b/app/views/mail_form/contact_edit_profile.erb new file mode 100644 index 00000000000..ea2b084d06c --- /dev/null +++ b/app/views/mail_form/contact_edit_profile.erb @@ -0,0 +1,18 @@ +

Hey WRT,

+

+ <%= @resource.wca_id %> requested following change in their profile: +

+ + + <% @resource.changes_requested.each do |change| %> + + + + + <% end %> + + + + +
<%= change[:field] %><%= change[:from] %> -> <%= change[:to] %>
+

You can edit this person <%= link_to "here", panel_index_url(panel_id: 'wrt', wcaId: @resource.wca_id, anchor: User.panel_pages[:editPerson]) %>.

diff --git a/app/webpacker/components/ContactEditProfilePage/EditProfileForm.jsx b/app/webpacker/components/ContactEditProfilePage/EditProfileForm.jsx new file mode 100644 index 00000000000..b25724b9f3a --- /dev/null +++ b/app/webpacker/components/ContactEditProfilePage/EditProfileForm.jsx @@ -0,0 +1,93 @@ +import React, { useEffect } from 'react'; +import { Form } from 'semantic-ui-react'; +import { QueryClient, useQuery } from '@tanstack/react-query'; +import i18n from '../../lib/i18n'; +import { fetchJsonOrError } from '../../lib/requests/fetchWithAuthenticityToken'; +import { genders, countries } from '../../lib/wca-data.js.erb'; +import { apiV0Urls } from '../../lib/requests/routes.js.erb'; +import Loading from '../Requests/Loading'; +import Errored from '../Requests/Errored'; +import UtcDatePicker from '../wca/UtcDatePicker'; + +const CONTACT_EDIT_PROFILE_FORM_QUERY_CLIENT = new QueryClient(); + +const genderOptions = _.map(genders.byId, (gender) => ({ + key: gender.id, + text: gender.name, + value: gender.id, +})); + +const countryOptions = _.map(countries.byIso2, (country) => ({ + key: country.iso2, + text: country.name, + value: country.iso2, +})); + +export default function EditProfileForm({ + wcaId, + setProfileDetailsChanged, + editedProfileDetails, + setEditedProfileDetails, +}) { + const { data, isLoading, isError } = useQuery({ + queryKey: ['profileData'], + queryFn: () => fetchJsonOrError(apiV0Urls.persons.show(wcaId)), + }, CONTACT_EDIT_PROFILE_FORM_QUERY_CLIENT); + const profileDetails = data?.data?.person; + + useEffect(() => { + setEditedProfileDetails(profileDetails); + }, [profileDetails, setEditedProfileDetails]); + + useEffect(() => { + setProfileDetailsChanged( + editedProfileDetails && !_.isEqual(editedProfileDetails, profileDetails), + ); + }, [editedProfileDetails, profileDetails, setProfileDetailsChanged]); + + const handleFormChange = (e, { name: formName, value }) => { + setEditedProfileDetails((prev) => ({ ...prev, [formName]: value })); + }; + + if (isLoading) return ; + if (isError) return ; + + return ( + <> + + + + handleFormChange(null, { + name: 'dob', + value: date, + })} + /> + + ); +} diff --git a/app/webpacker/components/ContactEditProfilePage/EditProfileFormWithWcaId.jsx b/app/webpacker/components/ContactEditProfilePage/EditProfileFormWithWcaId.jsx new file mode 100644 index 00000000000..f1a65b16aa9 --- /dev/null +++ b/app/webpacker/components/ContactEditProfilePage/EditProfileFormWithWcaId.jsx @@ -0,0 +1,126 @@ +import React, { useState } from 'react'; +import { Form, Message } from 'semantic-ui-react'; +import ReCAPTCHA from 'react-google-recaptcha'; +import i18n from '../../lib/i18n'; +import { IdWcaSearch } from '../SearchWidget/WcaSearch'; +import SEARCH_MODELS from '../SearchWidget/SearchModel'; +import EditProfileForm from './EditProfileForm'; +import { contactEditProfileActionUrl } from '../../lib/requests/routes.js.erb'; +import Loading from '../Requests/Loading'; +import Errored from '../Requests/Errored'; +import useSaveAction from '../../lib/hooks/useSaveAction'; + +export default function EditProfileFormWithWcaId({ + wcaId: loggedInUserWcaId, + loggedInUserData, + setContactSuccess, + recaptchaPublicKey, +}) { + const [contactEmail, setContactEmail] = useState(); + const [customWcaId, setCustomWcaId] = useState(); + const [editProfileReason, setEditProfileReason] = useState(); + const [editedProfileDetails, setEditedProfileDetails] = useState(); + const [profileDetailsChanged, setProfileDetailsChanged] = useState(false); + const [proof, setProof] = useState(); + const [captchaValue, setCaptchaValue] = useState(); + const [captchaError, setCaptchaError] = useState(false); + const [saveError, setSaveError] = useState(); + const { save, saving } = useSaveAction(); + const wcaId = loggedInUserWcaId || customWcaId; + + const formSubmitHandler = () => { + const formData = new FormData(); + formData.append('formValues', JSON.stringify({ + editedProfileDetails, editProfileReason, wcaId, contactEmail, + })); + formData.append('attachment', proof); + save( + contactEditProfileActionUrl, + formData, + () => setContactSuccess(true), + { method: 'POST', headers: {}, body: formData }, + setSaveError, + ); + }; + + const handleEditProfileReaconChange = (e, { value }) => { + setEditProfileReason(value); + }; + + const handleProofUpload = (event) => { + setProof(event.target.files[0]); + }; + + if (saving) return ; + if (saveError) return ; + + return ( +
+ {!loggedInUserData && ( + setContactEmail(value)} + required + /> + )} + {!loggedInUserWcaId && ( + setCustomWcaId(value)} + disabled={customWcaId} + multiple={false} + model={SEARCH_MODELS.person} + required + /> + )} + {wcaId && ( + <> + + + + + )} + + + {captchaError && ( + + )} + + + {i18n.t('page.contact_edit_profile.form.submit_edit_request_button.label')} + + + ); +} diff --git a/app/webpacker/components/ContactEditProfilePage/index.jsx b/app/webpacker/components/ContactEditProfilePage/index.jsx new file mode 100644 index 00000000000..92933270004 --- /dev/null +++ b/app/webpacker/components/ContactEditProfilePage/index.jsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import { Container, Header, Message } from 'semantic-ui-react'; +import { QueryClient, useQuery } from '@tanstack/react-query'; +import i18n from '../../lib/i18n'; +import I18nHTMLTranslate from '../I18nHTMLTranslate'; +import { apiV0Urls } from '../../lib/requests/routes.js.erb'; +import Loading from '../Requests/Loading'; +import { fetchJsonOrError } from '../../lib/requests/fetchWithAuthenticityToken'; +import Errored from '../Requests/Errored'; +import EditProfileFormWithWcaId from './EditProfileFormWithWcaId'; + +const CONTACT_EDIT_PROFILE_QUERY_CLIENT = new QueryClient(); + +export default function ContactEditProfilePage({ loggedInUserId, recaptchaPublicKey }) { + const { data: loggedInUserData, isLoading, isError } = useQuery({ + queryKey: ['userData'], + queryFn: () => fetchJsonOrError(apiV0Urls.users.me.userDetails), + enabled: !!loggedInUserId, + }, CONTACT_EDIT_PROFILE_QUERY_CLIENT); + const wcaId = loggedInUserData?.data?.user?.wca_id; + const [contactSuccess, setContactSuccess] = useState(false); + + if (isLoading) return ; + if (isError) return ; + if (loggedInUserData && !wcaId) { + return ( + + + + ); + } + + if (contactSuccess) { + return ( + + ); + } + + return ( + +
{i18n.t('page.contact_edit_profile.title')}
+ +
+ ); +} diff --git a/app/webpacker/lib/requests/routes.js.erb b/app/webpacker/lib/requests/routes.js.erb index 6616e9f5181..29604bbfc02 100644 --- a/app/webpacker/lib/requests/routes.js.erb +++ b/app/webpacker/lib/requests/routes.js.erb @@ -176,6 +176,8 @@ export const editUserAvatarUrl = (userId) => `<%= CGI.unescape(Rails.application export const contactUrl = `<%= CGI.unescape(Rails.application.routes.url_helpers.contact_path) %>` +export const contactEditProfileActionUrl = `<%= CGI.unescape(Rails.application.routes.url_helpers.contact_edit_profile_action_path) %>` + export const contactRecipientUrl = (contactRecipient) => `<%= CGI.unescape(Rails.application.routes.url_helpers.contact_path(contactRecipient: "${contactRecipient}")) %>` export const apiV0Urls = { diff --git a/config/locales/en.yml b/config/locales/en.yml index 95a4a2c9bb8..15a5bbcc7b5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -854,6 +854,24 @@ en: captcha: validation_error: "Please verify that you are not a robot." submit_button: "Submit" + contact_edit_profile: + title: "Edit Profile Form" + not_logged_in_error: "Looks like you are not logged in. Please log in to edit your profile. For any other queries, please contact us." + no_profile_error: "Looks like you don't have a WCA profile yet because you have not participated in any competitions. For any other queries, please contact us." + form: + contact_email: + label: "Contact Email" + wca_id: + label: "Enter WCA ID" + edit_reason: + label: "Reason for the change" + proof_attach: + label: "Attach proof" + submit_edit_request_button: + label: "Submit Edit Request" + captcha: + validation_error: "Please verify that you are not a robot." + success_message: "Request submitted - we'll get back to you soon." #context: Key used on the website homepage homepage: #context: the "Learn more" button diff --git a/config/routes.rb b/config/routes.rb index b5a64f119b0..49cfa9087dc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -235,8 +235,12 @@ get 'contact' => 'contacts#index' post 'contact' => 'contacts#contact' - get 'contact/dob' => 'contacts#dob' - post 'contact/dob' => 'contacts#dob_create' + scope 'contact' do + get 'edit_profile' => 'contacts#edit_profile' + post 'edit_profile' => 'contacts#edit_profile_action', as: :contact_edit_profile_action + get 'dob' => 'contacts#dob' + post 'dob' => 'contacts#dob_create' + end get '/regulations' => 'regulations#show', id: 'index' get '/regulations/wca-regulations-and-guidelines', to: redirect('https://regulations.worldcubeassociation.org/wca-regulations-and-guidelines.pdf', status: 302)