Skip to content
This repository has been archived by the owner on Feb 28, 2020. It is now read-only.

Commit

Permalink
Add a modal to alert failed login attempts. #1
Browse files Browse the repository at this point in the history
  • Loading branch information
thebearingedge committed Mar 15, 2019
1 parent f4723f7 commit 1b63fbe
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 18 deletions.
33 changes: 33 additions & 0 deletions client/components/modal/alert.jsx
@@ -0,0 +1,33 @@
import { Component, createRef } from 'react'
import { ModalHeader, ModalBody, Button } from 'reactstrap'

export default class Alert extends Component {
constructor(props) {
super(props)
this.dismissButtonRef = createRef()
}
handleDismissClick = () => {
this.props.close()
}
componentDidMount() {
setTimeout(() => this.dismissButtonRef.current.focus())
}
render() {
const { message } = this.props
const { handleDismissClick, dismissButtonRef } = this
return (
<>
<ModalHeader className="py-4 justify-content-center">
{ message }
</ModalHeader>
<ModalBody className="d-flex justify-content-center">
<Button
color="secondary"
innerRef={dismissButtonRef}
name="dismiss-modal"
onClick={handleDismissClick}>Dismiss</Button>
</ModalBody>
</>
)
}
}
1 change: 1 addition & 0 deletions client/components/modal/index.js
@@ -0,0 +1 @@
export { default as Alert } from './alert'
25 changes: 21 additions & 4 deletions client/containers/authenticate.jsx
@@ -1,11 +1,13 @@
import { FORM_ERROR } from 'final-form'
import { Component } from 'react'
import { LoginForm } from '../components/login'
import { Alert } from '../components/modal'
import { Modal } from '../containers'
import { withServices } from '../services/context'

class Authenticate extends Component {
handleLogin = async credentials => {
const { api, router, session } = this.props.services
const { api, modal, router, session } = this.props
const { status, data } = await api.post('/auth/login', credentials, {
validateStatus: status => [201, 400, 401].includes(status)
})
Expand All @@ -14,19 +16,34 @@ class Authenticate extends Component {
session.start(data)
router.push('/')
return
default:
case 400:
throw new Error('NOT IMPLEMENTED')
case 401:
modal.open({
render({ close }) {
return (
<Alert
close={close}
message="Incorrect username or password." />
)
}
})
return { [FORM_ERROR]: 'Invalid login.' }
}
}
render() {
return (
<LoginForm onSubmit={this.handleLogin}/>
<>
<LoginForm onSubmit={this.handleLogin} />
<Modal />
</>
)
}
}

