Skip to content

Commit

Permalink
Add component Header (#105)
Browse files Browse the repository at this point in the history
  • Loading branch information
claudivanfilho authored Jul 4, 2018
1 parent 735435c commit 3e393a7
Show file tree
Hide file tree
Showing 11 changed files with 353 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Component `Header`.

## [1.6.1] - 2018-6-27
### Changed
Expand Down
3 changes: 3 additions & 0 deletions react/Header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Header from './components/Header/index'

export default Header
32 changes: 32 additions & 0 deletions react/components/Header/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Header

Header is a canonical component that any VTEX app can import.

To import it into your code:

```js
import { Header } from 'vtex.store-components'
```
Also, you can import as a dependency in your `manifest.json`
```json
dependencies: {
"vtex.store-components: 1.x"
}
```

## Usage

You can use it in your code like a React component with the jsx tag: `<Header />`.

```html
<Header />
```

Or, you can add in your `pages.json`:
```json
"store/header": {
"component": "vtex.store-components/Header"
}
```

See an example at [Dreamstore](https://github.com/vtex-apps/dreamstore-theme/blob/master/pages/pages.json#L7) and [Store](https://github.com/vtex-apps/store/blob/master/react/StoreTemplate.js#L14) apps
68 changes: 68 additions & 0 deletions react/components/Header/__tests__/Header.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react'
import { mountWithIntl, loadTranslation, setLocale } from 'enzyme-react-intl'
import Header from '../components/Header'

describe('Header test', () => {
let wrapperPT

beforeEach(() => {
global.__RUNTIME__ = { account: 'store' }
loadTranslation('../locales/pt-BR.json')
setLocale('pt-BR')
wrapperPT = mountWithIntl(<Header />)
})

it('should have 4 div elements', () => {
expect(wrapperPT.find('div').length).toBe(4)
})

it('should simulate search', done => {
window.location.assign = jest.fn()
const seachString = 'product'
const input = wrapperPT.find('input')
const button = wrapperPT.find('[data-test-id="search"]').first()
process.nextTick(() => {
try {
input.simulate('change', { target: { value: seachString } })
button.simulate('click')
wrapperPT.update()
expect(window.location.assign).toBeCalledWith(`/${seachString}/s`)
} catch (e) {
return done(e)
}
done()
})
})

it('should simulate click on cart', () => {
window.location.assign = jest.fn()
const button = wrapperPT.find('[data-test-id="cart"]').first()
button.simulate('click')
expect(window.location.assign).toBeCalledWith('/checkout/#/cart')
})

it('should simulate filled input', () => {
const seachString = 'product'
const input = wrapperPT.find('input')
input.simulate('change', { target: { value: seachString } })
expect(wrapperPT).toMatchSnapshot()
})

it('should render correctly pt-BR', () => {
expect(wrapperPT).toMatchSnapshot()
})

it('should render correctly en-US', () => {
loadTranslation('../locales/en-US.json')
setLocale('en-US')
const wrapperEN = mountWithIntl(<Header />)
expect(wrapperEN).toMatchSnapshot()
})

it('should render correctly es-AR', () => {
loadTranslation('../locales/es-AR.json')
setLocale('es-AR')
const wrapperES = mountWithIntl(<Header />)
expect(wrapperES).toMatchSnapshot()
})
})
46 changes: 46 additions & 0 deletions react/components/Header/components/Modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Component } from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'

let _modalRoot

const getModalRoot = () => {
if (typeof _modalRoot === 'undefined') {
_modalRoot = document.createElement('div')
_modalRoot.classList.add('vtex-modal-root')

document.body.appendChild(_modalRoot)
}

return _modalRoot
}

class Modal extends Component {
static propTypes = {
children: PropTypes.node,
}

constructor(props) {
super(props)
this.el = document.createElement('div')
this.modalRoot = getModalRoot()
}

componentDidMount() {
this.modalRoot.appendChild(this.el)
}

componentWillUnmount() {
this.modalRoot.removeChild(this.el)
}

render() {
return ReactDOM.createPortal(
this.props.children,
this.el
)
}
}

export default Modal

53 changes: 53 additions & 0 deletions react/components/Header/components/TopMenu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react'
import PropTypes from 'prop-types'
import { intlShape, injectIntl } from 'react-intl'

import Logo from '../../../Logo'
import SearchBar from '../../../SearchBar'

import { ExtensionPoint } from 'render'

const TopMenu = ({ logoUrl, logoTitle, intl, fixed, offsetTop }) => {
const translate = id => intl.formatMessage({ id: `header.${id}` })

return (
<div
className={`${
fixed ? 'fixed shadow-5' : ''
} z-999 flex items-center w-100 flex-wrap pa4 pa5-ns bg-white tl`}
style={{top: `${offsetTop}px`}}
>
<div className="flex w-100 w-auto-ns pa4-ns items-center">
<a className="link b f3 near-black tc tl-ns serious-black flex-auto" href="/">
<Logo
url={logoUrl}
title={logoTitle}
/>
</a>
</div>
<div className="flex-auto pr2 pa4">
<SearchBar
placeholder={translate('search-placeholder')}
emptyPlaceholder={translate('search-emptyPlaceholder')}
/>
</div>
<div className="pr2 bg-black">
<ExtensionPoint id="minicart" />
<ExtensionPoint id="login" />
</div>
</div>
)
}

TopMenu.propTypes = {
logoUrl: PropTypes.string,
logoTitle: PropTypes.string,
intl: intlShape.isRequired,
fixed: PropTypes.bool,
}

TopMenu.defaultProps = {
fixed: false,
}

export default injectIntl(TopMenu)
2 changes: 2 additions & 0 deletions react/components/Header/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vtex-header {
}
138 changes: 138 additions & 0 deletions react/components/Header/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl, intlShape } from 'react-intl'

