Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c4536fdee | |||
| f11eb3d1a1 | |||
| 5f83464649 | |||
| 6e2e03f39e | |||
| cc62e24229 | |||
| cd24522b4d | |||
| 446638d6de | |||
| ae23ef2c58 | |||
| 38b9e8b31f | |||
| 84d657cabb | |||
| 4573b93921 | |||
| ab88a4c163 | |||
| baaf89c04e | |||
| d476f6c143 | |||
| b56e0818f3 | |||
| 1b5ffaf5d6 | |||
| f68cc2b49e | |||
| 27d430a465 | |||
| 28f52e2cea | |||
| 3f87f298fb | |||
| 585f32863d | |||
| 51e1c95e71 | |||
| 36c33c69b7 |
@@ -14,3 +14,4 @@ updates:
|
|||||||
- dependency-name: 'react'
|
- dependency-name: 'react'
|
||||||
- dependency-name: 'react-native'
|
- dependency-name: 'react-native'
|
||||||
- dependency-name: 'react-native-push-notifications'
|
- dependency-name: 'react-native-push-notifications'
|
||||||
|
- dependency-name: '@babel/core'
|
||||||
|
|||||||
@@ -201,7 +201,3 @@ More information about how the app calculates fertility status and bleeding pred
|
|||||||
react-native link
|
react-native link
|
||||||
|
|
||||||
5. You should be able to use the icon now within drip, e.g. in Cycle Day Overview and on the chart.
|
5. You should be able to use the icon now within drip, e.g. in Cycle Day Overview and on the chart.
|
||||||
|
|
||||||
## Translation
|
|
||||||
|
|
||||||
We are using [Weblate](https://weblate.org/) as translation software.
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
ext.kotlinVersion = "1.4.20"
|
ext.kotlinVersion = "1.3.10"
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:4.2.2")
|
classpath("com.android.tools.build:gradle:4.2.2")
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ allprojects {
|
|||||||
content {
|
content {
|
||||||
excludeGroup "com.facebook.react"
|
excludeGroup "com.facebook.react"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
google()
|
google()
|
||||||
maven { url 'https://www.jitpack.io' }
|
maven { url 'https://www.jitpack.io' }
|
||||||
maven {
|
maven {
|
||||||
@@ -53,5 +53,5 @@ ext {
|
|||||||
minSdkVersion = 23
|
minSdkVersion = 23
|
||||||
compileSdkVersion = 30
|
compileSdkVersion = 30
|
||||||
targetSdkVersion = 30
|
targetSdkVersion = 30
|
||||||
ndkVersion = "21.4.7075529"
|
ndkVersion = "21.4.7075529"
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-11
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from 'react'
|
import React from 'react'
|
||||||
import { ScrollView, StyleSheet, View } from 'react-native'
|
import { ScrollView, StyleSheet, View } from 'react-native'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
@@ -10,7 +10,6 @@ import Footnote from './common/Footnote'
|
|||||||
|
|
||||||
import cycleModule from '../lib/cycle'
|
import cycleModule from '../lib/cycle'
|
||||||
import { getFertilityStatusForDay } from '../lib/sympto-adapter'
|
import { getFertilityStatusForDay } from '../lib/sympto-adapter'
|
||||||
import setupNotifications from '../lib/notifications'
|
|
||||||
import {
|
import {
|
||||||
determinePredictionText,
|
determinePredictionText,
|
||||||
formatWithOrdinalSuffix,
|
formatWithOrdinalSuffix,
|
||||||
@@ -20,10 +19,13 @@ import { Colors, Fonts, Sizes, Spacing } from '../styles'
|
|||||||
import { LocalDate } from '@js-joda/core'
|
import { LocalDate } from '@js-joda/core'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const Home = ({ navigation }) => {
|
const Home = ({ navigate, setDate }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
useEffect(() => setupNotifications(navigation), [])
|
function navigateToCycleDayView() {
|
||||||
|
setDate(todayDateString)
|
||||||
|
navigate('CycleDay')
|
||||||
|
}
|
||||||
|
|
||||||
const todayDateString = LocalDate.now().toString()
|
const todayDateString = LocalDate.now().toString()
|
||||||
const { getCycleDayNumber, getPredictedMenses } = cycleModule()
|
const { getCycleDayNumber, getPredictedMenses } = cycleModule()
|
||||||
@@ -31,14 +33,11 @@ const Home = ({ navigation }) => {
|
|||||||
const { status, phase, statusText } =
|
const { status, phase, statusText } =
|
||||||
getFertilityStatusForDay(todayDateString)
|
getFertilityStatusForDay(todayDateString)
|
||||||
const prediction = determinePredictionText(getPredictedMenses(), t)
|
const prediction = determinePredictionText(getPredictedMenses(), t)
|
||||||
|
|
||||||
const cycleDayText = cycleDayNumber
|
const cycleDayText = cycleDayNumber
|
||||||
? formatWithOrdinalSuffix(cycleDayNumber)
|
? formatWithOrdinalSuffix(cycleDayNumber)
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
function navigateToCycleDayView() {
|
|
||||||
navigation.navigate('CycleDayOverview', { date: todayDateString })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
@@ -110,9 +109,8 @@ const styles = StyleSheet.create({
|
|||||||
})
|
})
|
||||||
|
|
||||||
Home.propTypes = {
|
Home.propTypes = {
|
||||||
navigation: PropTypes.shape({
|
navigate: PropTypes.func,
|
||||||
navigate: PropTypes.func.isRequired,
|
setDate: PropTypes.func,
|
||||||
}).isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Home
|
export default Home
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
||||||
import { getLicenseFlag, saveEncryptionFlag } from '../local-storage'
|
import { getLicenseFlag, saveEncryptionFlag } from '../local-storage'
|
||||||
import { closeDb, openDb } from '../db'
|
import { openDb } from '../db'
|
||||||
|
|
||||||
import App from './app'
|
import App from './app'
|
||||||
import AppLoadingView from './common/app-loading'
|
import AppLoadingView from './common/app-loading'
|
||||||
@@ -29,8 +29,6 @@ export default function AppWrapper() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkIsLicenseAccepted()
|
checkIsLicenseAccepted()
|
||||||
checkIsDbEncrypted()
|
checkIsDbEncrypted()
|
||||||
|
|
||||||
return () => closeDb()
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
+53
-64
@@ -1,82 +1,71 @@
|
|||||||
import React from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import 'react-native-gesture-handler'
|
import { BackHandler, StyleSheet, View } from 'react-native'
|
||||||
import { StyleSheet, Text } from 'react-native'
|
import PropTypes from 'prop-types'
|
||||||
import { NavigationContainer } from '@react-navigation/native'
|
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
import { LocalDate } from '@js-joda/core'
|
||||||
import { createStackNavigator } from '@react-navigation/stack'
|
|
||||||
|
|
||||||
import Home from './Home'
|
|
||||||
import Chart from './chart/chart'
|
|
||||||
import CalendarView from './calendar'
|
|
||||||
import CycleDayOverview from './cycle-day/cycle-day-overview'
|
|
||||||
import Stats from './stats'
|
|
||||||
import Icon from './common/menu-icon'
|
|
||||||
import Header from './header'
|
import Header from './header'
|
||||||
|
import Menu from './menu'
|
||||||
|
import { viewsList } from './views'
|
||||||
|
import { pages } from './pages'
|
||||||
|
|
||||||
import { Colors, Fonts, Sizes } from '../styles'
|
import setupNotifications from '../lib/notifications'
|
||||||
|
import { closeDb } from '../db'
|
||||||
|
|
||||||
const HomeStack = createStackNavigator()
|
const App = ({ restartApp }) => {
|
||||||
|
const [date, setDate] = useState(LocalDate.now().toString())
|
||||||
|
const [currentPage, setCurrentPage] = useState('Home')
|
||||||
|
const goBack = () => {
|
||||||
|
if (currentPage === 'Home') {
|
||||||
|
closeDb()
|
||||||
|
BackHandler.exitApp()
|
||||||
|
} else {
|
||||||
|
const { parent } = pages.find((p) => p.component === currentPage)
|
||||||
|
|
||||||
|
setCurrentPage(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const backHandler = BackHandler.addEventListener(
|
||||||
|
'hardwareBackPress',
|
||||||
|
goBack
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => backHandler.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => setupNotifications(setCurrentPage, setDate), [])
|
||||||
|
|
||||||
|
const Page = viewsList[currentPage]
|
||||||
|
const isTemperatureEditView = currentPage === 'TemperatureEditView'
|
||||||
|
const headerProps = { navigate: setCurrentPage }
|
||||||
|
const pageProps = {
|
||||||
|
date,
|
||||||
|
setDate,
|
||||||
|
isTemperatureEditView,
|
||||||
|
navigate: setCurrentPage,
|
||||||
|
}
|
||||||
|
|
||||||
function HomeStackScreen() {
|
|
||||||
return (
|
return (
|
||||||
<HomeStack.Navigator screenOptions={{ headerShown: false }}>
|
<View style={styles.container}>
|
||||||
<HomeStack.Screen name="Home" component={Home} />
|
<Header {...headerProps} />
|
||||||
<HomeStack.Screen name="CycleDayOverview" component={CycleDayOverview} />
|
<Page {...pageProps} restartApp={restartApp} />
|
||||||
</HomeStack.Navigator>
|
<Menu currentPage={currentPage} navigate={setCurrentPage} />
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator()
|
App.propTypes = {
|
||||||
|
restartApp: PropTypes.func,
|
||||||
const App = () => {
|
|
||||||
return (
|
|
||||||
<NavigationContainer>
|
|
||||||
<Tab.Navigator
|
|
||||||
screenOptions={({ route }) => ({
|
|
||||||
header: ({ navigation }) => <Header navigation={navigation} />,
|
|
||||||
tabBarIcon: ({ focused }) => {
|
|
||||||
let icon = 'chart'
|
|
||||||
|
|
||||||
if (route.name === 'CalendarStackScreen') {
|
|
||||||
icon = 'calendar'
|
|
||||||
} else if (route.name === 'Stats') {
|
|
||||||
icon = 'statistics'
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Icon name={icon} isActive={focused} />
|
|
||||||
},
|
|
||||||
tabBarLabel: ({ color }) => {
|
|
||||||
return (
|
|
||||||
<Text style={[styles.text, { color: color }]}>{route.name}</Text>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
tabBarActiveTintColor: Colors.orange,
|
|
||||||
tabBarInactiveTintColor: Colors.grey,
|
|
||||||
tabBarStyle: { height: 80 },
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Tab.Screen
|
|
||||||
name="HomeStackScreen"
|
|
||||||
component={HomeStackScreen}
|
|
||||||
options={{ tabBarButton: () => null, tabBarVisible: false }}
|
|
||||||
/>
|
|
||||||
<Tab.Screen name="Calendar" component={CalendarView} />
|
|
||||||
<Tab.Screen name="Chart" component={Chart} />
|
|
||||||
<Tab.Screen name="Stats" component={Stats} />
|
|
||||||
</Tab.Navigator>
|
|
||||||
</NavigationContainer>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
text: {
|
|
||||||
fontFamily: Fonts.bold,
|
|
||||||
fontSize: Sizes.small,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ import {
|
|||||||
todayToCalFormat,
|
todayToCalFormat,
|
||||||
} from './helpers/calendar'
|
} from './helpers/calendar'
|
||||||
|
|
||||||
const CalendarView = ({ navigation }) => {
|
const CalendarView = ({ setDate, navigate }) => {
|
||||||
const bleedingDays = getBleedingDaysSortedByDate()
|
const bleedingDays = getBleedingDaysSortedByDate()
|
||||||
const predictedMenses = cycleModule().getPredictedMenses()
|
const predictedMenses = cycleModule().getPredictedMenses()
|
||||||
|
|
||||||
const passDateToDayView = ({ dateString }) => {
|
const passDateToDayView = ({ dateString }) => {
|
||||||
navigation.navigate('CycleDayOverview', { date: dateString })
|
setDate(dateString)
|
||||||
|
navigate('CycleDay')
|
||||||
}
|
}
|
||||||
|
|
||||||
const markedDates = Object.assign(
|
const markedDates = Object.assign(
|
||||||
@@ -48,9 +49,8 @@ const styles = StyleSheet.create({
|
|||||||
})
|
})
|
||||||
|
|
||||||
CalendarView.propTypes = {
|
CalendarView.propTypes = {
|
||||||
navigation: PropTypes.shape({
|
setDate: PropTypes.func.isRequired,
|
||||||
navigate: PropTypes.func.isRequired,
|
navigate: PropTypes.func.isRequired,
|
||||||
}).isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CalendarView
|
export default CalendarView
|
||||||
|
|||||||
@@ -6,16 +6,17 @@ import AppText from '../common/app-text'
|
|||||||
import CloseIcon from '../common/close-icon'
|
import CloseIcon from '../common/close-icon'
|
||||||
|
|
||||||
import { Containers, Spacing } from '../../styles'
|
import { Containers, Spacing } from '../../styles'
|
||||||
import { chart } from '../../i18n/en/labels'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const image = require('../../assets/swipe.png')
|
const image = require('../../assets/swipe.png')
|
||||||
|
|
||||||
const Tutorial = ({ onClose }) => {
|
const Tutorial = ({ onClose }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Image resizeMode="contain" source={image} style={styles.image} />
|
<Image resizeMode="contain" source={image} style={styles.image} />
|
||||||
<View style={styles.textContainer}>
|
<View style={styles.textContainer}>
|
||||||
<AppText>{chart.tutorial}</AppText>
|
<AppText>{t('chart.tutorial')}</AppText>
|
||||||
</View>
|
</View>
|
||||||
<CloseIcon onClose={onClose} />
|
<CloseIcon onClose={onClose} />
|
||||||
</View>
|
</View>
|
||||||
@@ -9,7 +9,7 @@ import HorizontalGrid from './horizontal-grid'
|
|||||||
import MainGrid from './main-grid'
|
import MainGrid from './main-grid'
|
||||||
import NoData from './no-data'
|
import NoData from './no-data'
|
||||||
import NoTemperature from './no-temperature'
|
import NoTemperature from './no-temperature'
|
||||||
import Tutorial from './tutorial'
|
import Tutorial from './Tutorial'
|
||||||
import YAxis from './y-axis'
|
import YAxis from './y-axis'
|
||||||
|
|
||||||
import { getCycleDaysSortedByDate } from '../../db'
|
import { getCycleDaysSortedByDate } from '../../db'
|
||||||
@@ -28,7 +28,7 @@ import { Spacing } from '../../styles'
|
|||||||
const getSymptomsFromCycleDays = (cycleDays) =>
|
const getSymptomsFromCycleDays = (cycleDays) =>
|
||||||
SYMPTOMS.filter((symptom) => cycleDays.some((cycleDay) => cycleDay[symptom]))
|
SYMPTOMS.filter((symptom) => cycleDays.some((cycleDay) => cycleDay[symptom]))
|
||||||
|
|
||||||
const CycleChart = ({ navigation }) => {
|
const CycleChart = ({ navigate, setDate }) => {
|
||||||
const [shouldShowHint, setShouldShowHint] = useState(true)
|
const [shouldShowHint, setShouldShowHint] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -84,8 +84,9 @@ const CycleChart = ({ navigation }) => {
|
|||||||
const renderColumn = ({ item }) => {
|
const renderColumn = ({ item }) => {
|
||||||
return (
|
return (
|
||||||
<DayColumn
|
<DayColumn
|
||||||
|
setDate={setDate}
|
||||||
dateString={item}
|
dateString={item}
|
||||||
navigation={navigation}
|
navigate={navigate}
|
||||||
symptomHeight={symptomHeight}
|
symptomHeight={symptomHeight}
|
||||||
columnHeight={columnHeight}
|
columnHeight={columnHeight}
|
||||||
symptomRowSymptoms={symptomRowSymptoms}
|
symptomRowSymptoms={symptomRowSymptoms}
|
||||||
@@ -99,7 +100,7 @@ const CycleChart = ({ navigation }) => {
|
|||||||
const hasDataToDisplay = chartSymptoms.length > 0
|
const hasDataToDisplay = chartSymptoms.length > 0
|
||||||
|
|
||||||
if (!hasDataToDisplay) {
|
if (!hasDataToDisplay) {
|
||||||
return <NoData />
|
return <NoData navigate={navigate} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -134,9 +135,8 @@ const CycleChart = ({ navigation }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CycleChart.propTypes = {
|
CycleChart.propTypes = {
|
||||||
navigation: PropTypes.shape({
|
navigate: PropTypes.func,
|
||||||
navigate: PropTypes.func.isRequired,
|
setDate: PropTypes.func,
|
||||||
}).isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ const DayColumn = ({
|
|||||||
dateString,
|
dateString,
|
||||||
chartSymptoms,
|
chartSymptoms,
|
||||||
columnHeight,
|
columnHeight,
|
||||||
navigation,
|
setDate,
|
||||||
|
navigate,
|
||||||
shouldShowTemperatureColumn,
|
shouldShowTemperatureColumn,
|
||||||
symptomHeight,
|
symptomHeight,
|
||||||
symptomRowSymptoms,
|
symptomRowSymptoms,
|
||||||
@@ -59,8 +60,10 @@ const DayColumn = ({
|
|||||||
columnHeight
|
columnHeight
|
||||||
)
|
)
|
||||||
|
|
||||||
const onDaySelect = (date) =>
|
const onDaySelect = (date) => {
|
||||||
navigation.navigate('CycleDayOverview', { date })
|
setDate(date)
|
||||||
|
navigate('CycleDay')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity onPress={() => onDaySelect(dateString)} activeOpacity={1}>
|
<TouchableOpacity onPress={() => onDaySelect(dateString)} activeOpacity={1}>
|
||||||
@@ -101,13 +104,12 @@ DayColumn.propTypes = {
|
|||||||
dateString: PropTypes.string.isRequired,
|
dateString: PropTypes.string.isRequired,
|
||||||
chartSymptoms: PropTypes.array,
|
chartSymptoms: PropTypes.array,
|
||||||
columnHeight: PropTypes.number.isRequired,
|
columnHeight: PropTypes.number.isRequired,
|
||||||
|
navigate: PropTypes.func.isRequired,
|
||||||
|
setDate: PropTypes.func.isRequired,
|
||||||
shouldShowTemperatureColumn: PropTypes.bool,
|
shouldShowTemperatureColumn: PropTypes.bool,
|
||||||
symptomHeight: PropTypes.number.isRequired,
|
symptomHeight: PropTypes.number.isRequired,
|
||||||
symptomRowSymptoms: PropTypes.array,
|
symptomRowSymptoms: PropTypes.array,
|
||||||
xAxisHeight: PropTypes.number,
|
xAxisHeight: PropTypes.number,
|
||||||
navigation: PropTypes.shape({
|
|
||||||
navigate: PropTypes.func.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DayColumn
|
export default DayColumn
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import { getData, nextDate, prevDate } from '../helpers/cycle-day'
|
|||||||
import { Spacing } from '../../styles'
|
import { Spacing } from '../../styles'
|
||||||
import { SYMPTOMS } from '../../config'
|
import { SYMPTOMS } from '../../config'
|
||||||
|
|
||||||
const CycleDayOverView = ({ route }) => {
|
const CycleDayOverView = ({ date, setDate, isTemperatureEditView }) => {
|
||||||
const { date, isTemperatureEditView } = route.params
|
|
||||||
const cycleDay = getCycleDay(date)
|
const cycleDay = getCycleDay(date)
|
||||||
|
|
||||||
const [editedSymptom, setEditedSymptom] = useState(
|
const [editedSymptom, setEditedSymptom] = useState(
|
||||||
@@ -21,11 +20,11 @@ const CycleDayOverView = ({ route }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const showNextCycleDay = () => {
|
const showNextCycleDay = () => {
|
||||||
//setDate(nextDate(date))
|
setDate(nextDate(date))
|
||||||
}
|
}
|
||||||
|
|
||||||
const showPrevCycleDay = () => {
|
const showPrevCycleDay = () => {
|
||||||
//setDate(prevDate(date))
|
setDate(prevDate(date))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,12 +57,10 @@ const CycleDayOverView = ({ route }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CycleDayOverView.propTypes = {
|
CycleDayOverView.propTypes = {
|
||||||
route: PropTypes.shape({
|
cycleDay: PropTypes.object,
|
||||||
params: PropTypes.shape({
|
date: PropTypes.string,
|
||||||
date: PropTypes.string,
|
setDate: PropTypes.func,
|
||||||
isTemperatureEditView: PropTypes.bool,
|
isTemperatureEditView: PropTypes.bool,
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ import HamburgerMenu from './hamburger-menu'
|
|||||||
|
|
||||||
import { Colors, Containers, Sizes } from '../../styles'
|
import { Colors, Containers, Sizes } from '../../styles'
|
||||||
|
|
||||||
const Header = ({ isStatic, navigation }) => {
|
const Header = ({ isStatic, navigate }) => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
{isStatic ? (
|
{isStatic ? (
|
||||||
<Logo />
|
<Logo />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<TouchableOpacity onPress={() => navigation.navigate('Home')}>
|
<TouchableOpacity onPress={() => navigate('Home')}>
|
||||||
<Logo />
|
<Logo />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<HamburgerMenu navigate={navigation.navigate} />
|
<HamburgerMenu navigate={navigate} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -26,9 +26,7 @@ const Header = ({ isStatic, navigation }) => {
|
|||||||
|
|
||||||
Header.propTypes = {
|
Header.propTypes = {
|
||||||
isStatic: PropTypes.bool,
|
isStatic: PropTypes.bool,
|
||||||
navigation: PropTypes.shape({
|
navigate: PropTypes.func,
|
||||||
navigate: PropTypes.func.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Header.defaultProps = {
|
Header.defaultProps = {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { StyleSheet, View } from 'react-native'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import MenuItem from './menu-item'
|
||||||
|
|
||||||
|
import { Containers } from '../../styles'
|
||||||
|
import { pages } from '../pages'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
const Menu = ({ currentPage, navigate }) => {
|
||||||
|
const menuItems = pages.filter((page) => page.isInMenu)
|
||||||
|
|
||||||
|
const { t } = useTranslation(null, { keyPrefix: 'bottomMenu' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{menuItems.map(({ icon, labelKey, component }) => {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
isActive={component === currentPage}
|
||||||
|
onPress={() => navigate(component)}
|
||||||
|
icon={icon}
|
||||||
|
key={labelKey}
|
||||||
|
label={t(labelKey)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu.propTypes = {
|
||||||
|
currentPage: PropTypes.string,
|
||||||
|
navigate: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
...Containers.rowContainer,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Menu
|
||||||
+3
-15
@@ -1,75 +1,63 @@
|
|||||||
import settingsViews from './settings'
|
import settingsViews from './settings'
|
||||||
|
|
||||||
import settingsLabels from '../i18n/en/settings'
|
|
||||||
const labels = settingsLabels.menuItems
|
|
||||||
|
|
||||||
export const pages = [
|
export const pages = [
|
||||||
{
|
{
|
||||||
component: 'Home',
|
component: 'Home',
|
||||||
icon: 'home',
|
icon: 'home',
|
||||||
label: 'Home',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Calendar',
|
component: 'Calendar',
|
||||||
icon: 'calendar',
|
icon: 'calendar',
|
||||||
isInMenu: true,
|
isInMenu: true,
|
||||||
label: 'Calendar',
|
labelKey: 'calendar',
|
||||||
parent: 'Home',
|
parent: 'Home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Chart',
|
component: 'Chart',
|
||||||
icon: 'chart',
|
icon: 'chart',
|
||||||
isInMenu: true,
|
isInMenu: true,
|
||||||
label: 'Chart',
|
labelKey: 'chart',
|
||||||
parent: 'Home',
|
parent: 'Home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Stats',
|
component: 'Stats',
|
||||||
icon: 'statistics',
|
icon: 'statistics',
|
||||||
isInMenu: true,
|
isInMenu: true,
|
||||||
label: 'Stats',
|
labelKey: 'stats',
|
||||||
parent: 'Home',
|
parent: 'Home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
children: Object.keys(settingsViews),
|
children: Object.keys(settingsViews),
|
||||||
component: 'SettingsMenu',
|
component: 'SettingsMenu',
|
||||||
icon: 'settings',
|
icon: 'settings',
|
||||||
label: 'Settings',
|
|
||||||
parent: 'Home',
|
parent: 'Home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Reminders',
|
component: 'Reminders',
|
||||||
label: labels.reminders.name,
|
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'NfpSettings',
|
component: 'NfpSettings',
|
||||||
label: labels.nfpSettings.name,
|
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'DataManagement',
|
component: 'DataManagement',
|
||||||
label: labels.dataManagement.name,
|
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Password',
|
component: 'Password',
|
||||||
label: labels.password.name,
|
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'About',
|
component: 'About',
|
||||||
label: 'About',
|
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'License',
|
component: 'License',
|
||||||
label: 'License',
|
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'PrivacyPolicy',
|
component: 'PrivacyPolicy',
|
||||||
label: 'PrivacyPolicy',
|
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+8
-27
@@ -6,40 +6,23 @@ import AppText from '../../common/app-text'
|
|||||||
import Button from '../../common/button'
|
import Button from '../../common/button'
|
||||||
import Segment from '../../common/segment'
|
import Segment from '../../common/segment'
|
||||||
|
|
||||||
import { openImportDialog, getFileContent, importData } from './import-dialog'
|
|
||||||
import openShareDialogAndExport from './export-dialog'
|
import openShareDialogAndExport from './export-dialog'
|
||||||
import DeleteData from './delete-data'
|
import DeleteData from './delete-data'
|
||||||
|
|
||||||
import labels from '../../../i18n/en/settings'
|
import labels from '../../../i18n/en/settings'
|
||||||
import { ACTION_DELETE, ACTION_EXPORT, ACTION_IMPORT } from '../../../config'
|
import ImportData from './ImportData'
|
||||||
|
|
||||||
const DataManagement = () => {
|
const DataManagement = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [currentAction, setCurrentAction] = useState(null)
|
const [isDeletingData, setIsDeletingData] = useState(false)
|
||||||
|
|
||||||
const startImportFlow = async (shouldDeleteExistingData) => {
|
|
||||||
setIsLoading(true)
|
|
||||||
const fileContent = await getFileContent()
|
|
||||||
if (fileContent) {
|
|
||||||
await importData(shouldDeleteExistingData, fileContent)
|
|
||||||
}
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const startExport = () => {
|
const startExport = () => {
|
||||||
setCurrentAction(ACTION_EXPORT)
|
setIsDeletingData(false)
|
||||||
openShareDialogAndExport()
|
openShareDialogAndExport()
|
||||||
}
|
}
|
||||||
|
|
||||||
const startImport = () => {
|
|
||||||
setCurrentAction(ACTION_IMPORT)
|
|
||||||
openImportDialog(startImportFlow)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) return <AppLoadingView />
|
if (isLoading) return <AppLoadingView />
|
||||||
|
|
||||||
const isDeletingData = currentAction === ACTION_DELETE
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppPage>
|
<AppPage>
|
||||||
<Segment title={labels.export.button}>
|
<Segment title={labels.export.button}>
|
||||||
@@ -48,17 +31,15 @@ const DataManagement = () => {
|
|||||||
{labels.export.button}
|
{labels.export.button}
|
||||||
</Button>
|
</Button>
|
||||||
</Segment>
|
</Segment>
|
||||||
<Segment title={labels.import.button}>
|
<ImportData
|
||||||
<AppText>{labels.import.segmentExplainer}</AppText>
|
resetIsDeletingData={() => setIsDeletingData(false)}
|
||||||
<Button isCTA onPress={startImport}>
|
setIsLoading={setIsLoading}
|
||||||
{labels.import.button}
|
/>
|
||||||
</Button>
|
|
||||||
</Segment>
|
|
||||||
<Segment title={labels.deleteSegment.title} last>
|
<Segment title={labels.deleteSegment.title} last>
|
||||||
<AppText>{labels.deleteSegment.explainer}</AppText>
|
<AppText>{labels.deleteSegment.explainer}</AppText>
|
||||||
<DeleteData
|
<DeleteData
|
||||||
isDeletingData={isDeletingData}
|
isDeletingData={isDeletingData}
|
||||||
onStartDeletion={() => setCurrentAction(ACTION_DELETE)}
|
onStartDeletion={() => setIsDeletingData(true)}
|
||||||
/>
|
/>
|
||||||
</Segment>
|
</Segment>
|
||||||
</AppPage>
|
</AppPage>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Alert } from 'react-native'
|
||||||
|
import DocumentPicker from 'react-native-document-picker'
|
||||||
|
import rnfs from 'react-native-fs'
|
||||||
|
import importCsv from '../../../lib/import-export/import-from-csv'
|
||||||
|
import alertError from '../common/alert-error'
|
||||||
|
import Segment from '../../common/segment'
|
||||||
|
import AppText from '../../common/app-text'
|
||||||
|
import Button from '../../common/button'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export default function ImportData({ resetIsDeletingData, setIsLoading }) {
|
||||||
|
const { t } = useTranslation(null, {
|
||||||
|
keyPrefix: 'hamburgerMenu.settings.data.import',
|
||||||
|
})
|
||||||
|
|
||||||
|
async function startImport(shouldDeleteExistingData) {
|
||||||
|
setIsLoading(true)
|
||||||
|
await importData(shouldDeleteExistingData)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileInfo() {
|
||||||
|
try {
|
||||||
|
const fileInfo = await DocumentPicker.pickSingle({
|
||||||
|
type: [DocumentPicker.types.csv, 'text/comma-separated-values'],
|
||||||
|
})
|
||||||
|
return fileInfo
|
||||||
|
} catch (error) {
|
||||||
|
if (DocumentPicker.isCancel(error)) return // User cancelled the picker, exit any dialogs or menus and move on
|
||||||
|
showImportErrorAlert(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileContent() {
|
||||||
|
const fileInfo = await getFileInfo()
|
||||||
|
if (!fileInfo) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileContent = await rnfs.readFile(fileInfo.uri, 'utf8')
|
||||||
|
return fileContent
|
||||||
|
} catch (err) {
|
||||||
|
return showImportErrorAlert(t('error.couldNotOpenFile'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importData(shouldDeleteExistingData) {
|
||||||
|
const fileContent = await getFileContent()
|
||||||
|
if (!fileContent) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await importCsv(fileContent, shouldDeleteExistingData)
|
||||||
|
Alert.alert(t('success.title'), t('success.message'))
|
||||||
|
} catch (err) {
|
||||||
|
showImportErrorAlert(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openImportDialog() {
|
||||||
|
resetIsDeletingData()
|
||||||
|
Alert.alert(t('dialog.title'), t('dialog.message'), [
|
||||||
|
{
|
||||||
|
text: t('dialog.cancel'),
|
||||||
|
style: 'cancel',
|
||||||
|
onPress: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t('dialog.replace'),
|
||||||
|
onPress: () => startImport(false),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t('dialog.delete'),
|
||||||
|
onPress: () => startImport(true),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function showImportErrorAlert(message) {
|
||||||
|
const errorMessage = t('error.noDataImported', { message })
|
||||||
|
alertError(errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment title={t('button')}>
|
||||||
|
<AppText>{t('segmentExplainer')}</AppText>
|
||||||
|
<Button isCTA onPress={openImportDialog}>
|
||||||
|
{t('button')}
|
||||||
|
</Button>
|
||||||
|
</Segment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportData.propTypes = {
|
||||||
|
resetIsDeletingData: PropTypes.func.isRequired,
|
||||||
|
setIsLoading: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { Alert } from 'react-native'
|
|
||||||
import DocumentPicker from 'react-native-document-picker'
|
|
||||||
import rnfs from 'react-native-fs'
|
|
||||||
import importCsv from '../../../lib/import-export/import-from-csv'
|
|
||||||
import { shared as sharedLabels } from '../../../i18n/en/labels'
|
|
||||||
import labels from '../../../i18n/en/settings'
|
|
||||||
import alertError from '../common/alert-error'
|
|
||||||
|
|
||||||
export function openImportDialog(onImportData) {
|
|
||||||
Alert.alert(labels.import.title, labels.import.message, [
|
|
||||||
{
|
|
||||||
text: sharedLabels.cancel,
|
|
||||||
style: 'cancel',
|
|
||||||
onPress: () => {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: labels.import.replaceOption,
|
|
||||||
onPress: () => onImportData(false),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: labels.import.deleteOption,
|
|
||||||
onPress: () => onImportData(true),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getFileContent() {
|
|
||||||
let fileInfo
|
|
||||||
try {
|
|
||||||
fileInfo = await DocumentPicker.pickSingle({
|
|
||||||
type: [DocumentPicker.types.csv, 'text/comma-separated-values'],
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (DocumentPicker.isCancel(error)) {
|
|
||||||
// User cancelled the picker, exit any dialogs or menus and move on
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
importError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileContent
|
|
||||||
try {
|
|
||||||
fileContent = await rnfs.readFile(fileInfo.uri, 'utf8')
|
|
||||||
} catch (err) {
|
|
||||||
return importError(labels.import.errors.couldNotOpenFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileContent
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function importData(shouldDeleteExistingData, fileContent) {
|
|
||||||
try {
|
|
||||||
await importCsv(fileContent, shouldDeleteExistingData)
|
|
||||||
Alert.alert(sharedLabels.successTitle, labels.import.success.message)
|
|
||||||
} catch (err) {
|
|
||||||
importError(err.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function importError(msg) {
|
|
||||||
const postFixed = `${msg}\n\n${labels.import.errors.postFix}`
|
|
||||||
alertError(postFixed)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import Reminders from './reminders/reminders'
|
import Reminders from './reminders/reminders'
|
||||||
import NfpSettings from './nfp-settings'
|
import NfpSettings from './nfp-settings'
|
||||||
import DataManagement from './data-management'
|
import DataManagement from './data-management/DataManagement'
|
||||||
import Password from './password'
|
import Password from './password'
|
||||||
import About from './About'
|
import About from './About'
|
||||||
import License from './License'
|
import License from './License'
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { PixelRatio, StatusBar } from 'react-native'
|
import { PixelRatio, StatusBar } from 'react-native'
|
||||||
import { scale, verticalScale } from 'react-native-size-matters'
|
import { scale, verticalScale } from 'react-native-size-matters'
|
||||||
|
|
||||||
export const ACTION_DELETE = 'delete'
|
|
||||||
export const ACTION_EXPORT = 'export'
|
|
||||||
export const ACTION_IMPORT = 'import'
|
|
||||||
|
|
||||||
export const SYMPTOMS = [
|
export const SYMPTOMS = [
|
||||||
'bleeding',
|
'bleeding',
|
||||||
'temperature',
|
'temperature',
|
||||||
@@ -40,7 +36,7 @@ export const HIT_SLOP = {
|
|||||||
top: verticalScale(20),
|
top: verticalScale(20),
|
||||||
bottom: verticalScale(20),
|
bottom: verticalScale(20),
|
||||||
left: scale(20),
|
left: scale(20),
|
||||||
right: scale(20)
|
right: scale(20),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STATUSBAR_HEIGHT = StatusBar.currentHeight
|
export const STATUSBAR_HEIGHT = StatusBar.currentHeight
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"bottomMenu": {
|
||||||
|
"calendar": "Calendar",
|
||||||
|
"chart": "Chart",
|
||||||
|
"stats": "Stats"
|
||||||
|
},
|
||||||
|
"chart": {
|
||||||
|
"tutorial": "You can swipe the chart to view more dates."
|
||||||
|
},
|
||||||
"cycleDay": {
|
"cycleDay": {
|
||||||
"symptomBox": {
|
"symptomBox": {
|
||||||
"bleeding": "Bleeding",
|
"bleeding": "Bleeding",
|
||||||
@@ -80,6 +88,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"data": {
|
||||||
|
"import": {
|
||||||
|
"button": "Import data",
|
||||||
|
"dialog": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"delete": "Import and delete existing",
|
||||||
|
"message": "There are two options for the import:\n\n1. Keep existing cycle days and replace only the ones in the import file.\n\n2. Delete all existing cycle days and import cycle days from file",
|
||||||
|
"replace": "Import and replace",
|
||||||
|
"title": "Keep existing data?"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"couldNotOpenFile": "Could not open file",
|
||||||
|
"futureEdit": "Future dates may only contain a note, no other symptoms",
|
||||||
|
"incorrectColumns": "Expected CSV column titles to be {{incorrectColumns}}",
|
||||||
|
"noDataImported": "{{message}}\n\nNo data was imported or changed"
|
||||||
|
},
|
||||||
|
"segmentExplainer": "Import data in CSV format",
|
||||||
|
"success": {
|
||||||
|
"message": "Data successfully imported",
|
||||||
|
"title": "Success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"menuItem": {
|
"menuItem": {
|
||||||
"dataManagement": {
|
"dataManagement": {
|
||||||
"name": "Data",
|
"name": "Data",
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ export const home = {
|
|||||||
phase: (n) => `${['1st', '2nd', '3rd'][n - 1]} cycle phase`,
|
phase: (n) => `${['1st', '2nd', '3rd'][n - 1]} cycle phase`,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const chart = {
|
|
||||||
tutorial: 'You can swipe the chart to view more dates.',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const shared = {
|
export const shared = {
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
|
|||||||
@@ -1,24 +1,6 @@
|
|||||||
import links from './links'
|
import links from './links'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
menuItems: {
|
|
||||||
reminders: {
|
|
||||||
name: 'Reminders',
|
|
||||||
text: 'turn on/off reminders',
|
|
||||||
},
|
|
||||||
nfpSettings: {
|
|
||||||
name: 'NFP settings',
|
|
||||||
text: 'define how you want to use NFP',
|
|
||||||
},
|
|
||||||
dataManagement: {
|
|
||||||
name: 'Data',
|
|
||||||
text: 'import, export or delete your data',
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
name: 'Password',
|
|
||||||
text: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
export: {
|
export: {
|
||||||
errors: {
|
errors: {
|
||||||
noData: 'There is no data to export',
|
noData: 'There is no data to export',
|
||||||
@@ -31,24 +13,6 @@ export default {
|
|||||||
segmentExplainer:
|
segmentExplainer:
|
||||||
'Export data in CSV format for backup or so you can use it elsewhere',
|
'Export data in CSV format for backup or so you can use it elsewhere',
|
||||||
},
|
},
|
||||||
import: {
|
|
||||||
button: 'Import data',
|
|
||||||
title: 'Keep existing data?',
|
|
||||||
message: `There are two options for the import:
|
|
||||||
1. Keep existing cycle days and replace only the ones in the import file.
|
|
||||||
2. Delete all existing cycle days and import cycle days from file.`,
|
|
||||||
replaceOption: 'Import and replace',
|
|
||||||
deleteOption: 'Import and delete existing',
|
|
||||||
errors: {
|
|
||||||
couldNotOpenFile: 'Could not open file',
|
|
||||||
postFix: 'No data was imported or changed',
|
|
||||||
futureEdit: 'Future dates may only contain a note, no other symptoms',
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
message: 'Data successfully imported',
|
|
||||||
},
|
|
||||||
segmentExplainer: 'Import data in CSV format',
|
|
||||||
},
|
|
||||||
deleteSegment: {
|
deleteSegment: {
|
||||||
title: 'Delete app data',
|
title: 'Delete app data',
|
||||||
explainer: 'Delete app data from this phone',
|
explainer: 'Delete app data from this phone',
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ i18n
|
|||||||
compatibilityJSON: 'v3', // TODO: migrate json to v4 and afterwards remove it
|
compatibilityJSON: 'v3', // TODO: migrate json to v4 and afterwards remove it
|
||||||
resources,
|
resources,
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
debug: true,
|
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false, // not needed for react as it escapes by default
|
escapeValue: false, // not needed for react as it escapes by default
|
||||||
|
|||||||
@@ -5,4 +5,8 @@ module.exports = {
|
|||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: [
|
||||||
'node_modules/(?!((jest-)?react-native(-.*)?|@react-native(-community)?)/)',
|
'node_modules/(?!((jest-)?react-native(-.*)?|@react-native(-community)?)/)',
|
||||||
],
|
],
|
||||||
|
watchPlugins: [
|
||||||
|
'jest-watch-typeahead/filename',
|
||||||
|
'jest-watch-typeahead/testname',
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import getColumnNamesForCsv from './get-csv-column-names'
|
import getColumnNamesForCsv from './get-csv-column-names'
|
||||||
import replaceWithNullIfAllPropertiesAreNull from './replace-with-null'
|
import replaceWithNullIfAllPropertiesAreNull from './replace-with-null'
|
||||||
import { LocalDate } from '@js-joda/core'
|
import { LocalDate } from '@js-joda/core'
|
||||||
import labels from '../../i18n/en/settings'
|
import i18next from 'i18next'
|
||||||
|
|
||||||
export default async function importCsv(csv, deleteFirst) {
|
export default async function importCsv(csv, deleteFirst) {
|
||||||
const parseFuncs = {
|
const parseFuncs = {
|
||||||
@@ -46,7 +46,10 @@ export default async function importCsv(csv, deleteFirst) {
|
|||||||
|
|
||||||
const cycleDays = await csvParser(config)
|
const cycleDays = await csvParser(config)
|
||||||
.fromString(csv)
|
.fromString(csv)
|
||||||
.on('header', validateHeaders)
|
.on('header', (headers) => validateHeaders(headers))
|
||||||
|
.on('error', (error) => {
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
|
||||||
//remove symptoms where all fields are null
|
//remove symptoms where all fields are null
|
||||||
putNullForEmptySymptoms(cycleDays)
|
putNullForEmptySymptoms(cycleDays)
|
||||||
@@ -67,8 +70,11 @@ function validateHeaders(headers) {
|
|||||||
return expectedHeaders.indexOf(header) > -1
|
return expectedHeaders.indexOf(header) > -1
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
const msg = `Expected CSV column titles to be ${expectedHeaders.join()}`
|
throw new Error(
|
||||||
throw new Error(msg)
|
i18next.t('hamburgerMenu.settings.data.import.error.incorrectColumns', {
|
||||||
|
incorrectColumns: expectedHeaders.join(),
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +98,9 @@ function throwIfFutureData(cycleDays) {
|
|||||||
day.date > today &&
|
day.date > today &&
|
||||||
Object.keys(day).some((symptom) => symptom != 'date' && symptom != 'note')
|
Object.keys(day).some((symptom) => symptom != 'date' && symptom != 'note')
|
||||||
) {
|
) {
|
||||||
throw new Error(labels.import.errors.futureEdit)
|
throw new Error(
|
||||||
|
i18next.t('hamburgerMenu.settings.data.import.error.futureEdit')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-4
@@ -1,17 +1,16 @@
|
|||||||
export default function (feeling, texture) {
|
export default function getSensiplanMucus(feeling, texture) {
|
||||||
|
|
||||||
if (typeof feeling != 'number' || typeof texture != 'number') return null
|
if (typeof feeling != 'number' || typeof texture != 'number') return null
|
||||||
|
|
||||||
const feelingMapping = {
|
const feelingMapping = {
|
||||||
0: 0,
|
0: 0,
|
||||||
1: 1,
|
1: 1,
|
||||||
2: 2,
|
2: 2,
|
||||||
3: 4
|
3: 4,
|
||||||
}
|
}
|
||||||
const textureMapping = {
|
const textureMapping = {
|
||||||
0: 0,
|
0: 0,
|
||||||
1: 3,
|
1: 3,
|
||||||
2: 4
|
2: 4,
|
||||||
}
|
}
|
||||||
const nfpFeelingValue = feelingMapping[feeling]
|
const nfpFeelingValue = feelingMapping[feeling]
|
||||||
const nfpTextureValue = textureMapping[texture]
|
const nfpTextureValue = textureMapping[texture]
|
||||||
|
|||||||
@@ -11,15 +11,16 @@ import { getBleedingDaysSortedByDate } from '../db'
|
|||||||
import cycleModule from './cycle'
|
import cycleModule from './cycle'
|
||||||
import nothingChanged from '../db/db-unchanged'
|
import nothingChanged from '../db/db-unchanged'
|
||||||
|
|
||||||
export default function setupNotifications(navigation) {
|
export default function setupNotifications(navigate, setDate) {
|
||||||
Notification.configure({
|
Notification.configure({
|
||||||
onNotification: (notification) => {
|
onNotification: (notification) => {
|
||||||
const date = LocalDate.now().toString()
|
|
||||||
// https://github.com/zo0r/react-native-push-notification/issues/966#issuecomment-479069106
|
// https://github.com/zo0r/react-native-push-notification/issues/966#issuecomment-479069106
|
||||||
if (notification.data?.id === '1' || notification.id === '1') {
|
if (notification.data?.id === '1' || notification.id === '1') {
|
||||||
navigation.navigate('TemperatureEditView', { date })
|
const todayDate = LocalDate.now().toString()
|
||||||
|
setDate(todayDate)
|
||||||
|
navigate('TemperatureEditView')
|
||||||
} else {
|
} else {
|
||||||
navigation.navigate('Home', { date })
|
navigate('Home')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
+8
-14
@@ -35,28 +35,21 @@
|
|||||||
"@react-native-community/art": "^1.2.0",
|
"@react-native-community/art": "^1.2.0",
|
||||||
"@react-native-community/datetimepicker": "^6.3.1",
|
"@react-native-community/datetimepicker": "^6.3.1",
|
||||||
"@react-native-community/push-notification-ios": "^1.8.0",
|
"@react-native-community/push-notification-ios": "^1.8.0",
|
||||||
"@react-navigation/bottom-tabs": "^6.4.0",
|
|
||||||
"@react-navigation/native": "^6.0.13",
|
|
||||||
"@react-navigation/native-stack": "^6.9.0",
|
|
||||||
"@react-navigation/stack": "^6.3.1",
|
|
||||||
"csvtojson": "^2.0.8",
|
"csvtojson": "^2.0.8",
|
||||||
"i18next": "^21.9.0",
|
"i18next": "^22.4.13",
|
||||||
"jshashes": "^1.0.8",
|
"jshashes": "^1.0.8",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"object-path": "^0.11.4",
|
"object-path": "^0.11.4",
|
||||||
"obv": "0.0.1",
|
"obv": "0.0.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-i18next": "^11.18.3",
|
"react-i18next": "^12.0.0",
|
||||||
"react-native": "0.67.4",
|
"react-native": "0.67.4",
|
||||||
"react-native-calendars": "^1.1287.0",
|
"react-native-calendars": "^1.1287.0",
|
||||||
"react-native-document-picker": "^8.1.1",
|
"react-native-document-picker": "^8.1.1",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-gesture-handler": "^2.6.2",
|
|
||||||
"react-native-modal-datetime-picker": "14.0.0",
|
"react-native-modal-datetime-picker": "14.0.0",
|
||||||
"react-native-push-notification": "3.2.1",
|
"react-native-push-notification": "3.2.1",
|
||||||
"react-native-safe-area-context": "^4.3.4",
|
|
||||||
"react-native-screens": "^3.17.0",
|
|
||||||
"react-native-share": "^7.9.0",
|
"react-native-share": "^7.9.0",
|
||||||
"react-native-simple-toast": "^1.1.3",
|
"react-native-simple-toast": "^1.1.3",
|
||||||
"react-native-size-matters": "^0.4.0",
|
"react-native-size-matters": "^0.4.0",
|
||||||
@@ -65,17 +58,18 @@
|
|||||||
"sympto": "3.0.1"
|
"sympto": "3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.9",
|
"@babel/core": "^7.20.2",
|
||||||
"@babel/eslint-parser": "^7.19.1",
|
"@babel/eslint-parser": "^7.19.1",
|
||||||
"@babel/preset-react": "^7.16.0",
|
"@babel/preset-react": "^7.18.6",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@testing-library/jest-native": "^4.0.12",
|
"@testing-library/jest-native": "^4.0.12",
|
||||||
"@testing-library/react-native": "^11.1.0",
|
"@testing-library/react-native": "^11.1.0",
|
||||||
"basic-changelog": "gitlab:bloodyhealth/basic-changelog",
|
"basic-changelog": "gitlab:bloodyhealth/basic-changelog",
|
||||||
"eslint": "7.14.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-react": "^7.8.2",
|
"eslint-plugin-react": "^7.31.10",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"jest": "^28.1.3",
|
"jest": "^29.1.2",
|
||||||
|
"jest-watch-typeahead": "^2.2.0",
|
||||||
"jetifier": "^1.6.6",
|
"jetifier": "^1.6.6",
|
||||||
"metro-react-native-babel-preset": "^0.66.2",
|
"metro-react-native-babel-preset": "^0.66.2",
|
||||||
"prettier": "2.4.0",
|
"prettier": "2.4.0",
|
||||||
|
|||||||
@@ -1,27 +1,19 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render, screen, fireEvent } from '@testing-library/react-native'
|
|
||||||
import AcceptLicense from '../components/AcceptLicense'
|
import AcceptLicense from '../components/AcceptLicense'
|
||||||
|
|
||||||
import { saveLicenseFlag } from '../local-storage'
|
import { saveLicenseFlag } from '../local-storage'
|
||||||
|
import { render, screen, fireEvent } from './test-utils'
|
||||||
|
|
||||||
jest.mock('../local-storage', () => ({
|
jest.mock('../local-storage', () => ({
|
||||||
saveLicenseFlag: jest.fn(() => Promise.resolve()),
|
saveLicenseFlag: jest.fn(() => Promise.resolve()),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
jest.mock('react-i18next', () => ({
|
|
||||||
useTranslation: () => ({
|
|
||||||
t: (str, options) => {
|
|
||||||
return str + (options ? JSON.stringify(options) : '')
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('AcceptLicense', () => {
|
describe('AcceptLicense', () => {
|
||||||
test('On clicking OK button, the license is accepted', async () => {
|
test('should accept license when clicking ok button', async () => {
|
||||||
const mockedSetLicense = jest.fn()
|
const mockedSetLicense = jest.fn()
|
||||||
render(<AcceptLicense setLicense={mockedSetLicense} />)
|
render(<AcceptLicense setLicense={mockedSetLicense} />)
|
||||||
|
|
||||||
const okButton = screen.getByText('ok', { exact: false })
|
const okButton = screen.getByText('OK')
|
||||||
|
|
||||||
fireEvent(okButton, 'click')
|
fireEvent(okButton, 'click')
|
||||||
|
|
||||||
@@ -29,9 +21,9 @@ describe('AcceptLicense', () => {
|
|||||||
expect(mockedSetLicense).toHaveBeenCalled()
|
expect(mockedSetLicense).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('There is a Cancel button', async () => {
|
test('should render cancel button', async () => {
|
||||||
render(<AcceptLicense setLicense={jest.fn()} />)
|
render(<AcceptLicense setLicense={jest.fn()} />)
|
||||||
|
|
||||||
screen.getByText('cancel', { exact: false })
|
screen.getByText('Cancel')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+3
-11
@@ -1,24 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render, screen } from '@testing-library/react-native'
|
|
||||||
import License from '../components/settings/License'
|
import License from '../components/settings/License'
|
||||||
|
import { render, screen } from './test-utils'
|
||||||
jest.mock('react-i18next', () => ({
|
|
||||||
useTranslation: () => ({
|
|
||||||
t: (str, options) => {
|
|
||||||
return str + (options ? JSON.stringify(options) : '')
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('License screen', () => {
|
describe('License screen', () => {
|
||||||
test('It should have a correct year', async () => {
|
test('should display license text with correct year', async () => {
|
||||||
render(<License />)
|
render(<License />)
|
||||||
const year = new Date().getFullYear().toString()
|
const year = new Date().getFullYear().toString()
|
||||||
|
|
||||||
screen.getByText(year, { exact: false })
|
screen.getByText(year, { exact: false })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('It should match the snapshot', async () => {
|
test('should match the snapshot', async () => {
|
||||||
const licenseScreen = render(<License />)
|
const licenseScreen = render(<License />)
|
||||||
|
|
||||||
expect(licenseScreen).toMatchSnapshot()
|
expect(licenseScreen).toMatchSnapshot()
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`License screen It should match the snapshot 1`] = `
|
exports[`License screen should match the snapshot 1`] = `
|
||||||
<View
|
<View
|
||||||
style={
|
style={
|
||||||
Object {
|
{
|
||||||
"backgroundColor": "#E9F2ED",
|
"backgroundColor": "#E9F2ED",
|
||||||
"flex": 1,
|
"flex": 1,
|
||||||
}
|
}
|
||||||
@@ -11,8 +11,8 @@ exports[`License screen It should match the snapshot 1`] = `
|
|||||||
>
|
>
|
||||||
<RCTScrollView
|
<RCTScrollView
|
||||||
contentContainerStyle={
|
contentContainerStyle={
|
||||||
Array [
|
[
|
||||||
Object {
|
{
|
||||||
"backgroundColor": "#E9F2ED",
|
"backgroundColor": "#E9F2ED",
|
||||||
"flexGrow": 1,
|
"flexGrow": 1,
|
||||||
},
|
},
|
||||||
@@ -23,13 +23,13 @@ exports[`License screen It should match the snapshot 1`] = `
|
|||||||
<View>
|
<View>
|
||||||
<Text
|
<Text
|
||||||
style={
|
style={
|
||||||
Array [
|
[
|
||||||
Object {
|
{
|
||||||
"color": "#555",
|
"color": "#555",
|
||||||
"fontFamily": "Jost-Book",
|
"fontFamily": "Jost-Book",
|
||||||
"fontSize": 34.285714285714285,
|
"fontSize": 34.285714285714285,
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"alignSelf": "center",
|
"alignSelf": "center",
|
||||||
"color": "#3A2671",
|
"color": "#3A2671",
|
||||||
"fontFamily": "Jost-Bold",
|
"fontFamily": "Jost-Bold",
|
||||||
@@ -41,11 +41,11 @@ exports[`License screen It should match the snapshot 1`] = `
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
title
|
drip. an open-source cycle tracking app
|
||||||
</Text>
|
</Text>
|
||||||
<View
|
<View
|
||||||
style={
|
style={
|
||||||
Object {
|
{
|
||||||
"marginBottom": 34.285714285714285,
|
"marginBottom": 34.285714285714285,
|
||||||
"marginHorizontal": 34.285714285714285,
|
"marginHorizontal": 34.285714285714285,
|
||||||
}
|
}
|
||||||
@@ -53,8 +53,8 @@ exports[`License screen It should match the snapshot 1`] = `
|
|||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={
|
style={
|
||||||
Array [
|
[
|
||||||
Object {
|
{
|
||||||
"color": "#555",
|
"color": "#555",
|
||||||
"fontFamily": "Jost-Book",
|
"fontFamily": "Jost-Book",
|
||||||
"fontSize": 34.285714285714285,
|
"fontSize": 34.285714285714285,
|
||||||
@@ -63,12 +63,14 @@ exports[`License screen It should match the snapshot 1`] = `
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
text{"currentYear":2022}
|
Copyright (C) 2023 Heart of Code e.V.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details:
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
onPress={[Function]}
|
onPress={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
{
|
||||||
"color": "#3A2671",
|
"color": "#3A2671",
|
||||||
"fontFamily": "Jost-Book",
|
"fontFamily": "Jost-Book",
|
||||||
"fontSize": 34.285714285714285,
|
"fontSize": 34.285714285714285,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
exports[`Footnote component when children are present, renders them 1`] = `
|
exports[`Footnote component when children are present, renders them 1`] = `
|
||||||
<View
|
<View
|
||||||
style={
|
style={
|
||||||
Object {
|
{
|
||||||
"alignContent": "flex-start",
|
"alignContent": "flex-start",
|
||||||
"flexDirection": "row",
|
"flexDirection": "row",
|
||||||
"marginBottom": 8.571428571428571,
|
"marginBottom": 8.571428571428571,
|
||||||
@@ -13,13 +13,13 @@ exports[`Footnote component when children are present, renders them 1`] = `
|
|||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={
|
style={
|
||||||
Array [
|
[
|
||||||
Object {
|
{
|
||||||
"color": "#555",
|
"color": "#555",
|
||||||
"fontFamily": "Jost-Book",
|
"fontFamily": "Jost-Book",
|
||||||
"fontSize": 34.285714285714285,
|
"fontSize": 34.285714285714285,
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"color": "#F38337",
|
"color": "#F38337",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -29,18 +29,18 @@ exports[`Footnote component when children are present, renders them 1`] = `
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
linkStyle={
|
linkStyle={
|
||||||
Object {
|
{
|
||||||
"color": "white",
|
"color": "white",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
Array [
|
[
|
||||||
Object {
|
{
|
||||||
"color": "#555",
|
"color": "#555",
|
||||||
"fontFamily": "Jost-Book",
|
"fontFamily": "Jost-Book",
|
||||||
"fontSize": 34.285714285714285,
|
"fontSize": 34.285714285714285,
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"color": "#555",
|
"color": "#555",
|
||||||
"paddingLeft": 21.428571428571427,
|
"paddingLeft": 21.428571428571427,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { render } from '@testing-library/react-native'
|
||||||
|
import '../i18n/i18n'
|
||||||
|
|
||||||
|
const customRender = (ui, options) => render(ui, { ...options })
|
||||||
|
|
||||||
|
// re-export everything
|
||||||
|
export * from '@testing-library/react-native'
|
||||||
|
|
||||||
|
// override render method
|
||||||
|
export { customRender as render }
|
||||||
Reference in New Issue
Block a user