Compare commits

..

1 Commits

60 changed files with 539 additions and 1601 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"presets": ["module:metro-react-native-babel-preset"],
}
-2
View File
@@ -1,2 +0,0 @@
BUNDLE_PATH: "vendor/bundle"
BUNDLE_FORCE_RUBY_PLATFORM: 1
-1
View File
@@ -32,7 +32,6 @@ build/
.gradle
local.properties
*.iml
*.hprof
# node.js
#
-1
View File
@@ -1 +0,0 @@
2.7.4
-4
View File
@@ -1,4 +0,0 @@
source 'https://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby '2.7.4'
gem 'cocoapods', '~> 1.11', '>= 1.11.2'
-96
View File
@@ -1,96 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
rexml
activesupport (6.1.5)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
claide (1.1.0)
cocoapods (1.11.3)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.11.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 1.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.11.3)
activesupport (>= 5.0, < 7)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.5.1)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored2 (3.1.2)
concurrent-ruby (1.1.9)
escape (0.0.4)
ethon (0.15.0)
ffi (>= 1.15.0)
ffi (1.15.5)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
httpclient (2.8.3)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
json (2.6.1)
minitest (5.15.0)
molinillo (0.8.0)
nanaimo (0.3.0)
nap (1.1.0)
netrc (0.11.0)
public_suffix (4.0.6)
rexml (3.2.5)
ruby-macho (2.5.1)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
zeitwerk (2.5.4)
PLATFORMS
ruby
DEPENDENCIES
cocoapods (~> 1.11, >= 1.11.2)
RUBY VERSION
ruby 2.7.4p191
BUNDLED WITH
2.2.27
+1 -11
View File
@@ -114,17 +114,12 @@ def jscFlavor = 'org.webkit:android-jsc:+'
/**
* Whether to enable the Hermes VM.
*
* This should be set on project.ext.react and that value will be read here. If it is not set
* This should be set on project.ext.react and mirrored here. If it is not set
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
* and the benefits of using Hermes will therefore be sharply reduced.
*/
def enableHermes = project.ext.react.get("enableHermes", false);
/**
* Architectures to build native code for in debug.
*/
def nativeArchitectures = project.getProperties().get("reactNativeDebugArchitectures")
android {
ndkVersion rootProject.ext.ndkVersion
compileSdkVersion rootProject.ext.compileSdkVersion
@@ -169,11 +164,6 @@ android {
buildTypes {
debug {
signingConfig signingConfigs.debug
if (nativeArchitectures) {
ndk {
abiFilters nativeArchitectures.split(',')
}
}
}
release {
// Caution! In production, you need to generate your own keystore file.
+1 -1
View File
@@ -33,7 +33,7 @@
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustPan"
android:windowSoftInputMode="adjustResize"
android:screenOrientation="sensorPortrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
</selector>
</inset>
+1 -1
View File
@@ -3,7 +3,7 @@
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="android:textColor">#000000</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
+15 -9
View File
@@ -7,7 +7,7 @@ buildscript {
}
ext.kotlinVersion = "1.3.10"
dependencies {
classpath("com.android.tools.build:gradle:4.2.2")
classpath("com.android.tools.build:gradle:4.2.1")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -17,6 +17,8 @@ buildscript {
allprojects {
repositories {
mavenLocal()
mavenCentral()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url("$rootDir/../node_modules/react-native/android")
@@ -25,13 +27,6 @@ allprojects {
// Android JSC is installed from npm
url("$rootDir/../node_modules/jsc-android/dist")
}
mavenCentral {
// We don't want to fetch react-native from Maven Central as there are
// older versions over there.
content {
excludeGroup "com.facebook.react"
}
}
google()
maven { url 'https://www.jitpack.io' }
maven {
@@ -53,5 +48,16 @@ ext {
minSdkVersion = 23
compileSdkVersion = 30
targetSdkVersion = 30
ndkVersion = "21.4.7075529"
ndkVersion = "20.1.5948944"
}
subprojects {
afterEvaluate {project ->
if (project.hasProperty("android")) {
android {
compileSdkVersion 29
buildToolsVersion '29.0.3'
}
}
}
}
+2 -5
View File
@@ -9,7 +9,7 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
@@ -25,7 +25,4 @@ android.useAndroidX=true
android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.99.0
# https://github.com/facebook/react-native/issues/30729
org.gradle.jvmargs=-Xmx4g
FLIPPER_VERSION=0.93.0
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
}
-43
View File
@@ -1,43 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { BackHandler, StyleSheet, View } from 'react-native'
import { useTranslation } from 'react-i18next'
import License from './settings/License'
import Button from './common/button'
import { saveLicenseFlag } from '../local-storage'
import { Containers } from '../styles'
export default function AcceptLicense({ setLicense }) {
const onAcceptLicense = async () => {
await saveLicenseFlag()
setLicense()
}
const { t } = useTranslation()
return (
<License>
<View style={styles.container}>
<Button onPress={BackHandler.exitApp} testID="licenseCancelButton">
{t('labels.shared.cancel')}
</Button>
<Button isCTA onPress={onAcceptLicense} testID="licenseOkButton">
{t('labels.shared.ok')}
</Button>
</View>
</License>
)
}
AcceptLicense.propTypes = {
setLicense: PropTypes.func.isRequired,
}
const styles = StyleSheet.create({
container: {
...Containers.rowContainer,
},
})
+30 -6
View File
@@ -4,9 +4,7 @@ import PropTypes from 'prop-types'
import moment from 'moment'
import AppText from './common/app-text'
import Asterisk from './common/Asterisk'
import Button from './common/button'
import Footnote from './common/Footnote'
import cycleModule from '../lib/cycle'
import { getFertilityStatusForDay } from '../lib/sympto-adapter'
@@ -18,11 +16,9 @@ import {
import { Colors, Fonts, Sizes, Spacing } from '../styles'
import { LocalDate } from '@js-joda/core'
import { useTranslation } from 'react-i18next'
import { useDate } from '../hooks/useDate'
const Home = ({ navigate }) => {
const Home = ({ navigate, setDate }) => {
const { t } = useTranslation()
const { setDate } = useDate()
function navigateToCycleDayView() {
setDate(todayDateString)
@@ -73,12 +69,26 @@ const Home = ({ navigate }) => {
<Button isCTA isSmall={false} onPress={navigateToCycleDayView}>
{t('labels.home.addDataForToday')}
</Button>
{phase && <Footnote>{statusText}</Footnote>}
{phase && (
<View style={styles.asteriskLine}>
<Asterisk />
<AppText linkStyle={styles.whiteText} style={styles.greyText}>
{statusText}
</AppText>
</View>
)}
</ScrollView>
)
}
const Asterisk = () => {
return <AppText style={styles.asterisk}>*</AppText>
}
const styles = StyleSheet.create({
asterisk: {
color: Colors.orange,
},
container: {
backgroundColor: Colors.purple,
flex: 1,
@@ -94,6 +104,12 @@ const styles = StyleSheet.create({
marginBottom: Spacing.tiny,
marginTop: Spacing.small,
},
asteriskLine: {
flexDirection: 'row',
alignContent: 'flex-start',
marginBottom: Spacing.tiny,
marginTop: Spacing.small,
},
title: {
color: Colors.purpleLight,
fontFamily: Fonts.bold,
@@ -108,10 +124,18 @@ const styles = StyleSheet.create({
color: 'white',
fontSize: Sizes.subtitle,
},
whiteText: {
color: 'white',
},
greyText: {
color: Colors.greyLight,
paddingLeft: Spacing.base,
},
})
Home.propTypes = {
navigate: PropTypes.func,
setDate: PropTypes.func,
}
export default Home
+51
View File
@@ -0,0 +1,51 @@
import React from 'react'
import PropTypes from 'prop-types'
import { BackHandler, StyleSheet, View } from 'react-native'
import { useTranslation } from 'react-i18next'
import AppPage from './common/app-page'
import AppText from './common/app-text'
import Button from './common/button'
import Segment from './common/segment'
import { saveLicenseFlag } from '../local-storage'
import { Containers } from '../styles'
export default function License({ setLicense }) {
const onAcceptLicense = async () => {
await saveLicenseFlag()
setLicense()
}
const { t } = useTranslation()
const currentYear = new Date().getFullYear()
return (
<AppPage testID="licensePage">
<Segment last testID="test" title={t('settings.license.title')}>
<AppText testID="test">
{t('settings.license.text', { currentYear })}
</AppText>
<View style={styles.container}>
<Button onPress={BackHandler.exitApp} testID="licenseCancelButton">
{t('labels.shared.cancel')}
</Button>
<Button isCTA onPress={onAcceptLicense} testID="licenseOkButton">
{t('labels.shared.ok')}
</Button>
</View>
</Segment>
</AppPage>
)
}
License.propTypes = {
setLicense: PropTypes.func.isRequired,
}
const styles = StyleSheet.create({
container: {
...Containers.rowContainer,
},
})
+3 -7
View File
@@ -7,11 +7,9 @@ import { openDb } from '../db'
import App from './app'
import AppLoadingView from './common/app-loading'
import AppStatusBar from './common/app-status-bar'
import AcceptLicense from './AcceptLicense'
import License from './License'
import PasswordPrompt from './password-prompt'
import { DateProvider } from '../hooks/useDate'
export default function AppWrapper() {
const [isLoading, setIsLoading] = useState(true)
const [isLicenseAccepted, setIsLicenseAccepted] = useState(false)
@@ -40,7 +38,7 @@ export default function AppWrapper() {
}
if (!isLicenseAccepted) {
return <AcceptLicense setLicense={() => setIsLicenseAccepted(true)} />
return <License setLicense={() => setIsLicenseAccepted(true)} />
}
return (
@@ -49,9 +47,7 @@ export default function AppWrapper() {
{isDbEncrypted ? (
<PasswordPrompt enableShowApp={() => setIsDbEncrypted(false)} />
) : (
<DateProvider>
<App restartApp={() => checkIsDbEncrypted()} />
</DateProvider>
<App restartApp={() => checkIsDbEncrypted()} />
)}
</>
)
+4 -3
View File
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'
import { BackHandler, StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import { useDate } from '../hooks/useDate'
import { LocalDate } from '@js-joda/core'
import Header from './header'
import Menu from './menu'
@@ -13,8 +13,7 @@ import setupNotifications from '../lib/notifications'
import { closeDb } from '../db'
const App = ({ restartApp }) => {
const { setDate } = useDate()
const [date, setDate] = useState(LocalDate.now().toString())
const [currentPage, setCurrentPage] = useState('Home')
const goBack = () => {
if (currentPage === 'Home') {
@@ -44,6 +43,8 @@ const App = ({ restartApp }) => {
const isTemperatureEditView = currentPage === 'TemperatureEditView'
const headerProps = { navigate: setCurrentPage }
const pageProps = {
date,
setDate,
isTemperatureEditView,
navigate: setCurrentPage,
}
+2 -4
View File
@@ -3,8 +3,6 @@ import PropTypes from 'prop-types'
import { StyleSheet, View } from 'react-native'
import { CalendarList } from 'react-native-calendars'
import { useDate } from '../hooks/useDate'
import { getBleedingDaysSortedByDate } from '../db'
import cycleModule from '../lib/cycle'
import {
@@ -14,8 +12,7 @@ import {
todayToCalFormat,
} from './helpers/calendar'
const CalendarView = ({ navigate }) => {
const { setDate } = useDate()
const CalendarView = ({ setDate, navigate }) => {
const bleedingDays = getBleedingDaysSortedByDate()
const predictedMenses = cycleModule().getPredictedMenses()
@@ -52,6 +49,7 @@ const styles = StyleSheet.create({
})
CalendarView.propTypes = {
setDate: PropTypes.func.isRequired,
navigate: PropTypes.func.isRequired,
}
+6 -16
View File
@@ -28,24 +28,12 @@ import { Spacing } from '../../styles'
const getSymptomsFromCycleDays = (cycleDays) =>
SYMPTOMS.filter((symptom) => cycleDays.some((cycleDay) => cycleDay[symptom]))
const CycleChart = ({ navigate }) => {
const CycleChart = ({ navigate, setDate }) => {
const [shouldShowHint, setShouldShowHint] = useState(true)
useEffect(() => {
let isMounted = true
async function checkShouldShowHint() {
const flag = await getChartFlag()
if (isMounted) {
setShouldShowHint(flag === 'true')
}
}
checkShouldShowHint()
return () => {
isMounted = false
}
useEffect(async () => {
const flag = await getChartFlag()
setShouldShowHint(flag === 'true')
}, [])
const hideHint = () => {
@@ -84,6 +72,7 @@ const CycleChart = ({ navigate }) => {
const renderColumn = ({ item }) => {
return (
<DayColumn
setDate={setDate}
dateString={item}
navigate={navigate}
symptomHeight={symptomHeight}
@@ -135,6 +124,7 @@ const CycleChart = ({ navigate }) => {
CycleChart.propTypes = {
navigate: PropTypes.func,
setDate: PropTypes.func,
}
const styles = StyleSheet.create({
+2 -3
View File
@@ -8,8 +8,6 @@ import SymptomCell from './symptom-cell'
import TemperatureColumn from './temperature-column'
import CycleDayLabel from './cycle-day-label'
import { useDate } from '../../hooks/useDate'
import {
symptomColorMethods,
getTemperatureProps,
@@ -21,13 +19,13 @@ const DayColumn = ({
dateString,
chartSymptoms,
columnHeight,
setDate,
navigate,
shouldShowTemperatureColumn,
symptomHeight,
symptomRowSymptoms,
xAxisHeight,
}) => {
const { setDate } = useDate()
const cycleDayData = getCycleDay(dateString)
let data = {}
@@ -107,6 +105,7 @@ DayColumn.propTypes = {
chartSymptoms: PropTypes.array,
columnHeight: PropTypes.number.isRequired,
navigate: PropTypes.func.isRequired,
setDate: PropTypes.func.isRequired,
shouldShowTemperatureColumn: PropTypes.bool,
symptomHeight: PropTypes.number.isRequired,
symptomRowSymptoms: PropTypes.array,
-28
View File
@@ -1,28 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { StyleSheet, Text, Linking } from 'react-native'
import { Colors, Typography } from '../../styles'
const AppLink = ({ children, url, ...props }) => {
return (
<Text style={styles.link} {...props} onPress={() => Linking.openURL(url)}>
{children}
</Text>
)
}
AppLink.propTypes = {
children: PropTypes.node,
url: PropTypes.string,
}
const styles = StyleSheet.create({
link: {
...Typography.mainText,
color: Colors.purple,
textDecorationLine: 'underline',
},
})
export default AppLink
-16
View File
@@ -1,16 +0,0 @@
import React from 'react'
import { StyleSheet } from 'react-native'
import AppText from './app-text'
import { Colors } from '../../styles'
const Asterisk = () => <AppText style={styles.asterisk}>*</AppText>
const styles = StyleSheet.create({
asterisk: {
color: Colors.orange,
},
})
export default Asterisk
-43
View File
@@ -1,43 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { StyleSheet, View } from 'react-native'
import AppText from '../common/app-text'
import Asterisk from '../common/Asterisk'
import { Colors, Spacing } from '../../styles'
const Footnote = ({ children }) => {
if (!children) return false
return (
<View style={styles.container}>
<Asterisk />
<AppText linkStyle={styles.link} style={styles.text}>
{children}
</AppText>
</View>
)
}
Footnote.propTypes = {
children: PropTypes.node,
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignContent: 'flex-start',
marginBottom: Spacing.tiny,
marginTop: Spacing.small,
},
link: {
color: 'white',
},
text: {
color: Colors.greyLight,
paddingLeft: Spacing.base,
},
})
export default Footnote
+5 -2
View File
@@ -4,7 +4,7 @@ import { ScrollView, StyleSheet, View } from 'react-native'
import AppText from '../common/app-text'
import { Colors, Containers, Typography } from '../../styles'
import { Colors, Typography } from '../../styles'
const AppPage = ({
children,
@@ -35,7 +35,10 @@ AppPage.propTypes = {
}
const styles = StyleSheet.create({
container: { ...Containers.pageContainer },
container: {
backgroundColor: Colors.turquoiseLight,
flex: 1,
},
scrollView: {
backgroundColor: Colors.turquoiseLight,
flexGrow: 1,
+19 -4
View File
@@ -1,15 +1,30 @@
import React from 'react'
import { StyleSheet, TextInput } from 'react-native'
import { KeyboardAvoidingView, StyleSheet, TextInput } from 'react-native'
import PropTypes from 'prop-types'
import { Colors, Spacing, Typography } from '../../styles'
const AppTextInput = ({ style, ...props }) => (
<TextInput style={[styles.input, style]} {...props} />
)
const AppTextInput = ({ style, isKeyboardOffset, ...props }) => {
const behavior = isKeyboardOffset ? 'padding' : 'height'
const keyboardVerticalOffset = isKeyboardOffset ? 300 : 0
return (
<KeyboardAvoidingView
behavior={behavior}
keyboardVerticalOffset={keyboardVerticalOffset}
>
<TextInput style={[styles.input, style]} {...props} />
</KeyboardAvoidingView>
)
}
AppTextInput.propTypes = {
style: PropTypes.object,
isKeyboardOffset: PropTypes.bool,
}
AppTextInput.defultProps = {
isKeyboardOffset: true,
}
const styles = StyleSheet.create({
+10 -4
View File
@@ -2,18 +2,24 @@ import React from 'react'
import PropTypes from 'prop-types'
import { StyleSheet, Text } from 'react-native'
import Link from './link'
import { Colors, Typography } from '../../styles'
const AppText = ({ children, style, ...props }) => {
const AppText = ({ children, linkStyle, style, ...props }) => {
// we parse for links in case the text contains any
return (
<Text style={[styles.text, style]} {...props}>
{children}
</Text>
<Link style={linkStyle}>
<Text style={[styles.text, style]} {...props}>
{children}
</Text>
</Link>
)
}
AppText.propTypes = {
children: PropTypes.node,
linkStyle: PropTypes.object,
style: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}
+40
View File
@@ -0,0 +1,40 @@
import React from 'react'
import PropTypes from 'prop-types'
import Hyperlink from 'react-native-hyperlink'
import { StyleSheet } from 'react-native'
import { Colors, Typography } from '../../styles'
import links from '../../i18n/en/links'
const Link = ({ children, style }) => {
return (
<Hyperlink
linkStyle={[styles.link, style]}
linkText={replaceUrlWithText}
linkDefault
>
{children}
</Hyperlink>
)
}
Link.propTypes = {
children: PropTypes.node,
style: PropTypes.object,
}
const styles = StyleSheet.create({
link: {
color: Colors.purple,
textDecorationLine: 'underline',
...Typography.mainText,
},
})
function replaceUrlWithText(url) {
const link = Object.values(links).find((l) => l.url === url)
return (link && link.text) || url
}
export default Link
+8 -3
View File
@@ -4,7 +4,7 @@ import { StyleSheet, View } from 'react-native'
import AppText from './app-text'
import { Colors, Containers, Spacing, Typography } from '../../styles'
import { Colors, Spacing, Typography } from '../../styles'
const Segment = ({ children, last, title }) => {
const containerStyle = last ? styles.containerLast : styles.container
@@ -25,16 +25,21 @@ Segment.propTypes = {
title: PropTypes.string,
}
const segmentContainer = {
marginHorizontal: Spacing.base,
marginBottom: Spacing.base,
}
const styles = StyleSheet.create({
container: {
borderStyle: 'solid',
borderBottomWidth: 1,
borderBottomColor: Colors.greyLight,
paddingBottom: Spacing.base,
...Containers.segmentContainer,
...segmentContainer,
},
containerLast: {
...Containers.segmentContainer,
...segmentContainer,
},
title: {
...Typography.subtitle,
@@ -2,16 +2,18 @@ import React from 'react'
import { StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import AppText from '../common/app-text'
import AppText from './app-text'
import { Sizes, Spacing, Typography } from '../../styles'
const StatsOverview = ({ data }) => {
return data.map((rowContent, i) => <Row key={i} rowContent={rowContent} />)
const Table = ({ tableContent }) => {
return tableContent.map((rowContent, i) => (
<Row key={i} rowContent={rowContent} />
))
}
StatsOverview.propTypes = {
data: PropTypes.array.isRequired,
Table.propTypes = {
tableContent: PropTypes.array.isRequired,
}
const Row = ({ rowContent }) => {
@@ -63,11 +65,18 @@ const styles = StyleSheet.create({
},
cellLeft: {
alignItems: 'flex-end',
flex: 3,
flex: 5,
justifyContent: 'center',
},
cellRight: { flex: 5 },
row: { flexDirection: 'row' },
cellRight: {
flex: 5,
justifyContent: 'center',
},
row: {
flexDirection: 'row',
marginBottom: Spacing.tiny,
marginLeft: Spacing.tiny,
},
})
export default StatsOverview
export default Table
+3 -4
View File
@@ -9,13 +9,10 @@ import SymptomPageTitle from './symptom-page-title'
import { getCycleDay } from '../../db'
import { getData, nextDate, prevDate } from '../helpers/cycle-day'
import { useDate } from '../../hooks/useDate'
import { Spacing } from '../../styles'
import { SYMPTOMS } from '../../config'
const CycleDayOverView = ({ isTemperatureEditView }) => {
const { date, setDate } = useDate()
const CycleDayOverView = ({ date, setDate, isTemperatureEditView }) => {
const cycleDay = getCycleDay(date)
const [editedSymptom, setEditedSymptom] = useState(
@@ -61,6 +58,8 @@ const CycleDayOverView = ({ isTemperatureEditView }) => {
CycleDayOverView.propTypes = {
cycleDay: PropTypes.object,
date: PropTypes.string,
setDate: PropTypes.func,
isTemperatureEditView: PropTypes.bool,
}
+91 -109
View File
@@ -109,109 +109,101 @@ const SymptomEditView = ({ date, onClose, symptom, symptomData }) => {
}
const iconName = shouldShowInfo ? 'chevron-up' : 'chevron-down'
const noteText = symptom === 'note' ? data.value : data.note
const inputProps = {
multiline: true,
numberOfLines: 3,
scrollEnabled: false,
style: styles.input,
textAlignVertical: 'top',
}
return (
<AppModal onClose={onSave}>
<View style={styles.modalWindow}>
<ScrollView
contentContainerStyle={styles.modalContainer}
style={styles.modalWindow}
>
<View style={styles.headerContainer}>
<CloseIcon onClose={onSave} />
</View>
<ScrollView
contentContainerStyle={styles.modalContainer}
keyboardDismissMode="on-drag"
>
{symptom === 'temperature' && (
<Temperature
date={date}
data={data}
save={(value, field) => onSaveTemperature(value, field)}
/>
)}
{shouldShow(symptomConfig.selectTabGroups) &&
symtomPage[symptom].selectTabGroups.map((group) => {
return (
<Segment key={group.key} style={styles.segmentBorder}>
<AppText style={styles.title}>{group.title}</AppText>
<SelectTabGroup
activeButton={data[group.key]}
buttons={group.options}
onSelect={(value) => onSelectTab(group, value)}
/>
</Segment>
)
})}
{shouldShow(symptomConfig.selectBoxGroups) &&
symtomPage[symptom].selectBoxGroups.map((group) => {
const isOtherSelected =
data['other'] !== null &&
data['other'] !== false &&
Object.keys(group.options).includes('other')
{symptom === 'temperature' && (
<Temperature
date={date}
data={data}
save={(value, field) => onSaveTemperature(value, field)}
/>
)}
{shouldShow(symptomConfig.selectTabGroups) &&
symtomPage[symptom].selectTabGroups.map((group) => {
return (
<Segment key={group.key} style={styles.segmentBorder}>
<AppText style={styles.title}>{group.title}</AppText>
<SelectTabGroup
activeButton={data[group.key]}
buttons={group.options}
onSelect={(value) => onSelectTab(group, value)}
/>
</Segment>
)
})}
{shouldShow(symptomConfig.selectBoxGroups) &&
symtomPage[symptom].selectBoxGroups.map((group) => {
const isOtherSelected =
data['other'] !== null &&
data['other'] !== false &&
Object.keys(group.options).includes('other')
return (
<Segment key={group.key} style={styles.segmentBorder}>
<AppText style={styles.title}>{group.title}</AppText>
<SelectBoxGroup
labels={group.options}
onSelect={(value) => onSelectBox(value)}
optionsState={data}
return (
<Segment key={group.key} style={styles.segmentBorder}>
<AppText style={styles.title}>{group.title}</AppText>
<SelectBoxGroup
labels={group.options}
onSelect={(value) => onSelectBox(value)}
optionsState={data}
/>
{isOtherSelected && (
<AppTextInput
multiline={true}
placeholder={sharedLabels.enter}
value={data.note}
onChangeText={(value) => onSelectBoxNote(value)}
/>
{isOtherSelected && (
<AppTextInput
{...inputProps}
placeholder={sharedLabels.enter}
value={data.note}
onChangeText={(value) => onSelectBoxNote(value)}
/>
)}
</Segment>
)
})}
{shouldShow(symptomConfig.excludeText) && (
<Segment style={styles.segmentBorder}>
<AppSwitch
onToggle={onExcludeToggle}
text={symtomPage[symptom].excludeText}
value={data.exclude}
/>
</Segment>
)}
{shouldShow(symptomConfig.note) && (
<Segment style={styles.segmentBorder}>
<AppText>{symtomPage[symptom].note}</AppText>
<AppTextInput
{...inputProps}
onChangeText={onEditNote}
placeholder={sharedLabels.enter}
testID="noteInput"
value={noteText !== null ? noteText : ''}
/>
</Segment>
)}
<View style={styles.buttonsContainer}>
<Button iconName={iconName} isSmall onPress={onPressLearnMore}>
{sharedLabels.learnMore}
</Button>
<Button isSmall onPress={onRemove}>
{sharedLabels.remove}
</Button>
<Button isCTA isSmall onPress={onSave}>
{sharedLabels.save}
</Button>
</View>
{shouldShowInfo && (
<Segment last style={styles.segmentBorder}>
<AppText>{info[symptom].text}</AppText>
</Segment>
)}
</ScrollView>
</View>
)}
</Segment>
)
})}
{shouldShow(symptomConfig.excludeText) && (
<Segment style={styles.segmentBorder}>
<AppSwitch
onToggle={onExcludeToggle}
text={symtomPage[symptom].excludeText}
value={data.exclude}
/>
</Segment>
)}
{shouldShow(symptomConfig.note) && (
<Segment style={styles.segmentBorder}>
<AppText>{symtomPage[symptom].note}</AppText>
<AppTextInput
multiline={true}
numberOfLines={3}
onChangeText={onEditNote}
placeholder={sharedLabels.enter}
testID="noteInput"
value={noteText !== null ? noteText : ''}
/>
</Segment>
)}
<View style={styles.buttonsContainer}>
<Button iconName={iconName} isSmall onPress={onPressLearnMore}>
{sharedLabels.learnMore}
</Button>
<Button isSmall onPress={onRemove}>
{sharedLabels.remove}
</Button>
<Button isCTA isSmall onPress={onSave}>
{sharedLabels.save}
</Button>
</View>
{shouldShowInfo && (
<Segment last style={styles.segmentBorder}>
<AppText>{info[symptom].text}</AppText>
</Segment>
)}
</ScrollView>
</AppModal>
)
}
@@ -226,31 +218,21 @@ SymptomEditView.propTypes = {
const styles = StyleSheet.create({
buttonsContainer: {
...Containers.rowContainer,
paddingHorizontal: Spacing.base,
paddingBottom: Spacing.base,
},
headerContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
paddingTop: Spacing.base,
paddingHorizontal: Spacing.base,
position: 'absolute',
width: '100%',
zIndex: 3, // works on ios
elevation: 3, // works on android
},
input: {
height: Sizes.base * 5,
paddingVertical: Spacing.tiny,
},
modalContainer: {
paddingHorizontal: Spacing.base,
flex: 1,
padding: Spacing.base,
},
modalWindow: {
alignSelf: 'center',
backgroundColor: 'white',
borderRadius: 10,
marginTop: Sizes.huge * 2,
paddingTop: Spacing.large * 2,
marginVertical: Sizes.huge * 2,
position: 'absolute',
minHeight: '40%',
maxHeight: Dimensions.get('window').height * 0.7,
-15
View File
@@ -22,18 +22,3 @@ export function dateToTitle(dateString) {
? labels.today
: moment(dateString).format('ddd DD. MMM YY')
}
export function humanizeDate(dateString) {
if (!dateString) return ''
const today = LocalDate.now()
try {
const dateToDisplay = LocalDate.parse(dateString)
return today.equals(dateToDisplay)
? labels.today
: moment(dateString).format('DD. MMM YY')
} catch (e) {
return ''
}
}
+19 -20
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { Alert, KeyboardAvoidingView, StyleSheet, View } from 'react-native'
import { Alert, StyleSheet, View } from 'react-native'
import nodejs from 'nodejs-mobile-react-native'
import AppPage from './common/app-page'
@@ -36,9 +36,9 @@ const PasswordPrompt = ({ enableShowApp }) => {
}
useEffect(() => {
const listener = nodejs.channel.addListener('check-pw', passHashToDb, this)
nodejs.channel.addListener('check-pw', passHashToDb, this)
return () => listener.remove()
return () => nodejs.channel.remove('check-pw', passHashToDb)
}, [])
const onDeleteDataConfirmation = async () => {
@@ -68,23 +68,22 @@ const PasswordPrompt = ({ enableShowApp }) => {
<>
<Header isStatic />
<AppPage contentContainerStyle={styles.contentContainer}>
<KeyboardAvoidingView behavior="padding" keyboardVerticalOffset={150}>
<AppTextInput
onChangeText={setPassword}
secureTextEntry={true}
placeholder={labels.enterPassword}
/>
<View style={styles.containerButtons}>
<Button onPress={onConfirmDeletion}>{labels.forgotPassword}</Button>
<Button
disabled={!isPasswordEntered}
isCTA={isPasswordEntered}
onPress={unlockApp}
>
{labels.title}
</Button>
</View>
</KeyboardAvoidingView>
<AppTextInput
isKeyboardOffset={false}
onChangeText={setPassword}
secureTextEntry={true}
placeholder={labels.enterPassword}
/>
<View style={styles.containerButtons}>
<Button onPress={onConfirmDeletion}>{labels.forgotPassword}</Button>
<Button
disabled={!isPasswordEntered}
isCTA={isPasswordEntered}
onPress={unlockApp}
>
{labels.title}
</Button>
</View>
</AppPage>
</>
)
-29
View File
@@ -1,29 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import AppPage from '../common/app-page'
import AppText from '../common/app-text'
import AppLink from '../common/AppLink'
import Segment from '../common/segment'
const License = ({ children }) => {
const { t } = useTranslation(null, { keyPrefix: 'settings.license' })
const currentYear = new Date().getFullYear()
const link = 'https://www.gnu.org/licenses/gpl-3.0.html'
return (
<AppPage title={t('title')}>
<Segment last>
<AppText>{t('text', { currentYear })}</AppText>
<AppLink url={link}>{link}</AppLink>
{children}
</Segment>
</AppPage>
)
}
License.propTypes = {
children: PropTypes.node,
}
export default License
@@ -3,7 +3,6 @@ import { Platform, Linking } from 'react-native'
import AppPage from '../common/app-page'
import AppText from '../common/app-text'
import AppLink from '../common/AppLink'
import Segment from '../common/segment'
import Button from '../common/button'
import ButtonRow from '../common/button-row'
@@ -36,15 +35,13 @@ const AboutSection = () => {
</Segment>
<Segment title={t('credits.title')}>
<AppText>
{t('credits.text')}{' '}
<AppLink url={links.flaticon.url}>flaticon</AppLink>.{' '}
</AppText>
<AppText>
{t('credits.madeBy')}{' '}
<AppLink url={links.smashicons.url}>smashicons</AppLink>,{' '}
<AppLink url={links.pause08.url}>pause08</AppLink>,{' '}
<AppLink url={links.kazachek.url}>kazachek</AppLink>,{' '}
<AppLink url={links.freepik.url}>freepik</AppLink>.
{t('credits.text', {
smashicons: links.smashicons.url,
pause08: links.pause08.url,
kazachek: links.kazachek.url,
freepik: links.freepik.url,
flaticon: links.flaticon.url,
})}
</AppText>
</Segment>
<Segment title={t('donate.title')}>
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { Alert, KeyboardAvoidingView, StyleSheet, View } from 'react-native'
import { Alert, StyleSheet, View } from 'react-native'
import nodejs from 'nodejs-mobile-react-native'
import AppTextInput from '../../common/app-text-input'
@@ -24,12 +24,10 @@ const ConfirmWithPassword = ({ onSuccess, onCancel }) => {
}
useEffect(() => {
const listener = nodejs.channel.addListener(
'password-check',
checkPassword,
this
)
return () => listener.remove()
nodejs.channel.addListener('password-check', checkPassword, this)
return () => {
nodejs.channel.removeListener('password-check', checkPassword)
}
}, [])
const onIncorrectPassword = () => {
@@ -53,7 +51,7 @@ const ConfirmWithPassword = ({ onSuccess, onCancel }) => {
const isPassword = password !== null
return (
<KeyboardAvoidingView behavior="padding" keyboardVerticalOffset={150}>
<>
<AppTextInput
onChangeText={setPassword}
placeholder={labels.enterCurrent}
@@ -70,7 +68,7 @@ const ConfirmWithPassword = ({ onSuccess, onCancel }) => {
{shared.confirmToProceed}
</Button>
</View>
</KeyboardAvoidingView>
</>
)
}
+2 -2
View File
@@ -2,8 +2,8 @@ import Reminders from './reminders/reminders'
import NfpSettings from './nfp-settings'
import DataManagement from './data-management'
import Password from './password'
import About from './About'
import License from './License'
import About from './about'
import License from './license'
import PrivacyPolicy from './privacy-policy'
export default {
+21
View File
@@ -0,0 +1,21 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import AppPage from '../common/app-page'
import AppText from '../common/app-text'
import Segment from '../common/segment'
const License = () => {
const { t } = useTranslation()
const currentYear = new Date().getFullYear()
return (
<AppPage title={t('settings.license.title')}>
<Segment last>
<AppText>{t('settings.license.text', { currentYear })}</AppText>
</Segment>
</AppPage>
)
}
export default License
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'
import { KeyboardAvoidingView, StyleSheet } from 'react-native'
import { StyleSheet } from 'react-native'
import nodejs from 'nodejs-mobile-react-native'
import PropTypes from 'prop-types'
@@ -41,8 +41,9 @@ const EnterNewPassword = ({ changeEncryptionAndRestart }) => {
const isButtonActive = password.length > 0 && passwordConfirmation.length > 0
return (
<KeyboardAvoidingView behavior="padding" keyboardVerticalOffset={150}>
<>
<AppTextInput
isKeyboardOffset={false}
onChangeText={setPassword}
placeholder={labels.enterNew}
textContentType="password"
@@ -50,6 +51,7 @@ const EnterNewPassword = ({ changeEncryptionAndRestart }) => {
secureTextEntry={true}
/>
<AppTextInput
isKeyboardOffset={false}
onChangeText={setPasswordConfirmation}
placeholder={labels.confirmPassword}
textContentType="password"
@@ -66,7 +68,7 @@ const EnterNewPassword = ({ changeEncryptionAndRestart }) => {
>
{labels.savePassword}
</Button>
</KeyboardAvoidingView>
</>
)
}
@@ -1,45 +1,39 @@
import React from 'react'
import { ImageBackground, View } from 'react-native'
import { ScaledSheet } from 'react-native-size-matters'
import { useTranslation } from 'react-i18next'
import AppText from '../common/app-text'
import StatsOverview from './StatsOverview'
import StatsTable from './StatsTable'
import AppPage from './common/app-page'
import AppText from './common/app-text'
import Segment from './common/segment'
import Table from './common/table'
import cycleModule from '../../lib/cycle'
import { getCycleLengthStats as getCycleInfo } from '../../lib/cycle-length'
import cycleModule from '../lib/cycle'
import {getCycleLengthStats as getCycleInfo} from '../lib/cycle-length'
import {stats as labels} from '../i18n/en/labels'
import { Containers, Sizes, Spacing, Typography } from '../../styles'
import { Sizes, Spacing, Typography } from '../styles'
const image = require('../../assets/cycle-icon.png')
const image = require('../assets/cycle-icon.png')
const Stats = () => {
const { t } = useTranslation(null, { keyPrefix: 'stats' })
const cycleLengths = cycleModule().getAllCycleLengths()
const numberOfCycles = cycleLengths.length
const cycleData =
numberOfCycles > 0
? getCycleInfo(cycleLengths)
: { minimum: '—', maximum: '—', stdDeviation: '—' }
const standardDeviation = cycleData.stdDeviation
? cycleData.stdDeviation
: '—'
const hasAtLeastOneCycle = numberOfCycles >= 1
const cycleData = hasAtLeastOneCycle ? getCycleInfo(cycleLengths)
: { minimum: '—', maximum: '—', stdDeviation: '—' }
const statsData = [
[cycleData.minimum, t('overview.min')],
[cycleData.maximum, t('overview.max')],
[standardDeviation, t('overview.standardDeviation')],
[numberOfCycles, t('overview.completedCycles')],
[cycleData.minimum, labels.minLabel],
[cycleData.maximum, labels.maxLabel],
[cycleData.stdDeviation ? cycleData.stdDeviation : '—', labels.stdLabel],
[numberOfCycles, labels.basisOfStatsEnd]
]
return (
<View style={styles.pageContainer}>
<View style={styles.overviewContainer}>
<AppText>{t('intro')}</AppText>
{numberOfCycles === 0 ? (
<AppText>{t('noData')}</AppText>
) : (
<AppPage contentContainerStyle={styles.pageContainer}>
<Segment last style={styles.pageContainer}>
<AppText>{labels.cycleLengthExplainer}</AppText>
{!hasAtLeastOneCycle && <AppText>{labels.emptyStats}</AppText>}
{hasAtLeastOneCycle &&
<View style={styles.container}>
<View style={styles.columnLeft}>
<ImageBackground
@@ -55,21 +49,20 @@ const Stats = () => {
{cycleData.mean}
</AppText>
<AppText style={styles.accentPurpleHuge}>
{t('overview.days')}
{labels.daysLabel}
</AppText>
</ImageBackground>
<AppText style={styles.accentOrange}>
{t('overview.average')}
{labels.averageLabel}
</AppText>
</View>
<View style={styles.columnRight}>
<StatsOverview data={statsData} />
<Table tableContent={statsData} />
</View>
</View>
)}
</View>
<StatsTable />
</View>
}
</Segment>
</AppPage>
)
}
@@ -84,24 +77,25 @@ const styles = ScaledSheet.create({
},
accentPurpleGiant: {
...Typography.accentPurpleGiant,
marginTop: Spacing.base * -2,
marginTop: Spacing.base * (-2),
},
accentPurpleHuge: {
...Typography.accentPurpleHuge,
marginTop: Spacing.base * -1,
marginTop: Spacing.base * (-1),
},
container: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
paddingTop: Spacing.base
},
columnLeft: {
...column,
flex: 3,
flex: 2,
},
columnRight: {
...column,
flex: 5,
flex: 3,
paddingTop: Spacing.small,
},
image: {
@@ -111,13 +105,9 @@ const styles = ScaledSheet.create({
paddingTop: Spacing.large * 2.5,
marginBottom: Spacing.large,
},
overviewContainer: {
paddingHorizontal: Spacing.base,
paddingTop: Spacing.base,
},
pageContainer: {
...Containers.pageContainer,
},
marginTop: Spacing.base * 2,
}
})
export default Stats
-107
View File
@@ -1,107 +0,0 @@
import React from 'react'
import { FlatList, StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import AppText from '../common/app-text'
import cycleModule from '../../lib/cycle'
import { Spacing, Typography, Colors } from '../../styles'
import { humanizeDate } from '../helpers/format-date'
const Item = ({ data }) => {
const { t } = useTranslation(null, { keyPrefix: 'plurals' })
if (!data) return false
const { date, cycleLength, bleedingLength } = data
return (
<View style={styles.row}>
<View style={styles.accentCell}>
<AppText>{humanizeDate(date)}</AppText>
</View>
<View style={styles.cell}>
<AppText>{t('day', { count: cycleLength })}</AppText>
</View>
<View style={styles.cell}>
<AppText>{t('day', { count: bleedingLength })}</AppText>
</View>
</View>
)
}
Item.propTypes = {
data: PropTypes.object.isRequired,
}
const StatsTable = () => {
const renderItem = ({ item }) => <Item data={item} />
const data = cycleModule().getStats()
if (!data || data.length === 0) return false
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.date}
ItemSeparatorComponent={ItemDivider}
ListHeaderComponent={FlatListHeader}
ListHeaderComponentStyle={styles.headerDivider}
stickyHeaderIndices={[0]}
contentContainerStyle={styles.container}
/>
)
}
const ItemDivider = () => <View style={styles.divider} />
const FlatListHeader = () => (
<View style={styles.row}>
<View style={styles.accentCell}>
<AppText style={styles.header}>{'Cycle Start'}</AppText>
</View>
<View style={styles.cell}>
<AppText style={styles.header}>{'Cycle Length'}</AppText>
</View>
<View style={styles.cell}>
<AppText style={styles.header}>{'Bleeding'}</AppText>
</View>
</View>
)
const styles = StyleSheet.create({
divider: {
height: 1,
width: '100%',
backgroundColor: Colors.grey,
},
header: {
...Typography.accentOrange,
paddingVertical: Spacing.small,
},
headerDivider: {
borderBottomColor: Colors.purple,
borderBottomWidth: 2,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: Spacing.tiny,
backgroundColor: Colors.turquoiseLight,
},
cell: {
flex: 2,
justifyContent: 'center',
},
accentCell: {
flex: 3,
justifyContent: 'center',
},
container: {
paddingHorizontal: Spacing.base,
},
})
export default StatsTable
-24
View File
@@ -1,24 +0,0 @@
import React, { createContext, useContext, useState } from 'react'
import PropTypes from 'prop-types'
import { LocalDate } from '@js-joda/core'
const DateContext = createContext()
export const DateProvider = ({ children }) => {
const [date, setDate] = useState(LocalDate.now().toString())
return (
<DateContext.Provider value={{ date, setDate }}>
{children}
</DateContext.Provider>
)
}
DateProvider.propTypes = {
children: PropTypes.node,
}
export const useDate = () => {
const { date, setDate } = useContext(DateContext)
return { date, setDate }
}
+2 -26
View File
@@ -17,8 +17,7 @@
"about": {
"credits": {
"title": "Credits",
"text": "We love the drip. team. Thanks and lots of <3 to all of our condriputors. Thanks to Paula Härtel for the symptom tracking icons. All the other icons from:",
"madeBy": "Made by:"
"text": "We love the drip. team. Thanks and lots of <3 to all of our condriputors. Thanks to Paula Härtel for the symptom tracking icons. All the other icons are made by {{smashicons}}, {{pause08}}, {{kazachek}} & {{freepik}} from {{flaticon}}."
},
"donate": {
"button": "Donate here",
@@ -39,7 +38,7 @@
},
"license": {
"title": "drip. an open-source cycle tracking app",
"text": "Copyright (C) {{currentYear}} Heart of Code e.V.\n\nThis 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": "Copyright (C) {{currentYear}} Heart of Code e.V.\n\nThis 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: https://www.gnu.org/licenses/gpl-3.0.html."
},
"privacyPolicy": {
"title": "Privacy Policy",
@@ -60,28 +59,5 @@
"text": "You can read through the source code of drip. to ensure the given information is correct. The source code is like a recipe: It tells you how much and what kind of ingredients you need and how you prepare them to cook a tasty meal or program a funky app.\n\nBuon appetito!"
}
}
},
"stats": {
"noData": "At least one completed cycle is needed to display stats.",
"intro": "Basic statistics about the length of your cycles.",
"overview": {
"average": "Average cycle",
"days": "days",
"min": "Shortest",
"max": "Longest",
"standardDeviation": "Standard\ndeviation",
"completedCycles": "completed\ncycles"
},
"showStats": "Show period details",
"details": {
"cycleStart": "Cycle start",
"cycleLength": "Cycle length",
"bleedingDays": "Bleeding"
},
"footnote": "Based on the standard deviation of all your tracked periods drip. calculates a range for the starting day of the upcoming 3 periods. The range will be 3 days if your standard deviation is smaller than 1.5 and 5 days if the value is bigger.\n\nThe standard deviation tells you how much the length of your periods vary, 0 means all your periods are exactly the same length and the bigger the value the more the period length varies."
},
"plurals": {
"day": "{{count}} day",
"day_plural": "{{count}} days"
}
}
-1
View File
@@ -19,6 +19,5 @@ target 'drip' do
post_install do |installer|
react_native_post_install(installer)
__apply_Xcode_12_5_M1_post_install_workaround(installer)
end
end
+2 -2
View File
@@ -1038,8 +1038,8 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LIBRARY_SEARCH_PATHS = (
"$(SDKROOT)/usr/lib/swift",
"\"$(inherited)\"",
"\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
);
MTL_ENABLE_DEBUG_INFO = YES;
@@ -1094,8 +1094,8 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LIBRARY_SEARCH_PATHS = (
"$(SDKROOT)/usr/lib/swift",
"\"$(inherited)\"",
"\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
);
MTL_ENABLE_DEBUG_INFO = NO;
-2
View File
@@ -1,2 +0,0 @@
// Import Jest Native matchers
import '@testing-library/jest-native/extend-expect'
-8
View File
@@ -1,8 +0,0 @@
module.exports = {
preset: '@testing-library/react-native',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
setupFilesAfterEnv: ['./jest-setup.js'],
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native(-.*)?|@react-native(-community)?)/)',
],
}
+3 -23
View File
@@ -3,8 +3,6 @@ import { getCycleLengthStats } from './cycle-length'
const LocalDate = joda.LocalDate
const DAYS = joda.ChronoUnit.DAYS
const toJSON = (realmObj) => JSON.parse(JSON.stringify(realmObj))
export default function config(opts) {
let bleedingDaysSortedByDate
let cycleStartsSortedByDate
@@ -16,13 +14,9 @@ export default function config(opts) {
if (!opts) {
// we only want to require (and run) the db module
// when not running the tests
bleedingDaysSortedByDate = toJSON(
require('../db').getBleedingDaysSortedByDate()
)
cycleStartsSortedByDate = toJSON(
require('../db').getCycleStartsSortedByDate()
)
cycleDaysSortedByDate = toJSON(require('../db').getCycleDaysSortedByDate())
bleedingDaysSortedByDate = require('../db').getBleedingDaysSortedByDate()
cycleStartsSortedByDate = require('../db').getCycleStartsSortedByDate()
cycleDaysSortedByDate = require('../db').getCycleDaysSortedByDate()
maxBreakInBleeding = 1
maxCycleLength = 99
minCyclesForPrediction = 3
@@ -228,19 +222,6 @@ export default function config(opts) {
return predictedMenses
}
const getStats = () =>
cycleStartsSortedByDate.map((day, i) => {
const today = getTodayDate()
const cycleLength =
i === 0 ? getCycleDayNumber(today) : getAllCycleLengths()[i - 1]
return {
date: day.date,
cycleLength,
bleedingLength: ++getMensesDaysRightAfter(day).length,
}
})
return {
getCycleDayNumber,
getCycleForDay,
@@ -251,6 +232,5 @@ export default function config(opts) {
isMensesStart,
getMensesDaysRightAfter,
getCycleByStartDay,
getStats,
}
}
+8 -9
View File
@@ -14,7 +14,7 @@
"android": "react-native run-android",
"ios": "react-native run-ios --simulator=\"iPhone 8 Plus\"",
"log": "react-native log-android",
"test": "jest test && yarn lint",
"test": "jest test && npm run lint",
"test-watch": "jest --watch test",
"lint": "eslint components lib test styles db",
"devtool": "adb shell input keyevent 82",
@@ -44,11 +44,12 @@
"prop-types": "^15.8.1",
"react": "17.0.2",
"react-i18next": "^11.18.3",
"react-native": "0.67.4",
"react-native": "0.65.2",
"react-native-calendars": "^1.1287.0",
"react-native-document-picker": "^8.1.1",
"react-native-fs": "^2.20.0",
"react-native-modal-datetime-picker": "14.0.0",
"react-native-hyperlink": "0.0.19",
"react-native-modal-datetime-picker": "10.2.0",
"react-native-push-notification": "3.2.1",
"react-native-share": "^7.9.0",
"react-native-simple-toast": "^1.1.3",
@@ -58,23 +59,21 @@
"sympto": "3.0.1"
},
"devDependencies": {
"@babel/core": "^7.12.9",
"@babel/eslint-parser": "^7.19.1",
"@babel/core": "^7.19.1",
"@babel/eslint-parser": "^7.16.3",
"@babel/preset-react": "^7.16.0",
"@babel/runtime": "^7.12.5",
"@testing-library/jest-native": "^4.0.12",
"@testing-library/react-native": "^11.1.0",
"basic-changelog": "gitlab:bloodyhealth/basic-changelog",
"eslint": "7.14.0",
"eslint-plugin-react": "^7.8.2",
"husky": "^8.0.0",
"jest": "^28.1.3",
"jetifier": "^1.6.6",
"metro-react-native-babel-preset": "^0.66.2",
"metro-react-native-babel-preset": "^0.66.0",
"prettier": "2.4.0",
"pretty-quick": "^3.1.1",
"react-native-codegen": "^0.0.7",
"react-native-version": "^3.1.0",
"react-test-renderer": "17.0.2",
"readline": "^1.3.0"
},
"description": "A menstrual cycle tracking app that's open-source and leaves your data on your phone. Use it to track your menstrual cycle or for fertility awareness!",
+6 -14
View File
@@ -9,7 +9,7 @@ export default {
marginTop: Spacing.small,
marginRight: Spacing.small,
paddingHorizontal: Spacing.small,
paddingVertical: Spacing.tiny,
paddingVertical: Spacing.tiny
},
boxActive: {
backgroundColor: Colors.orange,
@@ -17,25 +17,17 @@ export default {
centerItems: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
pageContainer: {
backgroundColor: Colors.turquoiseLight,
flex: 1,
justifyContent: 'center'
},
rowContainer: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
justifyContent: 'space-between'
},
selectGroupContainer: {
alignItems: 'center',
flexDirection: 'row',
flexWrap: 'wrap',
marginVertical: Spacing.small,
},
segmentContainer: {
marginHorizontal: Spacing.base,
marginBottom: Spacing.base,
},
}
marginVertical: Spacing.small
}
}
-37
View File
@@ -1,37 +0,0 @@
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react-native'
import AcceptLicense from '../components/AcceptLicense'
import { saveLicenseFlag } from '../local-storage'
jest.mock('../local-storage', () => ({
saveLicenseFlag: jest.fn(() => Promise.resolve()),
}))
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (str, options) => {
return str + (options ? JSON.stringify(options) : '')
},
}),
}))
describe('AcceptLicense', () => {
test('On clicking OK button, the license is accepted', async () => {
const mockedSetLicense = jest.fn()
render(<AcceptLicense setLicense={mockedSetLicense} />)
const okButton = screen.getByText('ok', { exact: false })
fireEvent(okButton, 'click')
await expect(saveLicenseFlag).toHaveBeenCalled()
expect(mockedSetLicense).toHaveBeenCalled()
})
test('There is a Cancel button', async () => {
render(<AcceptLicense setLicense={jest.fn()} />)
screen.getByText('cancel', { exact: false })
})
})
-26
View File
@@ -1,26 +0,0 @@
import React from 'react'
import { render, screen } from '@testing-library/react-native'
import License from '../components/settings/License'
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (str, options) => {
return str + (options ? JSON.stringify(options) : '')
},
}),
}))
describe('License screen', () => {
test('It should have a correct year', async () => {
render(<License />)
const year = new Date().getFullYear().toString()
screen.getByText(year, { exact: false })
})
test('It should match the snapshot', async () => {
const licenseScreen = render(<License />)
expect(licenseScreen).toMatchSnapshot()
})
})
-85
View File
@@ -1,85 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`License screen It should match the snapshot 1`] = `
<View
style={
Object {
"backgroundColor": "#E9F2ED",
"flex": 1,
}
}
>
<RCTScrollView
contentContainerStyle={
Array [
Object {
"backgroundColor": "#E9F2ED",
"flexGrow": 1,
},
undefined,
]
}
>
<View>
<Text
style={
Array [
Object {
"color": "#555",
"fontFamily": "Jost-Book",
"fontSize": 34.285714285714285,
},
Object {
"alignSelf": "center",
"color": "#3A2671",
"fontFamily": "Jost-Bold",
"fontSize": 51.42857142857143,
"fontWeight": "700",
"marginHorizontal": 34.285714285714285,
"marginVertical": 42.857142857142854,
},
]
}
>
title
</Text>
<View
style={
Object {
"marginBottom": 34.285714285714285,
"marginHorizontal": 34.285714285714285,
}
}
>
<Text
style={
Array [
Object {
"color": "#555",
"fontFamily": "Jost-Book",
"fontSize": 34.285714285714285,
},
undefined,
]
}
>
text{"currentYear":2022}
</Text>
<Text
onPress={[Function]}
style={
Object {
"color": "#3A2671",
"fontFamily": "Jost-Book",
"fontSize": 34.285714285714285,
"textDecorationLine": "underline",
}
}
>
https://www.gnu.org/licenses/gpl-3.0.html
</Text>
</View>
</View>
</RCTScrollView>
</View>
`;
-19
View File
@@ -1,19 +0,0 @@
import React from 'react'
import { render } from '@testing-library/react-native'
import Footnote from '../../../components/common/Footnote'
describe('Footnote component', () => {
test('when children are present, renders them', () => {
const text = 'Some footnote text'
const { toJSON } = render(<Footnote>{text}</Footnote>)
expect(toJSON()).toMatchSnapshot()
})
test('when no children, renders nothing', () => {
const { toJSON } = render(<Footnote></Footnote>)
expect(toJSON()).toMatchSnapshot()
})
})
@@ -1,55 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Footnote component when children are present, renders them 1`] = `
<View
style={
Object {
"alignContent": "flex-start",
"flexDirection": "row",
"marginBottom": 8.571428571428571,
"marginTop": 21.428571428571427,
}
}
>
<Text
style={
Array [
Object {
"color": "#555",
"fontFamily": "Jost-Book",
"fontSize": 34.285714285714285,
},
Object {
"color": "#F38337",
},
]
}
>
*
</Text>
<Text
linkStyle={
Object {
"color": "white",
}
}
style={
Array [
Object {
"color": "#555",
"fontFamily": "Jost-Book",
"fontSize": 34.285714285714285,
},
Object {
"color": "#CCC",
"paddingLeft": 34.285714285714285,
},
]
}
>
Some footnote text
</Text>
</View>
`;
exports[`Footnote component when no children, renders nothing 1`] = `null`;
-27
View File
@@ -1,27 +0,0 @@
import { humanizeDate } from '../../components/helpers/format-date'
describe('humanizeDate', () => {
test('if receives null, returns empty string', () => {
const result = humanizeDate(null)
expect(result).toEqual('')
})
test('if receives undefined, returns empty string', () => {
const result = humanizeDate(undefined)
expect(result).toEqual('')
})
test('if receives incorrectly formatted date, returns empty string', () => {
const result = humanizeDate('abc')
expect(result).toEqual('')
})
test('if receives correct date string, returns date in humanized format', () => {
const result = humanizeDate('2022-01-07')
expect(result).toEqual('07. Jan 22')
})
})
-10
View File
@@ -1,10 +0,0 @@
import * as React from 'react'
import { render, screen } from '@testing-library/react-native'
import Logo from '../components/header/logo'
describe('Logo', () => {
test('It works', async () => {
render(<Logo />)
screen.getByText('drip.')
})
})
+107 -526
View File
File diff suppressed because it is too large Load Diff