Merge branch 'dev' into hotfix/posting-author

This commit is contained in:
Tony 2024-03-07 14:09:00 +03:00 committed by GitHub
commit bf9f0d9c7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 203 additions and 169 deletions

View File

@ -18,6 +18,7 @@
"Add signature": "Add signature",
"Add subtitle": "Add subtitle",
"Add url": "Add url",
"try": "попробуйте",
"Add": "Add",
"Address on Discours": "Address on Discours",
"Album name": "Название aльбома",
@ -144,7 +145,6 @@
"Enter your new password": "Enter your new password",
"Enter": "Enter",
"Error": "Error",
"Please give us your email address": "Please provide us your email address to get the password reset link",
"Experience": "Experience",
"FAQ": "Tips and suggestions",
"Favorite topics": "Favorite topics",
@ -254,7 +254,6 @@
"Nothing here yet": "There's nothing here yet",
"Nothing is here": "There is nothing here",
"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",
@ -323,7 +322,7 @@
"Self-publishing exists thanks to the help of wonderful people from all over the world. Thank you!": "Samizdat exists thanks to the help of wonderful people from all over the world. Thank you!",
"Send link again": "Send link again",
"Send": "Send",
"Set the new password": "Set the new password",
"Forgot password?": "Forgot password?",
"Settings": "Settings",
"Share publication": "Share publication",
"Share": "Share",
@ -526,5 +525,7 @@
"view": "view",
"viewsWithCount": "{count} {count, plural, one {view} other {views}}",
"yesterday": "yesterday",
"Failed to delete comment": "Failed to delete comment"
"Failed to delete comment": "Failed to delete comment",
"It's OK. Just enter your email to receive a link to change your password": "It's OK. Just enter your email to receive a link to change your password",
"Restore password": "Restore password"
}

View File

@ -151,7 +151,6 @@
"Enter": "Войти",
"This content is not published yet": "Содержимое ещё не опубликовано",
"Error": "Ошибка",
"Please give us your email address": "Пожалуйста, укажите свою почту, чтобы получить ссылку для сброса пароля",
"Experience": "Личный опыт",
"FAQ": "Советы и предложения",
"Favorite topics": "Избранные темы",
@ -267,7 +266,6 @@
"Nothing here yet": "Здесь пока ничего нет",
"Nothing is here": "Здесь ничего нет",
"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": "Наш постоянный автор",
@ -288,7 +286,7 @@
"Pin": "Закрепить",
"Platform Guide": "Гид по дискурсу",
"Please check your email address": "Пожалуйста, проверьте введенный адрес почты",
"Please check your inbox! We have sent a password reset link.": "Пожалуйста, проверьте ваш адрес почты, мы отправили ссылку для сброса пароля",
"Please check your inbox! We have sent a password reset link.": "Пожалуйста, проверьте свою почту, мы отправили вам письмо со ссылкой для сброса пароля",
"Please confirm your email to finish": "Подтвердите почту и действие совершится",
"Please enter a name to sign your comments and publication": "Пожалуйста, введите имя, которое будет отображаться на сайте",
"Please enter email": "Пожалуйста, введите почту",
@ -329,7 +327,7 @@
"Reports": "Репортажи",
"Required": "Поле обязательно для заполнения",
"Resend code": "Выслать подтверждение",
"Set the new password": "Задать новый пароль",
"Forgot password?": "Забыли пароль?",
"Rules of the journal Discours": "Правила журнала Дискурс",
"Save draft": "Сохранить черновик",
"Save settings": "Сохранить настройки",
@ -404,6 +402,7 @@
"This email is": "Этот email",
"This email is not verified": "Этот email не подтвержден",
"This email is verified": "Этот email подтвержден",
"try": "попробуйте",
"This email is registered": "Этот email уже зарегистрирован",
"This functionality is currently not available, we would like to work on this issue. Use the download link.": "В данный момент этот функционал не доступен, бы работаем над этой проблемой. Воспользуйтесь загрузкой по ссылке.",
"This month": "За месяц",
@ -553,5 +552,7 @@
"view": "просмотр",
"viewsWithCount": "{count} {count, plural, one {просмотр} few {просмотрa} other {просмотров}}",
"yesterday": "вчера",
"Failed to delete comment": "Не удалось удалить комментарий"
"Failed to delete comment": "Не удалось удалить комментарий",
"It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля",
"Restore password": "Восстановить пароль"
}

