Add Subscribe email validation (#97)

* refactor(AuthModal): move email validator to utils

* refactor(utils/validators): change email validation to regexp

* feat(Subscribe): add email and serve response validation

* refactor(Button): change secondary variant styles

* feat(Subscribe): styling controls

* fix(Subscribe): call email accessor

* fix(Subscribe): button non full width in ios
This commit is contained in:
Vyacheslav Ananev 2023-05-13 23:59:09 +07:00 committed by GitHub
parent 20ca55e1b2
commit cbb254a907
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 124 additions and 51 deletions

View File

@ -1,47 +1,77 @@
.subscribeForm { @mixin input-placeholder-overflow($direction: 'down') {
@if $direction == 'down' {
@media (max-width: 1410px) {
@content;
}
} @else if $direction == 'up' {
@media (min-width: 1411px) {
@content;
}
} @else {
@error "Unknown direction #{$direction}.";
}
}
.form {
display: flex;
flex-direction: column;
@include input-placeholder-overflow(down) {
margin-bottom: 2.4rem;
}
}
.controls {
display: flex; display: flex;
width: 100%; width: 100%;
@include media-breakpoint-down(md) { @include input-placeholder-overflow(down) {
margin-bottom: 2.4rem;
}
@include media-breakpoint-between(md, xl) {
flex-direction: column; flex-direction: column;
} }
input { .input {
@include font-size(2rem); @include font-size(2rem);
background: none; background: none;
border: none;
border-bottom: 1px solid;
color: #fff; color: #fff;
font-family: inherit; font-family: inherit;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
padding: 0.2em 0.5em 0.3em 0; padding: 0.2em 0.5em 0.2em 0.5em;
width: 100%; width: 100%;
outline: none;
border: 1px solid #fff;
border-radius: 0;
height: 4rem;
@include input-placeholder-overflow(up) {
border-right: none;
}
@include input-placeholder-overflow(down) {
border-bottom: none;
}
&::placeholder { &::placeholder {
color: #fff; color: #858585;
} }
} }
a {
@include font-size(1.5rem);
align-items: center;
background: #fff;
border: none;
color: #000;
display: flex;
padding: 0 0.5em;
}
.button { .button {
border-radius: 0; border-radius: 0;
margin: 0; flex-shrink: 0;
@include input-placeholder-overflow(down) {
width: 100%;
}
} }
} }
.error {
position: relative;
top: 4px;
font-size: 12px;
line-height: 16px;
color: #d00820;
}

View File

@ -1,14 +1,43 @@
import { createSignal, Show } from 'solid-js' import { createSignal, JSX, Show } from 'solid-js'
import styles from './Subscribe.module.scss'
import { clsx } from 'clsx'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { isValidEmail } from '../../utils/validators'
import { Button } from '../_shared/Button'
import styles from './Subscribe.module.scss'
export default () => { export default () => {
const { t } = useLocalize() const { t } = useLocalize()
let emailElement: HTMLInputElement | undefined
const [title, setTitle] = createSignal('') const [title, setTitle] = createSignal('')
const subscribe = async () => { const [email, setEmail] = createSignal('')
const [emailError, setEmailError] = createSignal<string>(null)
const validate = (): boolean => {
if (!email()) {
setEmailError(t('Please enter email'))
return false
}
if (!isValidEmail(email())) {
setEmailError(t('Please check your email address'))
return false
}
setEmailError(null)
return true
}
const handleInput: JSX.ChangeEventHandlerUnion<HTMLInputElement, Event> = (event) => {
setEmailError(null)
setEmail(event.target.value)
}
const handleSubmit = async (event: SubmitEvent) => {
event.preventDefault()
if (!validate()) return
setTitle(t('...subscribing')) setTitle(t('...subscribing'))
const requestOptions = { const requestOptions = {
@ -16,26 +45,42 @@ export default () => {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({ email: email() })
email: emailElement?.value
})
} }
const r = await fetch('/api/newsletter', requestOptions) const response = await fetch('/api/newsletter', requestOptions)
setTitle(r.ok ? t('You are subscribed') : '')
if (response.ok) {
setTitle(t('You are subscribed'))
} else {
if (response.status === 400) {
setEmailError(t('Please check your email address'))
} else {
setEmailError(t('Something went wrong, please try again'))
}
setTitle('')
}
} }
return ( return (
<div class={styles.subscribeForm}> <form class={styles.form} onSubmit={handleSubmit} novalidate>
<Show when={!title()} fallback={title()}> <Show when={!title()} fallback={title()}>
<input type="email" name="email" ref={emailElement} placeholder={t('Fill email')} /> <div class={styles.controls}>
<button <input
class={clsx(styles.button, 'button--light')} type="email"
onClick={() => emailElement?.value && subscribe()} name="email"
> value={email()}
{t('Subscribe')} onInput={handleInput}
</button> class={styles.input}
</Show> placeholder={t('Fill email')}
/>
<Button class={styles.button} type="submit" variant="secondary" value={t('Subscribe')} />
</div> </div>
<Show when={emailError()}>
<div class={styles.error}>{emailError()}</div>
</Show>
</Show>
</form>
) )
} }

