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