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

Commit

Permalink
Week 5 (#10)
Browse files Browse the repository at this point in the history
* Pagination + 404

* Link to detail

* Get Products hook

* Romoved products store

* Product detail woth hooks

* Fetch products in cart

* Padding removed from paggination

* Remove @flow

* Naive exception handling

* Inline exports

* Routes params changed to query, changed imports of actions

* Reordered imports

* Extract routes into constants

* Fix for resolve conditions passed into useApi hook
  • Loading branch information
arnostpleskot authored and dannytce committed Apr 17, 2019
1 parent 8781930 commit 320178f
Show file tree
Hide file tree
Showing 23 changed files with 291 additions and 202 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.js
Expand Up @@ -5,6 +5,7 @@ module.exports = {
'prettier',
'prettier/react',
],
plugins: ['react-hooks'],
root: true,
env: {
browser: true,
Expand All @@ -28,5 +29,7 @@ module.exports = {
'no-shadow': [2, { allow: ['name'] }],
// let's enforce this approach a bit
'import/no-default-export': 1,
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
}
4 changes: 4 additions & 0 deletions package.json
Expand Up @@ -36,9 +36,12 @@
],
"dependencies": {
"formik": "^1.5.2",
"qs": "^6.7.0",
"ramda": "^0.26.1",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"react-redux": "^6.0.1",
"react-router": "^5.0.0",
"react-router-dom": "^5.0.0",
"react-scripts": "2.1.8",
"redux": "^4.0.1",
Expand All @@ -51,6 +54,7 @@
"@strv/eslint-config-react": "^1.0.1",
"@strv/stylelint-config-styled-components": "^1.0.0",
"eslint-config-prettier": "^4.1.0",
"eslint-plugin-react-hooks": "^1.6.0",
"husky": "^1.3.1",
"lint-staged": "^8.1.5",
"prettier": "^1.16.4",
Expand Down
48 changes: 26 additions & 22 deletions src/App.js
@@ -1,5 +1,5 @@
import React, { Component } from 'react'
import { Switch, Route } from 'react-router-dom'
import React from 'react'
import { Switch, Route, Redirect } from 'react-router-dom'
import { Provider } from 'react-redux'

import GlobalStyles from './globalStyles'
Expand All @@ -9,32 +9,36 @@ import { Cart } from './pages/Cart'
import { SignUp } from './pages/SignUp'
import { LogIn } from './pages/LogIn'
import { Account } from './pages/Account'
import { NotFound } from './pages/NotFound'
import { PrivateRoute } from './components/PrivateRoute'
import { getCustomer } from './utils/customer'
import { configureStore } from './store'
import * as routes from './routes'

const store = configureStore({
customer: getCustomer(),
})

class App extends Component {
render() {
return (
<Provider store={store}>
<React.Fragment>
<GlobalStyles />
<Switch>
<Route path="/" exact component={ProductList} />
<Route path="/cart" component={Cart} />
<Route path="/signup" component={SignUp} />
<Route path="/login" component={LogIn} />
<PrivateRoute path="/account" component={Account} />
<Route path="/:productId" component={ProductDetail} />
</Switch>
</React.Fragment>
</Provider>
)
}
}
const App = () => (
<Provider store={store}>
<React.Fragment>
<GlobalStyles />
<Switch>
<Route
path={routes.HOMEPAGE}
exact
render={() => <Redirect to={routes.PRODUCT_LIST} />}
/>
<Route path={routes.PRODUCT_LIST} exact component={ProductList} />
<Route path={routes.PRODUCT_DETAIL} component={ProductDetail} />
<Route path={routes.CART} component={Cart} />
<Route path={routes.SIGN_UP} component={SignUp} />
<Route path={routes.LOGIN} component={LogIn} />
<PrivateRoute path={routes.ACCOUNT} component={Account} />
<Route component={NotFound} />
</Switch>
</React.Fragment>
</Provider>
)

export default App
export { App }
12 changes: 9 additions & 3 deletions src/api/products/get-products.js
@@ -1,8 +1,14 @@
import qs from 'qs'
import { api } from '../api-client'
import { formatProduct } from './utils/format-product'

export const getProducts = async () => {
const { data, included } = await api('/api/skus?include=prices')
export const getProducts = async urlQuery => {
const { data, meta, included } = await api(
`/api/skus?${qs.stringify({ include: 'prices', ...urlQuery })}`
)

return data.map(product => formatProduct(product, included))
return {
data: data.map(product => formatProduct(product, included)),
meta,
}
}
30 changes: 30 additions & 0 deletions src/api/use-api.js
@@ -0,0 +1,30 @@
import { useState, useEffect } from 'react'

const useApi = (fn, resolveCondition = []) => {
const [data, setData] = useState(null)
const [isLoading, setLoading] = useState(false)

if (!Array.isArray(resolveCondition)) {
// eslint-disable-next-line no-console
console.error('Passed resolve condition for useEffect hook is not an Array')
}

const request = (...args) => {
setLoading(true)
fn(...args)
.then(returnedData => setData(returnedData))
// eslint-disable-next-line no-console
.catch(console.error)
.finally(() => setLoading(false))
}

useEffect(request, resolveCondition)

return {
request,
data,
isLoading,
}
}

export { useApi }
14 changes: 8 additions & 6 deletions src/components/Layout/index.js
Expand Up @@ -3,6 +3,8 @@ import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'

import * as customerActions from '../../store/customer/actions'
import * as routes from '../../routes'

import { removeToken } from '../../utils/token'
import { removeCustomer } from '../../utils/customer'
import { Wrapper, Header, HeaderSection, HeaderLink } from './styled'
Expand All @@ -12,7 +14,7 @@ class Layout extends Component {
this.props.logout()
removeToken()
removeCustomer()
this.props.history.push('/')
this.props.history.push(routes.HOMEPAGE)
}

render() {
Expand All @@ -22,21 +24,21 @@ class Layout extends Component {
<Fragment>
<Header>
<HeaderSection>
<HeaderLink to="/">All Products</HeaderLink>
<HeaderLink to={routes.PRODUCT_LIST}>All Products</HeaderLink>
</HeaderSection>
<HeaderSection>
<HeaderLink to="/cart">My Cart</HeaderLink>|
<HeaderLink to={routes.CART}>My Cart</HeaderLink>|
{isAuthenticated ? (
<>
<HeaderLink to="/account">My Account</HeaderLink>|
<HeaderLink to={routes.ACCOUNT}>My Account</HeaderLink>|
<HeaderLink as="button" onClick={this.handleLogout}>
Logout
</HeaderLink>
</>
) : (
<>
<HeaderLink to="/login">Log In</HeaderLink> |
<HeaderLink to="/signup">Sign Up</HeaderLink>
<HeaderLink to={routes.LOGIN}>Log In</HeaderLink> |
<HeaderLink to={routes.SIGN_UP}>Sign Up</HeaderLink>
</>
)}
</HeaderSection>
Expand Down
20 changes: 20 additions & 0 deletions src/components/Pagination/index.js
@@ -0,0 +1,20 @@
import * as React from 'react'
import range from 'ramda/src/range'
import map from 'ramda/src/map'
import { Link } from 'react-router-dom'

import * as routes from '../../routes'

import { List, ListItem } from './styled'

const renderPaginationItem = number => (
<ListItem key={number}>
<Link to={`${routes.PRODUCT_LIST}?page=${number}`}>{number}</Link>
</ListItem>
)

const Pagination = ({ pages }) => (
<List>{map(renderPaginationItem, range(1, pages + 1))}</List>
)

export { Pagination }
16 changes: 16 additions & 0 deletions src/components/Pagination/styled.js
@@ -0,0 +1,16 @@
import styled from 'styled-components'

export const List = styled.ul`
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
list-style: none;
padding: 0;
text-align: center;
width: 100%;
`

export const ListItem = styled.li`
margin: 5px;
`
4 changes: 3 additions & 1 deletion src/components/PrivateRoute/index.js
Expand Up @@ -2,6 +2,8 @@ import React from 'react'
import { Route, Redirect } from 'react-router-dom'
import { connect } from 'react-redux'

import * as routes from '../../routes'

const PrivateRouteComponent = ({
isAuthenticated,
component: Component,
Expand All @@ -17,7 +19,7 @@ const PrivateRouteComponent = ({
return (
<Redirect
to={{
pathname: '/login',
pathname: routes.LOGIN,
state: {
from: routeProps.location.pathname,
},
Expand Down
3 changes: 2 additions & 1 deletion src/index.js
@@ -1,10 +1,11 @@
import * as serviceWorker from './serviceWorker'

import App from './App'
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter as Router } from 'react-router-dom'

import { App } from './App'

const render = () => {
ReactDOM.render(
<Router>
Expand Down
28 changes: 28 additions & 0 deletions src/pages/Cart/CartItem.js
@@ -0,0 +1,28 @@
import * as React from 'react'

import { getProductById } from '../../api/products/get-product'
import { useApi } from '../../api/use-api'

import Loader from '../../components/Loader'
import Button from '../../components/Button'

const CartItem = ({ productId, quantity, removeProduct }) => {
const { data: product, isLoading } = useApi(
() => getProductById(productId),
productId
)

return (
<li key={productId}>
{isLoading && <Loader small />}
<p>
{product ? product.name : productId} - {quantity}
</p>
<Button type="button" onClick={() => removeProduct(productId)}>
Remove
</Button>
</li>
)
}

export { CartItem }
53 changes: 23 additions & 30 deletions src/pages/Cart/index.js
@@ -1,50 +1,43 @@
import React, { Component } from 'react'
import React from 'react'
import { connect } from 'react-redux'

import Button from '../../components/Button'
import Layout from '../../components/Layout'
import { H1 } from '../../components/Typography'
import { removeProduct } from '../../store/cart/actions'
import * as cartActions from '../../store/cart/actions'
import { CartItem } from './CartItem'

class CartView extends Component {
render() {
return (
<Layout>
<H1>Your cart</H1>
<ul>
{this.props.items.map(item => (
<li key={item.product.id}>
<p>
{item.product.name} - {item.quantity}
</p>
<Button
type="button"
onClick={() => this.props.removeProduct(item.product.id)}
>
Remove
</Button>
</li>
))}
</ul>
</Layout>
)
}
const CartView = ({ items, removeProduct }) => {
return (
<Layout>
<H1>Your cart</H1>
<ul>
{items.map(item => (
<CartItem
key={item.product.id}
productId={item.product.id}
quantity={item.quantity}
removeProduct={removeProduct}
/>
))}
</ul>
</Layout>
)
}

const mapStateToProps = state => ({
items: Object.keys(state.cart).map(productId => ({
quantity: state.cart[productId],
product: state.products.find(p => p.id === productId),
product: { id: productId },
})),
})

const actionCreators = {
removeProduct,
const mapDispatchToProps = {
removeProduct: cartActions.removeProduct,
}

const Cart = connect(
mapStateToProps,
actionCreators
mapDispatchToProps
)(CartView)

export { Cart }
4 changes: 3 additions & 1 deletion src/pages/LogIn/index.js
Expand Up @@ -8,6 +8,8 @@ import { Form, GlobalFormError } from '../../components/Form'
import { Input } from '../../components/Input'
import Button from '../../components/Button'
import * as customerActions from '../../store/customer/actions'
import * as routes from '../../routes'

import { getCustomerToken } from '../../api/customers/get-customer-token'
import { getCustomer } from '../../api/customers/get-customer'
import { schema } from './schema'
Expand All @@ -31,7 +33,7 @@ class LogInPage extends Component {
})
const customer = await getCustomer(ownerId)
this.props.login(customer)
this.props.history.push('/account')
this.props.history.push(routes.ACCOUNT)
} catch (error) {
this.setState({
globalError: error.message,
Expand Down
5 changes: 5 additions & 0 deletions src/pages/NotFound/index.js
@@ -0,0 +1,5 @@
import * as React from 'react'

const NotFound = () => <div>{"Sorry, page doesn't exist"}</div>

export { NotFound }

0 comments on commit 320178f

Please sign in to comment.