From 9bccacea476dda33562c4b6b13afef34430b00cb Mon Sep 17 00:00:00 2001 From: Jack <jack@monkeytype.com> Date: Tue, 11 Jul 2023 22:19:24 +0200 Subject: [PATCH] Month picker rework (#1235) Co-authored-by: Jed Fox <git@jedfox.com> --- .../src/components/budget/MonthPicker.js | 167 ++++++++++++++++++ .../src/components/budget/misc.js | 151 +--------------- .../budget/rollover/BudgetSummary.tsx | 2 +- upcoming-release-notes/1235.md | 6 + 4 files changed, 175 insertions(+), 151 deletions(-) create mode 100644 packages/desktop-client/src/components/budget/MonthPicker.js create mode 100644 upcoming-release-notes/1235.md diff --git a/packages/desktop-client/src/components/budget/MonthPicker.js b/packages/desktop-client/src/components/budget/MonthPicker.js new file mode 100644 index 000000000..086869b67 --- /dev/null +++ b/packages/desktop-client/src/components/budget/MonthPicker.js @@ -0,0 +1,167 @@ +import { useState } from 'react'; + +import * as monthUtils from 'loot-core/src/shared/months'; + +import useResizeObserver from '../../hooks/useResizeObserver'; +import { styles, colors } from '../../style'; +import { View } from '../common'; + +export const MonthPicker = ({ + startMonth, + numDisplayed, + monthBounds, + style, + onSelect, +}) => { + const [hoverId, setHoverId] = useState(null); + const [targetMonthCount, setTargetMonthCount] = useState(12); + + const currentMonth = monthUtils.currentMonth(); + const firstSelectedMonth = startMonth; + + const lastSelectedMonth = monthUtils.addMonths( + firstSelectedMonth, + numDisplayed - 1, + ); + + const range = monthUtils.rangeInclusive( + monthUtils.subMonths( + firstSelectedMonth, + targetMonthCount / 2 - numDisplayed / 2, + ), + monthUtils.addMonths( + lastSelectedMonth, + targetMonthCount / 2 - numDisplayed / 2, + ), + ); + + const firstSelectedIndex = + Math.floor(range.length / 2) - Math.floor(numDisplayed / 2); + const lastSelectedIndex = firstSelectedIndex + numDisplayed - 1; + + const [size, setSize] = useState('small'); + const containerRef = useResizeObserver(rect => { + setSize(rect.width <= 400 ? 'small' : 'big'); + setTargetMonthCount( + Math.min(Math.max(Math.floor(rect.width / 50), 12), 24), + ); + }); + + let yearHeadersShown = []; + + return ( + <View + style={[ + { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + style, + ]} + > + <View + innerRef={containerRef} + style={{ + flexDirection: 'row', + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }} + > + {range.map((month, idx) => { + const monthName = monthUtils.format(month, 'MMM'); + const selected = + idx >= firstSelectedIndex && idx <= lastSelectedIndex; + + const lastHoverId = hoverId + numDisplayed - 1; + const hovered = + hoverId === null ? false : idx >= hoverId && idx <= lastHoverId; + + const current = currentMonth === month; + const year = monthUtils.getYear(month); + + let showYearHeader = false; + + if (!yearHeadersShown.includes(year)) { + yearHeadersShown.push(year); + showYearHeader = true; + } + + const isMonthBudgeted = + month >= monthBounds.start && month <= monthBounds.end; + + return ( + <View + key={month} + style={[ + { + padding: '3px 3px', + width: size === 'big' ? '35px' : '20px', + textAlign: 'center', + userSelect: 'none', + cursor: 'default', + borderRadius: 2, + border: 'none', + }, + !isMonthBudgeted && { + textDecoration: 'line-through', + color: colors.n7, + }, + styles.smallText, + selected && { + backgroundColor: colors.p6, + color: 'white', + }, + (hovered || selected) && { + borderRadius: 0, + cursor: 'pointer', + }, + hovered && + !selected && { + backgroundColor: 'rgba(100, 100, 100, .15)', + }, + hovered && + selected && { + backgroundColor: colors.p7, + }, + (idx === firstSelectedIndex || + (idx === hoverId && !selected)) && { + borderTopLeftRadius: 2, + borderBottomLeftRadius: 2, + }, + (idx === lastSelectedIndex || + (idx === lastHoverId && !selected)) && { + borderTopRightRadius: 2, + borderBottomRightRadius: 2, + }, + current && { fontWeight: 'bold' }, + ]} + onClick={() => onSelect(month)} + onMouseEnter={() => setHoverId(idx)} + onMouseLeave={() => setHoverId(null)} + > + {size === 'small' ? monthName[0] : monthName} + {showYearHeader && ( + <View + style={[ + { + position: 'absolute', + top: -14, + left: 0, + fontSize: 10, + fontWeight: 'bold', + color: isMonthBudgeted ? '#272630' : colors.n7, + }, + ]} + > + {year} + </View> + )} + </View> + ); + })} + </View> + </View> + ); +}; diff --git a/packages/desktop-client/src/components/budget/misc.js b/packages/desktop-client/src/components/budget/misc.js index 9ee02db87..2360e2204 100644 --- a/packages/desktop-client/src/components/budget/misc.js +++ b/packages/desktop-client/src/components/budget/misc.js @@ -13,10 +13,7 @@ import { bindActionCreators } from 'redux'; import * as actions from 'loot-core/src/client/actions'; import * as monthUtils from 'loot-core/src/shared/months'; -import useResizeObserver from '../../hooks/useResizeObserver'; import ExpandArrow from '../../icons/v0/ExpandArrow'; -import ArrowThinLeft from '../../icons/v1/ArrowThinLeft'; -import ArrowThinRight from '../../icons/v1/ArrowThinRight'; import CheveronDown from '../../icons/v1/CheveronDown'; import DotsHorizontalTriple from '../../icons/v1/DotsHorizontalTriple'; import { styles, colors } from '../../style'; @@ -40,6 +37,7 @@ import { Row, InputCell, ROW_HEIGHT } from '../table'; import BudgetSummaries from './BudgetSummaries'; import { INCOME_HEADER_HEIGHT, MONTH_BOX_SHADOW } from './constants'; +import { MonthPicker } from './MonthPicker'; import { MonthsProvider, MonthsContext } from './MonthsContext'; import { separateGroups, findSortDown, findSortUp } from './util'; @@ -1388,150 +1386,3 @@ export const BudgetPageHeader = memo( ); }, ); - -function getRangeForYear(year) { - return monthUtils.rangeInclusive( - monthUtils.getYearStart(year), - monthUtils.getYearEnd(year), - ); -} - -function getMonth(year, idx) { - return monthUtils.addMonths(year, idx); -} - -function getCurrentMonthName(startMonth, currentMonth) { - return monthUtils.getYear(startMonth) === monthUtils.getYear(currentMonth) - ? monthUtils.format(currentMonth, 'MMM') - : null; -} - -const MonthPicker = ({ - startMonth, - numDisplayed, - monthBounds, - style, - onSelect, -}) => { - const currentMonth = monthUtils.currentMonth(); - const range = getRangeForYear(currentMonth); - const monthNames = range.map(month => { - return monthUtils.format(month, 'MMM'); - }); - const currentMonthName = getCurrentMonthName(startMonth, currentMonth); - const year = monthUtils.getYear(startMonth); - const selectedIndex = monthUtils.getMonthIndex(startMonth); - - const [size, setSize] = useState('small'); - const containerRef = useResizeObserver(rect => { - setSize(rect.width <= 320 ? 'small' : rect.width <= 400 ? 'medium' : 'big'); - }); - - return ( - <View - style={[ - { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - style, - ]} - > - <View - style={{ - padding: '3px 0px', - margin: '3px 0', - fontWeight: 'bold', - fontSize: 14, - flex: '0 0 40px', - }} - > - {monthUtils.format(year, 'yyyy')} - </View> - <View - innerRef={containerRef} - style={{ - flexDirection: 'row', - flex: 1, - alignItems: 'center', - justifyContent: 'center', - }} - > - {monthNames.map((monthName, idx) => { - const lastSelectedIndex = selectedIndex + numDisplayed; - const selected = idx >= selectedIndex && idx < lastSelectedIndex; - const current = monthName === currentMonthName; - const month = getMonth(year, idx); - const isMonthBudgeted = - month >= monthBounds.start && month <= monthBounds.end; - - return ( - <View - key={monthName} - style={[ - { - marginRight: 1, - padding: size === 'big' ? '3px 5px' : '3px 3px', - textAlign: 'center', - cursor: 'default', - borderRadius: 2, - ':hover': isMonthBudgeted && { - backgroundColor: colors.p6, - color: 'white', - }, - }, - !isMonthBudgeted && { color: colors.n7 }, - styles.smallText, - selected && { - backgroundColor: colors.p6, - color: 'white', - borderRadius: 0, - }, - idx === selectedIndex && { - borderTopLeftRadius: 2, - borderBottomLeftRadius: 2, - }, - idx === lastSelectedIndex - 1 && { - borderTopRightRadius: 2, - borderBottomRightRadius: 2, - }, - idx >= selectedIndex && - idx < lastSelectedIndex - 1 && { - marginRight: 0, - borderRight: 'solid 1px', - borderColor: colors.p6, - }, - current && { textDecoration: 'underline' }, - ]} - onClick={() => onSelect(month)} - > - {size === 'small' ? monthName[0] : monthName} - </View> - ); - })} - </View> - <View - style={{ - flexDirection: 'row', - alignItems: 'center', - flex: '0 0 50px', - justifyContent: 'flex-end', - }} - > - <Button - onClick={() => onSelect(monthUtils.subMonths(startMonth, 1))} - bare - > - <ArrowThinLeft width={12} height={12} /> - </Button> - <Button - onClick={() => onSelect(monthUtils.addMonths(startMonth, 1))} - bare - > - <ArrowThinRight width={12} height={12} /> - </Button> - </View> - </View> - ); -}; diff --git a/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx index 15afe636f..69bdc3e77 100644 --- a/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx +++ b/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx @@ -343,7 +343,7 @@ export function BudgetSummary({ fontWeight: 500, textDecorationSkip: 'ink', }, - currentMonth === month && { textDecoration: 'underline' }, + currentMonth === month && { fontWeight: 'bold' }, ])} > {monthUtils.format(month, 'MMMM')} diff --git a/upcoming-release-notes/1235.md b/upcoming-release-notes/1235.md new file mode 100644 index 000000000..15dbb3d4d --- /dev/null +++ b/upcoming-release-notes/1235.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [Miodec] +--- + +Reworked the budget month picker -- GitLab