parent
d113d9ca8a
commit
37c27cc09c
|
@ -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",
|
"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": "Cancel",
|
||||||
"Cancel changes": "Cancel changes",
|
"Cancel changes": "Cancel changes",
|
||||||
|
"Change password": "Change password",
|
||||||
"Characters": "Знаков",
|
"Characters": "Знаков",
|
||||||
"Chat Title": "Chat Title",
|
"Chat Title": "Chat Title",
|
||||||
"Choose a post type": "Choose a post type",
|
"Choose a post type": "Choose a post type",
|
||||||
|
@ -131,6 +132,7 @@
|
||||||
"Email": "Mail",
|
"Email": "Mail",
|
||||||
"Enter": "Enter",
|
"Enter": "Enter",
|
||||||
"Enter URL address": "Enter URL address",
|
"Enter URL address": "Enter URL address",
|
||||||
|
"Enter a new password": "Enter a new password",
|
||||||
"Enter footnote text": "Enter footnote text",
|
"Enter footnote text": "Enter footnote text",
|
||||||
"Enter image description": "Enter image description",
|
"Enter image description": "Enter image description",
|
||||||
"Enter image title": "Enter image title",
|
"Enter image title": "Enter image title",
|
||||||
|
@ -253,6 +255,7 @@
|
||||||
"NotificationNewReplyText2": "from",
|
"NotificationNewReplyText2": "from",
|
||||||
"NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { and one more user} other { and more {restUsersCount} users}}",
|
"NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { and one more user} other { and more {restUsersCount} users}}",
|
||||||
"Notifications": "Notifications",
|
"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",
|
"Or paste a link to an image": "Or paste a link to an image",
|
||||||
"Ordered list": "Ordered list",
|
"Ordered list": "Ordered list",
|
||||||
"Our regular contributor": "Our regular contributor",
|
"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 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 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 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",
|
"Passwords are not equal": "Passwords are not equal",
|
||||||
"Paste Embed code": "Paste Embed code",
|
"Paste Embed code": "Paste Embed code",
|
||||||
"Personal": "Personal",
|
"Personal": "Personal",
|
||||||
|
@ -426,6 +430,7 @@
|
||||||
"Write to us": "Write to us",
|
"Write to us": "Write to us",
|
||||||
"Write your colleagues name or email": "Write your colleague's name or email",
|
"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 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 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 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",
|
"You've confirmed email": "You've confirmed email",
|
||||||
|
|
|
@ -68,6 +68,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": "Сменить пароль",
|
||||||
"Characters": "Знаков",
|
"Characters": "Знаков",
|
||||||
"Chat Title": "Тема дискурса",
|
"Chat Title": "Тема дискурса",
|
||||||
"Choose a post type": "Выберите тип публикации",
|
"Choose a post type": "Выберите тип публикации",
|
||||||
|
@ -137,6 +138,7 @@
|
||||||
"Email": "Почта",
|
"Email": "Почта",
|
||||||
"Enter": "Войти",
|
"Enter": "Войти",
|
||||||
"Enter URL address": "Введите адрес ссылки",
|
"Enter URL address": "Введите адрес ссылки",
|
||||||
|
"Enter a new password": "Введите новый пароль",
|
||||||
"Enter footnote text": "Введите текст сноски",
|
"Enter footnote text": "Введите текст сноски",
|
||||||
"Enter image description": "Введите описание изображения",
|
"Enter image description": "Введите описание изображения",
|
||||||
"Enter image title": "Введите название изображения",
|
"Enter image title": "Введите название изображения",
|
||||||
|
@ -265,6 +267,7 @@
|
||||||
"NotificationNewReplyText2": "от",
|
"NotificationNewReplyText2": "от",
|
||||||
"NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
|
"NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
|
||||||
"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": "Теперь можете ввести новый пароль, он должен содержать минимум 8 символов и не совпадать с предыдущим паролем",
|
||||||
"Or paste a link to an image": "Или вставьте ссылку на изображение",
|
"Or paste a link to an image": "Или вставьте ссылку на изображение",
|
||||||
"Ordered list": "Нумерованный список",
|
"Ordered list": "Нумерованный список",
|
||||||
"Our regular contributor": "Наш постоянный автор",
|
"Our regular contributor": "Наш постоянный автор",
|
||||||
|
@ -278,6 +281,7 @@
|
||||||
"Password should be at least 8 characters": "Пароль должен быть не менее 8 символов",
|
"Password should be at least 8 characters": "Пароль должен быть не менее 8 символов",
|
||||||
"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!": "Пароль обновлен!",
|
||||||
"Passwords are not equal": "Пароли не совпадают",
|
"Passwords are not equal": "Пароли не совпадают",
|
||||||
"Paste Embed code": "Вставьте embed код",
|
"Paste Embed code": "Вставьте embed код",
|
||||||
"Personal": "Личные",
|
"Personal": "Личные",
|
||||||
|
@ -447,6 +451,7 @@
|
||||||
"Write to us": "Напишите нам",
|
"Write to us": "Напишите нам",
|
||||||
"Write your colleagues name or email": "Напишите имя или e-mail коллеги",
|
"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 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 was 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": "Вы подтвердили почту",
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
background: #fff;
|
background: #fff;
|
||||||
min-height: 550px;
|
min-height: 550px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
|
@ -106,6 +107,10 @@
|
||||||
|
|
||||||
margin-top: 1.6rem;
|
margin-top: 1.6rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #9fa1a7;
|
color: #9fa1a7;
|
||||||
|
@ -125,7 +130,7 @@
|
||||||
.submitButton {
|
.submitButton {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-top: 32px;
|
margin-top: 36px;
|
||||||
padding: 1.6rem !important;
|
padding: 1.6rem !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -183,10 +188,6 @@
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
margin-top: 0.3em;
|
margin-top: 0.3em;
|
||||||
|
|
||||||
&.registerPassword {
|
|
||||||
margin-bottom: -32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Red/500 */
|
/* Red/500 */
|
||||||
color: #d00820;
|
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 {
|
.title {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
|
|
103
src/components/Nav/AuthModal/ChangePasswordForm.tsx
Normal file
103
src/components/Nav/AuthModal/ChangePasswordForm.tsx
Normal file
|
@ -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<Record<keyof FormFields, string | JSX.Element>>
|
||||||
|
|
||||||
|
export const ChangePasswordForm = () => {
|
||||||
|
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
|
||||||
|
const { t } = useLocalize()
|
||||||
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
|
const [newPassword, setNewPassword] = createSignal<string>()
|
||||||
|
const [passwordError, setPasswordError] = createSignal<string>()
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Show when={!isSuccess()}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
class={clsx(styles.authForm, styles.authFormForgetPassword)}
|
||||||
|
ref={(el) => (authFormRef.current = el)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h4>{t('Enter a new password')}</h4>
|
||||||
|
<div class={styles.authSubtitle}>
|
||||||
|
{t(
|
||||||
|
'Now you can enter a new password, it must contain at least 8 characters and not be the same as the previous password',
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PasswordField
|
||||||
|
errorMessage={(err) => setPasswordError(err)}
|
||||||
|
onInput={(value) => handlePasswordInput(value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
||||||
|
{isSubmitting() ? '...' : t('Change password')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class={styles.authControl}>
|
||||||
|
<span
|
||||||
|
class={styles.authLink}
|
||||||
|
onClick={() =>
|
||||||
|
changeSearchParams({
|
||||||
|
mode: 'login',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Show>
|
||||||
|
<Show when={isSuccess()}>
|
||||||
|
<div class={styles.title}>{t('Password updated!')}</div>
|
||||||
|
<div class={styles.text}>{t('You can now login using your new password')}</div>
|
||||||
|
<div>
|
||||||
|
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
|
||||||
|
{t('Back to main page')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -11,9 +11,9 @@ import { useRouter } from '../../../stores/router'
|
||||||
import { hideModal } from '../../../stores/ui'
|
import { hideModal } from '../../../stores/ui'
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
import { validateEmail } from '../../../utils/validateEmail'
|
import { validateEmail } from '../../../utils/validateEmail'
|
||||||
import { Icon } from '../../_shared/Icon'
|
|
||||||
|
|
||||||
import { AuthModalHeader } from './AuthModalHeader'
|
import { AuthModalHeader } from './AuthModalHeader'
|
||||||
|
import { PasswordField } from './PasswordField'
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
import { SocialProviders } from './SocialProviders'
|
import { SocialProviders } from './SocialProviders'
|
||||||
|
|
||||||
|
@ -35,7 +35,6 @@ export const LoginForm = () => {
|
||||||
// TODO: better solution for interactive error messages
|
// TODO: better solution for interactive error messages
|
||||||
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false)
|
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false)
|
||||||
const [isLinkSent, setIsLinkSent] = createSignal(false)
|
const [isLinkSent, setIsLinkSent] = createSignal(false)
|
||||||
const [showPassword, setShowPassword] = createSignal(false)
|
|
||||||
|
|
||||||
const authFormRef: { current: HTMLFormElement } = { current: null }
|
const authFormRef: { current: HTMLFormElement } = { current: null }
|
||||||
|
|
||||||
|
@ -166,31 +165,7 @@ export const LoginForm = () => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<PasswordField onInput={(value) => handlePasswordInput(value)} />
|
||||||
class={clsx('pretty-form__item', {
|
|
||||||
'pretty-form__item--error': validationErrors().password,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
autocomplete="password"
|
|
||||||
type={showPassword() ? 'text' : 'password'}
|
|
||||||
placeholder={t('Password')}
|
|
||||||
onInput={(event) => handlePasswordInput(event.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<label for="password">{t('Password')}</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.passwordToggle}
|
|
||||||
onClick={() => setShowPassword(!showPassword())}
|
|
||||||
>
|
|
||||||
<Icon class={styles.passwordToggleIcon} name={showPassword() ? 'eye-off' : 'eye'} />
|
|
||||||
</button>
|
|
||||||
<Show when={validationErrors().password}>
|
|
||||||
<div class={styles.validationError}>{validationErrors().password}</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
||||||
|
@ -208,6 +183,16 @@ export const LoginForm = () => {
|
||||||
>
|
>
|
||||||
{t('Forgot password?')}
|
{t('Forgot password?')}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
class="link"
|
||||||
|
onClick={() =>
|
||||||
|
changeSearchParams({
|
||||||
|
mode: 'change-password',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('Change password')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
src/components/Nav/AuthModal/PasswordField/PasswordField.tsx
Normal file
88
src/components/Nav/AuthModal/PasswordField/PasswordField.tsx
Normal file
|
@ -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<string>()
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div class={clsx(styles.PassportField, props.class)}>
|
||||||
|
<div
|
||||||
|
class={clsx('pretty-form__item', {
|
||||||
|
'pretty-form__item--error': error(),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
type={showPassword() ? 'text' : 'password'}
|
||||||
|
placeholder={t('Password')}
|
||||||
|
onInput={(event) => handleInputChange(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<label for="password">{t('Password')}</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={styles.passwordToggle}
|
||||||
|
onClick={() => setShowPassword(!showPassword())}
|
||||||
|
>
|
||||||
|
<Icon class={styles.passwordToggleIcon} name={showPassword() ? 'eye-off' : 'eye'} />
|
||||||
|
</button>
|
||||||
|
<Show when={error()}>
|
||||||
|
<div class={clsx(styles.registerPassword, styles.validationError)}>{error()}</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
1
src/components/Nav/AuthModal/PasswordField/index.ts
Normal file
1
src/components/Nav/AuthModal/PasswordField/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { PasswordField } from './PasswordField'
|
|
@ -11,9 +11,9 @@ import { useRouter } from '../../../stores/router'
|
||||||
import { hideModal } from '../../../stores/ui'
|
import { hideModal } from '../../../stores/ui'
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
import { validateEmail } from '../../../utils/validateEmail'
|
import { validateEmail } from '../../../utils/validateEmail'
|
||||||
import { Icon } from '../../_shared/Icon'
|
|
||||||
|
|
||||||
import { AuthModalHeader } from './AuthModalHeader'
|
import { AuthModalHeader } from './AuthModalHeader'
|
||||||
|
import { PasswordField } from './PasswordField'
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
import { SocialProviders } from './SocialProviders'
|
import { SocialProviders } from './SocialProviders'
|
||||||
|
|
||||||
|
@ -40,9 +40,9 @@ export const RegisterForm = () => {
|
||||||
const [fullName, setFullName] = createSignal('')
|
const [fullName, setFullName] = createSignal('')
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
const [showPassword, setShowPassword] = createSignal(false)
|
|
||||||
const [isSuccess, setIsSuccess] = createSignal(false)
|
const [isSuccess, setIsSuccess] = createSignal(false)
|
||||||
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
|
const [passwordError, setPasswordError] = createSignal<string>()
|
||||||
|
|
||||||
const authFormRef: { current: HTMLFormElement } = { current: null }
|
const authFormRef: { current: HTMLFormElement } = { current: null }
|
||||||
|
|
||||||
|
@ -52,37 +52,15 @@ export const RegisterForm = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidPassword(passwordToCheck) {
|
const handleNameInput = (newName: string) => {
|
||||||
const minLength = 8
|
setFullName(newName)
|
||||||
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 handleSubmit = async (event: Event) => {
|
const handleSubmit = async (event: Event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
const passwordError = isValidPassword(password())
|
if (passwordError()) {
|
||||||
if (passwordError) {
|
setValidationErrors((errors) => ({ ...errors, password: passwordError() }))
|
||||||
setValidationErrors((errors) => ({ ...errors, password: passwordError }))
|
|
||||||
} else {
|
} else {
|
||||||
setValidationErrors(({ password: _notNeeded, ...rest }) => rest)
|
setValidationErrors(({ password: _notNeeded, ...rest }) => rest)
|
||||||
}
|
}
|
||||||
|
@ -198,48 +176,17 @@ export const RegisterForm = () => {
|
||||||
<Show when={emailChecks()[email()]}>
|
<Show when={emailChecks()[email()]}>
|
||||||
<div class={styles.validationError}>
|
<div class={styles.validationError}>
|
||||||
{t("This email is already taken. If it's you")},{' '}
|
{t("This email is already taken. If it's you")},{' '}
|
||||||
<a
|
<span class="link" onClick={() => changeSearchParams({ mode: 'login' })}>
|
||||||
href="#"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
changeSearchParams({
|
|
||||||
mode: 'login',
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('enter')}
|
{t('enter')}
|
||||||
</a>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<PasswordField
|
||||||
class={clsx('pretty-form__item', {
|
errorMessage={(err) => setPasswordError(err)}
|
||||||
'pretty-form__item--error': validationErrors().password,
|
onInput={(value) => setPassword(value)}
|
||||||
})}
|
/>
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
type={showPassword() ? 'text' : 'password'}
|
|
||||||
placeholder={t('Password')}
|
|
||||||
onInput={(event) => handlePasswordInput(event.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<label for="password">{t('Password')}</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.passwordToggle}
|
|
||||||
onClick={() => setShowPassword(!showPassword())}
|
|
||||||
>
|
|
||||||
<Icon class={styles.passwordToggleIcon} name={showPassword() ? 'eye-off' : 'eye'} />
|
|
||||||
</button>
|
|
||||||
<Show when={validationErrors().password}>
|
|
||||||
<div class={clsx(styles.registerPassword, styles.validationError)}>
|
|
||||||
{validationErrors().password}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { useRouter } from '../../../stores/router'
|
||||||
import { hideModal } from '../../../stores/ui'
|
import { hideModal } from '../../../stores/ui'
|
||||||
import { isMobile } from '../../../utils/media-query'
|
import { isMobile } from '../../../utils/media-query'
|
||||||
|
|
||||||
|
import { ChangePasswordForm } from './ChangePasswordForm'
|
||||||
import { EmailConfirm } from './EmailConfirm'
|
import { EmailConfirm } from './EmailConfirm'
|
||||||
import { ForgotPasswordForm } from './ForgotPasswordForm'
|
import { ForgotPasswordForm } from './ForgotPasswordForm'
|
||||||
import { LoginForm } from './LoginForm'
|
import { LoginForm } from './LoginForm'
|
||||||
|
@ -21,10 +22,11 @@ const AUTH_MODAL_MODES: Record<AuthModalMode, Component> = {
|
||||||
register: RegisterForm,
|
register: RegisterForm,
|
||||||
'forgot-password': ForgotPasswordForm,
|
'forgot-password': ForgotPasswordForm,
|
||||||
'confirm-email': EmailConfirm,
|
'confirm-email': EmailConfirm,
|
||||||
|
'change-password': ChangePasswordForm,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthModal = () => {
|
export const AuthModal = () => {
|
||||||
let rootRef: HTMLDivElement
|
const rootRef: { current: HTMLDivElement } = { current: null }
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { searchParams } = useRouter<AuthModalSearchParams>()
|
const { searchParams } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
|
@ -36,17 +38,17 @@ export const AuthModal = () => {
|
||||||
|
|
||||||
createEffect((oldMode) => {
|
createEffect((oldMode) => {
|
||||||
if (oldMode !== mode() && !isMobile()) {
|
if (oldMode !== mode() && !isMobile()) {
|
||||||
rootRef?.querySelector('input')?.focus()
|
rootRef.current?.querySelector('input')?.focus()
|
||||||
}
|
}
|
||||||
}, null)
|
}, null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={rootRef}
|
ref={(el) => (rootRef.current = el)}
|
||||||
class={clsx(styles.view, {
|
class={clsx(styles.view, {
|
||||||
row: !source,
|
row: !source,
|
||||||
|
[styles.signUp]: mode() === 'register' || mode() === 'confirm-email',
|
||||||
})}
|
})}
|
||||||
classList={{ [styles.signUp]: mode() === 'register' || mode() === 'confirm-email' }}
|
|
||||||
>
|
>
|
||||||
<Show when={!source}>
|
<Show when={!source}>
|
||||||
<div class={clsx('col-md-12 d-none d-md-flex', styles.authImage)}>
|
<div class={clsx('col-md-12 d-none d-md-flex', styles.authImage)}>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'forgot-password'
|
export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'forgot-password' | 'change-password'
|
||||||
export type AuthModalSource =
|
export type AuthModalSource =
|
||||||
| 'discussions'
|
| 'discussions'
|
||||||
| 'vote'
|
| 'vote'
|
||||||
|
|
Loading…
Reference in New Issue
Block a user