Symptom view redesign
This commit is contained in:
committed by
Sofiya Tepikin
parent
ef16cfd041
commit
885da5c293
@@ -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)
|
||||
Reference in New Issue
Block a user