Create form abstraction

This commit is contained in:
Lachlan Campbell 2020-04-27 16:16:47 -04:00
parent 0c4f9d31c6
commit dc62a6d564
5 changed files with 154 additions and 71 deletions

View file

@ -1,24 +1,9 @@
import { useState, useEffect } from 'react'
import { Card, Label, Input, Button, Checkbox, Textarea } from 'theme-ui'
import fetch from 'isomorphic-unfetch'
import { Card, Label, Input, Checkbox, Textarea } from 'theme-ui'
import useForm from '../../lib/use-form'
import Submit from '../submit'
const JoinForm = () => {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [teen, setTeen] = useState(false)
const [reason, setReason] = useState('')
const [status, setStatus] = useState('')
useEffect(() => {
setTimeout(() => {
setName('')
setEmail('')
setTeen(false)
setReason('')
setStatus('')
}, 1500)
}, [status])
const { status, formProps, useField } = useForm('/api/join')
return (
<Card
@ -35,73 +20,38 @@ const JoinForm = () => {
}
}}
>
<form
action="https://v3.hackclub.com/api/join"
method="post"
onSubmit={(e) => {
e.preventDefault()
fetch('https://v3.hackclub.com/api/join', {
method: 'POST',
body: JSON.stringify({ name, email, teen, reason })
})
.then((r) => r.json())
.then((r) => setStatus(r.status))
.catch((e) => console.error(e))
}}
>
<Label htmlFor="name">
<form {...formProps}>
<Label>
Full name
<Input
name="name"
placeholder="Fiona Hackworth"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Input {...useField('name')} placeholder="Fiona Hackworth" required />
</Label>
<Label htmlFor="email">
<Label>
Email address
<Input
name="email"
type="email"
value={email}
{...useField('email')}
placeholder="fiona@hackclub.com"
onChange={(e) => setEmail(e.target.value)}
required
/>
</Label>
<Label sx={{ flexDirection: 'row !important', alignItems: 'center' }}>
<Checkbox
name="teen"
sx={{ color: 'muted' }}
checked={teen}
onChange={(e) => setTeen(e.target.checked)}
/>
<Label variant="labelCheckbox">
<Checkbox {...useField('teen', 'checkbox')} />
Are you a teenager?
</Label>
<Label htmlFor="reason">
<Label>
Why do you want to join Hack Club?
<Textarea
name="reason"
{...useField('reason')}
placeholder="Write a few sentences."
variant="forms.input"
value={reason}
onChange={(e) => setReason(e.target.value)}
sx={{ boxShadow: 'none !important' }}
required
/>
</Label>
<Button
as="input"
type="submit"
variant="cta"
sx={{
py: 2,
px: 3,
mt: 3,
fontSize: 2,
width: '100%',
fontFamily: 'inherit',
backgroundImage: (theme) => theme.util.gradient('cyan', 'blue')
<Submit
status={status}
labels={{
default: 'Request invitation',
error: 'Something went wrong',
success: 'Submitted!'
}}
value={status === 'success' ? 'Submitted!' : 'Queue signup'}
/>
</form>
</Card>

44
components/submit.js Normal file
View file

@ -0,0 +1,44 @@
import { Button } from 'theme-ui'
import theme from '../lib/theme'
const bg = {
default: {
bg: 'blue',
backgroundImage: theme.util.gradient('cyan', 'blue')
},
success: {
bg: 'green',
backgroundImage: theme.util.gradient('green', 'cyan')
},
error: {
bg: 'orange',
backgroundImage: theme.util.gradient('orange', 'red'),
boxShadow: `0 0 0 1px ${theme.colors.white}, 0 0 0 4px ${theme.colors.primary}`
}
}
const Submit = ({
status,
labels = { default: 'Submit', error: 'Error!', success: 'Submitted!' },
width = '100%',
sx,
...props
}) => (
<Button
as="button"
type="submit"
sx={{
py: 2,
px: 3,
mt: 3,
fontSize: 2,
width,
...bg[status],
...sx
}}
children={labels[status]}
{...props}
/>
)
export default Submit

View file

@ -31,6 +31,7 @@ theme.util.gradientText = (from, to) => ({
})
theme.buttons.primary = merge(theme.buttons.primary, {
justifyContent: 'center',
fontFamily: 'inherit',
borderRadius: 'circle',
boxShadow: 'card',
@ -81,4 +82,22 @@ theme.cards.translucentDark = {
}
}
theme.forms.input = merge(theme.forms.input, { boxShadow: 'none !important' })
theme.forms.textarea = { variant: 'forms.input' }
theme.forms.label = merge(theme.forms.label, {
display: 'flex',
flexDirection: 'column',
textAlign: 'left',
fontSize: 2,
mb: 3
})
theme.forms.labelCheckbox = {
variant: 'forms.label',
flexDirection: 'row !important',
alignItems: 'center',
svg: { color: 'muted' }
}
theme.text.lead = {}
export default theme

69
lib/use-form.js Normal file
View file

@ -0,0 +1,69 @@
import { useState, useEffect } from 'react'
import fetch from 'isomorphic-unfetch'
const useForm = (
submitURL = '/',
callback,
options = { clearOnSubmit: 2000, method: 'post' }
) => {
const [status, setStatus] = useState('default')
const [data, setData] = useState({})
const [touched, setTouched] = useState({})
const onFieldChange = (e, name, type) => {
e.persist()
const value = e.target[type === 'checkbox' ? 'checked' : 'value']
setData((data) => ({ ...data, [name]: value }))
}
useEffect(() => {
setTouched(Object.keys(data))
}, [data])
const useField = (name, type = 'text', ...props) => {
const checkbox = type === 'checkbox'
const empty = checkbox ? false : ''
const onChange = (e) => onFieldChange(e, name, type)
const value = data[name]
return {
name,
type: name === 'email' ? 'email' : type,
[checkbox ? 'checked' : 'value']: value || empty,
onChange,
...props
}
}
const { method = 'post' } = options
const action =
submitURL?.startsWith('/') && process.env.NODE_ENV !== 'development'
? `https://v3.hackclub.com${submitURL}`
: submitURL
const onSubmit = (e) => {
e.preventDefault()
fetch(action, {
method,
body: JSON.stringify(data)
})
.then((r) => r.json())
.then((r) => {
setStatus('success')
if (callback) callback(r)
setTimeout(() => setStatus('default'), 2000)
if (options.clearOnSubmit) {
setTimeout(() => setData({}), options.clearOnSubmit)
}
})
.catch((e) => {
console.error(e)
setStatus('error')
})
}
const formProps = { method, action, onSubmit }
return { status, data, touched, useField, formProps }
}
export default useForm

View file

@ -99,6 +99,7 @@ const Window = ({ title, children, ...props }) => (
</Card>
)
export default () => (
<>
<Head>
<Meta