Symptom view redesign
This commit is contained in:
committed by
Sofiya Tepikin
parent
ef16cfd041
commit
885da5c293
+11
-13
@@ -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 &&
|
||||
<Header { ...headerProps } />
|
||||
}
|
||||
|
||||
<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),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -2,14 +2,23 @@ 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]
|
||||
const textStyle = [textCTA, isSmall ? textSmall : text]
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
@@ -33,7 +33,7 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderStyle: 'solid',
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: Colors.grey,
|
||||
borderBottomColor: Colors.greyLight,
|
||||
paddingBottom: Spacing.base,
|
||||
...segmentContainer
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
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>
|
||||
</View>
|
||||
<View style={styles.container}>
|
||||
{SYMPTOMS.map(symptom => {
|
||||
const symptomData = cycleDay && cycleDay[symptom]
|
||||
? cycleDay[symptom] : null
|
||||
|
||||
return(
|
||||
<SymptomBox
|
||||
key={symptom}
|
||||
symptom={symptom}
|
||||
symptomData={symptomData}
|
||||
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),
|
||||
|
||||
@@ -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>
|
||||
<AppText style={textStyle}>{labels[key]}</AppText>
|
||||
</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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export const ACTION_IMPORT = 'import'
|
||||
|
||||
export const SYMPTOMS = [
|
||||
'bleeding',
|
||||
'temperature',
|
||||
'mucus',
|
||||
'cervix',
|
||||
'sex',
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -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
@@ -2,5 +2,7 @@ export default {
|
||||
tiny: 4,
|
||||
small: 10,
|
||||
base: 16,
|
||||
large: 20
|
||||
large: 20,
|
||||
symptomTileWidth: '48%',
|
||||
textWidth: '70%'
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user