Symptom view redesign

This commit is contained in:
Maria Zadnepryanets
2020-08-14 11:57:26 +00:00
committed by Sofiya Tepikin
parent ef16cfd041
commit 885da5c293
43 changed files with 1396 additions and 1649 deletions
+10 -12
View File
@@ -1,5 +1,5 @@
import React, { Component } from 'react'
import { View, BackHandler } from 'react-native'
import { BackHandler, StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
@@ -10,7 +10,7 @@ import { getNavigation, navigate, goBack } from '../slices/navigation'
import Header from './header'
import Menu from './menu'
import { viewsList } from './views'
import { isSymptomView, isSettingsView } from './pages'
import { isSettingsView } from './pages'
import { headerTitles } from '../i18n/en/labels'
import setupNotifications from '../lib/notifications'
@@ -64,9 +64,7 @@ class App extends Component {
const Page = viewsList[currentPage]
const title = headerTitles[currentPage]
const isSymptomEditView = isSymptomView(currentPage)
const isSettingsSubView = isSettingsView(currentPage)
const isCycleDayView = currentPage === 'CycleDay'
const headerProps = {
title,
@@ -79,21 +77,21 @@ class App extends Component {
}
return (
<View style={{ flex: 1 }}>
{
!isSymptomEditView &&
!isCycleDayView &&
<View style={styles.container}>
<Header { ...headerProps } />
}
<Page { ...pageProps } />
{ !isSymptomEditView && <Menu /> }
<Menu />
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1
}
})
const mapStateToProps = (state) => {
return({
date: getDate(state),
+1 -1
View File
@@ -73,7 +73,7 @@ class CycleChart extends Component {
prepareSymptomData = () => {
this.symptomRowSymptoms = SYMPTOMS.filter((symptomName) => {
return this.cycleDaysSortedByDate.some(cycleDay => {
return cycleDay[symptomName]
return (symptomName !== 'temperature') && cycleDay[symptomName]
})
})
this.chartSymptoms = [...this.symptomRowSymptoms]
+4 -10
View File
@@ -1,11 +1,11 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Image, StyleSheet, TouchableOpacity, View } from 'react-native'
import { Image, StyleSheet, View } from 'react-native'
import AppIcon from '../common/app-icon'
import AppText from '../common/app-text'
import CloseIcon from '../common/close-icon'
import { Colors, Containers, Sizes, Spacing } from '../../styles/redesign'
import { Containers, Spacing } from '../../styles/redesign'
import { chart } from '../../i18n/en/labels'
const image = require('../../assets/swipe.png')
@@ -17,9 +17,7 @@ const Tutorial = ({ onClose }) => {
<View style={styles.textContainer}>
<AppText>{chart.tutorial}</AppText>
</View>
<TouchableOpacity onPress={onClose} style={styles.iconContainer}>
<AppIcon name='cross' color={Colors.orange} />
</TouchableOpacity>
<CloseIcon onClose={onClose} />
</View>
)
}
@@ -33,10 +31,6 @@ const styles = StyleSheet.create({
...Containers.rowContainer,
padding: Spacing.large
},
iconContainer: {
alignSelf: 'flex-start',
marginBottom: Sizes.base
},
image: {
height: 40
},
+32
View File
@@ -0,0 +1,32 @@
import React from 'react'
import { Modal, StyleSheet, TouchableOpacity } from 'react-native'
import PropTypes from 'prop-types'
const AppModal = ({ children, onClose }) => {
return(
<Modal
animationType='fade'
onRequestClose={onClose}
transparent={true}
visible={true}
>
<TouchableOpacity onPress={onClose} style={styles.blackBackground} />
{children}
</Modal>
)
}
AppModal.propTypes = {
children: PropTypes.node,
onClose: PropTypes.func
}
const styles = StyleSheet.create({
blackBackground: {
backgroundColor: 'black',
flex: 1,
opacity: 0.5,
}
})
export default AppModal
+1 -1
View File
@@ -20,7 +20,7 @@ const AppSwitch = ({ onToggle, text, value }) => {
AppSwitch.propTypes = {
onToggle: PropTypes.func.isRequired,
text: PropTypes.string,
value: PropTypes.bool.isRequired
value: PropTypes.bool
}
const styles = StyleSheet.create({
+8 -2
View File
@@ -1,10 +1,15 @@
import React from 'react'
import { StyleSheet, TextInput } from 'react-native'
import PropTypes from 'prop-types'
import { Colors, Spacing, Typography } from '../../styles/redesign'
const AppTextInput = ({ ...props }) => {
return <TextInput style={styles.input} {...props} />
const AppTextInput = ({ style, ...props }) => {
return <TextInput style={[styles.input, style]} {...props} />
}
AppTextInput.propTypes = {
style: PropTypes.object
}
const styles = StyleSheet.create({
@@ -16,6 +21,7 @@ const styles = StyleSheet.create({
borderWidth: 1,
color: Colors.greyDark,
marginTop: Spacing.base,
minWidth: '80%',
paddingHorizontal: Spacing.base,
...Typography.mainText
}
+15 -2
View File
@@ -2,11 +2,20 @@ import React from 'react'
import PropTypes from 'prop-types'
import { StyleSheet, TouchableOpacity } from 'react-native'
import AppIcon from './app-icon'
import AppText from './app-text'
import { Colors, Fonts, Spacing } from '../../styles/redesign'
const Button = ({ children, isCTA, isSmall, onPress, testID, ...props }) => {
const Button = ({
children,
iconName,
isCTA,
isSmall,
onPress,
testID,
...props
}) => {
const buttonStyle = isCTA ? styles.cta : styles.regular
const textCTA = isCTA ? styles.buttonTextBold : styles.buttonTextRegular
const textStyle = [textCTA, isSmall ? textSmall : text]
@@ -19,12 +28,14 @@ const Button = ({ children, isCTA, isSmall, onPress, testID, ...props }) => {
{...props}
>
<AppText style={textStyle}>{children}</AppText>
{iconName && <AppIcon color={Colors.orange} name={iconName} />}
</TouchableOpacity>
)
}
Button.propTypes = {
children: PropTypes.node,
iconName: PropTypes.string,
isCTA: PropTypes.bool,
isSmall: PropTypes.bool,
onPress: PropTypes.func,
@@ -48,8 +59,10 @@ const textSmall = {
const button = {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
margin: Spacing.base
margin: Spacing.base,
minWidth: '15%'
}
const styles = StyleSheet.create({
+32
View File
@@ -0,0 +1,32 @@
import React from 'react'
import PropTypes from 'prop-types'
import { StyleSheet, TouchableOpacity } from 'react-native'
import AppIcon from './app-icon'
import { Colors, Sizes } from '../../styles/redesign'
const CloseIcon = ({ onClose, ...props }) => {
return (
<TouchableOpacity
onPress={onClose}
style={styles.container}
{...props}
>
<AppIcon name='cross' color={Colors.orange} />
</TouchableOpacity>
)
}
CloseIcon.propTypes = {
onClose: PropTypes.func.isRequired
}
const styles = StyleSheet.create({
container: {
alignSelf: 'flex-start',
marginBottom: Sizes.base
}
})
export default CloseIcon
+1 -1
View File
@@ -33,7 +33,7 @@ const styles = StyleSheet.create({
container: {
borderStyle: 'solid',
borderBottomWidth: 2,
borderBottomColor: Colors.grey,
borderBottomColor: Colors.greyLight,
paddingBottom: Spacing.base,
...segmentContainer
},
-20
View File
@@ -1,20 +0,0 @@
import React, { Component } from 'react'
import { View, Dimensions } from 'react-native'
import styles from '../../styles'
export default class FillerBoxes extends Component {
render() {
const n = Dimensions.get('window').width / styles.symptomBox.width
const fillerBoxes = []
for (let i = 0; i < Math.ceil(n); i++) {
fillerBoxes.push(
<View
width={styles.symptomBox.width}
height={0}
key={i.toString()}
/>
)
}
return fillerBoxes
}
}
-174
View File
@@ -1,174 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { View, TouchableOpacity } from 'react-native'
import AppText from '../common/app-text'
import DripIcon from '../../assets/drip-icons'
import styles from '../../styles'
import { headerTitles as symptomTitles } from '../../i18n/en/labels'
import * as labels from '../../i18n/en/cycle-day'
const bleedingLabels = labels.bleeding.labels
const intensityLabels = labels.intensity
const sexLabels = labels.sex.categories
const contraceptiveLabels = labels.contraceptives.categories
const painLabels = labels.pain.categories
const moodLabels = labels.mood.categories
function isNumber(val) {
return typeof val === 'number'
}
const l = {
bleeding: ({ value, exclude }) => {
if (isNumber(value)) {
const bleedingLabel = bleedingLabels[value]
return exclude ? `(${bleedingLabel})` : bleedingLabel
}
},
temperature: ({ value, time, exclude }) => {
if (isNumber(value)) {
let temperatureLabel = `${value} °C`
if (time) {
temperatureLabel += ` - ${time}`
}
if (exclude) {
temperatureLabel = `(${temperatureLabel})`
}
return temperatureLabel
}
},
mucus: mucus => {
const filledCategories = ['feeling', 'texture'].filter(c => isNumber(mucus[c]))
let label = filledCategories.map(category => {
return labels.mucus.subcategories[category] + ': ' + labels.mucus[category].categories[mucus[category]]
}).join(', ')
if (isNumber(mucus.value)) label += `\n => ${labels.mucusNFP[mucus.value]}`
if (mucus.exclude) label = `(${label})`
return label
},
cervix: cervix => {
const filledCategories = ['opening', 'firmness', 'position'].filter(c => isNumber(cervix[c]))
let label = filledCategories.map(category => {
return labels.cervix.subcategories[category] + ': ' + labels.cervix[category].categories[cervix[category]]
}).join(', ')
if (cervix.exclude) label = `(${label})`
return label
},
note: note => note.value,
desire: ({ value }) => {
if (isNumber(value)) {
return intensityLabels[value]
}
},
sex: sex => {
const sexLabel = []
if (sex && Object.values({...sex}).some(val => val)){
Object.keys(sex).forEach(key => {
if(sex[key] && key !== 'other' && key !== 'note') {
sexLabel.push(
sexLabels[key] ||
contraceptiveLabels[key]
)
}
if(key === 'other' && sex.other) {
let label = contraceptiveLabels[key]
if(sex.note) {
label = `${label} (${sex.note})`
}
sexLabel.push(label)
}
})
return sexLabel.join(', ')
}
},
pain: pain => {
const painLabel = []
if (pain && Object.values({...pain}).some(val => val)){
Object.keys(pain).forEach(key => {
if(pain[key] && key !== 'other' && key !== 'note') {
painLabel.push(painLabels[key])
}
if(key === 'other' && pain.other) {
let label = painLabels[key]
if(pain.note) {
label = `${label} (${pain.note})`
}
painLabel.push(label)
}
})
return painLabel.join(', ')
}
},
mood: mood => {
const moodLabel = []
if (mood && Object.values({...mood}).some(val => val)){
Object.keys(mood).forEach(key => {
if(mood[key] && key !== 'other' && key !== 'note') {
moodLabel.push(moodLabels[key])
}
if(key === 'other' && mood.other) {
let label = moodLabels[key]
if(mood.note) {
label = `${label} (${mood.note})`
}
moodLabel.push(label)
}
})
return moodLabel.join(', ')
}
}
}
const getLabel = (symptom, symptomData) => {
return symptomData && l[symptom](symptomData)
}
export default function SymptomBox(
{ disabled, onPress, symptom, symptomData }) {
const data = getLabel(symptom, symptomData)
const iconName = `drip-icon-${symptom}`
const disabledStyle = disabled ? styles.symptomInFuture : null
const containerStyle = [
styles.symptomBox,
data && styles.symptomBoxActive,
disabledStyle
]
const titleStyle = [
data && styles.symptomTextActive,
disabledStyle,
{fontSize: 15}
]
const dataBoxStyle = [styles.symptomDataBox, disabledStyle]
const iconColor = data ? 'white' : 'black'
return (
<TouchableOpacity onPress={onPress} disabled={disabled} testID={iconName}>
<View style={containerStyle}>
<DripIcon name={iconName} size={50} color={iconColor} />
<AppText style={titleStyle} numberOfLines={1}>
{symptomTitles[symptom].toLowerCase()}
</AppText>
</View>
<View style={dataBoxStyle}>
<AppText style={styles.symptomDataText} numberOfLines={3}>
{data}
</AppText>
</View>
</TouchableOpacity>
)
}
SymptomBox.propTypes = {
disabled: PropTypes.bool.isRequired,
onPress: PropTypes.func.isRequired,
symptom: PropTypes.string.isRequired,
symptomData: PropTypes.object
}
+40 -69
View File
@@ -1,118 +1,89 @@
import React, { Component } from 'react'
import { ScrollView, View } from 'react-native'
import { StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import AppPage from '../common/app-page'
import SymptomBox from './symptom-box'
import SymptomPageTitle from './symptom-page-title'
import { connect } from 'react-redux'
import { getDate, setDate } from '../../slices/date'
import { navigate } from '../../slices/navigation'
import { LocalDate } from 'js-joda'
import Header from '../header'
import FillerBoxes from './FillerBoxes'
import SymptomBox from './SymptomBox'
import cycleModule from '../../lib/cycle'
import formatDate from '../helpers/format-date'
import { dateToTitle } from '../helpers/format-date'
import { getCycleDay } from '../../db'
import styles from '../../styles'
import { getData } from '../helpers/cycle-day'
import { general as labels} from '../../i18n/en/cycle-day'
import { Spacing } from '../../styles/redesign'
import { SYMPTOMS } from '../../config'
class CycleDayOverView extends Component {
static propTypes = {
navigate: PropTypes.func,
setDate: PropTypes.func,
// The following are not being used,
// we could see if it's possible to not pass them from the <App />
cycleDay: PropTypes.object,
date: PropTypes.string,
}
constructor(props) {
super(props)
this.state = {
cycleDay: getCycleDay(props.date)
}
this.state = { cycleDay: getCycleDay(props.date), data: null }
}
updateCycleDay = (date) => {
this.props.setDate(date)
this.setState({
cycleDay: getCycleDay(date)
})
}
goToPrevDay = () => {
const { date } = this.props
const prevDate = LocalDate.parse(date).minusDays(1).toString()
this.updateCycleDay(prevDate)
}
goToNextDay = () => {
const { date } = this.props
const nextDate = LocalDate.parse(date).plusDays(1).toString()
this.updateCycleDay(nextDate)
this.setState({ cycleDay: getCycleDay(date) })
}
render() {
const { cycleDay } = this.state
const { date } = this.props
const dateInFuture = LocalDate.now().isBefore(LocalDate.parse(date))
const symptomBoxesList = [
'bleeding',
'temperature',
'mucus',
'cervix',
'desire',
'sex',
'pain',
'mood',
'note',
]
const { getCycleDayNumber } = cycleModule()
const cycleDayNumber = getCycleDayNumber(date)
const headerSubtitle = cycleDayNumber && `Cycle day ${cycleDayNumber}`
const subtitle = cycleDayNumber && `${labels.cycleDayNumber}${cycleDayNumber}`
return (
<View style={{ flex: 1 }}>
<Header
handleBack={this.goToPrevDay}
handleNext={this.goToNextDay}
title={formatDate(date)}
subtitle={headerSubtitle}
<AppPage>
<SymptomPageTitle
reloadSymptomData={this.updateCycleDay}
subtitle={subtitle}
title={dateToTitle(date)}
/>
<ScrollView>
<View style={styles.symptomBoxesView}>
{
symptomBoxesList.map(symptom => {
const symptomEditView =
`${symptom[0].toUpperCase() + symptom.substring(1)}EditView`
const symptomData =
cycleDay && cycleDay[symptom] ? cycleDay[symptom] : null
<View style={styles.container}>
{SYMPTOMS.map(symptom => {
const symptomData = cycleDay && cycleDay[symptom]
? cycleDay[symptom] : null
return(
<SymptomBox
key={symptom}
symptom={symptom}
symptomData={symptomData}
onPress={() => this.props.navigate(symptomEditView)}
disabled={dateInFuture && symptom !== 'note'}
/>)
})
}
{
// this is just to make the last row adhere to the grid
// (and) because there are no pseudo properties in RN
}
<FillerBoxes />
</View>
</ScrollView>
symptomDataToDisplay={getData(symptom, symptomData)}
updateCycleDayData={this.updateCycleDay}
/>
)
})}
</View>
</AppPage>
)
}
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
padding: Spacing.base
}
})
const mapStateToProps = (state) => {
return({
date: getDate(state),
+30 -13
View File
@@ -1,29 +1,26 @@
import React from 'react'
import PropTypes from 'prop-types'
import { View, TouchableOpacity } from 'react-native'
import { StyleSheet, TouchableOpacity, View } from 'react-native'
import AppText from '../common/app-text'
import styles from '../../styles'
import { Colors, Containers } from '../../styles/redesign'
export default function SelectBoxGroup({ labels, onSelect, optionsState }) {
const SelectBoxGroup = ({ labels, optionsState, onSelect }) => {
return (
<View style={styles.selectBoxSection}>
<View style={styles.container}>
{Object.keys(labels).map(key => {
const style = [styles.selectBox]
const textStyle = []
if (optionsState[key]) {
style.push(styles.selectBoxActive)
textStyle.push(styles.selectBoxTextActive)
}
const isActive = optionsState[key]
const boxStyle = [styles.box, isActive && styles.boxActive]
const textStyle = [styles.text, isActive && styles.textActive]
return (
<TouchableOpacity
onPress={() => onSelect(key)}
key={key}
onPress={() => onSelect(key)}
style={boxStyle}
>
<View style={style}>
<AppText style={textStyle}>{labels[key]}</AppText>
</View>
</TouchableOpacity>
)
})}
@@ -36,3 +33,23 @@ SelectBoxGroup.propTypes = {
onSelect: PropTypes.func.isRequired,
optionsState: PropTypes.object.isRequired
}
const styles = StyleSheet.create({
box: {
...Containers.box
},
boxActive: {
...Containers.boxActive
},
container: {
...Containers.selectGroupContainer
},
text: {
color: Colors.orange
},
textActive: {
color: 'white'
}
})
export default SelectBoxGroup
+30 -25
View File
@@ -1,40 +1,27 @@
import React from 'react'
import PropTypes from 'prop-types'
import { View, TouchableOpacity } from 'react-native'
import { StyleSheet, TouchableOpacity, View } from 'react-native'
import AppText from '../common/app-text'
import styles from '../../styles'
import { Colors, Containers } from '../../styles/redesign'
export default function SelectTabGroup({ active, buttons, onSelect }) {
export default function SelectTabGroup({ activeButton, buttons, onSelect }) {
return (
<View style={styles.selectTabGroup}>
<View style={styles.container}>
{
buttons.map(({ label, value }, i) => {
let firstOrLastStyle
if (i === buttons.length - 1) {
firstOrLastStyle = styles.selectTabLast
} else if (i === 0) {
firstOrLastStyle = styles.selectTabFirst
}
let activeStyle
const isActive = value === active
if (isActive) activeStyle = styles.selectTabActive
const isActive = value === activeButton
const boxStyle = [styles.box, isActive && styles.boxActive]
const textStyle = [styles.text, isActive && styles.textActive]
return (
<TouchableOpacity
onPress={() => onSelect(isActive ? null : value)}
onPress={() => onSelect(value)}
key={i}
activeOpacity={1}
style={boxStyle}
>
<View>
<View style={[
styles.selectTab,
firstOrLastStyle,
activeStyle
]}>
<AppText style={activeStyle}>{label}</AppText>
</View>
</View>
<AppText style={textStyle}>{label}</AppText>
</TouchableOpacity>
)
})
@@ -44,7 +31,25 @@ export default function SelectTabGroup({ active, buttons, onSelect }) {
}
SelectTabGroup.propTypes = {
active: PropTypes.number,
activeButton: PropTypes.number,
buttons: PropTypes.array.isRequired,
onSelect: PropTypes.func.isRequired
}
const styles = StyleSheet.create({
box: {
...Containers.box
},
boxActive: {
...Containers.boxActive
},
container: {
...Containers.selectGroupContainer
},
text: {
color: Colors.orange
},
textActive: {
color: 'white'
}
})
+167
View File
@@ -0,0 +1,167 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { StyleSheet, View, TouchableOpacity } from 'react-native'
import AppText from '../common/app-text'
import DripIcon from '../../assets/drip-icons'
import SymptomEditView from './symptom-edit-view'
import { connect } from 'react-redux'
import { getDate } from '../../slices/date'
import { isDateInFuture } from '../helpers/cycle-day'
import { Colors, Sizes, Spacing } from '../../styles/redesign'
import { headerTitles as symptomTitles } from '../../i18n/en/labels'
class SymptomBox extends Component {
static propTypes = {
date: PropTypes.string.isRequired,
symptom: PropTypes.string.isRequired,
symptomData: PropTypes.object,
symptomDataToDisplay: PropTypes.string,
updateCycleDayData: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = { isSymptomEdited: false }
}
onFinishEditing = () => {
const { date, updateCycleDayData } = this.props
updateCycleDayData(date)
this.setState({ isSymptomEdited: false })
}
onEditSymptom = () => {
this.setState({ isSymptomEdited: true })
}
render() {
const { date, symptom, symptomData, symptomDataToDisplay } = this.props
const { isSymptomEdited } = this.state
const isSymptomDisabled = isDateInFuture(date) && symptom !== 'note'
const isExcluded = symptomData !== null ? symptomData.exclude : false
const iconColor = isSymptomDisabled ? Colors.greyLight : Colors.grey
const iconName = `drip-icon-${symptom}`
const symptomNameStyle = [
styles.symptomName,
(isSymptomDisabled && styles.symptomNameDisabled),
(isExcluded && styles.symptomNameExcluded)
]
const textStyle = [
styles.text,
(isSymptomDisabled && styles.textDisabled),
(isExcluded && styles.textExcluded)
]
return (
<React.Fragment>
{isSymptomEdited &&
<SymptomEditView
symptom={symptom}
symptomData={symptomData}
onClose={this.onFinishEditing}
/>
}
<TouchableOpacity
disabled={isSymptomDisabled}
onPress={this.onEditSymptom}
style={styles.container}
testID={iconName}
>
<DripIcon
color={iconColor}
isActive={!isSymptomDisabled}
name={iconName}
size={40}
/>
<View style={styles.textContainer}>
<AppText style={symptomNameStyle}>
{symptomTitles[symptom].toLowerCase()}
</AppText>
{symptomDataToDisplay &&
<AppText style={textStyle}>
{symptomDataToDisplay}
</AppText>
}
</View>
</TouchableOpacity>
</React.Fragment>
)
}
}
const excluded = {
textDecorationLine: 'line-through'
}
const hint = {
fontSize: Sizes.small,
fontStyle: 'italic'
}
const main = {
fontSize: Sizes.base,
height: Sizes.base * 2,
lineHeight: Sizes.base,
marginBottom: (-1) * Sizes.tiny,
textAlignVertical: 'center'
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
backgroundColor: 'white',
borderRadius: 10,
elevation: 4,
flexDirection: 'row',
height: 110,
marginBottom: Spacing.base,
paddingHorizontal: Spacing.small,
paddingVertical: Spacing.base,
width: Spacing.symptomTileWidth
},
symptomName: {
color: Colors.purple,
...main
},
symptomNameDisabled: {
color: Colors.grey
},
symptomNameExcluded: {
color: Colors.greyDark,
...excluded
},
textContainer: {
flexDirection: 'column',
marginLeft: Spacing.small,
maxWidth: Spacing.textWidth
},
text: {
...hint
},
textDisabled: {
color: Colors.greyLight
},
textExcluded: {
color: Colors.grey,
...excluded
}
})
const mapStateToProps = (state) => {
return({
date: getDate(state),
})
}
export default connect(
mapStateToProps,
null,
)(SymptomBox)
+281
View File
@@ -0,0 +1,281 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { ScrollView, StyleSheet, View } from 'react-native'
import AppModal from '../common/app-modal'
import AppSwitch from '../common/app-switch'
import AppText from '../common/app-text'
import AppTextInput from '../common/app-text-input'
import Button from '../common/button'
import CloseIcon from '../common/close-icon'
import Segment from '../common/segment'
import SelectBoxGroup from './select-box-group'
import SelectTabGroup from './select-tab-group'
import Temperature from './temperature'
import { connect } from 'react-redux'
import { getDate } from '../../slices/date'
import { blank, save, shouldShow, symtomPage } from '../helpers/cycle-day'
import { shared as sharedLabels } from '../../i18n/en/labels'
import info from '../../i18n/en/symptom-info'
import { Containers, Sizes } from '../../styles/redesign'
class SymptomEditView extends Component {
static propTypes = {
date: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
symptom: PropTypes.string.isRequired,
symptomData: PropTypes.object
}
constructor(props) {
super(props)
const { symptomData, symptom } = this.props
const data = symptomData ? symptomData : blank[symptom]
const symptomConfig = symtomPage[symptom]
const shouldShowExclude = shouldShow(symptomConfig.excludeText)
const shouldShowNote = shouldShow(symptomConfig.note)
const shouldBoxGroup = shouldShow(symptomConfig.selectBoxGroups)
const shouldTabGroup = shouldShow(symptomConfig.selectTabGroups)
this.state = {
data,
shouldShowExclude,
shouldShowInfo: false,
shouldShowNote,
shouldBoxGroup,
shouldTabGroup
}
}
componentDidUpdate() {
this.saveData()
}
getParsedData = () => JSON.parse(JSON.stringify(this.state.data))
onEditNote = (note) => {
const data = this.getParsedData()
const { symptom } = this.props
if (symptom === 'note') {
Object.assign(data, { value: note })
} else {
data['note'] = note
}
this.setState({ data })
}
onExcludeToggle = () => {
const data = this.getParsedData()
Object.assign(data, { exclude: !data.exclude })
this.setState({ data })
}
onPressLearnMore = () => {
this.setState({ shouldShowInfo: !this.state.shouldShowInfo })
}
onRemove = () => {
this.saveData(true)
this.props.onClose()
}
onSave = () => {
this.saveData()
this.props.onClose()
}
onSaveTemperature = (value, field) => {
const data = this.getParsedData()
const dataToSave = field === 'value'
? { [field]: Number(value) } : { [field]: value }
Object.assign(data, { ...dataToSave })
this.setState({ data })
}
onSelectBox = (key) => {
const data = this.getParsedData()
if (key === "other") {
Object.assign(data, {
note: null,
[key]: !this.state.data[key]
})
} else {
Object.assign(data, { [key]: !this.state.data[key] })
}
this.setState({ data })
}
onSelectBoxNote= (value) => {
const data = this.getParsedData()
Object.assign(data, { note: value !== '' ? value : null })
this.setState({ data })
}
onSelectTab = (group, value) => {
const data = this.getParsedData()
Object.assign(data, { [group.key]: value })
this.setState({ data })
}
saveData = (shouldDeleteData) => {
const { date, symptom } = this.props
const { data } = this.state
save[symptom](data, date, shouldDeleteData)
}
render() {
const { onClose, symptom } = this.props
const { data,
shouldShowExclude,
shouldShowInfo,
shouldShowNote,
shouldBoxGroup,
shouldTabGroup
} = this.state
const iconName = shouldShowInfo ? "chevron-down" : "chevron-up"
const noteText = symptom === 'note' ? data.value : data.note
return (
<AppModal onClose={onClose}>
<ScrollView
contentContainerStyle={styles.modalContainer}
pagingEnabled={true}
style={styles.modalWindow}
>
<View style={styles.headerContainer}>
<CloseIcon onClose={onClose} />
</View>
{symptom === 'temperature' &&
<Temperature
data={data}
save={(value, field) => this.onSaveTemperature(value, field)}
/>
}
{shouldTabGroup && symtomPage[symptom].selectTabGroups.map(group => {
return (
<Segment key={group.key}>
<AppText style={styles.title}>{group.title}</AppText>
<SelectTabGroup
activeButton={data[group.key]}
buttons={group.options}
onSelect={value => this.onSelectTab(group, value)}
/>
</Segment>
)
})
}
{shouldBoxGroup && symtomPage[symptom].selectBoxGroups.map(group => {
const isOtherSelected =
data['other'] !== null
&& data['other'] !== false
&& Object.keys(group.options).includes('other')
return (
<Segment key={group.key}>
<AppText style={styles.title}>{group.title}</AppText>
<SelectBoxGroup
labels={group.options}
onSelect={value => this.onSelectBox(value)}
optionsState={data}
/>
{isOtherSelected &&
<AppTextInput
multiline={true}
placeholder={sharedLabels.enter}
value={data.note}
onChangeText={value => this.onSelectBoxNote(value)}
/>
}
</Segment>
)
})
}
{shouldShowExclude &&
<Segment>
<AppSwitch
onToggle={this.onExcludeToggle}
text={symtomPage[symptom].excludeText}
value={data.exclude}
/>
</Segment>
}
{shouldShowNote &&
<Segment>
<AppText>{symtomPage[symptom].note}</AppText>
<AppTextInput
multiline={true}
placeholder={sharedLabels.enter}
onChangeText={this.onEditNote}
value={noteText !== null ? noteText : ''}
testID='noteInput'
/>
</Segment>
}
<View style={styles.buttonsContainer}>
<Button iconName={iconName} isSmall onPress={this.onPressLearnMore}>
learn more
</Button>
<Button isSmall onPress={this.onRemove}>
remove
</Button>
<Button isCTA isSmall onPress={this.onSave}>save</Button>
</View>
{shouldShowInfo &&
<Segment last>
<AppText>{info[symptom].text}</AppText>
</Segment>
}
</ScrollView>
</AppModal>
)
}
}
const styles = StyleSheet.create({
buttonsContainer: {
...Containers.rowContainer
},
headerContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
paddingVertical: Sizes.tiny,
},
modalContainer: {
flexGrow: 1,
padding: Sizes.small
},
modalWindow: {
backgroundColor: 'white',
borderRadius: 10,
marginVertical: Sizes.huge * 2,
minHeight: '40%',
height: '70%',
position: 'absolute'
},
title: {
fontSize: Sizes.subtitle
}
})
const mapStateToProps = (state) => {
return({
date: getDate(state),
})
}
export default connect(
mapStateToProps,
null,
)(SymptomEditView)
@@ -0,0 +1,88 @@
import React from 'react'
import PropTypes from 'prop-types'
import { StyleSheet, TouchableOpacity, View } from 'react-native'
import AppIcon from '../common/app-icon'
import AppText from '../common/app-text'
import { connect } from 'react-redux'
import { getDate, setDate } from '../../slices/date'
import {
nextDate,
prevDate,
isTomorrowInFuture,
isYesterdayInFuture
} from '../helpers/cycle-day'
import { Colors, Containers, Spacing, Typography } from '../../styles/redesign'
const SymptomPageTitle = ({
date,
reloadSymptomData,
setDate,
subtitle,
title
}) => {
const rightArrowColor = isTomorrowInFuture(date) ? Colors.grey : Colors.orange
const leftArrowColor = isYesterdayInFuture(date) ? Colors.grey : Colors.orange
const navigate = (isForward) => {
const nextDay = isForward ? nextDate(date) : prevDate(date)
reloadSymptomData(nextDay)
setDate(nextDay)
}
return (
<View style={styles.container}>
<TouchableOpacity onPress={() => navigate(false)}>
<AppIcon name='chevron-left' color={leftArrowColor}/>
</TouchableOpacity>
<View style={styles.textContainer}>
<AppText style={styles.title}>{title}</AppText>
{subtitle && <AppText style={styles.subtitle}>{subtitle}</AppText>}
</View>
<TouchableOpacity onPress={() => navigate(true)}>
<AppIcon name='chevron-right' color={rightArrowColor}/>
</TouchableOpacity>
</View>
)
}
SymptomPageTitle.propTypes = {
date: PropTypes.string.isRequired,
reloadSymptomData: PropTypes.func.isRequired,
setDate: PropTypes.func.isRequired,
subtitle: PropTypes.string,
title: PropTypes.string
}
const styles = StyleSheet.create({
container: {
height: (Spacing.base * 4),
marginHorizontal: Spacing.base,
marginTop: Spacing.base,
...Containers.rowContainer
},
textContainer: {
alignItems: 'center'
},
title: {
...Typography.titleWithoutMargin
}
})
const mapStateToProps = (state) => {
return({
date: getDate(state),
})
}
const mapDispatchToProps = (dispatch) => {
return({
setDate: (date) => dispatch(setDate(date)),
})
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(SymptomPageTitle)
-85
View File
@@ -1,85 +0,0 @@
import React, { Component } from 'react'
import { Switch } from 'react-native'
import PropTypes from 'prop-types'
import { bleeding } from '../../../i18n/en/cycle-day'
import SelectTabGroup from '../select-tab-group'
import SymptomSection from './symptom-section'
import SymptomView from './symptom-view'
import { getLabelsList } from '../../helpers/labels'
import { saveSymptom } from '../../../db'
class Bleeding extends Component {
static propTypes = {
cycleDay: PropTypes.object,
date: PropTypes.string.isRequired,
}
constructor(props) {
super(props)
const symptom = 'bleeding'
const { cycleDay } = props
const defaultSymptomData = {
value: null,
exclude: false
}
const symptomData =
cycleDay && cycleDay[symptom] ? cycleDay[symptom] : defaultSymptomData
this.state = { ...symptomData }
this.bleedingRadioProps = getLabelsList(bleeding.labels)
this.symptom = symptom
}
autoSave = () => {
const { date } = this.props
const valuesToSave = { ...this.state }
const hasValueToSave = typeof this.state.value === 'number'
saveSymptom(this.symptom, date, hasValueToSave ? valuesToSave : null)
}
componentDidUpdate() {
this.autoSave()
}
render() {
return (
<SymptomView
symptom={this.symptom}
values={this.state}
date={this.props.date}
>
<SymptomSection
header={bleeding.heaviness.header}
explainer={bleeding.heaviness.explainer}
>
<SelectTabGroup
buttons={this.bleedingRadioProps}
active={this.state.value}
onSelect={val => this.setState({ value: val })}
/>
</SymptomSection>
<SymptomSection
header={bleeding.exclude.header}
explainer={bleeding.exclude.explainer}
inline={true}
>
<Switch
onValueChange={(val) => {
this.setState({ exclude: val })
}}
value={this.state.exclude}
/>
</SymptomSection>
</SymptomView>
)
}
}
export default Bleeding
-113
View File
@@ -1,113 +0,0 @@
import React, { Component } from 'react'
import { Switch } from 'react-native'
import PropTypes from 'prop-types'
import { cervix as labels } from '../../../i18n/en/cycle-day'
import SelectTabGroup from '../select-tab-group'
import SymptomSection from './symptom-section'
import SymptomView from './symptom-view'
import { getLabelsList } from '../../helpers/labels'
import { saveSymptom } from '../../../db'
class Cervix extends Component {
static propTypes = {
cycleDay: PropTypes.object,
date: PropTypes.string.isRequired,
}
constructor(props) {
super(props)
const symptom = 'cervix'
const { cycleDay } = props
const defaultSymptomData = {}
const symptomData =
cycleDay && cycleDay[symptom] ? cycleDay[symptom] : defaultSymptomData
this.state = { ...symptomData }
this.cervixOpeningRadioProps = getLabelsList(labels.opening.categories)
this.cervixFirmnessRadioProps = getLabelsList(labels.firmness.categories)
this.cervixPositionRadioProps = getLabelsList(labels.position.categories)
this.symptom = symptom
}
autoSave = () => {
const { date } = this.props
const { opening, firmness, position, exclude } = this.state
const valuesToSave = {
opening,
firmness,
position,
exclude: Boolean(exclude)
}
const nothingEntered = ['opening', 'firmness', 'position'].every(
val => typeof this.state[val] !== 'number')
saveSymptom(this.symptom, date, nothingEntered ? null : valuesToSave)
}
componentDidUpdate() {
this.autoSave()
}
render() {
// TODO saving this info for notice when leaving incomplete data
// const mandatoryNotCompleted = typeof this.state.opening != 'number' || typeof this.state.firmness != 'number'
return (
<SymptomView
symptom={this.symptom}
values={this.state}
date={this.props.date}
>
<SymptomSection
header="Opening"
explainer={labels.opening.explainer}
>
<SelectTabGroup
buttons={this.cervixOpeningRadioProps}
active={this.state.opening}
onSelect={val => this.setState({ opening: val })}
/>
</SymptomSection>
<SymptomSection
header="Firmness"
explainer={labels.firmness.explainer}
>
<SelectTabGroup
buttons={this.cervixFirmnessRadioProps}
active={this.state.firmness}
onSelect={val => this.setState({ firmness: val })}
/>
</SymptomSection>
<SymptomSection
header="Position"
explainer={labels.position.explainer}
>
<SelectTabGroup
buttons={this.cervixPositionRadioProps}
active={this.state.position}
onSelect={val => this.setState({ position: val })}
/>
</SymptomSection>
<SymptomSection
header="Exclude"
explainer="You can exclude this value if you don't want to use it for fertility detection"
inline={true}
>
<Switch
onValueChange={(val) => {
this.setState({ exclude: val })
}}
value={this.state.exclude}
/>
</SymptomSection>
</SymptomView>
)
}
}
export default Cervix
-69
View File
@@ -1,69 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { intensity, desire } from '../../../i18n/en/cycle-day'
import SelectTabGroup from '../select-tab-group'
import SymptomSection from './symptom-section'
import SymptomView from './symptom-view'
import { getLabelsList } from '../../helpers/labels'
import { saveSymptom } from '../../../db'
class Desire extends Component {
static propTypes = {
cycleDay: PropTypes.object,
date: PropTypes.string.isRequired,
}
constructor(props) {
super(props)
const symptom = 'desire'
const { cycleDay } = props
const defaultSymptomData = { value: null }
const symptomData =
cycleDay && cycleDay[symptom] ? cycleDay[symptom] : defaultSymptomData
this.state = { ...symptomData }
this.symptom = symptom
this.desireRadioProps = getLabelsList(intensity)
}
autoSave = () => {
const { date } = this.props
const valuesToSave = { ...this.state }
const hasValueToSave = typeof this.state.value === 'number'
saveSymptom(this.symptom, date, hasValueToSave ? valuesToSave : null)
}
componentDidUpdate() {
this.autoSave()
}
render() {
return (
<SymptomView
symptom={this.symptom}
values={this.state}
date={this.props.date}
>
<SymptomSection
header={desire.header}
explainer={desire.explainer}
>
<SelectTabGroup
buttons={this.desireRadioProps}
active={this.state.value}
onSelect={val => this.setState({ value: val })}
/>
</SymptomSection>
</SymptomView>
)
}
}
export default Desire
-21
View File
@@ -1,21 +0,0 @@
import BleedingEditView from './bleeding'
import TemperatureEditView from './temperature'
import MucusEditView from './mucus'
import CervixEditView from './cervix'
import NoteEditView from './note'
import DesireEditView from './desire'
import SexEditView from './sex'
import PainEditView from './pain'
import MoodEditView from './mood'
export default {
BleedingEditView,
TemperatureEditView,
MucusEditView,
CervixEditView,
NoteEditView,
DesireEditView,
SexEditView,
PainEditView,
MoodEditView
}
@@ -1,28 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { ScrollView, View, TouchableOpacity } from 'react-native'
import Icon from 'react-native-vector-icons/SimpleLineIcons'
import AppText from '../../common/app-text'
import labels from '../../../i18n/en/symptom-info.js'
import styles, {iconStyles} from '../../../styles/index'
export default function InfoSymptom({ close, symptom }) {
return (
<View style={styles.infoPopUpWrapper}>
<View style={styles.dimmed}></View>
<View style={styles.infoPopUp} testID="symptomInfoPopup">
<TouchableOpacity onPress={close} style={styles.infoSymptomClose}>
<Icon name='close' {...iconStyles.infoPopUpClose}/>
</TouchableOpacity>
<ScrollView style={styles.infoSymptomText}>
<AppText>{labels[symptom].text}</AppText>
</ScrollView>
</View>
</View>
)
}
InfoSymptom.propTypes = {
close: PropTypes.func.isRequired,
symptom: PropTypes.string.isRequired
}
-93
View File
@@ -1,93 +0,0 @@
import React, { Component } from 'react'
import { TextInput } from 'react-native'
import PropTypes from 'prop-types'
import { mood as labels } from '../../../i18n/en/cycle-day'
import SelectBoxGroup from '../select-box-group'
import SymptomSection from './symptom-section'
import SymptomView from './symptom-view'
import { saveSymptom } from '../../../db'
class Mood extends Component {
static propTypes = {
cycleDay: PropTypes.object,
date: PropTypes.string.isRequired,
}
constructor(props) {
super(props)
const symptom = 'mood'
const { cycleDay } = props
const defaultSymptomData = {}
const symptomData =
cycleDay && cycleDay[symptom] ? cycleDay[symptom] : defaultSymptomData
this.state = { ...symptomData }
// We make sure other is always true when there is a note,
// e.g. when import is messed up.
if (this.state.note) this.state.other = true
this.symptom = symptom
}
autoSave = () => {
const { date } = this.props
const valuesToSave = Object.assign({}, this.state)
if (!valuesToSave.other) {
valuesToSave.note = null
}
const nothingEntered = Object.values(this.state).every(val => !val)
saveSymptom(this.symptom, date, nothingEntered ? null : valuesToSave)
}
componentDidUpdate() {
this.autoSave()
}
toggleState = (key) => {
const curr = this.state[key]
this.setState({[key]: !curr})
if (key === 'other' && !curr) {
this.setState({focusTextArea: true})
}
}
render() {
return (
<SymptomView
symptom={this.symptom}
values={this.state}
date={this.props.date}
>
<SymptomSection
explainer={labels.explainer}
>
<SelectBoxGroup
labels={labels.categories}
onSelect={this.toggleState}
optionsState={this.state}
/>
{ this.state.other &&
<TextInput
autoFocus={this.state.focusTextArea}
multiline={true}
placeholder="Enter"
value={this.state.note}
onChangeText={(val) => {
this.setState({note: val})
}}
/>
}
</SymptomSection>
</SymptomView>
)
}
}
export default Mood
-104
View File
@@ -1,104 +0,0 @@
import React, { Component } from 'react'
import { Switch } from 'react-native'
import PropTypes from 'prop-types'
import { mucus as labels } from '../../../i18n/en/cycle-day'
import computeNfpValue from '../../../lib/nfp-mucus'
import SelectTabGroup from '../select-tab-group'
import SymptomSection from './symptom-section'
import SymptomView from './symptom-view'
import { getLabelsList } from '../../helpers/labels'
import { saveSymptom } from '../../../db'
class Mucus extends Component {
static propTypes = {
cycleDay: PropTypes.object,
date: PropTypes.string.isRequired,
}
constructor(props) {
super(props)
const symptom = 'mucus'
const { cycleDay } = props
const defaultSymptomData = {}
const symptomData =
cycleDay && cycleDay[symptom] ? cycleDay[symptom] : defaultSymptomData
this.state = { ...symptomData }
this.mucusFeeling = getLabelsList(labels.feeling.categories)
this.mucusTexture = getLabelsList(labels.texture.categories)
this.symptom = symptom
}
shouldAutoSave = () => {
const { date } = this.props
const nothingEntered = ['feeling', 'texture'].every(
val => typeof this.state[val] !== 'number'
)
const { feeling, texture, exclude} = this.state
const valuesToSave = {
feeling,
texture,
value: computeNfpValue(feeling, texture),
exclude: Boolean(exclude)
}
saveSymptom(this.symptom, date, nothingEntered ? null : valuesToSave)
}
componentDidUpdate() {
this.shouldAutoSave()
}
render() {
// TODO leaving this info for notice when leaving incomplete data
// const mandatoryNotCompletedYet = typeof this.state.feeling != 'number' || typeof this.state.texture != 'number'
return (
<SymptomView
symptom={this.symptom}
values={this.state}
date={this.props.date}
>
<SymptomSection
header='Feeling'
explainer={labels.feeling.explainer}
>
<SelectTabGroup
buttons={this.mucusFeeling}
onSelect={val => this.setState({ feeling: val })}
active={this.state.feeling}
/>
</SymptomSection>
<SymptomSection
header='Texture'
explainer={labels.texture.explainer}
>
<SelectTabGroup
buttons={this.mucusTexture}
onSelect={val => this.setState({ texture: val })}
active={this.state.texture}
/>
</SymptomSection>
<SymptomSection
header="Exclude"
explainer={labels.excludeExplainer}
inline={true}
>
<Switch
onValueChange={(val) => {
this.setState({ exclude: val })
}}
value={this.state.exclude}
/>
</SymptomSection>
</SymptomView>
)
}
}
export default Mucus
-65
View File
@@ -1,65 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { TextInput } from 'react-native'
import SymptomSection from './symptom-section'
import { noteExplainer } from '../../../i18n/en/cycle-day'
import { shared as sharedLabels } from '../../../i18n/en/labels'
import SymptomView from './symptom-view'
import { saveSymptom } from '../../../db'
class Note extends Component {
static propTypes = {
cycleDay: PropTypes.object,
date: PropTypes.string.isRequired
}
constructor(props) {
super(props)
const symptom = 'note'
const { cycleDay } = props
const defaultSymptomData = { value: '' }
const symptomData =
cycleDay && cycleDay[symptom] ? cycleDay[symptom] : defaultSymptomData
this.state = { ...symptomData }
this.symptom = symptom
}
autoSave = () => {
const { date } = this.props
const valuesToSave = { ...this.state }
saveSymptom(this.symptom, date, this.state.value ? valuesToSave : null)
}
componentDidUpdate() {
this.autoSave()
}
render() {
return (
<SymptomView
symptom={this.symptom}
values={this.state}
date={this.props.date}
>
<SymptomSection explainer={noteExplainer} >
<TextInput
autoFocus={true}
multiline={true}
placeholder={sharedLabels.enter}
onChangeText={(val) => { this.setState({ value: val })}}
value={this.state.value}
testID='noteInput'
/>
</SymptomSection>
</SymptomView>
)
}
}
export default Note
-94
View File
@@ -1,94 +0,0 @@
import React, { Component } from 'react'
import { TextInput } from 'react-native'
import PropTypes from 'prop-types'
import { pain as labels } from '../../../i18n/en/cycle-day'
import { shared as sharedLabels } from '../../../i18n/en/labels'
import SelectBoxGroup from '../select-box-group'
import SymptomSection from './symptom-section'
import SymptomView from './symptom-view'
import { saveSymptom } from '../../../db'
class Pain extends Component {
static propTypes = {
cycleDay: PropTypes.object,
date: PropTypes.string.isRequired,
}
constructor(props) {
super(props)
const symptom = 'pain'
const { cycleDay } = props
const defaultSymptomData = {}
const symptomData =
cycleDay && cycleDay[symptom] ? cycleDay[symptom] : defaultSymptomData
this.state = { ...symptomData }
// We make sure other is always true when there is a note,
// e.g. when import is messed up.
if (this.state.note) this.state.other = true
this.symptom = symptom
}
autoSave = () => {
const { date } = this.props
const valuesToSave = Object.assign({}, this.state)
if (!valuesToSave.other) {
valuesToSave.note = null
}
const nothingEntered = Object.values(this.state).every(val => !val)
saveSymptom(this.symptom, date, nothingEntered ? null : valuesToSave)
}
componentDidUpdate() {
this.autoSave()
}
toggleState = (key) => {
const curr = this.state[key]
this.setState({[key]: !curr})
if (key === 'other' && !curr) {
this.setState({focusTextArea: true})
}
}
render() {
return (
<SymptomView
symptom={this.symptom}
values={this.state}
date={this.props.date}
>
<SymptomSection
explainer={labels.explainer}
>
<SelectBoxGroup
labels={labels.categories}
onSelect={this.toggleState}
optionsState={this.state}
/>
{ this.state.other &&
<TextInput
autoFocus={this.state.focusTextArea}
multiline={true}
placeholder={sharedLabels.enter}
value={this.state.note}
onChangeText={(val) => {
this.setState({note: val})
}}
/>
}
</SymptomSection>
</SymptomView>
)
}
}
export default Pain
-110
View File
@@ -1,110 +0,0 @@
import React, { Component } from 'react'
import { TextInput } from 'react-native'
import PropTypes from 'prop-types'
import { sex as sexLabels, contraceptives as contraceptivesLabels } from '../../../i18n/en/cycle-day'
import { shared as sharedLabels } from '../../../i18n/en/labels'
import SelectBoxGroup from '../select-box-group'
import SymptomSection from './symptom-section'
import SymptomView from './symptom-view'
import { saveSymptom } from '../../../db'
class Sex extends Component {
static propTypes = {
cycleDay: PropTypes.object,
date: PropTypes.string.isRequired,
}
constructor(props) {
super(props)
const symptom = 'sex'
const { cycleDay } = props
const defaultSymptomData = {}
const symptomData =
cycleDay && cycleDay[symptom] ? cycleDay[symptom] : defaultSymptomData
this.state = { ...symptomData }
// We make sure other is always true when there is a note,
// e.g. when import is messed up.
if (this.state.note) this.state.other = true
this.symptom = symptom
}
autoSave = () => {
const { date } = this.props
const valuesToSave = Object.assign({}, this.state)
if (!valuesToSave.other) {
valuesToSave.note = null
}
const nothingEntered = Object.values(this.state).every(val => !val)
saveSymptom(this.symptom, date, nothingEntered ? null : valuesToSave)
}
componentDidUpdate() {
this.autoSave()
}
toggleState = (key) => {
const curr = this.state[key]
this.setState({[key]: !curr})
if (key === 'other'){
if (curr){
this.setState({note: ""})
} else {
this.setState({focusTextArea: true})
}
}
}
render() {
return (
<SymptomView
symptom={this.symptom}
values={this.state}
date={this.props.date}
>
<SymptomSection
header={sexLabels.header}
explainer={sexLabels.explainer}
>
<SelectBoxGroup
labels={sexLabels.categories}
onSelect={this.toggleState}
optionsState={this.state}
/>
</SymptomSection>
<SymptomSection
header={contraceptivesLabels.header}
explainer={contraceptivesLabels.explainer}
>
<SelectBoxGroup
labels={contraceptivesLabels.categories}
onSelect={this.toggleState}
optionsState={this.state}
/>
</SymptomSection>
{this.state.other &&
<TextInput
autoFocus={this.state.focusTextArea}
multiline={true}
placeholder={sharedLabels.enter}
value={this.state.note}
onChangeText={(val) => {
this.setState({ note: val })
}}
/>
}
</SymptomView>
)
}
}
export default Sex
@@ -1,41 +0,0 @@
import React, { Component } from 'react'
import { TouchableOpacity } from 'react-native'
import PropTypes from 'prop-types'
import Icon from 'react-native-vector-icons/Entypo'
import InfoPopUp from './info-symptom'
import styles, { iconStyles } from '../../../styles'
export default class SymptomInfo extends Component {
static propTypes = {
symptom: PropTypes.string
}
constructor() {
super()
this.state = { showInfo: false }
}
showInfo = () => this.setState({ showInfo: true })
hideInfo = () => this.setState({ showInfo: false })
render() {
return (
<React.Fragment>
<TouchableOpacity
onPress={this.showInfo}
style={styles.infoButtonSymptomView}
testID="symptomInfoButton"
>
<Icon name="info-with-circle" style={iconStyles.info} />
</TouchableOpacity>
{ this.state.showInfo &&
<InfoPopUp symptom={this.props.symptom} close={this.hideInfo} />
}
</React.Fragment>
)
}
}
@@ -1,45 +0,0 @@
import React, { Component } from 'react'
import { View } from 'react-native'
import PropTypes from 'prop-types'
import AppText from '../../common/app-text'
import styles from '../../../styles'
export default class SymptomSection extends Component {
render() {
const p = this.props
let placeHeadingInline
if (!p.explainer && p.inline) {
placeHeadingInline = {
flexDirection: 'row',
alignItems: "center"
}
}
return (
<View style={[placeHeadingInline, styles.symptomSection]}>
{ p.header &&
<AppText style={styles.symptomViewHeading}>{p.header}</AppText>
}
<View
flexDirection={p.inline ? 'row' : null}
flex={1}
alignItems={p.inline ? 'center' : null}
>
{ p.explainer && (
<View flex={1}>
<AppText>{p.explainer}</AppText>
</View>
)}
{p.children}
</View>
</View>
)
}
}
SymptomSection.propTypes = {
children: PropTypes.node,
explainer: PropTypes.string,
header: PropTypes.string,
inline: PropTypes.bool
}
@@ -1,117 +0,0 @@
import React, { Component } from 'react'
import { ScrollView, View, Alert } from 'react-native'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { getDate } from '../../../slices/date'
import { goBack } from '../../../slices/navigation'
import { saveSymptom } from '../../../db'
import formatDate from '../../helpers/format-date'
import Header from '../../header'
import SymptomInfo from './symptom-info'
import { headerTitles } from '../../../i18n/en/labels'
import { sharedDialogs } from '../../../i18n/en/cycle-day'
import styles from '../../../styles'
const checkIfHasValues = data => {
const isMeaningfulValue = value => value || value === 0
return Object.values(data).some(isMeaningfulValue)
}
class SymptomView extends Component {
static propTypes = {
symptom: PropTypes.string.isRequired,
values: PropTypes.object,
date: PropTypes.string,
children: PropTypes.node,
goBack: PropTypes.func,
}
constructor(props) {
super()
this.state = {
shouldShowDelete: checkIfHasValues(props.values)
}
}
componentDidUpdate() {
const shouldShowDelete = checkIfHasValues(this.props.values)
if (shouldShowDelete !== this.state.shouldShowDelete) {
this.setState({ shouldShowDelete })
}
}
deleteSymptomEntry() {
const { symptom, date } = this.props
saveSymptom(symptom, date, null)
}
onDeleteConfirmation = () => {
this.deleteSymptomEntry()
this.props.goBack()
}
showConfirmationAlert = () => {
const cancelButton = {
text: sharedDialogs.cancel,
style: 'cancel'
}
const confirmationButton = {
text: sharedDialogs.reallyDeleteData,
onPress: this.onDeleteConfirmation
}
return Alert.alert(
sharedDialogs.areYouSureTitle,
sharedDialogs.areYouSureToDelete,
[cancelButton, confirmationButton]
)
}
render() {
const { symptom, date, goBack } = this.props
const { shouldShowDelete } = this.state
const handleDelete = shouldShowDelete ? this.showConfirmationAlert : null
return (
<View style={{flex: 1}}>
<Header
title={headerTitles[symptom]}
subtitle={formatDate(date)}
handleBack={goBack}
handleDelete={handleDelete}
/>
<View flex={1}>
<ScrollView style={styles.page}>
{this.props.children}
</ScrollView>
<SymptomInfo symptom={symptom} />
</View>
</View>
)
}
}
const mapStateToProps = (state) => {
return({
date: getDate(state),
})
}
const mapDispatchToProps = (dispatch) => {
return({
goBack: () => dispatch(goBack()),
})
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(SymptomView)
@@ -1,107 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { View } from 'react-native'
import AppText from '../../common/app-text'
import AppTextInput from '../../common/app-text-input'
import { temperature as labels } from '../../../i18n/en/cycle-day'
import styles from '../../../styles'
import { getPreviousTemperature } from '../../../db'
import { scaleObservable } from '../../../local-storage'
import { TEMP_MAX, TEMP_MIN } from '../../../config'
export default class TemperatureInput extends Component {
static propTypes = {
temperature: PropTypes.string,
handleTemperatureChange: PropTypes.func,
date: PropTypes.string,
}
constructor(props) {
super(props)
let shouldShowSuggestion = false
let suggestedTemperature = null
if (!props.temperature) {
const prevTemp = getPreviousTemperature(props.date)
if (prevTemp) {
shouldShowSuggestion = true
suggestedTemperature = prevTemp.toString()
}
}
this.state = {
temperature: props.temperature,
shouldShowSuggestion,
suggestedTemperature
}
}
setTemperature = (temperature) => {
this.setState({
shouldShowSuggestion: false,
temperature
})
this.props.handleTemperatureChange(Number(temperature))
}
render () {
const {
shouldShowSuggestion,
suggestedTemperature,
temperature
} = this.state
const inputStyle = [
styles.temperatureTextInput,
shouldShowSuggestion ? styles.temperatureTextInputSuggestion : null
]
return (
<React.Fragment>
<View style={styles.framedSegmentInlineChildren}>
<AppTextInput
style={inputStyle}
autoFocus={true}
value={shouldShowSuggestion ? suggestedTemperature : temperature}
onChangeText={this.setTemperature}
keyboardType='numeric'
maxLength={5}
testID='temperatureInput'
/>
<AppText style={{ marginLeft: 5 }}>°C</AppText>
</View>
<OutOfRangeWarning temperature={this.props.temperature} />
</React.Fragment>
)
}
}
const OutOfRangeWarning = ({ temperature }) => {
if (temperature === '') {
return false
}
const value = Number(temperature)
const range = { min: TEMP_MIN, max: TEMP_MAX }
const scale = scaleObservable.value
let warningMsg
if (value < range.min || value > range.max) {
warningMsg = labels.outOfAbsoluteRangeWarning
} else if (value < scale.min || value > scale.max) {
warningMsg = labels.outOfRangeWarning
} else {
warningMsg = null
}
return <AppText style={styles.hint}>{warningMsg}</AppText>
}
OutOfRangeWarning.propTypes = {
temperature: PropTypes.string.isRequired
}
@@ -1,139 +0,0 @@
import React, { Component } from 'react'
import { Switch } from 'react-native'
import PropTypes from 'prop-types'
import { LocalTime, ChronoUnit } from 'js-joda'
import { temperature as labels } from '../../../i18n/en/cycle-day'
import { shared as sharedLabels } from '../../../i18n/en/labels'
import AppTextInput from '../../common/app-text-input'
import SymptomSection from './symptom-section'
import SymptomView from './symptom-view'
import TimeInput from './time-input'
import TemperatureInput from './temperature-input'
import { saveSymptom } from '../../../db'
const minutes = ChronoUnit.MINUTES
class Temperature extends Component {
static propTypes = {
cycleDay: PropTypes.object,
date: PropTypes.string.isRequired,
}
constructor(props) {
super(props)
const symptom = 'temperature'
const { cycleDay } = props
const defaultSymptomData = {
time: LocalTime.now().truncatedTo(minutes).toString(),
temperature: null,
note: '',
exclude: false
}
const symptomData =
cycleDay && cycleDay[symptom] ? cycleDay[symptom] : defaultSymptomData
const { value, ...restSymptomData } = symptomData
this.state = { temperature: value, ...restSymptomData }
this.symptom = symptom
}
isDeleteIconActive() {
return ['temperature', 'note', 'exclude'].some(key => {
// the time is always and the suggested temp sometimes prefilled, so they're not relevant for setting
// the delete button active.
return this.state[key] || this.state[key] === 0
})
}
autoSave = () => {
const { date } = this.props
const { temperature, exclude, time, note } = this.state
const valuesToSave = {
value: temperature,
exclude,
time,
note
}
saveSymptom(this.symptom, date, temperature ? valuesToSave : null)
}
setTime = (time) => {
this.setState({ time })
}
setTemperature = (temperature) => {
this.setState({ temperature })
}
setNote = (note) => {
this.setState({ note })
}
componentDidUpdate() {
this.autoSave()
}
render() {
const { temperature } = this.state
return (
<SymptomView
symptom={'temperature'}
values={this.state}
date={this.props.date}
>
<SymptomSection
header={labels.temperature.header}
explainer={labels.temperature.explainer}
>
<TemperatureInput
temperature={temperature ? temperature.toFixed(2) : ''}
date={this.props.date}
handleTemperatureChange={this.setTemperature}
/>
</SymptomSection>
<SymptomSection header={labels.time}>
<TimeInput
time={this.state.time}
handleTimeChange={this.setTime}
/>
</SymptomSection>
<SymptomSection
header={labels.note.header}
explainer={labels.note.explainer}
>
<AppTextInput
multiline={true}
placeholder={sharedLabels.enter}
value={this.state.note}
onChangeText={this.setNote}
testID='noteInput'
/>
</SymptomSection>
<SymptomSection
header={labels.exclude.header}
explainer={labels.exclude.explainer}
inline={true}
>
<Switch
onValueChange={(val) => {
this.setState({ exclude: val })
}}
value={this.state.exclude}
/>
</SymptomSection>
</SymptomView>
)
}
}
export default Temperature
@@ -1,62 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Keyboard } from 'react-native'
import AppTextInput from '../../common/app-text-input'
import styles from '../../../styles'
import DateTimePicker from 'react-native-modal-datetime-picker-nevo'
import moment from 'moment'
export default class TimeInput extends Component {
static propTypes = {
time: PropTypes.string,
handleTimeChange: PropTypes.func,
}
constructor(props) {
super(props)
this.state = {
isTimePickerVisible: false,
}
}
showTimePicker = () => {
Keyboard.dismiss()
this.setState({ isTimePickerVisible: true })
}
hideTimePicker = () => {
this.setState({ isTimePickerVisible: false })
}
handleConfirm = (jsDate) => {
// DateTimePicker also when in mode="time" returns full date (with time)
const time = moment(jsDate).format('HH:mm')
this.props.handleTimeChange(time)
this.setState({
isTimePickerVisible: false
})
}
render () {
return (
<React.Fragment>
<AppTextInput
style={styles.temperatureTextInput}
onFocus={this.showTimePicker}
value={this.props.time}
testID='timeInput'
/>
<DateTimePicker
mode="time"
isVisible={this.state.isTimePickerVisible}
onConfirm={this.handleConfirm}
onCancel={this.hideTimePicker}
/>
</React.Fragment>
)
}
}
+158
View File
@@ -0,0 +1,158 @@
import React, { Component } from 'react'
import { StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import { Keyboard } from 'react-native'
import DateTimePicker from 'react-native-modal-datetime-picker-nevo'
import moment from 'moment'
import AppText from '../common/app-text'
import AppTextInput from '../common/app-text-input'
import Segment from '../common/segment'
import { connect } from 'react-redux'
import { getDate } from '../../slices/date'
import { isTemperatureOutOfRange, isPreviousTemperature } from '../helpers/cycle-day'
import { temperature as labels } from '../../i18n/en/cycle-day'
import { Colors, Containers, Sizes, Spacing } from '../../styles/redesign'
const formatTemperature = value => value === null
? value
: Number.parseFloat(value).toFixed(2)
class Temperature extends Component {
static propTypes = {
data: PropTypes.object,
date: PropTypes.string.isRequired,
save: PropTypes.func
}
constructor(props) {
super(props)
const { data, date } = this.props
const { value } = data
const { shouldShowSuggestion, suggestedTemperature } =
isPreviousTemperature(date)
this.state = {
isTimePickerVisible: false,
shouldShowSuggestion,
suggestedTemperature: formatTemperature(suggestedTemperature),
value: formatTemperature(value)
}
}
onCancelTimePicker = () => {
this.setState({ isTimePickerVisible: false })
}
onChangeTemperature = (value) => {
this.setState({ value, shouldShowSuggestion: false })
}
onShowTimePicker = () => {
Keyboard.dismiss()
this.setState({ isTimePickerVisible: true })
}
setTemperature = () => {
const { value } = this.state
this.props.save(value, 'value')
}
setTime = (jsDate) => {
const time = moment(jsDate).format('HH:mm')
const isTimePickerVisible = false
this.props.save(time, 'time')
this.setState({ isTimePickerVisible })
}
render() {
const { shouldShowSuggestion, suggestedTemperature, value } = this.state
const { time } = this.props.data
const inputStyle = (shouldShowSuggestion && value === null)
? { color: Colors.grey }
: {color: Colors.greyDark}
const outOfRangeWarning = isTemperatureOutOfRange(value)
let temperatureToShow = null
if (value) {
temperatureToShow = value
} else if (shouldShowSuggestion) {
temperatureToShow = suggestedTemperature
}
return (
<React.Fragment>
<Segment>
<AppText style={styles.title}>{labels.temperature.explainer}</AppText>
<View style={styles.container}>
<AppTextInput
value={temperatureToShow === null ? '' : temperatureToShow}
onChangeText={this.onChangeTemperature}
onEndEditing={this.setTemperature}
keyboardType="numeric"
maxLength={5}
style={inputStyle}
testID="temperatureInput"
underlineColorAndroid="transparent"
/>
<AppText>°C</AppText>
</View>
{ outOfRangeWarning !== null &&
<View style={styles.hintContainer}>
<AppText style={styles.hint}>{outOfRangeWarning}</AppText>
</View>
}
</Segment>
<Segment>
<AppText style={styles.title}>{labels.time}</AppText>
<AppTextInput
onFocus={this.onShowTimePicker}
testID='timeInput'
value={time}
/>
<DateTimePicker
isVisible={this.state.isTimePickerVisible}
mode="time"
onConfirm={this.setTime}
onCancel={this.onCancelTimePicker}
/>
</Segment>
</React.Fragment>
)
}
}
const styles = StyleSheet.create({
container: {
...Containers.rowContainer
},
hint: {
fontStyle: 'italic',
fontSize: Sizes.small
},
hintContainer: {
marginVertical: Spacing.tiny
},
title: {
fontSize: Sizes.subtitle
}
})
const mapStateToProps = (state) => {
return({
date: getDate(state),
})
}
export default connect(
mapStateToProps,
null,
)(Temperature)
+437
View File
@@ -0,0 +1,437 @@
import { ChronoUnit, LocalDate, LocalTime } from 'js-joda'
import { getPreviousTemperature, saveSymptom } from '../../db'
import { scaleObservable } from '../../local-storage'
import * as labels from '../../i18n/en/cycle-day'
import { getLabelsList } from './labels'
import { TEMP_MAX, TEMP_MIN } from '../../config'
const bleedingLabels = labels.bleeding.labels
const cervixLabels = labels.cervix
const contraceptiveLabels = labels.contraceptives.categories
const intensityLabels = labels.intensity
const moodLabels = labels.mood.categories
const mucusLabels = labels.mucus
const noteDescription = labels.noteExplainer
const painLabels = labels.pain.categories
const sexLabels = labels.sex.categories
const temperatureLabels = labels.temperature
const minutes = ChronoUnit.MINUTES
const isNumber = (value) => typeof value === 'number'
export const shouldShow = (value) => value !== null ? true : false
export const isPreviousTemperature = (temperature) => {
const previousTemperature = getPreviousTemperature(temperature)
const shouldShowSuggestion = previousTemperature ? true : false
const suggestedTemperature = previousTemperature ?
previousTemperature.toString() : null
return { shouldShowSuggestion, suggestedTemperature }
}
export const isTemperatureOutOfRange = (temperature) => {
if (temperature === '') return null
const value = Number(temperature)
const range = { min: TEMP_MIN, max: TEMP_MAX }
const scale = scaleObservable.value
let warningMsg = null
if (value < range.min || value > range.max) {
warningMsg = labels.temperature.outOfAbsoluteRangeWarning
} else if (value < scale.min || value > scale.max) {
warningMsg = labels.temperature.outOfRangeWarning
}
return warningMsg
}
export const blank = {
bleeding: {
exclude: false,
value: null
},
cervix: {
exclude: false,
firmness: null,
opening: null,
position: null,
},
desire: {
value: null
},
mood:{
happy: null,
sad: null,
stressed: null,
balanced: null,
fine: null,
anxious: null,
energetic: null,
fatigue: null,
angry: null,
other: null,
note: null
},
mucus: {
exclude: false,
feeling: null,
texture: null,
value: null
},
note: {
value: null
},
pain: {
cramps: null,
ovulationPain: null,
headache: null,
backache: null,
nausea: null,
tenderBreasts: null,
migraine: null,
other: null,
note: null
},
sex: {
solo: null,
partner: null,
condom: null,
pill: null,
iud: null,
patch: null,
ring: null,
implant: null,
diaphragm: null,
none: null,
other: null,
note: null
},
temperature: {
exclude: false,
note: null,
time: LocalTime.now().truncatedTo(minutes).toString(),
value: null
}
}
export const symtomPage = {
bleeding: {
excludeText: labels.bleeding.exclude.explainer,
note: null,
selectBoxGroups: null,
selectTabGroups: [{
key: 'value',
options: getLabelsList(bleedingLabels),
title: labels.bleeding.heaviness.explainer,
}]
},
cervix: {
excludeText: cervixLabels.excludeExplainer,
note: null,
selectBoxGroups: null,
selectTabGroups: [
{
key: 'opening',
options: getLabelsList(cervixLabels.opening.categories),
title: cervixLabels.opening.explainer,
},
{
key: 'firmness',
options: getLabelsList(cervixLabels.firmness.categories),
title: cervixLabels.firmness.explainer,
},
{
key: 'position',
options: getLabelsList(cervixLabels.position.categories),
title: cervixLabels.position.explainer,
}
]
},
desire: {
excludeText: null,
note: null,
selectBoxGroups: null,
selectTabGroups: [{
key: 'value',
options: getLabelsList(intensityLabels),
title: labels.desire.explainer
}]
},
mucus: {
excludeText: mucusLabels.excludeExplainer,
note: null,
selectBoxGroups: null,
selectTabGroups: [
{
key: 'feeling',
options: getLabelsList(mucusLabels.feeling.categories),
title: mucusLabels.feeling.explainer,
},
{
key: 'texture',
options: getLabelsList(mucusLabels.texture.categories),
title: mucusLabels.texture.explainer,
}
]
},
mood: {
excludeText: null,
note: null,
selectBoxGroups: [{
key: 'mood',
options: moodLabels,
title: labels.mood.explainer
}],
selectTabGroups: null
},
note: {
excludeText: null,
note: noteDescription,
selectBoxGroups: null,
selectTabGroups: null
},
pain: {
excludeText: null,
note: null,
selectBoxGroups: [{
key: 'pain',
options: painLabels,
title: labels.pain.explainer
}],
selectTabGroups: null
},
sex: {
excludeText: null,
note: null,
selectBoxGroups: [
{
key: 'sex',
options: sexLabels,
title: labels.sex.explainer,
},
{
key: 'contraceptives',
options: contraceptiveLabels,
title: labels.contraceptives.explainer,
}
],
selectTabGroups: null
},
temperature: {
excludeText: temperatureLabels.exclude.explainer,
note: temperatureLabels.note.explainer,
selectBoxGroups: null,
selectTabGroups: null
}
}
export const save = {
bleeding: (data, date, shouldDeleteData) => {
const { exclude, value } = data
const isDataEntered = isNumber(value)
const valuesToSave = shouldDeleteData || !isDataEntered
? null : { value, exclude }
saveSymptom('bleeding', date, valuesToSave)
},
cervix: (data, date, shouldDeleteData) => {
const { opening, firmness, position, exclude } = data
const isDataEntered = ['opening', 'firmness', 'position'].some(
value => isNumber(data[value]))
const valuesToSave = shouldDeleteData || !isDataEntered
? null : { opening, firmness, position, exclude }
saveSymptom('cervix', date, valuesToSave)
},
desire: (data, date, shouldDeleteData) => {
const { value } = data
const valuesToSave = shouldDeleteData || !isNumber(value)
? null : { value }
saveSymptom('desire', date, valuesToSave)
},
mood: (data, date, shouldDeleteData) => {
saveBoxSymptom(data, date, shouldDeleteData, 'mood')
},
mucus: (data, date, shouldDeleteData) => {
const { feeling, texture, exclude } = data
const isDataEntered = ['feeling', 'texture'].some(
value => isNumber(data[value]))
const valuesToSave = shouldDeleteData || !isDataEntered
? null : { feeling, texture, exclude }
saveSymptom('mucus', date, valuesToSave)
},
note: (data, date, shouldDeleteData) => {
const { value } = data
const isValidData = value !== null && value !== ''
const valuesToSave = shouldDeleteData || !isValidData ? null : { value }
saveSymptom('note', date, valuesToSave)
},
pain: (data, date, shouldDeleteData) => {
saveBoxSymptom(data, date, shouldDeleteData, 'pain')
},
sex: (data, date, shouldDeleteData) => {
saveBoxSymptom(data, date, shouldDeleteData, 'sex')
},
temperature: (data, date, shouldDeleteData) => {
const { exclude, note, time, value } = data
const valuesToSave = {
exclude,
note,
time,
value: Number(value)
}
saveSymptom(
'temperature',
date,
(shouldDeleteData || value === null) ? null : valuesToSave
)
}
}
const saveBoxSymptom = (data, date, shouldDeleteData, symptom) => {
const isDataEntered = Object.keys(data).some(key => data[key] !== null)
const valuesToSave = shouldDeleteData || !isDataEntered
? null : data
saveSymptom(symptom, date, valuesToSave)
}
const label = {
bleeding: ({ value, exclude }) => {
if (isNumber(value)) {
const bleedingLabel = bleedingLabels[value]
return exclude ? `(${bleedingLabel})` : bleedingLabel
}
},
temperature: ({ value, time, exclude }) => {
if (isNumber(value)) {
let temperatureLabel = `${value} °C`
if (time) {
temperatureLabel += ` - ${time}`
}
if (exclude) {
temperatureLabel = `(${temperatureLabel})`
}
return temperatureLabel
}
},
mucus: mucus => {
const filledCategories = ['feeling', 'texture'].filter(c => isNumber(mucus[c]))
let label = filledCategories.map(category => {
return labels.mucus.subcategories[category] + ': ' + labels.mucus[category].categories[mucus[category]]
}).join(', ')
if (isNumber(mucus.value)) label += `\n => ${labels.mucusNFP[mucus.value]}`
if (mucus.exclude) label = `(${label})`
return label
},
cervix: cervix => {
const filledCategories = ['opening', 'firmness', 'position'].filter(c => isNumber(cervix[c]))
let label = filledCategories.map(category => {
return labels.cervix.subcategories[category] + ': ' + labels.cervix[category].categories[cervix[category]]
}).join(', ')
if (cervix.exclude) label = `(${label})`
return label
},
note: note => note.value,
desire: ({ value }) => {
if (isNumber(value)) {
return intensityLabels[value]
}
},
sex: sex => {
const sexLabel = []
if (sex && Object.values({...sex}).some(val => val)){
Object.keys(sex).forEach(key => {
if(sex[key] && key !== 'other' && key !== 'note') {
sexLabel.push(
sexLabels[key] ||
contraceptiveLabels[key]
)
}
if(key === 'other' && sex.other) {
let label = contraceptiveLabels[key]
if(sex.note) {
label = `${label} (${sex.note})`
}
sexLabel.push(label)
}
})
return sexLabel.join(', ')
}
},
pain: pain => {
const painLabel = []
if (pain && Object.values({...pain}).some(val => val)){
Object.keys(pain).forEach(key => {
if(pain[key] && key !== 'other' && key !== 'note') {
painLabel.push(painLabels[key])
}
if(key === 'other' && pain.other) {
let label = painLabels[key]
if(pain.note) {
label = `${label} (${pain.note})`
}
painLabel.push(label)
}
})
return painLabel.join(', ')
}
},
mood: mood => {
const moodLabel = []
if (mood && Object.values({...mood}).some(val => val)){
Object.keys(mood).forEach(key => {
if(mood[key] && key !== 'other' && key !== 'note') {
moodLabel.push(moodLabels[key])
}
if(key === 'other' && mood.other) {
let label = moodLabels[key]
if(mood.note) {
label = `${label} (${mood.note})`
}
moodLabel.push(label)
}
})
return moodLabel.join(', ')
}
}
}
export const getData = (symptom, symptomData) => {
return symptomData && label[symptom](symptomData)
}
export const prevDate = (dateString) => {
return LocalDate.parse(dateString).minusDays(1).toString()
}
export const nextDate = (dateString) => {
return LocalDate.parse(dateString).plusDays(1).toString()
}
export const isDateInFuture = (dateString) => {
return LocalDate.now().isBefore(LocalDate.parse(dateString))
}
export const isTomorrowInFuture = (dateString) => {
const tomorrow = nextDate(dateString)
return LocalDate.now().isBefore(LocalDate.parse(tomorrow))
}
export const isYesterdayInFuture = (dateString) => {
const yesterday = prevDate(dateString)
return LocalDate.now().isBefore(LocalDate.parse(yesterday))
}
+11 -1
View File
@@ -1,14 +1,24 @@
import { LocalDate } from 'js-joda'
import moment from 'moment'
import { general as labels } from '../../i18n/en/cycle-day'
export default function (date) {
const today = LocalDate.now()
const dateToDisplay = LocalDate.parse(date)
return today.equals(dateToDisplay) ?
'today' :
labels.today :
moment(date).format('MMMM Do YYYY')
}
export function formatDateForShortText (date) {
return moment(date.toString()).format('dddd, MMMM Do')
}
export function dateToTitle(dateString) {
const today = LocalDate.now()
const dateToDisplay = LocalDate.parse(dateString)
return today.equals(dateToDisplay) ?
labels.today :
moment(dateString).format('MMMM Do')
}
+1 -11
View File
@@ -1,17 +1,8 @@
import symptomViews from './cycle-day/symptoms'
import settingsViews from './settings'
import settingsLabels from '../i18n/en/settings'
const labels = settingsLabels.menuItems
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)
@@ -82,6 +73,5 @@ export const pages = [
{
component: 'CycleDay',
parent: 'Home',
},
...symptomsPages
}
]
+1 -3
View File
@@ -1,7 +1,6 @@
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'
@@ -14,6 +13,5 @@ export const viewsList = {
Chart,
SettingsMenu,
...settingsViews,
Stats,
...symptomViews
Stats
}
+1
View File
@@ -4,6 +4,7 @@ export const ACTION_IMPORT = 'import'
export const SYMPTOMS = [
'bleeding',
'temperature',
'mucus',
'cervix',
'sex',
+6
View File
@@ -31,6 +31,7 @@ export const cervix = {
categories: ['low', 'medium', 'high'],
explainer: 'How high up in the vagina is the cervix?'
},
excludeExplainer: "You can exclude this value if you don't want to use it for fertility detection.",
actionHint: 'Choose values for at least "Opening" and "Firmness" to save.'
}
@@ -132,6 +133,11 @@ export const temperature = {
export const noteExplainer = "Anything you want to add for the day?"
export const general = {
cycleDayNumber: "Cycle day ",
today: "Today"
}
export const sharedDialogs = {
cancel: 'Cancel',
areYouSureTitle: 'Are you sure?',
+21
View File
@@ -1,4 +1,19 @@
import Colors from './colors'
import Spacing from './spacing'
export default {
box: {
borderColor: Colors.orange,
borderRadius: 5,
borderWidth: 1,
marginTop: Spacing.small,
marginRight: Spacing.small,
paddingHorizontal: Spacing.small,
paddingVertical: Spacing.tiny
},
boxActive: {
backgroundColor: Colors.orange,
},
centerItems: {
alignItems: 'center',
flex: 1,
@@ -8,5 +23,11 @@ export default {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between'
},
selectGroupContainer: {
alignItems: 'center',
flexDirection: 'row',
flexWrap: 'wrap',
marginVertical: Spacing.small
}
}
+3 -1
View File
@@ -2,5 +2,7 @@ export default {
tiny: 4,
small: 10,
base: 16,
large: 20
large: 20,
symptomTileWidth: '48%',
textWidth: '70%'
}
+7
View File
@@ -96,5 +96,12 @@ export default {
fontSize: sizes.title,
marginHorizontal: Spacing.base,
...title
},
titleWithoutMargin: {
alignSelf: 'center',
color: Colors.purple,
fontFamily: fonts.bold,
fontWeight: '700',
fontSize: sizes.title,
}
}