diff --git a/package-lock.json b/package-lock.json index c2e00e0..5590c05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2548,6 +2548,11 @@ "supports-color": "^5.3.0" } }, + "change-emitter": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz", + "integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=" + }, "chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -5737,6 +5742,11 @@ "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" }, + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + }, "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", @@ -8027,6 +8037,11 @@ "minimist": "0.0.8" } }, + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -11259,6 +11274,19 @@ "util.promisify": "^1.0.0" } }, + "recompose": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/recompose/-/recompose-0.30.0.tgz", + "integrity": "sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w==", + "requires": { + "@babel/runtime": "^7.0.0", + "change-emitter": "^0.1.2", + "fbjs": "^0.8.1", + "hoist-non-react-statics": "^2.3.1", + "react-lifecycles-compat": "^3.0.2", + "symbol-observable": "^1.0.4" + } + }, "recursive-readdir": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", @@ -12923,6 +12951,11 @@ "util.promisify": "~1.0.0" } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "symbol-tree": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", diff --git a/package.json b/package.json index 2704d4d..52f0618 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "jest-pnp-resolver": "1.0.1", "jest-resolve": "23.6.0", "mini-css-extract-plugin": "0.4.3", + "moment": "^2.22.2", "optimize-css-assets-webpack-plugin": "5.0.1", "pnp-webpack-plugin": "1.1.0", "postcss-flexbugs-fixes": "4.1.0", @@ -46,6 +47,7 @@ "react-dev-utils": "^6.0.5", "react-dom": "^16.6.0", "react-emotion": "^9.2.12", + "recompose": "^0.30.0", "resolve": "1.8.1", "sass-loader": "7.1.0", "style-loader": "0.23.0", diff --git a/src/App.js b/src/App.js index c1ee2ea..b360fe9 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import { Router } from "@reach/router"; -import { Routes } from './constants'; +import { routes } from './constants'; import { AuthProvider } from './providers/Auth'; import { Home, @@ -13,9 +13,9 @@ class App extends Component { return ( - - - + + + ); diff --git a/src/constants/cookies.js b/src/constants/cookies.js new file mode 100644 index 0000000..9f40544 --- /dev/null +++ b/src/constants/cookies.js @@ -0,0 +1 @@ +export const OAUTH_TOKEN_COOKIE = 'meteorite-oauth-token'; diff --git a/src/constants/index.js b/src/constants/index.js index 8622a69..4fb100d 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -1 +1 @@ -export {default as Routes} from './routes'; +export {default as routes} from './routes'; diff --git a/src/pages/Home.js b/src/pages/Home.js index a2d861b..f9db4b1 100644 --- a/src/pages/Home.js +++ b/src/pages/Home.js @@ -1,11 +1,41 @@ import React from 'react'; import { Link } from "@reach/router"; -import { Routes } from '../constants'; +import { compose } from 'recompose'; +import { withAuthProvider } from '../providers/Auth'; +import { withCookiesProvider } from '../providers/Cookies'; +import { routes } from '../constants'; +import { OAUTH_TOKEN_COOKIE } from '../constants/cookies'; -export default props => ( -
- Home! - login - inbox -
+class HomePage extends React.Component { + render () { + return ( +
+ Home! + {this.props.authApi.token ? ( + +

inbox

+

{ + // Remove cookie and invalidate token on client. + this.props.cookiesApi.removeCookie(OAUTH_TOKEN_COOKIE); + this.props.authApi.invalidateToken(); + }} + > + soft logout +

+
+ ) : ( +

login

+ )} +
+ ); + } +}; + +const enhance = compose( + withAuthProvider, + withCookiesProvider ); + +export default enhance(HomePage); diff --git a/src/pages/Inbox.js b/src/pages/Inbox.js index 9285bd0..5907059 100644 --- a/src/pages/Inbox.js +++ b/src/pages/Inbox.js @@ -1,10 +1,31 @@ import React from 'react'; -import { Link } from "@reach/router"; -import { Routes } from '../constants'; +import { Link, Redirect } from "@reach/router"; +import { compose } from 'recompose'; +import { withNotificationsProvider } from '../providers/Notifications'; +import { withAuthProvider } from '../providers/Auth'; +import { routes } from '../constants'; -export default props => ( -
- Inbox - home -
+class InboxPage extends React.Component { + render () { + if (!this.props.authApi.token) { + return + } + + return ( +
+ Inbox + home + +
+ ); + } +}; + +const enhance = compose( + withAuthProvider, + withNotificationsProvider ); + +export default enhance(InboxPage); diff --git a/src/pages/Login/Scene.js b/src/pages/Login/Scene.js index ae80d21..8b06f3d 100644 --- a/src/pages/Login/Scene.js +++ b/src/pages/Login/Scene.js @@ -1,11 +1,11 @@ import React from 'react'; import { Link } from "@reach/router"; import styled from 'react-emotion'; -import { Routes } from '../../constants'; +import { routes } from '../../constants'; import { AuthenticationButton } from '../../components/buttons'; -const Container = styled.div({ - background: 'red', +const Container = styled('div')({ + background: '#f4f4f4', width: '100%', height: 100 }); @@ -13,8 +13,8 @@ const Container = styled.div({ export default function Scene ({ loading, error, loggedOut, ...props }) { return ( - home -

+ home +

{error ? (
error, try again? @@ -25,9 +25,9 @@ export default function Scene ({ loading, error, loggedOut, ...props }) { ) : loggedOut ? ( ) : ( - logged in!! + logged in! )} -

+
); } diff --git a/src/pages/Login/TokenHandler.js b/src/pages/Login/TokenHandler.js index 3964d78..e8159aa 100644 --- a/src/pages/Login/TokenHandler.js +++ b/src/pages/Login/TokenHandler.js @@ -1,8 +1,5 @@ import React from 'react'; import qs from 'query-string'; -import { Routes } from '../../constants'; -import { AuthenticationButton } from '../../components/buttons'; -import { AuthConsumer } from '../../providers/Auth'; export default class TokenHandler extends React.Component { componentDidMount() { diff --git a/src/pages/Login/index.js b/src/pages/Login/index.js index 91daee5..6ad824e 100644 --- a/src/pages/Login/index.js +++ b/src/pages/Login/index.js @@ -1,38 +1,53 @@ import React from 'react'; -import { Link } from "@reach/router"; -import { Routes } from '../../constants'; -import { AuthenticationButton } from '../../components/buttons'; -import { AuthConsumer } from '../../providers/Auth'; +import { Redirect } from '@reach/router'; +import { compose } from 'recompose'; +import { withAuthProvider } from '../../providers/Auth'; +import { withNotificationsProvider } from '../../providers/Notifications'; +import { withCookiesProvider } from '../../providers/Cookies'; import TokenHandler from './TokenHandler'; import Scene from './Scene'; +import { routes } from '../../constants'; -export default class LoginPage extends React.Component { +class LoginPage extends React.Component { state = { loading: false, error: null } - onSetLoading = loading => this.setState({ loading }); - onSetError = error => this.setState({ error }); + onSetLoading = loading => { + this.setState({ loading }); + } + + onSetError = error => { + this.setState({ error }); + } render () { + if (this.props.authApi.token) { + return + } + return ( - - {({ token, setToken }) => ( - - - - - )} - + + + + ); } } + +const enhance = compose( + withAuthProvider, + withNotificationsProvider, + withCookiesProvider +); + +export default enhance(LoginPage); diff --git a/src/providers/Auth.js b/src/providers/Auth.js index ed53f2d..33f45ba 100644 --- a/src/providers/Auth.js +++ b/src/providers/Auth.js @@ -1,21 +1,29 @@ import React from 'react'; +import { withCookiesProvider } from './Cookies'; +import { OAUTH_TOKEN_COOKIE } from '../constants/cookies'; const {Provider, Consumer} = React.createContext('foo'); class AuthProvider extends React.Component { state = { - token: null + token: this.props.cookiesApi.getCookie(OAUTH_TOKEN_COOKIE) } setToken = token => { + this.props.cookiesApi.setCookie(OAUTH_TOKEN_COOKIE, token); this.setState({ token }); } + invalidateToken = () => { + this.setState({ token: null }); + } + render () { return ( {this.props.children} @@ -23,7 +31,16 @@ class AuthProvider extends React.Component { } } +const withAuthProvider = WrappedComponent => props => ( + + {authApi => } + +); + +const AuthProviderWithCookies = withCookiesProvider(AuthProvider); + export { - AuthProvider, - Consumer as AuthConsumer + AuthProviderWithCookies as AuthProvider, + Consumer as AuthConsumer, + withAuthProvider }; diff --git a/src/providers/Cookies.js b/src/providers/Cookies.js new file mode 100644 index 0000000..9930094 --- /dev/null +++ b/src/providers/Cookies.js @@ -0,0 +1,61 @@ +import React from 'react'; +import moment from 'moment'; + +class CookiesProvider extends React.Component { + state = { + cookies: {} + } + + componentWillMount () { + this.hydrateCookies(); + } + + mapifyCookies = () => { + const cookiesPairs = document.cookie.split(';').map(cookie => cookie.trim()); + const cookies = cookiesPairs.reduce((map, cookiePair) => { + const [key, value] = cookiePair.split('='); + map[key] = value; + return map; + }, {}); + return cookies; + } + + hydrateCookies = () => { + const cookies = this.mapifyCookies(); + this.setState({ cookies }); + } + + setCookie = (name, value) => { + document.cookie = `${name}=${value}`; + this.hydrateCookies() + } + + getCookie = name => { + return this.state.cookies[name]; + } + + removeCookie = name => { + document.cookie = `${name}=''; expires=${moment().subtract(1, 'day').toString()}`; + this.hydrateCookies(); + } + + render () { + return this.props.children({ + ...this.state, + setCookie: this.setCookie, + getCookie: this.getCookie, + removeCookie: this.removeCookie + }); + } +} + +const withCookiesProvider = WrappedComponent => props => ( + + {cookiesApi => } + +); + +export { + CookiesProvider, + withCookiesProvider +}; diff --git a/src/providers/Notifications.js b/src/providers/Notifications.js new file mode 100644 index 0000000..b5b9871 --- /dev/null +++ b/src/providers/Notifications.js @@ -0,0 +1,59 @@ +import React from 'react'; +import { AuthConsumer } from './Auth'; + +const BASE_GITHUB_API_URL = 'https://api.github.com'; + +class NotificationsProvider extends React.Component { + state = { + loading: false, + error: null + } + + getNotifications = () => { + if (!this.props.token) { + console.error('unauthenitcated!') + return false; + } + + console.warn(this.props.token) + + this.setState({ loading: true }); + fetch(`${BASE_GITHUB_API_URL}/notifications`, { + method: 'GET', + headers: { + 'Authorization': `token ${this.props.token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }).then(response => response.json()) + .then(data => { + console.warn(data); + }) + .catch(error => this.setState({ error })) + .finally(() => this.setState({ loading: false })); + } + + render () { + return this.props.children({ + ...this.state, + getNotifications: this.getNotifications + }); + } +} + +const withNotificationsProvider = WrappedComponent => props => ( + + {({ token }) => ( + + {(notificationsApi) => ( + + )} + + )} + +); + +export { + NotificationsProvider, + withNotificationsProvider +}; diff --git a/src/styles/index.css b/src/styles/index.css index cee5f34..a550303 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,7 +1,10 @@ +@import url('https://rsms.me/inter/inter-ui.css'); + body { + background: #f4f4f4; margin: 0; padding: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + font-family: 'Inter UI', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased;