From 1362e27a47ae50ee7d4c0ae90d2d0b746719b2dd Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 17 Apr 2019 09:43:10 +0300 Subject: [PATCH] feat(category): make categories page ok --- front/pages/internal/categories.tsx | 19 ++++-- front/routes.js | 5 +- front/src/features/categories/Categories.css | 39 ++++++++++++ front/src/features/categories/Categories.tsx | 60 +++++++++++++------ .../categories/features/group-chooser.tsx | 49 +++++++++++++++ .../categories/features/period-chooser.tsx | 51 ++++++++++++++++ .../categories/helpers/useDateRange.ts | 23 +++++++ .../routing/transitions/animations/index.ts | 10 ++++ front/src/helpers/createRangeForGroup.ts | 23 +++++-- .../chart/pie-chart/helpers/createOptions.ts | 2 +- 10 files changed, 250 insertions(+), 31 deletions(-) create mode 100644 front/src/features/categories/Categories.css create mode 100644 front/src/features/categories/features/group-chooser.tsx create mode 100644 front/src/features/categories/features/period-chooser.tsx create mode 100644 front/src/features/categories/helpers/useDateRange.ts diff --git a/front/pages/internal/categories.tsx b/front/pages/internal/categories.tsx index 352ba024..d67eeade 100644 --- a/front/pages/internal/categories.tsx +++ b/front/pages/internal/categories.tsx @@ -7,9 +7,10 @@ import { getDefaultCurrency } from '@front/domain/user/selectors/getDefaultCurre import { Categories } from '@front/features/categories' import { GroupBy } from '@shared/enum/GroupBy' import { createRangeForGroup } from '@front/helpers/createRangeForGroup' +import { getFirstTransactionDate } from '@front/domain/money/selectors/getFirstTransactionDate' interface Query { - group: GroupBy + group?: GroupBy } export default class CateogiesPage extends React.Component { @@ -20,14 +21,20 @@ export default class CateogiesPage extends React.Component { query, }: AppContext) { const { group } = query - const { from, to } = createRangeForGroup(group) + + await reduxStore.dispatch(fetchFirstTransactionDate() as any) + const firstTransactionDate = getFirstTransactionDate(reduxStore.getState()) + + const { from, to } = !!group + ? createRangeForGroup(group) + : { + from: firstTransactionDate, + to: new Date(), + } const currency = getDefaultCurrency(reduxStore.getState()) - await Promise.all([ - reduxStore.dispatch(fetchFirstTransactionDate() as any), - reduxStore.dispatch(fetchStatsCategories(from, to, currency) as any), - ]) + await reduxStore.dispatch(fetchStatsCategories(from, to, currency) as any) return { group } } diff --git a/front/routes.js b/front/routes.js index 59f52052..00b2b90c 100644 --- a/front/routes.js +++ b/front/routes.js @@ -6,6 +6,9 @@ module.exports = nextRoutes() .add({ pattern: '/hello', page: 'internal/hello' }) .add({ pattern: '/app', page: 'internal/app' }) .add({ pattern: '/app/stats', page: 'internal/stats' }) - .add({ pattern: '/app/stats/categories/:group', page: 'internal/categories' }) + .add({ + pattern: '/app/stats/categories/:group?', + page: 'internal/categories', + }) .add({ pattern: '/app/history', page: 'internal/history' }) .add({ pattern: '/app/profile', page: 'internal/profile' }) diff --git a/front/src/features/categories/Categories.css b/front/src/features/categories/Categories.css new file mode 100644 index 00000000..bec1aef5 --- /dev/null +++ b/front/src/features/categories/Categories.css @@ -0,0 +1,39 @@ +.categories { + display: grid; + + grid-template: + 'aside chart' + / 1fr 2fr; + + gap: 16px; + + @media (max-width: 1024px) { + grid-template: + 'aside chart' + / 1fr 1fr; + } + + @media (max-width: 768px) { + grid-template: + 'aside' + 'chart'; + } +} + +.chart { + grid-area: chart; + + & > * { + @media (max-width: 768px) { + margin-bottom: 32px; + } + } +} + +.aside { + grid-area: aside; + + & > *:not(:last-child) { + margin-bottom: 16px; + } +} diff --git a/front/src/features/categories/Categories.tsx b/front/src/features/categories/Categories.tsx index a0b34d6d..07d2d7ac 100644 --- a/front/src/features/categories/Categories.tsx +++ b/front/src/features/categories/Categories.tsx @@ -1,24 +1,27 @@ +import useMedia from 'use-media' import { useMappedState } from 'redux-react-hook' -import { useMemo } from 'react' +import { useState } from 'react' import { GroupBy } from '@shared/enum/GroupBy' import { Container } from '@front/ui/components/layout/container' import { getStatsCategoriesFetchingStatus } from '@front/domain/money/selectors/getStatsCategoriesFetchingStatus' -import { createRangeForGroup } from '@front/helpers/createRangeForGroup' import { useMemoState } from '@front/domain/store' import { getStatsCategories } from '@front/domain/money/selectors/getStatsCategories' import { fetchStatsCategories } from '@front/domain/money/actions/fetchStatsCategories' import { getDefaultCurrency } from '@front/domain/user/selectors/getDefaultCurrency' import { PageHeader } from '@front/ui/components/layout/page-header' import { Loader } from '@front/ui/components/layout/loader' +import { displayMoney } from '@shared/helpers/displayMoney' import { PieChart } from '@front/ui/components/chart/pie-chart' import { pushRoute } from '../routing' -import { displayMoney } from '@shared/helpers/displayMoney' -import useMedia from 'use-media' +import { PeriodChooser } from './features/period-chooser' +import { useDateRange } from './helpers/useDateRange' +import * as styles from './Categories.css' +import { GroupChooser } from './features/group-chooser' interface Props { - group: GroupBy + group?: GroupBy } export const Categories = ({ group }: Props) => { @@ -26,7 +29,9 @@ export const Categories = ({ group }: Props) => { const currency = useMappedState(getDefaultCurrency) const isSmall = useMedia({ maxWidth: 768 }) - const { from, to } = useMemo(() => createRangeForGroup(group), [group]) + const [previousPeriodNumber, setPreviousPeriodNumber] = useState(0) + + const { from, to } = useDateRange(previousPeriodNumber, group) const stats = useMemoState( () => getStatsCategories(from, to, currency), @@ -38,18 +43,37 @@ export const Categories = ({ group }: Props) => { pushRoute('/app/stats')} /> - - ({ - name: category, - data: outcome, - }))} - displayValue={value => - displayMoney(currency)(value, { withPenny: false }) - } - fitToContainer={isSmall} - /> - +
+ + +
+ + {stats.nonEmpty() && ( + ({ + name: category, + data: outcome, + }))} + displayValue={value => + displayMoney(currency)(value, { withPenny: false }) + } + fitToContainer={isSmall} + /> + )} + +
+
) } diff --git a/front/src/features/categories/features/group-chooser.tsx b/front/src/features/categories/features/group-chooser.tsx new file mode 100644 index 00000000..53fa1c08 --- /dev/null +++ b/front/src/features/categories/features/group-chooser.tsx @@ -0,0 +1,49 @@ +import { GroupBy } from '@shared/enum/GroupBy' +import { Button, ButtonType } from '@front/ui/components/form/button' +import { Card } from '@front/ui/components/layout/card' +import { pushRoute } from '@front/features/routing' + +interface Props { + group?: GroupBy +} + +export const GroupChooser = ({ group }: Props) => { + const showYear = !group || group !== GroupBy.Year + const showMonth = !group || group !== GroupBy.Month + const showWhole = !!group + + return ( + pushRoute('/app/stats/categories/year')} + type={ButtonType.Text} + > + Show year + + ), + showMonth && ( + + ), + showWhole && ( + + ), + ].filter(Boolean)} + > + {!!group &&

You see data for one {group}

} + {!group &&

You see data for all time

} +
+ ) +} diff --git a/front/src/features/categories/features/period-chooser.tsx b/front/src/features/categories/features/period-chooser.tsx new file mode 100644 index 00000000..dcee4902 --- /dev/null +++ b/front/src/features/categories/features/period-chooser.tsx @@ -0,0 +1,51 @@ +import { format } from 'date-fns' +import { GroupBy } from '@shared/enum/GroupBy' +import { useCallback } from 'react' +import { Button, ButtonType } from '@front/ui/components/form/button' +import { Card } from '@front/ui/components/layout/card' + +interface Props { + previousPeriodNumber: number + setPreviousPeriodNumber: (t: (v: number) => number) => void + from: Date + to: Date + group: GroupBy +} + +export const PeriodChooser = ({ + from, + to, + group, + setPreviousPeriodNumber, + previousPeriodNumber, +}: Props) => { + const back = useCallback(() => setPreviousPeriodNumber(v => v + 1), [ + setPreviousPeriodNumber, + ]) + const next = useCallback(() => setPreviousPeriodNumber(v => v - 1), [ + setPreviousPeriodNumber, + ]) + + return ( + {`Previous ${group}`}, + previousPeriodNumber > 0 && ( + + ), + ].filter(Boolean)} + > +

