diff --git a/app/controllers/contacts_controller.rb b/app/controllers/contacts_controller.rb
index f7f593c16c..d950a896cd 100644
--- a/app/controllers/contacts_controller.rb
+++ b/app/controllers/contacts_controller.rb
@@ -37,7 +37,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 profile_data_to_change == 'country_iso2'
Country.find_by(iso2: new_profile_data).name
else
new_profile_data
@@ -99,6 +99,44 @@ 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]
+ attachment = params[:attachment]
+ wca_id = formValues[:wcaId]
+
+ return render status: :unauthorized, json: { error: "Cannot request profile change without login" } unless 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,
+ }
+ changes_requested = Person.fields_edit_requestable
+ .reject { |field| profile_to_edit[field].to_s == edited_profile_details[field].to_s }
+ .map { |field|
+ ContactEditProfile::EditProfileChange.new(
+ 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,
+ 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 0000000000..7570e68cb6
--- /dev/null
+++ b/app/models/contact_edit_profile.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class ContactEditProfile < ContactForm
+ attribute :wca_id
+ attribute :changes_requested
+ attribute :edit_profile_reason
+ attribute :document, attachment: true
+
+ EditProfileChange = Struct.new(
+ :field,
+ :from,
+ :to,
+ )
+
+ 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/models/person.rb b/app/models/person.rb
index babb089d19..819e930da4 100644
--- a/app/models/person.rb
+++ b/app/models/person.rb
@@ -269,6 +269,10 @@ def url
Rails.application.routes.url_helpers.person_url(wca_id, host: EnvConfig.ROOT_URL)
end
+ def self.fields_edit_requestable
+ [:name, :country_iso2, :gender, :dob].freeze
+ end
+
DEFAULT_SERIALIZE_OPTIONS = {
only: ["wca_id", "name", "gender"],
methods: ["url", "country_iso2"],
diff --git a/app/views/contacts/edit_profile.html.erb b/app/views/contacts/edit_profile.html.erb
new file mode 100644
index 0000000000..5c54d2b47d
--- /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 0000000000..1ca8340d40
--- /dev/null
+++ b/app/views/mail_form/contact_edit_profile.erb
@@ -0,0 +1,21 @@
+
Hey WRT,
+
+ <%= @resource.wca_id %> requested following change in their profile:
+
+
+
+ <% @resource.changes_requested.each do |change| %>
+
+ <%= change[:field] %> |
+ <%= change[:from] %> -> <%= change[:to] %> |
+
+ <% end %>
+
+ |
+
+
+
+You can edit this person <%= link_to "here", panel_index_url(panel_id: 'wrt', wcaId: @resource.wca_id, anchor: User.panel_pages[:editPerson]) %>.
+<% if @resource.document.present? %>
+ Note: There is a proof attachment to this email.
+<% end %>
diff --git a/app/webpacker/components/ContactEditProfilePage/EditProfileForm.jsx b/app/webpacker/components/ContactEditProfilePage/EditProfileForm.jsx
new file mode 100644
index 0000000000..a927dbe2b5
--- /dev/null
+++ b/app/webpacker/components/ContactEditProfilePage/EditProfileForm.jsx
@@ -0,0 +1,163 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { Form, Message } from 'semantic-ui-react';
+import ReCAPTCHA from 'react-google-recaptcha';
+import { QueryClient, useQuery } from '@tanstack/react-query';
+import i18n from '../../lib/i18n';
+import { apiV0Urls, contactEditProfileActionUrl } from '../../lib/requests/routes.js.erb';
+import { genders, countries } from '../../lib/wca-data.js.erb';
+import Loading from '../Requests/Loading';
+import Errored from '../Requests/Errored';
+import useSaveAction from '../../lib/hooks/useSaveAction';
+import { fetchJsonOrError } from '../../lib/requests/fetchWithAuthenticityToken';
+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,
+ onContactSuccess,
+ recaptchaPublicKey,
+}) {
+ const [editProfileReason, setEditProfileReason] = useState();
+ const [editedProfileDetails, setEditedProfileDetails] = useState();
+ const [proofAttachment, setProofAttachment] = useState();
+ const [captchaValue, setCaptchaValue] = useState();
+ const [captchaError, setCaptchaError] = useState(false);
+ const [saveError, setSaveError] = useState();
+ const { save, saving } = useSaveAction();
+
+ const { data, isLoading, isError } = useQuery({
+ queryKey: ['profileData'],
+ queryFn: () => fetchJsonOrError(apiV0Urls.persons.show(wcaId)),
+ }, CONTACT_EDIT_PROFILE_FORM_QUERY_CLIENT);
+
+ const profileDetails = data?.data?.person;
+
+ const isSubmitDisabled = useMemo(
+ () => !editedProfileDetails || _.isEqual(editedProfileDetails, profileDetails) || !captchaValue,
+ [captchaValue, editedProfileDetails, profileDetails],
+ );
+
+ useEffect(() => {
+ setEditedProfileDetails(profileDetails);
+ }, [profileDetails]);
+
+ const formSubmitHandler = () => {
+ const formData = new FormData();
+
+ formData.append('formValues', JSON.stringify({
+ editedProfileDetails, editProfileReason, wcaId,
+ }));
+ formData.append('attachment', proofAttachment);
+
+ save(
+ contactEditProfileActionUrl,
+ formData,
+ onContactSuccess,
+ { method: 'POST', headers: {}, body: formData },
+ setSaveError,
+ );
+ };
+
+ const handleEditProfileReasonChange = (e, { value }) => {
+ setEditProfileReason(value);
+ };
+
+ const handleProofUpload = (event) => {
+ setProofAttachment(event.target.files[0]);
+ };
+
+ const handleFormChange = (e, { name: formName, value }) => {
+ setEditedProfileDetails((prev) => ({ ...prev, [formName]: value }));
+ };
+
+ const handleDobChange = (date) => handleFormChange(null, {
+ name: 'dob',
+ value: date,
+ });
+
+ if (saving || isLoading) return ;
+ if (saveError || isError) return ;
+
+ return (
+
+
+
+
+
+
+
+
+ {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 0000000000..e75130288b
--- /dev/null
+++ b/app/webpacker/components/ContactEditProfilePage/index.jsx
@@ -0,0 +1,58 @@
+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 EditProfileForm from './EditProfileForm';
+
+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) {
+ return (
+
+
+
+ );
+ }
+ if (loggedInUserData && !wcaId) {
+ return (
+
+
+
+ );
+ }
+ if (contactSuccess) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {i18n.t('page.contact_edit_profile.title')}
+ setContactSuccess(true)}
+ recaptchaPublicKey={recaptchaPublicKey}
+ />
+
+ );
+}
diff --git a/app/webpacker/components/ContactsPage/ContactForm.jsx b/app/webpacker/components/ContactsPage/ContactForm.jsx
index fbd14076a8..04d9e030ad 100644
--- a/app/webpacker/components/ContactsPage/ContactForm.jsx
+++ b/app/webpacker/components/ContactsPage/ContactForm.jsx
@@ -4,7 +4,7 @@ import {
} from 'semantic-ui-react';
import ReCAPTCHA from 'react-google-recaptcha';
import _ from 'lodash';
-import { contactUrl } from '../../lib/requests/routes.js.erb';
+import { contactUrl, contactEditProfileActionUrl } from '../../lib/requests/routes.js.erb';
import useSaveAction from '../../lib/hooks/useSaveAction';
import I18n from '../../lib/i18n';
import UserData from './UserData';
@@ -26,6 +26,15 @@ const CONTACT_RECIPIENTS = [
const CONTACT_RECIPIENTS_MAP = _.keyBy(CONTACT_RECIPIENTS, _.camelCase);
+const getFormRedirection = (formValues) => {
+ if (formValues.contactRecipient === CONTACT_RECIPIENTS_MAP.wrt) {
+ if (formValues[CONTACT_RECIPIENTS_MAP.wrt].queryType === 'edit_profile') {
+ return contactEditProfileActionUrl;
+ }
+ }
+ return null;
+}
+
export default function ContactForm({
loggedInUserData,
recaptchaPublicKey,
@@ -38,9 +47,10 @@ export default function ContactForm({
const contactFormState = useStore();
const dispatch = useDispatch();
const { formValues: { contactRecipient: selectedContactRecipient, userData } } = contactFormState;
+ const formRedirection = useMemo(() => getFormRedirection(contactFormState.formValues));
const isFormValid = (
- selectedContactRecipient && userData.name && userData.email && captchaValue
+ selectedContactRecipient && userData.name && userData.email && (captchaValue || formRedirection)
);
const contactSuccessHandler = () => {
@@ -116,28 +126,39 @@ export default function ContactForm({
))}
{SubForm && }
-
-
- {captchaError && (
-
- )}
-
-
+ {formRedirection ? (
+
+ ) : (
+ <>
+
+
+ {captchaError && (
+
+ )}
+
+
+ >
+ )}
>
);
diff --git a/app/webpacker/components/ContactsPage/SubForms/Wrt/EditProfileQuery.jsx b/app/webpacker/components/ContactsPage/SubForms/Wrt/EditProfileQuery.jsx
deleted file mode 100644
index d75a4daffb..0000000000
--- a/app/webpacker/components/ContactsPage/SubForms/Wrt/EditProfileQuery.jsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import React from 'react';
-import {
- Form, FormField, FormGroup, Radio,
-} from 'semantic-ui-react';
-import { useDispatch, useStore } from '../../../../lib/providers/StoreProvider';
-import { updateSectionData, clearForm, uploadProfileChangeProof } from '../../store/actions';
-import I18n from '../../../../lib/i18n';
-import UtcDatePicker from '../../../wca/UtcDatePicker';
-import { genders, countries } from '../../../../lib/wca-data.js.erb';
-
-const SECTION = 'wrt';
-const PROFILE_DATA_FIELDS = ['name', 'country', 'gender', 'dob'];
-
-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 EditProfileQuery() {
- const {
- formValues: {
- userData: { name: userName, email: userEmail },
- contactRecipient,
- wrt: { profileDataToChange, newProfileData, editProfileReason },
- },
- } = useStore();
- const dispatch = useDispatch();
- const handleFormChange = (_, { name, value }) => dispatch(
- updateSectionData(SECTION, name, value),
- );
-
- const handleProfileDataFieldChange = (_, { value }) => dispatch(
- clearForm({
- userName,
- userEmail,
- contactRecipient,
- queryType: 'edit_profile',
- profileDataToChange: value,
- }),
- );
-
- const handleFileUpload = (event) => dispatch(
- uploadProfileChangeProof(event.target.files[0]),
- );
-
- return (
- <>
-
- {I18n.t('page.contacts.form.wrt.profile_data_to_change.label')}
- {PROFILE_DATA_FIELDS.map((profileDataField) => (
-
-
-
- ))}
-
- {profileDataToChange && (
- <>
- {profileDataToChange === 'name' && (
-
- )}
- {profileDataToChange === 'country' && (
-
- )}
- {profileDataToChange === 'gender' && (
-
- )}
- {profileDataToChange === 'dob' && (
- handleFormChange(null, {
- name: 'newProfileData',
- value: date,
- })}
- />
- )}
-
-
- >
- )}
- >
- );
-}
diff --git a/app/webpacker/components/ContactsPage/SubForms/Wrt/index.jsx b/app/webpacker/components/ContactsPage/SubForms/Wrt/index.jsx
index 3ac32cd9d4..94a59c51a1 100644
--- a/app/webpacker/components/ContactsPage/SubForms/Wrt/index.jsx
+++ b/app/webpacker/components/ContactsPage/SubForms/Wrt/index.jsx
@@ -5,7 +5,6 @@ import {
import I18n from '../../../../lib/i18n';
import { useDispatch, useStore } from '../../../../lib/providers/StoreProvider';
import { updateSectionData } from '../../store/actions';
-import EditProfileQuery from './EditProfileQuery';
import OtherQuery from './OtherQuery';
const SECTION = 'wrt';
@@ -21,13 +20,8 @@ export default function Wrt() {
);
const QueryForm = useMemo(() => {
- if (!selectedQueryType) return null;
- switch (selectedQueryType) {
- case QUERY_TYPES_MAP.editProfile:
- return EditProfileQuery;
- default:
- return OtherQuery;
- }
+ if (!selectedQueryType || selectedQueryType === QUERY_TYPES_MAP.editProfile) return null;
+ return OtherQuery;
}, [selectedQueryType]);
return (
diff --git a/app/webpacker/lib/requests/routes.js.erb b/app/webpacker/lib/requests/routes.js.erb
index 4e1e3b5f5d..2b1dfb194d 100644
--- a/app/webpacker/lib/requests/routes.js.erb
+++ b/app/webpacker/lib/requests/routes.js.erb
@@ -179,6 +179,8 @@ export const adminAvatarsUrl = `<%= CGI.unescape(Rails.application.routes.url_he
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 edf10661d2..47b4be031d 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -869,7 +869,26 @@ en:
label: "Message"
captcha:
validation_error: "Please verify that you are not a robot."
+ next_button: "Next"
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 febba88287..4fd794107f 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', as: :contact_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)