expense list: month accordions + month-year selector; drop unused x-data-grid
- make each month a card-sized accordion (single-open, first month expanded by default) instead of a sticky header over flat rows - turn the floating pill into a month-year selector: opens a menu of all months and expands + scrolls to the chosen month - remove tags from expense rows again until a placement is settled - remove unused @mui/x-data-grid@7, which pulled @mui/system@7 over material's v5 and caused the createTheme runtime crash
This commit is contained in:
265
package-lock.json
generated
265
package-lock.json
generated
@@ -13,7 +13,6 @@
|
|||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@mui/icons-material": "^5.16.7",
|
"@mui/icons-material": "^5.16.7",
|
||||||
"@mui/material": "^5.16.7",
|
"@mui/material": "^5.16.7",
|
||||||
"@mui/x-data-grid": "^7.22.0",
|
|
||||||
"@tanstack/react-query": "^5.96.1",
|
"@tanstack/react-query": "^5.96.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
@@ -1097,201 +1096,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/private-theming": {
|
|
||||||
"version": "7.3.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.11.tgz",
|
|
||||||
"integrity": "sha512-9B+YKms0fRHbNrqp9tOT/DNbNnU5gyvJ1o3qAGXfq8GmZcbJnE3At9x07Zr/o0pkhzg4aDdwXVqe4+AcgtOCPA==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.28.6",
|
|
||||||
"@mui/utils": "^7.3.11",
|
|
||||||
"prop-types": "^15.8.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/mui-org"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
||||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mui/private-theming/node_modules/@mui/types": {
|
|
||||||
"version": "7.4.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz",
|
|
||||||
"integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.28.6"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mui/private-theming/node_modules/@mui/utils": {
|
|
||||||
"version": "7.3.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz",
|
|
||||||
"integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.28.6",
|
|
||||||
"@mui/types": "^7.4.12",
|
|
||||||
"@types/prop-types": "^15.7.15",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"prop-types": "^15.8.1",
|
|
||||||
"react-is": "^19.2.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/mui-org"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
||||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mui/styled-engine": {
|
|
||||||
"version": "7.3.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.10.tgz",
|
|
||||||
"integrity": "sha512-WxE9SiF8xskAQqGjsp0poXCkCqsoXFEsSr0HBXfApmGHR+DBnXRp+z46Vsltg4gpPM4Z96DeAQRpeAOnhNg7Ng==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.28.6",
|
|
||||||
"@emotion/cache": "^11.14.0",
|
|
||||||
"@emotion/serialize": "^1.3.3",
|
|
||||||
"@emotion/sheet": "^1.4.0",
|
|
||||||
"csstype": "^3.2.3",
|
|
||||||
"prop-types": "^15.8.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/mui-org"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@emotion/react": "^11.4.1",
|
|
||||||
"@emotion/styled": "^11.3.0",
|
|
||||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@emotion/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@emotion/styled": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mui/system": {
|
|
||||||
"version": "7.3.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.11.tgz",
|
|
||||||
"integrity": "sha512-7izwGWdNawAKpBKcRlx7f2gFnAAjmASBWvMcyX4YYEeLOFsbfGRbUYGInvnAcUeql3rPxI7F9Ft4oY2OLRz44g==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.28.6",
|
|
||||||
"@mui/private-theming": "^7.3.11",
|
|
||||||
"@mui/styled-engine": "^7.3.10",
|
|
||||||
"@mui/types": "^7.4.12",
|
|
||||||
"@mui/utils": "^7.3.11",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"csstype": "^3.2.3",
|
|
||||||
"prop-types": "^15.8.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/mui-org"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@emotion/react": "^11.5.0",
|
|
||||||
"@emotion/styled": "^11.3.0",
|
|
||||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
||||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@emotion/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@emotion/styled": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mui/system/node_modules/@mui/types": {
|
|
||||||
"version": "7.4.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz",
|
|
||||||
"integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.28.6"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mui/system/node_modules/@mui/utils": {
|
|
||||||
"version": "7.3.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz",
|
|
||||||
"integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.28.6",
|
|
||||||
"@mui/types": "^7.4.12",
|
|
||||||
"@types/prop-types": "^15.7.15",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"prop-types": "^15.8.1",
|
|
||||||
"react-is": "^19.2.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/mui-org"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
||||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mui/types": {
|
"node_modules/@mui/types": {
|
||||||
"version": "7.2.24",
|
"version": "7.2.24",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz",
|
||||||
@@ -1334,62 +1138,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/x-data-grid": {
|
|
||||||
"version": "7.29.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.29.13.tgz",
|
|
||||||
"integrity": "sha512-XHrZTvpa61eqSgUIevDzXYfYCbI7cbN/aRxSCaw2cZzQZ/USdpECYbnTEhgw5XgUlvkAK0mItKZmgKM4X7zT+Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.25.7",
|
|
||||||
"@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0",
|
|
||||||
"@mui/x-internals": "7.29.0",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"prop-types": "^15.8.1",
|
|
||||||
"reselect": "^5.1.1",
|
|
||||||
"use-sync-external-store": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/mui-org"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@emotion/react": "^11.9.0",
|
|
||||||
"@emotion/styled": "^11.8.1",
|
|
||||||
"@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
|
|
||||||
"@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
|
|
||||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
||||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@emotion/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@emotion/styled": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mui/x-internals": {
|
|
||||||
"version": "7.29.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.29.0.tgz",
|
|
||||||
"integrity": "sha512-+Gk6VTZIFD70XreWvdXBwKd8GZ2FlSCuecQFzm6znwqXg1ZsndavrhG9tkxpxo2fM1Zf7Tk8+HcOO0hCbhTQFA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.25.7",
|
|
||||||
"@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/mui-org"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@popperjs/core": {
|
"node_modules/@popperjs/core": {
|
||||||
"version": "2.11.8",
|
"version": "2.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
@@ -4174,11 +3922,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/reselect": {
|
|
||||||
"version": "5.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
|
|
||||||
"integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="
|
|
||||||
},
|
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.12",
|
"version": "1.22.12",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||||
@@ -4516,14 +4259,6 @@
|
|||||||
"browserslist": ">= 4.21.0"
|
"browserslist": ">= 4.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/use-sync-external-store": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vfile": {
|
"node_modules/vfile": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@mui/icons-material": "^5.16.7",
|
"@mui/icons-material": "^5.16.7",
|
||||||
"@mui/material": "^5.16.7",
|
"@mui/material": "^5.16.7",
|
||||||
"@mui/x-data-grid": "^7.22.0",
|
|
||||||
"@tanstack/react-query": "^5.96.1",
|
"@tanstack/react-query": "^5.96.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Auto-generates an admin panel (CRUD, datatable, relationship management) from an
|
|||||||
```bash
|
```bash
|
||||||
npm install react-openapi
|
npm install react-openapi
|
||||||
# peer dependencies (install these in your app):
|
# peer dependencies (install these in your app):
|
||||||
npm install react react-dom react-router-dom @mui/material @mui/icons-material @mui/x-data-grid @emotion/react @emotion/styled axios js-yaml
|
npm install react react-dom react-router-dom @mui/material @mui/icons-material @emotion/react @emotion/styled axios js-yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"@mui/material": "^5.16.7",
|
"@mui/material": "^5.16.7",
|
||||||
"@mui/icons-material": "^5.16.7",
|
"@mui/icons-material": "^5.16.7",
|
||||||
"@mui/x-data-grid": "^7.22.0",
|
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Box, Typography } from "@mui/material";
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Accordion,
|
||||||
|
AccordionSummary,
|
||||||
|
AccordionDetails,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
ListItemText,
|
||||||
|
} from "@mui/material";
|
||||||
import { alpha } from "@mui/material/styles";
|
import { alpha } from "@mui/material/styles";
|
||||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
import { ListCellRenderer, CurrencyField, applyDisplayFormat, formatCurrency } from "../../react-openapi";
|
import { ListCellRenderer, CurrencyField, applyDisplayFormat, formatCurrency } from "../../react-openapi";
|
||||||
import type { ExpenseItem, ExpenseFieldConfigs } from "./types";
|
import type { ExpenseItem, ExpenseFieldConfigs } from "./types";
|
||||||
import { isExpense, monthKey, monthLabel, parseOccurredAt } from "./types";
|
import { isExpense, monthKey, monthLabel, parseOccurredAt } from "./types";
|
||||||
@@ -113,30 +123,51 @@ interface ExpenseListProps {
|
|||||||
|
|
||||||
export function ExpenseList({ items, fields }: ExpenseListProps) {
|
export function ExpenseList({ items, fields }: ExpenseListProps) {
|
||||||
const [activeMonth, setActiveMonth] = useState<string | null>(null);
|
const [activeMonth, setActiveMonth] = useState<string | null>(null);
|
||||||
|
const [openMonth, setOpenMonth] = useState<string | null>(null);
|
||||||
|
const [menuAnchor, setMenuAnchor] = useState<HTMLElement | null>(null);
|
||||||
const groups = useMemo(() => groupByMonth(items), [items]);
|
const groups = useMemo(() => groupByMonth(items), [items]);
|
||||||
const listRef = useRef<HTMLDivElement>(null);
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
|
const pillRef = useRef<HTMLDivElement>(null);
|
||||||
|
const didInitOpenMonth = useRef(false);
|
||||||
|
|
||||||
const jumpToActiveMonth = useCallback(() => {
|
useEffect(() => {
|
||||||
if (!activeMonth) return;
|
if (!didInitOpenMonth.current && groups.length > 0) {
|
||||||
|
didInitOpenMonth.current = true;
|
||||||
|
setOpenMonth(groups[0].key);
|
||||||
|
}
|
||||||
|
}, [groups]);
|
||||||
|
|
||||||
|
const scrollToMonth = useCallback((key: string) => {
|
||||||
const anchor = listRef.current?.querySelector<HTMLElement>(
|
const anchor = listRef.current?.querySelector<HTMLElement>(
|
||||||
`[data-month-anchor="${activeMonth}"]`,
|
`[data-month-anchor="${key}"]`,
|
||||||
);
|
);
|
||||||
if (!anchor) return;
|
if (!anchor) return;
|
||||||
const offset = window.matchMedia("(min-width: 900px)").matches ? 64 : 56;
|
const offset = window.matchMedia("(min-width: 900px)").matches ? 64 : 56;
|
||||||
const top = anchor.getBoundingClientRect().top + window.scrollY - offset;
|
const top = anchor.getBoundingClientRect().top + window.scrollY - offset;
|
||||||
window.scrollTo({ top, behavior: "smooth" });
|
window.scrollTo({ top, behavior: "smooth" });
|
||||||
}, [activeMonth]);
|
}, []);
|
||||||
|
|
||||||
|
const handleSelectMonth = useCallback(
|
||||||
|
(key: string) => {
|
||||||
|
setMenuAnchor(null);
|
||||||
|
setOpenMonth(key);
|
||||||
|
scrollToMonth(key);
|
||||||
|
},
|
||||||
|
[scrollToMonth],
|
||||||
|
);
|
||||||
|
|
||||||
const handlePillKeyDown = useCallback(
|
const handlePillKeyDown = useCallback(
|
||||||
(event: React.KeyboardEvent) => {
|
(event: React.KeyboardEvent) => {
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
jumpToActiveMonth();
|
if (pillRef.current) setMenuAnchor(pillRef.current);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[jumpToActiveMonth],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const selectedMonth = activeMonth ?? openMonth ?? groups[0]?.key ?? null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = listRef.current;
|
const root = listRef.current;
|
||||||
if (!root || groups.length === 0) return;
|
if (!root || groups.length === 0) return;
|
||||||
@@ -174,57 +205,72 @@ export function ExpenseList({ items, fields }: ExpenseListProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box ref={listRef} sx={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
<Box ref={listRef} sx={{ display: "flex", flexDirection: "column", gap: 2.5 }}>
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<Box key={group.key}>
|
<Box key={group.key}>
|
||||||
<span data-month-anchor={group.key} aria-hidden="true" style={{ display: "block", height: 0 }} />
|
<span data-month-anchor={group.key} aria-hidden="true" style={{ display: "block", height: 0 }} />
|
||||||
<Box
|
<Accordion
|
||||||
|
disableGutters
|
||||||
data-month-header={group.key}
|
data-month-header={group.key}
|
||||||
|
expanded={openMonth === group.key}
|
||||||
|
onChange={(_, isExpanded) => setOpenMonth(isExpanded ? group.key : null)}
|
||||||
|
TransitionProps={{ unmountOnExit: true }}
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
border: "1px solid",
|
||||||
alignItems: "baseline",
|
borderColor: openMonth === group.key ? "primary.main" : "divider",
|
||||||
gap: 1.5,
|
borderRadius: 2,
|
||||||
mb: 1.5,
|
overflow: "hidden",
|
||||||
position: "sticky",
|
boxShadow: "none",
|
||||||
top: { xs: 56, md: 64 },
|
backgroundColor: "background.paper",
|
||||||
zIndex: 2,
|
"&:before": { display: "none" },
|
||||||
py: 0.5,
|
transition: "border-color 160ms ease, background-color 160ms ease",
|
||||||
backgroundColor: (theme) => alpha(theme.palette.background.default, 0.85),
|
"&:hover": { borderColor: "primary.light" },
|
||||||
backdropFilter: "blur(8px)",
|
|
||||||
borderBottom: "1px solid",
|
|
||||||
borderColor: "divider",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" fontWeight={700} sx={{ letterSpacing: "-0.01em" }}>
|
<AccordionSummary
|
||||||
{monthLabel(group.key)}
|
expandIcon={<ExpandMoreIcon />}
|
||||||
</Typography>
|
sx={{
|
||||||
<Typography variant="caption" color="text.secondary">
|
px: 2,
|
||||||
{group.items.length} transaction{group.items.length === 1 ? "" : "s"}
|
py: 1,
|
||||||
</Typography>
|
"& .MuiAccordionSummary-content": {
|
||||||
<Box sx={{ flex: 1 }} />
|
alignItems: "center",
|
||||||
<Typography variant="body2" fontWeight={700} color="error.main">
|
gap: 1.5,
|
||||||
{formatCurrency(group.spent, group.currency)}
|
minWidth: 0,
|
||||||
</Typography>
|
},
|
||||||
<Typography variant="body2" color="text.disabled">
|
}}
|
||||||
/
|
>
|
||||||
</Typography>
|
<Typography variant="h6" fontWeight={700} sx={{ letterSpacing: "-0.01em" }}>
|
||||||
<Typography variant="body2" fontWeight={700} color="success.main">
|
{monthLabel(group.key)}
|
||||||
{formatCurrency(group.income, group.currency)}
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{group.items.length} transaction{group.items.length === 1 ? "" : "s"}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ flex: 1 }} />
|
||||||
|
<Typography variant="body2" fontWeight={700} color="error.main">
|
||||||
|
{formatCurrency(group.spent, group.currency)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.disabled">
|
||||||
|
/
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" fontWeight={700} color="success.main">
|
||||||
|
{formatCurrency(group.income, group.currency)}
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails sx={{ p: 1.5, pt: 1 }}>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
|
{group.items.map((item) => (
|
||||||
|
<ExpenseRow
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
currency={group.currency}
|
||||||
|
fields={fields}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
</Box>
|
</Box>
|
||||||
|
))}
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
|
||||||
{group.items.map((item) => (
|
|
||||||
<ExpenseRow
|
|
||||||
key={item.id}
|
|
||||||
item={item}
|
|
||||||
currency={group.currency}
|
|
||||||
fields={fields}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
@@ -240,10 +286,13 @@ export function ExpenseList({ items, fields }: ExpenseListProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
ref={pillRef}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={jumpToActiveMonth}
|
onClick={(e) => setMenuAnchor(e.currentTarget)}
|
||||||
onKeyDown={handlePillKeyDown}
|
onKeyDown={handlePillKeyDown}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={Boolean(menuAnchor)}
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -269,12 +318,33 @@ export function ExpenseList({ items, fields }: ExpenseListProps) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<KeyboardArrowUpIcon sx={{ fontSize: 14, color: "text.secondary" }} />
|
|
||||||
<Typography variant="caption" fontWeight={700} color="text.secondary">
|
<Typography variant="caption" fontWeight={700} color="text.secondary">
|
||||||
{activeMonth ? monthLabel(activeMonth) : ""}
|
{activeMonth ? monthLabel(activeMonth) : ""}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<KeyboardArrowDownIcon sx={{ fontSize: 14, color: "text.secondary" }} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
anchorEl={menuAnchor}
|
||||||
|
open={Boolean(menuAnchor)}
|
||||||
|
onClose={() => setMenuAnchor(null)}
|
||||||
|
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
||||||
|
transformOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||||
|
>
|
||||||
|
{groups.map((group) => (
|
||||||
|
<MenuItem
|
||||||
|
key={group.key}
|
||||||
|
selected={selectedMonth === group.key}
|
||||||
|
onClick={() => handleSelectMonth(group.key)}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={monthLabel(group.key)}
|
||||||
|
secondary={`${group.items.length} transactions`}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user