Skip to content

Commit

Permalink
Edit Profile Form
Browse files Browse the repository at this point in the history
  • Loading branch information
danieljames-dj committed Aug 24, 2024
1 parent e17e148 commit 47d7e5d
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 3 deletions.
49 changes: 48 additions & 1 deletion app/controllers/contacts_controller.rb
Original file line number Diff line number Diff line change
@@ -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" }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions app/models/contact_edit_profile.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions app/views/contacts/edit_profile.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<% provide(:title, t('page.contact_edit_profile.title')) %>
<%= react_component("ContactEditProfilePage", {
loggedInUserId: current_user&.id,
recaptchaPublicKey: AppSecrets.RECAPTCHA_PUBLIC_KEY,
}) %>
18 changes: 18 additions & 0 deletions app/views/mail_form/contact_edit_profile.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<p>Hey WRT,</p>
<p>
<%= @resource.wca_id %> requested following change in their profile:
</p>
<table>
<body>
<% @resource.changes_requested.each do |change| %>
<tr>
<td><%= change[:field] %></td>
<td><%= change[:from] %> -> <%= change[:to] %></td>
</tr>
<% end %>
<tr style="height: 1em">
<td colspan="2"></td>
</tr>
</body>
</table>
<p>You can edit this person <%= link_to "here", panel_index_url(panel_id: 'wrt', wcaId: @resource.wca_id, anchor: User.panel_pages[:editPerson]) %>.</p>
Original file line number Diff line number Diff line change
@@ -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 <Loading />;
if (isError) return <Errored />;

return (
<>
<Form.Input
label={i18n.t('activerecord.attributes.user.name')}
name="name"
value={editedProfileDetails?.name}
onChange={handleFormChange}
/>
<Form.Select
options={countryOptions}
label={i18n.t('activerecord.attributes.user.country_iso2')}
name="country_iso2"
search
value={editedProfileDetails?.country_iso2}
onChange={handleFormChange}
/>
<Form.Select
options={genderOptions}
label={i18n.t('activerecord.attributes.user.gender')}
name="gender"
value={editedProfileDetails?.gender}
onChange={handleFormChange}
/>
<Form.Field
label={i18n.t('activerecord.attributes.user.dob')}
name="dob"
control={UtcDatePicker}
showYearDropdown
dateFormatOverride="YYYY-MM-dd"
dropdownMode="select"
isoDate={editedProfileDetails?.dob}
onChange={(date) => handleFormChange(null, {
name: 'dob',
value: date,
})}
/>
</>
);
}
Original file line number Diff line number Diff line change
@@ -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 <Loading />;
if (saveError) return <Errored />;

return (
<Form onSubmit={formSubmitHandler}>
{!loggedInUserData && (
<Form.Input
label={i18n.t('page.contact_edit_profile.form.contact_email.label')}
value={contactEmail}
onChange={(e, { value }) => setContactEmail(value)}
required
/>
)}
{!loggedInUserWcaId && (
<Form.Field
control={IdWcaSearch}
name="wcaId"
label={i18n.t('page.contact_edit_profile.form.wca_id.label')}
value={customWcaId}
onChange={(e, { value }) => setCustomWcaId(value)}
disabled={customWcaId}
multiple={false}
model={SEARCH_MODELS.person}
required
/>
)}
{wcaId && (
<>
<EditProfileForm
wcaId={wcaId}
setProfileDetailsChanged={setProfileDetailsChanged}
editedProfileDetails={editedProfileDetails}
setEditedProfileDetails={setEditedProfileDetails}
/>
<Form.TextArea
label={i18n.t('page.contact_edit_profile.form.edit_reason.label')}
name="editProfileReason"
required
value={editProfileReason}
onChange={handleEditProfileReaconChange}
/>
<Form.Input
label={i18n.t('page.contact_edit_profile.form.proof_attach.label')}
type="file"
onChange={handleProofUpload}
/>
</>
)}
<Form.Field>
<ReCAPTCHA
sitekey={recaptchaPublicKey}
// onChange is a mandatory parameter for ReCAPTCHA. According to the documentation, this
// is called when user successfully completes the captcha, hence we are assuming that any
// existing errors will be cleared when onChange is called.
onChange={setCaptchaValue}
onErrored={setCaptchaError}
/>
{captchaError && (
<Message
error
content={i18n.t('page.contact_edit_profile.form.captcha.validation_error')}
/>
)}
</Form.Field>
<Form.Button
type="submit"
disabled={!profileDetailsChanged || !captchaValue}
>
{i18n.t('page.contact_edit_profile.form.submit_edit_request_button.label')}
</Form.Button>
</Form>
);
}
53 changes: 53 additions & 0 deletions app/webpacker/components/ContactEditProfilePage/index.jsx
Original file line number Diff line number Diff line change
@@ -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 <Loading />;
if (isError) return <Errored />;
if (loggedInUserData && !wcaId) {
return (
<Message error>
<I18nHTMLTranslate i18nKey="page.contact_edit_profile.no_profile_error" />
</Message>
);
}

if (contactSuccess) {
return (
<Message
success
content={i18n.t('page.contact_edit_profile.success_message')}
/>
);
}

return (
<Container text>
<Header as="h2">{i18n.t('page.contact_edit_profile.title')}</Header>
<EditProfileFormWithWcaId
wcaId={wcaId}
loggedInUserData={loggedInUserData}
setContactSuccess={setContactSuccess}
recaptchaPublicKey={recaptchaPublicKey}
/>
</Container>
);
}
2 changes: 2 additions & 0 deletions app/webpacker/lib/requests/routes.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading

0 comments on commit 47d7e5d

Please sign in to comment.