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

Commit 8781930

Browse files
apedroferreiradannytce
authored andcommitted
Week 4: Homework (#9)
* Week 4: Homework (Forms) * Add user fetching and store state * Connect components to store, add logout * Fix header links and move compnents to styled * Redirect unauthenticated to login * Rename user to customer * Refactor named exports and actions * Store customer in localStorage
1 parent 85ba1db commit 8781930

File tree

16 files changed

+296
-69
lines changed

16 files changed

+296
-69
lines changed

src/App.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ import { ProductList } from './pages/ProductList'
77
import { ProductDetail } from './pages/ProductDetail'
88
import { Cart } from './pages/Cart'
99
import { SignUp } from './pages/SignUp'
10+
import { LogIn } from './pages/LogIn'
1011
import { Account } from './pages/Account'
1112
import { PrivateRoute } from './components/PrivateRoute'
12-
import store from './store'
13+
import { getCustomer } from './utils/customer'
14+
import { configureStore } from './store'
15+
16+
const store = configureStore({
17+
customer: getCustomer(),
18+
})
1319

1420
class App extends Component {
1521
render() {
@@ -21,6 +27,7 @@ class App extends Component {
2127
<Route path="/" exact component={ProductList} />
2228
<Route path="/cart" component={Cart} />
2329
<Route path="/signup" component={SignUp} />
30+
<Route path="/login" component={LogIn} />
2431
<PrivateRoute path="/account" component={Account} />
2532
<Route path="/:productId" component={ProductDetail} />
2633
</Switch>

src/api/customers/create-customer.js

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { api } from '../api-client'
2-
import config from '../../config'
3-
import { setToken } from '../../utils/token'
2+
import { getCustomerToken } from './get-customer-token'
43

54
export const createCustomer = async ({ email, password, firstName }) => {
65
const requestBody = {
@@ -26,20 +25,11 @@ export const createCustomer = async ({ email, password, firstName }) => {
2625
data: { attributes },
2726
} = response
2827

29-
const { access_token } = await api('/oauth/token', {
30-
method: 'POST',
31-
body: JSON.stringify({
32-
grant_type: 'password',
33-
username: email,
34-
password: password,
35-
client_id: config.clientId,
36-
}),
37-
})
38-
39-
setToken(access_token)
28+
const { ownerId } = await getCustomerToken({ username: email, password })
4029

4130
return {
42-
email: attributes.email,
31+
ownerId,
32+
username: attributes.email,
4333
firstName: attributes.metadata.firstName,
4434
}
4535
} else {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import config from '../../config'
2+
import { setToken } from '../../utils/token'
3+
4+
export const getCustomerToken = async ({ username, password }) => {
5+
const response = await fetch(`${config.apiUrl}/oauth/token`, {
6+
method: 'POST',
7+
headers: {
8+
'Content-Type': 'application/json',
9+
},
10+
body: JSON.stringify({
11+
grant_type: 'password',
12+
client_id: config.clientId,
13+
scope: config.scope,
14+
username,
15+
password,
16+
}),
17+
})
18+
19+
switch (response.status) {
20+
case 200: {
21+
const { owner_id, access_token } = await response.json()
22+
setToken(access_token)
23+
24+
return { ownerId: owner_id, access_token }
25+
}
26+
case 401:
27+
throw new Error('Email or password are incorrect')
28+
default:
29+
throw new Error('Unexpected error')
30+
}
31+
}

src/api/customers/get-customer.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { api } from '../api-client'
2+
import { setCustomer } from '../../utils/customer'
3+
4+
export const getCustomer = async id => {
5+
const { data } = await api(`/api/customers/${id}`)
6+
setCustomer(data)
7+
return data
8+
}

src/components/Layout/index.js

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,44 @@
11
import React, { Component, Fragment } from 'react'
2-
import { Link } from 'react-router-dom'
3-
import styled from 'styled-components'
2+
import { withRouter } from 'react-router-dom'
3+
import { connect } from 'react-redux'
44

5-
const Wrapper = styled.div`
6-
padding: 2rem;
7-
`
8-
9-
const Header = styled.header`
10-
display: flex;
11-
border-bottom: 0.1rem solid gainsboro;
12-
justify-content: space-between;
13-
padding: 3rem;
14-
`
15-
16-
const HeaderSection = styled.div``
17-
18-
const StyledLink = styled(Link)`
19-
margin: 0 1rem;
20-
`
5+
import * as customerActions from '../../store/customer/actions'
6+
import { removeToken } from '../../utils/token'
7+
import { removeCustomer } from '../../utils/customer'
8+
import { Wrapper, Header, HeaderSection, HeaderLink } from './styled'
219

2210
class Layout extends Component {
11+
handleLogout = () => {
12+
this.props.logout()
13+
removeToken()
14+
removeCustomer()
15+
this.props.history.push('/')
16+
}
17+
2318
render() {
19+
const { isAuthenticated } = this.props
20+
2421
return (
2522
<Fragment>
2623
<Header>
2724
<HeaderSection>
28-
<StyledLink to="/">All Products</StyledLink>
25+
<HeaderLink to="/">All Products</HeaderLink>
2926
</HeaderSection>
3027
<HeaderSection>
31-
<StyledLink to="/cart">My Cart</StyledLink>|
32-
<StyledLink to="/account">My Account</StyledLink>|
33-
<StyledLink to="/signup">Sign Up</StyledLink>
28+
<HeaderLink to="/cart">My Cart</HeaderLink>|
29+
{isAuthenticated ? (
30+
<>
31+
<HeaderLink to="/account">My Account</HeaderLink>|
32+
<HeaderLink as="button" onClick={this.handleLogout}>
33+
Logout
34+
</HeaderLink>
35+
</>
36+
) : (
37+
<>
38+
<HeaderLink to="/login">Log In</HeaderLink> |
39+
<HeaderLink to="/signup">Sign Up</HeaderLink>
40+
</>
41+
)}
3442
</HeaderSection>
3543
</Header>
3644
<Wrapper>{this.props.children}</Wrapper>
@@ -39,4 +47,17 @@ class Layout extends Component {
3947
}
4048
}
4149

42-
export default Layout
50+
const mapStateToProps = state => ({
51+
isAuthenticated: Object.keys(state.customer).length !== 0,
52+
})
53+
54+
const mapDispatchToProps = {
55+
logout: customerActions.logout,
56+
}
57+
58+
export default withRouter(
59+
connect(
60+
mapStateToProps,
61+
mapDispatchToProps
62+
)(Layout)
63+
)

src/components/Layout/styled.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import styled from 'styled-components'
2+
import { Link } from 'react-router-dom'
3+
4+
export const Wrapper = styled.div`
5+
padding: 2rem;
6+
`
7+
8+
export const Header = styled.header`
9+
display: flex;
10+
border-bottom: 0.1rem solid gainsboro;
11+
justify-content: space-between;
12+
padding: 3rem;
13+
`
14+
15+
export const HeaderSection = styled.div``
16+
17+
export const HeaderLink = styled(Link)`
18+
margin: 0 1rem;
19+
`
Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
import React from 'react'
22
import { Route, Redirect } from 'react-router-dom'
3+
import { connect } from 'react-redux'
34

4-
// TODO: connect to global state
5-
const isAuthenticated = false
6-
7-
const PrivateRoute = ({ component: Component, ...rest }) => {
5+
const PrivateRouteComponent = ({
6+
isAuthenticated,
7+
component: Component,
8+
...rest
9+
}) => {
810
return (
911
<Route
1012
{...rest}
1113
render={routeProps => {
1214
if (isAuthenticated) {
1315
return <Component {...routeProps} />
1416
}
15-
1617
return (
1718
<Redirect
1819
to={{
19-
pathname: '/signup',
20+
pathname: '/login',
2021
state: {
2122
from: routeProps.location.pathname,
2223
},
@@ -28,4 +29,8 @@ const PrivateRoute = ({ component: Component, ...rest }) => {
2829
)
2930
}
3031

31-
export { PrivateRoute }
32+
const mapStateToProps = state => ({
33+
isAuthenticated: Object.keys(state.customer).length !== 0,
34+
})
35+
36+
export const PrivateRoute = connect(mapStateToProps)(PrivateRouteComponent)

src/pages/Account/index.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import React, { Component } from 'react'
1+
import React from 'react'
2+
import { connect } from 'react-redux'
23

34
import Layout from '../../components/Layout'
45
import { H1 } from '../../components/Typography'
56

6-
class Account extends Component {
7-
render() {
8-
return (
9-
<Layout>
10-
<H1>My Account</H1>
11-
</Layout>
12-
)
13-
}
14-
}
7+
const AccountPage = ({ customer }) => (
8+
<Layout>
9+
<H1>Welcome {customer.attributes.metadata.firstName}</H1>
10+
</Layout>
11+
)
1512

16-
export { Account }
13+
const mapStateToProps = state => ({
14+
customer: state.customer,
15+
})
16+
17+
export const Account = connect(mapStateToProps)(AccountPage)

src/pages/LogIn/index.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React, { Component } from 'react'
2+
import { Formik } from 'formik'
3+
import { connect } from 'react-redux'
4+
5+
import Layout from '../../components/Layout'
6+
import { H1 } from '../../components/Typography'
7+
import { Form, GlobalFormError } from '../../components/Form'
8+
import { Input } from '../../components/Input'
9+
import Button from '../../components/Button'
10+
import * as customerActions from '../../store/customer/actions'
11+
import { getCustomerToken } from '../../api/customers/get-customer-token'
12+
import { getCustomer } from '../../api/customers/get-customer'
13+
import { schema } from './schema'
14+
15+
class LogInPage extends Component {
16+
state = {
17+
globalError: '',
18+
}
19+
20+
initialValues = {
21+
email: '',
22+
password: '',
23+
}
24+
25+
handleSubmit = async ({ email, password }, { setSubmitting }) => {
26+
try {
27+
setSubmitting(true)
28+
const { ownerId } = await getCustomerToken({
29+
username: email,
30+
password,
31+
})
32+
const customer = await getCustomer(ownerId)
33+
this.props.login(customer)
34+
this.props.history.push('/account')
35+
} catch (error) {
36+
this.setState({
37+
globalError: error.message,
38+
})
39+
}
40+
setSubmitting(false)
41+
}
42+
43+
render() {
44+
const { globalError } = this.state
45+
46+
return (
47+
<Layout>
48+
<H1 textAlign="center">Log In</H1>
49+
<Formik
50+
initialValues={this.initialValues}
51+
validationSchema={schema}
52+
onSubmit={this.handleSubmit}
53+
>
54+
{({ handleSubmit, isSubmitting }) => (
55+
<Form onSubmit={handleSubmit}>
56+
{Boolean(globalError) && (
57+
<GlobalFormError>{globalError}</GlobalFormError>
58+
)}
59+
<Input name="email" type="email" label="Email address" />
60+
<Input name="password" type="password" label="Password" />
61+
<Button disabled={isSubmitting}>
62+
{isSubmitting ? 'Logging In...' : 'Log In'}
63+
</Button>
64+
</Form>
65+
)}
66+
</Formik>
67+
</Layout>
68+
)
69+
}
70+
}
71+
72+
const mapDispatchToProps = {
73+
login: customerActions.login,
74+
}
75+
76+
export const LogIn = connect(
77+
null,
78+
mapDispatchToProps
79+
)(LogInPage)

src/pages/LogIn/schema.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { object, string } from 'yup'
2+
3+
export const schema = object().shape({
4+
email: string()
5+
.email('Email is not valid')
6+
.required('Email is required'),
7+
password: string().required('Password is required'),
8+
})

0 commit comments

Comments
 (0)