Feature/change password modal (#344)

* Change Password Modal
This commit is contained in:
Ilya Y 2023-12-21 13:02:28 +03:00 committed by GitHub
parent d113d9ca8a
commit 37c27cc09c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 285 additions and 122 deletions

View File

@ -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",

View File

@ -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": "Вы подтвердили почту",

View File

@ -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;

View 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>
</>
)
}

View File

@ -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>

View File

@ -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);
}
}
}
}

View 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>
)
}

View File

@ -0,0 +1 @@
export { PasswordField } from './PasswordField'

View File

@ -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">

View File

@ -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)}>

View File

@ -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'