diff --git a/client/components/modal/alert.jsx b/client/components/modal/alert.jsx new file mode 100644 index 0000000..41f876f --- /dev/null +++ b/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 ( + <> + + { message } + + + + + + ) + } +} diff --git a/client/components/modal/index.js b/client/components/modal/index.js new file mode 100644 index 0000000..3f1b8ba --- /dev/null +++ b/client/components/modal/index.js @@ -0,0 +1 @@ +export { default as Alert } from './alert' diff --git a/client/containers/authenticate.jsx b/client/containers/authenticate.jsx index 3ad7cac..7e0c237 100644 --- a/client/containers/authenticate.jsx +++ b/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) }) @@ -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 ( + + ) + } + }) return { [FORM_ERROR]: 'Invalid login.' } } } render() { return ( - + <> + + + ) } } -export default withServices(Authenticate, ({ api, router, session }) => ({ +export default withServices(Authenticate, ({ api, router, modal, session }) => ({ api, + modal, router, session })) diff --git a/client/containers/index.js b/client/containers/index.js index 7342bda..26e37ef 100644 --- a/client/containers/index.js +++ b/client/containers/index.js @@ -1 +1,2 @@ export { default as Authenticate } from './authenticate' +export { default as Modal } from './modal' diff --git a/client/containers/modal.jsx b/client/containers/modal.jsx new file mode 100644 index 0000000..493b50c --- /dev/null +++ b/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 ( + + { render({ close }) } + + ) + } +} + +export default withServices(Modal, ({ modal }) => ({ + modal +})) diff --git a/client/lib/index.js b/client/lib/index.js index 80d2cde..d0020a6 100644 --- a/client/lib/index.js +++ b/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' diff --git a/client/lib/model.js b/client/lib/model.js new file mode 100644 index 0000000..6a7dbe1 --- /dev/null +++ b/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)) + } +} diff --git a/client/pages/_app.jsx b/client/pages/_app.jsx index 84b0f69..942a316 100644 --- a/client/pages/_app.jsx +++ b/client/pages/_app.jsx @@ -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 }) { @@ -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() { @@ -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 ( <> - + diff --git a/client/pages/index.jsx b/client/pages/index.jsx index 6e80525..9286d77 100644 --- a/client/pages/index.jsx +++ b/client/pages/index.jsx @@ -1,15 +1,8 @@ import { authorize } from '../lib' -import { Consumer } from '../services' export default function Index() { return ( - - { value => { - // eslint-disable-next-line no-console - console.log(Object.keys(value)) - return

Hello, World!

- }} -
+

Hello, World!

) } diff --git a/client/pages/login.jsx b/client/pages/login.jsx index d7c986c..8c18286 100644 --- a/client/pages/login.jsx +++ b/client/pages/login.jsx @@ -31,3 +31,10 @@ export default function Login() { ) } + +Login.getInitialProps = ({ res, router, isServer, session }) => { + if (!session.user) return + isServer + ? res.redirect('/') + : router.replace('/') +} diff --git a/client/services/context.jsx b/client/services/context.jsx index 662d91c..d6956df 100644 --- a/client/services/context.jsx +++ b/client/services/context.jsx @@ -3,7 +3,7 @@ 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 @@ -11,9 +11,9 @@ export const withServices = (Component, pickServices) => { return ( { services => { - this.services = this.services || pickServices(services) + this.services = this.services || selectServices(services) return ( - + ) }} diff --git a/client/services/index.js b/client/services/index.js index 9b7a539..35eb87c 100644 --- a/client/services/index.js +++ b/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' diff --git a/client/services/modal.js b/client/services/modal.js new file mode 100644 index 0000000..8ddaf93 --- /dev/null +++ b/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 +}