From 37c27cc09c60969cb3893085b6f859eaf8991fa7 Mon Sep 17 00:00:00 2001 From: Ilya Y <75578537+ilya-bkv@users.noreply.github.com> Date: Thu, 21 Dec 2023 13:02:28 +0300 Subject: [PATCH] Feature/change password modal (#344) * Change Password Modal --- public/locales/en/translation.json | 5 + public/locales/ru/translation.json | 5 + .../Nav/AuthModal/AuthModal.module.scss | 31 +----- .../Nav/AuthModal/ChangePasswordForm.tsx | 103 ++++++++++++++++++ src/components/Nav/AuthModal/LoginForm.tsx | 39 ++----- .../PasswordField/PasswordField.module.scss | 46 ++++++++ .../AuthModal/PasswordField/PasswordField.tsx | 88 +++++++++++++++ .../Nav/AuthModal/PasswordField/index.ts | 1 + src/components/Nav/AuthModal/RegisterForm.tsx | 77 ++----------- src/components/Nav/AuthModal/index.tsx | 10 +- src/components/Nav/AuthModal/types.ts | 2 +- 11 files changed, 285 insertions(+), 122 deletions(-) create mode 100644 src/components/Nav/AuthModal/ChangePasswordForm.tsx create mode 100644 src/components/Nav/AuthModal/PasswordField/PasswordField.module.scss create mode 100644 src/components/Nav/AuthModal/PasswordField/PasswordField.tsx create mode 100644 src/components/Nav/AuthModal/PasswordField/index.ts diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 447108f5..c708713f 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -65,6 +65,7 @@ "Can write and edit text directly, and accept or reject suggestions from others": "Can write and edit text directly, and accept or reject suggestions from others", "Cancel": "Cancel", "Cancel changes": "Cancel changes", + "Change password": "Change password", "Characters": "Знаков", "Chat Title": "Chat Title", "Choose a post type": "Choose a post type", @@ -131,6 +132,7 @@ "Email": "Mail", "Enter": "Enter", "Enter URL address": "Enter URL address", + "Enter a new password": "Enter a new password", "Enter footnote text": "Enter footnote text", "Enter image description": "Enter image description", "Enter image title": "Enter image title", @@ -253,6 +255,7 @@ "NotificationNewReplyText2": "from", "NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { and one more user} other { and more {restUsersCount} users}}", "Notifications": "Notifications", + "Now you can enter a new password, it must contain at least 8 characters and not be the same as the previous password": "Now you can enter a new password, it must contain at least 8 characters and not be the same as the previous password", "Or paste a link to an image": "Or paste a link to an image", "Ordered list": "Ordered list", "Our regular contributor": "Our regular contributor", @@ -266,6 +269,7 @@ "Password should be at least 8 characters": "Password should be at least 8 characters", "Password should contain at least one number": "Password should contain at least one number", "Password should contain at least one special character: !@#$%^&*": "Password should contain at least one special character: !@#$%^&*", + "Password updated!": "Password updated!", "Passwords are not equal": "Passwords are not equal", "Paste Embed code": "Paste Embed code", "Personal": "Personal", @@ -426,6 +430,7 @@ "Write to us": "Write to us", "Write your colleagues name or email": "Write your colleague's name or email", "You can download multiple tracks at once in .mp3, .wav or .flac formats": "You can download multiple tracks at once in .mp3, .wav or .flac formats", + "You can now login using your new password": "Теперь вы можете входить с помощью нового пароля", "You were successfully authorized": "You were successfully authorized", "You ll be able to participate in discussions, rate others' comments and learn about new responses": "You ll be able to participate in discussions, rate others' comments and learn about new responses", "You've confirmed email": "You've confirmed email", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 5910a6e4..7e3d05c3 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -68,6 +68,7 @@ "Can write and edit text directly, and accept or reject suggestions from others": "Может писать и редактировать текст напрямую, а также принимать или отклонять предложения других", "Cancel": "Отмена", "Cancel changes": "Отменить изменения", + "Change password": "Сменить пароль", "Characters": "Знаков", "Chat Title": "Тема дискурса", "Choose a post type": "Выберите тип публикации", @@ -137,6 +138,7 @@ "Email": "Почта", "Enter": "Войти", "Enter URL address": "Введите адрес ссылки", + "Enter a new password": "Введите новый пароль", "Enter footnote text": "Введите текст сноски", "Enter image description": "Введите описание изображения", "Enter image title": "Введите название изображения", @@ -265,6 +267,7 @@ "NotificationNewReplyText2": "от", "NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}", "Notifications": "Уведомления", + "Now you can enter a new password, it must contain at least 8 characters and not be the same as the previous password": "Теперь можете ввести новый пароль, он должен содержать минимум 8 символов и не совпадать с предыдущим паролем", "Or paste a link to an image": "Или вставьте ссылку на изображение", "Ordered list": "Нумерованный список", "Our regular contributor": "Наш постоянный автор", @@ -278,6 +281,7 @@ "Password should be at least 8 characters": "Пароль должен быть не менее 8 символов", "Password should contain at least one number": "Пароль должен содержать хотя бы одну цифру", "Password should contain at least one special character: !@#$%^&*": "Пароль должен содержать хотя бы один спецсимвол: !@#$%^&*", + "Password updated!": "Пароль обновлен!", "Passwords are not equal": "Пароли не совпадают", "Paste Embed code": "Вставьте embed код", "Personal": "Личные", @@ -447,6 +451,7 @@ "Write to us": "Напишите нам", "Write your colleagues name or email": "Напишите имя или e-mail коллеги", "You can download multiple tracks at once in .mp3, .wav or .flac formats": "Можно загрузить сразу несколько треков в форматах .mp3, .wav или .flac", + "You can now login using your new password": "Теперь вы можете входить с помощью нового пароля", "You was successfully authorized": "Вы были успешно авторизованы", "You ll be able to participate in discussions, rate others' comments and learn about new responses": "Вы сможете участвовать в обсуждениях, оценивать комментарии других и узнавать о новых ответах", "You've confirmed email": "Вы подтвердили почту", diff --git a/src/components/Nav/AuthModal/AuthModal.module.scss b/src/components/Nav/AuthModal/AuthModal.module.scss index e596c41f..d9b7daaa 100644 --- a/src/components/Nav/AuthModal/AuthModal.module.scss +++ b/src/components/Nav/AuthModal/AuthModal.module.scss @@ -2,6 +2,7 @@ background: #fff; min-height: 550px; position: relative; + justify-content: center; @include media-breakpoint-up(md) { min-height: 600px; @@ -106,6 +107,10 @@ margin-top: 1.6rem; text-align: center; + display: flex; + flex-direction: row; + justify-content: center; + gap: 1rem; a { color: #9fa1a7; @@ -125,7 +130,7 @@ .submitButton { display: block; font-weight: 700; - margin-top: 32px; + margin-top: 36px; padding: 1.6rem !important; width: 100%; } @@ -183,10 +188,6 @@ line-height: 16px; margin-top: 0.3em; - &.registerPassword { - margin-bottom: -32px; - } - /* Red/500 */ color: #d00820; @@ -201,26 +202,6 @@ } } -.passwordToggle { - position: absolute; - right: 10px; - top: 50%; - transform: translateY(-50%); - background: none; - border: none; - cursor: pointer; -} - -.passwordToggleIcon { - height: 1.6em; - display: inline-block; - margin-right: 0.2em; - max-width: 1.4em; - max-height: 1.4em; - transition: filter 0.2s; - vertical-align: middle; -} - .title { font-size: 26px; line-height: 32px; diff --git a/src/components/Nav/AuthModal/ChangePasswordForm.tsx b/src/components/Nav/AuthModal/ChangePasswordForm.tsx new file mode 100644 index 00000000..f519e5d9 --- /dev/null +++ b/src/components/Nav/AuthModal/ChangePasswordForm.tsx @@ -0,0 +1,103 @@ +import type { AuthModalSearchParams } from './types' + +import { clsx } from 'clsx' +import { createSignal, JSX, Show } from 'solid-js' + +import { useLocalize } from '../../../context/localize' +import { useRouter } from '../../../stores/router' +import { hideModal } from '../../../stores/ui' + +import { PasswordField } from './PasswordField' + +import styles from './AuthModal.module.scss' + +type FormFields = { + password: string +} + +type ValidationErrors = Partial> + +export const ChangePasswordForm = () => { + const { changeSearchParams } = useRouter() + const { t } = useLocalize() + const [isSubmitting, setIsSubmitting] = createSignal(false) + const [validationErrors, setValidationErrors] = createSignal({}) + const [newPassword, setNewPassword] = createSignal() + const [passwordError, setPasswordError] = createSignal() + const [isSuccess, setIsSuccess] = createSignal(false) + + const authFormRef: { current: HTMLFormElement } = { current: null } + + const handleSubmit = async (event: Event) => { + event.preventDefault() + setIsSubmitting(true) + // Fake change password logic + console.log('!!! sent new password:', newPassword) + setTimeout(() => { + setIsSubmitting(false) + setIsSuccess(true) + }, 1000) + } + + const handlePasswordInput = (value) => { + setNewPassword(value) + if (passwordError()) { + setValidationErrors((errors) => ({ ...errors, password: passwordError() })) + } else { + setValidationErrors(({ password: _notNeeded, ...rest }) => rest) + } + } + + return ( + <> + +
(authFormRef.current = el)} + > +
+