import Modal from './components/Modal'
import TopMenu from './components/TopMenu'

import { Alert } from 'vtex.styleguide'
import { ExtensionPoint } from 'render'

export const TOAST_TIMEOUT = 3000

class Header extends Component {
state = {
isAddToCart: false,
hasError: false,
error: null,
showMenuPopup: false,
}

static propTypes = {
name: PropTypes.string,
logoUrl: PropTypes.string,
logoTitle: PropTypes.string,
intl: intlShape.isRequired,
}

componentDidMount() {
this._timeouts = []
document.addEventListener('message:error', this.handleError)
document.addEventListener('item:add', this.handleItemAdd)
document.addEventListener('scroll', this.handleScroll)
}

componentWillUnmount() {
if (this._timeouts.length !== 0) {
this._timeouts.map(el => {
clearTimeout(el)
})
}

document.removeEventListener('message:error', this.handleError)
document.removeEventListener('item:add', this.handleItemAdd)
document.removeEventListener('scroll', this.handleScroll)
}

handleError = e => {
this.setState({ hasError: true, error: e })
const timeOut = window.setTimeout(() => {
this.setState({ hasError: false })
}, TOAST_TIMEOUT)

this._timeouts.push(timeOut)
}

handleItemAdd = () => {
this.setState({ isAddToCart: !this.state.isAddToCart })
const timeOut = window.setTimeout(() => {
this._timeoutId = undefined
this.setState({ isAddToCart: !this.state.isAddToCart })
}, TOAST_TIMEOUT)

this._timeouts.push(timeOut)
}

handleScroll = () => {
if (!this._el) {
return
}

const scroll = window.scrollY
const { scrollHeight } = this._el

if (scroll < scrollHeight && this.state.showMenuPopup) {
this.setState({
showMenuPopup: false,
})
} else if (scroll >= scrollHeight) {
this.setState({
showMenuPopup: true,
})
}
}

render() {
const { account } = global.__RUNTIME__
const { name, logoUrl, logoTitle } = this.props
const { isAddToCart, hasError, showMenuPopup, error } = this.state
const offsetTop = (this._el && this._el.offsetTop) || 0
return (
<div
className="vtex-header relative z-2 w-100 shadow-5"
ref={e => {
this._el = e
}}
>
<div className="z-2 items-center w-100 top-0 bg-white tl">
<ExtensionPoint id="menu-link" />
</div>
<TopMenu
logoUrl={logoUrl}
logoTitle={logoTitle}
/>
<ExtensionPoint id="category-menu" />
{showMenuPopup && (
<Modal>
<TopMenu
logoUrl={logoUrl}
logoTitle={logoTitle}
offsetTop={offsetTop}
fixed
/>
</Modal>
)}
<div
className="flex flex-column items-center fixed w-100"
style={{ top: offsetTop + 120 }}
>
{isAddToCart && (
<div className="pa2 mw9">
<Alert type="success">
<FormattedMessage id="header.buy-success" />
</Alert>
</div>
)}

{hasError && (
<div className="pa2 mw9">
<Alert type="error">{error.detail.message}</Alert>
</div>
)}
</div>
</div>
)
}
}

export default injectIntl(Header)
3 changes: 3 additions & 0 deletions react/locales/en-US.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"header.buy-success": "Your product was add to the cart!",
"header.search-placeholder": "Search for products, brands...",
"header.search-emptyPlaceholder": "No matches found",
"search.placeholder": "Search",
"search.noMatches": "No matches",
"editor.productPrice.title": "Product price",
Expand Down
3 changes: 3 additions & 0 deletions react/locales/es-AR.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"header.buy-success": "Su producto se ha agregado a la cesta!",
"header.search-placeholder": "Búsqueda por productos, marcas...",
"header.search-emptyPlaceholder": "Ningún resultado encontrado",
"search.placeholder": "Buscar",
"search.noMatches": "Sin resultado",
"editor.productPrice.title": "Precio del producto",
Expand Down
3 changes: 3 additions & 0 deletions react/locales/pt-BR.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"header.buy-success": "Seu produto foi adicionado ao carrinho!",
"header.search-placeholder": "Busque por produtos, marcas...",
"header.search-emptyPlaceholder": "Nenhum resultado encontrado",
"search.placeholder": "Buscar",
"search.noMatches": "Sem resultado",
"editor.productPrice.title": "Preço do produto",
Expand Down

0 comments on commit 3e393a7

Please sign in to comment.