diff --git a/components/common/table.js b/components/common/StatsOverview.js similarity index 77% rename from components/common/table.js rename to components/common/StatsOverview.js index 5066c64..1e07953 100644 --- a/components/common/table.js +++ b/components/common/StatsOverview.js @@ -6,14 +6,12 @@ import AppText from './app-text' import { Sizes, Spacing, Typography } from '../../styles' -const Table = ({ tableContent }) => { - return tableContent.map((rowContent, i) => ( - - )) +const StatsOverview = ({ data }) => { + return data.map((rowContent, i) => ) } -Table.propTypes = { - tableContent: PropTypes.array.isRequired, +StatsOverview.propTypes = { + data: PropTypes.array.isRequired, } const Row = ({ rowContent }) => { @@ -65,18 +63,11 @@ const styles = StyleSheet.create({ }, cellLeft: { alignItems: 'flex-end', - flex: 5, + flex: 3, justifyContent: 'center', }, - cellRight: { - flex: 5, - justifyContent: 'center', - }, - row: { - flexDirection: 'row', - marginBottom: Spacing.tiny, - marginLeft: Spacing.tiny, - }, + cellRight: { flex: 5 }, + row: { flexDirection: 'row' }, }) -export default Table +export default StatsOverview diff --git a/components/common/StatsTable.js b/components/common/StatsTable.js new file mode 100644 index 0000000..c628bcc --- /dev/null +++ b/components/common/StatsTable.js @@ -0,0 +1,107 @@ +import React from 'react' +import { FlatList, StyleSheet, View } from 'react-native' +import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' + +import AppText from './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 ( + + + {humanizeDate(date)} + + + {t('day', { count: cycleLength })} + + + {t('day', { count: bleedingLength })} + + + ) +} + +Item.propTypes = { + data: PropTypes.object.isRequired, +} + +const StatsTable = () => { + const renderItem = ({ item }) => + const data = cycleModule().getStats() + + if (!data || data.length === 0) return false + + return ( + item.date} + ItemSeparatorComponent={ItemDivider} + ListHeaderComponent={FlatListHeader} + ListHeaderComponentStyle={styles.headerDivider} + stickyHeaderIndices={[0]} + contentContainerStyle={styles.container} + /> + ) +} + +const ItemDivider = () => + +const FlatListHeader = () => ( + + + {'Cycle Start'} + + + {'Cycle Length'} + + + {'Bleeding'} + + +) + +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 diff --git a/components/common/app-page.js b/components/common/app-page.js index 74ced3a..784fd66 100644 --- a/components/common/app-page.js +++ b/components/common/app-page.js @@ -4,7 +4,7 @@ import { ScrollView, StyleSheet, View } from 'react-native' import AppText from '../common/app-text' -import { Colors, Typography } from '../../styles' +import { Colors, Containers, Typography } from '../../styles' const AppPage = ({ children, @@ -35,10 +35,7 @@ AppPage.propTypes = { } const styles = StyleSheet.create({ - container: { - backgroundColor: Colors.turquoiseLight, - flex: 1, - }, + container: { ...Containers.pageContainer }, scrollView: { backgroundColor: Colors.turquoiseLight, flexGrow: 1, diff --git a/components/common/segment.js b/components/common/segment.js index d17f1b7..0069666 100644 --- a/components/common/segment.js +++ b/components/common/segment.js @@ -4,7 +4,7 @@ import { StyleSheet, View } from 'react-native' import AppText from './app-text' -import { Colors, Spacing, Typography } from '../../styles' +import { Colors, Containers, Spacing, Typography } from '../../styles' const Segment = ({ children, last, title }) => { const containerStyle = last ? styles.containerLast : styles.container @@ -25,21 +25,16 @@ 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, - ...segmentContainer, + ...Containers.segmentContainer, }, containerLast: { - ...segmentContainer, + ...Containers.segmentContainer, }, title: { ...Typography.subtitle, diff --git a/components/helpers/format-date.js b/components/helpers/format-date.js index ef04668..7f91321 100644 --- a/components/helpers/format-date.js +++ b/components/helpers/format-date.js @@ -22,3 +22,18 @@ 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 '' + } +} diff --git a/components/stats.js b/components/stats.js index b3809b2..51af03f 100644 --- a/components/stats.js +++ b/components/stats.js @@ -2,16 +2,15 @@ import React from 'react' import { ImageBackground, View } from 'react-native' import { ScaledSheet } from 'react-native-size-matters' -import AppPage from './common/app-page' import AppText from './common/app-text' -import Segment from './common/segment' -import Table from './common/table' +import StatsOverview from './common/StatsOverview' +import StatsTable from './common/StatsTable' import cycleModule from '../lib/cycle' -import {getCycleLengthStats as getCycleInfo} from '../lib/cycle-length' -import {stats as labels} from '../i18n/en/labels' +import { getCycleLengthStats as getCycleInfo } from '../lib/cycle-length' +import { stats as labels } from '../i18n/en/labels' -import { Sizes, Spacing, Typography } from '../styles' +import { Containers, Sizes, Spacing, Typography } from '../styles' const image = require('../assets/cycle-icon.png') @@ -19,21 +18,22 @@ const Stats = () => { const cycleLengths = cycleModule().getAllCycleLengths() const numberOfCycles = cycleLengths.length const hasAtLeastOneCycle = numberOfCycles >= 1 - const cycleData = hasAtLeastOneCycle ? getCycleInfo(cycleLengths) + const cycleData = hasAtLeastOneCycle + ? getCycleInfo(cycleLengths) : { minimum: '—', maximum: '—', stdDeviation: '—' } const statsData = [ [cycleData.minimum, labels.minLabel], [cycleData.maximum, labels.maxLabel], [cycleData.stdDeviation ? cycleData.stdDeviation : '—', labels.stdLabel], - [numberOfCycles, labels.basisOfStatsEnd] + [numberOfCycles, labels.basisOfStatsEnd], ] return ( - - + + {labels.cycleLengthExplainer} {!hasAtLeastOneCycle && {labels.emptyStats}} - {hasAtLeastOneCycle && + {hasAtLeastOneCycle && ( { - + - } - - + )} + + + ) } @@ -77,25 +78,24 @@ 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: 2, + flex: 3, }, columnRight: { ...column, - flex: 3, + flex: 5, paddingTop: Spacing.small, }, image: { @@ -105,9 +105,13 @@ const styles = ScaledSheet.create({ paddingTop: Spacing.large * 2.5, marginBottom: Spacing.large, }, + overviewContainer: { + paddingHorizontal: Spacing.base, + paddingTop: Spacing.base, + }, pageContainer: { - marginTop: Spacing.base * 2, - } + ...Containers.pageContainer, + }, }) export default Stats diff --git a/i18n/en.json b/i18n/en.json index 74fbbfa..08c7576 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -60,5 +60,9 @@ "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!" } } + }, + "plurals": { + "day": "{{count}} day", + "day_plural": "{{count}} days" } } diff --git a/lib/cycle.js b/lib/cycle.js index 3a8ca62..90c4a30 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -3,6 +3,8 @@ 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 @@ -14,9 +16,13 @@ export default function config(opts) { if (!opts) { // we only want to require (and run) the db module // when not running the tests - bleedingDaysSortedByDate = require('../db').getBleedingDaysSortedByDate() - cycleStartsSortedByDate = require('../db').getCycleStartsSortedByDate() - cycleDaysSortedByDate = require('../db').getCycleDaysSortedByDate() + bleedingDaysSortedByDate = toJSON( + require('../db').getBleedingDaysSortedByDate() + ) + cycleStartsSortedByDate = toJSON( + require('../db').getCycleStartsSortedByDate() + ) + cycleDaysSortedByDate = toJSON(require('../db').getCycleDaysSortedByDate()) maxBreakInBleeding = 1 maxCycleLength = 99 minCyclesForPrediction = 3 @@ -222,6 +228,19 @@ 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, @@ -232,5 +251,6 @@ export default function config(opts) { isMensesStart, getMensesDaysRightAfter, getCycleByStartDay, + getStats, } } diff --git a/styles/containers.js b/styles/containers.js index 6526d3a..8560d10 100644 --- a/styles/containers.js +++ b/styles/containers.js @@ -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,17 +17,25 @@ export default { centerItems: { alignItems: 'center', flex: 1, - justifyContent: 'center' + justifyContent: 'center', + }, + pageContainer: { + backgroundColor: Colors.turquoiseLight, + flex: 1, }, rowContainer: { alignItems: 'center', flexDirection: 'row', - justifyContent: 'space-between' + justifyContent: 'space-between', }, selectGroupContainer: { alignItems: 'center', flexDirection: 'row', flexWrap: 'wrap', - marginVertical: Spacing.small - } -} \ No newline at end of file + marginVertical: Spacing.small, + }, + segmentContainer: { + marginHorizontal: Spacing.base, + marginBottom: Spacing.base, + }, +} diff --git a/test/helpers/format-date.spec.js b/test/helpers/format-date.spec.js new file mode 100644 index 0000000..9aa3a57 --- /dev/null +++ b/test/helpers/format-date.spec.js @@ -0,0 +1,27 @@ +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') + }) +})