export default withServices(Authenticate, ({ api, router, session }) => ({
export default withServices(Authenticate, ({ api, router, modal, session }) => ({
api,
modal,
router,
session
}))
1 change: 1 addition & 0 deletions client/containers/index.js
@@ -1 +1,2 @@
export { default as Authenticate } from './authenticate'
export { default as Modal } from './modal'
48 changes: 48 additions & 0 deletions client/containers/modal.jsx
@@ -0,0 +1,48 @@
import noop from 'lodash/noop'
import { Component } from 'react'
import { Modal as ReactstrapModal } from 'reactstrap'
import { withServices } from '../services'

class Modal extends Component {
constructor(props) {
super(props)
this.state = {
isOpen: false,
response: null
}
this.unsubscribe = noop
}
onClosed = () => {
this.props.modal.close(this.state.response)
}
close = response => {
this.setState({ isOpen: false, response })
}
componentDidMount() {
this.unsubscribe = this.props.modal.subscribe(({ isOpen }) => {
this.setState({ isOpen })
})
}
componentWillUnmount() {
this.unsubscribe()
}
render() {
const { isOpen } = this.state
const { modal: { render } } = this.props
const { close, onClosed, onOpened } = this
return (
<ReactstrapModal
autoFocus
centered
isOpen={isOpen}
onClosed={onClosed}
onOpened={onOpened}>
{ render({ close }) }
</ReactstrapModal>
)
}
}

export default withServices(Modal, ({ modal }) => ({
modal
}))
1 change: 1 addition & 0 deletions client/lib/index.js
@@ -1,2 +1,3 @@
export { default as authorize } from './authorize'
export { default as isServer } from './is-server'
export { default as Model } from './model'
29 changes: 29 additions & 0 deletions client/lib/model.js
@@ -0,0 +1,29 @@
export default class Model {
constructor() {
this._listeners = []
this._model = this.init
}
get init() {
return {}
}
subscribe(listener) {
const listenerIndex = this._listeners.push(listener) - 1
listener(this._model)
return () => {
this._listeners.splice(listenerIndex, 1)
}
}
update(updater) {
switch (typeof updater) {
case 'function':
this._model = { ...this._model, ...updater(this._model) }
break
case 'object':
this._model = { ...this._model, ...updater }
break
default:
throw new Error('updater passed to StateModel.setState must be a Function or an Object.')
}
this._listeners.forEach(listener => listener(this._model))
}
}
7 changes: 4 additions & 3 deletions client/pages/_app.jsx
Expand Up @@ -3,7 +3,7 @@ import Router from 'next/router'
import NProgress from 'nprogress'
import App, { Container } from 'next/app'
import { isServer } from '../lib'
import { Provider, initApi, initSession } from '../services'
import { Provider, initApi, initModal, initSession } from '../services'

export default class extends App {
static async getInitialProps({ Component, router, ctx }) {
Expand All @@ -22,6 +22,7 @@ export default class extends App {
constructor(props, ...args) {
super(props, ...args)
this.api = initApi()
this.modal = initModal()
this.session = initSession(props.user)
}
componentDidMount() {
Expand All @@ -32,12 +33,12 @@ export default class extends App {
}
render() {
const { router } = Router
const { api, session } = this
const { api, modal, session } = this
const { Component, pageProps = {} } = this.props
return (
<>
<Container>
<Provider value={{ api, router, session }}>
<Provider value={{ api, modal, router, session }}>
<Component {...pageProps}/>
</Provider>
</Container>
Expand Down
9 changes: 1 addition & 8 deletions client/pages/index.jsx
@@ -1,15 +1,8 @@
import { authorize } from '../lib'
import { Consumer } from '../services'

export default function Index() {
return (
<Consumer>
{ value => {
// eslint-disable-next-line no-console
console.log(Object.keys(value))
return <h1>Hello, World!</h1>
}}
</Consumer>
<h1>Hello, World!</h1>
)
}

Expand Down
7 changes: 7 additions & 0 deletions client/pages/login.jsx
Expand Up @@ -31,3 +31,10 @@ export default function Login() {
</Container>
)
}

Login.getInitialProps = ({ res, router, isServer, session }) => {
if (!session.user) return
isServer
? res.redirect('/')
: router.replace('/')
}
6 changes: 3 additions & 3 deletions client/services/context.jsx
Expand Up @@ -3,17 +3,17 @@ import getDisplayName from 'react-display-name'

export const { Provider, Consumer } = createContext({})

export const withServices = (Component, pickServices) => {
export const withServices = (Component, selectServices) => {
return class WithServices extends PureComponent {
static displayName = `WithServices(${getDisplayName(Component)})`
services = null
render() {
return (
<Consumer>
{ services => {
this.services = this.services || pickServices(services)
this.services = this.services || selectServices(services)
return (
<Component services={this.services} {...this.props} />
<Component {...this.services} {...this.props} />
)
}}
</Consumer>
Expand Down
1 change: 1 addition & 0 deletions client/services/index.js
@@ -1,3 +1,4 @@
export * from './context'
export { default as initApi } from './api'
export { default as initModal } from './modal'
export { default as initSession } from './session'
36 changes: 36 additions & 0 deletions client/services/modal.js
@@ -0,0 +1,36 @@
import noop from 'lodash/noop'
import { Model, isServer } from '../lib'

class Modal extends Model {
get init() {
return {
onClose: noop,
isOpen: false,
render: noop
}
}
get render() {
return this._model.render
}
open({ onClose = noop, render = noop }) {
if (this._model.isOpen) return
this.update({
isOpen: true,
onClose,
render
})
}
close(response) {
if (!this._model.isOpen) return
this._model.onClose(response)
this.update(this.init)
}
}

let modal

export default function initModal() {
if (isServer) return new Modal()
modal = modal || new Modal()
return modal
}

0 comments on commit 1b63fbe

Please sign in to comment.