diff --git a/components/bleeding.js b/components/bleeding.js deleted file mode 100644 index 0472c3c..0000000 --- a/components/bleeding.js +++ /dev/null @@ -1,102 +0,0 @@ -import React, { Component } from 'react' -import { - View, - Button, - Text, - Switch -} from 'react-native' -import RadioForm from 'react-native-simple-radio-button' -import styles from '../styles/index' -import { saveBleeding } from '../db' -import { bleeding as labels } from '../labels/labels' - -export default class Bleeding extends Component { - constructor(props) { - super(props) - this.cycleDay = props.cycleDay - this.showView = props.showView - let bleedingValue = this.cycleDay.bleeding && this.cycleDay.bleeding.value - if (! (typeof bleedingValue === 'number') ){ - bleedingValue = -1 - } - this.state = { - currentValue: bleedingValue, - exclude: this.cycleDay.bleeding ? this.cycleDay.bleeding.exclude : false - } - } - - render() { - const bleedingRadioProps = [ - {label: labels[0], value: 0 }, - {label: labels[1], value: 1 }, - {label: labels[2], value: 2 }, - {label: labels[3], value: 3 }, - ] - return ( - - - - - Bleeding - - - { - this.setState({currentValue: itemValue}) - }} - /> - - - - - Exclude - - - { - this.setState({exclude: val}) - }} - value={this.state.exclude} - /> - - - - - - - - - - - - - - - - ) - } -} \ No newline at end of file diff --git a/components/cycle-day-overview.js b/components/cycle-day-overview.js deleted file mode 100644 index 49068fe..0000000 --- a/components/cycle-day-overview.js +++ /dev/null @@ -1,83 +0,0 @@ -import React, { Component } from 'react' -import { - View, - Button, - Text -} from 'react-native' -import styles from '../styles/index' -import { bleeding as labels} from '../labels/labels' -import cycleModule from '../lib/cycle' -import { bleedingDaysSortedByDate } from '../db' - -const getCycleDayNumber = cycleModule().getCycleDayNumber - -export default class DayView extends Component { - constructor(props) { - super(props) - this.cycleDay = props.cycleDay - this.showView = props.showView - this.state = { - cycleDayNumber: getCycleDayNumber(this.cycleDay.date), - } - - this.setStateWithCurrentCycleDayNumber = (function (DayViewComponent) { - return function () { - DayViewComponent.setState({ - cycleDayNumber: getCycleDayNumber(DayViewComponent.cycleDay.date) - }) - } - })(this) - - bleedingDaysSortedByDate.addListener(this.setStateWithCurrentCycleDayNumber) - } - - componentWillUnmount() { - bleedingDaysSortedByDate.removeListener(this.setStateWithCurrentCycleDayNumber) - } - - render() { - const bleedingValue = this.cycleDay.bleeding && this.cycleDay.bleeding.value - let bleedingLabel - if (typeof bleedingValue === 'number') { - bleedingLabel = `${labels[bleedingValue]}` - if (this.cycleDay.bleeding.exclude) bleedingLabel = "( " + bleedingLabel + " )" - } else { - bleedingLabel = 'edit' - } - const temperatureValue = this.cycleDay.temperature && this.cycleDay.temperature.value - let temperatureLabel - if (typeof temperatureValue === 'number') { - temperatureLabel = `${temperatureValue} °C` - if (this.cycleDay.temperature.exclude) temperatureLabel = "( " + temperatureLabel + " )" - } else { - temperatureLabel = 'edit' - } - - return ( - - - - Bleeding - - - - - - - - Temperature - - - - - - - ) - } -} \ No newline at end of file diff --git a/components/cycle-day/action-buttons.js b/components/cycle-day/action-buttons.js new file mode 100644 index 0000000..eb33201 --- /dev/null +++ b/components/cycle-day/action-buttons.js @@ -0,0 +1,46 @@ +import React from 'react' +import { + View, + Button, +} from 'react-native' +import { saveSymptom } from '../../db' + +export default function (showView) { + return function ({ symptom, cycleDay, saveAction, saveDisabled}) { + const buttons = [ + { + title: 'Cancel', + action: () => showView('dayView') + }, + { + title: 'Delete', + action: () => { + saveSymptom(symptom, cycleDay) + showView('dayView') + } + }, { + title: 'Save', + action: () => { + saveAction() + showView('dayView') + }, + disabledCondition: saveDisabled + } + ] + + return buttons.map(({ title, action, disabledCondition }, i) => { + const style = { flex: 1, marginHorizontal: 10 } + if (i === 0) style.marginLeft = 0 + if (i === buttons.length - 1) style.marginRight = 0 + return ( + + + + ) + }) + } +} \ No newline at end of file diff --git a/components/cycle-day/cycle-day-overview.js b/components/cycle-day/cycle-day-overview.js new file mode 100644 index 0000000..d43aab4 --- /dev/null +++ b/components/cycle-day/cycle-day-overview.js @@ -0,0 +1,106 @@ +import React, { Component } from 'react' +import { + View, + Button, + Text +} from 'react-native' +import styles from '../../styles' +import { + bleeding as bleedingLabels, + mucusFeeling as feelingLabels, + mucusTexture as textureLabels, + mucusNFP as computeSensiplanMucusLabels, +} from './labels/labels' +import cycleDayModule from '../../lib/cycle' +import { bleedingDaysSortedByDate } from '../../db' + +const getCycleDayNumber = cycleDayModule().getCycleDayNumber + +export default class DayView extends Component { + constructor(props) { + super(props) + this.cycleDay = props.cycleDay + this.showView = props.showView + this.state = { + cycleDayNumber: getCycleDayNumber(this.cycleDay.date), + } + + this.setStateWithCurrentCycleDayNumber = (function (DayViewComponent) { + return function () { + DayViewComponent.setState({ + cycleDayNumber: getCycleDayNumber(DayViewComponent.cycleDay.date) + }) + } + })(this) + + bleedingDaysSortedByDate.addListener(this.setStateWithCurrentCycleDayNumber) + } + + componentWillUnmount() { + bleedingDaysSortedByDate.removeListener(this.setStateWithCurrentCycleDayNumber) + } + + render() { + const bleedingValue = this.cycleDay.bleeding && this.cycleDay.bleeding.value + let bleedingLabel + if (typeof bleedingValue === 'number') { + bleedingLabel = `${bleedingLabels[bleedingValue]}` + if (this.cycleDay.bleeding.exclude) bleedingLabel = "( " + bleedingLabel + " )" + } else { + bleedingLabel = 'edit' + } + const temperatureValue = this.cycleDay.temperature && this.cycleDay.temperature.value + let temperatureLabel + if (typeof temperatureValue === 'number') { + temperatureLabel = `${temperatureValue} °C - ${this.cycleDay.temperature.time}` + if (this.cycleDay.temperature.exclude) { + temperatureLabel = "( " + temperatureLabel + " )" + } + } else { + temperatureLabel = 'edit' + } + + const mucusFeelingValue = this.cycleDay.mucus && this.cycleDay.mucus.feeling + const mucusTextureValue = this.cycleDay.mucus && this.cycleDay.mucus.texture + const mucusComputedValue = this.cycleDay.mucus && this.cycleDay.mucus.computedNfp + let mucusLabel + if (typeof mucusFeelingValue === 'number' && typeof mucusTextureValue === 'number') { + mucusLabel = `${feelingLabels[mucusFeelingValue]} + ${textureLabels[mucusTextureValue]} ( ${computeSensiplanMucusLabels[mucusComputedValue]} )` + if (this.cycleDay.mucus.exclude) mucusLabel = "( " + mucusLabel + " )" + } else { + mucusLabel = 'edit' + } + + return ( + + + Bleeding + + + + + + Temperature + + + + + + Mucus + + + + + + ) + } +} diff --git a/components/cycle-day.js b/components/cycle-day/index.js similarity index 66% rename from components/cycle-day.js rename to components/cycle-day/index.js index bc3a674..3eeb983 100644 --- a/components/cycle-day.js +++ b/components/cycle-day/index.js @@ -3,13 +3,15 @@ import { View, Text } from 'react-native' -import cycleModule from '../lib/cycle' -import getFertilityStatus from '../lib/sympto-adapter' +import cycleModule from '../../lib/cycle' +import getFertilityStatus from '../../lib/sympto-adapter' import DayView from './cycle-day-overview' -import BleedingEditView from './bleeding' -import TemperatureEditView from './temperature' -import { formatDateForViewHeader } from '../labels/format' -import styles from '../styles/index' +import BleedingEditView from './symptoms/bleeding' +import TemperatureEditView from './symptoms/temperature' +import MucusEditView from './symptoms/mucus' +import { formatDateForViewHeader } from './labels/format' +import styles from '../../styles' +import actionButtonModule from './action-buttons' const getCycleDayNumber = cycleModule().getCycleDayNumber @@ -25,13 +27,15 @@ export default class Day extends Component { this.showView = view => { this.setState({visibleComponent: view}) } + + this.makeActionButtons = actionButtonModule(this.showView) } render() { const cycleDayNumber = getCycleDayNumber(this.cycleDay.date) const fertilityStatus = getFertilityStatus(this.cycleDay.date) return ( - + {formatDateForViewHeader(this.cycleDay.date)} @@ -47,15 +51,16 @@ export default class Day extends Component { {fertilityStatus} - + { { dayView: , - bleedingEditView: , - temperatureEditView: + bleedingEditView: , + temperatureEditView: , + mucusEditView: }[this.state.visibleComponent] } ) } -} \ No newline at end of file +} diff --git a/labels/format.js b/components/cycle-day/labels/format.js similarity index 100% rename from labels/format.js rename to components/cycle-day/labels/format.js diff --git a/components/cycle-day/labels/labels.js b/components/cycle-day/labels/labels.js new file mode 100644 index 0000000..459370b --- /dev/null +++ b/components/cycle-day/labels/labels.js @@ -0,0 +1,11 @@ +const bleeding = ['spotting', 'light', 'medium', 'heavy'] +const mucusFeeling = ['dry', 'nothing', 'wet', 'slippery'] +const mucusTexture = ['nothing', 'creamy', 'egg white'] +const mucusNFP = ['t', 'Ø', 'f', 'S', '+S'] + +export { + bleeding, + mucusFeeling, + mucusTexture, + mucusNFP +} diff --git a/components/cycle-day/symptoms/bleeding.js b/components/cycle-day/symptoms/bleeding.js new file mode 100644 index 0000000..7ec0574 --- /dev/null +++ b/components/cycle-day/symptoms/bleeding.js @@ -0,0 +1,76 @@ +import React, { Component } from 'react' +import { + View, + Text, + Switch +} from 'react-native' +import RadioForm from 'react-native-simple-radio-button' +import styles from '../../../styles' +import { saveSymptom } from '../../../db' +import { bleeding as labels } from '../labels/labels' + +export default class Bleeding extends Component { + constructor(props) { + super(props) + this.cycleDay = props.cycleDay + this.makeActionButtons = props.makeActionButtons + let bleedingValue = this.cycleDay.bleeding && this.cycleDay.bleeding.value + if (!(typeof bleedingValue === 'number')) { + bleedingValue = -1 + } + this.state = { + currentValue: bleedingValue, + exclude: this.cycleDay.bleeding ? this.cycleDay.bleeding.exclude : false + } + } + + render() { + const bleedingRadioProps = [ + { label: labels[0], value: 0 }, + { label: labels[1], value: 1 }, + { label: labels[2], value: 2 }, + { label: labels[3], value: 3 }, + ] + return ( + + Bleeding + + { + this.setState({ currentValue: itemValue }) + }} + /> + + + Exclude + { + this.setState({ exclude: val }) + }} + value={this.state.exclude} + /> + + + {this.makeActionButtons( + { + symptom: 'bleeding', + cycleDay: this.cycleDay, + saveAction: () => { + saveSymptom('bleeding', this.cycleDay, { + value: this.state.currentValue, + exclude: this.state.exclude + }) + }, + saveDisabled: this.state.currentValue === -1 + } + )} + + + ) + } +} \ No newline at end of file diff --git a/components/cycle-day/symptoms/mucus.js b/components/cycle-day/symptoms/mucus.js new file mode 100644 index 0000000..f94a656 --- /dev/null +++ b/components/cycle-day/symptoms/mucus.js @@ -0,0 +1,109 @@ +import React, { Component } from 'react' +import { + View, + Text, + Switch +} from 'react-native' +import RadioForm from 'react-native-simple-radio-button' +import styles from '../../../styles' +import { saveSymptom } from '../../../db' +import { + mucusFeeling as feelingLabels, + mucusTexture as textureLabels +} from '../labels/labels' +import computeSensiplanValue from '../../../lib/sensiplan-mucus' + + +export default class Mucus extends Component { + constructor(props) { + super(props) + this.cycleDay = props.cycleDay + this.makeActionButtons = props.makeActionButtons + this.state = { + exclude: this.cycleDay.mucus ? this.cycleDay.mucus.exclude : false + } + + this.state.currentFeelingValue = this.cycleDay.mucus && this.cycleDay.mucus.feeling + if (typeof this.state.currentFeelingValue !== 'number') { + this.state.currentFeelingValue = -1 + } + + this.state.currentTextureValue = this.cycleDay.mucus && this.cycleDay.mucus.texture + if (typeof this.state.currentTextureValue !== 'number') { + this.state.currentTextureValue = -1 + } + } + + render() { + const mucusFeelingRadioProps = [ + {label: feelingLabels[0], value: 0 }, + {label: feelingLabels[1], value: 1 }, + {label: feelingLabels[2], value: 2 }, + {label: feelingLabels[3], value: 3 } + ] + const mucusTextureRadioProps = [ + {label: textureLabels[0], value: 0 }, + {label: textureLabels[1], value: 1 }, + {label: textureLabels[2], value: 2 } + ] + return( + + Mucus + Feeling + + { + this.setState({ currentFeelingValue: itemValue }) + }} + /> + + Texture + + { + this.setState({ currentTextureValue: itemValue }) + }} + /> + + + Exclude + { + this.setState({ exclude: val }) + }} + value={this.state.exclude} + /> + + + + {this.makeActionButtons( + { + symptom: 'mucus', + cycleDay: this.cycleDay, + saveAction: () => { + saveSymptom('mucus', this.cycleDay, { + feeling: this.state.currentFeelingValue, + texture: this.state.currentTextureValue, + computedNfp: computeSensiplanValue(this.state.currentFeelingValue, this.state.currentTextureValue), + exclude: this.state.exclude + }) + }, + saveDisabled: this.state.currentFeelingValue === -1 || this.state.currentTextureValue === -1 + } + )} + + + + ) + } +} diff --git a/components/cycle-day/symptoms/temperature.js b/components/cycle-day/symptoms/temperature.js new file mode 100644 index 0000000..b9a3d5b --- /dev/null +++ b/components/cycle-day/symptoms/temperature.js @@ -0,0 +1,80 @@ +import React, { Component } from 'react' +import { + View, + Text, + TextInput, + Switch +} from 'react-native' + +import { getPreviousTemperature, saveSymptom } from '../../../db' +import styles from '../../../styles' +import { LocalTime, ChronoUnit } from 'js-joda' + +export default class Temp extends Component { + constructor(props) { + super(props) + this.cycleDay = props.cycleDay + this.makeActionButtons = props.makeActionButtons + let initialValue + + if (this.cycleDay.temperature) { + initialValue = this.cycleDay.temperature.value.toString() + this.time = this.cycleDay.temperature.time + } else { + const prevTemp = getPreviousTemperature(this.cycleDay) + initialValue = prevTemp ? prevTemp.toString() : '' + } + + this.state = { + currentValue: initialValue, + exclude: this.cycleDay.temperature ? this.cycleDay.temperature.exclude : false + } + } + + render() { + const cycleDay = this.cycleDay + return ( + + + Temperature (°C) + { + this.setState({ currentValue: val }) + }} + keyboardType='numeric' + value={this.state.currentValue} + /> + + + Exclude + { + this.setState({ exclude: val }) + }} + value={this.state.exclude} + /> + + + {this.makeActionButtons({ + symptom: 'temperature', + cycleDay: this.cycleDay, + saveAction: () => { + const dataToSave = { + value: Number(this.state.currentValue), + exclude: this.state.exclude + } + if (!cycleDay.temperature || cycleDay.temperature && !cycleDay.temperature.time) { + const now = LocalTime.now().truncatedTo(ChronoUnit.MINUTES).toString() + dataToSave.time = now + } + saveSymptom('temperature', cycleDay, dataToSave) + }, + saveDisabled: this.state.currentValue === '' + })} + + + ) + } +} diff --git a/components/home.js b/components/home.js index 816a10c..6c3795d 100644 --- a/components/home.js +++ b/components/home.js @@ -46,32 +46,28 @@ export default class Home extends Component { render() { const navigate = this.props.navigation.navigate return ( - - - - {this.state.welcomeText} - - - - + + {this.state.welcomeText} + + - + - + - + - - - - - - - - - - ) - } -} diff --git a/db.js b/db.js index 3fdd401..c0533bf 100644 --- a/db.js +++ b/db.js @@ -18,6 +18,16 @@ const BleedingSchema = { } } +const MucusSchema = { + name: 'Mucus', + properties: { + feeling: 'int', + texture: 'int', + computedNfp: 'int', + exclude: 'bool' + } +} + const CycleDaySchema = { name: 'CycleDay', primaryKey: 'date', @@ -30,6 +40,10 @@ const CycleDaySchema = { bleeding: { type: 'Bleeding', optional: true + }, + mucus: { + type: 'Mucus', + optional: true } } } @@ -38,7 +52,8 @@ const db = new Realm({ schema: [ CycleDaySchema, TemperatureSchema, - BleedingSchema + BleedingSchema, + MucusSchema ], // we only want this in dev mode deleteRealmIfMigrationNeeded: true @@ -47,20 +62,14 @@ const db = new Realm({ const bleedingDaysSortedByDate = db.objects('CycleDay').filtered('bleeding != null').sorted('date', true) const temperatureDaysSortedByDate = db.objects('CycleDay').filtered('temperature != null').sorted('date', true) -function saveTemperature(cycleDay, temperature) { +function saveSymptom(symptom, cycleDay, val) { db.write(() => { - cycleDay.temperature = temperature + cycleDay[symptom] = val }) } const cycleDaysSortedByDate = db.objects('CycleDay').sorted('date', true) -function saveBleeding(cycleDay, bleeding) { - db.write(() => { - cycleDay.bleeding = bleeding - }) -} - function getOrCreateCycleDay(localDate) { let result = db.objectForPrimaryKey('CycleDay', localDate) if (!result) { @@ -94,8 +103,7 @@ function getPreviousTemperature(cycleDay) { } export { - saveTemperature, - saveBleeding, + saveSymptom, getOrCreateCycleDay, bleedingDaysSortedByDate, temperatureDaysSortedByDate, diff --git a/lib/sensiplan-mucus.js b/lib/sensiplan-mucus.js new file mode 100644 index 0000000..27e1590 --- /dev/null +++ b/lib/sensiplan-mucus.js @@ -0,0 +1,16 @@ +export default function (feeling, texture) { + const feelingMapping = { + 0: 0, + 1: 1, + 2: 2, + 3: 4 + } + const textureMapping = { + 0: 0, + 1: 3, + 2: 4 + } + const nfpFeelingValue = feelingMapping[feeling] + const nfpTextureValue = textureMapping[texture] + return Math.max(nfpFeelingValue, nfpTextureValue) +} diff --git a/styles/index.js b/styles/index.js index 204e132..52fc65d 100644 --- a/styles/index.js +++ b/styles/index.js @@ -14,66 +14,41 @@ export default StyleSheet.create({ dateHeader: { fontSize: 20, fontWeight: 'bold', - margin: 20, + margin: 15, color: 'white', textAlign: 'center', textAlignVertical: 'center' }, cycleDayNumber: { fontSize: 18, + margin: 15, textAlign: 'center', textAlignVertical: 'center' }, symptomDayView: { fontSize: 20, - margin: 30, - textAlign: 'left', textAlignVertical: 'center' }, radioButton: { fontSize: 18, - margin: 5, + margin: 8, textAlign: 'center', textAlignVertical: 'center' }, - singleButtonView: { - flex: 1, - margin: 5 - }, - itemsInRowView: { - flex: 1, - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center' - }, - itemsInRowSeparatedView: { - flex: 1, - flexDirection: 'row', - justifyContent: 'space-evenly', - alignItems: 'center' - }, symptomEditView: { - flex: 1, - flexDirection: 'column', justifyContent: 'space-between', - alignItems: 'flex-start' + marginHorizontal: 15 }, - symptomEditSplitSymptomsAndLastRowButtons: { - flex: 4, - flexDirection: 'column', - justifyContent: 'flex-start', - alignItems: 'flex-start' + symptomEditRow: { + justifyContent: 'space-between', + marginBottom: 10, }, - symptomEditListedSymptomView: { - flex: 1, - flexDirection: 'column', - justifyContent: 'flex-start', - alignItems: 'flex-start' - }, - cycleDayOuterView: { - flex: 1, - flexDirection: 'column', - justifyContent: 'space-around' + symptomViewRowInline: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 10, + alignItems: 'center', + height: 50 }, cycleDayDateView: { justifyContent: 'center', @@ -82,21 +57,30 @@ export default StyleSheet.create({ cycleDayNumberView: { justifyContent: 'center', backgroundColor: 'skyblue', - padding: 10 + marginBottom: 15 }, - cycleDaySymptomsView: { - flex: 8, - justifyContent: 'center' + homeButtons: { + marginHorizontal: 15 }, - homeContainerView: { - flex: 0.5, - flexDirection: 'column', - justifyContent: 'space-around' + homeButton: { + marginBottom: 15 }, - homeButtonsView: { - flex: 3, - flexDirection: 'column', - justifyContent: 'space-around', - margin: 5 + temperatureTextInput: { + width: 80, + textAlign: 'center', + fontSize: 20 + }, + actionButtonRow: { + flexDirection: 'row', + justifyContent: 'space-evenly', + marginTop: 50 + }, + symptomEditButton: { + width: 130 + }, + radioButtonRow: { + marginTop: 15, + marginLeft: 'auto', + marginRight: 'auto' } }) \ No newline at end of file diff --git a/test/sensiplan-mucus.spec.js b/test/sensiplan-mucus.spec.js new file mode 100644 index 0000000..df1c461 --- /dev/null +++ b/test/sensiplan-mucus.spec.js @@ -0,0 +1,80 @@ +import chai from 'chai' +import dirtyChai from 'dirty-chai' + +const expect = chai.expect +chai.use(dirtyChai) + +import getSensiplanMucus from '../lib/sensiplan-mucus' + +describe('getSensiplanMucus', () => { + + describe('results in t for:', () => { + it('dry feeling and no texture', function () { + const sensiplanValue = getSensiplanMucus(0, 0) + expect(sensiplanValue).to.eql(0) + }) + }) + + describe('results in Ø for:', () => { + it('no feeling and no texture', function () { + const sensiplanValue = getSensiplanMucus(1, 0) + expect(sensiplanValue).to.eql(1) + }) + }) + + describe('results in f for:', () => { + it('wet feeling and no texture', function () { + const sensiplanValue = getSensiplanMucus(2, 0) + expect(sensiplanValue).to.eql(2) + }) + }) + + describe('results in S for:', () => { + it('dry feeling and creamy texture', function () { + const sensiplanValue = getSensiplanMucus(0, 1) + expect(sensiplanValue).to.eql(3) + }) + + it('no feeling and creamy texture', function () { + const sensiplanValue = getSensiplanMucus(1, 1) + expect(sensiplanValue).to.eql(3) + }) + + it('wet feeling and creamy texture', function () { + const sensiplanValue = getSensiplanMucus(2, 1) + expect(sensiplanValue).to.eql(3) + }) + }) + + describe('results in +S for:', () => { + it('dry feeling and egg white texture', function () { + const sensiplanValue = getSensiplanMucus(0, 2) + expect(sensiplanValue).to.eql(4) + }) + + it('no feeling and egg white texture', function () { + const sensiplanValue = getSensiplanMucus(1, 2) + expect(sensiplanValue).to.eql(4) + }) + + it('wet feeling and egg white texture', function () { + const sensiplanValue = getSensiplanMucus(2, 2) + expect(sensiplanValue).to.eql(4) + }) + + it('slippery feeling and egg white texture', function () { + const sensiplanValue = getSensiplanMucus(3, 2) + expect(sensiplanValue).to.eql(4) + }) + + it('slippery feeling and creamy texture', function () { + const sensiplanValue = getSensiplanMucus(3, 1) + expect(sensiplanValue).to.eql(4) + }) + + it('slippery feeling and no texture', function () { + const sensiplanValue = getSensiplanMucus(3, 0) + expect(sensiplanValue).to.eql(4) + }) + }) +})