{t('Enter a new password')}

+
+ {t( + 'Now you can enter a new password, it must contain at least 8 characters and not be the same as the previous password', + )} +
+ + setPasswordError(err)} + onInput={(value) => handlePasswordInput(value)} + /> + +
+ +
+
+ + changeSearchParams({ + mode: 'login', + }) + } + > + {t('Cancel')} + +
+
+
+
+ +
{t('Password updated!')}
+
{t('You can now login using your new password')}
+
+ +
+
+ + ) +} diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index 0a7e11fc..4cceb081 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -11,9 +11,9 @@ import { useRouter } from '../../../stores/router' import { hideModal } from '../../../stores/ui' import { ApiError } from '../../../utils/apiClient' import { validateEmail } from '../../../utils/validateEmail' -import { Icon } from '../../_shared/Icon' import { AuthModalHeader } from './AuthModalHeader' +import { PasswordField } from './PasswordField' import { email, setEmail } from './sharedLogic' import { SocialProviders } from './SocialProviders' @@ -35,7 +35,6 @@ export const LoginForm = () => { // TODO: better solution for interactive error messages const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false) const [isLinkSent, setIsLinkSent] = createSignal(false) - const [showPassword, setShowPassword] = createSignal(false) const authFormRef: { current: HTMLFormElement } = { current: null } @@ -166,31 +165,7 @@ export const LoginForm = () => { -
- handlePasswordInput(event.currentTarget.value)} - /> - - - -
{validationErrors().password}
-
-
+ handlePasswordInput(value)} />
diff --git a/src/components/Nav/AuthModal/PasswordField/PasswordField.module.scss b/src/components/Nav/AuthModal/PasswordField/PasswordField.module.scss new file mode 100644 index 00000000..7faae657 --- /dev/null +++ b/src/components/Nav/AuthModal/PasswordField/PasswordField.module.scss @@ -0,0 +1,46 @@ +.PassportField { + .passwordToggle { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + } + + .passwordToggleIcon { + height: 1.6em; + display: inline-block; + margin-right: 0.2em; + max-width: 1.4em; + max-height: 1.4em; + transition: filter 0.2s; + vertical-align: middle; + } + + .validationError { + position: absolute; + top: 100%; + font-size: 12px; + line-height: 16px; + margin-top: 0.3em; + + &.registerPassword { + margin-bottom: -32px; + } + + /* Red/500 */ + color: #d00820; + + a { + color: #d00820; + border-color: #d00820; + + &:hover { + color: var(--default-color-invert); + border-color: var(--background-color-invert); + } + } + } +} diff --git a/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx b/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx new file mode 100644 index 00000000..6dab7ef8 --- /dev/null +++ b/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx @@ -0,0 +1,88 @@ +import { clsx } from 'clsx' +import { createEffect, createSignal, JSX, on, Show } from 'solid-js' + +import { useLocalize } from '../../../../context/localize' +import { resetSortedArticles } from '../../../../stores/zine/articles' +import { Icon } from '../../../_shared/Icon' + +import styles from './PasswordField.module.scss' + +type Props = { + class?: string + errorMessage?: (error: string) => void + onInput: (value: string) => void +} + +export const PasswordField = (props: Props) => { + const { t } = useLocalize() + const [showPassword, setShowPassword] = createSignal(false) + const [error, setError] = createSignal() + + const validatePassword = (passwordToCheck) => { + const minLength = 8 + const hasNumber = /\d/ + const hasSpecial = /[!#$%&*@^]/ + + if (passwordToCheck.length < minLength) { + return t('Password should be at least 8 characters') + } + if (!hasNumber.test(passwordToCheck)) { + return t('Password should contain at least one number') + } + if (!hasSpecial.test(passwordToCheck)) { + return t('Password should contain at least one special character: !@#$%^&*') + } + + return null + } + + const handleInputChange = (value) => { + props.onInput(value) + const errorValue = validatePassword(value) + if (errorValue) { + setError(errorValue) + } else { + setError() + } + } + + createEffect( + on( + () => error(), + () => { + props.errorMessage(error()) + }, + { defer: true }, + ), + ) + + return ( +
+
+ handleInputChange(event.currentTarget.value)} + /> + + + +
{error()}
+
+
+
+ ) +} diff --git a/src/components/Nav/AuthModal/PasswordField/index.ts b/src/components/Nav/AuthModal/PasswordField/index.ts new file mode 100644 index 00000000..d743619c --- /dev/null +++ b/src/components/Nav/AuthModal/PasswordField/index.ts @@ -0,0 +1 @@ +export { PasswordField } from './PasswordField' diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index 7f642d3e..3b72303f 100644 --- a/src/components/Nav/AuthModal/RegisterForm.tsx +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -11,9 +11,9 @@ import { useRouter } from '../../../stores/router' import { hideModal } from '../../../stores/ui' import { ApiError } from '../../../utils/apiClient' import { validateEmail } from '../../../utils/validateEmail' -import { Icon } from '../../_shared/Icon' import { AuthModalHeader } from './AuthModalHeader' +import { PasswordField } from './PasswordField' import { email, setEmail } from './sharedLogic' import { SocialProviders } from './SocialProviders' @@ -40,9 +40,9 @@ export const RegisterForm = () => { const [fullName, setFullName] = createSignal('') const [password, setPassword] = createSignal('') const [isSubmitting, setIsSubmitting] = createSignal(false) - const [showPassword, setShowPassword] = createSignal(false) const [isSuccess, setIsSuccess] = createSignal(false) const [validationErrors, setValidationErrors] = createSignal({}) + const [passwordError, setPasswordError] = createSignal() const authFormRef: { current: HTMLFormElement } = { current: null } @@ -52,37 +52,15 @@ export const RegisterForm = () => { } } - function isValidPassword(passwordToCheck) { - const minLength = 8 - const hasNumber = /\d/ - const hasSpecial = /[!#$%&*@^]/ - - if (passwordToCheck.length < minLength) { - return t('Password should be at least 8 characters') - } - if (!hasNumber.test(passwordToCheck)) { - return t('Password should contain at least one number') - } - if (!hasSpecial.test(passwordToCheck)) { - return t('Password should contain at least one special character: !@#$%^&*') - } - return null - } - - const handlePasswordInput = (newPassword: string) => { - setPassword(newPassword) - } - - const handleNameInput = (newPasswordCopy: string) => { - setFullName(newPasswordCopy) + const handleNameInput = (newName: string) => { + setFullName(newName) } const handleSubmit = async (event: Event) => { event.preventDefault() - const passwordError = isValidPassword(password()) - if (passwordError) { - setValidationErrors((errors) => ({ ...errors, password: passwordError })) + if (passwordError()) { + setValidationErrors((errors) => ({ ...errors, password: passwordError() })) } else { setValidationErrors(({ password: _notNeeded, ...rest }) => rest) } @@ -198,48 +176,17 @@ export const RegisterForm = () => { -
- handlePasswordInput(event.currentTarget.value)} - /> - - - -
- {validationErrors().password} -
-
-
+ setPasswordError(err)} + onInput={(value) => setPassword(value)} + />