diff --git a/android/app/src/main/assets/fonts/Prompt-Light.ttf b/android/app/src/main/assets/fonts/Prompt-Light.ttf new file mode 100644 index 0000000..e00a747 Binary files /dev/null and b/android/app/src/main/assets/fonts/Prompt-Light.ttf differ diff --git a/android/app/src/main/assets/fonts/Prompt-Thin.ttf b/android/app/src/main/assets/fonts/Prompt-Thin.ttf new file mode 100644 index 0000000..2767999 Binary files /dev/null and b/android/app/src/main/assets/fonts/Prompt-Thin.ttf differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 2f5e5f8..6f1943f 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,12 +1,12 @@ - #ff7e5f + #000D19 - #c74e34 + #000D19 - #351c4d + #4FAFA7 \ No newline at end of file diff --git a/assets/bleeding.js b/assets/bleeding.js new file mode 100644 index 0000000..d2f6772 --- /dev/null +++ b/assets/bleeding.js @@ -0,0 +1,30 @@ +import React from 'react' +import { G, Path } from 'react-native-svg' + +export default function BleedingIcon() { + return ( + + + + + + + + + + + + + ) +} diff --git a/assets/cervix.js b/assets/cervix.js new file mode 100644 index 0000000..bab3667 --- /dev/null +++ b/assets/cervix.js @@ -0,0 +1,52 @@ +import React from 'react' +import { G, Path } from 'react-native-svg' + +export default function CervixIcon() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/assets/desire.js b/assets/desire.js new file mode 100644 index 0000000..aba2415 --- /dev/null +++ b/assets/desire.js @@ -0,0 +1,35 @@ +import React from 'react' +import { G, Path } from 'react-native-svg' + +export default function DesireIcon() { + return ( + + + + + + + + + + + + + ) +} diff --git a/assets/fonts/Prompt-Light.ttf b/assets/fonts/Prompt-Light.ttf new file mode 100644 index 0000000..e00a747 Binary files /dev/null and b/assets/fonts/Prompt-Light.ttf differ diff --git a/assets/fonts/Prompt-Thin.ttf b/assets/fonts/Prompt-Thin.ttf new file mode 100644 index 0000000..2767999 Binary files /dev/null and b/assets/fonts/Prompt-Thin.ttf differ diff --git a/assets/mucus.js b/assets/mucus.js new file mode 100644 index 0000000..cc721af --- /dev/null +++ b/assets/mucus.js @@ -0,0 +1,75 @@ +import React from 'react' +import { G, Path } from 'react-native-svg' + +export default function MucusIcon() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/assets/note.js b/assets/note.js new file mode 100644 index 0000000..3413143 --- /dev/null +++ b/assets/note.js @@ -0,0 +1,41 @@ +import React from 'react' +import { G, Path } from 'react-native-svg' + +export default function NoteIcon() { + return ( + + + + + + + + + + + + + ) +} diff --git a/assets/pain.js b/assets/pain.js new file mode 100644 index 0000000..dae6ea8 --- /dev/null +++ b/assets/pain.js @@ -0,0 +1,25 @@ +import React from 'react' +import { G, Path } from 'react-native-svg' + +export default function PainIcon() { + return ( + + + + + + + + ) +} diff --git a/assets/sex.js b/assets/sex.js new file mode 100644 index 0000000..db8e175 --- /dev/null +++ b/assets/sex.js @@ -0,0 +1,52 @@ +import React from 'react' +import { G, Path } from 'react-native-svg' + +export default function SexIcon() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/assets/temperature.js b/assets/temperature.js new file mode 100644 index 0000000..403d88e --- /dev/null +++ b/assets/temperature.js @@ -0,0 +1,48 @@ +import React from 'react' +import { G, Path } from 'react-native-svg' + +export default function TemperatureIcon() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/components/app.js b/components/app.js index 94472a1..aea1c6f 100644 --- a/components/app.js +++ b/components/app.js @@ -9,9 +9,20 @@ import symptomViews from './cycle-day/symptoms' import Chart from './chart/chart' import Settings from './settings' import Stats from './stats' -import {headerTitles as titles} from './labels' +import {headerTitles, menuTitles} from './labels' import setupNotifications from '../lib/notifications' +// design wants everyhting lowercased, but we don't +// have CSS pseudo properties +const headerTitlesLowerCase = Object.keys(headerTitles).reduce((acc, curr) => { + acc[curr] = headerTitles[curr].toLowerCase() + return acc +}, {}) +const menuTitlesLowerCase = Object.keys(menuTitles).reduce((acc, curr) => { + acc[curr] = menuTitles[curr].toLowerCase() + return acc +}, {}) + const isSymptomView = name => Object.keys(symptomViews).indexOf(name) > -1 export default class App extends Component { @@ -55,25 +66,28 @@ export default class App extends Component { }[this.state.currentPage] return ( - {this.state.currentPage != 'CycleDay' && !isSymptomView(this.state.currentPage) &&
} {isSymptomView(this.state.currentPage) &&
} + {React.createElement(page, { navigate: this.navigate, ...this.state.currentProps })} {!isSymptomView(this.state.currentPage) && - + } ) diff --git a/components/chart/chart.js b/components/chart/chart.js index 201f0f8..ccffbba 100644 --- a/components/chart/chart.js +++ b/components/chart/chart.js @@ -10,6 +10,7 @@ import styles from './styles' import { scaleObservable } from '../../local-storage' import config from '../../config' import { AppText } from '../app-text' +import { shared as labels } from '../labels' export default class CycleChart extends Component { constructor(props) { @@ -111,7 +112,7 @@ export default class CycleChart extends Component { (cycleDay.cervix.opening + cycleDay.cervix.firmness) } else if (symptom === 'sex') { // solo = 1 + partner = 2 - acc.sex = cycleDay.sex && (cycleDay.sex.solo + cycleDay.sex.partner) + acc.sex = cycleDay.sex && (cycleDay.sex.solo + 2 * cycleDay.sex.partner) } else if (symptom === 'pain') { // is any pain documented? acc.pain = cycleDay.pain && @@ -144,20 +145,33 @@ export default class CycleChart extends Component { > {!this.state.chartLoaded && - Loading... + {labels.loading} } {this.state.chartHeight && this.state.chartLoaded && - - {makeYAxisLabels(this.columnHeight)} + + + {this.symptomRowSymptoms.map(symptomName => { + return + {symptomName[0]} + + })} + + + {makeYAxisLabels(this.columnHeight)} + + + + {labels.cycleDayWithLinebreak} + + + {labels.date} + + } + {this.state.chartHeight && this.state.chartLoaded && makeHorizontalGrid(this.columnHeight, this.symptomRowHeight) } diff --git a/components/chart/day-column.js b/components/chart/day-column.js index f2ed95b..ec3dd95 100644 --- a/components/chart/day-column.js +++ b/components/chart/day-column.js @@ -3,7 +3,8 @@ import { Text, View, TouchableOpacity } from 'react-native' import Svg,{ G, Rect, Line } from 'react-native-svg' -import Icon from 'react-native-vector-icons/Entypo' +import { LocalDate } from 'js-joda' +import moment from 'moment' import styles from './styles' import config from '../../config' import { getOrCreateCycleDay } from '../../db' @@ -88,13 +89,18 @@ export default class DayColumn extends Component { } const cycleDayNumber = this.getCycleDayNumber(dateString) - const shortDate = dateString.split('-').slice(1).join('-') + const dayDate = LocalDate.parse(dateString) + const shortDate = dayDate.dayOfMonth() === 1 ? + moment(dateString, "YYYY-MM-DD").format('MMM') + : + moment(dateString, "YYYY-MM-DD").format('Do') + const boldDateLabel = dayDate.dayOfMonth() === 1 ? {fontWeight: 'bold'} : {} const cycleDayLabel = ( {cycleDayNumber ? cycleDayNumber : ' '} ) const dateLabel = ( - + {shortDate} ) @@ -116,10 +122,9 @@ export default class DayColumn extends Component { symptomHeight={symptomHeight} key='bleeding' > - ), @@ -130,8 +135,8 @@ export default class DayColumn extends Component { key='mucus' > ), @@ -142,9 +147,9 @@ export default class DayColumn extends Component { key='cervix' > 0 ? 'blue' : 'green'} + backgroundColor={this.props.cervix > 0 ? styles.iconShades.cervix[2] : styles.iconShades.cervix[0]} /> ), @@ -155,8 +160,8 @@ export default class DayColumn extends Component { key='sex' > ), @@ -167,8 +172,8 @@ export default class DayColumn extends Component { key='desire' > ), @@ -179,8 +184,8 @@ export default class DayColumn extends Component { key='pain' > ), @@ -191,8 +196,8 @@ export default class DayColumn extends Component { key='note' > ) diff --git a/components/chart/styles.js b/components/chart/styles.js index 8441f9b..8cc044f 100644 --- a/components/chart/styles.js +++ b/components/chart/styles.js @@ -8,6 +8,7 @@ const lineWidth = 1.5 const colorLtl = '#feb47b' const gridColor = 'lightgrey' const gridLineWidth = 0.5 +const numberLabelFontSize = 13 const styles = { curve: { @@ -32,10 +33,11 @@ const styles = { color: 'grey', fontSize: 9, fontWeight: '100', + textAlign: 'center', }, number: { color: primaryColor, - fontSize: 13, + fontSize: numberLabelFontSize, textAlign: 'center', } }, @@ -48,37 +50,62 @@ const styles = { fill: 'transparent' } }, - bleedingIcon: { - fill: '#fb2e01', - scale: 0.6, - x: 6, - y: 3 - }, - bleedingIconShades: shadesOfRed, - mucusIcon: { + symptomIcon: { width: 12, height: 12, borderRadius: 50, }, - mucusIconShades: [ - '#fef0e4', - '#fee1ca', - '#fed2af', - '#fec395', - '#feb47b' - ], + iconShades: { + 'bleeding': shadesOfRed, + 'mucus': [ + '#e3e7ed', + '#c8cfdc', + '#acb8cb', + '#91a0ba', + '#7689a9' + ], + 'cervix': [ + '#f0e19d', + '#e9d26d', + '#e2c33c', + '#dbb40c', + ], + 'sex': [ + '#a87ca2', + '#8b5083', + '#6f2565', + ], + 'desire': [ + '#c485a6', + '#b15c89', + '#9e346c', + ], + 'pain': ['#bccd67'], + 'note': ['#6CA299'] + }, yAxis: { width: 27, - borderRightWidth: 0.5, + borderRightWidth: 1, borderColor: 'lightgrey', borderStyle: 'solid' }, - yAxisLabel: { - position: 'absolute', - left: 3, - color: 'grey', - fontSize: 11, - textAlign: 'left' + yAxisLabels: { + tempScale: { + position: 'absolute', + right: 2, + color: 'grey', + fontSize: 9, + textAlign: 'left' + }, + cycleDayLabel: { + textAlign: 'center', + justifyContent: 'center', + fontSize: Math.ceil(numberLabelFontSize / 2) + }, + dateLabel: { + textAlign: 'center', + justifyContent: 'center' + } }, horizontalGrid: { position:'absolute', diff --git a/components/chart/y-axis.js b/components/chart/y-axis.js index 12fa08c..e1e0d6f 100644 --- a/components/chart/y-axis.js +++ b/components/chart/y-axis.js @@ -8,7 +8,7 @@ import { AppText } from '../app-text' export function makeYAxisLabels(columnHeight) { const units = unitObservable.value const scaleMax = scaleObservable.value.max - const style = styles.yAxisLabel + const style = styles.yAxisLabels.tempScale return getTickPositions(columnHeight).map((y, i) => { const tick = scaleMax - i * units @@ -17,10 +17,10 @@ export function makeYAxisLabels(columnHeight) { let tickBold if (units === 0.1) { showTick = (tick * 10 % 2) ? false : true - tickBold = tick * 10 % 5 ? {} : {fontWeight: 'bold'} + tickBold = tick * 10 % 5 ? {} : {fontWeight: 'bold', fontSize: 11} } else { showTick = (tick * 10 % 5) ? false : true - tickBold = tick * 10 % 10 ? {} : {fontWeight: 'bold'} + tickBold = tick * 10 % 10 ? {} : {fontWeight: 'bold', fontSize: 11} } // this eyeballing is sadly necessary because RN does not // support percentage values for transforms, which we'd need diff --git a/components/cycle-day/cycle-day-overview.js b/components/cycle-day/cycle-day-overview.js index 9823165..775e299 100644 --- a/components/cycle-day/cycle-day-overview.js +++ b/components/cycle-day/cycle-day-overview.js @@ -6,13 +6,21 @@ import { Dimensions } from 'react-native' import { LocalDate } from 'js-joda' +import Svg, { G } from 'react-native-svg' import Header from '../header' import { getOrCreateCycleDay } from '../../db' import cycleModule from '../../lib/cycle' -import Icon from 'react-native-vector-icons/FontAwesome' -import styles, { iconStyles } from '../../styles' +import styles from '../../styles' import * as labels from './labels/labels' import { AppText } from '../app-text' +import BleedingIcon from '../../assets/bleeding' +import CervixIcon from '../../assets/cervix' +import DesireIcon from '../../assets/desire' +import MucusIcon from '../../assets/mucus' +import NoteIcon from '../../assets/note' +import PainIcon from '../../assets/pain' +import SexIcon from '../../assets/sex' +import TemperatureIcon from '../../assets/temperature' const bleedingLabels = labels.bleeding const feelingLabels = labels.mucus.feeling.categories @@ -68,47 +76,64 @@ export default class CycleDayOverView extends Component { onPress={() => this.navigate('BleedingEditView')} data={getLabel('bleeding', cycleDay.bleeding)} disabled={dateInFuture} - /> + > + + this.navigate('TemperatureEditView')} data={getLabel('temperature', cycleDay.temperature)} disabled={dateInFuture} - /> + > + + this.navigate('MucusEditView')} data={getLabel('mucus', cycleDay.mucus)} disabled={dateInFuture} - /> + > + + this.navigate('CervixEditView')} data={getLabel('cervix', cycleDay.cervix)} disabled={dateInFuture} - /> + > + + this.navigate('DesireEditView')} data={getLabel('desire', cycleDay.desire)} disabled={dateInFuture} - /> + > + + this.navigate('SexEditView')} data={getLabel('sex', cycleDay.sex)} disabled={dateInFuture} - /> + > + + this.navigate('PainEditView')} data={getLabel('pain', cycleDay.pain)} - /> + disabled={dateInFuture} + > + + this.navigate('NoteEditView')} data={getLabel('note', cycleDay.note)} - /> + > + + {/* this is just to make the last row adhere to the grid (and) because there are no pseudo properties in RN */} @@ -221,10 +246,6 @@ class SymptomBox extends Component { render() { const d = this.props.data const boxActive = d ? styles.symptomBoxActive : {} - const iconActive = d ? iconStyles.symptomBoxActive : {} - const iconStyle = Object.assign( - {}, iconStyles.symptomBox, iconActive, disabledStyle - ) const textActive = d ? styles.symptomTextActive : {} const disabledStyle = this.props.disabled ? styles.symptomInFuture : {} @@ -234,10 +255,20 @@ class SymptomBox extends Component { disabled={this.props.disabled} > - + + {this.props.children ? + React.Children.map(this.props.children, child => { + return ( + + + {child} + + + ) + }) + : null + } + {this.props.title} diff --git a/components/cycle-day/labels/labels.js b/components/cycle-day/labels/labels.js index 0758cdd..aeff7bc 100644 --- a/components/cycle-day/labels/labels.js +++ b/components/cycle-day/labels/labels.js @@ -43,6 +43,8 @@ export const sex = { patch: 'Patch', ring: 'Ring', implant: 'Implant', + diaphragm: 'Diaphragm', + none: 'None', other: 'Other', activityExplainer: 'Were you sexually active today?', contraceptiveExplainer: 'Did you use contraceptives?' diff --git a/components/cycle-day/symptoms/action-button-footer.js b/components/cycle-day/symptoms/action-button-footer.js index d990f48..15f22eb 100644 --- a/components/cycle-day/symptoms/action-button-footer.js +++ b/components/cycle-day/symptoms/action-button-footer.js @@ -53,7 +53,8 @@ export default class ActionButtonFooter extends Component { return ( {buttons.map(({ title, action, disabledCondition, icon }, i) => { - const textStyle = disabledCondition ? styles.menuTextInActive : styles.menuText + const textStyle = [styles.menuText] + if (disabledCondition) textStyle.push(styles.menuTextInActive) const iconStyle = disabledCondition ? Object.assign({}, iconStyles.menuIcon, iconStyles.menuIconInactive) : iconStyles.menuIcon diff --git a/components/cycle-day/symptoms/sex.js b/components/cycle-day/symptoms/sex.js index a51b5c0..1e14b06 100644 --- a/components/cycle-day/symptoms/sex.js +++ b/components/cycle-day/symptoms/sex.js @@ -37,6 +37,12 @@ const contraceptiveBoxes = [{ }, { label: labels.implant, stateKey: 'implant' +}, { + label: labels.diaphragm, + stateKey: 'diaphragm' +}, { + label: labels.none, + stateKey: 'none' }, { label: labels.other, stateKey: 'other' diff --git a/components/header.js b/components/header.js index 94a0119..c9da0c7 100644 --- a/components/header.js +++ b/components/header.js @@ -1,7 +1,8 @@ import React, { Component } from 'react' import { View, - Text + Text, + Dimensions } from 'react-native' import styles, { iconStyles } from '../styles' import Icon from 'react-native-vector-icons/MaterialCommunityIcons' @@ -10,9 +11,11 @@ import { formatDateForViewHeader } from '../components/cycle-day/labels/format' export default class Header extends Component { render() { + const middle = Dimensions.get('window').width / 2 return ( this.props.isCycleDayOverView ? + : this.props.isSymptomView ? + : - + + {this.props.title} diff --git a/components/labels.js b/components/labels.js index 7fde2b8..e728bd5 100644 --- a/components/labels.js +++ b/components/labels.js @@ -8,7 +8,10 @@ export const shared = { incorrectPasswordMessage: 'That password is incorrect.', tryAgain: 'Try again', ok: 'OK', - unlock: 'Unlock' + unlock: 'Unlock', + date: 'Date', + cycleDayWithLinebreak: 'Cycle\nday', + loading: 'Loading ...' } export const settings = { @@ -54,6 +57,11 @@ export const settings = { timeSet: time => `Daily reminder set for ${time}`, notification: 'Record your morning temperature' }, + periodReminder: { + title: 'Next period reminder', + reminderText: 'Get a notification 3 days before your next period is likely to start.', + notification: daysToEndOfPrediction => `Your next period is likely to start in 3 to ${daysToEndOfPrediction} days.` + }, passwordSettings: { title: 'App password', explainerDisabled: "Encrypt the app's database with a password. You need to enter the password every time the app is started.", @@ -86,6 +94,14 @@ export const headerTitles = { PainEditView: 'Pain' } +export const menuTitles = { + Home: 'Home', + Calendar: 'Calendar', + Chart: 'Chart', + Stats: 'Stats', + Settings: 'Settings', +} + export const stats = { cycleLengthTitle: 'Cycle length', cycleLengthExplainer: 'Basic statistics about the length of your cycles.', diff --git a/components/menu.js b/components/menu.js index 737203b..8bdbb43 100644 --- a/components/menu.js +++ b/components/menu.js @@ -28,14 +28,15 @@ export default class Menu extends Component { } render() { + const t = this.props.titles return ( {[ - { title: 'Home', icon: 'home', onPress: () => this.goTo('Home') }, - { title: 'Calendar', icon: 'calendar-range', onPress: () => this.goTo('Calendar') }, - { title: 'Chart', icon: 'chart-line', onPress: () => this.goTo('Chart') }, - { title: 'Stats', icon: 'chart-pie', onPress: () => this.goTo('Stats') }, - { title: 'Settings', icon: 'settings', onPress: () => this.goTo('Settings') }, + { title: t.Home, icon: 'home', onPress: () => this.goTo('Home') }, + { title: t.Calendar, icon: 'calendar-range', onPress: () => this.goTo('Calendar') }, + { title: t.Chart, icon: 'chart-line', onPress: () => this.goTo('Chart') }, + { title: t.Stats, icon: 'chart-pie', onPress: () => this.goTo('Stats') }, + { title: t.Settings, icon: 'settings', onPress: () => this.goTo('Settings') }, ].map(this.makeMenuItem)} ) diff --git a/components/settings/index.js b/components/settings/index.js index e0828d8..1180f24 100644 --- a/components/settings/index.js +++ b/components/settings/index.js @@ -8,6 +8,7 @@ import styles from '../../styles/index' import { settings as labels } from '../labels' import { AppText } from '../app-text' import TempReminderPicker from './temp-reminder-picker' +import PeriodReminderPicker from './period-reminder' import TempSlider from './temp-slider' import openImportDialogAndImport from './import-dialog' import openShareDialogAndExport from './export-dialog' @@ -30,6 +31,7 @@ export default class Settings extends Component { {labels.tempScale.segmentExplainer} + diff --git a/components/settings/period-reminder.js b/components/settings/period-reminder.js new file mode 100644 index 0000000..70ec0fc --- /dev/null +++ b/components/settings/period-reminder.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react' +import { + View, + Switch +} from 'react-native' +import { AppText } from '../app-text' +import { + periodReminderObservable, + savePeriodReminder +} from '../../local-storage' +import styles from '../../styles/index' +import { settings as labels } from '../labels' + +export default class PeriodReminderPicker extends Component { + constructor(props) { + super(props) + this.state = periodReminderObservable.value + } + + render() { + return ( + + + {labels.periodReminder.title} + + + + {labels.periodReminder.reminderText} + + { + this.setState({ enabled: switchOn }) + savePeriodReminder({enabled: switchOn}) + }} + /> + + + ) + } +} \ No newline at end of file diff --git a/db/fixtures.js b/db/fixtures.js index fd4a949..421bcb8 100644 --- a/db/fixtures.js +++ b/db/fixtures.js @@ -1,16 +1,25 @@ function convertToSymptoFormat(val) { const sympto = { date: val.date } + if (val.bleeding) sympto.bleeding = { + value: val.bleeding, + exclude: false + } if (val.temperature) sympto.temperature = { value: val.temperature, + time: '08:00', exclude: false } if (val.mucus) sympto.mucus = { value: val.mucus, - exclude: false, feeling: val.mucus, - texture: val.mucus + texture: val.mucus, + exclude: false + } + if (val.cervix && typeof val.cervix.opening === 'number' && typeof val.cervix.firmness === 'number') sympto.cervix = { + opening: val.cervix.opening, + firmness: val.cervix.firmness, + exclude: false } - if (val.bleeding) sympto.bleeding = { value: val.bleeding, exclude: false } return sympto } @@ -75,7 +84,7 @@ export const cycleWithTempAndNoMucusShift = [ { date: '2018-05-27', temperature: 36.9, mucus: 4 } ].map(convertToSymptoFormat).reverse() -export const cycleWithFhmCervix = [ +export const cervixShiftAndFhmOnSameDay = [ { date: '2018-08-01', bleeding: 2 }, { date: '2018-08-02', bleeding: 1 }, { date: '2018-08-03', bleeding: 0 }, diff --git a/db/index.js b/db/index.js index 6dc4feb..34afd23 100644 --- a/db/index.js +++ b/db/index.js @@ -7,17 +7,42 @@ import { cycleWithFhmMucus, longAndComplicatedCycleWithMucus, cycleWithTempAndNoMucusShift, - cycleWithFhmCervix, + cervixShiftAndFhmOnSameDay, longAndComplicatedCycleWithCervix, cycleWithTempAndNoCervixShift } from './fixtures' -import dbSchema from './schema' +import schemas from './schemas' let db -const realmConfig = { - schema: dbSchema + +export async function openDb ({ hash, persistConnection }) { + const realmConfig = {} + if (hash) { + realmConfig.encryptionKey = hashToInt8Array(hash) + } + + // perform migrations if necessary, see https://realm.io/docs/javascript/2.8.0/#migrations + let nextSchemaIndex = Realm.schemaVersion(Realm.defaultPath) + while (nextSchemaIndex < schemas.length - 1) { + const tempConfig = Object.assign( + realmConfig, + schemas[nextSchemaIndex++] + ) + const migratedRealm = new Realm(tempConfig) + migratedRealm.close() + } + + // open the Realm with the latest schema + realmConfig.schema = schemas[schemas.length - 1] + const connection = await Realm.open(Object.assign( + realmConfig, + schemas[schemas.length - 1] + )) + + if (persistConnection) db = connection } + export function getBleedingDaysSortedByDate() { return db.objects('CycleDay').filtered('bleeding != null').sorted('date', true) } @@ -77,7 +102,7 @@ export function fillWithMucusDummyData() { export function fillWithCervixDummyData() { const dummyCycles = [ - cycleWithFhmCervix, + cervixShiftAndFhmOnSameDay, longAndComplicatedCycleWithCervix, cycleWithTempAndNoCervixShift ] @@ -160,16 +185,6 @@ export function requestHash(type, pw) { })) } -export async function openDb ({ hash, persistConnection }) { - if (hash) { - realmConfig.encryptionKey = hashToInt8Array(hash) - } - - const connection = await Realm.open(realmConfig) - - if (persistConnection) db = connection -} - export async function changeEncryptionAndRestartApp(hash) { let key if (hash) key = hashToInt8Array(hash) diff --git a/db/schema.js b/db/schemas/0.js similarity index 91% rename from db/schema.js rename to db/schemas/0.js index 8e8792a..1387195 100644 --- a/db/schema.js +++ b/db/schemas/0.js @@ -127,14 +127,17 @@ const CycleDaySchema = { } } -export default [ - CycleDaySchema, - TemperatureSchema, - BleedingSchema, - MucusSchema, - CervixSchema, - NoteSchema, - DesireSchema, - SexSchema, - PainSchema -] \ No newline at end of file +export default { + schema: [ + CycleDaySchema, + TemperatureSchema, + BleedingSchema, + MucusSchema, + CervixSchema, + NoteSchema, + DesireSchema, + SexSchema, + PainSchema + ], + schemaVersion: 0 +} diff --git a/db/schemas/1.js b/db/schemas/1.js new file mode 100644 index 0000000..572a221 --- /dev/null +++ b/db/schemas/1.js @@ -0,0 +1,156 @@ +const TemperatureSchema = { + name: 'Temperature', + properties: { + value: 'double', + exclude: 'bool', + time: { + type: 'string', + optional: true + }, + note: { + type: 'string', + optional: true + } + } +} + +const BleedingSchema = { + name: 'Bleeding', + properties: { + value: 'int', + exclude: 'bool' + } +} + +const MucusSchema = { + name: 'Mucus', + properties: { + feeling: 'int', + texture: 'int', + value: 'int', + exclude: 'bool' + } +} + +const CervixSchema = { + name: 'Cervix', + properties: { + opening: 'int', + firmness: 'int', + position: {type: 'int', optional: true }, + exclude: 'bool' + } +} + +const NoteSchema = { + name: 'Note', + properties: { + value: 'string' + } +} + +const DesireSchema = { + name: 'Desire', + properties: { + value: 'int' + } +} + +const SexSchema = { + name: 'Sex', + properties: { + solo: { type: 'bool', optional: true }, + partner: { type: 'bool', optional: true }, + condom: { type: 'bool', optional: true }, + pill: { type: 'bool', optional: true }, + iud: { type: 'bool', optional: true }, + patch: { type: 'bool', optional: true }, + ring: { type: 'bool', optional: true }, + implant: { type: 'bool', optional: true }, + diaphragm: { type: 'bool', optional: true }, + none: { type: 'bool', optional: true }, + other: { type: 'bool', optional: true }, + note: { type: 'string', optional: true } + } +} + +const PainSchema = { + name: 'Pain', + properties: { + cramps: { type: 'bool', optional: true }, + ovulationPain: { type: 'bool', optional: true }, + headache: { type: 'bool', optional: true }, + backache: { type: 'bool', optional: true }, + nausea: { type: 'bool', optional: true }, + tenderBreasts: { type: 'bool', optional: true }, + migraine: { type: 'bool', optional: true }, + other: { type: 'bool', optional: true }, + note: { type: 'string', optional: true } + } +} + +const CycleDaySchema = { + name: 'CycleDay', + primaryKey: 'date', + properties: { + date: 'string', + temperature: { + type: 'Temperature', + optional: true + }, + bleeding: { + type: 'Bleeding', + optional: true + }, + mucus: { + type: 'Mucus', + optional: true + }, + cervix: { + type: 'Cervix', + optional: true + }, + note: { + type: 'Note', + optional: true + }, + desire: { + type: 'Desire', + optional: true + }, + sex: { + type: 'Sex', + optional: true + }, + pain: { + type: 'Pain', + optional: true + } + } +} + +export default { + schema: [ + CycleDaySchema, + TemperatureSchema, + BleedingSchema, + MucusSchema, + CervixSchema, + NoteSchema, + DesireSchema, + SexSchema, + PainSchema + ], + schemaVersion: 1, + migration: (oldRealm, newRealm) => { + if (oldRealm.schemaVersion >= 1) return + const oldCycleDays = oldRealm.objects('CycleDay') + const newCycleDays = newRealm.objects('CycleDay') + + oldCycleDays.forEach((day, i) => { + if (!day.sex) return + newCycleDays[i].sex.diaphragm = null + newCycleDays[i].sex.none = null + }) + } +} diff --git a/db/schemas/index.js b/db/schemas/index.js new file mode 100644 index 0000000..eacf7dd --- /dev/null +++ b/db/schemas/index.js @@ -0,0 +1,4 @@ +import schema0 from './0.js' +import schema1 from './1.js' + +export default [schema0, schema1] \ No newline at end of file diff --git a/ios/drip.xcodeproj/project.pbxproj b/ios/drip.xcodeproj/project.pbxproj index c6a9665..ff698e3 100644 --- a/ios/drip.xcodeproj/project.pbxproj +++ b/ios/drip.xcodeproj/project.pbxproj @@ -747,9 +747,9 @@ 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 2B572382D4504B8FB4B9D251 /* Embed Frameworks */, - 7DDFD19623084447885928A6 /* Build NodeJS Mobile Native Modules */, - 554E2494DF2646B083F4BD1D /* Sign NodeJS Mobile Native Modules */, - 8F5D6E75B7D344BD80BC6EC0 /* Remove NodeJS Mobile Framework Simulator Strips */, + 2916172A40DD44AE85EB76AF /* Build NodeJS Mobile Native Modules */, + E93078AE736B464D9A7409A4 /* Sign NodeJS Mobile Native Modules */, + E95128D078C34495AFAAA808 /* Remove NodeJS Mobile Framework Simulator Strips */, ); buildRules = ( ); @@ -1232,7 +1232,7 @@ shellPath = /bin/sh; shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh"; }; - 7DDFD19623084447885928A6 /* Build NodeJS Mobile Native Modules */ = { + 2916172A40DD44AE85EB76AF /* Build NodeJS Mobile Native Modules */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1300,7 +1300,7 @@ fi popd "; }; - 554E2494DF2646B083F4BD1D /* Sign NodeJS Mobile Native Modules */ = { + E93078AE736B464D9A7409A4 /* Sign NodeJS Mobile Native Modules */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1360,7 +1360,7 @@ find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -path \"*/*.framework/*\" -del find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.framework\" -type d -delete "; }; - 8F5D6E75B7D344BD80BC6EC0 /* Remove NodeJS Mobile Framework Simulator Strips */ = { + E95128D078C34495AFAAA808 /* Remove NodeJS Mobile Framework Simulator Strips */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( diff --git a/lib/notifications.js b/lib/notifications.js index dc1a7a5..74427d2 100644 --- a/lib/notifications.js +++ b/lib/notifications.js @@ -1,21 +1,26 @@ -import {tempReminderObservable} from '../local-storage' +import {tempReminderObservable, periodReminderObservable} from '../local-storage' import Notification from 'react-native-push-notification' import { LocalDate } from 'js-joda' import Moment from 'moment' import { settings as labels } from '../components/labels' -import { getOrCreateCycleDay } from '../db' +import { getOrCreateCycleDay, getBleedingDaysSortedByDate } from '../db' +import cycleModule from './cycle' export default function setupNotifications(navigate) { Notification.configure({ - onNotification: () => { - const todayDateString = LocalDate.now().toString() - const cycleDay = getOrCreateCycleDay(todayDateString) - navigate('TemperatureEditView', { cycleDay }) + onNotification: (notification) => { + if (notification.id === '1') { + const todayDateString = LocalDate.now().toString() + const cycleDay = getOrCreateCycleDay(todayDateString) + navigate('TemperatureEditView', { cycleDay }) + } else { + navigate('Home') + } } }) tempReminderObservable(reminder => { - Notification.cancelAllLocalNotifications() + Notification.cancelLocalNotifications({id: '1'}) if (reminder.enabled) { const [hours, minutes] = reminder.time.split(':') let target = new Moment() @@ -28,6 +33,7 @@ export default function setupNotifications(navigate) { } Notification.localNotificationSchedule({ + id: '1', message: labels.tempReminder.notification, date: target.toDate(), vibrate: false, @@ -35,4 +41,40 @@ export default function setupNotifications(navigate) { }) } }) + + periodReminderObservable(reminder => { + Notification.cancelLocalNotifications({id: '2'}) + if (reminder.enabled) setupPeriodReminder() + }) + + getBleedingDaysSortedByDate().addListener(() => { + Notification.cancelLocalNotifications({id: '2'}) + if (periodReminderObservable.value.enabled) setupPeriodReminder() + }) + +} + +function setupPeriodReminder() { + const bleedingPrediction = cycleModule().getPredictedMenses() + if (bleedingPrediction.length > 0) { + const bleedingStart = Moment(bleedingPrediction[0][0], "YYYY-MM-DD") + // 3 days before and at 6 am + const reminderDate = bleedingStart + .subtract(3, 'days') + .hours(6) + .minutes(0) + .seconds(0) + + if (reminderDate.isAfter()) { + // period is likely to start in 3 to 3 + (length of prediction - 1) days + const daysToEndOfPrediction = bleedingPrediction[0].length + 2 + + Notification.localNotificationSchedule({ + id: '2', + message: labels.periodReminder.notification(daysToEndOfPrediction), + date: reminderDate.toDate(), + vibrate: false + }) + } + } } \ No newline at end of file diff --git a/lib/sympto/index.js b/lib/sympto/index.js index 575b608..96e2b49 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -23,7 +23,8 @@ export default function getSymptoThermalStatus(cycleInfo) { if (statusForLast.temperatureShift) { const preOvuPhase = getPreOvulatoryPhase( cycle, - [previousCycle, ...earlierCycles] + [previousCycle, ...earlierCycles], + secondarySymptom ) if (preOvuPhase) { status.phases.preOvulatory = preOvuPhase diff --git a/lib/sympto/minus-8-day-rule.js b/lib/sympto/minus-8-day-rule.js index 2964b37..5a07d76 100644 --- a/lib/sympto/minus-8-day-rule.js +++ b/lib/sympto/minus-8-day-rule.js @@ -1,10 +1,10 @@ import { LocalDate } from 'js-joda' import getNfpStatus from './index' -export default function (previousCycles) { +export default function (previousCycles, secondarySymptom) { const fhms = previousCycles .map(cycle => { - const status = getNfpStatus({ cycle }) + const status = getNfpStatus({ cycle, secondarySymptom }) if (status.temperatureShift) { const day = status.temperatureShift.firstHighMeasurementDay const firstCycleDayDate = LocalDate.parse(cycle[0].date) diff --git a/lib/sympto/pre-ovulatory.js b/lib/sympto/pre-ovulatory.js index 874e2bc..236eefd 100644 --- a/lib/sympto/pre-ovulatory.js +++ b/lib/sympto/pre-ovulatory.js @@ -1,10 +1,10 @@ import { LocalDate } from "js-joda" import apply8DayRule from './minus-8-day-rule' -export default function(cycle, previousCycles) { +export default function(cycle, previousCycles, secondarySymptom) { let preOvuPhaseLength = 5 - const minus8DayRuleResult = apply8DayRule(previousCycles) + const minus8DayRuleResult = apply8DayRule(previousCycles, secondarySymptom) if (minus8DayRuleResult) preOvuPhaseLength = minus8DayRuleResult const startDate = LocalDate.parse(cycle[0].date) @@ -12,7 +12,7 @@ export default function(cycle, previousCycles) { const maybePreOvuDays = cycle.slice(0, preOvuPhaseLength).filter(d => { return d.date <= preOvuEndDate }) - const preOvulatoryDays = getDaysUntilFertileSecondarySymptom(maybePreOvuDays) + const preOvulatoryDays = getDaysUntilFertileSecondarySymptom(maybePreOvuDays, secondarySymptom) // if fertile mucus or cervix occurs on the 1st cycle day, there is no pre-ovu phase if (!preOvulatoryDays.length) return null @@ -39,7 +39,8 @@ function getDaysUntilFertileSecondarySymptom(days, secondarySymptom = 'mucus') { if (secondarySymptom === 'mucus') { return day.mucus && day.mucus.value > 1 } else if (secondarySymptom === 'cervix') { - return day.cervix && !day.cervix.isClosedAndHard + return day.cervix && day.cervix.opening > 0 + || day.cervix && day.cervix.firmness > 0 } }) diff --git a/local-storage/index.js b/local-storage/index.js index d41d4d7..5f4af70 100644 --- a/local-storage/index.js +++ b/local-storage/index.js @@ -34,6 +34,16 @@ export async function saveTempReminder(reminder) { tempReminderObservable.set(reminder) } +export const periodReminderObservable = Observable() +setObvWithInitValue('periodReminder', periodReminderObservable, { + enabled: false +}) + +export async function savePeriodReminder(reminder) { + await AsyncStorage.setItem('periodReminder', JSON.stringify(reminder)) + periodReminderObservable.set(reminder) +} + export const hasEncryptionObservable = Observable() setObvWithInitValue('hasEncryption', hasEncryptionObservable, false) diff --git a/styles/index.js b/styles/index.js index 463a4c7..10c90c9 100644 --- a/styles/index.js +++ b/styles/index.js @@ -1,18 +1,31 @@ import { StyleSheet } from 'react-native' -export const primaryColor = '#ff7e5f' -export const secondaryColor = '#351c4d' +export const primaryColor = '#000D19' +export const secondaryColor = '#4FAFA7' export const secondaryColorLight = '#91749d' export const fontOnPrimaryColor = 'white' -export const shadesOfRed = ['#ffcbbf', '#ffb19f', '#ff977e', '#ff7e5f'] // light to dark +export const shadesOfRed = [ + '#e7999e', + '#db666d', + '#cf323d', + '#c3000d' +] // light to dark + +const fontRegular = 'Prompt-Light' +const fontLight = 'Prompt-Thin' + +const regularSize = 16 const defaultBottomMargin = 5 const defaultIndentation = 10 const defaultTopMargin = 10 +const colorInActive = '#666666' export default StyleSheet.create({ appText: { - color: 'black' + color: 'black', + fontFamily: fontRegular, + fontSize: regularSize }, paragraph: { marginBottom: defaultBottomMargin @@ -31,21 +44,36 @@ export default StyleSheet.create({ }, welcome: { fontSize: 20, + fontFamily: 'serif', margin: 30, textAlign: 'center', textAlignVertical: 'center' }, dateHeader: { - fontSize: 22, - fontWeight: 'bold', + fontSize: 20, + fontFamily: fontLight, color: fontOnPrimaryColor, textAlign: 'center', }, + headerText: { + fontSize: 30, + fontFamily: fontLight, + color: fontOnPrimaryColor, + textAlign: 'center', + }, + accentCircle: { + borderColor: secondaryColor, + borderWidth: 0.5, + width: 40, + height: 40, + borderRadius: 100, + position: 'absolute' + }, cycleDayNumber: { fontSize: 15, color: fontOnPrimaryColor, textAlign: 'center', - marginLeft: 15 + fontFamily: fontLight }, symptomViewHeading: { fontSize: 20, @@ -107,14 +135,14 @@ export default StyleSheet.create({ paddingHorizontal: 15, alignItems: 'center', justifyContent: 'center', - height: '10%' + height: 80 }, menu: { backgroundColor: primaryColor, alignItems: 'center', justifyContent: 'space-between', flexDirection: 'row', - height: '12%' + height: 60 }, menuItem: { alignItems: 'center', @@ -122,20 +150,19 @@ export default StyleSheet.create({ paddingVertical: 15 }, menuText: { - color: fontOnPrimaryColor + color: fontOnPrimaryColor, + fontFamily: fontLight }, menuTextInActive: { - color: 'lightgrey' + color: colorInActive }, headerCycleDay: { flexDirection: 'row', justifyContent: 'space-between', - height: '15%' }, headerSymptom: { flexDirection: 'row', justifyContent: 'space-between', - height: '12%' }, navigationArrow: { fontSize: 60, @@ -299,7 +326,7 @@ export const iconStyles = { color: fontOnPrimaryColor }, symptomHeaderIcons: { - size: 30, + size: 20, color: fontOnPrimaryColor }, symptomBox: { @@ -313,6 +340,6 @@ export const iconStyles = { color: fontOnPrimaryColor }, menuIconInactive: { - color: 'lightgrey', + color: colorInActive, }, } \ No newline at end of file diff --git a/test/sympto/cervix-temp-fixtures.js b/test/sympto/cervix-temp-fixtures.js index 58ff695..18adfe6 100644 --- a/test/sympto/cervix-temp-fixtures.js +++ b/test/sympto/cervix-temp-fixtures.js @@ -2,9 +2,9 @@ function convertToSymptoFormat(val) { const sympto = { date: val.date } if (val.temperature) sympto.temperature = { value: val.temperature, + time: '08:00', exclude: false } - if (val.cervix && typeof val.cervix.opening === 'number' && typeof val.cervix.firmness === 'number') sympto.cervix = { opening: val.cervix.opening, firmness: val.cervix.firmness, @@ -18,11 +18,11 @@ function convertToSymptoFormat(val) { } export const cervixShiftAndFhmOnSameDay = [ - { date: '2018-08-01', bleeding: 1, cervix: { opening: 1, firmness: 1 } }, - { date: '2018-08-02', bleeding: 2, cervix: { opening: 1, firmness: 1 } }, - { date: '2018-08-03', temperature: 36.6, bleeding: 2, cervix: { opening: 2, firmness: 1 } }, - { date: '2018-08-04', temperature: 36.55, bleeding: 1, cervix: { opening: 2, firmness: 0 } }, - { date: '2018-08-05', temperature: 36.6, cervix: { opening: 0, firmness: 1 } }, + { date: '2018-08-01', bleeding: 1 }, + { date: '2018-08-02', bleeding: 2 }, + { date: '2018-08-03', temperature: 36.6, bleeding: 2 }, + { date: '2018-08-04', temperature: 36.55, bleeding: 1 }, + { date: '2018-08-05', temperature: 36.6, cervix: { opening: 0, firmness: 0 } }, { date: '2018-08-06', temperature: 36.65, cervix: { opening: 0, firmness: 1 } }, { date: '2018-08-07', temperature: 36.71, cervix: { opening: 1, firmness: 0 } }, { date: '2018-08-08', temperature: 36.69, cervix: { opening: 1, firmness: 0 } }, @@ -99,6 +99,8 @@ export const longAndComplicatedCycle = [ { date: '2018-06-04', temperature: 36.6 }, { date: '2018-06-05', temperature: 36.55 }, { date: '2018-06-06', temperature: 36.7, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-07', temperature: 36.5, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-08', temperature: 36.52, cervix: { opening: 0, firmness: 0 } }, { date: '2018-06-09', temperature: 36.5, cervix: { opening: 2, firmness: 1 } }, { date: '2018-06-10', temperature: 36.4, cervix: { opening: 2, firmness: 1 } }, { date: '2018-06-13', temperature: 36.45, cervix: { opening: 1, firmness: 1 } }, @@ -110,7 +112,7 @@ export const longAndComplicatedCycle = [ { date: '2018-06-19', temperature: 36.8, cervix: { opening: 0, firmness: 0 } }, { date: '2018-06-20', temperature: 36.85, cervix: { opening: 1, firmness: 1 } }, { date: '2018-06-21', temperature: 36.8, cervix: { opening: 1, firmness: 1 } }, - { date: '2018-06-22', temperature: 36.9, cervix: { opening: 2, firmness: 1 } }, + { date: '2018-06-22', temperature: 36.9, cervix: { opening: 0, firmness: 0 } }, { date: '2018-06-25', temperature: 36.9, cervix: { opening: 0, firmness: 0 } }, { date: '2018-06-26', temperature: 36.8, cervix: { opening: 0, firmness: 0 } }, { date: '2018-06-27', temperature: 36.9, cervix: { opening: 0, firmness: 0 } } @@ -186,3 +188,80 @@ export const fiveDayCycle = [ { date: '2018-08-01', bleeding: 2 }, { date: '2018-08-03', bleeding: 3 } ].map(convertToSymptoFormat) + +export const fhmOnDay12 = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-09', temperature: 36.5, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-10', temperature: 36.4, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-12', temperature: 36.8, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-14', temperature: 36.9, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-17', temperature: 36.9, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-18', temperature: 36.9, cervix: { opening: 0, firmness: 0 } } +].map(convertToSymptoFormat) + +export const fhmOnDay15 = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-09', temperature: 36.5, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-10', temperature: 36.4, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-11', temperature: 36.4, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-12', temperature: 36.4, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-14', temperature: 36.4, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-15', temperature: 36.8, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-16', temperature: 36.9, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-17', temperature: 36.9, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-18', temperature: 36.9, cervix: { opening: 0, firmness: 0 } } +].map(convertToSymptoFormat) + +export const cycleWithEarlyCervix = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-08', temperature: 36.45, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-09', temperature: 36.5, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-10', temperature: 36.4, cervix: { opening: 2, firmness: 0 } }, + { date: '2018-06-11', temperature: 36.5, cervix: { opening: 2, firmness: 1 } }, + { date: '2018-06-13', temperature: 36.45, cervix: { opening: 2, firmness: 1 } }, + { date: '2018-06-14', temperature: 36.5, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-15', temperature: 36.55, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-16', temperature: 36.7, cervix: { opening: 1, firmness: 0 } }, + { date: '2018-06-17', temperature: 36.65, cervix: { opening: 0, firmness: 1 } }, + { date: '2018-06-18', temperature: 36.75, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-19', temperature: 36.8, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-20', temperature: 36.85, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-23', temperature: 36.9, cervix: { opening: 0, firmness: 1 } }, + { date: '2018-06-24', temperature: 36.85, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-26', temperature: 36.8, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-27', temperature: 36.9, cervix: { opening: 1, firmness: 1 } } +].map(convertToSymptoFormat) + +export const cycleWithCervixOnFirstDay = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-08', temperature: 36.45, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-09', temperature: 36.5, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-10', temperature: 36.4, cervix: { opening: 2, firmness: 0 } }, + { date: '2018-06-11', temperature: 36.5, cervix: { opening: 2, firmness: 1 } }, + { date: '2018-06-13', temperature: 36.45, cervix: { opening: 2, firmness: 1 } }, + { date: '2018-06-14', temperature: 36.5, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-15', temperature: 36.55, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-16', temperature: 36.7, cervix: { opening: 1, firmness: 0 } }, + { date: '2018-06-17', temperature: 36.65, cervix: { opening: 0, firmness: 1 } }, + { date: '2018-06-18', temperature: 36.75, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-19', temperature: 36.8, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-20', temperature: 36.85, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-06-23', temperature: 36.9, cervix: { opening: 0, firmness: 1 } }, + { date: '2018-06-24', temperature: 36.85, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-26', temperature: 36.8, cervix: { opening: 1, firmness: 1 } }, + { date: '2018-06-27', temperature: 36.9, cervix: { opening: 1, firmness: 1 } } +].map(convertToSymptoFormat) diff --git a/test/sympto/cervix-temp.spec.js b/test/sympto/cervix-temp.spec.js index e2d8fa0..964b1d6 100644 --- a/test/sympto/cervix-temp.spec.js +++ b/test/sympto/cervix-temp.spec.js @@ -1,14 +1,20 @@ import chai from 'chai' import getSensiplanStatus from '../../lib/sympto' +import { AssertionError } from 'assert' import { cervixShiftAndFhmOnSameDay, cycleWithFhmNoCervixShift, - cycleWithoutFhm, + cycleWithoutFhmNoCervixShift, longCycleWithoutAnyShifts, + longAndComplicatedCycle, tempShift3DaysAfterCervixShift, cervixShift2DaysAfterTempShift, noOvulationDetected, - fiveDayCycle + fiveDayCycle, + fhmOnDay12, + fhmOnDay15, + cycleWithEarlyCervix, + cycleWithCervixOnFirstDay } from './cervix-temp-fixtures' const expect = chai.expect @@ -19,7 +25,7 @@ describe('sympto', () => { it('with no temp or cervix shifts detects only peri-ovulatory', () => { const status = getSensiplanStatus({ cycle: longCycleWithoutAnyShifts, - previousCycle: cycleWithoutFhm, + previousCycle: cycleWithoutFhmNoCervixShift, secondarySymptom: 'cervix' }) expect(Object.keys(status.phases).length).to.eql(1) @@ -35,7 +41,7 @@ describe('sympto', () => { it('with temp but no cervix shift detects only peri-ovulatory', () => { const status = getSensiplanStatus({ cycle: cycleWithFhmNoCervixShift, - previousCycle: cycleWithoutFhm, + previousCycle: cycleWithoutFhmNoCervixShift, secondarySymptom: 'cervix' }) expect(Object.keys(status.phases).length).to.eql(1) @@ -217,5 +223,286 @@ describe('sympto', () => { }) }) }) + describe('applying the minus-8 rule', () => { + it('shortens the pre-ovu phase if there is a previous < 13 fhm', () => { + const status = getSensiplanStatus({ + cycle: longAndComplicatedCycle, + previousCycle: fhmOnDay15, + earlierCycles: [fhmOnDay12, ...Array(10).fill(fhmOnDay15)], + secondarySymptom: 'cervix' + }) + expect(status.temperatureShift).to.be.an('object') + expect(status.cervixShift).to.be.an('object') + + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-04' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date <= '2018-06-04') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-05' }, + end: { date: '2018-06-26', time: '18:00' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => { + return date > '2018-06-04' && date <= '2018-06-26' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-26', + time: '18:00' + }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date >= '2018-06-26') + }) + }) + it('shortens pre-ovu phase with prev < 13 fhm even with < 12 cycles', () => { + const status = getSensiplanStatus({ + cycle: longAndComplicatedCycle, + previousCycle: fhmOnDay12, + earlierCycles: Array(10).fill(fhmOnDay12), + secondarySymptom: 'cervix' + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.cervixShift).to.be.an('object') + + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-04' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date <= '2018-06-04') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-05' }, + end: { date: '2018-06-26', time: '18:00' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => { + return date > '2018-06-04' && date <= '2018-06-26' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-26', + time: '18:00' + }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date >= '2018-06-26') + }) + }) + it('shortens the pre-ovu phase if early fertile cervix occurs', () => { + const status = getSensiplanStatus({ + cycle: cycleWithEarlyCervix, + previousCycle: fhmOnDay12, + earlierCycles: Array(10).fill(fhmOnDay12), + secondarySymptom: 'cervix' + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.cervixShift).to.be.an('object') + + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-01' }, + cycleDays: cycleWithEarlyCervix + .filter(({date}) => date <= '2018-06-01') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-02' }, + end: { date: '2018-06-20', time: '18:00'}, + cycleDays: cycleWithEarlyCervix + .filter(({date}) => { + return date > '2018-06-01' && date <= '2018-06-20' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { date: '2018-06-20', time: '18:00' }, + cycleDays: cycleWithEarlyCervix + .filter(({date}) => date >= '2018-06-20') + }) + }) + it('shortens the pre-ovu phase if cervix occurs even on the first day', () => { + const status = getSensiplanStatus({ + cycle: cycleWithCervixOnFirstDay, + previousCycle: fhmOnDay12, + earlierCycles: Array(10).fill(fhmOnDay12), + secondarySymptom: 'cervix' + }) + + expect(Object.keys(status.phases).length).to.eql(2) + expect(status.temperatureShift).to.be.an('object') + expect(status.cervixShift).to.be.an('object') + + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-20', time: '18:00'}, + cycleDays: cycleWithCervixOnFirstDay + .filter(({date}) => { + return date >= '2018-06-01' && date <= '2018-06-20' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { date: '2018-06-20', time: '18:00' }, + cycleDays: cycleWithCervixOnFirstDay + .filter(({date}) => date >= '2018-06-20') + }) + }) + it('lengthens the pre-ovu phase if >= 12 cycles with fhm > 13', () => { + const status = getSensiplanStatus({ + cycle: longAndComplicatedCycle, + previousCycle: fhmOnDay15, + earlierCycles: Array(11).fill(fhmOnDay15), + secondarySymptom: 'cervix' + }) + expect(status.temperatureShift).to.be.an('object') + expect(status.cervixShift).to.be.an('object') + + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-07' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date <= '2018-06-07') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-08' }, + end: { date: '2018-06-26', time: '18:00'}, + cycleDays: longAndComplicatedCycle + .filter(({date}) => { + return date >= '2018-06-08' && date <= '2018-06-26' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { date: '2018-06-26', time: '18:00' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date >= '2018-06-26') + }) + }) + it('does not lengthen the pre-ovu phase if < 12 cycles', () => { + const status = getSensiplanStatus({ + cycle: longAndComplicatedCycle, + previousCycle: fhmOnDay15, + earlierCycles: Array(10).fill(fhmOnDay15), + secondarySymptom: 'cervix' + }) + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-05' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date <= '2018-06-05') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-06' }, + end: { date: '2018-06-26', time: '18:00' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => { + return date > '2018-06-05' && date <= '2018-06-26' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-26', + time: '18:00' + }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date >= '2018-06-26') + }) + }) + it('does not detect any pre-ovu phase if prev cycle had no fhm', () => { + const status = getSensiplanStatus({ + cycle: longAndComplicatedCycle, + previousCycle: cycleWithoutFhmNoCervixShift, + earlierCycles: [...Array(12).fill(fhmOnDay15)], + secondarySymptom: 'cervix' + }) + + expect(Object.keys(status.phases).length).to.eql(2) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-26', time: '18:00' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => { + return date >= '2018-06-01' && date <= '2018-06-26' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-26', + time: '18:00' + }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date >= '2018-06-26') + }) + }) + }) + describe('when args are wrong', () => { + it('throws when arg object is not in right format', () => { + const wrongObject = { ada: 'lovelace' } + expect(() => getSensiplanStatus(wrongObject)).to.throw(AssertionError) + }) + it('throws if cycle array is empty', () => { + expect(() => getSensiplanStatus({cycle: []})).to.throw(AssertionError) + }) + it('throws if cycle days are not in right format', () => { + expect(() => getSensiplanStatus({ + cycle: [{ + Ilike: "you" + }], + })).to.throw(AssertionError) + expect(() => getSensiplanStatus({ + cycle: [{ + date: '01.09.2018', + bleeding: { value: 0 }, + cervix: {opening: 0, firmness: 0} + }], + })).to.throw(AssertionError) + expect(() => getSensiplanStatus({ + cycle: [{ + date: '2018-01-01', + bleeding: { value: 'medium' }, + temperature: { value: 36.6 } + }], + })).to.throw(AssertionError) + expect(() => getSensiplanStatus({ + cycle: [{ + date: '2018-09-01', + bleeding: { value: 0 }, + temperature: { value: '36.6' } + }], + })).to.throw(AssertionError) + expect(() => getSensiplanStatus({ + cycle: [{ + date: '2018-09-01', + bleeding: { value: 0 }, + temperature: { value: 36.6 }, + cervix: {opening: 4, firmness: 18} + }], + })).to.throw(AssertionError) + expect(() => getSensiplanStatus({ + cycle: [{ + date: '2018-09-01', + bleeding: { value: 0 }, + temperature: 37.9 + }], + })).to.throw(AssertionError) + }) + it('throws if first cycle day does not have valid bleeding value', () => { + expect(() => getSensiplanStatus({ + cycle: [{ + date: '2018-09-01', + bleeding: { value: 0 } + }], + earlierCycles: [[{ + date: '2017-09-23', + bleeding: { value: '1' } + }]] + })).to.throw(AssertionError) + }) + }) }) })