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
+}