import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Box, Typography, Accordion, AccordionSummary, AccordionDetails, Menu, MenuItem, ListItemText, } from "@mui/material"; import { alpha } from "@mui/material/styles"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import { formatCurrency } from "../../../react-openapi"; import type { ExpenseItem, TxnFieldConfigs } from "../types"; import { groupByDate, groupByMonth } from "../utils/transactions"; import { monthLabel } from "../utils/dates"; import { TransactionRow } from "./TransactionRow"; interface TransactionListProps { items: ExpenseItem[]; fields: TxnFieldConfigs; } export function TransactionList({ items, fields }: TransactionListProps) { 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); 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="${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" }); }, []); 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(); if (pillRef.current) setMenuAnchor(pillRef.current); } }, [], ); const selectedMonth = activeMonth ?? openMonth ?? groups[0]?.key ?? null; useEffect(() => { const root = listRef.current; if (!root || groups.length === 0) return; let ticking = false; const update = () => { ticking = false; const headers = root.querySelectorAll("[data-month-header]"); let current: string | null = null; for (const header of headers) { if (header.getBoundingClientRect().top <= 72) { current = header.dataset.monthHeader ?? null; } else { break; } } setActiveMonth(current); }; const onScroll = () => { if (!ticking) { ticking = true; requestAnimationFrame(update); } }; update(); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); }; }, [groups]); return ( <> {groups.map((group) => ( ))} setMenuAnchor(e.currentTarget)} onKeyDown={handlePillKeyDown} aria-haspopup="menu" aria-expanded={Boolean(menuAnchor)} sx={{ display: "flex", alignItems: "center", gap: 0.5, px: 1.5, py: 0.75, borderRadius: "999px", cursor: "pointer", userSelect: "none", backgroundColor: (theme) => alpha(theme.palette.background.default, 0.85), backdropFilter: "blur(8px)", border: "1px solid", borderColor: "divider", boxShadow: 1, transition: "background-color 160ms ease, transform 160ms ease", "&:hover": { backgroundColor: (theme) => alpha(theme.palette.background.default, 0.95), transform: "translateY(-1px)", }, "&:focus-visible": { outline: "2px solid", outlineColor: "primary.main", }, }} > {activeMonth ? monthLabel(activeMonth) : ""} setMenuAnchor(null)} anchorOrigin={{ vertical: "top", horizontal: "center" }} transformOrigin={{ vertical: "bottom", horizontal: "center" }} > {groups.map((group) => ( handleSelectMonth(group.key)} > ))} ); }