From efe52f55e260b5b96eb8ac5ac3acca33463fa255 Mon Sep 17 00:00:00 2001 From: Attiya Ishaque Date: Tue, 23 Apr 2024 19:08:50 +0500 Subject: [PATCH] feat: design for forgot password and reset password --- .env.development | 2 + src/forms/fields/email-field/index.jsx | 8 +- src/forms/fields/email-field/messages.jsx | 11 -- src/forms/fields/password-field/index.jsx | 15 ++- src/forms/fields/password-field/messages.jsx | 7 +- src/forms/fields/text-field/index.jsx | 1 + src/forms/login-popup/index.jsx | 1 + src/forms/registration-popup/index.jsx | 2 + src/forms/registration-popup/messages.jsx | 10 ++ .../ResetPasswordHeader.jsx | 25 ++++ .../ForgotPasswordEmailSentConfirmation.jsx | 45 +++++++ .../forgot-password/ForgotPasswordPage.jsx | 95 +++++++++++++++ src/forms/reset-password-popup/index.scss | 13 ++ src/forms/reset-password-popup/messages.js | 94 ++++++++++++++ .../reset-password/ResetPasswordFailure.jsx | 29 +++++ .../reset-password/ResetPasswordPage.jsx | 115 ++++++++++++++++++ src/index.jsx | 0 src/index.scss | 2 +- 18 files changed, 448 insertions(+), 27 deletions(-) delete mode 100644 src/forms/fields/email-field/messages.jsx create mode 100644 src/forms/reset-password-popup/ResetPasswordHeader.jsx create mode 100644 src/forms/reset-password-popup/forgot-password/ForgotPasswordEmailSentConfirmation.jsx create mode 100644 src/forms/reset-password-popup/forgot-password/ForgotPasswordPage.jsx create mode 100644 src/forms/reset-password-popup/index.scss create mode 100644 src/forms/reset-password-popup/messages.js create mode 100644 src/forms/reset-password-popup/reset-password/ResetPasswordFailure.jsx create mode 100644 src/forms/reset-password-popup/reset-password/ResetPasswordPage.jsx delete mode 100644 src/index.jsx diff --git a/.env.development b/.env.development index ed5f193d..c9d3e1e2 100644 --- a/.env.development +++ b/.env.development @@ -9,3 +9,5 @@ REFRESH_ACCESS_TOKEN_ENDPOINT=http://localhost:18000/login_refresh SITE_NAME=edX AUTHN_ALGOLIA_APP_ID='' AUTHN_ALGOLIA_SEARCH_API_KEY='' +INFO_EMAIL=info@example.com +LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url' diff --git a/src/forms/fields/email-field/index.jsx b/src/forms/fields/email-field/index.jsx index f7f81837..be3f3cdf 100644 --- a/src/forms/fields/email-field/index.jsx +++ b/src/forms/fields/email-field/index.jsx @@ -1,11 +1,8 @@ import React from 'react'; -import { useIntl } from '@edx/frontend-platform/i18n'; import { Form } from '@openedx/paragon'; import PropTypes from 'prop-types'; -import messages from './messages'; - /** * Email field component. It accepts following handler(s) * - handleChange for setting value change and @@ -14,17 +11,17 @@ import messages from './messages'; * - setting value on change */ const EmailField = (props) => { - const { formatMessage } = useIntl(); const { name, value, handleChange } = props; return ( ); @@ -34,6 +31,7 @@ EmailField.propTypes = { name: PropTypes.string.isRequired, value: PropTypes.string.isRequired, handleChange: PropTypes.func.isRequired, + floatingLabel: PropTypes.func.isRequired, }; export default EmailField; diff --git a/src/forms/fields/email-field/messages.jsx b/src/forms/fields/email-field/messages.jsx deleted file mode 100644 index ece880a4..00000000 --- a/src/forms/fields/email-field/messages.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - registrationFormEmailFieldLabel: { - id: 'registration.form.email.label', - defaultMessage: 'Enter your email', - description: 'Label for email input field', - }, -}); - -export default messages; diff --git a/src/forms/fields/password-field/index.jsx b/src/forms/fields/password-field/index.jsx index 18cc4a00..6b2340b6 100644 --- a/src/forms/fields/password-field/index.jsx +++ b/src/forms/fields/password-field/index.jsx @@ -29,6 +29,7 @@ const PasswordField = (props) => { value, handleChange, handleFocus, + floatingLabel, } = props; const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true); @@ -76,23 +77,24 @@ const PasswordField = (props) => { {props.value.length >= 8 ? : } - {formatMessage(messages.eightCharcters)} + {formatMessage(messages.eightCharacters)} ); return ( - + {errorMessage !== '' && ( @@ -111,11 +113,16 @@ const PasswordField = (props) => { }; PasswordField.propTypes = { - errorMessage: PropTypes.string.isRequired, name: PropTypes.string.isRequired, value: PropTypes.string.isRequired, handleChange: PropTypes.func.isRequired, handleFocus: PropTypes.func.isRequired, + errorMessage: PropTypes.string, + floatingLabel: PropTypes.string.isRequired, +}; + +PasswordField.defaultProps = { + errorMessage: '', }; export default PasswordField; diff --git a/src/forms/fields/password-field/messages.jsx b/src/forms/fields/password-field/messages.jsx index 68af1f42..449b41e4 100644 --- a/src/forms/fields/password-field/messages.jsx +++ b/src/forms/fields/password-field/messages.jsx @@ -1,11 +1,6 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ - registrationFormPasswordFieldLabel: { - id: 'registration.form.password.label', - defaultMessage: 'Password', - description: 'Label for password input field', - }, showPasswordAlt: { id: 'show.password', defaultMessage: 'Show password', @@ -26,7 +21,7 @@ const messages = defineMessages({ defaultMessage: '1 number', description: 'password requirement to have 1 number', }, - eightCharcters: { + eightCharacters: { id: 'eight.characters', defaultMessage: '8 characters', description: 'password requirement to have a minimum of 8 characters', diff --git a/src/forms/fields/text-field/index.jsx b/src/forms/fields/text-field/index.jsx index 62256324..0ba32d2e 100644 --- a/src/forms/fields/text-field/index.jsx +++ b/src/forms/fields/text-field/index.jsx @@ -31,6 +31,7 @@ const TextField = (props) => { { errorMessage={formErrors.password} handleChange={handleOnChange} handleFocus={handleOnFocus} + floatingLabel={formatMessage(messages.loginFormPasswordFieldLabel)} /> {/* TODO: this destination will be replaced with actual links */} diff --git a/src/forms/registration-popup/index.jsx b/src/forms/registration-popup/index.jsx index f3bfea63..d4bef6ce 100644 --- a/src/forms/registration-popup/index.jsx +++ b/src/forms/registration-popup/index.jsx @@ -113,6 +113,7 @@ const RegistrationForm = () => { name="email" value={formFields.email} handleChange={handleOnChange} + floatingLabel={formatMessage(messages.registrationFormEmailFieldLabel)} /> { errorMessage="" handleChange={handleOnChange} handleFocus={() => { }} + floatingLabel={formatMessage(messages.registrationFormPasswordFieldLabel)} />
diff --git a/src/forms/registration-popup/messages.jsx b/src/forms/registration-popup/messages.jsx index 9c590841..2965fb35 100644 --- a/src/forms/registration-popup/messages.jsx +++ b/src/forms/registration-popup/messages.jsx @@ -56,6 +56,16 @@ const messages = defineMessages({ defaultMessage: 'Fill out the fields below to create your account', description: 'Heading that appear on the second step', }, + registrationFormPasswordFieldLabel: { + id: 'registration.form.password.label', + defaultMessage: 'Password', + description: 'Label for password input field', + }, + registrationFormEmailFieldLabel: { + id: 'registration.form.email.label', + defaultMessage: 'Enter your email', + description: 'Label for email input field', + }, }); export default messages; diff --git a/src/forms/reset-password-popup/ResetPasswordHeader.jsx b/src/forms/reset-password-popup/ResetPasswordHeader.jsx new file mode 100644 index 00000000..8d5e38db --- /dev/null +++ b/src/forms/reset-password-popup/ResetPasswordHeader.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import { useIntl } from '@edx/frontend-platform/i18n'; + +import messages from './messages'; + +/** + * Header component for the reset password form. + * Renders the heading for the reset password form along with a separator. + * @returns {JSX.Element} The rendered header component. + */ +const ResetPasswordHeader = () => { + const { formatMessage } = useIntl(); + + return ( + <> +

+ {formatMessage(messages.resetPasswordFormHeading)} +

+
+ + ); +}; + +export default ResetPasswordHeader; diff --git a/src/forms/reset-password-popup/forgot-password/ForgotPasswordEmailSentConfirmation.jsx b/src/forms/reset-password-popup/forgot-password/ForgotPasswordEmailSentConfirmation.jsx new file mode 100644 index 00000000..e7387faa --- /dev/null +++ b/src/forms/reset-password-popup/forgot-password/ForgotPasswordEmailSentConfirmation.jsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import { getConfig } from '@edx/frontend-platform'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; +import { Alert } from '@openedx/paragon'; + +import messages from '../messages'; + +/** + * Component that renders the display confirmation message after sending the password reset email. + * @returns {JSX.Element} The rendered confirmation message component. + */ +const ForgotPasswordEmailSentConfirmation = () => { + const { formatMessage } = useIntl(); + + return ( +
+ + {formatMessage(messages.emailSentMessage)} + + + email@email.com, + supportLink: ( + + {getConfig().INFO_EMAIL} + + ), + }} + /> + +
+ + ); +}; + +export default ForgotPasswordEmailSentConfirmation; diff --git a/src/forms/reset-password-popup/forgot-password/ForgotPasswordPage.jsx b/src/forms/reset-password-popup/forgot-password/ForgotPasswordPage.jsx new file mode 100644 index 00000000..5bd5483f --- /dev/null +++ b/src/forms/reset-password-popup/forgot-password/ForgotPasswordPage.jsx @@ -0,0 +1,95 @@ +import React, { useState } from 'react'; + +import { getConfig } from '@edx/frontend-platform'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + Button, Container, Form, +} from '@openedx/paragon'; + +import ForgotPasswordEmailSentConfirmation from './ForgotPasswordEmailSentConfirmation'; +import { InlineLink } from '../../../common-ui'; +import EmailField from '../../fields/email-field'; +import messages from '../messages'; +import ResetPasswordHeader from '../ResetPasswordHeader'; +import '../index.scss'; + +/** + * ForgotPasswordForm component for handling user password reset. + * This component provides a form for users to reset their password by entering their email. + */ +const ForgotPasswordPage = () => { + const { formatMessage } = useIntl(); + + const [formFields, setFormFields] = useState({ email: '' }); + const [isSuccess, setIsSuccess] = useState(false); + + const handleOnChange = (event) => { + const { name } = event.target; + const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value; + setFormFields(prevState => ({ ...prevState, [name]: value })); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + setIsSuccess(true); + }; + + return ( + + + {!isSuccess && ( +
+ + +
+ + +
+ + )} + {isSuccess && ( + + )} +
+ +
+
+ ); +}; + +export default ForgotPasswordPage; diff --git a/src/forms/reset-password-popup/index.scss b/src/forms/reset-password-popup/index.scss new file mode 100644 index 00000000..0d011082 --- /dev/null +++ b/src/forms/reset-password-popup/index.scss @@ -0,0 +1,13 @@ +@import "~@edx/brand/paragon/variables"; + +.separator { + border: 0; + border-top: 0.075rem solid $light-500 +} +.back-to-login__button { + border-color: $primary-500; + border: 2px solid; +} +#set-reset-password-form .pgn__form-control-floating-label-text:after { + content: none; +} diff --git a/src/forms/reset-password-popup/messages.js b/src/forms/reset-password-popup/messages.js new file mode 100644 index 00000000..82f4d709 --- /dev/null +++ b/src/forms/reset-password-popup/messages.js @@ -0,0 +1,94 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + resetPasswordFormHeading: { + id: 'reset.password.form.heading', + defaultMessage: 'Reset Password', + description: 'Reset password form main heading', + }, + resetPasswordFormSubmitButton: { + id: 'reset.password.form.submit.button', + defaultMessage: 'Submit', + description: 'Text for submit button on reset password form', + }, + resetPasswordFormNeedHelpText: { + id: 'reset.password.form.need.help.text', + defaultMessage: 'Need help signing in?', + description: 'reset Password help text', + }, + resetPasswordFormHelpCenterLink: { + id: 'reset.password.form.help.center.link', + defaultMessage: 'Help center', + description: 'Text for help center link', + }, + resetPasswordFormAdditionalHelpText: { + id: 'reset.password.form.additional.help.text', + defaultMessage: 'For additional help, contact edX support at', + description: 'Label for link that leads learners to the email page', + }, + resetPasswordBackToLoginButton: { + id: 'reset.password.back.to.login.button', + defaultMessage: 'Back to login', + description: 'Text for back to login button on reset password form', + }, + newPasswordLabel: { + id: 'new.password.label', + defaultMessage: 'New password', + description: 'New password field label for the reset password page.', + }, + confirmPasswordLabel: { + id: 'confirm.password.label', + defaultMessage: 'Confirm password', + description: 'Confirm password field label for the reset password page.', + }, + resetPasswordButton: { + id: 'reset.password.button', + defaultMessage: 'Reset password', + description: 'Button text for reset password popup.', + }, + enterConfirmPasswordMessage: { + id: 'enter.confirm.password.message', + defaultMessage: 'Enter and confirm the new password', + description: 'Message for entering and confirming the new password', + }, + // email sent messages + emailSentMessage: { + id: 'email.sent.message', + defaultMessage: 'Email has been sent', + description: 'Notification message indicating that an email has been sent', + }, + // validation errors + passwordRequiredMessage: { + id: 'password.required.message', + defaultMessage: 'Password is a required field', + description: 'Error message for empty password', + }, + passwordValidationMessage: { + id: 'password.validation.message', + defaultMessage: 'Password criteria has not been met', + description: 'Error message for invalid password', + }, + passwordDoNotMatch: { + id: 'passwords.do.not.match', + defaultMessage: 'Passwords do not match', + description: 'Password format error.', + }, + confirmYourPassword: { + id: 'confirm.your.password', + defaultMessage: 'Confirm your password', + description: 'Field validation message when confirm password is empty', + }, + // alert banner strings + resetPasswordFailureHeading: { + id: 'reset.password.failure.heading', + defaultMessage: 'We couldn\'t reset your password.', + description: 'Heading for reset password request failure', + }, + forgotPasswordFormEmailFieldLabel: { + id: 'forgot.Password.form.email.label', + defaultMessage: 'Email', + description: 'Label for email input field', + }, +}); + +export default messages; diff --git a/src/forms/reset-password-popup/reset-password/ResetPasswordFailure.jsx b/src/forms/reset-password-popup/reset-password/ResetPasswordFailure.jsx new file mode 100644 index 00000000..429d09f3 --- /dev/null +++ b/src/forms/reset-password-popup/reset-password/ResetPasswordFailure.jsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Alert } from '@openedx/paragon'; + +import messages from '../messages'; + +/** + * Component to display reset password failure message. + * @returns {JSX.Element | null} The rendered failure message component or null if no error. + */ +const ResetPasswordFailure = () => { + const { formatMessage } = useIntl(); + + const errorMessage = ' '; + const heading = formatMessage(messages.resetPasswordFailureHeading); + + if (errorMessage) { + return ( + + {heading} + + ); + } + + return null; +}; + +export default ResetPasswordFailure; diff --git a/src/forms/reset-password-popup/reset-password/ResetPasswordPage.jsx b/src/forms/reset-password-popup/reset-password/ResetPasswordPage.jsx new file mode 100644 index 00000000..a29bf54c --- /dev/null +++ b/src/forms/reset-password-popup/reset-password/ResetPasswordPage.jsx @@ -0,0 +1,115 @@ +import React, { useState } from 'react'; + +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + Button, Container, Form, +} from '@openedx/paragon'; +import PropTypes from 'prop-types'; + +import ResetPasswordFailure from './ResetPasswordFailure'; +import PasswordField from '../../fields/password-field'; +import messages from '../messages'; +import ResetPasswordHeader from '../ResetPasswordHeader'; + +export const LETTER_REGEX = /[a-zA-Z]/; +export const NUMBER_REGEX = /\d/; + +/** + * ResetPasswordForm component for completing user password reset. + * This component provides a form for users to reset their password. + * @param {string} newPassword - The new password entered by the user. + * @param {string} confirmNewPassword - The confirmation of the new password entered by the user. + * @returns {string} A message indicating the success or failure of the password reset process. + */ +const ResetPasswordPage = (props) => { + const { formatMessage } = useIntl(); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [formErrors, setFormErrors] = useState({}); + + const validateInput = (name, value) => { + switch (name) { + case 'newPassword': + if (!value) { + formErrors.newPassword = formatMessage(messages.passwordRequiredMessage); + } else if (!LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) { + formErrors.newPassword = formatMessage(messages.passwordValidationMessage); + } else { + // will add backend validation message + } + break; + case 'confirmPassword': + if (!value) { + formErrors.confirmPassword = formatMessage(messages.confirmYourPassword); + } else if (value !== newPassword) { + formErrors.confirmPassword = formatMessage(messages.passwordDoNotMatch); + } else { + formErrors.confirmPassword = ''; + } + break; + default: + break; + } + setFormErrors({ ...formErrors }); + return !Object.values(formErrors).some(x => (x !== '')); + }; + const handleOnBlur = (event) => { + const { name, value } = event.target; + validateInput(name, value); + }; + const handleOnFocus = (e) => { + setFormErrors({ ...formErrors, [e.target.name]: '' }); + }; + + return ( + + + + +
{formatMessage(messages.enterConfirmPasswordMessage)}
+
+ setNewPassword(e.target.value)} + handleFocus={handleOnFocus} + handleBlur={handleOnBlur} + errorMessage={formErrors.newPassword} + floatingLabel={formatMessage(messages.newPasswordLabel)} + /> + setConfirmPassword(e.target.value)} + handleFocus={handleOnFocus} + handleBlur={handleOnBlur} + errorMessage={formErrors.confirmPassword} + floatingLabel={formatMessage(messages.confirmPasswordLabel)} + /> + + +
+ ); +}; + +ResetPasswordPage.defaultProps = { + errorMsg: null, +}; + +ResetPasswordPage.propTypes = { + errorMsg: PropTypes.string, +}; + +export default ResetPasswordPage; diff --git a/src/index.jsx b/src/index.jsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/index.scss b/src/index.scss index 1d5f370e..ed9e89d4 100644 --- a/src/index.scss +++ b/src/index.scss @@ -5,7 +5,7 @@ padding: 1.75rem 1rem; } -.authn__popup-container > .pgn__form-control-floating-label-text:after { +.authn__popup-container .pgn__form-control-floating-label-text:after { content:"*"; color: $danger-500; }