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 (
+
+ )
+ })
+ : 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)
+ })
+ })
})
})