Skip to content

Commit

Permalink
Add tabs for mobile app
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanhogan committed Feb 19, 2020
1 parent fb52547 commit 8b018cc
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 52 deletions.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
Expand Down
17 changes: 11 additions & 6 deletions src/components/header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -37,18 +38,22 @@ class Header extends Component {

render () {
const { hidden, scroll } = this.state
const { tabs } = this.props

return window === window.top ? (
<header
ref='header'
className={['header', hidden && 'hidden', scroll > 20 && 'scrolled']
className={['header', hidden && 'hidden', scroll > 20 && 'scrolled', tabs.length && 'header-tabs']
.filter(Boolean)
.join(' ')}>
<Link className='logo' to='/'>
Home
</Link>
<Search />
<Settings />
<nav className='header-nav'>
<Link className='logo' to='/'>
Home
</Link>
<Search />
<Settings />
</nav>
<Tabs tabs={tabs} />
</header>
) : null
}
Expand Down
75 changes: 57 additions & 18 deletions src/components/page/index.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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: '',
Expand All @@ -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 })

Expand Down Expand Up @@ -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 (
<div className='container'>
{sections.length ? <Sections sections={sections} /> : null}
{loading && <Loading {...this.props} />}
{content ? (
<div className='page'>
<h1
className='page-title'
dangerouslySetInnerHTML={{ __html: title }}
/>
<div dangerouslySetInnerHTML={{ __html: content }} />
</div>
) : error ? (
<div>
<h1 className='page-title'>Error</h1>
<div>{error}</div>
</div>
) : null}
{loading ? <Loading {...this.props} /> : (
<Fragment>
{sections.length ? <Sections sections={sections} /> : null}
{content ? (
<div className='page'>
<h1
className='page-title'
dangerouslySetInnerHTML={{ __html: title }}
/>
<div onClick={this.handleClick} dangerouslySetInnerHTML={{ __html: content }} />
</div>
) : error ? (
<div>
<h1 className='page-title'>Error</h1>
<div>{error}</div>
</div>
) : null}
</Fragment>
)}
</div>
)
}
}

export default withRouter(Page)
const mapStateToProps = ({ tabs }) => ({ tabs })

export default compose(
connect(mapStateToProps, { addTab }),
withRouter
)(Page)
44 changes: 44 additions & 0 deletions src/components/tabs/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<nav className='tabs'>
{tabs.map(tab => {
const handleClick = e => {
window.scrollTo(0, 0)
return setActive(tab.id)
}

const handleRemove = e => {
e.preventDefault()
return removeTab(tab.id)
}
const isActive = active === tab.id

return (
<div key={tab.id} className={['tab-link', isActive && 'tab-link-active'].filter(Boolean).join(' ')}>
<Link
to={tab.path}
onClick={handleClick}
>
{tab.name}
</Link>
<button className='remove-tab-link' onClick={handleRemove}>Remove tab</button>
</div>
)
})}
</nav>
)
}

return null
}

export default connect(null, { removeTab })(Tabs)
9 changes: 5 additions & 4 deletions src/routes/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,22 @@ class App extends Component {
}

render () {
const { children, classNames } = this.props
const { children, classNames, tabs } = this.props

return (
<div className={`settings-wrapper ${classNames.join(' ')}`}>
<Header />
<Header tabs={tabs} />
{children}
<Footer />
</div>
)
}
}

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)
4 changes: 3 additions & 1 deletion src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions src/store/tabs/index.js
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 8b018cc

Please sign in to comment.