Merge branch 'navigation-state' into 'master'

Navigation state

See merge request bloodyhealth/drip!247
This commit is contained in:
Julia Friesel
2020-02-17 13:39:34 +00:00
16 changed files with 362 additions and 230 deletions
+66 -107
View File
@@ -1,140 +1,90 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { View, BackHandler } from 'react-native' import { View, BackHandler } from 'react-native'
import PropTypes from 'prop-types'
import { LocalDate } from 'js-joda'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { getDate } from '../slices/date' import { getDate, setDate } from '../slices/date'
import { getNavigation, navigate, goBack } from '../slices/navigation'
import Header from './header' import Header from './header'
import Menu from './menu' import Menu from './menu'
import Home from './home' import { viewsList } from './views'
import Calendar from './calendar' import { isSymptomView, isSettingsView } from './pages'
import CycleDay from './cycle-day/cycle-day-overview'
import symptomViews from './cycle-day/symptoms' import { headerTitles } from '../i18n/en/labels'
import Chart from './chart/chart'
import SettingsMenu from './settings/settings-menu'
import settingsViews from './settings'
import Stats from './stats'
import {headerTitles, menuTitles} from '../i18n/en/labels'
import setupNotifications from '../lib/notifications' import setupNotifications from '../lib/notifications'
import { closeDb } from '../db' import { getCycleDay } from '../db'
// design wants everyhting lowercased, but we don't
// have CSS pseudo properties
const headerTitlesLowerCase = Object.keys(headerTitles).reduce((acc, curr) => {
acc[curr] = headerTitles[curr].toLowerCase()
return acc
}, {})
const HOME_PAGE = 'Home'
const CYCLE_DAY_PAGE = 'CycleDay'
const SETTINGS_MENU_PAGE = 'SettingsMenu'
class App extends Component { class App extends Component {
static propTypes = {
date: PropTypes.string,
navigation: PropTypes.object.isRequired,
}
constructor(props) { constructor(props) {
super(props) super(props)
this.todayDateString = LocalDate.now().toString()
props.setDate(this.todayDateString)
this.state = { this.state = {
currentPage: HOME_PAGE, cycleDay: getCycleDay(this.todayDateString),
cycleDay: {},
} }
this.backHandler = BackHandler.addEventListener('hardwareBackPress', this.handleBackButtonPress)
setupNotifications(this.navigate) this.backHandler = BackHandler.addEventListener(
'hardwareBackPress',
props.goBack
)
setupNotifications(this.props.navigate)
} }
componentWillUnmount() { componentWillUnmount() {
this.backHandler.remove() this.backHandler.remove()
} }
navigate = (pageName, cycleDay) => { render() {
const { currentPage } = this.state const { date, navigation, goBack } = this.props
// for the back button to work properly, we want to const { currentPage } = navigation
// remember two origins: which menu item we came from
// and from where we navigated to the symptom view (day
// view or home page)
if (this.isMenuItem()) {
this.menuOrigin = currentPage
}
if (!this.isSymptomView()) {
this.originForSymptomView = currentPage
}
this.setState({ currentPage: pageName, cycleDay })
}
handleBackButtonPress = () => { if (!currentPage) {
const { currentPage } = this.state
if (currentPage === HOME_PAGE) {
closeDb()
return false return false
} }
if (this.isSymptomView()) {
this.navigate(this.originForSymptomView) const { cycleDay } = this.state
} else if (this.isSettingsView()) {
this.navigate(SETTINGS_MENU_PAGE) const Page = viewsList[currentPage]
} else if (currentPage === CYCLE_DAY_PAGE) { const title = headerTitles[currentPage]
this.navigate(this.menuOrigin)
} else { const isSymptomEditView = isSymptomView(currentPage)
this.navigate(HOME_PAGE) const isSettingsSubView = isSettingsView(currentPage)
const isCycleDayView = currentPage === 'CycleDay'
const headerProps = {
title,
handleBack: isSettingsSubView ? goBack : null,
} }
return true
}
isMenuItem() { const pageProps = {
return Object.keys(menuTitles).includes(this.state.currentPage) cycleDay,
} date,
handleBackButtonPress: goBack,
isSymptomView() {
return Object.keys(symptomViews).includes(this.state.currentPage)
}
isSettingsView() {
return Object.keys(settingsViews).includes(this.state.currentPage)
}
isDefaultView() {
const { currentPage } = this.state
return this.isMenuItem(currentPage) || currentPage === SETTINGS_MENU_PAGE
}
render() {
const { currentPage, cycleDay } = this.state
const pages = {
Home,
Calendar,
CycleDay,
Chart,
SettingsMenu,
...settingsViews,
Stats,
...symptomViews
} }
const Page = pages[currentPage]
const title = headerTitlesLowerCase[currentPage]
const hasDefaultHeader =
!this.isSymptomView() &&
currentPage !== CYCLE_DAY_PAGE
const isSettingsSubView = this.isSettingsView()
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
{
{ hasDefaultHeader && !isSymptomEditView &&
<Header !isCycleDayView &&
handleBack={isSettingsSubView ? this.handleBackButtonPress : null} <Header { ...headerProps } />
title={title}
/>
} }
<Page <Page { ...pageProps } />
navigate={this.navigate}
cycleDay={cycleDay}
date={this.props.date}
handleBackButtonPress={this.handleBackButtonPress}
/>
{!this.isSymptomView() && { !isSymptomEditView && <Menu /> }
<Menu navigate={this.navigate} currentPage={currentPage} />
}
</View> </View>
) )
} }
@@ -142,11 +92,20 @@ class App extends Component {
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
return({ return({
date: getDate(state) date: getDate(state),
navigation: getNavigation(state)
})
}
const mapDispatchToProps = (dispatch) => {
return({
setDate: (date) => dispatch(setDate(date)),
navigate: (page) => dispatch(navigate(page)),
goBack: () => dispatch(goBack()),
}) })
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
null mapDispatchToProps
)(App) )(App)
+2
View File
@@ -3,6 +3,7 @@ import { CalendarList } from 'react-native-calendars'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { setDate } from '../slices/date' import { setDate } from '../slices/date'
import { navigate } from '../slices/navigation'
import { LocalDate } from 'js-joda' import { LocalDate } from 'js-joda'
import { getBleedingDaysSortedByDate } from '../db' import { getBleedingDaysSortedByDate } from '../db'
@@ -68,6 +69,7 @@ class CalendarView extends Component {
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return({ return({
setDate: (date) => dispatch(setDate(date)), setDate: (date) => dispatch(setDate(date)),
navigate: (page) => dispatch(navigate(page)),
}) })
} }
+2
View File
@@ -3,6 +3,7 @@ import { TouchableOpacity } from 'react-native'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { setDate } from '../../slices/date' import { setDate } from '../../slices/date'
import { navigate } from '../../slices/navigation'
import { getCycleDay } from '../../db' import { getCycleDay } from '../../db'
@@ -109,6 +110,7 @@ class DayColumn extends Component {
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return({ return({
setDate: (date) => dispatch(setDate(date)), setDate: (date) => dispatch(setDate(date)),
navigate: (page) => dispatch(navigate(page)),
}) })
} }
+4 -8
View File
@@ -3,6 +3,7 @@ import { ScrollView, View } from 'react-native'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { getDate, setDate } from '../../slices/date' import { getDate, setDate } from '../../slices/date'
import { navigate } from '../../slices/navigation'
import { LocalDate } from 'js-joda' import { LocalDate } from 'js-joda'
import Header from '../header' import Header from '../header'
@@ -41,11 +42,6 @@ class CycleDayOverView extends Component {
this.updateCycleDay(nextDate) this.updateCycleDay(nextDate)
} }
navigate(symptom) {
const { cycleDay } = this.state
this.props.navigate(symptom, cycleDay)
}
render() { render() {
const { cycleDay } = this.state const { cycleDay } = this.state
const { date } = this.props const { date } = this.props
@@ -66,8 +62,7 @@ class CycleDayOverView extends Component {
const { getCycleDayNumber } = cycleModule() const { getCycleDayNumber } = cycleModule()
const cycleDayNumber = getCycleDayNumber(date) const cycleDayNumber = getCycleDayNumber(date)
const headerSubtitle = const headerSubtitle = cycleDayNumber && `Cycle day ${cycleDayNumber}`
cycleDayNumber && `Cycle day ${cycleDayNumber}`.toLowerCase()
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
@@ -90,7 +85,7 @@ class CycleDayOverView extends Component {
key={symptom} key={symptom}
symptom={symptom} symptom={symptom}
symptomData={symptomData} symptomData={symptomData}
onPress={() => this.navigate(symptomEditView, symptomData)} onPress={() => this.props.navigate(symptomEditView)}
disabled={dateInFuture} disabled={dateInFuture}
/>) />)
}) })
@@ -116,6 +111,7 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return({ return({
setDate: (date) => dispatch(setDate(date)), setDate: (date) => dispatch(setDate(date)),
navigate: (page) => dispatch(navigate(page)),
}) })
} }
@@ -106,7 +106,7 @@ class SymptomView extends Component {
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<Header <Header
title={headerTitles[symptom].toLowerCase()} title={headerTitles[symptom]}
subtitle={formatDate(this.date)} subtitle={formatDate(this.date)}
handleBack={this.props.handleBackButtonPress} handleBack={this.props.handleBackButtonPress}
handleDelete={ handleDelete={
+9 -3
View File
@@ -10,18 +10,24 @@ export default function Title({ title, subtitle }) {
return ( return (
<View> <View>
<Text style={styles.dateHeader} testID='headerTitle'> <Text style={styles.dateHeader} testID='headerTitle'>
{title} { // design wants everyhting lowercased, but we don't
// have CSS pseudo properties
title.toLowerCase()}
</Text> </Text>
{ subtitle && { subtitle &&
<Text style={styles.cycleDayNumber} testID='headerSubtitle'> <Text style={styles.cycleDayNumber} testID='headerSubtitle'>
{subtitle} {subtitle.toLowerCase()}
</Text> </Text>
} }
</View> </View>
) )
} }
return <Text testID='headerTitle' style={styles.headerText}>{title}</Text> return (
<Text testID='headerTitle' style={styles.headerText}>
{title.toLowerCase()}
</Text>
)
} }
Title.propTypes = { Title.propTypes = {
+3 -2
View File
@@ -4,8 +4,9 @@ import moment from 'moment'
export default function (date) { export default function (date) {
const today = LocalDate.now() const today = LocalDate.now()
const dateToDisplay = LocalDate.parse(date) const dateToDisplay = LocalDate.parse(date)
const formattedDate = today.equals(dateToDisplay) ? 'today' : moment(date).format('MMMM Do YYYY') return today.equals(dateToDisplay) ?
return formattedDate.toLowerCase() 'today' :
moment(date).format('MMMM Do YYYY')
} }
export function formatDateForShortText (date) { export function formatDateForShortText (date) {
+8 -15
View File
@@ -3,7 +3,7 @@ import React, { Component } from 'react'
import { ScrollView, View } from 'react-native' import { ScrollView, View } from 'react-native'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { setDate } from '../slices/date' import { navigate } from '../slices/navigation'
import DripHomeIcon from '../assets/drip-home-icons' import DripHomeIcon from '../assets/drip-home-icons'
import { import {
@@ -17,7 +17,6 @@ import styles, { cycleDayColor, periodColor, secondaryColor } from '../styles'
import AppText from './app-text' import AppText from './app-text'
import Button from './button' import Button from './button'
import { formatDateForShortText } from './helpers/format-date' import { formatDateForShortText } from './helpers/format-date'
import { getCycleDay } from '../db'
const IconText = ({ children, wrapperStyles }) => { const IconText = ({ children, wrapperStyles }) => {
return ( return (
@@ -71,26 +70,20 @@ class Home extends Component {
} }
} }
setTodayDate = () => {
this.props.setDate(this.todayDateString)
}
navigateToCycleDayView = () => { navigateToCycleDayView = () => {
this.setTodayDate()
this.props.navigate('CycleDay') this.props.navigate('CycleDay')
} }
navigateToBleedingEditView = () => { navigateToBleedingEditView = () => {
this.setTodayDate() this.props.navigate('BleedingEditView')
this.props.navigate( }
'BleedingEditView',
getCycleDay(this.todayDateString) navigateToChart = () => {
) this.props.navigate('Chart')
} }
render() { render() {
const { cycleDayNumber, phase, status } = this.state const { cycleDayNumber, phase, status } = this.state
const { navigate } = this.props
const cycleDayMoreText = cycleDayNumber ? const cycleDayMoreText = cycleDayNumber ?
labels.cycleDayKnown(cycleDayNumber) : labels.cycleDayKnown(cycleDayNumber) :
labels.cycleDayNotEnoughInfo labels.cycleDayNotEnoughInfo
@@ -136,7 +129,7 @@ class Home extends Component {
</HomeElement> </HomeElement>
<HomeElement <HomeElement
onPress={ () => navigate('Chart') } onPress={this.navigateToChart}
buttonColor={ secondaryColor } buttonColor={ secondaryColor }
buttonLabel={ labels.checkFertility } buttonLabel={ labels.checkFertility }
> >
@@ -164,7 +157,7 @@ class Home extends Component {
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return({ return({
setDate: (date) => dispatch(setDate(date)), navigate: (page) => dispatch(navigate(page))
}) })
} }
-87
View File
@@ -1,87 +0,0 @@
import React from 'react'
import {
View,
Text,
TouchableOpacity
} from 'react-native'
import settingsViews from './settings'
import { menuTitles } from '../i18n/en/labels'
import styles, { iconStyles, secondaryColor } from '../styles'
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
const menuTitlesLowerCase = Object.keys(menuTitles).reduce((acc, curr) => {
acc[curr] = menuTitles[curr].toLowerCase()
return acc
}, {})
const menuItems = [
{
labelKey: 'Home',
icon: 'home',
component: 'Home',
},
{
labelKey: 'Calendar',
icon: 'calendar-range',
component: 'Calendar',
},
{
labelKey: 'Chart',
icon: 'chart-line',
component: 'Chart',
},
{
labelKey: 'Stats',
icon: 'chart-pie',
component: 'Stats',
},
{
labelKey: 'Settings',
icon: 'settings',
component: 'SettingsMenu',
children: Object.keys(settingsViews),
}
]
const MenuItem = ({ icon, labelKey, active, onPress }) => {
const styleActive = active ? { color: secondaryColor } : null
return (
<TouchableOpacity
style={styles.menuItem}
onPress={onPress}
>
<Icon name={icon} {...iconStyles.menuIcon} {...styleActive} />
<Text
testID={active ? 'activeMenuItem' : `menuItem${labelKey}`}
style={[styles.menuText, styleActive]}
>
{menuTitlesLowerCase[labelKey]}
</Text>
</TouchableOpacity>
)
}
const Menu = ({ currentPage, navigate }) => {
return (
<View style={styles.menu}>
{ menuItems.map(({ icon, labelKey, component, children }) => {
const isActive = (component === currentPage) ||
(children && children.indexOf(currentPage) !== -1)
return (
<MenuItem
key={labelKey}
labelKey={labelKey}
icon={icon}
active={isActive}
onPress={() => navigate(component)}
/>
)}
)}
</View >
)
}
export default Menu
+55
View File
@@ -0,0 +1,55 @@
import React from 'react'
import { View } from 'react-native'
import PropTypes from 'prop-types'
import MenuItem from './menu-item'
import { connect } from 'react-redux'
import { getNavigation, navigate } from '../../slices/navigation'
import { pages } from '../pages'
import styles from '../../styles'
const Menu = ({ navigate, navigation }) => {
const menuItems = pages.filter(page => page.isInMenu)
return (
<View style={styles.menu}>
{ menuItems.map(({ icon, label, component, children }) => {
const isActive = (component === navigation.currentPage) ||
(children && children.indexOf(navigation.currentPage) !== -1)
return (
<MenuItem
key={label}
label={label}
icon={icon}
active={isActive}
onPress={() => navigate(component)}
/>
)}
)}
</View >
)
}
Menu.propTypes = {
navigation: PropTypes.object,
navigate: PropTypes.func,
}
const mapStateToProps = (state) => {
return({
navigation: getNavigation(state),
})
}
const mapDispatchToProps = (dispatch) => {
return({
navigate: (page) => dispatch(navigate(page)),
})
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Menu)
+32
View File
@@ -0,0 +1,32 @@
import React from 'react'
import { Text, TouchableOpacity } from 'react-native'
import styles, { iconStyles, secondaryColor } from '../../styles'
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
import { menuTitles } from '../../i18n/en/labels'
const menuTitlesLowerCase = Object.keys(menuTitles).reduce((acc, curr) => {
acc[curr] = menuTitles[curr].toLowerCase()
return acc
}, {})
const MenuItem = ({ icon, label, active, onPress }) => {
const styleActive = active ? { color: secondaryColor } : null
return (
<TouchableOpacity
style={styles.menuItem}
onPress={onPress}
>
<Icon name={icon} {...iconStyles.menuIcon} {...styleActive} />
<Text
testID={active ? 'activeMenuItem' : `menuItem${label}`}
style={[styles.menuText, styleActive]}
>
{menuTitlesLowerCase[label]}
</Text>
</TouchableOpacity>
)
}
export default MenuItem
+89
View File
@@ -0,0 +1,89 @@
import symptomViews from './cycle-day/symptoms'
import settingsViews from './settings'
import settingsLabels from '../i18n/en/settings'
const labels = settingsLabels.menuTitles
const symptomsPages = Object.keys(symptomViews).map(symptomView => ({
component: symptomView,
parent: 'CycleDay',
}))
export const isSymptomView =
(page) => Object.keys(symptomViews).includes(page)
export const isSettingsView =
(page) => Object.keys(settingsViews).includes(page)
export const pages = [
{
component: 'Home',
icon: 'home',
isInMenu: true,
label: 'Home',
},
{
component: 'Calendar',
icon: 'calendar-range',
isInMenu: true,
label: 'Calendar',
parent: 'Home',
},
{
component: 'Chart',
icon: 'chart-line',
isInMenu: true,
label: 'Chart',
parent: 'Home',
},
{
component: 'Stats',
icon: 'chart-pie',
isInMenu: true,
label: 'Stats',
parent: 'Home',
},
{
children: Object.keys(settingsViews),
component: 'SettingsMenu',
icon: 'settings',
isInMenu: true,
label: 'Settings',
parent: 'Home',
},
{
component: 'Reminders',
label: labels.reminders,
parent: 'SettingsMenu',
},
{
component: 'NfpSettings',
label: labels.nfpSettings,
parent: 'SettingsMenu',
},
{
component: 'DataManagement',
label: labels.dataManagement,
parent: 'SettingsMenu',
},
{
component: 'Password',
label: labels.password,
parent: 'SettingsMenu',
},
{
component: 'About',
label: labels.about,
parent: 'SettingsMenu',
},
{
component: 'License',
label: labels.license,
parent: 'SettingsMenu',
},
{
component: 'CycleDay',
parent: 'Home',
},
...symptomsPages
]
+20 -6
View File
@@ -1,10 +1,13 @@
import React from 'react' import React from 'react'
import { import { TouchableOpacity, ScrollView } from 'react-native'
TouchableOpacity, import { connect } from 'react-redux'
ScrollView,
} from 'react-native' import { navigate } from '../../slices/navigation'
import styles from '../../styles/index' import styles from '../../styles/index'
import settingsLabels from '../../i18n/en/settings' import settingsLabels from '../../i18n/en/settings'
import AppText from '../app-text' import AppText from '../app-text'
const labels = settingsLabels.menuTitles const labels = settingsLabels.menuTitles
@@ -18,7 +21,7 @@ const menu = [
{title: labels.license, component: 'License'} {title: labels.license, component: 'License'}
] ]
export default function SettingsMenu(props) { const SettingsMenu = (props) => {
return ( return (
<ScrollView> <ScrollView>
{ menu.map(menuItem)} { menu.map(menuItem)}
@@ -36,4 +39,15 @@ export default function SettingsMenu(props) {
</TouchableOpacity> </TouchableOpacity>
) )
} }
} }
const mapDispatchToProps = (dispatch) => {
return({
navigate: (page) => dispatch(navigate(page)),
})
}
export default connect(
null,
mapDispatchToProps
)(SettingsMenu)
+19
View File
@@ -0,0 +1,19 @@
import Home from './home'
import Calendar from './calendar'
import CycleDay from './cycle-day/cycle-day-overview'
import symptomViews from './cycle-day/symptoms'
import Chart from './chart/chart'
import SettingsMenu from './settings/settings-menu'
import settingsViews from './settings'
import Stats from './stats'
export const viewsList = {
Home,
Calendar,
CycleDay,
Chart,
SettingsMenu,
...settingsViews,
Stats,
...symptomViews
}
+49
View File
@@ -0,0 +1,49 @@
import { createSlice } from 'redux-starter-kit'
import { pages, isSymptomView } from '../components/pages'
import { closeDb } from '../db'
import { BackHandler } from 'react-native'
const navigationSlice = createSlice({
slice: 'navigation',
initialState: {
currentPage: 'Home',
},
reducers: {
navigate: (state, action) => {
return {
currentPage: action.payload,
previousPage: state.currentPage,
}
},
goBack: ({ currentPage, previousPage }) => {
if (currentPage === 'Home') {
closeDb()
BackHandler.exitApp()
return { currentPage }
}
if (currentPage === 'CycleDay' || isSymptomView(currentPage)) {
if (previousPage) {
return {
currentPage: previousPage
}
}
}
const page = pages.find(p => p.component === currentPage)
return {
currentPage: page.parent
}
}
}
})
// Extract the action creators object and the reducer
const { actions, reducer, selectors } = navigationSlice
// Extract and export each action creator by name
export const { navigate, goBack } = actions
export const { getNavigation } = selectors
export default reducer
+3 -1
View File
@@ -1,9 +1,11 @@
import { combineReducers, createStore } from "redux" import { combineReducers, createStore } from "redux"
import date from "./slices/date" import date from "./slices/date"
import navigation from "./slices/navigation"
const reducer = combineReducers({ const reducer = combineReducers({
date date,
navigation
}) })
const store = createStore(reducer) const store = createStore(reducer)