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:
parent
20ca55e1b2
commit
cbb254a907
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user