View File

@ -4,10 +4,10 @@ import { createSignal, JSX, Show } from 'solid-js'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import { email, setEmail } from './sharedLogic' import { email, setEmail } from './sharedLogic'
import type { AuthModalSearchParams } from './types' import type { AuthModalSearchParams } from './types'
import { isValidEmail } from './validators'
import { ApiError } from '../../../utils/apiClient' import { ApiError } from '../../../utils/apiClient'
import { signSendLink } from '../../../stores/auth' import { signSendLink } from '../../../stores/auth'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { isValidEmail } from '../../../utils/validators'
type FormFields = { type FormFields = {
email: string email: string

View File

@ -3,13 +3,13 @@ import { clsx } from 'clsx'
import { SocialProviders } from './SocialProviders' import { SocialProviders } from './SocialProviders'
import { ApiError } from '../../../utils/apiClient' import { ApiError } from '../../../utils/apiClient'
import { createSignal, Show } from 'solid-js' import { createSignal, Show } from 'solid-js'
import { isValidEmail } from './validators'
import { email, setEmail } from './sharedLogic' import { email, setEmail } from './sharedLogic'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import type { AuthModalSearchParams } from './types' import type { AuthModalSearchParams } from './types'
import { hideModal } from '../../../stores/ui' import { hideModal } from '../../../stores/ui'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { signSendLink } from '../../../stores/auth' import { signSendLink } from '../../../stores/auth'
import { isValidEmail } from '../../../utils/validators'
import { useSnackbar } from '../../../context/snackbar' import { useSnackbar } from '../../../context/snackbar'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'

View File

@ -3,7 +3,6 @@ import type { JSX } from 'solid-js'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { SocialProviders } from './SocialProviders' import { SocialProviders } from './SocialProviders'
import { isValidEmail } from './validators'
import { ApiError } from '../../../utils/apiClient' import { ApiError } from '../../../utils/apiClient'
import { email, setEmail } from './sharedLogic' import { email, setEmail } from './sharedLogic'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
@ -12,6 +11,7 @@ import { hideModal } from '../../../stores/ui'
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks' import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
import { register } from '../../../stores/auth' import { register } from '../../../stores/auth'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { isValidEmail } from '../../../utils/validators'
type FormFields = { type FormFields = {
name: string name: string

View File

@ -20,15 +20,13 @@
} }
&.secondary { &.secondary {
border: 1px solid #f7f7f7;
background: #f7f7f7; background: #f7f7f7;
color: #141414; color: #141414;
&:hover { &:hover {
background: #e8e8e8; background: #000;
} color: #fff;
&:active {
background: #ccc;
} }
} }

View File

@ -3,5 +3,5 @@ export const isValidEmail = (email: string) => {
return false return false
} }
return email.includes('@') && email.includes('.') && email.length > 5 return /^[\w%+.-]+@[\d.a-z-]+\.[a-z]{2,}$/i.test(email)
} }