+ List of categories for the period from{' '} + {format(from, 'YYYY.MM.DD')} to {format(to, 'YYYY.MM.DD')} +

+
+ ) +} diff --git a/front/src/features/categories/helpers/useDateRange.ts b/front/src/features/categories/helpers/useDateRange.ts new file mode 100644 index 00000000..fbbedbfc --- /dev/null +++ b/front/src/features/categories/helpers/useDateRange.ts @@ -0,0 +1,23 @@ +import { useMemo } from 'react' +import { useMappedState } from 'redux-react-hook' + +import { createRangeForGroup } from '@front/helpers/createRangeForGroup' +import { GroupBy } from '@shared/enum/GroupBy' +import { getFirstTransactionDate } from '@front/domain/money/selectors/getFirstTransactionDate' + +export const useDateRange = (previousPeriodNumber: number, group?: GroupBy) => { + const firstTransactionDate = useMappedState(getFirstTransactionDate) + + const { from, to } = useMemo(() => { + if (group) { + return createRangeForGroup(group, previousPeriodNumber) + } + + return { + from: firstTransactionDate, + to: new Date(), + } + }, [group, previousPeriodNumber]) + + return { from, to } +} diff --git a/front/src/features/routing/transitions/animations/index.ts b/front/src/features/routing/transitions/animations/index.ts index 160fdc70..82e64b24 100644 --- a/front/src/features/routing/transitions/animations/index.ts +++ b/front/src/features/routing/transitions/animations/index.ts @@ -12,4 +12,14 @@ export const routeAnimations = [ nextRoute: '/app', styles: backwards, }, + { + prevRoute: '/app/stats', + nextRoute: '/app/stats/(.+)', + styles: forwards, + }, + { + prevRoute: '/app/stats/(.+)', + nextRoute: '/app/stats', + styles: backwards, + }, ] diff --git a/front/src/helpers/createRangeForGroup.ts b/front/src/helpers/createRangeForGroup.ts index 1115e30a..ddbd8669 100644 --- a/front/src/helpers/createRangeForGroup.ts +++ b/front/src/helpers/createRangeForGroup.ts @@ -1,12 +1,25 @@ -import { startOfMonth, startOfYear, endOfYear, endOfMonth } from 'date-fns' +import { + startOfMonth, + startOfYear, + endOfYear, + endOfMonth, + subYears, + subMonths, +} from 'date-fns' import { GroupBy } from '@shared/enum/GroupBy' import { wantUTC } from './wantUTC' -export const createRangeForGroup = (group: GroupBy) => { - const start = group === GroupBy.Month ? startOfMonth : startOfYear - const end = group === GroupBy.Month ? endOfMonth : endOfYear +export const createRangeForGroup = ( + group: GroupBy, + previousPeriodNumber = 0, +) => { + const minus = wantUTC(group === GroupBy.Month ? subMonths : subYears) + const now = minus(new Date(), previousPeriodNumber) - return { from: wantUTC(start)(new Date()), to: wantUTC(end)(new Date()) } + const start = wantUTC(group === GroupBy.Month ? startOfMonth : startOfYear) + const end = wantUTC(group === GroupBy.Month ? endOfMonth : endOfYear) + + return { from: start(now), to: end(now) } } diff --git a/front/src/ui/components/chart/pie-chart/helpers/createOptions.ts b/front/src/ui/components/chart/pie-chart/helpers/createOptions.ts index 43cb2d5c..7bebcd7e 100644 --- a/front/src/ui/components/chart/pie-chart/helpers/createOptions.ts +++ b/front/src/ui/components/chart/pie-chart/helpers/createOptions.ts @@ -33,7 +33,7 @@ export const createOptions = ( }, }, legend: { - position: maintainAspectRatio ? 'bottom' : 'right', + position: !maintainAspectRatio ? 'bottom' : 'right', }, maintainAspectRatio, })