From 8b018cc2199661d3ffc9421ee809e7a20c29f3e4 Mon Sep 17 00:00:00 2001 From: Rowan Hogan Date: Wed, 19 Feb 2020 21:06:32 +1000 Subject: [PATCH] Add tabs for mobile app --- package.json | 7 +-- public/manifest.json | 2 +- src/components/header/index.js | 17 ++++--- src/components/page/index.js | 75 ++++++++++++++++++++++------- src/components/tabs/index.js | 44 +++++++++++++++++ src/routes/app/index.js | 9 ++-- src/store/index.js | 4 +- src/store/tabs/index.js | 34 +++++++++++++ src/styles/_layout.scss | 87 ++++++++++++++++++++++++++++++++-- src/styles/styles.css | 76 +++++++++++++++++++++++++++-- yarn.lock | 52 ++++++++++++++++---- 11 files changed, 355 insertions(+), 52 deletions(-) create mode 100644 src/components/tabs/index.js create mode 100644 src/store/tabs/index.js diff --git a/package.json b/package.json index a61b112..2627257 100644 --- a/package.json +++ b/package.json @@ -11,16 +11,17 @@ "lodash": "^4.17.13", "node-sass-chokidar": "^1.3.0", "npm-run-all": "^4.1.3", - "react": "^16.4.0", + "react": "^16.8.0", "react-click-outside": "^3.0.1", - "react-dom": "^16.4.0", + "react-dom": "^16.8.0", "react-fastclick": "^3.0.2", "react-redux": "^5.0.7", "react-router-dom": "^4.2.2", "react-scripts": "1.1.4", "redux": "^4.0.0", "redux-logger": "^3.0.6", - "redux-thunk": "^2.3.0" + "redux-thunk": "^2.3.0", + "uuid": "^3.4.0" }, "scripts": { "start": "npm-run-all -p watch-css start-js", diff --git a/public/manifest.json b/public/manifest.json index b9a260b..5058187 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -8,7 +8,7 @@ "type": "image/x-icon" } ], - "start_url": "./index.html", + "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" diff --git a/src/components/header/index.js b/src/components/header/index.js index 2e087ed..a50b5cb 100644 --- a/src/components/header/index.js +++ b/src/components/header/index.js @@ -2,6 +2,7 @@ import React, { Component } from 'react' import Search from '../search' import Settings from '../settings' +import Tabs from '../tabs' import { Link } from 'react-router-dom' class Header extends Component { @@ -37,18 +38,22 @@ class Header extends Component { render () { const { hidden, scroll } = this.state + const { tabs } = this.props return window === window.top ? ( ) : null } diff --git a/src/components/page/index.js b/src/components/page/index.js index 58b29e9..e391176 100644 --- a/src/components/page/index.js +++ b/src/components/page/index.js @@ -1,6 +1,9 @@ -import React, { Component } from 'react' +import React, { Component, Fragment } from 'react' +import { compose } from 'redux' +import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' import { fetchPage } from '../../lib/api' +import { addTab } from '../../store/tabs' import Loading from '../loading' import Sections from '../sections' @@ -9,6 +12,7 @@ class Page extends Component { constructor (props) { super(props) this.fetchPage = this.fetchPage.bind(this) + this.handleClick = this.handleClick.bind(this) this.state = { loading: false, title: '', @@ -22,6 +26,12 @@ class Page extends Component { return this.fetchPage(title) } + componentDidUpdate ({ title }) { + if (title !== this.props.title) { + return this.fetchPage(this.props.title) + } + } + fetchPage (title) { this.setState({ loading: true }) @@ -53,30 +63,59 @@ class Page extends Component { ) } + isWebApp () { + return ('standalone' in window.navigator) && window.navigator.standalone + } + + handleClick (e) { + const { tabs } = this.props + const { title } = this.state + + if (e.target.nodeName === 'A' && this.isWebApp()) { + e.preventDefault() + + const url = e.target.href.replace(window.location.origin, '') + + if (tabs.length === 0) { + this.props.addTab(title, `/${encodeURIComponent(title.replace(/ /g, '_'))}`) + } + + this.props.addTab(decodeURIComponent(url.split('/')[1].replace(/_/g, ' ')), url) + } + } + render () { const { content, error, loading, sections, title } = this.state return (
- {sections.length ? : null} - {loading && } - {content ? ( -
-

-
-
- ) : error ? ( -
-

Error

-
{error}
-
- ) : null} + {loading ? : ( + + {sections.length ? : null} + {content ? ( +
+

+
+
+ ) : error ? ( +
+

Error

+
{error}
+
+ ) : null} + + )}

) } } -export default withRouter(Page) +const mapStateToProps = ({ tabs }) => ({ tabs }) + +export default compose( + connect(mapStateToProps, { addTab }), + withRouter +)(Page) diff --git a/src/components/tabs/index.js b/src/components/tabs/index.js new file mode 100644 index 0000000..8b2be06 --- /dev/null +++ b/src/components/tabs/index.js @@ -0,0 +1,44 @@ +import React, { useState } from 'react' +import { connect } from 'react-redux' +import { removeTab } from '../../store/tabs' + +import { Link } from 'react-router-dom' + +const Tabs = ({ tabs, removeTab }) => { + if (tabs.length) { + const [active, setActive] = useState(tabs[0].id) + + return ( + + ) + } + + return null +} + +export default connect(null, { removeTab })(Tabs) diff --git a/src/routes/app/index.js b/src/routes/app/index.js index 876b94e..ae5358a 100644 --- a/src/routes/app/index.js +++ b/src/routes/app/index.js @@ -20,11 +20,11 @@ class App extends Component { } render () { - const { children, classNames } = this.props + const { children, classNames, tabs } = this.props return (
-
+
{children}
@@ -32,9 +32,10 @@ class App extends Component { } } -const mapStateToProps = ({ settings }) => ({ +const mapStateToProps = ({ settings, tabs }) => ({ darkMode: settings.darkMode, - classNames: Object.keys(settings).filter(setting => settings[setting]) + classNames: Object.keys(settings).filter(setting => settings[setting]), + tabs: tabs }) export default connect(mapStateToProps)(App) diff --git a/src/store/index.js b/src/store/index.js index 5b94545..593e519 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -3,13 +3,15 @@ import thunk from 'redux-thunk' import createLogger from 'redux-logger' import settings from './settings' +import tabs from './tabs' const initialState = {} export default () => createStore( combineReducers({ - settings + settings, + tabs }), initialState, applyMiddleware(thunk, createLogger) diff --git a/src/store/tabs/index.js b/src/store/tabs/index.js new file mode 100644 index 0000000..716ac96 --- /dev/null +++ b/src/store/tabs/index.js @@ -0,0 +1,34 @@ +import uuid from 'uuid/v4' + +export const addTab = (name, path) => dispatch => + dispatch({ + type: 'TABS/ADD', + payload: { name, path } + }) + +export const removeTab = id => dispatch => + dispatch({ + type: 'TABS/REMOVE', + payload: { id } + }) + +export default (state = [], action) => { + switch (action.type) { + case 'TABS/ADD': { + const { name, path } = action.payload + + return [ + ...state, + { name, path, id: uuid() } + ] + } + + case 'TABS/REMOVE': { + const { id } = action.payload + return state.filter(tab => tab.id !== id) + } + + default: + return state + } +} diff --git a/src/styles/_layout.scss b/src/styles/_layout.scss index 1d9b3c4..dd2b5de 100644 --- a/src/styles/_layout.scss +++ b/src/styles/_layout.scss @@ -39,14 +39,11 @@ .header { position: sticky; top: 0; - height: $header-height; + min-height: $header-height; background-color: rgba(white, 0.975); border-bottom: thin solid rgba(gray, 0.25); transition: all 350ms ease; - display: flex; - justify-content: space-between; z-index: 5; - padding-right: $header-height - 0.5rem; &.scrolled { box-shadow: 0 0 1rem rgba(gray, 0.1); @@ -63,6 +60,87 @@ } } +.header-tabs { + border-bottom-width: 0; +} + +.header-nav { + display: flex; + justify-content: space-between; + padding-right: $header-height - 0.5rem; +} + +.tabs { + display: flex; + border-top: thin solid rgba(128, 128, 128, 0.25); + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; + + .tab-link { + position: relative; + line-height: 3.5em; + font-size: 0.75em; + font-family: sans-serif; + border-right: thin solid rgba(128, 128, 128, 0.5); + border-bottom: thin solid rgba(128, 128, 128, 0.5); + font-weight: bold; + text-align: center; + text-overflow: ellipsis; + overflow: hidden; + flex: 1 1 auto; + min-width: 2em; + width: 100%; + transition: all ease 300ms; + opacity: 0.5; + + a { + color: currentColor !important; + text-decoration: none; + display: block; + padding: 0 1.5rem 0 1rem; + } + } + + .tab-link.tab-link-active { + border-bottom-color: transparent; + border-right-color: rgba(128, 128, 128, 0.25); + opacity: 1; + } +} + +.remove-tab-link { + position: absolute; + top: 50%; + right: 0; + width: 2rem; + height: 2rem; + margin-top: -1rem; + line-height: 1.8rem; + padding: 0; + text-indent: -9999px; + text-align: center; + overflow: hidden; + outline: 0; + opacity: 0.75; + cursor: pointer; + color: currentColor; + + &:before { + content: "\00D7"; + position: absolute; + top: 0; + left: 0; + right: 0; + text-indent: 0; + font-size: 1.25rem; + } +} + +.search { + flex: 1; +} + .search-form { position: relative; @@ -283,6 +361,7 @@ $bar-height: 0.166rem; z-index: 1000; max-width: calc(100vw - 2rem); max-height: calc(100vh - 2rem); + width: 25rem; overflow: auto; text-align: left; animation: modal 300ms ease forwards; diff --git a/src/styles/styles.css b/src/styles/styles.css index d2fe27c..ea964ed 100644 --- a/src/styles/styles.css +++ b/src/styles/styles.css @@ -109,14 +109,11 @@ table { .header { position: sticky; top: 0; - height: 3.25rem; + min-height: 3.25rem; background-color: rgba(255, 255, 255, 0.975); border-bottom: thin solid rgba(128, 128, 128, 0.25); transition: all 350ms ease; - display: flex; - justify-content: space-between; - z-index: 5; - padding-right: 2.75rem; } + z-index: 5; } .header.scrolled { box-shadow: 0 0 1rem rgba(128, 128, 128, 0.1); border-color: transparent; } @@ -126,6 +123,74 @@ table { .header > div { flex: 1; } +.header-tabs { + border-bottom-width: 0; } + +.header-nav { + display: flex; + justify-content: space-between; + padding-right: 2.75rem; } + +.tabs { + display: flex; + border-top: thin solid rgba(128, 128, 128, 0.25); + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; } + .tabs .tab-link { + position: relative; + line-height: 3.5em; + font-size: 0.75em; + font-family: sans-serif; + border-right: thin solid rgba(128, 128, 128, 0.5); + border-bottom: thin solid rgba(128, 128, 128, 0.5); + font-weight: bold; + text-align: center; + text-overflow: ellipsis; + overflow: hidden; + flex: 1 1 auto; + min-width: 2em; + width: 100%; + transition: all ease 300ms; + opacity: 0.5; } + .tabs .tab-link a { + color: currentColor !important; + text-decoration: none; + display: block; + padding: 0 1.5rem 0 1rem; } + .tabs .tab-link.tab-link-active { + border-bottom-color: transparent; + border-right-color: rgba(128, 128, 128, 0.25); + opacity: 1; } + +.remove-tab-link { + position: absolute; + top: 50%; + right: 0; + width: 2rem; + height: 2rem; + margin-top: -1rem; + line-height: 1.8rem; + padding: 0; + text-indent: -9999px; + text-align: center; + overflow: hidden; + outline: 0; + opacity: 0.75; + cursor: pointer; + color: currentColor; } + .remove-tab-link:before { + content: "\00D7"; + position: absolute; + top: 0; + left: 0; + right: 0; + text-indent: 0; + font-size: 1.25rem; } + +.search { + flex: 1; } + .search-form { position: relative; } .search-form input { @@ -290,6 +355,7 @@ table { z-index: 1000; max-width: calc(100vw - 2rem); max-height: calc(100vh - 2rem); + width: 25rem; overflow: auto; text-align: left; animation: modal 300ms ease forwards; diff --git a/yarn.lock b/yarn.lock index 18cd2a5..ed17555 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4217,6 +4217,10 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.9.1: version "3.11.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" @@ -4537,6 +4541,12 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3 dependencies: js-tokens "^3.0.0" +loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -5781,6 +5791,14 @@ prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0: loose-envify "^1.3.1" object-assign "^4.1.1" +prop-types@^15.6.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + proxy-addr@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341" @@ -5941,14 +5959,14 @@ react-dev-utils@^5.0.1: strip-ansi "3.0.1" text-table "0.2.0" -react-dom@^16.4.0: - version "16.4.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.0.tgz#099f067dd5827ce36a29eaf9a6cdc7cbf6216b1e" +react-dom@^16.8.0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.12.0.tgz#0da4b714b8d13c2038c9396b54a92baea633fe11" dependencies: - fbjs "^0.8.16" loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.0" + prop-types "^15.6.2" + scheduler "^0.18.0" react-error-overlay@^4.0.0: version "4.0.0" @@ -5958,6 +5976,10 @@ react-fastclick@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/react-fastclick/-/react-fastclick-3.0.2.tgz#2994c60088cda90b0b2cbfac4b6e7c6bc73d6a3a" +react-is@^16.8.1: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" + react-redux@^5.0.7: version "5.0.7" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8" @@ -6037,14 +6059,13 @@ react-scripts@1.1.4: optionalDependencies: fsevents "^1.1.3" -react@^16.4.0: - version "16.4.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.4.0.tgz#402c2db83335336fba1962c08b98c6272617d585" +react@^16.8.0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83" dependencies: - fbjs "^0.8.16" loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.0" + prop-types "^15.6.2" read-pkg-up@^1.0.1: version "1.0.1" @@ -6492,6 +6513,13 @@ sax@^1.2.1, sax@^1.2.4, sax@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" +scheduler@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4" + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" @@ -7389,6 +7417,10 @@ uuid@^3.0.0, uuid@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" +uuid@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + validate-npm-package-license@^3.0.1: version "3.0.3" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338"