Skip to content
This repository has been archived by the owner on Aug 5, 2022. It is now read-only.

Commit

Permalink
Week 4: Forms
Browse files Browse the repository at this point in the history
* Add create-customer API call

* Implement sign up form with styles and libraries

* General layout improvement

* ESLint and stylelint configuration fixes
  • Loading branch information
apedroferreira committed Apr 7, 2019
1 parent 231d811 commit 1411893
Show file tree
Hide file tree
Showing 13 changed files with 337 additions and 11 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ module.exports = {
'prefer-named-capture-group': 0,
'react/no-did-mount-set-state': 1,
'react/prop-types': 1,
'no-shadow': [2, { allow: ['name'] }],
},
}
3 changes: 2 additions & 1 deletion .stylelintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"extends": [
"@strv/stylelint-config-styled-components"
"@strv/stylelint-config-styled-components",
"stylelint-config-prettier"
]
}
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"not op_mini all"
],
"dependencies": {
"formik": "^1.5.2",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"react-redux": "^6.0.1",
Expand All @@ -43,7 +44,8 @@
"redux": "^4.0.1",
"sanitize.css": "^8.0.0",
"styled-components": "^4.2.0",
"styled-system": "^4.0.8"
"styled-system": "^4.0.8",
"yup": "^0.27.0"
},
"devDependencies": {
"@strv/eslint-config-react": "^1.0.1",
Expand All @@ -52,6 +54,7 @@
"husky": "^1.3.1",
"lint-staged": "^8.1.5",
"prettier": "^1.16.4",
"stylelint": "^9.10.1"
"stylelint": "^9.10.1",
"stylelint-config-prettier": "^5.0.0"
}
}
2 changes: 2 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import GlobalStyles from './globalStyles'
import { ProductList } from './pages/ProductList'
import { ProductDetail } from './pages/ProductDetail'
import { Cart } from './pages/Cart'
import { SignUp } from './pages/SignUp'
import store from './store'

class App extends Component {
Expand All @@ -17,6 +18,7 @@ class App extends Component {
<Switch>
<Route path="/" exact component={ProductList} />
<Route path="/cart" component={Cart} />
<Route path="/signup" component={SignUp} />
<Route path="/:productId" component={ProductDetail} />
</Switch>
</React.Fragment>
Expand Down
42 changes: 42 additions & 0 deletions src/api/create-customer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { getToken } from './get-token'
import config from '../config'

export const createCustomer = async ({ email, password, firstName }) => {
const token = await getToken()

const response = await fetch(`${config.apiUrl}/api/customers`, {
method: 'POST',
headers: {
'Content-Type': 'application/vnd.api+json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
data: {
type: 'customers',
attributes: {
email,
password,
metadata: {
firstName,
},
},
},
}),
})

switch (response.status) {
case 201: {
const {
data: { attributes },
} = await response.json()
return {
email: attributes.email,
firstName: attributes.metadata.firstName,
}
}
case 422:
throw new Error('Email is already registered')
default:
throw new Error('Unexpected error')
}
}
4 changes: 3 additions & 1 deletion src/components/Button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import styled from 'styled-components/macro'
import theme from '../../common/theme'

const Button = styled.button`
background: ${theme.color.red};
background: ${({ disabled }) =>
disabled ? theme.color.gray : theme.color.red};
cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')};
padding: 1rem;
margin-top: 0.5rem;
border: none;
Expand Down
20 changes: 20 additions & 0 deletions src/components/Form/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import styled from 'styled-components'
import theme from '../../common/theme'

const Form = styled.form`
display: flex;
flex-direction: column;
margin: 14px auto;
max-width: 100%;
padding: 0 12px;
width: 420px;
`

export const GlobalFormError = styled.div`
color: ${theme.color.red};
margin-bottom: 12px;
padding: 10px;
text-align: center;
`

export default Form
20 changes: 20 additions & 0 deletions src/components/Input/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react'

import { Wrapper, Label, StyledInput, StyledError } from './styled'

const Input = ({ error, label, name, onChange, type = 'text', value }) => (
<Wrapper>
<Label htmlFor={name}>{label}</Label>
<StyledInput
hasError={!!error}
id={name}
name={name}
onChange={onChange}
type={type}
value={value}
/>
{!!error && <StyledError>{error}</StyledError>}
</Wrapper>
)

export default Input
27 changes: 27 additions & 0 deletions src/components/Input/styled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import styled from 'styled-components'
import theme from '../../common/theme'

export const Wrapper = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 16px;
`

export const StyledInput = styled.input`
border: 2px solid
${({ hasError }) => (hasError ? theme.color.red : theme.color.gray)};
border-radius: 4px;
font-size: 16px;
font-weight: 100;
padding: 10px 12px;
`

export const Label = styled.label`
font-size: 16px;
margin-bottom: 6px;
`

export const StyledError = styled.div`
color: ${theme.color.red};
margin-top: 4px;
`
17 changes: 13 additions & 4 deletions src/components/Layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,30 @@ const Wrapper = styled.div`
`

const Header = styled.header`
padding: 3rem;
display: flex;
border-bottom: 0.1rem solid gainsboro;
justify-content: space-between;
padding: 3rem;
`

const HeaderSection = styled.div``

const StyledLink = styled(Link)`
margin-right: 1rem;
margin: 0 1rem;
`

class Layout extends Component {
render() {
return (
<Fragment>
<Header>
<StyledLink to="/">All Products</StyledLink>
<StyledLink to="/cart">My Cart</StyledLink>
<HeaderSection>
<StyledLink to="/">All Products</StyledLink>
</HeaderSection>
<HeaderSection>
<StyledLink to="/cart">My Cart</StyledLink>|
<StyledLink to="/signup">Sign Up</StyledLink>
</HeaderSection>
</Header>
<Wrapper>{this.props.children}</Wrapper>
</Fragment>
Expand Down
116 changes: 116 additions & 0 deletions src/pages/SignUp/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { Component } from 'react'
import { Formik } from 'formik'

import { createCustomer } from '../../api/create-customer'
import Layout from '../../components/Layout'
import { H1 } from '../../components/Typography'
import Form, { GlobalFormError } from '../../components/Form'
import Input from '../../components/Input'
import Button from '../../components/Button'
import schema from './schema'

class SignUp extends Component {
state = {
globalError: '',
hasSignedUp: false,
}

initialValues = {
firstName: '',
email: '',
password: '',
passwordConfirm: '',
}

handleSubmit = async (values, { setSubmitting }) => {
try {
setSubmitting(true)
await createCustomer(values)
this.setState({
hasSignedUp: true,
})
} catch (error) {
this.setState({
globalError: error.message,
})
}
setSubmitting(false)
}

renderSuccess() {
return (
<Layout>
<H1 textAlign="center">{`You've signed up!`}</H1>
</Layout>
)
}

render() {
const { hasSignedUp, globalError } = this.state

if (hasSignedUp) return this.renderSuccess()

return (
<Layout>
<H1 textAlign="center">Sign Up</H1>
<Formik
initialValues={this.initialValues}
validationSchema={schema}
onSubmit={this.handleSubmit}
>
{({
errors,
handleChange,
handleSubmit,
isSubmitting,
touched,
values,
}) => (
<Form onSubmit={handleSubmit}>
{!!globalError && (
<GlobalFormError>{globalError}</GlobalFormError>
)}
<Input
name="firstName"
type="text"
label="First name"
value={values.firstName}
onChange={handleChange}
error={touched.firstName && errors.firstName}
/>
<Input
name="email"
type="email"
label="Email address"
value={values.email}
onChange={handleChange}
error={touched.email && errors.email}
/>
<Input
name="password"
type="password"
label="Password"
value={values.password}
onChange={handleChange}
error={touched.password && errors.password}
/>
<Input
name="passwordConfirm"
type="password"
label="Confirm password"
value={values.passwordConfirm}
onChange={handleChange}
error={touched.passwordConfirm && errors.passwordConfirm}
/>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Signing Up...' : 'Sign Up'}
</Button>
</Form>
)}
</Formik>
</Layout>
)
}
}

export { SignUp }
19 changes: 19 additions & 0 deletions src/pages/SignUp/schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { object, string, ref } from 'yup'

const schema = object().shape({
email: string()
.email('Email is not valid')
.required('Email is required'),
firstName: string(),
password: string()
.min(6, 'Password is too short')
.max(30, 'Password is too long')
.matches(/[0-9]/u, 'Password should contain one number')
.matches(/[a-z]/u, 'Password should contain at least one lowercase letter')
.required('Password is required'),
passwordConfirm: string()
.oneOf([ref('password')], 'Passwords must match')
.required('Must confirm password'),
})

export default schema
Loading

0 comments on commit 1411893

Please sign in to comment.