Feature: add stats explainer take2

This commit is contained in:
Maria Zadnepryanets
2022-09-26 17:19:23 +00:00
parent db1388c3c4
commit 7b9293f7a2
8 changed files with 224 additions and 181 deletions
+1 -1
View File
@@ -71,7 +71,7 @@ const Home = ({ navigate, setDate }) => {
<Button isCTA isSmall={false} onPress={navigateToCycleDayView}> <Button isCTA isSmall={false} onPress={navigateToCycleDayView}>
{t('labels.home.addDataForToday')} {t('labels.home.addDataForToday')}
</Button> </Button>
{phase && <Footnote>{statusText}</Footnote>} {phase && <Footnote colorLabel="greyLight">{statusText}</Footnote>}
</ScrollView> </ScrollView>
) )
} }
+12 -5
View File
@@ -7,13 +7,16 @@ import Asterisk from '../common/Asterisk'
import { Colors, Spacing } from '../../styles' import { Colors, Spacing } from '../../styles'
const Footnote = ({ children }) => { const Footnote = ({ children, colorLabel }) => {
if (!children) return false if (!children) return false
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Asterisk /> <Asterisk />
<AppText linkStyle={styles.link} style={styles.text}> <AppText
linkStyle={styles.link}
style={{ ...styles.text, color: Colors[colorLabel] }}
>
{children} {children}
</AppText> </AppText>
</View> </View>
@@ -22,6 +25,11 @@ const Footnote = ({ children }) => {
Footnote.propTypes = { Footnote.propTypes = {
children: PropTypes.node, children: PropTypes.node,
colorLabel: PropTypes.string,
}
Footnote.defaultProps = {
colorLabel: 'greyDark',
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@@ -29,14 +37,13 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignContent: 'flex-start', alignContent: 'flex-start',
marginBottom: Spacing.tiny, marginBottom: Spacing.tiny,
marginTop: Spacing.small, marginTop: Spacing.base,
}, },
link: { link: {
color: 'white', color: 'white',
}, },
text: { text: {
color: Colors.greyLight, paddingLeft: Spacing.small,
paddingLeft: Spacing.base,
}, },
}) })
+48 -13
View File
@@ -1,20 +1,33 @@
import React from 'react' import React from 'react'
import { Modal, StyleSheet, TouchableOpacity } from 'react-native' import {
Dimensions,
Modal,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
const AppModal = ({ children, onClose }) => { import CloseIcon from './close-icon'
return (
<Modal import { Sizes, Spacing } from '../../styles'
animationType="fade"
onRequestClose={onClose} const AppModal = ({ children, onClose }) => (
transparent={true} <Modal
visible={true} animationType="fade"
> onRequestClose={onClose}
<TouchableOpacity onPress={onClose} style={styles.blackBackground} /> transparent={true}
visible={true}
>
<TouchableOpacity onPress={onClose} style={styles.blackBackground} />
<View style={styles.modalWindow}>
<View style={styles.headerContainer}>
<CloseIcon onClose={onClose} />
</View>
{children} {children}
</Modal> </View>
) </Modal>
} )
AppModal.propTypes = { AppModal.propTypes = {
children: PropTypes.node, children: PropTypes.node,
@@ -27,6 +40,28 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
opacity: 0.5, opacity: 0.5,
}, },
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
},
modalWindow: {
alignSelf: 'center',
backgroundColor: 'white',
borderRadius: 10,
marginTop: Sizes.huge * 2,
paddingVertical: Spacing.large * 2,
position: 'absolute',
maxHeight: Dimensions.get('window').height * 0.7,
zIndex: 2, // works on ios
elevation: 2, // works on android
minWidth: '80%',
},
}) })
export default AppModal export default AppModal
+87 -113
View File
@@ -1,13 +1,12 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Dimensions, ScrollView, StyleSheet, View } from 'react-native' import { ScrollView, StyleSheet, View } from 'react-native'
import AppModal from '../common/app-modal' import AppModal from '../common/app-modal'
import AppSwitch from '../common/app-switch' import AppSwitch from '../common/app-switch'
import AppText from '../common/app-text' import AppText from '../common/app-text'
import AppTextInput from '../common/app-text-input' import AppTextInput from '../common/app-text-input'
import Button from '../common/button' import Button from '../common/button'
import CloseIcon from '../common/close-icon'
import Segment from '../common/segment' import Segment from '../common/segment'
import SelectBoxGroup from './select-box-group' import SelectBoxGroup from './select-box-group'
import SelectTabGroup from './select-tab-group' import SelectTabGroup from './select-tab-group'
@@ -119,99 +118,94 @@ const SymptomEditView = ({ date, onClose, symptom, symptomData }) => {
return ( return (
<AppModal onClose={onSave}> <AppModal onClose={onSave}>
<View style={styles.modalWindow}> <ScrollView
<View style={styles.headerContainer}> contentContainerStyle={styles.modalContainer}
<CloseIcon onClose={onSave} /> keyboardDismissMode="on-drag"
</View> >
<ScrollView {symptom === 'temperature' && (
contentContainerStyle={styles.modalContainer} <Temperature
keyboardDismissMode="on-drag" date={date}
> data={data}
{symptom === 'temperature' && ( save={(value, field) => onSaveTemperature(value, field)}
<Temperature />
date={date} )}
data={data} {shouldShow(symptomConfig.selectTabGroups) &&
save={(value, field) => onSaveTemperature(value, field)} symtomPage[symptom].selectTabGroups.map((group) => {
/> return (
)} <Segment key={group.key} style={styles.segmentBorder}>
{shouldShow(symptomConfig.selectTabGroups) && <AppText style={styles.title}>{group.title}</AppText>
symtomPage[symptom].selectTabGroups.map((group) => { <SelectTabGroup
return ( activeButton={data[group.key]}
<Segment key={group.key} style={styles.segmentBorder}> buttons={group.options}
<AppText style={styles.title}>{group.title}</AppText> onSelect={(value) => onSelectTab(group, value)}
<SelectTabGroup />
activeButton={data[group.key]} </Segment>
buttons={group.options} )
onSelect={(value) => onSelectTab(group, value)} })}
/> {shouldShow(symptomConfig.selectBoxGroups) &&
</Segment> symtomPage[symptom].selectBoxGroups.map((group) => {
) const isOtherSelected =
})} data['other'] !== null &&
{shouldShow(symptomConfig.selectBoxGroups) && data['other'] !== false &&
symtomPage[symptom].selectBoxGroups.map((group) => { Object.keys(group.options).includes('other')
const isOtherSelected =
data['other'] !== null &&
data['other'] !== false &&
Object.keys(group.options).includes('other')
return ( return (
<Segment key={group.key} style={styles.segmentBorder}> <Segment key={group.key} style={styles.segmentBorder}>
<AppText style={styles.title}>{group.title}</AppText> <AppText style={styles.title}>{group.title}</AppText>
<SelectBoxGroup <SelectBoxGroup
labels={group.options} labels={group.options}
onSelect={(value) => onSelectBox(value)} onSelect={(value) => onSelectBox(value)}
optionsState={data} optionsState={data}
/>
{isOtherSelected && (
<AppTextInput
{...inputProps}
placeholder={sharedLabels.enter}
value={data.note}
onChangeText={(value) => onSelectBoxNote(value)}
/> />
{isOtherSelected && ( )}
<AppTextInput </Segment>
{...inputProps} )
placeholder={sharedLabels.enter} })}
value={data.note} {shouldShow(symptomConfig.excludeText) && (
onChangeText={(value) => onSelectBoxNote(value)} <Segment style={styles.segmentBorder}>
/> <AppSwitch
)} onToggle={onExcludeToggle}
</Segment> text={symtomPage[symptom].excludeText}
) value={data.exclude}
})} />
{shouldShow(symptomConfig.excludeText) && ( </Segment>
<Segment style={styles.segmentBorder}> )}
<AppSwitch {shouldShow(symptomConfig.note) && (
onToggle={onExcludeToggle} <Segment style={styles.segmentBorder}>
text={symtomPage[symptom].excludeText} <AppText>{symtomPage[symptom].note}</AppText>
value={data.exclude} <AppTextInput
/> {...inputProps}
</Segment> onChangeText={onEditNote}
)} placeholder={sharedLabels.enter}
{shouldShow(symptomConfig.note) && ( testID="noteInput"
<Segment style={styles.segmentBorder}> value={noteText !== null ? noteText : ''}
<AppText>{symtomPage[symptom].note}</AppText> />
<AppTextInput </Segment>
{...inputProps} )}
onChangeText={onEditNote} <View style={styles.buttonsContainer}>
placeholder={sharedLabels.enter} <Button iconName={iconName} isSmall onPress={onPressLearnMore}>
testID="noteInput" {sharedLabels.learnMore}
value={noteText !== null ? noteText : ''} </Button>
/> <Button isSmall onPress={onRemove}>
</Segment> {sharedLabels.remove}
)} </Button>
<View style={styles.buttonsContainer}> <Button isCTA isSmall onPress={onSave}>
<Button iconName={iconName} isSmall onPress={onPressLearnMore}> {sharedLabels.save}
{sharedLabels.learnMore} </Button>
</Button> </View>
<Button isSmall onPress={onRemove}> {shouldShowInfo && (
{sharedLabels.remove} <Segment last style={styles.segmentBorder}>
</Button> <AppText>{info[symptom].text}</AppText>
<Button isCTA isSmall onPress={onSave}> </Segment>
{sharedLabels.save} )}
</Button> </ScrollView>
</View>
{shouldShowInfo && (
<Segment last style={styles.segmentBorder}>
<AppText>{info[symptom].text}</AppText>
</Segment>
)}
</ScrollView>
</View>
</AppModal> </AppModal>
) )
} }
@@ -229,32 +223,12 @@ const styles = StyleSheet.create({
paddingHorizontal: Spacing.base, paddingHorizontal: Spacing.base,
paddingBottom: 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: { input: {
height: Sizes.base * 5, height: Sizes.base * 5,
}, },
modalContainer: { modalContainer: {
paddingHorizontal: Spacing.base, paddingHorizontal: Spacing.base,
}, },
modalWindow: {
alignSelf: 'center',
backgroundColor: 'white',
borderRadius: 10,
marginTop: Sizes.huge * 2,
paddingTop: Spacing.large * 2,
position: 'absolute',
minHeight: '40%',
maxHeight: Dimensions.get('window').height * 0.7,
},
segmentBorder: { segmentBorder: {
borderBottomColor: Colors.greyLight, borderBottomColor: Colors.greyLight,
}, },
@@ -3,6 +3,7 @@ import { FlatList, StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import AppModal from '../common/app-modal'
import AppText from '../common/app-text' import AppText from '../common/app-text'
import cycleModule from '../../lib/cycle' import cycleModule from '../../lib/cycle'
@@ -35,26 +36,34 @@ Item.propTypes = {
data: PropTypes.object.isRequired, data: PropTypes.object.isRequired,
} }
const StatsTable = () => { const PeriodDetailsModal = ({ onClose }) => {
const renderItem = ({ item }) => <Item data={item} /> const renderItem = ({ item }) => <Item data={item} />
const data = cycleModule().getStats() const data = cycleModule().getStats()
if (!data || data.length === 0) return false if (!data || data.length === 0) return false
return ( return (
<FlatList <AppModal onClose={onClose}>
data={data} <View>
renderItem={renderItem} <FlatList
keyExtractor={(item) => item.date} data={data}
ItemSeparatorComponent={ItemDivider} renderItem={renderItem}
ListHeaderComponent={FlatListHeader} keyExtractor={(item) => item.date}
ListHeaderComponentStyle={styles.headerDivider} ItemSeparatorComponent={ItemDivider}
stickyHeaderIndices={[0]} ListHeaderComponent={FlatListHeader}
contentContainerStyle={styles.container} ListHeaderComponentStyle={styles.headerDivider}
/> stickyHeaderIndices={[0]}
contentContainerStyle={styles.container}
/>
</View>
</AppModal>
) )
} }
PeriodDetailsModal.propTypes = {
onClose: PropTypes.func,
}
const ItemDivider = () => <View style={styles.divider} /> const ItemDivider = () => <View style={styles.divider} />
const FlatListHeader = () => ( const FlatListHeader = () => (
@@ -89,7 +98,7 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingVertical: Spacing.tiny, paddingVertical: Spacing.tiny,
backgroundColor: Colors.turquoiseLight, backgroundColor: 'white',
}, },
cell: { cell: {
flex: 2, flex: 2,
@@ -100,8 +109,10 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
}, },
container: { container: {
minHeight: '40%',
minWidth: '95%',
paddingHorizontal: Spacing.base, paddingHorizontal: Spacing.base,
}, },
}) })
export default StatsTable export default PeriodDetailsModal
+6 -2
View File
@@ -15,10 +15,12 @@ StatsOverview.propTypes = {
} }
const Row = ({ rowContent }) => { const Row = ({ rowContent }) => {
const isStandardDeviation = rowContent[1].includes('deviation')
return ( return (
<View style={styles.row}> <View style={styles.row}>
<Cell content={rowContent[0]} isLeft /> <Cell content={rowContent[0]} isLeft />
<Cell content={rowContent[1]} /> <Cell content={rowContent[1]} hasAsterisk={isStandardDeviation} />
</View> </View>
) )
} }
@@ -27,7 +29,7 @@ Row.propTypes = {
rowContent: PropTypes.array.isRequired, rowContent: PropTypes.array.isRequired,
} }
const Cell = ({ content, isLeft }) => { const Cell = ({ content, isLeft, hasAsterisk }) => {
const styleContainer = isLeft ? styles.cellLeft : styles.cellRight const styleContainer = isLeft ? styles.cellLeft : styles.cellRight
const styleText = isLeft ? styles.accentPurpleBig : styles.accentOrange const styleText = isLeft ? styles.accentPurpleBig : styles.accentOrange
const numberOfLines = isLeft ? 1 : 2 const numberOfLines = isLeft ? 1 : 2
@@ -41,6 +43,7 @@ const Cell = ({ content, isLeft }) => {
style={styleText} style={styleText}
> >
{content} {content}
{hasAsterisk && <AppText style={styles.accentOrange}>*</AppText>}
</AppText> </AppText>
</View> </View>
) )
@@ -49,6 +52,7 @@ const Cell = ({ content, isLeft }) => {
Cell.propTypes = { Cell.propTypes = {
content: PropTypes.node.isRequired, content: PropTypes.node.isRequired,
isLeft: PropTypes.bool, isLeft: PropTypes.bool,
hasAsterisk: PropTypes.bool,
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
+43 -31
View File
@@ -1,11 +1,13 @@
import React from 'react' import React, { useState } from 'react'
import { ImageBackground, View } from 'react-native' import { ImageBackground, SafeAreaView, ScrollView, View } from 'react-native'
import { ScaledSheet } from 'react-native-size-matters' import { ScaledSheet } from 'react-native-size-matters'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import AppText from '../common/app-text' import AppText from '../common/app-text'
import Button from '../common/button'
import Footnote from '../common/Footnote'
import StatsOverview from './StatsOverview' import StatsOverview from './StatsOverview'
import StatsTable from './StatsTable' import PeriodDetailsModal from './PeriodDetailsModal'
import cycleModule from '../../lib/cycle' import cycleModule from '../../lib/cycle'
import { getCycleLengthStats as getCycleInfo } from '../../lib/cycle-length' import { getCycleLengthStats as getCycleInfo } from '../../lib/cycle-length'
@@ -15,6 +17,8 @@ import { Containers, Sizes, Spacing, Typography } from '../../styles'
const image = require('../../assets/cycle-icon.png') const image = require('../../assets/cycle-icon.png')
const Stats = () => { const Stats = () => {
const [isStatsVisible, setIsStatsVisible] = useState(false)
const { t } = useTranslation(null, { keyPrefix: 'stats' }) const { t } = useTranslation(null, { keyPrefix: 'stats' })
const cycleLengths = cycleModule().getAllCycleLengths() const cycleLengths = cycleModule().getAllCycleLengths()
@@ -34,42 +38,50 @@ const Stats = () => {
] ]
return ( return (
<View style={styles.pageContainer}> <SafeAreaView style={styles.pageContainer}>
<View style={styles.overviewContainer}> <ScrollView contentContainerStyle={styles.overviewContainer}>
<AppText>{t('intro')}</AppText> <AppText>{t('intro')}</AppText>
{numberOfCycles === 0 ? ( {numberOfCycles === 0 ? (
<AppText>{t('noData')}</AppText> <AppText>{t('noData')}</AppText>
) : ( ) : (
<View style={styles.container}> <>
<View style={styles.columnLeft}> <View style={styles.container}>
<ImageBackground <View style={styles.columnLeft}>
source={image} <ImageBackground
imageStyle={styles.image} source={image}
style={styles.imageContainter} imageStyle={styles.image}
> style={styles.imageContainter}
<AppText
numberOfLines={1}
ellipsizeMode="clip"
style={styles.accentPurpleGiant}
> >
{cycleData.mean} <AppText
numberOfLines={1}
ellipsizeMode="clip"
style={styles.accentPurpleGiant}
>
{cycleData.mean}
</AppText>
<AppText style={styles.accentPurpleHuge}>
{t('overview.days')}
</AppText>
</ImageBackground>
<AppText style={styles.accentOrange}>
{t('overview.average')}
</AppText> </AppText>
<AppText style={styles.accentPurpleHuge}> </View>
{t('overview.days')} <View style={styles.columnRight}>
</AppText> <StatsOverview data={statsData} />
</ImageBackground> </View>
<AppText style={styles.accentOrange}>
{t('overview.average')}
</AppText>
</View> </View>
<View style={styles.columnRight}> <Button isCTA onPress={() => setIsStatsVisible(true)}>
<StatsOverview data={statsData} /> {t('showStats')}
</View> </Button>
</View> {isStatsVisible && (
<PeriodDetailsModal onClose={() => setIsStatsVisible(false)} />
)}
<Footnote>{t('footnote')}</Footnote>
</>
)} )}
</View> </ScrollView>
<StatsTable /> </SafeAreaView>
</View>
) )
} }
@@ -7,7 +7,7 @@ exports[`Footnote component when children are present, renders them 1`] = `
"alignContent": "flex-start", "alignContent": "flex-start",
"flexDirection": "row", "flexDirection": "row",
"marginBottom": 8.571428571428571, "marginBottom": 8.571428571428571,
"marginTop": 21.428571428571427, "marginTop": 34.285714285714285,
} }
} }
> >
@@ -41,8 +41,8 @@ exports[`Footnote component when children are present, renders them 1`] = `
"fontSize": 34.285714285714285, "fontSize": 34.285714285714285,
}, },
Object { Object {
"color": "#CCC", "color": "#555",
"paddingLeft": 34.285714285714285, "paddingLeft": 21.428571428571427,
}, },
] ]
} }