diff --git a/package-lock.json b/package-lock.json index 602fbae..0ed9598 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@emotion/styled": "^11.13.0", "@mui/icons-material": "^5.16.7", "@mui/material": "^5.16.7", - "@mui/x-data-grid": "^7.22.0", "@tanstack/react-query": "^5.96.1", "axios": "^1.7.7", "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": { "version": "7.2.24", "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": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -4174,11 +3922,6 @@ "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": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -4516,14 +4259,6 @@ "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": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/package.json b/package.json index 8989bbb..bda4408 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "@emotion/styled": "^11.13.0", "@mui/icons-material": "^5.16.7", "@mui/material": "^5.16.7", - "@mui/x-data-grid": "^7.22.0", "@tanstack/react-query": "^5.96.1", "axios": "^1.7.7", "buffer": "^6.0.3", diff --git a/react-openapi/README.md b/react-openapi/README.md index 10e6055..961f333 100644 --- a/react-openapi/README.md +++ b/react-openapi/README.md @@ -14,7 +14,7 @@ Auto-generates an admin panel (CRUD, datatable, relationship management) from an ```bash npm install react-openapi # 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 diff --git a/react-openapi/package.json b/react-openapi/package.json index 7fa45d3..b16a5f1 100644 --- a/react-openapi/package.json +++ b/react-openapi/package.json @@ -11,7 +11,6 @@ "react-router-dom": "^6.28.0", "@mui/material": "^5.16.7", "@mui/icons-material": "^5.16.7", - "@mui/x-data-grid": "^7.22.0", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "axios": "^1.7.7", diff --git a/src/Expense/ExpenseList.tsx b/src/Expense/ExpenseList.tsx index baa3117..5b10561 100644 --- a/src/Expense/ExpenseList.tsx +++ b/src/Expense/ExpenseList.tsx @@ -1,7 +1,17 @@ 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 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 type { ExpenseItem, ExpenseFieldConfigs } from "./types"; import { isExpense, monthKey, monthLabel, parseOccurredAt } from "./types"; @@ -113,30 +123,51 @@ interface ExpenseListProps { export function ExpenseList({ items, fields }: ExpenseListProps) { const [activeMonth, setActiveMonth] = useState(null); + const [openMonth, setOpenMonth] = useState(null); + const [menuAnchor, setMenuAnchor] = useState(null); const groups = useMemo(() => groupByMonth(items), [items]); const listRef = useRef(null); + const pillRef = useRef(null); + const didInitOpenMonth = useRef(false); - const jumpToActiveMonth = useCallback(() => { - if (!activeMonth) return; + useEffect(() => { + if (!didInitOpenMonth.current && groups.length > 0) { + didInitOpenMonth.current = true; + setOpenMonth(groups[0].key); + } + }, [groups]); + + const scrollToMonth = useCallback((key: string) => { const anchor = listRef.current?.querySelector( - `[data-month-anchor="${activeMonth}"]`, + `[data-month-anchor="${key}"]`, ); if (!anchor) return; const offset = window.matchMedia("(min-width: 900px)").matches ? 64 : 56; const top = anchor.getBoundingClientRect().top + window.scrollY - offset; window.scrollTo({ top, behavior: "smooth" }); - }, [activeMonth]); + }, []); + + const handleSelectMonth = useCallback( + (key: string) => { + setMenuAnchor(null); + setOpenMonth(key); + scrollToMonth(key); + }, + [scrollToMonth], + ); const handlePillKeyDown = useCallback( (event: React.KeyboardEvent) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); - jumpToActiveMonth(); + if (pillRef.current) setMenuAnchor(pillRef.current); } }, - [jumpToActiveMonth], + [], ); + const selectedMonth = activeMonth ?? openMonth ?? groups[0]?.key ?? null; + useEffect(() => { const root = listRef.current; if (!root || groups.length === 0) return; @@ -174,57 +205,72 @@ export function ExpenseList({ items, fields }: ExpenseListProps) { return ( <> - + {groups.map((group) => ( setMenuAnchor(e.currentTarget)} onKeyDown={handlePillKeyDown} + aria-haspopup="menu" + aria-expanded={Boolean(menuAnchor)} sx={{ display: "flex", alignItems: "center", @@ -269,12 +318,33 @@ export function ExpenseList({ items, fields }: ExpenseListProps) { }, }} > - {activeMonth ? monthLabel(activeMonth) : ""} + + + setMenuAnchor(null)} + anchorOrigin={{ vertical: "top", horizontal: "center" }} + transformOrigin={{ vertical: "bottom", horizontal: "center" }} + > + {groups.map((group) => ( + handleSelectMonth(group.key)} + > + + + ))} + ); }