View File

@ -2,8 +2,6 @@
@include font-size(1.2rem);
color: var(--secondary-color);
// align-self: center;
display: flex;
align-items: flex-start;
justify-content: flex-start;

View File

@ -1,5 +1,5 @@
.view {
background: #fff;
background: var(--background-color);
min-height: 550px;
position: relative;
justify-content: center;
@ -154,17 +154,6 @@
margin-bottom: 1em;
}
.authInfo {
font-weight: 400;
font-size: smaller;
margin-top: -2em;
position: absolute;
.warn {
color: #a00;
}
}
.authForm {
display: flex;
flex: 1;
@ -221,3 +210,7 @@
line-height: 24px;
margin-bottom: 52px;
}
.submitError {
margin: -1rem 0 -2rem;
}

View File

@ -33,7 +33,7 @@ export const ChangePasswordForm = () => {
event.preventDefault()
setIsSubmitting(true)
if (newPassword()) {
await changePassword(newPassword(), searchParams()?.token)
changePassword(newPassword(), searchParams()?.token)
setTimeout(() => {
setIsSubmitting(false)
setIsSuccess(true)
@ -60,11 +60,6 @@ export const ChangePasswordForm = () => {
>
<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>
<Show when={validationErrors()}>
<div>{validationErrors().password}</div>
</Show>

View File

@ -1,13 +0,0 @@
.title {
font-size: 26px;
line-height: 32px;
font-weight: 700;
color: #141414;
margin-bottom: 16px;
}
.text {
font-size: 15px;
line-height: 24px;
margin-bottom: 52px;
}

View File

@ -17,19 +17,20 @@ export const EmailConfirm = () => {
const [emailConfirmed, setEmailConfirmed] = createSignal(false)
createEffect(() => {
const e = session()?.user?.email
const v = session()?.user?.email_verified
if (e) {
setEmail(e.toLowerCase())
if (v) setEmailConfirmed(v)
const email = session()?.user?.email
const isVerified = session()?.user?.email_verified
if (email) {
setEmail(email.toLowerCase())
if (isVerified) setEmailConfirmed(isVerified)
if (authError()) {
changeSearchParams({}, true)
}
}
})
createEffect(() => {
if (authError()) console.debug('[AuthModal.EmailConfirm] auth error:', authError())
if (authError()) {
console.debug('[AuthModal.EmailConfirm] auth error:', authError())
}
})
return (

View File

@ -1,7 +1,7 @@
import type { AuthModalSearchParams } from './types'
import { clsx } from 'clsx'
import { Show, createSignal } from 'solid-js'
import { JSX, Show, createEffect, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
@ -27,12 +27,11 @@ type ValidationErrors = Partial<Record<keyof FormFields, string>>
export const LoginForm = () => {
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
const { t } = useLocalize()
const [submitError, setSubmitError] = createSignal('')
const [submitError, setSubmitError] = createSignal<string | JSX.Element>()
const [isSubmitting, setIsSubmitting] = createSignal(false)
const [password, setPassword] = createSignal('')
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
// TODO: better solution for interactive error messages
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false)
const [isLinkSent, setIsLinkSent] = createSignal(false)
const authFormRef: { current: HTMLFormElement } = { current: null }
const { showSnackbar } = useSnackbar()
@ -52,43 +51,43 @@ export const LoginForm = () => {
event.preventDefault()
setIsLinkSent(true)
setIsEmailNotConfirmed(false)
setSubmitError('')
changeSearchParams({ mode: 'send-reset-link' })
// NOTE: temporary solution, needs logic in authorizer
/* FIXME:
const { authorizer } = useSession()
const result = await authorizer().verifyEmail({ token })
if (!result) setSubmitError('cant sign send link')
*/
setSubmitError()
changeSearchParams({ mode: 'send-confirm-email' })
}
const preSendValidate = async (value: string, type: 'email' | 'password'): Promise<boolean> => {
if (type === 'email') {
if (value === '' || !validateEmail(value)) {
setValidationErrors((prev) => ({
...prev,
email: t('Invalid email'),
}))
return false
}
} else if (type === 'password') {
if (value === '') {
setValidationErrors((prev) => ({
...prev,
password: t('Please enter password'),
}))
return false
}
}
return true
}
const handleSubmit = async (event: Event) => {
event.preventDefault()
await preSendValidate(email(), 'email')
await preSendValidate(password(), 'password')
setIsLinkSent(false)
setIsEmailNotConfirmed(false)
setSubmitError('')
const newValidationErrors: ValidationErrors = {}
const validateAndSetError = (field, message) => {
if (!field()) {
newValidationErrors[field.name] = t(message)
}
}
validateAndSetError(email, 'Please enter email')
validateAndSetError(() => validateEmail(email()), 'Invalid email')
validateAndSetError(password, 'Please enter password')
if (Object.keys(newValidationErrors).length > 0) {
setValidationErrors(newValidationErrors)
setSubmitError()
if (Object.keys(validationErrors()).length > 0) {
authFormRef.current
.querySelector<HTMLInputElement>(`input[name="${Object.keys(newValidationErrors)[0]}"]`)
.querySelector<HTMLInputElement>(`input[name="${Object.keys(validationErrors())[0]}"]`)
?.focus()
return
}
@ -96,14 +95,27 @@ export const LoginForm = () => {
try {
const { errors } = await signIn({ email: email(), password: password() })
console.error('[signIn errors]', errors)
if (errors?.length > 0) {
if (errors.some((error) => error.message.includes('bad user credentials'))) {
setValidationErrors((prev) => ({
...prev,
password: t('Something went wrong, check email and password'),
}))
} else if (errors.some((error) => error.message.includes('user not found'))) {
setSubmitError('Пользователь не найден')
} else if (errors.some((error) => error.message.includes('email not verified'))) {
setSubmitError(
<div class={styles.info}>
{t('This email is not verified')}
{'. '}
<span class={'link'} onClick={handleSendLinkAgainClick}>
{t('Send link again')}
</span>
</div>,
)
} else {
setSubmitError(t('Error'))
setSubmitError(t('Error', errors[0].message))
}
return
}
@ -121,19 +133,6 @@ export const LoginForm = () => {
<form onSubmit={handleSubmit} class={styles.authForm} ref={(el) => (authFormRef.current = el)}>
<div>
<AuthModalHeader modalType="login" />
<Show when={submitError()}>
<div class={styles.authInfo}>
<div class={styles.warn}>{submitError()}</div>
<Show when={isEmailNotConfirmed()}>
<span class={'link'} onClick={handleSendLinkAgainClick}>
{t('Send link again')}
</span>
</Show>
</div>
</Show>
<Show when={isLinkSent()}>
<div class={styles.authInfo}>{t('Link sent, check your email')}</div>
</Show>
<div
class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().email,
@ -154,11 +153,14 @@ export const LoginForm = () => {
</Show>
</div>
<PasswordField variant={'login'} onInput={(value) => handlePasswordInput(value)} />
<Show when={validationErrors().password}>
<div class={styles.validationError} style={{ position: 'static', 'font-size': '1.4rem' }}>
{validationErrors().password}
</div>
<PasswordField
variant={'login'}
setError={validationErrors().password}
onInput={(value) => handlePasswordInput(value)}
/>
<Show when={submitError()}>
<div class={clsx('form-message--error', styles.submitError)}>{submitError()}</div>
</Show>
<div>
@ -175,7 +177,7 @@ export const LoginForm = () => {
})
}
>
{t('Set the new password')}
{t('Forgot password?')}
</span>
</div>
</div>

View File

@ -31,11 +31,11 @@
}
/* Red/500 */
color: #d00820;
color: orange;
a {
color: #d00820;
border-color: #d00820;
color: orange;
border-color: orange;
&:hover {
color: var(--default-color-invert);

View File

@ -11,21 +11,23 @@ type Props = {
disabled?: boolean
placeholder?: string
errorMessage?: (error: string) => void
setError?: string
onInput: (value: string) => void
onBlur?: (value: string) => void
variant?: 'login' | 'registration'
disableAutocomplete?: boolean
}
const minLength = 8
const hasNumber = /\d/
const hasSpecial = /[!#$%&*@^]/
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')
}
@ -35,11 +37,17 @@ export const PasswordField = (props: Props) => {
if (!hasSpecial.test(passwordToCheck)) {
return t('Password should contain at least one special character: !@#$%^&*')
}
return null
}
const handleInputChange = (value) => {
const handleInputBlur = (value: string) => {
if (props.variant === 'login') {
return props.onBlur(value)
}
if (value.length < 1) {
return
}
props.onInput(value)
const errorValue = validatePassword(value)
if (errorValue) {
@ -58,14 +66,13 @@ export const PasswordField = (props: Props) => {
{ defer: true },
),
)
createEffect(() => {
setError(props.setError)
})
return (
<div class={clsx(styles.PassportField, props.class)}>
<div
class={clsx('pretty-form__item', {
'pretty-form__item--error': error() && props.variant !== 'login',
})}
>
<div class="pretty-form__item">
<input
id="password"
name="password"
@ -73,7 +80,7 @@ export const PasswordField = (props: Props) => {
autocomplete={props.disableAutocomplete ? 'one-time-code' : 'current-password'}
type={showPassword() ? 'text' : 'password'}
placeholder={props.placeholder || t('Password')}
onInput={(event) => handleInputChange(event.currentTarget.value)}
onBlur={(event) => handleInputBlur(event.currentTarget.value)}
/>
<label for="password">{t('Password')}</label>
<button
@ -83,8 +90,14 @@ export const PasswordField = (props: Props) => {
>
<Icon class={styles.passwordToggleIcon} name={showPassword() ? 'eye-off' : 'eye'} />
</button>
<Show when={error() && props.variant !== 'login'}>
<div class={clsx(styles.registerPassword, styles.validationError)}>{error()}</div>
<Show when={error()}>
<div
class={clsx(styles.registerPassword, styles.validationError, {
'form-message--error': props.setError,
})}
>
{error()}
</div>
</Show>
</div>
</div>

View File

@ -28,10 +28,6 @@ type FormFields = {
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
const handleEmailInput = (newEmail: string) => {
setEmail(newEmail.toLowerCase())
}
export const RegisterForm = () => {
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
const { t } = useLocalize()
@ -52,6 +48,7 @@ export const RegisterForm = () => {
}
const handleSubmit = async (event: Event) => {
console.log('!!! handleSubmit:', handleSubmit)
event.preventDefault()
if (passwordError()) {
setValidationErrors((errors) => ({ ...errors, password: passwordError() }))
@ -137,7 +134,8 @@ export const RegisterForm = () => {
setValidationErrors((prev) => ({
email: (
<>
{t('This email is verified')}. {t('You can')}
{t('This email is registered')}. {t('try')}
{', '}
<span class="link" onClick={() => changeSearchParams({ mode: 'login' })}>
{t('enter')}
</span>
@ -173,17 +171,18 @@ export const RegisterForm = () => {
}
}
const handleEmailInput = (newEmail: string) => {
setEmailStatus('')
setValidationErrors({})
setEmail(newEmail.toLowerCase())
}
return (
<>
<Show when={!isSuccess()}>
<form onSubmit={handleSubmit} class={styles.authForm} ref={(el) => (authFormRef.current = el)}>
<div>
<AuthModalHeader modalType="register" />
<Show when={submitError()}>
<div class={styles.authInfo}>
<div class={styles.warn}>{submitError()}</div>
</div>
</Show>
<div
class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().fullName,
@ -218,9 +217,11 @@ export const RegisterForm = () => {
onBlur={handleEmailBlur}
/>
<label for="email">{t('Email')}</label>
<Show when={validationErrors().email || emailStatus()}>
<div class={clsx(styles.validationError, { info: Boolean(emailStatus()) })}>
{validationErrors().email}
</div>
</Show>
</div>
<PasswordField
@ -260,6 +261,7 @@ export const RegisterForm = () => {
</form>
</Show>
<Show when={isSuccess()}>
<div style={{ 'justify-content': 'center' }}>
<div class={styles.title}>{t('Almost done! Check your email.')}</div>
<div class={styles.text}>{t("We've sent you a message with a link to enter our website.")}</div>
<div>
@ -267,6 +269,7 @@ export const RegisterForm = () => {
{t('Back to main page')}
</button>
</div>
</div>
</Show>
</>
)

View File

@ -0,0 +1,24 @@
import { clsx } from 'clsx'
import { useLocalize } from '../../../context/localize'
import { hideModal } from '../../../stores/ui'
import styles from './AuthModal.module.scss'
export const SendEmailConfirm = () => {
const { t } = useLocalize()
return (
<div
style={{
'align-items': 'center',
'justify-content': 'center',
}}
>
<div class={styles.text}>{t('Link sent, check your email')}</div>
<div>
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
{t('Go to main page')}
</button>
</div>
</div>
)
}

View File

@ -85,8 +85,12 @@ export const SendResetLinkForm = () => {
ref={(el) => (authFormRef.current = el)}
>
<div>
<h4>{t('Set the new password')}</h4>
<div class={styles.authSubtitle}>{t(message()) || t('Please give us your email address')}</div>
<h4>{t('Forgot password?')}</h4>
<Show when={!message()}>
<div class={styles.authSubtitle}>
{t("It's OK. Just enter your email to receive a link to change your password")}
</div>
</Show>
<div
class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().email,
@ -110,7 +114,7 @@ export const SendResetLinkForm = () => {
class={'link'}
onClick={() =>
changeSearchParams({
mode: 'login',
mode: 'register',
})
}
>
@ -122,14 +126,15 @@ export const SendResetLinkForm = () => {
<div class={styles.validationError}>{validationErrors().email}</div>
</Show>
</div>
<Show when={!message()} fallback={<div class={styles.authSubtitle}>{t(message())}</div>}>
<>
<div style={{ 'margin-top': '5rem' }}>
<button
class={clsx('button', styles.submitButton)}
disabled={isSubmitting() || Boolean(message())}
type="submit"
>
{isSubmitting() ? '...' : t('Send')}
{isSubmitting() ? '...' : t('Restore password')}
</button>
</div>
<div class={styles.authControl}>
@ -144,6 +149,8 @@ export const SendResetLinkForm = () => {
{t('I know the password')}
</span>
</div>
</>
</Show>
</div>
</form>
)

View File

@ -1,8 +1,8 @@
import { For } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../../context/localize'
import { useSession } from '../../../../context/session'
import { Icon } from '../../../_shared/Icon'
import styles from './SocialProviders.module.scss'
@ -18,7 +18,7 @@ export const SocialProviders = () => {
<div class={styles.social}>
<For each={PROVIDERS}>
{(provider) => (
<button class={styles[provider]} onClick={(_e) => oauth(provider)}>
<button type="button" class={styles[provider]} onClick={(_e) => oauth(provider)}>
<Icon name={provider} />
</button>
)}

View File

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

View File

@ -16,12 +16,14 @@ import { RegisterForm } from './RegisterForm'
import { SendResetLinkForm } from './SendResetLinkForm'
import styles from './AuthModal.module.scss'
import { SendEmailConfirm } from './SendEmailConfirm'
const AUTH_MODAL_MODES: Record<AuthModalMode, Component> = {
login: LoginForm,
register: RegisterForm,
'send-reset-link': SendResetLinkForm,
'confirm-email': EmailConfirm,
'send-confirm-email': SendEmailConfirm,
'change-password': ChangePasswordForm,
}

View File

@ -1,4 +1,10 @@
export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'send-reset-link' | 'change-password'
export type AuthModalMode =
| 'login'
| 'register'
| 'confirm-email'
| 'send-confirm-email'
| 'send-reset-link'
| 'change-password'
export type AuthModalSource =
| 'discussions'
| 'vote'

View File

@ -464,7 +464,7 @@ form {
}
.form-message--error {
color: #d00820;
color: var(--danger-color) !important;
}
select {