Skip to content

Instantly share code, notes, and snippets.

@alobato
Last active March 10, 2019 22:54
Show Gist options
  • Save alobato/0e326449582b86f8408e20298a0f5efd to your computer and use it in GitHub Desktop.
Save alobato/0e326449582b86f8408e20298a0f5efd to your computer and use it in GitHub Desktop.
React Components
import React from 'react'
import styled, { css } from 'styled-components'
import { ExpandLessIcon } from '../components/Icons'
const Accordion = ({ open, title, children, onChange, ...rest }) => (
<div onKeyPress={e => (['Enter', ' '].includes(e.key)) && onChange(!open)} {...rest}>
<label onClick={() => onChange(!open)}>
<span>{title}</span>
<i><ExpandLessIcon /></i>
</label>
<div style={{ maxHeight: open ? 1000 : 0 }}>
{children}
</div>
</div>
)
const StyledAccordion = styled(Accordion)`
outline: 0;
user-select: none;
& > div {
overflow: auto;
transition: max-height 100ms cubic-bezier(0.39, 0.58, 0.57, 1);
}
& > label {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
& > label > i {
line-height: 0;
transition: transform 100ms cubic-bezier(0.39, 0.58, 0.57, 1);
${props => props.open && css`
transform: rotate(180deg);
`}
}
`
StyledAccordion.defaultProps = {
tabIndex: 0,
onChange: () => { },
}
export default StyledAccordion
import React, { useEffect } from 'react'
import Modal from './StyledModalAlert'
const Alert = ({ autoHideDuration = 3000, open, message, onClose }) => {
if (open) return (
<Modal
hasBackdrop={false}
onCloseCompleted={() => onClose()}
render={({onRequestClose}) => {
let timeout
useEffect(() => {
if (open) {
timeout = setTimeout(() => {
onRequestClose()
}, autoHideDuration)
return () => clearTimeout(timeout)
}
}, [open])
return (
<div onClick={() => onRequestClose()}>{message}</div>
)
}}
/>
)
return null
}
export default Alert
import React from 'react'
import styled, { css } from 'styled-components'
const primaryColor = props => props.theme.colors.primary
const darkenPrimaryColor = props => props.theme.colors.darkenPrimary
const Button = ({ children, loading, primary, ...rest }) => (
<button {...rest}>
<i>
<svg height={16} viewBox='0 0 100 100' preserveAspectRatio='xMidYMid'>
<circle cx='50' cy='50' fill='none' stroke='currentColor' strokeWidth='10' r='35' strokeDasharray='164.93361431346415 56.97787143782138' transform='rotate(305.844 50 50)'>
<animateTransform attributeName='transform' type='rotate' calcMode='linear' values='0 50 50;360 50 50' keyTimes='0;1' dur='1s' begin='0s' repeatCount='indefinite' />
</circle>
</svg>
</i>
<span style={{display: 'inline-block', pointerEvents: 'none'}}>
{children}
</span>
</button>
)
const StyledButton = styled(Button)`
cursor: pointer;
user-select: none;
display: inline-block;
border-radius: 22px;
border: none;
font-size: 16px;
font-weight: 700;
line-height: 1;
padding: 12px 32px;
transition: all .3s cubic-bezier(.645, .045, .355, 1);
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045);
color: white;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
background: ${primaryColor};
&:hover {
background: ${darkenPrimaryColor};
}
&:active {
background: hsla(216, 40%, 35%, 1);
}
${props => props.loading && css`
background: hsla(216, 40%, 70%, 1);
pointer-events: none;
`}
& > i {
display: inline-block;
line-height: 0;
pointer-events: none;
vertical-align: -2px;
opacity: 0;
margin-left: -16px;
transition: margin-left .3s cubic-bezier(.645, .045, .355, 1);
${props => props.loading && css`
opacity: 1;
margin-left: 0px;
margin-right: 8px;
`}
}
`
export default StyledButton
import styled from 'styled-components'
import { Box } from '../components/FlexBox'
const Card = styled(Box)`
background: white;
box-shadow: 0px 3px 6px 0px rgba(50, 50, 50, 0.15);
border-radius: 12px;
min-height: 400px;
`
export default Card
import React, { useRef } from 'react'
import styled from 'styled-components'
const Checkbox = ({ checked, label, labelStyle, containerStyle, checkAnimation, uncheckAnimation, ...rest }) => {
const checkboxRef = useRef()
const checkRef = useRef()
const handleClick = () => {
if (checked) {
if (uncheckAnimation) {
uncheckAnimation(checkRef, () => rest.onChange(false))
} else {
rest.onChange(false)
}
} else {
if (checkAnimation) {
checkAnimation(checkRef, () => rest.onChange(true))
} else {
rest.onChange(true)
}
}
}
const content = (
<div onKeyPress={e => (['Enter', ' '].includes(e.key)) && handleClick()} ref={checkboxRef} onClick={handleClick} {...rest}>
<div ref={checkRef} style={{opacity: checked ? 1 : 0}} />
</div>
)
if (label) return (
<div style={containerStyle}>
{content}
<label style={labelStyle} onClick={handleClick}>{label}</label>
</div>
)
return content
}
const checkAnimation = (ref, callback) => {
if (!ref.current) return
if ('animate' in ref.current) {
ref.current.animate([{opacity: 0, transform: 'rotate(45deg) scale(0)'}, {opacity: 1, transform: 'rotate(45deg) scale(1)'}], {duration: 200, easing: 'ease'})
.onfinish = () => {
callback()
}
} else {
callback()
}
}
const uncheckAnimation = (ref, callback) => {
if (!ref.current) return
if ('animate' in ref.current) {
ref.current.animate([{opacity: 1, transform: 'rotate(45deg) scale(1)'}, {opacity: 0, transform: 'rotate(45deg) scale(0)'}], {duration: 200, easing: 'ease'})
.onfinish = () => {
callback()
}
} else {
callback()
}
}
const StyledCheckbox = styled(Checkbox)`
display: inline-block;
position: relative;
cursor: pointer;
width: 18px;
height: 18px;
background-color: transparent;
border-radius: 2px;
border: 2px solid hsla(216, 20%, 50%, 0.85);
transition: background-color 500ms, border-color 500ms;
&:hover {
background-color: hsla(216, 20%, 50%, 0.2);
border-color: hsla(216, 20%, 50%, 1);
}
& > div {
position: absolute;
top: 1px;
left: 4px;
width: 6px;
height: 10px;
border-bottom: 2px solid hsla(216, 20%, 50%, 0.85);
border-left: none;
border-right: 2px solid hsla(216, 20%, 50%, 0.85);
border-top: none;
transform: rotate(45deg);
pointer-events: none;
}
`
const StyledCheckboxWithAnimations = props => (
<StyledCheckbox checkAnimation={checkAnimation} uncheckAnimation={uncheckAnimation} {...props} />
)
StyledCheckboxWithAnimations.defaultProps = {
tabIndex: 0,
onChange: () => {},
containerStyle: { display: 'flex', alignItems: 'center', cursor: 'pointer', userSelect: 'none' },
labelStyle: { cursor: 'pointer', marginLeft: 8 },
}
export default StyledCheckboxWithAnimations
import React from 'react'
const CircularProgress = props => (
<div style={{width: '100%', textAlign: 'center'}}>
<svg
height={120}
viewBox='0 0 100 100'
preserveAspectRatio='xMidYMid'
style={{background: 'none'}}
{...props}
>
<circle
cx='50'
cy='50'
fill='none'
r='20'
stroke='hsla(216, 40%, 55%, 1)'
strokeWidth='5'
/>
<circle
cx='50'
cy='50'
fill='none'
r='20'
stroke='#ffffff'
strokeWidth='4'
strokeLinecap='square'
transform='rotate(146.131 50 50)'
>
<animateTransform
attributeName='transform'
type='rotate'
calcMode='linear'
values='0 50 50;180 50 50;720 50 50'
keyTimes='0;0.5;1'
dur='2.9s'
begin='0s'
repeatCount='indefinite'
/>
<animate
attributeName='stroke-dasharray'
calcMode='linear'
values='12.566370614359172 113.09733552923255;62.83185307179586 62.83185307179586;12.566370614359172 113.09733552923255'
keyTimes='0;0.5;1'
dur='2.9'
begin='0s'
repeatCount='indefinite'
/>
</circle>
</svg>
</div>
)
export default CircularProgress
import React from 'react'
import styled from 'styled-components'
const Button = props => (
<button {...props}>
<i>
<svg height={32} viewBox='0 0 24 24' fill='currentColor'>
<path d='M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z' />
</svg>
</i>
</button>
)
const StyledButton = styled(Button)`
cursor: pointer;
user-select: none;
width: 56px; height: 56px;
position: fixed;
bottom: 24px;
right: 24px;
border-radius: 50%;
box-shadow: rgba(0, 0, 0, 0.14) 0px 6px 10px 0px, rgba(0, 0, 0, 0.12) 0px 1px 18px 0px, rgba(0, 0, 0, 0.2) 0px 3px 5px -1px;
display: flex; justify-content: center; align-items: center;
background: hsla(216, 40%, 55%, 1);
color: white;
&:hover {
background: hsla(216, 40%, 45%, 1);
}
&:active {
background: hsla(216, 40%, 35%, 1);
}
& > i {
display: block;
line-height: 0;
pointer-events: none;
}
`
export default StyledButton
import styled from 'styled-components'
import { style, space, color, border, borderRadius, width, height, opacity, fontSize, fontWeight, flex, order, alignSelf, textAlign, lineHeight, top, right, bottom, left, maxWidth, maxHeight, minWidth, minHeight, position, overflow, justifyContent, alignItems, flexDirection, flexWrap } from 'styled-system'
const lh = style({prop: 'lh', cssProperty: 'lineHeight'})
const ta = style({prop: 'ta', cssProperty: 'textAlign'})
const t = style({prop: 't', cssProperty: 'top', transformValue: n => /^\d+$/.test(n) ? n + 'px': n})
const r = style({prop: 'r', cssProperty: 'right', transformValue: n => /^\d+$/.test(n) ? n + 'px': n})
const b = style({prop: 'b', cssProperty: 'bottom', transformValue: n => /^\d+$/.test(n) ? n + 'px': n})
const l = style({prop: 'l', cssProperty: 'left', transformValue: n => /^\d+$/.test(n) ? n + 'px': n})
const w = style({prop: 'w', cssProperty: 'width', transformValue: n => /^\d+$/.test(n) ? n + 'px': n})
const h = style({prop: 'h', cssProperty: 'height', transformValue: n => /^\d+$/.test(n) ? n + 'px': n})
const cursor = style({prop: 'cursor', cssProperty: 'cursor'})
export const Box = styled('div')(
{boxSizing: 'border-box'},
space,
color,
border,
borderRadius,
width,
height,
opacity,
fontSize,
fontWeight,
flex,
order,
alignSelf,
textAlign,
ta,
lineHeight,
lh,
top, right, bottom, left,
t, r, b, l,
maxWidth, maxHeight, minWidth, minHeight,
w, h,
cursor,
position,
overflow,
props => props.css
)
Box.displayName = 'Box'
Box.propTypes = {
...space.propTypes,
...color.propTypes,
...width.propTypes,
...height.propTypes,
...fontSize.propTypes,
}
const jc = style({prop: 'jc', cssProperty: 'justifyContent'})
const ai = style({prop: 'ai', cssProperty: 'alignItems'})
const fd = style({prop: 'fd', cssProperty: 'flexDirection'})
const fw = style({prop: 'fd', cssProperty: 'flexWrap'})
export const Flex = styled(Box)(
{display: 'flex'},
justifyContent, alignItems, flexDirection, flexWrap,
jc, ai, fd, fw,
props => props.css
)
Flex.displayName = 'Flex'
Flex.propTypes = {
...justifyContent.propTypes,
...alignItems.propTypes,
...flexDirection.propTypes,
...flexWrap.propTypes,
}
export const FormatCpf = props => {
let formatedCpf = props.children
if (!formatedCpf) return ''
if (formatedCpf.length === 11)
formatedCpf = `${formatedCpf.substr(0, 3)}.${formatedCpf.substr(3, 3)}.${formatedCpf.substr(6, 3)}-${formatedCpf.substr(9, 2)}`
return formatedCpf
}
export const FormatCnpj = props => {
let formated = props.children
if (!formated) return ''
if (formated.length === 14)
formated = `${formated.substr(0, 2)}.${formated.substr(2, 3)}.${formated.substr(5, 3)}/${formated.substr(8, 4)}-${formated.substr(12, 2)}`
return formated
}
export const FormatCep = props => {
let formated = props.children
if (!formated) return ''
if (formated.length === 8)
formated = `${formated.substr(0, 5)}-${formated.substr(5, 3)}`
return formated
}
export const FormatCurrency = props => {
if (!props.children || props.children === '') return ''
return parseFloat(props.children).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
}
export const FormatGender = props => {
if (props.children && props.children.toString().toUpperCase() === 'M') return 'Masculino'
if (props.children && props.children.toString().toUpperCase() === 'F') return 'Feminino'
return ''
}
import React from 'react'
import styled from 'styled-components'
import { Box } from './FlexBox'
export const FormError = styled.div`
font-size: 10px;
line-height: 18px;
color: hsla(6, 66%, 47%, 1);
text-transform: uppercase;
font-weight: 500;
`
const FormErrorBox = ({ errors, touched, fieldName }) => {
return (
<Box minHeight={18}>
{errors[fieldName] && touched[fieldName] && <FormError>{errors[fieldName]}</FormError>}
</Box>
)
}
export default FormErrorBox
import { Formik } from 'formik'
export default Formik
import React from 'react'
export const MoreIcon = props => (
<svg viewBox='0 0 24 24' height={24} fill='currentColor' {...props}>
<path d='M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z' />
</svg>
)
export const ExpandLessIcon = props => (
<svg viewBox='0 0 24 24' height={24} fill='currentColor' {...props}>
<path d='M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z' />
</svg>
)
import React from 'react'
import ReactDOM from 'react-dom'
import App from './containers/App'
import * as serviceWorker from './serviceWorker'
import { ApolloClient } from 'apollo-client'
import { ApolloProvider } from 'react-apollo'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { split } from 'apollo-link'
import { HttpLink } from 'apollo-link-http'
import { WebSocketLink } from 'apollo-link-ws'
import { getMainDefinition } from 'apollo-utilities'
import { ApolloLink } from 'apollo-link'
import { setContext } from 'apollo-link-context'
import { onError } from 'apollo-link-error'
import { createUploadLink } from 'apollo-upload-client'
import { ApolloProvider as ApolloHooksProvider } from './hooks/useApollo'
import ApolloLinkTimeout from './utils/apolloLinkTimeout'
import BASE_API, { WS_BASE_API } from './constants/baseApi'
import AUTH_TOKEN from './constants/authToken'
const timeoutLink = new ApolloLinkTimeout(10000)
const httpLink = new HttpLink({uri: `${BASE_API}/graphql`})
const uploadLink = createUploadLink({uri: `${BASE_API}/graphql`})
const wsLink = new WebSocketLink({uri: `${WS_BASE_API}/graphql`, options: {reconnect: true}})
const logout = client => {
localStorage.removeItem(AUTH_TOKEN)
client.resetStore()
window.location.href = '/login'
}
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) => console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`))
graphQLErrors.forEach(({ message, locations, path }) => {
if (message === 'Context creation failed: Your session expired. Sign in again.') logout(client)
if (message === 'Not authenticated as user.') logout(client)
})
}
if (networkError && networkError.statusCode === 401) {
console.log(`[Network error]: ${networkError}`)
logout(client)
}
})
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem(AUTH_TOKEN)
return { headers: {...headers, authorization: token ? `Bearer ${token}` : ''} }
})
// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const splitLink = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLink, httpLink
)
const cache = new InMemoryCache()
const defaultOptions = {}
const link = ApolloLink.from([timeoutLink, authLink, errorLink, uploadLink, splitLink])
const client = new ApolloClient({link, cache, defaultOptions})
ReactDOM.render(
<ApolloProvider client={client}>
<ApolloHooksProvider client={client}>
<App logout={logout} client={client} />
</ApolloHooksProvider>
</ApolloProvider>,
document.getElementById('root')
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister()
import React, { memo, forwardRef } from 'react'
import styled, { css } from 'styled-components'
import { width, height } from 'styled-system'
const MyInput = memo(forwardRef(({withError, ...rest}, ref) => (
<input ref={ref} {...rest} />
)))
const StyledInput = styled(MyInput)`
border: 1px solid hsla(216, 40%, 90%, 1);
border-radius: 3px;
font-size: inherit;
line-height: 1;
padding: 8px;
width: 100%;
transition: all 0.3s;
${width}
${height}
::placeholder {
color: hsla(216, 40%, 55%, 1);
}
${props => props.withError && css`
border-color: hsla(6, 66%, 47%, 1);
`}
&:focus {
outline: 0;
box-shadow: 0 0 0 3px hsla(216, 40%, 90%, 0.5);
border: 1px solid hsla(216, 40%, 70%, 1);
}
`
export default StyledInput
import React, { memo, useState, useEffect } from 'react'
import Input from './Input'
const maskValue = (value = '') => {
value = value.toString()
value = value.replace(/\D/g, '')
if (!value) return ''
if (value.length === 4 && /\d{4}/.test(value)) return `${value.substr(0, 3)}.${value.substr(3, 1)}`
if (value.length === 5 && /\d{5}/.test(value)) return `${value.substr(0, 3)}.${value.substr(3, 2)}`
if (value.length === 6 && /\d{6}/.test(value)) return `${value.substr(0, 3)}.${value.substr(3, 3)}`
if (value.length === 7 && /\d{7}/.test(value)) return `${value.substr(0, 3)}.${value.substr(3, 3)}.${value.substr(6, 1)}`
if (value.length === 8 && /\d{8}/.test(value)) return `${value.substr(0, 3)}.${value.substr(3, 3)}.${value.substr(6, 2)}`
if (value.length === 9 && /\d{9}/.test(value)) return `${value.substr(0, 3)}.${value.substr(3, 3)}.${value.substr(6, 3)}`
if (value.length === 10 && /\d{10}/.test(value)) return `${value.substr(0, 3)}.${value.substr(3, 3)}.${value.substr(6, 3)}-${value.substr(9, 1)}`
if (value.length === 11 && /\d{11}/.test(value)) return `${value.substr(0, 3)}.${value.substr(3, 3)}.${value.substr(6, 3)}-${value.substr(9, 2)}`
return value
}
const unmaskValue = (maskedValue = '') => {
if (!maskedValue) return ''
if (maskedValue.length === 10) {
return maskedValue.replace(/\D/g, '')
} else {
return ''
}
}
const initialMaskedValue = ''
const InputCpf = memo(({ onChange = () => {}, value, defaultValue, ...rest }) => {
const [maskedValue, setMaskedValue] = useState(initialMaskedValue)
useEffect(
() => {
setMaskedValue(maskValue(value || defaultValue))
},
[defaultValue]
)
const handleChange = event => {
const { target } = event
const { value: inputValue = 0 } = target
const value = unmaskValue(inputValue)
const maskedValue = maskValue(inputValue)
setMaskedValue(maskedValue)
if (!onChange || typeof onChange !== 'function') return false
return onChange(event, value, maskedValue)
}
return (
<Input maxLength={14} type='tel' value={maskedValue} onChange={handleChange} {...rest} />
)
})
export default InputCpf
import React, { memo, useState, useEffect } from 'react'
import Input from './Input'
const maskValue = (value = '') => {
value = value.toString()
value = value.replace(/\D/g, '')
if (!value) return ''
if (value.length === 1 && /\d/.test(value)) return `(${value}`
if (value.length === 2 && /\d{2}/.test(value)) return `(${value}`
if (value.length === 3 && /\d{3}/.test(value)) return `(${value.substr(0, 2)}) ${value.substr(2, 1)}`
if (value.length === 4 && /\d{4}/.test(value)) return `(${value.substr(0, 2)}) ${value.substr(2, 2)}`
if (value.length === 5 && /\d{5}/.test(value)) return `(${value.substr(0, 2)}) ${value.substr(2, 3)}`
if (value.length === 6 && /\d{6}/.test(value)) return `(${value.substr(0, 2)}) ${value.substr(2, 4)}`
if (value.length === 7 && /\d{7}/.test(value)) return `(${value.substr(0, 2)}) ${value.substr(2, 4)}-${value.substr(6, 1)}`
if (value.length === 8 && /\d{8}/.test(value)) return `(${value.substr(0, 2)}) ${value.substr(2, 4)}-${value.substr(6, 2)}`
if (value.length === 9 && /\d{9}/.test(value)) return `(${value.substr(0, 2)}) ${value.substr(2, 4)}-${value.substr(6, 3)}`
if (value.length === 10 && /\d{10}/.test(value)) return `(${value.substr(0, 2)}) ${value.substr(2, 4)}-${value.substr(6, 4)}`
if (value.length === 11 && /\d{11}/.test(value)) return `(${value.substr(0, 2)}) ${value.substr(2, 5)}-${value.substr(7, 4)}`
return value
}
const unmaskValue = (maskedValue = '') => {
if (!maskedValue) return ''
if (maskedValue.length === 10) {
return maskedValue.replace(/\D/g, '')
} else {
return ''
}
}
const initialMaskedValue = ''
const InputPhone = memo(({ onChange = () => {}, value, defaultValue, ...rest }) => {
const [maskedValue, setMaskedValue] = useState(initialMaskedValue)
useEffect(
() => {
setMaskedValue(maskValue(value || defaultValue))
},
[defaultValue]
)
const handleChange = event => {
const { target } = event
const { value: inputValue = 0 } = target
const value = unmaskValue(inputValue)
const maskedValue = maskValue(inputValue)
setMaskedValue(maskedValue)
if (!onChange || typeof onChange !== 'function') return false
return onChange(event, value, maskedValue)
}
return (
<Input type='tel' maxLength={15} value={maskedValue} onChange={handleChange} {...rest} />
)
})
export default InputPhone
import styled from 'styled-components'
const Label = styled.label`
font-weight: 600;
line-height: 24px;
& > span {
color: hsla(216, 40%, 70%, 1);
}
`
export default Label
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Manager, Reference, Popper } from 'react-popper'
import Portal from './Portal'
class ClickOutside extends Component {
static propTypes = {
onClickOutside: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.getContainer = this.getContainer.bind(this)
this.isTouch = false
}
getContainer(ref) {
this.container = ref
}
render() {
const { children, onClickOutside, ...props } = this.props
return <div {...props} ref={this.getContainer}>{children}</div>
}
componentDidMount() {
document.addEventListener('touchend', this.handle, true)
document.addEventListener('click', this.handle, true)
}
componentWillUnmount() {
document.removeEventListener('touchend', this.handle, true)
document.removeEventListener('click', this.handle, true)
}
handle = e => {
if (e.type === 'touchend') this.isTouch = true
if (e.type === 'click' && this.isTouch) return
const { onClickOutside } = this.props
const el = this.container
if (!el.contains(e.target)) onClickOutside(e)
}
}
export default class MenuOver extends Component {
state = {
open: false
}
static defaultProps = {
placement: 'right',
hoverBgColor: 'hsla(216, 20%, 95%, 1)'
}
handleTouch = e => {
if (e.target.disabled) return
if (e.target.classList.contains('hover'))
e.target.classList.remove('hover')
else
e.target.classList.add('hover')
}
render() {
const { children, options, placement, hoverBgColor, id } = this.props
return (
<Manager>
<Reference>
{({ ref }) =>
<div ref={ref} onClick={() => this.setState({open: true})}>
{children}
</div>
}
</Reference>
<Portal>
{this.state.open &&
<Popper placement={placement}>
{({ ref, style, placement }) =>
<div ref={ref} data-placement={placement} style={{zIndex: '2', ...style}}>
<ClickOutside onClickOutside={() => this.setState({open: false})}>
<>
<style dangerouslySetInnerHTML={{__html: `.option:hover,.option.hover{background-color:${hoverBgColor} !important}`}} />
<div style={{borderRadius: 4, background: 'white', fontSize: 14, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', padding: '4px 0'}}>
{options.map((option, index) =>
<div
key={`${option}${index}`}
className='option'
role='option'
aria-selected='false'
style={{fontSize: 14, padding: '6px 16px', cursor: 'pointer', userSelect: 'none'}}
onTouchStart={this.handleTouch}
onTouchEnd={this.handleTouch}
onClick={() => this.setState({open: false}, () => this.props.onSelect(option.value, id)) }
>
{option.label}
</div>
)}
</div>
</>
</ClickOutside>
</div>
}
</Popper>
}
</Portal>
</Manager>
)
}
}
import React, { forwardRef, useRef, useEffect } from 'react'
import { createPortal } from 'react-dom'
import useKeyPress from '../hooks/useKeyPress'
import useLockBodyScroll from '../hooks/useLockBodyScroll'
const Portal = ({children}) => createPortal(children, document.getElementById('modal-root'))
const Backdrop = forwardRef(({ onClick, zIndex = 1000, style }, ref) => (
<div ref={ref} style={{position: 'fixed', top: 0, right: 0, bottom: 0, left: 0, backgroundColor: 'black', opacity: 0, zIndex: zIndex, outline: 'none', tabIndex: -1, ...style}} onClick={onClick} />
))
const Modal = ({ render, className, onCloseCompleted = () => {}, zIndex = 1001, hasBackdrop = true, clickOutsideDisabled = false, backdropOpacity = 0.6, exitAnimation, enterAnimation, backdropStyle }) => {
if (hasBackdrop) useLockBodyScroll()
const modal = useRef()
const backdrop = useRef()
let initialOpacity = 0
const handleExit = () => {
if (exitAnimation) {
exitAnimation(modal, backdrop, backdropOpacity, onCloseCompleted)
} else {
onCloseCompleted()
}
}
useEffect(() => {
if (enterAnimation) {
enterAnimation(modal, backdrop, backdropOpacity)
} else {
modal.current.style.opacity = 1
modal.current.style.transform = 'translateY(0)'
backdrop.current.style.opacity = backdropOpacity
}
}, [])
const escPress = useKeyPress('Escape')
if (escPress) handleExit()
return (
<Portal>
<div className={className} ref={modal} tabIndex='-1' style={{opacity: initialOpacity, position: 'fixed', top: 0, right: 0, bottom: 0, left: 0, zIndex: zIndex, overflow: 'hidden', pointerEvents: 'none', outline: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
<div>
{render({onRequestClose: handleExit})}
</div>
</div>
{hasBackdrop &&
<Backdrop
style={backdropStyle}
ref={backdrop}
onClick={() => {
if (clickOutsideDisabled) return false
handleExit()
}} />
}
</Portal>
)
}
export default Modal
import React, { memo, useState, useEffect } from 'react'
import memoize from 'memoize-one'
import isEqual from 'lodash.isequal'
const equalFn = (newArg, lastArg) => isEqual(newArg, lastArg)
const range = (start, end) => [...Array(end - start).keys()].map(k => k + start)
const getPager = memoize((totalItems, currentPage, pageSize) => {
// calculate total pages
const totalPages = Math.ceil(totalItems / pageSize)
let startPage, endPage
if (totalPages <= 10) {
// less than 10 total pages so show all
startPage = 1
endPage = totalPages
} else {
// more than 10 total pages so calculate start and end pages
if (currentPage <= 6) {
startPage = 1
endPage = 10
} else if (currentPage + 4 >= totalPages) {
startPage = totalPages - 9
endPage = totalPages
} else {
startPage = currentPage - 5
endPage = currentPage + 4
}
}
// calculate start and end item indexes
const startIndex = (currentPage - 1) * pageSize
const endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1)
// create an array of pages
const pages = range(startPage, endPage + 1)
// return object with all pager properties required by the view
return {
totalItems: totalItems,
currentPage: currentPage,
pageSize: pageSize,
totalPages: totalPages,
startPage: startPage,
endPage: endPage,
startIndex: startIndex,
endIndex: endIndex,
pages: pages
}
})
const getPageOfItems = memoize((items, pager) => items.slice(pager.startIndex, pager.endIndex + 1), equalFn)
const Pagination = memo(({ initialPage, pageSize, items, previousLabel, nextLabel, fistLabel, lastLabel, onChange, ...rest }) => {
const processedItems = () => items
const genPager = () => getPager(processedItems().length, initialPage, pageSize)
const [pager, setPager] = useState(genPager())
useEffect(() => {
const newPager = genPager()
setPager(newPager)
const pageOfItems = getPageOfItems(processedItems(), newPager)
onChange(pageOfItems)
}, [JSON.stringify(items)]
)
const setPage = page => {
const newPager = getPager(processedItems().length, page, pageSize)
setPager(newPager)
const pageOfItems = getPageOfItems(processedItems(), newPager)
onChange(pageOfItems)
}
const isFirstPage = pager.currentPage === 1
const isCurrentPage = page => pager.currentPage === page
return (
<div {...rest}>
<ul>
<li className={isFirstPage ? 'disabled' : ''}>
{isFirstPage ? (<span>{fistLabel}</span>) : (<span tabIndex={0} onKeyPress={e => (e.key === 'Enter') && setPage(1)} onClick={() => setPage(1)}>{fistLabel}</span>)}
</li>
<li className={isFirstPage ? 'disabled' : ''}>
{isFirstPage ? (<span>{previousLabel}</span>) : (<span tabIndex={0} onKeyPress={e => (e.key === 'Enter') && setPage(pager.currentPage - 1)} onClick={() => setPage(pager.currentPage - 1)}>{previousLabel}</span>)}
</li>
{pager.pages.map((page, index) =>
<li key={index} className={isCurrentPage(page) ? 'active' : ''}><span tabIndex={isCurrentPage(page) ? -1 : 0} onKeyPress={e => (e.key === 'Enter') && setPage(page)} onClick={() => setPage(page)}>{page}</span></li>
)}
<li className={pager.currentPage === pager.totalPages ? 'disabled' : ''}>
{pager.currentPage === pager.totalPages ? (<span>{nextLabel}</span>) : (<span tabIndex={0} onKeyPress={e => (e.key === 'Enter') && setPage(pager.currentPage + 1)} onClick={() => setPage(pager.currentPage + 1)}>{nextLabel}</span>)}
</li>
<li className={pager.currentPage === pager.totalPages ? 'disabled' : ''}>
{pager.currentPage === pager.totalPages ? (<span>{lastLabel}</span>) : (<span tabIndex={0} onKeyPress={e => (e.key === 'Enter') && setPage(pager.totalPages)} onClick={() => setPage(pager.totalPages)}>{lastLabel}</span>)}
</li>
</ul>
</div>
)
}, equalFn)
Pagination.defaultProps = {
initialPage: 1,
pageSize: 20,
items: [],
fistLabel: 'Primeira',
nextLabel: 'Próxima',
previousLabel: 'Anterior',
lastLabel: 'Última',
onChange: () => {}
}
export default Pagination
import { createPortal } from 'react-dom'
const Portal = ({ children }) => (
createPortal(children, document.getElementById('portal-root'))
)
export default Portal
import React, { useRef } from 'react'
import styled from 'styled-components'
const Radio = ({ checked, label, labelStyle, containerStyle, checkAnimation, uncheckAnimation, ...rest }) => {
const checkboxRef = useRef()
const checkRef = useRef()
const handleClick = () => {
if (checked) {
if (uncheckAnimation) {
uncheckAnimation(checkRef, () => rest.onChange(false))
} else {
rest.onChange(false)
}
} else {
if (checkAnimation) {
checkAnimation(checkRef, () => rest.onChange(true))
} else {
rest.onChange(true)
}
}
}
const content = (
<div onKeyPress={e => (['Enter', ' '].includes(e.key)) && handleClick()} ref={checkboxRef} onClick={handleClick} {...rest}>
<div ref={checkRef} style={{ opacity: checked ? 1 : 0 }} />
</div>
)
if (label) return (
<div style={containerStyle}>
{content}
<div style={labelStyle} onClick={handleClick}>{label}</div>
</div>
)
return content
}
const checkAnimation = (ref, callback) => {
if (!ref.current) return
if ('animate' in ref.current) {
ref.current.animate([{ opacity: 0, transform: 'scale(0)' }, { opacity: 1, transform: 'scale(1)' }], { duration: 200, easing: 'ease' })
.onfinish = () => {
callback()
}
} else {
callback()
}
}
const uncheckAnimation = (ref, callback) => {
if (!ref.current) return
if ('animate' in ref.current) {
ref.current.animate([{ opacity: 1, transform: 'scale(1)' }, { opacity: 0, transform: 'scale(0)' }], { duration: 200, easing: 'ease' })
.onfinish = () => {
callback()
}
} else {
callback()
}
}
const StyledRadio = styled(Radio)`
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
width: 18px;
height: 18px;
background-color: transparent;
border-radius: 50%;
border: 2px solid hsla(216, 20%, 50%, 0.85);
transition: background-color 500ms, border-color 500ms;
&:hover {
background-color: hsla(216, 20%, 50%, 0.2);
border-color: hsla(216, 20%, 50%, 1);
}
& > div {
width: 10px;
height: 10px;
border-radius: 50%;
background: hsla(216, 20%, 50%, 0.85);
pointer-events: none;
}
`
const StyledRadioWithAnimations = props => (
<StyledRadio checkAnimation={checkAnimation} uncheckAnimation={uncheckAnimation} {...props} />
)
StyledRadioWithAnimations.defaultProps = {
tabIndex: 0,
onChange: () => { },
containerStyle: { display: 'flex', alignItems: 'center', cursor: 'pointer', userSelect: 'none' },
labelStyle: { marginLeft: 8 },
}
export default StyledRadioWithAnimations
import React from 'react'
import Radio from './Radio'
const RadioGroup = ({value, options, onChange}) => {
const handleChange = optionValue => {
if (optionValue === value) return onChange('')
return onChange(optionValue)
}
return (
<>
{options.map(option => (
<Radio label={option.label} onChange={() => handleChange(option.value)} checked={option.value === value} />
))}
</>
)
}
export default RadioGroup
import { Component } from 'react'
import { Router as ReachRouter, Link as ReachLink } from '@reach/router'
export const Router = ReachRouter
export const Link = ReachLink
// https://github.com/reach/router/issues/100
class MyRedirect extends Component {
componentDidMount() {
setTimeout(() => this.props.navigate(this.props.to, { replace: true }), 0)
}
render() {
return null
}
}
export const Redirect = MyRedirect
import React from 'react'
import styled from 'styled-components'
import Modal from './Modal'
const enterAnimation = (modal, backdrop, backdropOpacity) => {
if (!modal.current) return
if ('animate' in modal.current) {
backdrop.current.animate([{opacity: 0}, {opacity: backdropOpacity}], {duration: 300, easing: 'ease'})
modal.current.animate([{opacity: 0, transform: 'translateY(-100px)'}, {opacity: 1, transform: 'translateY(0)'}], {duration: 300, easing: 'ease'})
.onfinish = () => {
if (modal.current) { modal.current.style.opacity = 1 }
if (backdrop.current) { backdrop.current.style.opacity = backdropOpacity }
}
} else {
modal.current.style.opacity = 1
modal.current.style.transform = 'translateY(0)'
backdrop.current.style.opacity = backdropOpacity
}
}
const exitAnimation = (modal, backdrop, backdropOpacity, callback) => {
if (!modal.current) return
if ('animate' in modal.current) {
backdrop.current.animate([{opacity: backdropOpacity}, {opacity: 0}], {duration: 300, easing: 'ease'})
modal.current.animate([{opacity: 1, transform: 'translateY(0)'}, {opacity: 0, transform: 'translateY(-100px)'}], {duration: 300, easing: 'ease'})
.onfinish = () => { callback() }
} else {
callback()
}
}
const StyledModal = styled(Modal)`
& > div {
position: relative;
background-color: white;
box-shadow: 0 7px 8px -4px rgba(0,0,0,0.2), 0 13px 19px 2px rgba(0,0,0,0.14), 0 5px 24px 4px rgba(0,0,0,0.12);
pointer-events: auto;
height: calc(100vh);
margin: 0;
overflow: scroll;
width: 100%;
@media (min-width: 40em) {
margin: 32px auto;
width: auto;
/* height: min-content; */
height: calc(100vh - 64px);
max-height: 800px;
min-width: 640px;
border-radius: 8px;
}
}
`
const StyledModalWithAnimations = props => (
<StyledModal enterAnimation={enterAnimation} exitAnimation={exitAnimation} {...props} />
)
export default StyledModalWithAnimations
import React from 'react'
import styled from 'styled-components'
import Modal from './Modal'
const enterAnimation = (modal, backdrop, backdropOpacity) => {
if (!modal.current) return
if ('animate' in modal.current) {
modal.current.animate([{opacity: 0, transform: 'translateY(-100px)'}, {opacity: 1, transform: 'translateY(0)'}], {duration: 300, easing: 'ease'})
.onfinish = () => {
if (modal.current) { modal.current.style.opacity = 1 }
}
} else {
modal.current.style.opacity = 1
modal.current.style.transform = 'translateY(0)'
}
}
const exitAnimation = (modal, backdrop, backdropOpacity, callback) => {
if (!modal.current) return
if ('animate' in modal.current) {
modal.current.animate([{opacity: 1, transform: 'translateY(0)'}, {opacity: 0, transform: 'translateY(-100px)'}], {duration: 300, easing: 'ease'})
.onfinish = () => { callback() }
} else {
callback()
}
}
const StyledModal = styled(Modal)`
align-items: stretch !important;
& > div {
cursor: pointer;
margin-top: 8px;
pointer-events: auto !important;
height: 40px;
border-radius: 4px;
padding: 0 40px;
background-color: hsla(0, 0%, 0%, 0.8);
color: hsla(0, 0%, 90%, 1);
min-width: 100px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
`
const StyledModalWithAnimations = props => (
<StyledModal enterAnimation={enterAnimation} exitAnimation={exitAnimation} {...props} />
)
export default StyledModalWithAnimations
import styled from 'styled-components'
import Pagination from './Pagination'
const StyledPagination = styled(Pagination)`
display: flex;
justify-content: center;
& > ul {
display: inline-block;
padding-left: 0;
border-radius: 4px;
user-select: none;
}
& > ul > li {
display:inline;
}
& > ul > li > a {
cursor: pointer;
}
& > ul > li > a, & > ul > li > span {
cursor: pointer;
position: relative;
float: left;
padding: 6px 12px;
margin-left: -1px;
line-height: 1.42857143;
color: hsla(216, 40%, 55%, 1);
text-decoration: none;
background-color: white;
border: 1px solid rgb(231, 231, 231);
}
& > ul > li:first-child > a, & ul > li:first-child > span {
margin-left: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
& > ul > li:last-child > a, & > ul > li:last-child > span {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
& > ul > li > a:focus, & > ul > li > a:hover, & > ul > li > span:focus, & > ul > li > span:hover {
z-index: 3;
color: rgb(66, 133, 244);
background-color: rgb(240, 240, 240);
border-color: rgb(231, 231, 231);
}
& > ul > .active > a,
& > ul > .active > a:focus,
& > ul > .active > a:hover,
& > ul > .active > span,
& > ul > .active > span:focus,
& > ul > .active > span:hover {
z-index: 2;
color: white;
cursor: default;
background-color: hsla(216, 40%, 55%, 1);
border-color: hsla(216, 40%, 55%, 1);
}
& > ul > .disabled > a,
& > ul > .disabled > a:focus,
& > ul > .disabled > a:hover,
& > ul > .disabled > span,
& > ul > .disabled > span:focus,
& > ul > .disabled > span:hover {
color: rgb(200, 200, 200);
cursor: not-allowed;
background-color: white;
border-color: rgb(231, 231, 231);
}
`
export default StyledPagination
import styled from 'styled-components'
import { width, height, lineHeight } from 'styled-system'
export const TableContainer = styled.div`
border: none;
@media (min-width: 768px) {
border: 1px solid hsla(216, 20%, 48%, 0.12);
}
`
export const Table = styled.div`
display: block;
&:last-child {
border-bottom: none;
}
@media (min-width: 768px) {
width: 100%;
display: table;
color: hsla(0, 0%, 0%, 0.65);
}
`
export const TableHeader = styled.div`
display: block;
@media (min-width: 768px) {
font-weight: 600;
color: hsla(0, 0%, 0%, 0.85);
background: hsla(216, 20%, 95%, 1);
}
`
export const TableRow = styled.div`
display: block;
border-bottom: 1px solid hsla(216, 20%, 91%, 1);
@media (min-width: 768px) {
display: table-row;
border-bottom: none;
box-shadow: inset 0 -1px 0 0 hsla(216, 20%, 48%, 0.12);
&:hover {
background-color: hsla(216, 20%, 96%, 1);
}
}
`
export const TableHeaderRow = styled(TableRow)`
border-bottom: none;
@media (min-width: 768px) {
font-weight: 600;
color: hsla(0, 0%, 0%, 0.85);
background: hsla(216, 20%, 96%, 1);
}
`
export const TableCell = styled.div`
display: block;
margin-bottom: 10px;
line-height: 20px;
@media (max-width: 767px) {
&:before {
margin-bottom: 3px;
content: attr(data-title);
min-width: 98px;
font-size: 10px;
line-height: 1;
font-weight: bold;
text-transform: uppercase;
color: hsla(0, 0%, 60%, 1);
display: block;
}
}
@media (min-width: 768px) {
display: table-cell;
vertical-align: middle;
font-size: inherit;
padding-right: 16px;
&:first-child {
height: 40px;
padding-left: 16px;
}
}
${width}
${height}
${lineHeight}
`
export const TableHeaderCell = styled(TableCell)`
margin-bottom: 0;
@media (max-width: 767px) {
display: none;
}
`
export default Table
import { ThemeProvider } from 'styled-components'
export default ThemeProvider
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment